diff --git a/CHANGELOG.md b/CHANGELOG.md index 40b4f17..8a1c34d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ Parts - Fix missing @login_required for set deletion - Ownership - Tags + - Storage - Socket - Add decorator for rebrickable, authenticated and threaded socket actions @@ -76,6 +77,7 @@ Parts - Ownership - Tags - Refresh + - Storage - Sets grid - Collapsible controls depending on screen size diff --git a/bricktracker/app.py b/bricktracker/app.py index 240bc63..4b6f0d4 100644 --- a/bricktracker/app.py +++ b/bricktracker/app.py @@ -19,6 +19,7 @@ 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.storage import admin_storage_page from bricktracker.views.admin.tag import admin_tag_page from bricktracker.views.admin.theme import admin_theme_page from bricktracker.views.error import error_404 @@ -86,6 +87,7 @@ def setup_app(app: Flask) -> None: app.register_blueprint(admin_retired_page) app.register_blueprint(admin_owner_page) app.register_blueprint(admin_status_page) + app.register_blueprint(admin_storage_page) app.register_blueprint(admin_tag_page) app.register_blueprint(admin_theme_page) diff --git a/bricktracker/metadata.py b/bricktracker/metadata.py index f43eaf5..07545f9 100644 --- a/bricktracker/metadata.py +++ b/bricktracker/metadata.py @@ -36,6 +36,9 @@ class BrickMetadata(BrickRecord): ): super().__init__() + # Defined an empty ID + self.fields.id = None + # Ingest the record if it has one if record is not None: self.ingest(record) @@ -129,8 +132,8 @@ class BrickMetadata(BrickRecord): json: Any | None = None, value: Any | None = None ) -> Any: - if value is None: - value = json.get('value', None) # type: ignore + if value is None and json is not None: + value = json.get('value', None) if value is None: raise ErrorException('"{field}" of a {kind} cannot be set to an empty value'.format( # noqa: E501 @@ -180,16 +183,15 @@ class BrickMetadata(BrickRecord): /, *, json: Any | None = None, - state: bool | None = None, + state: Any | None = None ) -> Any: - if state is None: - state = json.get('value', False) # type: ignore + if state is None and json is not None: + state = json.get('value', False) parameters = self.sql_parameters() parameters['set_id'] = brickset.fields.id parameters['state'] = state - # Update the status rows, _ = BrickSQL().execute_and_commit( self.update_set_state_query, parameters=parameters, @@ -205,7 +207,53 @@ class BrickMetadata(BrickRecord): )) # Info - logger.info('{kind} "{name}" state change to "{state}" for set {set} ({id})'.format( # noqa: E501 + logger.info('{kind} "{name}" state changed to "{state}" for set {set} ({id})'.format( # noqa: E501 + kind=self.kind, + name=self.fields.name, + state=state, + set=brickset.fields.set, + id=brickset.fields.id, + )) + + return state + + # Update the selected value of this metadata item for a set + def update_set_value( + self, + brickset: 'BrickSet', + /, + *, + json: Any | None = None, + state: Any | None = None, + ) -> Any: + if state is None and json is not None: + state = json.get('value', '') + + if state == '': + state = None + + parameters = self.sql_parameters() + parameters['set_id'] = brickset.fields.id + parameters['state'] = state + + rows, _ = BrickSQL().execute_and_commit( + self.update_set_state_query, + parameters=parameters, + ) + + # Update the status + if state is None and not hasattr(self.fields, 'name'): + self.fields.name = 'None' + + if rows != 1: + raise DatabaseException('Could not update the {kind} value for set {set} ({id})'.format( # noqa: E501 + kind=self.kind, + set=brickset.fields.set, + id=brickset.fields.id, + )) + + # Info + logger.info('{kind} value changed to "{name}" ({state}) for set {set} ({id})'.format( # noqa: E501 kind=self.kind, name=self.fields.name, state=state, diff --git a/bricktracker/metadata_list.py b/bricktracker/metadata_list.py index b0d42c3..5dfa73c 100644 --- a/bricktracker/metadata_list.py +++ b/bricktracker/metadata_list.py @@ -1,16 +1,19 @@ import logging -from typing import Type, TypeVar +from typing import List, overload, Self, Type, TypeVar + +from flask import url_for from .exceptions import NotFoundException from .fields import BrickRecordFields from .record_list import BrickRecordList from .set_owner import BrickSetOwner from .set_status import BrickSetStatus +from .set_storage import BrickSetStorage from .set_tag import BrickSetTag logger = logging.getLogger(__name__) -T = TypeVar('T', BrickSetStatus, BrickSetOwner, BrickSetTag) +T = TypeVar('T', BrickSetOwner, BrickSetStatus, BrickSetStorage, BrickSetTag) # Lego sets metadata list @@ -25,55 +28,119 @@ class BrickMetadataList(BrickRecordList[T]): # Queries select_query: str - def __init__(self, model: Type[T], /, *, force: bool = False): - # Load statuses only if there is none already loaded - records = getattr(self, 'records', None) + # Set state endpoint + set_state_endpoint: str - if records is None or force: - # Don't use super()__init__ as it would mask class variables - self.fields = BrickRecordFields() + def __init__( + self, + model: Type[T], + /, + *, + force: bool = False, + records: list[T] | None = None + ): + self.model = model - logger.info('Loading {kind} list'.format( - kind=self.kind - )) + # Records override (masking the class variables with instance ones) + if records is not None: + self.records = [] + self.mapping = {} - self.__class__.records = [] - self.__class__.mapping = {} + for metadata in records: + self.records.append(metadata) + self.mapping[metadata.fields.id] = metadata + else: + # Load metadata only if there is none already loaded + records = getattr(self, 'records', None) - # Load the statuses from the database - for record in self.select(): - status = model(record=record) + if records is None or force: + # Don't use super()__init__ as it would mask class variables + self.fields = BrickRecordFields() - self.__class__.records.append(status) - self.__class__.mapping[status.fields.id] = status + logger.info('Loading {kind} list'.format( + kind=self.kind + )) - # Return the items as columns for a select - def as_columns(self, /, **kwargs) -> str: - return ', '.join([ - '"{table}"."{column}"'.format( - table=self.table, - column=record.as_column(), - ) - for record - in self.filter(**kwargs) - ]) + self.__class__.records = [] + self.__class__.mapping = {} + + # Load the metadata from the database + for record in self.select(): + metadata = model(record=record) + + self.__class__.records.append(metadata) + self.__class__.mapping[metadata.fields.id] = metadata + + # HTML prefix name + def as_prefix(self, /) -> str: + return self.kind.replace(' ', '-') # Filter the list of records (this one does nothing) def filter(self) -> list[T]: return self.records + # Return the items as columns for a select + @classmethod + def as_columns(cls, /, **kwargs) -> str: + new = cls.new() + + return ', '.join([ + '"{table}"."{column}"'.format( + table=cls.table, + column=record.as_column(), + ) + for record + in new.filter(**kwargs) + ]) + # Grab a specific status - def get(self, id: str, /) -> T: - if id not in self.mapping: + @classmethod + def get(cls, id: str, /, *, allow_none: bool = False) -> T: + new = cls.new() + + if allow_none and id == '': + return new.model() + + if id not in new.mapping: raise NotFoundException( '{kind} with ID {id} was not found in the database'.format( - kind=self.kind.capitalize(), + kind=new.kind.capitalize(), id=id, ), ) - return self.mapping[id] + return new.mapping[id] # Get the list of statuses depending on the context - def list(self, /, **kwargs) -> list[T]: - return self.filter(**kwargs) + @overload + @classmethod + def list(cls, /, **kwargs) -> List[T]: ... + + @overload + @classmethod + def list(cls, /, as_class: bool = False, **kwargs) -> Self: ... + + @classmethod + def list(cls, /, as_class: bool = False, **kwargs) -> List[T] | Self: + new = cls.new() + list = new.filter(**kwargs) + + if as_class: + print(list) + # Return a copy of the metadata list with overriden records + return cls(new.model, records=list) + else: + return list + + # Instantiate the list with the proper class + @classmethod + def new(cls, /, *, force: bool = False) -> Self: + raise Exception('new() is not implemented for BrickMetadataList') + + # URL to change the selected state of this metadata item for a set + @classmethod + def url_for_set_state(cls, id: str, /) -> str: + return url_for( + cls.set_state_endpoint, + id=id, + ) diff --git a/bricktracker/record_list.py b/bricktracker/record_list.py index 3de9bf9..23da29b 100644 --- a/bricktracker/record_list.py +++ b/bricktracker/record_list.py @@ -10,17 +10,19 @@ if TYPE_CHECKING: from .set import BrickSet from .set_owner import BrickSetOwner from .set_status import BrickSetStatus + from .set_storage import BrickSetStorage from .set_tag import BrickSetTag from .wish import BrickWish T = TypeVar( 'T', + 'BrickMinifigure', + 'BrickPart', 'BrickSet', 'BrickSetOwner', 'BrickSetStatus', + 'BrickSetStorage', 'BrickSetTag', - 'BrickPart', - 'BrickMinifigure', 'BrickWish', 'RebrickableSet' ) diff --git a/bricktracker/reload.py b/bricktracker/reload.py index 6673ab1..b2247ea 100644 --- a/bricktracker/reload.py +++ b/bricktracker/reload.py @@ -2,6 +2,7 @@ from .instructions_list import BrickInstructionsList from .retired_list import BrickRetiredList from .set_owner_list import BrickSetOwnerList from .set_status_list import BrickSetStatusList +from .set_storage_list import BrickSetStorageList from .set_tag_list import BrickSetTagList from .theme_list import BrickThemeList @@ -19,6 +20,9 @@ def reload() -> None: # Reload the set statuses BrickSetStatusList.new(force=True) + # Reload the set storages + BrickSetStorageList.new(force=True) + # Reload the set tags BrickSetTagList.new(force=True) diff --git a/bricktracker/set.py b/bricktracker/set.py index 09b22ec..90b2679 100644 --- a/bricktracker/set.py +++ b/bricktracker/set.py @@ -11,6 +11,7 @@ from .part_list import BrickPartList from .rebrickable_set import RebrickableSet from .set_owner_list import BrickSetOwnerList from .set_status_list import BrickSetStatusList +from .set_storage_list import BrickSetStorageList from .set_tag_list import BrickSetTagList from .sql import BrickSQL if TYPE_CHECKING: @@ -55,9 +56,30 @@ class BrickSet(RebrickableSet): self.fields.id = str(uuid4()) if not refresh: + # Save the storage + storage = BrickSetStorageList.get( + data.get('storage', ''), + allow_none=True + ) + self.fields.storage = storage.fields.id + # Insert into database self.insert(commit=False) + # Save the owners + owners: list[str] = list(data.get('owners', [])) + + for id in owners: + owner = BrickSetOwnerList.get(id) + owner.update_set_state(self, state=True) + + # Save the tags + tags: list[str] = list(data.get('tags', [])) + + for id in tags: + tag = BrickSetTagList.get(id) + tag.update_set_state(self, state=True) + # Insert the rebrickable set into database self.insert_rebrickable() @@ -69,20 +91,6 @@ 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) - - # Save the tags - tags: list[str] = list(data.get('tags', [])) - - for id in tags: - tag = BrickSetTagList(BrickSetTag).get(id) - tag.update_set_state(self, state=True) - # Commit the transaction to the database socket.auto_progress( message='Set {set}: writing to the database'.format( @@ -166,9 +174,9 @@ class BrickSet(RebrickableSet): # Load from database if not self.select( - owners=BrickSetOwnerList.new().as_columns(), - statuses=BrickSetStatusList.new().as_columns(all=True), - tags=BrickSetTagList.new().as_columns(), + owners=BrickSetOwnerList.as_columns(), + statuses=BrickSetStatusList.as_columns(all=True), + tags=BrickSetTagList.as_columns(), ): raise NotFoundException( 'Set with ID {id} was not found in the database'.format( diff --git a/bricktracker/set_list.py b/bricktracker/set_list.py index 28d8153..e25594c 100644 --- a/bricktracker/set_list.py +++ b/bricktracker/set_list.py @@ -41,9 +41,9 @@ class BrickSetList(BrickRecordList[BrickSet]): # Load the sets from the database for record in self.select( order=self.order, - owners=BrickSetOwnerList.new().as_columns(), - statuses=BrickSetStatusList.new().as_columns(), - tags=BrickSetTagList.new().as_columns(), + owners=BrickSetOwnerList.as_columns(), + statuses=BrickSetStatusList.as_columns(), + tags=BrickSetTagList.as_columns(), ): brickset = BrickSet(record=record) @@ -112,9 +112,9 @@ class BrickSetList(BrickRecordList[BrickSet]): for record in self.select( order=order, limit=limit, - owners=BrickSetOwnerList.new().as_columns(), - statuses=BrickSetStatusList.new().as_columns(), - tags=BrickSetTagList.new().as_columns(), + owners=BrickSetOwnerList.as_columns(), + statuses=BrickSetStatusList.as_columns(), + tags=BrickSetTagList.as_columns(), ): brickset = BrickSet(record=record) diff --git a/bricktracker/set_storage.py b/bricktracker/set_storage.py new file mode 100644 index 0000000..0a54262 --- /dev/null +++ b/bricktracker/set_storage.py @@ -0,0 +1,13 @@ +from .metadata import BrickMetadata + + +# Lego set storage metadata +class BrickSetStorage(BrickMetadata): + kind: str = 'storage' + + # Queries + delete_query: str = 'set/metadata/storage/delete' + insert_query: str = 'set/metadata/storage/insert' + select_query: str = 'set/metadata/storage/select' + update_field_query: str = 'set/metadata/storage/update/field' + update_set_state_query: str = 'set/metadata/storage/update/state' diff --git a/bricktracker/set_storage_list.py b/bricktracker/set_storage_list.py new file mode 100644 index 0000000..72efde7 --- /dev/null +++ b/bricktracker/set_storage_list.py @@ -0,0 +1,23 @@ +import logging +from typing import Self + +from .metadata_list import BrickMetadataList +from .set_storage import BrickSetStorage + +logger = logging.getLogger(__name__) + + +# Lego sets storage list +class BrickSetStorageList(BrickMetadataList[BrickSetStorage]): + kind: str = 'set storages' + + # Queries + select_query = 'set/metadata/storage/list' + + # Set state endpoint + set_state_endpoint: str = 'set.update_storage' + + # Instantiate the list with the proper class + @classmethod + def new(cls, /, *, force: bool = False) -> Self: + return cls(BrickSetStorage, force=force) diff --git a/bricktracker/sql/set/base/base.sql b/bricktracker/sql/set/base/base.sql index 331b15e..fbc86e0 100644 --- a/bricktracker/sql/set/base/base.sql +++ b/bricktracker/sql/set/base/base.sql @@ -1,5 +1,6 @@ SELECT {% block id %}{% endblock %} + "bricktracker_sets"."storage", "rebrickable_sets"."set", "rebrickable_sets"."number", "rebrickable_sets"."version", diff --git a/bricktracker/sql/set/insert.sql b/bricktracker/sql/set/insert.sql index 7dd6dec..9a46f88 100644 --- a/bricktracker/sql/set/insert.sql +++ b/bricktracker/sql/set/insert.sql @@ -1,7 +1,9 @@ INSERT OR IGNORE INTO "bricktracker_sets" ( "id", - "set" + "set", + "storage" ) VALUES ( :id, - :set + :set, + :storage ) diff --git a/bricktracker/sql/set/metadata/storage/base.sql b/bricktracker/sql/set/metadata/storage/base.sql new file mode 100644 index 0000000..2417aa6 --- /dev/null +++ b/bricktracker/sql/set/metadata/storage/base.sql @@ -0,0 +1,6 @@ +SELECT + "bricktracker_metadata_storages"."id", + "bricktracker_metadata_storages"."name" +FROM "bricktracker_metadata_storages" + +{% block where %}{% endblock %} \ No newline at end of file diff --git a/bricktracker/sql/set/metadata/storage/delete.sql b/bricktracker/sql/set/metadata/storage/delete.sql new file mode 100644 index 0000000..c50b348 --- /dev/null +++ b/bricktracker/sql/set/metadata/storage/delete.sql @@ -0,0 +1,6 @@ +BEGIN TRANSACTION; + +DELETE FROM "bricktracker_metadata_storages" +WHERE "bricktracker_metadata_statuses"."id" IS NOT DISTINCT FROM '{{ id }}'; + +COMMIT; \ No newline at end of file diff --git a/bricktracker/sql/set/metadata/storage/insert.sql b/bricktracker/sql/set/metadata/storage/insert.sql new file mode 100644 index 0000000..262c651 --- /dev/null +++ b/bricktracker/sql/set/metadata/storage/insert.sql @@ -0,0 +1,11 @@ +BEGIN TRANSACTION; + +INSERT INTO "bricktracker_metadata_storages" ( + "id", + "name" +) VALUES ( + '{{ id }}', + '{{ name }}' +); + +COMMIT; \ No newline at end of file diff --git a/bricktracker/sql/set/metadata/storage/list.sql b/bricktracker/sql/set/metadata/storage/list.sql new file mode 100644 index 0000000..87ac7a4 --- /dev/null +++ b/bricktracker/sql/set/metadata/storage/list.sql @@ -0,0 +1 @@ +{% extends 'set/metadata/storage/base.sql' %} diff --git a/bricktracker/sql/set/metadata/storage/select.sql b/bricktracker/sql/set/metadata/storage/select.sql new file mode 100644 index 0000000..b37a7e8 --- /dev/null +++ b/bricktracker/sql/set/metadata/storage/select.sql @@ -0,0 +1,5 @@ +{% extends 'set/metadata/storage/base.sql' %} + +{% block where %} +WHERE "bricktracker_metadata_storages"."id" IS NOT DISTINCT FROM :id +{% endblock %} \ No newline at end of file diff --git a/bricktracker/sql/set/metadata/storage/update/field.sql b/bricktracker/sql/set/metadata/storage/update/field.sql new file mode 100644 index 0000000..d27d27c --- /dev/null +++ b/bricktracker/sql/set/metadata/storage/update/field.sql @@ -0,0 +1,3 @@ +UPDATE "bricktracker_metadata_storages" +SET "{{field}}" = :value +WHERE "bricktracker_metadata_storages"."id" IS NOT DISTINCT FROM :id diff --git a/bricktracker/sql/set/metadata/storage/update/state.sql b/bricktracker/sql/set/metadata/storage/update/state.sql new file mode 100644 index 0000000..7cc40d6 --- /dev/null +++ b/bricktracker/sql/set/metadata/storage/update/state.sql @@ -0,0 +1,3 @@ +UPDATE "bricktracker_sets" +SET "storage" = :state +WHERE "bricktracker_sets"."id" IS NOT DISTINCT FROM :set_id diff --git a/bricktracker/views/add.py b/bricktracker/views/add.py index e77e7dd..fb11efe 100644 --- a/bricktracker/views/add.py +++ b/bricktracker/views/add.py @@ -4,6 +4,7 @@ from flask_login import login_required from ..configuration_list import BrickConfigurationList from .exceptions import exception_handler from ..set_owner_list import BrickSetOwnerList +from ..set_storage_list import BrickSetStorageList from ..set_tag_list import BrickSetTagList from ..socket import MESSAGES @@ -19,8 +20,9 @@ def add() -> str: return render_template( 'add.html', - brickset_owners=BrickSetOwnerList.new().list(), - brickset_tags=BrickSetTagList.new().list(), + brickset_owners=BrickSetOwnerList.list(), + brickset_storages=BrickSetStorageList.list(), + brickset_tags=BrickSetTagList.list(), path=current_app.config['SOCKET_PATH'], namespace=current_app.config['SOCKET_NAMESPACE'], messages=MESSAGES @@ -36,8 +38,9 @@ def bulk() -> str: return render_template( 'add.html', - brickset_owners=BrickSetOwnerList.new().list(), - brickset_tags=BrickSetTagList.new().list(), + brickset_owners=BrickSetOwnerList.list(), + brickset_storages=BrickSetStorageList.list(), + brickset_tags=BrickSetTagList.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 c716b56..584a359 100644 --- a/bricktracker/views/admin/admin.py +++ b/bricktracker/views/admin/admin.py @@ -10,6 +10,8 @@ from ...rebrickable_image import RebrickableImage from ...retired_list import BrickRetiredList from ...set_owner import BrickSetOwner from ...set_owner_list import BrickSetOwnerList +from ...set_storage import BrickSetStorage +from ...set_storage_list import BrickSetStorageList from ...set_status import BrickSetStatus from ...set_status_list import BrickSetStatusList from ...set_tag import BrickSetTag @@ -34,6 +36,7 @@ def admin() -> str: database_version: int = -1 metadata_owners: list[BrickSetOwner] = [] metadata_statuses: list[BrickSetStatus] = [] + metadata_storages: list[BrickSetStorage] = [] metadata_tags: list[BrickSetTag] = [] nil_minifigure_name: str = '' nil_minifigure_url: str = '' @@ -47,9 +50,10 @@ def admin() -> str: database_version = database.version database_counters = BrickSQL().count_records() - metadata_owners = BrickSetOwnerList.new().list() - metadata_statuses = BrickSetStatusList.new().list(all=True) - metadata_tags = BrickSetTagList.new().list() + metadata_owners = BrickSetOwnerList.list() + metadata_statuses = BrickSetStatusList.list(all=True) + metadata_storages = BrickSetStorageList.list() + metadata_tags = BrickSetTagList.list() except Exception as e: database_exception = e @@ -76,6 +80,7 @@ def admin() -> str: 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_storage = request.args.get('open_storage', None) open_tag = request.args.get('open_tag', None) open_theme = request.args.get('open_theme', None) @@ -86,6 +91,7 @@ def admin() -> str: open_owner is None and open_retired is None and open_status is None and + open_storage is None and open_tag is None and open_theme is None ) @@ -101,6 +107,7 @@ def admin() -> str: instructions=BrickInstructionsList(), metadata_owners=metadata_owners, metadata_statuses=metadata_statuses, + metadata_storages=metadata_storages, metadata_tags=metadata_tags, nil_minifigure_name=nil_minifigure_name, nil_minifigure_url=nil_minifigure_url, @@ -113,11 +120,13 @@ def admin() -> str: open_owner=open_owner, open_retired=open_retired, open_status=open_status, + open_storage=open_storage, open_tag=open_tag, open_theme=open_theme, owner_error=request.args.get('owner_error'), retired=BrickRetiredList(), status_error=request.args.get('status_error'), + storage_error=request.args.get('storage_error'), tag_error=request.args.get('tag_error'), theme=BrickThemeList(), ) diff --git a/bricktracker/views/admin/storage.py b/bricktracker/views/admin/storage.py new file mode 100644 index 0000000..7c686bf --- /dev/null +++ b/bricktracker/views/admin/storage.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_storage import BrickSetStorage + +admin_storage_page = Blueprint( + 'admin_storage', + __name__, + url_prefix='/admin/storage' +) + + +# Add a metadata storage +@admin_storage_page.route('/add', methods=['POST']) +@login_required +@exception_handler( + __file__, + post_redirect='admin.admin', + error_name='storage_error', + open_storage=True +) +def add() -> Response: + BrickSetStorage().from_form(request.form).insert() + + reload() + + return redirect(url_for('admin.admin', open_storage=True)) + + +# Delete the metadata storage +@admin_storage_page.route('/delete', methods=['GET']) +@login_required +@exception_handler(__file__) +def delete(*, id: str) -> str: + return render_template( + 'admin.html', + delete_storage=True, + storage=BrickSetStorage().select_specific(id), + error=request.args.get('storage_error') + ) + + +# Actually delete the metadata storage +@admin_storage_page.route('/delete', methods=['POST']) +@login_required +@exception_handler( + __file__, + post_redirect='admin_storage.delete', + error_name='storage_error' +) +def do_delete(*, id: str) -> Response: + storage = BrickSetStorage().select_specific(id) + storage.delete() + + reload() + + return redirect(url_for('admin.admin', open_storage=True)) + + +# Rename the metadata storage +@admin_storage_page.route('/rename', methods=['POST']) +@login_required +@exception_handler( + __file__, + post_redirect='admin.admin', + error_name='storage_error', + open_storage=True +) +def rename(*, id: str) -> Response: + storage = BrickSetStorage().select_specific(id) + storage.from_form(request.form).rename() + + reload() + + return redirect(url_for('admin.admin', open_storage=True)) diff --git a/bricktracker/views/index.py b/bricktracker/views/index.py index 0a25e07..b64775b 100644 --- a/bricktracker/views/index.py +++ b/bricktracker/views/index.py @@ -4,6 +4,7 @@ from .exceptions import exception_handler from ..minifigure_list import BrickMinifigureList from ..set_owner_list import BrickSetOwnerList from ..set_status_list import BrickSetStatusList +from ..set_storage_list import BrickSetStorageList from ..set_tag_list import BrickSetTagList from ..set_list import BrickSetList @@ -17,8 +18,9 @@ def index() -> str: return render_template( 'index.html', brickset_collection=BrickSetList().last(), - brickset_owners=BrickSetOwnerList.new().list(), - brickset_statuses=BrickSetStatusList.new().list(), - brickset_tags=BrickSetTagList.new().list(), + brickset_owners=BrickSetOwnerList.list(), + brickset_statuses=BrickSetStatusList.list(), + brickset_storages=BrickSetStorageList.list(as_class=True), + brickset_tags=BrickSetTagList.list(), minifigure_collection=BrickMinifigureList().last(), ) diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py index 6d86bb1..8983cf9 100644 --- a/bricktracker/views/set.py +++ b/bricktracker/views/set.py @@ -19,6 +19,7 @@ from ..set import BrickSet from ..set_list import BrickSetList from ..set_owner_list import BrickSetOwnerList from ..set_status_list import BrickSetStatusList +from ..set_storage_list import BrickSetStorageList from ..set_tag_list import BrickSetTagList from ..socket import MESSAGES @@ -34,9 +35,10 @@ def list() -> str: return render_template( 'sets.html', collection=BrickSetList().all(), - brickset_owners=BrickSetOwnerList.new().list(), - brickset_statuses=BrickSetStatusList.new().list(), - brickset_tags=BrickSetTagList.new().list(), + brickset_owners=BrickSetOwnerList.list(), + brickset_statuses=BrickSetStatusList.list(), + brickset_storages=BrickSetStorageList.list(as_class=True), + brickset_tags=BrickSetTagList.list(), ) @@ -46,7 +48,7 @@ def list() -> str: @exception_handler(__file__, json=True) def update_owner(*, id: str, metadata_id: str) -> Response: brickset = BrickSet().select_light(id) - owner = BrickSetOwnerList.new().get(metadata_id) + owner = BrickSetOwnerList.get(metadata_id) state = owner.update_set_state(brickset, json=request.json) @@ -59,20 +61,36 @@ def update_owner(*, id: str, metadata_id: str) -> Response: @exception_handler(__file__, json=True) def update_status(*, id: str, metadata_id: str) -> Response: brickset = BrickSet().select_light(id) - status = BrickSetStatusList.new().get(metadata_id) + status = BrickSetStatusList.get(metadata_id) state = status.update_set_state(brickset, json=request.json) return jsonify({'value': state}) +# Change the state of a storage +@set_page.route('//storage', methods=['POST']) +@login_required +@exception_handler(__file__, json=True) +def update_storage(*, id: str) -> Response: + brickset = BrickSet().select_light(id) + storage = BrickSetStorageList.get( + request.json.get('value', ''), # type: ignore + allow_none=True + ) + + state = storage.update_set_value(brickset, state=storage.fields.id) + + return jsonify({'value': state}) + + # Change the state of a tag @set_page.route('//tag/', methods=['POST']) @login_required @exception_handler(__file__, json=True) def update_tag(*, id: str, metadata_id: str) -> Response: brickset = BrickSet().select_light(id) - tag = BrickSetTagList.new().get(metadata_id) + tag = BrickSetTagList.get(metadata_id) state = tag.update_set_state(brickset, json=request.json) @@ -127,9 +145,10 @@ def details(*, id: str) -> str: 'set.html', item=BrickSet().select_specific(id), open_instructions=request.args.get('open_instructions'), - brickset_owners=BrickSetOwnerList.new().list(), - brickset_statuses=BrickSetStatusList.new().list(all=True), - brickset_tags=BrickSetTagList.new().list(), + brickset_owners=BrickSetOwnerList.list(), + brickset_statuses=BrickSetStatusList.list(all=True), + brickset_storages=BrickSetStorageList.list(as_class=True), + brickset_tags=BrickSetTagList.list(), ) diff --git a/static/scripts/socket/set.js b/static/scripts/socket/set.js index d561261..311eac5 100644 --- a/static/scripts/socket/set.js +++ b/static/scripts/socket/set.js @@ -16,6 +16,7 @@ class BrickSetSocket extends BrickSocket { this.html_input = document.getElementById(`${id}-set`); this.html_no_confim = document.getElementById(`${id}-no-confirm`); this.html_owners = document.getElementById(`${id}-owners`); + this.html_storage = document.getElementById(`${id}-storage`); this.html_tags = document.getElementById(`${id}-tags`); // Card elements @@ -151,6 +152,12 @@ class BrickSetSocket extends BrickSocket { }); } + // Grab the storage + let storage = null; + if (this.html_storage) { + storage = this.html_storage.value; + } + // Grab the tags const tags = []; if (this.html_tags) { @@ -170,6 +177,7 @@ class BrickSetSocket extends BrickSocket { this.socket.emit(this.messages.IMPORT_SET, { set: (set !== undefined) ? set : this.html_input.value, owners: owners, + storage: storage, tags: tags, refresh: this.refresh }); @@ -285,6 +293,10 @@ class BrickSetSocket extends BrickSocket { this.html_owners.querySelectorAll('input').forEach(input => input.disabled = !enabled); } + if (this.html_storage) { + this.html_storage.disabled = !enabled; + } + if (this.html_tags) { this.html_tags.querySelectorAll('input').forEach(input => input.disabled = !enabled); } diff --git a/templates/add.html b/templates/add.html index 9f33c05..9a0deeb 100644 --- a/templates/add.html +++ b/templates/add.html @@ -51,6 +51,19 @@ {{ accordion.footer() }} {% endif %} + {% if brickset_storages | length %} + {{ accordion.header('Storage', 'storage', 'metadata', icon='archive-2-line') }} + +
+ +
+ {{ accordion.footer() }} + {% endif %} {% if brickset_tags | length %} {{ accordion.header('Tags', 'tags', 'metadata', icon='price-tag-2-line') }}
diff --git a/templates/admin.html b/templates/admin.html index 064526d..3582c7b 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -18,6 +18,8 @@ {% include 'admin/owner/delete.html' %} {% elif delete_status %} {% include 'admin/status/delete.html' %} + {% elif delete_storage %} + {% include 'admin/storage/delete.html' %} {% elif delete_tag %} {% include 'admin/tag/delete.html' %} {% elif drop_database %} @@ -36,6 +38,7 @@ {% include 'admin/retired.html' %} {% include 'admin/owner.html' %} {% include 'admin/status.html' %} + {% include 'admin/storage.html' %} {% include 'admin/tag.html' %} {% include 'admin/database.html' %} {% include 'admin/configuration.html' %} diff --git a/templates/admin/storage.html b/templates/admin/storage.html new file mode 100644 index 0000000..1f317e6 --- /dev/null +++ b/templates/admin/storage.html @@ -0,0 +1,42 @@ +{% import 'macro/accordion.html' as accordion %} + +{{ accordion.header('Set storages', 'storage', 'admin', expanded=open_storage, icon='archive-2-line', class='p-0') }} +{% if storage_error %}{% endif %} +
    + {% if metadata_storages | length %} + {% for storage in metadata_storages %} +
  • +
    +
    + +
    +
    Name
    + + +
    +
    +
    + Delete +
    +
    +
  • + {% endfor %} + {% else %} +
  • No storage found.
  • + {% endif %} +
  • +
    +
    + +
    +
    Name
    + +
    +
    +
    + +
    +
    +
  • +
