From a99669d9dcec61684c1df6b0d704f94c97ed523b Mon Sep 17 00:00:00 2001 From: Gregoo Date: Tue, 4 Feb 2025 23:05:36 +0100 Subject: [PATCH] List of sets to be refreshed --- CHANGELOG.md | 2 + bricktracker/app.py | 2 + bricktracker/rebrickable_set.py | 23 +++++++- bricktracker/rebrickable_set_list.py | 13 +++++ .../sql/rebrickable/set/need_refresh.sql | 53 +++++++++++++++++++ bricktracker/sql_counter.py | 7 +-- bricktracker/views/admin/set.py | 20 +++++++ bricktracker/views/set.py | 15 +++++- templates/admin.html | 3 ++ templates/admin/refresh.html | 5 ++ templates/admin/set/refresh.html | 34 ++++++++++++ templates/refresh.html | 6 ++- 12 files changed, 175 insertions(+), 8 deletions(-) create mode 100644 bricktracker/sql/rebrickable/set/need_refresh.sql create mode 100644 bricktracker/views/admin/set.py create mode 100644 templates/admin/refresh.html create mode 100644 templates/admin/set/refresh.html diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a6ba43..baad4bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,7 @@ - Admin - Grey out legacy tables in the database view - Checkboxes renamed to Set statuses + - List of sets that may need to be refreshed - Cards - Use macros for badge in the card header @@ -102,6 +103,7 @@ - Collapsible controls depending on screen size - Manually collapsible filters (with configuration variable for default state) - Manually collapsible sort (with configuration variable for default state) + - Clear search bar - Storage - Storage list diff --git a/bricktracker/app.py b/bricktracker/app.py index f0afe42..b4aad9e 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.purchase_location import admin_purchase_location_page # noqa: E501 from bricktracker.views.admin.retired import admin_retired_page +from bricktracker.views.admin.set import admin_set_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 @@ -90,6 +91,7 @@ def setup_app(app: Flask) -> None: app.register_blueprint(admin_retired_page) app.register_blueprint(admin_owner_page) app.register_blueprint(admin_purchase_location_page) + app.register_blueprint(admin_set_page) app.register_blueprint(admin_status_page) app.register_blueprint(admin_storage_page) app.register_blueprint(admin_tag_page) diff --git a/bricktracker/rebrickable_set.py b/bricktracker/rebrickable_set.py index fbf10f1..6beffc2 100644 --- a/bricktracker/rebrickable_set.py +++ b/bricktracker/rebrickable_set.py @@ -1,9 +1,9 @@ import logging from sqlite3 import Row import traceback -from typing import Any, TYPE_CHECKING +from typing import Any, Self, TYPE_CHECKING -from flask import current_app +from flask import current_app, url_for from .exceptions import ErrorException, NotFoundException from .instructions import BrickInstructions @@ -138,6 +138,21 @@ class RebrickableSet(BrickRecord): return False + # Select a specific set (with a set) + def select_specific(self, set: str, /) -> Self: + # Save the parameters to the fields + self.fields.set = set + + # Load from database + if not self.select(): + raise NotFoundException( + 'Set with set {set} was not found in the database'.format( + set=self.fields.set, + ), + ) + + return self + # Return a short form of the Rebrickable set def short(self, /, *, from_download: bool = False) -> dict[str, Any]: return { @@ -164,6 +179,10 @@ class RebrickableSet(BrickRecord): return '' + # Compute the url for the refresh button + def url_for_refresh(self, /) -> str: + return url_for('set.refresh', set=self.fields.set) + # Normalize from Rebrickable @staticmethod def from_rebrickable(data: dict[str, Any], /, **_) -> dict[str, Any]: diff --git a/bricktracker/rebrickable_set_list.py b/bricktracker/rebrickable_set_list.py index 8fe7ee9..0db84b7 100644 --- a/bricktracker/rebrickable_set_list.py +++ b/bricktracker/rebrickable_set_list.py @@ -9,6 +9,7 @@ class RebrickableSetList(BrickRecordList[RebrickableSet]): # Queries select_query: str = 'rebrickable/set/list' + refresh_query: str = 'rebrickable/set/need_refresh' # All the sets def all(self, /) -> Self: @@ -19,3 +20,15 @@ class RebrickableSetList(BrickRecordList[RebrickableSet]): self.records.append(rebrickable_set) return self + + # Sets needing refresh + def need_refresh(self, /) -> Self: + # Load the sets from the database + for record in self.select( + override_query=self.refresh_query + ): + rebrickable_set = RebrickableSet(record=record) + + self.records.append(rebrickable_set) + + return self diff --git a/bricktracker/sql/rebrickable/set/need_refresh.sql b/bricktracker/sql/rebrickable/set/need_refresh.sql new file mode 100644 index 0000000..8060a60 --- /dev/null +++ b/bricktracker/sql/rebrickable/set/need_refresh.sql @@ -0,0 +1,53 @@ +SELECT + "rebrickable_sets"."set", + "rebrickable_sets"."name", + "rebrickable_sets"."number_of_parts", + "rebrickable_sets"."image", + "rebrickable_sets"."url", + "null_join"."null_rgb", + "null_join"."null_transparent", + "null_join"."null_url" +FROM "rebrickable_sets" + +INNER JOIN ( + SELECT + "null_sums"."set", + "null_sums"."null_rgb", + "null_sums"."null_transparent", + "null_sums"."null_url" + FROM ( + SELECT + "unique_set_parts"."set", + SUM(CASE WHEN "unique_set_parts"."color_rgb" IS NULL THEN 1 ELSE 0 END) AS "null_rgb", + SUM(CASE WHEN "unique_set_parts"."color_transparent" IS NULL THEN 1 ELSE 0 END) AS "null_transparent", + SUM(CASE WHEN "unique_set_parts"."url" IS NULL THEN 1 ELSE 0 END) AS "null_url" + FROM ( + SELECT + "bricktracker_sets"."set", + "rebrickable_parts"."color_rgb", + "rebrickable_parts"."color_transparent", + "rebrickable_parts"."url" + FROM "bricktracker_sets" + + INNER JOIN "bricktracker_parts" + ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_parts"."id" + + LEFT JOIN "rebrickable_parts" + ON "bricktracker_parts"."part" IS NOT DISTINCT FROM "rebrickable_parts"."part" + AND "bricktracker_parts"."color" IS NOT DISTINCT FROM "rebrickable_parts"."color_id" + + GROUP BY + "bricktracker_sets"."set", + "bricktracker_parts"."part", + "bricktracker_parts"."color" + ) "unique_set_parts" + + GROUP BY "unique_set_parts"."set" + + ) "null_sums" + + WHERE "null_rgb" > 0 + OR "null_transparent" > 0 + OR "null_url" > 0 +) "null_join" +ON "rebrickable_sets"."set" IS NOT DISTINCT FROM "null_join"."set" diff --git a/bricktracker/sql_counter.py b/bricktracker/sql_counter.py index e5b9262..4d7a61e 100644 --- a/bricktracker/sql_counter.py +++ b/bricktracker/sql_counter.py @@ -10,11 +10,12 @@ 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_owners': ('Bricktracker set owners', 'checkbox-line'), # noqa: E501 - 'bricktracker_set_statuses': ('Bricktracker set statuses', 'user-line'), # noqa: E501 - 'bricktracker_set_tags': ('Bricktracker set tags', 'price-tag-2-line'), # noqa: E501 + 'bricktracker_set_owners': ('Bricktracker set owners', 'checkbox-line'), + 'bricktracker_set_statuses': ('Bricktracker set statuses', 'user-line'), + 'bricktracker_set_tags': ('Bricktracker set tags', 'price-tag-2-line'), 'bricktracker_sets': ('Bricktracker sets', 'hashtag'), 'bricktracker_wishes': ('Bricktracker wishes', 'gift-line'), + 'bricktracker_wish_owners': ('Bricktracker wish owners', 'checkbox-line'), 'inventory': ('Parts', 'shapes-line'), 'inventory_old': ('Parts (legacy)', 'shapes-line'), 'minifigures': ('Minifigures', 'group-line'), diff --git a/bricktracker/views/admin/set.py b/bricktracker/views/admin/set.py new file mode 100644 index 0000000..6f00910 --- /dev/null +++ b/bricktracker/views/admin/set.py @@ -0,0 +1,20 @@ +from flask import Blueprint, render_template, request +from flask_login import login_required + +from ..exceptions import exception_handler +from ...rebrickable_set_list import RebrickableSetList + +admin_set_page = Blueprint('admin_set', __name__, url_prefix='/admin/set') + + +# Sets that need o be refreshed +@admin_set_page.route('/refresh', methods=['GET']) +@login_required +@exception_handler(__file__) +def refresh() -> str: + return render_template( + 'admin.html', + refresh_set=True, + table_collection=RebrickableSetList().need_refresh(), + set_error=request.args.get('set_error') + ) diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py index bb9e845..1ffec55 100644 --- a/bricktracker/views/set.py +++ b/bricktracker/views/set.py @@ -13,8 +13,10 @@ from flask_login import login_required from werkzeug.wrappers.response import Response from .exceptions import exception_handler +from ..exceptions import ErrorException from ..minifigure import BrickMinifigure from ..part import BrickPart +from ..rebrickable_set import RebrickableSet from ..set import BrickSet from ..set_list import BrickSetList, set_metadata_lists from ..set_owner_list import BrickSetOwnerList @@ -241,13 +243,22 @@ def problem_part( # Refresh a set +@set_page.route('/refresh//', methods=['GET']) @set_page.route('//refresh', methods=['GET']) @login_required @exception_handler(__file__) -def refresh(*, id: str) -> str: +def refresh(*, id: str | None = None, set: str | None = None) -> str: + if id is not None: + item = BrickSet().select_specific(id) + elif set is not None: + item = RebrickableSet().select_specific(set) + else: + raise ErrorException('Could not load any set to refresh') + return render_template( 'refresh.html', - item=BrickSet().select_specific(id), + id=id, + item=item, path=current_app.config['SOCKET_PATH'], namespace=current_app.config['SOCKET_NAMESPACE'], messages=MESSAGES diff --git a/templates/admin.html b/templates/admin.html index 87dbdb4..2da5000 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -32,6 +32,8 @@ {% include 'admin/database/import.html' %} {% elif upgrade_database %} {% include 'admin/database/upgrade.html' %} + {% elif refresh_set %} + {% include 'admin/set/refresh.html' %} {% else %} {% include 'admin/logout.html' %} {% include 'admin/instructions.html' %} @@ -47,6 +49,7 @@ {% include 'admin/storage.html' %} {% include 'admin/tag.html' %} {{ accordion.footer() }} + {% include 'admin/refresh.html' %} {% include 'admin/database.html' %} {% include 'admin/configuration.html' %} {% endif %} diff --git a/templates/admin/refresh.html b/templates/admin/refresh.html new file mode 100644 index 0000000..ab81cc8 --- /dev/null +++ b/templates/admin/refresh.html @@ -0,0 +1,5 @@ +{% import 'macro/accordion.html' as accordion %} + +{{ accordion.header('Set refresh', 'refresh', 'admin', icon='refresh-line') }} + Check for sets that may need a refresh +{{ accordion.footer() }} diff --git a/templates/admin/set/refresh.html b/templates/admin/set/refresh.html new file mode 100644 index 0000000..0f4befc --- /dev/null +++ b/templates/admin/set/refresh.html @@ -0,0 +1,34 @@ +{% import 'macro/table.html' as table %} +{% import 'macro/badge.html' as badge %} + + +
+ + + + + + + + + + + + + + + {% for item in table_collection %} + + {{ table.image(item.url_for_image(), caption=item.fields.name, alt=item.fields.set) }} + + + + + + + + + {% endfor %} + +
Image Set Name Parts Empty RGB Empty transparent Empty URL Actions
{{ item.fields.set }} {{ table.rebrickable(item) }}{{ item.fields.name }}{{ item.fields.number_of_parts }}{{ item.fields.null_rgb }}{{ item.fields.null_transparent }}{{ item.fields.null_url }} Refresh
+
\ No newline at end of file diff --git a/templates/refresh.html b/templates/refresh.html index 5add93d..3a9804d 100644 --- a/templates/refresh.html +++ b/templates/refresh.html @@ -51,7 +51,11 @@