From 4e3ae491874d9d072f69c1abb78862f8063c9df0 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 23:45:35 +0100 Subject: [PATCH] Set storage details --- .env.sample | 16 +++++++-- CHANGELOG.md | 11 +++++- bricktracker/app.py | 2 ++ bricktracker/config.py | 2 ++ bricktracker/metadata_list.py | 10 ++++-- bricktracker/navbar.py | 1 + bricktracker/set.py | 12 ++++--- bricktracker/set_list.py | 12 +++++++ bricktracker/set_storage.py | 9 +++++ bricktracker/set_storage_list.py | 17 +++++++++ bricktracker/sql/set/list/using_storage.sql | 5 +++ bricktracker/sql/set/metadata/storage/all.sql | 14 ++++++++ .../sql/set/metadata/storage/base.sql | 15 ++++++-- bricktracker/views/storage.py | 36 +++++++++++++++++++ templates/macro/accordion.html | 4 +-- templates/macro/badge.html | 2 +- templates/macro/table.html | 10 +++--- templates/storage.html | 15 ++++++++ templates/storage/card.html | 16 +++++++++ templates/storage/table.html | 16 +++++++++ templates/storages.html | 11 ++++++ 21 files changed, 217 insertions(+), 19 deletions(-) create mode 100644 bricktracker/sql/set/list/using_storage.sql create mode 100644 bricktracker/sql/set/metadata/storage/all.sql create mode 100644 bricktracker/views/storage.py create mode 100644 templates/storage.html create mode 100644 templates/storage/card.html create mode 100644 templates/storage/table.html create mode 100644 templates/storages.html diff --git a/.env.sample b/.env.sample index b93e8c6..d14491e 100644 --- a/.env.sample +++ b/.env.sample @@ -91,6 +91,11 @@ # Default: false # BK_HIDE_ADMIN=true +# Optional: Hide the 'Problems' entry from the menu. Does not disable the route. +# Default: false +# Legacy name: BK_HIDE_MISSING_PARTS +# BK_HIDE_ALL_PROBLEMS_PARTS=true + # Optional: Hide the 'Instructions' entry from the menu. Does not disable the route. # Default: false # BK_HIDE_ALL_INSTRUCTIONS=true @@ -107,10 +112,9 @@ # Default: false # BK_HIDE_ALL_SETS=true -# Optional: Hide the 'Problems' entry from the menu. Does not disable the route. +# Optional: Hide the 'Storages' entry from the menu. Does not disable the route. # Default: false -# Legacy name: BK_HIDE_MISSING_PARTS -# BK_HIDE_ALL_PROBLEMS_PARTS=true +# BK_HIDE_ALL_STORAGES=true # Optional: Hide the 'Instructions' entry in a Set card # Default: false @@ -255,6 +259,12 @@ # Default: /bricksocket/ # BK_SOCKET_PATH=custompath +# Optional: Change the default order of storages. By default ordered by insertion order. +# Useful column names for this option are: +# - "bricktracker_metadata_storages"."name" ASC: storage name +# Default: "bricktracker_metadata_storages"."name" ASC +# BK_MINIFIGURES_DEFAULT_ORDER="bricktracker_metadata_storages"."name" ASC + # Optional: URL to the themes.csv.gz on Rebrickable # Default: https://cdn.rebrickable.com/media/downloads/themes.csv.gz # BK_THEMES_FILE_URL= diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ed15af..b50f342 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ - Added: `BK_HIDE_TABLE_DAMAGED_PARTS`, hide the Damaged column in all tables - Added: `BK_SHOW_GRID_SORT`, show the sort options on the grid by default - Added: `BK_SHOW_GRID_FILTERS`, show the filter options on the grid by default +- Added: `BK_HIDE_ALL_STORAGES`, hide the "Storages" menu entry +- Added: `BK_MINIFIGURES_DEFAULT_ORDER`, ordering of storages ### Code @@ -28,7 +30,7 @@ - Deduplicate - Compute number of parts -Parts +- Parts - Damaged parts - Sets @@ -38,6 +40,9 @@ Parts - Tags - Storage +- Storage + - Storage content and list + - Socket - Add decorator for rebrickable, authenticated and threaded socket actions @@ -86,6 +91,10 @@ Parts - Manually collapsible filters (with configuration variable for default state) - Manually collapsible sort (with configuration variable for default state) +- Storage + - Storage list + - Storage content + ## 1.1.1: PDF Instructions Download ### Instructions diff --git a/bricktracker/app.py b/bricktracker/app.py index 4b6f0d4..af005d9 100644 --- a/bricktracker/app.py +++ b/bricktracker/app.py @@ -29,6 +29,7 @@ from bricktracker.views.login import login_page from bricktracker.views.minifigure import minifigure_page from bricktracker.views.part import part_page from bricktracker.views.set import set_page +from bricktracker.views.storage import storage_page from bricktracker.views.wish import wish_page @@ -77,6 +78,7 @@ def setup_app(app: Flask) -> None: app.register_blueprint(minifigure_page) app.register_blueprint(part_page) app.register_blueprint(set_page) + app.register_blueprint(storage_page) app.register_blueprint(wish_page) # Register admin routes diff --git a/bricktracker/config.py b/bricktracker/config.py index cd7ef74..5b9788f 100644 --- a/bricktracker/config.py +++ b/bricktracker/config.py @@ -29,6 +29,7 @@ CONFIG: Final[list[dict[str, Any]]] = [ {'n': 'HIDE_ALL_MINIFIGURES', 'c': bool}, {'n': 'HIDE_ALL_PARTS', 'c': bool}, {'n': 'HIDE_ALL_SETS', 'c': bool}, + {'n': 'HIDE_ALL_STORAGES', 'c': bool}, {'n': 'HIDE_ALL_PROBLEMS_PARTS', 'e': 'BK_HIDE_MISSING_PARTS', 'c': bool}, {'n': 'HIDE_SET_INSTRUCTIONS', 'c': bool}, {'n': 'HIDE_TABLE_DAMAGED_PARTS', 'c': bool}, @@ -59,6 +60,7 @@ CONFIG: Final[list[dict[str, Any]]] = [ {'n': 'SKIP_SPARE_PARTS', 'c': bool}, {'n': 'SOCKET_NAMESPACE', 'd': 'bricksocket'}, {'n': 'SOCKET_PATH', 'd': '/bricksocket/'}, + {'n': 'STORAGE_DEFAULT_ORDER', 'd': '"bricktracker_metadata_storages"."name" ASC'}, # noqa: E501 {'n': 'THEMES_FILE_URL', 'd': 'https://cdn.rebrickable.com/media/downloads/themes.csv.gz'}, # noqa: E501 {'n': 'THEMES_PATH', 'd': './themes.csv'}, {'n': 'TIMEZONE', 'd': 'Etc/UTC'}, diff --git a/bricktracker/metadata_list.py b/bricktracker/metadata_list.py index 68238e9..60bfb5f 100644 --- a/bricktracker/metadata_list.py +++ b/bricktracker/metadata_list.py @@ -43,8 +43,7 @@ class BrickMetadataList(BrickRecordList[T]): # Records override (masking the class variables with instance ones) if records is not None: - self.records = [] - self.mapping = {} + self.override() for metadata in records: self.records.append(metadata) @@ -79,6 +78,13 @@ class BrickMetadataList(BrickRecordList[T]): def filter(self) -> list[T]: return self.records + # Add a layer of override data + def override(self) -> None: + self.fields = BrickRecordFields() + + self.records = [] + self.mapping = {} + # Return the items as columns for a select @classmethod def as_columns(cls, /, **kwargs) -> str: diff --git a/bricktracker/navbar.py b/bricktracker/navbar.py index 30007de..20a2b29 100644 --- a/bricktracker/navbar.py +++ b/bricktracker/navbar.py @@ -14,6 +14,7 @@ NAVBAR: Final[list[dict[str, Any]]] = [ {'e': 'part.problem', 't': 'Problems', 'i': 'error-warning-line', 'f': 'HIDE_ALL_PROBLEMS_PARTS'}, # noqa: E501 {'e': 'minifigure.list', 't': 'Minifigures', 'i': 'group-line', 'f': 'HIDE_ALL_MINIFIGURES'}, # noqa: E501 {'e': 'instructions.list', 't': 'Instructions', 'i': 'file-line', 'f': 'HIDE_ALL_INSTRUCTIONS'}, # noqa: E501 + {'e': 'storage.list', 't': 'Storages', 'i': 'archive-2-line', 'f': 'HIDE_ALL_STORAGES'}, # noqa: E501 {'e': 'wish.list', 't': 'Wishlist', 'i': 'gift-line', 'f': 'HIDE_WISHES'}, {'e': 'admin.admin', 't': 'Admin', 'i': 'settings-4-line', 'f': 'HIDE_ADMIN'}, # noqa: E501 ] diff --git a/bricktracker/set.py b/bricktracker/set.py index 90b2679..6368d40 100644 --- a/bricktracker/set.py +++ b/bricktracker/set.py @@ -214,7 +214,11 @@ class BrickSet(RebrickableSet): # Compute the url for the refresh button def url_for_refresh(self, /) -> str: - return url_for( - 'set.refresh', - id=self.fields.id, - ) + return url_for('set.refresh', id=self.fields.id) + + # Compute the url for the set storage + def url_for_storage(self, /) -> str: + if self.fields.storage is not None: + return url_for('storage.details', id=self.fields.storage) + else: + return '' diff --git a/bricktracker/set_list.py b/bricktracker/set_list.py index b12f971..6c3b928 100644 --- a/bricktracker/set_list.py +++ b/bricktracker/set_list.py @@ -5,6 +5,7 @@ from flask import current_app from .record_list import BrickRecordList from .set_owner_list import BrickSetOwnerList from .set_status_list import BrickSetStatusList +from .set_storage import BrickSetStorage from .set_tag_list import BrickSetTagList from .set import BrickSet @@ -24,6 +25,7 @@ class BrickSetList(BrickRecordList[BrickSet]): select_query: str = 'set/list/all' using_minifigure_query: str = 'set/list/using_minifigure' using_part_query: str = 'set/list/using_part' + using_storage_query: str = 'set/list/using_storage' def __init__(self, /): super().__init__() @@ -151,3 +153,13 @@ class BrickSetList(BrickRecordList[BrickSet]): self.list(override_query=self.using_part_query) return self + + # Sets using a storage + def using_storage(self, storage: BrickSetStorage, /) -> Self: + # Save the parameters to the fields + self.fields.storage = storage.fields.id + + # Load the sets from the database + self.list(override_query=self.using_storage_query) + + return self diff --git a/bricktracker/set_storage.py b/bricktracker/set_storage.py index 0a54262..30c559c 100644 --- a/bricktracker/set_storage.py +++ b/bricktracker/set_storage.py @@ -1,5 +1,7 @@ from .metadata import BrickMetadata +from flask import url_for + # Lego set storage metadata class BrickSetStorage(BrickMetadata): @@ -11,3 +13,10 @@ class BrickSetStorage(BrickMetadata): 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' + + # Self url + def url(self, /) -> str: + return url_for( + 'storage.details', + id=self.fields.id, + ) diff --git a/bricktracker/set_storage_list.py b/bricktracker/set_storage_list.py index 72efde7..8453f36 100644 --- a/bricktracker/set_storage_list.py +++ b/bricktracker/set_storage_list.py @@ -1,6 +1,8 @@ import logging from typing import Self +from flask import current_app + from .metadata_list import BrickMetadataList from .set_storage import BrickSetStorage @@ -13,10 +15,25 @@ class BrickSetStorageList(BrickMetadataList[BrickSetStorage]): # Queries select_query = 'set/metadata/storage/list' + all_query = 'set/metadata/storage/all' # Set state endpoint set_state_endpoint: str = 'set.update_storage' + # Load all storages + @classmethod + def all(cls, /) -> Self: + new = cls.new() + new.override() + + for record in new.select( + override_query=cls.all_query, + order=current_app.config['STORAGE_DEFAULT_ORDER'] + ): + new.records.append(new.model(record=record)) + + return new + # Instantiate the list with the proper class @classmethod def new(cls, /, *, force: bool = False) -> Self: diff --git a/bricktracker/sql/set/list/using_storage.sql b/bricktracker/sql/set/list/using_storage.sql new file mode 100644 index 0000000..0dc0f14 --- /dev/null +++ b/bricktracker/sql/set/list/using_storage.sql @@ -0,0 +1,5 @@ +{% extends 'set/base/full.sql' %} + +{% block where %} +WHERE "bricktracker_sets"."storage" IS NOT DISTINCT FROM :storage +{% endblock %} diff --git a/bricktracker/sql/set/metadata/storage/all.sql b/bricktracker/sql/set/metadata/storage/all.sql new file mode 100644 index 0000000..0bd8b8e --- /dev/null +++ b/bricktracker/sql/set/metadata/storage/all.sql @@ -0,0 +1,14 @@ +{% extends 'set/metadata/storage/base.sql' %} + +{% block total_sets %} +IFNULL(COUNT("bricktracker_sets"."id"), 0) AS "total_sets" +{% endblock %} + +{% block join %} +LEFT JOIN "bricktracker_sets" +ON "bricktracker_metadata_storages"."id" IS NOT DISTINCT FROM "bricktracker_sets"."storage" +{% endblock %} + +{% block group %} +GROUP BY "bricktracker_metadata_storages"."id" +{% endblock %} \ No newline at end of file diff --git a/bricktracker/sql/set/metadata/storage/base.sql b/bricktracker/sql/set/metadata/storage/base.sql index 2417aa6..bf616ed 100644 --- a/bricktracker/sql/set/metadata/storage/base.sql +++ b/bricktracker/sql/set/metadata/storage/base.sql @@ -1,6 +1,17 @@ SELECT "bricktracker_metadata_storages"."id", - "bricktracker_metadata_storages"."name" + "bricktracker_metadata_storages"."name", + {% block total_sets %} + NULL as "total_sets" -- dummy for order: total_sets + {% endblock %} FROM "bricktracker_metadata_storages" -{% block where %}{% endblock %} \ No newline at end of file +{% block join %}{% endblock %} + +{% block where %}{% endblock %} + +{% block group %}{% endblock %} + +{% if order %} +ORDER BY {{ order }} +{% endif %} diff --git a/bricktracker/views/storage.py b/bricktracker/views/storage.py new file mode 100644 index 0000000..e41e97a --- /dev/null +++ b/bricktracker/views/storage.py @@ -0,0 +1,36 @@ +from flask import Blueprint, render_template + +from .exceptions import exception_handler +from ..set_owner_list import BrickSetOwnerList +from ..set_list import BrickSetList +from ..set_storage import BrickSetStorage +from ..set_storage_list import BrickSetStorageList +from ..set_tag_list import BrickSetTagList + +storage_page = Blueprint('storage', __name__, url_prefix='/storages') + + +# Index +@storage_page.route('/', methods=['GET']) +@exception_handler(__file__) +def list() -> str: + return render_template( + 'storages.html', + table_collection=BrickSetStorageList.all(), + ) + + +# Storage details +@storage_page.route('//details') +@exception_handler(__file__) +def details(*, id: str) -> str: + storage = BrickSetStorage().select_specific(id) + + return render_template( + 'storage.html', + item=storage, + sets=BrickSetList().using_storage(storage), + brickset_owners=BrickSetOwnerList.list(), + brickset_storages=BrickSetStorageList.list(as_class=True), + brickset_tags=BrickSetTagList.list(), + ) diff --git a/templates/macro/accordion.html b/templates/macro/accordion.html index 1fd91f3..c5c8954 100644 --- a/templates/macro/accordion.html +++ b/templates/macro/accordion.html @@ -26,10 +26,10 @@ {% endmacro %} -{% macro cards(card_collection, title, id, parent, target, icon=none) %} +{% macro cards(card_collection, title, id, parent, target, expanded=false, icon=none) %} {% set size=card_collection | length %} {% if size %} - {{ header(title, id, parent, icon=icon) }} + {{ header(title, id, parent, expanded=expanded, icon=icon) }}
{% for item in card_collection %}
diff --git a/templates/macro/badge.html b/templates/macro/badge.html index bd683f4..e1f9071 100644 --- a/templates/macro/badge.html +++ b/templates/macro/badge.html @@ -80,7 +80,7 @@ {% 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) }} + {{ badge(url=item.url_for_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 %} diff --git a/templates/macro/table.html b/templates/macro/table.html index ebf1ded..0638380 100644 --- a/templates/macro/table.html +++ b/templates/macro/table.html @@ -1,7 +1,9 @@ -{% macro header(color=false, parts=false, quantity=false, missing_parts=false, damaged_parts=false, sets=false, minifigures=false) %} +{% macro header(image=true, color=false, parts=false, quantity=false, missing=true, missing_parts=false, damaged=true, damaged_parts=false, sets=false, minifigures=false) %} - Image + {% if image %} + Image + {% endif %} Name {% if color %} Color @@ -12,10 +14,10 @@ {% if quantity %} Quantity {% endif %} - {% if not config['HIDE_TABLE_MISSING_PARTS'] %} + {% if missing and not config['HIDE_TABLE_MISSING_PARTS'] %} Missing{% if missing_parts %} parts{% endif %} {% endif %} - {% if not config['HIDE_TABLE_DAMAGED_PARTS'] %} + {% if damaged and not config['HIDE_TABLE_DAMAGED_PARTS'] %} Damaged{% if damaged_parts %} parts{% endif %} {% endif %} {% if sets %} diff --git a/templates/storage.html b/templates/storage.html new file mode 100644 index 0000000..76e9549 --- /dev/null +++ b/templates/storage.html @@ -0,0 +1,15 @@ +{% extends 'base.html' %} + +{% block title %} - Storage {{ item.fields.name}}{% endblock %} + +{% block main %} +
+
+
+ {% with solo=true %} + {% include 'storage/card.html' %} + {% endwith %} +
+
+
+{% endblock %} diff --git a/templates/storage/card.html b/templates/storage/card.html new file mode 100644 index 0000000..cf29de3 --- /dev/null +++ b/templates/storage/card.html @@ -0,0 +1,16 @@ +{% import 'macro/accordion.html' as accordion with context %} +{% import 'macro/badge.html' as badge %} +{% import 'macro/card.html' as card %} + +
+ {{ card.header(item, item.fields.name, solo=solo, icon='archive-2-line') }} +
+ {{ badge.total_sets(sets | length, solo=solo, last=last) }} +
+ {% if solo %} +
+ {{ accordion.cards(sets, 'Sets', 'sets-stored', 'storage-details', 'set/card.html', expanded=true, icon='hashtag') }} +
+ + {% endif %} +
diff --git a/templates/storage/table.html b/templates/storage/table.html new file mode 100644 index 0000000..680c75c --- /dev/null +++ b/templates/storage/table.html @@ -0,0 +1,16 @@ +{% import 'macro/form.html' as form %} +{% import 'macro/table.html' as table %} + +
+ + {{ table.header(image=false, missing=false, damaged=false, sets=true) }} + + {% for item in table_collection %} + + + + + {% endfor %} + +
{{ item.fields.name }}{{ item.fields.total_sets }}
+
diff --git a/templates/storages.html b/templates/storages.html new file mode 100644 index 0000000..14ac354 --- /dev/null +++ b/templates/storages.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} + +{% block title %} - All storages{% endblock %} + +{% block main %} +
+ {% with all=true %} + {% include 'storage/table.html' %} + {% endwith %} +
+{% endblock %}