+{{ accordion.footer() }} diff --git a/templates/admin/storage/delete.html b/templates/admin/storage/delete.html new file mode 100644 index 0000000..b3eb990 --- /dev/null +++ b/templates/admin/storage/delete.html @@ -0,0 +1,19 @@ +{% import 'macro/accordion.html' as accordion %} + +{{ accordion.header('Set storages danger zone', 'storage-danger', 'admin', expanded=true, danger=true, class='text-end') }} +
+ {% if storage_error %}{% endif %} + +
+
+
+
Name
+ +
+
+
+
+ Back to the admin + +
+{{ accordion.footer() }} diff --git a/templates/macro/badge.html b/templates/macro/badge.html index a3ea6d7..dfc9384 100644 --- a/templates/macro/badge.html +++ b/templates/macro/badge.html @@ -72,6 +72,18 @@ {{ badge(check=set, url=url, solo=solo, last=last, color='secondary', icon='hashtag', collapsible='Set:', text=set, alt='Set') }} {% endmacro %} +{% macro storage(item, storages, solo=false, last=false) %} + {% if item.fields.storage in storages.mapping %} + {% set storage = storages.mapping[item.fields.storage] %} + {% if last %} + {% set tooltip=storage.fields.name %} + {% else %} + {% set text=storage.fields.name %} + {% endif %} + {{ badge(check=storage, solo=solo, last=last, color='light text-warning-emphasis bg-warning-subtle border border-warning-subtle', icon='archive-2-line', text=text, alt='Storage', tooltip=tooltip) }} + {% endif %} +{% endmacro %} + {% macro tag(item, tag, solo=false, last=false) %} {% if last %} {% set tooltip=tag.fields.name %} diff --git a/templates/macro/form.html b/templates/macro/form.html index b93ac17..d3d3512 100644 --- a/templates/macro/form.html +++ b/templates/macro/form.html @@ -39,3 +39,27 @@
{% endif %} {% endmacro %} + +{% macro select(name, item, field, metadata_list, nullable=true, icon=none, delete=false) %} + {% if g.login.is_authenticated() %} + {% set prefix=metadata_list.as_prefix() %} + +
+ {% if icon %}{% endif %} + + + +
+ {% endif %} +{% endmacro %} diff --git a/templates/set/card.html b/templates/set/card.html index 73a90a5..a327052 100644 --- a/templates/set/card.html +++ b/templates/set/card.html @@ -48,7 +48,8 @@ {{ badge.total_damaged(item.fields.total_damaged, solo=solo, last=last) }} {% for owner in brickset_owners %} {{ badge.owner(item, owner, solo=solo, last=last) }} - {% endfor %} + {% endfor %} + {{ badge.storage(item, brickset_storages, solo=solo, last=last) }} {% if not last %} {% if not solo %} {{ badge.instructions(item, solo=solo, last=last) }}