From 7047a28845edb6176d5f16382a228def03ac26a9 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 31 Jan 2025 16:34:52 +0100 Subject: [PATCH] Set owners --- bricktracker/app.py | 6 +- bricktracker/metadata.py | 12 ++- bricktracker/metadata_list.py | 5 +- bricktracker/record_list.py | 2 + bricktracker/reload.py | 5 ++ bricktracker/set.py | 10 +++ bricktracker/set_list.py | 4 + bricktracker/set_owner.py | 16 ++++ bricktracker/set_owner_list.py | 17 ++++ bricktracker/sql/migrations/0013.sql | 19 +++++ bricktracker/sql/set/base/base.sql | 3 + bricktracker/sql/set/base/full.sql | 5 ++ bricktracker/sql/set/delete/set.sql | 3 + bricktracker/sql/set/metadata/owner/base.sql | 6 ++ .../sql/set/metadata/owner/delete.sql | 9 ++ .../sql/set/metadata/owner/insert.sql | 14 ++++ bricktracker/sql/set/metadata/owner/list.sql | 1 + .../sql/set/metadata/owner/select.sql | 5 ++ .../sql/set/metadata/owner/update/field.sql | 3 + .../sql/set/metadata/owner/update/state.sql | 10 +++ bricktracker/sql_counter.py | 3 +- bricktracker/version.py | 2 +- bricktracker/views/add.py | 4 + bricktracker/views/admin/admin.py | 11 ++- bricktracker/views/admin/owner.py | 84 +++++++++++++++++++ bricktracker/views/index.py | 5 +- bricktracker/views/set.py | 19 ++++- static/scripts/socket/set.js | 16 ++++ templates/add.html | 13 +++ templates/admin.html | 9 +- templates/admin/owner.html | 42 ++++++++++ templates/admin/owner/delete.html | 19 +++++ templates/macro/badge.html | 11 ++- templates/set/card.html | 9 ++ templates/set/management.html | 18 +++- templates/sets.html | 16 +++- 36 files changed, 418 insertions(+), 18 deletions(-) create mode 100644 bricktracker/set_owner.py create mode 100644 bricktracker/set_owner_list.py create mode 100644 bricktracker/sql/migrations/0013.sql create mode 100644 bricktracker/sql/set/metadata/owner/base.sql create mode 100644 bricktracker/sql/set/metadata/owner/delete.sql create mode 100644 bricktracker/sql/set/metadata/owner/insert.sql create mode 100644 bricktracker/sql/set/metadata/owner/list.sql create mode 100644 bricktracker/sql/set/metadata/owner/select.sql create mode 100644 bricktracker/sql/set/metadata/owner/update/field.sql create mode 100644 bricktracker/sql/set/metadata/owner/update/state.sql create mode 100644 bricktracker/views/admin/owner.py create mode 100644 templates/admin/owner.html create mode 100644 templates/admin/owner/delete.html diff --git a/bricktracker/app.py b/bricktracker/app.py index f6054bc..15cb9a3 100644 --- a/bricktracker/app.py +++ b/bricktracker/app.py @@ -13,11 +13,12 @@ from bricktracker.sql import close from bricktracker.version import __version__ from bricktracker.views.add import add_page from bricktracker.views.admin.admin import admin_page -from bricktracker.views.admin.status import admin_status_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.owner import admin_owner_page from bricktracker.views.admin.retired import admin_retired_page +from bricktracker.views.admin.status import admin_status_page from bricktracker.views.admin.theme import admin_theme_page from bricktracker.views.error import error_404 from bricktracker.views.index import index_page @@ -78,11 +79,12 @@ def setup_app(app: Flask) -> None: # Register admin routes app.register_blueprint(admin_page) - app.register_blueprint(admin_status_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_owner_page) + app.register_blueprint(admin_status_page) app.register_blueprint(admin_theme_page) # An helper to make global variables available to the diff --git a/bricktracker/metadata.py b/bricktracker/metadata.py index c7a9678..4b7c54e 100644 --- a/bricktracker/metadata.py +++ b/bricktracker/metadata.py @@ -176,8 +176,16 @@ class BrickMetadata(BrickRecord): return value # Update the selected state of this metadata item for a set - def update_set_state(self, brickset: 'BrickSet', json: Any | None) -> Any: - state: bool = json.get('value', False) # type: ignore + def update_set_state( + self, + brickset: 'BrickSet', + /, + *, + json: Any | None = None, + state: bool | None = None, + ) -> Any: + if state is None: + state = json.get('value', False) # type: ignore parameters = self.sql_parameters() parameters['set_id'] = brickset.fields.id diff --git a/bricktracker/metadata_list.py b/bricktracker/metadata_list.py index 80bb3df..bb2e337 100644 --- a/bricktracker/metadata_list.py +++ b/bricktracker/metadata_list.py @@ -1,14 +1,15 @@ import logging -from typing import Type +from typing import Type, TypeVar from .exceptions import NotFoundException from .fields import BrickRecordFields from .record_list import BrickRecordList +from .set_owner import BrickSetOwner from .set_status import BrickSetStatus logger = logging.getLogger(__name__) -T = BrickSetStatus +T = TypeVar('T', 'BrickSetStatus', 'BrickSetOwner') # Lego sets metadata list diff --git a/bricktracker/record_list.py b/bricktracker/record_list.py index 2275203..8927e71 100644 --- a/bricktracker/record_list.py +++ b/bricktracker/record_list.py @@ -8,12 +8,14 @@ if TYPE_CHECKING: from .part import BrickPart from .rebrickable_set import RebrickableSet from .set import BrickSet + from .set_owner import BrickSetOwner from .set_status import BrickSetStatus from .wish import BrickWish T = TypeVar( 'T', 'BrickSet', + 'BrickSetOwner', 'BrickSetStatus', 'BrickPart', 'BrickMinifigure', diff --git a/bricktracker/reload.py b/bricktracker/reload.py index 259cffa..73e9e24 100644 --- a/bricktracker/reload.py +++ b/bricktracker/reload.py @@ -1,5 +1,7 @@ from .instructions_list import BrickInstructionsList from .retired_list import BrickRetiredList +from .set_owner import BrickSetOwner +from .set_owner_list import BrickSetOwnerList from .set_status import BrickSetStatus from .set_status_list import BrickSetStatusList from .theme_list import BrickThemeList @@ -12,6 +14,9 @@ def reload() -> None: # Reload the instructions BrickInstructionsList(force=True) + # Reload the set owners + BrickSetOwnerList(BrickSetOwner, force=True) + # Reload the set statuses BrickSetStatusList(BrickSetStatus, force=True) diff --git a/bricktracker/set.py b/bricktracker/set.py index 32bf8da..f4bf1a2 100644 --- a/bricktracker/set.py +++ b/bricktracker/set.py @@ -9,6 +9,8 @@ from .exceptions import NotFoundException from .minifigure_list import BrickMinifigureList from .part_list import BrickPartList from .rebrickable_set import RebrickableSet +from .set_owner import BrickSetOwner +from .set_owner_list import BrickSetOwnerList from .set_status import BrickSetStatus from .set_status_list import BrickSetStatusList from .sql import BrickSQL @@ -68,6 +70,13 @@ class BrickSet(RebrickableSet): if not BrickMinifigureList.download(socket, self, refresh=refresh): return False + # Save the owners + owners: list[str] = list(data.get('owners', [])) + + for id in owners: + owner = BrickSetOwnerList(BrickSetOwner).get(id) + owner.update_set_state(self, state=True) + # Commit the transaction to the database socket.auto_progress( message='Set {set}: writing to the database'.format( @@ -162,6 +171,7 @@ class BrickSet(RebrickableSet): # Load from database if not self.select( + owners=BrickSetOwnerList(BrickSetOwner).as_columns(), statuses=BrickSetStatusList(BrickSetStatus).as_columns(all=True) ): raise NotFoundException( diff --git a/bricktracker/set_list.py b/bricktracker/set_list.py index 251cfdc..6a94185 100644 --- a/bricktracker/set_list.py +++ b/bricktracker/set_list.py @@ -3,6 +3,8 @@ from typing import Self from flask import current_app from .record_list import BrickRecordList +from .set_owner import BrickSetOwner +from .set_owner_list import BrickSetOwnerList from .set_status import BrickSetStatus from .set_status_list import BrickSetStatusList from .set import BrickSet @@ -38,6 +40,7 @@ class BrickSetList(BrickRecordList[BrickSet]): # Load the sets from the database for record in self.select( order=self.order, + owners=BrickSetOwnerList(BrickSetOwner).as_columns(), statuses=BrickSetStatusList(BrickSetStatus).as_columns() ): brickset = BrickSet(record=record) @@ -74,6 +77,7 @@ class BrickSetList(BrickRecordList[BrickSet]): for record in self.select( order=order, limit=limit, + owners=BrickSetOwnerList(BrickSetOwner).as_columns(), statuses=BrickSetStatusList(BrickSetStatus).as_columns() ): brickset = BrickSet(record=record) diff --git a/bricktracker/set_owner.py b/bricktracker/set_owner.py new file mode 100644 index 0000000..3c07647 --- /dev/null +++ b/bricktracker/set_owner.py @@ -0,0 +1,16 @@ +from .metadata import BrickMetadata + + +# Lego set owner metadata +class BrickSetOwner(BrickMetadata): + kind: str = 'owner' + + # Set state endpoint + set_state_endpoint: str = 'set.update_owner' + + # Queries + delete_query: str = 'set/metadata/owner/delete' + insert_query: str = 'set/metadata/owner/insert' + select_query: str = 'set/metadata/owner/select' + update_field_query: str = 'set/metadata/owner/update/field' + update_set_state_query: str = 'set/metadata/owner/update/state' diff --git a/bricktracker/set_owner_list.py b/bricktracker/set_owner_list.py new file mode 100644 index 0000000..1309749 --- /dev/null +++ b/bricktracker/set_owner_list.py @@ -0,0 +1,17 @@ +import logging + +from .metadata_list import BrickMetadataList +from .set_owner import BrickSetOwner + +logger = logging.getLogger(__name__) + + +# Lego sets owner list +class BrickSetOwnerList(BrickMetadataList[BrickSetOwner]): + kind: str = 'set owners' + + # Database table + table: str = 'bricktracker_set_owners' + + # Queries + select_query = 'set/metadata/owner/list' diff --git a/bricktracker/sql/migrations/0013.sql b/bricktracker/sql/migrations/0013.sql new file mode 100644 index 0000000..33f8a6f --- /dev/null +++ b/bricktracker/sql/migrations/0013.sql @@ -0,0 +1,19 @@ +-- description: Add set owners + +BEGIN TRANSACTION; + +-- Create a table to define each set owners: an id and a name +CREATE TABLE "bricktracker_metadata_owners" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + PRIMARY KEY("id") +); + +-- Create a table for the set owners +CREATE TABLE "bricktracker_set_owners" ( + "id" TEXT NOT NULL, + PRIMARY KEY("id"), + FOREIGN KEY("id") REFERENCES "bricktracker_sets"("id") +); + +COMMIT; diff --git a/bricktracker/sql/set/base/base.sql b/bricktracker/sql/set/base/base.sql index 8b1f4c8..940dab9 100644 --- a/bricktracker/sql/set/base/base.sql +++ b/bricktracker/sql/set/base/base.sql @@ -9,6 +9,9 @@ SELECT "rebrickable_sets"."number_of_parts", "rebrickable_sets"."image", "rebrickable_sets"."url", + {% block owners %} + {% if owners %}{{ owners }},{% endif %} + {% endblock %} {% block statuses %} {% if statuses %}{{ statuses }},{% endif %} {% endblock %} diff --git a/bricktracker/sql/set/base/full.sql b/bricktracker/sql/set/base/full.sql index 70730ff..725b56d 100644 --- a/bricktracker/sql/set/base/full.sql +++ b/bricktracker/sql/set/base/full.sql @@ -13,6 +13,11 @@ IFNULL("minifigures_join"."total", 0) AS "total_minifigures" {% endblock %} {% block join %} +{% if owners %} +LEFT JOIN "bricktracker_set_owners" +ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_owners"."id" +{% endif %} + {% if statuses %} LEFT JOIN "bricktracker_set_statuses" ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_statuses"."id" diff --git a/bricktracker/sql/set/delete/set.sql b/bricktracker/sql/set/delete/set.sql index 49b0e88..2db140d 100644 --- a/bricktracker/sql/set/delete/set.sql +++ b/bricktracker/sql/set/delete/set.sql @@ -6,6 +6,9 @@ BEGIN TRANSACTION; DELETE FROM "bricktracker_sets" WHERE "bricktracker_sets"."id" IS NOT DISTINCT FROM '{{ id }}'; +DELETE FROM "bricktracker_set_owners" +WHERE "bricktracker_set_owners"."id" IS NOT DISTINCT FROM '{{ id }}'; + DELETE FROM "bricktracker_set_statuses" WHERE "bricktracker_set_statuses"."id" IS NOT DISTINCT FROM '{{ id }}'; diff --git a/bricktracker/sql/set/metadata/owner/base.sql b/bricktracker/sql/set/metadata/owner/base.sql new file mode 100644 index 0000000..095ae3d --- /dev/null +++ b/bricktracker/sql/set/metadata/owner/base.sql @@ -0,0 +1,6 @@ +SELECT + "bricktracker_metadata_owners"."id", + "bricktracker_metadata_owners"."name" +FROM "bricktracker_metadata_owners" + +{% block where %}{% endblock %} \ No newline at end of file diff --git a/bricktracker/sql/set/metadata/owner/delete.sql b/bricktracker/sql/set/metadata/owner/delete.sql new file mode 100644 index 0000000..e9df18d --- /dev/null +++ b/bricktracker/sql/set/metadata/owner/delete.sql @@ -0,0 +1,9 @@ +BEGIN TRANSACTION; + +ALTER TABLE "bricktracker_set_owners" +DROP COLUMN "owner_{{ id }}"; + +DELETE FROM "bricktracker_metadata_owners" +WHERE "bricktracker_metadata_owners"."id" IS NOT DISTINCT FROM '{{ id }}'; + +COMMIT; \ No newline at end of file diff --git a/bricktracker/sql/set/metadata/owner/insert.sql b/bricktracker/sql/set/metadata/owner/insert.sql new file mode 100644 index 0000000..cc54a2a --- /dev/null +++ b/bricktracker/sql/set/metadata/owner/insert.sql @@ -0,0 +1,14 @@ +BEGIN TRANSACTION; + +ALTER TABLE "bricktracker_set_owners" +ADD COLUMN "owner_{{ id }}" BOOLEAN NOT NULL DEFAULT 0; + +INSERT INTO "bricktracker_metadata_owners" ( + "id", + "name" +) VALUES ( + '{{ id }}', + '{{ name }}' +); + +COMMIT; \ No newline at end of file diff --git a/bricktracker/sql/set/metadata/owner/list.sql b/bricktracker/sql/set/metadata/owner/list.sql new file mode 100644 index 0000000..e970cf9 --- /dev/null +++ b/bricktracker/sql/set/metadata/owner/list.sql @@ -0,0 +1 @@ +{% extends 'set/metadata/owner/base.sql' %} diff --git a/bricktracker/sql/set/metadata/owner/select.sql b/bricktracker/sql/set/metadata/owner/select.sql new file mode 100644 index 0000000..8224565 --- /dev/null +++ b/bricktracker/sql/set/metadata/owner/select.sql @@ -0,0 +1,5 @@ +{% extends 'set/metadata/owner/base.sql' %} + +{% block where %} +WHERE "bricktracker_metadata_owners"."id" IS NOT DISTINCT FROM :id +{% endblock %} \ No newline at end of file diff --git a/bricktracker/sql/set/metadata/owner/update/field.sql b/bricktracker/sql/set/metadata/owner/update/field.sql new file mode 100644 index 0000000..5f047a3 --- /dev/null +++ b/bricktracker/sql/set/metadata/owner/update/field.sql @@ -0,0 +1,3 @@ +UPDATE "bricktracker_metadata_owners" +SET "{{field}}" = :value +WHERE "bricktracker_metadata_owners"."id" IS NOT DISTINCT FROM :id diff --git a/bricktracker/sql/set/metadata/owner/update/state.sql b/bricktracker/sql/set/metadata/owner/update/state.sql new file mode 100644 index 0000000..2469207 --- /dev/null +++ b/bricktracker/sql/set/metadata/owner/update/state.sql @@ -0,0 +1,10 @@ +INSERT INTO "bricktracker_set_owners" ( + "id", + "{{name}}" +) VALUES ( + :set_id, + :state +) +ON CONFLICT("id") +DO UPDATE SET "{{name}}" = :state +WHERE "bricktracker_set_owners"."id" IS NOT DISTINCT FROM :set_id diff --git a/bricktracker/sql_counter.py b/bricktracker/sql_counter.py index 28c03a3..30c83d6 100644 --- a/bricktracker/sql_counter.py +++ b/bricktracker/sql_counter.py @@ -6,7 +6,8 @@ ALIASES: dict[str, Tuple[str, str]] = { 'bricktracker_minifigures': ('Bricktracker minifigures', 'group-line'), 'bricktracker_parts': ('Bricktracker parts', 'shapes-line'), 'bricktracker_set_checkboxes': ('Bricktracker set checkboxes (legacy)', 'checkbox-line'), # noqa: E501 - 'bricktracker_set_statuses': ('Bricktracker set statuses', 'checkbox-line'), # noqa: E501 + 'bricktracker_set_owners': ('Bricktracker set owners', 'checkbox-line'), # noqa: E501 + 'bricktracker_set_statuses': ('Bricktracker set statuses', 'user-line'), # noqa: E501 'bricktracker_set_storages': ('Bricktracker set storages', 'archive-2-line'), # noqa: E501 'bricktracker_sets': ('Bricktracker sets', 'hashtag'), 'bricktracker_wishes': ('Bricktracker wishes', 'gift-line'), diff --git a/bricktracker/version.py b/bricktracker/version.py index 11dd9c9..996b4f6 100644 --- a/bricktracker/version.py +++ b/bricktracker/version.py @@ -1,4 +1,4 @@ from typing import Final __version__: Final[str] = '1.2.0' -__database_version__: Final[int] = 12 +__database_version__: Final[int] = 13 diff --git a/bricktracker/views/add.py b/bricktracker/views/add.py index 44f3ddc..20607dc 100644 --- a/bricktracker/views/add.py +++ b/bricktracker/views/add.py @@ -3,6 +3,8 @@ from flask_login import login_required from ..configuration_list import BrickConfigurationList from .exceptions import exception_handler +from ..set_owner import BrickSetOwner +from ..set_owner_list import BrickSetOwnerList from ..socket import MESSAGES add_page = Blueprint('add', __name__, url_prefix='/add') @@ -17,6 +19,7 @@ def add() -> str: return render_template( 'add.html', + brickset_owners=BrickSetOwnerList(BrickSetOwner).list(), path=current_app.config['SOCKET_PATH'], namespace=current_app.config['SOCKET_NAMESPACE'], messages=MESSAGES @@ -32,6 +35,7 @@ def bulk() -> str: return render_template( 'add.html', + brickset_owners=BrickSetOwnerList(BrickSetOwner).list(), path=current_app.config['SOCKET_PATH'], namespace=current_app.config['SOCKET_NAMESPACE'], messages=MESSAGES, diff --git a/bricktracker/views/admin/admin.py b/bricktracker/views/admin/admin.py index c18a74b..36037d3 100644 --- a/bricktracker/views/admin/admin.py +++ b/bricktracker/views/admin/admin.py @@ -8,6 +8,8 @@ from ..exceptions import exception_handler from ...instructions_list import BrickInstructionsList from ...rebrickable_image import RebrickableImage from ...retired_list import BrickRetiredList +from ...set_owner import BrickSetOwner +from ...set_owner_list import BrickSetOwnerList from ...set_status import BrickSetStatus from ...set_status_list import BrickSetStatusList from ...sql_counter import BrickCounter @@ -28,6 +30,7 @@ def admin() -> str: database_exception: Exception | None = None database_upgrade_needed: bool = False database_version: int = -1 + metadata_owners: list[BrickSetOwner] = [] metadata_statuses: list[BrickSetStatus] = [] nil_minifigure_name: str = '' nil_minifigure_url: str = '' @@ -41,6 +44,7 @@ def admin() -> str: database_version = database.version database_counters = BrickSQL().count_records() + metadata_owners = BrickSetOwnerList(BrickSetOwner).list() metadata_statuses = BrickSetStatusList(BrickSetStatus).list(all=True) except Exception as e: database_exception = e @@ -65,6 +69,7 @@ def admin() -> str: 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_owner = request.args.get('open_owner', None) open_retired = request.args.get('open_retired', None) open_status = request.args.get('open_status', None) open_theme = request.args.get('open_theme', None) @@ -73,6 +78,7 @@ def admin() -> str: open_image is None and open_instructions is None and open_logout is None and + open_owner is None and open_retired is None and open_status is None and open_theme is None @@ -81,13 +87,13 @@ def admin() -> str: return render_template( 'admin.html', configuration=BrickConfigurationList.list(), - status_error=request.args.get('status_error'), database_counters=database_counters, database_error=request.args.get('database_error'), database_exception=database_exception, database_upgrade_needed=database_upgrade_needed, database_version=database_version, instructions=BrickInstructionsList(), + metadata_owners=metadata_owners, metadata_statuses=metadata_statuses, nil_minifigure_name=nil_minifigure_name, nil_minifigure_url=nil_minifigure_url, @@ -98,8 +104,11 @@ def admin() -> str: open_image=open_image, open_instructions=open_instructions, open_logout=open_logout, + open_owner=open_owner, open_retired=open_retired, open_theme=open_theme, + owner_error=request.args.get('owner_error'), + status_error=request.args.get('status_error'), retired=BrickRetiredList(), theme=BrickThemeList(), ) diff --git a/bricktracker/views/admin/owner.py b/bricktracker/views/admin/owner.py new file mode 100644 index 0000000..bfa799e --- /dev/null +++ b/bricktracker/views/admin/owner.py @@ -0,0 +1,84 @@ +from flask import ( + Blueprint, + redirect, + request, + render_template, + url_for, +) +from flask_login import login_required +from werkzeug.wrappers.response import Response + +from ..exceptions import exception_handler +from ...reload import reload +from ...set_owner import BrickSetOwner + +admin_owner_page = Blueprint( + 'admin_owner', + __name__, + url_prefix='/admin/owner' +) + + +# Add a metadata owner +@admin_owner_page.route('/add', methods=['POST']) +@login_required +@exception_handler( + __file__, + post_redirect='admin.admin', + error_name='owner_error', + open_owner=True +) +def add() -> Response: + BrickSetOwner().from_form(request.form).insert() + + reload() + + return redirect(url_for('admin.admin', open_owner=True)) + + +# Delete the metadata owner +@admin_owner_page.route('/delete', methods=['GET']) +@login_required +@exception_handler(__file__) +def delete(*, id: str) -> str: + return render_template( + 'admin.html', + delete_owner=True, + owner=BrickSetOwner().select_specific(id), + error=request.args.get('owner_error') + ) + + +# Actually delete the metadata owner +@admin_owner_page.route('/delete', methods=['POST']) +@login_required +@exception_handler( + __file__, + post_redirect='admin_owner.delete', + error_name='owner_error' +) +def do_delete(*, id: str) -> Response: + owner = BrickSetOwner().select_specific(id) + owner.delete() + + reload() + + return redirect(url_for('admin.admin', open_owner=True)) + + +# Rename the metadata owner +@admin_owner_page.route('/rename', methods=['POST']) +@login_required +@exception_handler( + __file__, + post_redirect='admin.admin', + error_name='owner_error', + open_owner=True +) +def rename(*, id: str) -> Response: + owner = BrickSetOwner().select_specific(id) + owner.from_form(request.form).rename() + + reload() + + return redirect(url_for('admin.admin', open_owner=True)) diff --git a/bricktracker/views/index.py b/bricktracker/views/index.py index f8fe7b7..3d8a55e 100644 --- a/bricktracker/views/index.py +++ b/bricktracker/views/index.py @@ -2,6 +2,8 @@ from flask import Blueprint, render_template from .exceptions import exception_handler from ..minifigure_list import BrickMinifigureList +from ..set_owner import BrickSetOwner +from ..set_owner_list import BrickSetOwnerList from ..set_status import BrickSetStatus from ..set_status_list import BrickSetStatusList from ..set_list import BrickSetList @@ -16,6 +18,7 @@ def index() -> str: return render_template( 'index.html', brickset_collection=BrickSetList().last(), - minifigure_collection=BrickMinifigureList().last(), + brickset_owners=BrickSetOwnerList(BrickSetOwner).list(), brickset_statuses=BrickSetStatusList(BrickSetStatus).list(), + minifigure_collection=BrickMinifigureList().last(), ) diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py index 9f1990c..fd922ec 100644 --- a/bricktracker/views/set.py +++ b/bricktracker/views/set.py @@ -16,6 +16,8 @@ from .exceptions import exception_handler from ..minifigure import BrickMinifigure from ..part import BrickPart from ..set import BrickSet +from ..set_owner import BrickSetOwner +from ..set_owner_list import BrickSetOwnerList from ..set_status import BrickSetStatus from ..set_status_list import BrickSetStatusList from ..set_list import BrickSetList @@ -33,11 +35,25 @@ def list() -> str: return render_template( 'sets.html', collection=BrickSetList().all(), + brickset_owners=BrickSetOwnerList(BrickSetOwner).list(), brickset_statuses=BrickSetStatusList(BrickSetStatus).list(), ) -# Change the status of a status +# Change the state of a owner +@set_page.route('//owner/', methods=['POST']) +@login_required +@exception_handler(__file__, json=True) +def update_owner(*, id: str, metadata_id: str) -> Response: + brickset = BrickSet().select_light(id) + owner = BrickSetOwnerList(BrickSetOwner).get(metadata_id) + + state = owner.update_set_state(brickset, json=request.json) + + return jsonify({'value': state}) + + +# Change the state of a status @set_page.route('//status/', methods=['POST']) @login_required @exception_handler(__file__, json=True) @@ -98,6 +114,7 @@ def details(*, id: str) -> str: 'set.html', item=BrickSet().select_specific(id), open_instructions=request.args.get('open_instructions'), + brickset_owners=BrickSetOwnerList(BrickSetOwner).list(), brickset_statuses=BrickSetStatusList(BrickSetStatus).list(all=True), ) diff --git a/static/scripts/socket/set.js b/static/scripts/socket/set.js index 07d7cc7..6459aa6 100644 --- a/static/scripts/socket/set.js +++ b/static/scripts/socket/set.js @@ -15,6 +15,7 @@ class BrickSetSocket extends BrickSocket { this.html_button = document.getElementById(id); this.html_input = document.getElementById(`${id}-set`); this.html_no_confim = document.getElementById(`${id}-no-confirm`); + this.html_owners = document.getElementById(`${id}-owners`); // Card elements this.html_card = document.getElementById(`${id}-card`); @@ -139,10 +140,21 @@ class BrickSetSocket extends BrickSocket { this.set_list_last_set = set; } + // Grab the owners + const owners = []; + if (this.html_owners) { + this.html_owners.querySelectorAll('input').forEach(input => { + if (input.checked) { + owners.push(input.value); + } + }); + } + this.spinner(true); this.socket.emit(this.messages.IMPORT_SET, { set: (set !== undefined) ? set : this.html_input.value, + owners: owners, refresh: this.refresh }); } else { @@ -247,6 +259,10 @@ class BrickSetSocket extends BrickSocket { this.html_input.disabled = !enabled; } + if (this.html_owners) { + this.html_owners.querySelectorAll('input').forEach(input => input.disabled = !enabled); + } + if (this.html_card_confirm) { this.html_card_confirm.disabled = !enabled; } diff --git a/templates/add.html b/templates/add.html index 1238739..59c5029 100644 --- a/templates/add.html +++ b/templates/add.html @@ -33,6 +33,19 @@ Add without confirmation + {% if brickset_owners | length %} +
Owners
+
+ {% for owner in brickset_owners %} + {% with id=owner.as_dataset() %} +
+ + +
+ {% endwith %} + {% endfor %} +
+ {% endif %}

