diff --git a/bricktracker/app.py b/bricktracker/app.py index 8938e95..a55a9f7 100644 --- a/bricktracker/app.py +++ b/bricktracker/app.py @@ -12,7 +12,13 @@ from bricktracker.navbar import Navbar from bricktracker.sql import close from bricktracker.version import __version__ from bricktracker.views.add import add_page -from bricktracker.views.admin import admin_page +from bricktracker.views.admin.admin import admin_page +from bricktracker.views.admin.checkbox import admin_checkbox_page +from bricktracker.views.admin.database import admin_database_page +from bricktracker.views.admin.image import admin_image_page +from bricktracker.views.admin.instructions import admin_instructions_page +from bricktracker.views.admin.retired import admin_retired_page +from bricktracker.views.admin.theme import admin_theme_page from bricktracker.views.error import error_404 from bricktracker.views.index import index_page from bricktracker.views.instructions import instructions_page @@ -60,9 +66,8 @@ def setup_app(app: Flask) -> None: # Register errors app.register_error_handler(404, error_404) - # Register routes + # Register app routes app.register_blueprint(add_page) - app.register_blueprint(admin_page) app.register_blueprint(index_page) app.register_blueprint(instructions_page) app.register_blueprint(login_page) @@ -71,6 +76,15 @@ def setup_app(app: Flask) -> None: app.register_blueprint(set_page) app.register_blueprint(wish_page) + # Register admin routes + app.register_blueprint(admin_page) + app.register_blueprint(admin_checkbox_page) + app.register_blueprint(admin_database_page) + app.register_blueprint(admin_image_page) + app.register_blueprint(admin_instructions_page) + app.register_blueprint(admin_retired_page) + app.register_blueprint(admin_theme_page) + # An helper to make global variables available to the # request @app.before_request diff --git a/bricktracker/reload.py b/bricktracker/reload.py new file mode 100644 index 0000000..20ddad2 --- /dev/null +++ b/bricktracker/reload.py @@ -0,0 +1,19 @@ +from .instructions_list import BrickInstructionsList +from .retired_list import BrickRetiredList +from .theme_list import BrickThemeList + + +# Reload everything related to a database after an operation +def reload() -> None: + # Failsafe + try: + # Reload the instructions + BrickInstructionsList(force=True) + + # Reload retired sets + BrickRetiredList(force=True) + + # Reload themes + BrickThemeList(force=True) + except Exception: + pass diff --git a/bricktracker/views/admin.py b/bricktracker/views/admin.py deleted file mode 100644 index a5eba0d..0000000 --- a/bricktracker/views/admin.py +++ /dev/null @@ -1,334 +0,0 @@ -from datetime import datetime -import logging -import os - -from flask import ( - Blueprint, - current_app, - g, - redirect, - request, - render_template, - send_file, - url_for, -) -from flask_login import login_required -from werkzeug.wrappers.response import Response - -from ..configuration_list import BrickConfigurationList -from .exceptions import exception_handler -from ..instructions_list import BrickInstructionsList -from ..minifigure import BrickMinifigure -from ..part import BrickPart -from ..rebrickable_image import RebrickableImage -from ..retired_list import BrickRetiredList -from ..set import BrickSet -from ..sql_counter import BrickCounter -from ..sql_migration_list import BrickSQLMigrationList -from ..sql import BrickSQL -from ..theme_list import BrickThemeList -from .upload import upload_helper - -logger = logging.getLogger(__name__) - -admin_page = Blueprint('admin', __name__, url_prefix='/admin') - - -# Admin -@admin_page.route('/', methods=['GET']) -@login_required -@exception_handler(__file__) -def admin() -> str: - database_counters: list[BrickCounter] = [] - database_exception: Exception | None = None - database_upgrade_needed: bool = False - database_version: int = -1 - nil_minifigure_name: str = '' - nil_minifigure_url: str = '' - nil_part_name: str = '' - nil_part_url: str = '' - - # This view needs to be protected against SQL errors - try: - database = BrickSQL(failsafe=True) - database_upgrade_needed = database.upgrade_needed() - database_version = database.version - - if not database_upgrade_needed: - database_counters = BrickSQL().count_records() - except Exception as e: - database_exception = e - - # Warning - logger.warning('A database exception occured while loading the admin page: {exception}'.format( # noqa: E501 - exception=str(e), - )) - - nil_minifigure_name = RebrickableImage.nil_minifigure_name() - nil_minifigure_url = RebrickableImage.static_url( - nil_minifigure_name, - 'MINIFIGURES_FOLDER' - ) - - nil_part_name = RebrickableImage.nil_name() - nil_part_url = RebrickableImage.static_url( - nil_part_name, - 'PARTS_FOLDER' - ) - - open_image = request.args.get('open_image', None) - open_instructions = request.args.get('open_instructions', None) - open_logout = request.args.get('open_logout', None) - open_retired = request.args.get('open_retired', None) - open_theme = request.args.get('open_theme', None) - - open_database = ( - open_image is None and - open_instructions is None and - open_logout is None and - open_retired is None and - open_theme is None - ) - - return render_template( - 'admin.html', - configuration=BrickConfigurationList.list(), - database_counters=database_counters, - database_error=request.args.get('error'), - database_exception=database_exception, - database_upgrade_needed=database_upgrade_needed, - database_version=database_version, - instructions=BrickInstructionsList(), - nil_minifigure_name=nil_minifigure_name, - nil_minifigure_url=nil_minifigure_url, - nil_part_name=nil_part_name, - nil_part_url=nil_part_url, - open_database=open_database, - open_image=open_image, - open_instructions=open_instructions, - open_logout=open_logout, - open_retired=open_retired, - open_theme=open_theme, - retired=BrickRetiredList(), - theme=BrickThemeList(), - ) - - -# Delete the database -@admin_page.route('/delete-database', methods=['GET']) -@login_required -@exception_handler(__file__) -def delete_database() -> str: - return render_template( - 'admin.html', - delete_database=True, - error=request.args.get('error') - ) - - -# Actually delete the database -@admin_page.route('/delete-database', methods=['POST']) -@login_required -@exception_handler(__file__, post_redirect='admin.delete_database') -def do_delete_database() -> Response: - BrickSQL.delete() - - # Reload the instructions - BrickInstructionsList(force=True) - - return redirect(url_for('admin.admin')) - - -# Download the database -@admin_page.route('/download-database', methods=['GET']) -@login_required -@exception_handler(__file__) -def download_database() -> Response: - # Create a file name with a timestamp embedded - name, extension = os.path.splitext( - os.path.basename(current_app.config['DATABASE_PATH']) - ) - - # Info - logger.info('The database has been downloaded') - - return send_file( - current_app.config['DATABASE_PATH'], - as_attachment=True, - download_name='{name}-v{version}-{timestamp}{extension}'.format( - name=name, - version=BrickSQL(failsafe=True).version, - timestamp=datetime.now().astimezone(g.timezone).strftime( - current_app.config['DATABASE_TIMESTAMP_FORMAT'] - ), - extension=extension - ) - ) - - -# Drop the database -@admin_page.route('/drop-database', methods=['GET']) -@login_required -@exception_handler(__file__) -def drop_database() -> str: - return render_template( - 'admin.html', - drop_database=True, - error=request.args.get('error') - ) - - -# Actually drop the database -@admin_page.route('/drop-database', methods=['POST']) -@login_required -@exception_handler(__file__, post_redirect='admin.drop_database') -def do_drop_database() -> Response: - BrickSQL.drop() - - # Reload the instructions - BrickInstructionsList(force=True) - - return redirect(url_for('admin.admin')) - - -# Actually upgrade the database -@admin_page.route('/upgrade-database', methods=['POST']) -@login_required -@exception_handler(__file__, post_redirect='admin.upgrade_database') -def do_upgrade_database() -> Response: - BrickSQL(failsafe=True).upgrade() - - return redirect(url_for('admin.admin')) - - -# Import a database -@admin_page.route('/import-database', methods=['GET']) -@login_required -@exception_handler(__file__) -def import_database() -> str: - return render_template( - 'admin.html', - import_database=True, - error=request.args.get('error') - ) - - -# Actually import a database -@admin_page.route('/import-database', methods=['POST']) -@login_required -@exception_handler(__file__, post_redirect='admin.import_database') -def do_import_database() -> Response: - file = upload_helper( - 'database', - 'admin.import_database', - extensions=['.db'], - ) - - if isinstance(file, Response): - return file - - BrickSQL.upload(file) - - # Reload the instructions - BrickInstructionsList(force=True) - - return redirect(url_for('admin.admin')) - - -# Refresh the instructions cache -@admin_page.route('/refresh-instructions', methods=['GET']) -@login_required -@exception_handler(__file__) -def refresh_instructions() -> Response: - BrickInstructionsList(force=True) - - return redirect(url_for('admin.admin', open_instructions=True)) - - -# Refresh the retired sets cache -@admin_page.route('/refresh-retired', methods=['GET']) -@login_required -@exception_handler(__file__) -def refresh_retired() -> Response: - BrickRetiredList(force=True) - - return redirect(url_for('admin.admin', open_retired=True)) - - -# Refresh the themes cache -@admin_page.route('/refresh-themes', methods=['GET']) -@login_required -@exception_handler(__file__) -def refresh_themes() -> Response: - BrickThemeList(force=True) - - return redirect(url_for('admin.admin', open_theme=True)) - - -# Update the default images -@admin_page.route('/update-image', methods=['GET']) -@login_required -@exception_handler(__file__) -def update_image() -> Response: - # Abusing the object to create a 'nil' minifigure - RebrickableImage( - BrickSet(), - minifigure=BrickMinifigure(record={ - 'set_img_url': None, - }) - ).download() - - # Abusing the object to create a 'nil' part - RebrickableImage( - BrickSet(), - part=BrickPart(record={ - 'part_img_url': None, - 'part_img_url_id': None - }) - ).download() - - return redirect(url_for('admin.admin', open_image=True)) - - -# Update the themes file -@admin_page.route('/update-retired', methods=['GET']) -@login_required -@exception_handler(__file__) -def update_retired() -> Response: - BrickRetiredList().update() - - BrickRetiredList(force=True) - - return redirect(url_for('admin.admin', open_retired=True)) - - -# Update the themes file -@admin_page.route('/update-themes', methods=['GET']) -@login_required -@exception_handler(__file__) -def update_themes() -> Response: - BrickThemeList().update() - - BrickThemeList(force=True) - - return redirect(url_for('admin.admin', open_theme=True)) - - -# Upgrade the database -@admin_page.route('/upgrade-database', methods=['GET']) -@login_required -@exception_handler(__file__, post_redirect='admin.admin') -def upgrade_database() -> str | Response: - database = BrickSQL(failsafe=True) - - if not database.upgrade_needed(): - return redirect(url_for('admin.admin')) - - return render_template( - 'admin.html', - upgrade_database=True, - migrations=BrickSQLMigrationList().pending( - database.version - ), - error=request.args.get('error') - ) diff --git a/bricktracker/views/admin/__init__.py b/bricktracker/views/admin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bricktracker/views/admin/admin.py b/bricktracker/views/admin/admin.py new file mode 100644 index 0000000..847e42a --- /dev/null +++ b/bricktracker/views/admin/admin.py @@ -0,0 +1,104 @@ +import logging + +from flask import Blueprint, request, render_template +from flask_login import login_required + +from ...configuration_list import BrickConfigurationList +from ..exceptions import exception_handler +from ...instructions_list import BrickInstructionsList +from ...rebrickable_image import RebrickableImage +from ...retired_list import BrickRetiredList +from ...set_checkbox import BrickSetCheckbox +from ...set_checkbox_list import BrickSetCheckboxList +from ...sql_counter import BrickCounter +from ...sql import BrickSQL +from ...theme_list import BrickThemeList + +logger = logging.getLogger(__name__) + +admin_page = Blueprint('admin', __name__, url_prefix='/admin') + + +# Admin +@admin_page.route('/', methods=['GET']) +@login_required +@exception_handler(__file__) +def admin() -> str: + brickset_checkboxes: list[BrickSetCheckbox] = [] + database_counters: list[BrickCounter] = [] + database_exception: Exception | None = None + database_upgrade_needed: bool = False + database_version: int = -1 + nil_minifigure_name: str = '' + nil_minifigure_url: str = '' + nil_part_name: str = '' + nil_part_url: str = '' + + # This view needs to be protected against SQL errors + try: + database = BrickSQL(failsafe=True) + database_upgrade_needed = database.upgrade_needed() + database_version = database.version + database_counters = BrickSQL().count_records() + + brickset_checkboxes = BrickSetCheckboxList().list(all=True) + except Exception as e: + database_exception = e + + # Warning + logger.warning('A database exception occured while loading the admin page: {exception}'.format( # noqa: E501 + exception=str(e), + )) + + nil_minifigure_name = RebrickableImage.nil_minifigure_name() + nil_minifigure_url = RebrickableImage.static_url( + nil_minifigure_name, + 'MINIFIGURES_FOLDER' + ) + + nil_part_name = RebrickableImage.nil_name() + nil_part_url = RebrickableImage.static_url( + nil_part_name, + 'PARTS_FOLDER' + ) + + open_checkbox = request.args.get('open_checkbox', None) + open_image = request.args.get('open_image', None) + open_instructions = request.args.get('open_instructions', None) + open_logout = request.args.get('open_logout', None) + open_retired = request.args.get('open_retired', None) + open_theme = request.args.get('open_theme', None) + + open_database = ( + open_checkbox is None and + open_image is None and + open_instructions is None and + open_logout is None and + open_retired is None and + open_theme is None + ) + + return render_template( + 'admin.html', + configuration=BrickConfigurationList.list(), + brickset_checkboxes=brickset_checkboxes, + database_counters=database_counters, + database_error=request.args.get('error'), + database_exception=database_exception, + database_upgrade_needed=database_upgrade_needed, + database_version=database_version, + instructions=BrickInstructionsList(), + nil_minifigure_name=nil_minifigure_name, + nil_minifigure_url=nil_minifigure_url, + nil_part_name=nil_part_name, + nil_part_url=nil_part_url, + open_checkbox=open_checkbox, + open_database=open_database, + open_image=open_image, + open_instructions=open_instructions, + open_logout=open_logout, + open_retired=open_retired, + open_theme=open_theme, + retired=BrickRetiredList(), + theme=BrickThemeList(), + ) diff --git a/bricktracker/views/admin/database.py b/bricktracker/views/admin/database.py new file mode 100644 index 0000000..bd0f213 --- /dev/null +++ b/bricktracker/views/admin/database.py @@ -0,0 +1,170 @@ +from datetime import datetime +import logging +import os + +from flask import ( + Blueprint, + current_app, + g, + redirect, + request, + render_template, + send_file, + url_for, +) +from flask_login import login_required +from werkzeug.wrappers.response import Response + +from ..exceptions import exception_handler +from ...reload import reload +from ...sql_migration_list import BrickSQLMigrationList +from ...sql import BrickSQL +from ..upload import upload_helper + +logger = logging.getLogger(__name__) + +admin_database_page = Blueprint( + 'admin_database', + __name__, + url_prefix='/admin/database' +) + + +# Delete the database +@admin_database_page.route('/delete', methods=['GET']) +@login_required +@exception_handler(__file__) +def delete() -> str: + return render_template( + 'admin.html', + delete_database=True, + error=request.args.get('error') + ) + + +# Actually delete the database +@admin_database_page.route('/delete', methods=['POST']) +@login_required +@exception_handler(__file__, post_redirect='admin_database.delete') +def do_delete() -> Response: + BrickSQL.delete() + + reload() + + return redirect(url_for('admin.admin')) + + +# Download the database +@admin_database_page.route('/download', methods=['GET']) +@login_required +@exception_handler(__file__) +def download() -> Response: + # Create a file name with a timestamp embedded + name, extension = os.path.splitext( + os.path.basename(current_app.config['DATABASE_PATH']) + ) + + # Info + logger.info('The database has been downloaded') + + return send_file( + current_app.config['DATABASE_PATH'], + as_attachment=True, + download_name='{name}-v{version}-{timestamp}{extension}'.format( + name=name, + version=BrickSQL(failsafe=True).version, + timestamp=datetime.now().astimezone(g.timezone).strftime( + current_app.config['DATABASE_TIMESTAMP_FORMAT'] + ), + extension=extension + ) + ) + + +# Drop the database +@admin_database_page.route('/drop', methods=['GET']) +@login_required +@exception_handler(__file__) +def drop() -> str: + return render_template( + 'admin.html', + drop_database=True, + error=request.args.get('error') + ) + + +# Actually drop the database +@admin_database_page.route('/drop', methods=['POST']) +@login_required +@exception_handler(__file__, post_redirect='admin_database.drop') +def do_drop() -> Response: + BrickSQL.drop() + + reload() + + return redirect(url_for('admin.admin')) + + +# Actually upgrade the database +@admin_database_page.route('/upgrade', methods=['POST']) +@login_required +@exception_handler(__file__, post_redirect='admin_database.upgrade') +def do_upgrade() -> Response: + BrickSQL(failsafe=True).upgrade() + + reload() + + return redirect(url_for('admin.admin')) + + +# Import a database +@admin_database_page.route('/import', methods=['GET']) +@login_required +@exception_handler(__file__) +def upload() -> str: + return render_template( + 'admin.html', + import_database=True, + error=request.args.get('error') + ) + + +# Actually import a database +@admin_database_page.route('/import', methods=['POST']) +@login_required +@exception_handler(__file__, post_redirect='admin_database.upload') +def do_upload() -> Response: + file = upload_helper( + 'database', + 'admin_database.upload', + extensions=['.db'], + ) + + if isinstance(file, Response): + return file + + BrickSQL.upload(file) + + reload() + + return redirect(url_for('admin.admin')) + + +# Upgrade the database +@admin_database_page.route('/upgrade', methods=['GET']) +@login_required +@exception_handler(__file__, post_redirect='admin.admin') +def upgrade() -> str | Response: + database = BrickSQL(failsafe=True) + + if not database.upgrade_needed(): + return redirect(url_for('admin.admin')) + + return render_template( + 'admin.html', + upgrade_database=True, + migrations=BrickSQLMigrationList().pending( + database.version + ), + error=request.args.get('error') + ) diff --git a/bricktracker/views/admin/image.py b/bricktracker/views/admin/image.py new file mode 100644 index 0000000..30dce28 --- /dev/null +++ b/bricktracker/views/admin/image.py @@ -0,0 +1,44 @@ +import logging + +from flask import Blueprint, redirect, url_for +from flask_login import login_required +from werkzeug.wrappers.response import Response + +from ..exceptions import exception_handler +from ...minifigure import BrickMinifigure +from ...part import BrickPart +from ...rebrickable_image import RebrickableImage +from ...set import BrickSet + +logger = logging.getLogger(__name__) + +admin_image_page = Blueprint( + 'admin_image', + __name__, + url_prefix='/admin/image' +) + + +# Update the default images +@admin_image_page.route('/update', methods=['GET']) +@login_required +@exception_handler(__file__) +def update() -> Response: + # Abusing the object to create a 'nil' minifigure + RebrickableImage( + BrickSet(), + minifigure=BrickMinifigure(record={ + 'set_img_url': None, + }) + ).download() + + # Abusing the object to create a 'nil' part + RebrickableImage( + BrickSet(), + part=BrickPart(record={ + 'part_img_url': None, + 'part_img_url_id': None + }) + ).download() + + return redirect(url_for('admin.admin', open_image=True)) diff --git a/bricktracker/views/admin/instructions.py b/bricktracker/views/admin/instructions.py new file mode 100644 index 0000000..354782d --- /dev/null +++ b/bricktracker/views/admin/instructions.py @@ -0,0 +1,26 @@ +import logging + +from flask import Blueprint, redirect, url_for +from flask_login import login_required +from werkzeug.wrappers.response import Response + +from ..exceptions import exception_handler +from ...instructions_list import BrickInstructionsList + +logger = logging.getLogger(__name__) + +admin_instructions_page = Blueprint( + 'admin_instructions', + __name__, + url_prefix='/admin/instructions' +) + + +# Refresh the instructions cache +@admin_instructions_page.route('/refresh', methods=['GET']) +@login_required +@exception_handler(__file__) +def refresh() -> Response: + BrickInstructionsList(force=True) + + return redirect(url_for('admin.admin', open_instructions=True)) diff --git a/bricktracker/views/admin/retired.py b/bricktracker/views/admin/retired.py new file mode 100644 index 0000000..c3aa2f2 --- /dev/null +++ b/bricktracker/views/admin/retired.py @@ -0,0 +1,38 @@ +import logging + +from flask import Blueprint, redirect, url_for +from flask_login import login_required +from werkzeug.wrappers.response import Response + +from ..exceptions import exception_handler +from ...retired_list import BrickRetiredList + +logger = logging.getLogger(__name__) + +admin_retired_page = Blueprint( + 'admin_retired', + __name__, + url_prefix='/admin/retired' +) + + +# Refresh the retired sets cache +@admin_retired_page.route('/refresh', methods=['GET']) +@login_required +@exception_handler(__file__) +def refresh() -> Response: + BrickRetiredList(force=True) + + return redirect(url_for('admin.admin', open_retired=True)) + + +# Update the retired sets +@admin_retired_page.route('/update', methods=['GET']) +@login_required +@exception_handler(__file__) +def update() -> Response: + BrickRetiredList().update() + + BrickRetiredList(force=True) + + return redirect(url_for('admin.admin', open_retired=True)) diff --git a/bricktracker/views/admin/theme.py b/bricktracker/views/admin/theme.py new file mode 100644 index 0000000..d5f15bb --- /dev/null +++ b/bricktracker/views/admin/theme.py @@ -0,0 +1,38 @@ +import logging + +from flask import Blueprint, redirect, url_for +from flask_login import login_required +from werkzeug.wrappers.response import Response + +from ..exceptions import exception_handler +from ...theme_list import BrickThemeList + +logger = logging.getLogger(__name__) + +admin_theme_page = Blueprint( + 'admin_theme', + __name__, + url_prefix='/admin/theme' +) + + +# Refresh the themes cache +@admin_theme_page.route('/refresh', methods=['GET']) +@login_required +@exception_handler(__file__) +def refresh() -> Response: + BrickThemeList(force=True) + + return redirect(url_for('admin.admin', open_theme=True)) + + +# Update the themes file +@admin_theme_page.route('/update', methods=['GET']) +@login_required +@exception_handler(__file__) +def update() -> Response: + BrickThemeList().update() + + BrickThemeList(force=True) + + return redirect(url_for('admin.admin', open_theme=True)) diff --git a/templates/admin/database.html b/templates/admin/database.html index b52ebab..36d2b0d 100644 --- a/templates/admin/database.html +++ b/templates/admin/database.html @@ -9,13 +9,13 @@
Your database needs to be upgraded.
The database file is: {{ config['DATABASE_PATH'] }}
at version {{ database_version }}
- Download the database file + Download the database file
{% if database_counters %}