Removed React and added HTMX

This commit is contained in:
itdominator 2023-09-09 18:18:43 -05:00
parent 60ea5945be
commit 62834b7280
18 changed files with 170 additions and 93 deletions

View File

@ -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 ```

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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...'

View File

@ -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():

View File

@ -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();

View File

@ -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;
};

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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:&nbsp; <p>An HTMX Example Page:&nbsp;
<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>

View File

@ -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

View File

@ -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

61
src/core/utils/logger.py Normal file
View File

@ -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

View File

@ -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 + '" } }'

View File

@ -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

View File

@ -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)