From 4350ade65b66e80ccdeb72238eea582763f54f45 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Sun, 26 Jan 2025 09:59:53 +0100 Subject: [PATCH 001/154] Add a flag to hide instructions in a set card --- .env.sample | 4 ++++ bricktracker/config.py | 1 + bricktracker/set.py | 7 +++++-- templates/set/card.html | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.env.sample b/.env.sample index 7cbc5c0..06584db 100644 --- a/.env.sample +++ b/.env.sample @@ -111,6 +111,10 @@ # Default: false # BK_HIDE_MISSING_PARTS=true +# Optional: Hide the 'Instructions' entry in a Set card +# Default: false +# BK_HIDE_SET_INSTRUCTIONS=true + # Optional: Hide the 'Wishlist' entry from the menu. Does not disable the route. # Default: false # BK_HIDE_WISHES=true diff --git a/bricktracker/config.py b/bricktracker/config.py index 08db61b..f9d8f2b 100644 --- a/bricktracker/config.py +++ b/bricktracker/config.py @@ -30,6 +30,7 @@ CONFIG: Final[list[dict[str, Any]]] = [ {'n': 'HIDE_ALL_PARTS', 'c': bool}, {'n': 'HIDE_ALL_SETS', 'c': bool}, {'n': 'HIDE_MISSING_PARTS', 'c': bool}, + {'n': 'HIDE_SET_INSTRUCTIONS', 'c': bool}, {'n': 'HIDE_WISHES', 'c': bool}, {'n': 'MINIFIGURES_DEFAULT_ORDER', 'd': '"minifigures"."name" ASC'}, {'n': 'MINIFIGURES_FOLDER', 'd': 'minifigs', 's': True}, diff --git a/bricktracker/set.py b/bricktracker/set.py index aa536b8..f16ea6f 100644 --- a/bricktracker/set.py +++ b/bricktracker/set.py @@ -3,7 +3,7 @@ import traceback from typing import Any, Self from uuid import uuid4 -from flask import url_for +from flask import current_app, url_for from .exceptions import DatabaseException, NotFoundException from .minifigure_list import BrickMinifigureList @@ -179,7 +179,10 @@ class BrickSet(RebrickableSet): # Compute the url for the set instructions def url_for_instructions(self, /) -> str: - if len(self.instructions): + if ( + not current_app.config['HIDE_SET_INSTRUCTIONS'] and + len(self.instructions) + ): return url_for( 'set.details', id=self.fields.id, diff --git a/templates/set/card.html b/templates/set/card.html index 9fcea3d..1308c71 100644 --- a/templates/set/card.html +++ b/templates/set/card.html @@ -35,7 +35,7 @@ {% endfor %} {% endif %} - {% if solo %} + {% if solo and not config['HIDE_SET_INSTRUCTIONS'] %}
{% if not delete %} {{ accordion.header('Instructions', 'instructions', 'set-details', expanded=open_instructions, quantity=item.instructions | length, icon='file-line', class='p-0') }} From 0f53674d8a365bc352b4ad0a47bc1b8614d849d4 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Sun, 26 Jan 2025 10:29:33 +0100 Subject: [PATCH 002/154] Grey out legacy database tables in the admin --- bricktracker/sql_counter.py | 3 +++ templates/admin/database.html | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/bricktracker/sql_counter.py b/bricktracker/sql_counter.py index d104269..7175494 100644 --- a/bricktracker/sql_counter.py +++ b/bricktracker/sql_counter.py @@ -22,6 +22,7 @@ class BrickCounter(object): table: str icon: str count: int + legacy: bool def __init__( self, @@ -44,3 +45,5 @@ class BrickCounter(object): self.name = name self.icon = icon + + self.legacy = '(legacy)' in self.name diff --git a/templates/admin/database.html b/templates/admin/database.html index 36d2b0d..9704365 100644 --- a/templates/admin/database.html +++ b/templates/admin/database.html @@ -22,8 +22,8 @@
    {% for counter in database_counters %} -
  • - {{ counter.name }} {{ counter.count }} +
  • + {{ counter.name }} {{ counter.count }}
  • {% if not (loop.index % 5) %}
From 25aec890a0368196e939b42ffbc742b02d7b2f10 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 27 Jan 2025 10:04:24 +0100 Subject: [PATCH 003/154] Rename download_rebrickable to insert_rebrickable and make it return if an insertion occured --- bricktracker/rebrickable_set.py | 10 +++++++--- bricktracker/set.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/bricktracker/rebrickable_set.py b/bricktracker/rebrickable_set.py index 5a1c41f..23072d3 100644 --- a/bricktracker/rebrickable_set.py +++ b/bricktracker/rebrickable_set.py @@ -52,8 +52,8 @@ class RebrickableSet(BrickRecord): if record is not None: self.ingest(record) - # Import the set from Rebrickable - def download_rebrickable(self, /) -> None: + # Insert the set from Rebrickable + def insert_rebrickable(self, /) -> bool: # Insert the Rebrickable set to the database rows, _ = self.insert( commit=False, @@ -61,10 +61,14 @@ class RebrickableSet(BrickRecord): override_query=RebrickableSet.insert_query ) - if rows > 0: + inserted = rows > 0 + + if inserted: if not current_app.config['USE_REMOTE_IMAGES']: RebrickableImage(self).download() + return inserted + # Ingest a set def ingest(self, record: Row | dict[str, Any], /): super().ingest(record) diff --git a/bricktracker/set.py b/bricktracker/set.py index f16ea6f..cdd5678 100644 --- a/bricktracker/set.py +++ b/bricktracker/set.py @@ -54,7 +54,7 @@ class BrickSet(RebrickableSet): self.insert(commit=False) # Execute the parent download method - self.download_rebrickable() + self.insert_rebrickable() # Load the inventory RebrickableParts(self.socket, self).download() From ee78457e82a986850743b9903bcee0d33c24c9ca Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 27 Jan 2025 10:04:37 +0100 Subject: [PATCH 004/154] Remove unused insert_rebrickable --- bricktracker/set.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/bricktracker/set.py b/bricktracker/set.py index cdd5678..3e348e9 100644 --- a/bricktracker/set.py +++ b/bricktracker/set.py @@ -97,10 +97,6 @@ class BrickSet(RebrickableSet): logger.debug(traceback.format_exc()) - # Insert a Rebrickable set - def insert_rebrickable(self, /) -> None: - self.insert() - # Minifigures def minifigures(self, /) -> BrickMinifigureList: return BrickMinifigureList().load(self) From 1afb6f841cf90d01ff70ff8f923f59b7b9a2f37f Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 27 Jan 2025 11:21:15 +0100 Subject: [PATCH 005/154] Rename routes --- bricktracker/views/minifigure.py | 10 ++++----- bricktracker/views/set.py | 35 ++++++++++++++------------------ 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/bricktracker/views/minifigure.py b/bricktracker/views/minifigure.py index 29c5822..60647fa 100644 --- a/bricktracker/views/minifigure.py +++ b/bricktracker/views/minifigure.py @@ -19,12 +19,12 @@ def list() -> str: # Minifigure details -@minifigure_page.route('//details') +@minifigure_page.route('/
/details') @exception_handler(__file__) -def details(*, number: str) -> str: +def details(*, figure: str) -> str: return render_template( 'minifigure.html', - item=BrickMinifigure().select_generic(number), - using=BrickSetList().using_minifigure(number), - missing=BrickSetList().missing_minifigure(number), + item=BrickMinifigure().select_generic(figure), + using=BrickSetList().using_minifigure(figure), + missing=BrickSetList().missing_minifigure(figure), ) diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py index b36f7e1..02353c1 100644 --- a/bricktracker/views/set.py +++ b/bricktracker/views/set.py @@ -108,33 +108,28 @@ def details(*, id: str) -> str: # Update the missing pieces of a minifig part -@set_page.route('//minifigures//parts//missing', methods=['POST']) # noqa: E501 +@set_page.route('//minifigures/
/parts//missing', methods=['POST']) # noqa: E501 @login_required @exception_handler(__file__, json=True) -def missing_minifigure_part( - *, - id: str, - minifigure_id: str, - part_id: str -) -> Response: +def missing_minifigure_part(*, id: str, figure: str, part: str) -> Response: brickset = BrickSet().select_specific(id) - minifigure = BrickMinifigure().select_specific(brickset, minifigure_id) - part = BrickPart().select_specific( + brickminifigure = BrickMinifigure().select_specific(brickset, figure) + brickpart = BrickPart().select_specific( brickset, - part_id, - minifigure=minifigure, + part, + minifigure=brickminifigure, ) missing = request.json.get('missing', '') # type: ignore - part.update_missing(missing) + brickpart.update_missing(missing) # Info - logger.info('Set {number} ({id}): updated minifigure ({minifigure}) part ({part}) missing count to {missing}'.format( # noqa: E501 + logger.info('Set {number} ({id}): updated minifigure ({figure}) part ({part}) missing count to {missing}'.format( # noqa: E501 number=brickset.fields.set, id=brickset.fields.id, - minifigure=minifigure.fields.fig_num, - part=part.fields.id, + figure=brickminifigure.fields.fig_num, + part=brickpart.fields.id, missing=missing, )) @@ -142,22 +137,22 @@ def missing_minifigure_part( # Update the missing pieces of a part -@set_page.route('//parts//missing', methods=['POST']) +@set_page.route('//parts//missing', methods=['POST']) @login_required @exception_handler(__file__, json=True) -def missing_part(*, id: str, part_id: str) -> Response: +def missing_part(*, id: str, part: str) -> Response: brickset = BrickSet().select_specific(id) - part = BrickPart().select_specific(brickset, part_id) + brickpart = BrickPart().select_specific(brickset, part) missing = request.json.get('missing', '') # type: ignore - part.update_missing(missing) + brickpart.update_missing(missing) # Info logger.info('Set {number} ({id}): updated part ({part}) missing count to {missing}'.format( # noqa: E501 number=brickset.fields.set, id=brickset.fields.id, - part=part.fields.id, + part=brickpart.fields.id, missing=missing, )) From bdf635e42785d0eb30838343fe3cce36c8eb1295 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 27 Jan 2025 12:04:20 +0100 Subject: [PATCH 006/154] Remove confusing reference to number for sets --- bricktracker/set.py | 24 ++++++++++++------------ bricktracker/wish.py | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/bricktracker/set.py b/bricktracker/set.py index 3e348e9..523d336 100644 --- a/bricktracker/set.py +++ b/bricktracker/set.py @@ -41,8 +41,8 @@ class BrickSet(RebrickableSet): try: # Insert into the database self.socket.auto_progress( - message='Set {number}: inserting into database'.format( - number=self.fields.set + message='Set {set}: inserting into database'.format( + set=self.fields.set ), increment_total=True, ) @@ -64,8 +64,8 @@ class BrickSet(RebrickableSet): # Commit the transaction to the database self.socket.auto_progress( - message='Set {number}: writing to the database'.format( - number=self.fields.set + message='Set {set}: writing to the database'.format( + set=self.fields.set ), increment_total=True, ) @@ -73,15 +73,15 @@ class BrickSet(RebrickableSet): BrickSQL().commit() # Info - logger.info('Set {number}: imported (id: {id})'.format( - number=self.fields.set, + logger.info('Set {set}: imported (id: {id})'.format( + set=self.fields.set, id=self.fields.id, )) # Complete self.socket.complete( - message='Set {number}: imported (Go to the set)'.format( # noqa: E501 - number=self.fields.set, + message='Set {set}: imported (Go to the set)'.format( # noqa: E501 + set=self.fields.set, url=self.url() ), download=True @@ -89,8 +89,8 @@ class BrickSet(RebrickableSet): except Exception as e: self.socket.fail( - message='Error while importing set {number}: {error}'.format( - number=self.fields.set, + message='Error while importing set {set}: {error}'.format( + set=self.fields.set, error=e, ) ) @@ -155,9 +155,9 @@ class BrickSet(RebrickableSet): ) if rows != 1: - raise DatabaseException('Could not update the status "{status}" for set {number} ({id})'.format( # noqa: E501 + raise DatabaseException('Could not update the status "{status}" for set {set} ({id})'.format( # noqa: E501 status=checkbox.fields.name, - number=self.fields.set, + set=self.fields.set, id=self.fields.id, )) diff --git a/bricktracker/wish.py b/bricktracker/wish.py index 1e301fa..def41e2 100644 --- a/bricktracker/wish.py +++ b/bricktracker/wish.py @@ -31,8 +31,8 @@ class BrickWish(RebrickableSet): # Load from database if not self.select(): raise NotFoundException( - 'Wish with number {number} was not found in the database'.format( # noqa: E501 - number=self.fields.set, + 'Wish for set {set} was not found in the database'.format( # noqa: E501 + set=self.fields.set, ), ) From 900492ae14a093116be593052fb0af4050840cc6 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 27 Jan 2025 14:15:07 +0100 Subject: [PATCH 007/154] Provide decorator for socket actions, for repetitive tasks like checking if authenticated or ready for Rebrickable actions --- bricktracker/socket.py | 74 +++---------------------- bricktracker/socket_decorator.py | 93 ++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 66 deletions(-) create mode 100644 bricktracker/socket_decorator.py diff --git a/bricktracker/socket.py b/bricktracker/socket.py index c7215ae..3fbd6cd 100644 --- a/bricktracker/socket.py +++ b/bricktracker/socket.py @@ -1,14 +1,13 @@ import logging from typing import Any, Final, Tuple -from flask import copy_current_request_context, Flask, request +from flask import Flask, request from flask_socketio import SocketIO -from .configuration_list import BrickConfigurationList from .instructions import BrickInstructions from .instructions_list import BrickInstructionsList -from .login import LoginManager from .set import BrickSet +from .socket_decorator import authenticated_socket, rebrickable_socket from .sql import close as sql_close logger = logging.getLogger(__name__) @@ -87,12 +86,8 @@ class BrickSocket(object): self.disconnected() @self.socket.on(MESSAGES['DOWNLOAD_INSTRUCTIONS'], namespace=self.namespace) # noqa: E501 + @authenticated_socket(self) def download_instructions(data: dict[str, Any], /) -> None: - # Needs to be authenticated - if LoginManager.is_not_authenticated(): - self.fail(message='You need to be authenticated') - return - instructions = BrickInstructions( '{name}.pdf'.format(name=data.get('alt', '')), socket=self @@ -107,71 +102,18 @@ class BrickSocket(object): except Exception: pass - # Start it in a thread if requested - if self.threaded: - @copy_current_request_context - def do_download() -> None: - instructions.download(path) + instructions.download(path) - BrickInstructionsList(force=True) - - self.socket.start_background_task(do_download) - else: - instructions.download(path) - - BrickInstructionsList(force=True) + BrickInstructionsList(force=True) @self.socket.on(MESSAGES['IMPORT_SET'], namespace=self.namespace) + @rebrickable_socket(self) def import_set(data: dict[str, Any], /) -> None: - # Needs to be authenticated - if LoginManager.is_not_authenticated(): - self.fail(message='You need to be authenticated') - return - - # Needs the Rebrickable API key - try: - BrickConfigurationList.error_unless_is_set('REBRICKABLE_API_KEY') # noqa: E501 - except Exception as e: - self.fail(message=str(e)) - return - - brickset = BrickSet(socket=self) - - # Start it in a thread if requested - if self.threaded: - @copy_current_request_context - def do_download() -> None: - brickset.download(data) - - self.socket.start_background_task(do_download) - else: - brickset.download(data) + BrickSet(socket=self).download(data) @self.socket.on(MESSAGES['LOAD_SET'], namespace=self.namespace) def load_set(data: dict[str, Any], /) -> None: - # Needs to be authenticated - if LoginManager.is_not_authenticated(): - self.fail(message='You need to be authenticated') - return - - # Needs the Rebrickable API key - try: - BrickConfigurationList.error_unless_is_set('REBRICKABLE_API_KEY') # noqa: E501 - except Exception as e: - self.fail(message=str(e)) - return - - brickset = BrickSet(socket=self) - - # Start it in a thread if requested - if self.threaded: - @copy_current_request_context - def do_load() -> None: - brickset.load(data) - - self.socket.start_background_task(do_load) - else: - brickset.load(data) + BrickSet(socket=self).load(data) # Update the progress auto-incrementing def auto_progress( diff --git a/bricktracker/socket_decorator.py b/bricktracker/socket_decorator.py new file mode 100644 index 0000000..331b457 --- /dev/null +++ b/bricktracker/socket_decorator.py @@ -0,0 +1,93 @@ +from functools import wraps +from threading import Thread +from typing import Callable, ParamSpec, TYPE_CHECKING, Union + +from flask import copy_current_request_context + +from .configuration_list import BrickConfigurationList +from .login import LoginManager +if TYPE_CHECKING: + from .socket import BrickSocket + +# What a threaded function can return (None or Thread) +SocketReturn = Union[None, Thread] + +# Threaded signature (*arg, **kwargs -> (None or Thread) +P = ParamSpec('P') +SocketCallable = Callable[P, SocketReturn] + + +# Fail if not authenticated +def authenticated_socket( + self: 'BrickSocket', + /, + *, + threaded: bool = True, +) -> Callable[[SocketCallable], SocketCallable]: + def outer(function: SocketCallable, /) -> SocketCallable: + @wraps(function) + def wrapper(*args, **kwargs) -> SocketReturn: + # Needs to be authenticated + if LoginManager.is_not_authenticated(): + self.fail(message='You need to be authenticated') + return + + # Apply threading + if threaded: + return threaded_socket(self)(function)(*args, **kwargs) + else: + return function(*args, **kwargs) + + return wrapper + return outer + + +# Fail if not ready for Rebrickable (authenticated, API key) +# Automatically makes it threaded +def rebrickable_socket( + self: 'BrickSocket', + /, + *, + threaded: bool = True, +) -> Callable[[SocketCallable], SocketCallable]: + def outer(function: SocketCallable, /) -> SocketCallable: + @wraps(function) + # Automatically authenticated + @authenticated_socket(self, threaded=False) + def wrapper(*args, **kwargs) -> SocketReturn: + # Needs the Rebrickable API key + try: + BrickConfigurationList.error_unless_is_set('REBRICKABLE_API_KEY') # noqa: E501 + except Exception as e: + self.fail(message=str(e)) + return + + # Apply threading + if threaded: + return threaded_socket(self)(function)(*args, **kwargs) + else: + return function(*args, **kwargs) + + return wrapper + return outer + + +# Start the function in a thread if the socket is threaded +def threaded_socket( + self: 'BrickSocket', + / +) -> Callable[[SocketCallable], SocketCallable]: + def outer(function: SocketCallable, /) -> SocketCallable: + @wraps(function) + def wrapper(*args, **kwargs) -> SocketReturn: + # Start it in a thread if requested + if self.threaded: + @copy_current_request_context + def do_function() -> None: + function(*args, **kwargs) + + return self.socket.start_background_task(do_function) + else: + return function(*args, **kwargs) + return wrapper + return outer From d1325b595cb642c958fd0f81677e9ec166f20dc7 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 27 Jan 2025 14:20:12 +0100 Subject: [PATCH 008/154] Inject the socket only where necessary --- bricktracker/rebrickable_set.py | 21 ++++++++------------- bricktracker/set.py | 20 +++++++++++--------- bricktracker/socket.py | 4 ++-- 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/bricktracker/rebrickable_set.py b/bricktracker/rebrickable_set.py index 23072d3..1cd4b8d 100644 --- a/bricktracker/rebrickable_set.py +++ b/bricktracker/rebrickable_set.py @@ -21,7 +21,6 @@ logger = logging.getLogger(__name__) # A set from Rebrickable class RebrickableSet(BrickRecord): - socket: 'BrickSocket' theme: 'BrickTheme' instructions: list[BrickInstructions] @@ -36,7 +35,6 @@ class RebrickableSet(BrickRecord): self, /, *, - socket: 'BrickSocket | None' = None, record: Row | dict[str, Any] | None = None ): super().__init__() @@ -44,10 +42,6 @@ class RebrickableSet(BrickRecord): # Placeholders self.instructions = [] - # Save the socket - if socket is not None: - self.socket = socket - # Ingest the record if it has one if record is not None: self.ingest(record) @@ -92,20 +86,21 @@ class RebrickableSet(BrickRecord): # Load the set from Rebrickable def load( self, + socket: 'BrickSocket', data: dict[str, Any], /, *, from_download=False, ) -> bool: # Reset the progress - self.socket.progress_count = 0 - self.socket.progress_total = 2 + socket.progress_count = 0 + socket.progress_total = 2 try: - self.socket.auto_progress(message='Parsing set number') + socket.auto_progress(message='Parsing set number') set = parse_set(str(data['set'])) - self.socket.auto_progress( + socket.auto_progress( message='Set {set}: loading from Rebrickable'.format( set=set, ), @@ -122,12 +117,12 @@ class RebrickableSet(BrickRecord): instance=self, ).get() - self.socket.emit('SET_LOADED', self.short( + socket.emit('SET_LOADED', self.short( from_download=from_download )) if not from_download: - self.socket.complete( + socket.complete( message='Set {set}: loaded from Rebrickable'.format( set=self.fields.set ) @@ -136,7 +131,7 @@ class RebrickableSet(BrickRecord): return True except Exception as e: - self.socket.fail( + socket.fail( message='Could not load the set from Rebrickable: {error}. Data: {data}'.format( # noqa: E501 error=str(e), data=data, diff --git a/bricktracker/set.py b/bricktracker/set.py index 523d336..e521268 100644 --- a/bricktracker/set.py +++ b/bricktracker/set.py @@ -1,6 +1,6 @@ import logging import traceback -from typing import Any, Self +from typing import Any, Self, TYPE_CHECKING from uuid import uuid4 from flask import current_app, url_for @@ -14,6 +14,8 @@ from .rebrickable_set import RebrickableSet from .set_checkbox import BrickSetCheckbox from .set_checkbox_list import BrickSetCheckboxList from .sql import BrickSQL +if TYPE_CHECKING: + from .socket import BrickSocket logger = logging.getLogger(__name__) @@ -33,14 +35,14 @@ class BrickSet(RebrickableSet): ) # Import a set into the database - def download(self, data: dict[str, Any], /) -> None: + def download(self, socket: 'BrickSocket', data: dict[str, Any], /) -> None: # Load the set - if not self.load(data, from_download=True): + if not self.load(socket, data, from_download=True): return try: # Insert into the database - self.socket.auto_progress( + socket.auto_progress( message='Set {set}: inserting into database'.format( set=self.fields.set ), @@ -57,13 +59,13 @@ class BrickSet(RebrickableSet): self.insert_rebrickable() # Load the inventory - RebrickableParts(self.socket, self).download() + RebrickableParts(socket, self).download() # Load the minifigures - RebrickableMinifigures(self.socket, self).download() + RebrickableMinifigureList(socket, self).download() # Commit the transaction to the database - self.socket.auto_progress( + socket.auto_progress( message='Set {set}: writing to the database'.format( set=self.fields.set ), @@ -79,7 +81,7 @@ class BrickSet(RebrickableSet): )) # Complete - self.socket.complete( + socket.complete( message='Set {set}: imported (Go to the set)'.format( # noqa: E501 set=self.fields.set, url=self.url() @@ -88,7 +90,7 @@ class BrickSet(RebrickableSet): ) except Exception as e: - self.socket.fail( + socket.fail( message='Error while importing set {set}: {error}'.format( set=self.fields.set, error=e, diff --git a/bricktracker/socket.py b/bricktracker/socket.py index 3fbd6cd..7aedaf2 100644 --- a/bricktracker/socket.py +++ b/bricktracker/socket.py @@ -109,11 +109,11 @@ class BrickSocket(object): @self.socket.on(MESSAGES['IMPORT_SET'], namespace=self.namespace) @rebrickable_socket(self) def import_set(data: dict[str, Any], /) -> None: - BrickSet(socket=self).download(data) + BrickSet().download(self, data) @self.socket.on(MESSAGES['LOAD_SET'], namespace=self.namespace) def load_set(data: dict[str, Any], /) -> None: - BrickSet(socket=self).load(data) + BrickSet().load(self, data) # Update the progress auto-incrementing def auto_progress( From 1f7a984692ffe17abeabc36c79c22427a56f4ad0 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 27 Jan 2025 17:07:30 +0100 Subject: [PATCH 009/154] Rename load to from_set for clarity --- bricktracker/minifigure_list.py | 2 +- bricktracker/set.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bricktracker/minifigure_list.py b/bricktracker/minifigure_list.py index 04ece73..4b5f183 100644 --- a/bricktracker/minifigure_list.py +++ b/bricktracker/minifigure_list.py @@ -61,7 +61,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): return self # Load minifigures from a brickset - def load(self, brickset: 'BrickSet', /) -> Self: + def from_set(self, brickset: 'BrickSet', /) -> Self: # Save the brickset self.brickset = brickset diff --git a/bricktracker/set.py b/bricktracker/set.py index e521268..17f71b8 100644 --- a/bricktracker/set.py +++ b/bricktracker/set.py @@ -101,7 +101,7 @@ class BrickSet(RebrickableSet): # Minifigures def minifigures(self, /) -> BrickMinifigureList: - return BrickMinifigureList().load(self) + return BrickMinifigureList().from_set(self) # Parts def parts(self, /) -> BrickPartList: From a0fd62b9d25a61025edb9ef94e38707fd1b510cb Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 27 Jan 2025 18:39:35 +0100 Subject: [PATCH 010/154] Deduplicate minifigures --- .env.sample | 11 +- bricktracker/config.py | 4 +- bricktracker/minifigure.py | 161 ++++++------------ bricktracker/minifigure_list.py | 66 +++++-- bricktracker/part.py | 8 +- bricktracker/part_list.py | 2 +- bricktracker/rebrickable_image.py | 14 +- bricktracker/rebrickable_minifigure.py | 130 ++++++++++++++ bricktracker/rebrickable_minifigures.py | 85 --------- bricktracker/rebrickable_parts.py | 2 +- bricktracker/set.py | 5 +- bricktracker/set_list.py | 16 +- bricktracker/sql/migrations/0007.sql | 30 ++++ bricktracker/sql/migrations/0008.sql | 32 ++++ .../minifigure/base/{select.sql => base.sql} | 17 +- bricktracker/sql/minifigure/insert.sql | 20 +-- bricktracker/sql/minifigure/list/all.sql | 12 +- bricktracker/sql/minifigure/list/from_set.sql | 5 +- bricktracker/sql/minifigure/list/last.sql | 10 +- .../sql/minifigure/list/missing_part.sql | 10 +- .../sql/minifigure/list/using_part.sql | 8 +- .../sql/minifigure/select/generic.sql | 28 +-- .../sql/minifigure/select/specific.sql | 7 +- bricktracker/sql/part/list/all.sql | 15 +- bricktracker/sql/part/list/missing.sql | 10 +- bricktracker/sql/part/select/generic.sql | 10 +- .../sql/rebrickable/minifigure/insert.sql | 11 ++ .../sql/rebrickable/minifigure/list.sql | 6 + .../sql/rebrickable/minifigure/select.sql | 8 + bricktracker/sql/schema/drop.sql | 3 + bricktracker/sql/set/base/full.sql | 10 +- bricktracker/sql/set/delete/set.sql | 4 +- .../sql/set/list/missing_minifigure.sql | 2 +- .../sql/set/list/using_minifigure.sql | 2 +- bricktracker/sql/set/select/full.sql | 2 +- bricktracker/sql_counter.py | 4 +- bricktracker/views/set.py | 2 +- templates/minifigure/card.html | 8 +- templates/minifigure/table.html | 2 +- templates/set/card.html | 2 +- 40 files changed, 441 insertions(+), 343 deletions(-) create mode 100644 bricktracker/rebrickable_minifigure.py delete mode 100644 bricktracker/rebrickable_minifigures.py create mode 100644 bricktracker/sql/migrations/0007.sql create mode 100644 bricktracker/sql/migrations/0008.sql rename bricktracker/sql/minifigure/base/{select.sql => base.sql} (56%) create mode 100644 bricktracker/sql/rebrickable/minifigure/insert.sql create mode 100644 bricktracker/sql/rebrickable/minifigure/list.sql create mode 100644 bricktracker/sql/rebrickable/minifigure/select.sql diff --git a/.env.sample b/.env.sample index 06584db..91caf76 100644 --- a/.env.sample +++ b/.env.sample @@ -121,10 +121,11 @@ # Optional: Change the default order of minifigures. By default ordered by insertion order. # Useful column names for this option are: -# - "minifigures"."fig_num": minifigure ID (fig-xxxxx) -# - "minifigures"."name": minifigure name -# Default: "minifigures"."name" ASC -# BK_MINIFIGURES_DEFAULT_ORDER="minifigures"."name" ASC +# - "rebrickable_minifigures"."figure": minifigure ID (fig-xxxxx) +# - "rebrickable_minifigures"."number": minifigure ID as an integer (xxxxx) +# - "rebrickable_minifigures"."name": minifigure name +# Default: "rebrickable_minifigures"."name" ASC +# BK_MINIFIGURES_DEFAULT_ORDER="rebrickable_minifigures"."name" ASC # Optional: Folder where to store the minifigures images, relative to the '/app/static/' folder # Default: minifigs @@ -175,7 +176,7 @@ # BK_REBRICKABLE_IMAGE_NIL_MINIFIGURE= # Optional: Pattern of the link to Rebrickable for a minifigure. Will be passed to Python .format() -# Default: https://rebrickable.com/minifigs/{number} +# Default: https://rebrickable.com/minifigs/{figure} # BK_REBRICKABLE_LINK_MINIFIGURE_PATTERN= # Optional: Pattern of the link to Rebrickable for a part. Will be passed to Python .format() diff --git a/bricktracker/config.py b/bricktracker/config.py index f9d8f2b..236eb54 100644 --- a/bricktracker/config.py +++ b/bricktracker/config.py @@ -32,7 +32,7 @@ CONFIG: Final[list[dict[str, Any]]] = [ {'n': 'HIDE_MISSING_PARTS', 'c': bool}, {'n': 'HIDE_SET_INSTRUCTIONS', 'c': bool}, {'n': 'HIDE_WISHES', 'c': bool}, - {'n': 'MINIFIGURES_DEFAULT_ORDER', 'd': '"minifigures"."name" ASC'}, + {'n': 'MINIFIGURES_DEFAULT_ORDER', 'd': '"rebrickable_minifigures"."name" ASC'}, # noqa: E501 {'n': 'MINIFIGURES_FOLDER', 'd': 'minifigs', 's': True}, {'n': 'NO_THREADED_SOCKET', 'c': bool}, {'n': 'PARTS_DEFAULT_ORDER', 'd': '"inventory"."name" ASC, "inventory"."color_name" ASC, "inventory"."is_spare" ASC'}, # noqa: E501 @@ -42,7 +42,7 @@ CONFIG: Final[list[dict[str, Any]]] = [ {'n': 'REBRICKABLE_API_KEY', 'e': 'REBRICKABLE_API_KEY', 'd': ''}, {'n': 'REBRICKABLE_IMAGE_NIL', 'd': 'https://rebrickable.com/static/img/nil.png'}, # noqa: E501 {'n': 'REBRICKABLE_IMAGE_NIL_MINIFIGURE', 'd': 'https://rebrickable.com/static/img/nil_mf.jpg'}, # noqa: E501 - {'n': 'REBRICKABLE_LINK_MINIFIGURE_PATTERN', 'd': 'https://rebrickable.com/minifigs/{number}'}, # noqa: E501 + {'n': 'REBRICKABLE_LINK_MINIFIGURE_PATTERN', 'd': 'https://rebrickable.com/minifigs/{figure}'}, # noqa: E501 {'n': 'REBRICKABLE_LINK_PART_PATTERN', 'd': 'https://rebrickable.com/parts/{number}/_/{color}'}, # noqa: E501 {'n': 'REBRICKABLE_LINK_INSTRUCTIONS_PATTERN', 'd': 'https://rebrickable.com/instructions/{path}'}, # noqa: E501 {'n': 'REBRICKABLE_USER_AGENT', 'd': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'}, # noqa: E501 diff --git a/bricktracker/minifigure.py b/bricktracker/minifigure.py index 0ad55b1..76a482e 100644 --- a/bricktracker/minifigure.py +++ b/bricktracker/minifigure.py @@ -1,48 +1,61 @@ -from sqlite3 import Row -from typing import Any, Self, TYPE_CHECKING - -from flask import current_app, url_for +import logging +import traceback +from typing import Self, TYPE_CHECKING from .exceptions import ErrorException, NotFoundException from .part_list import BrickPartList -from .rebrickable_image import RebrickableImage -from .record import BrickRecord +from .rebrickable_parts import RebrickableParts +from .rebrickable_minifigure import RebrickableMinifigure if TYPE_CHECKING: from .set import BrickSet + from .socket import BrickSocket + +logger = logging.getLogger(__name__) # Lego minifigure -class BrickMinifigure(BrickRecord): - brickset: 'BrickSet | None' - +class BrickMinifigure(RebrickableMinifigure): # Queries insert_query: str = 'minifigure/insert' generic_query: str = 'minifigure/select/generic' select_query: str = 'minifigure/select/specific' - def __init__( - self, - /, - *, - brickset: 'BrickSet | None' = None, - record: Row | dict[str, Any] | None = None, - ): - super().__init__() + def download(self, socket: 'BrickSocket'): + if self.brickset is None: + raise ErrorException('Importing a minifigure from Rebrickable outside of a set is not supported') # noqa: E501 - # Save the brickset - self.brickset = brickset + try: + # Insert into the database + socket.auto_progress( + message='Set {set}: inserting minifigure {figure} into database'.format( # noqa: E501 + set=self.brickset.fields.set, + figure=self.fields.figure + ) + ) - # Ingest the record if it has one - if record is not None: - self.ingest(record) + # Insert into database + self.insert(commit=False) - # Return the number just in digits format - def clean_number(self, /) -> str: - number: str = self.fields.fig_num - number = number.removeprefix('fig-') - number = number.lstrip('0') + # Insert the rebrickable set into database + self.insert_rebrickable() - return number + # Load the inventory + RebrickableParts( + socket, + self.brickset, + minifigure=self, + ).download() + + except Exception as e: + socket.fail( + message='Error while importing minifigure {figure} from {set}: {error}'.format( # noqa: E501 + figure=self.fields.figure, + set=self.brickset.fields.set, + error=e, + ) + ) + + logger.debug(traceback.format_exc()) # Parts def generic_parts(self, /) -> BrickPartList: @@ -51,108 +64,38 @@ class BrickMinifigure(BrickRecord): # Parts def parts(self, /) -> BrickPartList: if self.brickset is None: - raise ErrorException('Part list for minifigure {number} requires a brickset'.format( # noqa: E501 - number=self.fields.fig_num, + raise ErrorException('Part list for minifigure {figure} requires a brickset'.format( # noqa: E501 + figure=self.fields.figure, )) return BrickPartList().load(self.brickset, minifigure=self) # Select a generic minifigure - def select_generic(self, fig_num: str, /) -> Self: + def select_generic(self, figure: str, /) -> Self: # Save the parameters to the fields - self.fields.fig_num = fig_num + self.fields.figure = figure if not self.select(override_query=self.generic_query): raise NotFoundException( - 'Minifigure with number {number} was not found in the database'.format( # noqa: E501 - number=self.fields.fig_num, + 'Minifigure with figure {figure} was not found in the database'.format( # noqa: E501 + figure=self.fields.figure, ), ) return self - # Select a specific minifigure (with a set and an number) - def select_specific(self, brickset: 'BrickSet', fig_num: str, /) -> Self: + # Select a specific minifigure (with a set and a figure) + def select_specific(self, brickset: 'BrickSet', figure: str, /) -> Self: # Save the parameters to the fields self.brickset = brickset - self.fields.fig_num = fig_num + self.fields.figure = figure if not self.select(): raise NotFoundException( - 'Minifigure with number {number} from set {set} was not found in the database'.format( # noqa: E501 - number=self.fields.fig_num, + 'Minifigure with figure {figure} from set {set} was not found in the database'.format( # noqa: E501 + figure=self.fields.figure, set=self.brickset.fields.set, ), ) return self - - # Return a dict with common SQL parameters for a minifigure - def sql_parameters(self, /) -> dict[str, Any]: - parameters = super().sql_parameters() - - # Supplement from the brickset - if self.brickset is not None: - if 'u_id' not in parameters: - parameters['u_id'] = self.brickset.fields.id - - if 'set_num' not in parameters: - parameters['set_num'] = self.brickset.fields.set - - return parameters - - # Self url - def url(self, /) -> str: - return url_for( - 'minifigure.details', - number=self.fields.fig_num, - ) - - # Compute the url for minifigure part image - def url_for_image(self, /) -> str: - if not current_app.config['USE_REMOTE_IMAGES']: - if self.fields.set_img_url is None: - file = RebrickableImage.nil_minifigure_name() - else: - file = self.fields.fig_num - - return RebrickableImage.static_url(file, 'MINIFIGURES_FOLDER') - else: - if self.fields.set_img_url is None: - return current_app.config['REBRICKABLE_IMAGE_NIL_MINIFIGURE'] - else: - return self.fields.set_img_url - - # Compute the url for the rebrickable page - def url_for_rebrickable(self, /) -> str: - if current_app.config['REBRICKABLE_LINKS']: - try: - return current_app.config['REBRICKABLE_LINK_MINIFIGURE_PATTERN'].format( # noqa: E501 - number=self.fields.fig_num.lower(), - ) - except Exception: - pass - - return '' - - # Normalize from Rebrickable - @staticmethod - def from_rebrickable( - data: dict[str, Any], - /, - *, - brickset: 'BrickSet | None' = None, - **_, - ) -> dict[str, Any]: - record = { - 'fig_num': data['set_num'], - 'name': data['set_name'], - 'quantity': data['quantity'], - 'set_img_url': data['set_img_url'], - } - - if brickset is not None: - record['set_num'] = brickset.fields.set - record['u_id'] = brickset.fields.id - - return record diff --git a/bricktracker/minifigure_list.py b/bricktracker/minifigure_list.py index 4b5f183..81affa6 100644 --- a/bricktracker/minifigure_list.py +++ b/bricktracker/minifigure_list.py @@ -1,11 +1,17 @@ +import logging +import traceback from typing import Any, Self, TYPE_CHECKING from flask import current_app from .minifigure import BrickMinifigure +from .rebrickable import Rebrickable from .record_list import BrickRecordList if TYPE_CHECKING: from .set import BrickSet + from .socket import BrickSocket + +logger = logging.getLogger(__name__) # Lego minifigures @@ -47,7 +53,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): if current_app.config['RANDOM']: order = 'RANDOM()' else: - order = 'minifigures.rowid DESC' + order = '"bricktracker_minifigures"."rowid" DESC' for record in self.select( override_query=self.last_query, @@ -73,16 +79,6 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): return self - # Return a dict with common SQL parameters for a minifigures list - def sql_parameters(self, /) -> dict[str, Any]: - parameters: dict[str, Any] = super().sql_parameters() - - if self.brickset is not None: - parameters['u_id'] = self.brickset.fields.id - parameters['set_num'] = self.brickset.fields.set - - return parameters - # Minifigures missing a part def missing_part( self, @@ -132,3 +128,51 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): self.records.append(minifigure) return self + + # Return a dict with common SQL parameters for a minifigures list + def sql_parameters(self, /) -> dict[str, Any]: + parameters: dict[str, Any] = super().sql_parameters() + + if self.brickset is not None: + parameters['bricktracker_set_id'] = self.brickset.fields.id + + return parameters + + # Import the minifigures from Rebrickable + @staticmethod + def download(socket: 'BrickSocket', brickset: 'BrickSet', /) -> None: + try: + socket.auto_progress( + message='Set {set}: loading minifigures from Rebrickable'.format( # noqa: E501 + set=brickset.fields.set, + ), + increment_total=True, + ) + + logger.debug('rebrick.lego.get_set_minifigs("{set}")'.format( + set=brickset.fields.set, + )) + + minifigures = Rebrickable[BrickMinifigure]( + 'get_set_minifigs', + brickset.fields.set, + BrickMinifigure, + socket=socket, + brickset=brickset, + ).list() + + # Process each minifigure + socket.update_total(len(minifigures), add=True) + + for minifigure in minifigures: + minifigure.download(socket) + + except Exception as e: + socket.fail( + message='Error while importing set {set} minifigure list: {error}'.format( # noqa: E501 + set=brickset.fields.set, + error=e, + ) + ) + + logger.debug(traceback.format_exc()) diff --git a/bricktracker/part.py b/bricktracker/part.py index 80a51bd..b6dc153 100644 --- a/bricktracker/part.py +++ b/bricktracker/part.py @@ -137,7 +137,7 @@ class BrickPart(BrickRecord): if 'set_num' not in parameters: if self.minifigure is not None: - parameters['set_num'] = self.minifigure.fields.fig_num + parameters['set_num'] = self.minifigure.fields.figure elif self.brickset is not None: parameters['set_num'] = self.brickset.fields.set @@ -215,14 +215,14 @@ class BrickPart(BrickRecord): return url_for( 'set.missing_minifigure_part', id=self.fields.u_id, - minifigure_id=self.minifigure.fields.fig_num, - part_id=self.fields.id, + figure=self.minifigure.fields.figure, + part=self.fields.id, ) return url_for( 'set.missing_part', id=self.fields.u_id, - part_id=self.fields.id + part=self.fields.id ) # Compute the url for the rebrickable page diff --git a/bricktracker/part_list.py b/bricktracker/part_list.py index 93897f8..7805d57 100644 --- a/bricktracker/part_list.py +++ b/bricktracker/part_list.py @@ -120,7 +120,7 @@ class BrickPartList(BrickRecordList[BrickPart]): # Use the minifigure number if present, # otherwise use the set number if self.minifigure is not None: - parameters['set_num'] = self.minifigure.fields.fig_num + parameters['set_num'] = self.minifigure.fields.figure elif self.brickset is not None: parameters['set_num'] = self.brickset.fields.set diff --git a/bricktracker/rebrickable_image.py b/bricktracker/rebrickable_image.py index 0a0d9f4..f15a9b4 100644 --- a/bricktracker/rebrickable_image.py +++ b/bricktracker/rebrickable_image.py @@ -8,7 +8,7 @@ from shutil import copyfileobj from .exceptions import DownloadException if TYPE_CHECKING: - from .minifigure import BrickMinifigure + from .rebrickable_minifigure import RebrickableMinifigure from .part import BrickPart from .rebrickable_set import RebrickableSet @@ -16,7 +16,7 @@ if TYPE_CHECKING: # A set, part or minifigure image from Rebrickable class RebrickableImage(object): set: 'RebrickableSet' - minifigure: 'BrickMinifigure | None' + minifigure: 'RebrickableMinifigure | None' part: 'BrickPart | None' extension: str | None @@ -26,7 +26,7 @@ class RebrickableImage(object): set: 'RebrickableSet', /, *, - minifigure: 'BrickMinifigure | None' = None, + minifigure: 'RebrickableMinifigure | None' = None, part: 'BrickPart | None' = None, ): # Save all objects @@ -87,10 +87,10 @@ class RebrickableImage(object): return self.part.fields.part_img_url_id if self.minifigure is not None: - if self.minifigure.fields.set_img_url is None: + if self.minifigure.fields.image is None: return RebrickableImage.nil_minifigure_name() else: - return self.minifigure.fields.fig_num + return self.minifigure.fields.figure return self.set.fields.set @@ -111,10 +111,10 @@ class RebrickableImage(object): return self.part.fields.part_img_url if self.minifigure is not None: - if self.minifigure.fields.set_img_url is None: + if self.minifigure.fields.image is None: return current_app.config['REBRICKABLE_IMAGE_NIL_MINIFIGURE'] else: - return self.minifigure.fields.set_img_url + return self.minifigure.fields.image return self.set.fields.image diff --git a/bricktracker/rebrickable_minifigure.py b/bricktracker/rebrickable_minifigure.py new file mode 100644 index 0000000..28d3d75 --- /dev/null +++ b/bricktracker/rebrickable_minifigure.py @@ -0,0 +1,130 @@ +import logging +from sqlite3 import Row +from typing import Any, TYPE_CHECKING + +from flask import current_app, url_for + +from .exceptions import ErrorException +from .rebrickable_image import RebrickableImage +from .record import BrickRecord +if TYPE_CHECKING: + from .set import BrickSet + from .socket import BrickSocket + +logger = logging.getLogger(__name__) + + +# A minifigure from Rebrickable +class RebrickableMinifigure(BrickRecord): + socket: 'BrickSocket' + brickset: 'BrickSet | None' + + # Queries + select_query: str = 'rebrickable/minifigure/select' + insert_query: str = 'rebrickable/minifigure/insert' + + def __init__( + self, + /, + *, + brickset: 'BrickSet | None' = None, + socket: 'BrickSocket | None' = None, + record: Row | dict[str, Any] | None = None + ): + super().__init__() + + # Placeholders + self.instructions = [] + + # Save the brickset + self.brickset = brickset + + # Save the socket + if socket is not None: + self.socket = socket + + # Ingest the record if it has one + if record is not None: + self.ingest(record) + + # Insert the minifigure from Rebrickable + def insert_rebrickable(self, /) -> bool: + if self.brickset is None: + raise ErrorException('Importing a minifigure from Rebrickable outside of a set is not supported') # noqa: E501 + + # Insert the Rebrickable minifigure to the database + rows, _ = self.insert( + commit=False, + no_defer=True, + override_query=RebrickableMinifigure.insert_query + ) + + inserted = rows > 0 + + if inserted: + if not current_app.config['USE_REMOTE_IMAGES']: + RebrickableImage( + self.brickset, + minifigure=self, + ).download() + + return inserted + + # Return a dict with common SQL parameters for a minifigure + def sql_parameters(self, /) -> dict[str, Any]: + parameters = super().sql_parameters() + + # Supplement from the brickset + if self.brickset is not None: + if 'bricktracker_set_id' not in parameters: + parameters['bricktracker_set_id'] = self.brickset.fields.id + + return parameters + + # Self url + def url(self, /) -> str: + return url_for( + 'minifigure.details', + figure=self.fields.figure, + ) + + # Compute the url for minifigure image + def url_for_image(self, /) -> str: + if not current_app.config['USE_REMOTE_IMAGES']: + if self.fields.image is None: + file = RebrickableImage.nil_minifigure_name() + else: + file = self.fields.figure + + return RebrickableImage.static_url(file, 'MINIFIGURES_FOLDER') + else: + if self.fields.image is None: + return current_app.config['REBRICKABLE_IMAGE_NIL_MINIFIGURE'] + else: + return self.fields.image + + # Compute the url for the rebrickable page + def url_for_rebrickable(self, /) -> str: + if current_app.config['REBRICKABLE_LINKS']: + try: + return current_app.config['REBRICKABLE_LINK_MINIFIGURE_PATTERN'].format( # noqa: E501 + number=self.fields.figure, + ) + except Exception: + pass + + return '' + + # Normalize from Rebrickable + @staticmethod + def from_rebrickable(data: dict[str, Any], /, **_) -> dict[str, Any]: + # Extracting number + number = int(str(data['set_num'])[5:]) + + return { + 'figure': str(data['set_num']), + 'number': int(number), + 'name': str(data['set_name']), + 'quantity': int(data['quantity']), + 'image': data['set_img_url'], + } diff --git a/bricktracker/rebrickable_minifigures.py b/bricktracker/rebrickable_minifigures.py deleted file mode 100644 index eb72e06..0000000 --- a/bricktracker/rebrickable_minifigures.py +++ /dev/null @@ -1,85 +0,0 @@ -import logging -from typing import TYPE_CHECKING - -from flask import current_app - -from .minifigure import BrickMinifigure -from .rebrickable import Rebrickable -from .rebrickable_image import RebrickableImage -from .rebrickable_parts import RebrickableParts -if TYPE_CHECKING: - from .set import BrickSet - from .socket import BrickSocket - -logger = logging.getLogger(__name__) - - -# Minifigures from Rebrickable -class RebrickableMinifigures(object): - socket: 'BrickSocket' - brickset: 'BrickSet' - - def __init__(self, socket: 'BrickSocket', brickset: 'BrickSet', /): - # Save the socket - self.socket = socket - - # Save the objects - self.brickset = brickset - - # Import the minifigures from Rebrickable - def download(self, /) -> None: - self.socket.auto_progress( - message='Set {number}: loading minifigures from Rebrickable'.format( # noqa: E501 - number=self.brickset.fields.set, - ), - increment_total=True, - ) - - logger.debug('rebrick.lego.get_set_minifigs("{set}")'.format( - set=self.brickset.fields.set, - )) - - minifigures = Rebrickable[BrickMinifigure]( - 'get_set_minifigs', - self.brickset.fields.set, - BrickMinifigure, - socket=self.socket, - brickset=self.brickset, - ).list() - - # Process each minifigure - total = len(minifigures) - for index, minifigure in enumerate(minifigures): - # Insert into the database - self.socket.auto_progress( - message='Set {number}: inserting minifigure {current}/{total} into database'.format( # noqa: E501 - number=self.brickset.fields.set, - current=index+1, - total=total, - ) - ) - - # Insert into database - minifigure.insert(commit=False) - - # Grab the image - self.socket.progress( - message='Set {number}: downloading minifigure {current}/{total} image'.format( # noqa: E501 - number=self.brickset.fields.set, - current=index+1, - total=total, - ) - ) - - if not current_app.config['USE_REMOTE_IMAGES']: - RebrickableImage( - self.brickset, - minifigure=minifigure - ).download() - - # Load the inventory - RebrickableParts( - self.socket, - self.brickset, - minifigure=minifigure, - ).download() diff --git a/bricktracker/rebrickable_parts.py b/bricktracker/rebrickable_parts.py index 69c42dc..9fd2341 100644 --- a/bricktracker/rebrickable_parts.py +++ b/bricktracker/rebrickable_parts.py @@ -40,7 +40,7 @@ class RebrickableParts(object): self.minifigure = minifigure if self.minifigure is not None: - self.number = self.minifigure.fields.fig_num + self.number = self.minifigure.fields.figure self.kind = 'Minifigure' self.method = 'get_minifig_elements' else: diff --git a/bricktracker/set.py b/bricktracker/set.py index 17f71b8..52a2ed9 100644 --- a/bricktracker/set.py +++ b/bricktracker/set.py @@ -8,7 +8,6 @@ from flask import current_app, url_for from .exceptions import DatabaseException, NotFoundException from .minifigure_list import BrickMinifigureList from .part_list import BrickPartList -from .rebrickable_minifigures import RebrickableMinifigures from .rebrickable_parts import RebrickableParts from .rebrickable_set import RebrickableSet from .set_checkbox import BrickSetCheckbox @@ -55,14 +54,14 @@ class BrickSet(RebrickableSet): # Insert into database self.insert(commit=False) - # Execute the parent download method + # Insert the rebrickable set into database self.insert_rebrickable() # Load the inventory RebrickableParts(socket, self).download() # Load the minifigures - RebrickableMinifigureList(socket, self).download() + BrickMinifigureList.download(socket, self) # Commit the transaction to the database socket.auto_progress( diff --git a/bricktracker/set_list.py b/bricktracker/set_list.py index 3b229e8..58ae8ec 100644 --- a/bricktracker/set_list.py +++ b/bricktracker/set_list.py @@ -82,13 +82,9 @@ class BrickSetList(BrickRecordList[BrickSet]): return self # Sets missing a minifigure - def missing_minifigure( - self, - fig_num: str, - / - ) -> Self: + def missing_minifigure(self, figure: str, /) -> Self: # Save the parameters to the fields - self.fields.fig_num = fig_num + self.fields.figure = figure # Load the sets from the database for record in self.select( @@ -127,13 +123,9 @@ class BrickSetList(BrickRecordList[BrickSet]): return self # Sets using a minifigure - def using_minifigure( - self, - fig_num: str, - / - ) -> Self: + def using_minifigure(self, figure: str, /) -> Self: # Save the parameters to the fields - self.fields.fig_num = fig_num + self.fields.figure = figure # Load the sets from the database for record in self.select( diff --git a/bricktracker/sql/migrations/0007.sql b/bricktracker/sql/migrations/0007.sql new file mode 100644 index 0000000..09830c4 --- /dev/null +++ b/bricktracker/sql/migrations/0007.sql @@ -0,0 +1,30 @@ +-- description: Creation of the deduplicated table of Rebrickable minifigures + +BEGIN TRANSACTION; + +-- Create a Rebrickable minifigures table: each unique minifigure imported from Rebrickable +CREATE TABLE "rebrickable_minifigures" ( + "figure" TEXT NOT NULL, + "number" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "image" TEXT, + PRIMARY KEY("figure") +); + +-- Insert existing sets into the new table +INSERT INTO "rebrickable_minifigures" ( + "figure", + "number", + "name", + "image" +) +SELECT + "minifigures"."fig_num", + CAST(SUBSTR("minifigures"."fig_num", 5) AS INTEGER), + "minifigures"."name", + "minifigures"."set_img_url" +FROM "minifigures" +GROUP BY + "minifigures"."fig_num"; + +COMMIT; \ No newline at end of file diff --git a/bricktracker/sql/migrations/0008.sql b/bricktracker/sql/migrations/0008.sql new file mode 100644 index 0000000..48b905a --- /dev/null +++ b/bricktracker/sql/migrations/0008.sql @@ -0,0 +1,32 @@ +-- description: Migrate the Bricktracker minifigures + +PRAGMA foreign_keys = ON; + +BEGIN TRANSACTION; + +-- Create a Bricktable minifigures table: an amount of minifigures linked to a Bricktracker set +CREATE TABLE "bricktracker_minifigures" ( + "bricktracker_set_id" TEXT NOT NULL, + "rebrickable_figure" TEXT NOT NULL, + "quantity" INTEGER NOT NULL, + PRIMARY KEY("bricktracker_set_id", "rebrickable_figure"), + FOREIGN KEY("bricktracker_set_id") REFERENCES "bricktracker_sets"("id"), + FOREIGN KEY("rebrickable_figure") REFERENCES "rebrickable_minifigures"("figure") +); + +-- Insert existing sets into the new table +INSERT INTO "bricktracker_minifigures" ( + "bricktracker_set_id", + "rebrickable_figure", + "quantity" +) +SELECT + "minifigures"."u_id", + "minifigures"."fig_num", + "minifigures"."quantity" +FROM "minifigures"; + +-- Rename the original table (don't delete it yet?) +ALTER TABLE "minifigures" RENAME TO "minifigures_old"; + +COMMIT; \ No newline at end of file diff --git a/bricktracker/sql/minifigure/base/select.sql b/bricktracker/sql/minifigure/base/base.sql similarity index 56% rename from bricktracker/sql/minifigure/base/select.sql rename to bricktracker/sql/minifigure/base/base.sql index 8182998..bfaf10d 100644 --- a/bricktracker/sql/minifigure/base/select.sql +++ b/bricktracker/sql/minifigure/base/base.sql @@ -1,10 +1,10 @@ SELECT - "minifigures"."fig_num", - "minifigures"."set_num", - "minifigures"."name", - "minifigures"."quantity", - "minifigures"."set_img_url", - "minifigures"."u_id", + {% block set %}{% endblock %} + "bricktracker_minifigures"."quantity", + "rebrickable_minifigures"."figure", + "rebrickable_minifigures"."number", + "rebrickable_minifigures"."name", + "rebrickable_minifigures"."image", {% block total_missing %} NULL AS "total_missing", -- dummy for order: total_missing {% endblock %} @@ -14,7 +14,10 @@ SELECT {% block total_sets %} NULL AS "total_sets" -- dummy for order: total_sets {% endblock %} -FROM "minifigures" +FROM "bricktracker_minifigures" + +INNER JOIN "rebrickable_minifigures" +ON "bricktracker_minifigures"."rebrickable_figure" IS NOT DISTINCT FROM "rebrickable_minifigures"."figure" {% block join %}{% endblock %} diff --git a/bricktracker/sql/minifigure/insert.sql b/bricktracker/sql/minifigure/insert.sql index d72a2a3..cd7c413 100644 --- a/bricktracker/sql/minifigure/insert.sql +++ b/bricktracker/sql/minifigure/insert.sql @@ -1,15 +1,9 @@ -INSERT INTO "minifigures" ( - "fig_num", - "set_num", - "name", - "quantity", - "set_img_url", - "u_id" +INSERT INTO "bricktracker_minifigures" ( + "bricktracker_set_id", + "rebrickable_figure", + "quantity" ) VALUES ( - :fig_num, - :set_num, - :name, - :quantity, - :set_img_url, - :u_id + :bricktracker_set_id, + :figure, + :quantity ) diff --git a/bricktracker/sql/minifigure/list/all.sql b/bricktracker/sql/minifigure/list/all.sql index a00f474..82e61a2 100644 --- a/bricktracker/sql/minifigure/list/all.sql +++ b/bricktracker/sql/minifigure/list/all.sql @@ -1,15 +1,15 @@ -{% extends 'minifigure/base/select.sql' %} +{% extends 'minifigure/base/base.sql' %} {% block total_missing %} SUM(IFNULL("missing_join"."total", 0)) AS "total_missing", {% endblock %} {% block total_quantity %} -SUM(IFNULL("minifigures"."quantity", 0)) AS "total_quantity", +SUM(IFNULL("bricktracker_minifigures"."quantity", 0)) AS "total_quantity", {% endblock %} {% block total_sets %} -COUNT("minifigures"."set_num") AS "total_sets" +COUNT("bricktracker_minifigures"."bricktracker_set_id") AS "total_sets" {% endblock %} {% block join %} @@ -24,11 +24,11 @@ LEFT JOIN ( "missing"."set_num", "missing"."u_id" ) missing_join -ON "minifigures"."u_id" IS NOT DISTINCT FROM "missing_join"."u_id" -AND "minifigures"."fig_num" IS NOT DISTINCT FROM "missing_join"."set_num" +ON "bricktracker_minifigures"."bricktracker_set_id" IS NOT DISTINCT FROM "missing_join"."u_id" +AND "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "missing_join"."set_num" {% endblock %} {% block group %} GROUP BY - "minifigures"."fig_num" + "rebrickable_minifigures"."figure" {% endblock %} diff --git a/bricktracker/sql/minifigure/list/from_set.sql b/bricktracker/sql/minifigure/list/from_set.sql index ea2dcbe..65b4e69 100644 --- a/bricktracker/sql/minifigure/list/from_set.sql +++ b/bricktracker/sql/minifigure/list/from_set.sql @@ -1,6 +1,5 @@ -{% extends 'minifigure/base/select.sql' %} +{% extends 'minifigure/base/base.sql' %} {% block where %} -WHERE "minifigures"."u_id" IS NOT DISTINCT FROM :u_id -AND "minifigures"."set_num" IS NOT DISTINCT FROM :set_num +WHERE "bricktracker_minifigures"."bricktracker_set_id" IS NOT DISTINCT FROM :bricktracker_set_id {% endblock %} diff --git a/bricktracker/sql/minifigure/list/last.sql b/bricktracker/sql/minifigure/list/last.sql index faf3f40..266b7c0 100644 --- a/bricktracker/sql/minifigure/list/last.sql +++ b/bricktracker/sql/minifigure/list/last.sql @@ -1,4 +1,4 @@ -{% extends 'minifigure/base/select.sql' %} +{% extends 'minifigure/base/base.sql' %} {% block total_missing %} SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", @@ -6,12 +6,12 @@ SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", {% block join %} LEFT JOIN "missing" -ON "minifigures"."fig_num" IS NOT DISTINCT FROM "missing"."set_num" -AND "minifigures"."u_id" IS NOT DISTINCT FROM "missing"."u_id" +ON "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "missing"."set_num" +AND "bricktracker_minifigures"."bricktracker_set_id" IS NOT DISTINCT FROM "missing"."u_id" {% endblock %} {% block group %} GROUP BY - "minifigures"."fig_num", - "minifigures"."u_id" + "rebrickable_minifigures"."figure", + "bricktracker_minifigures"."bricktracker_set_id" {% endblock %} diff --git a/bricktracker/sql/minifigure/list/missing_part.sql b/bricktracker/sql/minifigure/list/missing_part.sql index e0bc54d..660da6d 100644 --- a/bricktracker/sql/minifigure/list/missing_part.sql +++ b/bricktracker/sql/minifigure/list/missing_part.sql @@ -1,4 +1,4 @@ -{% extends 'minifigure/base/select.sql' %} +{% extends 'minifigure/base/base.sql' %} {% block total_missing %} SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", @@ -6,12 +6,12 @@ SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", {% block join %} LEFT JOIN "missing" -ON "minifigures"."fig_num" IS NOT DISTINCT FROM "missing"."set_num" -AND "minifigures"."u_id" IS NOT DISTINCT FROM "missing"."u_id" +ON "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "missing"."set_num" +AND "bricktracker_minifigures"."bricktracker_set_id" IS NOT DISTINCT FROM "missing"."u_id" {% endblock %} {% block where %} -WHERE "minifigures"."fig_num" IN ( +WHERE "rebrickable_minifigures"."figure" IN ( SELECT "missing"."set_num" FROM "missing" @@ -26,5 +26,5 @@ WHERE "minifigures"."fig_num" IN ( {% block group %} GROUP BY - "minifigures"."fig_num" + "rebrickable_minifigures"."figure" {% endblock %} diff --git a/bricktracker/sql/minifigure/list/using_part.sql b/bricktracker/sql/minifigure/list/using_part.sql index c40d379..e701d8d 100644 --- a/bricktracker/sql/minifigure/list/using_part.sql +++ b/bricktracker/sql/minifigure/list/using_part.sql @@ -1,11 +1,11 @@ -{% extends 'minifigure/base/select.sql' %} +{% extends 'minifigure/base/base.sql' %} {% block total_quantity %} -SUM("minifigures"."quantity") AS "total_quantity", +SUM("bricktracker_minifigures"."quantity") AS "total_quantity", {% endblock %} {% block where %} -WHERE "minifigures"."fig_num" IN ( +WHERE "rebrickable_minifigures"."figure" IN ( SELECT "inventory"."set_num" FROM "inventory" @@ -20,5 +20,5 @@ WHERE "minifigures"."fig_num" IN ( {% block group %} GROUP BY - "minifigures"."fig_num" + "rebrickable_minifigures"."figure" {% endblock %} diff --git a/bricktracker/sql/minifigure/select/generic.sql b/bricktracker/sql/minifigure/select/generic.sql index 114810d..966c022 100644 --- a/bricktracker/sql/minifigure/select/generic.sql +++ b/bricktracker/sql/minifigure/select/generic.sql @@ -1,38 +1,28 @@ -{% extends 'minifigure/base/select.sql' %} +{% extends 'minifigure/base/base.sql' %} {% block total_missing %} -SUM(IFNULL("missing_join"."total", 0)) AS "total_missing", +SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", {% endblock %} {% block total_quantity %} -SUM(IFNULL("minifigures"."quantity", 0)) AS "total_quantity", +SUM(IFNULL("bricktracker_minifigures"."quantity", 0)) AS "total_quantity", {% endblock %} {% block total_sets %} -COUNT("minifigures"."set_num") AS "total_sets" +COUNT(DISTINCT "bricktracker_minifigures"."bricktracker_set_id") AS "total_sets" {% endblock %} {% block join %} --- LEFT JOIN + SELECT to avoid messing the total -LEFT JOIN ( - SELECT - "missing"."set_num", - "missing"."u_id", - SUM("missing"."quantity") AS "total" - FROM "missing" - GROUP BY - "missing"."set_num", - "missing"."u_id" -) "missing_join" -ON "minifigures"."u_id" IS NOT DISTINCT FROM "missing_join"."u_id" -AND "minifigures"."fig_num" IS NOT DISTINCT FROM "missing_join"."set_num" +LEFT JOIN "missing" +ON "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "missing"."set_num" +AND "bricktracker_minifigures"."bricktracker_set_id" IS NOT DISTINCT FROM "missing"."u_id" {% endblock %} {% block where %} -WHERE "minifigures"."fig_num" IS NOT DISTINCT FROM :fig_num +WHERE "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM :figure {% endblock %} {% block group %} GROUP BY - "minifigures"."fig_num" + "rebrickable_minifigures"."figure" {% endblock %} diff --git a/bricktracker/sql/minifigure/select/specific.sql b/bricktracker/sql/minifigure/select/specific.sql index 34a8b3d..479c9e5 100644 --- a/bricktracker/sql/minifigure/select/specific.sql +++ b/bricktracker/sql/minifigure/select/specific.sql @@ -1,7 +1,6 @@ -{% extends 'minifigure/base/select.sql' %} +{% extends 'minifigure/base/base.sql' %} {% block where %} -WHERE "minifigures"."fig_num" IS NOT DISTINCT FROM :fig_num -AND "minifigures"."u_id" IS NOT DISTINCT FROM :u_id -AND "minifigures"."set_num" IS NOT DISTINCT FROM :set_num +WHERE "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM :figure +AND "bricktracker_minifigures"."bricktracker_set_id" IS NOT DISTINCT FROM :bricktracker_set_id {% endblock %} diff --git a/bricktracker/sql/part/list/all.sql b/bricktracker/sql/part/list/all.sql index b1ff2ac..9d73fcc 100644 --- a/bricktracker/sql/part/list/all.sql +++ b/bricktracker/sql/part/list/all.sql @@ -5,15 +5,15 @@ SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", {% endblock %} {% block total_quantity %} -SUM("inventory"."quantity" * IFNULL("minifigures"."quantity", 1)) AS "total_quantity", +SUM("inventory"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)) AS "total_quantity", {% endblock %} {% block total_sets %} -COUNT(DISTINCT "bricktracker_sets"."id") AS "total_sets", +COUNT(DISTINCT "bricktracker_minifigures"."bricktracker_set_id") AS "total_sets", {% endblock %} {% block total_minifigures %} -SUM(IFNULL("minifigures"."quantity", 0)) AS "total_minifigures" +SUM(IFNULL("bricktracker_minifigures"."quantity", 0)) AS "total_minifigures" {% endblock %} {% block join %} @@ -25,12 +25,9 @@ AND "inventory"."color_id" IS NOT DISTINCT FROM "missing"."color_id" AND "inventory"."element_id" IS NOT DISTINCT FROM "missing"."element_id" AND "inventory"."u_id" IS NOT DISTINCT FROM "missing"."u_id" -LEFT JOIN "minifigures" -ON "inventory"."set_num" IS NOT DISTINCT FROM "minifigures"."fig_num" -AND "inventory"."u_id" IS NOT DISTINCT FROM "minifigures"."u_id" - -LEFT JOIN "bricktracker_sets" -ON "inventory"."u_id" IS NOT DISTINCT FROM "bricktracker_sets"."id" +LEFT JOIN "bricktracker_minifigures" +ON "inventory"."set_num" IS NOT DISTINCT FROM "bricktracker_minifigures"."rebrickable_figure" +AND "inventory"."u_id" IS NOT DISTINCT FROM "bricktracker_minifigures"."bricktracker_set_id" {% endblock %} {% block group %} diff --git a/bricktracker/sql/part/list/missing.sql b/bricktracker/sql/part/list/missing.sql index 555916f..fc64e25 100644 --- a/bricktracker/sql/part/list/missing.sql +++ b/bricktracker/sql/part/list/missing.sql @@ -5,11 +5,11 @@ SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", {% endblock %} {% block total_sets %} -COUNT("inventory"."u_id") - COUNT("minifigures"."u_id") AS "total_sets", +COUNT("inventory"."u_id") - COUNT("bricktracker_minifigures"."bricktracker_set_id") AS "total_sets", {% endblock %} {% block total_minifigures %} -SUM(IFNULL("minifigures"."quantity", 0)) AS "total_minifigures" +SUM(IFNULL("bricktracker_minifigures"."quantity", 0)) AS "total_minifigures" {% endblock %} {% block join %} @@ -21,9 +21,9 @@ AND "missing"."color_id" IS NOT DISTINCT FROM "inventory"."color_id" AND "missing"."element_id" IS NOT DISTINCT FROM "inventory"."element_id" AND "missing"."u_id" IS NOT DISTINCT FROM "inventory"."u_id" -LEFT JOIN "minifigures" -ON "missing"."set_num" IS NOT DISTINCT FROM "minifigures"."fig_num" -AND "missing"."u_id" IS NOT DISTINCT FROM "minifigures"."u_id" +LEFT JOIN "bricktracker_minifigures" +ON "inventory"."set_num" IS NOT DISTINCT FROM "bricktracker_minifigures"."rebrickable_figure" +AND "inventory"."u_id" IS NOT DISTINCT FROM "bricktracker_minifigures"."bricktracker_set_id" {% endblock %} {% block group %} diff --git a/bricktracker/sql/part/select/generic.sql b/bricktracker/sql/part/select/generic.sql index 4a75b4c..28b32a9 100644 --- a/bricktracker/sql/part/select/generic.sql +++ b/bricktracker/sql/part/select/generic.sql @@ -5,11 +5,11 @@ SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", {% endblock %} {% block total_quantity %} -SUM((NOT "inventory"."is_spare") * "inventory"."quantity" * IFNULL("minifigures"."quantity", 1)) AS "total_quantity", +SUM((NOT "inventory"."is_spare") * "inventory"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)) AS "total_quantity", {% endblock %} {% block total_spare %} -SUM("inventory"."is_spare" * "inventory"."quantity" * IFNULL("minifigures"."quantity", 1)) AS "total_spare", +SUM("inventory"."is_spare" * "inventory"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)) AS "total_spare", {% endblock %} {% block join %} @@ -21,9 +21,9 @@ AND "inventory"."color_id" IS NOT DISTINCT FROM "missing"."color_id" AND "inventory"."element_id" IS NOT DISTINCT FROM "missing"."element_id" AND "inventory"."u_id" IS NOT DISTINCT FROM "missing"."u_id" -LEFT JOIN "minifigures" -ON "inventory"."set_num" IS NOT DISTINCT FROM "minifigures"."fig_num" -AND "inventory"."u_id" IS NOT DISTINCT FROM "minifigures"."u_id" +LEFT JOIN "bricktracker_minifigures" +ON "inventory"."set_num" IS NOT DISTINCT FROM "bricktracker_minifigures"."rebrickable_figure" +AND "inventory"."u_id" IS NOT DISTINCT FROM "bricktracker_minifigures"."bricktracker_set_id" {% endblock %} {% block where %} diff --git a/bricktracker/sql/rebrickable/minifigure/insert.sql b/bricktracker/sql/rebrickable/minifigure/insert.sql new file mode 100644 index 0000000..0671925 --- /dev/null +++ b/bricktracker/sql/rebrickable/minifigure/insert.sql @@ -0,0 +1,11 @@ +INSERT OR IGNORE INTO "rebrickable_minifigures" ( + "figure", + "number", + "name", + "image" +) VALUES ( + :figure, + :number, + :name, + :image +) diff --git a/bricktracker/sql/rebrickable/minifigure/list.sql b/bricktracker/sql/rebrickable/minifigure/list.sql new file mode 100644 index 0000000..ec379d8 --- /dev/null +++ b/bricktracker/sql/rebrickable/minifigure/list.sql @@ -0,0 +1,6 @@ +SELECT + "rebrickable_minifigures"."figure", + "rebrickable_minifigures"."number", + "rebrickable_minifigures"."name", + "rebrickable_minifigures"."image" +FROM "rebrickable_minifigures" diff --git a/bricktracker/sql/rebrickable/minifigure/select.sql b/bricktracker/sql/rebrickable/minifigure/select.sql new file mode 100644 index 0000000..f1c68c1 --- /dev/null +++ b/bricktracker/sql/rebrickable/minifigure/select.sql @@ -0,0 +1,8 @@ +SELECT + "rebrickable_minifigures"."figure", + "rebrickable_minifigures"."number", + "rebrickable_minifigures"."name", + "rebrickable_minifigures"."image" +FROM "rebrickable_minifigures" + +WHERE "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM :figure diff --git a/bricktracker/sql/schema/drop.sql b/bricktracker/sql/schema/drop.sql index b961b28..1d39d99 100644 --- a/bricktracker/sql/schema/drop.sql +++ b/bricktracker/sql/schema/drop.sql @@ -1,12 +1,15 @@ BEGIN transaction; +DROP TABLE IF EXISTS "bricktracker_minifigures"; DROP TABLE IF EXISTS "bricktracker_sets"; DROP TABLE IF EXISTS "bricktracker_set_checkboxes"; DROP TABLE IF EXISTS "bricktracker_set_statuses"; DROP TABLE IF EXISTS "bricktracker_wishes"; DROP TABLE IF EXISTS "inventory"; DROP TABLE IF EXISTS "minifigures"; +DROP TABLE IF EXISTS "minifigures_old"; DROP TABLE IF EXISTS "missing"; +DROP TABLE IF EXISTS "rebrickable_minifigures"; DROP TABLE IF EXISTS "rebrickable_sets"; DROP TABLE IF EXISTS "sets"; DROP TABLE IF EXISTS "sets_old"; diff --git a/bricktracker/sql/set/base/full.sql b/bricktracker/sql/set/base/full.sql index c169c7a..68333c2 100644 --- a/bricktracker/sql/set/base/full.sql +++ b/bricktracker/sql/set/base/full.sql @@ -32,11 +32,11 @@ ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "missing_join"."u_id" -- LEFT JOIN + SELECT to avoid messing the total LEFT JOIN ( SELECT - "minifigures"."u_id", - SUM("minifigures"."quantity") AS "total" - FROM "minifigures" + "bricktracker_minifigures"."bricktracker_set_id", + SUM("bricktracker_minifigures"."quantity") AS "total" + FROM "bricktracker_minifigures" {% block where_minifigures %}{% endblock %} - GROUP BY "u_id" + GROUP BY "bricktracker_minifigures"."bricktracker_set_id" ) "minifigures_join" -ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "minifigures_join"."u_id" +ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "minifigures_join"."bricktracker_set_id" {% endblock %} \ No newline at end of file diff --git a/bricktracker/sql/set/delete/set.sql b/bricktracker/sql/set/delete/set.sql index dd2c856..93a51df 100644 --- a/bricktracker/sql/set/delete/set.sql +++ b/bricktracker/sql/set/delete/set.sql @@ -9,8 +9,8 @@ WHERE "bricktracker_sets"."id" IS NOT DISTINCT FROM '{{ id }}'; DELETE FROM "bricktracker_set_statuses" WHERE "bricktracker_set_statuses"."bricktracker_set_id" IS NOT DISTINCT FROM '{{ id }}'; -DELETE FROM "minifigures" -WHERE "minifigures"."u_id" IS NOT DISTINCT FROM '{{ id }}'; +DELETE FROM "bricktracker_minifigures" +WHERE "bricktracker_minifigures"."bricktracker_set_id" IS NOT DISTINCT FROM '{{ id }}'; DELETE FROM "missing" WHERE "missing"."u_id" IS NOT DISTINCT FROM '{{ id }}'; diff --git a/bricktracker/sql/set/list/missing_minifigure.sql b/bricktracker/sql/set/list/missing_minifigure.sql index 5f27088..2f19bfe 100644 --- a/bricktracker/sql/set/list/missing_minifigure.sql +++ b/bricktracker/sql/set/list/missing_minifigure.sql @@ -6,7 +6,7 @@ WHERE "bricktracker_sets"."id" IN ( "missing"."u_id" FROM "missing" - WHERE "missing"."set_num" IS NOT DISTINCT FROM :fig_num + WHERE "missing"."set_num" IS NOT DISTINCT FROM :figure GROUP BY "missing"."u_id" ) diff --git a/bricktracker/sql/set/list/using_minifigure.sql b/bricktracker/sql/set/list/using_minifigure.sql index f08a5d7..711866b 100644 --- a/bricktracker/sql/set/list/using_minifigure.sql +++ b/bricktracker/sql/set/list/using_minifigure.sql @@ -6,7 +6,7 @@ WHERE "bricktracker_sets"."id" IN ( "inventory"."u_id" FROM "inventory" - WHERE "inventory"."set_num" IS NOT DISTINCT FROM :fig_num + WHERE "inventory"."set_num" IS NOT DISTINCT FROM :figure GROUP BY "inventory"."u_id" ) diff --git a/bricktracker/sql/set/select/full.sql b/bricktracker/sql/set/select/full.sql index 4b19136..a89d2c9 100644 --- a/bricktracker/sql/set/select/full.sql +++ b/bricktracker/sql/set/select/full.sql @@ -5,7 +5,7 @@ WHERE "missing"."u_id" IS NOT DISTINCT FROM :id {% endblock %} {% block where_minifigures %} -WHERE "minifigures"."u_id" IS NOT DISTINCT FROM :id +WHERE "bricktracker_minifigures"."bricktracker_set_id" IS NOT DISTINCT FROM :id {% endblock %} {% block where %} diff --git a/bricktracker/sql_counter.py b/bricktracker/sql_counter.py index 7175494..d01546f 100644 --- a/bricktracker/sql_counter.py +++ b/bricktracker/sql_counter.py @@ -2,13 +2,15 @@ from typing import Tuple # Some table aliases to make it look cleaner (id: (name, icon)) ALIASES: dict[str, Tuple[str, str]] = { - 'bricktracker_set_checkboxes': ('Checkboxes', 'checkbox-line'), + 'bricktracker_minifigures': ('Bricktracker minifigures', 'group-line'), 'bricktracker_set_statuses': ('Bricktracker sets status', 'checkbox-line'), 'bricktracker_sets': ('Bricktracker sets', 'hashtag'), 'bricktracker_wishes': ('Bricktracker wishes', 'gift-line'), 'inventory': ('Parts', 'shapes-line'), 'minifigures': ('Minifigures', 'group-line'), + 'minifigures_old': ('Minifigures (legacy)', 'group-line'), 'missing': ('Missing', 'error-warning-line'), + 'rebrickable_minifigures': ('Rebrickable minifigures', 'group-line'), 'rebrickable_sets': ('Rebrickable sets', 'hashtag'), 'sets': ('Sets', 'hashtag'), 'sets_old': ('Sets (legacy)', 'hashtag'), diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py index 02353c1..1683175 100644 --- a/bricktracker/views/set.py +++ b/bricktracker/views/set.py @@ -128,7 +128,7 @@ def missing_minifigure_part(*, id: str, figure: str, part: str) -> Response: logger.info('Set {number} ({id}): updated minifigure ({figure}) part ({part}) missing count to {missing}'.format( # noqa: E501 number=brickset.fields.set, id=brickset.fields.id, - figure=brickminifigure.fields.fig_num, + figure=brickminifigure.fields.figure, part=brickpart.fields.id, missing=missing, )) diff --git a/templates/minifigure/card.html b/templates/minifigure/card.html index 79ed414..b1949ef 100644 --- a/templates/minifigure/card.html +++ b/templates/minifigure/card.html @@ -3,11 +3,11 @@ {% import 'macro/card.html' as card %}
- {{ card.header(item, item.fields.name, solo=solo, number=item.clean_number(), icon='user-line') }} - {{ card.image(item, solo=solo, last=last, caption=item.fields.name, alt=item.fields.fig_num, medium=true) }} + {{ card.header(item, item.fields.name, solo=solo, number=item.fields.number, icon='user-line') }} + {{ card.image(item, solo=solo, last=last, caption=item.fields.name, alt=item.fields.figure, medium=true) }}
{% if last %} - {{ badge.set(item.fields.set_num, solo=solo, last=last, id=item.fields.u_id) }} + {{ badge.set(item.fields.set, solo=solo, last=last, id=item.fields.rebrickable_set_id) }} {{ badge.quantity(item.fields.quantity, solo=solo, last=last) }} {% endif %} {{ badge.quantity(item.fields.total_quantity, solo=solo, last=last) }} @@ -19,7 +19,7 @@
{% if solo %}
- {{ accordion.table(item.generic_parts(), 'Parts', item.fields.fig_num, 'minifigure-details', 'part/table.html', icon='shapes-line', alt=item.fields.fig_num, read_only_missing=read_only_missing)}} + {{ accordion.table(item.generic_parts(), 'Parts', item.fields.figure, 'minifigure-details', 'part/table.html', icon='shapes-line', alt=item.fields.figure, read_only_missing=read_only_missing)}} {{ accordion.cards(using, 'Sets using this minifigure', 'using-inventory', 'minifigure-details', 'set/card.html', icon='hashtag') }} {{ accordion.cards(missing, 'Sets missing parts of this minifigure', 'missing-inventory', 'minifigure-details', 'set/card.html', icon='error-warning-line') }}
diff --git a/templates/minifigure/table.html b/templates/minifigure/table.html index 94ccef7..66ece79 100644 --- a/templates/minifigure/table.html +++ b/templates/minifigure/table.html @@ -6,7 +6,7 @@ {% for item in table_collection %} - {{ table.image(item.url_for_image(), caption=item.fields.name, alt=item.fields.fig_num) }} + {{ table.image(item.url_for_image(), caption=item.fields.name, alt=item.fields.figure) }} {{ item.fields.name }} {% if all %} diff --git a/templates/set/card.html b/templates/set/card.html index 1308c71..d6729ee 100644 --- a/templates/set/card.html +++ b/templates/set/card.html @@ -57,7 +57,7 @@ {{ accordion.footer() }} {{ accordion.table(item.parts(), 'Parts', 'parts-inventory', 'set-details', 'part/table.html', icon='shapes-line')}} {% for minifigure in item.minifigures() %} - {{ accordion.table(minifigure.parts(), minifigure.fields.name, minifigure.fields.fig_num, 'set-details', 'part/table.html', quantity=minifigure.fields.quantity, icon='group-line', image=minifigure.url_for_image(), alt=minifigure.fields.fig_num, details=minifigure.url())}} + {{ accordion.table(minifigure.parts(), minifigure.fields.name, minifigure.fields.figure, 'set-details', 'part/table.html', quantity=minifigure.fields.quantity, icon='group-line', image=minifigure.url_for_image(), alt=minifigure.fields.figure, details=minifigure.url())}} {% endfor %} {% endif %} {% if g.login.is_authenticated() %} From 32044dffe4cc8cd0760ef7c0ad6e8fc247fdcd58 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 27 Jan 2025 18:40:51 +0100 Subject: [PATCH 011/154] Remove confusing reference to number for sets --- bricktracker/views/set.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py index 1683175..3e2304c 100644 --- a/bricktracker/views/set.py +++ b/bricktracker/views/set.py @@ -47,8 +47,8 @@ def update_status(*, id: str, checkbox_id: str) -> Response: brickset.update_status(checkbox, value) # Info - logger.info('Set {number} ({id}): status "{status}" changed to "{state}"'.format( # noqa: E501 - number=brickset.fields.set, + logger.info('Set {set} ({id}): status "{status}" changed to "{state}"'.format( # noqa: E501 + set=brickset.fields.set, id=brickset.fields.id, status=checkbox.fields.name, state=value, @@ -77,8 +77,8 @@ def do_delete(*, id: str) -> Response: brickset.delete() # Info - logger.info('Set {number} ({id}): deleted'.format( - number=brickset.fields.set, + logger.info('Set {set} ({id}): deleted'.format( + set=brickset.fields.set, id=brickset.fields.id, )) @@ -125,8 +125,8 @@ def missing_minifigure_part(*, id: str, figure: str, part: str) -> Response: brickpart.update_missing(missing) # Info - logger.info('Set {number} ({id}): updated minifigure ({figure}) part ({part}) missing count to {missing}'.format( # noqa: E501 - number=brickset.fields.set, + logger.info('Set {set} ({id}): updated minifigure ({figure}) part ({part}) missing count to {missing}'.format( # noqa: E501 + set=brickset.fields.set, id=brickset.fields.id, figure=brickminifigure.fields.figure, part=brickpart.fields.id, @@ -149,8 +149,8 @@ def missing_part(*, id: str, part: str) -> Response: brickpart.update_missing(missing) # Info - logger.info('Set {number} ({id}): updated part ({part}) missing count to {missing}'.format( # noqa: E501 - number=brickset.fields.set, + logger.info('Set {set} ({id}): updated part ({part}) missing count to {missing}'.format( # noqa: E501 + set=brickset.fields.set, id=brickset.fields.id, part=brickpart.fields.id, missing=missing, From 26fd9aa3f9cc0f374f9cc3fd16f70d1bf09e45a5 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 27 Jan 2025 18:41:08 +0100 Subject: [PATCH 012/154] Fix hide instructions block placement --- templates/set/card.html | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/templates/set/card.html b/templates/set/card.html index d6729ee..e9612eb 100644 --- a/templates/set/card.html +++ b/templates/set/card.html @@ -35,26 +35,28 @@ {% endfor %} {% endif %} - {% if solo and not config['HIDE_SET_INSTRUCTIONS'] %} + {% if solo %}
{% if not delete %} - {{ accordion.header('Instructions', 'instructions', 'set-details', expanded=open_instructions, quantity=item.instructions | length, icon='file-line', class='p-0') }} -
- {% if item.instructions | length %} - {% for instruction in item.instructions %} - {{ instruction.filename }} - {% endfor %} - {% else %} - No instructions file found. - {% if g.login.is_authenticated() %} - Upload an instructions file + {% if not config['HIDE_SET_INSTRUCTIONS'] %} + {{ accordion.header('Instructions', 'instructions', 'set-details', expanded=open_instructions, quantity=item.instructions | length, icon='file-line', class='p-0') }} +
+ {% if item.instructions | length %} + {% for instruction in item.instructions %} + {{ instruction.filename }} + {% endfor %} + {% else %} + No instructions file found. + {% if g.login.is_authenticated() %} + Upload an instructions file + {% endif %} {% endif %} - {% endif %} - {% if g.login.is_authenticated() %} - Download instructions from Rebrickable - {% endif %} -
- {{ accordion.footer() }} + {% if g.login.is_authenticated() %} + Download instructions from Rebrickable + {% endif %} +
+ {{ accordion.footer() }} + {% endif %} {{ accordion.table(item.parts(), 'Parts', 'parts-inventory', 'set-details', 'part/table.html', icon='shapes-line')}} {% for minifigure in item.minifigures() %} {{ accordion.table(minifigure.parts(), minifigure.fields.name, minifigure.fields.figure, 'set-details', 'part/table.html', quantity=minifigure.fields.quantity, icon='group-line', image=minifigure.url_for_image(), alt=minifigure.fields.figure, details=minifigure.url())}} From 6dd42ed52d0341d140ddee29e6fba106aee5cb81 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 27 Jan 2025 18:41:44 +0100 Subject: [PATCH 013/154] Add missing checkboxes counter alias --- bricktracker/sql_counter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bricktracker/sql_counter.py b/bricktracker/sql_counter.py index d01546f..f2d1cc5 100644 --- a/bricktracker/sql_counter.py +++ b/bricktracker/sql_counter.py @@ -3,6 +3,7 @@ from typing import Tuple # Some table aliases to make it look cleaner (id: (name, icon)) ALIASES: dict[str, Tuple[str, str]] = { 'bricktracker_minifigures': ('Bricktracker minifigures', 'group-line'), + 'bricktracker_set_checkboxes': ('Bricktracker set checkboxes', 'checkbox-line'), # noqa: E501 'bricktracker_set_statuses': ('Bricktracker sets status', 'checkbox-line'), 'bricktracker_sets': ('Bricktracker sets', 'hashtag'), 'bricktracker_wishes': ('Bricktracker wishes', 'gift-line'), From 8b82594512011fb43a320d2f401da5d3774ee1e0 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 27 Jan 2025 18:41:53 +0100 Subject: [PATCH 014/154] Documentation about base SQL files --- .env.sample | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.sample b/.env.sample index 91caf76..04e84ee 100644 --- a/.env.sample +++ b/.env.sample @@ -2,7 +2,7 @@ # If set, it will append a direct ORDER BY to the SQL query # while listing objects. You can look at the structure of the SQLite database to # see the schema and the column names. Some fields are compound and not visible -# directly from the schema (joins). You can check the query in the */list.sql files +# directly from the schema (joins). You can check the query in the */list.sql and */base/*.sql files # in the source to see all column names. # The usual syntax for those variables is ""."" [ASC|DESC]. # For composite fields (CASE, SUM, COUNT) the syntax is , there is no
name. From 0a129209a505ad8f4d37fa89cba8a95591c13855 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 27 Jan 2025 18:55:26 +0100 Subject: [PATCH 015/154] Add remixicon in the libraries --- docs/development.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/development.md b/docs/development.md index 6799be0..8657590 100644 --- a/docs/development.md +++ b/docs/development.md @@ -16,6 +16,7 @@ It uses the following Python/pip packages: It also uses the following libraries and frameworks: - Boostrap (https://getbootstrap.com/) +- Remixicon (https://remixicon.com/) - `baguettebox` (https://github.com/feimosi/baguetteBox.js) - `tinysort` (https://github.com/Sjeiti/TinySort) - `sortable` (https://github.com/tofsjonas/sortable) From 2e36db4d3d34044529cef04e5a59eeaa28fb2812 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 27 Jan 2025 22:23:54 +0100 Subject: [PATCH 016/154] Allow more advanced migration action through a companion python file --- bricktracker/migrations/__init__.py | 0 bricktracker/sql.py | 28 +++++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 bricktracker/migrations/__init__.py diff --git a/bricktracker/migrations/__init__.py b/bricktracker/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bricktracker/sql.py b/bricktracker/sql.py index 07811d9..9e47d9a 100644 --- a/bricktracker/sql.py +++ b/bricktracker/sql.py @@ -1,3 +1,4 @@ +from importlib import import_module import logging import os import sqlite3 @@ -301,7 +302,32 @@ class BrickSQL(object): version=pending.version) ) - self.executescript(pending.get_query()) + # Load context from the migrations if it exists + # It looks for a file in migrations/ named after the SQL file + # and containing one function named migration_xxxx, also named + # after the SQL file, returning a context dict. + # + # For instance: + # - sql/migrations/0007.sql + # - migrations/0007.py + # - def migration_0007(BrickSQL) -> dict[str, Any] + try: + module = import_module( + '.migrations.{name}'.format( + name=pending.name + ), + package='bricktracker' + ) + + function = getattr(module, 'migration_{name}'.format( + name=pending.name + )) + + context: dict[str, Any] = function(self) + except Exception: + context: dict[str, Any] = {} + + self.executescript(pending.get_query(), **context) self.execute('schema/set_version', version=pending.version) # Tells whether the database needs upgrade From 270838a54952520861c1b7289eb100d1b4d29906 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 27 Jan 2025 23:07:10 +0100 Subject: [PATCH 017/154] Simplify fields name in the database --- bricktracker/migrations/0007.py | 27 ++++++++ bricktracker/minifigure_list.py | 2 +- bricktracker/rebrickable_minifigure.py | 5 +- bricktracker/sql/migrations/0007.sql | 63 +++++++++++++------ bricktracker/sql/migrations/0008.sql | 40 ++++++------ bricktracker/sql/migrations/0009.sql | 32 ++++++++++ bricktracker/sql/minifigure/base/base.sql | 2 +- bricktracker/sql/minifigure/insert.sql | 6 +- bricktracker/sql/minifigure/list/all.sql | 4 +- bricktracker/sql/minifigure/list/from_set.sql | 2 +- bricktracker/sql/minifigure/list/last.sql | 4 +- .../sql/minifigure/list/missing_part.sql | 2 +- .../sql/minifigure/select/generic.sql | 4 +- .../sql/minifigure/select/specific.sql | 2 +- bricktracker/sql/part/list/all.sql | 6 +- bricktracker/sql/part/list/missing.sql | 6 +- bricktracker/sql/part/select/generic.sql | 4 +- bricktracker/sql/set/base/base.sql | 2 +- bricktracker/sql/set/base/full.sql | 8 +-- bricktracker/sql/set/base/light.sql | 2 +- bricktracker/sql/set/delete/set.sql | 4 +- bricktracker/sql/set/insert.sql | 2 +- bricktracker/sql/set/list/generic.sql | 2 +- bricktracker/sql/set/select/full.sql | 2 +- bricktracker/sql/set/update/status.sql | 6 +- 25 files changed, 159 insertions(+), 80 deletions(-) create mode 100644 bricktracker/migrations/0007.py create mode 100644 bricktracker/sql/migrations/0009.sql diff --git a/bricktracker/migrations/0007.py b/bricktracker/migrations/0007.py new file mode 100644 index 0000000..fb5b723 --- /dev/null +++ b/bricktracker/migrations/0007.py @@ -0,0 +1,27 @@ +from typing import Any, TYPE_CHECKING + +if TYPE_CHECKING: + from ..sql import BrickSQL + + +# Grab the list of checkboxes to create a list of SQL columns +def migration_0007(self: 'BrickSQL') -> dict[str, Any]: + records = self.fetchall('checkbox/list') + + return { + 'sources': ', '.join([ + '"bricktracker_set_statuses_old"."status_{id}"'.format(id=record['id']) # noqa: E501 + for record + in records + ]), + 'targets': ', '.join([ + '"status_{id}"'.format(id=record['id']) + for record + in records + ]), + 'structure': ', '.join([ + '"status_{id}" BOOLEAN NOT NULL DEFAULT 0'.format(id=record['id']) + for record + in records + ]) + } diff --git a/bricktracker/minifigure_list.py b/bricktracker/minifigure_list.py index 81affa6..24b9933 100644 --- a/bricktracker/minifigure_list.py +++ b/bricktracker/minifigure_list.py @@ -134,7 +134,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): parameters: dict[str, Any] = super().sql_parameters() if self.brickset is not None: - parameters['bricktracker_set_id'] = self.brickset.fields.id + parameters['id'] = self.brickset.fields.id return parameters diff --git a/bricktracker/rebrickable_minifigure.py b/bricktracker/rebrickable_minifigure.py index 28d3d75..e296356 100644 --- a/bricktracker/rebrickable_minifigure.py +++ b/bricktracker/rebrickable_minifigure.py @@ -75,9 +75,8 @@ class RebrickableMinifigure(BrickRecord): parameters = super().sql_parameters() # Supplement from the brickset - if self.brickset is not None: - if 'bricktracker_set_id' not in parameters: - parameters['bricktracker_set_id'] = self.brickset.fields.id + if self.brickset is not None and 'id' not in parameters: + parameters['id'] = self.brickset.fields.id return parameters diff --git a/bricktracker/sql/migrations/0007.sql b/bricktracker/sql/migrations/0007.sql index 09830c4..036f12e 100644 --- a/bricktracker/sql/migrations/0007.sql +++ b/bricktracker/sql/migrations/0007.sql @@ -1,30 +1,53 @@ --- description: Creation of the deduplicated table of Rebrickable minifigures +-- description: Renaming various complicated field names to something simpler + +PRAGMA foreign_keys = ON; BEGIN TRANSACTION; --- Create a Rebrickable minifigures table: each unique minifigure imported from Rebrickable -CREATE TABLE "rebrickable_minifigures" ( - "figure" TEXT NOT NULL, - "number" INTEGER NOT NULL, - "name" TEXT NOT NULL, - "image" TEXT, - PRIMARY KEY("figure") +-- Rename sets table +ALTER TABLE "bricktracker_sets" RENAME TO "bricktracker_sets_old"; + +-- Re-Create a Bricktable set table with the simplified name +CREATE TABLE "bricktracker_sets" ( + "id" TEXT NOT NULL, + "set" TEXT NOT NULL, + PRIMARY KEY("id"), + FOREIGN KEY("set") REFERENCES "rebrickable_sets"("set") ); -- Insert existing sets into the new table -INSERT INTO "rebrickable_minifigures" ( - "figure", - "number", - "name", - "image" +INSERT INTO "bricktracker_sets" ( + "id", + "set" ) SELECT - "minifigures"."fig_num", - CAST(SUBSTR("minifigures"."fig_num", 5) AS INTEGER), - "minifigures"."name", - "minifigures"."set_img_url" -FROM "minifigures" -GROUP BY - "minifigures"."fig_num"; + "bricktracker_sets_old"."id", + "bricktracker_sets_old"."rebrickable_set" +FROM "bricktracker_sets_old"; + +-- Rename status table +ALTER TABLE "bricktracker_set_statuses" RENAME TO "bricktracker_set_statuses_old"; + +-- Re-create a table for the status of each checkbox +CREATE TABLE "bricktracker_set_statuses" ( + "id" TEXT NOT NULL, + {% if structure %}{{ structure }},{% endif %} + PRIMARY KEY("id"), + FOREIGN KEY("id") REFERENCES "bricktracker_sets"("id") +); + +-- Insert existing status into the new table +INSERT INTO "bricktracker_set_statuses" ( + {% if targets %}{{ targets }},{% endif %} + "id" +) +SELECT + {% if sources %}{{ sources }},{% endif %} + "bricktracker_set_statuses_old"."bricktracker_set_id" +FROM "bricktracker_set_statuses_old"; + +-- Delete the original tables +DROP TABLE "bricktracker_set_statuses_old"; +DROP TABLE "bricktracker_sets_old"; COMMIT; \ No newline at end of file diff --git a/bricktracker/sql/migrations/0008.sql b/bricktracker/sql/migrations/0008.sql index 48b905a..09830c4 100644 --- a/bricktracker/sql/migrations/0008.sql +++ b/bricktracker/sql/migrations/0008.sql @@ -1,32 +1,30 @@ --- description: Migrate the Bricktracker minifigures - -PRAGMA foreign_keys = ON; +-- description: Creation of the deduplicated table of Rebrickable minifigures BEGIN TRANSACTION; --- Create a Bricktable minifigures table: an amount of minifigures linked to a Bricktracker set -CREATE TABLE "bricktracker_minifigures" ( - "bricktracker_set_id" TEXT NOT NULL, - "rebrickable_figure" TEXT NOT NULL, - "quantity" INTEGER NOT NULL, - PRIMARY KEY("bricktracker_set_id", "rebrickable_figure"), - FOREIGN KEY("bricktracker_set_id") REFERENCES "bricktracker_sets"("id"), - FOREIGN KEY("rebrickable_figure") REFERENCES "rebrickable_minifigures"("figure") +-- Create a Rebrickable minifigures table: each unique minifigure imported from Rebrickable +CREATE TABLE "rebrickable_minifigures" ( + "figure" TEXT NOT NULL, + "number" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "image" TEXT, + PRIMARY KEY("figure") ); -- Insert existing sets into the new table -INSERT INTO "bricktracker_minifigures" ( - "bricktracker_set_id", - "rebrickable_figure", - "quantity" +INSERT INTO "rebrickable_minifigures" ( + "figure", + "number", + "name", + "image" ) SELECT - "minifigures"."u_id", "minifigures"."fig_num", - "minifigures"."quantity" -FROM "minifigures"; - --- Rename the original table (don't delete it yet?) -ALTER TABLE "minifigures" RENAME TO "minifigures_old"; + CAST(SUBSTR("minifigures"."fig_num", 5) AS INTEGER), + "minifigures"."name", + "minifigures"."set_img_url" +FROM "minifigures" +GROUP BY + "minifigures"."fig_num"; COMMIT; \ No newline at end of file diff --git a/bricktracker/sql/migrations/0009.sql b/bricktracker/sql/migrations/0009.sql new file mode 100644 index 0000000..135f95d --- /dev/null +++ b/bricktracker/sql/migrations/0009.sql @@ -0,0 +1,32 @@ +-- description: Migrate the Bricktracker minifigures + +PRAGMA foreign_keys = ON; + +BEGIN TRANSACTION; + +-- Create a Bricktable minifigures table: an amount of minifigures linked to a Bricktracker set +CREATE TABLE "bricktracker_minifigures" ( + "id" TEXT NOT NULL, + "figure" TEXT NOT NULL, + "quantity" INTEGER NOT NULL, + PRIMARY KEY("id", "figure"), + FOREIGN KEY("id") REFERENCES "bricktracker_sets"("id"), + FOREIGN KEY("figure") REFERENCES "rebrickable_minifigures"("figure") +); + +-- Insert existing sets into the new table +INSERT INTO "bricktracker_minifigures" ( + "id", + "figure", + "quantity" +) +SELECT + "minifigures"."u_id", + "minifigures"."fig_num", + "minifigures"."quantity" +FROM "minifigures"; + +-- Rename the original table (don't delete it yet?) +ALTER TABLE "minifigures" RENAME TO "minifigures_old"; + +COMMIT; \ No newline at end of file diff --git a/bricktracker/sql/minifigure/base/base.sql b/bricktracker/sql/minifigure/base/base.sql index bfaf10d..c580b38 100644 --- a/bricktracker/sql/minifigure/base/base.sql +++ b/bricktracker/sql/minifigure/base/base.sql @@ -17,7 +17,7 @@ SELECT FROM "bricktracker_minifigures" INNER JOIN "rebrickable_minifigures" -ON "bricktracker_minifigures"."rebrickable_figure" IS NOT DISTINCT FROM "rebrickable_minifigures"."figure" +ON "bricktracker_minifigures"."figure" IS NOT DISTINCT FROM "rebrickable_minifigures"."figure" {% block join %}{% endblock %} diff --git a/bricktracker/sql/minifigure/insert.sql b/bricktracker/sql/minifigure/insert.sql index cd7c413..0a2679e 100644 --- a/bricktracker/sql/minifigure/insert.sql +++ b/bricktracker/sql/minifigure/insert.sql @@ -1,9 +1,9 @@ INSERT INTO "bricktracker_minifigures" ( - "bricktracker_set_id", - "rebrickable_figure", + "id", + "figure", "quantity" ) VALUES ( - :bricktracker_set_id, + :id, :figure, :quantity ) diff --git a/bricktracker/sql/minifigure/list/all.sql b/bricktracker/sql/minifigure/list/all.sql index 82e61a2..498062d 100644 --- a/bricktracker/sql/minifigure/list/all.sql +++ b/bricktracker/sql/minifigure/list/all.sql @@ -9,7 +9,7 @@ SUM(IFNULL("bricktracker_minifigures"."quantity", 0)) AS "total_quantity", {% endblock %} {% block total_sets %} -COUNT("bricktracker_minifigures"."bricktracker_set_id") AS "total_sets" +COUNT("bricktracker_minifigures"."id") AS "total_sets" {% endblock %} {% block join %} @@ -24,7 +24,7 @@ LEFT JOIN ( "missing"."set_num", "missing"."u_id" ) missing_join -ON "bricktracker_minifigures"."bricktracker_set_id" IS NOT DISTINCT FROM "missing_join"."u_id" +ON "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "missing_join"."u_id" AND "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "missing_join"."set_num" {% endblock %} diff --git a/bricktracker/sql/minifigure/list/from_set.sql b/bricktracker/sql/minifigure/list/from_set.sql index 65b4e69..e22ee95 100644 --- a/bricktracker/sql/minifigure/list/from_set.sql +++ b/bricktracker/sql/minifigure/list/from_set.sql @@ -1,5 +1,5 @@ {% extends 'minifigure/base/base.sql' %} {% block where %} -WHERE "bricktracker_minifigures"."bricktracker_set_id" IS NOT DISTINCT FROM :bricktracker_set_id +WHERE "bricktracker_minifigures"."id" IS NOT DISTINCT FROM :id {% endblock %} diff --git a/bricktracker/sql/minifigure/list/last.sql b/bricktracker/sql/minifigure/list/last.sql index 266b7c0..cacc2f7 100644 --- a/bricktracker/sql/minifigure/list/last.sql +++ b/bricktracker/sql/minifigure/list/last.sql @@ -7,11 +7,11 @@ SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", {% block join %} LEFT JOIN "missing" ON "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "missing"."set_num" -AND "bricktracker_minifigures"."bricktracker_set_id" IS NOT DISTINCT FROM "missing"."u_id" +AND "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "missing"."u_id" {% endblock %} {% block group %} GROUP BY "rebrickable_minifigures"."figure", - "bricktracker_minifigures"."bricktracker_set_id" + "bricktracker_minifigures"."id" {% endblock %} diff --git a/bricktracker/sql/minifigure/list/missing_part.sql b/bricktracker/sql/minifigure/list/missing_part.sql index 660da6d..3fe4210 100644 --- a/bricktracker/sql/minifigure/list/missing_part.sql +++ b/bricktracker/sql/minifigure/list/missing_part.sql @@ -7,7 +7,7 @@ SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", {% block join %} LEFT JOIN "missing" ON "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "missing"."set_num" -AND "bricktracker_minifigures"."bricktracker_set_id" IS NOT DISTINCT FROM "missing"."u_id" +AND "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "missing"."u_id" {% endblock %} {% block where %} diff --git a/bricktracker/sql/minifigure/select/generic.sql b/bricktracker/sql/minifigure/select/generic.sql index 966c022..16dc56d 100644 --- a/bricktracker/sql/minifigure/select/generic.sql +++ b/bricktracker/sql/minifigure/select/generic.sql @@ -9,13 +9,13 @@ SUM(IFNULL("bricktracker_minifigures"."quantity", 0)) AS "total_quantity", {% endblock %} {% block total_sets %} -COUNT(DISTINCT "bricktracker_minifigures"."bricktracker_set_id") AS "total_sets" +COUNT(DISTINCT "bricktracker_minifigures"."id") AS "total_sets" {% endblock %} {% block join %} LEFT JOIN "missing" ON "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "missing"."set_num" -AND "bricktracker_minifigures"."bricktracker_set_id" IS NOT DISTINCT FROM "missing"."u_id" +AND "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "missing"."u_id" {% endblock %} {% block where %} diff --git a/bricktracker/sql/minifigure/select/specific.sql b/bricktracker/sql/minifigure/select/specific.sql index 479c9e5..00a66af 100644 --- a/bricktracker/sql/minifigure/select/specific.sql +++ b/bricktracker/sql/minifigure/select/specific.sql @@ -2,5 +2,5 @@ {% block where %} WHERE "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM :figure -AND "bricktracker_minifigures"."bricktracker_set_id" IS NOT DISTINCT FROM :bricktracker_set_id +AND "bricktracker_minifigures"."id" IS NOT DISTINCT FROM :id {% endblock %} diff --git a/bricktracker/sql/part/list/all.sql b/bricktracker/sql/part/list/all.sql index 9d73fcc..c5bbf69 100644 --- a/bricktracker/sql/part/list/all.sql +++ b/bricktracker/sql/part/list/all.sql @@ -9,7 +9,7 @@ SUM("inventory"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)) A {% endblock %} {% block total_sets %} -COUNT(DISTINCT "bricktracker_minifigures"."bricktracker_set_id") AS "total_sets", +COUNT(DISTINCT "bricktracker_minifigures"."id") AS "total_sets", {% endblock %} {% block total_minifigures %} @@ -26,8 +26,8 @@ AND "inventory"."element_id" IS NOT DISTINCT FROM "missing"."element_id" AND "inventory"."u_id" IS NOT DISTINCT FROM "missing"."u_id" LEFT JOIN "bricktracker_minifigures" -ON "inventory"."set_num" IS NOT DISTINCT FROM "bricktracker_minifigures"."rebrickable_figure" -AND "inventory"."u_id" IS NOT DISTINCT FROM "bricktracker_minifigures"."bricktracker_set_id" +ON "inventory"."set_num" IS NOT DISTINCT FROM "bricktracker_minifigures"."figure" +AND "inventory"."u_id" IS NOT DISTINCT FROM "bricktracker_minifigures"."id" {% endblock %} {% block group %} diff --git a/bricktracker/sql/part/list/missing.sql b/bricktracker/sql/part/list/missing.sql index fc64e25..8f17ae3 100644 --- a/bricktracker/sql/part/list/missing.sql +++ b/bricktracker/sql/part/list/missing.sql @@ -5,7 +5,7 @@ SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", {% endblock %} {% block total_sets %} -COUNT("inventory"."u_id") - COUNT("bricktracker_minifigures"."bricktracker_set_id") AS "total_sets", +COUNT("inventory"."u_id") - COUNT("bricktracker_minifigures"."id") AS "total_sets", {% endblock %} {% block total_minifigures %} @@ -22,8 +22,8 @@ AND "missing"."element_id" IS NOT DISTINCT FROM "inventory"."element_id" AND "missing"."u_id" IS NOT DISTINCT FROM "inventory"."u_id" LEFT JOIN "bricktracker_minifigures" -ON "inventory"."set_num" IS NOT DISTINCT FROM "bricktracker_minifigures"."rebrickable_figure" -AND "inventory"."u_id" IS NOT DISTINCT FROM "bricktracker_minifigures"."bricktracker_set_id" +ON "inventory"."set_num" IS NOT DISTINCT FROM "bricktracker_minifigures"."figure" +AND "inventory"."u_id" IS NOT DISTINCT FROM "bricktracker_minifigures"."id" {% endblock %} {% block group %} diff --git a/bricktracker/sql/part/select/generic.sql b/bricktracker/sql/part/select/generic.sql index 28b32a9..eb7e194 100644 --- a/bricktracker/sql/part/select/generic.sql +++ b/bricktracker/sql/part/select/generic.sql @@ -22,8 +22,8 @@ AND "inventory"."element_id" IS NOT DISTINCT FROM "missing"."element_id" AND "inventory"."u_id" IS NOT DISTINCT FROM "missing"."u_id" LEFT JOIN "bricktracker_minifigures" -ON "inventory"."set_num" IS NOT DISTINCT FROM "bricktracker_minifigures"."rebrickable_figure" -AND "inventory"."u_id" IS NOT DISTINCT FROM "bricktracker_minifigures"."bricktracker_set_id" +ON "inventory"."set_num" IS NOT DISTINCT FROM "bricktracker_minifigures"."figure" +AND "inventory"."u_id" IS NOT DISTINCT FROM "bricktracker_minifigures"."id" {% endblock %} {% block where %} diff --git a/bricktracker/sql/set/base/base.sql b/bricktracker/sql/set/base/base.sql index 2f4d683..8b1f4c8 100644 --- a/bricktracker/sql/set/base/base.sql +++ b/bricktracker/sql/set/base/base.sql @@ -21,7 +21,7 @@ SELECT FROM "bricktracker_sets" INNER JOIN "rebrickable_sets" -ON "bricktracker_sets"."rebrickable_set" IS NOT DISTINCT FROM "rebrickable_sets"."set" +ON "bricktracker_sets"."set" IS NOT DISTINCT FROM "rebrickable_sets"."set" {% block join %}{% endblock %} diff --git a/bricktracker/sql/set/base/full.sql b/bricktracker/sql/set/base/full.sql index 68333c2..092e487 100644 --- a/bricktracker/sql/set/base/full.sql +++ b/bricktracker/sql/set/base/full.sql @@ -15,7 +15,7 @@ IFNULL("minifigures_join"."total", 0) AS "total_minifigures" {% block join %} {% if statuses %} LEFT JOIN "bricktracker_set_statuses" -ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_statuses"."bricktracker_set_id" +ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_statuses"."id" {% endif %} -- LEFT JOIN + SELECT to avoid messing the total @@ -32,11 +32,11 @@ ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "missing_join"."u_id" -- LEFT JOIN + SELECT to avoid messing the total LEFT JOIN ( SELECT - "bricktracker_minifigures"."bricktracker_set_id", + "bricktracker_minifigures"."id", SUM("bricktracker_minifigures"."quantity") AS "total" FROM "bricktracker_minifigures" {% block where_minifigures %}{% endblock %} - GROUP BY "bricktracker_minifigures"."bricktracker_set_id" + GROUP BY "bricktracker_minifigures"."id" ) "minifigures_join" -ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "minifigures_join"."bricktracker_set_id" +ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "minifigures_join"."id" {% endblock %} \ No newline at end of file diff --git a/bricktracker/sql/set/base/light.sql b/bricktracker/sql/set/base/light.sql index b599a87..12df8e2 100644 --- a/bricktracker/sql/set/base/light.sql +++ b/bricktracker/sql/set/base/light.sql @@ -1,6 +1,6 @@ SELECT "bricktracker_sets"."id", - "bricktracker_sets"."rebrickable_set" AS "set" + "bricktracker_sets"."set" FROM "bricktracker_sets" {% block join %}{% endblock %} diff --git a/bricktracker/sql/set/delete/set.sql b/bricktracker/sql/set/delete/set.sql index 93a51df..b477f81 100644 --- a/bricktracker/sql/set/delete/set.sql +++ b/bricktracker/sql/set/delete/set.sql @@ -7,10 +7,10 @@ DELETE FROM "bricktracker_sets" WHERE "bricktracker_sets"."id" IS NOT DISTINCT FROM '{{ id }}'; DELETE FROM "bricktracker_set_statuses" -WHERE "bricktracker_set_statuses"."bricktracker_set_id" IS NOT DISTINCT FROM '{{ id }}'; +WHERE "bricktracker_set_statuses"."id" IS NOT DISTINCT FROM '{{ id }}'; DELETE FROM "bricktracker_minifigures" -WHERE "bricktracker_minifigures"."bricktracker_set_id" IS NOT DISTINCT FROM '{{ id }}'; +WHERE "bricktracker_minifigures"."id" IS NOT DISTINCT FROM '{{ id }}'; DELETE FROM "missing" WHERE "missing"."u_id" IS NOT DISTINCT FROM '{{ id }}'; diff --git a/bricktracker/sql/set/insert.sql b/bricktracker/sql/set/insert.sql index 2462ac5..7dd6dec 100644 --- a/bricktracker/sql/set/insert.sql +++ b/bricktracker/sql/set/insert.sql @@ -1,6 +1,6 @@ INSERT OR IGNORE INTO "bricktracker_sets" ( "id", - "rebrickable_set" + "set" ) VALUES ( :id, :set diff --git a/bricktracker/sql/set/list/generic.sql b/bricktracker/sql/set/list/generic.sql index 0177c2b..d5b2da4 100644 --- a/bricktracker/sql/set/list/generic.sql +++ b/bricktracker/sql/set/list/generic.sql @@ -2,5 +2,5 @@ {% block group %} GROUP BY - "bricktracker_sets"."rebrickable_set" + "bricktracker_sets"."set" {% endblock %} diff --git a/bricktracker/sql/set/select/full.sql b/bricktracker/sql/set/select/full.sql index a89d2c9..80d1161 100644 --- a/bricktracker/sql/set/select/full.sql +++ b/bricktracker/sql/set/select/full.sql @@ -5,7 +5,7 @@ WHERE "missing"."u_id" IS NOT DISTINCT FROM :id {% endblock %} {% block where_minifigures %} -WHERE "bricktracker_minifigures"."bricktracker_set_id" IS NOT DISTINCT FROM :id +WHERE "bricktracker_minifigures"."id" IS NOT DISTINCT FROM :id {% endblock %} {% block where %} diff --git a/bricktracker/sql/set/update/status.sql b/bricktracker/sql/set/update/status.sql index d72616e..4fc78e4 100644 --- a/bricktracker/sql/set/update/status.sql +++ b/bricktracker/sql/set/update/status.sql @@ -1,10 +1,10 @@ INSERT INTO "bricktracker_set_statuses" ( - "bricktracker_set_id", + "id", "{{name}}" ) VALUES ( :id, :status ) -ON CONFLICT("bricktracker_set_id") +ON CONFLICT("id") DO UPDATE SET "{{name}}" = :status -WHERE "bricktracker_set_statuses"."bricktracker_set_id" IS NOT DISTINCT FROM :id +WHERE "bricktracker_set_statuses"."id" IS NOT DISTINCT FROM :id From 420ff7af7ad705e8305ecee61a7199dc4aba89ab Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 27 Jan 2025 23:13:42 +0100 Subject: [PATCH 018/154] Properly use the _listener variables as expected, and allow Enter key to execute the action --- static/scripts/socket/instructions.js | 21 ++++++++------ static/scripts/socket/set.js | 42 +++++++++++++++++---------- 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/static/scripts/socket/instructions.js b/static/scripts/socket/instructions.js index 271c0c3..fb19224 100644 --- a/static/scripts/socket/instructions.js +++ b/static/scripts/socket/instructions.js @@ -11,15 +11,9 @@ class BrickInstructionsSocket extends BrickSocket { this.html_files = document.getElementById(`${id}-files`); if (this.html_button) { - this.download_listener = ((bricksocket) => (e) => { - if (!bricksocket.disabled && bricksocket.socket !== undefined && bricksocket.socket.connected) { - bricksocket.toggle(false); - - bricksocket.download_instructions(); - } - })(this); - - this.html_button.addEventListener("click", this.download_listener); + this.download_listener = this.html_button.addEventListener("click", ((bricksocket) => (e) => { + bricksocket.execute(); + })(this)); } if (this.html_card_dismiss && this.html_card) { @@ -43,6 +37,15 @@ class BrickInstructionsSocket extends BrickSocket { this.download_instructions(true); } + // Execute the action + execute() { + if (!this.disabled && this.socket !== undefined && this.socket.connected) { + this.toggle(false); + + this.download_instructions(); + } + } + // Get the list of checkboxes describing files get_files(checked=false) { let files = []; diff --git a/static/scripts/socket/set.js b/static/scripts/socket/set.js index 60a2244..41056b8 100644 --- a/static/scripts/socket/set.js +++ b/static/scripts/socket/set.js @@ -5,6 +5,7 @@ class BrickSetSocket extends BrickSocket { // Listeners this.add_listener = undefined; + this.input_listener = undefined; this.confirm_listener = undefined; // Form elements (built based on the initial id) @@ -23,24 +24,15 @@ class BrickSetSocket extends BrickSocket { this.html_card_dismiss = document.getElementById(`${id}-card-dismiss`); if (this.html_button) { - this.add_listener = ((bricksocket) => (e) => { - if (!bricksocket.disabled && bricksocket.socket !== undefined && bricksocket.socket.connected) { - bricksocket.toggle(false); + this.add_listener = this.html_button.addEventListener("click", ((bricksocket) => (e) => { + bricksocket.execute(); + })(this)); - // Split and save the list if bulk - if (bricksocket.bulk) { - bricksocket.read_set_list() - } - - if (bricksocket.bulk || (bricksocket.html_no_confim && bricksocket.html_no_confim.checked)) { - bricksocket.import_set(true); - } else { - bricksocket.load_set(); - } + this.input_listener = this.html_input.addEventListener("keyup", ((bricksocket) => (e) => { + if (e.key === 'Enter') { + bricksocket.execute(); } - })(this); - - this.html_button.addEventListener("click", this.add_listener); + })(this)) } if (this.html_card_dismiss && this.html_card) { @@ -80,6 +72,24 @@ class BrickSetSocket extends BrickSocket { } } + // Execute the action + execute() { + if (!this.disabled && this.socket !== undefined && this.socket.connected) { + this.toggle(false); + + // Split and save the list if bulk + if (this.bulk) { + this.read_set_list(); + } + + if (this.bulk || (this.html_no_confim && this.html_no_confim.checked)) { + this.import_set(true); + } else { + this.load_set(); + } + } + } + // Upon receiving a fail message fail(data) { super.fail(data); From 9878f426b129cb908a845664457879a34ce634ef Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 27 Jan 2025 23:24:16 +0100 Subject: [PATCH 019/154] Update versions and changelog --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ bricktracker/version.py | 4 ++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b87c902..8bdc3b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## Unreleased + +## Code + +- General cleanup + +- Minifigure + - Deduplicate + +- Socket + - Add decorator for rebrickable, authenticated and threaded socket actions + +- SQL + - Allow for advanced migration scenarios through companion python files + +### UI + +- Add + - Allow adding or bulk adding by pressing Enter in the input field + +- Admin + - Grey out legacy tables in the database view + +- Sets + - Add a flag to hide instructions in a set + + ## 1.1.1: PDF Instructions Download ### Instructions diff --git a/bricktracker/version.py b/bricktracker/version.py index b055c6b..4424778 100644 --- a/bricktracker/version.py +++ b/bricktracker/version.py @@ -1,4 +1,4 @@ from typing import Final -__version__: Final[str] = '1.1.1' -__database_version__: Final[int] = 6 +__version__: Final[str] = '1.2.0' +__database_version__: Final[int] = 9 From 711c020c27fb27fa7261dd05dad4289ce059c034 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Tue, 28 Jan 2025 10:49:16 +0100 Subject: [PATCH 020/154] Add extra fields to set for the future while we are refactoring it --- bricktracker/sql/migrations/0007.sql | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/bricktracker/sql/migrations/0007.sql b/bricktracker/sql/migrations/0007.sql index 036f12e..a75f3f2 100644 --- a/bricktracker/sql/migrations/0007.sql +++ b/bricktracker/sql/migrations/0007.sql @@ -1,4 +1,4 @@ --- description: Renaming various complicated field names to something simpler +-- description: Renaming various complicated field names to something simpler, and add a bunch of extra fields for later PRAGMA foreign_keys = ON; @@ -7,12 +7,25 @@ BEGIN TRANSACTION; -- Rename sets table ALTER TABLE "bricktracker_sets" RENAME TO "bricktracker_sets_old"; --- Re-Create a Bricktable set table with the simplified name +-- Create a Bricktracker set storage table for later +CREATE TABLE "bricktracker_set_storages" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + PRIMARY KEY("id") +); + +-- Re-Create a Bricktracker set table with the simplified name CREATE TABLE "bricktracker_sets" ( "id" TEXT NOT NULL, "set" TEXT NOT NULL, + "description" TEXT, + "theme" TEXT, -- Custom theme name + "storage" TEXT, -- Storage bin location + "purchase_date" INTEGER, -- Purchase data + "purchase_price" REAL, -- Purchase price PRIMARY KEY("id"), - FOREIGN KEY("set") REFERENCES "rebrickable_sets"("set") + FOREIGN KEY("set") REFERENCES "rebrickable_sets"("set"), + FOREIGN KEY("storage") REFERENCES "bricktracker_set_storages"("id") ); -- Insert existing sets into the new table From d5f66151b94ceea87711b182c11cb8cd71def3c0 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Tue, 28 Jan 2025 10:49:47 +0100 Subject: [PATCH 021/154] Documentation touch up --- bricktracker/sql/migrations/0004.sql | 2 +- bricktracker/sql/migrations/0009.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bricktracker/sql/migrations/0004.sql b/bricktracker/sql/migrations/0004.sql index 3828204..ac7aa37 100644 --- a/bricktracker/sql/migrations/0004.sql +++ b/bricktracker/sql/migrations/0004.sql @@ -4,7 +4,7 @@ PRAGMA foreign_keys = ON; BEGIN TRANSACTION; --- Create a Bricktable set table: with their unique IDs, and a reference to the Rebrickable set +-- Create a Bricktracker set table: with their unique IDs, and a reference to the Rebrickable set CREATE TABLE "bricktracker_sets" ( "id" TEXT NOT NULL, "rebrickable_set" TEXT NOT NULL, diff --git a/bricktracker/sql/migrations/0009.sql b/bricktracker/sql/migrations/0009.sql index 135f95d..679a761 100644 --- a/bricktracker/sql/migrations/0009.sql +++ b/bricktracker/sql/migrations/0009.sql @@ -4,7 +4,7 @@ PRAGMA foreign_keys = ON; BEGIN TRANSACTION; --- Create a Bricktable minifigures table: an amount of minifigures linked to a Bricktracker set +-- Create a Bricktracker minifigures table: an amount of minifigures linked to a Bricktracker set CREATE TABLE "bricktracker_minifigures" ( "id" TEXT NOT NULL, "figure" TEXT NOT NULL, From 50e5981c58cc40a48f9919c3f90d08b50134b833 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Tue, 28 Jan 2025 11:00:39 +0100 Subject: [PATCH 022/154] Cosmetics --- templates/admin/checkbox.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/admin/checkbox.html b/templates/admin/checkbox.html index b71cc2a..dbe5360 100644 --- a/templates/admin/checkbox.html +++ b/templates/admin/checkbox.html @@ -22,7 +22,7 @@ data-changer-id="{{ checkbox.fields.id }}" data-changer-prefix="grid" data-changer-url="{{ url_for('admin_checkbox.update_status', id=checkbox.fields.id, name='displayed_on_grid')}}" {% if checkbox.fields.displayed_on_grid %}checked{% endif %} autocomplete="off"> @@ -49,7 +49,7 @@
From 964dd9070417ca8bf1d483f5368d7fc50b849c7c Mon Sep 17 00:00:00 2001 From: Gregoo Date: Tue, 28 Jan 2025 14:09:14 +0100 Subject: [PATCH 023/154] Remove unused socket --- bricktracker/rebrickable_minifigure.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/bricktracker/rebrickable_minifigure.py b/bricktracker/rebrickable_minifigure.py index e296356..af2c926 100644 --- a/bricktracker/rebrickable_minifigure.py +++ b/bricktracker/rebrickable_minifigure.py @@ -9,14 +9,12 @@ from .rebrickable_image import RebrickableImage from .record import BrickRecord if TYPE_CHECKING: from .set import BrickSet - from .socket import BrickSocket logger = logging.getLogger(__name__) # A minifigure from Rebrickable class RebrickableMinifigure(BrickRecord): - socket: 'BrickSocket' brickset: 'BrickSet | None' # Queries @@ -28,7 +26,6 @@ class RebrickableMinifigure(BrickRecord): /, *, brickset: 'BrickSet | None' = None, - socket: 'BrickSocket | None' = None, record: Row | dict[str, Any] | None = None ): super().__init__() @@ -39,10 +36,6 @@ class RebrickableMinifigure(BrickRecord): # Save the brickset self.brickset = brickset - # Save the socket - if socket is not None: - self.socket = socket - # Ingest the record if it has one if record is not None: self.ingest(record) From 7ff1605c21474239cfa05a7cb48b1785f7be4a97 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Tue, 28 Jan 2025 14:19:26 +0100 Subject: [PATCH 024/154] Garbage leftover from copy-paste --- bricktracker/rebrickable_minifigure.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/bricktracker/rebrickable_minifigure.py b/bricktracker/rebrickable_minifigure.py index af2c926..973b9fb 100644 --- a/bricktracker/rebrickable_minifigure.py +++ b/bricktracker/rebrickable_minifigure.py @@ -30,9 +30,6 @@ class RebrickableMinifigure(BrickRecord): ): super().__init__() - # Placeholders - self.instructions = [] - # Save the brickset self.brickset = brickset From c4bb3c7607cd8910632438bdbf40f7e0a4d669bf Mon Sep 17 00:00:00 2001 From: Gregoo Date: Tue, 28 Jan 2025 19:18:51 +0100 Subject: [PATCH 025/154] Deduplicated parts and missing parts --- .env.sample | 16 +- bricktracker/config.py | 6 +- bricktracker/minifigure.py | 17 +- bricktracker/minifigure_list.py | 33 +- bricktracker/part.py | 319 ++++++------------ bricktracker/part_list.py | 86 ++++- bricktracker/rebrickable_image.py | 14 +- bricktracker/rebrickable_part.py | 203 +++++++++++ bricktracker/rebrickable_parts.py | 113 ------- bricktracker/set.py | 17 +- bricktracker/set_list.py | 26 +- bricktracker/sql/migrations/0010.sql | 42 +++ bricktracker/sql/migrations/0011.sql | 60 ++++ bricktracker/sql/minifigure/base/base.sql | 1 - bricktracker/sql/minifigure/list/all.sql | 17 +- bricktracker/sql/minifigure/list/last.sql | 8 +- .../sql/minifigure/list/missing_part.sql | 24 +- .../sql/minifigure/list/using_part.sql | 15 +- .../sql/minifigure/select/generic.sql | 15 +- .../sql/minifigure/select/specific.sql | 4 +- bricktracker/sql/missing/delete/from_set.sql | 4 - bricktracker/sql/missing/insert.sql | 20 -- bricktracker/sql/missing/update/from_set.sql | 5 - bricktracker/sql/part/base/base.sql | 56 +++ bricktracker/sql/part/base/select.sql | 43 --- bricktracker/sql/part/insert.sql | 34 +- bricktracker/sql/part/list/all.sql | 28 +- .../sql/part/list/from_minifigure.sql | 23 +- bricktracker/sql/part/list/from_set.sql | 21 -- bricktracker/sql/part/list/missing.sql | 30 +- bricktracker/sql/part/list/specific.sql | 11 + bricktracker/sql/part/select/generic.sql | 30 +- bricktracker/sql/part/select/specific.sql | 28 +- bricktracker/sql/part/update/missing.sql | 7 + bricktracker/sql/rebrickable/part/insert.sql | 25 ++ bricktracker/sql/rebrickable/part/list.sql | 13 + bricktracker/sql/rebrickable/part/select.sql | 16 + bricktracker/sql/schema/drop.sql | 5 + bricktracker/sql/set/base/full.sql | 10 +- bricktracker/sql/set/delete/set.sql | 7 +- .../sql/set/list/missing_minifigure.sql | 12 +- bricktracker/sql/set/list/missing_part.sql | 15 +- .../sql/set/list/using_minifigure.sql | 11 +- bricktracker/sql/set/list/using_part.sql | 14 +- bricktracker/sql/set/select/full.sql | 2 +- bricktracker/sql_counter.py | 7 +- bricktracker/version.py | 2 +- bricktracker/views/part.py | 27 +- bricktracker/views/set.py | 59 ++-- templates/macro/card.html | 4 +- templates/minifigure/card.html | 2 +- templates/part/card.html | 4 +- templates/part/table.html | 10 +- templates/set/card.html | 4 +- templates/set/mini.html | 2 +- 55 files changed, 871 insertions(+), 756 deletions(-) create mode 100644 bricktracker/rebrickable_part.py delete mode 100644 bricktracker/rebrickable_parts.py create mode 100644 bricktracker/sql/migrations/0010.sql create mode 100644 bricktracker/sql/migrations/0011.sql delete mode 100644 bricktracker/sql/missing/delete/from_set.sql delete mode 100644 bricktracker/sql/missing/insert.sql delete mode 100644 bricktracker/sql/missing/update/from_set.sql create mode 100644 bricktracker/sql/part/base/base.sql delete mode 100644 bricktracker/sql/part/base/select.sql delete mode 100644 bricktracker/sql/part/list/from_set.sql create mode 100644 bricktracker/sql/part/list/specific.sql create mode 100644 bricktracker/sql/part/update/missing.sql create mode 100644 bricktracker/sql/rebrickable/part/insert.sql create mode 100644 bricktracker/sql/rebrickable/part/list.sql create mode 100644 bricktracker/sql/rebrickable/part/select.sql diff --git a/.env.sample b/.env.sample index 04e84ee..46b2131 100644 --- a/.env.sample +++ b/.env.sample @@ -28,7 +28,7 @@ # BK_AUTHENTICATION_KEY=change-this-to-something-random # Optional: Pattern of the link to Bricklink for a part. Will be passed to Python .format() -# Default: https://www.bricklink.com/v2/catalog/catalogitem.page?P={number} +# Default: https://www.bricklink.com/v2/catalog/catalogitem.page?P={part} # BK_BRICKLINK_LINK_PART_PATTERN= # Optional: Display Bricklink links wherever applicable @@ -139,13 +139,13 @@ # Optional: Change the default order of parts. By default ordered by insertion order. # Useful column names for this option are: -# - "inventory"."part_num": part number -# - "inventory"."name": part name -# - "inventory"."color_name": part color name -# - "inventory"."is_spare": par is a spare part +# - "bricktracker_parts"."part": part number +# - "bricktracker_parts"."spare": part is a spare part +# - "rebrickable_parts"."name": part name +# - "rebrickable_parts"."color_name": part color name # - "total_missing": number of missing parts -# Default: "inventory"."name" ASC, "inventory"."color_name" ASC, "inventory"."is_spare" ASC -# BK_PARTS_DEFAULT_ORDER="total_missing" DESC, "inventory"."name" ASC +# Default: "rebrickable_parts"."name" ASC, "rebrickable_parts"."color_name" ASC, "bricktracker_parts"."spare" ASC +# BK_PARTS_DEFAULT_ORDER="total_missing" DESC, "rebrickable_parts"."name"."name" ASC # Optional: Folder where to store the parts images, relative to the '/app/static/' folder # Default: parts @@ -180,7 +180,7 @@ # BK_REBRICKABLE_LINK_MINIFIGURE_PATTERN= # Optional: Pattern of the link to Rebrickable for a part. Will be passed to Python .format() -# Default: https://rebrickable.com/parts/{number}/_/{color} +# Default: https://rebrickable.com/parts/{part}/_/{color} # BK_REBRICKABLE_LINK_PART_PATTERN= # Optional: Pattern of the link to Rebrickable for instructions. Will be passed to Python .format() diff --git a/bricktracker/config.py b/bricktracker/config.py index 236eb54..83cd99a 100644 --- a/bricktracker/config.py +++ b/bricktracker/config.py @@ -10,7 +10,7 @@ from typing import Any, Final CONFIG: Final[list[dict[str, Any]]] = [ {'n': 'AUTHENTICATION_PASSWORD', 'd': ''}, {'n': 'AUTHENTICATION_KEY', 'd': ''}, - {'n': 'BRICKLINK_LINK_PART_PATTERN', 'd': 'https://www.bricklink.com/v2/catalog/catalogitem.page?P={number}'}, # noqa: E501 + {'n': 'BRICKLINK_LINK_PART_PATTERN', 'd': 'https://www.bricklink.com/v2/catalog/catalogitem.page?P={part}'}, # noqa: E501 {'n': 'BRICKLINK_LINKS', 'c': bool}, {'n': 'DATABASE_PATH', 'd': './app.db'}, {'n': 'DATABASE_TIMESTAMP_FORMAT', 'd': '%Y-%m-%d-%H-%M-%S'}, @@ -35,7 +35,7 @@ CONFIG: Final[list[dict[str, Any]]] = [ {'n': 'MINIFIGURES_DEFAULT_ORDER', 'd': '"rebrickable_minifigures"."name" ASC'}, # noqa: E501 {'n': 'MINIFIGURES_FOLDER', 'd': 'minifigs', 's': True}, {'n': 'NO_THREADED_SOCKET', 'c': bool}, - {'n': 'PARTS_DEFAULT_ORDER', 'd': '"inventory"."name" ASC, "inventory"."color_name" ASC, "inventory"."is_spare" ASC'}, # noqa: E501 + {'n': 'PARTS_DEFAULT_ORDER', 'd': '"rebrickable_parts"."name" ASC, "rebrickable_parts"."color_name" ASC, "bricktracker_parts"."spare" ASC'}, # noqa: E501 {'n': 'PARTS_FOLDER', 'd': 'parts', 's': True}, {'n': 'PORT', 'd': 3333, 'c': int}, {'n': 'RANDOM', 'e': 'RANDOM', 'c': bool}, @@ -43,7 +43,7 @@ CONFIG: Final[list[dict[str, Any]]] = [ {'n': 'REBRICKABLE_IMAGE_NIL', 'd': 'https://rebrickable.com/static/img/nil.png'}, # noqa: E501 {'n': 'REBRICKABLE_IMAGE_NIL_MINIFIGURE', 'd': 'https://rebrickable.com/static/img/nil_mf.jpg'}, # noqa: E501 {'n': 'REBRICKABLE_LINK_MINIFIGURE_PATTERN', 'd': 'https://rebrickable.com/minifigs/{figure}'}, # noqa: E501 - {'n': 'REBRICKABLE_LINK_PART_PATTERN', 'd': 'https://rebrickable.com/parts/{number}/_/{color}'}, # noqa: E501 + {'n': 'REBRICKABLE_LINK_PART_PATTERN', 'd': 'https://rebrickable.com/parts/{part}/_/{color}'}, # noqa: E501 {'n': 'REBRICKABLE_LINK_INSTRUCTIONS_PATTERN', 'd': 'https://rebrickable.com/instructions/{path}'}, # noqa: E501 {'n': 'REBRICKABLE_USER_AGENT', 'd': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'}, # noqa: E501 {'n': 'REBRICKABLE_LINKS', 'e': 'LINKS', 'c': bool}, diff --git a/bricktracker/minifigure.py b/bricktracker/minifigure.py index 76a482e..e0318a0 100644 --- a/bricktracker/minifigure.py +++ b/bricktracker/minifigure.py @@ -4,7 +4,6 @@ from typing import Self, TYPE_CHECKING from .exceptions import ErrorException, NotFoundException from .part_list import BrickPartList -from .rebrickable_parts import RebrickableParts from .rebrickable_minifigure import RebrickableMinifigure if TYPE_CHECKING: from .set import BrickSet @@ -20,7 +19,8 @@ class BrickMinifigure(RebrickableMinifigure): generic_query: str = 'minifigure/select/generic' select_query: str = 'minifigure/select/specific' - def download(self, socket: 'BrickSocket'): + # Import a minifigure into the database + def download(self, socket: 'BrickSocket') -> bool: if self.brickset is None: raise ErrorException('Importing a minifigure from Rebrickable outside of a set is not supported') # noqa: E501 @@ -40,11 +40,12 @@ class BrickMinifigure(RebrickableMinifigure): self.insert_rebrickable() # Load the inventory - RebrickableParts( + if not BrickPartList.download( socket, self.brickset, - minifigure=self, - ).download() + minifigure=self + ): + return False except Exception as e: socket.fail( @@ -57,6 +58,10 @@ class BrickMinifigure(RebrickableMinifigure): logger.debug(traceback.format_exc()) + return False + + return True + # Parts def generic_parts(self, /) -> BrickPartList: return BrickPartList().from_minifigure(self) @@ -68,7 +73,7 @@ class BrickMinifigure(RebrickableMinifigure): figure=self.fields.figure, )) - return BrickPartList().load(self.brickset, minifigure=self) + return BrickPartList().list_specific(self.brickset, minifigure=self) # Select a generic minifigure def select_generic(self, figure: str, /) -> Self: diff --git a/bricktracker/minifigure_list.py b/bricktracker/minifigure_list.py index 24b9933..a59fee5 100644 --- a/bricktracker/minifigure_list.py +++ b/bricktracker/minifigure_list.py @@ -82,16 +82,13 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): # Minifigures missing a part def missing_part( self, - part_num: str, - color_id: int, + part: str, + color: int, /, - *, - element_id: int | None = None, ) -> Self: # Save the parameters to the fields - self.fields.part_num = part_num - self.fields.color_id = color_id - self.fields.element_id = element_id + self.fields.part = part + self.fields.color = color # Load the minifigures from the database for record in self.select( @@ -107,16 +104,13 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): # Minifigure using a part def using_part( self, - part_num: str, - color_id: int, + part: str, + color: int, /, - *, - element_id: int | None = None, ) -> Self: # Save the parameters to the fields - self.fields.part_num = part_num - self.fields.color_id = color_id - self.fields.element_id = element_id + self.fields.part = part + self.fields.color = color # Load the minifigures from the database for record in self.select( @@ -140,7 +134,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): # Import the minifigures from Rebrickable @staticmethod - def download(socket: 'BrickSocket', brickset: 'BrickSet', /) -> None: + def download(socket: 'BrickSocket', brickset: 'BrickSet', /) -> bool: try: socket.auto_progress( message='Set {set}: loading minifigures from Rebrickable'.format( # noqa: E501 @@ -162,10 +156,11 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): ).list() # Process each minifigure - socket.update_total(len(minifigures), add=True) - for minifigure in minifigures: - minifigure.download(socket) + if not minifigure.download(socket): + return False + + return True except Exception as e: socket.fail( @@ -176,3 +171,5 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): ) logger.debug(traceback.format_exc()) + + return False diff --git a/bricktracker/part.py b/bricktracker/part.py index b6dc153..7e82c45 100644 --- a/bricktracker/part.py +++ b/bricktracker/part.py @@ -1,103 +1,104 @@ -import os -from sqlite3 import Row +import logging from typing import Any, Self, TYPE_CHECKING -from urllib.parse import urlparse +import traceback -from flask import current_app, url_for - -from .exceptions import DatabaseException, ErrorException, NotFoundException -from .rebrickable_image import RebrickableImage -from .record import BrickRecord +from .exceptions import ErrorException, NotFoundException +from .rebrickable_part import RebrickablePart from .sql import BrickSQL if TYPE_CHECKING: from .minifigure import BrickMinifigure from .set import BrickSet + from .socket import BrickSocket + +logger = logging.getLogger(__name__) # Lego set or minifig part -class BrickPart(BrickRecord): - brickset: 'BrickSet | None' - minifigure: 'BrickMinifigure | None' +class BrickPart(RebrickablePart): + identifier: str + kind: str # Queries insert_query: str = 'part/insert' generic_query: str = 'part/select/generic' select_query: str = 'part/select/specific' - def __init__( - self, - /, - *, - brickset: 'BrickSet | None' = None, - minifigure: 'BrickMinifigure | None' = None, - record: Row | dict[str, Any] | None = None, - ): - super().__init__() + def __init__(self, /, **kwargs): + super().__init__(**kwargs) - # Save the brickset and minifigure - self.brickset = brickset - self.minifigure = minifigure + if self.minifigure is not None: + self.identifier = self.minifigure.fields.figure + self.kind = 'Minifigure' + elif self.brickset is not None: + self.identifier = self.brickset.fields.set + self.kind = 'Set' - # Ingest the record if it has one - if record is not None: - self.ingest(record) + # Import a part into the database + def download(self, socket: 'BrickSocket') -> bool: + if self.brickset is None: + raise ErrorException('Importing a part from Rebrickable outside of a set is not supported') # noqa: E501 - # Delete missing part - def delete_missing(self, /) -> None: - BrickSQL().execute_and_commit( - 'missing/delete/from_set', - parameters=self.sql_parameters() - ) - - # Set missing part - def set_missing(self, quantity: int, /) -> None: - parameters = self.sql_parameters() - parameters['quantity'] = quantity - - # Can't use UPSERT because the database has no keys - # Try to update - database = BrickSQL() - rows, _ = database.execute( - 'missing/update/from_set', - parameters=parameters, - ) - - # Insert if no row has been affected - if not rows: - rows, _ = database.execute( - 'missing/insert', - parameters=parameters, + try: + # Insert into the database + socket.auto_progress( + message='{kind} {identifier}: inserting part {part} into database'.format( # noqa: E501 + kind=self.kind, + identifier=self.identifier, + part=self.fields.part + ) ) - if rows != 1: - raise DatabaseException( - 'Could not update the missing quantity for part {id}'.format( # noqa: E501 - id=self.fields.id - ) - ) + # Insert into database + self.insert(commit=False) - database.commit() + # Insert the rebrickable set into database + self.insert_rebrickable() + + except Exception as e: + socket.fail( + message='Error while importing part {part} from {kind} {identifier}: {error}'.format( # noqa: E501 + part=self.fields.part, + kind=self.kind, + identifier=self.identifier, + error=e, + ) + ) + + logger.debug(traceback.format_exc()) + + return False + + return True + + # A identifier for HTML component + def html_id(self) -> str: + components: list[str] = [] + + if self.fields.figure is not None: + components.append(self.fields.figure) + + components.append(self.fields.part) + components.append(str(self.fields.color)) + components.append(str(self.fields.spare)) + + return '-'.join(components) # Select a generic part def select_generic( self, - part_num: str, - color_id: int, + part: str, + color: int, /, - *, - element_id: int | None = None ) -> Self: # Save the parameters to the fields - self.fields.part_num = part_num - self.fields.color_id = color_id - self.fields.element_id = element_id + self.fields.part = part + self.fields.color = color if not self.select(override_query=self.generic_query): raise NotFoundException( - 'Part with number {number}, color ID {color} and element ID {element} was not found in the database'.format( # noqa: E501 - number=self.fields.part_num, - color=self.fields.color_id, - element=self.fields.element_id, + 'Part with number {number}, color ID {color} was not found in the database'.format( # noqa: E501 + number=self.fields.part, + color=self.fields.color, ), ) @@ -107,7 +108,9 @@ class BrickPart(BrickRecord): def select_specific( self, brickset: 'BrickSet', - id: str, + part: str, + color: int, + spare: int, /, *, minifigure: 'BrickMinifigure | None' = None, @@ -115,168 +118,48 @@ class BrickPart(BrickRecord): # Save the parameters to the fields self.brickset = brickset self.minifigure = minifigure - self.fields.id = id + self.fields.part = part + self.fields.color = color + self.fields.spare = spare if not self.select(): + if self.minifigure is not None: + figure = self.minifigure.fields.figure + else: + figure = None + raise NotFoundException( - 'Part with ID {id} from set {set} was not found in the database'.format( # noqa: E501 + 'Part {part} with color {color} (spare: {spare}) from set {set} ({id}) (minifigure: {figure}) was not found in the database'.format( # noqa: E501 + part=self.fields.part, + color=self.fields.color, + spare=self.fields.spare, id=self.fields.id, set=self.brickset.fields.set, + figure=figure, ), ) return self - # Return a dict with common SQL parameters for a part - def sql_parameters(self, /) -> dict[str, Any]: - parameters = super().sql_parameters() - - # Supplement from the brickset - if 'u_id' not in parameters and self.brickset is not None: - parameters['u_id'] = self.brickset.fields.id - - if 'set_num' not in parameters: - if self.minifigure is not None: - parameters['set_num'] = self.minifigure.fields.figure - - elif self.brickset is not None: - parameters['set_num'] = self.brickset.fields.set - - return parameters - # Update the missing part def update_missing(self, missing: Any, /) -> None: - # If empty, delete it - if missing == '': - self.delete_missing() + # We need a positive integer + try: + missing = int(missing) - else: - # Try to understand it as a number - try: - missing = int(missing) - except Exception: - raise ErrorException('"{missing}" is not a valid integer'.format( # noqa: E501 - missing=missing - )) + if missing < 0: + missing = 0 + except Exception: + raise ErrorException('"{missing}" is not a valid integer'.format( + missing=missing + )) - # If 0, delete it - if missing == 0: - self.delete_missing() + if missing < 0: + raise ErrorException('Cannot set a negative missing value') - else: - # If negative, it's an error - if missing < 0: - raise ErrorException('Cannot set a negative missing value') + self.fields.missing = missing - # Otherwise upsert it - # Not checking if it is too much, you do you - self.set_missing(missing) - - # Self url - def url(self, /) -> str: - return url_for( - 'part.details', - number=self.fields.part_num, - color=self.fields.color_id, - element=self.fields.element_id, + BrickSQL().execute_and_commit( + 'part/update/missing', + parameters=self.sql_parameters() ) - - # Compute the url for the bricklink page - def url_for_bricklink(self, /) -> str: - if current_app.config['BRICKLINK_LINKS']: - try: - return current_app.config['BRICKLINK_LINK_PART_PATTERN'].format( # noqa: E501 - number=self.fields.part_num, - ) - except Exception: - pass - - return '' - - # Compute the url for the part image - def url_for_image(self, /) -> str: - if not current_app.config['USE_REMOTE_IMAGES']: - if self.fields.part_img_url is None: - file = RebrickableImage.nil_name() - else: - file = self.fields.part_img_url_id - - return RebrickableImage.static_url(file, 'PARTS_FOLDER') - else: - if self.fields.part_img_url is None: - return current_app.config['REBRICKABLE_IMAGE_NIL'] - else: - return self.fields.part_img_url - - # Compute the url for missing part - def url_for_missing(self, /) -> str: - # Different URL for a minifigure part - if self.minifigure is not None: - return url_for( - 'set.missing_minifigure_part', - id=self.fields.u_id, - figure=self.minifigure.fields.figure, - part=self.fields.id, - ) - - return url_for( - 'set.missing_part', - id=self.fields.u_id, - part=self.fields.id - ) - - # Compute the url for the rebrickable page - def url_for_rebrickable(self, /) -> str: - if current_app.config['REBRICKABLE_LINKS']: - try: - return current_app.config['REBRICKABLE_LINK_PART_PATTERN'].format( # noqa: E501 - number=self.fields.part_num, - color=self.fields.color_id, - ) - except Exception: - pass - - return '' - - # Normalize from Rebrickable - @staticmethod - def from_rebrickable( - data: dict[str, Any], - /, - *, - brickset: 'BrickSet | None' = None, - minifigure: 'BrickMinifigure | None' = None, - **_, - ) -> dict[str, Any]: - record = { - 'set_num': data['set_num'], - 'id': data['id'], - 'part_num': data['part']['part_num'], - 'name': data['part']['name'], - 'part_img_url': data['part']['part_img_url'], - 'part_img_url_id': None, - 'color_id': data['color']['id'], - 'color_name': data['color']['name'], - 'quantity': data['quantity'], - 'is_spare': data['is_spare'], - 'element_id': data['element_id'], - } - - if brickset is not None: - record['u_id'] = brickset.fields.id - - if minifigure is not None: - record['set_num'] = data['fig_num'] - - # Extract the file name - if data['part']['part_img_url'] is not None: - part_img_url_file = os.path.basename( - urlparse(data['part']['part_img_url']).path - ) - - part_img_url_id, _ = os.path.splitext(part_img_url_file) - - if part_img_url_id is not None or part_img_url_id != '': - record['part_img_url_id'] = part_img_url_id - - return record diff --git a/bricktracker/part_list.py b/bricktracker/part_list.py index 7805d57..0074b9b 100644 --- a/bricktracker/part_list.py +++ b/bricktracker/part_list.py @@ -1,12 +1,18 @@ +import logging from typing import Any, Self, TYPE_CHECKING +import traceback from flask import current_app from .part import BrickPart +from .rebrickable import Rebrickable from .record_list import BrickRecordList if TYPE_CHECKING: from .minifigure import BrickMinifigure from .set import BrickSet + from .socket import BrickSocket + +logger = logging.getLogger(__name__) # Lego set or minifig parts @@ -20,7 +26,7 @@ class BrickPartList(BrickRecordList[BrickPart]): last_query: str = 'part/list/last' minifigure_query: str = 'part/list/from_minifigure' missing_query: str = 'part/list/missing' - select_query: str = 'part/list/from_set' + select_query: str = 'part/list/specific' def __init__(self, /): super().__init__() @@ -44,8 +50,8 @@ class BrickPartList(BrickRecordList[BrickPart]): return self - # Load parts from a brickset or minifigure - def load( + # List specific parts from a brickset or minifigure + def list_specific( self, brickset: 'BrickSet', /, @@ -64,7 +70,7 @@ class BrickPartList(BrickRecordList[BrickPart]): record=record, ) - if current_app.config['SKIP_SPARE_PARTS'] and part.fields.is_spare: + if current_app.config['SKIP_SPARE_PARTS'] and part.fields.spare: continue self.records.append(part) @@ -90,7 +96,7 @@ class BrickPartList(BrickRecordList[BrickPart]): record=record, ) - if current_app.config['SKIP_SPARE_PARTS'] and part.fields.is_spare: + if current_app.config['SKIP_SPARE_PARTS'] and part.fields.spare: continue self.records.append(part) @@ -115,13 +121,73 @@ class BrickPartList(BrickRecordList[BrickPart]): # Set id if self.brickset is not None: - parameters['u_id'] = self.brickset.fields.id + parameters['id'] = self.brickset.fields.id # Use the minifigure number if present, - # otherwise use the set number if self.minifigure is not None: - parameters['set_num'] = self.minifigure.fields.figure - elif self.brickset is not None: - parameters['set_num'] = self.brickset.fields.set + parameters['figure'] = self.minifigure.fields.figure + else: + parameters['figure'] = None return parameters + + # Import the parts from Rebrickable + @staticmethod + def download( + socket: 'BrickSocket', + brickset: 'BrickSet', + /, + *, + minifigure: 'BrickMinifigure | None' = None, + ) -> bool: + if minifigure is not None: + identifier = minifigure.fields.figure + kind = 'Minifigure' + method = 'get_minifig_elements' + else: + identifier = brickset.fields.set + kind = 'Set' + method = 'get_set_elements' + + try: + socket.auto_progress( + message='{kind} {identifier}: loading parts inventory from Rebrickable'.format( # noqa: E501 + kind=kind, + identifier=identifier, + ), + increment_total=True, + ) + + logger.debug('rebrick.lego.{method}("{identifier}")'.format( + method=method, + identifier=identifier, + )) + + inventory = Rebrickable[BrickPart]( + method, + identifier, + BrickPart, + socket=socket, + brickset=brickset, + minifigure=minifigure, + ).list() + + # Process each part + for part in inventory: + if not part.download(socket): + return False + + except Exception as e: + socket.fail( + message='Error while importing {kind} {identifier} parts list: {error}'.format( # noqa: E501 + kind=kind, + identifier=identifier, + error=e, + ) + ) + + logger.debug(traceback.format_exc()) + + return False + + return True diff --git a/bricktracker/rebrickable_image.py b/bricktracker/rebrickable_image.py index f15a9b4..509e718 100644 --- a/bricktracker/rebrickable_image.py +++ b/bricktracker/rebrickable_image.py @@ -9,7 +9,7 @@ from shutil import copyfileobj from .exceptions import DownloadException if TYPE_CHECKING: from .rebrickable_minifigure import RebrickableMinifigure - from .part import BrickPart + from .rebrickable_part import RebrickablePart from .rebrickable_set import RebrickableSet @@ -17,7 +17,7 @@ if TYPE_CHECKING: class RebrickableImage(object): set: 'RebrickableSet' minifigure: 'RebrickableMinifigure | None' - part: 'BrickPart | None' + part: 'RebrickablePart | None' extension: str | None @@ -27,7 +27,7 @@ class RebrickableImage(object): /, *, minifigure: 'RebrickableMinifigure | None' = None, - part: 'BrickPart | None' = None, + part: 'RebrickablePart | None' = None, ): # Save all objects self.set = set @@ -81,10 +81,10 @@ class RebrickableImage(object): # Return the id depending on the objects provided def id(self, /) -> str: if self.part is not None: - if self.part.fields.part_img_url_id is None: + if self.part.fields.image_id is None: return RebrickableImage.nil_name() else: - return self.part.fields.part_img_url_id + return self.part.fields.image_id if self.minifigure is not None: if self.minifigure.fields.image is None: @@ -105,10 +105,10 @@ class RebrickableImage(object): # Return the url depending on the objects provided def url(self, /) -> str: if self.part is not None: - if self.part.fields.part_img_url is None: + if self.part.fields.image is None: return current_app.config['REBRICKABLE_IMAGE_NIL'] else: - return self.part.fields.part_img_url + return self.part.fields.image if self.minifigure is not None: if self.minifigure.fields.image is None: diff --git a/bricktracker/rebrickable_part.py b/bricktracker/rebrickable_part.py new file mode 100644 index 0000000..93c6b34 --- /dev/null +++ b/bricktracker/rebrickable_part.py @@ -0,0 +1,203 @@ +import os +import logging +from sqlite3 import Row +from typing import Any, TYPE_CHECKING +from urllib.parse import urlparse + +from flask import current_app, url_for + +from .exceptions import ErrorException +from .rebrickable_image import RebrickableImage +from .record import BrickRecord +if TYPE_CHECKING: + from .minifigure import BrickMinifigure + from .set import BrickSet + from .socket import BrickSocket + +logger = logging.getLogger(__name__) + + +# A part from Rebrickable +class RebrickablePart(BrickRecord): + socket: 'BrickSocket' + brickset: 'BrickSet | None' + minifigure: 'BrickMinifigure | None' + + # Queries + select_query: str = 'rebrickable/part/select' + insert_query: str = 'rebrickable/part/insert' + + def __init__( + self, + /, + *, + brickset: 'BrickSet | None' = None, + minifigure: 'BrickMinifigure | None' = None, + record: Row | dict[str, Any] | None = None + ): + super().__init__() + + # Save the brickset + self.brickset = brickset + + # Save the minifigure + self.minifigure = minifigure + + # Ingest the record if it has one + if record is not None: + self.ingest(record) + + # Insert the part from Rebrickable + def insert_rebrickable(self, /) -> bool: + if self.brickset is None: + raise ErrorException('Importing a part from Rebrickable outside of a set is not supported') # noqa: E501 + + # Insert the Rebrickable part to the database + rows, _ = self.insert( + commit=False, + no_defer=True, + override_query=RebrickablePart.insert_query + ) + + inserted = rows > 0 + + if inserted: + if not current_app.config['USE_REMOTE_IMAGES']: + RebrickableImage( + self.brickset, + minifigure=self.minifigure, + part=self, + ).download() + + return inserted + + # Return a dict with common SQL parameters for a part + def sql_parameters(self, /) -> dict[str, Any]: + parameters = super().sql_parameters() + + # Set id + if self.brickset is not None: + parameters['id'] = self.brickset.fields.id + + # Use the minifigure number if present, + if self.minifigure is not None: + parameters['figure'] = self.minifigure.fields.figure + else: + parameters['figure'] = None + + return parameters + + # Self url + def url(self, /) -> str: + return url_for( + 'part.details', + part=self.fields.part, + color=self.fields.color, + ) + + # Compute the url for the bricklink page + def url_for_bricklink(self, /) -> str: + if current_app.config['BRICKLINK_LINKS']: + try: + return current_app.config['BRICKLINK_LINK_PART_PATTERN'].format( # noqa: E501 + part=self.fields.part, + ) + except Exception: + pass + + return '' + + # Compute the url for the part image + def url_for_image(self, /) -> str: + if not current_app.config['USE_REMOTE_IMAGES']: + if self.fields.image is None: + file = RebrickableImage.nil_name() + else: + file = self.fields.image_id + + return RebrickableImage.static_url(file, 'PARTS_FOLDER') + else: + if self.fields.image is None: + return current_app.config['REBRICKABLE_IMAGE_NIL'] + else: + return self.fields.image + + # Compute the url for missing part + def url_for_missing(self, /) -> str: + # Different URL for a minifigure part + if self.minifigure is not None: + figure = self.minifigure.fields.figure + else: + figure = None + + return url_for( + 'set.missing_part', + id=self.fields.id, + figure=figure, + part=self.fields.part, + color=self.fields.color, + spare=self.fields.spare, + ) + + # Compute the url for the rebrickable page + def url_for_rebrickable(self, /) -> str: + if current_app.config['REBRICKABLE_LINKS']: + try: + return current_app.config['REBRICKABLE_LINK_PART_PATTERN'].format( # noqa: E501 + part=self.fields.part, + color=self.fields.color, + ) + except Exception: + pass + + return '' + + # Normalize from Rebrickable + @staticmethod + def from_rebrickable( + data: dict[str, Any], + /, + *, + brickset: 'BrickSet | None' = None, + minifigure: 'BrickMinifigure | None' = None, + **_, + ) -> dict[str, Any]: + record = { + 'id': None, + 'figure': None, + 'part': data['part']['part_num'], + 'color': data['color']['id'], + 'spare': data['is_spare'], + 'quantity': data['quantity'], + 'rebrickable_inventory': data['id'], + 'element': data['element_id'], + 'color_id': data['color']['id'], + 'color_name': data['color']['name'], + 'color_rgb': data['color']['rgb'], + 'color_transparent': data['color']['is_trans'], + 'name': data['part']['name'], + 'category': data['part']['part_cat_id'], + 'image': data['part']['part_img_url'], + 'image_id': None, + 'url': data['part']['part_url'], + 'print': data['part']['print_of'] + } + + if brickset is not None: + record['id'] = brickset.fields.id + + if minifigure is not None: + record['figure'] = minifigure.fields.figure + + # Extract the file name + if record['image'] is not None: + image_id, _ = os.path.splitext( + os.path.basename( + urlparse(record['image']).path + ) + ) + + if image_id is not None or image_id != '': + record['image_id'] = image_id + + return record diff --git a/bricktracker/rebrickable_parts.py b/bricktracker/rebrickable_parts.py deleted file mode 100644 index 9fd2341..0000000 --- a/bricktracker/rebrickable_parts.py +++ /dev/null @@ -1,113 +0,0 @@ -import logging -from typing import TYPE_CHECKING - -from flask import current_app - -from .part import BrickPart -from .rebrickable import Rebrickable -from .rebrickable_image import RebrickableImage -if TYPE_CHECKING: - from .minifigure import BrickMinifigure - from .set import BrickSet - from .socket import BrickSocket - -logger = logging.getLogger(__name__) - - -# A list of parts from Rebrickable -class RebrickableParts(object): - socket: 'BrickSocket' - brickset: 'BrickSet' - minifigure: 'BrickMinifigure | None' - - number: str - kind: str - method: str - - def __init__( - self, - socket: 'BrickSocket', - brickset: 'BrickSet', - /, - *, - minifigure: 'BrickMinifigure | None' = None, - ): - # Save the socket - self.socket = socket - - # Save the objects - self.brickset = brickset - self.minifigure = minifigure - - if self.minifigure is not None: - self.number = self.minifigure.fields.figure - self.kind = 'Minifigure' - self.method = 'get_minifig_elements' - else: - self.number = self.brickset.fields.set - self.kind = 'Set' - self.method = 'get_set_elements' - - # Import the parts from Rebrickable - def download(self, /) -> None: - self.socket.auto_progress( - message='{kind} {number}: loading parts inventory from Rebrickable'.format( # noqa: E501 - kind=self.kind, - number=self.number, - ), - increment_total=True, - ) - - logger.debug('rebrick.lego.{method}("{number}")'.format( - method=self.method, - number=self.number, - )) - - inventory = Rebrickable[BrickPart]( - self.method, - self.number, - BrickPart, - socket=self.socket, - brickset=self.brickset, - minifigure=self.minifigure, - ).list() - - # Process each part - total = len(inventory) - for index, part in enumerate(inventory): - # Skip spare parts - if ( - current_app.config['SKIP_SPARE_PARTS'] and - part.fields.is_spare - ): - continue - - # Insert into the database - self.socket.auto_progress( - message='{kind} {number}: inserting part {current}/{total} into database'.format( # noqa: E501 - kind=self.kind, - number=self.number, - current=index+1, - total=total, - ) - ) - - # Insert into database - part.insert(commit=False) - - # Grab the image - self.socket.progress( - message='{kind} {number}: downloading part {current}/{total} image'.format( # noqa: E501 - kind=self.kind, - number=self.number, - current=index+1, - total=total, - ) - ) - - if not current_app.config['USE_REMOTE_IMAGES']: - RebrickableImage( - self.brickset, - minifigure=self.minifigure, - part=part, - ).download() diff --git a/bricktracker/set.py b/bricktracker/set.py index 52a2ed9..63d4128 100644 --- a/bricktracker/set.py +++ b/bricktracker/set.py @@ -8,7 +8,6 @@ from flask import current_app, url_for from .exceptions import DatabaseException, NotFoundException from .minifigure_list import BrickMinifigureList from .part_list import BrickPartList -from .rebrickable_parts import RebrickableParts from .rebrickable_set import RebrickableSet from .set_checkbox import BrickSetCheckbox from .set_checkbox_list import BrickSetCheckboxList @@ -34,10 +33,10 @@ class BrickSet(RebrickableSet): ) # Import a set into the database - def download(self, socket: 'BrickSocket', data: dict[str, Any], /) -> None: + def download(self, socket: 'BrickSocket', data: dict[str, Any], /) -> bool: # Load the set if not self.load(socket, data, from_download=True): - return + return False try: # Insert into the database @@ -58,10 +57,12 @@ class BrickSet(RebrickableSet): self.insert_rebrickable() # Load the inventory - RebrickableParts(socket, self).download() + if not BrickPartList.download(socket, self): + return False # Load the minifigures - BrickMinifigureList.download(socket, self) + if not BrickMinifigureList.download(socket, self): + return False # Commit the transaction to the database socket.auto_progress( @@ -98,13 +99,17 @@ class BrickSet(RebrickableSet): logger.debug(traceback.format_exc()) + return False + + return True + # Minifigures def minifigures(self, /) -> BrickMinifigureList: return BrickMinifigureList().from_set(self) # Parts def parts(self, /) -> BrickPartList: - return BrickPartList().load(self) + return BrickPartList().list_specific(self) # Select a light set (with an id) def select_light(self, id: str, /) -> Self: diff --git a/bricktracker/set_list.py b/bricktracker/set_list.py index 58ae8ec..349af66 100644 --- a/bricktracker/set_list.py +++ b/bricktracker/set_list.py @@ -100,16 +100,13 @@ class BrickSetList(BrickRecordList[BrickSet]): # Sets missing a part def missing_part( self, - part_num: str, - color_id: int, - /, - *, - element_id: int | None = None, + part: str, + color: int, + / ) -> Self: # Save the parameters to the fields - self.fields.part_num = part_num - self.fields.color_id = color_id - self.fields.element_id = element_id + self.fields.part = part + self.fields.color = color # Load the sets from the database for record in self.select( @@ -141,16 +138,13 @@ class BrickSetList(BrickRecordList[BrickSet]): # Sets using a part def using_part( self, - part_num: str, - color_id: int, - /, - *, - element_id: int | None = None, + part: str, + color: int, + / ) -> Self: # Save the parameters to the fields - self.fields.part_num = part_num - self.fields.color_id = color_id - self.fields.element_id = element_id + self.fields.part = part + self.fields.color = color # Load the sets from the database for record in self.select( diff --git a/bricktracker/sql/migrations/0010.sql b/bricktracker/sql/migrations/0010.sql new file mode 100644 index 0000000..8b4b6e6 --- /dev/null +++ b/bricktracker/sql/migrations/0010.sql @@ -0,0 +1,42 @@ +-- description: Creation of the deduplicated table of Rebrickable parts, and add a bunch of extra fields for later + +BEGIN TRANSACTION; + +-- Create a Rebrickable parts table: each unique part imported from Rebrickable +CREATE TABLE "rebrickable_parts" ( + "part" TEXT NOT NULL, + "color_id" INTEGER NOT NULL, + "color_name" TEXT NOT NULL, + "color_rgb" TEXT, -- can be NULL because it was not saved before + "color_transparent" BOOLEAN, -- can be NULL because it was not saved before + "name" TEXT NOT NULL, + "category" INTEGER, -- can be NULL because it was not saved before + "image" TEXT, + "image_id" TEXT, + "url" TEXT, -- can be NULL because it was not saved before + "print" INTEGER, -- can be NULL, was not saved before + PRIMARY KEY("part", "color_id") +); + +-- Insert existing parts into the new table +INSERT INTO "rebrickable_parts" ( + "part", + "color_id", + "color_name", + "name", + "image", + "image_id" +) +SELECT + "inventory"."part_num", + "inventory"."color_id", + "inventory"."color_name", + "inventory"."name", + "inventory"."part_img_url", + "inventory"."part_img_url_id" +FROM "inventory" +GROUP BY + "inventory"."part_num", + "inventory"."color_id"; + +COMMIT; \ No newline at end of file diff --git a/bricktracker/sql/migrations/0011.sql b/bricktracker/sql/migrations/0011.sql new file mode 100644 index 0000000..54962f3 --- /dev/null +++ b/bricktracker/sql/migrations/0011.sql @@ -0,0 +1,60 @@ +-- description: Migrate the Bricktracker parts (and missing parts), and add a bunch of extra fields for later + +PRAGMA foreign_keys = ON; + +BEGIN TRANSACTION; + +-- Create a Bricktracker parts table: an amount of parts linked to a Bricktracker set +CREATE TABLE "bricktracker_parts" ( + "id" TEXT NOT NULL, + "figure" TEXT, + "part" TEXT NOT NULL, + "color" INTEGER NOT NULL, + "spare" BOOLEAN NOT NULL, + "quantity" INTEGER NOT NULL, + "element" INTEGER, + "rebrickable_inventory" INTEGER NOT NULL, + "missing" INTEGER NOT NULL DEFAULT 0, + "damaged" INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY("id", "figure", "part", "color", "spare"), + FOREIGN KEY("id") REFERENCES "bricktracker_sets"("id"), + FOREIGN KEY("figure") REFERENCES "rebrickable_minifigures"("figure"), + FOREIGN KEY("part", "color") REFERENCES "rebrickable_parts"("part", "color_id") +); + +-- Insert existing parts into the new table +INSERT INTO "bricktracker_parts" ( + "id", + "figure", + "part", + "color", + "spare", + "quantity", + "element", + "rebrickable_inventory", + "missing" +) +SELECT + "inventory"."u_id", + CASE WHEN SUBSTR("inventory"."set_num", 0, 5) = 'fig-' THEN "inventory"."set_num" ELSE NULL END, + "inventory"."part_num", + "inventory"."color_id", + "inventory"."is_spare", + "inventory"."quantity", + "inventory"."element_id", + "inventory"."id", + IFNULL("missing"."quantity", 0) +FROM "inventory" +LEFT JOIN "missing" +ON "inventory"."set_num" IS NOT DISTINCT FROM "missing"."set_num" +AND "inventory"."id" IS NOT DISTINCT FROM "missing"."id" +AND "inventory"."part_num" IS NOT DISTINCT FROM "missing"."part_num" +AND "inventory"."color_id" IS NOT DISTINCT FROM "missing"."color_id" +AND "inventory"."element_id" IS NOT DISTINCT FROM "missing"."element_id" +AND "inventory"."u_id" IS NOT DISTINCT FROM "missing"."u_id"; + +-- Rename the original table (don't delete it yet?) +ALTER TABLE "inventory" RENAME TO "inventory_old"; +ALTER TABLE "missing" RENAME TO "missing_old"; + +COMMIT; \ No newline at end of file diff --git a/bricktracker/sql/minifigure/base/base.sql b/bricktracker/sql/minifigure/base/base.sql index c580b38..dbfc428 100644 --- a/bricktracker/sql/minifigure/base/base.sql +++ b/bricktracker/sql/minifigure/base/base.sql @@ -1,5 +1,4 @@ SELECT - {% block set %}{% endblock %} "bricktracker_minifigures"."quantity", "rebrickable_minifigures"."figure", "rebrickable_minifigures"."number", diff --git a/bricktracker/sql/minifigure/list/all.sql b/bricktracker/sql/minifigure/list/all.sql index 498062d..ca23068 100644 --- a/bricktracker/sql/minifigure/list/all.sql +++ b/bricktracker/sql/minifigure/list/all.sql @@ -16,16 +16,17 @@ COUNT("bricktracker_minifigures"."id") AS "total_sets" -- LEFT JOIN + SELECT to avoid messing the total LEFT JOIN ( SELECT - "missing"."set_num", - "missing"."u_id", - SUM("missing"."quantity") AS total - FROM "missing" + "bricktracker_parts"."id", + "bricktracker_parts"."figure", + SUM("bricktracker_parts"."missing") AS total + FROM "bricktracker_parts" + WHERE "bricktracker_parts"."figure" IS NOT NULL GROUP BY - "missing"."set_num", - "missing"."u_id" + "bricktracker_parts"."id", + "bricktracker_parts"."figure" ) missing_join -ON "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "missing_join"."u_id" -AND "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "missing_join"."set_num" +ON "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "missing_join"."id" +AND "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "missing_join"."figure" {% endblock %} {% block group %} diff --git a/bricktracker/sql/minifigure/list/last.sql b/bricktracker/sql/minifigure/list/last.sql index cacc2f7..372610d 100644 --- a/bricktracker/sql/minifigure/list/last.sql +++ b/bricktracker/sql/minifigure/list/last.sql @@ -1,13 +1,13 @@ {% extends 'minifigure/base/base.sql' %} {% block total_missing %} -SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", +SUM("bricktracker_parts"."missing") AS "total_missing", {% endblock %} {% block join %} -LEFT JOIN "missing" -ON "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "missing"."set_num" -AND "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "missing"."u_id" +LEFT JOIN "bricktracker_parts" +ON "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "bricktracker_parts"."id" +AND "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "bricktracker_parts"."figure" {% endblock %} {% block group %} diff --git a/bricktracker/sql/minifigure/list/missing_part.sql b/bricktracker/sql/minifigure/list/missing_part.sql index 3fe4210..32144bd 100644 --- a/bricktracker/sql/minifigure/list/missing_part.sql +++ b/bricktracker/sql/minifigure/list/missing_part.sql @@ -1,26 +1,24 @@ {% extends 'minifigure/base/base.sql' %} {% block total_missing %} -SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", +SUM("bricktracker_parts"."missing") AS "total_missing", {% endblock %} {% block join %} -LEFT JOIN "missing" -ON "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "missing"."set_num" -AND "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "missing"."u_id" +LEFT JOIN "bricktracker_parts" +ON "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "bricktracker_parts"."id" +AND "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "bricktracker_parts"."figure" {% endblock %} {% block where %} WHERE "rebrickable_minifigures"."figure" IN ( - SELECT - "missing"."set_num" - FROM "missing" - - WHERE "missing"."color_id" IS NOT DISTINCT FROM :color_id - AND "missing"."element_id" IS NOT DISTINCT FROM :element_id - AND "missing"."part_num" IS NOT DISTINCT FROM :part_num - - GROUP BY "missing"."set_num" + SELECT "bricktracker_parts"."figure" + FROM "bricktracker_parts" + WHERE "bricktracker_parts"."part" IS NOT DISTINCT FROM :part + AND "bricktracker_parts"."color" IS NOT DISTINCT FROM :color + AND "bricktracker_parts"."figure" IS NOT NULL + AND "bricktracker_parts"."missing" > 0 + GROUP BY "bricktracker_parts"."figure" ) {% endblock %} diff --git a/bricktracker/sql/minifigure/list/using_part.sql b/bricktracker/sql/minifigure/list/using_part.sql index e701d8d..d6ea0d1 100644 --- a/bricktracker/sql/minifigure/list/using_part.sql +++ b/bricktracker/sql/minifigure/list/using_part.sql @@ -6,15 +6,12 @@ SUM("bricktracker_minifigures"."quantity") AS "total_quantity", {% block where %} WHERE "rebrickable_minifigures"."figure" IN ( - SELECT - "inventory"."set_num" - FROM "inventory" - - WHERE "inventory"."color_id" IS NOT DISTINCT FROM :color_id - AND "inventory"."element_id" IS NOT DISTINCT FROM :element_id - AND "inventory"."part_num" IS NOT DISTINCT FROM :part_num - - GROUP BY "inventory"."set_num" + SELECT "bricktracker_parts"."figure" + FROM "bricktracker_parts" + WHERE "bricktracker_parts"."part" IS NOT DISTINCT FROM :part + AND "bricktracker_parts"."color" IS NOT DISTINCT FROM :color + AND "bricktracker_parts"."figure" IS NOT NULL + GROUP BY "bricktracker_parts"."figure" ) {% endblock %} diff --git a/bricktracker/sql/minifigure/select/generic.sql b/bricktracker/sql/minifigure/select/generic.sql index 16dc56d..f5bacd7 100644 --- a/bricktracker/sql/minifigure/select/generic.sql +++ b/bricktracker/sql/minifigure/select/generic.sql @@ -1,7 +1,7 @@ {% extends 'minifigure/base/base.sql' %} {% block total_missing %} -SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", +IFNULL("missing_join"."total", 0) AS "total_missing", {% endblock %} {% block total_quantity %} @@ -13,9 +13,16 @@ COUNT(DISTINCT "bricktracker_minifigures"."id") AS "total_sets" {% endblock %} {% block join %} -LEFT JOIN "missing" -ON "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "missing"."set_num" -AND "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "missing"."u_id" +-- LEFT JOIN + SELECT to avoid messing the total +LEFT JOIN ( + SELECT + "bricktracker_parts"."figure", + SUM("bricktracker_parts"."missing") AS "total" + FROM "bricktracker_parts" + WHERE "bricktracker_parts"."figure" IS NOT DISTINCT FROM :figure + GROUP BY "bricktracker_parts"."figure" +) "missing_join" +ON "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "missing_join"."figure" {% endblock %} {% block where %} diff --git a/bricktracker/sql/minifigure/select/specific.sql b/bricktracker/sql/minifigure/select/specific.sql index 00a66af..970494f 100644 --- a/bricktracker/sql/minifigure/select/specific.sql +++ b/bricktracker/sql/minifigure/select/specific.sql @@ -1,6 +1,6 @@ {% extends 'minifigure/base/base.sql' %} {% block where %} -WHERE "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM :figure -AND "bricktracker_minifigures"."id" IS NOT DISTINCT FROM :id +WHERE "bricktracker_minifigures"."id" IS NOT DISTINCT FROM :id +AND "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM :figure {% endblock %} diff --git a/bricktracker/sql/missing/delete/from_set.sql b/bricktracker/sql/missing/delete/from_set.sql deleted file mode 100644 index ceedc78..0000000 --- a/bricktracker/sql/missing/delete/from_set.sql +++ /dev/null @@ -1,4 +0,0 @@ -DELETE FROM "missing" -WHERE "missing"."set_num" IS NOT DISTINCT FROM :set_num -AND "missing"."id" IS NOT DISTINCT FROM :id -AND "missing"."u_id" IS NOT DISTINCT FROM :u_id diff --git a/bricktracker/sql/missing/insert.sql b/bricktracker/sql/missing/insert.sql deleted file mode 100644 index c645028..0000000 --- a/bricktracker/sql/missing/insert.sql +++ /dev/null @@ -1,20 +0,0 @@ -INSERT INTO "missing" ( - "set_num", - "id", - "part_num", - "part_img_url_id", - "color_id", - "quantity", - "element_id", - "u_id" -) -VALUES( - :set_num, - :id, - :part_num, - :part_img_url_id, - :color_id, - :quantity, - :element_id, - :u_id -) diff --git a/bricktracker/sql/missing/update/from_set.sql b/bricktracker/sql/missing/update/from_set.sql deleted file mode 100644 index 335dd06..0000000 --- a/bricktracker/sql/missing/update/from_set.sql +++ /dev/null @@ -1,5 +0,0 @@ -UPDATE "missing" -SET "quantity" = :quantity -WHERE "missing"."set_num" IS NOT DISTINCT FROM :set_num -AND "missing"."id" IS NOT DISTINCT FROM :id -AND "missing"."u_id" IS NOT DISTINCT FROM :u_id diff --git a/bricktracker/sql/part/base/base.sql b/bricktracker/sql/part/base/base.sql new file mode 100644 index 0000000..d9226b3 --- /dev/null +++ b/bricktracker/sql/part/base/base.sql @@ -0,0 +1,56 @@ +SELECT + "bricktracker_parts"."id", + "bricktracker_parts"."figure", + "bricktracker_parts"."part", + "bricktracker_parts"."color", + "bricktracker_parts"."spare", + "bricktracker_parts"."quantity", + "bricktracker_parts"."element", + --"bricktracker_parts"."rebrickable_inventory", + "bricktracker_parts"."missing", + "bricktracker_parts"."damaged", + --"rebrickable_parts"."part", + --"rebrickable_parts"."color_id", + "rebrickable_parts"."color_name", + "rebrickable_parts"."color_rgb", + "rebrickable_parts"."color_transparent", + "rebrickable_parts"."name", + --"rebrickable_parts"."category", + "rebrickable_parts"."image", + "rebrickable_parts"."image_id", + "rebrickable_parts"."url", + --"rebrickable_parts"."print", + {% block total_missing %} + NULL AS "total_missing", -- dummy for order: total_missing + {% endblock %} + {% block total_quantity %} + NULL AS "total_quantity", -- dummy for order: total_quantity + {% endblock %} + {% block total_spare %} + NULL AS "total_spare", -- dummy for order: total_spare + {% endblock %} + {% block total_sets %} + NULL AS "total_sets", -- dummy for order: total_sets + {% endblock %} + {% block total_minifigures %} + NULL AS "total_minifigures" -- dummy for order: total_minifigures + {% endblock %} +FROM "bricktracker_parts" + +INNER 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" + +{% block join %}{% endblock %} + +{% block where %}{% endblock %} + +{% block group %}{% endblock %} + +{% if order %} +ORDER BY {{ order }} +{% endif %} + +{% if limit %} +LIMIT {{ limit }} +{% endif %} diff --git a/bricktracker/sql/part/base/select.sql b/bricktracker/sql/part/base/select.sql deleted file mode 100644 index 3966a75..0000000 --- a/bricktracker/sql/part/base/select.sql +++ /dev/null @@ -1,43 +0,0 @@ -SELECT - "inventory"."set_num", - "inventory"."id", - "inventory"."part_num", - "inventory"."name", - "inventory"."part_img_url", - "inventory"."part_img_url_id", - "inventory"."color_id", - "inventory"."color_name", - "inventory"."quantity", - "inventory"."is_spare", - "inventory"."element_id", - "inventory"."u_id", - {% block total_missing %} - NULL AS "total_missing", -- dummy for order: total_missing - {% endblock %} - {% block total_quantity %} - NULL AS "total_quantity", -- dummy for order: total_quantity - {% endblock %} - {% block total_spare %} - NULL AS "total_spare", -- dummy for order: total_spare - {% endblock %} - {% block total_sets %} - NULL AS "total_sets", -- dummy for order: total_sets - {% endblock %} - {% block total_minifigures %} - NULL AS "total_minifigures" -- dummy for order: total_minifigures - {% endblock %} -FROM "inventory" - -{% block join %}{% endblock %} - -{% block where %}{% endblock %} - -{% block group %}{% endblock %} - -{% if order %} -ORDER BY {{ order }} -{% endif %} - -{% if limit %} -LIMIT {{ limit }} -{% endif %} diff --git a/bricktracker/sql/part/insert.sql b/bricktracker/sql/part/insert.sql index 39b2d14..e127386 100644 --- a/bricktracker/sql/part/insert.sql +++ b/bricktracker/sql/part/insert.sql @@ -1,27 +1,19 @@ -INSERT INTO inventory ( - "set_num", +INSERT INTO "bricktracker_parts" ( "id", - "part_num", - "name", - "part_img_url", - "part_img_url_id", - "color_id", - "color_name", + "figure", + "part", + "color", + "spare", "quantity", - "is_spare", - "element_id", - "u_id" + "element", + "rebrickable_inventory" ) VALUES ( - :set_num, :id, - :part_num, - :name, - :part_img_url, - :part_img_url_id, - :color_id, - :color_name, + :figure, + :part, + :color, + :spare, :quantity, - :is_spare, - :element_id, - :u_id + :element, + :rebrickable_inventory ) diff --git a/bricktracker/sql/part/list/all.sql b/bricktracker/sql/part/list/all.sql index c5bbf69..c1d0ed1 100644 --- a/bricktracker/sql/part/list/all.sql +++ b/bricktracker/sql/part/list/all.sql @@ -1,15 +1,15 @@ -{% extends 'part/base/select.sql' %} +{% extends 'part/base/base.sql' %} {% block total_missing %} -SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", +SUM("bricktracker_parts"."missing") AS "total_missing", {% endblock %} {% block total_quantity %} -SUM("inventory"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)) AS "total_quantity", +SUM("bricktracker_parts"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)) AS "total_quantity", {% endblock %} {% block total_sets %} -COUNT(DISTINCT "bricktracker_minifigures"."id") AS "total_sets", +COUNT(DISTINCT "bricktracker_parts"."id") AS "total_sets", {% endblock %} {% block total_minifigures %} @@ -17,24 +17,14 @@ SUM(IFNULL("bricktracker_minifigures"."quantity", 0)) AS "total_minifigures" {% endblock %} {% block join %} -LEFT JOIN "missing" -ON "inventory"."set_num" IS NOT DISTINCT FROM "missing"."set_num" -AND "inventory"."id" IS NOT DISTINCT FROM "missing"."id" -AND "inventory"."part_num" IS NOT DISTINCT FROM "missing"."part_num" -AND "inventory"."color_id" IS NOT DISTINCT FROM "missing"."color_id" -AND "inventory"."element_id" IS NOT DISTINCT FROM "missing"."element_id" -AND "inventory"."u_id" IS NOT DISTINCT FROM "missing"."u_id" - LEFT JOIN "bricktracker_minifigures" -ON "inventory"."set_num" IS NOT DISTINCT FROM "bricktracker_minifigures"."figure" -AND "inventory"."u_id" IS NOT DISTINCT FROM "bricktracker_minifigures"."id" +ON "bricktracker_parts"."id" IS NOT DISTINCT FROM "bricktracker_minifigures"."id" +AND "bricktracker_parts"."figure" IS NOT DISTINCT FROM "bricktracker_minifigures"."figure" {% endblock %} {% block group %} GROUP BY - "inventory"."part_num", - "inventory"."name", - "inventory"."color_id", - "inventory"."is_spare", - "inventory"."element_id" + "bricktracker_parts"."part", + "bricktracker_parts"."color", + "bricktracker_parts"."spare" {% endblock %} diff --git a/bricktracker/sql/part/list/from_minifigure.sql b/bricktracker/sql/part/list/from_minifigure.sql index cf4135f..c840938 100644 --- a/bricktracker/sql/part/list/from_minifigure.sql +++ b/bricktracker/sql/part/list/from_minifigure.sql @@ -1,28 +1,17 @@ -{% extends 'part/base/select.sql' %} +{% extends 'part/base/base.sql' %} {% block total_missing %} -SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", -{% endblock %} - -{% block join %} -LEFT JOIN "missing" -ON "missing"."set_num" IS NOT DISTINCT FROM "inventory"."set_num" -AND "missing"."id" IS NOT DISTINCT FROM "inventory"."id" -AND "missing"."part_num" IS NOT DISTINCT FROM "inventory"."part_num" -AND "missing"."color_id" IS NOT DISTINCT FROM "inventory"."color_id" -AND "missing"."element_id" IS NOT DISTINCT FROM "inventory"."element_id" +SUM("bricktracker_parts"."missing") AS "total_missing", {% endblock %} {% block where %} -WHERE "inventory"."set_num" IS NOT DISTINCT FROM :set_num +WHERE "bricktracker_parts"."figure" IS NOT DISTINCT FROM :figure {% endblock %} {% block group %} GROUP BY - "inventory"."part_num", - "inventory"."name", - "inventory"."color_id", - "inventory"."is_spare", - "inventory"."element_id" + "bricktracker_parts"."part", + "bricktracker_parts"."color", + "bricktracker_parts"."spare" {% endblock %} diff --git a/bricktracker/sql/part/list/from_set.sql b/bricktracker/sql/part/list/from_set.sql deleted file mode 100644 index 2646eeb..0000000 --- a/bricktracker/sql/part/list/from_set.sql +++ /dev/null @@ -1,21 +0,0 @@ - -{% extends 'part/base/select.sql' %} - -{% block total_missing %} -IFNULL("missing"."quantity", 0) AS "total_missing", -{% endblock %} - -{% block join %} -LEFT JOIN "missing" -ON "inventory"."set_num" IS NOT DISTINCT FROM "missing"."set_num" -AND "inventory"."id" IS NOT DISTINCT FROM "missing"."id" -AND "inventory"."part_num" IS NOT DISTINCT FROM "missing"."part_num" -AND "inventory"."color_id" IS NOT DISTINCT FROM "missing"."color_id" -AND "inventory"."element_id" IS NOT DISTINCT FROM "missing"."element_id" -AND "inventory"."u_id" IS NOT DISTINCT FROM "missing"."u_id" -{% endblock %} - -{% block where %} -WHERE "inventory"."u_id" IS NOT DISTINCT FROM :u_id -AND "inventory"."set_num" IS NOT DISTINCT FROM :set_num -{% endblock %} diff --git a/bricktracker/sql/part/list/missing.sql b/bricktracker/sql/part/list/missing.sql index 8f17ae3..9d3446e 100644 --- a/bricktracker/sql/part/list/missing.sql +++ b/bricktracker/sql/part/list/missing.sql @@ -1,11 +1,11 @@ -{% extends 'part/base/select.sql' %} +{% extends 'part/base/base.sql' %} {% block total_missing %} -SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", +SUM("bricktracker_parts"."missing") AS "total_missing", {% endblock %} {% block total_sets %} -COUNT("inventory"."u_id") - COUNT("bricktracker_minifigures"."id") AS "total_sets", +COUNT("bricktracker_parts"."id") - COUNT("bricktracker_parts"."figure") AS "total_sets", {% endblock %} {% block total_minifigures %} @@ -13,24 +13,18 @@ SUM(IFNULL("bricktracker_minifigures"."quantity", 0)) AS "total_minifigures" {% endblock %} {% block join %} -INNER JOIN "missing" -ON "missing"."set_num" IS NOT DISTINCT FROM "inventory"."set_num" -AND "missing"."id" IS NOT DISTINCT FROM "inventory"."id" -AND "missing"."part_num" IS NOT DISTINCT FROM "inventory"."part_num" -AND "missing"."color_id" IS NOT DISTINCT FROM "inventory"."color_id" -AND "missing"."element_id" IS NOT DISTINCT FROM "inventory"."element_id" -AND "missing"."u_id" IS NOT DISTINCT FROM "inventory"."u_id" - LEFT JOIN "bricktracker_minifigures" -ON "inventory"."set_num" IS NOT DISTINCT FROM "bricktracker_minifigures"."figure" -AND "inventory"."u_id" IS NOT DISTINCT FROM "bricktracker_minifigures"."id" +ON "bricktracker_parts"."id" IS NOT DISTINCT FROM "bricktracker_minifigures"."id" +AND "bricktracker_parts"."figure" IS NOT DISTINCT FROM "bricktracker_minifigures"."figure" +{% endblock %} + +{% block where %} +WHERE "bricktracker_parts"."missing" > 0 {% endblock %} {% block group %} GROUP BY - "inventory"."part_num", - "inventory"."name", - "inventory"."color_id", - "inventory"."is_spare", - "inventory"."element_id" + "bricktracker_parts"."part", + "bricktracker_parts"."color", + "bricktracker_parts"."spare" {% endblock %} diff --git a/bricktracker/sql/part/list/specific.sql b/bricktracker/sql/part/list/specific.sql new file mode 100644 index 0000000..d3e291a --- /dev/null +++ b/bricktracker/sql/part/list/specific.sql @@ -0,0 +1,11 @@ + +{% extends 'part/base/base.sql' %} + +{% block total_missing %} +IFNULL("bricktracker_parts"."missing", 0) AS "total_missing", +{% endblock %} + +{% block where %} +WHERE "bricktracker_parts"."id" IS NOT DISTINCT FROM :id +AND "bricktracker_parts"."figure" IS NOT DISTINCT FROM :figure +{% endblock %} diff --git a/bricktracker/sql/part/select/generic.sql b/bricktracker/sql/part/select/generic.sql index eb7e194..a1760d6 100644 --- a/bricktracker/sql/part/select/generic.sql +++ b/bricktracker/sql/part/select/generic.sql @@ -1,40 +1,30 @@ -{% extends 'part/base/select.sql' %} +{% extends 'part/base/base.sql' %} {% block total_missing %} -SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", +SUM("bricktracker_parts"."missing") AS "total_missing", {% endblock %} {% block total_quantity %} -SUM((NOT "inventory"."is_spare") * "inventory"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)) AS "total_quantity", +SUM((NOT "bricktracker_parts"."spare") * "bricktracker_parts"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)) AS "total_quantity", {% endblock %} {% block total_spare %} -SUM("inventory"."is_spare" * "inventory"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)) AS "total_spare", +SUM("bricktracker_parts"."spare" * "bricktracker_parts"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)) AS "total_spare", {% endblock %} {% block join %} -LEFT JOIN "missing" -ON "inventory"."set_num" IS NOT DISTINCT FROM "missing"."set_num" -AND "inventory"."id" IS NOT DISTINCT FROM "missing"."id" -AND "inventory"."part_num" IS NOT DISTINCT FROM "missing"."part_num" -AND "inventory"."color_id" IS NOT DISTINCT FROM "missing"."color_id" -AND "inventory"."element_id" IS NOT DISTINCT FROM "missing"."element_id" -AND "inventory"."u_id" IS NOT DISTINCT FROM "missing"."u_id" - LEFT JOIN "bricktracker_minifigures" -ON "inventory"."set_num" IS NOT DISTINCT FROM "bricktracker_minifigures"."figure" -AND "inventory"."u_id" IS NOT DISTINCT FROM "bricktracker_minifigures"."id" +ON "bricktracker_parts"."id" IS NOT DISTINCT FROM "bricktracker_minifigures"."id" +AND "bricktracker_parts"."figure" IS NOT DISTINCT FROM "bricktracker_minifigures"."figure" {% endblock %} {% block where %} -WHERE "inventory"."part_num" IS NOT DISTINCT FROM :part_num -AND "inventory"."color_id" IS NOT DISTINCT FROM :color_id -AND "inventory"."element_id" IS NOT DISTINCT FROM :element_id +WHERE "bricktracker_parts"."part" IS NOT DISTINCT FROM :part +AND "bricktracker_parts"."color" IS NOT DISTINCT FROM :color {% endblock %} {% block group %} GROUP BY - "inventory"."part_num", - "inventory"."color_id", - "inventory"."element_id" + "bricktracker_parts"."part", + "bricktracker_parts"."color" {% endblock %} diff --git a/bricktracker/sql/part/select/specific.sql b/bricktracker/sql/part/select/specific.sql index ebdd5f5..c74a535 100644 --- a/bricktracker/sql/part/select/specific.sql +++ b/bricktracker/sql/part/select/specific.sql @@ -1,24 +1,18 @@ -{% extends 'part/base/select.sql' %} - -{% block join %} -LEFT JOIN "missing" -ON "inventory"."set_num" IS NOT DISTINCT FROM "missing"."set_num" -AND "inventory"."id" IS NOT DISTINCT FROM "missing"."id" -AND "inventory"."u_id" IS NOT DISTINCT FROM "missing"."u_id" -{% endblock %} +{% extends 'part/base/base.sql' %} {% block where %} -WHERE "inventory"."u_id" IS NOT DISTINCT FROM :u_id -AND "inventory"."set_num" IS NOT DISTINCT FROM :set_num -AND "inventory"."id" IS NOT DISTINCT FROM :id +WHERE "bricktracker_parts"."id" IS NOT DISTINCT FROM :id +AND "bricktracker_parts"."figure" IS NOT DISTINCT FROM :figure +AND "bricktracker_parts"."part" IS NOT DISTINCT FROM :part +AND "bricktracker_parts"."color" IS NOT DISTINCT FROM :color +AND "bricktracker_parts"."spare" IS NOT DISTINCT FROM :spare {% endblock %} {% block group %} GROUP BY - "inventory"."set_num", - "inventory"."id", - "inventory"."part_num", - "inventory"."color_id", - "inventory"."element_id", - "inventory"."u_id" + "bricktracker_parts"."id", + "bricktracker_parts"."figure", + "bricktracker_parts"."part", + "bricktracker_parts"."color", + "bricktracker_parts"."spare" {% endblock %} diff --git a/bricktracker/sql/part/update/missing.sql b/bricktracker/sql/part/update/missing.sql new file mode 100644 index 0000000..c1dd368 --- /dev/null +++ b/bricktracker/sql/part/update/missing.sql @@ -0,0 +1,7 @@ +UPDATE "bricktracker_parts" +SET "missing" = :missing +WHERE "bricktracker_parts"."id" IS NOT DISTINCT FROM :id +AND "bricktracker_parts"."figure" IS NOT DISTINCT FROM :figure +AND "bricktracker_parts"."part" IS NOT DISTINCT FROM :part +AND "bricktracker_parts"."color" IS NOT DISTINCT FROM :color +AND "bricktracker_parts"."spare" IS NOT DISTINCT FROM :spare diff --git a/bricktracker/sql/rebrickable/part/insert.sql b/bricktracker/sql/rebrickable/part/insert.sql new file mode 100644 index 0000000..d989258 --- /dev/null +++ b/bricktracker/sql/rebrickable/part/insert.sql @@ -0,0 +1,25 @@ +INSERT OR IGNORE INTO "rebrickable_parts" ( + "part", + "color_id", + "color_name", + "color_rgb", + "color_transparent", + "name", + "category", + "image", + "image_id", + "url", + "print" +) VALUES ( + :part, + :color_id, + :color_name, + :color_rgb, + :color_transparent, + :name, + :category, + :image, + :image_id, + :url, + :print +) diff --git a/bricktracker/sql/rebrickable/part/list.sql b/bricktracker/sql/rebrickable/part/list.sql new file mode 100644 index 0000000..026f465 --- /dev/null +++ b/bricktracker/sql/rebrickable/part/list.sql @@ -0,0 +1,13 @@ +SELECT + "rebrickable_parts"."part", + "rebrickable_parts"."color_id", + "rebrickable_parts"."color_name", + "rebrickable_parts"."color_rgb", + "rebrickable_parts"."color_transparent", + "rebrickable_parts"."name", + "rebrickable_parts"."category", + "rebrickable_parts"."image", + "rebrickable_parts"."image_id", + "rebrickable_parts"."url", + "rebrickable_parts"."print" +FROM "rebrickable_parts" diff --git a/bricktracker/sql/rebrickable/part/select.sql b/bricktracker/sql/rebrickable/part/select.sql new file mode 100644 index 0000000..54f6305 --- /dev/null +++ b/bricktracker/sql/rebrickable/part/select.sql @@ -0,0 +1,16 @@ +SELECT + "rebrickable_parts"."part", + "rebrickable_parts"."color_id", + "rebrickable_parts"."color_name", + "rebrickable_parts"."color_rgb", + "rebrickable_parts"."color_transparent", + "rebrickable_parts"."name", + "rebrickable_parts"."category", + "rebrickable_parts"."image", + "rebrickable_parts"."image_id", + "rebrickable_parts"."url", + "rebrickable_parts"."print" +FROM "rebrickable_parts" + +WHERE "rebrickable_minifigures"."part" IS NOT DISTINCT FROM :figure +AND "rebrickable_minifigures"."color_id" IS NOT DISTINCT FROM :color diff --git a/bricktracker/sql/schema/drop.sql b/bricktracker/sql/schema/drop.sql index 1d39d99..78ea32c 100644 --- a/bricktracker/sql/schema/drop.sql +++ b/bricktracker/sql/schema/drop.sql @@ -1,15 +1,20 @@ BEGIN transaction; DROP TABLE IF EXISTS "bricktracker_minifigures"; +DROP TABLE IF EXISTS "bricktracker_parts"; DROP TABLE IF EXISTS "bricktracker_sets"; DROP TABLE IF EXISTS "bricktracker_set_checkboxes"; DROP TABLE IF EXISTS "bricktracker_set_statuses"; +DROP TABLE IF EXISTS "bricktracker_set_storages"; DROP TABLE IF EXISTS "bricktracker_wishes"; DROP TABLE IF EXISTS "inventory"; +DROP TABLE IF EXISTS "inventory_old"; DROP TABLE IF EXISTS "minifigures"; DROP TABLE IF EXISTS "minifigures_old"; DROP TABLE IF EXISTS "missing"; +DROP TABLE IF EXISTS "missing_old"; DROP TABLE IF EXISTS "rebrickable_minifigures"; +DROP TABLE IF EXISTS "rebrickable_parts"; DROP TABLE IF EXISTS "rebrickable_sets"; DROP TABLE IF EXISTS "sets"; DROP TABLE IF EXISTS "sets_old"; diff --git a/bricktracker/sql/set/base/full.sql b/bricktracker/sql/set/base/full.sql index 092e487..70730ff 100644 --- a/bricktracker/sql/set/base/full.sql +++ b/bricktracker/sql/set/base/full.sql @@ -21,13 +21,13 @@ ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_statuses"."id -- LEFT JOIN + SELECT to avoid messing the total LEFT JOIN ( SELECT - "missing"."u_id", - SUM("missing"."quantity") AS "total" - FROM "missing" + "bricktracker_parts"."id", + SUM("bricktracker_parts"."missing") AS "total" + FROM "bricktracker_parts" {% block where_missing %}{% endblock %} - GROUP BY "u_id" + GROUP BY "bricktracker_parts"."id" ) "missing_join" -ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "missing_join"."u_id" +ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "missing_join"."id" -- LEFT JOIN + SELECT to avoid messing the total LEFT JOIN ( diff --git a/bricktracker/sql/set/delete/set.sql b/bricktracker/sql/set/delete/set.sql index b477f81..49b0e88 100644 --- a/bricktracker/sql/set/delete/set.sql +++ b/bricktracker/sql/set/delete/set.sql @@ -12,10 +12,7 @@ WHERE "bricktracker_set_statuses"."id" IS NOT DISTINCT FROM '{{ id }}'; DELETE FROM "bricktracker_minifigures" WHERE "bricktracker_minifigures"."id" IS NOT DISTINCT FROM '{{ id }}'; -DELETE FROM "missing" -WHERE "missing"."u_id" IS NOT DISTINCT FROM '{{ id }}'; - -DELETE FROM "inventory" -WHERE "inventory"."u_id" IS NOT DISTINCT FROM '{{ id }}'; +DELETE FROM "bricktracker_parts" +WHERE "bricktracker_parts"."id" IS NOT DISTINCT FROM '{{ id }}'; COMMIT; \ No newline at end of file diff --git a/bricktracker/sql/set/list/missing_minifigure.sql b/bricktracker/sql/set/list/missing_minifigure.sql index 2f19bfe..51a615d 100644 --- a/bricktracker/sql/set/list/missing_minifigure.sql +++ b/bricktracker/sql/set/list/missing_minifigure.sql @@ -2,12 +2,10 @@ {% block where %} WHERE "bricktracker_sets"."id" IN ( - SELECT - "missing"."u_id" - FROM "missing" - - WHERE "missing"."set_num" IS NOT DISTINCT FROM :figure - - GROUP BY "missing"."u_id" + SELECT "bricktracker_parts"."id" + FROM "bricktracker_parts" + WHERE "bricktracker_parts"."figure" IS NOT DISTINCT FROM :figure + AND "bricktracker_parts"."missing" > 0 + GROUP BY "bricktracker_parts"."id" ) {% endblock %} diff --git a/bricktracker/sql/set/list/missing_part.sql b/bricktracker/sql/set/list/missing_part.sql index 781754c..9438b67 100644 --- a/bricktracker/sql/set/list/missing_part.sql +++ b/bricktracker/sql/set/list/missing_part.sql @@ -2,14 +2,11 @@ {% block where %} WHERE "bricktracker_sets"."id" IN ( - SELECT - "missing"."u_id" - FROM "missing" - - WHERE "missing"."color_id" IS NOT DISTINCT FROM :color_id - AND "missing"."element_id" IS NOT DISTINCT FROM :element_id - AND "missing"."part_num" IS NOT DISTINCT FROM :part_num - - GROUP BY "missing"."u_id" + SELECT "bricktracker_parts"."id" + FROM "bricktracker_parts" + WHERE "bricktracker_parts"."part" IS NOT DISTINCT FROM :part + AND "bricktracker_parts"."color" IS NOT DISTINCT FROM :color + AND "bricktracker_parts"."missing" > 0 + GROUP BY "bricktracker_parts"."id" ) {% endblock %} diff --git a/bricktracker/sql/set/list/using_minifigure.sql b/bricktracker/sql/set/list/using_minifigure.sql index 711866b..00a1fb0 100644 --- a/bricktracker/sql/set/list/using_minifigure.sql +++ b/bricktracker/sql/set/list/using_minifigure.sql @@ -2,12 +2,9 @@ {% block where %} WHERE "bricktracker_sets"."id" IN ( - SELECT - "inventory"."u_id" - FROM "inventory" - - WHERE "inventory"."set_num" IS NOT DISTINCT FROM :figure - - GROUP BY "inventory"."u_id" + SELECT "bricktracker_parts"."id" + FROM "bricktracker_parts" + WHERE "bricktracker_parts"."figure" IS NOT DISTINCT FROM :figure + GROUP BY "bricktracker_parts"."id" ) {% endblock %} diff --git a/bricktracker/sql/set/list/using_part.sql b/bricktracker/sql/set/list/using_part.sql index 8877cff..a037173 100644 --- a/bricktracker/sql/set/list/using_part.sql +++ b/bricktracker/sql/set/list/using_part.sql @@ -2,14 +2,10 @@ {% block where %} WHERE "bricktracker_sets"."id" IN ( - SELECT - "inventory"."u_id" - FROM "inventory" - - WHERE "inventory"."color_id" IS NOT DISTINCT FROM :color_id - AND "inventory"."element_id" IS NOT DISTINCT FROM :element_id - AND "inventory"."part_num" IS NOT DISTINCT FROM :part_num - - GROUP BY "inventory"."u_id" + SELECT "bricktracker_parts"."id" + FROM "bricktracker_parts" + WHERE "bricktracker_parts"."part" IS NOT DISTINCT FROM :part + AND "bricktracker_parts"."color" IS NOT DISTINCT FROM :color + GROUP BY "bricktracker_parts"."id" ) {% endblock %} diff --git a/bricktracker/sql/set/select/full.sql b/bricktracker/sql/set/select/full.sql index 80d1161..0d12ae8 100644 --- a/bricktracker/sql/set/select/full.sql +++ b/bricktracker/sql/set/select/full.sql @@ -1,7 +1,7 @@ {% extends 'set/base/full.sql' %} {% block where_missing %} -WHERE "missing"."u_id" IS NOT DISTINCT FROM :id +WHERE "bricktracker_parts"."id" IS NOT DISTINCT FROM :id {% endblock %} {% block where_minifigures %} diff --git a/bricktracker/sql_counter.py b/bricktracker/sql_counter.py index f2d1cc5..2e5c072 100644 --- a/bricktracker/sql_counter.py +++ b/bricktracker/sql_counter.py @@ -3,15 +3,20 @@ from typing import Tuple # Some table aliases to make it look cleaner (id: (name, icon)) ALIASES: dict[str, Tuple[str, str]] = { 'bricktracker_minifigures': ('Bricktracker minifigures', 'group-line'), + 'bricktracker_parts': ('Bricktracker parts', 'shapes-line'), 'bricktracker_set_checkboxes': ('Bricktracker set checkboxes', 'checkbox-line'), # noqa: E501 - 'bricktracker_set_statuses': ('Bricktracker sets status', 'checkbox-line'), + 'bricktracker_set_statuses': ('Bricktracker sets status', 'checkbox-circle-line'), # noqa: E501 + 'bricktracker_set_storages': ('Bricktracker sets storages', 'archive-2-line'), # noqa: E501 'bricktracker_sets': ('Bricktracker sets', 'hashtag'), 'bricktracker_wishes': ('Bricktracker wishes', 'gift-line'), 'inventory': ('Parts', 'shapes-line'), + 'inventory_old': ('Parts (legacy)', 'shapes-line'), 'minifigures': ('Minifigures', 'group-line'), 'minifigures_old': ('Minifigures (legacy)', 'group-line'), 'missing': ('Missing', 'error-warning-line'), + 'missing_old': ('Missing (legacy)', 'error-warning-line'), 'rebrickable_minifigures': ('Rebrickable minifigures', 'group-line'), + 'rebrickable_parts': ('Rebrickable parts', 'shapes-line'), 'rebrickable_sets': ('Rebrickable sets', 'hashtag'), 'sets': ('Sets', 'hashtag'), 'sets_old': ('Sets (legacy)', 'hashtag'), diff --git a/bricktracker/version.py b/bricktracker/version.py index 4424778..172ecf1 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] = 9 +__database_version__: Final[int] = 11 diff --git a/bricktracker/views/part.py b/bricktracker/views/part.py index 2505122..5f20997 100644 --- a/bricktracker/views/part.py +++ b/bricktracker/views/part.py @@ -30,31 +30,26 @@ def missing() -> str: # Part details -@part_page.route('///details', defaults={'element': None}, methods=['GET']) # noqa: E501 -@part_page.route('////details', methods=['GET']) # noqa: E501 +@part_page.route('///details', methods=['GET']) # noqa: E501 @exception_handler(__file__) -def details(*, number: str, color: int, element: int | None) -> str: +def details(*, part: str, color: int) -> str: return render_template( 'part.html', - item=BrickPart().select_generic(number, color, element_id=element), + item=BrickPart().select_generic(part, color), sets_using=BrickSetList().using_part( - number, - color, - element_id=element + part, + color ), sets_missing=BrickSetList().missing_part( - number, - color, - element_id=element + part, + color ), minifigures_using=BrickMinifigureList().using_part( - number, - color, - element_id=element + part, + color ), minifigures_missing=BrickMinifigureList().missing_part( - number, - color, - element_id=element + part, + color ), ) diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py index 3e2304c..0b8d843 100644 --- a/bricktracker/views/set.py +++ b/bricktracker/views/set.py @@ -107,16 +107,34 @@ def details(*, id: str) -> str: ) -# Update the missing pieces of a minifig part -@set_page.route('//minifigures/
/parts//missing', methods=['POST']) # noqa: E501 +# Update the missing pieces of a part +@set_page.route('//parts////missing', defaults={'figure': None}, methods=['POST']) # noqa: E501 +@set_page.route('//minifigures/
/parts////missing', methods=['POST']) # noqa: E501 @login_required @exception_handler(__file__, json=True) -def missing_minifigure_part(*, id: str, figure: str, part: str) -> Response: +def missing_part( + *, + id: str, + figure: str | None, + part: str, + color: int, + spare: int, +) -> Response: + from pprint import pprint + pprint(locals()) + brickset = BrickSet().select_specific(id) - brickminifigure = BrickMinifigure().select_specific(brickset, figure) + + if figure is not None: + brickminifigure = BrickMinifigure().select_specific(brickset, figure) + else: + brickminifigure = None + brickpart = BrickPart().select_specific( brickset, part, + color, + spare, minifigure=brickminifigure, ) @@ -125,35 +143,14 @@ def missing_minifigure_part(*, id: str, figure: str, part: str) -> Response: brickpart.update_missing(missing) # Info - logger.info('Set {set} ({id}): updated minifigure ({figure}) part ({part}) missing count to {missing}'.format( # noqa: E501 + logger.info('Set {set} ({id}): updated part ({part} color: {color}, spare: {spare}, minifigure: {figure}) missing count to {missing}'.format( # noqa: E501 set=brickset.fields.set, id=brickset.fields.id, - figure=brickminifigure.fields.figure, - part=brickpart.fields.id, - missing=missing, - )) - - return jsonify({'missing': missing}) - - -# Update the missing pieces of a part -@set_page.route('//parts//missing', methods=['POST']) -@login_required -@exception_handler(__file__, json=True) -def missing_part(*, id: str, part: str) -> Response: - brickset = BrickSet().select_specific(id) - brickpart = BrickPart().select_specific(brickset, part) - - missing = request.json.get('missing', '') # type: ignore - - brickpart.update_missing(missing) - - # Info - logger.info('Set {set} ({id}): updated part ({part}) missing count to {missing}'.format( # noqa: E501 - set=brickset.fields.set, - id=brickset.fields.id, - part=brickpart.fields.id, - missing=missing, + figure=figure, + part=brickpart.fields.part, + color=brickpart.fields.color, + spare=brickpart.fields.spare, + missing=brickpart.fields.missing, )) return jsonify({'missing': missing}) diff --git a/templates/macro/card.html b/templates/macro/card.html index 3b52220..1cc92ac 100644 --- a/templates/macro/card.html +++ b/templates/macro/card.html @@ -1,10 +1,10 @@ -{% macro header(item, name, solo=false, number=none, color=none, icon='hashtag') %} +{% macro header(item, name, solo=false, identifier=none, color=none, icon='hashtag') %}
{% if not solo %} {% endif %}
- {% if number %}{{ number }}{% endif %} + {% if identifier %}{{ identifier }}{% endif %} {% if color %} {{ color }}{% endif %} {{ name }}
diff --git a/templates/minifigure/card.html b/templates/minifigure/card.html index b1949ef..1b70193 100644 --- a/templates/minifigure/card.html +++ b/templates/minifigure/card.html @@ -3,7 +3,7 @@ {% import 'macro/card.html' as card %}
- {{ card.header(item, item.fields.name, solo=solo, number=item.fields.number, icon='user-line') }} + {{ card.header(item, item.fields.name, solo=solo, identifier=item.fields.figure, icon='user-line') }} {{ card.image(item, solo=solo, last=last, caption=item.fields.name, alt=item.fields.figure, medium=true) }}
{% if last %} diff --git a/templates/part/card.html b/templates/part/card.html index 2f86664..8c23b59 100644 --- a/templates/part/card.html +++ b/templates/part/card.html @@ -3,8 +3,8 @@ {% import 'macro/card.html' as card %}
- {{ card.header(item, item.fields.name, solo=solo, number=item.fields.part_num, color=item.fields.color_name, icon='shapes-line') }} - {{ card.image(item, solo=solo, last=last, caption=item.fields.name, alt=item.fields.part_img_url_id, medium=true) }} + {{ card.header(item, item.fields.name, solo=solo, identifier=item.fields.part, color=item.fields.color, icon='shapes-line') }} + {{ card.image(item, solo=solo, last=last, caption=item.fields.name, alt=item.fields.image_id, medium=true) }}
{{ badge.total_sets(sets_using | length, solo=solo, last=last) }} {{ badge.total_minifigures(minifigures_using | length, solo=solo, last=last) }} diff --git a/templates/part/table.html b/templates/part/table.html index 1fca264..78d0efe 100644 --- a/templates/part/table.html +++ b/templates/part/table.html @@ -6,10 +6,10 @@
{% for item in table_collection %} - {{ table.image(item.url_for_image(), caption=item.fields.name, alt=item.fields.part_num, accordion=solo) }} + {{ table.image(item.url_for_image(), caption=item.fields.name, alt=item.fields.part, accordion=solo) }} - + {% if not no_quantity %} {% if all %} From e033dec988149c90f0d431d5fb10cf6fe3f61065 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Wed, 29 Jan 2025 10:37:22 +0100 Subject: [PATCH 032/154] Use data-sort to sort colums with complex data --- templates/part/table.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/part/table.html b/templates/part/table.html index f9f8c34..87b02f6 100644 --- a/templates/part/table.html +++ b/templates/part/table.html @@ -7,7 +7,7 @@ {% for item in table_collection %} {{ table.image(item.url_for_image(), caption=item.fields.name, alt=item.fields.part, accordion=solo) }} - - From a2aafbf93a984d08443ac16bf3783ae867197da6 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Wed, 29 Jan 2025 11:43:18 +0100 Subject: [PATCH 033/154] Visual fix for Any/No color --- static/styles.css | 13 +++++++++++++ templates/macro/card.html | 2 +- templates/part/table.html | 2 +- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/static/styles.css b/static/styles.css index af00525..ee82ffe 100644 --- a/static/styles.css +++ b/static/styles.css @@ -64,4 +64,17 @@ display: inline-block; width: 15px; height: 15px; +} + +.color-rgb-table { + width: 20px !important; + height: 20px !important; +} + +.color-any { + background: + linear-gradient(217deg, rgb(255 0 0 / 80%), rgb(255 0 0 / 0%) 70.71%), + linear-gradient(127deg, rgb(0 255 0 / 80%), rgb(0 255 0 / 0%) 70.71%), + linear-gradient(336deg, rgb(0 0 255 / 80%), rgb(0 0 255 / 0%) 70.71%) + ; } \ No newline at end of file diff --git a/templates/macro/card.html b/templates/macro/card.html index 7f2924f..3c26de7 100644 --- a/templates/macro/card.html +++ b/templates/macro/card.html @@ -8,7 +8,7 @@ {% if item.fields.color_name %} {% if item.fields.color_rgb %} - + {% endif %} {{ item.fields.color_name }} diff --git a/templates/part/table.html b/templates/part/table.html index 87b02f6..8e1da01 100644 --- a/templates/part/table.html +++ b/templates/part/table.html @@ -16,7 +16,7 @@ {% endif %} {% if not no_quantity %} From 468cc7ede9e0ec719a7f133ca9d0929665ab5c3c Mon Sep 17 00:00:00 2001 From: Gregoo Date: Wed, 29 Jan 2025 14:03:48 +0100 Subject: [PATCH 034/154] Display prints based on a part --- bricktracker/part_list.py | 37 ++++++++++++++++++++++- bricktracker/sql/part/list/from_print.sql | 17 +++++++++++ bricktracker/views/part.py | 5 ++- templates/macro/card.html | 20 ++++++------ templates/part/card.html | 27 ++++++++++------- 5 files changed, 84 insertions(+), 22 deletions(-) create mode 100644 bricktracker/sql/part/list/from_print.sql diff --git a/bricktracker/part_list.py b/bricktracker/part_list.py index 667c26e..833ae61 100644 --- a/bricktracker/part_list.py +++ b/bricktracker/part_list.py @@ -26,6 +26,7 @@ class BrickPartList(BrickRecordList[BrickPart]): last_query: str = 'part/list/last' minifigure_query: str = 'part/list/from_minifigure' missing_query: str = 'part/list/missing' + print_query: str = 'part/list/from_print' select_query: str = 'part/list/specific' def __init__(self, /): @@ -103,6 +104,40 @@ class BrickPartList(BrickRecordList[BrickPart]): return self + # Load generic parts from a print + def from_print( + self, + brickpart: BrickPart, + /, + ) -> Self: + # Save the part and print + if brickpart.fields.print is not None: + self.fields.print = brickpart.fields.print + else: + self.fields.print = brickpart.fields.part + + self.fields.part = brickpart.fields.part + self.fields.color = brickpart.fields.color + + # Load the parts from the database + for record in self.select( + override_query=self.print_query, + order=self.order + ): + part = BrickPart( + record=record, + ) + + if ( + current_app.config['SKIP_SPARE_PARTS'] and + part.fields.spare + ): + continue + + self.records.append(part) + + return self + # Load missing parts def missing(self, /) -> Self: for record in self.select( @@ -117,7 +152,7 @@ class BrickPartList(BrickRecordList[BrickPart]): # Return a dict with common SQL parameters for a parts list def sql_parameters(self, /) -> dict[str, Any]: - parameters: dict[str, Any] = {} + parameters: dict[str, Any] = super().sql_parameters() # Set id if self.brickset is not None: diff --git a/bricktracker/sql/part/list/from_print.sql b/bricktracker/sql/part/list/from_print.sql new file mode 100644 index 0000000..f996864 --- /dev/null +++ b/bricktracker/sql/part/list/from_print.sql @@ -0,0 +1,17 @@ + +{% extends 'part/base/base.sql' %} + +{% block total_missing %} +{% endblock %} + +{% block where %} +WHERE "rebrickable_parts"."print" IS NOT DISTINCT FROM :print +AND "bricktracker_parts"."color" IS NOT DISTINCT FROM :color +AND "bricktracker_parts"."part" IS DISTINCT FROM :part +{% endblock %} + +{% block group %} +GROUP BY + "bricktracker_parts"."part", + "bricktracker_parts"."color" +{% endblock %} diff --git a/bricktracker/views/part.py b/bricktracker/views/part.py index 5f20997..dbcfe0d 100644 --- a/bricktracker/views/part.py +++ b/bricktracker/views/part.py @@ -33,9 +33,11 @@ def missing() -> str: @part_page.route('///details', methods=['GET']) # noqa: E501 @exception_handler(__file__) def details(*, part: str, color: int) -> str: + brickpart = BrickPart().select_generic(part, color) + return render_template( 'part.html', - item=BrickPart().select_generic(part, color), + item=brickpart, sets_using=BrickSetList().using_part( part, color @@ -52,4 +54,5 @@ def details(*, part: str, color: int) -> str: part, color ), + similar_prints=BrickPartList().from_print(brickpart) ) diff --git a/templates/macro/card.html b/templates/macro/card.html index 3c26de7..ae8bce0 100644 --- a/templates/macro/card.html +++ b/templates/macro/card.html @@ -5,16 +5,18 @@ {% endif %}
{% if identifier %}{{ identifier }}{% endif %} - {% if item.fields.color_name %} - - {% if item.fields.color_rgb %} - - {% endif %} - {{ item.fields.color_name }} - + {% if solo %} + {% if item.fields.color_name %} + + {% if item.fields.color_rgb %} + + {% endif %} + {{ item.fields.color_name }} + + {% endif %} + {% if item.fields.color_transparent %} Transparent{% endif %} + {% if item.fields.print %} Print{% endif %} {% endif %} - {% if item.fields.color_transparent %} Transparent{% endif %} - {% if item.fields.print %} Print{% endif %} {{ name }}
{% if not solo %} diff --git a/templates/part/card.html b/templates/part/card.html index 9a14f4a..43dfcdc 100644 --- a/templates/part/card.html +++ b/templates/part/card.html @@ -2,23 +2,28 @@ {% import 'macro/badge.html' as badge %} {% import 'macro/card.html' as card %} -
+
{{ card.header(item, item.fields.name, solo=solo, identifier=item.fields.part, icon='shapes-line') }} {{ card.image(item, solo=solo, last=last, caption=item.fields.name, alt=item.fields.image_id, medium=true) }} -
+
{{ badge.total_sets(sets_using | length, solo=solo, last=last) }} {{ badge.total_minifigures(minifigures_using | length, solo=solo, last=last) }} {{ badge.total_quantity(item.fields.total_quantity, solo=solo, last=last) }} {{ badge.total_spare(item.fields.total_spare, solo=solo, last=last) }} {{ badge.total_missing(item.fields.total_missing, solo=solo, last=last) }} - {{ badge.rebrickable(item, solo=solo, last=last) }} - {{ badge.bricklink(item, solo=solo, last=last) }} + {% if not last %} + {{ badge.rebrickable(item, solo=solo, last=last) }} + {{ badge.bricklink(item, solo=solo, last=last) }} + {% endif %}
-
- {{ accordion.cards(sets_using, 'Sets using this part', 'sets-using-inventory', 'part-details', 'set/card.html', icon='hashtag') }} - {{ accordion.cards(sets_missing, 'Sets missing this part', 'sets-missing-inventory', 'part-details', 'set/card.html', icon='error-warning-line') }} - {{ accordion.cards(minifigures_using, 'Minifigures using this part', 'minifigures-using-inventory', 'part-details', 'minifigure/card.html', icon='group-line') }} - {{ accordion.cards(minifigures_missing, 'Minifigures missing this part', 'minifigures-missing-inventory', 'part-details', 'minifigure/card.html', icon='error-warning-line') }} -
- + {% if solo %} +
+ {{ accordion.cards(sets_using, 'Sets using this part', 'sets-using-inventory', 'part-details', 'set/card.html', icon='hashtag') }} + {{ accordion.cards(sets_missing, 'Sets missing this part', 'sets-missing-inventory', 'part-details', 'set/card.html', icon='error-warning-line') }} + {{ accordion.cards(minifigures_using, 'Minifigures using this part', 'minifigures-using-inventory', 'part-details', 'minifigure/card.html', icon='group-line') }} + {{ accordion.cards(minifigures_missing, 'Minifigures missing this part', 'minifigures-missing-inventory', 'part-details', 'minifigure/card.html', icon='error-warning-line') }} + {{ accordion.cards(similar_prints, 'Prints using the same base', 'similar-prints', 'part-details', 'part/card.html', icon='palette-line') }} +
+ + {% endif %}
From cf11e4d71878588dfc2bb1b1090b35f99010a780 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Wed, 29 Jan 2025 15:12:10 +0100 Subject: [PATCH 035/154] Move the dynamic input into a macro --- templates/macro/form.html | 15 +++++++++++++++ templates/part/table.html | 11 ++--------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/templates/macro/form.html b/templates/macro/form.html index f9c5a5d..cd126e9 100644 --- a/templates/macro/form.html +++ b/templates/macro/form.html @@ -15,3 +15,18 @@ {{ text }} {% endif %} {% endmacro %} + +{% macro input(id, html_id, url, value) %} + + {% if g.login.is_authenticated() %} + + {% else %} + + {% endif %} +{% endmacro %} diff --git a/templates/part/table.html b/templates/part/table.html index 8e1da01..b7139ed 100644 --- a/templates/part/table.html +++ b/templates/part/table.html @@ -1,3 +1,4 @@ +{% import 'macro/form.html' as form %} {% import 'macro/table.html' as table %}
@@ -32,15 +33,7 @@ {{ item.fields.total_missing }} {% else %}
- {% if g.login.is_authenticated() %} - - - {% else %} - - - {% endif %} + {{ form.input(item.fields.id, item.html_id(), item.url_for_missing(), item.fields.total_missing) }}
{% endif %} From f44192a1148b04ddc74f8bf4d3a30658280638eb Mon Sep 17 00:00:00 2001 From: Gregoo Date: Wed, 29 Jan 2025 15:57:19 +0100 Subject: [PATCH 036/154] Add visually hidden label for dynamic input, move read-only logic in the macro --- bricktracker/part.py | 2 +- static/styles.css | 2 +- templates/macro/form.html | 13 ++++++++++--- templates/part/table.html | 10 ++-------- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/bricktracker/part.py b/bricktracker/part.py index af901b2..c847db5 100644 --- a/bricktracker/part.py +++ b/bricktracker/part.py @@ -73,7 +73,7 @@ class BrickPart(RebrickablePart): # A identifier for HTML component def html_id(self) -> str: - components: list[str] = [] + components: list[str] = ['part'] if self.fields.figure is not None: components.append(self.fields.figure) diff --git a/static/styles.css b/static/styles.css index ee82ffe..0651e20 100644 --- a/static/styles.css +++ b/static/styles.css @@ -46,7 +46,7 @@ object-fit:contain; } -.table-td-missing { +.table-td-input { max-width: 150px; } diff --git a/templates/macro/form.html b/templates/macro/form.html index cd126e9..f88b673 100644 --- a/templates/macro/form.html +++ b/templates/macro/form.html @@ -16,8 +16,13 @@ {% endif %} {% endmacro %} -{% macro input(id, html_id, url, value) %} - {{ name }} +
+ {% if g.login.is_authenticated() %} - + {% else %} + {% endif %} +
{% endif %} {% endmacro %} diff --git a/templates/part/table.html b/templates/part/table.html index b7139ed..692a7dc 100644 --- a/templates/part/table.html +++ b/templates/part/table.html @@ -28,14 +28,8 @@ {% endif %} {% endif %} {% if not no_missing %} -
{% endif %} {% if all %} From e2b8b51db8d4a5e37a77df6dd936092c10b4f9d2 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Wed, 29 Jan 2025 15:58:41 +0100 Subject: [PATCH 037/154] Move dynamic input to BrickChanger --- bricktracker/part.py | 4 +++- bricktracker/views/set.py | 6 ++---- static/scripts/changer.js | 8 ++++++-- templates/macro/form.html | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/bricktracker/part.py b/bricktracker/part.py index c847db5..d81bb85 100644 --- a/bricktracker/part.py +++ b/bricktracker/part.py @@ -143,7 +143,9 @@ class BrickPart(RebrickablePart): return self # Update the missing part - def update_missing(self, missing: Any, /) -> None: + def update_missing(self, json: Any | None, /) -> None: + missing = json.get('value', '') # type: ignore + # We need a positive integer try: missing = int(missing) diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py index 44eabed..d82896c 100644 --- a/bricktracker/views/set.py +++ b/bricktracker/views/set.py @@ -137,9 +137,7 @@ def missing_part( minifigure=brickminifigure, ) - missing = request.json.get('missing', '') # type: ignore - - brickpart.update_missing(missing) + brickpart.update_missing(request.json) # Info logger.info('Set {set} ({id}): updated part ({part} color: {color}, spare: {spare}, minifigure: {figure}) missing count to {missing}'.format( # noqa: E501 @@ -152,7 +150,7 @@ def missing_part( missing=brickpart.fields.missing, )) - return jsonify({'missing': missing}) + return jsonify({'missing': brickpart.fields.missing}) # Refresh a set diff --git a/static/scripts/changer.js b/static/scripts/changer.js index 224e24b..e3a32f8 100644 --- a/static/scripts/changer.js +++ b/static/scripts/changer.js @@ -15,8 +15,10 @@ class BrickChanger { // Register an event depending on the type if (this.html_type == "checkbox") { var listener = "change"; + } else if(this.html_type == "text") { + var listener = "change"; } else { - var listener = "click"; + throw Error("Unsupported input type for BrickChanger"); } this.html_element.addEventListener(listener, ((changer) => (e) => { @@ -70,8 +72,10 @@ class BrickChanger { // Grab the value depending on the type if (this.html_type == "checkbox") { var value = this.html_element.checked; - } else { + } else if(this.html_type == "text") { var value = this.html_element.value; + } else { + throw Error("Unsupported input type for BrickChanger"); } const response = await fetch(this.url, { diff --git a/templates/macro/form.html b/templates/macro/form.html index f88b673..4844822 100644 --- a/templates/macro/form.html +++ b/templates/macro/form.html @@ -24,7 +24,7 @@
Date: Wed, 29 Jan 2025 15:59:00 +0100 Subject: [PATCH 038/154] Fix missing logic to handle empty string from dynamic input --- bricktracker/part.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bricktracker/part.py b/bricktracker/part.py index d81bb85..495059f 100644 --- a/bricktracker/part.py +++ b/bricktracker/part.py @@ -148,6 +148,9 @@ class BrickPart(RebrickablePart): # We need a positive integer try: + if missing == '': + missing = 0 + missing = int(missing) if missing < 0: From f016e65b69bc94d1ce6aeccc1f6be4700570810a Mon Sep 17 00:00:00 2001 From: Gregoo Date: Wed, 29 Jan 2025 16:11:48 +0100 Subject: [PATCH 039/154] Rename read_only_missing to a more generic read_only --- templates/macro/accordion.html | 2 +- templates/minifigure.html | 2 +- templates/minifigure/card.html | 2 +- templates/part/table.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/macro/accordion.html b/templates/macro/accordion.html index fbfd01b..417ef47 100644 --- a/templates/macro/accordion.html +++ b/templates/macro/accordion.html @@ -43,7 +43,7 @@ {% endif %} {% endmacro %} -{% macro table(table_collection, title, id, parent, target, quantity=none, icon=none, image=none, alt=none, details=none, no_missing=none, read_only_missing=none) %} +{% macro table(table_collection, title, id, parent, target, quantity=none, icon=none, image=none, alt=none, details=none, no_missing=none, read_only=none) %} {% set size=table_collection | length %} {% if size %} {{ header(title, id, parent, quantity=quantity, icon=icon, class='p-0', image=image, alt=alt) }} diff --git a/templates/minifigure.html b/templates/minifigure.html index 52ed7a1..33b6322 100644 --- a/templates/minifigure.html +++ b/templates/minifigure.html @@ -6,7 +6,7 @@
- {% with solo=true, read_only_missing=true %} + {% with solo=true, read_only=true %} {% include 'minifigure/card.html' %} {% endwith %}
diff --git a/templates/minifigure/card.html b/templates/minifigure/card.html index 1b70193..446c164 100644 --- a/templates/minifigure/card.html +++ b/templates/minifigure/card.html @@ -19,7 +19,7 @@
{% if solo %}
- {{ accordion.table(item.generic_parts(), 'Parts', item.fields.figure, 'minifigure-details', 'part/table.html', icon='shapes-line', alt=item.fields.figure, read_only_missing=read_only_missing)}} + {{ accordion.table(item.generic_parts(), 'Parts', item.fields.figure, 'minifigure-details', 'part/table.html', icon='shapes-line', alt=item.fields.figure, read_only=read_only)}} {{ accordion.cards(using, 'Sets using this minifigure', 'using-inventory', 'minifigure-details', 'set/card.html', icon='hashtag') }} {{ accordion.cards(missing, 'Sets missing parts of this minifigure', 'missing-inventory', 'minifigure-details', 'set/card.html', icon='error-warning-line') }}
diff --git a/templates/part/table.html b/templates/part/table.html index 692a7dc..cd16f9b 100644 --- a/templates/part/table.html +++ b/templates/part/table.html @@ -29,7 +29,7 @@ {% endif %} {% if not no_missing %}
{% endif %} {% if all %} From cb58ef83cccbfbb7966e4af51cc1eccef003b87f Mon Sep 17 00:00:00 2001 From: Gregoo Date: Wed, 29 Jan 2025 16:14:52 +0100 Subject: [PATCH 040/154] Add a clear button for dynamic input --- static/scripts/changer.js | 8 ++++++++ templates/macro/form.html | 29 +++++++++++++++-------------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/static/scripts/changer.js b/static/scripts/changer.js index e3a32f8..8cb005f 100644 --- a/static/scripts/changer.js +++ b/static/scripts/changer.js @@ -3,6 +3,7 @@ class BrickChanger { constructor(prefix, id, url, parent = undefined) { this.prefix = prefix this.html_element = document.getElementById(`${prefix}-${id}`); + this.html_clear = document.getElementById(`clear-${prefix}-${id}`); this.html_status = document.getElementById(`status-${prefix}-${id}`); this.html_type = this.html_element.getAttribute("type"); this.url = url; @@ -24,6 +25,13 @@ class BrickChanger { this.html_element.addEventListener(listener, ((changer) => (e) => { changer.change(); })(this)); + + if (this.html_clear) { + this.html_clear.addEventListener("click", ((changer) => (e) => { + changer.html_element.value = ""; + changer.change(); + })(this)); + } } // Clean the status diff --git a/templates/macro/form.html b/templates/macro/form.html index 4844822..499c220 100644 --- a/templates/macro/form.html +++ b/templates/macro/form.html @@ -1,12 +1,12 @@ {% macro checkbox(prefix, id, text, url, checked, delete=false) %} {% if g.login.is_authenticated() %} + {% if not delete %} + data-changer-id="{{ id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ url }}" data-changer-parent="set" + {% else %} + disabled + {% endif %} + autocomplete="off"> @@ -23,16 +23,17 @@
- {% if g.login.is_authenticated() %} + {% else %} + disabled + {% endif %} + autocomplete="off"> + {% if g.login.is_authenticated() %} - {% else %} - + + {% else %} + {% endif %}
{% endif %} From 130b3fa84a34a9c33a77bd0b67e470bb02af4d37 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Wed, 29 Jan 2025 17:35:15 +0100 Subject: [PATCH 041/154] Fix undefined id variable used when a checkbox does not exist --- bricktracker/set_checkbox_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bricktracker/set_checkbox_list.py b/bricktracker/set_checkbox_list.py index 0f32240..6564e8d 100644 --- a/bricktracker/set_checkbox_list.py +++ b/bricktracker/set_checkbox_list.py @@ -58,7 +58,7 @@ class BrickSetCheckboxList(BrickRecordList[BrickSetCheckbox]): if id not in self.checkboxes: raise NotFoundException( 'Checkbox with ID {id} was not found in the database'.format( - id=self.fields.id, + id=id, ), ) From b8d600333913fc4083f212f8f20108a17b6d24b8 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Wed, 29 Jan 2025 17:35:54 +0100 Subject: [PATCH 042/154] Add a tooltip with an error message on the visual status --- static/scripts/changer.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/static/scripts/changer.js b/static/scripts/changer.js index 8cb005f..89f012d 100644 --- a/static/scripts/changer.js +++ b/static/scripts/changer.js @@ -1,10 +1,12 @@ // Generic state changer with visual feedback +// Tooltips require boostrap.Tooltip class BrickChanger { constructor(prefix, id, url, parent = undefined) { this.prefix = prefix this.html_element = document.getElementById(`${prefix}-${id}`); this.html_clear = document.getElementById(`clear-${prefix}-${id}`); this.html_status = document.getElementById(`status-${prefix}-${id}`); + this.html_status_tooltip = undefined; this.html_type = this.html_element.getAttribute("type"); this.url = url; @@ -46,14 +48,24 @@ class BrickChanger { if (to_remove.length) { this.html_status.classList.remove(...to_remove); } + + if (this.html_status_tooltip) { + this.html_status_tooltip.dispose(); + this.html_status_tooltip = undefined; + } } } // Set the status to Error - status_error() { + status_error(message) { if (this.html_status) { this.status_clean(); this.html_status.classList.add("ri-alert-line", "text-danger"); + + this.html_status_tooltip = new bootstrap.Tooltip(this.html_status, { + "title": message, + }) + this.html_status_tooltip.show(); } } @@ -98,7 +110,7 @@ class BrickChanger { }); if (!response.ok) { - throw new Error(`Response status: ${response.status}`); + throw new Error(`Response status: ${response.status} (${response.statusText})`); } const json = await response.json(); @@ -121,7 +133,7 @@ class BrickChanger { } catch (error) { console.log(error.message); - this.status_error(); + this.status_error(error.message); } } } From acbd58ca7149481a924aac3e1d1531da0b34f538 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Wed, 29 Jan 2025 17:41:34 +0100 Subject: [PATCH 043/154] Add missing @login_required for set deletion --- bricktracker/views/set.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py index d82896c..809d46b 100644 --- a/bricktracker/views/set.py +++ b/bricktracker/views/set.py @@ -73,6 +73,7 @@ def delete(*, id: str) -> str: # Actually delete of a set @set_page.route('//delete', methods=['POST']) +@login_required @exception_handler(__file__, post_redirect='set.delete') def do_delete(*, id: str) -> Response: brickset = BrickSet().select_light(id) @@ -89,6 +90,7 @@ def do_delete(*, id: str) -> Response: # Set is deleted @set_page.route('//deleted', methods=['GET']) +@login_required @exception_handler(__file__) def deleted(*, id: str) -> str: return render_template( @@ -155,6 +157,7 @@ def missing_part( # Refresh a set @set_page.route('//refresh', methods=['GET']) +@login_required @exception_handler(__file__) def refresh(*, id: str) -> str: return render_template( From 69c7dbaefeb0c783f80734c2c8989d98c2c81855 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Wed, 29 Jan 2025 17:42:13 +0100 Subject: [PATCH 044/154] Don't display the set management section when deleting it --- templates/set/card.html | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/templates/set/card.html b/templates/set/card.html index 02d77b3..0abb2c1 100644 --- a/templates/set/card.html +++ b/templates/set/card.html @@ -61,11 +61,13 @@ {% for minifigure in item.minifigures() %} {{ accordion.table(minifigure.parts(), minifigure.fields.name, minifigure.fields.figure, 'set-details', 'part/table.html', quantity=minifigure.fields.quantity, icon='group-line', image=minifigure.url_for_image(), alt=minifigure.fields.figure, details=minifigure.url())}} {% endfor %} + {% if g.login.is_authenticated() %} + {{ accordion.header('Management', 'management', 'set-details', icon='settings-4-line', class='text-end') }} + Refresh the set + {{ accordion.footer() }} + {% endif %} {% endif %} {% if g.login.is_authenticated() %} - {{ accordion.header('Management', 'management', 'set-details', icon='settings-4-line', class='text-end') }} - Refresh the set - {{ accordion.footer() }} {{ accordion.header('Danger zone', 'danger-zone', 'set-details', expanded=delete, danger=true, class='text-end') }} {% if delete %} From 160ab066b2b0c3c5591de3c4b1c26b48b5511a70 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Wed, 29 Jan 2025 17:44:46 +0100 Subject: [PATCH 045/154] Update container versions --- compose.legacy.yml | 2 +- compose.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compose.legacy.yml b/compose.legacy.yml index 0647bcb..2d48e3b 100644 --- a/compose.legacy.yml +++ b/compose.legacy.yml @@ -2,7 +2,7 @@ services: bricktracker: container_name: BrickTracker restart: unless-stopped - image: gitea.baerentsen.space/frederikbaerentsen/bricktracker:1.1.1 + image: gitea.baerentsen.space/frederikbaerentsen/bricktracker:1.2.0 ports: - "3333:3333" volumes: diff --git a/compose.yaml b/compose.yaml index 861b8a9..6c38ed9 100644 --- a/compose.yaml +++ b/compose.yaml @@ -2,7 +2,7 @@ services: bricktracker: container_name: BrickTracker restart: unless-stopped - image: gitea.baerentsen.space/frederikbaerentsen/bricktracker:1.1.1 + image: gitea.baerentsen.space/frederikbaerentsen/bricktracker:1.2.0 ports: - "3333:3333" volumes: From 56ad9fba13a67abe5b26f8d1a1300516333894e8 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Wed, 29 Jan 2025 17:59:59 +0100 Subject: [PATCH 046/154] url_for_missing should be part of BrickPart, not RebrickablePart --- bricktracker/part.py | 19 +++++++++++++++++++ bricktracker/rebrickable_part.py | 17 ----------------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/bricktracker/part.py b/bricktracker/part.py index 495059f..ac85a11 100644 --- a/bricktracker/part.py +++ b/bricktracker/part.py @@ -2,6 +2,8 @@ import logging from typing import Any, Self, TYPE_CHECKING import traceback +from flask import url_for + from .exceptions import ErrorException, NotFoundException from .rebrickable_part import RebrickablePart from .sql import BrickSQL @@ -169,3 +171,20 @@ class BrickPart(RebrickablePart): 'part/update/missing', parameters=self.sql_parameters() ) + + # Compute the url for missing part + def url_for_missing(self, /) -> str: + # Different URL for a minifigure part + if self.minifigure is not None: + figure = self.minifigure.fields.figure + else: + figure = None + + return url_for( + 'set.missing_part', + id=self.fields.id, + figure=figure, + part=self.fields.part, + color=self.fields.color, + spare=self.fields.spare, + ) diff --git a/bricktracker/rebrickable_part.py b/bricktracker/rebrickable_part.py index 8224d98..eea6d08 100644 --- a/bricktracker/rebrickable_part.py +++ b/bricktracker/rebrickable_part.py @@ -117,23 +117,6 @@ class RebrickablePart(BrickRecord): else: return self.fields.image - # Compute the url for missing part - def url_for_missing(self, /) -> str: - # Different URL for a minifigure part - if self.minifigure is not None: - figure = self.minifigure.fields.figure - else: - figure = None - - return url_for( - 'set.missing_part', - id=self.fields.id, - figure=figure, - part=self.fields.part, - color=self.fields.color, - spare=self.fields.spare, - ) - # Compute the url for the original of the printed part def url_for_print(self, /) -> str: return url_for( From 728e0050b310852671f08cf2ad939f19b8d31df8 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Wed, 29 Jan 2025 18:12:39 +0100 Subject: [PATCH 047/154] Fix functions definition --- bricktracker/migrations/0007.py | 2 +- bricktracker/part.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bricktracker/migrations/0007.py b/bricktracker/migrations/0007.py index fb5b723..7cf3c9e 100644 --- a/bricktracker/migrations/0007.py +++ b/bricktracker/migrations/0007.py @@ -5,7 +5,7 @@ if TYPE_CHECKING: # Grab the list of checkboxes to create a list of SQL columns -def migration_0007(self: 'BrickSQL') -> dict[str, Any]: +def migration_0007(self: 'BrickSQL', /) -> dict[str, Any]: records = self.fetchall('checkbox/list') return { diff --git a/bricktracker/part.py b/bricktracker/part.py index ac85a11..58f6510 100644 --- a/bricktracker/part.py +++ b/bricktracker/part.py @@ -74,7 +74,7 @@ class BrickPart(RebrickablePart): return True # A identifier for HTML component - def html_id(self) -> str: + def html_id(self, /) -> str: components: list[str] = ['part'] if self.fields.figure is not None: From 257bccc3395960b48b14f01329c523b618dbc7ed Mon Sep 17 00:00:00 2001 From: Gregoo Date: Wed, 29 Jan 2025 18:14:44 +0100 Subject: [PATCH 048/154] Move set management to its own file --- templates/set/card.html | 6 +----- templates/set/management.html | 5 +++++ 2 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 templates/set/management.html diff --git a/templates/set/card.html b/templates/set/card.html index 0abb2c1..fee3040 100644 --- a/templates/set/card.html +++ b/templates/set/card.html @@ -61,11 +61,7 @@ {% for minifigure in item.minifigures() %} {{ accordion.table(minifigure.parts(), minifigure.fields.name, minifigure.fields.figure, 'set-details', 'part/table.html', quantity=minifigure.fields.quantity, icon='group-line', image=minifigure.url_for_image(), alt=minifigure.fields.figure, details=minifigure.url())}} {% endfor %} - {% if g.login.is_authenticated() %} - {{ accordion.header('Management', 'management', 'set-details', icon='settings-4-line', class='text-end') }} - Refresh the set - {{ accordion.footer() }} - {% endif %} + {% include 'set/management.html' %} {% endif %} {% if g.login.is_authenticated() %} {{ accordion.header('Danger zone', 'danger-zone', 'set-details', expanded=delete, danger=true, class='text-end') }} diff --git a/templates/set/management.html b/templates/set/management.html new file mode 100644 index 0000000..b27c9d0 --- /dev/null +++ b/templates/set/management.html @@ -0,0 +1,5 @@ +{% if g.login.is_authenticated() %} +{{ accordion.header('Management', 'management', 'set-details', icon='settings-4-line', class='text-end') }} + Refresh the set data +{{ accordion.footer() }} +{% endif %} From b2d2019bfd978edf96a7b273bcea73f53a08393d Mon Sep 17 00:00:00 2001 From: Gregoo Date: Wed, 29 Jan 2025 19:28:00 +0100 Subject: [PATCH 049/154] Set theme override --- bricktracker/rebrickable_set.py | 10 ++++- bricktracker/set.py | 63 ++++++++++++++++++++++++++- bricktracker/sql/set/base/base.sql | 1 + bricktracker/sql/set/update/theme.sql | 3 ++ bricktracker/theme.py | 10 ++--- bricktracker/theme_list.py | 9 ++-- bricktracker/views/set.py | 19 ++++++++ templates/set/management.html | 8 +++- 8 files changed, 111 insertions(+), 12 deletions(-) create mode 100644 bricktracker/sql/set/update/theme.sql diff --git a/bricktracker/rebrickable_set.py b/bricktracker/rebrickable_set.py index fbf10f1..d2878d6 100644 --- a/bricktracker/rebrickable_set.py +++ b/bricktracker/rebrickable_set.py @@ -11,10 +11,10 @@ from .parser import parse_set from .rebrickable import Rebrickable from .rebrickable_image import RebrickableImage from .record import BrickRecord +from .theme import BrickTheme from .theme_list import BrickThemeList if TYPE_CHECKING: from .socket import BrickSocket - from .theme import BrickTheme logger = logging.getLogger(__name__) @@ -66,7 +66,13 @@ class RebrickableSet(BrickRecord): if not hasattr(self.fields, 'theme_id'): self.fields.theme_id = 0 - self.theme = BrickThemeList().get(self.fields.theme_id) + if not hasattr(self.fields, 'theme_name'): + self.fields.theme_name = None + + self.theme = BrickThemeList().get( + str(self.fields.theme_id), + name=self.fields.theme_name + ) # Resolve instructions if self.resolve_instructions: diff --git a/bricktracker/set.py b/bricktracker/set.py index 28f0341..893acc0 100644 --- a/bricktracker/set.py +++ b/bricktracker/set.py @@ -1,11 +1,12 @@ import logging +from sqlite3 import Row import traceback from typing import Any, Self, TYPE_CHECKING from uuid import uuid4 from flask import current_app, url_for -from .exceptions import DatabaseException, NotFoundException +from .exceptions import DatabaseException, ErrorException, NotFoundException from .minifigure_list import BrickMinifigureList from .part_list import BrickPartList from .rebrickable_set import RebrickableSet @@ -121,6 +122,29 @@ class BrickSet(RebrickableSet): return True + # Ingest a set + def ingest(self, record: Row | dict[str, Any], /): + # Super charge the record with theme override + if 'theme' in record.keys() and record['theme'] is not None: + if isinstance(record, Row): + record = dict(record) + + record['theme_id'] = record['theme'] + record['theme_name'] = record['theme'] + + super().ingest(record) + + # A identifier for HTML component + def html_id(self, prefix: str | None = None, /) -> str: + components: list[str] = [] + + if prefix is not None: + components.append(prefix) + + components.append(self.fields.id) + + return '-'.join(components) + # Minifigures def minifigures(self, /) -> BrickMinifigureList: return BrickMinifigureList().from_set(self) @@ -185,6 +209,36 @@ class BrickSet(RebrickableSet): id=self.fields.id, )) + # Update theme + def update_theme(self, json: Any | None, /) -> None: + theme: str | None = json.get('value', '') # type: ignore + + # We need a string + try: + theme = str(theme) + theme = theme.strip() + except Exception: + raise ErrorException('"{theme}" is not a valid string'.format( + theme=theme + )) + + if theme == '': + theme = None + + self.fields.theme = theme + + # Update the status + rows, _ = BrickSQL().execute_and_commit( + 'set/update/theme', + parameters=self.sql_parameters() + ) + + if rows != 1: + raise DatabaseException('Could not update the theme override for set {set} ({id})'.format( # noqa: E501 + set=self.fields.set, + id=self.fields.id, + )) + # Self url def url(self, /) -> str: return url_for('set.details', id=self.fields.id) @@ -217,3 +271,10 @@ class BrickSet(RebrickableSet): 'set.refresh', id=self.fields.id, ) + + # Compute the url for the theme override + def url_for_theme(self, /) -> str: + return url_for( + 'set.update_theme', + id=self.fields.id, + ) diff --git a/bricktracker/sql/set/base/base.sql b/bricktracker/sql/set/base/base.sql index 8b1f4c8..7bc1bc3 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"."theme", "rebrickable_sets"."set", "rebrickable_sets"."number", "rebrickable_sets"."version", diff --git a/bricktracker/sql/set/update/theme.sql b/bricktracker/sql/set/update/theme.sql new file mode 100644 index 0000000..1d884ed --- /dev/null +++ b/bricktracker/sql/set/update/theme.sql @@ -0,0 +1,3 @@ +UPDATE "bricktracker_sets" +SET "theme" = :theme +WHERE "bricktracker_sets"."id" IS NOT DISTINCT FROM :id diff --git a/bricktracker/theme.py b/bricktracker/theme.py index 3ee1068..df677f4 100644 --- a/bricktracker/theme.py +++ b/bricktracker/theme.py @@ -1,14 +1,14 @@ # Lego set theme class BrickTheme(object): - id: int + id: str name: str - parent: int | None + parent: str | None - def __init__(self, id: str | int, name: str, parent: str | None = None, /): - self.id = int(id) + def __init__(self, id: str, name: str, parent: str | None = None, /): + self.id = id self.name = name if parent is not None and parent != '': - self.parent = int(parent) + self.parent = parent else: self.parent = None diff --git a/bricktracker/theme_list.py b/bricktracker/theme_list.py index 22cac8e..af1667e 100644 --- a/bricktracker/theme_list.py +++ b/bricktracker/theme_list.py @@ -17,7 +17,7 @@ logger = logging.getLogger(__name__) # Lego sets themes class BrickThemeList(object): - themes: dict[int, BrickTheme] + themes: dict[str, BrickTheme] mtime: datetime | None size: int | None exception: Exception | None @@ -57,12 +57,15 @@ class BrickThemeList(object): BrickThemeList.mtime = None # Get a theme - def get(self, id: int, /) -> BrickTheme: + def get(self, id: str, /, *, name: str | None = None) -> BrickTheme: # Seed a fake entry if missing if id not in self.themes: + if name is None: + name = 'Unknown ({id})'.format(id=id) + BrickThemeList.themes[id] = BrickTheme( id, - 'Unknown ({id})'.format(id=id) + name, ) return self.themes[id] diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py index 809d46b..59289ff 100644 --- a/bricktracker/views/set.py +++ b/bricktracker/views/set.py @@ -167,3 +167,22 @@ def refresh(*, id: str) -> str: namespace=current_app.config['SOCKET_NAMESPACE'], messages=MESSAGES ) + + +# Change the theme override +@set_page.route('//theme', methods=['POST']) +@login_required +@exception_handler(__file__, json=True) +def update_theme(*, id: str) -> Response: + brickset = BrickSet().select_light(id) + + brickset.update_theme(request.json) + + # Info + logger.info('Set {set} ({id}): theme override changed to "{theme}"'.format( # noqa: E501 + set=brickset.fields.set, + id=brickset.fields.id, + theme=brickset.fields.theme, + )) + + return jsonify({'value': brickset.fields.theme}) diff --git a/templates/set/management.html b/templates/set/management.html index b27c9d0..b299972 100644 --- a/templates/set/management.html +++ b/templates/set/management.html @@ -1,5 +1,11 @@ {% if g.login.is_authenticated() %} -{{ accordion.header('Management', 'management', 'set-details', icon='settings-4-line', class='text-end') }} +{{ accordion.header('Management', 'management', 'set-details', expanded=true, icon='settings-4-line') }} +
Theme override
+

+ You can override the current theme ({{ badge.theme(item.theme.name, solo=solo, last=last) }}) with any string you want. + {{ form.input('Theme', item.fields.id, item.html_id('theme'), item.url_for_theme(), item.fields.theme, all=all, read_only=read_only) }} +

+
Data
Refresh the set data {{ accordion.footer() }} {% endif %} From 51f729a18b74e36c18fdc4abc0f194280ecfb91c Mon Sep 17 00:00:00 2001 From: Gregoo Date: Wed, 29 Jan 2025 19:28:23 +0100 Subject: [PATCH 050/154] Fix variable type hint --- bricktracker/part.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bricktracker/part.py b/bricktracker/part.py index 58f6510..64d71df 100644 --- a/bricktracker/part.py +++ b/bricktracker/part.py @@ -146,7 +146,7 @@ class BrickPart(RebrickablePart): # Update the missing part def update_missing(self, json: Any | None, /) -> None: - missing = json.get('value', '') # type: ignore + missing: str | int = json.get('value', '') # type: ignore # We need a positive integer try: From 3893f2aa19ab37b4e9d0fb1347b08bc54ccc6c45 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Wed, 29 Jan 2025 21:19:56 +0100 Subject: [PATCH 051/154] Theme override nobody cares actually --- bricktracker/rebrickable_set.py | 10 ++---- bricktracker/set.py | 52 +-------------------------- bricktracker/sql/migrations/0007.sql | 1 - bricktracker/sql/set/base/base.sql | 1 - bricktracker/sql/set/update/theme.sql | 3 -- bricktracker/theme.py | 10 +++--- bricktracker/theme_list.py | 9 ++--- bricktracker/views/set.py | 19 ---------- templates/set/management.html | 5 --- 9 files changed, 11 insertions(+), 99 deletions(-) delete mode 100644 bricktracker/sql/set/update/theme.sql diff --git a/bricktracker/rebrickable_set.py b/bricktracker/rebrickable_set.py index d2878d6..fbf10f1 100644 --- a/bricktracker/rebrickable_set.py +++ b/bricktracker/rebrickable_set.py @@ -11,10 +11,10 @@ from .parser import parse_set from .rebrickable import Rebrickable from .rebrickable_image import RebrickableImage from .record import BrickRecord -from .theme import BrickTheme from .theme_list import BrickThemeList if TYPE_CHECKING: from .socket import BrickSocket + from .theme import BrickTheme logger = logging.getLogger(__name__) @@ -66,13 +66,7 @@ class RebrickableSet(BrickRecord): if not hasattr(self.fields, 'theme_id'): self.fields.theme_id = 0 - if not hasattr(self.fields, 'theme_name'): - self.fields.theme_name = None - - self.theme = BrickThemeList().get( - str(self.fields.theme_id), - name=self.fields.theme_name - ) + self.theme = BrickThemeList().get(self.fields.theme_id) # Resolve instructions if self.resolve_instructions: diff --git a/bricktracker/set.py b/bricktracker/set.py index 893acc0..fa05b0b 100644 --- a/bricktracker/set.py +++ b/bricktracker/set.py @@ -1,12 +1,11 @@ import logging -from sqlite3 import Row import traceback from typing import Any, Self, TYPE_CHECKING from uuid import uuid4 from flask import current_app, url_for -from .exceptions import DatabaseException, ErrorException, NotFoundException +from .exceptions import DatabaseException, NotFoundException from .minifigure_list import BrickMinifigureList from .part_list import BrickPartList from .rebrickable_set import RebrickableSet @@ -122,18 +121,6 @@ class BrickSet(RebrickableSet): return True - # Ingest a set - def ingest(self, record: Row | dict[str, Any], /): - # Super charge the record with theme override - if 'theme' in record.keys() and record['theme'] is not None: - if isinstance(record, Row): - record = dict(record) - - record['theme_id'] = record['theme'] - record['theme_name'] = record['theme'] - - super().ingest(record) - # A identifier for HTML component def html_id(self, prefix: str | None = None, /) -> str: components: list[str] = [] @@ -209,36 +196,6 @@ class BrickSet(RebrickableSet): id=self.fields.id, )) - # Update theme - def update_theme(self, json: Any | None, /) -> None: - theme: str | None = json.get('value', '') # type: ignore - - # We need a string - try: - theme = str(theme) - theme = theme.strip() - except Exception: - raise ErrorException('"{theme}" is not a valid string'.format( - theme=theme - )) - - if theme == '': - theme = None - - self.fields.theme = theme - - # Update the status - rows, _ = BrickSQL().execute_and_commit( - 'set/update/theme', - parameters=self.sql_parameters() - ) - - if rows != 1: - raise DatabaseException('Could not update the theme override for set {set} ({id})'.format( # noqa: E501 - set=self.fields.set, - id=self.fields.id, - )) - # Self url def url(self, /) -> str: return url_for('set.details', id=self.fields.id) @@ -271,10 +228,3 @@ class BrickSet(RebrickableSet): 'set.refresh', id=self.fields.id, ) - - # Compute the url for the theme override - def url_for_theme(self, /) -> str: - return url_for( - 'set.update_theme', - id=self.fields.id, - ) diff --git a/bricktracker/sql/migrations/0007.sql b/bricktracker/sql/migrations/0007.sql index f955bb9..89ef71f 100644 --- a/bricktracker/sql/migrations/0007.sql +++ b/bricktracker/sql/migrations/0007.sql @@ -24,7 +24,6 @@ CREATE TABLE "bricktracker_sets" ( "id" TEXT NOT NULL, "set" TEXT NOT NULL, "description" TEXT, - "theme" TEXT, -- Custom theme name "storage" TEXT, -- Storage bin location "purchase_date" INTEGER, -- Purchase data "purchase_location" TEXT, -- Purchase location diff --git a/bricktracker/sql/set/base/base.sql b/bricktracker/sql/set/base/base.sql index 7bc1bc3..8b1f4c8 100644 --- a/bricktracker/sql/set/base/base.sql +++ b/bricktracker/sql/set/base/base.sql @@ -1,6 +1,5 @@ SELECT {% block id %}{% endblock %} - "bricktracker_sets"."theme", "rebrickable_sets"."set", "rebrickable_sets"."number", "rebrickable_sets"."version", diff --git a/bricktracker/sql/set/update/theme.sql b/bricktracker/sql/set/update/theme.sql deleted file mode 100644 index 1d884ed..0000000 --- a/bricktracker/sql/set/update/theme.sql +++ /dev/null @@ -1,3 +0,0 @@ -UPDATE "bricktracker_sets" -SET "theme" = :theme -WHERE "bricktracker_sets"."id" IS NOT DISTINCT FROM :id diff --git a/bricktracker/theme.py b/bricktracker/theme.py index df677f4..3ee1068 100644 --- a/bricktracker/theme.py +++ b/bricktracker/theme.py @@ -1,14 +1,14 @@ # Lego set theme class BrickTheme(object): - id: str + id: int name: str - parent: str | None + parent: int | None - def __init__(self, id: str, name: str, parent: str | None = None, /): - self.id = id + def __init__(self, id: str | int, name: str, parent: str | None = None, /): + self.id = int(id) self.name = name if parent is not None and parent != '': - self.parent = parent + self.parent = int(parent) else: self.parent = None diff --git a/bricktracker/theme_list.py b/bricktracker/theme_list.py index af1667e..22cac8e 100644 --- a/bricktracker/theme_list.py +++ b/bricktracker/theme_list.py @@ -17,7 +17,7 @@ logger = logging.getLogger(__name__) # Lego sets themes class BrickThemeList(object): - themes: dict[str, BrickTheme] + themes: dict[int, BrickTheme] mtime: datetime | None size: int | None exception: Exception | None @@ -57,15 +57,12 @@ class BrickThemeList(object): BrickThemeList.mtime = None # Get a theme - def get(self, id: str, /, *, name: str | None = None) -> BrickTheme: + def get(self, id: int, /) -> BrickTheme: # Seed a fake entry if missing if id not in self.themes: - if name is None: - name = 'Unknown ({id})'.format(id=id) - BrickThemeList.themes[id] = BrickTheme( id, - name, + 'Unknown ({id})'.format(id=id) ) return self.themes[id] diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py index 59289ff..809d46b 100644 --- a/bricktracker/views/set.py +++ b/bricktracker/views/set.py @@ -167,22 +167,3 @@ def refresh(*, id: str) -> str: namespace=current_app.config['SOCKET_NAMESPACE'], messages=MESSAGES ) - - -# Change the theme override -@set_page.route('//theme', methods=['POST']) -@login_required -@exception_handler(__file__, json=True) -def update_theme(*, id: str) -> Response: - brickset = BrickSet().select_light(id) - - brickset.update_theme(request.json) - - # Info - logger.info('Set {set} ({id}): theme override changed to "{theme}"'.format( # noqa: E501 - set=brickset.fields.set, - id=brickset.fields.id, - theme=brickset.fields.theme, - )) - - return jsonify({'value': brickset.fields.theme}) diff --git a/templates/set/management.html b/templates/set/management.html index b299972..3807480 100644 --- a/templates/set/management.html +++ b/templates/set/management.html @@ -1,10 +1,5 @@ {% if g.login.is_authenticated() %} {{ accordion.header('Management', 'management', 'set-details', expanded=true, icon='settings-4-line') }} -
Theme override
-

- You can override the current theme ({{ badge.theme(item.theme.name, solo=solo, last=last) }}) with any string you want. - {{ form.input('Theme', item.fields.id, item.html_id('theme'), item.url_for_theme(), item.fields.theme, all=all, read_only=read_only) }} -

Data
Refresh the set data {{ accordion.footer() }} From aed7a520bd30ac1603050b872cab1ff3ffa74408 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Wed, 29 Jan 2025 22:44:08 +0100 Subject: [PATCH 052/154] Parametrable error names --- bricktracker/views/error.py | 80 ++++++++++++++++++++++++-------- bricktracker/views/exceptions.py | 2 + 2 files changed, 63 insertions(+), 19 deletions(-) diff --git a/bricktracker/views/error.py b/bricktracker/views/error.py index c034ea8..67a97a1 100644 --- a/bricktracker/views/error.py +++ b/bricktracker/views/error.py @@ -1,7 +1,7 @@ import logging from sqlite3 import Error, OperationalError import traceback -from typing import Tuple +from typing import Any, Tuple from flask import jsonify, redirect, request, render_template, url_for from werkzeug.wrappers.response import Response @@ -33,12 +33,16 @@ def error( *, json: bool = False, post_redirect: str | None = None, + error_name: str = 'error', **kwargs, ) -> str | Tuple[str | Response, int] | Response: # Back to the index if no error (not sure if this can happen) if error is None: if json: - return jsonify({'error': 'error() called without an error'}) + return json_error( + 'error() called without an error', + error_name=error_name + ) else: return redirect(url_for('index.index')) @@ -56,6 +60,7 @@ def error( error, json=json, post_redirect=post_redirect, + error_name=error_name, **kwargs ) @@ -71,13 +76,17 @@ def error( logger.debug(cleaned_exception(error)) if json: - return jsonify({'error': str(error)}) + return json_error( + str(error), + error_name=error_name + ) elif post_redirect is not None: - return redirect(url_for( + return redirect_error( post_redirect, error=str(error), + error_name=error_name, **kwargs, - )) + ) else: return render_template( 'error.html', @@ -96,18 +105,20 @@ def error( line = None if json: - return jsonify({ - 'error': 'Exception: {error}'.format(error=str(error)), - 'name': type(error).__name__, - 'line': line, - 'file': file, - }), 500 + return json_error( + 'Exception: {error}'.format(error=str(error)), + error_name=error_name, + name=type(error).__name__, + line=line, + file=file + ), 500 elif post_redirect is not None: - return redirect(url_for( + return redirect_error( post_redirect, error=str(error), + error_name=error_name, **kwargs, - )) + ) else: return render_template( 'exception.html', @@ -125,6 +136,7 @@ def error_404( *, json: bool = False, post_redirect: str | None = None, + error_name: str = 'error', **kwargs, ) -> Tuple[str | Response, int]: # Warning @@ -134,14 +146,44 @@ def error_404( )) if json: - return jsonify({ - 'error': 'Not found: {error}'.format(error=str(error)) - }), 404 + return json_error( + 'Not found: {error}'.format(error=str(error)), + error_name=error_name + ), 404 elif post_redirect is not None: - return redirect(url_for( + return redirect_error( post_redirect, error=str(error), - **kwargs - )), 404 + error_name=error_name, + **kwargs, + ), 404 else: return render_template('404.html', error=str(error)), 404 + + +# JSON error with parametric error name +def json_error( + error: str, + error_name: str = 'error', + **parameters: Any +) -> Response: + parameters[error_name] = error + + return jsonify(parameters) + + +# Redirect error with parametric error name +def redirect_error( + url: str, + error: str, + error_name: str = 'error', + **kwargs +) -> Response: + error_parameter: dict[str, str] = {} + error_parameter[error_name] = str(error) + + return redirect(url_for( + url, + **error_parameter, + **kwargs + )) diff --git a/bricktracker/views/exceptions.py b/bricktracker/views/exceptions.py index b78c390..aa01b79 100644 --- a/bricktracker/views/exceptions.py +++ b/bricktracker/views/exceptions.py @@ -28,6 +28,7 @@ def exception_handler( *, json: bool = False, post_redirect: str | None = None, + error_name: str = 'error', **superkwargs, ) -> Callable[[ViewCallable], ViewCallable]: def outer(function: ViewCallable, /) -> ViewCallable: @@ -42,6 +43,7 @@ def exception_handler( file, json=json, post_redirect=post_redirect, + error_name=error_name, **kwargs, **superkwargs, ) From bba741b4a59b627b6a22fbc5ed76a24539091547 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Wed, 29 Jan 2025 22:49:17 +0100 Subject: [PATCH 053/154] Rename database_error --- bricktracker/views/admin/admin.py | 2 +- bricktracker/views/admin/database.py | 34 ++++++++++++++++++++------- templates/admin/database.html | 2 +- templates/admin/database/delete.html | 2 +- templates/admin/database/drop.html | 2 +- templates/admin/database/import.html | 2 +- templates/admin/database/upgrade.html | 2 +- 7 files changed, 31 insertions(+), 15 deletions(-) diff --git a/bricktracker/views/admin/admin.py b/bricktracker/views/admin/admin.py index 847e42a..5d6a4e4 100644 --- a/bricktracker/views/admin/admin.py +++ b/bricktracker/views/admin/admin.py @@ -83,7 +83,7 @@ def admin() -> str: configuration=BrickConfigurationList.list(), brickset_checkboxes=brickset_checkboxes, database_counters=database_counters, - database_error=request.args.get('error'), + database_error=request.args.get('database_error'), database_exception=database_exception, database_upgrade_needed=database_upgrade_needed, database_version=database_version, diff --git a/bricktracker/views/admin/database.py b/bricktracker/views/admin/database.py index bd0f213..e2fc4bc 100644 --- a/bricktracker/views/admin/database.py +++ b/bricktracker/views/admin/database.py @@ -38,14 +38,18 @@ def delete() -> str: return render_template( 'admin.html', delete_database=True, - error=request.args.get('error') + database_error=request.args.get('database_error') ) # Actually delete the database @admin_database_page.route('/delete', methods=['POST']) @login_required -@exception_handler(__file__, post_redirect='admin_database.delete') +@exception_handler( + __file__, + post_redirect='admin_database.delete', + error_name='database_error' +) def do_delete() -> Response: BrickSQL.delete() @@ -89,14 +93,18 @@ def drop() -> str: return render_template( 'admin.html', drop_database=True, - error=request.args.get('error') + database_error=request.args.get('database_error') ) # Actually drop the database @admin_database_page.route('/drop', methods=['POST']) @login_required -@exception_handler(__file__, post_redirect='admin_database.drop') +@exception_handler( + __file__, + post_redirect='admin_database.drop', + error_name='database_error' +) def do_drop() -> Response: BrickSQL.drop() @@ -108,7 +116,11 @@ def do_drop() -> Response: # Actually upgrade the database @admin_database_page.route('/upgrade', methods=['POST']) @login_required -@exception_handler(__file__, post_redirect='admin_database.upgrade') +@exception_handler( + __file__, + post_redirect='admin_database.upgrade', + error_name='database_error' +) def do_upgrade() -> Response: BrickSQL(failsafe=True).upgrade() @@ -125,14 +137,18 @@ def upload() -> str: return render_template( 'admin.html', import_database=True, - error=request.args.get('error') + database_error=request.args.get('database_error') ) # Actually import a database @admin_database_page.route('/import', methods=['POST']) @login_required -@exception_handler(__file__, post_redirect='admin_database.upload') +@exception_handler( + __file__, + post_redirect='admin_database.upload', + error_name='database_error' +) def do_upload() -> Response: file = upload_helper( 'database', @@ -153,7 +169,7 @@ def do_upload() -> Response: # Upgrade the database @admin_database_page.route('/upgrade', methods=['GET']) @login_required -@exception_handler(__file__, post_redirect='admin.admin') +@exception_handler(__file__) def upgrade() -> str | Response: database = BrickSQL(failsafe=True) @@ -166,5 +182,5 @@ def upgrade() -> str | Response: migrations=BrickSQLMigrationList().pending( database.version ), - error=request.args.get('error') + database_error=request.args.get('database_error') ) diff --git a/templates/admin/database.html b/templates/admin/database.html index 9704365..7a15325 100644 --- a/templates/admin/database.html +++ b/templates/admin/database.html @@ -36,7 +36,7 @@ {{ accordion.footer() }} {{ accordion.header('Database danger zone', 'database-danger', 'admin', danger=true, class='text-end') }} -{% if error %}{% endif %} +{% if database_error %}{% endif %} Import a database file Drop the database Delete the database file diff --git a/templates/admin/database/delete.html b/templates/admin/database/delete.html index 9a2d286..9bbbf2f 100644 --- a/templates/admin/database/delete.html +++ b/templates/admin/database/delete.html @@ -2,7 +2,7 @@ {{ accordion.header('Database danger zone', 'database-danger', 'admin', expanded=true, danger=true, class='text-end') }} - {% if error %}{% endif %} + {% if database_error %}{% endif %} Back to the admin diff --git a/templates/admin/database/drop.html b/templates/admin/database/drop.html index e0f5b2e..1ebe0d5 100644 --- a/templates/admin/database/drop.html +++ b/templates/admin/database/drop.html @@ -2,7 +2,7 @@ {{ accordion.header('Database danger zone', 'database-danger', 'admin', expanded=true, danger=true, class='text-end') }} - {% if error %}{% endif %} + {% if database_error %}{% endif %} Back to the admin diff --git a/templates/admin/database/import.html b/templates/admin/database/import.html index f7a7763..a6d5e9c 100644 --- a/templates/admin/database/import.html +++ b/templates/admin/database/import.html @@ -2,7 +2,7 @@ {{ accordion.header('Database danger zone', 'database-danger', 'admin', expanded=true, danger=true) }} - {% if error %}{% endif %} + {% if database_error %}{% endif %}
diff --git a/templates/admin/database/upgrade.html b/templates/admin/database/upgrade.html index 2739c79..29e59ef 100644 --- a/templates/admin/database/upgrade.html +++ b/templates/admin/database/upgrade.html @@ -2,7 +2,7 @@ {{ accordion.header('Database', 'database', 'admin', expanded=true, icon='database-2-line') }} - {% if error %}{% endif %} + {% if database_error %}{% endif %}
-
+
Status - @@ -56,7 +58,9 @@
Theme - {% for theme in collection.themes %} From 069ba37e138dd7527c120925757c6657a47b3bee Mon Sep 17 00:00:00 2001 From: Gregoo Date: Thu, 30 Jan 2025 23:25:42 +0100 Subject: [PATCH 070/154] Fix database counters display --- templates/admin/database.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/admin/database.html b/templates/admin/database.html index 7a15325..86e82c8 100644 --- a/templates/admin/database.html +++ b/templates/admin/database.html @@ -19,15 +19,15 @@

{% if database_counters %}
Records
-
-
    +
    +
      {% for counter in database_counters %}
    • {{ counter.name }} {{ counter.count }}
    • {% if not (loop.index % 5) %}
    -
      +
        {% endif %} {% endfor %}
      From 0e3637e5efd53d5f5d9ef944d6ce14a899cfee2e Mon Sep 17 00:00:00 2001 From: Gregoo Date: Thu, 30 Jan 2025 23:34:47 +0100 Subject: [PATCH 071/154] Make checkbox clickable in the entire width of their container --- templates/macro/form.html | 2 +- templates/set/card.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/macro/form.html b/templates/macro/form.html index 379309f..dfd8211 100644 --- a/templates/macro/form.html +++ b/templates/macro/form.html @@ -8,7 +8,7 @@ disabled {% endif %} autocomplete="off"> -
    From 2260774a582d5da3f580852319b6047d5122e701 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 31 Jan 2025 10:54:11 +0100 Subject: [PATCH 074/154] Rename solo and attribute to value and metadata in grid filter --- static/scripts/grid/filter.js | 6 +++--- templates/sets.html | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/static/scripts/grid/filter.js b/static/scripts/grid/filter.js index d0d8903..e2eff7b 100644 --- a/static/scripts/grid/filter.js +++ b/static/scripts/grid/filter.js @@ -63,15 +63,15 @@ class BrickGridFilter { // Multi-attribute filter switch (select.dataset.filter) { // List contains values - case "solo": + case "value": options.filters.push({ attribute: select.dataset.filterAttribute, value: select.value, }) break; - // List contains attribute name, looking for true/false - case "status": + // List contains metadata attribute name, looking for true/false + case "metadata": if (select.value.startsWith("-")) { options.filters.push({ attribute: select.value.substring(1), diff --git a/templates/sets.html b/templates/sets.html index e8d70a2..27aff00 100644 --- a/templates/sets.html +++ b/templates/sets.html @@ -41,7 +41,7 @@
    Status {% for theme in collection.themes %} From e9f97a6f5e328d4313195b73f8085debf73e0045 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 31 Jan 2025 11:05:19 +0100 Subject: [PATCH 075/154] Use a with block rather than set to avoid leaking variables --- templates/wish/table.html | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/templates/wish/table.html b/templates/wish/table.html index 73a5eb8..3b03782 100644 --- a/templates/wish/table.html +++ b/templates/wish/table.html @@ -19,23 +19,24 @@
{% for item in table_collection %} - {% set retirement_date = retired.get(item.fields.set) %} - - {{ table.image(item.url_for_image(), caption=item.fields.name, alt=item.fields.set) }} - - - - - - - {% if g.login.is_authenticated() %} - - {% endif %} - + {% with retirement_date = retired.get(item.fields.set) %} + + {{ table.image(item.url_for_image(), caption=item.fields.name, alt=item.fields.set) }} + + + + + + + {% if g.login.is_authenticated() %} + + {% endif %} + + {% endwith %} {% endfor %}
{{ item.fields.name }} - {% if item.fields.is_spare %} Spare{% endif %} + {% if item.fields.spare %} Spare{% endif %} {% if all %} {{ table.rebrickable(item) }} {{ table.bricklink(item) }} @@ -24,15 +24,15 @@ {% endif %} {% endif %} {% if not no_missing %} - + {% if all or read_only_missing %} {{ item.fields.total_missing }} {% else %}
{% if g.login.is_authenticated() %} - + onchange="change_part_missing_amount(this, '{{ item.fields.id }}', '{{ item.html_id() }}', '{{ item.url_for_missing() }}')" autocomplete="off"> + {% else %} diff --git a/templates/set/card.html b/templates/set/card.html index e9612eb..20bfe4a 100644 --- a/templates/set/card.html +++ b/templates/set/card.html @@ -11,8 +11,8 @@ {% for checkbox in brickset_checkboxes %}data-{{ checkbox.as_dataset() }}="{{ item.fields[checkbox.as_column()] }}" {% endfor %} {% endif %} > - {{ card.header(item, item.fields.name, solo=solo, number=item.fields.number) }} - {{ card.image(item, solo=solo, last=last, caption=item.fields.name, alt=item.fields.number) }} + {{ card.header(item, item.fields.name, solo=solo, identifier=item.fields.set) }} + {{ card.image(item, solo=solo, last=last, caption=item.fields.name, alt=item.fields.set) }}
{{ badge.theme(item.theme.name, solo=solo, last=last) }} {{ badge.year(item.fields.year, solo=solo, last=last) }} diff --git a/templates/set/mini.html b/templates/set/mini.html index 6bdc028..6305e0e 100644 --- a/templates/set/mini.html +++ b/templates/set/mini.html @@ -2,7 +2,7 @@ {% import 'macro/card.html' as card %}
- {{ card.header(item, item.fields.name, solo=true, number=item.fields.set) }} + {{ card.header(item, item.fields.name, solo=true, identifier=item.fields.set) }} {{ card.image(item, solo=true, last=false, caption=item.fields.name, alt=item.fields.set) }}
{{ badge.theme(item.theme.name) }} From 482817fd962fe3c86e9e7675d24d00847ca1bc21 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Tue, 28 Jan 2025 21:10:14 +0100 Subject: [PATCH 026/154] Add purchase location to the database --- bricktracker/sql/migrations/0007.sql | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/bricktracker/sql/migrations/0007.sql b/bricktracker/sql/migrations/0007.sql index a75f3f2..f955bb9 100644 --- a/bricktracker/sql/migrations/0007.sql +++ b/bricktracker/sql/migrations/0007.sql @@ -9,9 +9,14 @@ ALTER TABLE "bricktracker_sets" RENAME TO "bricktracker_sets_old"; -- Create a Bricktracker set storage table for later CREATE TABLE "bricktracker_set_storages" ( - "id" TEXT NOT NULL, "name" TEXT NOT NULL, - PRIMARY KEY("id") + PRIMARY KEY("name") +); + +-- Create a Bricktracker set storage table for later +CREATE TABLE "bricktracker_set_purchase_locations" ( + "name" TEXT NOT NULL, + PRIMARY KEY("name") ); -- Re-Create a Bricktracker set table with the simplified name @@ -22,10 +27,12 @@ CREATE TABLE "bricktracker_sets" ( "theme" TEXT, -- Custom theme name "storage" TEXT, -- Storage bin location "purchase_date" INTEGER, -- Purchase data + "purchase_location" TEXT, -- Purchase location "purchase_price" REAL, -- Purchase price PRIMARY KEY("id"), FOREIGN KEY("set") REFERENCES "rebrickable_sets"("set"), - FOREIGN KEY("storage") REFERENCES "bricktracker_set_storages"("id") + FOREIGN KEY("storage") REFERENCES "bricktracker_set_storages"("name"), + FOREIGN KEY("purchase_location") REFERENCES "bricktracker_set_purchase_locations"("name") ); -- Insert existing sets into the new table From fc6ff5dd497cd53a4820d7167fb0fc42eb3e959e Mon Sep 17 00:00:00 2001 From: Gregoo Date: Tue, 28 Jan 2025 23:07:12 +0100 Subject: [PATCH 027/154] Add a refresh mode for sets --- bricktracker/minifigure.py | 10 +-- bricktracker/minifigure_list.py | 10 ++- bricktracker/part.py | 7 +- bricktracker/part_list.py | 3 +- bricktracker/rebrickable_minifigure.py | 19 ++---- bricktracker/rebrickable_part.py | 21 +++--- bricktracker/rebrickable_set.py | 13 ++-- bricktracker/record.py | 8 +-- bricktracker/set.py | 59 ++++++++++++----- bricktracker/set_checkbox.py | 7 +- .../sql/rebrickable/minifigure/insert.sql | 6 ++ bricktracker/sql/rebrickable/part/insert.sql | 13 ++++ bricktracker/sql/rebrickable/set/insert.sql | 12 ++++ bricktracker/views/set.py | 15 +++++ static/scripts/socket/set.js | 8 ++- templates/add.html | 4 +- templates/bulk.html | 2 +- templates/refresh.html | 64 +++++++++++++++++++ templates/set/card.html | 3 + templates/set/socket.html | 23 ++++--- 20 files changed, 224 insertions(+), 83 deletions(-) create mode 100644 templates/refresh.html diff --git a/bricktracker/minifigure.py b/bricktracker/minifigure.py index e0318a0..1ad6aa6 100644 --- a/bricktracker/minifigure.py +++ b/bricktracker/minifigure.py @@ -20,7 +20,7 @@ class BrickMinifigure(RebrickableMinifigure): select_query: str = 'minifigure/select/specific' # Import a minifigure into the database - def download(self, socket: 'BrickSocket') -> bool: + def download(self, socket: 'BrickSocket', refresh: bool = False) -> bool: if self.brickset is None: raise ErrorException('Importing a minifigure from Rebrickable outside of a set is not supported') # noqa: E501 @@ -33,8 +33,9 @@ class BrickMinifigure(RebrickableMinifigure): ) ) - # Insert into database - self.insert(commit=False) + if not refresh: + # Insert into database + self.insert(commit=False) # Insert the rebrickable set into database self.insert_rebrickable() @@ -43,7 +44,8 @@ class BrickMinifigure(RebrickableMinifigure): if not BrickPartList.download( socket, self.brickset, - minifigure=self + minifigure=self, + refresh=refresh ): return False diff --git a/bricktracker/minifigure_list.py b/bricktracker/minifigure_list.py index a59fee5..790018a 100644 --- a/bricktracker/minifigure_list.py +++ b/bricktracker/minifigure_list.py @@ -134,7 +134,13 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): # Import the minifigures from Rebrickable @staticmethod - def download(socket: 'BrickSocket', brickset: 'BrickSet', /) -> bool: + def download( + socket: 'BrickSocket', + brickset: 'BrickSet', + /, + *, + refresh: bool = False + ) -> bool: try: socket.auto_progress( message='Set {set}: loading minifigures from Rebrickable'.format( # noqa: E501 @@ -157,7 +163,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): # Process each minifigure for minifigure in minifigures: - if not minifigure.download(socket): + if not minifigure.download(socket, refresh=refresh): return False return True diff --git a/bricktracker/part.py b/bricktracker/part.py index 7e82c45..af901b2 100644 --- a/bricktracker/part.py +++ b/bricktracker/part.py @@ -34,7 +34,7 @@ class BrickPart(RebrickablePart): self.kind = 'Set' # Import a part into the database - def download(self, socket: 'BrickSocket') -> bool: + def download(self, socket: 'BrickSocket', refresh: bool = False) -> bool: if self.brickset is None: raise ErrorException('Importing a part from Rebrickable outside of a set is not supported') # noqa: E501 @@ -48,8 +48,9 @@ class BrickPart(RebrickablePart): ) ) - # Insert into database - self.insert(commit=False) + if not refresh: + # Insert into database + self.insert(commit=False) # Insert the rebrickable set into database self.insert_rebrickable() diff --git a/bricktracker/part_list.py b/bricktracker/part_list.py index 0074b9b..667c26e 100644 --- a/bricktracker/part_list.py +++ b/bricktracker/part_list.py @@ -139,6 +139,7 @@ class BrickPartList(BrickRecordList[BrickPart]): /, *, minifigure: 'BrickMinifigure | None' = None, + refresh: bool = False ) -> bool: if minifigure is not None: identifier = minifigure.fields.figure @@ -174,7 +175,7 @@ class BrickPartList(BrickRecordList[BrickPart]): # Process each part for part in inventory: - if not part.download(socket): + if not part.download(socket, refresh=refresh): return False except Exception as e: diff --git a/bricktracker/rebrickable_minifigure.py b/bricktracker/rebrickable_minifigure.py index 973b9fb..30d61ee 100644 --- a/bricktracker/rebrickable_minifigure.py +++ b/bricktracker/rebrickable_minifigure.py @@ -38,27 +38,22 @@ class RebrickableMinifigure(BrickRecord): self.ingest(record) # Insert the minifigure from Rebrickable - def insert_rebrickable(self, /) -> bool: + def insert_rebrickable(self, /) -> None: if self.brickset is None: raise ErrorException('Importing a minifigure from Rebrickable outside of a set is not supported') # noqa: E501 # Insert the Rebrickable minifigure to the database - rows, _ = self.insert( + self.insert( commit=False, no_defer=True, override_query=RebrickableMinifigure.insert_query ) - inserted = rows > 0 - - if inserted: - if not current_app.config['USE_REMOTE_IMAGES']: - RebrickableImage( - self.brickset, - minifigure=self, - ).download() - - return inserted + if not current_app.config['USE_REMOTE_IMAGES']: + RebrickableImage( + self.brickset, + minifigure=self, + ).download() # Return a dict with common SQL parameters for a minifigure def sql_parameters(self, /) -> dict[str, Any]: diff --git a/bricktracker/rebrickable_part.py b/bricktracker/rebrickable_part.py index 93c6b34..704990c 100644 --- a/bricktracker/rebrickable_part.py +++ b/bricktracker/rebrickable_part.py @@ -48,28 +48,23 @@ class RebrickablePart(BrickRecord): self.ingest(record) # Insert the part from Rebrickable - def insert_rebrickable(self, /) -> bool: + def insert_rebrickable(self, /) -> None: if self.brickset is None: raise ErrorException('Importing a part from Rebrickable outside of a set is not supported') # noqa: E501 # Insert the Rebrickable part to the database - rows, _ = self.insert( + self.insert( commit=False, no_defer=True, override_query=RebrickablePart.insert_query ) - inserted = rows > 0 - - if inserted: - if not current_app.config['USE_REMOTE_IMAGES']: - RebrickableImage( - self.brickset, - minifigure=self.minifigure, - part=self, - ).download() - - return inserted + if not current_app.config['USE_REMOTE_IMAGES']: + RebrickableImage( + self.brickset, + minifigure=self.minifigure, + part=self, + ).download() # Return a dict with common SQL parameters for a part def sql_parameters(self, /) -> dict[str, Any]: diff --git a/bricktracker/rebrickable_set.py b/bricktracker/rebrickable_set.py index 1cd4b8d..fbf10f1 100644 --- a/bricktracker/rebrickable_set.py +++ b/bricktracker/rebrickable_set.py @@ -47,21 +47,16 @@ class RebrickableSet(BrickRecord): self.ingest(record) # Insert the set from Rebrickable - def insert_rebrickable(self, /) -> bool: + def insert_rebrickable(self, /) -> None: # Insert the Rebrickable set to the database - rows, _ = self.insert( + self.insert( commit=False, no_defer=True, override_query=RebrickableSet.insert_query ) - inserted = rows > 0 - - if inserted: - if not current_app.config['USE_REMOTE_IMAGES']: - RebrickableImage(self).download() - - return inserted + if not current_app.config['USE_REMOTE_IMAGES']: + RebrickableImage(self).download() # Ingest a set def ingest(self, record: Row | dict[str, Any], /): diff --git a/bricktracker/record.py b/bricktracker/record.py index 08651d2..f7cc889 100644 --- a/bricktracker/record.py +++ b/bricktracker/record.py @@ -1,5 +1,5 @@ from sqlite3 import Row -from typing import Any, ItemsView, Tuple +from typing import Any, ItemsView from .fields import BrickRecordFields from .sql import BrickSQL @@ -31,14 +31,14 @@ class BrickRecord(object): commit=True, no_defer=False, override_query: str | None = None - ) -> Tuple[int, str]: + ) -> None: if override_query: query = override_query else: query = self.insert_query database = BrickSQL() - rows, q = database.execute( + database.execute( query, parameters=self.sql_parameters(), defer=not commit and not no_defer, @@ -47,8 +47,6 @@ class BrickRecord(object): if commit: database.commit() - return rows, q - # Shorthand to field items def items(self, /) -> ItemsView[str, Any]: return self.fields.__dict__.items() diff --git a/bricktracker/set.py b/bricktracker/set.py index 63d4128..28f0341 100644 --- a/bricktracker/set.py +++ b/bricktracker/set.py @@ -47,21 +47,25 @@ class BrickSet(RebrickableSet): increment_total=True, ) + # Grabbing the refresh flag + refresh: bool = bool(data.get('refresh', False)) + # Generate an UUID for self self.fields.id = str(uuid4()) - # Insert into database - self.insert(commit=False) + if not refresh: + # Insert into database + self.insert(commit=False) # Insert the rebrickable set into database self.insert_rebrickable() # Load the inventory - if not BrickPartList.download(socket, self): + if not BrickPartList.download(socket, self, refresh=refresh): return False # Load the minifigures - if not BrickMinifigureList.download(socket, self): + if not BrickMinifigureList.download(socket, self, refresh=refresh): return False # Commit the transaction to the database @@ -74,20 +78,34 @@ class BrickSet(RebrickableSet): BrickSQL().commit() - # Info - logger.info('Set {set}: imported (id: {id})'.format( - set=self.fields.set, - id=self.fields.id, - )) - - # Complete - socket.complete( - message='Set {set}: imported (Go to the set)'.format( # noqa: E501 + if refresh: + # Info + logger.info('Set {set}: imported (id: {id})'.format( set=self.fields.set, - url=self.url() - ), - download=True - ) + id=self.fields.id, + )) + + # Complete + socket.complete( + message='Set {set}: refreshed'.format( # noqa: E501 + set=self.fields.set, + ), + download=True + ) + else: + # Info + logger.info('Set {set}: refreshed'.format( + set=self.fields.set, + )) + + # Complete + socket.complete( + message='Set {set}: imported (Go to the set)'.format( # noqa: E501 + set=self.fields.set, + url=self.url() + ), + download=True + ) except Exception as e: socket.fail( @@ -192,3 +210,10 @@ class BrickSet(RebrickableSet): ) else: return '' + + # Compute the url for the refresh button + def url_for_refresh(self, /) -> str: + return url_for( + 'set.refresh', + id=self.fields.id, + ) diff --git a/bricktracker/set_checkbox.py b/bricktracker/set_checkbox.py index ea6d6d2..38a10f0 100644 --- a/bricktracker/set_checkbox.py +++ b/bricktracker/set_checkbox.py @@ -1,5 +1,5 @@ from sqlite3 import Row -from typing import Any, Self, Tuple +from typing import Any, Self from uuid import uuid4 from flask import url_for @@ -60,7 +60,7 @@ class BrickSetCheckbox(BrickRecord): return self # Insert into database - def insert(self, **_) -> Tuple[int, str]: + def insert(self, **_) -> None: # Generate an ID for the checkbox (with underscores to make it # column name friendly) self.fields.id = str(uuid4()).replace('-', '_') @@ -72,9 +72,6 @@ class BrickSetCheckbox(BrickRecord): displayed_on_grid=self.fields.displayed_on_grid ) - # To accomodate the parent().insert we have overriden - return 0, '' - # Rename the checkbox def rename(self, /) -> None: # Update the name diff --git a/bricktracker/sql/rebrickable/minifigure/insert.sql b/bricktracker/sql/rebrickable/minifigure/insert.sql index 0671925..6c0ac8e 100644 --- a/bricktracker/sql/rebrickable/minifigure/insert.sql +++ b/bricktracker/sql/rebrickable/minifigure/insert.sql @@ -9,3 +9,9 @@ INSERT OR IGNORE INTO "rebrickable_minifigures" ( :name, :image ) +ON CONFLICT("figure") +DO UPDATE SET +"number" = :number, +"name" = :name, +"image" = :image +WHERE "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM :figure diff --git a/bricktracker/sql/rebrickable/part/insert.sql b/bricktracker/sql/rebrickable/part/insert.sql index d989258..fcec4ef 100644 --- a/bricktracker/sql/rebrickable/part/insert.sql +++ b/bricktracker/sql/rebrickable/part/insert.sql @@ -23,3 +23,16 @@ INSERT OR IGNORE INTO "rebrickable_parts" ( :url, :print ) +ON CONFLICT("part", "color_id") +DO UPDATE SET +"color_name" = :color_name, +"color_rgb" = :color_rgb, +"color_transparent" = :color_transparent, +"name" = :name, +"category" = :category, +"image" = :image, +"image_id" = :image_id, +"url" = :url, +"print" = :print +WHERE "rebrickable_parts"."part" IS NOT DISTINCT FROM :part +AND "rebrickable_parts"."color_id" IS NOT DISTINCT FROM :color_id \ No newline at end of file diff --git a/bricktracker/sql/rebrickable/set/insert.sql b/bricktracker/sql/rebrickable/set/insert.sql index 88b2b44..39e6964 100644 --- a/bricktracker/sql/rebrickable/set/insert.sql +++ b/bricktracker/sql/rebrickable/set/insert.sql @@ -21,3 +21,15 @@ INSERT OR IGNORE INTO "rebrickable_sets" ( :url, :last_modified ) +ON CONFLICT("set") +DO UPDATE SET + "number" = :number, + "version" = :version, + "name" = :name, + "year" = :year, + "theme_id" = :theme_id, + "number_of_parts" = :number_of_parts, + "image" = :image, + "url" = :url, + "last_modified" = :last_modified +WHERE "rebrickable_sets"."set" IS NOT DISTINCT FROM :set diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py index 0b8d843..117ff03 100644 --- a/bricktracker/views/set.py +++ b/bricktracker/views/set.py @@ -2,6 +2,7 @@ import logging from flask import ( Blueprint, + current_app, jsonify, render_template, redirect, @@ -17,6 +18,7 @@ from ..part import BrickPart from ..set import BrickSet from ..set_checkbox_list import BrickSetCheckboxList from ..set_list import BrickSetList +from ..socket import MESSAGES logger = logging.getLogger(__name__) @@ -154,3 +156,16 @@ def missing_part( )) return jsonify({'missing': missing}) + + +# Refresh a set +@set_page.route('//refresh', methods=['GET']) +@exception_handler(__file__) +def refresh(*, id: str) -> str: + return render_template( + 'refresh.html', + item=BrickSet().select_specific(id), + path=current_app.config['SOCKET_PATH'], + namespace=current_app.config['SOCKET_NAMESPACE'], + messages=MESSAGES + ) diff --git a/static/scripts/socket/set.js b/static/scripts/socket/set.js index 41056b8..4f6bf97 100644 --- a/static/scripts/socket/set.js +++ b/static/scripts/socket/set.js @@ -1,8 +1,11 @@ // Set Socket class class BrickSetSocket extends BrickSocket { - constructor(id, path, namespace, messages, bulk=false) { + constructor(id, path, namespace, messages, bulk=false, refresh=false) { super(id, path, namespace, messages, bulk); + // Refresh mode + this.refresh = true + // Listeners this.add_listener = undefined; this.input_listener = undefined; @@ -82,7 +85,7 @@ class BrickSetSocket extends BrickSocket { this.read_set_list(); } - if (this.bulk || (this.html_no_confim && this.html_no_confim.checked)) { + if (this.bulk || this.refresh || (this.html_no_confim && this.html_no_confim.checked)) { this.import_set(true); } else { this.load_set(); @@ -140,6 +143,7 @@ class BrickSetSocket extends BrickSocket { this.socket.emit(this.messages.IMPORT_SET, { set: (set !== undefined) ? set : this.html_input.value, + refresh: this.refresh }); } else { this.fail("Could not find the input field for the set number"); diff --git a/templates/add.html b/templates/add.html index 140eec6..5316ea1 100644 --- a/templates/add.html +++ b/templates/add.html @@ -68,5 +68,7 @@
-{% include 'set/socket.html' %} +{% with id='add' %} + {% include 'set/socket.html' %} +{% endwith %} {% endblock %} diff --git a/templates/bulk.html b/templates/bulk.html index 6e6e5d8..00d4779 100644 --- a/templates/bulk.html +++ b/templates/bulk.html @@ -58,7 +58,7 @@
-{% with bulk=true %} +{% with id='add', bulk=true %} {% include 'set/socket.html' %} {% endwith %} {% endblock %} diff --git a/templates/refresh.html b/templates/refresh.html new file mode 100644 index 0000000..5add93d --- /dev/null +++ b/templates/refresh.html @@ -0,0 +1,64 @@ +{% extends 'base.html' %} + +{% block title %} - Refresh set {{ item.fields.set }}{% endblock %} + +{% block main %} +
+ +
+
+
+
+
Refresh a set
+
+
+ + +
+ + +
+
+
+

+ Progress + + + Loading... + +

+
+
+
+

+
+
+
+
+
+ {{ item.fields.set }} + {{ item.fields.name }} +
+
+
+ +
+
+
+
+ +
+
+
+
+{% with id='refresh', refresh=true %} + {% include 'set/socket.html' %} +{% endwith %} +{% endblock %} diff --git a/templates/set/card.html b/templates/set/card.html index 20bfe4a..02d77b3 100644 --- a/templates/set/card.html +++ b/templates/set/card.html @@ -63,6 +63,9 @@ {% endfor %} {% endif %} {% if g.login.is_authenticated() %} + {{ accordion.header('Management', 'management', 'set-details', icon='settings-4-line', class='text-end') }} + Refresh the set + {{ accordion.footer() }} {{ accordion.header('Danger zone', 'danger-zone', 'set-details', expanded=delete, danger=true, class='text-end') }} {% if delete %}
diff --git a/templates/set/socket.html b/templates/set/socket.html index a566a95..c4000a8 100644 --- a/templates/set/socket.html +++ b/templates/set/socket.html @@ -1,12 +1,19 @@ From 71ccfcd23d50a8c8c677cbe0afdf1127c56063e7 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Tue, 28 Jan 2025 23:07:54 +0100 Subject: [PATCH 028/154] Remove leftover debug prints --- bricktracker/views/set.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py index 117ff03..44eabed 100644 --- a/bricktracker/views/set.py +++ b/bricktracker/views/set.py @@ -122,9 +122,6 @@ def missing_part( color: int, spare: int, ) -> Response: - from pprint import pprint - pprint(locals()) - brickset = BrickSet().select_specific(id) if figure is not None: From fe13cfdb081327e01e974a8f446f119d2e054005 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Tue, 28 Jan 2025 23:31:20 +0100 Subject: [PATCH 029/154] Collapsible grid controls --- templates/sets.html | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/templates/sets.html b/templates/sets.html index ea08c2d..c62d3f3 100644 --- a/templates/sets.html +++ b/templates/sets.html @@ -9,14 +9,14 @@
- Search + Search
- Filter + Filter
{{ item.fields.color_name }} + {% if item.fields.color_rgb %}{% endif %} + {{ item.fields.color_name }} + {{ item.fields.total_quantity }}
+ {{ item.fields.name }} {% if item.fields.spare %} Spare{% endif %} {% if all %} @@ -15,7 +15,7 @@ {{ table.bricklink(item) }} {% endif %} + {% if item.fields.color_rgb %}{% endif %} {{ item.fields.color_name }} - {% if item.fields.color_rgb %}{% endif %} + {% if item.fields.color_rgb %}{% endif %} {{ item.fields.color_name }} - {% if all or read_only_missing %} - {{ item.fields.total_missing }} - {% else %} -
- {{ form.input(item.fields.id, item.html_id(), item.url_for_missing(), item.fields.total_missing) }} -
- {% endif %} +
+ {{ form.input('Missing', item.fields.id, item.html_id(), item.url_for_missing(), item.fields.total_missing, all=all, read_only=read_only_missing) }} - {{ form.input('Missing', item.fields.id, item.html_id(), item.url_for_missing(), item.fields.total_missing, all=all, read_only=read_only_missing) }} + {{ form.input('Missing', item.fields.id, item.html_id(), item.url_for_missing(), item.fields.total_missing, all=all, read_only=read_only) }}
{{ item.fields.set }} {{ table.rebrickable(item) }}{{ item.fields.name }}{{ item.theme.name }}{{ item.fields.year }}{{ item.fields.number_of_parts }}{% if retirement_date %}{{ retirement_date }}{% else %}Not found{% endif %} -
- -
-
{{ item.fields.set }} {{ table.rebrickable(item) }}{{ item.fields.name }}{{ item.theme.name }}{{ item.fields.year }}{{ item.fields.number_of_parts }}{% if retirement_date %}{{ retirement_date }}{% else %}Not found{% endif %} +
+ +
+
From 23515526c829ebdfd68d4362934ad295c9485bc2 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 31 Jan 2025 11:21:10 +0100 Subject: [PATCH 076/154] Make the grid controls normal sized --- templates/sets.html | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/templates/sets.html b/templates/sets.html index 27aff00..2b3271c 100644 --- a/templates/sets.html +++ b/templates/sets.html @@ -16,21 +16,21 @@
Sort - - - - - - - -
@@ -40,7 +40,7 @@
Status - From 6ec4f160f76d846fc148bea950e25c6129d8e746 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 31 Jan 2025 11:23:38 +0100 Subject: [PATCH 077/154] Make filters collapsible --- templates/sets.html | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/templates/sets.html b/templates/sets.html index 2b3271c..c3a93d6 100644 --- a/templates/sets.html +++ b/templates/sets.html @@ -31,11 +31,18 @@ + data-sort-clear="true"> Clear +
+
+
+
+
-
+
From 6011173c1f394e7dc9a1e11d8ca9ea1b8619f0dc Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 31 Jan 2025 11:31:38 +0100 Subject: [PATCH 078/154] Make the default collapsed state of grid filters configurable through a variable --- .env.sample | 4 ++++ bricktracker/config.py | 1 + templates/sets.html | 4 ++-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.env.sample b/.env.sample index 46b2131..d6141e6 100644 --- a/.env.sample +++ b/.env.sample @@ -226,6 +226,10 @@ # Default: sets # BK_SETS_FOLDER=sets +# Optional: Make the grid filters displayed by default, rather than collapsed +# Default: false +# BK_SHOW_GRID_FILTERS=true + # Optional: Skip saving or displaying spare parts # Default: false # BK_SKIP_SPARE_PARTS=true diff --git a/bricktracker/config.py b/bricktracker/config.py index 83cd99a..cbb64a8 100644 --- a/bricktracker/config.py +++ b/bricktracker/config.py @@ -52,6 +52,7 @@ CONFIG: Final[list[dict[str, Any]]] = [ {'n': 'RETIRED_SETS_PATH', 'd': './retired_sets.csv'}, {'n': 'SETS_DEFAULT_ORDER', 'd': '"rebrickable_sets"."number" DESC, "rebrickable_sets"."version" ASC'}, # noqa: E501 {'n': 'SETS_FOLDER', 'd': 'sets', 's': True}, + {'n': 'SHOW_GRID_FILTERS', 'c': bool}, {'n': 'SKIP_SPARE_PARTS', 'c': bool}, {'n': 'SOCKET_NAMESPACE', 'd': 'bricksocket'}, {'n': 'SOCKET_PATH', 'd': '/bricksocket/'}, diff --git a/templates/sets.html b/templates/sets.html index c3a93d6..97a7922 100644 --- a/templates/sets.html +++ b/templates/sets.html @@ -36,13 +36,13 @@
-
-
+
From ece15e97fb4323f7e6b2e65615d788f78797bfdc Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 31 Jan 2025 11:44:37 +0100 Subject: [PATCH 079/154] Fix the similar prints icon --- templates/part/card.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/part/card.html b/templates/part/card.html index 43dfcdc..547ffbc 100644 --- a/templates/part/card.html +++ b/templates/part/card.html @@ -22,7 +22,7 @@ {{ accordion.cards(sets_missing, 'Sets missing this part', 'sets-missing-inventory', 'part-details', 'set/card.html', icon='error-warning-line') }} {{ accordion.cards(minifigures_using, 'Minifigures using this part', 'minifigures-using-inventory', 'part-details', 'minifigure/card.html', icon='group-line') }} {{ accordion.cards(minifigures_missing, 'Minifigures missing this part', 'minifigures-missing-inventory', 'part-details', 'minifigure/card.html', icon='error-warning-line') }} - {{ accordion.cards(similar_prints, 'Prints using the same base', 'similar-prints', 'part-details', 'part/card.html', icon='palette-line') }} + {{ accordion.cards(similar_prints, 'Prints using the same base', 'similar-prints', 'part-details', 'part/card.html', icon='paint-brush-line') }}
{% endif %} From 6262ac7889d7343b2d59f5a805c3c3860d1f4337 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 31 Jan 2025 14:46:50 +0100 Subject: [PATCH 080/154] Use badge macros in the card header --- bricktracker/rebrickable_part.py | 13 ++++++++----- templates/macro/badge.html | 23 +++++++++++++++++++++-- templates/macro/card.html | 16 +++++----------- 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/bricktracker/rebrickable_part.py b/bricktracker/rebrickable_part.py index eea6d08..8fdd3cb 100644 --- a/bricktracker/rebrickable_part.py +++ b/bricktracker/rebrickable_part.py @@ -119,11 +119,14 @@ class RebrickablePart(BrickRecord): # Compute the url for the original of the printed part def url_for_print(self, /) -> str: - return url_for( - 'part.details', - part=self.fields.print, - color=self.fields.color, - ) + if self.fields.print is not None: + return url_for( + 'part.details', + part=self.fields.print, + color=self.fields.color, + ) + else: + return '' # Compute the url for the rebrickable page def url_for_rebrickable(self, /) -> str: diff --git a/templates/macro/badge.html b/templates/macro/badge.html index 70b10f1..722a357 100644 --- a/templates/macro/badge.html +++ b/templates/macro/badge.html @@ -1,14 +1,15 @@ -{% macro badge(check=none, url=none, solo=false, last=false, color='primary', blank=none, icon=none, alt=none, collapsible=none, text=none, tooltip=none) %} +{% macro badge(check=none, url=none, solo=false, last=false, header=false, color='primary', rgb=none, blank=none, icon=none, alt=none, collapsible=none, text=none, tooltip=none) %} {% if check or url %} {% if url %} {% if icon %}{% endif %} + {% if rgb %}{% endif %} {% if collapsible and not last %} {{ collapsible }} {% endif %} {% if text %}{{ text }}{% endif %} {% if url %} @@ -23,6 +24,20 @@ {{ badge(url=item.url_for_bricklink(), solo=solo, last=last, blank=true, color='light border', icon='external-link-line', collapsible='Bricklink', alt='Bricklink') }} {% endmacro %} +{% macro color(item, icon=none, solo=false, last=false, header=false) %} + {% if item.fields.color == 9999 %} + {% set rgb = 'any' %} + {% else %} + {% set rgb = item.fields.color_rgb %} + {% endif %} + {{ badge(check=item.fields.color_name, solo=solo, last=last, header=header, color=' bg-white text-black border', rgb=rgb, icon='palette-line', collapsible=item.fields.color_name) }} + {{ badge(check=item.fields.color_transparent, solo=solo, last=last, header=header, color='light border', icon='blur-off-line', collapsible='Transparent') }} +{% endmacro %} + +{% macro identifier(id, icon=none, solo=false, last=false, header=false) %} + {{ badge(check=id, solo=solo, last=last, header=header, color='secondary', icon=icon, text=id) }} +{% endmacro %} + {% macro instructions(item, solo=false, last=false) %} {{ badge(url=item.url_for_instructions(), solo=solo, last=last, blank=true, color='light border', icon='file-line', collapsible='Instructions:', text=item.instructions | length, alt='Instructions') }} {% endmacro %} @@ -35,6 +50,10 @@ {{ badge(check=quantity, solo=solo, last=last, color='success', icon='close-line', collapsible='Quantity:', text=quantity, alt='Quantity') }} {% endmacro %} +{% macro print(item, solo=false, last=false, header=false) %} + {{ badge(url=item.url_for_print(), solo=solo, last=last, color='light border', icon='paint-brush-line', collapsible='Print') }} +{% endmacro %} + {% macro set(set, solo=false, last=false, url=None, id=None) %} {% if id %} {% set url=url_for('set.details', id=id) %} diff --git a/templates/macro/card.html b/templates/macro/card.html index ae8bce0..be6ad74 100644 --- a/templates/macro/card.html +++ b/templates/macro/card.html @@ -1,21 +1,15 @@ +{% import 'macro/badge.html' as badge %} + {% macro header(item, name, solo=false, identifier=none, icon='hashtag') %}
{% if not solo %} {% endif %}
- {% if identifier %}{{ identifier }}{% endif %} + {{ badge.identifier(identifier, icon=icon, solo=solo, header=true) }} {% if solo %} - {% if item.fields.color_name %} - - {% if item.fields.color_rgb %} - - {% endif %} - {{ item.fields.color_name }} - - {% endif %} - {% if item.fields.color_transparent %} Transparent{% endif %} - {% if item.fields.print %} Print{% endif %} + {{ badge.color(item, header=true) }} + {{ badge.print(item, header=true) }} {% endif %} {{ name }}
From adb2170d47f7b833002195d652ec6af7a24b45c7 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 31 Jan 2025 14:54:00 +0100 Subject: [PATCH 081/154] Fix print badge for elements no having this field --- templates/macro/badge.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/templates/macro/badge.html b/templates/macro/badge.html index 722a357..3d8a5e2 100644 --- a/templates/macro/badge.html +++ b/templates/macro/badge.html @@ -51,7 +51,9 @@ {% 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') }} + {% endif %} {% endmacro %} {% macro set(set, solo=false, last=false, url=None, id=None) %} From 47261ed4208399252e4823a872e7df14bd7ce21f Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 31 Jan 2025 14:57:16 +0100 Subject: [PATCH 082/154] Display color and print for part cards not solo --- templates/part/card.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/templates/part/card.html b/templates/part/card.html index 547ffbc..4cb4031 100644 --- a/templates/part/card.html +++ b/templates/part/card.html @@ -6,6 +6,10 @@ {{ card.header(item, item.fields.name, solo=solo, identifier=item.fields.part, icon='shapes-line') }} {{ card.image(item, solo=solo, last=last, caption=item.fields.name, alt=item.fields.image_id, medium=true) }}
+ {% if not solo %} + {{ badge.color(item) }} + {{ badge.print(item) }} + {% endif %} {{ badge.total_sets(sets_using | length, solo=solo, last=last) }} {{ badge.total_minifigures(minifigures_using | length, solo=solo, last=last) }} {{ badge.total_quantity(item.fields.total_quantity, solo=solo, last=last) }} From 5fcd76febb30e30cff5c65e4e7a2587f5ea2e606 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 31 Jan 2025 15:29:32 +0100 Subject: [PATCH 083/154] Missing quotes around SQL identifier --- bricktracker/sql/minifigure/list/all.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bricktracker/sql/minifigure/list/all.sql b/bricktracker/sql/minifigure/list/all.sql index ca23068..e3ce2bd 100644 --- a/bricktracker/sql/minifigure/list/all.sql +++ b/bricktracker/sql/minifigure/list/all.sql @@ -24,7 +24,7 @@ LEFT JOIN ( GROUP BY "bricktracker_parts"."id", "bricktracker_parts"."figure" -) missing_join +) "missing_join" ON "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "missing_join"."id" AND "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "missing_join"."figure" {% endblock %} From d4037cd953525c238de62c68667775d7a663a0af Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 31 Jan 2025 15:43:32 +0100 Subject: [PATCH 084/154] Fix socket always in refresh mode --- static/scripts/socket/set.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/scripts/socket/set.js b/static/scripts/socket/set.js index 4f6bf97..07d7cc7 100644 --- a/static/scripts/socket/set.js +++ b/static/scripts/socket/set.js @@ -4,7 +4,7 @@ class BrickSetSocket extends BrickSocket { super(id, path, namespace, messages, bulk); // Refresh mode - this.refresh = true + this.refresh = refresh // Listeners this.add_listener = undefined; From ba8744befb85448f123a0abd6022d1d3ea9d4b8c Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 31 Jan 2025 15:45:28 +0100 Subject: [PATCH 085/154] Merge add and bulk add templates --- bricktracker/views/add.py | 5 +-- templates/add.html | 33 ++++++++++++-------- templates/bulk.html | 64 --------------------------------------- 3 files changed, 23 insertions(+), 79 deletions(-) delete mode 100644 templates/bulk.html diff --git a/bricktracker/views/add.py b/bricktracker/views/add.py index 218a0bf..44f3ddc 100644 --- a/bricktracker/views/add.py +++ b/bricktracker/views/add.py @@ -31,8 +31,9 @@ def bulk() -> str: BrickConfigurationList.error_unless_is_set('REBRICKABLE_API_KEY') return render_template( - 'bulk.html', + 'add.html', path=current_app.config['SOCKET_PATH'], namespace=current_app.config['SOCKET_NAMESPACE'], - messages=MESSAGES + messages=MESSAGES, + bulk=True ) diff --git a/templates/add.html b/templates/add.html index 5316ea1..1238739 100644 --- a/templates/add.html +++ b/templates/add.html @@ -1,10 +1,10 @@ {% extends 'base.html' %} -{% block title %} - Add a set{% endblock %} +{% block title %} - {% if not bulk %}Add a set{% else %}Bulk add sets{% endif %}{% endblock %} {% block main %}
- {% if not config['HIDE_ADD_BULK_SET'] %} + {% if not bulk and not config['HIDE_ADD_BULK_SET'] %}
-{% with id='add' %} +{% with id='add', bulk=bulk %} {% include 'set/socket.html' %} {% endwith %} {% endblock %} diff --git a/templates/bulk.html b/templates/bulk.html deleted file mode 100644 index 00d4779..0000000 --- a/templates/bulk.html +++ /dev/null @@ -1,64 +0,0 @@ -{% extends 'base.html' %} - -{% block title %} - Bulk add sets{% endblock %} - -{% block main %} -
-
-
-
-
-
Bulk add sets
-
-
- -
-
- - -
-
- - -
-
-
-

- Progress - - - Loading... - -

-
-
-
-

-
-
-
-
-
- - -
-
-
- -
-
-
-
- -
-
-
-
-{% with id='add', bulk=true %} - {% include 'set/socket.html' %} -{% endwith %} -{% endblock %} From b8d4f23a84955cc182cc249044ba9823018b7c24 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 31 Jan 2025 16:34:52 +0100 Subject: [PATCH 086/154] 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 %} From 030345fe6b669225b9056d08f910d8e878e3ac22 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 31 Jan 2025 16:37:42 +0100 Subject: [PATCH 087/154] Fix functions definition --- bricktracker/set_list.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/bricktracker/set_list.py b/bricktracker/set_list.py index 6a94185..e071dc6 100644 --- a/bricktracker/set_list.py +++ b/bricktracker/set_list.py @@ -103,12 +103,7 @@ class BrickSetList(BrickRecordList[BrickSet]): return self # Sets missing a part - def missing_part( - self, - part: str, - color: int, - / - ) -> Self: + def missing_part(self, part: str, color: int, /) -> Self: # Save the parameters to the fields self.fields.part = part self.fields.color = color @@ -141,12 +136,7 @@ class BrickSetList(BrickRecordList[BrickSet]): return self # Sets using a part - def using_part( - self, - part: str, - color: int, - / - ) -> Self: + def using_part(self, part: str, color: int, /) -> Self: # Save the parameters to the fields self.fields.part = part self.fields.color = color From c02321368aee8c8ca4d790d935f2471b99951106 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 31 Jan 2025 16:38:06 +0100 Subject: [PATCH 088/154] Disable no confirm checkbox when toggling the form --- static/scripts/socket/set.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/static/scripts/socket/set.js b/static/scripts/socket/set.js index 6459aa6..01c5106 100644 --- a/static/scripts/socket/set.js +++ b/static/scripts/socket/set.js @@ -259,6 +259,10 @@ class BrickSetSocket extends BrickSocket { this.html_input.disabled = !enabled; } + if (this.html_no_confim) { + this.html_no_confim.disabled = !enabled; + } + if (this.html_owners) { this.html_owners.querySelectorAll('input').forEach(input => input.disabled = !enabled); } From 739d933900b323b7e330c602f6cf7c10ab36ab14 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 31 Jan 2025 17:52:51 +0100 Subject: [PATCH 089/154] Fix broken list filtering on the grid --- static/scripts/grid/filter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/scripts/grid/filter.js b/static/scripts/grid/filter.js index e2eff7b..f5b075f 100644 --- a/static/scripts/grid/filter.js +++ b/static/scripts/grid/filter.js @@ -112,7 +112,7 @@ class BrickGridFilter { } else { // List search for (const list of this.search_list) { - if (set.startsWith(this.search_list)) { + if (set.startsWith(list)) { if (current.dataset[set].includes(options.search)) { current.parentElement.classList.remove("d-none"); return; From 5ad94078ed111f08e52cec36408874e28b37a9ea Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 31 Jan 2025 17:56:51 +0100 Subject: [PATCH 090/154] Don't toggle the no confirm button in bulk mode --- static/scripts/socket/set.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/scripts/socket/set.js b/static/scripts/socket/set.js index 01c5106..08e70dd 100644 --- a/static/scripts/socket/set.js +++ b/static/scripts/socket/set.js @@ -259,7 +259,7 @@ class BrickSetSocket extends BrickSocket { this.html_input.disabled = !enabled; } - if (this.html_no_confim) { + if (!this.bulk && this.html_no_confim) { this.html_no_confim.disabled = !enabled; } From f34bbe0602dc4e7354ed4aa4d089e90376ac151f Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 31 Jan 2025 18:08:53 +0100 Subject: [PATCH 091/154] Set tags --- bricktracker/app.py | 2 + bricktracker/metadata_list.py | 3 +- bricktracker/record_list.py | 2 + bricktracker/reload.py | 5 ++ bricktracker/set.py | 12 ++- bricktracker/set_list.py | 8 +- bricktracker/set_tag.py | 16 ++++ bricktracker/set_tag_list.py | 17 ++++ bricktracker/sql/migrations/0014.sql | 19 +++++ bricktracker/sql/schema/drop.sql | 2 + 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/tag/base.sql | 6 ++ bricktracker/sql/set/metadata/tag/delete.sql | 9 ++ bricktracker/sql/set/metadata/tag/insert.sql | 14 ++++ bricktracker/sql/set/metadata/tag/list.sql | 1 + bricktracker/sql/set/metadata/tag/select.sql | 5 ++ .../sql/set/metadata/tag/update/field.sql | 3 + .../sql/set/metadata/tag/update/state.sql | 10 +++ bricktracker/sql_counter.py | 1 + bricktracker/version.py | 2 +- bricktracker/views/add.py | 4 + bricktracker/views/admin/admin.py | 13 ++- bricktracker/views/admin/tag.py | 84 +++++++++++++++++++ bricktracker/views/index.py | 3 + bricktracker/views/set.py | 19 ++++- static/scripts/socket/set.js | 16 ++++ templates/add.html | 13 +++ templates/admin.html | 3 + templates/admin/tag.html | 42 ++++++++++ templates/admin/tag/delete.html | 19 +++++ templates/macro/badge.html | 13 ++- templates/set/card.html | 11 ++- templates/set/management.html | 50 +++++++---- templates/sets.html | 16 +++- 36 files changed, 424 insertions(+), 30 deletions(-) create mode 100644 bricktracker/set_tag.py create mode 100644 bricktracker/set_tag_list.py create mode 100644 bricktracker/sql/migrations/0014.sql create mode 100644 bricktracker/sql/set/metadata/tag/base.sql create mode 100644 bricktracker/sql/set/metadata/tag/delete.sql create mode 100644 bricktracker/sql/set/metadata/tag/insert.sql create mode 100644 bricktracker/sql/set/metadata/tag/list.sql create mode 100644 bricktracker/sql/set/metadata/tag/select.sql create mode 100644 bricktracker/sql/set/metadata/tag/update/field.sql create mode 100644 bricktracker/sql/set/metadata/tag/update/state.sql create mode 100644 bricktracker/views/admin/tag.py create mode 100644 templates/admin/tag.html create mode 100644 templates/admin/tag/delete.html diff --git a/bricktracker/app.py b/bricktracker/app.py index 15cb9a3..240bc63 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.tag import admin_tag_page from bricktracker.views.admin.theme import admin_theme_page from bricktracker.views.error import error_404 from bricktracker.views.index import index_page @@ -85,6 +86,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_tag_page) app.register_blueprint(admin_theme_page) # An helper to make global variables available to the diff --git a/bricktracker/metadata_list.py b/bricktracker/metadata_list.py index bb2e337..b0d42c3 100644 --- a/bricktracker/metadata_list.py +++ b/bricktracker/metadata_list.py @@ -6,10 +6,11 @@ from .fields import BrickRecordFields from .record_list import BrickRecordList from .set_owner import BrickSetOwner from .set_status import BrickSetStatus +from .set_tag import BrickSetTag logger = logging.getLogger(__name__) -T = TypeVar('T', 'BrickSetStatus', 'BrickSetOwner') +T = TypeVar('T', BrickSetStatus, BrickSetOwner, BrickSetTag) # Lego sets metadata list diff --git a/bricktracker/record_list.py b/bricktracker/record_list.py index 8927e71..3de9bf9 100644 --- a/bricktracker/record_list.py +++ b/bricktracker/record_list.py @@ -10,6 +10,7 @@ if TYPE_CHECKING: from .set import BrickSet from .set_owner import BrickSetOwner from .set_status import BrickSetStatus + from .set_tag import BrickSetTag from .wish import BrickWish T = TypeVar( @@ -17,6 +18,7 @@ T = TypeVar( 'BrickSet', 'BrickSetOwner', 'BrickSetStatus', + 'BrickSetTag', 'BrickPart', 'BrickMinifigure', 'BrickWish', diff --git a/bricktracker/reload.py b/bricktracker/reload.py index 73e9e24..16fca2f 100644 --- a/bricktracker/reload.py +++ b/bricktracker/reload.py @@ -4,6 +4,8 @@ from .set_owner import BrickSetOwner from .set_owner_list import BrickSetOwnerList from .set_status import BrickSetStatus from .set_status_list import BrickSetStatusList +from .set_tag import BrickSetTag +from .set_tag_list import BrickSetTagList from .theme_list import BrickThemeList @@ -20,6 +22,9 @@ def reload() -> None: # Reload the set statuses BrickSetStatusList(BrickSetStatus, force=True) + # Reload the set tags + BrickSetTagList(BrickSetTag, force=True) + # Reload retired sets BrickRetiredList(force=True) diff --git a/bricktracker/set.py b/bricktracker/set.py index f4bf1a2..eb2bafb 100644 --- a/bricktracker/set.py +++ b/bricktracker/set.py @@ -13,6 +13,8 @@ from .set_owner import BrickSetOwner from .set_owner_list import BrickSetOwnerList from .set_status import BrickSetStatus from .set_status_list import BrickSetStatusList +from .set_tag import BrickSetTag +from .set_tag_list import BrickSetTagList from .sql import BrickSQL if TYPE_CHECKING: from .socket import BrickSocket @@ -77,6 +79,13 @@ class BrickSet(RebrickableSet): 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( @@ -172,7 +181,8 @@ class BrickSet(RebrickableSet): # Load from database if not self.select( owners=BrickSetOwnerList(BrickSetOwner).as_columns(), - statuses=BrickSetStatusList(BrickSetStatus).as_columns(all=True) + statuses=BrickSetStatusList(BrickSetStatus).as_columns(all=True), + tags=BrickSetTagList(BrickSetTag).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 e071dc6..54a3cb8 100644 --- a/bricktracker/set_list.py +++ b/bricktracker/set_list.py @@ -7,6 +7,8 @@ from .set_owner import BrickSetOwner from .set_owner_list import BrickSetOwnerList from .set_status import BrickSetStatus from .set_status_list import BrickSetStatusList +from .set_tag import BrickSetTag +from .set_tag_list import BrickSetTagList from .set import BrickSet @@ -41,7 +43,8 @@ class BrickSetList(BrickRecordList[BrickSet]): for record in self.select( order=self.order, owners=BrickSetOwnerList(BrickSetOwner).as_columns(), - statuses=BrickSetStatusList(BrickSetStatus).as_columns() + statuses=BrickSetStatusList(BrickSetStatus).as_columns(), + tags=BrickSetTagList(BrickSetTag).as_columns(), ): brickset = BrickSet(record=record) @@ -78,7 +81,8 @@ class BrickSetList(BrickRecordList[BrickSet]): order=order, limit=limit, owners=BrickSetOwnerList(BrickSetOwner).as_columns(), - statuses=BrickSetStatusList(BrickSetStatus).as_columns() + statuses=BrickSetStatusList(BrickSetStatus).as_columns(), + tags=BrickSetTagList(BrickSetTag).as_columns(), ): brickset = BrickSet(record=record) diff --git a/bricktracker/set_tag.py b/bricktracker/set_tag.py new file mode 100644 index 0000000..6d81c18 --- /dev/null +++ b/bricktracker/set_tag.py @@ -0,0 +1,16 @@ +from .metadata import BrickMetadata + + +# Lego set tag metadata +class BrickSetTag(BrickMetadata): + kind: str = 'tag' + + # Set state endpoint + set_state_endpoint: str = 'set.update_tag' + + # Queries + delete_query: str = 'set/metadata/tag/delete' + insert_query: str = 'set/metadata/tag/insert' + select_query: str = 'set/metadata/tag/select' + update_field_query: str = 'set/metadata/tag/update/field' + update_set_state_query: str = 'set/metadata/tag/update/state' diff --git a/bricktracker/set_tag_list.py b/bricktracker/set_tag_list.py new file mode 100644 index 0000000..92806f2 --- /dev/null +++ b/bricktracker/set_tag_list.py @@ -0,0 +1,17 @@ +import logging + +from .metadata_list import BrickMetadataList +from .set_tag import BrickSetTag + +logger = logging.getLogger(__name__) + + +# Lego sets tag list +class BrickSetTagList(BrickMetadataList[BrickSetTag]): + kind: str = 'set tags' + + # Database table + table: str = 'bricktracker_set_tags' + + # Queries + select_query = 'set/metadata/tag/list' diff --git a/bricktracker/sql/migrations/0014.sql b/bricktracker/sql/migrations/0014.sql new file mode 100644 index 0000000..37c655e --- /dev/null +++ b/bricktracker/sql/migrations/0014.sql @@ -0,0 +1,19 @@ +-- description: Add set tags + +BEGIN TRANSACTION; + +-- Create a table to define each set tags: an id and a name +CREATE TABLE "bricktracker_metadata_tags" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + PRIMARY KEY("id") +); + +-- Create a table for the set tags +CREATE TABLE "bricktracker_set_tags" ( + "id" TEXT NOT NULL, + PRIMARY KEY("id"), + FOREIGN KEY("id") REFERENCES "bricktracker_sets"("id") +); + +COMMIT; diff --git a/bricktracker/sql/schema/drop.sql b/bricktracker/sql/schema/drop.sql index abc8522..1bab7d6 100644 --- a/bricktracker/sql/schema/drop.sql +++ b/bricktracker/sql/schema/drop.sql @@ -1,12 +1,14 @@ BEGIN transaction; DROP TABLE IF EXISTS "bricktracker_metadata_statuses"; +DROP TABLE IF EXISTS "bricktracker_metadata_tags"; DROP TABLE IF EXISTS "bricktracker_minifigures"; DROP TABLE IF EXISTS "bricktracker_parts"; DROP TABLE IF EXISTS "bricktracker_sets"; DROP TABLE IF EXISTS "bricktracker_set_checkboxes"; DROP TABLE IF EXISTS "bricktracker_set_statuses"; DROP TABLE IF EXISTS "bricktracker_set_storages"; +DROP TABLE IF EXISTS "bricktracker_set_tags"; DROP TABLE IF EXISTS "bricktracker_wishes"; DROP TABLE IF EXISTS "inventory"; DROP TABLE IF EXISTS "inventory_old"; diff --git a/bricktracker/sql/set/base/base.sql b/bricktracker/sql/set/base/base.sql index 940dab9..ffefe95 100644 --- a/bricktracker/sql/set/base/base.sql +++ b/bricktracker/sql/set/base/base.sql @@ -12,6 +12,9 @@ SELECT {% block owners %} {% if owners %}{{ owners }},{% endif %} {% endblock %} + {% block tags %} + {% if tags %}{{ tags }},{% 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 725b56d..271f890 100644 --- a/bricktracker/sql/set/base/full.sql +++ b/bricktracker/sql/set/base/full.sql @@ -23,6 +23,11 @@ LEFT JOIN "bricktracker_set_statuses" ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_statuses"."id" {% endif %} +{% if tags %} +LEFT JOIN "bricktracker_set_tags" +ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_tags"."id" +{% endif %} + -- LEFT JOIN + SELECT to avoid messing the total LEFT JOIN ( SELECT diff --git a/bricktracker/sql/set/delete/set.sql b/bricktracker/sql/set/delete/set.sql index 2db140d..4eca845 100644 --- a/bricktracker/sql/set/delete/set.sql +++ b/bricktracker/sql/set/delete/set.sql @@ -12,6 +12,9 @@ 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 }}'; +DELETE FROM "bricktracker_set_tags" +WHERE "bricktracker_set_tags"."id" IS NOT DISTINCT FROM '{{ id }}'; + DELETE FROM "bricktracker_minifigures" WHERE "bricktracker_minifigures"."id" IS NOT DISTINCT FROM '{{ id }}'; diff --git a/bricktracker/sql/set/metadata/tag/base.sql b/bricktracker/sql/set/metadata/tag/base.sql new file mode 100644 index 0000000..3ec5725 --- /dev/null +++ b/bricktracker/sql/set/metadata/tag/base.sql @@ -0,0 +1,6 @@ +SELECT + "bricktracker_metadata_tags"."id", + "bricktracker_metadata_tags"."name" +FROM "bricktracker_metadata_tags" + +{% block where %}{% endblock %} \ No newline at end of file diff --git a/bricktracker/sql/set/metadata/tag/delete.sql b/bricktracker/sql/set/metadata/tag/delete.sql new file mode 100644 index 0000000..a80bb8f --- /dev/null +++ b/bricktracker/sql/set/metadata/tag/delete.sql @@ -0,0 +1,9 @@ +BEGIN TRANSACTION; + +ALTER TABLE "bricktracker_set_tags" +DROP COLUMN "tag_{{ id }}"; + +DELETE FROM "bricktracker_metadata_tags" +WHERE "bricktracker_metadata_tags"."id" IS NOT DISTINCT FROM '{{ id }}'; + +COMMIT; \ No newline at end of file diff --git a/bricktracker/sql/set/metadata/tag/insert.sql b/bricktracker/sql/set/metadata/tag/insert.sql new file mode 100644 index 0000000..7a62866 --- /dev/null +++ b/bricktracker/sql/set/metadata/tag/insert.sql @@ -0,0 +1,14 @@ +BEGIN TRANSACTION; + +ALTER TABLE "bricktracker_set_tags" +ADD COLUMN "tag_{{ id }}" BOOLEAN NOT NULL DEFAULT 0; + +INSERT INTO "bricktracker_metadata_tags" ( + "id", + "name" +) VALUES ( + '{{ id }}', + '{{ name }}' +); + +COMMIT; \ No newline at end of file diff --git a/bricktracker/sql/set/metadata/tag/list.sql b/bricktracker/sql/set/metadata/tag/list.sql new file mode 100644 index 0000000..fe44b5f --- /dev/null +++ b/bricktracker/sql/set/metadata/tag/list.sql @@ -0,0 +1 @@ +{% extends 'set/metadata/tag/base.sql' %} diff --git a/bricktracker/sql/set/metadata/tag/select.sql b/bricktracker/sql/set/metadata/tag/select.sql new file mode 100644 index 0000000..2d52076 --- /dev/null +++ b/bricktracker/sql/set/metadata/tag/select.sql @@ -0,0 +1,5 @@ +{% extends 'set/metadata/tag/base.sql' %} + +{% block where %} +WHERE "bricktracker_metadata_tags"."id" IS NOT DISTINCT FROM :id +{% endblock %} \ No newline at end of file diff --git a/bricktracker/sql/set/metadata/tag/update/field.sql b/bricktracker/sql/set/metadata/tag/update/field.sql new file mode 100644 index 0000000..629a9e8 --- /dev/null +++ b/bricktracker/sql/set/metadata/tag/update/field.sql @@ -0,0 +1,3 @@ +UPDATE "bricktracker_metadata_tags" +SET "{{field}}" = :value +WHERE "bricktracker_metadata_tags"."id" IS NOT DISTINCT FROM :id diff --git a/bricktracker/sql/set/metadata/tag/update/state.sql b/bricktracker/sql/set/metadata/tag/update/state.sql new file mode 100644 index 0000000..18de40a --- /dev/null +++ b/bricktracker/sql/set/metadata/tag/update/state.sql @@ -0,0 +1,10 @@ +INSERT INTO "bricktracker_set_tags" ( + "id", + "{{name}}" +) VALUES ( + :set_id, + :state +) +ON CONFLICT("id") +DO UPDATE SET "{{name}}" = :state +WHERE "bricktracker_set_tags"."id" IS NOT DISTINCT FROM :set_id diff --git a/bricktracker/sql_counter.py b/bricktracker/sql_counter.py index 30c83d6..74c18cc 100644 --- a/bricktracker/sql_counter.py +++ b/bricktracker/sql_counter.py @@ -9,6 +9,7 @@ ALIASES: dict[str, Tuple[str, str]] = { '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_set_tags': ('Bricktracker set tags', 'price-tag-2-line'), # noqa: E501 'bricktracker_sets': ('Bricktracker sets', 'hashtag'), 'bricktracker_wishes': ('Bricktracker wishes', 'gift-line'), 'inventory': ('Parts', 'shapes-line'), diff --git a/bricktracker/version.py b/bricktracker/version.py index 996b4f6..767fad5 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] = 13 +__database_version__: Final[int] = 14 diff --git a/bricktracker/views/add.py b/bricktracker/views/add.py index 20607dc..9072973 100644 --- a/bricktracker/views/add.py +++ b/bricktracker/views/add.py @@ -5,6 +5,8 @@ from ..configuration_list import BrickConfigurationList from .exceptions import exception_handler from ..set_owner import BrickSetOwner from ..set_owner_list import BrickSetOwnerList +from ..set_tag import BrickSetTag +from ..set_tag_list import BrickSetTagList from ..socket import MESSAGES add_page = Blueprint('add', __name__, url_prefix='/add') @@ -20,6 +22,7 @@ def add() -> str: return render_template( 'add.html', brickset_owners=BrickSetOwnerList(BrickSetOwner).list(), + brickset_tags=BrickSetTagList(BrickSetTag).list(), path=current_app.config['SOCKET_PATH'], namespace=current_app.config['SOCKET_NAMESPACE'], messages=MESSAGES @@ -36,6 +39,7 @@ def bulk() -> str: return render_template( 'add.html', brickset_owners=BrickSetOwnerList(BrickSetOwner).list(), + brickset_tags=BrickSetTagList(BrickSetTag).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 36037d3..415cf48 100644 --- a/bricktracker/views/admin/admin.py +++ b/bricktracker/views/admin/admin.py @@ -12,6 +12,8 @@ from ...set_owner import BrickSetOwner from ...set_owner_list import BrickSetOwnerList from ...set_status import BrickSetStatus from ...set_status_list import BrickSetStatusList +from ...set_tag import BrickSetTag +from ...set_tag_list import BrickSetTagList from ...sql_counter import BrickCounter from ...sql import BrickSQL from ...theme_list import BrickThemeList @@ -32,6 +34,7 @@ def admin() -> str: database_version: int = -1 metadata_owners: list[BrickSetOwner] = [] metadata_statuses: list[BrickSetStatus] = [] + metadata_tags: list[BrickSetTag] = [] nil_minifigure_name: str = '' nil_minifigure_url: str = '' nil_part_name: str = '' @@ -46,6 +49,7 @@ def admin() -> str: metadata_owners = BrickSetOwnerList(BrickSetOwner).list() metadata_statuses = BrickSetStatusList(BrickSetStatus).list(all=True) + metadata_tags = BrickSetTagList(BrickSetTag).list() except Exception as e: database_exception = e @@ -72,6 +76,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_tag = request.args.get('open_tag', None) open_theme = request.args.get('open_theme', None) open_database = ( @@ -81,6 +86,7 @@ def admin() -> str: open_owner is None and open_retired is None and open_status is None and + open_tag is None and open_theme is None ) @@ -95,20 +101,23 @@ def admin() -> str: instructions=BrickInstructionsList(), metadata_owners=metadata_owners, metadata_statuses=metadata_statuses, + metadata_tags=metadata_tags, nil_minifigure_name=nil_minifigure_name, nil_minifigure_url=nil_minifigure_url, nil_part_name=nil_part_name, nil_part_url=nil_part_url, - open_status=open_status, open_database=open_database, open_image=open_image, open_instructions=open_instructions, open_logout=open_logout, open_owner=open_owner, open_retired=open_retired, + open_status=open_status, + open_tag=open_tag, open_theme=open_theme, owner_error=request.args.get('owner_error'), - status_error=request.args.get('status_error'), retired=BrickRetiredList(), + status_error=request.args.get('status_error'), + tag_error=request.args.get('tag_error'), theme=BrickThemeList(), ) diff --git a/bricktracker/views/admin/tag.py b/bricktracker/views/admin/tag.py new file mode 100644 index 0000000..d31bc49 --- /dev/null +++ b/bricktracker/views/admin/tag.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_tag import BrickSetTag + +admin_tag_page = Blueprint( + 'admin_tag', + __name__, + url_prefix='/admin/tag' +) + + +# Add a metadata tag +@admin_tag_page.route('/add', methods=['POST']) +@login_required +@exception_handler( + __file__, + post_redirect='admin.admin', + error_name='tag_error', + open_tag=True +) +def add() -> Response: + BrickSetTag().from_form(request.form).insert() + + reload() + + return redirect(url_for('admin.admin', open_tag=True)) + + +# Delete the metadata tag +@admin_tag_page.route('/delete', methods=['GET']) +@login_required +@exception_handler(__file__) +def delete(*, id: str) -> str: + return render_template( + 'admin.html', + delete_tag=True, + tag=BrickSetTag().select_specific(id), + error=request.args.get('tag_error') + ) + + +# Actually delete the metadata tag +@admin_tag_page.route('/delete', methods=['POST']) +@login_required +@exception_handler( + __file__, + post_redirect='admin_tag.delete', + error_name='tag_error' +) +def do_delete(*, id: str) -> Response: + tag = BrickSetTag().select_specific(id) + tag.delete() + + reload() + + return redirect(url_for('admin.admin', open_tag=True)) + + +# Rename the metadata tag +@admin_tag_page.route('/rename', methods=['POST']) +@login_required +@exception_handler( + __file__, + post_redirect='admin.admin', + error_name='tag_error', + open_tag=True +) +def rename(*, id: str) -> Response: + tag = BrickSetTag().select_specific(id) + tag.from_form(request.form).rename() + + reload() + + return redirect(url_for('admin.admin', open_tag=True)) diff --git a/bricktracker/views/index.py b/bricktracker/views/index.py index 3d8a55e..1cbcd56 100644 --- a/bricktracker/views/index.py +++ b/bricktracker/views/index.py @@ -6,6 +6,8 @@ from ..set_owner import BrickSetOwner from ..set_owner_list import BrickSetOwnerList from ..set_status import BrickSetStatus from ..set_status_list import BrickSetStatusList +from ..set_tag import BrickSetTag +from ..set_tag_list import BrickSetTagList from ..set_list import BrickSetList index_page = Blueprint('index', __name__) @@ -20,5 +22,6 @@ def index() -> str: brickset_collection=BrickSetList().last(), brickset_owners=BrickSetOwnerList(BrickSetOwner).list(), brickset_statuses=BrickSetStatusList(BrickSetStatus).list(), + brickset_tags=BrickSetTagList(BrickSetTag).list(), minifigure_collection=BrickMinifigureList().last(), ) diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py index fd922ec..344b0e6 100644 --- a/bricktracker/views/set.py +++ b/bricktracker/views/set.py @@ -16,11 +16,13 @@ from .exceptions import exception_handler from ..minifigure import BrickMinifigure from ..part import BrickPart from ..set import BrickSet +from ..set_list import BrickSetList 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 +from ..set_tag import BrickSetTag +from ..set_tag_list import BrickSetTagList from ..socket import MESSAGES logger = logging.getLogger(__name__) @@ -37,6 +39,7 @@ def list() -> str: collection=BrickSetList().all(), brickset_owners=BrickSetOwnerList(BrickSetOwner).list(), brickset_statuses=BrickSetStatusList(BrickSetStatus).list(), + brickset_tags=BrickSetTagList(BrickSetTag).list(), ) @@ -66,6 +69,19 @@ def update_status(*, id: str, metadata_id: str) -> Response: 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(BrickSetTag).get(metadata_id) + + state = tag.update_set_state(brickset, json=request.json) + + return jsonify({'value': state}) + + # Ask for deletion of a set @set_page.route('//delete', methods=['GET']) @login_required @@ -116,6 +132,7 @@ def details(*, id: str) -> str: open_instructions=request.args.get('open_instructions'), brickset_owners=BrickSetOwnerList(BrickSetOwner).list(), brickset_statuses=BrickSetStatusList(BrickSetStatus).list(all=True), + brickset_tags=BrickSetTagList(BrickSetTag).list(), ) diff --git a/static/scripts/socket/set.js b/static/scripts/socket/set.js index 08e70dd..a7e660b 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_tags = document.getElementById(`${id}-tags`); // Card elements this.html_card = document.getElementById(`${id}-card`); @@ -150,11 +151,22 @@ class BrickSetSocket extends BrickSocket { }); } + // Grab the tags + const tags = []; + if (this.html_tags) { + this.html_tags.querySelectorAll('input').forEach(input => { + if (input.checked) { + tags.push(input.value); + } + }); + } + this.spinner(true); this.socket.emit(this.messages.IMPORT_SET, { set: (set !== undefined) ? set : this.html_input.value, owners: owners, + tags: tags, refresh: this.refresh }); } else { @@ -267,6 +279,10 @@ class BrickSetSocket extends BrickSocket { this.html_owners.querySelectorAll('input').forEach(input => input.disabled = !enabled); } + if (this.html_tags) { + this.html_tags.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 59c5029..3a0b784 100644 --- a/templates/add.html +++ b/templates/add.html @@ -46,6 +46,19 @@ {% endfor %}
{% endif %} + {% if brickset_tags | length %} +
Tags
+
+ {% for tag in brickset_tags %} + {% with id=tag.as_dataset() %} +
+ + +
+ {% endwith %} + {% endfor %} +
+ {% endif %}

diff --git a/templates/admin.html b/templates/admin.html index 962730b..064526d 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_tag %} + {% include 'admin/tag/delete.html' %} {% elif drop_database %} {% include 'admin/database/drop.html' %} {% elif import_database %} @@ -34,6 +36,7 @@ {% include 'admin/retired.html' %} {% include 'admin/owner.html' %} {% include 'admin/status.html' %} + {% include 'admin/tag.html' %} {% include 'admin/database.html' %} {% include 'admin/configuration.html' %} {% endif %} diff --git a/templates/admin/tag.html b/templates/admin/tag.html new file mode 100644 index 0000000..7c67a56 --- /dev/null +++ b/templates/admin/tag.html @@ -0,0 +1,42 @@ +{% import 'macro/accordion.html' as accordion %} + +{{ accordion.header('Set tags', 'tag', 'admin', expanded=open_tag, icon='price-tag-2-line', class='p-0') }} +{% if tag_error %}

{% endif %} +
    + {% if metadata_tags | length %} + {% for tag in metadata_tags %} +
  • +
    +
    + +
    +
    Name
    + + +
    +
    +
    + Delete +
    +
    +
  • + {% endfor %} + {% else %} +
  • No tag found.
  • + {% endif %} +
  • +
    +
    + +
    +
    Name
    + +
    +
    +
    + +
    +
    +
  • +
+{{ accordion.footer() }} diff --git a/templates/admin/tag/delete.html b/templates/admin/tag/delete.html new file mode 100644 index 0000000..69dd334 --- /dev/null +++ b/templates/admin/tag/delete.html @@ -0,0 +1,19 @@ +{% import 'macro/accordion.html' as accordion %} + +{{ accordion.header('Set tags danger zone', 'tag-danger', 'admin', expanded=true, danger=true, class='text-end') }} +
+ {% if tag_error %}{% endif %} + +
+
+
+
Name
+ +
+
+
+
+ Back to the admin + +
+{{ accordion.footer() }} diff --git a/templates/macro/badge.html b/templates/macro/badge.html index 2be95ca..ca57769 100644 --- a/templates/macro/badge.html +++ b/templates/macro/badge.html @@ -51,7 +51,7 @@ {% endmacro %} {% macro owner(item, owner, solo=false, last=false) %} - {% if last %} + {% if last %} {% set tooltip=owner.fields.name %} {% else %} {% set text=owner.fields.name %} @@ -72,8 +72,17 @@ {{ badge(check=set, url=url, solo=solo, last=last, color='secondary', icon='hashtag', collapsible='Set:', text=set, alt='Set') }} {% endmacro %} +{% macro tag(item, tag, solo=false, last=false) %} + {% if last %} + {% set tooltip=tag.fields.name %} + {% else %} + {% set text=tag.fields.name %} + {% endif %} + {{ badge(check=item.fields[tag.as_column()], solo=solo, last=last, color='light text-primary-emphasis bg-primary-subtle border border-primary-subtle', icon='price-tag-2-line', text=text, alt='Tag', tooltip=tooltip) }} +{% endmacro %} + {% macro theme(theme, solo=false, last=false) %} - {% if last %} + {% if last %} {% set tooltip=theme %} {% else %} {% set text=theme %} diff --git a/templates/set/card.html b/templates/set/card.html index 9f2f83a..bc302e3 100644 --- a/templates/set/card.html +++ b/templates/set/card.html @@ -12,7 +12,13 @@ {% 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 %} + {% if checked %} data-search-owner-{{ loop.index }}="{{ owner.fields.name | lower }}"{% endif %} + {% endwith %} + {% endfor %} + {% for tag in brickset_tags %} + {% with checked=item.fields[tag.as_column()] %} + data-{{ tag.as_dataset() }}="{{ checked }}" + {% if checked %} data-search-tag-{{ loop.index }}="{{ tag.fields.name | lower }}"{% endif %} {% endwith %} {% endfor %} {% endif %} @@ -21,6 +27,9 @@ {{ card.image(item, solo=solo, last=last, caption=item.fields.name, alt=item.fields.set) }}
{{ badge.theme(item.theme.name, solo=solo, last=last) }} + {% for tag in brickset_tags %} + {{ badge.tag(item, tag, solo=solo, last=last) }} + {% endfor %} {{ badge.year(item.fields.year, solo=solo, last=last) }} {{ badge.parts(item.fields.number_of_parts, solo=solo, last=last) }} {{ badge.total_minifigures(item.fields.total_minifigures, solo=solo, last=last) }} diff --git a/templates/set/management.html b/templates/set/management.html index 957db81..b1b2019 100644 --- a/templates/set/management.html +++ b/templates/set/management.html @@ -1,20 +1,34 @@ {% 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 -{{ accordion.footer() }} + {{ 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('Tags', 'tag', 'set-details', icon='price-tag-2-line', class='p-0') }} +
    + {% if brickset_tags | length %} + {% for tag in brickset_tags %} +
  • {{ form.checkbox(item, tag, delete=delete) }}
  • + {% endfor %} + {% else %} +
  • No tag found.
  • + {% endif %} +
+ + {{ accordion.footer() }} + {{ accordion.header('Management', 'management', 'set-details', expanded=true, icon='settings-4-line') }} +
Data
+ Refresh the set data + {{ accordion.footer() }} {% endif %} diff --git a/templates/sets.html b/templates/sets.html index b06c94d..7541f1c 100644 --- a/templates/sets.html +++ b/templates/sets.html @@ -10,7 +10,7 @@
Search - +
@@ -89,6 +89,20 @@
+
+ +
+ Tag + +
+
{% for item in collection %} From 418a332f0396dd6c689057c0abdefc4c0d1b0a92 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 31 Jan 2025 18:09:06 +0100 Subject: [PATCH 092/154] Add missing set owners SQL drop --- bricktracker/sql/schema/drop.sql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bricktracker/sql/schema/drop.sql b/bricktracker/sql/schema/drop.sql index 1bab7d6..8c4cedb 100644 --- a/bricktracker/sql/schema/drop.sql +++ b/bricktracker/sql/schema/drop.sql @@ -1,11 +1,13 @@ BEGIN transaction; +DROP TABLE IF EXISTS "bricktracker_metadata_owners"; DROP TABLE IF EXISTS "bricktracker_metadata_statuses"; DROP TABLE IF EXISTS "bricktracker_metadata_tags"; DROP TABLE IF EXISTS "bricktracker_minifigures"; DROP TABLE IF EXISTS "bricktracker_parts"; DROP TABLE IF EXISTS "bricktracker_sets"; DROP TABLE IF EXISTS "bricktracker_set_checkboxes"; +DROP TABLE IF EXISTS "bricktracker_set_owners"; DROP TABLE IF EXISTS "bricktracker_set_statuses"; DROP TABLE IF EXISTS "bricktracker_set_storages"; DROP TABLE IF EXISTS "bricktracker_set_tags"; From 302eafe08c00c45319a6f039d7d8f7d2392b5d20 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 31 Jan 2025 18:37:44 +0100 Subject: [PATCH 093/154] Fix broken set status --- bricktracker/set_status_list.py | 2 +- bricktracker/views/set.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bricktracker/set_status_list.py b/bricktracker/set_status_list.py index b96f213..dabd3b0 100644 --- a/bricktracker/set_status_list.py +++ b/bricktracker/set_status_list.py @@ -7,7 +7,7 @@ logger = logging.getLogger(__name__) # Lego sets status list -class BrickSetStatusList(BrickMetadataList): +class BrickSetStatusList(BrickMetadataList[BrickSetStatus]): kind: str = 'set statuses' # Database table diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py index 344b0e6..a6691e7 100644 --- a/bricktracker/views/set.py +++ b/bricktracker/views/set.py @@ -64,7 +64,7 @@ def update_status(*, id: str, metadata_id: str) -> Response: brickset = BrickSet().select_light(id) status = BrickSetStatusList(BrickSetStatus).get(metadata_id) - state = status.update_set_state(brickset, request.json) + state = status.update_set_state(brickset, json=request.json) return jsonify({'value': state}) From 5ffea66de0e03a7cddbae133bb534db308048bec Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 31 Jan 2025 18:38:03 +0100 Subject: [PATCH 094/154] Leaner card dataset --- static/scripts/grid/filter.js | 17 ++++++++++++++++- templates/set/card.html | 18 +++++++++++++----- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/static/scripts/grid/filter.js b/static/scripts/grid/filter.js index f5b075f..2825c53 100644 --- a/static/scripts/grid/filter.js +++ b/static/scripts/grid/filter.js @@ -75,11 +75,13 @@ class BrickGridFilter { if (select.value.startsWith("-")) { options.filters.push({ attribute: select.value.substring(1), + bool: true, value: "0" }) } else { options.filters.push({ attribute: select.value, + bool: true, value: "1" }) } @@ -93,7 +95,20 @@ class BrickGridFilter { cards.forEach(current => { // Process all filters for (const filter of options.filters) { - if (current.getAttribute(`data-${filter.attribute}`) != filter.value) { + const attribute = current.getAttribute(`data-${filter.attribute}`); + + // Bool check + // Attribute not equal value, or undefined and value is truthy + if (filter.bool) { + if ((attribute != null && attribute != filter.value) || (attribute == null && filter.value == "1")) { + current.parentElement.classList.add("d-none"); + return; + } + } + + // Value check + // Attribute not equal value, or attribute undefined + else if ((attribute != null && attribute != filter.value) || attribute == null) { current.parentElement.classList.add("d-none"); return; } diff --git a/templates/set/card.html b/templates/set/card.html index bc302e3..eb48c18 100644 --- a/templates/set/card.html +++ b/templates/set/card.html @@ -8,17 +8,25 @@ data-index="{{ index }}" data-number="{{ item.fields.set }}" data-name="{{ item.fields.name | lower }}" data-parts="{{ item.fields.number_of_parts }}" 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 status in brickset_statuses %} + {% with checked=item.fields[status.as_column()] %} + {% if checked %} + data-{{ status.as_dataset() }}="{{ checked }}" + {% endif %} + {% endwith %} + {% endfor %} {% for owner in brickset_owners %} {% with checked=item.fields[owner.as_column()] %} - data-{{ owner.as_dataset() }}="{{ checked }}" - {% if checked %} data-search-owner-{{ loop.index }}="{{ owner.fields.name | lower }}"{% endif %} + {% if checked %} + data-{{ owner.as_dataset() }}="{{ checked }}" data-search-owner-{{ loop.index }}="{{ owner.fields.name | lower }}" + {% endif %} {% endwith %} {% endfor %} {% for tag in brickset_tags %} {% with checked=item.fields[tag.as_column()] %} - data-{{ tag.as_dataset() }}="{{ checked }}" - {% if checked %} data-search-tag-{{ loop.index }}="{{ tag.fields.name | lower }}"{% endif %} + {% if checked %} + data-{{ tag.as_dataset() }}="{{ checked }}" data-search-tag-{{ loop.index }}="{{ tag.fields.name | lower }}" + {% endif %} {% endwith %} {% endfor %} {% endif %} From 271effd5d265a3b0afe19ba438b3c1b9fcdbfd38 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 31 Jan 2025 20:46:36 +0100 Subject: [PATCH 095/154] Support for damaged parts --- .env.sample | 5 ++- bricktracker/config.py | 2 +- bricktracker/minifigure_list.py | 34 +++++++++------ bricktracker/navbar.py | 2 +- bricktracker/part.py | 42 +++++++++++-------- bricktracker/part_list.py | 8 ++-- bricktracker/set_list.py | 37 +++++++++++++++- bricktracker/sql/minifigure/base/base.sql | 3 ++ bricktracker/sql/minifigure/list/all.sql | 15 ++++--- .../sql/minifigure/list/damaged_part.sql | 28 +++++++++++++ bricktracker/sql/minifigure/list/last.sql | 4 ++ .../sql/minifigure/select/generic.sql | 13 ++++-- bricktracker/sql/part/base/base.sql | 3 ++ bricktracker/sql/part/list/all.sql | 4 ++ .../sql/part/list/from_minifigure.sql | 4 ++ bricktracker/sql/part/list/from_print.sql | 5 ++- .../part/list/{missing.sql => problem.sql} | 5 +++ bricktracker/sql/part/list/specific.sql | 4 ++ bricktracker/sql/part/select/generic.sql | 4 ++ bricktracker/sql/part/update/damaged.sql | 7 ++++ bricktracker/sql/set/base/base.sql | 3 ++ bricktracker/sql/set/base/full.sql | 13 ++++-- .../sql/set/list/damaged_minifigure.sql | 11 +++++ bricktracker/sql/set/list/damaged_part.sql | 12 ++++++ bricktracker/views/minifigure.py | 1 + bricktracker/views/part.py | 18 +++++--- bricktracker/views/set.py | 18 ++++---- templates/macro/accordion.html | 2 +- templates/macro/badge.html | 6 ++- templates/macro/table.html | 10 ++--- templates/minifigure/card.html | 4 +- templates/minifigure/table.html | 3 +- templates/part/card.html | 7 +++- templates/part/table.html | 13 +++--- templates/{missing.html => problem.html} | 2 +- templates/set/card.html | 8 +++- templates/sets.html | 5 ++- 37 files changed, 274 insertions(+), 91 deletions(-) create mode 100644 bricktracker/sql/minifigure/list/damaged_part.sql rename bricktracker/sql/part/list/{missing.sql => problem.sql} (86%) create mode 100644 bricktracker/sql/part/update/damaged.sql create mode 100644 bricktracker/sql/set/list/damaged_minifigure.sql create mode 100644 bricktracker/sql/set/list/damaged_part.sql rename templates/{missing.html => problem.html} (78%) diff --git a/.env.sample b/.env.sample index d6141e6..fb52132 100644 --- a/.env.sample +++ b/.env.sample @@ -107,9 +107,10 @@ # Default: false # BK_HIDE_ALL_SETS=true -# Optional: Hide the 'Missing' entry from the menu. Does not disable the route. +# Optional: Hide the 'Problems' entry from the menu. Does not disable the route. # Default: false -# BK_HIDE_MISSING_PARTS=true +# Legacy name: BK_HIDE_MISSING_PARTS +# BK_HIDE_PROBLEMS_PARTS=true # Optional: Hide the 'Instructions' entry in a Set card # Default: false diff --git a/bricktracker/config.py b/bricktracker/config.py index cbb64a8..8ab193a 100644 --- a/bricktracker/config.py +++ b/bricktracker/config.py @@ -29,7 +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_MISSING_PARTS', 'c': bool}, + {'n': 'HIDE_PROBLEMS_PARTS', 'e': 'BK_HIDE_MISSING_PARTS', 'c': bool}, {'n': 'HIDE_SET_INSTRUCTIONS', 'c': bool}, {'n': 'HIDE_WISHES', 'c': bool}, {'n': 'MINIFIGURES_DEFAULT_ORDER', 'd': '"rebrickable_minifigures"."name" ASC'}, # noqa: E501 diff --git a/bricktracker/minifigure_list.py b/bricktracker/minifigure_list.py index 790018a..24a4d2e 100644 --- a/bricktracker/minifigure_list.py +++ b/bricktracker/minifigure_list.py @@ -21,10 +21,11 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): # Queries all_query: str = 'minifigure/list/all' + damaged_part_query: str = 'minifigure/list/damaged_part' last_query: str = 'minifigure/list/last' + missing_part_query: str = 'minifigure/list/missing_part' select_query: str = 'minifigure/list/from_set' using_part_query: str = 'minifigure/list/using_part' - missing_part_query: str = 'minifigure/list/missing_part' def __init__(self, /): super().__init__() @@ -47,6 +48,23 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): return self + # Minifigures with a part damaged part + def damaged_part(self, part: str, color: int, /) -> Self: + # Save the parameters to the fields + self.fields.part = part + self.fields.color = color + + # Load the minifigures from the database + for record in self.select( + override_query=self.damaged_part_query, + order=self.order + ): + minifigure = BrickMinifigure(record=record) + + self.records.append(minifigure) + + return self + # Last added minifigure def last(self, /, *, limit: int = 6) -> Self: # Randomize @@ -80,12 +98,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): return self # Minifigures missing a part - def missing_part( - self, - part: str, - color: int, - /, - ) -> Self: + def missing_part(self, part: str, color: int, /) -> Self: # Save the parameters to the fields self.fields.part = part self.fields.color = color @@ -102,12 +115,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): return self # Minifigure using a part - def using_part( - self, - part: str, - color: int, - /, - ) -> Self: + def using_part(self, part: str, color: int, /) -> Self: # Save the parameters to the fields self.fields.part = part self.fields.color = color diff --git a/bricktracker/navbar.py b/bricktracker/navbar.py index 17853eb..04b7053 100644 --- a/bricktracker/navbar.py +++ b/bricktracker/navbar.py @@ -11,7 +11,7 @@ NAVBAR: Final[list[dict[str, Any]]] = [ {'e': 'set.list', 't': 'Sets', 'i': 'grid-line', 'f': 'HIDE_ALL_SETS'}, # noqa: E501 {'e': 'add.add', 't': 'Add', 'i': 'add-circle-line', 'f': 'HIDE_ADD_SET'}, # noqa: E501 {'e': 'part.list', 't': 'Parts', 'i': 'shapes-line', 'f': 'HIDE_ALL_PARTS'}, # noqa: E501 - {'e': 'part.missing', 't': 'Missing', 'i': 'error-warning-line', 'f': 'HIDE_MISSING_PARTS'}, # noqa: E501 + {'e': 'part.problem', 't': 'Problems', 'i': 'error-warning-line', 'f': 'HIDE_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': 'wish.list', 't': 'Wishlist', 'i': 'gift-line', 'f': 'HIDE_WISHES'}, diff --git a/bricktracker/part.py b/bricktracker/part.py index 64d71df..fa463be 100644 --- a/bricktracker/part.py +++ b/bricktracker/part.py @@ -74,9 +74,12 @@ class BrickPart(RebrickablePart): return True # A identifier for HTML component - def html_id(self, /) -> str: + def html_id(self, prefix: str | None = None, /) -> str: components: list[str] = ['part'] + if prefix is not None: + components.append(prefix) + if self.fields.figure is not None: components.append(self.fields.figure) @@ -144,36 +147,38 @@ class BrickPart(RebrickablePart): return self - # Update the missing part - def update_missing(self, json: Any | None, /) -> None: - missing: str | int = json.get('value', '') # type: ignore + # Update a problematic part + def update_problem(self, problem: str, json: Any | None, /) -> int: + amount: str | int = json.get('value', '') # type: ignore # We need a positive integer try: - if missing == '': - missing = 0 + if amount == '': + amount = 0 - missing = int(missing) + amount = int(amount) - if missing < 0: - missing = 0 + if amount < 0: + amount = 0 except Exception: - raise ErrorException('"{missing}" is not a valid integer'.format( - missing=missing + raise ErrorException('"{amount}" is not a valid integer'.format( + amount=amount )) - if missing < 0: - raise ErrorException('Cannot set a negative missing value') + if amount < 0: + raise ErrorException('Cannot set a negative amount') - self.fields.missing = missing + setattr(self.fields, problem, amount) BrickSQL().execute_and_commit( - 'part/update/missing', + 'part/update/{problem}'.format(problem=problem), parameters=self.sql_parameters() ) - # Compute the url for missing part - def url_for_missing(self, /) -> str: + return amount + + # Compute the url for problematic part + def url_for_problem(self, problem: str, /) -> str: # Different URL for a minifigure part if self.minifigure is not None: figure = self.minifigure.fields.figure @@ -181,10 +186,11 @@ class BrickPart(RebrickablePart): figure = None return url_for( - 'set.missing_part', + 'set.problem_part', id=self.fields.id, figure=figure, part=self.fields.part, color=self.fields.color, spare=self.fields.spare, + problem=problem, ) diff --git a/bricktracker/part_list.py b/bricktracker/part_list.py index 833ae61..86ca34a 100644 --- a/bricktracker/part_list.py +++ b/bricktracker/part_list.py @@ -25,7 +25,7 @@ class BrickPartList(BrickRecordList[BrickPart]): all_query: str = 'part/list/all' last_query: str = 'part/list/last' minifigure_query: str = 'part/list/from_minifigure' - missing_query: str = 'part/list/missing' + problem_query: str = 'part/list/problem' print_query: str = 'part/list/from_print' select_query: str = 'part/list/specific' @@ -138,10 +138,10 @@ class BrickPartList(BrickRecordList[BrickPart]): return self - # Load missing parts - def missing(self, /) -> Self: + # Load problematic parts + def problem(self, /) -> Self: for record in self.select( - override_query=self.missing_query, + override_query=self.problem_query, order=self.order ): part = BrickPart(record=record) diff --git a/bricktracker/set_list.py b/bricktracker/set_list.py index 54a3cb8..d538daa 100644 --- a/bricktracker/set_list.py +++ b/bricktracker/set_list.py @@ -18,6 +18,8 @@ class BrickSetList(BrickRecordList[BrickSet]): order: str # Queries + damaged_minifigure_query: str = 'set/list/damaged_minifigure' + damaged_part_query: str = 'set/list/damaged_part' generic_query: str = 'set/list/generic' light_query: str = 'set/list/light' missing_minifigure_query: str = 'set/list/missing_minifigure' @@ -57,6 +59,39 @@ class BrickSetList(BrickRecordList[BrickSet]): return self + # Sets with a minifigure part damaged + def damaged_minifigure(self, figure: str, /) -> Self: + # Save the parameters to the fields + self.fields.figure = figure + + # Load the sets from the database + for record in self.select( + override_query=self.damaged_minifigure_query, + order=self.order + ): + brickset = BrickSet(record=record) + + self.records.append(brickset) + + return self + + # Sets with a part damaged + def damaged_part(self, part: str, color: int, /) -> Self: + # Save the parameters to the fields + self.fields.part = part + self.fields.color = color + + # Load the sets from the database + for record in self.select( + override_query=self.damaged_part_query, + order=self.order + ): + brickset = BrickSet(record=record) + + self.records.append(brickset) + + return self + # A generic list of the different sets def generic(self, /) -> Self: for record in self.select( @@ -90,7 +125,7 @@ class BrickSetList(BrickRecordList[BrickSet]): return self - # Sets missing a minifigure + # Sets missing a minifigure part def missing_minifigure(self, figure: str, /) -> Self: # Save the parameters to the fields self.fields.figure = figure diff --git a/bricktracker/sql/minifigure/base/base.sql b/bricktracker/sql/minifigure/base/base.sql index dbfc428..d651bf0 100644 --- a/bricktracker/sql/minifigure/base/base.sql +++ b/bricktracker/sql/minifigure/base/base.sql @@ -7,6 +7,9 @@ SELECT {% block total_missing %} NULL AS "total_missing", -- dummy for order: total_missing {% endblock %} + {% block total_damaged %} + NULL AS "total_damaged", -- dummy for order: total_damaged + {% endblock %} {% block total_quantity %} NULL AS "total_quantity", -- dummy for order: total_quantity {% endblock %} diff --git a/bricktracker/sql/minifigure/list/all.sql b/bricktracker/sql/minifigure/list/all.sql index e3ce2bd..d0bb6eb 100644 --- a/bricktracker/sql/minifigure/list/all.sql +++ b/bricktracker/sql/minifigure/list/all.sql @@ -1,7 +1,11 @@ {% extends 'minifigure/base/base.sql' %} {% block total_missing %} -SUM(IFNULL("missing_join"."total", 0)) AS "total_missing", +SUM(IFNULL("problem_join"."total_missing", 0)) AS "total_missing", +{% endblock %} + +{% block total_damaged %} +SUM(IFNULL("problem_join"."total_damaged", 0)) AS "total_damaged", {% endblock %} {% block total_quantity %} @@ -18,15 +22,16 @@ LEFT JOIN ( SELECT "bricktracker_parts"."id", "bricktracker_parts"."figure", - SUM("bricktracker_parts"."missing") AS total + SUM("bricktracker_parts"."missing") AS "total_missing", + SUM("bricktracker_parts"."damaged") AS "total_damaged" FROM "bricktracker_parts" WHERE "bricktracker_parts"."figure" IS NOT NULL GROUP BY "bricktracker_parts"."id", "bricktracker_parts"."figure" -) "missing_join" -ON "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "missing_join"."id" -AND "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "missing_join"."figure" +) "problem_join" +ON "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "problem_join"."id" +AND "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "problem_join"."figure" {% endblock %} {% block group %} diff --git a/bricktracker/sql/minifigure/list/damaged_part.sql b/bricktracker/sql/minifigure/list/damaged_part.sql new file mode 100644 index 0000000..5cd18db --- /dev/null +++ b/bricktracker/sql/minifigure/list/damaged_part.sql @@ -0,0 +1,28 @@ +{% extends 'minifigure/base/base.sql' %} + +{% block total_damaged %} +SUM("bricktracker_parts"."damaged") AS "total_damaged", +{% endblock %} + +{% block join %} +LEFT JOIN "bricktracker_parts" +ON "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "bricktracker_parts"."id" +AND "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "bricktracker_parts"."figure" +{% endblock %} + +{% block where %} +WHERE "rebrickable_minifigures"."figure" IN ( + SELECT "bricktracker_parts"."figure" + FROM "bricktracker_parts" + WHERE "bricktracker_parts"."part" IS NOT DISTINCT FROM :part + AND "bricktracker_parts"."color" IS NOT DISTINCT FROM :color + AND "bricktracker_parts"."figure" IS NOT NULL + AND "bricktracker_parts"."damaged" > 0 + GROUP BY "bricktracker_parts"."figure" +) +{% endblock %} + +{% block group %} +GROUP BY + "rebrickable_minifigures"."figure" +{% endblock %} diff --git a/bricktracker/sql/minifigure/list/last.sql b/bricktracker/sql/minifigure/list/last.sql index 372610d..ddae212 100644 --- a/bricktracker/sql/minifigure/list/last.sql +++ b/bricktracker/sql/minifigure/list/last.sql @@ -4,6 +4,10 @@ SUM("bricktracker_parts"."missing") AS "total_missing", {% endblock %} +{% block total_damaged %} +SUM("bricktracker_parts"."damaged") AS "total_damaged", +{% endblock %} + {% block join %} LEFT JOIN "bricktracker_parts" ON "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "bricktracker_parts"."id" diff --git a/bricktracker/sql/minifigure/select/generic.sql b/bricktracker/sql/minifigure/select/generic.sql index f5bacd7..b48bfb7 100644 --- a/bricktracker/sql/minifigure/select/generic.sql +++ b/bricktracker/sql/minifigure/select/generic.sql @@ -1,7 +1,11 @@ {% extends 'minifigure/base/base.sql' %} {% block total_missing %} -IFNULL("missing_join"."total", 0) AS "total_missing", +IFNULL("problem_join"."total_missing", 0) AS "total_missing", +{% endblock %} + +{% block total_damaged %} +IFNULL("problem_join"."total_damaged", 0) AS "total_damaged", {% endblock %} {% block total_quantity %} @@ -17,12 +21,13 @@ COUNT(DISTINCT "bricktracker_minifigures"."id") AS "total_sets" LEFT JOIN ( SELECT "bricktracker_parts"."figure", - SUM("bricktracker_parts"."missing") AS "total" + SUM("bricktracker_parts"."missing") AS "total_missing", + SUM("bricktracker_parts"."damaged") AS "total_damaged" FROM "bricktracker_parts" WHERE "bricktracker_parts"."figure" IS NOT DISTINCT FROM :figure GROUP BY "bricktracker_parts"."figure" -) "missing_join" -ON "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "missing_join"."figure" +) "problem_join" +ON "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "problem_join"."figure" {% endblock %} {% block where %} diff --git a/bricktracker/sql/part/base/base.sql b/bricktracker/sql/part/base/base.sql index 7849d4c..24c1c56 100644 --- a/bricktracker/sql/part/base/base.sql +++ b/bricktracker/sql/part/base/base.sql @@ -23,6 +23,9 @@ SELECT {% block total_missing %} NULL AS "total_missing", -- dummy for order: total_missing {% endblock %} + {% block total_damaged %} + NULL AS "total_damaged", -- dummy for order: total_damaged + {% endblock %} {% block total_quantity %} NULL AS "total_quantity", -- dummy for order: total_quantity {% endblock %} diff --git a/bricktracker/sql/part/list/all.sql b/bricktracker/sql/part/list/all.sql index c1d0ed1..77831a6 100644 --- a/bricktracker/sql/part/list/all.sql +++ b/bricktracker/sql/part/list/all.sql @@ -4,6 +4,10 @@ SUM("bricktracker_parts"."missing") AS "total_missing", {% endblock %} +{% block total_damaged %} +SUM("bricktracker_parts"."damaged") AS "total_damaged", +{% endblock %} + {% block total_quantity %} SUM("bricktracker_parts"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)) AS "total_quantity", {% endblock %} diff --git a/bricktracker/sql/part/list/from_minifigure.sql b/bricktracker/sql/part/list/from_minifigure.sql index c840938..115b791 100644 --- a/bricktracker/sql/part/list/from_minifigure.sql +++ b/bricktracker/sql/part/list/from_minifigure.sql @@ -5,6 +5,10 @@ SUM("bricktracker_parts"."missing") AS "total_missing", {% endblock %} +{% block total_damaged %} +SUM("bricktracker_parts"."damaged") AS "total_damaged", +{% endblock %} + {% block where %} WHERE "bricktracker_parts"."figure" IS NOT DISTINCT FROM :figure {% endblock %} diff --git a/bricktracker/sql/part/list/from_print.sql b/bricktracker/sql/part/list/from_print.sql index f996864..fe1198c 100644 --- a/bricktracker/sql/part/list/from_print.sql +++ b/bricktracker/sql/part/list/from_print.sql @@ -1,8 +1,9 @@ {% extends 'part/base/base.sql' %} -{% block total_missing %} -{% endblock %} +{% block total_missing %}{% endblock %} + +{% block total_damaged %}{% endblock %} {% block where %} WHERE "rebrickable_parts"."print" IS NOT DISTINCT FROM :print diff --git a/bricktracker/sql/part/list/missing.sql b/bricktracker/sql/part/list/problem.sql similarity index 86% rename from bricktracker/sql/part/list/missing.sql rename to bricktracker/sql/part/list/problem.sql index 9d3446e..dbf411b 100644 --- a/bricktracker/sql/part/list/missing.sql +++ b/bricktracker/sql/part/list/problem.sql @@ -4,6 +4,10 @@ SUM("bricktracker_parts"."missing") AS "total_missing", {% endblock %} +{% block total_damaged %} +SUM("bricktracker_parts"."damaged") AS "total_damaged", +{% endblock %} + {% block total_sets %} COUNT("bricktracker_parts"."id") - COUNT("bricktracker_parts"."figure") AS "total_sets", {% endblock %} @@ -20,6 +24,7 @@ AND "bricktracker_parts"."figure" IS NOT DISTINCT FROM "bricktracker_minifigures {% block where %} WHERE "bricktracker_parts"."missing" > 0 +OR "bricktracker_parts"."damaged" > 0 {% endblock %} {% block group %} diff --git a/bricktracker/sql/part/list/specific.sql b/bricktracker/sql/part/list/specific.sql index d3e291a..7c62c68 100644 --- a/bricktracker/sql/part/list/specific.sql +++ b/bricktracker/sql/part/list/specific.sql @@ -5,6 +5,10 @@ IFNULL("bricktracker_parts"."missing", 0) AS "total_missing", {% endblock %} +{% block total_damaged %} +IFNULL("bricktracker_parts"."damaged", 0) AS "total_damaged", +{% endblock %} + {% block where %} WHERE "bricktracker_parts"."id" IS NOT DISTINCT FROM :id AND "bricktracker_parts"."figure" IS NOT DISTINCT FROM :figure diff --git a/bricktracker/sql/part/select/generic.sql b/bricktracker/sql/part/select/generic.sql index a1760d6..43a26da 100644 --- a/bricktracker/sql/part/select/generic.sql +++ b/bricktracker/sql/part/select/generic.sql @@ -4,6 +4,10 @@ SUM("bricktracker_parts"."missing") AS "total_missing", {% endblock %} +{% block total_damaged %} +SUM("bricktracker_parts"."damaged") AS "total_damaged", +{% endblock %} + {% block total_quantity %} SUM((NOT "bricktracker_parts"."spare") * "bricktracker_parts"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)) AS "total_quantity", {% endblock %} diff --git a/bricktracker/sql/part/update/damaged.sql b/bricktracker/sql/part/update/damaged.sql new file mode 100644 index 0000000..d4bbabd --- /dev/null +++ b/bricktracker/sql/part/update/damaged.sql @@ -0,0 +1,7 @@ +UPDATE "bricktracker_parts" +SET "damaged" = :damaged +WHERE "bricktracker_parts"."id" IS NOT DISTINCT FROM :id +AND "bricktracker_parts"."figure" IS NOT DISTINCT FROM :figure +AND "bricktracker_parts"."part" IS NOT DISTINCT FROM :part +AND "bricktracker_parts"."color" IS NOT DISTINCT FROM :color +AND "bricktracker_parts"."spare" IS NOT DISTINCT FROM :spare diff --git a/bricktracker/sql/set/base/base.sql b/bricktracker/sql/set/base/base.sql index ffefe95..331b15e 100644 --- a/bricktracker/sql/set/base/base.sql +++ b/bricktracker/sql/set/base/base.sql @@ -21,6 +21,9 @@ SELECT {% block total_missing %} NULL AS "total_missing", -- dummy for order: total_missing {% endblock %} + {% block total_damaged %} + NULL AS "total_damaged", -- dummy for order: total_damaged + {% endblock %} {% block total_quantity %} NULL AS "total_quantity", -- dummy for order: total_quantity {% endblock %} diff --git a/bricktracker/sql/set/base/full.sql b/bricktracker/sql/set/base/full.sql index 271f890..92612df 100644 --- a/bricktracker/sql/set/base/full.sql +++ b/bricktracker/sql/set/base/full.sql @@ -5,7 +5,11 @@ {% endblock %} {% block total_missing %} -IFNULL("missing_join"."total", 0) AS "total_missing", +IFNULL("problem_join"."total_missing", 0) AS "total_missing", +{% endblock %} + +{% block total_damaged %} +IFNULL("problem_join"."total_damaged", 0) AS "total_damaged", {% endblock %} {% block total_quantity %} @@ -32,12 +36,13 @@ ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_tags"."id" LEFT JOIN ( SELECT "bricktracker_parts"."id", - SUM("bricktracker_parts"."missing") AS "total" + SUM("bricktracker_parts"."missing") AS "total_missing", + SUM("bricktracker_parts"."damaged") AS "total_damaged" FROM "bricktracker_parts" {% block where_missing %}{% endblock %} GROUP BY "bricktracker_parts"."id" -) "missing_join" -ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "missing_join"."id" +) "problem_join" +ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "problem_join"."id" -- LEFT JOIN + SELECT to avoid messing the total LEFT JOIN ( diff --git a/bricktracker/sql/set/list/damaged_minifigure.sql b/bricktracker/sql/set/list/damaged_minifigure.sql new file mode 100644 index 0000000..51a615d --- /dev/null +++ b/bricktracker/sql/set/list/damaged_minifigure.sql @@ -0,0 +1,11 @@ +{% extends 'set/base/full.sql' %} + +{% block where %} +WHERE "bricktracker_sets"."id" IN ( + SELECT "bricktracker_parts"."id" + FROM "bricktracker_parts" + WHERE "bricktracker_parts"."figure" IS NOT DISTINCT FROM :figure + AND "bricktracker_parts"."missing" > 0 + GROUP BY "bricktracker_parts"."id" +) +{% endblock %} diff --git a/bricktracker/sql/set/list/damaged_part.sql b/bricktracker/sql/set/list/damaged_part.sql new file mode 100644 index 0000000..128931f --- /dev/null +++ b/bricktracker/sql/set/list/damaged_part.sql @@ -0,0 +1,12 @@ +{% extends 'set/base/full.sql' %} + +{% block where %} +WHERE "bricktracker_sets"."id" IN ( + SELECT "bricktracker_parts"."id" + FROM "bricktracker_parts" + WHERE "bricktracker_parts"."part" IS NOT DISTINCT FROM :part + AND "bricktracker_parts"."color" IS NOT DISTINCT FROM :color + AND "bricktracker_parts"."damaged" > 0 + GROUP BY "bricktracker_parts"."id" +) +{% endblock %} diff --git a/bricktracker/views/minifigure.py b/bricktracker/views/minifigure.py index 60647fa..5d9cc85 100644 --- a/bricktracker/views/minifigure.py +++ b/bricktracker/views/minifigure.py @@ -27,4 +27,5 @@ def details(*, figure: str) -> str: item=BrickMinifigure().select_generic(figure), using=BrickSetList().using_minifigure(figure), missing=BrickSetList().missing_minifigure(figure), + damaged=BrickSetList().damaged_minifigure(figure), ) diff --git a/bricktracker/views/part.py b/bricktracker/views/part.py index dbcfe0d..0bea4ab 100644 --- a/bricktracker/views/part.py +++ b/bricktracker/views/part.py @@ -19,13 +19,13 @@ def list() -> str: ) -# Missing -@part_page.route('/missing', methods=['GET']) +# Problem +@part_page.route('/problem', methods=['GET']) @exception_handler(__file__) -def missing() -> str: +def problem() -> str: return render_template( - 'missing.html', - table_collection=BrickPartList().missing() + 'problem.html', + table_collection=BrickPartList().problem() ) @@ -46,6 +46,10 @@ def details(*, part: str, color: int) -> str: part, color ), + sets_damaged=BrickSetList().damaged_part( + part, + color + ), minifigures_using=BrickMinifigureList().using_part( part, color @@ -54,5 +58,9 @@ def details(*, part: str, color: int) -> str: part, color ), + minifigures_damaged=BrickMinifigureList().damaged_part( + part, + color + ), similar_prints=BrickPartList().from_print(brickpart) ) diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py index a6691e7..add66d2 100644 --- a/bricktracker/views/set.py +++ b/bricktracker/views/set.py @@ -136,18 +136,19 @@ def details(*, id: str) -> str: ) -# Update the missing pieces of a part -@set_page.route('//parts////missing', defaults={'figure': None}, methods=['POST']) # noqa: E501 -@set_page.route('//minifigures/
/parts////missing', methods=['POST']) # noqa: E501 +# Update problematic pieces of a set +@set_page.route('//parts////', defaults={'figure': None}, methods=['POST']) # noqa: E501 +@set_page.route('//minifigures/
/parts////', methods=['POST']) # noqa: E501 @login_required @exception_handler(__file__, json=True) -def missing_part( +def problem_part( *, id: str, figure: str | None, part: str, color: int, spare: int, + problem: str, ) -> Response: brickset = BrickSet().select_specific(id) @@ -164,20 +165,21 @@ def missing_part( minifigure=brickminifigure, ) - brickpart.update_missing(request.json) + amount = brickpart.update_problem(problem, request.json) # Info - logger.info('Set {set} ({id}): updated part ({part} color: {color}, spare: {spare}, minifigure: {figure}) missing count to {missing}'.format( # noqa: E501 + logger.info('Set {set} ({id}): updated part ({part} color: {color}, spare: {spare}, minifigure: {figure}) {problem} count to {amount}'.format( # noqa: E501 set=brickset.fields.set, id=brickset.fields.id, figure=figure, part=brickpart.fields.part, color=brickpart.fields.color, spare=brickpart.fields.spare, - missing=brickpart.fields.missing, + problem=problem, + amount=amount )) - return jsonify({'missing': brickpart.fields.missing}) + return jsonify({problem: amount}) # Refresh a set diff --git a/templates/macro/accordion.html b/templates/macro/accordion.html index 417ef47..8ae8a88 100644 --- a/templates/macro/accordion.html +++ b/templates/macro/accordion.html @@ -43,7 +43,7 @@ {% endif %} {% endmacro %} -{% macro table(table_collection, title, id, parent, target, quantity=none, icon=none, image=none, alt=none, details=none, no_missing=none, read_only=none) %} +{% macro table(table_collection, title, id, parent, target, quantity=none, icon=none, image=none, alt=none, details=none, read_only=none) %} {% set size=table_collection | length %} {% if size %} {{ header(title, id, parent, quantity=quantity, icon=icon, class='p-0', image=image, alt=alt) }} diff --git a/templates/macro/badge.html b/templates/macro/badge.html index ca57769..a3ea6d7 100644 --- a/templates/macro/badge.html +++ b/templates/macro/badge.html @@ -90,6 +90,10 @@ {{ badge(check=theme, solo=solo, last=last, color='primary', icon='price-tag-3-line', text=text, alt='Theme', tooltip=tooltip) }} {% endmacro %} +{% macro total_damaged(damaged, solo=false, last=false) %} + {{ badge(check=damaged, solo=solo, last=last, color='danger', icon='error-warning-line', collapsible='Damaged:', text=damaged, alt='Damaged') }} +{% endmacro %} + {% macro total_quantity(quantity, solo=false, last=false) %} {{ badge(check=quantity, solo=solo, last=last, color='success', icon='functions', collapsible='Quantity:', text=quantity, alt='Quantity') }} {% endmacro %} @@ -99,7 +103,7 @@ {% endmacro %} {% macro total_missing(missing, solo=false, last=false) %} - {{ badge(check=missing, solo=solo, last=last, color='danger', icon='error-warning-line', collapsible='Missing:', text=missing, alt='Missing') }} + {{ badge(check=missing, solo=solo, last=last, color='light text-danger-emphasis bg-danger-subtle border border-danger-subtle', icon='question-line', collapsible='Missing:', text=missing, alt='Missing') }} {% endmacro %} {% macro total_sets(sets, solo=false, last=false) %} diff --git a/templates/macro/table.html b/templates/macro/table.html index 91bb1c9..8db1dda 100644 --- a/templates/macro/table.html +++ b/templates/macro/table.html @@ -1,4 +1,4 @@ -{% macro header(color=false, quantity=false, missing=false, missing_parts=false, sets=false, minifigures=false) %} +{% macro header(color=false, quantity=false, missing_parts=false, damaged_parts=false, sets=false, minifigures=false) %} Image @@ -9,12 +9,8 @@ {% if quantity %} Quantity {% endif %} - {% if missing %} - Missing - {% endif %} - {% if missing_parts %} - Missing parts - {% endif %} + Missing{% if missing_parts %} parts{% endif %} + Damaged{% if damaged_parts %} parts{% endif %} {% if sets %} Sets {% endif %} diff --git a/templates/minifigure/card.html b/templates/minifigure/card.html index 446c164..80459e3 100644 --- a/templates/minifigure/card.html +++ b/templates/minifigure/card.html @@ -13,6 +13,7 @@ {{ badge.quantity(item.fields.total_quantity, solo=solo, last=last) }} {{ badge.total_sets(using | length, solo=solo, last=last) }} {{ badge.total_missing(item.fields.total_missing, solo=solo, last=last) }} + {{ badge.total_damaged(item.fields.total_damaged, solo=solo, last=last) }} {% if not last %} {{ badge.rebrickable(item, solo=solo, last=last) }} {% endif %} @@ -21,7 +22,8 @@
{{ accordion.table(item.generic_parts(), 'Parts', item.fields.figure, 'minifigure-details', 'part/table.html', icon='shapes-line', alt=item.fields.figure, read_only=read_only)}} {{ accordion.cards(using, 'Sets using this minifigure', 'using-inventory', 'minifigure-details', 'set/card.html', icon='hashtag') }} - {{ accordion.cards(missing, 'Sets missing parts of this minifigure', 'missing-inventory', 'minifigure-details', 'set/card.html', icon='error-warning-line') }} + {{ accordion.cards(missing, 'Sets missing parts for this minifigure', 'missing-inventory', 'minifigure-details', 'set/card.html', icon='question-line') }} + {{ accordion.cards(damaged, 'Sets with damaged parts for this minifigure', 'damaged-inventory', 'minifigure-details', 'set/card.html', icon='error-warning-line') }}
{% endif %} diff --git a/templates/minifigure/table.html b/templates/minifigure/table.html index 66ece79..aeb0bb7 100644 --- a/templates/minifigure/table.html +++ b/templates/minifigure/table.html @@ -2,7 +2,7 @@
- {{ table.header(quantity=true, missing_parts=true, sets=true) }} + {{ table.header(quantity=true, missing_parts=true, damaged_parts=true, sets=true) }} {% for item in table_collection %} @@ -15,6 +15,7 @@ + {% endfor %} diff --git a/templates/part/card.html b/templates/part/card.html index 4cb4031..8a0d7aa 100644 --- a/templates/part/card.html +++ b/templates/part/card.html @@ -15,6 +15,7 @@ {{ badge.total_quantity(item.fields.total_quantity, solo=solo, last=last) }} {{ badge.total_spare(item.fields.total_spare, solo=solo, last=last) }} {{ badge.total_missing(item.fields.total_missing, solo=solo, last=last) }} + {{ badge.total_damaged(item.fields.total_damaged, solo=solo, last=last) }} {% if not last %} {{ badge.rebrickable(item, solo=solo, last=last) }} {{ badge.bricklink(item, solo=solo, last=last) }} @@ -23,9 +24,11 @@ {% if solo %}
{{ accordion.cards(sets_using, 'Sets using this part', 'sets-using-inventory', 'part-details', 'set/card.html', icon='hashtag') }} - {{ accordion.cards(sets_missing, 'Sets missing this part', 'sets-missing-inventory', 'part-details', 'set/card.html', icon='error-warning-line') }} + {{ accordion.cards(sets_missing, 'Sets missing this part', 'sets-missing-inventory', 'part-details', 'set/card.html', icon='question-line') }} + {{ accordion.cards(sets_damaged, 'Sets with this part damaged', 'sets-damaged-inventory', 'part-details', 'set/card.html', icon='error-warning-line') }} {{ accordion.cards(minifigures_using, 'Minifigures using this part', 'minifigures-using-inventory', 'part-details', 'minifigure/card.html', icon='group-line') }} - {{ accordion.cards(minifigures_missing, 'Minifigures missing this part', 'minifigures-missing-inventory', 'part-details', 'minifigure/card.html', icon='error-warning-line') }} + {{ accordion.cards(minifigures_missing, 'Minifigures missing this part', 'minifigures-missing-inventory', 'part-details', 'minifigure/card.html', icon='question-line') }} + {{ accordion.cards(minifigures_damaged, 'Minifigures with this part damaged', 'minifigures-damaged-inventory', 'part-details', 'minifigure/card.html', icon='error-warning-line') }} {{ accordion.cards(similar_prints, 'Prints using the same base', 'similar-prints', 'part-details', 'part/card.html', icon='paint-brush-line') }}
diff --git a/templates/part/table.html b/templates/part/table.html index cd16f9b..ac96af5 100644 --- a/templates/part/table.html +++ b/templates/part/table.html @@ -3,7 +3,7 @@
{{ item.fields.total_quantity }} {{ item.fields.total_missing }}{{ item.fields.total_damaged }} {{ item.fields.total_sets }}
- {{ table.header(color=true, quantity=not no_quantity, missing=not no_missing, sets=all, minifigures=all) }} + {{ table.header(color=true, quantity=not no_quantity, sets=all, minifigures=all) }} {% for item in table_collection %} @@ -27,11 +27,12 @@ {% endif %} {% endif %} - {% if not no_missing %} - - {% endif %} + + {% if all %} diff --git a/templates/missing.html b/templates/problem.html similarity index 78% rename from templates/missing.html rename to templates/problem.html index d7c82a5..76f61f5 100644 --- a/templates/missing.html +++ b/templates/problem.html @@ -1,6 +1,6 @@ {% extends 'base.html' %} -{% block title %} - Missing parts{% endblock %} +{% block title %} - Problematic parts{% endblock %} {% block main %}
diff --git a/templates/set/card.html b/templates/set/card.html index eb48c18..262c640 100644 --- a/templates/set/card.html +++ b/templates/set/card.html @@ -6,8 +6,11 @@
Parts + data-sort-attribute="missing" data-sort-desc="true"> Missing +
@@ -53,6 +55,7 @@ + {% for status in brickset_statuses %} From 2c06ca511eb7d385f387e375800e0fe919faaf74 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 31 Jan 2025 20:49:55 +0100 Subject: [PATCH 096/154] Fix management always opened for sets --- templates/set/management.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/set/management.html b/templates/set/management.html index b1b2019..9b48448 100644 --- a/templates/set/management.html +++ b/templates/set/management.html @@ -27,7 +27,7 @@ Manage the set tags
{{ accordion.footer() }} - {{ accordion.header('Management', 'management', 'set-details', expanded=true, icon='settings-4-line') }} + {{ accordion.header('Management', 'management', 'set-details', icon='settings-4-line') }}
Data
Refresh the set data {{ accordion.footer() }} From b6c004c0457a9e04749056d01c9649ede01fc0b2 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 31 Jan 2025 20:50:08 +0100 Subject: [PATCH 097/154] Remove unused html_id for sets --- bricktracker/set.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/bricktracker/set.py b/bricktracker/set.py index eb2bafb..000047d 100644 --- a/bricktracker/set.py +++ b/bricktracker/set.py @@ -139,17 +139,6 @@ class BrickSet(RebrickableSet): return True - # A identifier for HTML component - def html_id(self, prefix: str | None = None, /) -> str: - components: list[str] = [] - - if prefix is not None: - components.append(prefix) - - components.append(self.fields.id) - - return '-'.join(components) - # Minifigures def minifigures(self, /) -> BrickMinifigureList: return BrickMinifigureList().from_set(self) From 1e2f9fb11ac4979e0df132d21f264755a3bd088b Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 31 Jan 2025 20:51:34 +0100 Subject: [PATCH 098/154] Fix database counters layout --- templates/admin/database.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/admin/database.html b/templates/admin/database.html index 86e82c8..a10fbe3 100644 --- a/templates/admin/database.html +++ b/templates/admin/database.html @@ -20,7 +20,7 @@ {% if database_counters %}
Records
-
    +
      {% for counter in database_counters %}
    • {{ counter.name }} {{ counter.count }} From 9d6bc332cb5fab2d97caf5c92830c61fdde6acad Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 31 Jan 2025 20:53:53 +0100 Subject: [PATCH 099/154] Add missing database counters --- bricktracker/sql_counter.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bricktracker/sql_counter.py b/bricktracker/sql_counter.py index 74c18cc..7d8c882 100644 --- a/bricktracker/sql_counter.py +++ b/bricktracker/sql_counter.py @@ -2,11 +2,14 @@ from typing import Tuple # Some table aliases to make it look cleaner (id: (name, icon)) ALIASES: dict[str, Tuple[str, str]] = { + 'bricktracker_metadata_owners': ('Bricktracker set owners metadata', 'user-line'), # noqa: E501 'bricktracker_metadata_statuses': ('Bricktracker set status metadata', 'checkbox-line'), # noqa: E501 + 'bricktracker_metadata_tags': ('Bricktracker set tags metadata', 'price-tag-2-line'), # noqa: E501 '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_purchase_locations': ('Bricktracker set purchase locations', 'building-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_set_tags': ('Bricktracker set tags', 'price-tag-2-line'), # noqa: E501 From 690366794659458d15fb8afa224a141578b263bf Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 31 Jan 2025 20:56:10 +0100 Subject: [PATCH 100/154] Update changelog --- CHANGELOG.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bdc3b8..6a7379e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,18 +2,38 @@ ## Unreleased +> **Warning** +> "Missing" part has been renamed to "Problems" to accomodate for missing and damaged parts. +> The associated environment variables have changed named (the old names are still valid) + ## Code +- Form + - Migrate missing input fields to BrickChanger + - General cleanup +- Metadata + - Underlying class to implement more metadata-like features + - Minifigure - Deduplicate +Parts + - Damaged parts + +- Sets + - Refresh data from Rebrickable + - Fix missing @login_required for set deletion + - Ownership + - Tags + - Socket - Add decorator for rebrickable, authenticated and threaded socket actions - SQL - Allow for advanced migration scenarios through companion python files + - Add a bunch of the requested fields into the database for future implementation ### UI @@ -22,10 +42,33 @@ - Admin - Grey out legacy tables in the database view + - Checkboxes renamed to Set statuses + +- Cards + - Use macros for badge in the card header + +- Form + - Add a clear button for dynamic text inputs + - Add error message in a tooltip for dynamic inputs + +- Parts + - Use Rebrickable URL if stored (+ color code) + - Display color and transparency + - Display if print of another part + - Display prints using the same base + - Damaged parts - Sets - Add a flag to hide instructions in a set + - Make checkbox clickable on the whole width of the card + - Management + - Ownership + - Tags + - Refresh +- Sets grid + - Collapsible controls depending on screen size + - Manually collapsible filters (with configuration variable for default state) ## 1.1.1: PDF Instructions Download From eac9fc179345925ec795b9da782281e861aca777 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 09:52:33 +0100 Subject: [PATCH 101/154] Allow hiding the damaged and missing columns from the parts table --- .env.sample | 10 +++++++++- CHANGELOG.md | 8 +++++++- bricktracker/config.py | 4 +++- bricktracker/navbar.py | 2 +- templates/macro/table.html | 6 +++++- templates/part/table.html | 16 ++++++++++------ 6 files changed, 35 insertions(+), 11 deletions(-) diff --git a/.env.sample b/.env.sample index fb52132..0bfc3f0 100644 --- a/.env.sample +++ b/.env.sample @@ -110,12 +110,20 @@ # Optional: Hide the 'Problems' entry from the menu. Does not disable the route. # Default: false # Legacy name: BK_HIDE_MISSING_PARTS -# BK_HIDE_PROBLEMS_PARTS=true +# BK_HIDE_ALL_PROBLEMS_PARTS=true # Optional: Hide the 'Instructions' entry in a Set card # Default: false # BK_HIDE_SET_INSTRUCTIONS=true +# Optional: Hide the 'Damaged' column from the parts table. +# Default: false +# BK_HIDE_TABLE_DAMAGED_PARTS=true + +# Optional: Hide the 'Missing' column from the parts table. +# Default: false +# BK_HIDE_TABLE_MISSING_PARTS=true + # Optional: Hide the 'Wishlist' entry from the menu. Does not disable the route. # Default: false # BK_HIDE_WISHES=true diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a7379e..28a7f0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,13 @@ > "Missing" part has been renamed to "Problems" to accomodate for missing and damaged parts. > The associated environment variables have changed named (the old names are still valid) -## Code +### Environment + +- Renamed: `BK_HIDE_MISSING_PARTS` -> `BK_HIDE_ALL_PROBLEMS_PARTS` +- Added: `BK_HIDE_TABLE_MISSING_PARTS`, hide the Missing column in all tables +- Added: `BK_HIDE_TABLE_DAMAGED_PARTS`, hide the Damaged column in all tables + +### Code - Form - Migrate missing input fields to BrickChanger diff --git a/bricktracker/config.py b/bricktracker/config.py index 8ab193a..62e23a1 100644 --- a/bricktracker/config.py +++ b/bricktracker/config.py @@ -29,8 +29,10 @@ 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_PROBLEMS_PARTS', 'e': 'BK_HIDE_MISSING_PARTS', '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}, + {'n': 'HIDE_TABLE_MISSING_PARTS', 'c': bool}, {'n': 'HIDE_WISHES', 'c': bool}, {'n': 'MINIFIGURES_DEFAULT_ORDER', 'd': '"rebrickable_minifigures"."name" ASC'}, # noqa: E501 {'n': 'MINIFIGURES_FOLDER', 'd': 'minifigs', 's': True}, diff --git a/bricktracker/navbar.py b/bricktracker/navbar.py index 04b7053..30007de 100644 --- a/bricktracker/navbar.py +++ b/bricktracker/navbar.py @@ -11,7 +11,7 @@ NAVBAR: Final[list[dict[str, Any]]] = [ {'e': 'set.list', 't': 'Sets', 'i': 'grid-line', 'f': 'HIDE_ALL_SETS'}, # noqa: E501 {'e': 'add.add', 't': 'Add', 'i': 'add-circle-line', 'f': 'HIDE_ADD_SET'}, # noqa: E501 {'e': 'part.list', 't': 'Parts', 'i': 'shapes-line', 'f': 'HIDE_ALL_PARTS'}, # noqa: E501 - {'e': 'part.problem', 't': 'Problems', 'i': 'error-warning-line', 'f': 'HIDE_PROBLEMS_PARTS'}, # noqa: E501 + {'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': 'wish.list', 't': 'Wishlist', 'i': 'gift-line', 'f': 'HIDE_WISHES'}, diff --git a/templates/macro/table.html b/templates/macro/table.html index 8db1dda..d31e1c2 100644 --- a/templates/macro/table.html +++ b/templates/macro/table.html @@ -9,8 +9,12 @@ {% if quantity %}
{% endif %} - + {% if not config['HIDE_TABLE_MISSING_PARTS'] %} + + {% endif %} + {% if not config['HIDE_TABLE_DAMAGED_PARTS'] %} + {% endif %} {% if sets %} {% endif %} diff --git a/templates/part/table.html b/templates/part/table.html index ac96af5..d2f6569 100644 --- a/templates/part/table.html +++ b/templates/part/table.html @@ -27,12 +27,16 @@ {% endif %} {% endif %} - - + {% if not config['HIDE_TABLE_MISSING_PARTS'] %} + + {% endif %} + {% if not config['HIDE_TABLE_DAMAGED_PARTS'] %} + + {% endif %} {% if all %} From 34408a1bff0071e71b448fceeab54563ac39f751 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 10:10:06 +0100 Subject: [PATCH 102/154] Display same parts using a different color --- CHANGELOG.md | 1 + bricktracker/part_list.py | 30 +++++++++++++++++++ .../sql/part/list/with_different_color.sql | 17 +++++++++++ bricktracker/views/part.py | 3 +- templates/part/card.html | 1 + 5 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 bricktracker/sql/part/list/with_different_color.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 28a7f0f..54b0fa8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,7 @@ Parts - Display if print of another part - Display prints using the same base - Damaged parts + - Display same parts using a different color - Sets - Add a flag to hide instructions in a set diff --git a/bricktracker/part_list.py b/bricktracker/part_list.py index 86ca34a..d0e7138 100644 --- a/bricktracker/part_list.py +++ b/bricktracker/part_list.py @@ -23,6 +23,7 @@ class BrickPartList(BrickRecordList[BrickPart]): # Queries all_query: str = 'part/list/all' + different_color_query = 'part/list/with_different_color' last_query: str = 'part/list/last' minifigure_query: str = 'part/list/from_minifigure' problem_query: str = 'part/list/problem' @@ -166,6 +167,35 @@ class BrickPartList(BrickRecordList[BrickPart]): return parameters + # Load generic parts with same base but different color + def with_different_color( + self, + brickpart: BrickPart, + /, + ) -> Self: + # Save the part + self.fields.part = brickpart.fields.part + self.fields.color = brickpart.fields.color + + # Load the parts from the database + for record in self.select( + override_query=self.different_color_query, + order=self.order + ): + part = BrickPart( + record=record, + ) + + if ( + current_app.config['SKIP_SPARE_PARTS'] and + part.fields.spare + ): + continue + + self.records.append(part) + + return self + # Import the parts from Rebrickable @staticmethod def download( diff --git a/bricktracker/sql/part/list/with_different_color.sql b/bricktracker/sql/part/list/with_different_color.sql new file mode 100644 index 0000000..d75501d --- /dev/null +++ b/bricktracker/sql/part/list/with_different_color.sql @@ -0,0 +1,17 @@ + +{% extends 'part/base/base.sql' %} + +{% block total_missing %}{% endblock %} + +{% block total_damaged %}{% endblock %} + +{% block where %} +WHERE "bricktracker_parts"."color" IS DISTINCT FROM :color +AND "bricktracker_parts"."part" IS NOT DISTINCT FROM :part +{% endblock %} + +{% block group %} +GROUP BY + "bricktracker_parts"."part", + "bricktracker_parts"."color" +{% endblock %} diff --git a/bricktracker/views/part.py b/bricktracker/views/part.py index 0bea4ab..7cbc1c8 100644 --- a/bricktracker/views/part.py +++ b/bricktracker/views/part.py @@ -62,5 +62,6 @@ def details(*, part: str, color: int) -> str: part, color ), - similar_prints=BrickPartList().from_print(brickpart) + different_color=BrickPartList().with_different_color(brickpart), + similar_prints=BrickPartList().from_print(brickpart), ) diff --git a/templates/part/card.html b/templates/part/card.html index 8a0d7aa..16f2103 100644 --- a/templates/part/card.html +++ b/templates/part/card.html @@ -29,6 +29,7 @@ {{ accordion.cards(minifigures_using, 'Minifigures using this part', 'minifigures-using-inventory', 'part-details', 'minifigure/card.html', icon='group-line') }} {{ accordion.cards(minifigures_missing, 'Minifigures missing this part', 'minifigures-missing-inventory', 'part-details', 'minifigure/card.html', icon='question-line') }} {{ accordion.cards(minifigures_damaged, 'Minifigures with this part damaged', 'minifigures-damaged-inventory', 'part-details', 'minifigure/card.html', icon='error-warning-line') }} + {{ accordion.cards(different_color, 'Same part with a different color', 'different-color', 'part-details', 'part/card.html', icon='palette-line') }} {{ accordion.cards(similar_prints, 'Prints using the same base', 'similar-prints', 'part-details', 'part/card.html', icon='paint-brush-line') }} From 4cf91a6edd7ba38ed9aa1abb421c608f3fe82c6e Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 10:35:42 +0100 Subject: [PATCH 103/154] Compute and display number of parts for minifigures --- CHANGELOG.md | 4 +++ bricktracker/minifigure.py | 6 ++-- bricktracker/part_list.py | 8 +++++ bricktracker/sql/migrations/0015.sql | 32 +++++++++++++++++++ bricktracker/sql/minifigure/base/base.sql | 1 + .../sql/rebrickable/minifigure/insert.sql | 9 ++++-- bricktracker/version.py | 2 +- templates/macro/table.html | 5 ++- templates/minifigure/card.html | 1 + templates/minifigure/table.html | 3 +- 10 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 bricktracker/sql/migrations/0015.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 54b0fa8..40b4f17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - Minifigure - Deduplicate + - Compute number of parts Parts - Damaged parts @@ -57,6 +58,9 @@ Parts - Add a clear button for dynamic text inputs - Add error message in a tooltip for dynamic inputs +- Minifigure + - Display number of parts + - Parts - Use Rebrickable URL if stored (+ color code) - Display color and transparency diff --git a/bricktracker/minifigure.py b/bricktracker/minifigure.py index 1ad6aa6..c09589e 100644 --- a/bricktracker/minifigure.py +++ b/bricktracker/minifigure.py @@ -37,9 +37,6 @@ class BrickMinifigure(RebrickableMinifigure): # Insert into database self.insert(commit=False) - # Insert the rebrickable set into database - self.insert_rebrickable() - # Load the inventory if not BrickPartList.download( socket, @@ -49,6 +46,9 @@ class BrickMinifigure(RebrickableMinifigure): ): return False + # Insert the rebrickable set into database (after counting parts) + self.insert_rebrickable() + except Exception as e: socket.fail( message='Error while importing minifigure {figure} from {set}: {error}'.format( # noqa: E501 diff --git a/bricktracker/part_list.py b/bricktracker/part_list.py index d0e7138..eb3f58d 100644 --- a/bricktracker/part_list.py +++ b/bricktracker/part_list.py @@ -239,10 +239,18 @@ class BrickPartList(BrickRecordList[BrickPart]): ).list() # Process each part + number_of_parts: int = 0 for part in inventory: + # Count the number of parts for minifigures + if minifigure is not None: + number_of_parts += part.fields.quantity + if not part.download(socket, refresh=refresh): return False + if minifigure is not None: + minifigure.fields.number_of_parts = number_of_parts + except Exception as e: socket.fail( message='Error while importing {kind} {identifier} parts list: {error}'.format( # noqa: E501 diff --git a/bricktracker/sql/migrations/0015.sql b/bricktracker/sql/migrations/0015.sql new file mode 100644 index 0000000..de2d6ec --- /dev/null +++ b/bricktracker/sql/migrations/0015.sql @@ -0,0 +1,32 @@ +-- description: Add number of parts for minifigures + +BEGIN TRANSACTION; + +-- Add the number_of_parts column to the minifigures +ALTER TABLE "rebrickable_minifigures" +ADD COLUMN "number_of_parts" INTEGER NOT NULL DEFAULT 0; + +-- Update the number of parts for each minifigure +UPDATE "rebrickable_minifigures" +SET "number_of_parts" = "parts_sum"."number_of_parts" +FROM ( + SELECT + "parts"."figure", + SUM("parts"."quantity") as "number_of_parts" + FROM ( + SELECT + "bricktracker_parts"."figure", + "bricktracker_parts"."quantity" + FROM "bricktracker_parts" + WHERE "bricktracker_parts"."figure" IS NOT NULL + GROUP BY + "bricktracker_parts"."figure", + "bricktracker_parts"."part", + "bricktracker_parts"."color", + "bricktracker_parts"."spare" + ) "parts" + GROUP BY "parts"."figure" +) "parts_sum" +WHERE "rebrickable_minifigures"."figure" = "parts_sum"."figure"; + +COMMIT; diff --git a/bricktracker/sql/minifigure/base/base.sql b/bricktracker/sql/minifigure/base/base.sql index d651bf0..a3a30a7 100644 --- a/bricktracker/sql/minifigure/base/base.sql +++ b/bricktracker/sql/minifigure/base/base.sql @@ -2,6 +2,7 @@ SELECT "bricktracker_minifigures"."quantity", "rebrickable_minifigures"."figure", "rebrickable_minifigures"."number", + "rebrickable_minifigures"."number_of_parts", "rebrickable_minifigures"."name", "rebrickable_minifigures"."image", {% block total_missing %} diff --git a/bricktracker/sql/rebrickable/minifigure/insert.sql b/bricktracker/sql/rebrickable/minifigure/insert.sql index 6c0ac8e..3db1680 100644 --- a/bricktracker/sql/rebrickable/minifigure/insert.sql +++ b/bricktracker/sql/rebrickable/minifigure/insert.sql @@ -2,16 +2,19 @@ INSERT OR IGNORE INTO "rebrickable_minifigures" ( "figure", "number", "name", - "image" + "image", + "number_of_parts" ) VALUES ( :figure, :number, :name, - :image + :image, + :number_of_parts ) ON CONFLICT("figure") DO UPDATE SET "number" = :number, "name" = :name, -"image" = :image +"image" = :image, +"number_of_parts" = :number_of_parts WHERE "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM :figure diff --git a/bricktracker/version.py b/bricktracker/version.py index 767fad5..4efb1e6 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] = 14 +__database_version__: Final[int] = 15 diff --git a/templates/macro/table.html b/templates/macro/table.html index d31e1c2..ebf1ded 100644 --- a/templates/macro/table.html +++ b/templates/macro/table.html @@ -1,4 +1,4 @@ -{% macro header(color=false, quantity=false, missing_parts=false, damaged_parts=false, sets=false, minifigures=false) %} +{% macro header(color=false, parts=false, quantity=false, missing_parts=false, damaged_parts=false, sets=false, minifigures=false) %} @@ -6,6 +6,9 @@ {% if color %} {% endif %} + {% if parts %} + + {% endif %} {% if quantity %} {% endif %} diff --git a/templates/minifigure/card.html b/templates/minifigure/card.html index 80459e3..2cba9b4 100644 --- a/templates/minifigure/card.html +++ b/templates/minifigure/card.html @@ -6,6 +6,7 @@ {{ card.header(item, item.fields.name, solo=solo, identifier=item.fields.figure, icon='user-line') }} {{ card.image(item, solo=solo, last=last, caption=item.fields.name, alt=item.fields.figure, medium=true) }}
+ {{ badge.parts(item.fields.number_of_parts, solo=solo, last=last) }} {% if last %} {{ badge.set(item.fields.set, solo=solo, last=last, id=item.fields.rebrickable_set_id) }} {{ badge.quantity(item.fields.quantity, solo=solo, last=last) }} diff --git a/templates/minifigure/table.html b/templates/minifigure/table.html index aeb0bb7..c91ab58 100644 --- a/templates/minifigure/table.html +++ b/templates/minifigure/table.html @@ -2,7 +2,7 @@
{% if quantity %}{{ item.fields.quantity * quantity }}{% else %}{{ item.fields.quantity }}{% endif %} - {{ form.input('Missing', item.fields.id, item.html_id(), item.url_for_missing(), item.fields.total_missing, all=all, read_only=read_only) }} - + {{ form.input('Missing', item.fields.id, item.html_id('missing'), item.url_for_problem('missing'), item.fields.total_missing, all=all, read_only=read_only) }} + + {{ form.input('Damaged', item.fields.id, item.html_id('damaged'), item.url_for_problem('damaged'), item.fields.total_damaged, all=all, read_only=read_only) }} + {{ item.fields.total_sets }} {{ item.fields.total_minifigures }} Quantity Missing{% if missing_parts %} parts{% endif %} Missing{% if missing_parts %} parts{% endif %} Damaged{% if damaged_parts %} parts{% endif %} Sets{% if quantity %}{{ item.fields.quantity * quantity }}{% else %}{{ item.fields.quantity }}{% endif %} - {{ form.input('Missing', item.fields.id, item.html_id('missing'), item.url_for_problem('missing'), item.fields.total_missing, all=all, read_only=read_only) }} - - {{ form.input('Damaged', item.fields.id, item.html_id('damaged'), item.url_for_problem('damaged'), item.fields.total_damaged, all=all, read_only=read_only) }} - + {{ form.input('Missing', item.fields.id, item.html_id('missing'), item.url_for_problem('missing'), item.fields.total_missing, all=all, read_only=read_only) }} + + {{ form.input('Damaged', item.fields.id, item.html_id('damaged'), item.url_for_problem('damaged'), item.fields.total_damaged, all=all, read_only=read_only) }} + {{ item.fields.total_sets }} {{ item.fields.total_minifigures }}
Image Color Parts Quantity
- {{ table.header(quantity=true, missing_parts=true, damaged_parts=true, sets=true) }} + {{ table.header(parts=true, quantity=true, missing_parts=true, damaged_parts=true, sets=true) }} {% for item in table_collection %} @@ -13,6 +13,7 @@ {{ table.rebrickable(item) }} {% endif %} + From 7453d97c81ec6c6bc67b6e89ffbabd866b2699c5 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 10:49:23 +0100 Subject: [PATCH 104/154] Wrap form metadata in accordion for legibility --- templates/add.html | 59 ++++++++++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/templates/add.html b/templates/add.html index 3a0b784..9f33c05 100644 --- a/templates/add.html +++ b/templates/add.html @@ -1,3 +1,5 @@ +{% import 'macro/accordion.html' as accordion %} + {% extends 'base.html' %} {% block title %} - {% if not bulk %}Add a set{% else %}Bulk add sets{% endif %}{% endblock %} @@ -33,32 +35,37 @@ Add without confirmation - {% if brickset_owners | length %} -
Owners
-
- {% for owner in brickset_owners %} - {% with id=owner.as_dataset() %} -
- - -
- {% endwith %} - {% endfor %} -
- {% endif %} - {% if brickset_tags | length %} -
Tags
-
- {% for tag in brickset_tags %} - {% with id=tag.as_dataset() %} -
- - -
- {% endwith %} - {% endfor %} -
- {% endif %} +
Metadata
+
+ {% if brickset_owners | length %} + {{ accordion.header('Owners', 'owners', 'metadata', icon='user-line') }} +
+ {% for owner in brickset_owners %} + {% with id=owner.as_dataset() %} +
+ + +
+ {% endwith %} + {% endfor %} +
+ {{ accordion.footer() }} + {% endif %} + {% if brickset_tags | length %} + {{ accordion.header('Tags', 'tags', 'metadata', icon='price-tag-2-line') }} +
+ {% for tag in brickset_tags %} + {% with id=tag.as_dataset() %} +
+ + +
+ {% endwith %} + {% endfor %} +
+ {{ accordion.footer() }} + {% endif %} +

From d0d1e53acc069967332d1e732124c9a3f5af88e6 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 10:56:32 +0100 Subject: [PATCH 105/154] Fix set storages and purchase locations to be normal metadata --- bricktracker/sql/migrations/0007.sql | 18 ++++++++++-------- bricktracker/sql_counter.py | 4 ++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/bricktracker/sql/migrations/0007.sql b/bricktracker/sql/migrations/0007.sql index 89ef71f..7d52d33 100644 --- a/bricktracker/sql/migrations/0007.sql +++ b/bricktracker/sql/migrations/0007.sql @@ -7,16 +7,18 @@ BEGIN TRANSACTION; -- Rename sets table ALTER TABLE "bricktracker_sets" RENAME TO "bricktracker_sets_old"; --- Create a Bricktracker set storage table for later -CREATE TABLE "bricktracker_set_storages" ( +-- Create a Bricktracker metadata storage table for later +CREATE TABLE "bricktracker_metadata_storages" ( + "id" TEXT NOT NULL, "name" TEXT NOT NULL, - PRIMARY KEY("name") + PRIMARY KEY("id") ); --- Create a Bricktracker set storage table for later -CREATE TABLE "bricktracker_set_purchase_locations" ( +-- Create a Bricktracker metadata purchase location table for later +CREATE TABLE "bricktracker_metadata_purchase_locations" ( + "id" TEXT NOT NULL, "name" TEXT NOT NULL, - PRIMARY KEY("name") + PRIMARY KEY("id") ); -- Re-Create a Bricktracker set table with the simplified name @@ -30,8 +32,8 @@ CREATE TABLE "bricktracker_sets" ( "purchase_price" REAL, -- Purchase price PRIMARY KEY("id"), FOREIGN KEY("set") REFERENCES "rebrickable_sets"("set"), - FOREIGN KEY("storage") REFERENCES "bricktracker_set_storages"("name"), - FOREIGN KEY("purchase_location") REFERENCES "bricktracker_set_purchase_locations"("name") + FOREIGN KEY("storage") REFERENCES "bricktracker_metadata_storages"("id"), + FOREIGN KEY("purchase_location") REFERENCES "bricktracker_metadata_purchase_locations"("id") ); -- Insert existing sets into the new table diff --git a/bricktracker/sql_counter.py b/bricktracker/sql_counter.py index 7d8c882..e5b9262 100644 --- a/bricktracker/sql_counter.py +++ b/bricktracker/sql_counter.py @@ -3,15 +3,15 @@ from typing import Tuple # Some table aliases to make it look cleaner (id: (name, icon)) ALIASES: dict[str, Tuple[str, str]] = { 'bricktracker_metadata_owners': ('Bricktracker set owners metadata', 'user-line'), # noqa: E501 + 'bricktracker_metadata_purchase_locations': ('Bricktracker set purchase locations metadata', 'building-line'), # noqa: E501 'bricktracker_metadata_statuses': ('Bricktracker set status metadata', 'checkbox-line'), # noqa: E501 + 'bricktracker_metadata_storages': ('Bricktracker set storages metadata', 'archive-2-line'), # noqa: E501 'bricktracker_metadata_tags': ('Bricktracker set tags metadata', 'price-tag-2-line'), # noqa: E501 '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_purchase_locations': ('Bricktracker set purchase locations', 'building-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_set_tags': ('Bricktracker set tags', 'price-tag-2-line'), # noqa: E501 'bricktracker_sets': ('Bricktracker sets', 'hashtag'), 'bricktracker_wishes': ('Bricktracker wishes', 'gift-line'), From 2b3793450379036bfac460d0cbbcb9030abec353 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 12:19:49 +0100 Subject: [PATCH 106/154] Make form.checkbox parent configurable --- templates/macro/form.html | 4 ++-- templates/set/card.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/macro/form.html b/templates/macro/form.html index dfd8211..b93ac17 100644 --- a/templates/macro/form.html +++ b/templates/macro/form.html @@ -1,9 +1,9 @@ -{% macro checkbox(item, metadata, delete=false) %} +{% macro checkbox(item, metadata, parent=none, delete=false) %} {% if g.login.is_authenticated() %} {% set prefix=metadata.as_dataset() %} {% for status in brickset_statuses %} -

  • {{ form.checkbox(item, status, delete=delete) }}
  • +
  • {{ form.checkbox(item, status, parent='set', delete=delete) }}
  • {% endfor %} {% endif %} From 53d1603e3e30d01d40d5d77a7d8f5e97eb3b9f32 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 12:25:42 +0100 Subject: [PATCH 107/154] Simplify the instantiation of metadata list --- bricktracker/reload.py | 9 +++------ bricktracker/set.py | 9 +++------ bricktracker/set_list.py | 15 ++++++--------- bricktracker/set_owner_list.py | 6 ++++++ bricktracker/set_status_list.py | 6 ++++++ bricktracker/set_tag_list.py | 6 ++++++ bricktracker/views/add.py | 10 ++++------ bricktracker/views/admin/admin.py | 6 +++--- bricktracker/views/index.py | 9 +++------ bricktracker/views/set.py | 21 +++++++++------------ 10 files changed, 49 insertions(+), 48 deletions(-) diff --git a/bricktracker/reload.py b/bricktracker/reload.py index 16fca2f..6673ab1 100644 --- a/bricktracker/reload.py +++ b/bricktracker/reload.py @@ -1,10 +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 .set_tag import BrickSetTag from .set_tag_list import BrickSetTagList from .theme_list import BrickThemeList @@ -17,13 +14,13 @@ def reload() -> None: BrickInstructionsList(force=True) # Reload the set owners - BrickSetOwnerList(BrickSetOwner, force=True) + BrickSetOwnerList.new(force=True) # Reload the set statuses - BrickSetStatusList(BrickSetStatus, force=True) + BrickSetStatusList.new(force=True) # Reload the set tags - BrickSetTagList(BrickSetTag, force=True) + BrickSetTagList.new(force=True) # Reload retired sets BrickRetiredList(force=True) diff --git a/bricktracker/set.py b/bricktracker/set.py index 000047d..09b22ec 100644 --- a/bricktracker/set.py +++ b/bricktracker/set.py @@ -9,11 +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 .set_tag import BrickSetTag from .set_tag_list import BrickSetTagList from .sql import BrickSQL if TYPE_CHECKING: @@ -169,9 +166,9 @@ class BrickSet(RebrickableSet): # Load from database if not self.select( - owners=BrickSetOwnerList(BrickSetOwner).as_columns(), - statuses=BrickSetStatusList(BrickSetStatus).as_columns(all=True), - tags=BrickSetTagList(BrickSetTag).as_columns(), + owners=BrickSetOwnerList.new().as_columns(), + statuses=BrickSetStatusList.new().as_columns(all=True), + tags=BrickSetTagList.new().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 d538daa..28d8153 100644 --- a/bricktracker/set_list.py +++ b/bricktracker/set_list.py @@ -3,11 +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_tag import BrickSetTag from .set_tag_list import BrickSetTagList from .set import BrickSet @@ -44,9 +41,9 @@ 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(), - tags=BrickSetTagList(BrickSetTag).as_columns(), + owners=BrickSetOwnerList.new().as_columns(), + statuses=BrickSetStatusList.new().as_columns(), + tags=BrickSetTagList.new().as_columns(), ): brickset = BrickSet(record=record) @@ -115,9 +112,9 @@ class BrickSetList(BrickRecordList[BrickSet]): for record in self.select( order=order, limit=limit, - owners=BrickSetOwnerList(BrickSetOwner).as_columns(), - statuses=BrickSetStatusList(BrickSetStatus).as_columns(), - tags=BrickSetTagList(BrickSetTag).as_columns(), + owners=BrickSetOwnerList.new().as_columns(), + statuses=BrickSetStatusList.new().as_columns(), + tags=BrickSetTagList.new().as_columns(), ): brickset = BrickSet(record=record) diff --git a/bricktracker/set_owner_list.py b/bricktracker/set_owner_list.py index 1309749..7d3b8f5 100644 --- a/bricktracker/set_owner_list.py +++ b/bricktracker/set_owner_list.py @@ -1,4 +1,5 @@ import logging +from typing import Self from .metadata_list import BrickMetadataList from .set_owner import BrickSetOwner @@ -15,3 +16,8 @@ class BrickSetOwnerList(BrickMetadataList[BrickSetOwner]): # Queries select_query = 'set/metadata/owner/list' + + # Instantiate the list with the proper class + @classmethod + def new(cls, /, *, force: bool = False) -> Self: + return cls(BrickSetOwner, force=force) diff --git a/bricktracker/set_status_list.py b/bricktracker/set_status_list.py index dabd3b0..e238f62 100644 --- a/bricktracker/set_status_list.py +++ b/bricktracker/set_status_list.py @@ -1,4 +1,5 @@ import logging +from typing import Self from .metadata_list import BrickMetadataList from .set_status import BrickSetStatus @@ -24,3 +25,8 @@ class BrickSetStatusList(BrickMetadataList[BrickSetStatus]): in self.records if all or record.fields.displayed_on_grid ] + + # Instantiate the list with the proper class + @classmethod + def new(cls, /, *, force: bool = False) -> Self: + return cls(BrickSetStatus, force=force) diff --git a/bricktracker/set_tag_list.py b/bricktracker/set_tag_list.py index 92806f2..822ac3b 100644 --- a/bricktracker/set_tag_list.py +++ b/bricktracker/set_tag_list.py @@ -1,4 +1,5 @@ import logging +from typing import Self from .metadata_list import BrickMetadataList from .set_tag import BrickSetTag @@ -15,3 +16,8 @@ class BrickSetTagList(BrickMetadataList[BrickSetTag]): # Queries select_query = 'set/metadata/tag/list' + + # Instantiate the list with the proper class + @classmethod + def new(cls, /, *, force: bool = False) -> Self: + return cls(BrickSetTag, force=force) diff --git a/bricktracker/views/add.py b/bricktracker/views/add.py index 9072973..e77e7dd 100644 --- a/bricktracker/views/add.py +++ b/bricktracker/views/add.py @@ -3,9 +3,7 @@ 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 ..set_tag import BrickSetTag from ..set_tag_list import BrickSetTagList from ..socket import MESSAGES @@ -21,8 +19,8 @@ def add() -> str: return render_template( 'add.html', - brickset_owners=BrickSetOwnerList(BrickSetOwner).list(), - brickset_tags=BrickSetTagList(BrickSetTag).list(), + brickset_owners=BrickSetOwnerList.new().list(), + brickset_tags=BrickSetTagList.new().list(), path=current_app.config['SOCKET_PATH'], namespace=current_app.config['SOCKET_NAMESPACE'], messages=MESSAGES @@ -38,8 +36,8 @@ def bulk() -> str: return render_template( 'add.html', - brickset_owners=BrickSetOwnerList(BrickSetOwner).list(), - brickset_tags=BrickSetTagList(BrickSetTag).list(), + brickset_owners=BrickSetOwnerList.new().list(), + brickset_tags=BrickSetTagList.new().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 415cf48..c716b56 100644 --- a/bricktracker/views/admin/admin.py +++ b/bricktracker/views/admin/admin.py @@ -47,9 +47,9 @@ def admin() -> str: database_version = database.version database_counters = BrickSQL().count_records() - metadata_owners = BrickSetOwnerList(BrickSetOwner).list() - metadata_statuses = BrickSetStatusList(BrickSetStatus).list(all=True) - metadata_tags = BrickSetTagList(BrickSetTag).list() + metadata_owners = BrickSetOwnerList.new().list() + metadata_statuses = BrickSetStatusList.new().list(all=True) + metadata_tags = BrickSetTagList.new().list() except Exception as e: database_exception = e diff --git a/bricktracker/views/index.py b/bricktracker/views/index.py index 1cbcd56..0a25e07 100644 --- a/bricktracker/views/index.py +++ b/bricktracker/views/index.py @@ -2,11 +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_tag import BrickSetTag from ..set_tag_list import BrickSetTagList from ..set_list import BrickSetList @@ -20,8 +17,8 @@ def index() -> str: return render_template( 'index.html', brickset_collection=BrickSetList().last(), - brickset_owners=BrickSetOwnerList(BrickSetOwner).list(), - brickset_statuses=BrickSetStatusList(BrickSetStatus).list(), - brickset_tags=BrickSetTagList(BrickSetTag).list(), + brickset_owners=BrickSetOwnerList.new().list(), + brickset_statuses=BrickSetStatusList.new().list(), + brickset_tags=BrickSetTagList.new().list(), minifigure_collection=BrickMinifigureList().last(), ) diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py index add66d2..6d86bb1 100644 --- a/bricktracker/views/set.py +++ b/bricktracker/views/set.py @@ -17,11 +17,8 @@ from ..minifigure import BrickMinifigure from ..part import BrickPart from ..set import BrickSet from ..set_list import BrickSetList -from ..set_owner import BrickSetOwner from ..set_owner_list import BrickSetOwnerList -from ..set_status import BrickSetStatus from ..set_status_list import BrickSetStatusList -from ..set_tag import BrickSetTag from ..set_tag_list import BrickSetTagList from ..socket import MESSAGES @@ -37,9 +34,9 @@ def list() -> str: return render_template( 'sets.html', collection=BrickSetList().all(), - brickset_owners=BrickSetOwnerList(BrickSetOwner).list(), - brickset_statuses=BrickSetStatusList(BrickSetStatus).list(), - brickset_tags=BrickSetTagList(BrickSetTag).list(), + brickset_owners=BrickSetOwnerList.new().list(), + brickset_statuses=BrickSetStatusList.new().list(), + brickset_tags=BrickSetTagList.new().list(), ) @@ -49,7 +46,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(BrickSetOwner).get(metadata_id) + owner = BrickSetOwnerList.new().get(metadata_id) state = owner.update_set_state(brickset, json=request.json) @@ -62,7 +59,7 @@ 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(BrickSetStatus).get(metadata_id) + status = BrickSetStatusList.new().get(metadata_id) state = status.update_set_state(brickset, json=request.json) @@ -75,7 +72,7 @@ def update_status(*, id: str, metadata_id: str) -> Response: @exception_handler(__file__, json=True) def update_tag(*, id: str, metadata_id: str) -> Response: brickset = BrickSet().select_light(id) - tag = BrickSetTagList(BrickSetTag).get(metadata_id) + tag = BrickSetTagList.new().get(metadata_id) state = tag.update_set_state(brickset, json=request.json) @@ -130,9 +127,9 @@ 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), - brickset_tags=BrickSetTagList(BrickSetTag).list(), + brickset_owners=BrickSetOwnerList.new().list(), + brickset_statuses=BrickSetStatusList.new().list(all=True), + brickset_tags=BrickSetTagList.new().list(), ) From 8ea6a3d003e36acf21541ee9410747dd3548b9b4 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 16:03:21 +0100 Subject: [PATCH 108/154] Remove useless format() --- bricktracker/metadata.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bricktracker/metadata.py b/bricktracker/metadata.py index 4b7c54e..f43eaf5 100644 --- a/bricktracker/metadata.py +++ b/bricktracker/metadata.py @@ -49,9 +49,7 @@ class BrickMetadata(BrickRecord): # HTML dataset name def as_dataset(self, /) -> str: - return '{id}'.format( - id=self.as_column().replace('_', '-') - ) + return self.as_column().replace('_', '-') # Delete from database def delete(self, /) -> None: From b87ff162c1404cb30790d791e9907fa87448d870 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 16:06:56 +0100 Subject: [PATCH 109/154] Center not found message for metadata --- templates/admin/owner.html | 2 +- templates/admin/status.html | 2 +- templates/admin/tag.html | 2 +- templates/set/management.html | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/templates/admin/owner.html b/templates/admin/owner.html index 7447a6d..8c7c261 100644 --- a/templates/admin/owner.html +++ b/templates/admin/owner.html @@ -22,7 +22,7 @@ {% endfor %} {% else %} -
  • No owner found.
  • +
  • No owner found.
  • {% endif %}
  • diff --git a/templates/admin/status.html b/templates/admin/status.html index a73462a..609bc68 100644 --- a/templates/admin/status.html +++ b/templates/admin/status.html @@ -33,7 +33,7 @@
  • {% endfor %} {% else %} -
  • No status found.
  • +
  • No status found.
  • {% endif %}
  • diff --git a/templates/admin/tag.html b/templates/admin/tag.html index 7c67a56..c3ca1f6 100644 --- a/templates/admin/tag.html +++ b/templates/admin/tag.html @@ -22,7 +22,7 @@
  • {% endfor %} {% else %} -
  • No tag found.
  • +
  • No tag found.
  • {% endif %}
  • diff --git a/templates/set/management.html b/templates/set/management.html index 9b48448..f80830b 100644 --- a/templates/set/management.html +++ b/templates/set/management.html @@ -6,7 +6,7 @@
  • {{ form.checkbox(item, owner, delete=delete) }}
  • {% endfor %} {% else %} -
  • No owner found.
  • +
  • No owner found.
  • {% endif %}
    @@ -20,7 +20,7 @@
  • {{ form.checkbox(item, tag, delete=delete) }}
  • {% endfor %} {% else %} -
  • No tag found.
  • +
  • No tag found.
  • {% endif %}
    From 187afdc2cf2e0bf4c0de114db28f54c199e7ee28 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 16:08:11 +0100 Subject: [PATCH 110/154] Add support for select to BrickChanger --- static/scripts/changer.js | 51 +++++++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/static/scripts/changer.js b/static/scripts/changer.js index 89f012d..4db9cd7 100644 --- a/static/scripts/changer.js +++ b/static/scripts/changer.js @@ -7,7 +7,7 @@ class BrickChanger { this.html_clear = document.getElementById(`clear-${prefix}-${id}`); this.html_status = document.getElementById(`status-${prefix}-${id}`); this.html_status_tooltip = undefined; - this.html_type = this.html_element.getAttribute("type"); + this.html_type = undefined; this.url = url; if (parent) { @@ -16,12 +16,29 @@ class BrickChanger { } // Register an event depending on the type - if (this.html_type == "checkbox") { - var listener = "change"; - } else if(this.html_type == "text") { - var listener = "change"; - } else { - throw Error("Unsupported input type for BrickChanger"); + let listener = undefined; + switch (this.html_element.tagName) { + case "INPUT": + this.html_type = this.html_element.getAttribute("type"); + + switch (this.html_type) { + case "checkbox": + case "text": + listener = "change"; + break; + + default: + throw Error(`Unsupported input type for BrickChanger: ${this.html_type}`); + } + break; + + case "SELECT": + this.html_type = "select"; + listener = "change"; + break; + + default: + throw Error(`Unsupported HTML tag type for BrickChanger: ${this.html_element.tagName}`); } this.html_element.addEventListener(listener, ((changer) => (e) => { @@ -90,12 +107,20 @@ class BrickChanger { this.status_unknown(); // Grab the value depending on the type - if (this.html_type == "checkbox") { - var value = this.html_element.checked; - } else if(this.html_type == "text") { - var value = this.html_element.value; - } else { - throw Error("Unsupported input type for BrickChanger"); + let value = undefined; + + switch(this.html_type) { + case "checkbox": + value = this.html_element.checked; + break; + + case "text": + case "select": + value = this.html_element.value; + break; + + default: + throw Error("Unsupported input type for BrickChanger"); } const response = await fetch(this.url, { From ec7fab2a7a506dbafd56cc46cb69494af96c7d1a Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 16:35:09 +0100 Subject: [PATCH 111/154] Scroll confirm and progress to view when adding a set through the socket --- static/scripts/socket/set.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/static/scripts/socket/set.js b/static/scripts/socket/set.js index a7e660b..d561261 100644 --- a/static/scripts/socket/set.js +++ b/static/scripts/socket/set.js @@ -163,6 +163,10 @@ class BrickSetSocket extends BrickSocket { this.spinner(true); + if (this.html_progress_bar) { + this.html_progress_bar.scrollIntoView(); + } + this.socket.emit(this.messages.IMPORT_SET, { set: (set !== undefined) ? set : this.html_input.value, owners: owners, @@ -240,6 +244,8 @@ class BrickSetSocket extends BrickSocket { })(this, data["set"]); this.html_card_confirm.addEventListener("click", this.confirm_listener); + + this.html_card_confirm.scrollIntoView(); } } } From 9aff7e622d6ebd2b79f4ae3544bf8110129b7755 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 16:46:45 +0100 Subject: [PATCH 112/154] Set storage --- CHANGELOG.md | 2 + bricktracker/app.py | 2 + bricktracker/metadata.py | 62 +++++++- bricktracker/metadata_list.py | 135 +++++++++++++----- bricktracker/record_list.py | 6 +- bricktracker/reload.py | 4 + bricktracker/set.py | 42 +++--- bricktracker/set_list.py | 12 +- bricktracker/set_storage.py | 13 ++ bricktracker/set_storage_list.py | 23 +++ bricktracker/sql/set/base/base.sql | 1 + bricktracker/sql/set/insert.sql | 6 +- .../sql/set/metadata/storage/base.sql | 6 + .../sql/set/metadata/storage/delete.sql | 6 + .../sql/set/metadata/storage/insert.sql | 11 ++ .../sql/set/metadata/storage/list.sql | 1 + .../sql/set/metadata/storage/select.sql | 5 + .../sql/set/metadata/storage/update/field.sql | 3 + .../sql/set/metadata/storage/update/state.sql | 3 + bricktracker/views/add.py | 11 +- bricktracker/views/admin/admin.py | 15 +- bricktracker/views/admin/storage.py | 84 +++++++++++ bricktracker/views/index.py | 8 +- bricktracker/views/set.py | 37 +++-- static/scripts/socket/set.js | 12 ++ templates/add.html | 13 ++ templates/admin.html | 3 + templates/admin/storage.html | 42 ++++++ templates/admin/storage/delete.html | 19 +++ templates/macro/badge.html | 12 ++ templates/macro/form.html | 24 ++++ templates/set/card.html | 3 +- 32 files changed, 538 insertions(+), 88 deletions(-) create mode 100644 bricktracker/set_storage.py create mode 100644 bricktracker/set_storage_list.py create mode 100644 bricktracker/sql/set/metadata/storage/base.sql create mode 100644 bricktracker/sql/set/metadata/storage/delete.sql create mode 100644 bricktracker/sql/set/metadata/storage/insert.sql create mode 100644 bricktracker/sql/set/metadata/storage/list.sql create mode 100644 bricktracker/sql/set/metadata/storage/select.sql create mode 100644 bricktracker/sql/set/metadata/storage/update/field.sql create mode 100644 bricktracker/sql/set/metadata/storage/update/state.sql create mode 100644 bricktracker/views/admin/storage.py create mode 100644 templates/admin/storage.html create mode 100644 templates/admin/storage/delete.html 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) }} From 103c3c3017785c2eb69fd04c7f78b1102bacaddd Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 16:47:09 +0100 Subject: [PATCH 113/154] Additional socket debug messages --- bricktracker/socket.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/bricktracker/socket.py b/bricktracker/socket.py index 7aedaf2..99cd625 100644 --- a/bricktracker/socket.py +++ b/bricktracker/socket.py @@ -109,10 +109,20 @@ class BrickSocket(object): @self.socket.on(MESSAGES['IMPORT_SET'], namespace=self.namespace) @rebrickable_socket(self) def import_set(data: dict[str, Any], /) -> None: + logger.debug('Socket: IMPORT_SET={data} (from: {fr})'.format( + data=data, + fr=request.sid, # type: ignore + )) + BrickSet().download(self, data) @self.socket.on(MESSAGES['LOAD_SET'], namespace=self.namespace) def load_set(data: dict[str, Any], /) -> None: + logger.debug('Socket: LOAD_SET={data} (from: {fr})'.format( + data=data, + fr=request.sid, # type: ignore + )) + BrickSet().load(self, data) # Update the progress auto-incrementing From 714e84ea09fdde7902172421336a5391366f0beb Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 16:47:26 +0100 Subject: [PATCH 114/154] Missed set storage management --- templates/set/management.html | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/templates/set/management.html b/templates/set/management.html index f80830b..54b2e87 100644 --- a/templates/set/management.html +++ b/templates/set/management.html @@ -13,6 +13,15 @@ Manage the set owners
    {{ accordion.footer() }} + {{ accordion.header('Storage', 'storage', 'set-details', icon='archive-2-line') }} + {% if brickset_storages | length %} + {{ form.select('Storage', item, 'storage', brickset_storages, delete=delete) }} + {% else %} +

    No storage found.

    + {% endif %} +
    + Manage the set storages + {{ accordion.footer() }} {{ accordion.header('Tags', 'tag', 'set-details', icon='price-tag-2-line', class='p-0') }}
      {% if brickset_tags | length %} From 56c926a8ef65d1ef5cfdb96daa450b031f2639e6 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 16:47:35 +0100 Subject: [PATCH 115/154] Cosmetics --- templates/macro/form.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/macro/form.html b/templates/macro/form.html index d3d3512..6fb3890 100644 --- a/templates/macro/form.html +++ b/templates/macro/form.html @@ -9,7 +9,7 @@ {% endif %} autocomplete="off"> {% else %} From ac2d2a0b5d9bf0c52f0660754e10aa25fc1949d0 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 17:09:59 +0100 Subject: [PATCH 116/154] Storage filterable and searchable on the Grid --- templates/set/card.html | 5 +++++ templates/sets.html | 19 ++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/templates/set/card.html b/templates/set/card.html index a327052..3585acc 100644 --- a/templates/set/card.html +++ b/templates/set/card.html @@ -11,6 +11,11 @@ data-has-minifigures="{{ (item.fields.total_minifigures > 0) | int }}" data-minifigures="{{ item.fields.total_minifigures }}" data-has-missing="{{ (item.fields.total_missing > 0) | int }}" data-missing="{{ item.fields.total_missing }}" data-has-damaged="{{ (item.fields.total_damaged > 0) | int }}" data-damaged="{{ item.fields.total_damaged }}" + data-has-storage="{{ item.fields.storage is not none | int }}" + {% if item.fields.storage is not none %} + data-storage="{{ item.fields.storage }}" + {% if item.fields.storage in brickset_storages.mapping %}data-search-storage="{{ brickset_storages.mapping[item.fields.storage].fields.name | lower }}"{% endif %} + {% endif %} {% for status in brickset_statuses %} {% with checked=item.fields[status.as_column()] %} {% if checked %} diff --git a/templates/sets.html b/templates/sets.html index dc1c10e..dc0a557 100644 --- a/templates/sets.html +++ b/templates/sets.html @@ -10,7 +10,7 @@
      Search - +
    @@ -57,6 +57,7 @@ + {% if brickset_storage | length %}{% endif %} {% for status in brickset_statuses %} @@ -92,6 +93,22 @@
    + {% if brickset_storages | length %} +
    + +
    + Storage + +
    +
    + {% endif %}
    From d45070eb74c57e6365dffb46f3e08526f9855166 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 17:10:13 +0100 Subject: [PATCH 117/154] Display metadata filters only if they have values --- templates/sets.html | 56 ++++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/templates/sets.html b/templates/sets.html index dc0a557..943f794 100644 --- a/templates/sets.html +++ b/templates/sets.html @@ -79,20 +79,22 @@
    -
    - -
    - Owner - + {% if brickset_owners | length %} +
    + +
    + Owner + +
    -
    + {% endif %} {% if brickset_storages | length %}
    @@ -109,20 +111,22 @@
    {% endif %} -
    - -
    - Tag - + {% if brickset_tags | length %} +
    + +
    + Tag + +
    -
    + {% endif %}
    {% for item in collection %} From 561720343b746ffc6bbdd87b45b2f663b41df1a9 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 17:12:03 +0100 Subject: [PATCH 118/154] Remove year from tiny cards --- templates/set/card.html | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/templates/set/card.html b/templates/set/card.html index 3585acc..126c9b3 100644 --- a/templates/set/card.html +++ b/templates/set/card.html @@ -46,7 +46,9 @@ {% for tag in brickset_tags %} {{ badge.tag(item, tag, solo=solo, last=last) }} {% endfor %} - {{ badge.year(item.fields.year, solo=solo, last=last) }} + {% if not last %} + {{ badge.year(item.fields.year, solo=solo, last=last) }} + {% endif %} {{ 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) }} From d8046ac1744c6075d467f8ec24f090ee44daa708 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 17:38:39 +0100 Subject: [PATCH 119/154] Add missing metadata for set loaded from minifigures or parts --- bricktracker/set_list.py | 20 ++++++++++++++++---- bricktracker/views/minifigure.py | 6 ++++++ bricktracker/views/part.py | 6 ++++++ templates/macro/badge.html | 2 +- templates/minifigure/card.html | 2 +- templates/part/card.html | 2 +- 6 files changed, 31 insertions(+), 7 deletions(-) diff --git a/bricktracker/set_list.py b/bricktracker/set_list.py index e25594c..ffc436d 100644 --- a/bricktracker/set_list.py +++ b/bricktracker/set_list.py @@ -130,7 +130,10 @@ class BrickSetList(BrickRecordList[BrickSet]): # Load the sets from the database for record in self.select( override_query=self.missing_minifigure_query, - order=self.order + order=self.order, + owners=BrickSetOwnerList.as_columns(), + statuses=BrickSetStatusList.as_columns(), + tags=BrickSetTagList.as_columns(), ): brickset = BrickSet(record=record) @@ -147,7 +150,10 @@ class BrickSetList(BrickRecordList[BrickSet]): # Load the sets from the database for record in self.select( override_query=self.missing_part_query, - order=self.order + order=self.order, + owners=BrickSetOwnerList.as_columns(), + statuses=BrickSetStatusList.as_columns(), + tags=BrickSetTagList.as_columns(), ): brickset = BrickSet(record=record) @@ -163,7 +169,10 @@ class BrickSetList(BrickRecordList[BrickSet]): # Load the sets from the database for record in self.select( override_query=self.using_minifigure_query, - order=self.order + order=self.order, + owners=BrickSetOwnerList.as_columns(), + statuses=BrickSetStatusList.as_columns(), + tags=BrickSetTagList.as_columns(), ): brickset = BrickSet(record=record) @@ -180,7 +189,10 @@ class BrickSetList(BrickRecordList[BrickSet]): # Load the sets from the database for record in self.select( override_query=self.using_part_query, - order=self.order + order=self.order, + owners=BrickSetOwnerList.as_columns(), + statuses=BrickSetStatusList.as_columns(), + tags=BrickSetTagList.as_columns(), ): brickset = BrickSet(record=record) diff --git a/bricktracker/views/minifigure.py b/bricktracker/views/minifigure.py index 5d9cc85..9958728 100644 --- a/bricktracker/views/minifigure.py +++ b/bricktracker/views/minifigure.py @@ -3,7 +3,10 @@ from flask import Blueprint, render_template from .exceptions import exception_handler from ..minifigure import BrickMinifigure from ..minifigure_list import BrickMinifigureList +from ..set_owner_list import BrickSetOwnerList from ..set_list import BrickSetList +from ..set_storage_list import BrickSetStorageList +from ..set_tag_list import BrickSetTagList minifigure_page = Blueprint('minifigure', __name__, url_prefix='/minifigures') @@ -28,4 +31,7 @@ def details(*, figure: str) -> str: using=BrickSetList().using_minifigure(figure), missing=BrickSetList().missing_minifigure(figure), damaged=BrickSetList().damaged_minifigure(figure), + brickset_owners=BrickSetOwnerList.list(), + brickset_storages=BrickSetStorageList.list(as_class=True), + brickset_tags=BrickSetTagList.list(), ) diff --git a/bricktracker/views/part.py b/bricktracker/views/part.py index 7cbc1c8..b2a9eed 100644 --- a/bricktracker/views/part.py +++ b/bricktracker/views/part.py @@ -4,7 +4,10 @@ from .exceptions import exception_handler from ..minifigure_list import BrickMinifigureList from ..part import BrickPart from ..part_list import BrickPartList +from ..set_owner_list import BrickSetOwnerList from ..set_list import BrickSetList +from ..set_storage_list import BrickSetStorageList +from ..set_tag_list import BrickSetTagList part_page = Blueprint('part', __name__, url_prefix='/parts') @@ -64,4 +67,7 @@ def details(*, part: str, color: int) -> str: ), different_color=BrickPartList().with_different_color(brickpart), similar_prints=BrickPartList().from_print(brickpart), + brickset_owners=BrickSetOwnerList.list(), + brickset_storages=BrickSetStorageList.list(as_class=True), + brickset_tags=BrickSetTagList.list(), ) diff --git a/templates/macro/badge.html b/templates/macro/badge.html index dfc9384..bd683f4 100644 --- a/templates/macro/badge.html +++ b/templates/macro/badge.html @@ -73,7 +73,7 @@ {% endmacro %} {% macro storage(item, storages, solo=false, last=false) %} - {% if item.fields.storage in storages.mapping %} + {% if storages and item.fields.storage in storages.mapping %} {% set storage = storages.mapping[item.fields.storage] %} {% if last %} {% set tooltip=storage.fields.name %} diff --git a/templates/minifigure/card.html b/templates/minifigure/card.html index 2cba9b4..812c2cd 100644 --- a/templates/minifigure/card.html +++ b/templates/minifigure/card.html @@ -1,4 +1,4 @@ -{% import 'macro/accordion.html' as accordion %} +{% import 'macro/accordion.html' as accordion with context %} {% import 'macro/badge.html' as badge %} {% import 'macro/card.html' as card %} diff --git a/templates/part/card.html b/templates/part/card.html index 16f2103..a83a2f7 100644 --- a/templates/part/card.html +++ b/templates/part/card.html @@ -1,4 +1,4 @@ -{% import 'macro/accordion.html' as accordion %} +{% import 'macro/accordion.html' as accordion with context %} {% import 'macro/badge.html' as badge %} {% import 'macro/card.html' as card %} From 38e664b733b730fbe5ae90ca4dae18fc5de1c629 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 17:38:54 +0100 Subject: [PATCH 120/154] Don't load card dataset for tiny cards --- templates/set/card.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/set/card.html b/templates/set/card.html index 126c9b3..f47a7ca 100644 --- a/templates/set/card.html +++ b/templates/set/card.html @@ -4,7 +4,7 @@ {% import 'macro/form.html' as form %}
    Date: Mon, 3 Feb 2025 18:07:03 +0100 Subject: [PATCH 121/154] Fix storage status filters --- templates/sets.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/templates/sets.html b/templates/sets.html index 943f794..236c378 100644 --- a/templates/sets.html +++ b/templates/sets.html @@ -57,7 +57,10 @@ - {% if brickset_storage | length %}{% endif %} + {% if brickset_storages | length %} + + + {% endif %} {% for status in brickset_statuses %} From 0e485ddb71af1a118f6dbf9dbec3f43dbd0f0b22 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 18:07:56 +0100 Subject: [PATCH 122/154] Collapsible sort on the grid --- .env.sample | 4 ++++ CHANGELOG.md | 3 +++ bricktracker/config.py | 1 + templates/sets.html | 25 +++++++++++++++++-------- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/.env.sample b/.env.sample index 0bfc3f0..b93e8c6 100644 --- a/.env.sample +++ b/.env.sample @@ -239,6 +239,10 @@ # Default: false # BK_SHOW_GRID_FILTERS=true +# Optional: Make the grid sort displayed by default, rather than collapsed +# Default: false +# BK_SHOW_GRID_SORT=true + # Optional: Skip saving or displaying spare parts # Default: false # BK_SKIP_SPARE_PARTS=true diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a1c34d..5ed15af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ - Renamed: `BK_HIDE_MISSING_PARTS` -> `BK_HIDE_ALL_PROBLEMS_PARTS` - Added: `BK_HIDE_TABLE_MISSING_PARTS`, hide the Missing column in all tables - 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 ### Code @@ -82,6 +84,7 @@ Parts - Sets grid - Collapsible controls depending on screen size - Manually collapsible filters (with configuration variable for default state) + - Manually collapsible sort (with configuration variable for default state) ## 1.1.1: PDF Instructions Download diff --git a/bricktracker/config.py b/bricktracker/config.py index 62e23a1..cd7ef74 100644 --- a/bricktracker/config.py +++ b/bricktracker/config.py @@ -55,6 +55,7 @@ CONFIG: Final[list[dict[str, Any]]] = [ {'n': 'SETS_DEFAULT_ORDER', 'd': '"rebrickable_sets"."number" DESC, "rebrickable_sets"."version" ASC'}, # noqa: E501 {'n': 'SETS_FOLDER', 'd': 'sets', 's': True}, {'n': 'SHOW_GRID_FILTERS', 'c': bool}, + {'n': 'SHOW_GRID_SORT', 'c': bool}, {'n': 'SKIP_SPARE_PARTS', 'c': bool}, {'n': 'SOCKET_NAMESPACE', 'd': 'bricksocket'}, {'n': 'SOCKET_PATH', 'd': '/bricksocket/'}, diff --git a/templates/sets.html b/templates/sets.html index 236c378..60101b5 100644 --- a/templates/sets.html +++ b/templates/sets.html @@ -14,7 +14,23 @@
    -
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    Sort @@ -36,13 +52,6 @@ data-sort-clear="true"> Clear
    -
    -
    - -
    -
    From 8ad525926a2214b0d9e613701aa99e15943f183d Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 18:12:31 +0100 Subject: [PATCH 123/154] Fix metadata storage deletion --- bricktracker/sql/set/metadata/storage/delete.sql | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bricktracker/sql/set/metadata/storage/delete.sql b/bricktracker/sql/set/metadata/storage/delete.sql index c50b348..e1c5d22 100644 --- a/bricktracker/sql/set/metadata/storage/delete.sql +++ b/bricktracker/sql/set/metadata/storage/delete.sql @@ -1,6 +1,10 @@ BEGIN TRANSACTION; DELETE FROM "bricktracker_metadata_storages" -WHERE "bricktracker_metadata_statuses"."id" IS NOT DISTINCT FROM '{{ id }}'; +WHERE "bricktracker_metadata_storages"."id" IS NOT DISTINCT FROM '{{ id }}'; + +UPDATE "bricktracker_sets" +SET "storage" = NULL +WHERE "bricktracker_sets"."storage" IS NOT DISTINCT FROM '{{ id }}'; COMMIT; \ No newline at end of file From 8e40b1fd7e6e501bd5e73e39d4dbfd4739cf339a Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 22:20:43 +0100 Subject: [PATCH 124/154] Simplify BrickRecord based lists to deduplicate code --- bricktracker/minifigure_list.py | 74 +++++++++---------- bricktracker/part_list.py | 114 ++++++++++++----------------- bricktracker/set_list.py | 122 ++++++++++---------------------- 3 files changed, 117 insertions(+), 193 deletions(-) diff --git a/bricktracker/minifigure_list.py b/bricktracker/minifigure_list.py index 24a4d2e..fa73562 100644 --- a/bricktracker/minifigure_list.py +++ b/bricktracker/minifigure_list.py @@ -38,13 +38,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): # Load all minifigures def all(self, /) -> Self: - for record in self.select( - override_query=self.all_query, - order=self.order - ): - minifigure = BrickMinifigure(record=record) - - self.records.append(minifigure) + self.list(override_query=self.all_query) return self @@ -55,13 +49,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): self.fields.color = color # Load the minifigures from the database - for record in self.select( - override_query=self.damaged_part_query, - order=self.order - ): - minifigure = BrickMinifigure(record=record) - - self.records.append(minifigure) + self.list(override_query=self.damaged_part_query) return self @@ -73,27 +61,45 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): else: order = '"bricktracker_minifigures"."rowid" DESC' - for record in self.select( - override_query=self.last_query, - order=order, - limit=limit - ): - minifigure = BrickMinifigure(record=record) - - self.records.append(minifigure) + self.list(override_query=self.last_query, order=order, limit=limit) return self + # Base minifigure list + def list( + self, + /, + *, + override_query: str | None = None, + order: str | None = None, + limit: int | None = None, + **context: Any, + ) -> None: + if order is None: + order = self.order + + if hasattr(self, 'brickset'): + brickset = self.brickset + else: + brickset = None + + # Load the sets from the database + for record in super().select( + override_query=override_query, + order=order, + limit=limit, + ): + minifigure = BrickMinifigure(brickset=brickset, record=record) + + self.records.append(minifigure) + # Load minifigures from a brickset def from_set(self, brickset: 'BrickSet', /) -> Self: # Save the brickset self.brickset = brickset # Load the minifigures from the database - for record in self.select(order=self.order): - minifigure = BrickMinifigure(brickset=self.brickset, record=record) - - self.records.append(minifigure) + self.list() return self @@ -104,13 +110,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): self.fields.color = color # Load the minifigures from the database - for record in self.select( - override_query=self.missing_part_query, - order=self.order - ): - minifigure = BrickMinifigure(record=record) - - self.records.append(minifigure) + self.list(override_query=self.missing_part_query) return self @@ -121,13 +121,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): self.fields.color = color # Load the minifigures from the database - for record in self.select( - override_query=self.using_part_query, - order=self.order - ): - minifigure = BrickMinifigure(record=record) - - self.records.append(minifigure) + self.list(override_query=self.using_part_query) return self diff --git a/bricktracker/part_list.py b/bricktracker/part_list.py index eb3f58d..a12ef89 100644 --- a/bricktracker/part_list.py +++ b/bricktracker/part_list.py @@ -42,16 +42,50 @@ class BrickPartList(BrickRecordList[BrickPart]): # Load all parts def all(self, /) -> Self: - for record in self.select( - override_query=self.all_query, - order=self.order - ): - part = BrickPart(record=record) - - self.records.append(part) + self.list(override_query=self.all_query) return self + # Base part list + def list( + self, + /, + *, + override_query: str | None = None, + order: str | None = None, + limit: int | None = None, + **context: Any, + ) -> None: + if order is None: + order = self.order + + if hasattr(self, 'brickset'): + brickset = self.brickset + else: + brickset = None + + if hasattr(self, 'minifigure'): + minifigure = self.minifigure + else: + minifigure = None + + # Load the sets from the database + for record in super().select( + override_query=override_query, + order=order, + limit=limit, + ): + part = BrickPart( + brickset=brickset, + minifigure=minifigure, + record=record, + ) + + if current_app.config['SKIP_SPARE_PARTS'] and part.fields.spare: + continue + + self.records.append(part) + # List specific parts from a brickset or minifigure def list_specific( self, @@ -65,17 +99,7 @@ class BrickPartList(BrickRecordList[BrickPart]): self.minifigure = minifigure # Load the parts from the database - for record in self.select(order=self.order): - part = BrickPart( - brickset=self.brickset, - minifigure=minifigure, - record=record, - ) - - if current_app.config['SKIP_SPARE_PARTS'] and part.fields.spare: - continue - - self.records.append(part) + self.list() return self @@ -89,19 +113,7 @@ class BrickPartList(BrickRecordList[BrickPart]): self.minifigure = minifigure # Load the parts from the database - for record in self.select( - override_query=self.minifigure_query, - order=self.order - ): - part = BrickPart( - minifigure=minifigure, - record=record, - ) - - if current_app.config['SKIP_SPARE_PARTS'] and part.fields.spare: - continue - - self.records.append(part) + self.list(override_query=self.minifigure_query) return self @@ -121,33 +133,13 @@ class BrickPartList(BrickRecordList[BrickPart]): self.fields.color = brickpart.fields.color # Load the parts from the database - for record in self.select( - override_query=self.print_query, - order=self.order - ): - part = BrickPart( - record=record, - ) - - if ( - current_app.config['SKIP_SPARE_PARTS'] and - part.fields.spare - ): - continue - - self.records.append(part) + self.list(override_query=self.print_query) return self # Load problematic parts def problem(self, /) -> Self: - for record in self.select( - override_query=self.problem_query, - order=self.order - ): - part = BrickPart(record=record) - - self.records.append(part) + self.list(override_query=self.problem_query) return self @@ -178,21 +170,7 @@ class BrickPartList(BrickRecordList[BrickPart]): self.fields.color = brickpart.fields.color # Load the parts from the database - for record in self.select( - override_query=self.different_color_query, - order=self.order - ): - part = BrickPart( - record=record, - ) - - if ( - current_app.config['SKIP_SPARE_PARTS'] and - part.fields.spare - ): - continue - - self.records.append(part) + self.list(override_query=self.different_color_query) return self diff --git a/bricktracker/set_list.py b/bricktracker/set_list.py index ffc436d..b12f971 100644 --- a/bricktracker/set_list.py +++ b/bricktracker/set_list.py @@ -1,4 +1,4 @@ -from typing import Self +from typing import Any, Self from flask import current_app @@ -36,23 +36,8 @@ class BrickSetList(BrickRecordList[BrickSet]): # All the sets def all(self, /) -> Self: - themes = set() - # Load the sets from the database - for record in self.select( - order=self.order, - owners=BrickSetOwnerList.as_columns(), - statuses=BrickSetStatusList.as_columns(), - tags=BrickSetTagList.as_columns(), - ): - brickset = BrickSet(record=record) - - self.records.append(brickset) - themes.add(brickset.theme.name) - - # Convert the set into a list and sort it - self.themes = list(themes) - self.themes.sort() + self.list(do_theme=True) return self @@ -62,13 +47,7 @@ class BrickSetList(BrickRecordList[BrickSet]): self.fields.figure = figure # Load the sets from the database - for record in self.select( - override_query=self.damaged_minifigure_query, - order=self.order - ): - brickset = BrickSet(record=record) - - self.records.append(brickset) + self.list(override_query=self.damaged_minifigure_query) return self @@ -79,25 +58,7 @@ class BrickSetList(BrickRecordList[BrickSet]): self.fields.color = color # Load the sets from the database - for record in self.select( - override_query=self.damaged_part_query, - order=self.order - ): - brickset = BrickSet(record=record) - - self.records.append(brickset) - - return self - - # A generic list of the different sets - def generic(self, /) -> Self: - for record in self.select( - override_query=self.generic_query, - order=self.order - ): - brickset = BrickSet(record=record) - - self.records.append(brickset) + self.list(override_query=self.damaged_part_query) return self @@ -109,7 +70,29 @@ class BrickSetList(BrickRecordList[BrickSet]): else: order = '"bricktracker_sets"."rowid" DESC' - for record in self.select( + self.list(order=order, limit=limit) + + return self + + # Base set list + def list( + self, + /, + *, + override_query: str | None = None, + order: str | None = None, + limit: int | None = None, + do_theme: bool = False, + **context: Any, + ) -> None: + themes = set() + + if order is None: + order = self.order + + # Load the sets from the database + for record in super().select( + override_query=override_query, order=order, limit=limit, owners=BrickSetOwnerList.as_columns(), @@ -119,8 +102,13 @@ class BrickSetList(BrickRecordList[BrickSet]): brickset = BrickSet(record=record) self.records.append(brickset) + if do_theme: + themes.add(brickset.theme.name) - return self + # Convert the set into a list and sort it + if do_theme: + self.themes = list(themes) + self.themes.sort() # Sets missing a minifigure part def missing_minifigure(self, figure: str, /) -> Self: @@ -128,16 +116,7 @@ class BrickSetList(BrickRecordList[BrickSet]): self.fields.figure = figure # Load the sets from the database - for record in self.select( - override_query=self.missing_minifigure_query, - order=self.order, - owners=BrickSetOwnerList.as_columns(), - statuses=BrickSetStatusList.as_columns(), - tags=BrickSetTagList.as_columns(), - ): - brickset = BrickSet(record=record) - - self.records.append(brickset) + self.list(override_query=self.missing_minifigure_query) return self @@ -148,16 +127,7 @@ class BrickSetList(BrickRecordList[BrickSet]): self.fields.color = color # Load the sets from the database - for record in self.select( - override_query=self.missing_part_query, - order=self.order, - owners=BrickSetOwnerList.as_columns(), - statuses=BrickSetStatusList.as_columns(), - tags=BrickSetTagList.as_columns(), - ): - brickset = BrickSet(record=record) - - self.records.append(brickset) + self.list(override_query=self.missing_part_query) return self @@ -167,16 +137,7 @@ class BrickSetList(BrickRecordList[BrickSet]): self.fields.figure = figure # Load the sets from the database - for record in self.select( - override_query=self.using_minifigure_query, - order=self.order, - owners=BrickSetOwnerList.as_columns(), - statuses=BrickSetStatusList.as_columns(), - tags=BrickSetTagList.as_columns(), - ): - brickset = BrickSet(record=record) - - self.records.append(brickset) + self.list(override_query=self.using_minifigure_query) return self @@ -187,15 +148,6 @@ class BrickSetList(BrickRecordList[BrickSet]): self.fields.color = color # Load the sets from the database - for record in self.select( - override_query=self.using_part_query, - order=self.order, - owners=BrickSetOwnerList.as_columns(), - statuses=BrickSetStatusList.as_columns(), - tags=BrickSetTagList.as_columns(), - ): - brickset = BrickSet(record=record) - - self.records.append(brickset) + self.list(override_query=self.using_part_query) return self From 9a9b5af7f4adabfbbc7068de825d2ef95dddf3b2 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 22:21:26 +0100 Subject: [PATCH 125/154] Restore RebrickablePart __init__ definition --- bricktracker/part.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/bricktracker/part.py b/bricktracker/part.py index fa463be..12eab28 100644 --- a/bricktracker/part.py +++ b/bricktracker/part.py @@ -1,4 +1,5 @@ import logging +from sqlite3 import Row from typing import Any, Self, TYPE_CHECKING import traceback @@ -25,8 +26,19 @@ class BrickPart(RebrickablePart): generic_query: str = 'part/select/generic' select_query: str = 'part/select/specific' - def __init__(self, /, **kwargs): - super().__init__(**kwargs) + def __init__( + self, + /, + *, + brickset: 'BrickSet | None' = None, + minifigure: 'BrickMinifigure | None' = None, + record: Row | dict[str, Any] | None = None + ): + super().__init__( + brickset=brickset, + minifigure=minifigure, + record=record + ) if self.minifigure is not None: self.identifier = self.minifigure.fields.figure From 76ccb20dfacb58d52ecab76221adecb2fbda7059 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 22:28:36 +0100 Subject: [PATCH 126/154] Add a little border at the left of accordion to separate sections --- templates/macro/accordion.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/macro/accordion.html b/templates/macro/accordion.html index 8ae8a88..1fd91f3 100644 --- a/templates/macro/accordion.html +++ b/templates/macro/accordion.html @@ -16,7 +16,7 @@ {% endif %} -
    +
    {% endmacro %} From f9e9edd50662b7bd59c537fe1002e12693f353e6 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 22:46:34 +0100 Subject: [PATCH 127/154] Remove debug print --- bricktracker/metadata_list.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bricktracker/metadata_list.py b/bricktracker/metadata_list.py index 5dfa73c..68238e9 100644 --- a/bricktracker/metadata_list.py +++ b/bricktracker/metadata_list.py @@ -126,7 +126,6 @@ class BrickMetadataList(BrickRecordList[T]): 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: From 4e3ae491874d9d072f69c1abb78862f8063c9df0 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 23:45:35 +0100 Subject: [PATCH 128/154] 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) %}
    - + {% if image %} + + {% endif %} {% if color %} @@ -12,10 +14,10 @@ {% if quantity %} {% endif %} - {% if not config['HIDE_TABLE_MISSING_PARTS'] %} + {% if missing and not config['HIDE_TABLE_MISSING_PARTS'] %} {% endif %} - {% if not config['HIDE_TABLE_DAMAGED_PARTS'] %} + {% if damaged and not config['HIDE_TABLE_DAMAGED_PARTS'] %} {% 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 %} + +
    +
    {{ item.fields.number_of_parts }} {{ item.fields.total_quantity }} {{ item.fields.total_missing }} {{ item.fields.total_damaged }}
    Image Image Name Color Quantity Missing{% if missing_parts %} parts{% endif %} Damaged{% if damaged_parts %} parts{% endif %}
    + {{ 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 %} From 48e4b59344bfa630d7a993fad8e72705a121b462 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Mon, 3 Feb 2025 23:46:05 +0100 Subject: [PATCH 129/154] Make sure COUNT() does not return NULL --- bricktracker/sql/minifigure/list/all.sql | 2 +- bricktracker/sql/minifigure/select/generic.sql | 2 +- bricktracker/sql/part/list/all.sql | 2 +- bricktracker/sql/part/list/problem.sql | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bricktracker/sql/minifigure/list/all.sql b/bricktracker/sql/minifigure/list/all.sql index d0bb6eb..904e818 100644 --- a/bricktracker/sql/minifigure/list/all.sql +++ b/bricktracker/sql/minifigure/list/all.sql @@ -13,7 +13,7 @@ SUM(IFNULL("bricktracker_minifigures"."quantity", 0)) AS "total_quantity", {% endblock %} {% block total_sets %} -COUNT("bricktracker_minifigures"."id") AS "total_sets" +IFNULL(COUNT("bricktracker_minifigures"."id"), 0) AS "total_sets" {% endblock %} {% block join %} diff --git a/bricktracker/sql/minifigure/select/generic.sql b/bricktracker/sql/minifigure/select/generic.sql index b48bfb7..6301550 100644 --- a/bricktracker/sql/minifigure/select/generic.sql +++ b/bricktracker/sql/minifigure/select/generic.sql @@ -13,7 +13,7 @@ SUM(IFNULL("bricktracker_minifigures"."quantity", 0)) AS "total_quantity", {% endblock %} {% block total_sets %} -COUNT(DISTINCT "bricktracker_minifigures"."id") AS "total_sets" +IFNULL(COUNT(DISTINCT "bricktracker_minifigures"."id"), 0) AS "total_sets" {% endblock %} {% block join %} diff --git a/bricktracker/sql/part/list/all.sql b/bricktracker/sql/part/list/all.sql index 77831a6..8bb8dcb 100644 --- a/bricktracker/sql/part/list/all.sql +++ b/bricktracker/sql/part/list/all.sql @@ -13,7 +13,7 @@ SUM("bricktracker_parts"."quantity" * IFNULL("bricktracker_minifigures"."quantit {% endblock %} {% block total_sets %} -COUNT(DISTINCT "bricktracker_parts"."id") AS "total_sets", +IFNULL(COUNT(DISTINCT "bricktracker_parts"."id"), 0) AS "total_sets", {% endblock %} {% block total_minifigures %} diff --git a/bricktracker/sql/part/list/problem.sql b/bricktracker/sql/part/list/problem.sql index dbf411b..068b8d8 100644 --- a/bricktracker/sql/part/list/problem.sql +++ b/bricktracker/sql/part/list/problem.sql @@ -9,7 +9,7 @@ SUM("bricktracker_parts"."damaged") AS "total_damaged", {% endblock %} {% block total_sets %} -COUNT("bricktracker_parts"."id") - COUNT("bricktracker_parts"."figure") AS "total_sets", +IFNULL(COUNT("bricktracker_parts"."id"), 0) - IFNULL(COUNT("bricktracker_parts"."figure"), 0) AS "total_sets", {% endblock %} {% block total_minifigures %} From bd8c52941ad642d196e1c51d8d8c632d0b759a7c Mon Sep 17 00:00:00 2001 From: Gregoo Date: Tue, 4 Feb 2025 08:47:38 +0100 Subject: [PATCH 130/154] Move grid filters and sort to their own files (plus cosmetics) --- templates/set/filter.html | 87 ++++++++++++++++++++++++++++ templates/set/sort.html | 25 ++++++++ templates/sets.html | 116 +------------------------------------- 3 files changed, 115 insertions(+), 113 deletions(-) create mode 100644 templates/set/filter.html create mode 100644 templates/set/sort.html diff --git a/templates/set/filter.html b/templates/set/filter.html new file mode 100644 index 0000000..65c3343 --- /dev/null +++ b/templates/set/filter.html @@ -0,0 +1,87 @@ +
    +
    + +
    + Status + +
    +
    +
    + +
    + Theme + +
    +
    + {% if brickset_owners | length %} +
    + +
    + Owner + +
    +
    + {% endif %} + {% if brickset_storages | length %} +
    + +
    + Storage + +
    +
    + {% endif %} + {% if brickset_tags | length %} +
    + +
    + Tag + +
    +
    + {% endif %} +
    diff --git a/templates/set/sort.html b/templates/set/sort.html new file mode 100644 index 0000000..09f31f0 --- /dev/null +++ b/templates/set/sort.html @@ -0,0 +1,25 @@ +
    +
    +
    + Sort + + + + + + + + + +
    +
    +
    diff --git a/templates/sets.html b/templates/sets.html index 60101b5..5af93c9 100644 --- a/templates/sets.html +++ b/templates/sets.html @@ -9,7 +9,7 @@
    - Search + Search
    @@ -28,118 +28,8 @@
    -
    -
    -
    - Sort - - - - - - - - - -
    -
    -
    -
    -
    - -
    - Status - -
    -
    -
    - -
    - Theme - -
    -
    - {% if brickset_owners | length %} -
    - -
    - Owner - -
    -
    - {% endif %} - {% if brickset_storages | length %} -
    - -
    - Storage - -
    -
    - {% endif %} - {% if brickset_tags | length %} -
    - -
    - Tag - -
    -
    - {% endif %} -
    + {% include 'set/sort.html' %} + {% include 'set/filter.html' %}
    {% for item in collection %}
    From b0c7cd7da57abe03c68ba3116e4a083b830b8f8c Mon Sep 17 00:00:00 2001 From: Gregoo Date: Tue, 4 Feb 2025 09:32:57 +0100 Subject: [PATCH 131/154] Enforce hidden features in the card and grid filters/sort --- templates/macro/badge.html | 8 ++++++-- templates/set/card.html | 12 +++++++++--- templates/set/filter.html | 16 ++++++++++++---- templates/set/sort.html | 4 ++++ 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/templates/macro/badge.html b/templates/macro/badge.html index e1f9071..ea2be58 100644 --- a/templates/macro/badge.html +++ b/templates/macro/badge.html @@ -103,7 +103,9 @@ {% endmacro %} {% macro total_damaged(damaged, solo=false, last=false) %} - {{ badge(check=damaged, solo=solo, last=last, color='danger', icon='error-warning-line', collapsible='Damaged:', text=damaged, alt='Damaged') }} + {% if not config['HIDE_TABLE_DAMAGED_PARTS'] %} + {{ badge(check=damaged, solo=solo, last=last, color='danger', icon='error-warning-line', collapsible='Damaged:', text=damaged, alt='Damaged') }} + {% endif %} {% endmacro %} {% macro total_quantity(quantity, solo=false, last=false) %} @@ -115,7 +117,9 @@ {% endmacro %} {% macro total_missing(missing, solo=false, last=false) %} - {{ badge(check=missing, solo=solo, last=last, color='light text-danger-emphasis bg-danger-subtle border border-danger-subtle', icon='question-line', collapsible='Missing:', text=missing, alt='Missing') }} + {% if not config['HIDE_TABLE_MISSING_PARTS'] %} + {{ badge(check=missing, solo=solo, last=last, color='light text-danger-emphasis bg-danger-subtle border border-danger-subtle', icon='question-line', collapsible='Missing:', text=missing, alt='Missing') }} + {% endif %} {% endmacro %} {% macro total_sets(sets, solo=false, last=false) %} diff --git a/templates/set/card.html b/templates/set/card.html index f47a7ca..2064206 100644 --- a/templates/set/card.html +++ b/templates/set/card.html @@ -7,10 +7,16 @@ {% if not solo and not tiny %} data-index="{{ index }}" data-number="{{ item.fields.set }}" data-name="{{ item.fields.name | lower }}" data-parts="{{ item.fields.number_of_parts }}" data-year="{{ item.fields.year }}" data-theme="{{ item.theme.name | lower }}" - data-has-missing-instructions="{{ (not (item.instructions | length)) | int }}" + {% if not config['HIDE_SET_INSTRUCTIONS'] %} + data-has-missing-instructions="{{ (item.instructions | length == 0) | int }}" + {% endif %} data-has-minifigures="{{ (item.fields.total_minifigures > 0) | int }}" data-minifigures="{{ item.fields.total_minifigures }}" - data-has-missing="{{ (item.fields.total_missing > 0) | int }}" data-missing="{{ item.fields.total_missing }}" - data-has-damaged="{{ (item.fields.total_damaged > 0) | int }}" data-damaged="{{ item.fields.total_damaged }}" + {% if not config['HIDE_TABLE_MISSING_PARTS'] %} + data-has-missing="{{ (item.fields.total_missing > 0) | int }}" data-missing="{{ item.fields.total_missing }}" + {% endif %} + {% if not config['HIDE_TABLE_DAMAGED_PARTS'] %} + data-has-damaged="{{ (item.fields.total_damaged > 0) | int }}" data-damaged="{{ item.fields.total_damaged }}" + {% endif %} data-has-storage="{{ item.fields.storage is not none | int }}" {% if item.fields.storage is not none %} data-storage="{{ item.fields.storage }}" diff --git a/templates/set/filter.html b/templates/set/filter.html index 65c3343..8f3b410 100644 --- a/templates/set/filter.html +++ b/templates/set/filter.html @@ -7,10 +7,18 @@ data-filter="metadata" autocomplete="off"> - - - - + {% if not config['HIDE_TABLE_MISSING_PARTS'] %} + + + {% endif %} + {% if not config['HIDE_TABLE_DAMAGED_PARTS'] %} + + + {% endif %} + {% if not config['HIDE_SET_INSTRUCTIONS'] %} + + + {% endif %} {% if brickset_storages | length %} diff --git a/templates/set/sort.html b/templates/set/sort.html index 09f31f0..3315a1d 100644 --- a/templates/set/sort.html +++ b/templates/set/sort.html @@ -14,10 +14,14 @@ data-sort-attribute="minifigures" data-sort-desc="true"> Figures + {% if not config['HIDE_TABLE_MISSING_PARTS'] %} + {% endif %} + {% if not config['HIDE_TABLE_DAMAGED_PARTS'] %} + {% endif %}
    From 82b744334f577f14ab11dbfea9e9b45a80b73492 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Tue, 4 Feb 2025 10:08:25 +0100 Subject: [PATCH 132/154] Add helper to produce the set metadata lists --- bricktracker/set_list.py | 24 +++++++++++++++++++++++- bricktracker/views/add.py | 16 +++++----------- bricktracker/views/index.py | 9 ++------- bricktracker/views/minifigure.py | 9 ++------- bricktracker/views/part.py | 9 ++------- bricktracker/views/set.py | 10 +++------- bricktracker/views/storage.py | 8 ++------ 7 files changed, 39 insertions(+), 46 deletions(-) diff --git a/bricktracker/set_list.py b/bricktracker/set_list.py index 6c3b928..deaf269 100644 --- a/bricktracker/set_list.py +++ b/bricktracker/set_list.py @@ -1,11 +1,14 @@ -from typing import Any, Self +from typing import Any, Self, Union from flask import current_app from .record_list import BrickRecordList +from .set_owner import BrickSetOwner from .set_owner_list import BrickSetOwnerList from .set_status_list import BrickSetStatusList from .set_storage import BrickSetStorage +from .set_storage_list import BrickSetStorageList +from .set_tag import BrickSetTag from .set_tag_list import BrickSetTagList from .set import BrickSet @@ -163,3 +166,22 @@ class BrickSetList(BrickRecordList[BrickSet]): self.list(override_query=self.using_storage_query) return self + + +# Helper to build the metadata lists +def set_metadata_lists( + as_class: bool = False +) -> dict[ + str, + Union[ + list[BrickSetOwner], + list[BrickSetStorage], + BrickSetStorageList, + list[BrickSetTag] + ] +]: + return { + 'brickset_owners': BrickSetOwnerList.list(), + 'brickset_storages': BrickSetStorageList.list(as_class=as_class), + 'brickset_tags': BrickSetTagList.list(), + } diff --git a/bricktracker/views/add.py b/bricktracker/views/add.py index fb11efe..db4671e 100644 --- a/bricktracker/views/add.py +++ b/bricktracker/views/add.py @@ -3,9 +3,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 ..set_list import set_metadata_lists from ..socket import MESSAGES add_page = Blueprint('add', __name__, url_prefix='/add') @@ -20,12 +18,10 @@ def add() -> str: return render_template( 'add.html', - 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 + messages=MESSAGES, + **set_metadata_lists() ) @@ -38,11 +34,9 @@ def bulk() -> str: return render_template( 'add.html', - 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, - bulk=True + bulk=True, + **set_metadata_lists() ) diff --git a/bricktracker/views/index.py b/bricktracker/views/index.py index b64775b..1bf7c22 100644 --- a/bricktracker/views/index.py +++ b/bricktracker/views/index.py @@ -2,11 +2,8 @@ from flask import Blueprint, render_template 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 +from ..set_list import BrickSetList, set_metadata_lists index_page = Blueprint('index', __name__) @@ -18,9 +15,7 @@ def index() -> str: return render_template( 'index.html', brickset_collection=BrickSetList().last(), - brickset_owners=BrickSetOwnerList.list(), brickset_statuses=BrickSetStatusList.list(), - brickset_storages=BrickSetStorageList.list(as_class=True), - brickset_tags=BrickSetTagList.list(), minifigure_collection=BrickMinifigureList().last(), + **set_metadata_lists(as_class=True) ) diff --git a/bricktracker/views/minifigure.py b/bricktracker/views/minifigure.py index 9958728..7123e4a 100644 --- a/bricktracker/views/minifigure.py +++ b/bricktracker/views/minifigure.py @@ -3,10 +3,7 @@ from flask import Blueprint, render_template from .exceptions import exception_handler from ..minifigure import BrickMinifigure from ..minifigure_list import BrickMinifigureList -from ..set_owner_list import BrickSetOwnerList -from ..set_list import BrickSetList -from ..set_storage_list import BrickSetStorageList -from ..set_tag_list import BrickSetTagList +from ..set_list import BrickSetList, set_metadata_lists minifigure_page = Blueprint('minifigure', __name__, url_prefix='/minifigures') @@ -31,7 +28,5 @@ def details(*, figure: str) -> str: using=BrickSetList().using_minifigure(figure), missing=BrickSetList().missing_minifigure(figure), damaged=BrickSetList().damaged_minifigure(figure), - brickset_owners=BrickSetOwnerList.list(), - brickset_storages=BrickSetStorageList.list(as_class=True), - brickset_tags=BrickSetTagList.list(), + **set_metadata_lists(as_class=True) ) diff --git a/bricktracker/views/part.py b/bricktracker/views/part.py index b2a9eed..fc800c4 100644 --- a/bricktracker/views/part.py +++ b/bricktracker/views/part.py @@ -4,10 +4,7 @@ from .exceptions import exception_handler from ..minifigure_list import BrickMinifigureList from ..part import BrickPart from ..part_list import BrickPartList -from ..set_owner_list import BrickSetOwnerList -from ..set_list import BrickSetList -from ..set_storage_list import BrickSetStorageList -from ..set_tag_list import BrickSetTagList +from ..set_list import BrickSetList, set_metadata_lists part_page = Blueprint('part', __name__, url_prefix='/parts') @@ -67,7 +64,5 @@ def details(*, part: str, color: int) -> str: ), different_color=BrickPartList().with_different_color(brickpart), similar_prints=BrickPartList().from_print(brickpart), - brickset_owners=BrickSetOwnerList.list(), - brickset_storages=BrickSetStorageList.list(as_class=True), - brickset_tags=BrickSetTagList.list(), + **set_metadata_lists(as_class=True) ) diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py index 8983cf9..de57cd3 100644 --- a/bricktracker/views/set.py +++ b/bricktracker/views/set.py @@ -16,7 +16,7 @@ from .exceptions import exception_handler from ..minifigure import BrickMinifigure from ..part import BrickPart from ..set import BrickSet -from ..set_list import BrickSetList +from ..set_list import BrickSetList, set_metadata_lists from ..set_owner_list import BrickSetOwnerList from ..set_status_list import BrickSetStatusList from ..set_storage_list import BrickSetStorageList @@ -35,10 +35,8 @@ def list() -> str: return render_template( 'sets.html', collection=BrickSetList().all(), - brickset_owners=BrickSetOwnerList.list(), brickset_statuses=BrickSetStatusList.list(), - brickset_storages=BrickSetStorageList.list(as_class=True), - brickset_tags=BrickSetTagList.list(), + **set_metadata_lists(as_class=True) ) @@ -145,10 +143,8 @@ def details(*, id: str) -> str: 'set.html', item=BrickSet().select_specific(id), open_instructions=request.args.get('open_instructions'), - brickset_owners=BrickSetOwnerList.list(), brickset_statuses=BrickSetStatusList.list(all=True), - brickset_storages=BrickSetStorageList.list(as_class=True), - brickset_tags=BrickSetTagList.list(), + **set_metadata_lists(as_class=True) ) diff --git a/bricktracker/views/storage.py b/bricktracker/views/storage.py index e41e97a..7d5ba3f 100644 --- a/bricktracker/views/storage.py +++ b/bricktracker/views/storage.py @@ -1,11 +1,9 @@ from flask import Blueprint, render_template from .exceptions import exception_handler -from ..set_owner_list import BrickSetOwnerList -from ..set_list import BrickSetList +from ..set_list import BrickSetList, set_metadata_lists 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') @@ -30,7 +28,5 @@ def details(*, id: str) -> str: 'storage.html', item=storage, sets=BrickSetList().using_storage(storage), - brickset_owners=BrickSetOwnerList.list(), - brickset_storages=BrickSetStorageList.list(as_class=True), - brickset_tags=BrickSetTagList.list(), + **set_metadata_lists(as_class=True) ) From 7ce029029ddbb510fdbede3313371d3fb4c4d1df Mon Sep 17 00:00:00 2001 From: Gregoo Date: Tue, 4 Feb 2025 10:37:43 +0100 Subject: [PATCH 133/154] Properly separate setting state and value for metadata --- bricktracker/metadata.py | 23 ++++++++++--------- bricktracker/set_storage.py | 2 +- .../storage/update/{state.sql => value.sql} | 2 +- bricktracker/views/set.py | 4 ++-- 4 files changed, 16 insertions(+), 15 deletions(-) rename bricktracker/sql/set/metadata/storage/update/{state.sql => value.sql} (79%) diff --git a/bricktracker/metadata.py b/bricktracker/metadata.py index 07545f9..0b2f61c 100644 --- a/bricktracker/metadata.py +++ b/bricktracker/metadata.py @@ -27,6 +27,7 @@ class BrickMetadata(BrickRecord): select_query: str update_field_query: str update_set_state_query: str + update_set_value_query: str def __init__( self, @@ -224,25 +225,25 @@ class BrickMetadata(BrickRecord): /, *, json: Any | None = None, - state: Any | None = None, + value: Any | None = None, ) -> Any: - if state is None and json is not None: - state = json.get('value', '') + if value is None and json is not None: + value = json.get('value', '') - if state == '': - state = None + if value == '': + value = None parameters = self.sql_parameters() parameters['set_id'] = brickset.fields.id - parameters['state'] = state + parameters['value'] = value rows, _ = BrickSQL().execute_and_commit( - self.update_set_state_query, + self.update_set_value_query, parameters=parameters, ) # Update the status - if state is None and not hasattr(self.fields, 'name'): + if value is None and not hasattr(self.fields, 'name'): self.fields.name = 'None' if rows != 1: @@ -253,12 +254,12 @@ class BrickMetadata(BrickRecord): )) # Info - logger.info('{kind} value changed to "{name}" ({state}) for set {set} ({id})'.format( # noqa: E501 + logger.info('{kind} value changed to "{name}" ({value}) for set {set} ({id})'.format( # noqa: E501 kind=self.kind, name=self.fields.name, - state=state, + value=value, set=brickset.fields.set, id=brickset.fields.id, )) - return state + return value diff --git a/bricktracker/set_storage.py b/bricktracker/set_storage.py index 30c559c..08c2429 100644 --- a/bricktracker/set_storage.py +++ b/bricktracker/set_storage.py @@ -12,7 +12,7 @@ class BrickSetStorage(BrickMetadata): 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' + update_set_value_query: str = 'set/metadata/storage/update/value' # Self url def url(self, /) -> str: diff --git a/bricktracker/sql/set/metadata/storage/update/state.sql b/bricktracker/sql/set/metadata/storage/update/value.sql similarity index 79% rename from bricktracker/sql/set/metadata/storage/update/state.sql rename to bricktracker/sql/set/metadata/storage/update/value.sql index 7cc40d6..b758f08 100644 --- a/bricktracker/sql/set/metadata/storage/update/state.sql +++ b/bricktracker/sql/set/metadata/storage/update/value.sql @@ -1,3 +1,3 @@ UPDATE "bricktracker_sets" -SET "storage" = :state +SET "storage" = :value WHERE "bricktracker_sets"."id" IS NOT DISTINCT FROM :set_id diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py index de57cd3..66a2a50 100644 --- a/bricktracker/views/set.py +++ b/bricktracker/views/set.py @@ -77,9 +77,9 @@ def update_storage(*, id: str) -> Response: allow_none=True ) - state = storage.update_set_value(brickset, state=storage.fields.id) + value = storage.update_set_value(brickset, value=storage.fields.id) - return jsonify({'value': state}) + return jsonify({'value': value}) # Change the state of a tag From 3d660c594be621a1b3b24ed5a8310b39bdde7b7f Mon Sep 17 00:00:00 2001 From: Gregoo Date: Tue, 4 Feb 2025 10:47:22 +0100 Subject: [PATCH 134/154] Make instructions failsafe in the admin --- bricktracker/views/admin/admin.py | 5 +++- templates/admin/instructions.html | 40 ++++++++++++++++--------------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/bricktracker/views/admin/admin.py b/bricktracker/views/admin/admin.py index 584a359..bb6a5e6 100644 --- a/bricktracker/views/admin/admin.py +++ b/bricktracker/views/admin/admin.py @@ -34,6 +34,7 @@ def admin() -> str: database_exception: Exception | None = None database_upgrade_needed: bool = False database_version: int = -1 + instructions: BrickInstructionsList | None = None metadata_owners: list[BrickSetOwner] = [] metadata_statuses: list[BrickSetStatus] = [] metadata_storages: list[BrickSetStorage] = [] @@ -50,6 +51,8 @@ def admin() -> str: database_version = database.version database_counters = BrickSQL().count_records() + instructions = BrickInstructionsList() + metadata_owners = BrickSetOwnerList.list() metadata_statuses = BrickSetStatusList.list(all=True) metadata_storages = BrickSetStorageList.list() @@ -104,7 +107,7 @@ def admin() -> str: database_exception=database_exception, database_upgrade_needed=database_upgrade_needed, database_version=database_version, - instructions=BrickInstructionsList(), + instructions=instructions, metadata_owners=metadata_owners, metadata_statuses=metadata_statuses, metadata_storages=metadata_storages, diff --git a/templates/admin/instructions.html b/templates/admin/instructions.html index 99fbe5e..997348c 100644 --- a/templates/admin/instructions.html +++ b/templates/admin/instructions.html @@ -6,25 +6,27 @@ The instructions files folder is: {{ config['INSTRUCTIONS_FOLDER'] }}.
    Allowed file formats for instructions are the following: {{ ', '.join(config['INSTRUCTIONS_ALLOWED_EXTENSIONS']) }}.

    -
    Counters
    -

    -

    -
      -
    • - Sets {{ instructions.sets | length }} -
    • -
    • - Instructions for sets {{ instructions.sets_total }} -
    • -
    • - Unknown {{ instructions.unknown_total }} -
    • -
    • - Rejected files {{ instructions.rejected_total }} -
    • -
    -
    -

    +{% if instructions %} +
    Counters
    +

    +

    +
      +
    • + Sets {{ instructions.sets | length }} +
    • +
    • + Instructions for sets {{ instructions.sets_total }} +
    • +
    • + Unknown {{ instructions.unknown_total }} +
    • +
    • + Rejected files {{ instructions.rejected_total }} +
    • +
    +
    +

    +{% endif %}
    Refresh

    Refresh the instructions cache From 584389e2051557392b55ad00358adb7f1a203e34 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Tue, 4 Feb 2025 10:55:59 +0100 Subject: [PATCH 135/154] Typo --- .env.sample | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.sample b/.env.sample index d14491e..44d52fe 100644 --- a/.env.sample +++ b/.env.sample @@ -263,7 +263,7 @@ # 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 +# BK_STORAGE_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 From 16e4c28516346ad3f1246417f56d289abe8bec56 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Tue, 4 Feb 2025 12:34:19 +0100 Subject: [PATCH 136/154] Continue separation of state and value --- bricktracker/metadata_list.py | 11 ++++++++++- bricktracker/set_storage_list.py | 4 ++-- bricktracker/views/set.py | 2 +- templates/macro/form.html | 2 +- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/bricktracker/metadata_list.py b/bricktracker/metadata_list.py index 60bfb5f..3b98a13 100644 --- a/bricktracker/metadata_list.py +++ b/bricktracker/metadata_list.py @@ -28,8 +28,9 @@ class BrickMetadataList(BrickRecordList[T]): # Queries select_query: str - # Set state endpoint + # Set endpoints set_state_endpoint: str + set_value_endpoint: str def __init__( self, @@ -149,3 +150,11 @@ class BrickMetadataList(BrickRecordList[T]): cls.set_state_endpoint, id=id, ) + + # URL to change the selected value of this metadata item for a set + @classmethod + def url_for_set_value(cls, id: str, /) -> str: + return url_for( + cls.set_value_endpoint, + id=id, + ) diff --git a/bricktracker/set_storage_list.py b/bricktracker/set_storage_list.py index 8453f36..77be716 100644 --- a/bricktracker/set_storage_list.py +++ b/bricktracker/set_storage_list.py @@ -17,8 +17,8 @@ class BrickSetStorageList(BrickMetadataList[BrickSetStorage]): select_query = 'set/metadata/storage/list' all_query = 'set/metadata/storage/all' - # Set state endpoint - set_state_endpoint: str = 'set.update_storage' + # Set value endpoint + set_value_endpoint: str = 'set.update_storage' # Load all storages @classmethod diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py index 66a2a50..7f397da 100644 --- a/bricktracker/views/set.py +++ b/bricktracker/views/set.py @@ -66,7 +66,7 @@ def update_status(*, id: str, metadata_id: str) -> Response: return jsonify({'value': state}) -# Change the state of a storage +# Change the value of storage @set_page.route('//storage', methods=['POST']) @login_required @exception_handler(__file__, json=True) diff --git a/templates/macro/form.html b/templates/macro/form.html index 6fb3890..45ec2b5 100644 --- a/templates/macro/form.html +++ b/templates/macro/form.html @@ -48,7 +48,7 @@ {% if icon %}{% endif %} + + {% for purchase_location in brickset_purchase_locations %} + + {% endfor %} + +

    + {{ accordion.footer() }} + {% endif %} {% if brickset_storages | length %} {{ accordion.header('Storage', 'storage', 'metadata', icon='archive-2-line') }} - +
    + +
    +
    +
    + Delete +
    + + + {% endfor %} + {% else %} +
  • No purchase location found.
  • + {% endif %} +
  • +
    +
    + +
    +
    Name
    + +
    +
    +
    + +
    +
    +
  • + +{{ accordion.footer() }} diff --git a/templates/admin/purchase_location/delete.html b/templates/admin/purchase_location/delete.html new file mode 100644 index 0000000..c53d8c2 --- /dev/null +++ b/templates/admin/purchase_location/delete.html @@ -0,0 +1,19 @@ +{% import 'macro/accordion.html' as accordion %} + +{{ accordion.header('Set purchase locations danger zone', 'purchase-location-danger', 'admin', expanded=true, danger=true, class='text-end') }} +
    + {% if purchase_location_error %}{% endif %} + +
    +
    +
    +
    Name
    + +
    +
    +
    +
    + Back to the admin + +
    +{{ accordion.footer() }} diff --git a/templates/macro/badge.html b/templates/macro/badge.html index ea2be58..f1c9f89 100644 --- a/templates/macro/badge.html +++ b/templates/macro/badge.html @@ -65,6 +65,18 @@ {% endif %} {% endmacro %} +{% macro purchase_location(item, purchase_locations, solo=false, last=false) %} + {% if purchase_locations and item.fields.purchase_location in purchase_locations.mapping %} + {% set purchase_location = purchase_locations.mapping[item.fields.purchase_location] %} + {% if last %} + {% set tooltip=purchase_location.fields.name %} + {% else %} + {% set text=purchase_location.fields.name %} + {% endif %} + {{ badge(check=purchase_location, solo=solo, last=last, color='light border', icon='building-line', text=text, tooltip=tooltip) }} + {% endif %} +{% endmacro %} + {% macro set(set, solo=false, last=false, url=None, id=None) %} {% if id %} {% set url=url_for('set.details', id=id) %} diff --git a/templates/set/card.html b/templates/set/card.html index 2064206..7c2525d 100644 --- a/templates/set/card.html +++ b/templates/set/card.html @@ -17,6 +17,11 @@ {% if not config['HIDE_TABLE_DAMAGED_PARTS'] %} data-has-damaged="{{ (item.fields.total_damaged > 0) | int }}" data-damaged="{{ item.fields.total_damaged }}" {% endif %} + data-has-purchase-location="{{ item.fields.purchase_location is not none | int }}" + {% if item.fields.purchase_location is not none %} + data-purchase-location="{{ item.fields.purchase_location }}" + {% if item.fields.purchase_location in brickset_purchase_locations.mapping %}data-search-purchase-location="{{ brickset_purchase_locations.mapping[item.fields.purchase_location].fields.name | lower }}"{% endif %} + {% endif %} data-has-storage="{{ item.fields.storage is not none | int }}" {% if item.fields.storage is not none %} data-storage="{{ item.fields.storage }}" @@ -63,6 +68,7 @@ {{ badge.owner(item, owner, solo=solo, last=last) }} {% endfor %} {{ badge.storage(item, brickset_storages, solo=solo, last=last) }} + {{ badge.purchase_location(item, brickset_purchase_locations, solo=solo, last=last) }} {% if not last %} {% if not solo %} {{ badge.instructions(item, solo=solo, last=last) }} diff --git a/templates/set/filter.html b/templates/set/filter.html index 8f3b410..24454c2 100644 --- a/templates/set/filter.html +++ b/templates/set/filter.html @@ -60,6 +60,22 @@
    {% endif %} + {% if brickset_purchase_locations | length %} +
    + +
    + Purchase location + +
    +
    + {% endif %} {% if brickset_storages | length %}
    diff --git a/templates/set/management.html b/templates/set/management.html index 1a8c2b7..8d675f9 100644 --- a/templates/set/management.html +++ b/templates/set/management.html @@ -1,44 +1,53 @@ {% if g.login.is_authenticated() %} {{ accordion.header('Management', 'set-management', 'set-details', icon='settings-4-line', class='p-0') }} {{ accordion.header('Owners', 'owner', 'set-management', icon='group-line', class='p-0') }} - - - {{ accordion.footer() }} +
    + Manage the set purchase locations + {{ accordion.footer() }} {{ accordion.header('Storage', 'storage', 'set-management', icon='archive-2-line') }} - {% if brickset_storages | length %} - {{ form.select('Storage', item, 'storage', brickset_storages, delete=delete) }} - {% else %} -

    No storage found.

    - {% endif %} -
    - Manage the set storages - {{ accordion.footer() }} - {{ accordion.header('Tags', 'tag', 'set-management', icon='price-tag-2-line', class='p-0') }} - - - {{ accordion.footer() }} +
    + Manage the set storages + {{ accordion.footer() }} + {{ accordion.header('Tags', 'tag', 'set-management', icon='price-tag-2-line', class='p-0') }} + + + {{ accordion.footer() }} {{ accordion.header('Data', 'data', 'set-management', icon='database-2-line') }} - Refresh the set data + Refresh the set data {{ accordion.footer() }} {{ accordion.footer() }} {% endif %} diff --git a/templates/sets.html b/templates/sets.html index 5af93c9..4832160 100644 --- a/templates/sets.html +++ b/templates/sets.html @@ -10,7 +10,7 @@
    Search - +
    From f0cec23da9566f044bd8435d5baeebe36b4901db Mon Sep 17 00:00:00 2001 From: Gregoo Date: Tue, 4 Feb 2025 17:03:39 +0100 Subject: [PATCH 140/154] Set purchase date and price --- .env.sample | 9 ++ CHANGELOG.md | 6 +- bricktracker/config.py | 2 + bricktracker/set.py | 111 +++++++++++++++++- bricktracker/sql/migrations/0007.sql | 2 +- bricktracker/sql/set/base/base.sql | 2 + bricktracker/sql/set/update/purchase_date.sql | 3 + .../sql/set/update/purchase_price.sql | 3 + bricktracker/views/set.py | 24 ++++ docs/development.md | 1 + static/scripts/changer.js | 17 ++- templates/base.html | 2 + templates/macro/badge.html | 20 +++- templates/macro/form.html | 7 +- templates/set/card.html | 6 +- templates/set/management.html | 25 ++-- templates/set/sort.html | 4 + 17 files changed, 228 insertions(+), 16 deletions(-) create mode 100644 bricktracker/sql/set/update/purchase_date.sql create mode 100644 bricktracker/sql/set/update/purchase_price.sql diff --git a/.env.sample b/.env.sample index 8f12939..f4d6a48 100644 --- a/.env.sample +++ b/.env.sample @@ -168,6 +168,15 @@ # Default: 3333 # BK_PORT=3333 +# Optional: Format of the timestamp for purchase dates +# Check https://docs.python.org/3/library/time.html#time.strftime for format details +# Default: %d/%m/%Y +# BK_PURCHASE_DATE_FORMAT=%m/%d/%Y + +# Optional: Currency to display for purchase prices. +# Default: € +# BK_PURCHASE_CURRENCY=£ + # Optional: Change the default order of purchase locations. By default ordered by insertion order. # Useful column names for this option are: # - "bricktracker_metadata_purchase_locations"."name" ASC: storage name diff --git a/CHANGELOG.md b/CHANGELOG.md index efcdb16..2bbb26c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ - Added: `BK_HIDE_ALL_STORAGES`, hide the "Storages" menu entry - Added: `BK_STORAGE_DEFAULT_ORDER`, ordering of storages - Added: `BK_PURCHASE_LOCATION_DEFAULT_ORDER`, ordering of purchase locations +- Added: `BK_PURCHASE_CURRENCY`, currency to display for purchase prices +- Added: `BK_PURCHASE_DATE_FORMAT`, date format for purchase dates ### Code @@ -40,7 +42,7 @@ - Ownership - Tags - Storage - - Purchase location + - Purchase location, date, price - Storage - Storage content and list @@ -87,7 +89,7 @@ - Tags - Refresh - Storage - - Purchase location + - Purchase location, date, price - Sets grid - Collapsible controls depending on screen size diff --git a/bricktracker/config.py b/bricktracker/config.py index 729049a..5452f93 100644 --- a/bricktracker/config.py +++ b/bricktracker/config.py @@ -41,6 +41,8 @@ CONFIG: Final[list[dict[str, Any]]] = [ {'n': 'PARTS_DEFAULT_ORDER', 'd': '"rebrickable_parts"."name" ASC, "rebrickable_parts"."color_name" ASC, "bricktracker_parts"."spare" ASC'}, # noqa: E501 {'n': 'PARTS_FOLDER', 'd': 'parts', 's': True}, {'n': 'PORT', 'd': 3333, 'c': int}, + {'n': 'PURCHASE_DATE_FORMAT', 'd': '%d/%m/%Y'}, + {'n': 'PURCHASE_CURRENCY', 'd': '€'}, {'n': 'PURCHASE_LOCATION_DEFAULT_ORDER', 'd': '"bricktracker_metadata_purchase_locations"."name" ASC'}, # noqa: E501 {'n': 'RANDOM', 'e': 'RANDOM', 'c': bool}, {'n': 'REBRICKABLE_API_KEY', 'e': 'REBRICKABLE_API_KEY', 'd': ''}, diff --git a/bricktracker/set.py b/bricktracker/set.py index fb97209..c397b13 100644 --- a/bricktracker/set.py +++ b/bricktracker/set.py @@ -1,3 +1,4 @@ +from datetime import datetime import logging import traceback from typing import Any, Self, TYPE_CHECKING @@ -5,7 +6,7 @@ from uuid import uuid4 from flask import current_app, url_for -from .exceptions import NotFoundException +from .exceptions import NotFoundException, DatabaseException, ErrorException from .minifigure_list import BrickMinifigureList from .part_list import BrickPartList from .rebrickable_set import RebrickableSet @@ -27,6 +28,8 @@ class BrickSet(RebrickableSet): select_query: str = 'set/select/full' light_query: str = 'set/select/light' insert_query: str = 'set/insert' + update_purchase_date_query: str = 'set/update/purchase_date' + update_purchase_price_query: str = 'set/update/purchase_price' # Delete a set def delete(self, /) -> None: @@ -152,6 +155,30 @@ class BrickSet(RebrickableSet): return True + # Purchase date + def purchase_date(self, /, *, standard: bool = False) -> str: + if self.fields.purchase_date is not None: + time = datetime.fromtimestamp(self.fields.purchase_date) + + if standard: + return time.strftime('%Y/%m/%d') + else: + return time.strftime( + current_app.config['PURCHASE_DATE_FORMAT'] + ) + else: + return '' + + # Purchase price with currency + def purchase_price(self, /) -> str: + if self.fields.purchase_price is not None: + return '{price}{currency}'.format( + price=self.fields.purchase_price, + currency=current_app.config['PURCHASE_CURRENCY'] + ) + else: + return '' + # Minifigures def minifigures(self, /) -> BrickMinifigureList: return BrickMinifigureList().from_set(self) @@ -194,6 +221,80 @@ class BrickSet(RebrickableSet): return self + # Update the purchase date + def update_purchase_date(self, json: Any | None, /) -> Any: + value = json.get('value', None) # type: ignore + + try: + if value == '': + value = None + + if value is not None: + value = datetime.strptime(value, '%Y/%m/%d').timestamp() + except Exception: + raise ErrorException('{value} is not a date'.format( + value=value, + )) + + self.fields.purchase_date = value + + rows, _ = BrickSQL().execute_and_commit( + self.update_purchase_date_query, + parameters=self.sql_parameters() + ) + + if rows != 1: + raise DatabaseException('Could not update the purchase date for set {set} ({id})'.format( # noqa: E501 + set=self.fields.set, + id=self.fields.id, + )) + + # Info + logger.info('Purchase date changed to "{value}" for set {set} ({id})'.format( # noqa: E501 + value=value, + set=self.fields.set, + id=self.fields.id, + )) + + return value + + # Update the purchase price + def update_purchase_price(self, json: Any | None, /) -> Any: + value = json.get('value', None) # type: ignore + + try: + if value == '': + value = None + + if value is not None: + value = float(value) + except Exception: + raise ErrorException('{value} is not a number or empty'.format( + value=value, + )) + + self.fields.purchase_price = value + + rows, _ = BrickSQL().execute_and_commit( + self.update_purchase_price_query, + parameters=self.sql_parameters() + ) + + if rows != 1: + raise DatabaseException('Could not update the purchase price for set {set} ({id})'.format( # noqa: E501 + set=self.fields.set, + id=self.fields.id, + )) + + # Info + logger.info('Purchase price changed to "{value}" for set {set} ({id})'.format( # noqa: E501 + value=value, + set=self.fields.set, + id=self.fields.id, + )) + + return value + # Self url def url(self, /) -> str: return url_for('set.details', id=self.fields.id) @@ -230,3 +331,11 @@ class BrickSet(RebrickableSet): return url_for('storage.details', id=self.fields.storage) else: return '' + + # Update purchase date url + def url_for_purchase_date(self, /) -> str: + return url_for('set.update_purchase_date', id=self.fields.id) + + # Update purchase price url + def url_for_purchase_price(self, /) -> str: + return url_for('set.update_purchase_price', id=self.fields.id) diff --git a/bricktracker/sql/migrations/0007.sql b/bricktracker/sql/migrations/0007.sql index 7d52d33..720be76 100644 --- a/bricktracker/sql/migrations/0007.sql +++ b/bricktracker/sql/migrations/0007.sql @@ -27,7 +27,7 @@ CREATE TABLE "bricktracker_sets" ( "set" TEXT NOT NULL, "description" TEXT, "storage" TEXT, -- Storage bin location - "purchase_date" INTEGER, -- Purchase data + "purchase_date" REAL, -- Purchase data "purchase_location" TEXT, -- Purchase location "purchase_price" REAL, -- Purchase price PRIMARY KEY("id"), diff --git a/bricktracker/sql/set/base/base.sql b/bricktracker/sql/set/base/base.sql index 333868d..02ef771 100644 --- a/bricktracker/sql/set/base/base.sql +++ b/bricktracker/sql/set/base/base.sql @@ -1,7 +1,9 @@ SELECT {% block id %}{% endblock %} "bricktracker_sets"."storage", + "bricktracker_sets"."purchase_date", "bricktracker_sets"."purchase_location", + "bricktracker_sets"."purchase_price", "rebrickable_sets"."set", "rebrickable_sets"."number", "rebrickable_sets"."version", diff --git a/bricktracker/sql/set/update/purchase_date.sql b/bricktracker/sql/set/update/purchase_date.sql new file mode 100644 index 0000000..cb8516e --- /dev/null +++ b/bricktracker/sql/set/update/purchase_date.sql @@ -0,0 +1,3 @@ +UPDATE "bricktracker_sets" +SET "purchase_date" = :purchase_date +WHERE "bricktracker_sets"."id" IS NOT DISTINCT FROM :id diff --git a/bricktracker/sql/set/update/purchase_price.sql b/bricktracker/sql/set/update/purchase_price.sql new file mode 100644 index 0000000..f742a8f --- /dev/null +++ b/bricktracker/sql/set/update/purchase_price.sql @@ -0,0 +1,3 @@ +UPDATE "bricktracker_sets" +SET "purchase_price" = :purchase_price +WHERE "bricktracker_sets"."id" IS NOT DISTINCT FROM :id diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py index 0777f4e..f1493b8 100644 --- a/bricktracker/views/set.py +++ b/bricktracker/views/set.py @@ -41,6 +41,18 @@ def list() -> str: ) +# Change the value of purchase date +@set_page.route('//purchase_date', methods=['POST']) +@login_required +@exception_handler(__file__, json=True) +def update_purchase_date(*, id: str) -> Response: + brickset = BrickSet().select_light(id) + + value = brickset.update_purchase_date(request.json) + + return jsonify({'value': value}) + + # Change the value of purchase location @set_page.route('//purchase_location', methods=['POST']) @login_required @@ -60,6 +72,18 @@ def update_purchase_location(*, id: str) -> Response: return jsonify({'value': value}) +# Change the value of purchase price +@set_page.route('//purchase_price', methods=['POST']) +@login_required +@exception_handler(__file__, json=True) +def update_purchase_price(*, id: str) -> Response: + brickset = BrickSet().select_light(id) + + value = brickset.update_purchase_price(request.json) + + return jsonify({'value': value}) + + # Change the state of a owner @set_page.route('//owner/', methods=['POST']) @login_required diff --git a/docs/development.md b/docs/development.md index 8657590..dd3beee 100644 --- a/docs/development.md +++ b/docs/development.md @@ -21,6 +21,7 @@ It also uses the following libraries and frameworks: - `tinysort` (https://github.com/Sjeiti/TinySort) - `sortable` (https://github.com/tofsjonas/sortable) - `simple-datatables` (https://github.com/fiduswriter/simple-datatables) +- `vanillajs-datepicker` (https://github.com/mymth/vanillajs-datepicker) The BrickTracker brick logo is part of the Small n' Flat Icons set designed by [Arnaud Chesne](https://iconduck.com/designers/arnaud-chesne). diff --git a/static/scripts/changer.js b/static/scripts/changer.js index 4db9cd7..ffa41ac 100644 --- a/static/scripts/changer.js +++ b/static/scripts/changer.js @@ -1,5 +1,6 @@ // Generic state changer with visual feedback -// Tooltips require boostrap.Tooltip +// Tooltips requires boostrap.Tooltip +// Date requires vanillajs-datepicker class BrickChanger { constructor(prefix, id, url, parent = undefined) { this.prefix = prefix @@ -51,6 +52,20 @@ class BrickChanger { changer.change(); })(this)); } + + // Date picker + this.picker = undefined; + if (this.html_element.dataset.changerDate == "true") { + this.picker = new Datepicker(this.html_element, { + buttonClass: 'btn', + format: 'yyyy/mm/dd', + }); + + // Picker fires a custom "changeDate" event + this.html_element.addEventListener("changeDate", ((changer) => (e) => { + changer.change(); + })(this)); + } } // Clean the status diff --git a/templates/base.html b/templates/base.html index a82a4ea..658ef37 100644 --- a/templates/base.html +++ b/templates/base.html @@ -9,6 +9,7 @@ + @@ -78,6 +79,7 @@ + diff --git a/templates/macro/badge.html b/templates/macro/badge.html index f1c9f89..ee68a69 100644 --- a/templates/macro/badge.html +++ b/templates/macro/badge.html @@ -65,6 +65,15 @@ {% endif %} {% endmacro %} +{% macro purchase_date(date, solo=false, last=false) %} + {% if last %} + {% set tooltip=date %} + {% else %} + {% set text=date %} + {% endif %} + {{ badge(check=date, solo=solo, last=last, color='light border', icon='calendar-line', text=text, tooltip=tooltip, collapsible='Date:') }} +{% endmacro %} + {% macro purchase_location(item, purchase_locations, solo=false, last=false) %} {% if purchase_locations and item.fields.purchase_location in purchase_locations.mapping %} {% set purchase_location = purchase_locations.mapping[item.fields.purchase_location] %} @@ -73,10 +82,19 @@ {% else %} {% set text=purchase_location.fields.name %} {% endif %} - {{ badge(check=purchase_location, solo=solo, last=last, color='light border', icon='building-line', text=text, tooltip=tooltip) }} + {{ badge(check=purchase_location, solo=solo, last=last, color='light border', icon='building-line', text=text, tooltip=tooltip, collapsible='Location:') }} {% endif %} {% endmacro %} +{% macro purchase_price(price, solo=false, last=false) %} + {% if last %} + {% set tooltip=price %} + {% else %} + {% set text=price %} + {% endif %} + {{ badge(check=price, solo=solo, last=last, color='light border', icon='wallet-3-line', text=text, tooltip=tooltip, collapsible='Price:') }} +{% endmacro %} + {% macro set(set, solo=false, last=false, url=None, id=None) %} {% if id %} {% set url=url_for('set.details', id=id) %} diff --git a/templates/macro/form.html b/templates/macro/form.html index 45ec2b5..9564f35 100644 --- a/templates/macro/form.html +++ b/templates/macro/form.html @@ -17,19 +17,22 @@ {% endif %} {% endmacro %} -{% macro input(name, id, prefix, url, value, all=none, read_only=none) %} +{% macro input(name, id, prefix, url, value, all=none, read_only=none, icon=none, suffix=none, date=false) %} {% if all or read_only %} {{ value }} {% else %}
    + {% if icon %} {{ name }}{% endif %} + {% if suffix %}{{ suffix }}{% endif %} {% if g.login.is_authenticated() %} @@ -45,7 +48,7 @@ {% set prefix=metadata_list.as_prefix() %}
    - {% if icon %}{% endif %} + {% if icon %} {{ name }}{% endif %} - {% if nullable %}{% endif %} + {% if nullable %}{% endif %} {% for metadata in metadata_list %} - + {% endfor %} - - + +
    {% endif %} {% endmacro %} diff --git a/templates/set/management.html b/templates/set/management.html index 744c961..ade6729 100644 --- a/templates/set/management.html +++ b/templates/set/management.html @@ -25,7 +25,7 @@
    {% if brickset_purchase_locations | length %} - {{ form.select('Location', item, 'purchase_location', brickset_purchase_locations, icon='building-line', delete=delete) }} + {{ form.select('Location', item.fields.id, brickset_purchase_locations.as_prefix(), brickset_purchase_locations.url_for_set_value(item.fields.id), item.fields.purchase_location, brickset_purchase_locations, icon='building-line', delete=delete) }} {% else %} No purchase location found. {% endif %} @@ -36,7 +36,7 @@ {{ accordion.footer() }} {{ accordion.header('Storage', 'storage', 'set-management', icon='archive-2-line') }} {% if brickset_storages | length %} - {{ form.select('Storage', item, 'storage', brickset_storages, delete=delete) }} + {{ form.select('Storage', item.fields.id, brickset_storages.as_prefix(), brickset_storages.url_for_set_value(item.fields.id), item.fields.storage, brickset_storages, icon='building-line', delete=delete) }} {% else %}

    No storage found.

    {% endif %} From e022a6bc1ef32d10866ffa24c064e828e14eaac9 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Tue, 4 Feb 2025 19:06:36 +0100 Subject: [PATCH 146/154] Remove unused logging --- bricktracker/rebrickable_minifigure.py | 3 --- bricktracker/rebrickable_part.py | 3 --- bricktracker/set_owner_list.py | 3 --- bricktracker/set_purchase_location_list.py | 3 --- bricktracker/set_status_list.py | 3 --- bricktracker/set_storage_list.py | 3 --- bricktracker/set_tag_list.py | 3 --- bricktracker/views/admin/image.py | 4 ---- bricktracker/views/admin/instructions.py | 4 ---- bricktracker/views/admin/retired.py | 4 ---- bricktracker/views/admin/theme.py | 4 ---- bricktracker/views/exceptions.py | 3 --- 12 files changed, 40 deletions(-) diff --git a/bricktracker/rebrickable_minifigure.py b/bricktracker/rebrickable_minifigure.py index 30d61ee..0ef7d43 100644 --- a/bricktracker/rebrickable_minifigure.py +++ b/bricktracker/rebrickable_minifigure.py @@ -1,4 +1,3 @@ -import logging from sqlite3 import Row from typing import Any, TYPE_CHECKING @@ -10,8 +9,6 @@ from .record import BrickRecord if TYPE_CHECKING: from .set import BrickSet -logger = logging.getLogger(__name__) - # A minifigure from Rebrickable class RebrickableMinifigure(BrickRecord): diff --git a/bricktracker/rebrickable_part.py b/bricktracker/rebrickable_part.py index 8fdd3cb..ae34b3a 100644 --- a/bricktracker/rebrickable_part.py +++ b/bricktracker/rebrickable_part.py @@ -1,5 +1,4 @@ import os -import logging from sqlite3 import Row from typing import Any, TYPE_CHECKING from urllib.parse import urlparse @@ -14,8 +13,6 @@ if TYPE_CHECKING: from .set import BrickSet from .socket import BrickSocket -logger = logging.getLogger(__name__) - # A part from Rebrickable class RebrickablePart(BrickRecord): diff --git a/bricktracker/set_owner_list.py b/bricktracker/set_owner_list.py index 74219a7..ec2af9d 100644 --- a/bricktracker/set_owner_list.py +++ b/bricktracker/set_owner_list.py @@ -1,11 +1,8 @@ -import logging from typing import Self from .metadata_list import BrickMetadataList from .set_owner import BrickSetOwner -logger = logging.getLogger(__name__) - # Lego sets owner list class BrickSetOwnerList(BrickMetadataList[BrickSetOwner]): diff --git a/bricktracker/set_purchase_location_list.py b/bricktracker/set_purchase_location_list.py index d49a1eb..65e5f1b 100644 --- a/bricktracker/set_purchase_location_list.py +++ b/bricktracker/set_purchase_location_list.py @@ -1,4 +1,3 @@ -import logging from typing import Self from flask import current_app @@ -6,8 +5,6 @@ from flask import current_app from .metadata_list import BrickMetadataList from .set_purchase_location import BrickSetPurchaseLocation -logger = logging.getLogger(__name__) - # Lego sets purchase location list class BrickSetPurchaseLocationList( diff --git a/bricktracker/set_status_list.py b/bricktracker/set_status_list.py index c40731c..ff4603b 100644 --- a/bricktracker/set_status_list.py +++ b/bricktracker/set_status_list.py @@ -1,11 +1,8 @@ -import logging from typing import Self from .metadata_list import BrickMetadataList from .set_status import BrickSetStatus -logger = logging.getLogger(__name__) - # Lego sets status list class BrickSetStatusList(BrickMetadataList[BrickSetStatus]): diff --git a/bricktracker/set_storage_list.py b/bricktracker/set_storage_list.py index 7e62333..7cd9e13 100644 --- a/bricktracker/set_storage_list.py +++ b/bricktracker/set_storage_list.py @@ -1,4 +1,3 @@ -import logging from typing import Self from flask import current_app @@ -6,8 +5,6 @@ from flask import current_app from .metadata_list import BrickMetadataList from .set_storage import BrickSetStorage -logger = logging.getLogger(__name__) - # Lego sets storage list class BrickSetStorageList(BrickMetadataList[BrickSetStorage]): diff --git a/bricktracker/set_tag_list.py b/bricktracker/set_tag_list.py index 9ed0d91..93817ba 100644 --- a/bricktracker/set_tag_list.py +++ b/bricktracker/set_tag_list.py @@ -1,11 +1,8 @@ -import logging from typing import Self from .metadata_list import BrickMetadataList from .set_tag import BrickSetTag -logger = logging.getLogger(__name__) - # Lego sets tag list class BrickSetTagList(BrickMetadataList[BrickSetTag]): diff --git a/bricktracker/views/admin/image.py b/bricktracker/views/admin/image.py index 30dce28..85b995d 100644 --- a/bricktracker/views/admin/image.py +++ b/bricktracker/views/admin/image.py @@ -1,5 +1,3 @@ -import logging - from flask import Blueprint, redirect, url_for from flask_login import login_required from werkzeug.wrappers.response import Response @@ -10,8 +8,6 @@ from ...part import BrickPart from ...rebrickable_image import RebrickableImage from ...set import BrickSet -logger = logging.getLogger(__name__) - admin_image_page = Blueprint( 'admin_image', __name__, diff --git a/bricktracker/views/admin/instructions.py b/bricktracker/views/admin/instructions.py index 354782d..90ac201 100644 --- a/bricktracker/views/admin/instructions.py +++ b/bricktracker/views/admin/instructions.py @@ -1,5 +1,3 @@ -import logging - from flask import Blueprint, redirect, url_for from flask_login import login_required from werkzeug.wrappers.response import Response @@ -7,8 +5,6 @@ from werkzeug.wrappers.response import Response from ..exceptions import exception_handler from ...instructions_list import BrickInstructionsList -logger = logging.getLogger(__name__) - admin_instructions_page = Blueprint( 'admin_instructions', __name__, diff --git a/bricktracker/views/admin/retired.py b/bricktracker/views/admin/retired.py index c3aa2f2..17ae4f0 100644 --- a/bricktracker/views/admin/retired.py +++ b/bricktracker/views/admin/retired.py @@ -1,5 +1,3 @@ -import logging - from flask import Blueprint, redirect, url_for from flask_login import login_required from werkzeug.wrappers.response import Response @@ -7,8 +5,6 @@ from werkzeug.wrappers.response import Response from ..exceptions import exception_handler from ...retired_list import BrickRetiredList -logger = logging.getLogger(__name__) - admin_retired_page = Blueprint( 'admin_retired', __name__, diff --git a/bricktracker/views/admin/theme.py b/bricktracker/views/admin/theme.py index d5f15bb..ca9511a 100644 --- a/bricktracker/views/admin/theme.py +++ b/bricktracker/views/admin/theme.py @@ -1,5 +1,3 @@ -import logging - from flask import Blueprint, redirect, url_for from flask_login import login_required from werkzeug.wrappers.response import Response @@ -7,8 +5,6 @@ from werkzeug.wrappers.response import Response from ..exceptions import exception_handler from ...theme_list import BrickThemeList -logger = logging.getLogger(__name__) - admin_theme_page = Blueprint( 'admin_theme', __name__, diff --git a/bricktracker/views/exceptions.py b/bricktracker/views/exceptions.py index aa01b79..e51b66b 100644 --- a/bricktracker/views/exceptions.py +++ b/bricktracker/views/exceptions.py @@ -1,13 +1,10 @@ from functools import wraps -import logging from typing import Callable, ParamSpec, Tuple, Union from werkzeug.wrappers.response import Response from .error import error -logger = logging.getLogger(__name__) - # Decorator type hinting is hard. # What a view can return (str or Response or (Response, xxx)) ViewReturn = Union[ From 9e709039c560b9acb34160b557a4e88ae7bd19f8 Mon Sep 17 00:00:00 2001 From: Gregoo Date: Tue, 4 Feb 2025 19:35:14 +0100 Subject: [PATCH 147/154] Make form.checkbox generic --- templates/macro/form.html | 15 +++++++-------- templates/set/card.html | 2 +- templates/set/management.html | 4 ++-- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/templates/macro/form.html b/templates/macro/form.html index 72af87f..7f7952b 100644 --- a/templates/macro/form.html +++ b/templates/macro/form.html @@ -1,19 +1,18 @@ -{% macro checkbox(item, metadata, parent=none, delete=false) %} +{% macro checkbox(name, id, prefix, url, checked, parent=none, delete=false) %} {% if g.login.is_authenticated() %} - {% set prefix=metadata.as_dataset() %} - -
    \ 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 @@