diff --git a/templates/admin.html b/templates/admin.html index b2a54f3..962730b 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -12,10 +12,12 @@

Administration
- {% if delete_status %} - {% include 'admin/status/delete.html' %} - {% elif delete_database %} + {% if delete_database %} {% include 'admin/database/delete.html' %} + {% elif delete_owner %} + {% include 'admin/owner/delete.html' %} + {% elif delete_status %} + {% include 'admin/status/delete.html' %} {% elif drop_database %} {% include 'admin/database/drop.html' %} {% elif import_database %} @@ -30,6 +32,7 @@ {% endif %} {% include 'admin/theme.html' %} {% include 'admin/retired.html' %} + {% include 'admin/owner.html' %} {% include 'admin/status.html' %} {% include 'admin/database.html' %} {% include 'admin/configuration.html' %} diff --git a/templates/admin/owner.html b/templates/admin/owner.html new file mode 100644 index 0000000..7447a6d --- /dev/null +++ b/templates/admin/owner.html @@ -0,0 +1,42 @@ +{% import 'macro/accordion.html' as accordion %} + +{{ accordion.header('Set owners', 'owner', 'admin', expanded=open_owner, icon='user-line', class='p-0') }} +{% if owner_error %}{% endif %} +
    + {% if metadata_owners | length %} + {% for owner in metadata_owners %} +
  • +
    +
    + +
    +
    Name
    + + +
    +
    +
    + Delete +
    +
    +
  • + {% endfor %} + {% else %} +
  • No owner found.
  • + {% endif %} +
  • +
    +
    + +
    +
    Name
    + +
    +
    +
    + +
    +
    +
  • +
