Removed React and added HTMX
This commit is contained in:
parent
60ea5945be
commit
62834b7280
|
@ -1,5 +1,5 @@
|
||||||
# Flask-Project-Template
|
# Flask-Project-Template
|
||||||
A template to quickly standup a Flask app with Bootstrap 5, React, OIDC IAM, and a database.
|
A template to quickly standup a Flask app with Bootstrap 5, HTMX, OIDC IAM, and a database.
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
``` sudo apt install python3 ```
|
``` sudo apt install python3 ```
|
||||||
|
|
|
@ -32,5 +32,5 @@ builtins.app_name = ':::APP TITLE:::'
|
||||||
builtins.threaded = threaded_wrapper
|
builtins.threaded = threaded_wrapper
|
||||||
builtins.daemon_threaded = daemon_threaded_wrapper
|
builtins.daemon_threaded = daemon_threaded_wrapper
|
||||||
builtins.ROOT_FILE_PTH = os.path.dirname(os.path.realpath(__file__))
|
builtins.ROOT_FILE_PTH = os.path.dirname(os.path.realpath(__file__))
|
||||||
builtins.logger = Logger().get_logger()
|
builtins.logger = Logger(f"{ROOT_FILE_PTH}/../logs", _ch_log_lvl=10, _fh_log_lvl=20).get_logger()
|
||||||
builtins.json_message = MessageHandler()
|
builtins.json_message = MessageHandler()
|
||||||
|
|
|
@ -44,10 +44,6 @@ app.jinja_env.globals['TITLE'] = app.config["TITLE"]
|
||||||
from core.models import db
|
from core.models import db
|
||||||
from core.models import User
|
from core.models import User
|
||||||
|
|
||||||
db.init_app(app)
|
|
||||||
with app.app_context():
|
|
||||||
db.create_all()
|
|
||||||
|
|
||||||
from core.forms import RegisterForm
|
from core.forms import RegisterForm
|
||||||
from core.forms import LoginForm
|
from core.forms import LoginForm
|
||||||
from core import routes
|
from core import routes
|
||||||
|
|
|
@ -5,12 +5,14 @@ from flask_login import UserMixin
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
|
||||||
# Apoplication imports
|
# Apoplication imports
|
||||||
from core import app, login_manager
|
from core import app
|
||||||
|
from core import login_manager
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
db = SQLAlchemy(app)
|
db = SQLAlchemy(app)
|
||||||
|
|
||||||
|
|
||||||
@login_manager.user_loader
|
@login_manager.user_loader
|
||||||
def load_user(user_id):
|
def load_user(user_id):
|
||||||
return User.query.get(int(user_id))
|
return User.query.get(int(user_id))
|
||||||
|
@ -24,3 +26,8 @@ class User(db.Model, UserMixin):
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"['{self.username}', '{self.email}', '{self.password}', '{self.id}']"
|
return f"['{self.username}', '{self.email}', '{self.password}', '{self.id}']"
|
||||||
|
|
||||||
|
|
||||||
|
db.init_app(app)
|
||||||
|
with app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
|
|
@ -8,3 +8,4 @@ from .login_controller import oidc_register
|
||||||
from .login_controller import controller
|
from .login_controller import controller
|
||||||
|
|
||||||
from . import routes
|
from . import routes
|
||||||
|
from . import htmx_page
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Python imports
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
from flask import request
|
||||||
|
from flask import render_template
|
||||||
|
from flask_login import current_user
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
# Get from __init__
|
||||||
|
from core import app
|
||||||
|
from core import oidc
|
||||||
|
from core import db
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/htmx-page', methods=['GET', 'POST'])
|
||||||
|
def htmx_page():
|
||||||
|
if request.method == 'GET':
|
||||||
|
return render_template('pages/htmx-page.html')
|
||||||
|
|
||||||
|
return render_template('error.html', title = 'Error!',
|
||||||
|
message = 'Must use GET request type...')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/load-hello-world', methods=['GET', 'POST'])
|
||||||
|
def load_hello_world():
|
||||||
|
if request.method == 'GET':
|
||||||
|
return "<h1>Hello, World!</h1>"
|
||||||
|
|
||||||
|
return 'Must use GET request type...'
|
|
@ -21,6 +21,16 @@ def home():
|
||||||
title='Error!',
|
title='Error!',
|
||||||
message='Must use GET request type...')
|
message='Must use GET request type...')
|
||||||
|
|
||||||
|
# NOTE: Yeah, not exactly 'logged' but mostly used with the terminal anyway.
|
||||||
|
@app.route('/log-client-exception', methods=['GET', 'POST'])
|
||||||
|
def ui_failure_exception_tracker():
|
||||||
|
if request.method == 'POST':
|
||||||
|
DATA = str(request.values['exception_data']).strip()
|
||||||
|
print(f"\n\n{DATA}")
|
||||||
|
return json_message.create("success", "UI Exception logged...")
|
||||||
|
|
||||||
|
return json_message.create("danger", "Must use POST request type...")
|
||||||
|
|
||||||
|
|
||||||
@app.route('/about', methods=['GET', 'POST'])
|
@app.route('/about', methods=['GET', 'POST'])
|
||||||
def about():
|
def about():
|
||||||
|
@ -30,14 +40,6 @@ def about():
|
||||||
return render_template('error.html', title = 'Error!',
|
return render_template('error.html', title = 'Error!',
|
||||||
message = 'Must use GET request type...')
|
message = 'Must use GET request type...')
|
||||||
|
|
||||||
@app.route('/react-page', methods=['GET', 'POST'])
|
|
||||||
def react_page():
|
|
||||||
if request.method == 'GET':
|
|
||||||
return render_template('pages/react-page.html')
|
|
||||||
|
|
||||||
return render_template('error.html', title = 'Error!',
|
|
||||||
message = 'Must use GET request type...')
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/protected-zone', methods=['GET', 'POST'])
|
@app.route('/protected-zone', methods=['GET', 'POST'])
|
||||||
def protected_zone():
|
def protected_zone():
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
|
const logUIExceptionAjax = (data, action = "client-exception-logger") => {
|
||||||
|
const postArgs = 'exception_data=' + data;
|
||||||
|
|
||||||
|
doAjax('log-client-exception', postArgs, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const doAjax = (actionPath, data, action) => {
|
const doAjax = (actionPath, data, action) => {
|
||||||
let xhttp = new XMLHttpRequest();
|
let xhttp = new XMLHttpRequest();
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,16 @@
|
||||||
window.onload = (eve) => {
|
window.onload = (eve) => {
|
||||||
console.log("Loaded...");
|
console.log("Loaded...");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.onerror = function(msg, url, line, col, error) {
|
||||||
|
// Note that col & error are new to the HTML 5 spec and may not be supported in every browser.
|
||||||
|
const suppressErrorAlert = false;
|
||||||
|
let extra = !col ? '' : '\ncolumn: ' + col;
|
||||||
|
extra += !error ? '' : '\nerror: ' + error;
|
||||||
|
const data = `Error: ${msg} \nurl: ${url} \nline: ${line} ${extra}`
|
||||||
|
|
||||||
|
logUIExceptionAjax(data);
|
||||||
|
|
||||||
|
// If you return true, then error alerts (like in older versions of Internet Explorer) will be suppressed.
|
||||||
|
return suppressErrorAlert;
|
||||||
|
};
|
||||||
|
|
|
@ -101,15 +101,13 @@
|
||||||
|
|
||||||
{% block body_scripts %}
|
{% block body_scripts %}
|
||||||
<!-- For internal scripts... -->
|
<!-- For internal scripts... -->
|
||||||
<script src="{{ url_for('static', filename='js/libs/jquery-3.6.0.min.js')}}"></script>
|
<script src="{{ url_for('static', filename='js/libs/jquery-3.7.1.min.js')}}"></script>
|
||||||
|
|
||||||
<!-- For Bootstrap in this exact order... -->
|
<!-- For Bootstrap... -->
|
||||||
<script src="{{ url_for('static', filename='js/libs/bootstrap5/bootstrap.bundle.min.js')}}"></script>
|
<script src="{{ url_for('static', filename='js/libs/bootstrap5/bootstrap.bundle.min.js')}}"></script>
|
||||||
|
|
||||||
<!-- For React -->
|
<!-- For HTMX... -->
|
||||||
<script src="{{ url_for('static', filename='js/libs/babel.min.js')}}"></script>
|
<script src="{{ url_for('static', filename='js/libs/htmx-1.9.5.min.js')}}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/libs/react/react.production.min.js')}}"></script>
|
|
||||||
<script src="{{ url_for('static', filename='js/libs/react/react-dom.production.min.js')}}"></script>
|
|
||||||
|
|
||||||
<!-- Application Imports -->
|
<!-- Application Imports -->
|
||||||
{% block body_scripts_additional %}
|
{% block body_scripts_additional %}
|
||||||
|
|
|
@ -14,7 +14,8 @@
|
||||||
{% endblock body_header_additional %}
|
{% endblock body_header_additional %}
|
||||||
|
|
||||||
{% block body_content_additional %}
|
{% block body_content_additional %}
|
||||||
<div id="react-container">
|
<div id="htmx-container" hx-get="/load-hello-world" hx-trigger="load" hx-swap="innerHTML">
|
||||||
|
I got replaced...
|
||||||
</div>
|
</div>
|
||||||
{% endblock body_content_additional %}
|
{% endblock body_content_additional %}
|
||||||
|
|
||||||
|
@ -26,5 +27,4 @@
|
||||||
<script src="{{ url_for('static', filename='js/post-ajax.js')}}"></script>
|
<script src="{{ url_for('static', filename='js/post-ajax.js')}}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/ajax.js')}}"></script>
|
<script src="{{ url_for('static', filename='js/ajax.js')}}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/events.js')}}"></script>
|
<script src="{{ url_for('static', filename='js/events.js')}}"></script>
|
||||||
<script type="text/jsx" src="{{ url_for('static', filename='js/react-ui-logic.js')}}"></script>
|
|
||||||
{% endblock body_scripts_additional %}
|
{% endblock body_scripts_additional %}
|
|
@ -17,9 +17,9 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col justify-content-center text-center">
|
<div class="col justify-content-center text-center">
|
||||||
<p>Using Bootstrap 5 Themeing with Dark Mode defaulted...</p>
|
<p>Using Bootstrap 5 Themeing with Dark Mode defaulted...</p>
|
||||||
<p>With React available...</p>
|
<p>With HTMX available...</p>
|
||||||
<p>A React Example Page:
|
<p>An HTMX Example Page:
|
||||||
<a href="{{ url_for('react_page') }}">
|
<a href="{{ url_for('htmx_page') }}">
|
||||||
<button type="button" class="btn btn-dark">
|
<button type="button" class="btn btn-dark">
|
||||||
Try Me!
|
Try Me!
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -1,63 +0,0 @@
|
||||||
# Python imports
|
|
||||||
import os
|
|
||||||
import logging
|
|
||||||
|
|
||||||
# Lib imports
|
|
||||||
|
|
||||||
# Apoplication imports
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Logger:
|
|
||||||
def __init__(self, name = "NO_LOGGER_NAME_PASSED_ON_INIT"):
|
|
||||||
self.logger = self.create_logger(name)
|
|
||||||
|
|
||||||
def get_logger(self):
|
|
||||||
return self.logger
|
|
||||||
|
|
||||||
def create_logger(self, loggerName, createFile = True):
|
|
||||||
"""
|
|
||||||
Create a new logging object and return it.
|
|
||||||
:note:
|
|
||||||
NOSET # Don't know the actual log level of this... (defaulting or literally none?)
|
|
||||||
Log Levels (From least to most)
|
|
||||||
Type Value
|
|
||||||
CRITICAL 50
|
|
||||||
ERROR 40
|
|
||||||
WARNING 30
|
|
||||||
INFO 20
|
|
||||||
DEBUG 10
|
|
||||||
:param loggerName: Sets the name of the logger object. (Used in log lines)
|
|
||||||
:param createFile: Whether we create a log file or just pump to terminal
|
|
||||||
|
|
||||||
:return: the logging object we created
|
|
||||||
"""
|
|
||||||
|
|
||||||
globalLogLvl = logging.DEBUG # Keep this at highest so that handlers can filter to their desired levels
|
|
||||||
chLogLevel = logging.CRITICAL # Prety musch the only one we change ever
|
|
||||||
fhLogLevel = logging.DEBUG
|
|
||||||
log = logging.getLogger(loggerName)
|
|
||||||
log.setLevel(globalLogLvl)
|
|
||||||
|
|
||||||
# Set our log output styles
|
|
||||||
fFormatter = logging.Formatter('[%(asctime)s] %(pathname)s:%(lineno)d %(levelname)s - %(message)s', '%m-%d %H:%M:%S')
|
|
||||||
cFormatter = logging.Formatter('%(pathname)s:%(lineno)d] %(levelname)s - %(message)s')
|
|
||||||
|
|
||||||
ch = logging.StreamHandler()
|
|
||||||
ch.setLevel(level=chLogLevel)
|
|
||||||
ch.setFormatter(cFormatter)
|
|
||||||
log.addHandler(ch)
|
|
||||||
|
|
||||||
if createFile:
|
|
||||||
folder = "logs"
|
|
||||||
file = f"{folder}/flask-application.log"
|
|
||||||
|
|
||||||
if not os.path.exists(folder):
|
|
||||||
os.mkdir(folder)
|
|
||||||
|
|
||||||
fh = logging.FileHandler(file)
|
|
||||||
fh.setLevel(level=fhLogLevel)
|
|
||||||
fh.setFormatter(fFormatter)
|
|
||||||
log.addHandler(fh)
|
|
||||||
|
|
||||||
return log
|
|
|
@ -1,2 +1,3 @@
|
||||||
from .Logger import Logger
|
from .singleton import Singleton
|
||||||
from .MessageHandler import MessageHandler
|
from .logger import Logger
|
||||||
|
from .message_handler import MessageHandler
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
# Python imports
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
from . import Singleton
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Logger(Singleton):
|
||||||
|
"""
|
||||||
|
Create a new logging object and return it.
|
||||||
|
:note:
|
||||||
|
NOSET # Don't know the actual log level of this... (defaulting or literally none?)
|
||||||
|
Log Levels (From least to most)
|
||||||
|
Type Value
|
||||||
|
CRITICAL 50
|
||||||
|
ERROR 40
|
||||||
|
WARNING 30
|
||||||
|
INFO 20
|
||||||
|
DEBUG 10
|
||||||
|
:param loggerName: Sets the name of the logger object. (Used in log lines)
|
||||||
|
:param createFile: Whether we create a log file or just pump to terminal
|
||||||
|
|
||||||
|
:return: the logging object we created
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config_path: str, _ch_log_lvl = logging.CRITICAL, _fh_log_lvl = logging.INFO):
|
||||||
|
self._CONFIG_PATH = config_path
|
||||||
|
self.global_lvl = logging.DEBUG # Keep this at highest so that handlers can filter to their desired levels
|
||||||
|
self.ch_log_lvl = _ch_log_lvl # Prety much the only one we ever change
|
||||||
|
self.fh_log_lvl = _fh_log_lvl
|
||||||
|
|
||||||
|
def get_logger(self, loggerName: str = "NO_LOGGER_NAME_PASSED", createFile: bool = True) -> logging.Logger:
|
||||||
|
log = logging.getLogger(loggerName)
|
||||||
|
log.setLevel(self.global_lvl)
|
||||||
|
|
||||||
|
# Set our log output styles
|
||||||
|
fFormatter = logging.Formatter('[%(asctime)s] %(pathname)s:%(lineno)d %(levelname)s - %(message)s', '%m-%d %H:%M:%S')
|
||||||
|
cFormatter = logging.Formatter('%(pathname)s:%(lineno)d] %(levelname)s - %(message)s')
|
||||||
|
|
||||||
|
ch = logging.StreamHandler()
|
||||||
|
ch.setLevel(level=self.ch_log_lvl)
|
||||||
|
ch.setFormatter(cFormatter)
|
||||||
|
log.addHandler(ch)
|
||||||
|
|
||||||
|
if createFile:
|
||||||
|
folder = self._CONFIG_PATH
|
||||||
|
file = f"{folder}/application.log"
|
||||||
|
|
||||||
|
if not os.path.exists(folder):
|
||||||
|
os.mkdir(folder)
|
||||||
|
|
||||||
|
fh = logging.FileHandler(file)
|
||||||
|
fh.setLevel(level=self.fh_log_lvl)
|
||||||
|
fh.setFormatter(fFormatter)
|
||||||
|
log.addHandler(fh)
|
||||||
|
|
||||||
|
return log
|
|
@ -11,5 +11,5 @@ class MessageHandler:
|
||||||
print("MessageHandler initialized...")
|
print("MessageHandler initialized...")
|
||||||
|
|
||||||
|
|
||||||
def create_JSON_message(self, type, text):
|
def create(self, type, text):
|
||||||
return '{"message": { "type": "' + type + '", "text": "' + text + '" } }'
|
return '{"message": { "type": "' + type + '", "text": "' + text + '" } }'
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Python imports
|
||||||
|
|
||||||
|
# Lib imports
|
||||||
|
|
||||||
|
# Application imports
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class SingletonError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Singleton:
|
||||||
|
ccount = 0
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
obj = super(Singleton, cls).__new__(cls)
|
||||||
|
cls.ccount += 1
|
||||||
|
|
||||||
|
if cls.ccount == 2:
|
||||||
|
raise SingletonError(f"Exceeded {cls.__name__} instantiation limit...")
|
||||||
|
|
||||||
|
return obj
|
|
@ -1,4 +1,4 @@
|
||||||
from core import app
|
from core import app
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(debug=False)
|
app.run(debug = False, host = '127.0.0.1', port = 5000)
|
||||||
|
|
Loading…
Reference in New Issue