+{{ accordion.footer() }} diff --git a/templates/admin/owner/delete.html b/templates/admin/owner/delete.html new file mode 100644 index 0000000..56821f3 --- /dev/null +++ b/templates/admin/owner/delete.html @@ -0,0 +1,19 @@ +{% import 'macro/accordion.html' as accordion %} + +{{ accordion.header('Set owners danger zone', 'owner-danger', 'admin', expanded=true, danger=true, class='text-end') }} +
+ {% if owner_error %}{% endif %} + +
+
+
+
Name
+ +
+
+
+
+ Back to the admin + +
+{{ accordion.footer() }} diff --git a/templates/macro/badge.html b/templates/macro/badge.html index 3d8a5e2..2be95ca 100644 --- a/templates/macro/badge.html +++ b/templates/macro/badge.html @@ -50,9 +50,18 @@ {{ badge(check=quantity, solo=solo, last=last, color='success', icon='close-line', collapsible='Quantity:', text=quantity, alt='Quantity') }} {% endmacro %} +{% macro owner(item, owner, solo=false, last=false) %} + {% if last %} + {% set tooltip=owner.fields.name %} + {% else %} + {% set text=owner.fields.name %} + {% endif %} + {{ badge(check=item.fields[owner.as_column()], solo=solo, last=last, color='light text-success-emphasis bg-success-subtle border border-success-subtle', icon='user-line', text=text, alt='Owner', tooltip=tooltip) }} +{% endmacro %} + {% macro print(item, solo=false, last=false, header=false) %} {% if item.fields.print %} - {{ badge(url=item.url_for_print(), solo=solo, last=last, color='light border', icon='paint-brush-line', collapsible='Print') }} + {{ badge(url=item.url_for_print(), solo=solo, last=last, color='light border', icon='paint-brush-line', collapsible='Print') }} {% endif %} {% endmacro %} diff --git a/templates/set/card.html b/templates/set/card.html index c9afbdd..9f2f83a 100644 --- a/templates/set/card.html +++ b/templates/set/card.html @@ -9,6 +9,12 @@ data-year="{{ item.fields.year }}" data-theme="{{ item.theme.name | lower }}" data-minifigures="{{ item.fields.total_minifigures }}" data-has-minifigures="{{ (item.fields.total_minifigures > 0) | int }}" data-has-missing="{{ (item.fields.total_missing > 0) | int }}" data-has-missing-instructions="{{ (not (item.instructions | length)) | int }}" data-missing="{{ item.fields.total_missing }}" {% for status in brickset_statuses %}data-{{ status.as_dataset() }}="{{ item.fields[status.as_column()] }}" {% endfor %} + {% for owner in brickset_owners %} + {% with checked=item.fields[owner.as_column()] %} + data-{{ owner.as_dataset() }}="{{ checked }}" + {% if checked %} data-owner-{{ loop.index }}="{{ owner.fields.name | lower }}"{% endif %} + {% endwith %} + {% endfor %} {% endif %} > {{ card.header(item, item.fields.name, solo=solo, identifier=item.fields.set) }} @@ -19,6 +25,9 @@ {{ badge.parts(item.fields.number_of_parts, solo=solo, last=last) }} {{ badge.total_minifigures(item.fields.total_minifigures, solo=solo, last=last) }} {{ badge.total_missing(item.fields.total_missing, solo=solo, last=last) }} + {% for owner in brickset_owners %} + {{ badge.owner(item, owner, solo=solo, last=last) }} + {% endfor %} {% if not last %} {% if not solo %} {{ badge.instructions(item, solo=solo, last=last) }} diff --git a/templates/set/management.html b/templates/set/management.html index 3807480..957db81 100644 --- a/templates/set/management.html +++ b/templates/set/management.html @@ -1,6 +1,20 @@ {% if g.login.is_authenticated() %} +{{ accordion.header('Owners', 'owner', 'set-details', icon='group-line', class='p-0') }} +
    + {% if brickset_owners | length %} + {% for owner in brickset_owners %} +
  • {{ form.checkbox(item, owner, delete=delete) }}
  • + {% endfor %} + {% else %} +
  • No owner found.
  • + {% endif %} +
+ +{{ accordion.footer() }} {{ accordion.header('Management', 'management', 'set-details', expanded=true, icon='settings-4-line') }} -
Data
- Refresh the set data +
Data
+ Refresh the set data {{ accordion.footer() }} {% endif %} diff --git a/templates/sets.html b/templates/sets.html index 97a7922..b06c94d 100644 --- a/templates/sets.html +++ b/templates/sets.html @@ -10,7 +10,7 @@
Search - +
@@ -75,6 +75,20 @@
+
+ +
+ Owner + +
+
{% for item in collection %}