From 4350ade65b66e80ccdeb72238eea582763f54f45 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 7cbc5c04..06584dba 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 08db61be..f9d8f2bb 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 aa536b8c..f16ea6f6 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 9fcea3d7..1308c71e 100644
--- a/templates/set/card.html
+++ b/templates/set/card.html
@@ -35,7 +35,7 @@
{% endfor %}
</ul>
{% endif %}
- {% if solo %}
+ {% if solo and not config['HIDE_SET_INSTRUCTIONS'] %}
<div class="accordion accordion-flush border-top" id="set-details">
{% 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 <versatile.mailbox@gmail.com>
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 d104269d..71754941 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 36d2b0de..97043653 100644
--- a/templates/admin/database.html
+++ b/templates/admin/database.html
@@ -22,8 +22,8 @@
<div class="d-flex justify-content-start">
<ul class="list-group me-2">
{% for counter in database_counters %}
- <li class="list-group-item d-flex justify-content-between align-items-start">
- <span><i class="ri-{{ counter.icon }}"></i> {{ counter.name }}</span> <span class="badge text-bg-primary rounded-pill ms-2">{{ counter.count }}</span>
+ <li class="list-group-item d-flex justify-content-between align-items-start {% if counter.legacy %}list-group-item-dark{% endif %}">
+ <span><i class="ri-{{ counter.icon }}"></i> {{ counter.name }}</span> <span class="badge {% if counter.legacy %}text-bg-light border{% else %}text-bg-primary{% endif %} rounded-pill ms-2">{{ counter.count }}</span>
</li>
{% if not (loop.index % 5) %}
</ul>
From 25aec890a0368196e939b42ffbc742b02d7b2f10 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 5a1c41f2..23072d37 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 f16ea6f6..cdd56783 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 <versatile.mailbox@gmail.com>
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 cdd56783..3e348e9a 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 <versatile.mailbox@gmail.com>
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 29c5822e..60647fab 100644
--- a/bricktracker/views/minifigure.py
+++ b/bricktracker/views/minifigure.py
@@ -19,12 +19,12 @@ def list() -> str:
# Minifigure details
-@minifigure_page.route('/<number>/details')
+@minifigure_page.route('/<figure>/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 b36f7e13..02353c1c 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('/<id>/minifigures/<minifigure_id>/parts/<part_id>/missing', methods=['POST']) # noqa: E501
+@set_page.route('/<id>/minifigures/<figure>/parts/<part>/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('/<id>/parts/<part_id>/missing', methods=['POST'])
+@set_page.route('/<id>/parts/<part>/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 <versatile.mailbox@gmail.com>
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 3e348e9a..523d336a 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 (<a href="{url}">Go to the set</a>)'.format( # noqa: E501
- number=self.fields.set,
+ message='Set {set}: imported (<a href="{url}">Go to the set</a>)'.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 1e301fa6..def41e28 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 <versatile.mailbox@gmail.com>
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 c7215ae7..3fbd6cd5 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 00000000..331b457c
--- /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 <versatile.mailbox@gmail.com>
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 23072d37..1cd4b8d6 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 523d336a..e521268a 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 (<a href="{url}">Go to the set</a>)'.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 3fbd6cd5..7aedaf26 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 <versatile.mailbox@gmail.com>
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 04ece731..4b5f1837 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 e521268a..17f71b88 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 <versatile.mailbox@gmail.com>
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 06584dba..91caf765 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 f9d8f2bb..236eb544 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 0ad55b13..76a482e0 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 4b5f1837..81affa6d 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 80a51bd4..b6dc1530 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 93897f8e..7805d57e 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 0a0d9f43..f15a9b4c 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 00000000..28d3d754
--- /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 eb72e06f..00000000
--- 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 69c42dc6..9fd23412 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 17f71b88..52a2ed93 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 3b229e8e..58ae8ec3 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 00000000..09830c47
--- /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 00000000..48b905aa
--- /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 81829987..bfaf10d7 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 d72a2a3b..cd7c413e 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 a00f474f..82e61a24 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 ea2dcbea..65b4e69a 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 faf3f402..266b7c07 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 e0bc54d2..660da6df 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 c40d379b..e701d8d6 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 114810d2..966c0221 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 34a8b3dd..479c9e51 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 b1ff2ace..9d73fccf 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 555916ff..fc64e250 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 4a75b4ca..28b32a94 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 00000000..06719257
--- /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 00000000..ec379d89
--- /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 00000000..f1c68c1c
--- /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 b961b282..1d39d990 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 c169c7a1..68333c2f 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 dd2c8567..93a51dfd 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 5f270882..2f19bfe0 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 f08a5d7c..711866b2 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 4b191368..a89d2c91 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 71754941..d01546f9 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 02353c1c..16831752 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 79ed4141..b1949ef7 100644
--- a/templates/minifigure/card.html
+++ b/templates/minifigure/card.html
@@ -3,11 +3,11 @@
{% import 'macro/card.html' as card %}
<div class="card mb-3 flex-fill {% if solo %}card-solo{% endif %}">
- {{ 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) }}
<div class="card-body border-bottom {% if not solo %}p-1{% endif %}">
{% 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 @@
</div>
{% if solo %}
<div class="accordion accordion-flush" id="minifigure-details">
- {{ 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') }}
</div>
diff --git a/templates/minifigure/table.html b/templates/minifigure/table.html
index 94ccef75..66ece790 100644
--- a/templates/minifigure/table.html
+++ b/templates/minifigure/table.html
@@ -6,7 +6,7 @@
<tbody>
{% for item in table_collection %}
<tr>
- {{ 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) }}
<td >
<a class="text-reset" href="{{ item.url() }}" style="max-width:auto">{{ item.fields.name }}</a>
{% if all %}
diff --git a/templates/set/card.html b/templates/set/card.html
index 1308c71e..d6729ee9 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 <versatile.mailbox@gmail.com>
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 16831752..3e2304c4 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 <versatile.mailbox@gmail.com>
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 d6729ee9..e9612eb8 100644
--- a/templates/set/card.html
+++ b/templates/set/card.html
@@ -35,26 +35,28 @@
{% endfor %}
</ul>
{% endif %}
- {% if solo and not config['HIDE_SET_INSTRUCTIONS'] %}
+ {% if solo %}
<div class="accordion accordion-flush border-top" id="set-details">
{% if not delete %}
- {{ accordion.header('Instructions', 'instructions', 'set-details', expanded=open_instructions, quantity=item.instructions | length, icon='file-line', class='p-0') }}
- <div class="list-group list-group-flush">
- {% if item.instructions | length %}
- {% for instruction in item.instructions %}
- <a class="list-group-item list-group-item-action" href="{{ instruction.url() }}" target="_blank"><i class="ri-arrow-right-long-line"></i> <i class="ri-{{ instruction.icon() }}"></i> {{ instruction.filename }}</a>
- {% endfor %}
- {% else %}
- <span class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No instructions file found.</span>
- {% if g.login.is_authenticated() %}
- <a class="list-group-item list-group-item-action" href="{{ url_for('instructions.upload') }}"><i class="ri-upload-line"></i> Upload an instructions file</a>
+ {% 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') }}
+ <div class="list-group list-group-flush">
+ {% if item.instructions | length %}
+ {% for instruction in item.instructions %}
+ <a class="list-group-item list-group-item-action" href="{{ instruction.url() }}" target="_blank"><i class="ri-arrow-right-long-line"></i> <i class="ri-{{ instruction.icon() }}"></i> {{ instruction.filename }}</a>
+ {% endfor %}
+ {% else %}
+ <span class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No instructions file found.</span>
+ {% if g.login.is_authenticated() %}
+ <a class="list-group-item list-group-item-action" href="{{ url_for('instructions.upload') }}"><i class="ri-upload-line"></i> Upload an instructions file</a>
+ {% endif %}
{% endif %}
- {% endif %}
- {% if g.login.is_authenticated() %}
- <a class="list-group-item list-group-item-action" href="{{ url_for('instructions.download', set=item.fields.set) }}"><i class="ri-download-line"></i> Download instructions from Rebrickable</a>
- {% endif %}
- </div>
- {{ accordion.footer() }}
+ {% if g.login.is_authenticated() %}
+ <a class="list-group-item list-group-item-action" href="{{ url_for('instructions.download', set=item.fields.set) }}"><i class="ri-download-line"></i> Download instructions from Rebrickable</a>
+ {% endif %}
+ </div>
+ {{ 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 <versatile.mailbox@gmail.com>
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 d01546f9..f2d1cc52 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 <versatile.mailbox@gmail.com>
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 91caf765..04e84ee9 100644
--- a/.env.sample
+++ b/.env.sample
@@ -2,7 +2,7 @@
# If set, it will append a direct ORDER BY <whatever you set> 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 "<table>"."<column>" [ASC|DESC].
# For composite fields (CASE, SUM, COUNT) the syntax is <field>, there is no <table> name.
From 0a129209a505ad8f4d37fa89cba8a95591c13855 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 6799be07..8657590c 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 <versatile.mailbox@gmail.com>
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 00000000..e69de29b
diff --git a/bricktracker/sql.py b/bricktracker/sql.py
index 07811d9b..9e47d9a4 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 <versatile.mailbox@gmail.com>
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 00000000..fb5b7235
--- /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 81affa6d..24b9933a 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 28d3d754..e2963560 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 09830c47..036f12e5 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 48b905aa..09830c47 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 00000000..135f95d7
--- /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 bfaf10d7..c580b38e 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 cd7c413e..0a2679e7 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 82e61a24..498062d5 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 65b4e69a..e22ee951 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 266b7c07..cacc2f7f 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 660da6df..3fe42107 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 966c0221..16dc56d6 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 479c9e51..00a66af0 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 9d73fccf..c5bbf690 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 fc64e250..8f17ae36 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 28b32a94..eb7e194c 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 2f4d6831..8b1f4c88 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 68333c2f..092e4871 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 b599a873..12df8e2e 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 93a51dfd..b477f81c 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 2462ac5c..7dd6dec8 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 0177c2bd..d5b2da42 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 a89d2c91..80d11614 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 d72616e8..4fc78e46 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 <versatile.mailbox@gmail.com>
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 271c0c38..fb192243 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 60a2244a..41056b8b 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 <versatile.mailbox@gmail.com>
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 b87c9028..8bdc3b8d 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 b055c6bb..4424778d 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 <versatile.mailbox@gmail.com>
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 036f12e5..a75f3f2e 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 <versatile.mailbox@gmail.com>
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 3828204d..ac7aa372 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 135f95d7..679a7612 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 <versatile.mailbox@gmail.com>
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 b71cc2a6..dbe53606 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">
<label class="form-check-label" for="grid-{{ checkbox.fields.id }}">
- Displayed on the Set Grid
+ <i class="ri-grid-line"></i> Displayed on the Set Grid
<i id="status-grid-{{ checkbox.fields.id }}" class="mb-1"></i>
</label>
</div>
@@ -49,7 +49,7 @@
<div class="form-check">
<input class="form-check-input" type="checkbox" id="grid" name="grid" checked>
<label class="form-check-label" for="grid-">
- Displayed on the Set Grid
+ <i class="ri-grid-line"></i> Displayed on the Set Grid
</label>
</div>
</div>
From 964dd9070417ca8bf1d483f5368d7fc50b849c7c Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 e2963560..af2c9260 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 <versatile.mailbox@gmail.com>
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 af2c9260..973b9fb9 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 <versatile.mailbox@gmail.com>
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 04e84ee9..46b21317 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 236eb544..83cd99a4 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 76a482e0..e0318a05 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 24b9933a..a59fee57 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 b6dc1530..7e82c454 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 7805d57e..0074b9bf 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 f15a9b4c..509e7186 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 00000000..93c6b34e
--- /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 9fd23412..00000000
--- 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 52a2ed93..63d41281 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 58ae8ec3..349af66b 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 00000000..8b4b6e63
--- /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 00000000..54962f37
--- /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 c580b38e..dbfc4289 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 498062d5..ca23068b 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 cacc2f7f..372610d4 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 3fe42107..32144bd9 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 e701d8d6..d6ea0d1c 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 16dc56d6..f5bacd7c 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 00a66af0..970494f7 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 ceedc789..00000000
--- 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 c6450287..00000000
--- 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 335dd065..00000000
--- 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 00000000..d9226b39
--- /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 3966a756..00000000
--- 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 39b2d145..e127386f 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 c5bbf690..c1d0ed1f 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 cf4135f5..c8409382 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 2646eeb8..00000000
--- 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 8f17ae36..9d3446e4 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 00000000..d3e291a3
--- /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 eb7e194c..a1760d67 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 ebdd5f5e..c74a535d 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 00000000..c1dd3682
--- /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 00000000..d989258c
--- /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 00000000..026f465c
--- /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 00000000..54f6305e
--- /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 1d39d990..78ea32ce 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 092e4871..70730ff6 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 b477f81c..49b0e884 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 2f19bfe0..51a615de 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 781754c2..9438b67d 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 711866b2..00a1fb0b 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 8877cff7..a0371737 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 80d11614..0d12ae8d 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 f2d1cc52..2e5c072e 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 4424778d..172ecf1d 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 25051228..5f20997c 100644
--- a/bricktracker/views/part.py
+++ b/bricktracker/views/part.py
@@ -30,31 +30,26 @@ def missing() -> str:
# Part details
-@part_page.route('/<number>/<int:color>/details', defaults={'element': None}, methods=['GET']) # noqa: E501
-@part_page.route('/<number>/<int:color>/<int:element>/details', methods=['GET']) # noqa: E501
+@part_page.route('/<part>/<int:color>/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 3e2304c4..0b8d843a 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('/<id>/minifigures/<figure>/parts/<part>/missing', methods=['POST']) # noqa: E501
+# Update the missing pieces of a part
+@set_page.route('/<id>/parts/<part>/<int:color>/<int:spare>/missing', defaults={'figure': None}, methods=['POST']) # noqa: E501
+@set_page.route('/<id>/minifigures/<figure>/parts/<part>/<int:color>/<int:spare>/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('/<id>/parts/<part>/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 3b52220c..1cc92ac6 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') %}
<div class="card-header">
{% if not solo %}
<a class="text-decoration-none text-reset" href="{{ item.url() }}" data-bs-toggle="tooltip" title="{{ name }}">
{% endif %}
<h5 class="mb-0 {% if not solo %}fs-6 text-nowrap overflow-x-hidden text-truncate{% endif %}">
- {% if number %}<span class="badge text-bg-secondary fw-normal"><i class="ri-{{ icon }}"></i>{{ number }}</span>{% endif %}
+ {% if identifier %}<span class="badge text-bg-secondary fw-normal"><i class="ri-{{ icon }}"></i>{{ identifier }}</span>{% endif %}
{% if color %}<span class="badge text-bg-light fw-normal border"><i class="ri-palette-line"></i> {{ color }}</span>{% endif %}
{{ name }}
</h5>
diff --git a/templates/minifigure/card.html b/templates/minifigure/card.html
index b1949ef7..1b701938 100644
--- a/templates/minifigure/card.html
+++ b/templates/minifigure/card.html
@@ -3,7 +3,7 @@
{% import 'macro/card.html' as card %}
<div class="card mb-3 flex-fill {% if solo %}card-solo{% endif %}">
- {{ 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) }}
<div class="card-body border-bottom {% if not solo %}p-1{% endif %}">
{% if last %}
diff --git a/templates/part/card.html b/templates/part/card.html
index 2f86664e..8c23b59e 100644
--- a/templates/part/card.html
+++ b/templates/part/card.html
@@ -3,8 +3,8 @@
{% import 'macro/card.html' as card %}
<div class="card mb-3 flex-fill card-solo">
- {{ 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) }}
<div class="card-body border-bottom {% if not solo %}p-1{% endif %}">
{{ 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 1fca2648..78d0efe6 100644
--- a/templates/part/table.html
+++ b/templates/part/table.html
@@ -6,10 +6,10 @@
<tbody>
{% for item in table_collection %}
<tr>
- {{ 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) }}
<td>
<a class="text-reset" href="{{ item.url() }}">{{ item.fields.name }}</a>
- {% if item.fields.is_spare %}<span class="badge rounded-pill text-bg-warning fw-normal"><i class="ri-loop-left-line"></i> Spare</span>{% endif %}
+ {% if item.fields.spare %}<span class="badge rounded-pill text-bg-warning fw-normal"><i class="ri-loop-left-line"></i> Spare</span>{% endif %}
{% if all %}
{{ table.rebrickable(item) }}
{{ table.bricklink(item) }}
@@ -24,15 +24,15 @@
{% endif %}
{% endif %}
{% if not no_missing %}
- <td {% if not all %}id="sort-part-{{ item.fields.u_id }}-{{ item.fields.id }}" data-sort="{{ item.fields.total_missing }}"{% endif %} class="table-td-missing">
+ <td {% if not all %}id="sort-part-{{ item.fields.id }}-{{ item.html_id() }}" data-sort="{{ item.fields.total_missing }}"{% endif %} class="table-td-missing">
{% if all or read_only_missing %}
{{ item.fields.total_missing }}
{% else %}
<div class="input-group">
{% if g.login.is_authenticated() %}
<input class="form-control form-control-sm flex-shrink-1" type="text" {% if item.fields.total_missing %}value="{{ item.fields.total_missing }}"{% endif %}
- onchange="change_part_missing_amount(this, '{{ item.fields.u_id }}', '{{ item.fields.id }}', '{{ item.url_for_missing() }}')" autocomplete="off">
- <span id="status-part-{{ item.fields.u_id }}-{{ item.fields.id }}" class="input-group-text ri-save-line"></span>
+ onchange="change_part_missing_amount(this, '{{ item.fields.id }}', '{{ item.html_id() }}', '{{ item.url_for_missing() }}')" autocomplete="off">
+ <span id="status-part-{{ item.fields.id }}-{{ item.html_id() }}" class="input-group-text ri-save-line"></span>
{% else %}
<input class="form-control form-control-sm" type="text" {% if item.fields.total_missing %}value="{{ item.fields.total_missing }}"{% endif %}
disabled autocomplete="off">
diff --git a/templates/set/card.html b/templates/set/card.html
index e9612eb8..20bfe4a5 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) }}
<div class="card-body border-bottom-0 {% if not solo %}p-1{% endif %}">
{{ 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 6bdc028e..6305e0ec 100644
--- a/templates/set/mini.html
+++ b/templates/set/mini.html
@@ -2,7 +2,7 @@
{% import 'macro/card.html' as card %}
<div class="card mb-3">
- {{ 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) }}
<div class="card-body p-1">
{{ badge.theme(item.theme.name) }}
From 482817fd962fe3c86e9e7675d24d00847ca1bc21 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 a75f3f2e..f955bb9d 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 <versatile.mailbox@gmail.com>
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 e0318a05..1ad6aa6a 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 a59fee57..790018a7 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 7e82c454..af901b2b 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 0074b9bf..667c26e4 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 973b9fb9..30d61eef 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 93c6b34e..704990c6 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 1cd4b8d6..fbf10f1a 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 08651d2f..f7cc8892 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 63d41281..28f0341c 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 (<a href="{url}">Go to the set</a>)'.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 (<a href="{url}">Go to the set</a>)'.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 ea6d6d2b..38a10f0d 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 06719257..6c0ac8e8 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 d989258c..fcec4ef0 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 88b2b44f..39e69646 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 0b8d843a..117ff03b 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('/<id>/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 41056b8b..4f6bf978 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 140eec62..5316ea17 100644
--- a/templates/add.html
+++ b/templates/add.html
@@ -68,5 +68,7 @@
</div>
</div>
</div>
-{% include 'set/socket.html' %}
+{% with id='add' %}
+ {% include 'set/socket.html' %}
+{% endwith %}
{% endblock %}
diff --git a/templates/bulk.html b/templates/bulk.html
index 6e6e5d8a..00d47797 100644
--- a/templates/bulk.html
+++ b/templates/bulk.html
@@ -58,7 +58,7 @@
</div>
</div>
</div>
-{% 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 00000000..5add93d8
--- /dev/null
+++ b/templates/refresh.html
@@ -0,0 +1,64 @@
+{% extends 'base.html' %}
+
+{% block title %} - Refresh set {{ item.fields.set }}{% endblock %}
+
+{% block main %}
+<div class="container">
+ <div class="alert alert-primary" role="alert">
+ <h4 class="alert-heading">Refreshing from Rebrickable</h4>
+ <p class="mb-0">This will refresh all the Rebrickable data (set, minifigures, parts) associated with this set.</p>
+ </div>
+ <div class="row">
+ <div class="col-12">
+ <div class="card mb-3">
+ <div class="card-header">
+ <h5 class="mb-0"><i class="ri-refresh-line"></i> Refresh a set</h5>
+ </div>
+ <div class="card-body">
+ <div id="refresh-fail" class="alert alert-danger d-none" role="alert"></div>
+ <div id="refresh-complete" class="alert alert-success d-none" role="alert"></div>
+ <div class="mb-3">
+ <label for="refresh-set" class="form-label">Set number</label>
+ <input type="text" class="form-control" id="refresh-set" value="{{ item.fields.set }}">
+ </div>
+ <hr>
+ <div class="mb-3">
+ <p>
+ Progress <span id="refresh-count"></span>
+ <span id="refresh-spinner" class="d-none">
+ <span class="spinner-border spinner-border-sm" aria-hidden="true"></span>
+ <span class="visually-hidden" role="status">Loading...</span>
+ </span>
+ </p>
+ <div id="refresh-progress" class="progress" role="progressbar" aria-label="Refresh a set progress" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
+ <div id="refresh-progress-bar" class="progress-bar" style="width: 0%"></div>
+ </div>
+ <p id="refresh-progress-message" class="text-center d-none"></p>
+ </div>
+ <div id="refresh-card" class="d-flex justify-content-center">
+ <div class="card mb-3 col-6">
+ <div class="card-header">
+ <h5 class="mb-0">
+ <span class="badge text-bg-secondary fw-normal"><i class="ri-hashtag"></i> <span id="refresh-card-set">{{ item.fields.set }}</span></span>
+ <span id="refresh-card-name">{{ item.fields.name }}</span>
+ </h5>
+ </div>
+ <div id="refresh-card-image-container" class="card-img" style="background-image: url({{ item.url_for_image() }})">
+ <img id="refresh-card-image" src="{{ item.url_for_image() }}">
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="card-footer text-end">
+ <span id="refresh-status-icon" class="me-1"></span><span id="refresh-status" class="me-1"></span>
+ <a href="{{ url_for('set.details', id=item.fields.id) }}" class="btn btn-primary" role="button"><i class="ri-hashtag"></i> Back to the set details</a>
+ <button id="refresh" type="button" class="btn btn-primary"><i class="ri-refresh-line"></i> Refresh</button>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+{% with id='refresh', refresh=true %}
+ {% include 'set/socket.html' %}
+{% endwith %}
+{% endblock %}
diff --git a/templates/set/card.html b/templates/set/card.html
index 20bfe4a5..02d77b30 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') }}
+ <a href="{{ item.url_for_refresh() }}" class="btn btn-primary" role="button"><i class="ri-refresh-line"></i> Refresh the set</a>
+ {{ accordion.footer() }}
{{ accordion.header('Danger zone', 'danger-zone', 'set-details', expanded=delete, danger=true, class='text-end') }}
{% if delete %}
<form action="{{ item.url_for_do_delete() }}" method="post">
diff --git a/templates/set/socket.html b/templates/set/socket.html
index a566a95d..c4000a82 100644
--- a/templates/set/socket.html
+++ b/templates/set/socket.html
@@ -1,12 +1,19 @@
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", () => {
- new BrickSetSocket('add', '{{ path }}', '{{ namespace }}', {
- COMPLETE: '{{ messages['COMPLETE'] }}',
- FAIL: '{{ messages['FAIL'] }}',
- IMPORT_SET: '{{ messages['IMPORT_SET'] }}',
- LOAD_SET: '{{ messages['LOAD_SET'] }}',
- PROGRESS: '{{ messages['PROGRESS'] }}',
- SET_LOADED: '{{ messages['SET_LOADED'] }}',
- }{% if bulk %}, true{% endif %});
+ new BrickSetSocket(
+ '{{ id }}',
+ '{{ path }}',
+ '{{ namespace }}',
+ {
+ COMPLETE: '{{ messages['COMPLETE'] }}',
+ FAIL: '{{ messages['FAIL'] }}',
+ IMPORT_SET: '{{ messages['IMPORT_SET'] }}',
+ LOAD_SET: '{{ messages['LOAD_SET'] }}',
+ PROGRESS: '{{ messages['PROGRESS'] }}',
+ SET_LOADED: '{{ messages['SET_LOADED'] }}',
+ },
+ {% if bulk %}true{% else %}false{% endif %},
+ {% if refresh %}true{% else %}false{% endif %}
+ );
});
</script>
From 71ccfcd23d50a8c8c677cbe0afdf1127c56063e7 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 117ff03b..44eabed4 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 <versatile.mailbox@gmail.com>
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 ea08c2d5..c62d3f3b 100644
--- a/templates/sets.html
+++ b/templates/sets.html
@@ -9,14 +9,14 @@
<div class="col-12 flex-grow-1">
<label class="visually-hidden" for="grid-search">Search</label>
<div class="input-group">
- <span class="input-group-text">Search</span>
+ <span class="input-group-text"><i class="ri-search-line"></i><span class="ms-1 d-none d-xl-inline"> Search</span></span>
<input id="grid-search" class="form-control form-control-sm" type="text" placeholder="Set name, set number, set theme or number of parts..." value="">
</div>
</div>
<div class="col-12 flex-grow-1">
<label class="visually-hidden" for="grid-filter">Filter</label>
<div class="input-group">
- <span class="input-group-text">Filter</span>
+ <span class="input-group-text"><i class="ri-dropdown-list"></i><span class="ms-1 d-none d-xl-inline"> Filter</span></span>
<select id="grid-filter" class="form-select form-select-sm" autocomplete="off">
<option value="" selected>All sets</option>
<option value="-has-missing">Set is complete</option>
@@ -37,23 +37,23 @@
</div>
<div class="col-12">
<div id="grid-sort" class="input-group">
- <span class="input-group-text">Sort</span>
+ <span class="input-group-text"><i class="ri-sort-asc"></i><span class="ms-1 d-none d-xxl-inline"> Sort</span></span>
<button id="sort-number" type="button" class="btn btn-sm btn-outline-primary"
- data-sort-target="div#grid>div" data-sort-attribute="number" data-sort-natural="true">Num.</button>
+ data-sort-target="div#grid>div" data-sort-attribute="number" data-sort-natural="true"><i class="ri-hashtag"></i><span class="d-none d-xxl-inline"> Number</span></button>
<button id="sort-name" type="button" class="btn btn-sm btn-outline-primary"
- data-sort-target="div#grid>div" data-sort-attribute="name">Name</button>
+ data-sort-target="div#grid>div" data-sort-attribute="name"><i class="ri-pencil-line"></i><span class="d-none d-xxl-inline"> Name</span></button>
<button id="sort-theme" type="button" class="btn btn-sm btn-outline-primary"
- data-sort-target="div#grid>div" data-sort-attribute="theme">Theme</button>
+ data-sort-target="div#grid>div" data-sort-attribute="theme"><i class="ri-price-tag-3-line"></i><span class="d-none d-xxl-inline"> Theme</span></button>
<button id="sort-year" type="button" class="btn btn-sm btn-outline-primary"
- data-sort-target="div#grid>div" data-sort-attribute="year">Year</button>
+ data-sort-target="div#grid>div" data-sort-attribute="year"><i class="ri-calendar-line"></i><span class="d-none d-xxl-inline"> Year</span></button>
<button id="sort-minifigure" type="button" class="btn btn-sm btn-outline-primary"
- data-sort-target="div#grid>div" data-sort-attribute="minifigures" data-sort-desc="true">Fig.</button>
+ data-sort-target="div#grid>div" data-sort-attribute="minifigures" data-sort-desc="true"><i class="ri-group-line"></i><span class="d-none d-xxl-inline"> Figures</span></button>
<button id="sort-parts" type="button" class="btn btn-sm btn-outline-primary"
- data-sort-target="div#grid>div" data-sort-attribute="parts" data-sort-desc="true">Parts</button>
+ data-sort-target="div#grid>div" data-sort-attribute="parts" data-sort-desc="true"><i class="ri-shapes-line"></i><span class="d-none d-xxl-inline"> Parts</span></button>
<button id="sort-missing" type="button" class="btn btn-sm btn-outline-primary"
- data-sort-target="div#grid>div" data-sort-attribute="missing" data-sort-desc="true">Miss.</button>
+ data-sort-target="div#grid>div" data-sort-attribute="missing" data-sort-desc="true"><i class="ri-error-warning-line"></i><span class="d-none d-xxl-inline"> Missing</span></button>
<button id="sort-clear" type="button" class="btn btn-sm btn-outline-dark"
- data-sort-target="div#grid>div" data-sort-clear="true"><i class="ri-filter-off-line"></i></button>
+ data-sort-target="div#grid>div" data-sort-clear="true"><i class="ri-filter-off-line"></i><span class="d-none d-xxl-inline"> Clear</span></button>
</div>
</div>
</div>
From d93723ab4e5a6733202930481172aaf10ff7143f Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Wed, 29 Jan 2025 09:04:09 +0100
Subject: [PATCH 030/154] Use Rebrickable URL for cosmetics if available
---
bricktracker/rebrickable_part.py | 15 +++++++++++----
1 file changed, 11 insertions(+), 4 deletions(-)
diff --git a/bricktracker/rebrickable_part.py b/bricktracker/rebrickable_part.py
index 704990c6..b0282be4 100644
--- a/bricktracker/rebrickable_part.py
+++ b/bricktracker/rebrickable_part.py
@@ -138,10 +138,17 @@ class RebrickablePart(BrickRecord):
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,
- )
+ if self.fields.url is not None:
+ # The URL does not contain color info...
+ return '{url}{color}'.format(
+ url=self.fields.url,
+ color=self.fields.color
+ )
+ else:
+ return current_app.config['REBRICKABLE_LINK_PART_PATTERN'].format( # noqa: E501
+ part=self.fields.part,
+ color=self.fields.color,
+ )
except Exception:
pass
From d08b7bb063d9df455d02c7494668ff1df41de117 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Wed, 29 Jan 2025 10:36:43 +0100
Subject: [PATCH 031/154] Display RGB color, transparency and prints for parts
---
bricktracker/rebrickable_part.py | 8 ++++++++
bricktracker/sql/part/base/base.sql | 2 +-
static/styles.css | 6 ++++++
templates/macro/card.html | 13 +++++++++++--
templates/part/card.html | 2 +-
templates/part/table.html | 5 ++++-
6 files changed, 31 insertions(+), 5 deletions(-)
diff --git a/bricktracker/rebrickable_part.py b/bricktracker/rebrickable_part.py
index b0282be4..8224d98f 100644
--- a/bricktracker/rebrickable_part.py
+++ b/bricktracker/rebrickable_part.py
@@ -134,6 +134,14 @@ class RebrickablePart(BrickRecord):
spare=self.fields.spare,
)
+ # 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,
+ )
+
# Compute the url for the rebrickable page
def url_for_rebrickable(self, /) -> str:
if current_app.config['REBRICKABLE_LINKS']:
diff --git a/bricktracker/sql/part/base/base.sql b/bricktracker/sql/part/base/base.sql
index d9226b39..7849d4cc 100644
--- a/bricktracker/sql/part/base/base.sql
+++ b/bricktracker/sql/part/base/base.sql
@@ -19,7 +19,7 @@ SELECT
"rebrickable_parts"."image",
"rebrickable_parts"."image_id",
"rebrickable_parts"."url",
- --"rebrickable_parts"."print",
+ "rebrickable_parts"."print",
{% block total_missing %}
NULL AS "total_missing", -- dummy for order: total_missing
{% endblock %}
diff --git a/static/styles.css b/static/styles.css
index 9ac95de8..af00525a 100644
--- a/static/styles.css
+++ b/static/styles.css
@@ -59,3 +59,9 @@
.sortable thead th {
font-weight: bold !important;
}
+
+.color-rgb {
+ display: inline-block;
+ width: 15px;
+ height: 15px;
+}
\ No newline at end of file
diff --git a/templates/macro/card.html b/templates/macro/card.html
index 1cc92ac6..7f2924ff 100644
--- a/templates/macro/card.html
+++ b/templates/macro/card.html
@@ -1,11 +1,20 @@
-{% macro header(item, name, solo=false, identifier=none, color=none, icon='hashtag') %}
+{% macro header(item, name, solo=false, identifier=none, icon='hashtag') %}
<div class="card-header">
{% if not solo %}
<a class="text-decoration-none text-reset" href="{{ item.url() }}" data-bs-toggle="tooltip" title="{{ name }}">
{% endif %}
<h5 class="mb-0 {% if not solo %}fs-6 text-nowrap overflow-x-hidden text-truncate{% endif %}">
{% if identifier %}<span class="badge text-bg-secondary fw-normal"><i class="ri-{{ icon }}"></i>{{ identifier }}</span>{% endif %}
- {% if color %}<span class="badge text-bg-light fw-normal border"><i class="ri-palette-line"></i> {{ color }}</span>{% endif %}
+ {% if item.fields.color_name %}
+ <span class="badge bg-white text-black fw-normal border"><i class="ri-palette-line"></i>
+ {% if item.fields.color_rgb %}
+ <span class="color-rgb align-bottom border border-black" style="background-color: #{{ item.fields.color_rgb }};"></span>
+ {% endif %}
+ {{ item.fields.color_name }}
+ </span>
+ {% endif %}
+ {% if item.fields.color_transparent %}<span class="badge text-bg-light fw-normal border"><i class="ri-blur-off-line"></i> Transparent</span>{% endif %}
+ {% if item.fields.print %}<span class="badge text-bg-light fw-normal border"><a class="text-reset" href="{{ item.url_for_print() }}"><i class="ri-paint-brush-line"></i> Print</a></span>{% endif %}
{{ name }}
</h5>
{% if not solo %}
diff --git a/templates/part/card.html b/templates/part/card.html
index 8c23b59e..9a14f4ae 100644
--- a/templates/part/card.html
+++ b/templates/part/card.html
@@ -3,7 +3,7 @@
{% import 'macro/card.html' as card %}
<div class="card mb-3 flex-fill card-solo">
- {{ card.header(item, item.fields.name, solo=solo, identifier=item.fields.part, color=item.fields.color, icon='shapes-line') }}
+ {{ 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) }}
<div class="card-body border-bottom {% if not solo %}p-1{% endif %}">
{{ badge.total_sets(sets_using | length, solo=solo, last=last) }}
diff --git a/templates/part/table.html b/templates/part/table.html
index 78d0efe6..f9f8c34d 100644
--- a/templates/part/table.html
+++ b/templates/part/table.html
@@ -15,7 +15,10 @@
{{ table.bricklink(item) }}
{% endif %}
</td>
- <td>{{ item.fields.color_name }}</td>
+ <td>
+ {% if item.fields.color_rgb %}<span class="color-rgb align-middle border border-black" style="background-color: #{{ item.fields.color_rgb }};"></span>{% endif %}
+ <span class="align-middle">{{ item.fields.color_name }}</span>
+ </td>
{% if not no_quantity %}
{% if all %}
<td>{{ item.fields.total_quantity }}</td>
From e033dec988149c90f0d431d5fb10cf6fe3f61065 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 f9f8c34d..87b02f6d 100644
--- a/templates/part/table.html
+++ b/templates/part/table.html
@@ -7,7 +7,7 @@
{% for item in table_collection %}
<tr>
{{ table.image(item.url_for_image(), caption=item.fields.name, alt=item.fields.part, accordion=solo) }}
- <td>
+ <td data-sort="{{ item.fields.name }}">
<a class="text-reset" href="{{ item.url() }}">{{ item.fields.name }}</a>
{% if item.fields.spare %}<span class="badge rounded-pill text-bg-warning fw-normal"><i class="ri-loop-left-line"></i> Spare</span>{% endif %}
{% if all %}
@@ -15,7 +15,7 @@
{{ table.bricklink(item) }}
{% endif %}
</td>
- <td>
+ <td data-sort="{{ item.fields.color_name }}">
{% if item.fields.color_rgb %}<span class="color-rgb align-middle border border-black" style="background-color: #{{ item.fields.color_rgb }};"></span>{% endif %}
<span class="align-middle">{{ item.fields.color_name }}</span>
</td>
From a2aafbf93a984d08443ac16bf3783ae867197da6 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 af00525a..ee82ffe0 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 7f2924ff..3c26de7b 100644
--- a/templates/macro/card.html
+++ b/templates/macro/card.html
@@ -8,7 +8,7 @@
{% if item.fields.color_name %}
<span class="badge bg-white text-black fw-normal border"><i class="ri-palette-line"></i>
{% if item.fields.color_rgb %}
- <span class="color-rgb align-bottom border border-black" style="background-color: #{{ item.fields.color_rgb }};"></span>
+ <span class="color-rgb {% if item.fields.color == 9999 %}color-any{% endif %} align-bottom border border-black" {% if item.fields.color != 9999 %}style="background-color: #{{ item.fields.color_rgb }};"{% endif %}></span>
{% endif %}
{{ item.fields.color_name }}
</span>
diff --git a/templates/part/table.html b/templates/part/table.html
index 87b02f6d..8e1da015 100644
--- a/templates/part/table.html
+++ b/templates/part/table.html
@@ -16,7 +16,7 @@
{% endif %}
</td>
<td data-sort="{{ item.fields.color_name }}">
- {% if item.fields.color_rgb %}<span class="color-rgb align-middle border border-black" style="background-color: #{{ item.fields.color_rgb }};"></span>{% endif %}
+ {% if item.fields.color_rgb %}<span class="color-rgb color-rgb-table {% if item.fields.color == 9999 %}color-any{% endif %} align-middle border border-black" {% if item.fields.color != 9999 %}style="background-color: #{{ item.fields.color_rgb }};"{% endif %}></span>{% endif %}
<span class="align-middle">{{ item.fields.color_name }}</span>
</td>
{% if not no_quantity %}
From 468cc7ede9e0ec719a7f133ca9d0929665ab5c3c Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 667c26e4..833ae61c 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 00000000..f996864c
--- /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 5f20997c..dbcfe0dd 100644
--- a/bricktracker/views/part.py
+++ b/bricktracker/views/part.py
@@ -33,9 +33,11 @@ def missing() -> str:
@part_page.route('/<part>/<int:color>/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 3c26de7b..ae8bce01 100644
--- a/templates/macro/card.html
+++ b/templates/macro/card.html
@@ -5,16 +5,18 @@
{% endif %}
<h5 class="mb-0 {% if not solo %}fs-6 text-nowrap overflow-x-hidden text-truncate{% endif %}">
{% if identifier %}<span class="badge text-bg-secondary fw-normal"><i class="ri-{{ icon }}"></i>{{ identifier }}</span>{% endif %}
- {% if item.fields.color_name %}
- <span class="badge bg-white text-black fw-normal border"><i class="ri-palette-line"></i>
- {% if item.fields.color_rgb %}
- <span class="color-rgb {% if item.fields.color == 9999 %}color-any{% endif %} align-bottom border border-black" {% if item.fields.color != 9999 %}style="background-color: #{{ item.fields.color_rgb }};"{% endif %}></span>
- {% endif %}
- {{ item.fields.color_name }}
- </span>
+ {% if solo %}
+ {% if item.fields.color_name %}
+ <span class="badge bg-white text-black fw-normal border"><i class="ri-palette-line"></i>
+ {% if item.fields.color_rgb %}
+ <span class="color-rgb {% if item.fields.color == 9999 %}color-any{% endif %} align-bottom border border-black" {% if item.fields.color != 9999 %}style="background-color: #{{ item.fields.color_rgb }};"{% endif %}></span>
+ {% endif %}
+ {{ item.fields.color_name }}
+ </span>
+ {% endif %}
+ {% if item.fields.color_transparent %}<span class="badge text-bg-light fw-normal border"><i class="ri-blur-off-line"></i> Transparent</span>{% endif %}
+ {% if item.fields.print %}<span class="badge text-bg-light fw-normal border"><a class="text-reset" href="{{ item.url_for_print() }}"><i class="ri-paint-brush-line"></i> Print</a></span>{% endif %}
{% endif %}
- {% if item.fields.color_transparent %}<span class="badge text-bg-light fw-normal border"><i class="ri-blur-off-line"></i> Transparent</span>{% endif %}
- {% if item.fields.print %}<span class="badge text-bg-light fw-normal border"><a class="text-reset" href="{{ item.url_for_print() }}"><i class="ri-paint-brush-line"></i> Print</a></span>{% endif %}
{{ name }}
</h5>
{% if not solo %}
diff --git a/templates/part/card.html b/templates/part/card.html
index 9a14f4ae..43dfcdc5 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 %}
-<div class="card mb-3 flex-fill card-solo">
+<div class="card mb-3 flex-fill {% if solo %}card-solo{% endif %}">
{{ 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) }}
- <div class="card-body border-bottom {% if not solo %}p-1{% endif %}">
+ <div class="card-body border-bottom-0 {% if not solo %}p-1{% 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) }}
{{ 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 %}
</div>
- <div class="accordion accordion-flush" id="part-details">
- {{ 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') }}
- </div>
- <div class="card-footer"></div>
+ {% if solo %}
+ <div class="accordion accordion-flush border-top" id="part-details">
+ {{ 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') }}
+ </div>
+ <div class="card-footer"></div>
+ {% endif %}
</div>
From cf11e4d71878588dfc2bb1b1090b35f99010a780 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 f9c5a5d8..cd126e9f 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) %}
+ <input class="form-control form-control-sm flex-shrink-1" type="text" value="{% if value %}{{ value }}{% endif %}"
+ {% if g.login.is_authenticated() %}
+ onchange="change_part_missing_amount(this, '{{ id }}', '{{ html_id }}', '{{ url }}')"
+ {% else %}
+ disabled
+ {% endif %}
+ autocomplete="off">
+ {% if g.login.is_authenticated() %}
+ <span id="status-part-{{ id }}-{{ html_id }}" class="input-group-text ri-save-line"></span>
+ {% else %}
+ <span class="input-group-text ri-prohibited-line"></span>
+ {% endif %}
+{% endmacro %}
diff --git a/templates/part/table.html b/templates/part/table.html
index 8e1da015..b7139ede 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 %}
<div class="table-responsive-sm">
@@ -32,15 +33,7 @@
{{ item.fields.total_missing }}
{% else %}
<div class="input-group">
- {% if g.login.is_authenticated() %}
- <input class="form-control form-control-sm flex-shrink-1" type="text" {% if item.fields.total_missing %}value="{{ item.fields.total_missing }}"{% endif %}
- onchange="change_part_missing_amount(this, '{{ item.fields.id }}', '{{ item.html_id() }}', '{{ item.url_for_missing() }}')" autocomplete="off">
- <span id="status-part-{{ item.fields.id }}-{{ item.html_id() }}" class="input-group-text ri-save-line"></span>
- {% else %}
- <input class="form-control form-control-sm" type="text" {% if item.fields.total_missing %}value="{{ item.fields.total_missing }}"{% endif %}
- disabled autocomplete="off">
- <span class="input-group-text ri-prohibited-line"></span>
- {% endif %}
+ {{ form.input(item.fields.id, item.html_id(), item.url_for_missing(), item.fields.total_missing) }}
</div>
{% endif %}
</td>
From f44192a1148b04ddc74f8bf4d3a30658280638eb Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 af901b2b..c847db58 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 ee82ffe0..0651e203 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 cd126e9f..f88b6731 100644
--- a/templates/macro/form.html
+++ b/templates/macro/form.html
@@ -16,8 +16,13 @@
{% endif %}
{% endmacro %}
-{% macro input(id, html_id, url, value) %}
- <input class="form-control form-control-sm flex-shrink-1" type="text" value="{% if value %}{{ value }}{% endif %}"
+{% macro input(name, id, prefix, url, value, all=none, read_only=none) %}
+ {% if all or read_only %}
+ {{ value }}
+ {% else %}
+ <label class="visually-hidden" for="{{ prefix }}-{{ id }}">{{ name }}</label>
+ <div class="input-group">
+ <input class="form-control form-control-sm flex-shrink-1" type="text" id="{{ prefix }}-{{ id }}" value="{% if value %}{{ value }}{% endif %}"
{% if g.login.is_authenticated() %}
onchange="change_part_missing_amount(this, '{{ id }}', '{{ html_id }}', '{{ url }}')"
{% else %}
@@ -25,8 +30,10 @@
{% endif %}
autocomplete="off">
{% if g.login.is_authenticated() %}
- <span id="status-part-{{ id }}-{{ html_id }}" class="input-group-text ri-save-line"></span>
+ <span id="status-{{ prefix }}-{{ id }}" class="input-group-text ri-save-line"></span>
{% else %}
<span class="input-group-text ri-prohibited-line"></span>
+ {% endif %}
+ </div>
{% endif %}
{% endmacro %}
diff --git a/templates/part/table.html b/templates/part/table.html
index b7139ede..692a7dc6 100644
--- a/templates/part/table.html
+++ b/templates/part/table.html
@@ -28,14 +28,8 @@
{% endif %}
{% endif %}
{% if not no_missing %}
- <td {% if not all %}id="sort-part-{{ item.fields.id }}-{{ item.html_id() }}" data-sort="{{ item.fields.total_missing }}"{% endif %} class="table-td-missing">
- {% if all or read_only_missing %}
- {{ item.fields.total_missing }}
- {% else %}
- <div class="input-group">
- {{ form.input(item.fields.id, item.html_id(), item.url_for_missing(), item.fields.total_missing) }}
- </div>
- {% endif %}
+ <td data-sort="{{ item.fields.total_missing }}" class="table-td-input">
+ {{ form.input('Missing', item.fields.id, item.html_id(), item.url_for_missing(), item.fields.total_missing, all=all, read_only=read_only_missing) }}
</td>
{% endif %}
{% if all %}
From e2b8b51db8d4a5e37a77df6dd936092c10b4f9d2 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 c847db58..d81bb85c 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 44eabed4..d82896c0 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 224e24ba..e3a32f84 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 f88b6731..4844822d 100644
--- a/templates/macro/form.html
+++ b/templates/macro/form.html
@@ -24,7 +24,7 @@
<div class="input-group">
<input class="form-control form-control-sm flex-shrink-1" type="text" id="{{ prefix }}-{{ id }}" value="{% if value %}{{ value }}{% endif %}"
{% if g.login.is_authenticated() %}
- onchange="change_part_missing_amount(this, '{{ id }}', '{{ html_id }}', '{{ url }}')"
+ data-changer-id="{{ id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ url }}"
{% else %}
disabled
{% endif %}
From b142ff5bed6c3ac6387ef12557d47f97b199cdc9 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 d81bb85c..495059f3 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 <versatile.mailbox@gmail.com>
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 fbfd01b5..417ef474 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 52ed7a1a..33b6322d 100644
--- a/templates/minifigure.html
+++ b/templates/minifigure.html
@@ -6,7 +6,7 @@
<div class="container">
<div class="row">
<div class="col-12">
- {% with solo=true, read_only_missing=true %}
+ {% with solo=true, read_only=true %}
{% include 'minifigure/card.html' %}
{% endwith %}
</div>
diff --git a/templates/minifigure/card.html b/templates/minifigure/card.html
index 1b701938..446c1647 100644
--- a/templates/minifigure/card.html
+++ b/templates/minifigure/card.html
@@ -19,7 +19,7 @@
</div>
{% if solo %}
<div class="accordion accordion-flush" id="minifigure-details">
- {{ 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') }}
</div>
diff --git a/templates/part/table.html b/templates/part/table.html
index 692a7dc6..cd16f9be 100644
--- a/templates/part/table.html
+++ b/templates/part/table.html
@@ -29,7 +29,7 @@
{% endif %}
{% if not no_missing %}
<td data-sort="{{ item.fields.total_missing }}" class="table-td-input">
- {{ 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) }}
</td>
{% endif %}
{% if all %}
From cb58ef83cccbfbb7966e4af51cc1eccef003b87f Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 e3a32f84..8cb005f7 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 4844822d..499c2205 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() %}
<input class="form-check-input" type="checkbox" id="{{ prefix }}-{{ id }}" {% if checked %}checked{% endif %}
- {% if not delete %}
- data-changer-id="{{ id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ url }}" data-changer-parent="set"
- {% else %}
- disabled
- {% endif %}
- autocomplete="off">
+ {% if not delete %}
+ data-changer-id="{{ id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ url }}" data-changer-parent="set"
+ {% else %}
+ disabled
+ {% endif %}
+ autocomplete="off">
<label class="form-check-label" for="{{ prefix }}-{{ id }}">
{{ text }} <i id="status-{{ prefix }}-{{ id }}" class="mb-1"></i>
</label>
@@ -23,16 +23,17 @@
<label class="visually-hidden" for="{{ prefix }}-{{ id }}">{{ name }}</label>
<div class="input-group">
<input class="form-control form-control-sm flex-shrink-1" type="text" id="{{ prefix }}-{{ id }}" value="{% if value %}{{ value }}{% endif %}"
- {% if g.login.is_authenticated() %}
+ {% if g.login.is_authenticated() %}
data-changer-id="{{ id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ url }}"
- {% else %}
- disabled
- {% endif %}
- autocomplete="off">
- {% if g.login.is_authenticated() %}
+ {% else %}
+ disabled
+ {% endif %}
+ autocomplete="off">
+ {% if g.login.is_authenticated() %}
<span id="status-{{ prefix }}-{{ id }}" class="input-group-text ri-save-line"></span>
- {% else %}
- <span class="input-group-text ri-prohibited-line"></span>
+ <button id="clear-{{ prefix }}-{{ id }}" type="button" class="btn btn-sm btn-light btn-outline-danger border"><i class="ri-eraser-line"></i></button>
+ {% else %}
+ <span class="input-group-text ri-prohibited-line"></span>
{% endif %}
</div>
{% endif %}
From 130b3fa84a34a9c33a77bd0b67e470bb02af4d37 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 0f32240b..6564e8d0 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 <versatile.mailbox@gmail.com>
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 8cb005f7..89f012da 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 <versatile.mailbox@gmail.com>
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 d82896c0..809d46b4 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('/<id>/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('/<id>/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('/<id>/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 <versatile.mailbox@gmail.com>
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 02d77b30..0abb2c16 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') }}
+ <a href="{{ item.url_for_refresh() }}" class="btn btn-primary" role="button"><i class="ri-refresh-line"></i> Refresh the set</a>
+ {{ accordion.footer() }}
+ {% endif %}
{% endif %}
{% if g.login.is_authenticated() %}
- {{ accordion.header('Management', 'management', 'set-details', icon='settings-4-line', class='text-end') }}
- <a href="{{ item.url_for_refresh() }}" class="btn btn-primary" role="button"><i class="ri-refresh-line"></i> Refresh the set</a>
- {{ accordion.footer() }}
{{ accordion.header('Danger zone', 'danger-zone', 'set-details', expanded=delete, danger=true, class='text-end') }}
{% if delete %}
<form action="{{ item.url_for_do_delete() }}" method="post">
From 160ab066b2b0c3c5591de3c4b1c26b48b5511a70 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 0647bcb2..2d48e3ba 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 861b8a9e..6c38ed91 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 <versatile.mailbox@gmail.com>
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 495059f3..ac85a114 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 8224d98f..eea6d083 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 <versatile.mailbox@gmail.com>
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 fb5b7235..7cf3c9e8 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 ac85a114..58f65100 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 <versatile.mailbox@gmail.com>
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 0abb2c16..fee30404 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') }}
- <a href="{{ item.url_for_refresh() }}" class="btn btn-primary" role="button"><i class="ri-refresh-line"></i> Refresh the set</a>
- {{ 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 00000000..b27c9d05
--- /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') }}
+ <a href="{{ item.url_for_refresh() }}" class="btn btn-primary" role="button"><i class="ri-refresh-line"></i> Refresh the set data</a>
+{{ accordion.footer() }}
+{% endif %}
From b2d2019bfd978edf96a7b273bcea73f53a08393d Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 fbf10f1a..d2878d6c 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 28f0341c..893acc05 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 8b1f4c88..7bc1bc37 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 00000000..1d884edf
--- /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 3ee1068b..df677f4d 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 22cac8ee..af1667ed 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 809d46b4..59289ff0 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('/<id>/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 b27c9d05..b2999728 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') }}
+ <h5>Theme override</h5>
+ <p>
+ 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) }}
+ </p>
+ <h5 class="border-bottom">Data</h5>
<a href="{{ item.url_for_refresh() }}" class="btn btn-primary" role="button"><i class="ri-refresh-line"></i> Refresh the set data</a>
{{ accordion.footer() }}
{% endif %}
From 51f729a18b74e36c18fdc4abc0f194280ecfb91c Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 58f65100..64d71df8 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 <versatile.mailbox@gmail.com>
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 d2878d6c..fbf10f1a 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 893acc05..fa05b0b3 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 f955bb9d..89ef71f8 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 7bc1bc37..8b1f4c88 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 1d884edf..00000000
--- 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 df677f4d..3ee1068b 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 af1667ed..22cac8ee 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 59289ff0..809d46b4 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('/<id>/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 b2999728..38074808 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') }}
- <h5>Theme override</h5>
- <p>
- 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) }}
- </p>
<h5 class="border-bottom">Data</h5>
<a href="{{ item.url_for_refresh() }}" class="btn btn-primary" role="button"><i class="ri-refresh-line"></i> Refresh the set data</a>
{{ accordion.footer() }}
From aed7a520bd30ac1603050b872cab1ff3ffa74408 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 c034ea85..67a97a19 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 b78c390e..aa01b79c 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 <versatile.mailbox@gmail.com>
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 847e42a7..5d6a4e4c 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 bd0f2137..e2fc4bc2 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 97043653..7a15325a 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 %}<div class="alert alert-danger text-start" role="alert"><strong>Error:</strong> {{ error }}.</div>{% endif %}
+{% if database_error %}<div class="alert alert-danger text-start" role="alert"><strong>Error:</strong> {{ database_error }}.</div>{% endif %}
<a href="{{ url_for('admin_database.upload') }}" class="btn btn-warning" role="button"><i class="ri-upload-line"></i> Import a database file</a>
<a href="{{ url_for('admin_database.drop') }}" class="btn btn-danger" role="button"><i class="ri-close-line"></i> Drop the database</a>
<a href="{{ url_for('admin_database.delete') }}" class="btn btn-danger" role="button"><i class="ri-delete-bin-2-line"></i> Delete the database file</a>
diff --git a/templates/admin/database/delete.html b/templates/admin/database/delete.html
index 9a2d2861..9bbbf2f0 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') }}
<form action="{{ url_for('admin_database.do_delete') }}" method="post">
- {% if error %}<div class="alert alert-danger text-start" role="alert"><strong>Error:</strong> {{ error }}.</div>{% endif %}
+ {% if database_error %}<div class="alert alert-danger text-start" role="alert"><strong>Error:</strong> {{ database_error }}.</div>{% endif %}
<div class="alert alert-danger text-center" role="alert">You are about to <strong>delete the database file</strong>. This action is irreversible.</div>
<a class="btn btn-danger" href="{{ url_for('admin.admin') }}" role="button"><i class="ri-arrow-left-long-line"></i> Back to the admin</a>
<button type="submit" class="btn btn-danger"><i class="ri-delete-bin-2-line"></i> Delete <strong>the database file</strong></button>
diff --git a/templates/admin/database/drop.html b/templates/admin/database/drop.html
index e0f5b2e9..1ebe0d58 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') }}
<form action="{{ url_for('admin_database.do_drop') }}" method="post">
- {% if error %}<div class="alert alert-danger text-start" role="alert"><strong>Error:</strong> {{ error }}.</div>{% endif %}
+ {% if database_error %}<div class="alert alert-danger text-start" role="alert"><strong>Error:</strong> {{ database_error }}.</div>{% endif %}
<div class="alert alert-danger text-center" role="alert">You are about to <strong>drop all the tables from the database</strong>. This action is irreversible.</div>
<a class="btn btn-danger" href="{{ url_for('admin.admin') }}" role="button"><i class="ri-arrow-left-long-line"></i> Back to the admin</a>
<button type="submit" class="btn btn-danger"><i class="ri-close-line"></i> Drop <strong>the whole database</strong></button>
diff --git a/templates/admin/database/import.html b/templates/admin/database/import.html
index f7a77634..a6d5e9c6 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) }}
<form action="{{ url_for('admin_database.do_upload') }}" method="post" enctype="multipart/form-data">
- {% if error %}<div class="alert alert-danger text-start" role="alert"><strong>Error:</strong> {{ error }}.</div>{% endif %}
+ {% if database_error %}<div class="alert alert-danger text-start" role="alert"><strong>Error:</strong> {{ database_error }}.</div>{% endif %}
<div class="alert alert-warning text-center" role="alert">You are about to <strong>import a database file</strong>. This will replace the <strong>whole content</strong> of the database. This action is irreversible.</div>
<div class="mb-3">
<label for="database" class="form-label">New database file</label>
diff --git a/templates/admin/database/upgrade.html b/templates/admin/database/upgrade.html
index 2739c79c..29e59ef1 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') }}
<form action="{{ url_for('admin_database.do_upgrade') }}" method="post">
- {% if error %}<div class="alert alert-danger text-start" role="alert"><strong>Error:</strong> {{ error }}.</div>{% endif %}
+ {% if database_error %}<div class="alert alert-danger text-start" role="alert"><strong>Error:</strong> {{ database_error }}.</div>{% endif %}
<div class="alert alert-warning text-center" role="alert">
You are about to <strong>upgrade your database file</strong>. This action is irreversible.<br>
The process shold be lossless, but just to be sure, grab a copy of your database before proceeding.<br>
From 4fc96ec38fd7e94e38abb36d04ecbb260dd1fd9e Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Thu, 30 Jan 2025 09:33:54 +0100
Subject: [PATCH 054/154] Rename checkox_error
---
bricktracker/views/admin/admin.py | 1 +
bricktracker/views/admin/checkbox.py | 22 ++++++++++++++++++----
templates/admin/checkbox.html | 3 +--
3 files changed, 20 insertions(+), 6 deletions(-)
diff --git a/bricktracker/views/admin/admin.py b/bricktracker/views/admin/admin.py
index 5d6a4e4c..d5586eab 100644
--- a/bricktracker/views/admin/admin.py
+++ b/bricktracker/views/admin/admin.py
@@ -82,6 +82,7 @@ def admin() -> str:
'admin.html',
configuration=BrickConfigurationList.list(),
brickset_checkboxes=brickset_checkboxes,
+ checkbox_error=request.args.get('checkbox_error'),
database_counters=database_counters,
database_error=request.args.get('database_error'),
database_exception=database_exception,
diff --git a/bricktracker/views/admin/checkbox.py b/bricktracker/views/admin/checkbox.py
index 1dd58bbb..134c8861 100644
--- a/bricktracker/views/admin/checkbox.py
+++ b/bricktracker/views/admin/checkbox.py
@@ -27,7 +27,12 @@ admin_checkbox_page = Blueprint(
# Add a checkbox
@admin_checkbox_page.route('/add', methods=['POST'])
@login_required
-@exception_handler(__file__, post_redirect='admin.admin', open_checkbox=True)
+@exception_handler(
+ __file__,
+ post_redirect='admin.admin',
+ error_name='checkbox_error',
+ open_checkbox=True
+)
def add() -> Response:
BrickSetCheckbox().from_form(request.form).insert()
@@ -45,14 +50,18 @@ def delete(*, id: str) -> str:
'admin.html',
delete_checkbox=True,
checkbox=BrickSetCheckbox().select_specific(id),
- error=request.args.get('error')
+ error=request.args.get('checkbox_error')
)
# Actually delete the checkbox
@admin_checkbox_page.route('<id>/delete', methods=['POST'])
@login_required
-@exception_handler(__file__, post_redirect='admin_checkbox.delete')
+@exception_handler(
+ __file__,
+ post_redirect='admin_checkbox.delete',
+ error_name='checkbox_error'
+)
def do_delete(*, id: str) -> Response:
checkbox = BrickSetCheckbox().select_specific(id)
checkbox.delete()
@@ -88,7 +97,12 @@ def update_status(*, id: str, name: str) -> Response:
# Rename the checkbox
@admin_checkbox_page.route('<id>/rename', methods=['POST'])
@login_required
-@exception_handler(__file__, post_redirect='admin.admin', open_checkbox=True)
+@exception_handler(
+ __file__,
+ post_redirect='admin.admin',
+ error_name='checkbox_error',
+ open_checkbox=True
+)
def rename(*, id: str) -> Response:
checkbox = BrickSetCheckbox().select_specific(id)
checkbox.from_form(request.form).rename()
diff --git a/templates/admin/checkbox.html b/templates/admin/checkbox.html
index dbe53606..0ba484ab 100644
--- a/templates/admin/checkbox.html
+++ b/templates/admin/checkbox.html
@@ -1,8 +1,7 @@
{% import 'macro/accordion.html' as accordion %}
{{ accordion.header('Checkboxes', 'checkbox', 'admin', expanded=open_checkbox, icon='checkbox-line', class='p-0') }}
-{% if error %}<div class="alert alert-danger m-2" role="alert"><strong>Error:</strong> {{ error }}.</div>{% endif %}
-{% if database_error %}<div class="alert alert-danger m-2" role="alert"><strong>Error:</strong> {{ database_error }}.</div>{% endif %}
+{% if checkbox_error %}<div class="alert alert-danger m-2" role="alert"><strong>Error:</strong> {{ checkbox_error }}.</div>{% endif %}
<ul class="list-group list-group-flush">
{% if brickset_checkboxes | length %}
{% for checkbox in brickset_checkboxes %}
From a832ff27f799b0144a1b9c2339f171c5d3a7272a Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Thu, 30 Jan 2025 12:17:25 +0100
Subject: [PATCH 055/154] Create a Metadata object as a base for checkboxes
---
bricktracker/metadata.py | 197 ++++++++++++++++++
bricktracker/set.py | 27 +--
bricktracker/set_checkbox.py | 134 ++----------
.../sql/checkbox/{add.sql => insert.sql} | 0
.../checkbox/update/{name.sql => field.sql} | 2 +-
bricktracker/sql/checkbox/update/status.sql | 3 -
bricktracker/sql/set/update/status.sql | 8 +-
bricktracker/views/admin/checkbox.py | 22 +-
bricktracker/views/set.py | 20 +-
templates/admin/checkbox.html | 2 +-
templates/set/card.html | 2 +-
11 files changed, 231 insertions(+), 186 deletions(-)
create mode 100644 bricktracker/metadata.py
rename bricktracker/sql/checkbox/{add.sql => insert.sql} (100%)
rename bricktracker/sql/checkbox/update/{name.sql => field.sql} (80%)
delete mode 100644 bricktracker/sql/checkbox/update/status.sql
diff --git a/bricktracker/metadata.py b/bricktracker/metadata.py
new file mode 100644
index 00000000..df93c93f
--- /dev/null
+++ b/bricktracker/metadata.py
@@ -0,0 +1,197 @@
+import logging
+from sqlite3 import Row
+from typing import Any, Self, TYPE_CHECKING
+from uuid import uuid4
+
+from flask import url_for
+
+from .exceptions import DatabaseException, ErrorException, NotFoundException
+from .record import BrickRecord
+from .sql import BrickSQL
+if TYPE_CHECKING:
+ from .set import BrickSet
+
+logger = logging.getLogger(__name__)
+
+
+# Lego set metadata (customizable list of entries that can be checked)
+class BrickMetadata(BrickRecord):
+ kind: str
+ prefix: str
+
+ # Set state endpoint
+ set_state_endpoint: str
+
+ # Queries
+ delete_query: str
+ insert_query: str
+ select_query: str
+ update_field_query: str
+ update_set_state_query: str
+
+ def __init__(
+ self,
+ /,
+ *,
+ record: Row | dict[str, Any] | None = None,
+ ):
+ super().__init__()
+
+ # Ingest the record if it has one
+ if record is not None:
+ self.ingest(record)
+
+ # SQL column name
+ def as_column(self, /) -> str:
+ return '{prefix}_{id}'.format(id=self.fields.id, prefix=self.prefix)
+
+ # HTML dataset name
+ def as_dataset(self, /) -> str:
+ return '{id}'.format(
+ id=self.as_column().replace('_', '-')
+ )
+
+ # Delete from database
+ def delete(self, /) -> None:
+ BrickSQL().executescript(
+ self.delete_query,
+ id=self.fields.id,
+ )
+
+ # Insert into database
+ def insert(self, /, **context) -> None:
+ self.safe()
+
+ # Generate an ID for the metadata (with underscores to make it
+ # column name friendly)
+ self.fields.id = str(uuid4()).replace('-', '_')
+
+ BrickSQL().executescript(
+ self.insert_query,
+ id=self.fields.id,
+ name=self.fields.safe_name,
+ **context
+ )
+
+ # Rename the entry
+ def rename(self, /) -> None:
+ self.safe()
+
+ self.update_field('name', value=self.fields.name)
+
+ # Make the name "safe"
+ # Security: eh.
+ def safe(self, /) -> None:
+ # Prevent self-ownage with accidental quote escape
+ self.fields.safe_name = self.fields.name.replace("'", "''")
+
+ # URL to change the selected state of this metadata item for a set
+ def url_for_set_state(self, id: str, /) -> str:
+ return url_for(
+ self.set_state_endpoint,
+ id=id,
+ metadata_id=self.fields.id
+ )
+
+ # Select a specific checkbox (with an id)
+ def select_specific(self, id: str, /) -> Self:
+ # Save the parameters to the fields
+ self.fields.id = id
+
+ # Load from database
+ if not self.select():
+ raise NotFoundException(
+ '{kind} with ID {id} was not found in the database'.format(
+ kind=self.kind.capitalize(),
+ id=self.fields.id,
+ ),
+ )
+
+ return self
+
+ # Update a field
+ def update_field(
+ self,
+ field: str,
+ /,
+ *,
+ json: Any | None = None,
+ value: Any | None = None
+ ) -> Any:
+ if value is None:
+ value = json.get('value', None) # type: ignore
+
+ if value is None:
+ raise ErrorException('"{field}" of a {kind} cannot be set to an empty value'.format( # noqa: E501
+ field=field,
+ kind=self.kind
+ ))
+
+ if field == 'id' or not hasattr(self.fields, field):
+ raise NotFoundException('"{field}" is not a field of a {kind}'.format( # noqa: E501
+ kind=self.kind,
+ field=field
+ ))
+
+ parameters = self.sql_parameters()
+ parameters['value'] = value
+
+ # Update the status
+ rows, _ = BrickSQL().execute_and_commit(
+ self.update_field_query,
+ parameters=parameters,
+ field=field,
+ )
+
+ if rows != 1:
+ raise DatabaseException('Could not update the field "{field}" for {kind} {name} ({id})'.format( # noqa: E501
+ field=field,
+ kind=self.kind,
+ name=self.fields.name,
+ id=self.fields.id,
+ ))
+
+ # Info
+ logger.info('{kind} "{name}" ({id}): field "{field}" changed to "{value}"'.format( # noqa: E501
+ kind=self.kind.capitalize(),
+ name=self.fields.name,
+ id=self.fields.id,
+ field=field,
+ value=value,
+ ))
+
+ 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
+
+ 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,
+ name=self.as_column(),
+ )
+
+ if rows != 1:
+ raise DatabaseException('Could not update the {kind} "{name}" state for set {set} ({id})'.format( # noqa: E501
+ kind=self.kind,
+ name=self.fields.name,
+ set=brickset.fields.set,
+ id=brickset.fields.id,
+ ))
+
+ # Info
+ logger.info('{kind} "{name}" state change 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
diff --git a/bricktracker/set.py b/bricktracker/set.py
index fa05b0b3..6b2da47a 100644
--- a/bricktracker/set.py
+++ b/bricktracker/set.py
@@ -5,11 +5,10 @@ from uuid import uuid4
from flask import current_app, url_for
-from .exceptions import DatabaseException, NotFoundException
+from .exceptions import NotFoundException
from .minifigure_list import BrickMinifigureList
from .part_list import BrickPartList
from .rebrickable_set import RebrickableSet
-from .set_checkbox import BrickSetCheckbox
from .set_checkbox_list import BrickSetCheckboxList
from .sql import BrickSQL
if TYPE_CHECKING:
@@ -172,30 +171,6 @@ class BrickSet(RebrickableSet):
return self
- # Update a status
- def update_status(
- self,
- checkbox: BrickSetCheckbox,
- status: bool,
- /
- ) -> None:
- parameters = self.sql_parameters()
- parameters['status'] = status
-
- # Update the status
- rows, _ = BrickSQL().execute_and_commit(
- 'set/update/status',
- parameters=parameters,
- name=checkbox.as_column(),
- )
-
- if rows != 1:
- raise DatabaseException('Could not update the status "{status}" for set {set} ({id})'.format( # noqa: E501
- status=checkbox.fields.name,
- set=self.fields.set,
- id=self.fields.id,
- ))
-
# Self url
def url(self, /) -> str:
return url_for('set.details', id=self.fields.id)
diff --git a/bricktracker/set_checkbox.py b/bricktracker/set_checkbox.py
index 38a10f0d..f38b5f6e 100644
--- a/bricktracker/set_checkbox.py
+++ b/bricktracker/set_checkbox.py
@@ -1,139 +1,39 @@
-from sqlite3 import Row
-from typing import Any, Self
-from uuid import uuid4
+from typing import Self
-from flask import url_for
-
-from .exceptions import DatabaseException, ErrorException, NotFoundException
-from .record import BrickRecord
-from .sql import BrickSQL
+from .exceptions import ErrorException
+from .metadata import BrickMetadata
# Lego set checkbox
-class BrickSetCheckbox(BrickRecord):
+class BrickSetCheckbox(BrickMetadata):
+ kind: str = 'checkbox'
+ prefix: str = 'status'
+
+ # Set state endpoint
+ set_state_endpoint: str = 'set.update_status'
+
# Queries
+ delete_query: str = 'checkbox/delete'
+ insert_query: str = 'checkbox/insert'
select_query: str = 'checkbox/select'
-
- def __init__(
- self,
- /,
- *,
- record: Row | dict[str, Any] | None = None,
- ):
- super().__init__()
-
- # Ingest the record if it has one
- if record is not None:
- self.ingest(record)
-
- # SQL column name
- def as_column(self) -> str:
- return 'status_{id}'.format(id=self.fields.id)
-
- # HTML dataset name
- def as_dataset(self) -> str:
- return '{id}'.format(
- id=self.as_column().replace('_', '-')
- )
-
- # Delete from database
- def delete(self) -> None:
- BrickSQL().executescript(
- 'checkbox/delete',
- id=self.fields.id,
- )
+ update_field_query: str = 'checkbox/update/field'
+ update_set_state_query: str = 'set/update/status'
# Grab data from a form
- def from_form(self, form: dict[str, str]) -> Self:
+ def from_form(self, form: dict[str, str], /) -> Self:
name = form.get('name', None)
grid = form.get('grid', None)
if name is None or name == '':
raise ErrorException('Checkbox name cannot be empty')
- # Security: eh.
- # Prevent self-ownage with accidental quote escape
self.fields.name = name
- self.fields.safe_name = self.fields.name.replace("'", "''")
self.fields.displayed_on_grid = grid == 'on'
return self
# Insert into database
- def insert(self, **_) -> None:
- # Generate an ID for the checkbox (with underscores to make it
- # column name friendly)
- self.fields.id = str(uuid4()).replace('-', '_')
-
- BrickSQL().executescript(
- 'checkbox/add',
- id=self.fields.id,
- name=self.fields.safe_name,
+ def insert(self, /, **_) -> None:
+ super().insert(
displayed_on_grid=self.fields.displayed_on_grid
)
-
- # Rename the checkbox
- def rename(self, /) -> None:
- # Update the name
- rows, _ = BrickSQL().execute_and_commit(
- 'checkbox/update/name',
- parameters=self.sql_parameters(),
- )
-
- if rows != 1:
- raise DatabaseException('Could not update the name for checkbox {name} ({id})'.format( # noqa: E501
- name=self.fields.name,
- id=self.fields.id,
- ))
-
- # URL to change the status
- def status_url(self, id: str) -> str:
- return url_for(
- 'set.update_status',
- id=id,
- checkbox_id=self.fields.id
- )
-
- # Select a specific checkbox (with an id)
- def select_specific(self, id: str, /) -> Self:
- # Save the parameters to the fields
- self.fields.id = id
-
- # Load from database
- if not self.select():
- raise NotFoundException(
- 'Checkbox with ID {id} was not found in the database'.format(
- id=self.fields.id,
- ),
- )
-
- return self
-
- # Update a status
- def update_status(
- self,
- name: str,
- status: bool,
- /
- ) -> None:
- if not hasattr(self.fields, name) or name in ['id', 'name']:
- raise NotFoundException('{name} is not a field of a checkbox'.format( # noqa: E501
- name=name
- ))
-
- parameters = self.sql_parameters()
- parameters['status'] = status
-
- # Update the status
- rows, _ = BrickSQL().execute_and_commit(
- 'checkbox/update/status',
- parameters=parameters,
- name=name,
- )
-
- if rows != 1:
- raise DatabaseException('Could not update the status "{status}" for checkbox {name} ({id})'.format( # noqa: E501
- status=name,
- name=self.fields.name,
- id=self.fields.id,
- ))
diff --git a/bricktracker/sql/checkbox/add.sql b/bricktracker/sql/checkbox/insert.sql
similarity index 100%
rename from bricktracker/sql/checkbox/add.sql
rename to bricktracker/sql/checkbox/insert.sql
diff --git a/bricktracker/sql/checkbox/update/name.sql b/bricktracker/sql/checkbox/update/field.sql
similarity index 80%
rename from bricktracker/sql/checkbox/update/name.sql
rename to bricktracker/sql/checkbox/update/field.sql
index 19fccc03..a65e3c09 100644
--- a/bricktracker/sql/checkbox/update/name.sql
+++ b/bricktracker/sql/checkbox/update/field.sql
@@ -1,3 +1,3 @@
UPDATE "bricktracker_set_checkboxes"
-SET "name" = :safe_name
+SET "{{field}}" = :value
WHERE "bricktracker_set_checkboxes"."id" IS NOT DISTINCT FROM :id
diff --git a/bricktracker/sql/checkbox/update/status.sql b/bricktracker/sql/checkbox/update/status.sql
deleted file mode 100644
index 3c04c22d..00000000
--- a/bricktracker/sql/checkbox/update/status.sql
+++ /dev/null
@@ -1,3 +0,0 @@
-UPDATE "bricktracker_set_checkboxes"
-SET "{{name}}" = :status
-WHERE "bricktracker_set_checkboxes"."id" IS NOT DISTINCT FROM :id
diff --git a/bricktracker/sql/set/update/status.sql b/bricktracker/sql/set/update/status.sql
index 4fc78e46..7697ca5c 100644
--- a/bricktracker/sql/set/update/status.sql
+++ b/bricktracker/sql/set/update/status.sql
@@ -2,9 +2,9 @@ INSERT INTO "bricktracker_set_statuses" (
"id",
"{{name}}"
) VALUES (
- :id,
- :status
+ :set_id,
+ :state
)
ON CONFLICT("id")
-DO UPDATE SET "{{name}}" = :status
-WHERE "bricktracker_set_statuses"."id" IS NOT DISTINCT FROM :id
+DO UPDATE SET "{{name}}" = :state
+WHERE "bricktracker_set_statuses"."id" IS NOT DISTINCT FROM :set_id
diff --git a/bricktracker/views/admin/checkbox.py b/bricktracker/views/admin/checkbox.py
index 134c8861..6db6c5df 100644
--- a/bricktracker/views/admin/checkbox.py
+++ b/bricktracker/views/admin/checkbox.py
@@ -1,5 +1,3 @@
-import logging
-
from flask import (
Blueprint,
jsonify,
@@ -15,8 +13,6 @@ from ..exceptions import exception_handler
from ...reload import reload
from ...set_checkbox import BrickSetCheckbox
-logger = logging.getLogger(__name__)
-
admin_checkbox_page = Blueprint(
'admin_checkbox',
__name__,
@@ -71,23 +67,13 @@ def do_delete(*, id: str) -> Response:
return redirect(url_for('admin.admin', open_checkbox=True))
-# Change the status of a checkbox
-@admin_checkbox_page.route('/<id>/status/<name>', methods=['POST'])
+# Change the field of a checkbox
+@admin_checkbox_page.route('/<id>/field/<name>', methods=['POST'])
@login_required
@exception_handler(__file__, json=True)
-def update_status(*, id: str, name: str) -> Response:
- value: bool = request.json.get('value', False) # type: ignore
-
+def update_field(*, id: str, name: str) -> Response:
checkbox = BrickSetCheckbox().select_specific(id)
- checkbox.update_status(name, value)
-
- # Info
- logger.info('Checkbox {name} ({id}): status "{status}" changed to "{state}"'.format( # noqa: E501
- name=checkbox.fields.name,
- id=checkbox.fields.id,
- status=name,
- state=value,
- ))
+ value = checkbox.update_field(name, json=request.json)
reload()
diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py
index 809d46b4..bb3c2348 100644
--- a/bricktracker/views/set.py
+++ b/bricktracker/views/set.py
@@ -37,26 +37,16 @@ def list() -> str:
# Change the status of a checkbox
-@set_page.route('/<id>/status/<checkbox_id>', methods=['POST'])
+@set_page.route('/<id>/status/<metadata_id>', methods=['POST'])
@login_required
@exception_handler(__file__, json=True)
-def update_status(*, id: str, checkbox_id: str) -> Response:
- value: bool = request.json.get('value', False) # type: ignore
-
+def update_status(*, id: str, metadata_id: str) -> Response:
brickset = BrickSet().select_light(id)
- checkbox = BrickSetCheckboxList().get(checkbox_id)
+ checkbox = BrickSetCheckboxList().get(metadata_id)
- brickset.update_status(checkbox, value)
+ state = checkbox.update_set_state(brickset, request.json)
- # Info
- 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,
- ))
-
- return jsonify({'value': value})
+ return jsonify({'value': state})
# Ask for deletion of a set
diff --git a/templates/admin/checkbox.html b/templates/admin/checkbox.html
index 0ba484ab..22a3215b 100644
--- a/templates/admin/checkbox.html
+++ b/templates/admin/checkbox.html
@@ -18,7 +18,7 @@
<div class="col-12">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="grid-{{ checkbox.fields.id }}"
- 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')}}"
+ data-changer-id="{{ checkbox.fields.id }}" data-changer-prefix="grid" data-changer-url="{{ url_for('admin_checkbox.update_field', id=checkbox.fields.id, name='displayed_on_grid')}}"
{% if checkbox.fields.displayed_on_grid %}checked{% endif %} autocomplete="off">
<label class="form-check-label" for="grid-{{ checkbox.fields.id }}">
<i class="ri-grid-line"></i> Displayed on the Set Grid
diff --git a/templates/set/card.html b/templates/set/card.html
index fee30404..6a658c98 100644
--- a/templates/set/card.html
+++ b/templates/set/card.html
@@ -30,7 +30,7 @@
<ul class="list-group list-group-flush card-check border-bottom-0">
{% for checkbox in brickset_checkboxes %}
<li class="list-group-item {% if not solo %}p-1{% endif %}">
- {{ form.checkbox(checkbox.as_dataset(), item.fields.id, checkbox.fields.name, checkbox.status_url(item.fields.id), item.fields[checkbox.as_column()], delete=delete) }}
+ {{ form.checkbox(checkbox.as_dataset(), item.fields.id, checkbox.fields.name, checkbox.url_for_set_state(item.fields.id), item.fields[checkbox.as_column()], delete=delete) }}
</li>
{% endfor %}
</ul>
From 8f5d59394c542c54680c5bc13d15add71db435f2 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Thu, 30 Jan 2025 12:42:37 +0100
Subject: [PATCH 056/154] Remove the 404 code from post redirect as it will
cause the browser to not redirect
---
bricktracker/views/error.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/bricktracker/views/error.py b/bricktracker/views/error.py
index 67a97a19..3a1c7427 100644
--- a/bricktracker/views/error.py
+++ b/bricktracker/views/error.py
@@ -138,7 +138,7 @@ def error_404(
post_redirect: str | None = None,
error_name: str = 'error',
**kwargs,
-) -> Tuple[str | Response, int]:
+) -> Response | Tuple[str | Response, int]:
# Warning
logger.warning('Not found: {error} (path: {path})'.format(
error=str(error),
@@ -156,7 +156,7 @@ def error_404(
error=str(error),
error_name=error_name,
**kwargs,
- ), 404
+ )
else:
return render_template('404.html', error=str(error)), 404
From 050b1993da41ec035f68a28c99526f6a4a24965a Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Thu, 30 Jan 2025 12:56:14 +0100
Subject: [PATCH 057/154] Don't rely on SQL files for migration patches as
their existence is not guaranteed
---
bricktracker/migrations/0007.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/bricktracker/migrations/0007.py b/bricktracker/migrations/0007.py
index 7cf3c9e8..9b0f5892 100644
--- a/bricktracker/migrations/0007.py
+++ b/bricktracker/migrations/0007.py
@@ -5,8 +5,10 @@ if TYPE_CHECKING:
# 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')
+def migration_0007(sql: 'BrickSQL', /) -> dict[str, Any]:
+ # Don't realy on sql files as they could be removed in the future
+ sql.cursor.execute('SELECT "bricktracker_set_checkboxes"."id" FROM "bricktracker_set_checkboxes') # noqa: E501
+ records = sql.cursor.fetchall()
return {
'sources': ', '.join([
From 7d16e491c8ea063f0fb75afdd1918255fd0878e7 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Thu, 30 Jan 2025 15:03:16 +0100
Subject: [PATCH 058/154] Rename checkboxes (too generic) to status (and some
bug fixes)
---
bricktracker/app.py | 4 +-
bricktracker/metadata.py | 2 +-
bricktracker/migrations/0007.py | 2 +-
bricktracker/record_list.py | 4 +-
bricktracker/reload.py | 6 +-
bricktracker/set.py | 4 +-
bricktracker/set_list.py | 6 +-
.../{set_checkbox.py => set_status.py} | 18 ++--
...et_checkbox_list.py => set_status_list.py} | 42 ++++----
bricktracker/sql.py | 7 +-
bricktracker/sql/checkbox/base.sql | 7 --
bricktracker/sql/checkbox/delete.sql | 9 --
bricktracker/sql/checkbox/list.sql | 1 -
bricktracker/sql/checkbox/select.sql | 5 -
bricktracker/sql/checkbox/update/field.sql | 3 -
bricktracker/sql/migrations/0012.sql | 7 ++
bricktracker/sql/schema/drop.sql | 1 +
bricktracker/sql/set/metadata/status/base.sql | 7 ++
.../sql/set/metadata/status/delete.sql | 9 ++
.../metadata/status}/insert.sql | 2 +-
bricktracker/sql/set/metadata/status/list.sql | 1 +
.../sql/set/metadata/status/select.sql | 5 +
.../sql/set/metadata/status/update/field.sql | 3 +
.../status/update/state.sql} | 0
bricktracker/sql_counter.py | 7 +-
bricktracker/version.py | 2 +-
bricktracker/views/admin/admin.py | 18 ++--
bricktracker/views/admin/checkbox.py | 98 ------------------
bricktracker/views/admin/status.py | 98 ++++++++++++++++++
bricktracker/views/index.py | 4 +-
bricktracker/views/set.py | 12 +--
docs/DOCS.md | 2 +-
docs/checkbox.md | 58 -----------
.../images/{checkbox-01.png => status-01.png} | Bin
.../images/{checkbox-02.png => status-02.png} | Bin
.../images/{checkbox-03.png => status-03.png} | Bin
.../images/{checkbox-04.png => status-04.png} | Bin
.../images/{checkbox-05.png => status-05.png} | Bin
.../images/{checkbox-06.png => status-06.png} | Bin
.../images/{checkbox-07.png => status-07.png} | Bin
docs/set-statuses.md | 67 ++++++++++++
templates/admin.html | 6 +-
.../admin/{checkbox.html => status.html} | 30 +++---
.../admin/{checkbox => status}/delete.html | 14 +--
templates/set/card.html | 8 +-
templates/sets.html | 6 +-
46 files changed, 304 insertions(+), 281 deletions(-)
rename bricktracker/{set_checkbox.py => set_status.py} (60%)
rename bricktracker/{set_checkbox_list.py => set_status_list.py} (51%)
delete mode 100644 bricktracker/sql/checkbox/base.sql
delete mode 100644 bricktracker/sql/checkbox/delete.sql
delete mode 100644 bricktracker/sql/checkbox/list.sql
delete mode 100644 bricktracker/sql/checkbox/select.sql
delete mode 100644 bricktracker/sql/checkbox/update/field.sql
create mode 100644 bricktracker/sql/migrations/0012.sql
create mode 100644 bricktracker/sql/set/metadata/status/base.sql
create mode 100644 bricktracker/sql/set/metadata/status/delete.sql
rename bricktracker/sql/{checkbox => set/metadata/status}/insert.sql (84%)
create mode 100644 bricktracker/sql/set/metadata/status/list.sql
create mode 100644 bricktracker/sql/set/metadata/status/select.sql
create mode 100644 bricktracker/sql/set/metadata/status/update/field.sql
rename bricktracker/sql/set/{update/status.sql => metadata/status/update/state.sql} (100%)
delete mode 100644 bricktracker/views/admin/checkbox.py
create mode 100644 bricktracker/views/admin/status.py
delete mode 100644 docs/checkbox.md
rename docs/images/{checkbox-01.png => status-01.png} (100%)
rename docs/images/{checkbox-02.png => status-02.png} (100%)
rename docs/images/{checkbox-03.png => status-03.png} (100%)
rename docs/images/{checkbox-04.png => status-04.png} (100%)
rename docs/images/{checkbox-05.png => status-05.png} (100%)
rename docs/images/{checkbox-06.png => status-06.png} (100%)
rename docs/images/{checkbox-07.png => status-07.png} (100%)
create mode 100644 docs/set-statuses.md
rename templates/admin/{checkbox.html => status.html} (54%)
rename templates/admin/{checkbox => status}/delete.html (53%)
diff --git a/bricktracker/app.py b/bricktracker/app.py
index a55a9f77..f6054bcb 100644
--- a/bricktracker/app.py
+++ b/bricktracker/app.py
@@ -13,7 +13,7 @@ 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.checkbox import admin_checkbox_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
@@ -78,7 +78,7 @@ def setup_app(app: Flask) -> None:
# Register admin routes
app.register_blueprint(admin_page)
- app.register_blueprint(admin_checkbox_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)
diff --git a/bricktracker/metadata.py b/bricktracker/metadata.py
index df93c93f..e21bfdd4 100644
--- a/bricktracker/metadata.py
+++ b/bricktracker/metadata.py
@@ -93,7 +93,7 @@ class BrickMetadata(BrickRecord):
metadata_id=self.fields.id
)
- # Select a specific checkbox (with an id)
+ # Select a specific metadata (with an id)
def select_specific(self, id: str, /) -> Self:
# Save the parameters to the fields
self.fields.id = id
diff --git a/bricktracker/migrations/0007.py b/bricktracker/migrations/0007.py
index 9b0f5892..81e4535b 100644
--- a/bricktracker/migrations/0007.py
+++ b/bricktracker/migrations/0007.py
@@ -7,7 +7,7 @@ if TYPE_CHECKING:
# Grab the list of checkboxes to create a list of SQL columns
def migration_0007(sql: 'BrickSQL', /) -> dict[str, Any]:
# Don't realy on sql files as they could be removed in the future
- sql.cursor.execute('SELECT "bricktracker_set_checkboxes"."id" FROM "bricktracker_set_checkboxes') # noqa: E501
+ sql.cursor.execute('SELECT "bricktracker_set_checkboxes"."id" FROM "bricktracker_set_checkboxes"') # noqa: E501
records = sql.cursor.fetchall()
return {
diff --git a/bricktracker/record_list.py b/bricktracker/record_list.py
index 07989916..22752032 100644
--- a/bricktracker/record_list.py
+++ b/bricktracker/record_list.py
@@ -8,13 +8,13 @@ if TYPE_CHECKING:
from .part import BrickPart
from .rebrickable_set import RebrickableSet
from .set import BrickSet
- from .set_checkbox import BrickSetCheckbox
+ from .set_status import BrickSetStatus
from .wish import BrickWish
T = TypeVar(
'T',
'BrickSet',
- 'BrickSetCheckbox',
+ 'BrickSetStatus',
'BrickPart',
'BrickMinifigure',
'BrickWish',
diff --git a/bricktracker/reload.py b/bricktracker/reload.py
index 62564df1..28de7bff 100644
--- a/bricktracker/reload.py
+++ b/bricktracker/reload.py
@@ -1,6 +1,6 @@
from .instructions_list import BrickInstructionsList
from .retired_list import BrickRetiredList
-from .set_checkbox_list import BrickSetCheckboxList
+from .set_status_list import BrickSetStatusList
from .theme_list import BrickThemeList
@@ -11,8 +11,8 @@ def reload() -> None:
# Reload the instructions
BrickInstructionsList(force=True)
- # Reload the checkboxes
- BrickSetCheckboxList(force=True)
+ # Reload the set statuses
+ BrickSetStatusList(force=True)
# Reload retired sets
BrickRetiredList(force=True)
diff --git a/bricktracker/set.py b/bricktracker/set.py
index 6b2da47a..1b11a943 100644
--- a/bricktracker/set.py
+++ b/bricktracker/set.py
@@ -9,7 +9,7 @@ from .exceptions import NotFoundException
from .minifigure_list import BrickMinifigureList
from .part_list import BrickPartList
from .rebrickable_set import RebrickableSet
-from .set_checkbox_list import BrickSetCheckboxList
+from .set_status_list import BrickSetStatusList
from .sql import BrickSQL
if TYPE_CHECKING:
from .socket import BrickSocket
@@ -161,7 +161,7 @@ class BrickSet(RebrickableSet):
# Load from database
if not self.select(
- statuses=BrickSetCheckboxList().as_columns(solo=True)
+ statuses=BrickSetStatusList().as_columns(solo=True)
):
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 349af66b..47d31b21 100644
--- a/bricktracker/set_list.py
+++ b/bricktracker/set_list.py
@@ -3,7 +3,7 @@ from typing import Self
from flask import current_app
from .record_list import BrickRecordList
-from .set_checkbox_list import BrickSetCheckboxList
+from .set_status_list import BrickSetStatusList
from .set import BrickSet
@@ -37,7 +37,7 @@ class BrickSetList(BrickRecordList[BrickSet]):
# Load the sets from the database
for record in self.select(
order=self.order,
- statuses=BrickSetCheckboxList().as_columns()
+ statuses=BrickSetStatusList().as_columns()
):
brickset = BrickSet(record=record)
@@ -73,7 +73,7 @@ class BrickSetList(BrickRecordList[BrickSet]):
for record in self.select(
order=order,
limit=limit,
- statuses=BrickSetCheckboxList().as_columns()
+ statuses=BrickSetStatusList().as_columns()
):
brickset = BrickSet(record=record)
diff --git a/bricktracker/set_checkbox.py b/bricktracker/set_status.py
similarity index 60%
rename from bricktracker/set_checkbox.py
rename to bricktracker/set_status.py
index f38b5f6e..3b874f52 100644
--- a/bricktracker/set_checkbox.py
+++ b/bricktracker/set_status.py
@@ -4,20 +4,20 @@ from .exceptions import ErrorException
from .metadata import BrickMetadata
-# Lego set checkbox
-class BrickSetCheckbox(BrickMetadata):
- kind: str = 'checkbox'
+# Lego set status metadata
+class BrickSetStatus(BrickMetadata):
+ kind: str = 'status'
prefix: str = 'status'
# Set state endpoint
set_state_endpoint: str = 'set.update_status'
# Queries
- delete_query: str = 'checkbox/delete'
- insert_query: str = 'checkbox/insert'
- select_query: str = 'checkbox/select'
- update_field_query: str = 'checkbox/update/field'
- update_set_state_query: str = 'set/update/status'
+ delete_query: str = 'set/metadata/status/delete'
+ insert_query: str = 'set/metadata/status/insert'
+ select_query: str = 'set/metadata/status/select'
+ update_field_query: str = 'set/metadata/status/update/field'
+ update_set_state_query: str = 'set/metadata/status/update/state'
# Grab data from a form
def from_form(self, form: dict[str, str], /) -> Self:
@@ -25,7 +25,7 @@ class BrickSetCheckbox(BrickMetadata):
grid = form.get('grid', None)
if name is None or name == '':
- raise ErrorException('Checkbox name cannot be empty')
+ raise ErrorException('Status name cannot be empty')
self.fields.name = name
self.fields.displayed_on_grid = grid == 'on'
diff --git a/bricktracker/set_checkbox_list.py b/bricktracker/set_status_list.py
similarity index 51%
rename from bricktracker/set_checkbox_list.py
rename to bricktracker/set_status_list.py
index 6564e8d0..12ce85a1 100644
--- a/bricktracker/set_checkbox_list.py
+++ b/bricktracker/set_status_list.py
@@ -3,39 +3,39 @@ import logging
from .exceptions import NotFoundException
from .fields import BrickRecordFields
from .record_list import BrickRecordList
-from .set_checkbox import BrickSetCheckbox
+from .set_status import BrickSetStatus
logger = logging.getLogger(__name__)
-# Lego sets checkbox list
-class BrickSetCheckboxList(BrickRecordList[BrickSetCheckbox]):
- checkboxes: dict[str, BrickSetCheckbox]
+# Lego sets status list
+class BrickSetStatusList(BrickRecordList[BrickSetStatus]):
+ statuses: dict[str, BrickSetStatus]
# Queries
- select_query = 'checkbox/list'
+ select_query = 'set/metadata/status/list'
def __init__(self, /, *, force: bool = False):
- # Load checkboxes only if there is none already loaded
+ # Load statuses only if there is none already loaded
records = getattr(self, 'records', None)
if records is None or force:
# Don't use super()__init__ as it would mask class variables
self.fields = BrickRecordFields()
- logger.info('Loading set checkboxes list')
+ logger.info('Loading set statuses list')
- BrickSetCheckboxList.records = []
- BrickSetCheckboxList.checkboxes = {}
+ BrickSetStatusList.records = []
+ BrickSetStatusList.statuses = {}
- # Load the checkboxes from the database
+ # Load the statuses from the database
for record in self.select():
- checkbox = BrickSetCheckbox(record=record)
+ status = BrickSetStatus(record=record)
- BrickSetCheckboxList.records.append(checkbox)
- BrickSetCheckboxList.checkboxes[checkbox.fields.id] = checkbox
+ BrickSetStatusList.records.append(status)
+ BrickSetStatusList.statuses[status.fields.id] = status
- # Return the checkboxes as columns for a select
+ # Return the statuses as columns for a select
def as_columns(
self,
/,
@@ -53,19 +53,19 @@ class BrickSetCheckboxList(BrickRecordList[BrickSetCheckbox]):
if solo or record.fields.displayed_on_grid
])
- # Grab a specific checkbox
- def get(self, id: str, /) -> BrickSetCheckbox:
- if id not in self.checkboxes:
+ # Grab a specific status
+ def get(self, id: str, /) -> BrickSetStatus:
+ if id not in self.statuses:
raise NotFoundException(
- 'Checkbox with ID {id} was not found in the database'.format(
+ 'Status with ID {id} was not found in the database'.format(
id=id,
),
)
- return self.checkboxes[id]
+ return self.statuses[id]
- # Get the list of checkboxes depending on the context
- def list(self, /, *, all: bool = False) -> list[BrickSetCheckbox]:
+ # Get the list of statuses depending on the context
+ def list(self, /, *, all: bool = False) -> list[BrickSetStatus]:
return [
record
for record
diff --git a/bricktracker/sql.py b/bricktracker/sql.py
index 9e47d9a4..18f7e991 100644
--- a/bricktracker/sql.py
+++ b/bricktracker/sql.py
@@ -318,13 +318,18 @@ class BrickSQL(object):
),
package='bricktracker'
)
+ except Exception:
+ module = None
+ # If a module has been loaded, we need to fail if an error
+ # occured while executing the migration function
+ if module is not None:
function = getattr(module, 'migration_{name}'.format(
name=pending.name
))
context: dict[str, Any] = function(self)
- except Exception:
+ else:
context: dict[str, Any] = {}
self.executescript(pending.get_query(), **context)
diff --git a/bricktracker/sql/checkbox/base.sql b/bricktracker/sql/checkbox/base.sql
deleted file mode 100644
index 9726a6c1..00000000
--- a/bricktracker/sql/checkbox/base.sql
+++ /dev/null
@@ -1,7 +0,0 @@
-SELECT
- "bricktracker_set_checkboxes"."id",
- "bricktracker_set_checkboxes"."name",
- "bricktracker_set_checkboxes"."displayed_on_grid"
-FROM "bricktracker_set_checkboxes"
-
-{% block where %}{% endblock %}
\ No newline at end of file
diff --git a/bricktracker/sql/checkbox/delete.sql b/bricktracker/sql/checkbox/delete.sql
deleted file mode 100644
index 6eae9d0b..00000000
--- a/bricktracker/sql/checkbox/delete.sql
+++ /dev/null
@@ -1,9 +0,0 @@
-BEGIN TRANSACTION;
-
-ALTER TABLE "bricktracker_set_statuses"
-DROP COLUMN "status_{{ id }}";
-
-DELETE FROM "bricktracker_set_checkboxes"
-WHERE "bricktracker_set_checkboxes"."id" IS NOT DISTINCT FROM '{{ id }}';
-
-COMMIT;
\ No newline at end of file
diff --git a/bricktracker/sql/checkbox/list.sql b/bricktracker/sql/checkbox/list.sql
deleted file mode 100644
index 7420eb33..00000000
--- a/bricktracker/sql/checkbox/list.sql
+++ /dev/null
@@ -1 +0,0 @@
-{% extends 'checkbox/base.sql' %}
diff --git a/bricktracker/sql/checkbox/select.sql b/bricktracker/sql/checkbox/select.sql
deleted file mode 100644
index 76557a8b..00000000
--- a/bricktracker/sql/checkbox/select.sql
+++ /dev/null
@@ -1,5 +0,0 @@
-{% extends 'checkbox/base.sql' %}
-
-{% block where %}
-WHERE "bricktracker_set_checkboxes"."id" IS NOT DISTINCT FROM :id
-{% endblock %}
\ No newline at end of file
diff --git a/bricktracker/sql/checkbox/update/field.sql b/bricktracker/sql/checkbox/update/field.sql
deleted file mode 100644
index a65e3c09..00000000
--- a/bricktracker/sql/checkbox/update/field.sql
+++ /dev/null
@@ -1,3 +0,0 @@
-UPDATE "bricktracker_set_checkboxes"
-SET "{{field}}" = :value
-WHERE "bricktracker_set_checkboxes"."id" IS NOT DISTINCT FROM :id
diff --git a/bricktracker/sql/migrations/0012.sql b/bricktracker/sql/migrations/0012.sql
new file mode 100644
index 00000000..d656e29c
--- /dev/null
+++ b/bricktracker/sql/migrations/0012.sql
@@ -0,0 +1,7 @@
+-- description: Rename checkboxes to status metadata
+
+BEGIN TRANSACTION;
+
+ALTER TABLE "bricktracker_set_checkboxes" RENAME TO "bricktracker_metadata_statuses";
+
+COMMIT;
\ No newline at end of file
diff --git a/bricktracker/sql/schema/drop.sql b/bricktracker/sql/schema/drop.sql
index 78ea32ce..abc85229 100644
--- a/bricktracker/sql/schema/drop.sql
+++ b/bricktracker/sql/schema/drop.sql
@@ -1,5 +1,6 @@
BEGIN transaction;
+DROP TABLE IF EXISTS "bricktracker_metadata_statuses";
DROP TABLE IF EXISTS "bricktracker_minifigures";
DROP TABLE IF EXISTS "bricktracker_parts";
DROP TABLE IF EXISTS "bricktracker_sets";
diff --git a/bricktracker/sql/set/metadata/status/base.sql b/bricktracker/sql/set/metadata/status/base.sql
new file mode 100644
index 00000000..b1b2167d
--- /dev/null
+++ b/bricktracker/sql/set/metadata/status/base.sql
@@ -0,0 +1,7 @@
+SELECT
+ "bricktracker_metadata_statuses"."id",
+ "bricktracker_metadata_statuses"."name",
+ "bricktracker_metadata_statuses"."displayed_on_grid"
+FROM "bricktracker_metadata_statuses"
+
+{% block where %}{% endblock %}
\ No newline at end of file
diff --git a/bricktracker/sql/set/metadata/status/delete.sql b/bricktracker/sql/set/metadata/status/delete.sql
new file mode 100644
index 00000000..cac284e2
--- /dev/null
+++ b/bricktracker/sql/set/metadata/status/delete.sql
@@ -0,0 +1,9 @@
+BEGIN TRANSACTION;
+
+ALTER TABLE "bricktracker_set_statuses"
+DROP COLUMN "status_{{ id }}";
+
+DELETE FROM "bricktracker_metadata_statuses"
+WHERE "bricktracker_metadata_statuses"."id" IS NOT DISTINCT FROM '{{ id }}';
+
+COMMIT;
\ No newline at end of file
diff --git a/bricktracker/sql/checkbox/insert.sql b/bricktracker/sql/set/metadata/status/insert.sql
similarity index 84%
rename from bricktracker/sql/checkbox/insert.sql
rename to bricktracker/sql/set/metadata/status/insert.sql
index 5de9c179..2704d720 100644
--- a/bricktracker/sql/checkbox/insert.sql
+++ b/bricktracker/sql/set/metadata/status/insert.sql
@@ -3,7 +3,7 @@ BEGIN TRANSACTION;
ALTER TABLE "bricktracker_set_statuses"
ADD COLUMN "status_{{ id }}" BOOLEAN NOT NULL DEFAULT 0;
-INSERT INTO "bricktracker_set_checkboxes" (
+INSERT INTO "bricktracker_metadata_statuses" (
"id",
"name",
"displayed_on_grid"
diff --git a/bricktracker/sql/set/metadata/status/list.sql b/bricktracker/sql/set/metadata/status/list.sql
new file mode 100644
index 00000000..2fe9994b
--- /dev/null
+++ b/bricktracker/sql/set/metadata/status/list.sql
@@ -0,0 +1 @@
+{% extends 'set/metadata/status/base.sql' %}
diff --git a/bricktracker/sql/set/metadata/status/select.sql b/bricktracker/sql/set/metadata/status/select.sql
new file mode 100644
index 00000000..09afe875
--- /dev/null
+++ b/bricktracker/sql/set/metadata/status/select.sql
@@ -0,0 +1,5 @@
+{% extends 'set/metadata/status/base.sql' %}
+
+{% block where %}
+WHERE "bricktracker_metadata_statuses"."id" IS NOT DISTINCT FROM :id
+{% endblock %}
\ No newline at end of file
diff --git a/bricktracker/sql/set/metadata/status/update/field.sql b/bricktracker/sql/set/metadata/status/update/field.sql
new file mode 100644
index 00000000..d17681ec
--- /dev/null
+++ b/bricktracker/sql/set/metadata/status/update/field.sql
@@ -0,0 +1,3 @@
+UPDATE "bricktracker_metadata_statuses"
+SET "{{field}}" = :value
+WHERE "bricktracker_metadata_statuses"."id" IS NOT DISTINCT FROM :id
diff --git a/bricktracker/sql/set/update/status.sql b/bricktracker/sql/set/metadata/status/update/state.sql
similarity index 100%
rename from bricktracker/sql/set/update/status.sql
rename to bricktracker/sql/set/metadata/status/update/state.sql
diff --git a/bricktracker/sql_counter.py b/bricktracker/sql_counter.py
index 2e5c072e..28c03a32 100644
--- a/bricktracker/sql_counter.py
+++ b/bricktracker/sql_counter.py
@@ -2,11 +2,12 @@ from typing import Tuple
# Some table aliases to make it look cleaner (id: (name, icon))
ALIASES: dict[str, Tuple[str, str]] = {
+ 'bricktracker_metadata_statuses': ('Bricktracker set status metadata', 'checkbox-line'), # noqa: E501
'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-circle-line'), # noqa: E501
- 'bricktracker_set_storages': ('Bricktracker sets storages', 'archive-2-line'), # noqa: E501
+ 'bricktracker_set_checkboxes': ('Bricktracker set checkboxes (legacy)', 'checkbox-line'), # noqa: E501
+ 'bricktracker_set_statuses': ('Bricktracker set statuses', 'checkbox-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'),
'inventory': ('Parts', 'shapes-line'),
diff --git a/bricktracker/version.py b/bricktracker/version.py
index 172ecf1d..11dd9c9a 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] = 11
+__database_version__: Final[int] = 12
diff --git a/bricktracker/views/admin/admin.py b/bricktracker/views/admin/admin.py
index d5586eab..a7f8ce70 100644
--- a/bricktracker/views/admin/admin.py
+++ b/bricktracker/views/admin/admin.py
@@ -8,8 +8,8 @@ from ..exceptions import exception_handler
from ...instructions_list import BrickInstructionsList
from ...rebrickable_image import RebrickableImage
from ...retired_list import BrickRetiredList
-from ...set_checkbox import BrickSetCheckbox
-from ...set_checkbox_list import BrickSetCheckboxList
+from ...set_status import BrickSetStatus
+from ...set_status_list import BrickSetStatusList
from ...sql_counter import BrickCounter
from ...sql import BrickSQL
from ...theme_list import BrickThemeList
@@ -24,11 +24,11 @@ admin_page = Blueprint('admin', __name__, url_prefix='/admin')
@login_required
@exception_handler(__file__)
def admin() -> str:
- brickset_checkboxes: list[BrickSetCheckbox] = []
database_counters: list[BrickCounter] = []
database_exception: Exception | None = None
database_upgrade_needed: bool = False
database_version: int = -1
+ metadata_statuses: list[BrickSetStatus] = []
nil_minifigure_name: str = ''
nil_minifigure_url: str = ''
nil_part_name: str = ''
@@ -41,7 +41,7 @@ def admin() -> str:
database_version = database.version
database_counters = BrickSQL().count_records()
- brickset_checkboxes = BrickSetCheckboxList().list(all=True)
+ metadata_statuses = BrickSetStatusList().list(all=True)
except Exception as e:
database_exception = e
@@ -62,38 +62,38 @@ def admin() -> str:
'PARTS_FOLDER'
)
- open_checkbox = request.args.get('open_checkbox', None)
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_retired = request.args.get('open_retired', None)
+ open_status = request.args.get('open_status', None)
open_theme = request.args.get('open_theme', None)
open_database = (
- open_checkbox is None and
open_image is None and
open_instructions is None and
open_logout is None and
open_retired is None and
+ open_status is None and
open_theme is None
)
return render_template(
'admin.html',
configuration=BrickConfigurationList.list(),
- brickset_checkboxes=brickset_checkboxes,
- checkbox_error=request.args.get('checkbox_error'),
+ 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_statuses=metadata_statuses,
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_checkbox=open_checkbox,
+ open_status=open_status,
open_database=open_database,
open_image=open_image,
open_instructions=open_instructions,
diff --git a/bricktracker/views/admin/checkbox.py b/bricktracker/views/admin/checkbox.py
deleted file mode 100644
index 6db6c5df..00000000
--- a/bricktracker/views/admin/checkbox.py
+++ /dev/null
@@ -1,98 +0,0 @@
-from flask import (
- Blueprint,
- jsonify,
- 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_checkbox import BrickSetCheckbox
-
-admin_checkbox_page = Blueprint(
- 'admin_checkbox',
- __name__,
- url_prefix='/admin/checkbox'
-)
-
-
-# Add a checkbox
-@admin_checkbox_page.route('/add', methods=['POST'])
-@login_required
-@exception_handler(
- __file__,
- post_redirect='admin.admin',
- error_name='checkbox_error',
- open_checkbox=True
-)
-def add() -> Response:
- BrickSetCheckbox().from_form(request.form).insert()
-
- reload()
-
- return redirect(url_for('admin.admin', open_checkbox=True))
-
-
-# Delete the checkbox
-@admin_checkbox_page.route('<id>/delete', methods=['GET'])
-@login_required
-@exception_handler(__file__)
-def delete(*, id: str) -> str:
- return render_template(
- 'admin.html',
- delete_checkbox=True,
- checkbox=BrickSetCheckbox().select_specific(id),
- error=request.args.get('checkbox_error')
- )
-
-
-# Actually delete the checkbox
-@admin_checkbox_page.route('<id>/delete', methods=['POST'])
-@login_required
-@exception_handler(
- __file__,
- post_redirect='admin_checkbox.delete',
- error_name='checkbox_error'
-)
-def do_delete(*, id: str) -> Response:
- checkbox = BrickSetCheckbox().select_specific(id)
- checkbox.delete()
-
- reload()
-
- return redirect(url_for('admin.admin', open_checkbox=True))
-
-
-# Change the field of a checkbox
-@admin_checkbox_page.route('/<id>/field/<name>', methods=['POST'])
-@login_required
-@exception_handler(__file__, json=True)
-def update_field(*, id: str, name: str) -> Response:
- checkbox = BrickSetCheckbox().select_specific(id)
- value = checkbox.update_field(name, json=request.json)
-
- reload()
-
- return jsonify({'value': value})
-
-
-# Rename the checkbox
-@admin_checkbox_page.route('<id>/rename', methods=['POST'])
-@login_required
-@exception_handler(
- __file__,
- post_redirect='admin.admin',
- error_name='checkbox_error',
- open_checkbox=True
-)
-def rename(*, id: str) -> Response:
- checkbox = BrickSetCheckbox().select_specific(id)
- checkbox.from_form(request.form).rename()
-
- reload()
-
- return redirect(url_for('admin.admin', open_checkbox=True))
diff --git a/bricktracker/views/admin/status.py b/bricktracker/views/admin/status.py
new file mode 100644
index 00000000..8178d693
--- /dev/null
+++ b/bricktracker/views/admin/status.py
@@ -0,0 +1,98 @@
+from flask import (
+ Blueprint,
+ jsonify,
+ 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_status import BrickSetStatus
+
+admin_status_page = Blueprint(
+ 'admin_status',
+ __name__,
+ url_prefix='/admin/status'
+)
+
+
+# Add a metadata status
+@admin_status_page.route('/add', methods=['POST'])
+@login_required
+@exception_handler(
+ __file__,
+ post_redirect='admin.admin',
+ error_name='status_error',
+ open_status=True
+)
+def add() -> Response:
+ BrickSetStatus().from_form(request.form).insert()
+
+ reload()
+
+ return redirect(url_for('admin.admin', open_status=True))
+
+
+# Delete the metadata status
+@admin_status_page.route('<id>/delete', methods=['GET'])
+@login_required
+@exception_handler(__file__)
+def delete(*, id: str) -> str:
+ return render_template(
+ 'admin.html',
+ delete_status=True,
+ status=BrickSetStatus().select_specific(id),
+ error=request.args.get('status_error')
+ )
+
+
+# Actually delete the metadata status
+@admin_status_page.route('<id>/delete', methods=['POST'])
+@login_required
+@exception_handler(
+ __file__,
+ post_redirect='admin_status.delete',
+ error_name='status_error'
+)
+def do_delete(*, id: str) -> Response:
+ status = BrickSetStatus().select_specific(id)
+ status.delete()
+
+ reload()
+
+ return redirect(url_for('admin.admin', open_status=True))
+
+
+# Change the field of a metadata status
+@admin_status_page.route('/<id>/field/<name>', methods=['POST'])
+@login_required
+@exception_handler(__file__, json=True)
+def update_field(*, id: str, name: str) -> Response:
+ status = BrickSetStatus().select_specific(id)
+ value = status.update_field(name, json=request.json)
+
+ reload()
+
+ return jsonify({'value': value})
+
+
+# Rename the metadata status
+@admin_status_page.route('<id>/rename', methods=['POST'])
+@login_required
+@exception_handler(
+ __file__,
+ post_redirect='admin.admin',
+ error_name='status_error',
+ open_status=True
+)
+def rename(*, id: str) -> Response:
+ status = BrickSetStatus().select_specific(id)
+ status.from_form(request.form).rename()
+
+ reload()
+
+ return redirect(url_for('admin.admin', open_status=True))
diff --git a/bricktracker/views/index.py b/bricktracker/views/index.py
index c1f08118..3e3a8802 100644
--- a/bricktracker/views/index.py
+++ b/bricktracker/views/index.py
@@ -2,7 +2,7 @@ from flask import Blueprint, render_template
from .exceptions import exception_handler
from ..minifigure_list import BrickMinifigureList
-from ..set_checkbox_list import BrickSetCheckboxList
+from ..set_status_list import BrickSetStatusList
from ..set_list import BrickSetList
index_page = Blueprint('index', __name__)
@@ -16,5 +16,5 @@ def index() -> str:
'index.html',
brickset_collection=BrickSetList().last(),
minifigure_collection=BrickMinifigureList().last(),
- brickset_checkboxes=BrickSetCheckboxList().list(),
+ brickset_statuses=BrickSetStatusList().list(),
)
diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py
index bb3c2348..17fcff98 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_checkbox_list import BrickSetCheckboxList
+from ..set_status_list import BrickSetStatusList
from ..set_list import BrickSetList
from ..socket import MESSAGES
@@ -32,19 +32,19 @@ def list() -> str:
return render_template(
'sets.html',
collection=BrickSetList().all(),
- brickset_checkboxes=BrickSetCheckboxList().list(),
+ brickset_statuses=BrickSetStatusList().list(),
)
-# Change the status of a checkbox
+# Change the status of a status
@set_page.route('/<id>/status/<metadata_id>', methods=['POST'])
@login_required
@exception_handler(__file__, json=True)
def update_status(*, id: str, metadata_id: str) -> Response:
brickset = BrickSet().select_light(id)
- checkbox = BrickSetCheckboxList().get(metadata_id)
+ status = BrickSetStatusList().get(metadata_id)
- state = checkbox.update_set_state(brickset, request.json)
+ state = status.update_set_state(brickset, request.json)
return jsonify({'value': state})
@@ -97,7 +97,7 @@ def details(*, id: str) -> str:
'set.html',
item=BrickSet().select_specific(id),
open_instructions=request.args.get('open_instructions'),
- brickset_checkboxes=BrickSetCheckboxList().list(all=True),
+ brickset_statuses=BrickSetStatusList().list(all=True),
)
diff --git a/docs/DOCS.md b/docs/DOCS.md
index 28deffec..f13165d2 100644
--- a/docs/DOCS.md
+++ b/docs/DOCS.md
@@ -15,7 +15,7 @@ This page helps you navigate the documentation of BrickTracker.
- [First steps](first-steps.md)
- [Managing your sets](set.md)
-- [Managing your set checkboxes](checkbox.md)
+- [Managing your set statuses](set-statuses.md)
## Specific procedures
diff --git a/docs/checkbox.md b/docs/checkbox.md
deleted file mode 100644
index 3071e67f..00000000
--- a/docs/checkbox.md
+++ /dev/null
@@ -1,58 +0,0 @@
-# Manage your set chechboxes
-
-> **Note**
-> The following page is based on version `1.1.0` of BrickTracker.
-
-They are useful to store "yes/no" info about a set and quickly set it. Once clicked the change is immediatly stored in the database. A visual indicator tells you the change was succesful.
-
-
-
-## Default checkboxes
-
-The original version of BrickTracker defined the following checkboxes
-
-- Minifigures are collected
-- Set is checked
-- Set is collected and boxed
-
-## Visibility
-
-The checkboxes are **never visible** on the front page. The display here tries to be as minimalistic as possible.
-
-Prior to version `1.1.0`, the checkboxes were visible both on the Grid view (**Sets**) and the details of a set.
-
-
-
-
-From version `1.1.0`, it is possible to decide if a checkbox is visible from the Grid or not. It will always be visible in a set details.
-
-### Change the visibility of a checkbox
-
-To change the visibility of a checkbox, head to the **Admin page** and open the **Checkboxes** section.
-
-
-
-Simply click on the **Displayed on the Set Grid** checkbox to select whether it is displayed or not. The change is immediately saved to the database.
-
-
-
-In this example, we have decided to have no checkbox visible on the Grid view.
-
-
-
-## Management
-
-Starting version `1.1.0`, you can manage the checkboxes for the **Checkboxes** section of the **Admin page**.
-
-
-
-From there you can do the following:
-
-- Add a new checkbox: use the last line of the list and press the **Add** button
-- Rename an existing checkbox: use the **Name** field to change the name and press the **Rename** button
-- Change the Grid display of an existing checkbox: tick or untick the **Displayed on the Set Grid** checkbox
-- Delete an existing checkbox: use the **Delete** button and confirm on the following screen
-
-It is possible to delete all the checkboxes, they are an optional component of a set.
-
-
diff --git a/docs/images/checkbox-01.png b/docs/images/status-01.png
similarity index 100%
rename from docs/images/checkbox-01.png
rename to docs/images/status-01.png
diff --git a/docs/images/checkbox-02.png b/docs/images/status-02.png
similarity index 100%
rename from docs/images/checkbox-02.png
rename to docs/images/status-02.png
diff --git a/docs/images/checkbox-03.png b/docs/images/status-03.png
similarity index 100%
rename from docs/images/checkbox-03.png
rename to docs/images/status-03.png
diff --git a/docs/images/checkbox-04.png b/docs/images/status-04.png
similarity index 100%
rename from docs/images/checkbox-04.png
rename to docs/images/status-04.png
diff --git a/docs/images/checkbox-05.png b/docs/images/status-05.png
similarity index 100%
rename from docs/images/checkbox-05.png
rename to docs/images/status-05.png
diff --git a/docs/images/checkbox-06.png b/docs/images/status-06.png
similarity index 100%
rename from docs/images/checkbox-06.png
rename to docs/images/status-06.png
diff --git a/docs/images/checkbox-07.png b/docs/images/status-07.png
similarity index 100%
rename from docs/images/checkbox-07.png
rename to docs/images/status-07.png
diff --git a/docs/set-statuses.md b/docs/set-statuses.md
new file mode 100644
index 00000000..8739b41e
--- /dev/null
+++ b/docs/set-statuses.md
@@ -0,0 +1,67 @@
+# Manage your set statuses
+
+> **Note**
+> The following page is based on version `1.1.0` of BrickTracker.
+
+> **Note**
+> On version `1.2.0`, this feature has been renommed from `Checkboxes` to `Set statuses`. It works exactly the same.
+
+They are useful to store "yes/no" info about a set and quickly set it. Once clicked the change is immediatly stored in the database. A visual indicator tells you the change was succesful.
+
+
+
+## Default statuses
+
+The original version of BrickTracker defined the following statuses
+
+- Minifigures are collected
+- Set is checked
+- Set is collected and boxed
+
+## Visibility
+
+The statuses are **never visible** on the front page. The display here tries to be as minimalistic as possible.
+
+Prior to version `1.1.0`, the statuses were visible both on the Grid view (**Sets**) and the details of a set.
+
+
+
+
+From version `1.1.0`, it is possible to decide if a status is visible from the Grid or not. It will always be visible in a set details.
+
+### Change the visibility of a status
+
+> **Note**
+> On version `1.2.0`, the Admin page section has been renamed from `Checkboxes` to `Set statuses`. It works exactly the same.
+
+To change the visibility of a status, head to the **Admin page** and open the **Set statuses** section.
+
+
+
+Simply click on the **Displayed on the Set Grid** status to select whether it is displayed or not. The change is immediately saved to the database.
+
+
+
+In this example, we have decided to have no status visible on the Grid view.
+
+
+
+## Management
+
+> **Note**
+> On version `1.2.0`, the Admin page section has been renamed from `Checkboxes` to `Set statuses`. It works exactly the same.
+
+Starting version `1.1.0`, you can manage the set statuses for the **Set statuses** section of the **Admin page**.
+
+
+
+From there you can do the following:
+
+- Add a new set status: use the last line of the list and press the **Add** button
+- Rename an existing set status: use the **Name** field to change the name and press the **Rename** button
+- Change the Grid display of an existing status: tick or untick the **Displayed on the Set Grid** checkbox
+- Delete an existing set status: use the **Delete** button and confirm on the following screen
+
+It is possible to delete all the set statuses, they are an optional component of a set.
+
+
diff --git a/templates/admin.html b/templates/admin.html
index dc872566..b2a54f3a 100644
--- a/templates/admin.html
+++ b/templates/admin.html
@@ -12,8 +12,8 @@
<h5 class="mb-0"><i class="ri-settings-4-line"></i> Administration</h5>
</div>
<div class="accordion accordion-flush" id="admin">
- {% if delete_checkbox %}
- {% include 'admin/checkbox/delete.html' %}
+ {% if delete_status %}
+ {% include 'admin/status/delete.html' %}
{% elif delete_database %}
{% include 'admin/database/delete.html' %}
{% elif drop_database %}
@@ -30,7 +30,7 @@
{% endif %}
{% include 'admin/theme.html' %}
{% include 'admin/retired.html' %}
- {% include 'admin/checkbox.html' %}
+ {% include 'admin/status.html' %}
{% include 'admin/database.html' %}
{% include 'admin/configuration.html' %}
{% endif %}
diff --git a/templates/admin/checkbox.html b/templates/admin/status.html
similarity index 54%
rename from templates/admin/checkbox.html
rename to templates/admin/status.html
index 22a3215b..a73462ad 100644
--- a/templates/admin/checkbox.html
+++ b/templates/admin/status.html
@@ -1,42 +1,42 @@
{% import 'macro/accordion.html' as accordion %}
-{{ accordion.header('Checkboxes', 'checkbox', 'admin', expanded=open_checkbox, icon='checkbox-line', class='p-0') }}
-{% if checkbox_error %}<div class="alert alert-danger m-2" role="alert"><strong>Error:</strong> {{ checkbox_error }}.</div>{% endif %}
+{{ accordion.header('Set statuses', 'status', 'admin', expanded=open_status, icon='checkbox-line', class='p-0') }}
+{% if status_error %}<div class="alert alert-danger m-2" role="alert"><strong>Error:</strong> {{ status_error }}.</div>{% endif %}
<ul class="list-group list-group-flush">
- {% if brickset_checkboxes | length %}
- {% for checkbox in brickset_checkboxes %}
+ {% if metadata_statuses | length %}
+ {% for status in metadata_statuses %}
<li class="list-group-item">
- <form action="{{ url_for('admin_checkbox.rename', id=checkbox.fields.id) }}" method="post" class="row row-cols-lg-auto g-3 align-items-center">
+ <form action="{{ url_for('admin_status.rename', id=status.fields.id) }}" method="post" class="row row-cols-lg-auto g-3 align-items-center">
<div class="col-12 flex-grow-1">
- <label class="visually-hidden" for="name-{{ checkbox.fields.id }}">Name</label>
+ <label class="visually-hidden" for="name-{{ status.fields.id }}">Name</label>
<div class="input-group">
<div class="input-group-text">Name</div>
- <input type="text" class="form-control" id="name-{{ checkbox.fields.id }}" name="name" value="{{ checkbox.fields.name }}">
+ <input type="text" class="form-control" id="name-{{ status.fields.id }}" name="name" value="{{ status.fields.name }}">
<button type="submit" class="btn btn-primary"><i class="ri-edit-line"></i> Rename</button>
</div>
</div>
<div class="col-12">
<div class="form-check">
- <input class="form-check-input" type="checkbox" id="grid-{{ checkbox.fields.id }}"
- data-changer-id="{{ checkbox.fields.id }}" data-changer-prefix="grid" data-changer-url="{{ url_for('admin_checkbox.update_field', id=checkbox.fields.id, name='displayed_on_grid')}}"
- {% if checkbox.fields.displayed_on_grid %}checked{% endif %} autocomplete="off">
- <label class="form-check-label" for="grid-{{ checkbox.fields.id }}">
+ <input class="form-check-input" type="checkbox" id="grid-{{ status.fields.id }}"
+ data-changer-id="{{ status.fields.id }}" data-changer-prefix="grid" data-changer-url="{{ url_for('admin_status.update_field', id=status.fields.id, name='displayed_on_grid')}}"
+ {% if status.fields.displayed_on_grid %}checked{% endif %} autocomplete="off">
+ <label class="form-check-label" for="grid-{{ status.fields.id }}">
<i class="ri-grid-line"></i> Displayed on the Set Grid
- <i id="status-grid-{{ checkbox.fields.id }}" class="mb-1"></i>
+ <i id="status-grid-{{ status.fields.id }}" class="mb-1"></i>
</label>
</div>
</div>
<div class="col-12">
- <a href="{{ url_for('admin_checkbox.delete', id=checkbox.fields.id) }}" class="btn btn-danger" role="button"><i class="ri-delete-bin-2-line"></i> Delete</a>
+ <a href="{{ url_for('admin_status.delete', id=status.fields.id) }}" class="btn btn-danger" role="button"><i class="ri-delete-bin-2-line"></i> Delete</a>
</div>
</form>
</li>
{% endfor %}
{% else %}
- <li class="list-group-item"><i class="ri-error-warning-line"></i> No checkbox found.</li>
+ <li class="list-group-item"><i class="ri-error-warning-line"></i> No status found.</li>
{% endif %}
<li class="list-group-item">
- <form action="{{ url_for('admin_checkbox.add') }}" method="post" class="row row-cols-lg-auto g-3 align-items-center">
+ <form action="{{ url_for('admin_status.add') }}" method="post" class="row row-cols-lg-auto g-3 align-items-center">
<div class="col-12 flex-grow-1">
<label class="visually-hidden" for="name">Name</label>
<div class="input-group">
diff --git a/templates/admin/checkbox/delete.html b/templates/admin/status/delete.html
similarity index 53%
rename from templates/admin/checkbox/delete.html
rename to templates/admin/status/delete.html
index 49d507ad..31998c4b 100644
--- a/templates/admin/checkbox/delete.html
+++ b/templates/admin/status/delete.html
@@ -1,25 +1,25 @@
{% import 'macro/accordion.html' as accordion %}
-{{ accordion.header('Checkbox danger zone', 'checkbox-danger', 'admin', expanded=true, danger=true, class='text-end') }}
-<form action="{{ url_for('admin_checkbox.do_delete', id=checkbox.fields.id) }}" method="post">
+{{ accordion.header('Set statuses danger zone', 'status-danger', 'admin', expanded=true, danger=true, class='text-end') }}
+<form action="{{ url_for('admin_status.do_delete', id=status.fields.id) }}" method="post">
{% if error %}<div class="alert alert-danger text-start" role="alert"><strong>Error:</strong> {{ error }}.</div>{% endif %}
- <div class="alert alert-danger text-center" role="alert">You are about to <strong>delete a checkbox</strong>. This action is irreversible.</div>
+ <div class="alert alert-danger text-center" role="alert">You are about to <strong>delete a set status</strong>. This action is irreversible.</div>
<div class="row row-cols-lg-auto g-3 align-items-center">
<div class="col-12 flex-grow-1">
<div class="input-group">
<div class="input-group-text">Name</div>
- <input type="text" class="form-control" value="{{ checkbox.fields.name }}" disabled>
+ <input type="text" class="form-control" value="{{ status.fields.name }}" disabled>
</div>
</div>
<div class="col-12">
<div class="form-check">
- <input class="form-check-input" type="checkbox" {% if checkbox.fields.displayed_on_grid %}checked{% endif %} disabled>
+ <input class="form-check-input" type="checkbox" {% if status.fields.displayed_on_grid %}checked{% endif %} disabled>
<span class="form-check-label">Displayed on the Set Grid</span>
</div>
</div>
</div>
<hr class="border-bottom">
- <a class="btn btn-danger" href="{{ url_for('admin.admin', open_checkbox=true) }}" role="button"><i class="ri-arrow-left-long-line"></i> Back to the admin</a>
- <button type="submit" class="btn btn-danger"><i class="ri-delete-bin-2-line"></i> Delete <strong>the checkbox</strong></button>
+ <a class="btn btn-danger" href="{{ url_for('admin.admin', open_status=true) }}" role="button"><i class="ri-arrow-left-long-line"></i> Back to the admin</a>
+ <button type="submit" class="btn btn-danger"><i class="ri-delete-bin-2-line"></i> Delete <strong>the set status</strong></button>
</form>
{{ accordion.footer() }}
diff --git a/templates/set/card.html b/templates/set/card.html
index 6a658c98..fea4fc04 100644
--- a/templates/set/card.html
+++ b/templates/set/card.html
@@ -8,7 +8,7 @@
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 checkbox in brickset_checkboxes %}data-{{ checkbox.as_dataset() }}="{{ item.fields[checkbox.as_column()] }}" {% endfor %}
+ {% for status in brickset_statuses %}data-{{ status.as_dataset() }}="{{ item.fields[status.as_column()] }}" {% endfor %}
{% endif %}
>
{{ card.header(item, item.fields.name, solo=solo, identifier=item.fields.set) }}
@@ -26,11 +26,11 @@
{{ badge.rebrickable(item, solo=solo, last=last) }}
{% endif %}
</div>
- {% if not tiny and brickset_checkboxes | length %}
+ {% if not tiny and brickset_statuses | length %}
<ul class="list-group list-group-flush card-check border-bottom-0">
- {% for checkbox in brickset_checkboxes %}
+ {% for status in brickset_statuses %}
<li class="list-group-item {% if not solo %}p-1{% endif %}">
- {{ form.checkbox(checkbox.as_dataset(), item.fields.id, checkbox.fields.name, checkbox.url_for_set_state(item.fields.id), item.fields[checkbox.as_column()], delete=delete) }}
+ {{ form.checkbox(status.as_dataset(), item.fields.id, status.fields.name, status.url_for_set_state(item.fields.id), item.fields[status.as_column()], delete=delete) }}
</li>
{% endfor %}
</ul>
diff --git a/templates/sets.html b/templates/sets.html
index c62d3f3b..c43925fa 100644
--- a/templates/sets.html
+++ b/templates/sets.html
@@ -22,9 +22,9 @@
<option value="-has-missing">Set is complete</option>
<option value="has-missing">Set has missing pieces</option>
<option value="has-missing-instructions">Set has missing instructions</option>
- {% for checkbox in brickset_checkboxes %}
- <option value="{{ checkbox.as_dataset() }}">{{ checkbox.fields.name }}</option>
- <option value="-{{ checkbox.as_dataset() }}">NOT: {{ checkbox.fields.name }}</option>
+ {% for status in brickset_statuses %}
+ <option value="{{ status.as_dataset() }}">{{ status.fields.name }}</option>
+ <option value="-{{ status.as_dataset() }}">NOT: {{ status.fields.name }}</option>
{% endfor %}
</select>
<select id="grid-theme" class="form-select form-select-sm" autocomplete="off">
From 344d4fb5753d1fa8220168050b6b00a494653aa8 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Thu, 30 Jan 2025 16:23:47 +0100
Subject: [PATCH 059/154] Metadata list
---
bricktracker/metadata_list.py | 77 +++++++++++++++++++++++++++++++
bricktracker/reload.py | 3 +-
bricktracker/set.py | 3 +-
bricktracker/set_list.py | 5 +-
bricktracker/set_status_list.py | 64 ++++---------------------
bricktracker/views/admin/admin.py | 2 +-
bricktracker/views/index.py | 3 +-
bricktracker/views/set.py | 7 +--
8 files changed, 99 insertions(+), 65 deletions(-)
create mode 100644 bricktracker/metadata_list.py
diff --git a/bricktracker/metadata_list.py b/bricktracker/metadata_list.py
new file mode 100644
index 00000000..80bb3df0
--- /dev/null
+++ b/bricktracker/metadata_list.py
@@ -0,0 +1,77 @@
+import logging
+from typing import Type
+
+from .exceptions import NotFoundException
+from .fields import BrickRecordFields
+from .record_list import BrickRecordList
+from .set_status import BrickSetStatus
+
+logger = logging.getLogger(__name__)
+
+T = BrickSetStatus
+
+
+# Lego sets metadata list
+class BrickMetadataList(BrickRecordList[T]):
+ kind: str
+ mapping: dict[str, T]
+ model: Type[T]
+
+ # Database table
+ table: str
+
+ # 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)
+
+ if records is None or force:
+ # Don't use super()__init__ as it would mask class variables
+ self.fields = BrickRecordFields()
+
+ logger.info('Loading {kind} list'.format(
+ kind=self.kind
+ ))
+
+ self.__class__.records = []
+ self.__class__.mapping = {}
+
+ # Load the statuses from the database
+ for record in self.select():
+ status = model(record=record)
+
+ self.__class__.records.append(status)
+ self.__class__.mapping[status.fields.id] = status
+
+ # 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)
+ ])
+
+ # Filter the list of records (this one does nothing)
+ def filter(self) -> list[T]:
+ return self.records
+
+ # Grab a specific status
+ def get(self, id: str, /) -> T:
+ if id not in self.mapping:
+ raise NotFoundException(
+ '{kind} with ID {id} was not found in the database'.format(
+ kind=self.kind.capitalize(),
+ id=id,
+ ),
+ )
+
+ return self.mapping[id]
+
+ # Get the list of statuses depending on the context
+ def list(self, /, **kwargs) -> list[T]:
+ return self.filter(**kwargs)
diff --git a/bricktracker/reload.py b/bricktracker/reload.py
index 28de7bff..259cffad 100644
--- a/bricktracker/reload.py
+++ b/bricktracker/reload.py
@@ -1,5 +1,6 @@
from .instructions_list import BrickInstructionsList
from .retired_list import BrickRetiredList
+from .set_status import BrickSetStatus
from .set_status_list import BrickSetStatusList
from .theme_list import BrickThemeList
@@ -12,7 +13,7 @@ def reload() -> None:
BrickInstructionsList(force=True)
# Reload the set statuses
- BrickSetStatusList(force=True)
+ BrickSetStatusList(BrickSetStatus, force=True)
# Reload retired sets
BrickRetiredList(force=True)
diff --git a/bricktracker/set.py b/bricktracker/set.py
index 1b11a943..32bf8daa 100644
--- a/bricktracker/set.py
+++ b/bricktracker/set.py
@@ -9,6 +9,7 @@ from .exceptions import NotFoundException
from .minifigure_list import BrickMinifigureList
from .part_list import BrickPartList
from .rebrickable_set import RebrickableSet
+from .set_status import BrickSetStatus
from .set_status_list import BrickSetStatusList
from .sql import BrickSQL
if TYPE_CHECKING:
@@ -161,7 +162,7 @@ class BrickSet(RebrickableSet):
# Load from database
if not self.select(
- statuses=BrickSetStatusList().as_columns(solo=True)
+ statuses=BrickSetStatusList(BrickSetStatus).as_columns(all=True)
):
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 47d31b21..251cfdcd 100644
--- a/bricktracker/set_list.py
+++ b/bricktracker/set_list.py
@@ -3,6 +3,7 @@ from typing import Self
from flask import current_app
from .record_list import BrickRecordList
+from .set_status import BrickSetStatus
from .set_status_list import BrickSetStatusList
from .set import BrickSet
@@ -37,7 +38,7 @@ class BrickSetList(BrickRecordList[BrickSet]):
# Load the sets from the database
for record in self.select(
order=self.order,
- statuses=BrickSetStatusList().as_columns()
+ statuses=BrickSetStatusList(BrickSetStatus).as_columns()
):
brickset = BrickSet(record=record)
@@ -73,7 +74,7 @@ class BrickSetList(BrickRecordList[BrickSet]):
for record in self.select(
order=order,
limit=limit,
- statuses=BrickSetStatusList().as_columns()
+ statuses=BrickSetStatusList(BrickSetStatus).as_columns()
):
brickset = BrickSet(record=record)
diff --git a/bricktracker/set_status_list.py b/bricktracker/set_status_list.py
index 12ce85a1..b96f2139 100644
--- a/bricktracker/set_status_list.py
+++ b/bricktracker/set_status_list.py
@@ -1,71 +1,23 @@
import logging
-from .exceptions import NotFoundException
-from .fields import BrickRecordFields
-from .record_list import BrickRecordList
+from .metadata_list import BrickMetadataList
from .set_status import BrickSetStatus
logger = logging.getLogger(__name__)
# Lego sets status list
-class BrickSetStatusList(BrickRecordList[BrickSetStatus]):
- statuses: dict[str, BrickSetStatus]
+class BrickSetStatusList(BrickMetadataList):
+ kind: str = 'set statuses'
+
+ # Database table
+ table: str = 'bricktracker_set_statuses'
# Queries
select_query = 'set/metadata/status/list'
- def __init__(self, /, *, force: bool = False):
- # Load statuses only if there is none already loaded
- records = getattr(self, 'records', None)
-
- if records is None or force:
- # Don't use super()__init__ as it would mask class variables
- self.fields = BrickRecordFields()
-
- logger.info('Loading set statuses list')
-
- BrickSetStatusList.records = []
- BrickSetStatusList.statuses = {}
-
- # Load the statuses from the database
- for record in self.select():
- status = BrickSetStatus(record=record)
-
- BrickSetStatusList.records.append(status)
- BrickSetStatusList.statuses[status.fields.id] = status
-
- # Return the statuses as columns for a select
- def as_columns(
- self,
- /,
- *,
- solo: bool = False,
- table: str = 'bricktracker_set_statuses'
- ) -> str:
- return ', '.join([
- '"{table}"."{column}"'.format(
- table=table,
- column=record.as_column(),
- )
- for record
- in self.records
- if solo or record.fields.displayed_on_grid
- ])
-
- # Grab a specific status
- def get(self, id: str, /) -> BrickSetStatus:
- if id not in self.statuses:
- raise NotFoundException(
- 'Status with ID {id} was not found in the database'.format(
- id=id,
- ),
- )
-
- return self.statuses[id]
-
- # Get the list of statuses depending on the context
- def list(self, /, *, all: bool = False) -> list[BrickSetStatus]:
+ # Filter the list of set status
+ def filter(self, all: bool = False) -> list[BrickSetStatus]:
return [
record
for record
diff --git a/bricktracker/views/admin/admin.py b/bricktracker/views/admin/admin.py
index a7f8ce70..c18a74b2 100644
--- a/bricktracker/views/admin/admin.py
+++ b/bricktracker/views/admin/admin.py
@@ -41,7 +41,7 @@ def admin() -> str:
database_version = database.version
database_counters = BrickSQL().count_records()
- metadata_statuses = BrickSetStatusList().list(all=True)
+ metadata_statuses = BrickSetStatusList(BrickSetStatus).list(all=True)
except Exception as e:
database_exception = e
diff --git a/bricktracker/views/index.py b/bricktracker/views/index.py
index 3e3a8802..f8fe7b71 100644
--- a/bricktracker/views/index.py
+++ b/bricktracker/views/index.py
@@ -2,6 +2,7 @@ from flask import Blueprint, render_template
from .exceptions import exception_handler
from ..minifigure_list import BrickMinifigureList
+from ..set_status import BrickSetStatus
from ..set_status_list import BrickSetStatusList
from ..set_list import BrickSetList
@@ -16,5 +17,5 @@ def index() -> str:
'index.html',
brickset_collection=BrickSetList().last(),
minifigure_collection=BrickMinifigureList().last(),
- brickset_statuses=BrickSetStatusList().list(),
+ brickset_statuses=BrickSetStatusList(BrickSetStatus).list(),
)
diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py
index 17fcff98..9f1990cc 100644
--- a/bricktracker/views/set.py
+++ b/bricktracker/views/set.py
@@ -16,6 +16,7 @@ from .exceptions import exception_handler
from ..minifigure import BrickMinifigure
from ..part import BrickPart
from ..set import BrickSet
+from ..set_status import BrickSetStatus
from ..set_status_list import BrickSetStatusList
from ..set_list import BrickSetList
from ..socket import MESSAGES
@@ -32,7 +33,7 @@ def list() -> str:
return render_template(
'sets.html',
collection=BrickSetList().all(),
- brickset_statuses=BrickSetStatusList().list(),
+ brickset_statuses=BrickSetStatusList(BrickSetStatus).list(),
)
@@ -42,7 +43,7 @@ def list() -> str:
@exception_handler(__file__, json=True)
def update_status(*, id: str, metadata_id: str) -> Response:
brickset = BrickSet().select_light(id)
- status = BrickSetStatusList().get(metadata_id)
+ status = BrickSetStatusList(BrickSetStatus).get(metadata_id)
state = status.update_set_state(brickset, request.json)
@@ -97,7 +98,7 @@ def details(*, id: str) -> str:
'set.html',
item=BrickSet().select_specific(id),
open_instructions=request.args.get('open_instructions'),
- brickset_statuses=BrickSetStatusList().list(all=True),
+ brickset_statuses=BrickSetStatusList(BrickSetStatus).list(all=True),
)
From fc3c92e9a33b0aea670b102e7375fc4fe970e980 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Thu, 30 Jan 2025 16:35:26 +0100
Subject: [PATCH 060/154] Remove metadata prefix, it's identical to kind
---
bricktracker/metadata.py | 6 ++++--
bricktracker/set_status.py | 1 -
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/bricktracker/metadata.py b/bricktracker/metadata.py
index e21bfdd4..ac13c64a 100644
--- a/bricktracker/metadata.py
+++ b/bricktracker/metadata.py
@@ -17,7 +17,6 @@ logger = logging.getLogger(__name__)
# Lego set metadata (customizable list of entries that can be checked)
class BrickMetadata(BrickRecord):
kind: str
- prefix: str
# Set state endpoint
set_state_endpoint: str
@@ -43,7 +42,10 @@ class BrickMetadata(BrickRecord):
# SQL column name
def as_column(self, /) -> str:
- return '{prefix}_{id}'.format(id=self.fields.id, prefix=self.prefix)
+ return '{kind}_{id}'.format(
+ id=self.fields.id,
+ kind=self.kind.lower()
+ )
# HTML dataset name
def as_dataset(self, /) -> str:
diff --git a/bricktracker/set_status.py b/bricktracker/set_status.py
index 3b874f52..0165c501 100644
--- a/bricktracker/set_status.py
+++ b/bricktracker/set_status.py
@@ -7,7 +7,6 @@ from .metadata import BrickMetadata
# Lego set status metadata
class BrickSetStatus(BrickMetadata):
kind: str = 'status'
- prefix: str = 'status'
# Set state endpoint
set_state_endpoint: str = 'set.update_status'
From d15d7ffb61e5ac5f908aa634720a95db5104e10a Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Thu, 30 Jan 2025 16:38:11 +0100
Subject: [PATCH 061/154] Move from_form function about name to the base
metadata class
---
bricktracker/metadata.py | 11 +++++++++++
bricktracker/set_status.py | 8 ++------
2 files changed, 13 insertions(+), 6 deletions(-)
diff --git a/bricktracker/metadata.py b/bricktracker/metadata.py
index ac13c64a..c7a9678b 100644
--- a/bricktracker/metadata.py
+++ b/bricktracker/metadata.py
@@ -60,6 +60,17 @@ class BrickMetadata(BrickRecord):
id=self.fields.id,
)
+ # Grab data from a form
+ def from_form(self, form: dict[str, str], /) -> Self:
+ name = form.get('name', None)
+
+ if name is None or name == '':
+ raise ErrorException('Status name cannot be empty')
+
+ self.fields.name = name
+
+ return self
+
# Insert into database
def insert(self, /, **context) -> None:
self.safe()
diff --git a/bricktracker/set_status.py b/bricktracker/set_status.py
index 0165c501..d114d651 100644
--- a/bricktracker/set_status.py
+++ b/bricktracker/set_status.py
@@ -1,6 +1,5 @@
from typing import Self
-from .exceptions import ErrorException
from .metadata import BrickMetadata
@@ -20,13 +19,10 @@ class BrickSetStatus(BrickMetadata):
# Grab data from a form
def from_form(self, form: dict[str, str], /) -> Self:
- name = form.get('name', None)
+ super().from_form(form)
+
grid = form.get('grid', None)
- if name is None or name == '':
- raise ErrorException('Status name cannot be empty')
-
- self.fields.name = name
self.fields.displayed_on_grid = grid == 'on'
return self
From 637be0d272f2e124752bacf8be9751b03635bab9 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Thu, 30 Jan 2025 16:54:22 +0100
Subject: [PATCH 062/154] Fix admin status error
---
bricktracker/views/admin/status.py | 2 +-
templates/admin/status/delete.html | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/bricktracker/views/admin/status.py b/bricktracker/views/admin/status.py
index 8178d693..49037f3d 100644
--- a/bricktracker/views/admin/status.py
+++ b/bricktracker/views/admin/status.py
@@ -46,7 +46,7 @@ def delete(*, id: str) -> str:
'admin.html',
delete_status=True,
status=BrickSetStatus().select_specific(id),
- error=request.args.get('status_error')
+ status_error=request.args.get('status_error')
)
diff --git a/templates/admin/status/delete.html b/templates/admin/status/delete.html
index 31998c4b..76e2f715 100644
--- a/templates/admin/status/delete.html
+++ b/templates/admin/status/delete.html
@@ -2,7 +2,7 @@
{{ accordion.header('Set statuses danger zone', 'status-danger', 'admin', expanded=true, danger=true, class='text-end') }}
<form action="{{ url_for('admin_status.do_delete', id=status.fields.id) }}" method="post">
- {% if error %}<div class="alert alert-danger text-start" role="alert"><strong>Error:</strong> {{ error }}.</div>{% endif %}
+ {% if status_error %}<div class="alert alert-danger text-start" role="alert"><strong>Error:</strong> {{ status_error }}.</div>{% endif %}
<div class="alert alert-danger text-center" role="alert">You are about to <strong>delete a set status</strong>. This action is irreversible.</div>
<div class="row row-cols-lg-auto g-3 align-items-center">
<div class="col-12 flex-grow-1">
From d6a729b5a522ecf2a8038937da4cd8e7bb942c83 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Thu, 30 Jan 2025 17:39:55 +0100
Subject: [PATCH 063/154] Move the checkbox logic inside the macro
---
templates/macro/form.html | 13 +++++++------
templates/set/card.html | 4 +---
2 files changed, 8 insertions(+), 9 deletions(-)
diff --git a/templates/macro/form.html b/templates/macro/form.html
index 499c2205..379309f7 100644
--- a/templates/macro/form.html
+++ b/templates/macro/form.html
@@ -1,17 +1,18 @@
-{% macro checkbox(prefix, id, text, url, checked, delete=false) %}
+{% macro checkbox(item, metadata, delete=false) %}
{% if g.login.is_authenticated() %}
- <input class="form-check-input" type="checkbox" id="{{ prefix }}-{{ id }}" {% if checked %}checked{% endif %}
+ {% set prefix=metadata.as_dataset() %}
+ <input class="form-check-input" type="checkbox" id="{{ prefix }}-{{ item.fields.id }}" {% if item.fields[metadata.as_column()] %}checked{% endif %}
{% if not delete %}
- data-changer-id="{{ id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ url }}" data-changer-parent="set"
+ data-changer-id="{{ item.fields.id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ metadata.url_for_set_state(item.fields.id) }}" data-changer-parent="set"
{% else %}
disabled
{% endif %}
autocomplete="off">
- <label class="form-check-label" for="{{ prefix }}-{{ id }}">
- {{ text }} <i id="status-{{ prefix }}-{{ id }}" class="mb-1"></i>
+ <label class="form-check-label" for="{{ prefix }}-{{ item.fields.id }}">
+ {{ metadata.fields.name }} <i id="status-{{ prefix }}-{{ item.fields.id }}" class="mb-1"></i>
</label>
{% else %}
- <input class="form-check-input text-reset" type="checkbox" {% if checked %}checked{% endif %} disabled>
+ <input class="form-check-input text-reset" type="checkbox" {% if item.fields[metadata.as_column()] %}checked{% endif %} disabled>
{{ text }}
{% endif %}
{% endmacro %}
diff --git a/templates/set/card.html b/templates/set/card.html
index fea4fc04..19e493cb 100644
--- a/templates/set/card.html
+++ b/templates/set/card.html
@@ -29,9 +29,7 @@
{% if not tiny and brickset_statuses | length %}
<ul class="list-group list-group-flush card-check border-bottom-0">
{% for status in brickset_statuses %}
- <li class="list-group-item {% if not solo %}p-1{% endif %}">
- {{ form.checkbox(status.as_dataset(), item.fields.id, status.fields.name, status.url_for_set_state(item.fields.id), item.fields[status.as_column()], delete=delete) }}
- </li>
+ <li class="list-group-item {% if not solo %}p-1{% endif %}">{{ form.checkbox(item, status, delete=delete) }}</li>
{% endfor %}
</ul>
{% endif %}
From cf641b3199efb6573251e4f40d0085228fc422a4 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Thu, 30 Jan 2025 21:57:42 +0100
Subject: [PATCH 064/154] Separate the filters from the search and sort in the
set grid
---
static/scripts/grid.js | 14 +++++------
templates/sets.html | 53 ++++++++++++++++++++++++------------------
2 files changed, 37 insertions(+), 30 deletions(-)
diff --git a/static/scripts/grid.js b/static/scripts/grid.js
index 42b8ac3b..7f40b0ab 100644
--- a/static/scripts/grid.js
+++ b/static/scripts/grid.js
@@ -57,7 +57,7 @@ class BrickGrid {
this.html_grid = document.getElementById(this.id);
this.html_sort = document.getElementById(`${this.id}-sort`);
this.html_search = document.getElementById(`${this.id}-search`);
- this.html_filter = document.getElementById(`${this.id}-filter`);
+ this.html_status = document.getElementById(`${this.id}-status`);
this.html_theme = document.getElementById(`${this.id}-theme`);
// Sort buttons
@@ -83,8 +83,8 @@ class BrickGrid {
})(this));
}
- if (this.html_filter) {
- this.html_filter.addEventListener("change", ((grid) => () => {
+ if (this.html_status) {
+ this.html_status.addEventListener("change", ((grid) => () => {
grid.filter();
})(this));
}
@@ -147,12 +147,12 @@ class BrickGrid {
}
// Check if there is a set filter
- if (this.html_filter && this.html_filter.value != "") {
- if (this.html_filter.value.startsWith("-")) {
- filters["filter"] = this.html_filter.value.substring(1);
+ if (this.html_status && this.html_status.value != "") {
+ if (this.html_status.value.startsWith("-")) {
+ filters["filter"] = this.html_status.value.substring(1);
filters["filter-target"] = "0";
} else {
- filters["filter"] = this.html_filter.value;
+ filters["filter"] = this.html_status.value;
filters["filter-target"] = "1";
}
}
diff --git a/templates/sets.html b/templates/sets.html
index c43925fa..e7003c36 100644
--- a/templates/sets.html
+++ b/templates/sets.html
@@ -13,28 +13,6 @@
<input id="grid-search" class="form-control form-control-sm" type="text" placeholder="Set name, set number, set theme or number of parts..." value="">
</div>
</div>
- <div class="col-12 flex-grow-1">
- <label class="visually-hidden" for="grid-filter">Filter</label>
- <div class="input-group">
- <span class="input-group-text"><i class="ri-dropdown-list"></i><span class="ms-1 d-none d-xl-inline"> Filter</span></span>
- <select id="grid-filter" class="form-select form-select-sm" autocomplete="off">
- <option value="" selected>All sets</option>
- <option value="-has-missing">Set is complete</option>
- <option value="has-missing">Set has missing pieces</option>
- <option value="has-missing-instructions">Set has missing instructions</option>
- {% for status in brickset_statuses %}
- <option value="{{ status.as_dataset() }}">{{ status.fields.name }}</option>
- <option value="-{{ status.as_dataset() }}">NOT: {{ status.fields.name }}</option>
- {% endfor %}
- </select>
- <select id="grid-theme" class="form-select form-select-sm" autocomplete="off">
- <option value="" selected>All themes</option>
- {% for theme in collection.themes %}
- <option value="{{ theme | lower }}">{{ theme }}</option>
- {% endfor %}
- </select>
- </div>
- </div>
<div class="col-12">
<div id="grid-sort" class="input-group">
<span class="input-group-text"><i class="ri-sort-asc"></i><span class="ms-1 d-none d-xxl-inline"> Sort</span></span>
@@ -57,7 +35,36 @@
</div>
</div>
</div>
-
+ <div class="row row-cols-lg-auto g-1 justify-content-center align-items-center pb-2">
+ <div class="col-12 flex-grow-1">
+ <label class="visually-hidden" for="grid-status">Status</label>
+ <div class="input-group">
+ <span class="input-group-text"><i class="ri-checkbox-line"></i><span class="ms-1 d-none d-xl-inline"> Status</span></span>
+ <select id="grid-status" class="form-select form-select-sm" autocomplete="off">
+ <option value="" selected>All</option>
+ <option value="-has-missing">Set is complete</option>
+ <option value="has-missing">Set has missing pieces</option>
+ <option value="has-missing-instructions">Set has missing instructions</option>
+ {% for status in brickset_statuses %}
+ <option value="{{ status.as_dataset() }}">{{ status.fields.name }}</option>
+ <option value="-{{ status.as_dataset() }}">NOT: {{ status.fields.name }}</option>
+ {% endfor %}
+ </select>
+ </div>
+ </div>
+ <div class="col-12 flex-grow-1">
+ <label class="visually-hidden" for="grid-theme">Theme</label>
+ <div class="input-group">
+ <span class="input-group-text"><i class="ri-price-tag-3-line"></i><span class="ms-1 d-none d-xl-inline"> Theme</span></span>
+ <select id="grid-theme" class="form-select form-select-sm" autocomplete="off">
+ <option value="" selected>All</option>
+ {% for theme in collection.themes %}
+ <option value="{{ theme | lower }}">{{ theme }}</option>
+ {% endfor %}
+ </select>
+ </div>
+ </div>
+ </div>
<div class="row" data-grid="true" id="grid">
{% for item in collection %}
<div class="col-md-6 col-xl-3 d-flex align-items-stretch">
From 2eb8ebfeca39c49b6e4932873bc4309e2bb71d30 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Thu, 30 Jan 2025 22:07:09 +0100
Subject: [PATCH 065/154] Remove sort-target attribute, handle it internally
---
static/scripts/grid.js | 14 +++++++-------
templates/sets.html | 16 ++++++++--------
2 files changed, 15 insertions(+), 15 deletions(-)
diff --git a/static/scripts/grid.js b/static/scripts/grid.js
index 7f40b0ab..fbd4e08a 100644
--- a/static/scripts/grid.js
+++ b/static/scripts/grid.js
@@ -50,8 +50,9 @@ class BrickGridSortButton {
// Grid class
class BrickGrid {
- constructor(grid) {
+ constructor(grid, target = "div#grid>div") {
this.id = grid.id;
+ this.target = target;
// Grid elements (built based on the initial id)
this.html_grid = document.getElementById(this.id);
@@ -72,7 +73,7 @@ class BrickGrid {
this.html_clear = document.querySelector("button[data-sort-clear]")
if (this.html_clear) {
this.html_clear.addEventListener("click", ((grid) => (e) => {
- grid.clear(e.currentTarget)
+ grid.clear();
})(this))
}
@@ -117,7 +118,7 @@ class BrickGrid {
}
// Clear
- clear(current) {
+ clear() {
// Cleanup all
for (const [id, button] of Object.entries(this.html_sort_buttons)) {
button.toggle();
@@ -129,7 +130,7 @@ class BrickGrid {
document.cookie = `sort-order=""; Path=/; SameSite=strict`;
// Reset sorting
- tinysort(current.dataset.sortTarget, {
+ tinysort(this.target, {
selector: "div",
attr: "data-index",
order: "asc",
@@ -204,7 +205,6 @@ class BrickGrid {
// Sort
sort(current, no_flip=false) {
- const target = current.data.sortTarget;
const attribute = current.data.sortAttribute;
const natural = current.data.sortNatural;
@@ -217,7 +217,7 @@ class BrickGrid {
}
// Sort
- if (target && attribute) {
+ if (attribute) {
let order = current.data.sortOrder;
// First ordering
@@ -242,7 +242,7 @@ class BrickGrid {
document.cookie = `sort-order="${encodeURIComponent(order)}"; Path=/; SameSite=strict`;
// Do the sorting
- tinysort(target, {
+ tinysort(this.target, {
selector: "div",
attr: "data-" + attribute,
natural: natural == "true",
diff --git a/templates/sets.html b/templates/sets.html
index e7003c36..cfcbbfcc 100644
--- a/templates/sets.html
+++ b/templates/sets.html
@@ -17,21 +17,21 @@
<div id="grid-sort" class="input-group">
<span class="input-group-text"><i class="ri-sort-asc"></i><span class="ms-1 d-none d-xxl-inline"> Sort</span></span>
<button id="sort-number" type="button" class="btn btn-sm btn-outline-primary"
- data-sort-target="div#grid>div" data-sort-attribute="number" data-sort-natural="true"><i class="ri-hashtag"></i><span class="d-none d-xxl-inline"> Number</span></button>
+ data-sort-attribute="number" data-sort-natural="true"><i class="ri-hashtag"></i><span class="d-none d-xxl-inline"> Set</span></button>
<button id="sort-name" type="button" class="btn btn-sm btn-outline-primary"
- data-sort-target="div#grid>div" data-sort-attribute="name"><i class="ri-pencil-line"></i><span class="d-none d-xxl-inline"> Name</span></button>
+ data-sort-attribute="name"><i class="ri-pencil-line"></i><span class="d-none d-xxl-inline"> Name</span></button>
<button id="sort-theme" type="button" class="btn btn-sm btn-outline-primary"
- data-sort-target="div#grid>div" data-sort-attribute="theme"><i class="ri-price-tag-3-line"></i><span class="d-none d-xxl-inline"> Theme</span></button>
+ data-sort-attribute="theme"><i class="ri-price-tag-3-line"></i><span class="d-none d-xxl-inline"> Theme</span></button>
<button id="sort-year" type="button" class="btn btn-sm btn-outline-primary"
- data-sort-target="div#grid>div" data-sort-attribute="year"><i class="ri-calendar-line"></i><span class="d-none d-xxl-inline"> Year</span></button>
+ data-sort-attribute="year"><i class="ri-calendar-line"></i><span class="d-none d-xxl-inline"> Year</span></button>
<button id="sort-minifigure" type="button" class="btn btn-sm btn-outline-primary"
- data-sort-target="div#grid>div" data-sort-attribute="minifigures" data-sort-desc="true"><i class="ri-group-line"></i><span class="d-none d-xxl-inline"> Figures</span></button>
+ data-sort-attribute="minifigures" data-sort-desc="true"><i class="ri-group-line"></i><span class="d-none d-xxl-inline"> Figures</span></button>
<button id="sort-parts" type="button" class="btn btn-sm btn-outline-primary"
- data-sort-target="div#grid>div" data-sort-attribute="parts" data-sort-desc="true"><i class="ri-shapes-line"></i><span class="d-none d-xxl-inline"> Parts</span></button>
+ data-sort-attribute="parts" data-sort-desc="true"><i class="ri-shapes-line"></i><span class="d-none d-xxl-inline"> Parts</span></button>
<button id="sort-missing" type="button" class="btn btn-sm btn-outline-primary"
- data-sort-target="div#grid>div" data-sort-attribute="missing" data-sort-desc="true"><i class="ri-error-warning-line"></i><span class="d-none d-xxl-inline"> Missing</span></button>
+ data-sort-attribute="missing" data-sort-desc="true"><i class="ri-error-warning-line"></i><span class="d-none d-xxl-inline"> Missing</span></button>
<button id="sort-clear" type="button" class="btn btn-sm btn-outline-dark"
- data-sort-target="div#grid>div" data-sort-clear="true"><i class="ri-filter-off-line"></i><span class="d-none d-xxl-inline"> Clear</span></button>
+ data-sort-clear="true"><i class="ri-filter-off-line"></i><span class="d-none d-xxl-inline"> Clear</span></button>
</div>
</div>
</div>
From f854a01925986ab223edb7e9ba62cb3aeef8d3c0 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Thu, 30 Jan 2025 22:10:47 +0100
Subject: [PATCH 066/154] Split the grid javascript code
---
static/scripts/{ => grid}/grid.js | 50 -------------------------------
static/scripts/grid/sort.js | 49 ++++++++++++++++++++++++++++++
templates/base.html | 3 +-
3 files changed, 51 insertions(+), 51 deletions(-)
rename static/scripts/{ => grid}/grid.js (85%)
create mode 100644 static/scripts/grid/sort.js
diff --git a/static/scripts/grid.js b/static/scripts/grid/grid.js
similarity index 85%
rename from static/scripts/grid.js
rename to static/scripts/grid/grid.js
index fbd4e08a..8a929e4d 100644
--- a/static/scripts/grid.js
+++ b/static/scripts/grid/grid.js
@@ -1,53 +1,3 @@
-// Sort button
-class BrickGridSortButton {
- constructor(button, grid) {
- this.button = button;
- this.grid = grid;
- this.data = this.button.dataset;
-
- // Setup
- button.addEventListener("click", ((grid, button) => (e) => {
- grid.sort(button);
- })(grid, this));
- }
-
- // Active
- active() {
- this.button.classList.remove("btn-outline-primary");
- this.button.classList.add("btn-primary");
- }
-
- // Inactive
- inactive() {
- delete this.button.dataset.sortOrder;
- this.button.classList.remove("btn-primary");
- this.button.classList.add("btn-outline-primary");
- }
-
- // Toggle sorting
- toggle(order) {
- // Cleanup
- delete this.button.dataset.sortOrder;
-
- let icon = this.button.querySelector("i.ri");
- if (icon) {
- this.button.removeChild(icon);
- }
-
- // Set order
- if (order) {
- this.active();
-
- this.button.dataset.sortOrder = order;
-
- icon = document.createElement("i");
- icon.classList.add("ri", "ms-1", `ri-sort-${order}`);
-
- this.button.append(icon);
- }
- }
-}
-
// Grid class
class BrickGrid {
constructor(grid, target = "div#grid>div") {
diff --git a/static/scripts/grid/sort.js b/static/scripts/grid/sort.js
new file mode 100644
index 00000000..dcb23f02
--- /dev/null
+++ b/static/scripts/grid/sort.js
@@ -0,0 +1,49 @@
+// Sort button
+class BrickGridSortButton {
+ constructor(button, grid) {
+ this.button = button;
+ this.grid = grid;
+ this.data = this.button.dataset;
+
+ // Setup
+ button.addEventListener("click", ((grid, button) => (e) => {
+ grid.sort(button);
+ })(grid, this));
+ }
+
+ // Active
+ active() {
+ this.button.classList.remove("btn-outline-primary");
+ this.button.classList.add("btn-primary");
+ }
+
+ // Inactive
+ inactive() {
+ delete this.button.dataset.sortOrder;
+ this.button.classList.remove("btn-primary");
+ this.button.classList.add("btn-outline-primary");
+ }
+
+ // Toggle sorting
+ toggle(order) {
+ // Cleanup
+ delete this.button.dataset.sortOrder;
+
+ let icon = this.button.querySelector("i.ri");
+ if (icon) {
+ this.button.removeChild(icon);
+ }
+
+ // Set order
+ if (order) {
+ this.active();
+
+ this.button.dataset.sortOrder = order;
+
+ icon = document.createElement("i");
+ icon.classList.add("ri", "ms-1", `ri-sort-${order}`);
+
+ this.button.append(icon);
+ }
+ }
+}
diff --git a/templates/base.html b/templates/base.html
index 6d89d3df..846a861a 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -80,7 +80,8 @@
<script src="https://cdn.jsdelivr.net/npm/simple-datatables@9.2.1/dist/umd/simple-datatables.min.js"></script>
<!-- BrickTracker scripts -->
<script src="{{ url_for('static', filename='scripts/changer.js') }}"></script>
- <script src="{{ url_for('static', filename='scripts/grid.js') }}"></script>
+ <script src="{{ url_for('static', filename='scripts/grid/grid.js') }}"></script>
+ <script src="{{ url_for('static', filename='scripts/grid/sort.js') }}"></script>
<script src="{{ url_for('static', filename='scripts/set.js') }}"></script>
<script src="{{ url_for('static', filename='scripts/socket/socket.js') }}"></script>
<script src="{{ url_for('static', filename='scripts/socket/instructions.js') }}"></script>
From d80728d1334053b425bd05288b7b76dd0a87bb83 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Thu, 30 Jan 2025 22:30:14 +0100
Subject: [PATCH 067/154] Create dedicated javascript object for Grid sort
---
static/scripts/grid/grid.js | 105 +--------------------
static/scripts/grid/sort.js | 143 +++++++++++++++++++++--------
static/scripts/grid/sort_button.js | 49 ++++++++++
templates/base.html | 1 +
4 files changed, 158 insertions(+), 140 deletions(-)
create mode 100644 static/scripts/grid/sort_button.js
diff --git a/static/scripts/grid/grid.js b/static/scripts/grid/grid.js
index 8a929e4d..e11c2bd3 100644
--- a/static/scripts/grid/grid.js
+++ b/static/scripts/grid/grid.js
@@ -6,26 +6,12 @@ class BrickGrid {
// Grid elements (built based on the initial id)
this.html_grid = document.getElementById(this.id);
- this.html_sort = document.getElementById(`${this.id}-sort`);
this.html_search = document.getElementById(`${this.id}-search`);
this.html_status = document.getElementById(`${this.id}-status`);
this.html_theme = document.getElementById(`${this.id}-theme`);
- // Sort buttons
- this.html_sort_buttons = {};
- if (this.html_sort) {
- this.html_sort.querySelectorAll("button[data-sort-attribute]").forEach(button => {
- this.html_sort_buttons[button.id] = new BrickGridSortButton(button, this);
- });
- }
-
- // Clear button
- this.html_clear = document.querySelector("button[data-sort-clear]")
- if (this.html_clear) {
- this.html_clear.addEventListener("click", ((grid) => (e) => {
- grid.clear();
- })(this))
- }
+ // Sort setup
+ this.sort = new BrickGridSort(this);
// Filter setup
if (this.html_search) {
@@ -46,46 +32,6 @@ class BrickGrid {
})(this));
}
- // Cookie setup
- const cookies = document.cookie.split(";").reduce((acc, cookieString) => {
- const [key, value] = cookieString.split("=").map(s => s.trim().replace(/^"|"$/g, ""));
- if (key && value) {
- acc[key] = decodeURIComponent(value);
- }
- return acc;
- }, {});
-
- // Initial sort
- if ("sort-id" in cookies && cookies["sort-id"] in this.html_sort_buttons) {
- const current = this.html_sort_buttons[cookies["sort-id"]];
-
- if("sort-order" in cookies) {
- current.button.setAttribute("data-sort-order", cookies["sort-order"]);
- }
-
- this.sort(current, true);
- }
- }
-
- // Clear
- clear() {
- // Cleanup all
- for (const [id, button] of Object.entries(this.html_sort_buttons)) {
- button.toggle();
- button.inactive();
- }
-
- // Clear cookies
- document.cookie = `sort-id=""; Path=/; SameSite=strict`;
- document.cookie = `sort-order=""; Path=/; SameSite=strict`;
-
- // Reset sorting
- tinysort(this.target, {
- selector: "div",
- attr: "data-index",
- order: "asc",
- });
-
}
// Filter
@@ -153,53 +99,6 @@ class BrickGrid {
}
}
- // Sort
- sort(current, no_flip=false) {
- const attribute = current.data.sortAttribute;
- const natural = current.data.sortNatural;
-
- // Cleanup all
- for (const [id, button] of Object.entries(this.html_sort_buttons)) {
- if (button != current) {
- button.toggle();
- button.inactive();
- }
- }
-
- // Sort
- if (attribute) {
- let order = current.data.sortOrder;
-
- // First ordering
- if (!no_flip) {
- if(!order) {
- if (current.data.sortDesc) {
- order = "desc"
- } else {
- order = "asc"
- }
- } else {
- // Flip the sorting order
- order = (order == "desc") ? "asc" : "desc";
- }
- }
-
- // Toggle the ordering
- current.toggle(order);
-
- // Store cookies
- document.cookie = `sort-id="${encodeURIComponent(current.button.id)}"; Path=/; SameSite=strict`;
- document.cookie = `sort-order="${encodeURIComponent(order)}"; Path=/; SameSite=strict`;
-
- // Do the sorting
- tinysort(this.target, {
- selector: "div",
- attr: "data-" + attribute,
- natural: natural == "true",
- order: order,
- });
- }
- }
}
// Helper to setup the grids
diff --git a/static/scripts/grid/sort.js b/static/scripts/grid/sort.js
index dcb23f02..0fc4e5fe 100644
--- a/static/scripts/grid/sort.js
+++ b/static/scripts/grid/sort.js
@@ -1,49 +1,118 @@
-// Sort button
-class BrickGridSortButton {
- constructor(button, grid) {
- this.button = button;
+// Grid sort
+class BrickGridSort {
+ constructor(grid) {
this.grid = grid;
- this.data = this.button.dataset;
- // Setup
- button.addEventListener("click", ((grid, button) => (e) => {
- grid.sort(button);
- })(grid, this));
+ // Grid sort elements (built based on the initial id)
+ this.html_sort = document.getElementById(`${this.grid.id}-sort`);
+
+ if (this.html_sort) {
+ // Cookie names
+ this.cookie_id = `${this.grid.id}-sort-id`;
+ this.cookie_order = `${this.grid.id}-sort-order`;
+
+ // Sort buttons
+ this.html_sort_buttons = {};
+ this.html_sort.querySelectorAll("button[data-sort-attribute]").forEach(button => {
+ this.html_sort_buttons[button.id] = new BrickGridSortButton(button, this);
+ });
+
+ // Clear button
+ this.html_clear = this.html_sort.querySelector("button[data-sort-clear]")
+ if (this.html_clear) {
+ this.html_clear.addEventListener("click", ((grid) => () => {
+ grid.clear();
+ })(this))
+ }
+
+ // Cookie setup
+ const cookies = document.cookie.split(";").reduce((acc, cookieString) => {
+ const [key, value] = cookieString.split("=").map(s => s.trim().replace(/^"|"$/g, ""));
+ if (key && value) {
+ acc[key] = decodeURIComponent(value);
+ }
+ return acc;
+ }, {});
+
+ // Initial sort
+ if (this.cookie_id in cookies && cookies[this.cookie_id] in this.html_sort_buttons) {
+ const current = this.html_sort_buttons[cookies[this.cookie_id]];
+
+ if(this.cookie_order in cookies) {
+ current.button.setAttribute("data-sort-order", cookies[this.cookie_order]);
+ }
+
+ this.sort(current, true);
+ }
+ }
}
- // Active
- active() {
- this.button.classList.remove("btn-outline-primary");
- this.button.classList.add("btn-primary");
- }
-
- // Inactive
- inactive() {
- delete this.button.dataset.sortOrder;
- this.button.classList.remove("btn-primary");
- this.button.classList.add("btn-outline-primary");
- }
-
- // Toggle sorting
- toggle(order) {
- // Cleanup
- delete this.button.dataset.sortOrder;
-
- let icon = this.button.querySelector("i.ri");
- if (icon) {
- this.button.removeChild(icon);
+ // Clear sort
+ clear() {
+ // Cleanup all
+ for (const [id, button] of Object.entries(this.html_sort_buttons)) {
+ button.toggle();
+ button.inactive();
}
- // Set order
- if (order) {
- this.active();
+ // Clear cookies
+ document.cookie = `${this.cookie_id}=""; Path=/; SameSite=strict`;
+ document.cookie = `${this.cookie_order}=""; Path=/; SameSite=strict`;
- this.button.dataset.sortOrder = order;
+ // Reset sorting
+ tinysort(this.grid.target, {
+ selector: "div",
+ attr: "data-index",
+ order: "asc",
+ });
- icon = document.createElement("i");
- icon.classList.add("ri", "ms-1", `ri-sort-${order}`);
+ }
- this.button.append(icon);
+ // Sort
+ sort(current, no_flip=false) {
+ const attribute = current.data.sortAttribute;
+ const natural = current.data.sortNatural;
+
+ // Cleanup all
+ for (const [id, button] of Object.entries(this.html_sort_buttons)) {
+ if (button != current) {
+ button.toggle();
+ button.inactive();
+ }
+ }
+
+ // Sort
+ if (attribute) {
+ let order = current.data.sortOrder;
+
+ // First ordering
+ if (!no_flip) {
+ if(!order) {
+ if (current.data.sortDesc) {
+ order = "desc";
+ } else {
+ order = "asc";
+ }
+ } else {
+ // Flip the sorting order
+ order = (order == "desc") ? "asc" : "desc";
+ }
+ }
+
+ // Toggle the ordering
+ current.toggle(order);
+
+ // Store cookies
+ document.cookie = `${this.cookie_id}="${encodeURIComponent(current.button.id)}"; Path=/; SameSite=strict`;
+ document.cookie = `${this.cookie_order}="${encodeURIComponent(order)}"; Path=/; SameSite=strict`;
+
+ // Do the sorting
+ tinysort(this.grid.target, {
+ selector: "div",
+ attr: "data-" + attribute,
+ natural: natural == "true",
+ order: order,
+ });
}
}
}
diff --git a/static/scripts/grid/sort_button.js b/static/scripts/grid/sort_button.js
new file mode 100644
index 00000000..7d287421
--- /dev/null
+++ b/static/scripts/grid/sort_button.js
@@ -0,0 +1,49 @@
+// Grid sort button
+class BrickGridSortButton {
+ constructor(button, grid) {
+ this.button = button;
+ this.grid = grid;
+ this.data = this.button.dataset;
+
+ // Setup
+ button.addEventListener("click", ((grid, button) => (e) => {
+ grid.sort(button);
+ })(grid, this));
+ }
+
+ // Active
+ active() {
+ this.button.classList.remove("btn-outline-primary");
+ this.button.classList.add("btn-primary");
+ }
+
+ // Inactive
+ inactive() {
+ delete this.button.dataset.sortOrder;
+ this.button.classList.remove("btn-primary");
+ this.button.classList.add("btn-outline-primary");
+ }
+
+ // Toggle sorting
+ toggle(order) {
+ // Cleanup
+ delete this.button.dataset.sortOrder;
+
+ let icon = this.button.querySelector("i.ri");
+ if (icon) {
+ this.button.removeChild(icon);
+ }
+
+ // Set order
+ if (order) {
+ this.active();
+
+ this.button.dataset.sortOrder = order;
+
+ icon = document.createElement("i");
+ icon.classList.add("ri", "ms-1", `ri-sort-${order}`);
+
+ this.button.append(icon);
+ }
+ }
+}
diff --git a/templates/base.html b/templates/base.html
index 846a861a..8aa083d2 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -82,6 +82,7 @@
<script src="{{ url_for('static', filename='scripts/changer.js') }}"></script>
<script src="{{ url_for('static', filename='scripts/grid/grid.js') }}"></script>
<script src="{{ url_for('static', filename='scripts/grid/sort.js') }}"></script>
+ <script src="{{ url_for('static', filename='scripts/grid/sort_button.js') }}"></script>
<script src="{{ url_for('static', filename='scripts/set.js') }}"></script>
<script src="{{ url_for('static', filename='scripts/socket/socket.js') }}"></script>
<script src="{{ url_for('static', filename='scripts/socket/instructions.js') }}"></script>
From 8e3816e2e2e5a6a63eb00d5997c21c00fc5bafb6 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Thu, 30 Jan 2025 22:40:53 +0100
Subject: [PATCH 068/154] Create dedicated object for Grid filter
---
static/scripts/grid/filter.js | 93 +++++++++++++++++++++++++++++++++++
static/scripts/grid/grid.js | 92 ++--------------------------------
templates/base.html | 1 +
3 files changed, 98 insertions(+), 88 deletions(-)
create mode 100644 static/scripts/grid/filter.js
diff --git a/static/scripts/grid/filter.js b/static/scripts/grid/filter.js
new file mode 100644
index 00000000..a5c46887
--- /dev/null
+++ b/static/scripts/grid/filter.js
@@ -0,0 +1,93 @@
+// Grid filter
+class BrickGridFilter {
+ constructor(grid) {
+ this.grid = grid;
+
+ // Grid sort elements (built based on the initial id)
+ this.html_search = document.getElementById(`${this.grid.id}-search`);
+ this.html_status = document.getElementById(`${this.grid.id}-status`);
+ this.html_theme = document.getElementById(`${this.grid.id}-theme`);
+
+ // Filter setup
+ if (this.html_search) {
+ this.html_search.addEventListener("keyup", ((grid) => () => {
+ grid.filter();
+ })(this));
+ }
+
+ if (this.html_status) {
+ this.html_status.addEventListener("change", ((grid) => () => {
+ grid.filter();
+ })(this));
+ }
+
+ if (this.html_theme) {
+ this.html_theme.addEventListener("change", ((grid) => () => {
+ grid.filter();
+ })(this));
+ }
+ }
+
+ // Filter
+ filter() {
+ var filters = {};
+
+ // Check if there is a search filter
+ if (this.html_search && this.html_search.value != "") {
+ filters["search"] = this.html_search.value.toLowerCase();
+ }
+
+ // Check if there is a set filter
+ if (this.html_status && this.html_status.value != "") {
+ if (this.html_status.value.startsWith("-")) {
+ filters["filter"] = this.html_status.value.substring(1);
+ filters["filter-target"] = "0";
+ } else {
+ filters["filter"] = this.html_status.value;
+ filters["filter-target"] = "1";
+ }
+ }
+
+ // Check if there is a theme filter
+ if (this.html_theme && this.html_theme.value != "") {
+ filters["theme"] = this.html_theme.value;
+ }
+
+ // Filter all cards
+ const cards = this.grid.html_grid.querySelectorAll(`${this.grid.target} > .card`);
+ cards.forEach(current => {
+ // Set filter
+ if ("filter" in filters) {
+ if (current.getAttribute("data-" + filters["filter"]) != filters["filter-target"]) {
+ current.parentElement.classList.add("d-none");
+ return;
+ }
+ }
+
+ // Theme filter
+ if ("theme" in filters) {
+ if (current.getAttribute("data-theme") != filters["theme"]) {
+ current.parentElement.classList.add("d-none");
+ return;
+ }
+ }
+
+ // Check all searchable fields for a match
+ if ("search" in filters) {
+ for (let attribute of ["data-name", "data-number", "data-parts", "data-theme", "data-year"]) {
+ if (current.getAttribute(attribute).includes(filters["search"])) {
+ current.parentElement.classList.remove("d-none");
+ return;
+ }
+ }
+
+ // If no match, we need to hide it
+ current.parentElement.classList.add("d-none");
+ return;
+ }
+
+ // If we passed all filters, we need to display it
+ current.parentElement.classList.remove("d-none");
+ });
+ }
+}
diff --git a/static/scripts/grid/grid.js b/static/scripts/grid/grid.js
index e11c2bd3..d0b9dab7 100644
--- a/static/scripts/grid/grid.js
+++ b/static/scripts/grid/grid.js
@@ -6,99 +6,15 @@ class BrickGrid {
// Grid elements (built based on the initial id)
this.html_grid = document.getElementById(this.id);
- this.html_search = document.getElementById(`${this.id}-search`);
- this.html_status = document.getElementById(`${this.id}-status`);
- this.html_theme = document.getElementById(`${this.id}-theme`);
- // Sort setup
- this.sort = new BrickGridSort(this);
-
- // Filter setup
- if (this.html_search) {
- this.html_search.addEventListener("keyup", ((grid) => () => {
- grid.filter();
- })(this));
- }
-
- if (this.html_status) {
- this.html_status.addEventListener("change", ((grid) => () => {
- grid.filter();
- })(this));
- }
-
- if (this.html_theme) {
- this.html_theme.addEventListener("change", ((grid) => () => {
- grid.filter();
- })(this));
- }
-
- }
-
- // Filter
- filter() {
- var filters = {};
-
- // Check if there is a search filter
- if (this.html_search && this.html_search.value != "") {
- filters["search"] = this.html_search.value.toLowerCase();
- }
-
- // Check if there is a set filter
- if (this.html_status && this.html_status.value != "") {
- if (this.html_status.value.startsWith("-")) {
- filters["filter"] = this.html_status.value.substring(1);
- filters["filter-target"] = "0";
- } else {
- filters["filter"] = this.html_status.value;
- filters["filter-target"] = "1";
- }
- }
-
- // Check if there is a theme filter
- if (this.html_theme && this.html_theme.value != "") {
- filters["theme"] = this.html_theme.value;
- }
-
- // Filter all cards
if (this.html_grid) {
- const cards = this.html_grid.querySelectorAll("div > div.card");
- cards.forEach(current => {
- // Set filter
- if ("filter" in filters) {
- if (current.getAttribute("data-" + filters["filter"]) != filters["filter-target"]) {
- current.parentElement.classList.add("d-none");
- return;
- }
- }
+ // Sort setup
+ this.sort = new BrickGridSort(this);
- // Theme filter
- if ("theme" in filters) {
- if (current.getAttribute("data-theme") != filters["theme"]) {
- current.parentElement.classList.add("d-none");
- return;
- }
- }
-
- // Check all searchable fields for a match
- if ("search" in filters) {
- for (let attribute of ["data-name", "data-number", "data-parts", "data-theme", "data-year"]) {
- if (current.getAttribute(attribute).includes(filters["search"])) {
- current.parentElement.classList.remove("d-none");
- return;
- }
- }
-
- // If no match, we need to hide it
- current.parentElement.classList.add("d-none");
- return;
- }
-
- // If we passed all filters, we need to display it
- current.parentElement.classList.remove("d-none");
- })
+ // Filter setup
+ this.filter = new BrickGridFilter(this);
}
}
-
}
// Helper to setup the grids
diff --git a/templates/base.html b/templates/base.html
index 8aa083d2..a82a4eaa 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -80,6 +80,7 @@
<script src="https://cdn.jsdelivr.net/npm/simple-datatables@9.2.1/dist/umd/simple-datatables.min.js"></script>
<!-- BrickTracker scripts -->
<script src="{{ url_for('static', filename='scripts/changer.js') }}"></script>
+ <script src="{{ url_for('static', filename='scripts/grid/filter.js') }}"></script>
<script src="{{ url_for('static', filename='scripts/grid/grid.js') }}"></script>
<script src="{{ url_for('static', filename='scripts/grid/sort.js') }}"></script>
<script src="{{ url_for('static', filename='scripts/grid/sort_button.js') }}"></script>
From ca3d4d09d5374ffa20c04956878a41f45d129f90 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Thu, 30 Jan 2025 23:24:22 +0100
Subject: [PATCH 069/154] Make grid filters controlled through data- fields
---
static/scripts/grid/filter.js | 88 ++++++++++++++++++++---------------
templates/sets.html | 10 ++--
2 files changed, 58 insertions(+), 40 deletions(-)
diff --git a/static/scripts/grid/filter.js b/static/scripts/grid/filter.js
index a5c46887..7406274a 100644
--- a/static/scripts/grid/filter.js
+++ b/static/scripts/grid/filter.js
@@ -5,20 +5,24 @@ class BrickGridFilter {
// Grid sort elements (built based on the initial id)
this.html_search = document.getElementById(`${this.grid.id}-search`);
- this.html_status = document.getElementById(`${this.grid.id}-status`);
- this.html_theme = document.getElementById(`${this.grid.id}-theme`);
+ this.html_filter = document.getElementById(`${this.grid.id}-filter`);
- // Filter setup
+ // Search setup
if (this.html_search) {
- this.html_search.addEventListener("keyup", ((grid) => () => {
- grid.filter();
+ this.html_search.addEventListener("keyup", ((gridfilter) => () => {
+ gridfilter.filter();
})(this));
}
- if (this.html_status) {
- this.html_status.addEventListener("change", ((grid) => () => {
- grid.filter();
- })(this));
+ // Filters setup
+ this.selects = [];
+ if (this.html_filter) {
+ this.html_filter.querySelectorAll("select[data-filter]").forEach(select => {
+ select.addEventListener("change", ((gridfilter) => () => {
+ gridfilter.filter();
+ })(this));
+ this.selects.push(select);
+ });
}
if (this.html_theme) {
@@ -30,52 +34,62 @@ class BrickGridFilter {
// Filter
filter() {
- var filters = {};
+ let options = {
+ search: undefined,
+ filters: [],
+ };
// Check if there is a search filter
if (this.html_search && this.html_search.value != "") {
- filters["search"] = this.html_search.value.toLowerCase();
+ options.search = this.html_search.value.toLowerCase();
}
- // Check if there is a set filter
- if (this.html_status && this.html_status.value != "") {
- if (this.html_status.value.startsWith("-")) {
- filters["filter"] = this.html_status.value.substring(1);
- filters["filter-target"] = "0";
- } else {
- filters["filter"] = this.html_status.value;
- filters["filter-target"] = "1";
+ // Build filters
+ for (const select of this.selects) {
+ if (select.value != "") {
+ // Multi-attribute filter
+ switch (select.dataset.filter) {
+ // List contains values
+ case "solo":
+ options.filters.push({
+ attribute: select.dataset.filterAttribute,
+ value: select.value,
+ })
+ break;
+
+ // List contains attribute name, looking for true/false
+ case "status":
+ if (select.value.startsWith("-")) {
+ options.filters.push({
+ attribute: select.value.substring(1),
+ value: "0"
+ })
+ } else {
+ options.filters.push({
+ attribute: select.value,
+ value: "1"
+ })
+ }
+ break;
+ }
}
}
- // Check if there is a theme filter
- if (this.html_theme && this.html_theme.value != "") {
- filters["theme"] = this.html_theme.value;
- }
-
// Filter all cards
const cards = this.grid.html_grid.querySelectorAll(`${this.grid.target} > .card`);
cards.forEach(current => {
- // Set filter
- if ("filter" in filters) {
- if (current.getAttribute("data-" + filters["filter"]) != filters["filter-target"]) {
- current.parentElement.classList.add("d-none");
- return;
- }
- }
-
- // Theme filter
- if ("theme" in filters) {
- if (current.getAttribute("data-theme") != filters["theme"]) {
+ // Process all filters
+ for (const filter of options.filters) {
+ if (current.getAttribute(`data-${filter.attribute}`) != filter.value) {
current.parentElement.classList.add("d-none");
return;
}
}
// Check all searchable fields for a match
- if ("search" in filters) {
+ if (options.search) {
for (let attribute of ["data-name", "data-number", "data-parts", "data-theme", "data-year"]) {
- if (current.getAttribute(attribute).includes(filters["search"])) {
+ if (current.getAttribute(attribute).includes(options.search)) {
current.parentElement.classList.remove("d-none");
return;
}
diff --git a/templates/sets.html b/templates/sets.html
index cfcbbfcc..6a07d731 100644
--- a/templates/sets.html
+++ b/templates/sets.html
@@ -35,12 +35,14 @@
</div>
</div>
</div>
- <div class="row row-cols-lg-auto g-1 justify-content-center align-items-center pb-2">
+ <div id="grid-filter" class="row row-cols-lg-auto g-1 justify-content-center align-items-center pb-2">
<div class="col-12 flex-grow-1">
<label class="visually-hidden" for="grid-status">Status</label>
<div class="input-group">
<span class="input-group-text"><i class="ri-checkbox-line"></i><span class="ms-1 d-none d-xl-inline"> Status</span></span>
- <select id="grid-status" class="form-select form-select-sm" autocomplete="off">
+ <select id="grid-status" class="form-select form-select-sm"
+ data-filter="status"
+ autocomplete="off">
<option value="" selected>All</option>
<option value="-has-missing">Set is complete</option>
<option value="has-missing">Set has missing pieces</option>
@@ -56,7 +58,9 @@
<label class="visually-hidden" for="grid-theme">Theme</label>
<div class="input-group">
<span class="input-group-text"><i class="ri-price-tag-3-line"></i><span class="ms-1 d-none d-xl-inline"> Theme</span></span>
- <select id="grid-theme" class="form-select form-select-sm" autocomplete="off">
+ <select id="grid-theme" class="form-select form-select-sm"
+ data-filter="solo" data-filter-attribute="theme"
+ autocomplete="off">
<option value="" selected>All</option>
{% for theme in collection.themes %}
<option value="{{ theme | lower }}">{{ theme }}</option>
From 069ba37e138dd7527c120925757c6657a47b3bee Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 7a15325a..86e82c81 100644
--- a/templates/admin/database.html
+++ b/templates/admin/database.html
@@ -19,15 +19,15 @@
</p>
{% if database_counters %}
<h5 class="border-bottom">Records</h5>
- <div class="d-flex justify-content-start">
- <ul class="list-group me-2">
+ <div class="row">
+ <ul class="list-group col-4">
{% for counter in database_counters %}
<li class="list-group-item d-flex justify-content-between align-items-start {% if counter.legacy %}list-group-item-dark{% endif %}">
<span><i class="ri-{{ counter.icon }}"></i> {{ counter.name }}</span> <span class="badge {% if counter.legacy %}text-bg-light border{% else %}text-bg-primary{% endif %} rounded-pill ms-2">{{ counter.count }}</span>
</li>
{% if not (loop.index % 5) %}
</ul>
- <ul class="list-group me-2">
+ <ul class="list-group col-4">
{% endif %}
{% endfor %}
</ul>
From 0e3637e5efd53d5f5d9ef944d6ce14a899cfee2e Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 379309f7..dfd82112 100644
--- a/templates/macro/form.html
+++ b/templates/macro/form.html
@@ -8,7 +8,7 @@
disabled
{% endif %}
autocomplete="off">
- <label class="form-check-label" for="{{ prefix }}-{{ item.fields.id }}">
+ <label class="form-check-label flex-grow-1 ms-1" for="{{ prefix }}-{{ item.fields.id }}">
{{ metadata.fields.name }} <i id="status-{{ prefix }}-{{ item.fields.id }}" class="mb-1"></i>
</label>
{% else %}
diff --git a/templates/set/card.html b/templates/set/card.html
index 19e493cb..c9afbdd4 100644
--- a/templates/set/card.html
+++ b/templates/set/card.html
@@ -29,7 +29,7 @@
{% if not tiny and brickset_statuses | length %}
<ul class="list-group list-group-flush card-check border-bottom-0">
{% for status in brickset_statuses %}
- <li class="list-group-item {% if not solo %}p-1{% endif %}">{{ form.checkbox(item, status, delete=delete) }}</li>
+ <li class="d-flex list-group-item {% if not solo %}p-1{% endif %} text-nowrap">{{ form.checkbox(item, status, delete=delete) }}</li>
{% endfor %}
</ul>
{% endif %}
From 6fdc933c3297ced49db95abbc9c88dc51b5104c3 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Thu, 30 Jan 2025 23:35:51 +0100
Subject: [PATCH 072/154] Cosmetics
---
static/scripts/grid/sort.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/static/scripts/grid/sort.js b/static/scripts/grid/sort.js
index 0fc4e5fe..93602fec 100644
--- a/static/scripts/grid/sort.js
+++ b/static/scripts/grid/sort.js
@@ -20,8 +20,8 @@ class BrickGridSort {
// Clear button
this.html_clear = this.html_sort.querySelector("button[data-sort-clear]")
if (this.html_clear) {
- this.html_clear.addEventListener("click", ((grid) => () => {
- grid.clear();
+ this.html_clear.addEventListener("click", ((gridsort) => () => {
+ gridsort.clear();
})(this))
}
From 1f73ae2323317d7edbfa93f7e3ac39be2579d5cf Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Fri, 31 Jan 2025 10:19:59 +0100
Subject: [PATCH 073/154] Configure the Grid search through data- attributes
---
static/scripts/grid/filter.js | 35 +++++++++++++++++++++++++++++++----
templates/sets.html | 2 +-
2 files changed, 32 insertions(+), 5 deletions(-)
diff --git a/static/scripts/grid/filter.js b/static/scripts/grid/filter.js
index 7406274a..d0d8903b 100644
--- a/static/scripts/grid/filter.js
+++ b/static/scripts/grid/filter.js
@@ -9,6 +9,19 @@ class BrickGridFilter {
// Search setup
if (this.html_search) {
+ // Exact attributes
+ if (this.html_search.dataset.searchExact) {
+ this.search_exact = new Set(this.html_search.dataset.searchExact.split(",").map(el => el.trim()));
+ } else {
+ this.search_exact = new Set();
+ }
+
+ // List attributes
+ this.search_list = [];
+ if (this.html_search.dataset.searchList) {
+ this.search_list = this.html_search.dataset.searchList.split(",").map(el => el.trim());
+ }
+
this.html_search.addEventListener("keyup", ((gridfilter) => () => {
gridfilter.filter();
})(this));
@@ -88,10 +101,24 @@ class BrickGridFilter {
// Check all searchable fields for a match
if (options.search) {
- for (let attribute of ["data-name", "data-number", "data-parts", "data-theme", "data-year"]) {
- if (current.getAttribute(attribute).includes(options.search)) {
- current.parentElement.classList.remove("d-none");
- return;
+ // Browse the whole dataset
+ for (const set in current.dataset) {
+ // Exact attribute
+ if (this.search_exact.has(set)) {
+ if (current.dataset[set].includes(options.search)) {
+ current.parentElement.classList.remove("d-none");
+ return;
+ }
+ } else {
+ // List search
+ for (const list of this.search_list) {
+ if (set.startsWith(this.search_list)) {
+ if (current.dataset[set].includes(options.search)) {
+ current.parentElement.classList.remove("d-none");
+ return;
+ }
+ }
+ }
}
}
diff --git a/templates/sets.html b/templates/sets.html
index 6a07d731..e8d70a24 100644
--- a/templates/sets.html
+++ b/templates/sets.html
@@ -10,7 +10,7 @@
<label class="visually-hidden" for="grid-search">Search</label>
<div class="input-group">
<span class="input-group-text"><i class="ri-search-line"></i><span class="ms-1 d-none d-xl-inline"> Search</span></span>
- <input id="grid-search" class="form-control form-control-sm" type="text" placeholder="Set name, set number, set theme or number of parts..." value="">
+ <input id="grid-search" data-search-exact="name,number,parts,theme,year" class="form-control form-control-sm" type="text" placeholder="Set name, set number, set theme or number of parts..." value="">
</div>
</div>
<div class="col-12">
From 2260774a582d5da3f580852319b6047d5122e701 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 d0d8903b..e2eff7bd 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 e8d70a24..27aff004 100644
--- a/templates/sets.html
+++ b/templates/sets.html
@@ -41,7 +41,7 @@
<div class="input-group">
<span class="input-group-text"><i class="ri-checkbox-line"></i><span class="ms-1 d-none d-xl-inline"> Status</span></span>
<select id="grid-status" class="form-select form-select-sm"
- data-filter="status"
+ data-filter="metadata"
autocomplete="off">
<option value="" selected>All</option>
<option value="-has-missing">Set is complete</option>
@@ -59,7 +59,7 @@
<div class="input-group">
<span class="input-group-text"><i class="ri-price-tag-3-line"></i><span class="ms-1 d-none d-xl-inline"> Theme</span></span>
<select id="grid-theme" class="form-select form-select-sm"
- data-filter="solo" data-filter-attribute="theme"
+ data-filter="value" data-filter-attribute="theme"
autocomplete="off">
<option value="" selected>All</option>
{% for theme in collection.themes %}
From e9f97a6f5e328d4313195b73f8085debf73e0045 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 73a5eb89..3b037824 100644
--- a/templates/wish/table.html
+++ b/templates/wish/table.html
@@ -19,23 +19,24 @@
</thead>
<tbody>
{% for item in table_collection %}
- {% set retirement_date = retired.get(item.fields.set) %}
- <tr>
- {{ table.image(item.url_for_image(), caption=item.fields.name, alt=item.fields.set) }}
- <td>{{ item.fields.set }} {{ table.rebrickable(item) }}</td>
- <td>{{ item.fields.name }}</td>
- <td>{{ item.theme.name }}</td>
- <td>{{ item.fields.year }}</td>
- <td>{{ item.fields.number_of_parts }}</td>
- <td>{% if retirement_date %}{{ retirement_date }}{% else %}<span class="badge rounded-pill text-bg-light border">Not found</span>{% endif %}</td>
- {% if g.login.is_authenticated() %}
- <td>
- <form action="{{ item.url_for_delete() }}" method="post">
- <button type="submit" class="btn btn-sm btn-danger"><i class="ri-delete-bin-2-line"></i> Delete</button>
- </form>
- </td>
- {% endif %}
- </tr>
+ {% with retirement_date = retired.get(item.fields.set) %}
+ <tr>
+ {{ table.image(item.url_for_image(), caption=item.fields.name, alt=item.fields.set) }}
+ <td>{{ item.fields.set }} {{ table.rebrickable(item) }}</td>
+ <td>{{ item.fields.name }}</td>
+ <td>{{ item.theme.name }}</td>
+ <td>{{ item.fields.year }}</td>
+ <td>{{ item.fields.number_of_parts }}</td>
+ <td>{% if retirement_date %}{{ retirement_date }}{% else %}<span class="badge rounded-pill text-bg-light border">Not found</span>{% endif %}</td>
+ {% if g.login.is_authenticated() %}
+ <td>
+ <form action="{{ item.url_for_delete() }}" method="post">
+ <button type="submit" class="btn btn-sm btn-danger"><i class="ri-delete-bin-2-line"></i> Delete</button>
+ </form>
+ </td>
+ {% endif %}
+ </tr>
+ {% endwith %}
{% endfor %}
</tbody>
</table>
From 23515526c829ebdfd68d4362934ad295c9485bc2 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 27aff004..2b3271cf 100644
--- a/templates/sets.html
+++ b/templates/sets.html
@@ -16,21 +16,21 @@
<div class="col-12">
<div id="grid-sort" class="input-group">
<span class="input-group-text"><i class="ri-sort-asc"></i><span class="ms-1 d-none d-xxl-inline"> Sort</span></span>
- <button id="sort-number" type="button" class="btn btn-sm btn-outline-primary"
+ <button id="sort-number" type="button" class="btn btn-outline-primary"
data-sort-attribute="number" data-sort-natural="true"><i class="ri-hashtag"></i><span class="d-none d-xxl-inline"> Set</span></button>
- <button id="sort-name" type="button" class="btn btn-sm btn-outline-primary"
+ <button id="sort-name" type="button" class="btn btn-outline-primary"
data-sort-attribute="name"><i class="ri-pencil-line"></i><span class="d-none d-xxl-inline"> Name</span></button>
- <button id="sort-theme" type="button" class="btn btn-sm btn-outline-primary"
+ <button id="sort-theme" type="button" class="btn btn-outline-primary"
data-sort-attribute="theme"><i class="ri-price-tag-3-line"></i><span class="d-none d-xxl-inline"> Theme</span></button>
- <button id="sort-year" type="button" class="btn btn-sm btn-outline-primary"
+ <button id="sort-year" type="button" class="btn btn-outline-primary"
data-sort-attribute="year"><i class="ri-calendar-line"></i><span class="d-none d-xxl-inline"> Year</span></button>
- <button id="sort-minifigure" type="button" class="btn btn-sm btn-outline-primary"
+ <button id="sort-minifigure" type="button" class="btn btn-outline-primary"
data-sort-attribute="minifigures" data-sort-desc="true"><i class="ri-group-line"></i><span class="d-none d-xxl-inline"> Figures</span></button>
- <button id="sort-parts" type="button" class="btn btn-sm btn-outline-primary"
+ <button id="sort-parts" type="button" class="btn btn-outline-primary"
data-sort-attribute="parts" data-sort-desc="true"><i class="ri-shapes-line"></i><span class="d-none d-xxl-inline"> Parts</span></button>
- <button id="sort-missing" type="button" class="btn btn-sm btn-outline-primary"
+ <button id="sort-missing" type="button" class="btn btn-outline-primary"
data-sort-attribute="missing" data-sort-desc="true"><i class="ri-error-warning-line"></i><span class="d-none d-xxl-inline"> Missing</span></button>
- <button id="sort-clear" type="button" class="btn btn-sm btn-outline-dark"
+ <button id="sort-clear" type="button" class="btn btn-outline-dark"
data-sort-clear="true"><i class="ri-filter-off-line"></i><span class="d-none d-xxl-inline"> Clear</span></button>
</div>
</div>
@@ -40,7 +40,7 @@
<label class="visually-hidden" for="grid-status">Status</label>
<div class="input-group">
<span class="input-group-text"><i class="ri-checkbox-line"></i><span class="ms-1 d-none d-xl-inline"> Status</span></span>
- <select id="grid-status" class="form-select form-select-sm"
+ <select id="grid-status" class="form-select"
data-filter="metadata"
autocomplete="off">
<option value="" selected>All</option>
@@ -58,7 +58,7 @@
<label class="visually-hidden" for="grid-theme">Theme</label>
<div class="input-group">
<span class="input-group-text"><i class="ri-price-tag-3-line"></i><span class="ms-1 d-none d-xl-inline"> Theme</span></span>
- <select id="grid-theme" class="form-select form-select-sm"
+ <select id="grid-theme" class="form-select"
data-filter="value" data-filter-attribute="theme"
autocomplete="off">
<option value="" selected>All</option>
From 6ec4f160f76d846fc148bea950e25c6129d8e746 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 2b3271cf..c3a93d63 100644
--- a/templates/sets.html
+++ b/templates/sets.html
@@ -31,11 +31,18 @@
<button id="sort-missing" type="button" class="btn btn-outline-primary"
data-sort-attribute="missing" data-sort-desc="true"><i class="ri-error-warning-line"></i><span class="d-none d-xxl-inline"> Missing</span></button>
<button id="sort-clear" type="button" class="btn btn-outline-dark"
- data-sort-clear="true"><i class="ri-filter-off-line"></i><span class="d-none d-xxl-inline"> Clear</span></button>
+ data-sort-clear="true"><i class="ri-close-circle-line"></i><span class="d-none d-xxl-inline"> Clear</span></button>
+ </div>
+ </div>
+ <div class="col-12">
+ <div class="input-group">
+ <button class="btn btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#grid-filter" aria-expanded="false" aria-controls="grid-filter">
+ <i class="ri-filter-line"></i> Filters
+ </button>
</div>
</div>
</div>
- <div id="grid-filter" class="row row-cols-lg-auto g-1 justify-content-center align-items-center pb-2">
+ <div id="grid-filter" class="collapse row row-cols-lg-auto g-1 justify-content-center align-items-center pb-2">
<div class="col-12 flex-grow-1">
<label class="visually-hidden" for="grid-status">Status</label>
<div class="input-group">
From 6011173c1f394e7dc9a1e11d8ca9ea1b8619f0dc Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 46b21317..d6141e63 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 83cd99a4..cbb64a81 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 c3a93d63..97a7922c 100644
--- a/templates/sets.html
+++ b/templates/sets.html
@@ -36,13 +36,13 @@
</div>
<div class="col-12">
<div class="input-group">
- <button class="btn btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#grid-filter" aria-expanded="false" aria-controls="grid-filter">
+ <button class="btn btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#grid-filter" aria-expanded="{% if config['SHOW_GRID_FILTERS'] %}true{% else %}false{% endif %}" aria-controls="grid-filter">
<i class="ri-filter-line"></i> Filters
</button>
</div>
</div>
</div>
- <div id="grid-filter" class="collapse row row-cols-lg-auto g-1 justify-content-center align-items-center pb-2">
+ <div id="grid-filter" class="collapse {% if config['SHOW_GRID_FILTERS'] %}show{% endif %} row row-cols-lg-auto g-1 justify-content-center align-items-center pb-2">
<div class="col-12 flex-grow-1">
<label class="visually-hidden" for="grid-status">Status</label>
<div class="input-group">
From ece15e97fb4323f7e6b2e65615d788f78797bfdc Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 43dfcdc5..547ffbc6 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') }}
</div>
<div class="card-footer"></div>
{% endif %}
From 6262ac7889d7343b2d59f5a805c3c3860d1f4337 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 eea6d083..8fdd3cb0 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 70b10f1e..722a357b 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 %}
<a href="{{ url }}" {% if blank %}target="_blank"{% endif %}
{% else %}
<span
{% endif %}
- class="badge text-bg-{{ color }} fw-normal mb-1 {% if solo %}fs-6{% endif %}" {% if alt %}alt="{{ alt }}"{% endif %}
+ class="badge text-bg-{{ color }} fw-normal {% if not header %}mb-1{% endif %} {% if solo and not header %}fs-6{% endif %}" {% if alt %}alt="{{ alt }}"{% endif %}
{% if tooltip %} data-bs-toggle="tooltip" title="{{ tooltip }}"{% endif %}
>
{% if icon %}<i class="ri-{{ icon }}"></i>{% endif %}
+ {% if rgb %}<span class="color-rgb {% if rgb == 'any' %}color-any{% endif %} align-bottom border border-black" {% if rgb != 'any' %}style="background-color: #{{ rgb }};"{% endif %}></span>{% endif %}
{% if collapsible and not last %}<span {% if not solo %}class="d-none d-md-inline"{% endif %}> {{ collapsible }} </span>{% 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 ae8bce01..be6ad741 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') %}
<div class="card-header">
{% if not solo %}
<a class="text-decoration-none text-reset" href="{{ item.url() }}" data-bs-toggle="tooltip" title="{{ name }}">
{% endif %}
<h5 class="mb-0 {% if not solo %}fs-6 text-nowrap overflow-x-hidden text-truncate{% endif %}">
- {% if identifier %}<span class="badge text-bg-secondary fw-normal"><i class="ri-{{ icon }}"></i>{{ identifier }}</span>{% endif %}
+ {{ badge.identifier(identifier, icon=icon, solo=solo, header=true) }}
{% if solo %}
- {% if item.fields.color_name %}
- <span class="badge bg-white text-black fw-normal border"><i class="ri-palette-line"></i>
- {% if item.fields.color_rgb %}
- <span class="color-rgb {% if item.fields.color == 9999 %}color-any{% endif %} align-bottom border border-black" {% if item.fields.color != 9999 %}style="background-color: #{{ item.fields.color_rgb }};"{% endif %}></span>
- {% endif %}
- {{ item.fields.color_name }}
- </span>
- {% endif %}
- {% if item.fields.color_transparent %}<span class="badge text-bg-light fw-normal border"><i class="ri-blur-off-line"></i> Transparent</span>{% endif %}
- {% if item.fields.print %}<span class="badge text-bg-light fw-normal border"><a class="text-reset" href="{{ item.url_for_print() }}"><i class="ri-paint-brush-line"></i> Print</a></span>{% endif %}
+ {{ badge.color(item, header=true) }}
+ {{ badge.print(item, header=true) }}
{% endif %}
{{ name }}
</h5>
From adb2170d47f7b833002195d652ec6af7a24b45c7 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 722a357b..3d8a5e26 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 <versatile.mailbox@gmail.com>
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 547ffbc6..4cb40314 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) }}
<div class="card-body border-bottom-0 {% if not solo %}p-1{% endif %}">
+ {% 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 <versatile.mailbox@gmail.com>
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 ca23068b..e3ce2bda 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 <versatile.mailbox@gmail.com>
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 4f6bf978..07d7cc7e 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 <versatile.mailbox@gmail.com>
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 218a0bfa..44f3ddca 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 5316ea17..12387398 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 %}
<div class="container">
- {% if not config['HIDE_ADD_BULK_SET'] %}
+ {% if not bulk and not config['HIDE_ADD_BULK_SET'] %}
<div class="alert alert-primary" role="alert">
<h4 class="alert-heading">Too many to add?</h4>
<p class="mb-0">You can import multiple sets at once with <a href="{{ url_for('add.bulk') }}" class="btn btn-primary"><i class="ri-function-add-line"></i> Bulk add</a>.</p>
@@ -14,17 +14,21 @@
<div class="col-12">
<div class="card mb-3">
<div class="card-header">
- <h5 class="mb-0"><i class="ri-add-circle-line"></i> Add a set</h5>
+ <h5 class="mb-0"><i class="ri-add-circle-line"></i> {% if not bulk %}Add a set{% else %}Bulk add sets{% endif %}</h5>
</div>
<div class="card-body">
<div id="add-fail" class="alert alert-danger d-none" role="alert"></div>
- <div id="add-complete" class="alert alert-success d-none" role="alert"></div>
+ {% if not bulk %}
+ <div id="add-complete" class="alert alert-success d-none" role="alert"></div>
+ {% else %}
+ <div id="add-complete"></div>
+ {% endif %}
<div class="mb-3">
- <label for="add-set" class="form-label">Set number (only one)</label>
- <input type="text" class="form-control" id="add-set" placeholder="107-1 or 1642-1 or ...">
+ <label for="add-set" class="form-label">{% if not bulk %}Set number (only one){% else %}List of sets (separated by a comma){% endif %}</label>
+ <input type="text" class="form-control" id="add-set" placeholder="{% if not bulk %}107-1 or 1642-1 or ...{% else %}107-1, 1642-1, ...{% endif %}">
</div>
<div class="form-check">
- <input type="checkbox" class="form-check-input" id="add-no-confirm">
+ <input type="checkbox" class="form-check-input" id="add-no-confirm" {% if bulk %}checked disabled{% endif %}>
<label class="form-check-label" for="add-no-confirm">
Add without confirmation
</label>
@@ -54,21 +58,24 @@
<div id="add-card-image-container" class="card-img">
<img id="add-card-image" loading="lazy">
</div>
- <div id="add-card-footer" class="card-footer text-end d-none">
- <button id="add-card-dismiss" type="button" class="btn btn-danger"><i class="ri-close-line"></i> Dismiss</button>
- <button id="add-card-confirm" type="button" class="btn btn-primary"><i class="ri-check-double-line"></i> Confirm add</button>
- </div>
+ {% if not bulk %}
+ <div id="add-card-footer" class="card-footer text-end d-none">
+ <button id="add-card-dismiss" type="button" class="btn btn-danger"><i class="ri-close-line"></i> Dismiss</button>
+ <button id="add-card-confirm" type="button" class="btn btn-primary"><i class="ri-check-double-line"></i> Confirm add</button>
+ </div>
+ {% endif %}
</div>
</div>
</div>
<div class="card-footer text-end">
- <span id="add-status-icon" class="me-1"></span><span id="add-status" class="me-1"></span><button id="add" type="button" class="btn btn-primary"><i class="ri-add-circle-line"></i> Add</button>
+ <span id="add-status-icon" class="me-1"></span><span id="add-status" class="me-1"></span>
+ <button id="add" type="button" class="btn btn-primary">{% if not bulk %}<i class="ri-add-circle-line"></i> Add{% else %}<i class="ri-function-add-line"></i> Bulk add{% endif %}</button>
</div>
</div>
</div>
</div>
</div>
-{% 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 00d47797..00000000
--- a/templates/bulk.html
+++ /dev/null
@@ -1,64 +0,0 @@
-{% extends 'base.html' %}
-
-{% block title %} - Bulk add sets{% endblock %}
-
-{% block main %}
-<div class="container">
- <div class="row">
- <div class="col-12">
- <div class="card mb-3">
- <div class="card-header">
- <h5 class="mb-0"><i class="ri-add-circle-line"></i> Bulk add sets</h5>
- </div>
- <div class="card-body">
- <div id="add-fail" class="alert alert-danger d-none" role="alert"></div>
- <div id="add-complete"></div>
- <div class="mb-3">
- <label for="add-set" class="form-label">List of sets (separated by a comma)</label>
- <input type="text" class="form-control" id="add-set" placeholder="107-1, 1642-1, ...">
- </div>
- <div class="form-check">
- <input type="checkbox" class="form-check-input" id="add-no-confirm" checked disabled>
- <label class="form-check-label" for="add-no-confirm">
- Add without confirmation
- </label>
- </div>
- <hr>
- <div class="mb-3">
- <p>
- Progress <span id="add-count"></span>
- <span id="add-spinner" class="d-none">
- <span class="spinner-border spinner-border-sm" aria-hidden="true"></span>
- <span class="visually-hidden" role="status">Loading...</span>
- </span>
- </p>
- <div id="add-progress" class="progress" role="progressbar" aria-label="Add a set progress" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
- <div id="add-progress-bar" class="progress-bar" style="width: 0%"></div>
- </div>
- <p id="add-progress-message" class="text-center d-none"></p>
- </div>
- <div id="add-card" class="d-flex d-none justify-content-center">
- <div class="card mb-3 col-6">
- <div class="card-header">
- <h5 class="mb-0">
- <span class="badge text-bg-secondary fw-normal"><i class="ri-hashtag"></i> <span id="add-card-number"></span></span>
- <span id="add-card-name"></span>
- </h5>
- </div>
- <div id="add-card-image-container" class="card-img">
- <img id="add-card-image" loading="lazy">
- </div>
- </div>
- </div>
- </div>
- <div class="card-footer text-end">
- <span id="add-status-icon" class="me-1"></span><span id="add-status" class="me-1"></span><button id="add" type="button" class="btn btn-primary"><i class="ri-function-add-line"></i> Bulk add</button>
- </div>
- </div>
- </div>
- </div>
-</div>
-{% with id='add', bulk=true %}
- {% include 'set/socket.html' %}
-{% endwith %}
-{% endblock %}
From b8d4f23a84955cc182cc249044ba9823018b7c24 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 f6054bcb..15cb9a3d 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 c7a9678b..4b7c54ea 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 80bb3df0..bb2e337b 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 22752032..8927e717 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 259cffad..73e9e241 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 32bf8daa..f4bf1a26 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 251cfdcd..6a94185b 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 00000000..3c07647b
--- /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 00000000..13097490
--- /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 00000000..33f8a6f0
--- /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 8b1f4c88..940dab9f 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 70730ff6..725b56dc 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 49b0e884..2db140de 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 00000000..095ae3d6
--- /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 00000000..e9df18d8
--- /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 00000000..cc54a2a5
--- /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 00000000..e970cf9c
--- /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 00000000..82245656
--- /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 00000000..5f047a33
--- /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 00000000..24692075
--- /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 28c03a32..30c83d6d 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 11dd9c9a..996b4f61 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 44f3ddca..20607dcf 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 c18a74b2..36037d36 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 00000000..bfa799e3
--- /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('<id>/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('<id>/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('<id>/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 f8fe7b71..3d8a55ec 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 9f1990cc..fd922ec4 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('/<id>/owner/<metadata_id>', 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('/<id>/status/<metadata_id>', 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 07d7cc7e..6459aa66 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 12387398..59c50299 100644
--- a/templates/add.html
+++ b/templates/add.html
@@ -33,6 +33,19 @@
Add without confirmation
</label>
</div>
+ {% if brickset_owners | length %}
+ <h5 class="border-bottom mt-2">Owners</h5>
+ <div id="add-owners">
+ {% for owner in brickset_owners %}
+ {% with id=owner.as_dataset() %}
+ <div class="form-check">
+ <input class="form-check-input" type="checkbox" value="{{ owner.fields.id }}" id="{{ id }}" autocomplete="off">
+ <label class="form-check-label" for="{{ id }}">{{ owner.fields.name }}</label>
+ </div>
+ {% endwith %}
+ {% endfor %}
+ </div>
+ {% endif %}
<hr>
<div class="mb-3">
<p>
diff --git a/templates/admin.html b/templates/admin.html
index b2a54f3a..962730bc 100644
--- a/templates/admin.html
+++ b/templates/admin.html
@@ -12,10 +12,12 @@
<h5 class="mb-0"><i class="ri-settings-4-line"></i> Administration</h5>
</div>
<div class="accordion accordion-flush" id="admin">
- {% 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 00000000..7447a6da
--- /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 %}<div class="alert alert-danger m-2" role="alert"><strong>Error:</strong> {{ owner_error }}.</div>{% endif %}
+<ul class="list-group list-group-flush">
+ {% if metadata_owners | length %}
+ {% for owner in metadata_owners %}
+ <li class="list-group-item">
+ <form action="{{ url_for('admin_owner.rename', id=owner.fields.id) }}" method="post" class="row row-cols-lg-auto g-3 align-items-center">
+ <div class="col-12 flex-grow-1">
+ <label class="visually-hidden" for="name-{{ owner.fields.id }}">Name</label>
+ <div class="input-group">
+ <div class="input-group-text">Name</div>
+ <input type="text" class="form-control" id="name-{{ owner.fields.id }}" name="name" value="{{ owner.fields.name }}">
+ <button type="submit" class="btn btn-primary"><i class="ri-edit-line"></i> Rename</button>
+ </div>
+ </div>
+ <div class="col-12">
+ <a href="{{ url_for('admin_owner.delete', id=owner.fields.id) }}" class="btn btn-danger" role="button"><i class="ri-delete-bin-2-line"></i> Delete</a>
+ </div>
+ </form>
+ </li>
+ {% endfor %}
+ {% else %}
+ <li class="list-group-item"><i class="ri-error-warning-line"></i> No owner found.</li>
+ {% endif %}
+ <li class="list-group-item">
+ <form action="{{ url_for('admin_owner.add') }}" method="post" class="row row-cols-lg-auto g-3 align-items-center">
+ <div class="col-12 flex-grow-1">
+ <label class="visually-hidden" for="name">Name</label>
+ <div class="input-group">
+ <div class="input-group-text">Name</div>
+ <input type="text" class="form-control" id="name" name="name" value="">
+ </div>
+ </div>
+ <div class="col-12">
+ <button type="submit" class="btn btn-primary"><i class="ri-add-circle-line"></i> Add</button>
+ </div>
+ </form>
+ </li>
+</ul>
+{{ accordion.footer() }}
diff --git a/templates/admin/owner/delete.html b/templates/admin/owner/delete.html
new file mode 100644
index 00000000..56821f35
--- /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') }}
+<form action="{{ url_for('admin_owner.do_delete', id=owner.fields.id) }}" method="post">
+ {% if owner_error %}<div class="alert alert-danger text-start" role="alert"><strong>Error:</strong> {{ owner_error }}.</div>{% endif %}
+ <div class="alert alert-danger text-center" role="alert">You are about to <strong>delete a set owner</strong>. This action is irreversible.</div>
+ <div class="row row-cols-lg-auto g-3 align-items-center">
+ <div class="col-12 flex-grow-1">
+ <div class="input-group">
+ <div class="input-group-text">Name</div>
+ <input type="text" class="form-control" value="{{ owner.fields.name }}" disabled>
+ </div>
+ </div>
+ </div>
+ <hr class="border-bottom">
+ <a class="btn btn-danger" href="{{ url_for('admin.admin', open_owner=true) }}" role="button"><i class="ri-arrow-left-long-line"></i> Back to the admin</a>
+ <button type="submit" class="btn btn-danger"><i class="ri-delete-bin-2-line"></i> Delete <strong>the set owner</strong></button>
+</form>
+{{ accordion.footer() }}
diff --git a/templates/macro/badge.html b/templates/macro/badge.html
index 3d8a5e26..2be95ca8 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 c9afbdd4..9f2f83a7 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 38074808..957db816 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') }}
+ <ul class="list-group list-group-flush">
+ {% if brickset_owners | length %}
+ {% for owner in brickset_owners %}
+ <li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(item, owner, delete=delete) }}</li>
+ {% endfor %}
+ {% else %}
+ <li class="list-group-item list-group-item-action"><i class="ri-error-warning-line"></i> No owner found.</li>
+ {% endif %}
+ </ul>
+ <div class="list-group list-group-flush border-top">
+ <a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_owner=true) }}"><i class="ri-settings-4-line"></i> Manage the set owners</a>
+ </div>
+{{ accordion.footer() }}
{{ accordion.header('Management', 'management', 'set-details', expanded=true, icon='settings-4-line') }}
- <h5 class="border-bottom">Data</h5>
- <a href="{{ item.url_for_refresh() }}" class="btn btn-primary" role="button"><i class="ri-refresh-line"></i> Refresh the set data</a>
+ <h5 class="border-bottom">Data</h5>
+ <a href="{{ item.url_for_refresh() }}" class="btn btn-primary" role="button"><i class="ri-refresh-line"></i> Refresh the set data</a>
{{ accordion.footer() }}
{% endif %}
diff --git a/templates/sets.html b/templates/sets.html
index 97a7922c..b06c94dd 100644
--- a/templates/sets.html
+++ b/templates/sets.html
@@ -10,7 +10,7 @@
<label class="visually-hidden" for="grid-search">Search</label>
<div class="input-group">
<span class="input-group-text"><i class="ri-search-line"></i><span class="ms-1 d-none d-xl-inline"> Search</span></span>
- <input id="grid-search" data-search-exact="name,number,parts,theme,year" class="form-control form-control-sm" type="text" placeholder="Set name, set number, set theme or number of parts..." value="">
+ <input id="grid-search" data-search-exact="name,number,parts,theme,year" data-search-list="owner" class="form-control form-control-sm" type="text" placeholder="Set, name, number of parts, theme, year, owner" value="">
</div>
</div>
<div class="col-12">
@@ -75,6 +75,20 @@
</select>
</div>
</div>
+ <div class="col-12 flex-grow-1">
+ <label class="visually-hidden" for="grid-owner">Owner</label>
+ <div class="input-group">
+ <span class="input-group-text"><i class="ri-user-line"></i><span class="ms-1 d-none d-xl-inline"> Owner</span></span>
+ <select id="grid-owner" class="form-select"
+ data-filter="metadata"
+ autocomplete="off">
+ <option value="" selected>All</option>
+ {% for owner in brickset_owners %}
+ <option value="{{ owner.as_dataset() }}">{{ owner.fields.name }}</option>
+ {% endfor %}
+ </select>
+ </div>
+ </div>
</div>
<div class="row" data-grid="true" id="grid">
{% for item in collection %}
From 030345fe6b669225b9056d08f910d8e878e3ac22 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 6a94185b..e071dc63 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 <versatile.mailbox@gmail.com>
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 6459aa66..01c51062 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 <versatile.mailbox@gmail.com>
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 e2eff7bd..f5b075f9 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 <versatile.mailbox@gmail.com>
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 01c51062..08e70dd8 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 <versatile.mailbox@gmail.com>
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 15cb9a3d..240bc637 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 bb2e337b..b0d42c3b 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 8927e717..3de9bf96 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 73e9e241..16fca2f5 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 f4bf1a26..eb2bafb5 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 e071dc63..54a3cb8c 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 00000000..6d81c18c
--- /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 00000000..92806f22
--- /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 00000000..37c655ed
--- /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 abc85229..1bab7d65 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 940dab9f..ffefe956 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 725b56dc..271f8909 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 2db140de..4eca8456 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 00000000..3ec57259
--- /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 00000000..a80bb8f2
--- /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 00000000..7a62866b
--- /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 00000000..fe44b5fc
--- /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 00000000..2d52076d
--- /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 00000000..629a9e8a
--- /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 00000000..18de40a7
--- /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 30c83d6d..74c18cc8 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 996b4f61..767fad59 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 20607dcf..90729733 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 36037d36..415cf487 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 00000000..d31bc49c
--- /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('<id>/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('<id>/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('<id>/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 3d8a55ec..1cbcd564 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 fd922ec4..344b0e60 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('/<id>/tag/<metadata_id>', 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('/<id>/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 08e70dd8..a7e660b4 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 59c50299..3a0b7840 100644
--- a/templates/add.html
+++ b/templates/add.html
@@ -46,6 +46,19 @@
{% endfor %}
</div>
{% endif %}
+ {% if brickset_tags | length %}
+ <h5 class="border-bottom mt-2">Tags</h5>
+ <div id="add-tags">
+ {% for tag in brickset_tags %}
+ {% with id=tag.as_dataset() %}
+ <div class="form-check">
+ <input class="form-check-input" type="checkbox" value="{{ tag.fields.id }}" id="{{ id }}" autocomplete="off">
+ <label class="form-check-label" for="{{ id }}">{{ tag.fields.name }}</label>
+ </div>
+ {% endwith %}
+ {% endfor %}
+ </div>
+ {% endif %}
<hr>
<div class="mb-3">
<p>
diff --git a/templates/admin.html b/templates/admin.html
index 962730bc..064526d8 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 00000000..7c67a56a
--- /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 %}<div class="alert alert-danger m-2" role="alert"><strong>Error:</strong> {{ tag_error }}.</div>{% endif %}
+<ul class="list-group list-group-flush">
+ {% if metadata_tags | length %}
+ {% for tag in metadata_tags %}
+ <li class="list-group-item">
+ <form action="{{ url_for('admin_tag.rename', id=tag.fields.id) }}" method="post" class="row row-cols-lg-auto g-3 align-items-center">
+ <div class="col-12 flex-grow-1">
+ <label class="visually-hidden" for="name-{{ tag.fields.id }}">Name</label>
+ <div class="input-group">
+ <div class="input-group-text">Name</div>
+ <input type="text" class="form-control" id="name-{{ tag.fields.id }}" name="name" value="{{ tag.fields.name }}">
+ <button type="submit" class="btn btn-primary"><i class="ri-edit-line"></i> Rename</button>
+ </div>
+ </div>
+ <div class="col-12">
+ <a href="{{ url_for('admin_tag.delete', id=tag.fields.id) }}" class="btn btn-danger" role="button"><i class="ri-delete-bin-2-line"></i> Delete</a>
+ </div>
+ </form>
+ </li>
+ {% endfor %}
+ {% else %}
+ <li class="list-group-item"><i class="ri-error-warning-line"></i> No tag found.</li>
+ {% endif %}
+ <li class="list-group-item">
+ <form action="{{ url_for('admin_tag.add') }}" method="post" class="row row-cols-lg-auto g-3 align-items-center">
+ <div class="col-12 flex-grow-1">
+ <label class="visually-hidden" for="name">Name</label>
+ <div class="input-group">
+ <div class="input-group-text">Name</div>
+ <input type="text" class="form-control" id="name" name="name" value="">
+ </div>
+ </div>
+ <div class="col-12">
+ <button type="submit" class="btn btn-primary"><i class="ri-add-circle-line"></i> Add</button>
+ </div>
+ </form>
+ </li>
+</ul>
+{{ accordion.footer() }}
diff --git a/templates/admin/tag/delete.html b/templates/admin/tag/delete.html
new file mode 100644
index 00000000..69dd3342
--- /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') }}
+<form action="{{ url_for('admin_tag.do_delete', id=tag.fields.id) }}" method="post">
+ {% if tag_error %}<div class="alert alert-danger text-start" role="alert"><strong>Error:</strong> {{ tag_error }}.</div>{% endif %}
+ <div class="alert alert-danger text-center" role="alert">You are about to <strong>delete a set tag</strong>. This action is irreversible.</div>
+ <div class="row row-cols-lg-auto g-3 align-items-center">
+ <div class="col-12 flex-grow-1">
+ <div class="input-group">
+ <div class="input-group-text">Name</div>
+ <input type="text" class="form-control" value="{{ tag.fields.name }}" disabled>
+ </div>
+ </div>
+ </div>
+ <hr class="border-bottom">
+ <a class="btn btn-danger" href="{{ url_for('admin.admin', open_tag=true) }}" role="button"><i class="ri-arrow-left-long-line"></i> Back to the admin</a>
+ <button type="submit" class="btn btn-danger"><i class="ri-delete-bin-2-line"></i> Delete <strong>the set tag</strong></button>
+</form>
+{{ accordion.footer() }}
diff --git a/templates/macro/badge.html b/templates/macro/badge.html
index 2be95ca8..ca577693 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 9f2f83a7..bc302e36 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) }}
<div class="card-body border-bottom-0 {% if not solo %}p-1{% endif %}">
{{ 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 957db816..b1b20194 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') }}
- <ul class="list-group list-group-flush">
- {% if brickset_owners | length %}
- {% for owner in brickset_owners %}
- <li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(item, owner, delete=delete) }}</li>
- {% endfor %}
- {% else %}
- <li class="list-group-item list-group-item-action"><i class="ri-error-warning-line"></i> No owner found.</li>
- {% endif %}
- </ul>
- <div class="list-group list-group-flush border-top">
- <a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_owner=true) }}"><i class="ri-settings-4-line"></i> Manage the set owners</a>
- </div>
-{{ accordion.footer() }}
-{{ accordion.header('Management', 'management', 'set-details', expanded=true, icon='settings-4-line') }}
- <h5 class="border-bottom">Data</h5>
- <a href="{{ item.url_for_refresh() }}" class="btn btn-primary" role="button"><i class="ri-refresh-line"></i> Refresh the set data</a>
-{{ accordion.footer() }}
+ {{ accordion.header('Owners', 'owner', 'set-details', icon='group-line', class='p-0') }}
+ <ul class="list-group list-group-flush">
+ {% if brickset_owners | length %}
+ {% for owner in brickset_owners %}
+ <li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(item, owner, delete=delete) }}</li>
+ {% endfor %}
+ {% else %}
+ <li class="list-group-item list-group-item-action"><i class="ri-error-warning-line"></i> No owner found.</li>
+ {% endif %}
+ </ul>
+ <div class="list-group list-group-flush border-top">
+ <a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_owner=true) }}"><i class="ri-settings-4-line"></i> Manage the set owners</a>
+ </div>
+ {{ accordion.footer() }}
+ {{ accordion.header('Tags', 'tag', 'set-details', icon='price-tag-2-line', class='p-0') }}
+ <ul class="list-group list-group-flush">
+ {% if brickset_tags | length %}
+ {% for tag in brickset_tags %}
+ <li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(item, tag, delete=delete) }}</li>
+ {% endfor %}
+ {% else %}
+ <li class="list-group-item list-group-item-action"><i class="ri-error-warning-line"></i> No tag found.</li>
+ {% endif %}
+ </ul>
+ <div class="list-group list-group-flush border-top">
+ <a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_tag=true) }}"><i class="ri-settings-4-line"></i> Manage the set tags</a>
+ </div>
+ {{ accordion.footer() }}
+ {{ accordion.header('Management', 'management', 'set-details', expanded=true, icon='settings-4-line') }}
+ <h5 class="border-bottom">Data</h5>
+ <a href="{{ item.url_for_refresh() }}" class="btn btn-primary" role="button"><i class="ri-refresh-line"></i> Refresh the set data</a>
+ {{ accordion.footer() }}
{% endif %}
diff --git a/templates/sets.html b/templates/sets.html
index b06c94dd..7541f1c9 100644
--- a/templates/sets.html
+++ b/templates/sets.html
@@ -10,7 +10,7 @@
<label class="visually-hidden" for="grid-search">Search</label>
<div class="input-group">
<span class="input-group-text"><i class="ri-search-line"></i><span class="ms-1 d-none d-xl-inline"> Search</span></span>
- <input id="grid-search" data-search-exact="name,number,parts,theme,year" data-search-list="owner" class="form-control form-control-sm" type="text" placeholder="Set, name, number of parts, theme, year, owner" value="">
+ <input id="grid-search" data-search-exact="name,number,parts,theme,year" data-search-list="searchOwner,searchTag" class="form-control form-control-sm" type="text" placeholder="Set, name, number of parts, theme, year, owner, tag" value="">
</div>
</div>
<div class="col-12">
@@ -89,6 +89,20 @@
</select>
</div>
</div>
+ <div class="col-12 flex-grow-1">
+ <label class="visually-hidden" for="grid-tag">Tag</label>
+ <div class="input-group">
+ <span class="input-group-text"><i class="ri-price-tag-2-line"></i><span class="ms-1 d-none d-xl-inline"> Tag</span></span>
+ <select id="grid-tag" class="form-select"
+ data-filter="metadata"
+ autocomplete="off">
+ <option value="" selected>All</option>
+ {% for tag in brickset_tags %}
+ <option value="{{ tag.as_dataset() }}">{{ tag.fields.name }}</option>
+ {% endfor %}
+ </select>
+ </div>
+ </div>
</div>
<div class="row" data-grid="true" id="grid">
{% for item in collection %}
From 418a332f0396dd6c689057c0abdefc4c0d1b0a92 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 1bab7d65..8c4cedb9 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 <versatile.mailbox@gmail.com>
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 b96f2139..dabd3b0a 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 344b0e60..a6691e7d 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 <versatile.mailbox@gmail.com>
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 f5b075f9..2825c53a 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 bc302e36..eb48c189 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 <versatile.mailbox@gmail.com>
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 d6141e63..fb52132b 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 cbb64a81..8ab193a5 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 790018a7..24a4d2e1 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 17853ebd..04b70533 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 64d71df8..fa463bec 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 833ae61c..86ca34ad 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 54a3cb8c..d538daa7 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 dbfc4289..d651bf08 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 e3ce2bda..d0bb6eb3 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 00000000..5cd18db6
--- /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 372610d4..ddae2125 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 f5bacd7c..b48bfb71 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 7849d4cc..24c1c567 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 c1d0ed1f..77831a67 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 c8409382..115b7917 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 f996864c..fe1198c0 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 9d3446e4..dbf411b9 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 d3e291a3..7c62c68b 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 a1760d67..43a26da6 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 00000000..d4bbabd4
--- /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 ffefe956..331b15e5 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 271f8909..92612df9 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 00000000..51a615de
--- /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 00000000..128931f3
--- /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 60647fab..5d9cc85f 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 dbcfe0dd..0bea4ab7 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 a6691e7d..add66d20 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('/<id>/parts/<part>/<int:color>/<int:spare>/missing', defaults={'figure': None}, methods=['POST']) # noqa: E501
-@set_page.route('/<id>/minifigures/<figure>/parts/<part>/<int:color>/<int:spare>/missing', methods=['POST']) # noqa: E501
+# Update problematic pieces of a set
+@set_page.route('/<id>/parts/<part>/<int:color>/<int:spare>/<problem>', defaults={'figure': None}, methods=['POST']) # noqa: E501
+@set_page.route('/<id>/minifigures/<figure>/parts/<part>/<int:color>/<int:spare>/<problem>', 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 417ef474..8ae8a883 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 ca577693..a3ea6d7c 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 91bb1c92..8db1ddaf 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) %}
<thead>
<tr>
<th data-table-no-sort="true" class="no-sort" scope="col"><i class="ri-image-line fw-normal"></i> Image</th>
@@ -9,12 +9,8 @@
{% if quantity %}
<th data-table-number="true" scope="col"><i class="ri-functions fw-normal"></i> Quantity</th>
{% endif %}
- {% if missing %}
- <th data-table-number="true" scope="col"><i class="ri-error-warning-line fw-normal"></i> Missing</th>
- {% endif %}
- {% if missing_parts %}
- <th data-table-number="true" scope="col"><i class="ri-error-warning-line fw-normal"></i> Missing parts</th>
- {% endif %}
+ <th data-table-number="true" scope="col"><i class="ri-question-line fw-normal"></i> Missing{% if missing_parts %} parts{% endif %}</th>
+ <th data-table-number="true" scope="col"><i class="ri-error-warning-line fw-normal"></i> Damaged{% if damaged_parts %} parts{% endif %}</th>
{% if sets %}
<th data-table-number="true" scope="col"><i class="ri-hashtag fw-normal"></i> Sets</th>
{% endif %}
diff --git a/templates/minifigure/card.html b/templates/minifigure/card.html
index 446c1647..80459e3a 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 @@
<div class="accordion accordion-flush" id="minifigure-details">
{{ 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') }}
</div>
<div class="card-footer"></div>
{% endif %}
diff --git a/templates/minifigure/table.html b/templates/minifigure/table.html
index 66ece790..aeb0bb78 100644
--- a/templates/minifigure/table.html
+++ b/templates/minifigure/table.html
@@ -2,7 +2,7 @@
<div class="table-responsive-sm">
<table data-table="{% if all %}true{% endif %}" class="table table-striped align-middle" id="minifigures">
- {{ table.header(quantity=true, missing_parts=true, sets=true) }}
+ {{ table.header(quantity=true, missing_parts=true, damaged_parts=true, sets=true) }}
<tbody>
{% for item in table_collection %}
<tr>
@@ -15,6 +15,7 @@
</td>
<td>{{ item.fields.total_quantity }}</td>
<td>{{ item.fields.total_missing }}</td>
+ <td>{{ item.fields.total_damaged }}</td>
<td>{{ item.fields.total_sets }}</td>
</tr>
{% endfor %}
diff --git a/templates/part/card.html b/templates/part/card.html
index 4cb40314..8a0d7aae 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 %}
<div class="accordion accordion-flush border-top" id="part-details">
{{ 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') }}
</div>
<div class="card-footer"></div>
diff --git a/templates/part/table.html b/templates/part/table.html
index cd16f9be..ac96af59 100644
--- a/templates/part/table.html
+++ b/templates/part/table.html
@@ -3,7 +3,7 @@
<div class="table-responsive-sm">
<table data-table="{% if all %}true{% endif %}" class="table table-striped align-middle {% if not all %}sortable mb-0{% endif %}" {% if all %}id="parts"{% endif %}>
- {{ 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) }}
<tbody>
{% for item in table_collection %}
<tr>
@@ -27,11 +27,12 @@
<td>{% if quantity %}{{ item.fields.quantity * quantity }}{% else %}{{ item.fields.quantity }}{% endif %}</td>
{% endif %}
{% endif %}
- {% if not no_missing %}
- <td data-sort="{{ item.fields.total_missing }}" class="table-td-input">
- {{ form.input('Missing', item.fields.id, item.html_id(), item.url_for_missing(), item.fields.total_missing, all=all, read_only=read_only) }}
- </td>
- {% endif %}
+ <td data-sort="{{ item.fields.total_missing }}" class="table-td-input">
+ {{ 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) }}
+ </td>
+ <td data-sort="{{ item.fields.total_damaged }}" class="table-td-input">
+ {{ 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) }}
+ </td>
{% if all %}
<td>{{ item.fields.total_sets }}</td>
<td>{{ item.fields.total_minifigures }}</td>
diff --git a/templates/missing.html b/templates/problem.html
similarity index 78%
rename from templates/missing.html
rename to templates/problem.html
index d7c82a54..76f61f55 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 %}
<div class="container-fluid px-0">
diff --git a/templates/set/card.html b/templates/set/card.html
index eb48c189..262c6409 100644
--- a/templates/set/card.html
+++ b/templates/set/card.html
@@ -6,8 +6,11 @@
<div {% if not solo %}id="set-{{ item.fields.id }}"{% endif %} class="card mb-3 flex-fill {% if solo %}card-solo{% endif %}"
{% if not solo %}
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 }}"
+ data-year="{{ item.fields.year }}" data-theme="{{ item.theme.name | lower }}"
+ data-has-missing-instructions="{{ (not (item.instructions | length)) | int }}"
+ 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 }}"
{% for status in brickset_statuses %}
{% with checked=item.fields[status.as_column()] %}
{% if checked %}
@@ -42,6 +45,7 @@
{{ 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) }}
+ {{ 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 %}
diff --git a/templates/sets.html b/templates/sets.html
index 7541f1c9..dc1c10ef 100644
--- a/templates/sets.html
+++ b/templates/sets.html
@@ -29,7 +29,9 @@
<button id="sort-parts" type="button" class="btn btn-outline-primary"
data-sort-attribute="parts" data-sort-desc="true"><i class="ri-shapes-line"></i><span class="d-none d-xxl-inline"> Parts</span></button>
<button id="sort-missing" type="button" class="btn btn-outline-primary"
- data-sort-attribute="missing" data-sort-desc="true"><i class="ri-error-warning-line"></i><span class="d-none d-xxl-inline"> Missing</span></button>
+ data-sort-attribute="missing" data-sort-desc="true"><i class="ri-question-line"></i><span class="d-none d-xxl-inline"> Missing</span></button>
+ <button id="sort-damaged" type="button" class="btn btn-outline-primary"
+ data-sort-attribute="damaged" data-sort-desc="true"><i class="ri-error-warning-line"></i><span class="d-none d-xxl-inline"> Damaged</span></button>
<button id="sort-clear" type="button" class="btn btn-outline-dark"
data-sort-clear="true"><i class="ri-close-circle-line"></i><span class="d-none d-xxl-inline"> Clear</span></button>
</div>
@@ -53,6 +55,7 @@
<option value="" selected>All</option>
<option value="-has-missing">Set is complete</option>
<option value="has-missing">Set has missing pieces</option>
+ <option value="has-damaged">Set has damaged pieces</option>
<option value="has-missing-instructions">Set has missing instructions</option>
{% for status in brickset_statuses %}
<option value="{{ status.as_dataset() }}">{{ status.fields.name }}</option>
From 2c06ca511eb7d385f387e375800e0fe919faaf74 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 b1b20194..9b48448f 100644
--- a/templates/set/management.html
+++ b/templates/set/management.html
@@ -27,7 +27,7 @@
<a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_tag=true) }}"><i class="ri-settings-4-line"></i> Manage the set tags</a>
</div>
{{ accordion.footer() }}
- {{ accordion.header('Management', 'management', 'set-details', expanded=true, icon='settings-4-line') }}
+ {{ accordion.header('Management', 'management', 'set-details', icon='settings-4-line') }}
<h5 class="border-bottom">Data</h5>
<a href="{{ item.url_for_refresh() }}" class="btn btn-primary" role="button"><i class="ri-refresh-line"></i> Refresh the set data</a>
{{ accordion.footer() }}
From b6c004c0457a9e04749056d01c9649ede01fc0b2 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 eb2bafb5..000047d3 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 <versatile.mailbox@gmail.com>
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 86e82c81..a10fbe34 100644
--- a/templates/admin/database.html
+++ b/templates/admin/database.html
@@ -20,7 +20,7 @@
{% if database_counters %}
<h5 class="border-bottom">Records</h5>
<div class="row">
- <ul class="list-group col-4">
+ <ul class="list-group col-4 mb-2">
{% for counter in database_counters %}
<li class="list-group-item d-flex justify-content-between align-items-start {% if counter.legacy %}list-group-item-dark{% endif %}">
<span><i class="ri-{{ counter.icon }}"></i> {{ counter.name }}</span> <span class="badge {% if counter.legacy %}text-bg-light border{% else %}text-bg-primary{% endif %} rounded-pill ms-2">{{ counter.count }}</span>
From 9d6bc332cb5fab2d97caf5c92830c61fdde6acad Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 74c18cc8..7d8c8822 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 <versatile.mailbox@gmail.com>
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 8bdc3b8d..6a7379e0 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 <versatile.mailbox@gmail.com>
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 fb52132b..0bfc3f02 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 6a7379e0..28a7f0f5 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 8ab193a5..62e23a13 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 04b70533..30007dee 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 8db1ddaf..d31e1c2a 100644
--- a/templates/macro/table.html
+++ b/templates/macro/table.html
@@ -9,8 +9,12 @@
{% if quantity %}
<th data-table-number="true" scope="col"><i class="ri-functions fw-normal"></i> Quantity</th>
{% endif %}
- <th data-table-number="true" scope="col"><i class="ri-question-line fw-normal"></i> Missing{% if missing_parts %} parts{% endif %}</th>
+ {% if not config['HIDE_TABLE_MISSING_PARTS'] %}
+ <th data-table-number="true" scope="col"><i class="ri-question-line fw-normal"></i> Missing{% if missing_parts %} parts{% endif %}</th>
+ {% endif %}
+ {% if not config['HIDE_TABLE_DAMAGED_PARTS'] %}
<th data-table-number="true" scope="col"><i class="ri-error-warning-line fw-normal"></i> Damaged{% if damaged_parts %} parts{% endif %}</th>
+ {% endif %}
{% if sets %}
<th data-table-number="true" scope="col"><i class="ri-hashtag fw-normal"></i> Sets</th>
{% endif %}
diff --git a/templates/part/table.html b/templates/part/table.html
index ac96af59..d2f6569b 100644
--- a/templates/part/table.html
+++ b/templates/part/table.html
@@ -27,12 +27,16 @@
<td>{% if quantity %}{{ item.fields.quantity * quantity }}{% else %}{{ item.fields.quantity }}{% endif %}</td>
{% endif %}
{% endif %}
- <td data-sort="{{ item.fields.total_missing }}" class="table-td-input">
- {{ 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) }}
- </td>
- <td data-sort="{{ item.fields.total_damaged }}" class="table-td-input">
- {{ 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) }}
- </td>
+ {% if not config['HIDE_TABLE_MISSING_PARTS'] %}
+ <td data-sort="{{ item.fields.total_missing }}" class="table-td-input">
+ {{ 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) }}
+ </td>
+ {% endif %}
+ {% if not config['HIDE_TABLE_DAMAGED_PARTS'] %}
+ <td data-sort="{{ item.fields.total_damaged }}" class="table-td-input">
+ {{ 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) }}
+ </td>
+ {% endif %}
{% if all %}
<td>{{ item.fields.total_sets }}</td>
<td>{{ item.fields.total_minifigures }}</td>
From 34408a1bff0071e71b448fceeab54563ac39f751 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 28a7f0f5..54b0fa8c 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 86ca34ad..d0e71380 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 00000000..d75501db
--- /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 0bea4ab7..7cbc1c80 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 8a0d7aae..16f2103e 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') }}
</div>
<div class="card-footer"></div>
From 4cf91a6edd7ba38ed9aa1abb421c608f3fe82c6e Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 54b0fa8c..40b4f173 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 1ad6aa6a..c09589e9 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 d0e71380..eb3f58d5 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 00000000..de2d6ecb
--- /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 d651bf08..a3a30a75 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 6c0ac8e8..3db1680a 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 767fad59..4efb1e64 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 d31e1c2a..ebf1ded6 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) %}
<thead>
<tr>
<th data-table-no-sort="true" class="no-sort" scope="col"><i class="ri-image-line fw-normal"></i> Image</th>
@@ -6,6 +6,9 @@
{% if color %}
<th scope="col"><i class="ri-palette-line fw-normal"></i> Color</th>
{% endif %}
+ {% if parts %}
+ <th data-table-number="true" scope="col"><i class="ri-shapes-line fw-normal"></i> Parts</th>
+ {% endif %}
{% if quantity %}
<th data-table-number="true" scope="col"><i class="ri-functions fw-normal"></i> Quantity</th>
{% endif %}
diff --git a/templates/minifigure/card.html b/templates/minifigure/card.html
index 80459e3a..2cba9b4c 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) }}
<div class="card-body border-bottom {% if not solo %}p-1{% endif %}">
+ {{ 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 aeb0bb78..c91ab584 100644
--- a/templates/minifigure/table.html
+++ b/templates/minifigure/table.html
@@ -2,7 +2,7 @@
<div class="table-responsive-sm">
<table data-table="{% if all %}true{% endif %}" class="table table-striped align-middle" id="minifigures">
- {{ 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) }}
<tbody>
{% for item in table_collection %}
<tr>
@@ -13,6 +13,7 @@
{{ table.rebrickable(item) }}
{% endif %}
</td>
+ <td>{{ item.fields.number_of_parts }}</td>
<td>{{ item.fields.total_quantity }}</td>
<td>{{ item.fields.total_missing }}</td>
<td>{{ item.fields.total_damaged }}</td>
From 7453d97c81ec6c6bc67b6e89ffbabd866b2699c5 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 3a0b7840..9f33c059 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
</label>
</div>
- {% if brickset_owners | length %}
- <h5 class="border-bottom mt-2">Owners</h5>
- <div id="add-owners">
- {% for owner in brickset_owners %}
- {% with id=owner.as_dataset() %}
- <div class="form-check">
- <input class="form-check-input" type="checkbox" value="{{ owner.fields.id }}" id="{{ id }}" autocomplete="off">
- <label class="form-check-label" for="{{ id }}">{{ owner.fields.name }}</label>
- </div>
- {% endwith %}
- {% endfor %}
- </div>
- {% endif %}
- {% if brickset_tags | length %}
- <h5 class="border-bottom mt-2">Tags</h5>
- <div id="add-tags">
- {% for tag in brickset_tags %}
- {% with id=tag.as_dataset() %}
- <div class="form-check">
- <input class="form-check-input" type="checkbox" value="{{ tag.fields.id }}" id="{{ id }}" autocomplete="off">
- <label class="form-check-label" for="{{ id }}">{{ tag.fields.name }}</label>
- </div>
- {% endwith %}
- {% endfor %}
- </div>
- {% endif %}
+ <h6 class="border-bottom mt-2">Metadata</h6>
+ <div class="accordion accordion" id="metadata">
+ {% if brickset_owners | length %}
+ {{ accordion.header('Owners', 'owners', 'metadata', icon='user-line') }}
+ <div id="add-owners">
+ {% for owner in brickset_owners %}
+ {% with id=owner.as_dataset() %}
+ <div class="form-check">
+ <input class="form-check-input" type="checkbox" value="{{ owner.fields.id }}" id="{{ id }}" autocomplete="off">
+ <label class="form-check-label" for="{{ id }}">{{ owner.fields.name }}</label>
+ </div>
+ {% endwith %}
+ {% endfor %}
+ </div>
+ {{ accordion.footer() }}
+ {% endif %}
+ {% if brickset_tags | length %}
+ {{ accordion.header('Tags', 'tags', 'metadata', icon='price-tag-2-line') }}
+ <div id="add-tags">
+ {% for tag in brickset_tags %}
+ {% with id=tag.as_dataset() %}
+ <div class="form-check">
+ <input class="form-check-input" type="checkbox" value="{{ tag.fields.id }}" id="{{ id }}" autocomplete="off">
+ <label class="form-check-label" for="{{ id }}">{{ tag.fields.name }}</label>
+ </div>
+ {% endwith %}
+ {% endfor %}
+ </div>
+ {{ accordion.footer() }}
+ {% endif %}
+ </div>
<hr>
<div class="mb-3">
<p>
From d0d1e53acc069967332d1e732124c9a3f5af88e6 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 89ef71f8..7d52d33a 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 7d8c8822..e5b92624 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 <versatile.mailbox@gmail.com>
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 dfd82112..b93ac174 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() %}
<input class="form-check-input" type="checkbox" id="{{ prefix }}-{{ item.fields.id }}" {% if item.fields[metadata.as_column()] %}checked{% endif %}
{% if not delete %}
- data-changer-id="{{ item.fields.id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ metadata.url_for_set_state(item.fields.id) }}" data-changer-parent="set"
+ data-changer-id="{{ item.fields.id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ metadata.url_for_set_state(item.fields.id) }}" {% if parent %}data-changer-parent="{{ parent }}"{% endif %}
{% else %}
disabled
{% endif %}
diff --git a/templates/set/card.html b/templates/set/card.html
index 262c6409..73a90a5c 100644
--- a/templates/set/card.html
+++ b/templates/set/card.html
@@ -59,7 +59,7 @@
{% if not tiny and brickset_statuses | length %}
<ul class="list-group list-group-flush card-check border-bottom-0">
{% for status in brickset_statuses %}
- <li class="d-flex list-group-item {% if not solo %}p-1{% endif %} text-nowrap">{{ form.checkbox(item, status, delete=delete) }}</li>
+ <li class="d-flex list-group-item {% if not solo %}p-1{% endif %} text-nowrap">{{ form.checkbox(item, status, parent='set', delete=delete) }}</li>
{% endfor %}
</ul>
{% endif %}
From 53d1603e3e30d01d40d5d77a7d8f5e97eb3b9f32 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 16fca2f5..6673ab17 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 000047d3..09b22ecf 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 d538daa7..28d8153c 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 13097490..7d3b8f5e 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 dabd3b0a..e238f62c 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 92806f22..822ac3bf 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 90729733..e77e7dd6 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 415cf487..c716b567 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 1cbcd564..0a25e072 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 add66d20..6d86bb1d 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 <versatile.mailbox@gmail.com>
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 4b7c54ea..f43eaf59 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 <versatile.mailbox@gmail.com>
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 7447a6da..8c7c2619 100644
--- a/templates/admin/owner.html
+++ b/templates/admin/owner.html
@@ -22,7 +22,7 @@
</li>
{% endfor %}
{% else %}
- <li class="list-group-item"><i class="ri-error-warning-line"></i> No owner found.</li>
+ <li class="list-group-item text-center"><i class="ri-error-warning-line"></i> No owner found.</li>
{% endif %}
<li class="list-group-item">
<form action="{{ url_for('admin_owner.add') }}" method="post" class="row row-cols-lg-auto g-3 align-items-center">
diff --git a/templates/admin/status.html b/templates/admin/status.html
index a73462ad..609bc68d 100644
--- a/templates/admin/status.html
+++ b/templates/admin/status.html
@@ -33,7 +33,7 @@
</li>
{% endfor %}
{% else %}
- <li class="list-group-item"><i class="ri-error-warning-line"></i> No status found.</li>
+ <li class="list-group-item text-center"><i class="ri-error-warning-line"></i> No status found.</li>
{% endif %}
<li class="list-group-item">
<form action="{{ url_for('admin_status.add') }}" method="post" class="row row-cols-lg-auto g-3 align-items-center">
diff --git a/templates/admin/tag.html b/templates/admin/tag.html
index 7c67a56a..c3ca1f61 100644
--- a/templates/admin/tag.html
+++ b/templates/admin/tag.html
@@ -22,7 +22,7 @@
</li>
{% endfor %}
{% else %}
- <li class="list-group-item"><i class="ri-error-warning-line"></i> No tag found.</li>
+ <li class="list-group-item text-center"><i class="ri-error-warning-line"></i> No tag found.</li>
{% endif %}
<li class="list-group-item">
<form action="{{ url_for('admin_tag.add') }}" method="post" class="row row-cols-lg-auto g-3 align-items-center">
diff --git a/templates/set/management.html b/templates/set/management.html
index 9b48448f..f80830bd 100644
--- a/templates/set/management.html
+++ b/templates/set/management.html
@@ -6,7 +6,7 @@
<li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(item, owner, delete=delete) }}</li>
{% endfor %}
{% else %}
- <li class="list-group-item list-group-item-action"><i class="ri-error-warning-line"></i> No owner found.</li>
+ <li class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No owner found.</li>
{% endif %}
</ul>
<div class="list-group list-group-flush border-top">
@@ -20,7 +20,7 @@
<li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(item, tag, delete=delete) }}</li>
{% endfor %}
{% else %}
- <li class="list-group-item list-group-item-action"><i class="ri-error-warning-line"></i> No tag found.</li>
+ <li class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No tag found.</li>
{% endif %}
</ul>
<div class="list-group list-group-flush border-top">
From 187afdc2cf2e0bf4c0de114db28f54c199e7ee28 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 89f012da..4db9cd7a 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 <versatile.mailbox@gmail.com>
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 a7e660b4..d561261e 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 <versatile.mailbox@gmail.com>
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 40b4f173..8a1c34d7 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 240bc637..4b6f0d4f 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 f43eaf59..07545f92 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 b0d42c3b..5dfa73c3 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 3de9bf96..23da29bc 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 6673ab17..b2247ea4 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 09b22ecf..90b2679b 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 28d8153c..e25594ca 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 00000000..0a54262f
--- /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 00000000..72efde70
--- /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 331b15e5..fbc86e0d 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 7dd6dec8..9a46f88b 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 00000000..2417aa69
--- /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 00000000..c50b348e
--- /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 00000000..262c6515
--- /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 00000000..87ac7a48
--- /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 00000000..b37a7e83
--- /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 00000000..d27d27c5
--- /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 00000000..7cc40d6f
--- /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 e77e7dd6..fb11efe4 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 c716b567..584a359f 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 00000000..7c686bfa
--- /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('<id>/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('<id>/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('<id>/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 0a25e072..b64775bf 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 6d86bb1d..8983cf94 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('/<id>/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('/<id>/tag/<metadata_id>', 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 d561261e..311eac50 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 9f33c059..9a0deebf 100644
--- a/templates/add.html
+++ b/templates/add.html
@@ -51,6 +51,19 @@
</div>
{{ accordion.footer() }}
{% endif %}
+ {% if brickset_storages | length %}
+ {{ accordion.header('Storage', 'storage', 'metadata', icon='archive-2-line') }}
+ <label class="visually-hidden" for="storage">{{ name }}</label>
+ <div class="input-group">
+ <select id="add-storage" class="form-select" autocomplete="off">
+ <option value="" selected><i>None</i></option>
+ {% for storage in brickset_storages %}
+ <option value="{{ storage.fields.id }}">{{ storage.fields.name }}</option>
+ {% endfor %}
+ </select>
+ </div>
+ {{ accordion.footer() }}
+ {% endif %}
{% if brickset_tags | length %}
{{ accordion.header('Tags', 'tags', 'metadata', icon='price-tag-2-line') }}
<div id="add-tags">
diff --git a/templates/admin.html b/templates/admin.html
index 064526d8..3582c7b6 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 00000000..1f317e6c
--- /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 %}<div class="alert alert-danger m-2" role="alert"><strong>Error:</strong> {{ storage_error }}.</div>{% endif %}
+<ul class="list-group list-group-flush">
+ {% if metadata_storages | length %}
+ {% for storage in metadata_storages %}
+ <li class="list-group-item">
+ <form action="{{ url_for('admin_storage.rename', id=storage.fields.id) }}" method="post" class="row row-cols-lg-auto g-3 align-items-center">
+ <div class="col-12 flex-grow-1">
+ <label class="visually-hidden" for="name-{{ storage.fields.id }}">Name</label>
+ <div class="input-group">
+ <div class="input-group-text">Name</div>
+ <input type="text" class="form-control" id="name-{{ storage.fields.id }}" name="name" value="{{ storage.fields.name }}">
+ <button type="submit" class="btn btn-primary"><i class="ri-edit-line"></i> Rename</button>
+ </div>
+ </div>
+ <div class="col-12">
+ <a href="{{ url_for('admin_storage.delete', id=storage.fields.id) }}" class="btn btn-danger" role="button"><i class="ri-delete-bin-2-line"></i> Delete</a>
+ </div>
+ </form>
+ </li>
+ {% endfor %}
+ {% else %}
+ <li class="list-group-item text-center"><i class="ri-error-warning-line"></i> No storage found.</li>
+ {% endif %}
+ <li class="list-group-item">
+ <form action="{{ url_for('admin_storage.add') }}" method="post" class="row row-cols-lg-auto g-3 align-items-center">
+ <div class="col-12 flex-grow-1">
+ <label class="visually-hidden" for="name">Name</label>
+ <div class="input-group">
+ <div class="input-group-text">Name</div>
+ <input type="text" class="form-control" id="name" name="name" value="">
+ </div>
+ </div>
+ <div class="col-12">
+ <button type="submit" class="btn btn-primary"><i class="ri-add-circle-line"></i> Add</button>
+ </div>
+ </form>
+ </li>
+</ul>
+{{ accordion.footer() }}
diff --git a/templates/admin/storage/delete.html b/templates/admin/storage/delete.html
new file mode 100644
index 00000000..b3eb990c
--- /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') }}
+<form action="{{ url_for('admin_storage.do_delete', id=storage.fields.id) }}" method="post">
+ {% if storage_error %}<div class="alert alert-danger text-start" role="alert"><strong>Error:</strong> {{ storage_error }}.</div>{% endif %}
+ <div class="alert alert-danger text-center" role="alert">You are about to <strong>delete a set storage</strong>. This action is irreversible.</div>
+ <div class="row row-cols-lg-auto g-3 align-items-center">
+ <div class="col-12 flex-grow-1">
+ <div class="input-group">
+ <div class="input-group-text">Name</div>
+ <input type="text" class="form-control" value="{{ storage.fields.name }}" disabled>
+ </div>
+ </div>
+ </div>
+ <hr class="border-bottom">
+ <a class="btn btn-danger" href="{{ url_for('admin.admin', open_storage=true) }}" role="button"><i class="ri-arrow-left-long-line"></i> Back to the admin</a>
+ <button type="submit" class="btn btn-danger"><i class="ri-delete-bin-2-line"></i> Delete <strong>the set storage</strong></button>
+</form>
+{{ accordion.footer() }}
diff --git a/templates/macro/badge.html b/templates/macro/badge.html
index a3ea6d7c..dfc93840 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 b93ac174..d3d35128 100644
--- a/templates/macro/form.html
+++ b/templates/macro/form.html
@@ -39,3 +39,27 @@
</div>
{% 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() %}
+ <label class="visually-hidden" for="{{ prefix }}-{{ item.fields.id }}">{{ name }}</label>
+ <div class="input-group">
+ {% if icon %}<span class="input-group-text"><i class="ri-{{ icon }}"></i></span>{% endif %}
+ <select id="{{ prefix }}-{{ item.fields.id }}" class="form-select"
+ {% if not delete %}
+ data-changer-id="{{ item.fields.id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ metadata_list.url_for_set_state(item.fields.id) }}"
+ {% else %}
+ disabled
+ {% endif %}
+ autocomplete="off">
+ {% if nullable %}<option value="" {% if item.fields[field] is none %}selected{% endif %}><i>None</i></option>{% endif %}
+ {% for metadata in metadata_list %}
+ <option value="{{ metadata.fields.id }}" {% if metadata.fields.id == item.fields[field] %}selected{% endif %}>{{ metadata.fields.name }}</option>
+ {% endfor %}
+ </select>
+ <span id="status-{{ prefix }}-{{ item.fields.id }}" class="input-group-text ri-save-line"></span>
+ <button id="clear-{{ prefix }}-{{ item.fields.id }}" type="button" class="btn btn-sm btn-light btn-outline-danger border"><i class="ri-eraser-line"></i></button>
+ </div>
+ {% endif %}
+{% endmacro %}
diff --git a/templates/set/card.html b/templates/set/card.html
index 73a90a5c..a3270526 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 <versatile.mailbox@gmail.com>
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 7aedaf26..99cd6258 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 <versatile.mailbox@gmail.com>
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 f80830bd..54b2e879 100644
--- a/templates/set/management.html
+++ b/templates/set/management.html
@@ -13,6 +13,15 @@
<a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_owner=true) }}"><i class="ri-settings-4-line"></i> Manage the set owners</a>
</div>
{{ 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 %}
+ <p class="text-center"><i class="ri-error-warning-line"></i> No storage found.</p>
+ {% endif %}
+ <hr>
+ <a href="{{ url_for('admin.admin', open_tag=true) }}" class="btn btn-primary" role="button"><i class="ri-settings-4-line"></i> Manage the set storages</a>
+ {{ accordion.footer() }}
{{ accordion.header('Tags', 'tag', 'set-details', icon='price-tag-2-line', class='p-0') }}
<ul class="list-group list-group-flush">
{% if brickset_tags | length %}
From 56c926a8ef65d1ef5cfdb96daa450b031f2639e6 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 d3d35128..6fb38902 100644
--- a/templates/macro/form.html
+++ b/templates/macro/form.html
@@ -9,7 +9,7 @@
{% endif %}
autocomplete="off">
<label class="form-check-label flex-grow-1 ms-1" for="{{ prefix }}-{{ item.fields.id }}">
- {{ metadata.fields.name }} <i id="status-{{ prefix }}-{{ item.fields.id }}" class="mb-1"></i>
+ {{ metadata.fields.name }} <i id="status-{{ prefix }}-{{ item.fields.id }}"></i>
</label>
{% else %}
<input class="form-check-input text-reset" type="checkbox" {% if item.fields[metadata.as_column()] %}checked{% endif %} disabled>
From ac2d2a0b5d9bf0c52f0660754e10aa25fc1949d0 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 a3270526..3585acc4 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 dc1c10ef..dc0a5574 100644
--- a/templates/sets.html
+++ b/templates/sets.html
@@ -10,7 +10,7 @@
<label class="visually-hidden" for="grid-search">Search</label>
<div class="input-group">
<span class="input-group-text"><i class="ri-search-line"></i><span class="ms-1 d-none d-xl-inline"> Search</span></span>
- <input id="grid-search" data-search-exact="name,number,parts,theme,year" data-search-list="searchOwner,searchTag" class="form-control form-control-sm" type="text" placeholder="Set, name, number of parts, theme, year, owner, tag" value="">
+ <input id="grid-search" data-search-exact="name,number,parts,searchStorage,theme,year" data-search-list="searchOwner,searchTag" class="form-control form-control-sm" type="text" placeholder="Set, name, number of parts, theme, year, owner, storage, tag" value="">
</div>
</div>
<div class="col-12">
@@ -57,6 +57,7 @@
<option value="has-missing">Set has missing pieces</option>
<option value="has-damaged">Set has damaged pieces</option>
<option value="has-missing-instructions">Set has missing instructions</option>
+ {% if brickset_storage | length %}<option value="has-storage">Is in storage</option>{% endif %}
{% for status in brickset_statuses %}
<option value="{{ status.as_dataset() }}">{{ status.fields.name }}</option>
<option value="-{{ status.as_dataset() }}">NOT: {{ status.fields.name }}</option>
@@ -92,6 +93,22 @@
</select>
</div>
</div>
+ {% if brickset_storages | length %}
+ <div class="col-12 flex-grow-1">
+ <label class="visually-hidden" for="grid-owner">Storage</label>
+ <div class="input-group">
+ <span class="input-group-text"><i class="ri-archive-2-line"></i><span class="ms-1 d-none d-xl-inline"> Storage</span></span>
+ <select id="grid-storage" class="form-select"
+ data-filter="value" data-filter-attribute="storage"
+ autocomplete="off">
+ <option value="" selected>All</option>
+ {% for storage in brickset_storages %}
+ <option value="{{ storage.fields.id }}">{{ storage.fields.name }}</option>
+ {% endfor %}
+ </select>
+ </div>
+ </div>
+ {% endif %}
<div class="col-12 flex-grow-1">
<label class="visually-hidden" for="grid-tag">Tag</label>
<div class="input-group">
From d45070eb74c57e6365dffb46f3e08526f9855166 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 dc0a5574..943f7941 100644
--- a/templates/sets.html
+++ b/templates/sets.html
@@ -79,20 +79,22 @@
</select>
</div>
</div>
- <div class="col-12 flex-grow-1">
- <label class="visually-hidden" for="grid-owner">Owner</label>
- <div class="input-group">
- <span class="input-group-text"><i class="ri-user-line"></i><span class="ms-1 d-none d-xl-inline"> Owner</span></span>
- <select id="grid-owner" class="form-select"
- data-filter="metadata"
- autocomplete="off">
- <option value="" selected>All</option>
- {% for owner in brickset_owners %}
- <option value="{{ owner.as_dataset() }}">{{ owner.fields.name }}</option>
- {% endfor %}
- </select>
+ {% if brickset_owners | length %}
+ <div class="col-12 flex-grow-1">
+ <label class="visually-hidden" for="grid-owner">Owner</label>
+ <div class="input-group">
+ <span class="input-group-text"><i class="ri-user-line"></i><span class="ms-1 d-none d-xl-inline"> Owner</span></span>
+ <select id="grid-owner" class="form-select"
+ data-filter="metadata"
+ autocomplete="off">
+ <option value="" selected>All</option>
+ {% for owner in brickset_owners %}
+ <option value="{{ owner.as_dataset() }}">{{ owner.fields.name }}</option>
+ {% endfor %}
+ </select>
+ </div>
</div>
- </div>
+ {% endif %}
{% if brickset_storages | length %}
<div class="col-12 flex-grow-1">
<label class="visually-hidden" for="grid-owner">Storage</label>
@@ -109,20 +111,22 @@
</div>
</div>
{% endif %}
- <div class="col-12 flex-grow-1">
- <label class="visually-hidden" for="grid-tag">Tag</label>
- <div class="input-group">
- <span class="input-group-text"><i class="ri-price-tag-2-line"></i><span class="ms-1 d-none d-xl-inline"> Tag</span></span>
- <select id="grid-tag" class="form-select"
- data-filter="metadata"
- autocomplete="off">
- <option value="" selected>All</option>
- {% for tag in brickset_tags %}
- <option value="{{ tag.as_dataset() }}">{{ tag.fields.name }}</option>
- {% endfor %}
- </select>
+ {% if brickset_tags | length %}
+ <div class="col-12 flex-grow-1">
+ <label class="visually-hidden" for="grid-tag">Tag</label>
+ <div class="input-group">
+ <span class="input-group-text"><i class="ri-price-tag-2-line"></i><span class="ms-1 d-none d-xl-inline"> Tag</span></span>
+ <select id="grid-tag" class="form-select"
+ data-filter="metadata"
+ autocomplete="off">
+ <option value="" selected>All</option>
+ {% for tag in brickset_tags %}
+ <option value="{{ tag.as_dataset() }}">{{ tag.fields.name }}</option>
+ {% endfor %}
+ </select>
+ </div>
</div>
- </div>
+ {% endif %}
</div>
<div class="row" data-grid="true" id="grid">
{% for item in collection %}
From 561720343b746ffc6bbdd87b45b2f663b41df1a9 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 3585acc4..126c9b38 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 <versatile.mailbox@gmail.com>
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 e25594ca..ffc436d2 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 5d9cc85f..99587287 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 7cbc1c80..b2a9eedd 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 dfc93840..bd683f40 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 2cba9b4c..812c2cd5 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 16f2103e..a83a2f7f 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 <versatile.mailbox@gmail.com>
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 126c9b38..f47a7ca3 100644
--- a/templates/set/card.html
+++ b/templates/set/card.html
@@ -4,7 +4,7 @@
{% import 'macro/form.html' as form %}
<div {% if not solo %}id="set-{{ item.fields.id }}"{% endif %} class="card mb-3 flex-fill {% if solo %}card-solo{% endif %}"
- {% if not solo %}
+ {% 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 }}"
@@ -55,7 +55,7 @@
{{ 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 %}
From 9b55fd5e33234abc9de9d5a811d81a46f8499096 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 943f7941..236c3785 100644
--- a/templates/sets.html
+++ b/templates/sets.html
@@ -57,7 +57,10 @@
<option value="has-missing">Set has missing pieces</option>
<option value="has-damaged">Set has damaged pieces</option>
<option value="has-missing-instructions">Set has missing instructions</option>
- {% if brickset_storage | length %}<option value="has-storage">Is in storage</option>{% endif %}
+ {% if brickset_storages | length %}
+ <option value="has-storage">Is in storage</option>
+ <option value="-has-storage">Is NOT in storage</option>
+ {% endif %}
{% for status in brickset_statuses %}
<option value="{{ status.as_dataset() }}">{{ status.fields.name }}</option>
<option value="-{{ status.as_dataset() }}">NOT: {{ status.fields.name }}</option>
From 0e485ddb71af1a118f6dbf9dbec3f43dbd0f0b22 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 0bfc3f02..b93e8c6b 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 8a1c34d7..5ed15af7 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 62e23a13..cd7ef74a 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 236c3785..60101b54 100644
--- a/templates/sets.html
+++ b/templates/sets.html
@@ -14,7 +14,23 @@
</div>
</div>
<div class="col-12">
- <div id="grid-sort" class="input-group">
+ <div class="input-group">
+ <button class="btn btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#grid-sort" aria-expanded="{% if config['SHOW_GRID_SORT'] %}true{% else %}false{% endif %}" aria-controls="grid-sort">
+ <i class="ri-sort-asc"></i> Sort
+ </button>
+ </div>
+ </div>
+ <div class="col-12">
+ <div class="input-group">
+ <button class="btn btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#grid-filter" aria-expanded="{% if config['SHOW_GRID_FILTERS'] %}true{% else %}false{% endif %}" aria-controls="grid-filter">
+ <i class="ri-filter-line"></i> Filters
+ </button>
+ </div>
+ </div>
+ </div>
+ <div id="grid-sort" class="collapse {% if config['SHOW_GRID_SORT'] %}show{% endif %} row row-cols-lg-auto g-1 justify-content-center align-items-center pb-2">
+ <div class="col-12 flex-grow-1">
+ <div class="input-group">
<span class="input-group-text"><i class="ri-sort-asc"></i><span class="ms-1 d-none d-xxl-inline"> Sort</span></span>
<button id="sort-number" type="button" class="btn btn-outline-primary"
data-sort-attribute="number" data-sort-natural="true"><i class="ri-hashtag"></i><span class="d-none d-xxl-inline"> Set</span></button>
@@ -36,13 +52,6 @@
data-sort-clear="true"><i class="ri-close-circle-line"></i><span class="d-none d-xxl-inline"> Clear</span></button>
</div>
</div>
- <div class="col-12">
- <div class="input-group">
- <button class="btn btn-outline-primary" type="button" data-bs-toggle="collapse" data-bs-target="#grid-filter" aria-expanded="{% if config['SHOW_GRID_FILTERS'] %}true{% else %}false{% endif %}" aria-controls="grid-filter">
- <i class="ri-filter-line"></i> Filters
- </button>
- </div>
- </div>
</div>
<div id="grid-filter" class="collapse {% if config['SHOW_GRID_FILTERS'] %}show{% endif %} row row-cols-lg-auto g-1 justify-content-center align-items-center pb-2">
<div class="col-12 flex-grow-1">
From 8ad525926a2214b0d9e613701aa99e15943f183d Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 c50b348e..e1c5d226 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 <versatile.mailbox@gmail.com>
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 24a4d2e1..fa735629 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 eb3f58d5..a12ef89e 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 ffc436d2..b12f9717 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 <versatile.mailbox@gmail.com>
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 fa463bec..12eab28c 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 <versatile.mailbox@gmail.com>
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 8ae8a883..1fd91f3c 100644
--- a/templates/macro/accordion.html
+++ b/templates/macro/accordion.html
@@ -16,7 +16,7 @@
{% endif %}
</button>
</h2>
- <div id="{{ id }}" class="accordion-collapse collapse {% if expanded %}show{% endif %}" {% if not config['INDEPENDENT_ACCORDIONS'] %}data-bs-parent="#{{ parent }}"{% endif %}>
+ <div id="{{ id }}" class="accordion-collapse collapse border-start border-5 rounded-0 {% if expanded %}show{% endif %}" {% if not config['INDEPENDENT_ACCORDIONS'] %}data-bs-parent="#{{ parent }}"{% endif %}>
<div class="accordion-body {% if class %}{{ class }}{% endif %}">
{% endmacro %}
From f9e9edd50662b7bd59c537fe1002e12693f353e6 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 5dfa73c3..68238e9d 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 <versatile.mailbox@gmail.com>
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 b93e8c6b..d14491e5 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 5ed15af7..b50f3427 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 4b6f0d4f..af005d96 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 cd7ef74a..5b9788fb 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 68238e9d..60bfb5f1 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 30007dee..20a2b292 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 90b2679b..6368d40c 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 b12f9717..6c3b9283 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 0a54262f..30c559c9 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 72efde70..8453f366 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 00000000..0dc0f14a
--- /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 00000000..0bd8b8eb
--- /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 2417aa69..bf616ed9 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 00000000..e41e97a4
--- /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('/<id>/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 1fd91f3c..c5c89545 100644
--- a/templates/macro/accordion.html
+++ b/templates/macro/accordion.html
@@ -26,10 +26,10 @@
</div>
{% 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) }}
<div class="row">
{% for item in card_collection %}
<div class="col-md-6 col-xl-3 d-flex align-items-stretch">
diff --git a/templates/macro/badge.html b/templates/macro/badge.html
index bd683f40..e1f9071a 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 ebf1ded6..0638380f 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) %}
<thead>
<tr>
- <th data-table-no-sort="true" class="no-sort" scope="col"><i class="ri-image-line fw-normal"></i> Image</th>
+ {% if image %}
+ <th data-table-no-sort="true" class="no-sort" scope="col"><i class="ri-image-line fw-normal"></i> Image</th>
+ {% endif %}
<th scope="col"><i class="ri-pencil-line fw-normal"></i> Name</th>
{% if color %}
<th scope="col"><i class="ri-palette-line fw-normal"></i> Color</th>
@@ -12,10 +14,10 @@
{% if quantity %}
<th data-table-number="true" scope="col"><i class="ri-functions fw-normal"></i> Quantity</th>
{% endif %}
- {% if not config['HIDE_TABLE_MISSING_PARTS'] %}
+ {% if missing and not config['HIDE_TABLE_MISSING_PARTS'] %}
<th data-table-number="true" scope="col"><i class="ri-question-line fw-normal"></i> Missing{% if missing_parts %} parts{% endif %}</th>
{% endif %}
- {% if not config['HIDE_TABLE_DAMAGED_PARTS'] %}
+ {% if damaged and not config['HIDE_TABLE_DAMAGED_PARTS'] %}
<th data-table-number="true" scope="col"><i class="ri-error-warning-line fw-normal"></i> Damaged{% if damaged_parts %} parts{% endif %}</th>
{% endif %}
{% if sets %}
diff --git a/templates/storage.html b/templates/storage.html
new file mode 100644
index 00000000..76e95497
--- /dev/null
+++ b/templates/storage.html
@@ -0,0 +1,15 @@
+{% extends 'base.html' %}
+
+{% block title %} - Storage {{ item.fields.name}}{% endblock %}
+
+{% block main %}
+<div class="container">
+ <div class="row">
+ <div class="col-12">
+ {% with solo=true %}
+ {% include 'storage/card.html' %}
+ {% endwith %}
+ </div>
+ </div>
+</div>
+{% endblock %}
diff --git a/templates/storage/card.html b/templates/storage/card.html
new file mode 100644
index 00000000..cf29de3f
--- /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 %}
+
+<div class="card mb-3 flex-fill {% if solo %}card-solo{% endif %}">
+ {{ card.header(item, item.fields.name, solo=solo, icon='archive-2-line') }}
+ <div class="card-body border-bottom-0 {% if not solo %}p-1{% endif %}">
+ {{ badge.total_sets(sets | length, solo=solo, last=last) }}
+ </div>
+ {% if solo %}
+ <div class="accordion accordion-flush border-top" id="storage-details">
+ {{ accordion.cards(sets, 'Sets', 'sets-stored', 'storage-details', 'set/card.html', expanded=true, icon='hashtag') }}
+ </div>
+ <div class="card-footer"></div>
+ {% endif %}
+</div>
diff --git a/templates/storage/table.html b/templates/storage/table.html
new file mode 100644
index 00000000..680c75c8
--- /dev/null
+++ b/templates/storage/table.html
@@ -0,0 +1,16 @@
+{% import 'macro/form.html' as form %}
+{% import 'macro/table.html' as table %}
+
+<div class="table-responsive-sm">
+ <table data-table="true" class="table table-striped align-middle" id="storage">
+ {{ table.header(image=false, missing=false, damaged=false, sets=true) }}
+ <tbody>
+ {% for item in table_collection %}
+ <tr>
+ <td data-sort="{{ item.fields.name }}"><a class="text-reset" href="{{ item.url() }}">{{ item.fields.name }}</a></td>
+ <td>{{ item.fields.total_sets }}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+</div>
diff --git a/templates/storages.html b/templates/storages.html
new file mode 100644
index 00000000..14ac3548
--- /dev/null
+++ b/templates/storages.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+
+{% block title %} - All storages{% endblock %}
+
+{% block main %}
+<div class="container-fluid px-0">
+ {% with all=true %}
+ {% include 'storage/table.html' %}
+ {% endwith %}
+</div>
+{% endblock %}
From 48e4b59344bfa630d7a993fad8e72705a121b462 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 d0bb6eb3..904e818e 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 b48bfb71..6301550b 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 77831a67..8bb8dcba 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 dbf411b9..068b8d86 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 <versatile.mailbox@gmail.com>
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 00000000..65c33433
--- /dev/null
+++ b/templates/set/filter.html
@@ -0,0 +1,87 @@
+<div id="grid-filter" class="collapse {% if config['SHOW_GRID_FILTERS'] %}show{% endif %} row row-cols-lg-auto g-1 justify-content-center align-items-center pb-2">
+ <div class="col-12 flex-grow-1">
+ <label class="visually-hidden" for="grid-status">Status</label>
+ <div class="input-group">
+ <span class="input-group-text"><i class="ri-checkbox-line"></i><span class="ms-1 d-none d-md-inline"> Status</span></span>
+ <select id="grid-status" class="form-select"
+ data-filter="metadata"
+ autocomplete="off">
+ <option value="" selected>All</option>
+ <option value="-has-missing">Set is complete</option>
+ <option value="has-missing">Set has missing pieces</option>
+ <option value="has-damaged">Set has damaged pieces</option>
+ <option value="has-missing-instructions">Set has missing instructions</option>
+ {% if brickset_storages | length %}
+ <option value="has-storage">Is in storage</option>
+ <option value="-has-storage">Is NOT in storage</option>
+ {% endif %}
+ {% for status in brickset_statuses %}
+ <option value="{{ status.as_dataset() }}">{{ status.fields.name }}</option>
+ <option value="-{{ status.as_dataset() }}">NOT: {{ status.fields.name }}</option>
+ {% endfor %}
+ </select>
+ </div>
+ </div>
+ <div class="col-12 flex-grow-1">
+ <label class="visually-hidden" for="grid-theme">Theme</label>
+ <div class="input-group">
+ <span class="input-group-text"><i class="ri-price-tag-3-line"></i><span class="ms-1 d-none d-md-inline"> Theme</span></span>
+ <select id="grid-theme" class="form-select"
+ data-filter="value" data-filter-attribute="theme"
+ autocomplete="off">
+ <option value="" selected>All</option>
+ {% for theme in collection.themes %}
+ <option value="{{ theme | lower }}">{{ theme }}</option>
+ {% endfor %}
+ </select>
+ </div>
+ </div>
+ {% if brickset_owners | length %}
+ <div class="col-12 flex-grow-1">
+ <label class="visually-hidden" for="grid-owner">Owner</label>
+ <div class="input-group">
+ <span class="input-group-text"><i class="ri-user-line"></i><span class="ms-1 d-none d-md-inline"> Owner</span></span>
+ <select id="grid-owner" class="form-select"
+ data-filter="metadata"
+ autocomplete="off">
+ <option value="" selected>All</option>
+ {% for owner in brickset_owners %}
+ <option value="{{ owner.as_dataset() }}">{{ owner.fields.name }}</option>
+ {% endfor %}
+ </select>
+ </div>
+ </div>
+ {% endif %}
+ {% if brickset_storages | length %}
+ <div class="col-12 flex-grow-1">
+ <label class="visually-hidden" for="grid-owner">Storage</label>
+ <div class="input-group">
+ <span class="input-group-text"><i class="ri-archive-2-line"></i><span class="ms-1 d-none d-md-inline"> Storage</span></span>
+ <select id="grid-storage" class="form-select"
+ data-filter="value" data-filter-attribute="storage"
+ autocomplete="off">
+ <option value="" selected>All</option>
+ {% for storage in brickset_storages %}
+ <option value="{{ storage.fields.id }}">{{ storage.fields.name }}</option>
+ {% endfor %}
+ </select>
+ </div>
+ </div>
+ {% endif %}
+ {% if brickset_tags | length %}
+ <div class="col-12 flex-grow-1">
+ <label class="visually-hidden" for="grid-tag">Tag</label>
+ <div class="input-group">
+ <span class="input-group-text"><i class="ri-price-tag-2-line"></i><span class="ms-1 d-none d-md-inline"> Tag</span></span>
+ <select id="grid-tag" class="form-select"
+ data-filter="metadata"
+ autocomplete="off">
+ <option value="" selected>All</option>
+ {% for tag in brickset_tags %}
+ <option value="{{ tag.as_dataset() }}">{{ tag.fields.name }}</option>
+ {% endfor %}
+ </select>
+ </div>
+ </div>
+ {% endif %}
+</div>
diff --git a/templates/set/sort.html b/templates/set/sort.html
new file mode 100644
index 00000000..09f31f05
--- /dev/null
+++ b/templates/set/sort.html
@@ -0,0 +1,25 @@
+<div id="grid-sort" class="collapse {% if config['SHOW_GRID_SORT'] %}show{% endif %} row row-cols-lg-auto g-1 justify-content-center align-items-center">
+ <div class="col-12 flex-grow-1">
+ <div class="input-group">
+ <span class="input-group-text mb-2"><i class="ri-sort-asc"></i><span class="ms-1 d-none d-md-inline"> Sort</span></span>
+ <button id="sort-number" type="button" class="btn btn-outline-primary mb-2"
+ data-sort-attribute="number" data-sort-natural="true"><i class="ri-hashtag"></i><span class="d-none d-md-inline"> Set</span></button>
+ <button id="sort-name" type="button" class="btn btn-outline-primary mb-2"
+ data-sort-attribute="name"><i class="ri-pencil-line"></i><span class="d-none d-md-inline"> Name</span></button>
+ <button id="sort-theme" type="button" class="btn btn-outline-primary mb-2"
+ data-sort-attribute="theme"><i class="ri-price-tag-3-line"></i><span class="d-none d-md-inline"> Theme</span></button>
+ <button id="sort-year" type="button" class="btn btn-outline-primary mb-2"
+ data-sort-attribute="year"><i class="ri-calendar-line"></i><span class="d-none d-md-inline"> Year</span></button>
+ <button id="sort-minifigure" type="button" class="btn btn-outline-primary mb-2"
+ data-sort-attribute="minifigures" data-sort-desc="true"><i class="ri-group-line"></i><span class="d-none d-xl-inline"> Figures</span></button>
+ <button id="sort-parts" type="button" class="btn btn-outline-primary mb-2"
+ data-sort-attribute="parts" data-sort-desc="true"><i class="ri-shapes-line"></i><span class="d-none d-xl-inline"> Parts</span></button>
+ <button id="sort-missing" type="button" class="btn btn-outline-primary mb-2"
+ data-sort-attribute="missing" data-sort-desc="true"><i class="ri-question-line"></i><span class="d-none d-xl-inline"> Missing</span></button>
+ <button id="sort-damaged" type="button" class="btn btn-outline-primary mb-2"
+ data-sort-attribute="damaged" data-sort-desc="true"><i class="ri-error-warning-line"></i><span class="d-none d-xl-inline"> Damaged</span></button>
+ <button id="sort-clear" type="button" class="btn btn-outline-dark mb-2"
+ data-sort-clear="true"><i class="ri-close-circle-line"></i><span class="d-none d-xl-inline"> Clear</span></button>
+ </div>
+ </div>
+</div>
diff --git a/templates/sets.html b/templates/sets.html
index 60101b54..5af93c93 100644
--- a/templates/sets.html
+++ b/templates/sets.html
@@ -9,7 +9,7 @@
<div class="col-12 flex-grow-1">
<label class="visually-hidden" for="grid-search">Search</label>
<div class="input-group">
- <span class="input-group-text"><i class="ri-search-line"></i><span class="ms-1 d-none d-xl-inline"> Search</span></span>
+ <span class="input-group-text"><i class="ri-search-line"></i><span class="ms-1 d-none d-md-inline"> Search</span></span>
<input id="grid-search" data-search-exact="name,number,parts,searchStorage,theme,year" data-search-list="searchOwner,searchTag" class="form-control form-control-sm" type="text" placeholder="Set, name, number of parts, theme, year, owner, storage, tag" value="">
</div>
</div>
@@ -28,118 +28,8 @@
</div>
</div>
</div>
- <div id="grid-sort" class="collapse {% if config['SHOW_GRID_SORT'] %}show{% endif %} row row-cols-lg-auto g-1 justify-content-center align-items-center pb-2">
- <div class="col-12 flex-grow-1">
- <div class="input-group">
- <span class="input-group-text"><i class="ri-sort-asc"></i><span class="ms-1 d-none d-xxl-inline"> Sort</span></span>
- <button id="sort-number" type="button" class="btn btn-outline-primary"
- data-sort-attribute="number" data-sort-natural="true"><i class="ri-hashtag"></i><span class="d-none d-xxl-inline"> Set</span></button>
- <button id="sort-name" type="button" class="btn btn-outline-primary"
- data-sort-attribute="name"><i class="ri-pencil-line"></i><span class="d-none d-xxl-inline"> Name</span></button>
- <button id="sort-theme" type="button" class="btn btn-outline-primary"
- data-sort-attribute="theme"><i class="ri-price-tag-3-line"></i><span class="d-none d-xxl-inline"> Theme</span></button>
- <button id="sort-year" type="button" class="btn btn-outline-primary"
- data-sort-attribute="year"><i class="ri-calendar-line"></i><span class="d-none d-xxl-inline"> Year</span></button>
- <button id="sort-minifigure" type="button" class="btn btn-outline-primary"
- data-sort-attribute="minifigures" data-sort-desc="true"><i class="ri-group-line"></i><span class="d-none d-xxl-inline"> Figures</span></button>
- <button id="sort-parts" type="button" class="btn btn-outline-primary"
- data-sort-attribute="parts" data-sort-desc="true"><i class="ri-shapes-line"></i><span class="d-none d-xxl-inline"> Parts</span></button>
- <button id="sort-missing" type="button" class="btn btn-outline-primary"
- data-sort-attribute="missing" data-sort-desc="true"><i class="ri-question-line"></i><span class="d-none d-xxl-inline"> Missing</span></button>
- <button id="sort-damaged" type="button" class="btn btn-outline-primary"
- data-sort-attribute="damaged" data-sort-desc="true"><i class="ri-error-warning-line"></i><span class="d-none d-xxl-inline"> Damaged</span></button>
- <button id="sort-clear" type="button" class="btn btn-outline-dark"
- data-sort-clear="true"><i class="ri-close-circle-line"></i><span class="d-none d-xxl-inline"> Clear</span></button>
- </div>
- </div>
- </div>
- <div id="grid-filter" class="collapse {% if config['SHOW_GRID_FILTERS'] %}show{% endif %} row row-cols-lg-auto g-1 justify-content-center align-items-center pb-2">
- <div class="col-12 flex-grow-1">
- <label class="visually-hidden" for="grid-status">Status</label>
- <div class="input-group">
- <span class="input-group-text"><i class="ri-checkbox-line"></i><span class="ms-1 d-none d-xl-inline"> Status</span></span>
- <select id="grid-status" class="form-select"
- data-filter="metadata"
- autocomplete="off">
- <option value="" selected>All</option>
- <option value="-has-missing">Set is complete</option>
- <option value="has-missing">Set has missing pieces</option>
- <option value="has-damaged">Set has damaged pieces</option>
- <option value="has-missing-instructions">Set has missing instructions</option>
- {% if brickset_storages | length %}
- <option value="has-storage">Is in storage</option>
- <option value="-has-storage">Is NOT in storage</option>
- {% endif %}
- {% for status in brickset_statuses %}
- <option value="{{ status.as_dataset() }}">{{ status.fields.name }}</option>
- <option value="-{{ status.as_dataset() }}">NOT: {{ status.fields.name }}</option>
- {% endfor %}
- </select>
- </div>
- </div>
- <div class="col-12 flex-grow-1">
- <label class="visually-hidden" for="grid-theme">Theme</label>
- <div class="input-group">
- <span class="input-group-text"><i class="ri-price-tag-3-line"></i><span class="ms-1 d-none d-xl-inline"> Theme</span></span>
- <select id="grid-theme" class="form-select"
- data-filter="value" data-filter-attribute="theme"
- autocomplete="off">
- <option value="" selected>All</option>
- {% for theme in collection.themes %}
- <option value="{{ theme | lower }}">{{ theme }}</option>
- {% endfor %}
- </select>
- </div>
- </div>
- {% if brickset_owners | length %}
- <div class="col-12 flex-grow-1">
- <label class="visually-hidden" for="grid-owner">Owner</label>
- <div class="input-group">
- <span class="input-group-text"><i class="ri-user-line"></i><span class="ms-1 d-none d-xl-inline"> Owner</span></span>
- <select id="grid-owner" class="form-select"
- data-filter="metadata"
- autocomplete="off">
- <option value="" selected>All</option>
- {% for owner in brickset_owners %}
- <option value="{{ owner.as_dataset() }}">{{ owner.fields.name }}</option>
- {% endfor %}
- </select>
- </div>
- </div>
- {% endif %}
- {% if brickset_storages | length %}
- <div class="col-12 flex-grow-1">
- <label class="visually-hidden" for="grid-owner">Storage</label>
- <div class="input-group">
- <span class="input-group-text"><i class="ri-archive-2-line"></i><span class="ms-1 d-none d-xl-inline"> Storage</span></span>
- <select id="grid-storage" class="form-select"
- data-filter="value" data-filter-attribute="storage"
- autocomplete="off">
- <option value="" selected>All</option>
- {% for storage in brickset_storages %}
- <option value="{{ storage.fields.id }}">{{ storage.fields.name }}</option>
- {% endfor %}
- </select>
- </div>
- </div>
- {% endif %}
- {% if brickset_tags | length %}
- <div class="col-12 flex-grow-1">
- <label class="visually-hidden" for="grid-tag">Tag</label>
- <div class="input-group">
- <span class="input-group-text"><i class="ri-price-tag-2-line"></i><span class="ms-1 d-none d-xl-inline"> Tag</span></span>
- <select id="grid-tag" class="form-select"
- data-filter="metadata"
- autocomplete="off">
- <option value="" selected>All</option>
- {% for tag in brickset_tags %}
- <option value="{{ tag.as_dataset() }}">{{ tag.fields.name }}</option>
- {% endfor %}
- </select>
- </div>
- </div>
- {% endif %}
- </div>
+ {% include 'set/sort.html' %}
+ {% include 'set/filter.html' %}
<div class="row" data-grid="true" id="grid">
{% for item in collection %}
<div class="col-md-6 col-xl-3 d-flex align-items-stretch">
From b0c7cd7da57abe03c68ba3116e4a083b830b8f8c Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 e1f9071a..ea2be584 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 f47a7ca3..20642066 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 65c33433..8f3b4109 100644
--- a/templates/set/filter.html
+++ b/templates/set/filter.html
@@ -7,10 +7,18 @@
data-filter="metadata"
autocomplete="off">
<option value="" selected>All</option>
- <option value="-has-missing">Set is complete</option>
- <option value="has-missing">Set has missing pieces</option>
- <option value="has-damaged">Set has damaged pieces</option>
- <option value="has-missing-instructions">Set has missing instructions</option>
+ {% if not config['HIDE_TABLE_MISSING_PARTS'] %}
+ <option value="has-missing">Has missing pieces</option>
+ <option value="-has-missing">Has NO missing pieces</option>
+ {% endif %}
+ {% if not config['HIDE_TABLE_DAMAGED_PARTS'] %}
+ <option value="has-damaged">Has damaged pieces</option>
+ <option value="-has-damaged">Has NO damaged pieces</option>
+ {% endif %}
+ {% if not config['HIDE_SET_INSTRUCTIONS'] %}
+ <option value="-has-missing-instructions">Has instructions</option>
+ <option value="has-missing-instructions">Is MISSING instructions</option>
+ {% endif %}
{% if brickset_storages | length %}
<option value="has-storage">Is in storage</option>
<option value="-has-storage">Is NOT in storage</option>
diff --git a/templates/set/sort.html b/templates/set/sort.html
index 09f31f05..3315a1df 100644
--- a/templates/set/sort.html
+++ b/templates/set/sort.html
@@ -14,10 +14,14 @@
data-sort-attribute="minifigures" data-sort-desc="true"><i class="ri-group-line"></i><span class="d-none d-xl-inline"> Figures</span></button>
<button id="sort-parts" type="button" class="btn btn-outline-primary mb-2"
data-sort-attribute="parts" data-sort-desc="true"><i class="ri-shapes-line"></i><span class="d-none d-xl-inline"> Parts</span></button>
+ {% if not config['HIDE_TABLE_MISSING_PARTS'] %}
<button id="sort-missing" type="button" class="btn btn-outline-primary mb-2"
data-sort-attribute="missing" data-sort-desc="true"><i class="ri-question-line"></i><span class="d-none d-xl-inline"> Missing</span></button>
+ {% endif %}
+ {% if not config['HIDE_TABLE_DAMAGED_PARTS'] %}
<button id="sort-damaged" type="button" class="btn btn-outline-primary mb-2"
data-sort-attribute="damaged" data-sort-desc="true"><i class="ri-error-warning-line"></i><span class="d-none d-xl-inline"> Damaged</span></button>
+ {% endif %}
<button id="sort-clear" type="button" class="btn btn-outline-dark mb-2"
data-sort-clear="true"><i class="ri-close-circle-line"></i><span class="d-none d-xl-inline"> Clear</span></button>
</div>
From 82b744334f577f14ab11dbfea9e9b45a80b73492 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 6c3b9283..deaf269b 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 fb11efe4..db4671e8 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 b64775bf..1bf7c22e 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 99587287..7123e4a7 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 b2a9eedd..fc800c4e 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 8983cf94..de57cd30 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 e41e97a4..7d5ba3f0 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 <versatile.mailbox@gmail.com>
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 07545f92..0b2f61cd 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 30c559c9..08c2429d 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 7cc40d6f..b758f08c 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 de57cd30..66a2a506 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 <versatile.mailbox@gmail.com>
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 584a359f..bb6a5e60 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 99fbe5e7..997348cd 100644
--- a/templates/admin/instructions.html
+++ b/templates/admin/instructions.html
@@ -6,25 +6,27 @@
The instructions files folder is: <code>{{ config['INSTRUCTIONS_FOLDER'] }}</code>. <br>
Allowed file formats for instructions are the following: <code>{{ ', '.join(config['INSTRUCTIONS_ALLOWED_EXTENSIONS']) }}</code>.
</p>
-<h5 class="border-bottom">Counters</h5>
-<p>
- <div class="d-flex justify-content-start">
- <ul class="list-group">
- <li class="list-group-item d-flex justify-content-between align-items-start">
- <span><i class="ri-hashtag"></i> Sets</span> <span class="badge text-bg-primary rounded-pill ms-2">{{ instructions.sets | length }}</span>
- </li>
- <li class="list-group-item d-flex justify-content-between align-items-start">
- <span><i class="ri-file-line"></i> Instructions for sets</span> <span class="badge text-bg-primary rounded-pill ms-2">{{ instructions.sets_total }}</span>
- </li>
- <li class="list-group-item d-flex justify-content-between align-items-start">
- <span><i class="ri-question-line"></i> Unknown</span> <span class="badge text-bg-primary rounded-pill ms-2">{{ instructions.unknown_total }}</span>
- </li>
- <li class="list-group-item d-flex justify-content-between align-items-start">
- <span><i class="ri-prohibited-line"></i> Rejected files</span> <span class="badge text-bg-primary rounded-pill ms-2">{{ instructions.rejected_total }}</span>
- </li>
- </ul>
- </div>
-</p>
+{% if instructions %}
+ <h5 class="border-bottom">Counters</h5>
+ <p>
+ <div class="d-flex justify-content-start">
+ <ul class="list-group">
+ <li class="list-group-item d-flex justify-content-between align-items-start">
+ <span><i class="ri-hashtag"></i> Sets</span> <span class="badge text-bg-primary rounded-pill ms-2">{{ instructions.sets | length }}</span>
+ </li>
+ <li class="list-group-item d-flex justify-content-between align-items-start">
+ <span><i class="ri-file-line"></i> Instructions for sets</span> <span class="badge text-bg-primary rounded-pill ms-2">{{ instructions.sets_total }}</span>
+ </li>
+ <li class="list-group-item d-flex justify-content-between align-items-start">
+ <span><i class="ri-question-line"></i> Unknown</span> <span class="badge text-bg-primary rounded-pill ms-2">{{ instructions.unknown_total }}</span>
+ </li>
+ <li class="list-group-item d-flex justify-content-between align-items-start">
+ <span><i class="ri-prohibited-line"></i> Rejected files</span> <span class="badge text-bg-primary rounded-pill ms-2">{{ instructions.rejected_total }}</span>
+ </li>
+ </ul>
+ </div>
+ </p>
+{% endif %}
<h5 class="border-bottom">Refresh</h5>
<p>
<a href="{{ url_for('admin_instructions.refresh') }}" class="btn btn-primary" role="button"><i class="ri-refresh-line"></i> Refresh the instructions cache</a>
From 584389e2051557392b55ad00358adb7f1a203e34 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 d14491e5..44d52fec 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 <versatile.mailbox@gmail.com>
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 60bfb5f1..3b98a132 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 8453f366..77be7164 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 66a2a506..7f397da7 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('/<id>/storage', methods=['POST'])
@login_required
@exception_handler(__file__, json=True)
diff --git a/templates/macro/form.html b/templates/macro/form.html
index 6fb38902..45ec2b5d 100644
--- a/templates/macro/form.html
+++ b/templates/macro/form.html
@@ -48,7 +48,7 @@
{% if icon %}<span class="input-group-text"><i class="ri-{{ icon }}"></i></span>{% endif %}
<select id="{{ prefix }}-{{ item.fields.id }}" class="form-select"
{% if not delete %}
- data-changer-id="{{ item.fields.id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ metadata_list.url_for_set_state(item.fields.id) }}"
+ data-changer-id="{{ item.fields.id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ metadata_list.url_for_set_value(item.fields.id) }}"
{% else %}
disabled
{% endif %}
From 7f684c5f02c15c57bf3f878ef5d1d423bf54110b Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Tue, 4 Feb 2025 12:34:46 +0100
Subject: [PATCH 137/154] Fix improper open flag
---
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 54b2e879..26959d3a 100644
--- a/templates/set/management.html
+++ b/templates/set/management.html
@@ -20,7 +20,7 @@
<p class="text-center"><i class="ri-error-warning-line"></i> No storage found.</p>
{% endif %}
<hr>
- <a href="{{ url_for('admin.admin', open_tag=true) }}" class="btn btn-primary" role="button"><i class="ri-settings-4-line"></i> Manage the set storages</a>
+ <a href="{{ url_for('admin.admin', open_storage=true) }}" class="btn btn-primary" role="button"><i class="ri-settings-4-line"></i> Manage the set storages</a>
{{ accordion.footer() }}
{{ accordion.header('Tags', 'tag', 'set-details', icon='price-tag-2-line', class='p-0') }}
<ul class="list-group list-group-flush">
From e7bfa66512dafa07ae1e246d6afb149e60036cd2 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Tue, 4 Feb 2025 12:40:49 +0100
Subject: [PATCH 138/154] Put set metadata in nested accordion to reduce
footprint
---
bricktracker/views/admin/admin.py | 13 +++++++++----
templates/admin.html | 4 ++++
templates/admin/owner.html | 2 +-
templates/admin/status.html | 2 +-
templates/admin/storage.html | 2 +-
templates/admin/tag.html | 2 +-
templates/set/management.html | 11 ++++++-----
7 files changed, 23 insertions(+), 13 deletions(-)
diff --git a/bricktracker/views/admin/admin.py b/bricktracker/views/admin/admin.py
index bb6a5e60..08cdafcb 100644
--- a/bricktracker/views/admin/admin.py
+++ b/bricktracker/views/admin/admin.py
@@ -87,15 +87,19 @@ def admin() -> str:
open_tag = request.args.get('open_tag', None)
open_theme = request.args.get('open_theme', None)
+ open_metadata = (
+ open_owner or
+ open_status or
+ open_storage or
+ open_tag
+ )
+
open_database = (
open_image is None and
open_instructions is None and
open_logout is None and
- open_owner is None and
+ not open_metadata 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
)
@@ -120,6 +124,7 @@ def admin() -> str:
open_image=open_image,
open_instructions=open_instructions,
open_logout=open_logout,
+ open_metadata=open_metadata,
open_owner=open_owner,
open_retired=open_retired,
open_status=open_status,
diff --git a/templates/admin.html b/templates/admin.html
index 3582c7b6..22a7e1a7 100644
--- a/templates/admin.html
+++ b/templates/admin.html
@@ -1,3 +1,5 @@
+{% import 'macro/accordion.html' as accordion %}
+
{% extends 'base.html' %}
{% block title %} - Administration{% endblock %}
@@ -36,10 +38,12 @@
{% endif %}
{% include 'admin/theme.html' %}
{% include 'admin/retired.html' %}
+ {{ accordion.header('Set metadata', 'metadata', 'admin', expanded=open_metadata, icon='profile-line', class='p-0') }}
{% include 'admin/owner.html' %}
{% include 'admin/status.html' %}
{% include 'admin/storage.html' %}
{% include 'admin/tag.html' %}
+ {{ accordion.footer() }}
{% include 'admin/database.html' %}
{% include 'admin/configuration.html' %}
{% endif %}
diff --git a/templates/admin/owner.html b/templates/admin/owner.html
index 8c7c2619..351eaa51 100644
--- a/templates/admin/owner.html
+++ b/templates/admin/owner.html
@@ -1,6 +1,6 @@
{% import 'macro/accordion.html' as accordion %}
-{{ accordion.header('Set owners', 'owner', 'admin', expanded=open_owner, icon='user-line', class='p-0') }}
+{{ accordion.header('Set owners', 'owner', 'metadata', expanded=open_owner, icon='user-line', class='p-0') }}
{% if owner_error %}<div class="alert alert-danger m-2" role="alert"><strong>Error:</strong> {{ owner_error }}.</div>{% endif %}
<ul class="list-group list-group-flush">
{% if metadata_owners | length %}
diff --git a/templates/admin/status.html b/templates/admin/status.html
index 609bc68d..0377a4c3 100644
--- a/templates/admin/status.html
+++ b/templates/admin/status.html
@@ -1,6 +1,6 @@
{% import 'macro/accordion.html' as accordion %}
-{{ accordion.header('Set statuses', 'status', 'admin', expanded=open_status, icon='checkbox-line', class='p-0') }}
+{{ accordion.header('Set statuses', 'status', 'metadata', expanded=open_status, icon='checkbox-line', class='p-0') }}
{% if status_error %}<div class="alert alert-danger m-2" role="alert"><strong>Error:</strong> {{ status_error }}.</div>{% endif %}
<ul class="list-group list-group-flush">
{% if metadata_statuses | length %}
diff --git a/templates/admin/storage.html b/templates/admin/storage.html
index 1f317e6c..ddd3e4b4 100644
--- a/templates/admin/storage.html
+++ b/templates/admin/storage.html
@@ -1,6 +1,6 @@
{% import 'macro/accordion.html' as accordion %}
-{{ accordion.header('Set storages', 'storage', 'admin', expanded=open_storage, icon='archive-2-line', class='p-0') }}
+{{ accordion.header('Set storages', 'storage', 'metadata', expanded=open_storage, icon='archive-2-line', class='p-0') }}
{% if storage_error %}<div class="alert alert-danger m-2" role="alert"><strong>Error:</strong> {{ storage_error }}.</div>{% endif %}
<ul class="list-group list-group-flush">
{% if metadata_storages | length %}
diff --git a/templates/admin/tag.html b/templates/admin/tag.html
index c3ca1f61..30bfa711 100644
--- a/templates/admin/tag.html
+++ b/templates/admin/tag.html
@@ -1,6 +1,6 @@
{% import 'macro/accordion.html' as accordion %}
-{{ accordion.header('Set tags', 'tag', 'admin', expanded=open_tag, icon='price-tag-2-line', class='p-0') }}
+{{ accordion.header('Set tags', 'tag', 'metadata', expanded=open_tag, icon='price-tag-2-line', class='p-0') }}
{% if tag_error %}<div class="alert alert-danger m-2" role="alert"><strong>Error:</strong> {{ tag_error }}.</div>{% endif %}
<ul class="list-group list-group-flush">
{% if metadata_tags | length %}
diff --git a/templates/set/management.html b/templates/set/management.html
index 26959d3a..1a8c2b7b 100644
--- a/templates/set/management.html
+++ b/templates/set/management.html
@@ -1,5 +1,6 @@
{% if g.login.is_authenticated() %}
- {{ accordion.header('Owners', 'owner', 'set-details', icon='group-line', class='p-0') }}
+ {{ 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') }}
<ul class="list-group list-group-flush">
{% if brickset_owners | length %}
{% for owner in brickset_owners %}
@@ -13,7 +14,7 @@
<a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_owner=true) }}"><i class="ri-settings-4-line"></i> Manage the set owners</a>
</div>
{{ accordion.footer() }}
- {{ accordion.header('Storage', 'storage', 'set-details', icon='archive-2-line') }}
+ {{ accordion.header('Storage', 'storage', 'set-management', icon='archive-2-line') }}
{% if brickset_storages | length %}
{{ form.select('Storage', item, 'storage', brickset_storages, delete=delete) }}
{% else %}
@@ -22,7 +23,7 @@
<hr>
<a href="{{ url_for('admin.admin', open_storage=true) }}" class="btn btn-primary" role="button"><i class="ri-settings-4-line"></i> Manage the set storages</a>
{{ accordion.footer() }}
- {{ accordion.header('Tags', 'tag', 'set-details', icon='price-tag-2-line', class='p-0') }}
+ {{ accordion.header('Tags', 'tag', 'set-management', icon='price-tag-2-line', class='p-0') }}
<ul class="list-group list-group-flush">
{% if brickset_tags | length %}
{% for tag in brickset_tags %}
@@ -36,8 +37,8 @@
<a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_tag=true) }}"><i class="ri-settings-4-line"></i> Manage the set tags</a>
</div>
{{ accordion.footer() }}
- {{ accordion.header('Management', 'management', 'set-details', icon='settings-4-line') }}
- <h5 class="border-bottom">Data</h5>
+ {{ accordion.header('Data', 'data', 'set-management', icon='database-2-line') }}
<a href="{{ item.url_for_refresh() }}" class="btn btn-primary" role="button"><i class="ri-refresh-line"></i> Refresh the set data</a>
+ {{ accordion.footer() }}
{{ accordion.footer() }}
{% endif %}
From 195f18f141a4ff5dd6c3d10d7109b22f2b9c7f26 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Tue, 4 Feb 2025 12:52:18 +0100
Subject: [PATCH 139/154] Set purchase location
---
.env.sample | 6 ++
CHANGELOG.md | 5 +-
bricktracker/app.py | 2 +
bricktracker/config.py | 1 +
bricktracker/metadata.py | 6 +-
bricktracker/metadata_list.py | 10 ++-
bricktracker/record_list.py | 2 +
bricktracker/reload.py | 4 +
bricktracker/set.py | 8 ++
bricktracker/set_list.py | 5 ++
bricktracker/set_purchase_location.py | 13 +++
bricktracker/set_purchase_location_list.py | 42 ++++++++++
bricktracker/sql/set/base/base.sql | 1 +
bricktracker/sql/set/insert.sql | 6 +-
.../set/metadata/purchase_location/base.sql | 6 ++
.../set/metadata/purchase_location/delete.sql | 10 +++
.../set/metadata/purchase_location/insert.sql | 11 +++
.../set/metadata/purchase_location/list.sql | 1 +
.../set/metadata/purchase_location/select.sql | 5 ++
.../purchase_location/update/field.sql | 3 +
.../purchase_location/update/value.sql | 3 +
bricktracker/views/admin/admin.py | 9 ++
bricktracker/views/admin/purchase_location.py | 84 +++++++++++++++++++
bricktracker/views/set.py | 20 +++++
static/scripts/socket/set.js | 12 +++
templates/add.html | 15 +++-
templates/admin.html | 11 ++-
templates/admin/purchase_location.html | 42 ++++++++++
templates/admin/purchase_location/delete.html | 19 +++++
templates/macro/badge.html | 12 +++
templates/set/card.html | 6 ++
templates/set/filter.html | 16 ++++
templates/set/management.html | 77 +++++++++--------
templates/sets.html | 2 +-
34 files changed, 427 insertions(+), 48 deletions(-)
create mode 100644 bricktracker/set_purchase_location.py
create mode 100644 bricktracker/set_purchase_location_list.py
create mode 100644 bricktracker/sql/set/metadata/purchase_location/base.sql
create mode 100644 bricktracker/sql/set/metadata/purchase_location/delete.sql
create mode 100644 bricktracker/sql/set/metadata/purchase_location/insert.sql
create mode 100644 bricktracker/sql/set/metadata/purchase_location/list.sql
create mode 100644 bricktracker/sql/set/metadata/purchase_location/select.sql
create mode 100644 bricktracker/sql/set/metadata/purchase_location/update/field.sql
create mode 100644 bricktracker/sql/set/metadata/purchase_location/update/value.sql
create mode 100644 bricktracker/views/admin/purchase_location.py
create mode 100644 templates/admin/purchase_location.html
create mode 100644 templates/admin/purchase_location/delete.html
diff --git a/.env.sample b/.env.sample
index 44d52fec..8f129392 100644
--- a/.env.sample
+++ b/.env.sample
@@ -168,6 +168,12 @@
# Default: 3333
# BK_PORT=3333
+# 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
+# Default: "bricktracker_metadata_purchase_locations"."name" ASC
+# BK_PURCHASE_LOCATION_DEFAULT_ORDER="bricktracker_metadata_purchase_locations"."name" ASC
+
# Optional: Shuffle the lists on the front page.
# Default: false
# Legacy name: RANDOM
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b50f3427..efcdb16d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,7 +14,8 @@
- 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
+- Added: `BK_STORAGE_DEFAULT_ORDER`, ordering of storages
+- Added: `BK_PURCHASE_LOCATION_DEFAULT_ORDER`, ordering of purchase locations
### Code
@@ -39,6 +40,7 @@
- Ownership
- Tags
- Storage
+ - Purchase location
- Storage
- Storage content and list
@@ -85,6 +87,7 @@
- Tags
- Refresh
- Storage
+ - Purchase location
- Sets grid
- Collapsible controls depending on screen size
diff --git a/bricktracker/app.py b/bricktracker/app.py
index af005d96..f0afe429 100644
--- a/bricktracker/app.py
+++ b/bricktracker/app.py
@@ -17,6 +17,7 @@ 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.purchase_location import admin_purchase_location_page # noqa: E501
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
@@ -88,6 +89,7 @@ def setup_app(app: Flask) -> None:
app.register_blueprint(admin_instructions_page)
app.register_blueprint(admin_retired_page)
app.register_blueprint(admin_owner_page)
+ app.register_blueprint(admin_purchase_location_page)
app.register_blueprint(admin_status_page)
app.register_blueprint(admin_storage_page)
app.register_blueprint(admin_tag_page)
diff --git a/bricktracker/config.py b/bricktracker/config.py
index 5b9788fb..729049a9 100644
--- a/bricktracker/config.py
+++ b/bricktracker/config.py
@@ -41,6 +41,7 @@ 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_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': ''},
{'n': 'REBRICKABLE_IMAGE_NIL', 'd': 'https://rebrickable.com/static/img/nil.png'}, # noqa: E501
diff --git a/bricktracker/metadata.py b/bricktracker/metadata.py
index 0b2f61cd..88d26235 100644
--- a/bricktracker/metadata.py
+++ b/bricktracker/metadata.py
@@ -48,7 +48,7 @@ class BrickMetadata(BrickRecord):
def as_column(self, /) -> str:
return '{kind}_{id}'.format(
id=self.fields.id,
- kind=self.kind.lower()
+ kind=self.kind.lower().replace(' ', '-')
)
# HTML dataset name
@@ -90,8 +90,6 @@ class BrickMetadata(BrickRecord):
# Rename the entry
def rename(self, /) -> None:
- self.safe()
-
self.update_field('name', value=self.fields.name)
# Make the name "safe"
@@ -159,7 +157,7 @@ class BrickMetadata(BrickRecord):
)
if rows != 1:
- raise DatabaseException('Could not update the field "{field}" for {kind} {name} ({id})'.format( # noqa: E501
+ raise DatabaseException('Could not update the field "{field}" for {kind} "{name}" ({id})'.format( # noqa: E501
field=field,
kind=self.kind,
name=self.fields.name,
diff --git a/bricktracker/metadata_list.py b/bricktracker/metadata_list.py
index 3b98a132..71245483 100644
--- a/bricktracker/metadata_list.py
+++ b/bricktracker/metadata_list.py
@@ -7,13 +7,21 @@ from .exceptions import NotFoundException
from .fields import BrickRecordFields
from .record_list import BrickRecordList
from .set_owner import BrickSetOwner
+from .set_purchase_location import BrickSetPurchaseLocation
from .set_status import BrickSetStatus
from .set_storage import BrickSetStorage
from .set_tag import BrickSetTag
logger = logging.getLogger(__name__)
-T = TypeVar('T', BrickSetOwner, BrickSetStatus, BrickSetStorage, BrickSetTag)
+T = TypeVar(
+ 'T',
+ BrickSetOwner,
+ BrickSetPurchaseLocation,
+ BrickSetStatus,
+ BrickSetStorage,
+ BrickSetTag
+)
# Lego sets metadata list
diff --git a/bricktracker/record_list.py b/bricktracker/record_list.py
index 23da29bc..18dcb10c 100644
--- a/bricktracker/record_list.py
+++ b/bricktracker/record_list.py
@@ -9,6 +9,7 @@ if TYPE_CHECKING:
from .rebrickable_set import RebrickableSet
from .set import BrickSet
from .set_owner import BrickSetOwner
+ from .set_purchase_location import BrickSetPurchaseLocation
from .set_status import BrickSetStatus
from .set_storage import BrickSetStorage
from .set_tag import BrickSetTag
@@ -20,6 +21,7 @@ T = TypeVar(
'BrickPart',
'BrickSet',
'BrickSetOwner',
+ 'BrickSetPurchaseLocation',
'BrickSetStatus',
'BrickSetStorage',
'BrickSetTag',
diff --git a/bricktracker/reload.py b/bricktracker/reload.py
index b2247ea4..38929f68 100644
--- a/bricktracker/reload.py
+++ b/bricktracker/reload.py
@@ -1,6 +1,7 @@
from .instructions_list import BrickInstructionsList
from .retired_list import BrickRetiredList
from .set_owner_list import BrickSetOwnerList
+from .set_purchase_location_list import BrickSetPurchaseLocationList
from .set_status_list import BrickSetStatusList
from .set_storage_list import BrickSetStorageList
from .set_tag_list import BrickSetTagList
@@ -17,6 +18,9 @@ def reload() -> None:
# Reload the set owners
BrickSetOwnerList.new(force=True)
+ # Reload the set purchase locations
+ BrickSetPurchaseLocationList.new(force=True)
+
# Reload the set statuses
BrickSetStatusList.new(force=True)
diff --git a/bricktracker/set.py b/bricktracker/set.py
index 6368d40c..fb972092 100644
--- a/bricktracker/set.py
+++ b/bricktracker/set.py
@@ -10,6 +10,7 @@ from .minifigure_list import BrickMinifigureList
from .part_list import BrickPartList
from .rebrickable_set import RebrickableSet
from .set_owner_list import BrickSetOwnerList
+from .set_purchase_location_list import BrickSetPurchaseLocationList
from .set_status_list import BrickSetStatusList
from .set_storage_list import BrickSetStorageList
from .set_tag_list import BrickSetTagList
@@ -63,6 +64,13 @@ class BrickSet(RebrickableSet):
)
self.fields.storage = storage.fields.id
+ # Save the purchase location
+ purchase_location = BrickSetPurchaseLocationList.get(
+ data.get('purchase_location', ''),
+ allow_none=True
+ )
+ self.fields.purchase_location = purchase_location.fields.id
+
# Insert into database
self.insert(commit=False)
diff --git a/bricktracker/set_list.py b/bricktracker/set_list.py
index deaf269b..a8f5faa4 100644
--- a/bricktracker/set_list.py
+++ b/bricktracker/set_list.py
@@ -5,6 +5,8 @@ from flask import current_app
from .record_list import BrickRecordList
from .set_owner import BrickSetOwner
from .set_owner_list import BrickSetOwnerList
+from .set_purchase_location import BrickSetPurchaseLocation
+from .set_purchase_location_list import BrickSetPurchaseLocationList
from .set_status_list import BrickSetStatusList
from .set_storage import BrickSetStorage
from .set_storage_list import BrickSetStorageList
@@ -175,6 +177,8 @@ def set_metadata_lists(
str,
Union[
list[BrickSetOwner],
+ list[BrickSetPurchaseLocation],
+ BrickSetPurchaseLocation,
list[BrickSetStorage],
BrickSetStorageList,
list[BrickSetTag]
@@ -182,6 +186,7 @@ def set_metadata_lists(
]:
return {
'brickset_owners': BrickSetOwnerList.list(),
+ 'brickset_purchase_locations': BrickSetPurchaseLocationList.list(as_class=as_class), # noqa: E501
'brickset_storages': BrickSetStorageList.list(as_class=as_class),
'brickset_tags': BrickSetTagList.list(),
}
diff --git a/bricktracker/set_purchase_location.py b/bricktracker/set_purchase_location.py
new file mode 100644
index 00000000..801ccf82
--- /dev/null
+++ b/bricktracker/set_purchase_location.py
@@ -0,0 +1,13 @@
+from .metadata import BrickMetadata
+
+
+# Lego set purchase location metadata
+class BrickSetPurchaseLocation(BrickMetadata):
+ kind: str = 'purchase location'
+
+ # Queries
+ delete_query: str = 'set/metadata/purchase_location/delete'
+ insert_query: str = 'set/metadata/purchase_location/insert'
+ select_query: str = 'set/metadata/purchase_location/select'
+ update_field_query: str = 'set/metadata/purchase_location/update/field'
+ update_set_value_query: str = 'set/metadata/purchase_location/update/value'
diff --git a/bricktracker/set_purchase_location_list.py b/bricktracker/set_purchase_location_list.py
new file mode 100644
index 00000000..3ffae4bc
--- /dev/null
+++ b/bricktracker/set_purchase_location_list.py
@@ -0,0 +1,42 @@
+import logging
+from typing import Self
+
+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(
+ BrickMetadataList[BrickSetPurchaseLocation]
+):
+ kind: str = 'set purchase locations'
+
+ # Queries
+ select_query = 'set/metadata/purchase_location/list'
+ all_query = 'set/metadata/purchase_location/all'
+
+ # Set value endpoint
+ set_value_endpoint: str = 'set.update_purchase_location'
+
+ # Load all purchase locations
+ @classmethod
+ def all(cls, /) -> Self:
+ new = cls.new()
+ new.override()
+
+ for record in new.select(
+ override_query=cls.all_query,
+ order=current_app.config['PURCHASE_LOCATION_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:
+ return cls(BrickSetPurchaseLocation, force=force)
diff --git a/bricktracker/sql/set/base/base.sql b/bricktracker/sql/set/base/base.sql
index fbc86e0d..333868d0 100644
--- a/bricktracker/sql/set/base/base.sql
+++ b/bricktracker/sql/set/base/base.sql
@@ -1,6 +1,7 @@
SELECT
{% block id %}{% endblock %}
"bricktracker_sets"."storage",
+ "bricktracker_sets"."purchase_location",
"rebrickable_sets"."set",
"rebrickable_sets"."number",
"rebrickable_sets"."version",
diff --git a/bricktracker/sql/set/insert.sql b/bricktracker/sql/set/insert.sql
index 9a46f88b..bc933c33 100644
--- a/bricktracker/sql/set/insert.sql
+++ b/bricktracker/sql/set/insert.sql
@@ -1,9 +1,11 @@
INSERT OR IGNORE INTO "bricktracker_sets" (
"id",
"set",
- "storage"
+ "storage",
+ "purchase_location"
) VALUES (
:id,
:set,
- :storage
+ :storage,
+ :purchase_location
)
diff --git a/bricktracker/sql/set/metadata/purchase_location/base.sql b/bricktracker/sql/set/metadata/purchase_location/base.sql
new file mode 100644
index 00000000..8ac33ca1
--- /dev/null
+++ b/bricktracker/sql/set/metadata/purchase_location/base.sql
@@ -0,0 +1,6 @@
+SELECT
+ "bricktracker_metadata_purchase_locations"."id",
+ "bricktracker_metadata_purchase_locations"."name"
+FROM "bricktracker_metadata_purchase_locations"
+
+{% block where %}{% endblock %}
diff --git a/bricktracker/sql/set/metadata/purchase_location/delete.sql b/bricktracker/sql/set/metadata/purchase_location/delete.sql
new file mode 100644
index 00000000..489dfd07
--- /dev/null
+++ b/bricktracker/sql/set/metadata/purchase_location/delete.sql
@@ -0,0 +1,10 @@
+BEGIN TRANSACTION;
+
+DELETE FROM "bricktracker_metadata_purchase_locations"
+WHERE "bricktracker_metadata_purchase_locations"."id" IS NOT DISTINCT FROM '{{ id }}';
+
+UPDATE "bricktracker_sets"
+SET "purchase_location" = NULL
+WHERE "bricktracker_sets"."purchase_location" IS NOT DISTINCT FROM '{{ id }}';
+
+COMMIT;
\ No newline at end of file
diff --git a/bricktracker/sql/set/metadata/purchase_location/insert.sql b/bricktracker/sql/set/metadata/purchase_location/insert.sql
new file mode 100644
index 00000000..22fc5879
--- /dev/null
+++ b/bricktracker/sql/set/metadata/purchase_location/insert.sql
@@ -0,0 +1,11 @@
+BEGIN TRANSACTION;
+
+INSERT INTO "bricktracker_metadata_purchase_locations" (
+ "id",
+ "name"
+) VALUES (
+ '{{ id }}',
+ '{{ name }}'
+);
+
+COMMIT;
\ No newline at end of file
diff --git a/bricktracker/sql/set/metadata/purchase_location/list.sql b/bricktracker/sql/set/metadata/purchase_location/list.sql
new file mode 100644
index 00000000..2a0813b5
--- /dev/null
+++ b/bricktracker/sql/set/metadata/purchase_location/list.sql
@@ -0,0 +1 @@
+{% extends 'set/metadata/purchase_location/base.sql' %}
diff --git a/bricktracker/sql/set/metadata/purchase_location/select.sql b/bricktracker/sql/set/metadata/purchase_location/select.sql
new file mode 100644
index 00000000..a9e6161f
--- /dev/null
+++ b/bricktracker/sql/set/metadata/purchase_location/select.sql
@@ -0,0 +1,5 @@
+{% extends 'set/metadata/purchase_location/base.sql' %}
+
+{% block where %}
+WHERE "bricktracker_metadata_purchase_locations"."id" IS NOT DISTINCT FROM :id
+{% endblock %}
\ No newline at end of file
diff --git a/bricktracker/sql/set/metadata/purchase_location/update/field.sql b/bricktracker/sql/set/metadata/purchase_location/update/field.sql
new file mode 100644
index 00000000..323d98d1
--- /dev/null
+++ b/bricktracker/sql/set/metadata/purchase_location/update/field.sql
@@ -0,0 +1,3 @@
+UPDATE "bricktracker_metadata_purchase_locations"
+SET "{{field}}" = :value
+WHERE "bricktracker_metadata_purchase_locations"."id" IS NOT DISTINCT FROM :id
diff --git a/bricktracker/sql/set/metadata/purchase_location/update/value.sql b/bricktracker/sql/set/metadata/purchase_location/update/value.sql
new file mode 100644
index 00000000..d27469e1
--- /dev/null
+++ b/bricktracker/sql/set/metadata/purchase_location/update/value.sql
@@ -0,0 +1,3 @@
+UPDATE "bricktracker_sets"
+SET "purchase_location" = :value
+WHERE "bricktracker_sets"."id" IS NOT DISTINCT FROM :set_id
diff --git a/bricktracker/views/admin/admin.py b/bricktracker/views/admin/admin.py
index 08cdafcb..749b3df2 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_purchase_location import BrickSetPurchaseLocation
+from ...set_purchase_location_list import BrickSetPurchaseLocationList
from ...set_storage import BrickSetStorage
from ...set_storage_list import BrickSetStorageList
from ...set_status import BrickSetStatus
@@ -36,6 +38,7 @@ def admin() -> str:
database_version: int = -1
instructions: BrickInstructionsList | None = None
metadata_owners: list[BrickSetOwner] = []
+ metadata_purchase_locations: list[BrickSetPurchaseLocation] = []
metadata_statuses: list[BrickSetStatus] = []
metadata_storages: list[BrickSetStorage] = []
metadata_tags: list[BrickSetTag] = []
@@ -54,6 +57,7 @@ def admin() -> str:
instructions = BrickInstructionsList()
metadata_owners = BrickSetOwnerList.list()
+ metadata_purchase_locations = BrickSetPurchaseLocationList.list()
metadata_statuses = BrickSetStatusList.list(all=True)
metadata_storages = BrickSetStorageList.list()
metadata_tags = BrickSetTagList.list()
@@ -81,6 +85,7 @@ def admin() -> str:
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_purchase_location = request.args.get('open_purchase_location', 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)
@@ -89,6 +94,7 @@ def admin() -> str:
open_metadata = (
open_owner or
+ open_purchase_location or
open_status or
open_storage or
open_tag
@@ -113,6 +119,7 @@ def admin() -> str:
database_version=database_version,
instructions=instructions,
metadata_owners=metadata_owners,
+ metadata_purchase_locations=metadata_purchase_locations,
metadata_statuses=metadata_statuses,
metadata_storages=metadata_storages,
metadata_tags=metadata_tags,
@@ -126,12 +133,14 @@ def admin() -> str:
open_logout=open_logout,
open_metadata=open_metadata,
open_owner=open_owner,
+ open_purchase_location=open_purchase_location,
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'),
+ purchase_location_error=request.args.get('purchase_location_error'),
retired=BrickRetiredList(),
status_error=request.args.get('status_error'),
storage_error=request.args.get('storage_error'),
diff --git a/bricktracker/views/admin/purchase_location.py b/bricktracker/views/admin/purchase_location.py
new file mode 100644
index 00000000..48e7b7d5
--- /dev/null
+++ b/bricktracker/views/admin/purchase_location.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_purchase_location import BrickSetPurchaseLocation
+
+admin_purchase_location_page = Blueprint(
+ 'admin_purchase_location',
+ __name__,
+ url_prefix='/admin/purchase_location'
+)
+
+
+# Add a metadata purchase location
+@admin_purchase_location_page.route('/add', methods=['POST'])
+@login_required
+@exception_handler(
+ __file__,
+ post_redirect='admin.admin',
+ error_name='purchase_location_error',
+ open_purchase_location=True
+)
+def add() -> Response:
+ BrickSetPurchaseLocation().from_form(request.form).insert()
+
+ reload()
+
+ return redirect(url_for('admin.admin', open_purchase_location=True))
+
+
+# Delete the metadata purchase location
+@admin_purchase_location_page.route('<id>/delete', methods=['GET'])
+@login_required
+@exception_handler(__file__)
+def delete(*, id: str) -> str:
+ return render_template(
+ 'admin.html',
+ delete_purchase_location=True,
+ purchase_location=BrickSetPurchaseLocation().select_specific(id),
+ error=request.args.get('purchase_location_error')
+ )
+
+
+# Actually delete the metadata purchase location
+@admin_purchase_location_page.route('<id>/delete', methods=['POST'])
+@login_required
+@exception_handler(
+ __file__,
+ post_redirect='admin_purchase_location.delete',
+ error_name='purchase_location_error'
+)
+def do_delete(*, id: str) -> Response:
+ purchase_location = BrickSetPurchaseLocation().select_specific(id)
+ purchase_location.delete()
+
+ reload()
+
+ return redirect(url_for('admin.admin', open_purchase_location=True))
+
+
+# Rename the metadata purchase location
+@admin_purchase_location_page.route('<id>/rename', methods=['POST'])
+@login_required
+@exception_handler(
+ __file__,
+ post_redirect='admin.admin',
+ error_name='purchase_location_error',
+ open_purchase_location=True
+)
+def rename(*, id: str) -> Response:
+ purchase_location = BrickSetPurchaseLocation().select_specific(id)
+ purchase_location.from_form(request.form).rename()
+
+ reload()
+
+ return redirect(url_for('admin.admin', open_purchase_location=True))
diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py
index 7f397da7..0777f4e9 100644
--- a/bricktracker/views/set.py
+++ b/bricktracker/views/set.py
@@ -18,6 +18,7 @@ from ..part import BrickPart
from ..set import BrickSet
from ..set_list import BrickSetList, set_metadata_lists
from ..set_owner_list import BrickSetOwnerList
+from ..set_purchase_location_list import BrickSetPurchaseLocationList
from ..set_status_list import BrickSetStatusList
from ..set_storage_list import BrickSetStorageList
from ..set_tag_list import BrickSetTagList
@@ -40,6 +41,25 @@ def list() -> str:
)
+# Change the value of purchase location
+@set_page.route('/<id>/purchase_location', methods=['POST'])
+@login_required
+@exception_handler(__file__, json=True)
+def update_purchase_location(*, id: str) -> Response:
+ brickset = BrickSet().select_light(id)
+ purchase_location = BrickSetPurchaseLocationList.get(
+ request.json.get('value', ''), # type: ignore
+ allow_none=True
+ )
+
+ value = purchase_location.update_set_value(
+ brickset,
+ value=purchase_location.fields.id
+ )
+
+ return jsonify({'value': value})
+
+
# Change the state of a owner
@set_page.route('/<id>/owner/<metadata_id>', methods=['POST'])
@login_required
diff --git a/static/scripts/socket/set.js b/static/scripts/socket/set.js
index 311eac50..8a4e5bba 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_purchase_location = document.getElementById(`${id}-purchase-location`);
this.html_storage = document.getElementById(`${id}-storage`);
this.html_tags = document.getElementById(`${id}-tags`);
@@ -152,6 +153,12 @@ class BrickSetSocket extends BrickSocket {
});
}
+ // Grab the purchase location
+ let purchase_location = null;
+ if (this.html_purchase_location) {
+ purchase_location = this.html_purchase_location.value;
+ }
+
// Grab the storage
let storage = null;
if (this.html_storage) {
@@ -177,6 +184,7 @@ class BrickSetSocket extends BrickSocket {
this.socket.emit(this.messages.IMPORT_SET, {
set: (set !== undefined) ? set : this.html_input.value,
owners: owners,
+ purchase_location: purchase_location,
storage: storage,
tags: tags,
refresh: this.refresh
@@ -293,6 +301,10 @@ class BrickSetSocket extends BrickSocket {
this.html_owners.querySelectorAll('input').forEach(input => input.disabled = !enabled);
}
+ if (this.html_purchase_location) {
+ this.html_purchase_location.disabled = !enabled;
+ }
+
if (this.html_storage) {
this.html_storage.disabled = !enabled;
}
diff --git a/templates/add.html b/templates/add.html
index 9a0deebf..d9a94629 100644
--- a/templates/add.html
+++ b/templates/add.html
@@ -51,9 +51,22 @@
</div>
{{ accordion.footer() }}
{% endif %}
+ {% if brickset_purchase_locations | length %}
+ {{ accordion.header('Purchase location', 'purchase-location', 'metadata', icon='building-line') }}
+ <label class="visually-hidden" for="add-purchase-location">{{ name }}</label>
+ <div class="input-group">
+ <select id="add-purchase-location" class="form-select" autocomplete="off">
+ <option value="" selected><i>None</i></option>
+ {% for purchase_location in brickset_purchase_locations %}
+ <option value="{{ purchase_location.fields.id }}">{{ purchase_location.fields.name }}</option>
+ {% endfor %}
+ </select>
+ </div>
+ {{ accordion.footer() }}
+ {% endif %}
{% if brickset_storages | length %}
{{ accordion.header('Storage', 'storage', 'metadata', icon='archive-2-line') }}
- <label class="visually-hidden" for="storage">{{ name }}</label>
+ <label class="visually-hidden" for="add-storage">{{ name }}</label>
<div class="input-group">
<select id="add-storage" class="form-select" autocomplete="off">
<option value="" selected><i>None</i></option>
diff --git a/templates/admin.html b/templates/admin.html
index 22a7e1a7..87dbdb4a 100644
--- a/templates/admin.html
+++ b/templates/admin.html
@@ -18,6 +18,8 @@
{% include 'admin/database/delete.html' %}
{% elif delete_owner %}
{% include 'admin/owner/delete.html' %}
+ {% elif delete_purchase_location %}
+ {% include 'admin/purchase_location/delete.html' %}
{% elif delete_status %}
{% include 'admin/status/delete.html' %}
{% elif delete_storage %}
@@ -39,10 +41,11 @@
{% include 'admin/theme.html' %}
{% include 'admin/retired.html' %}
{{ accordion.header('Set metadata', 'metadata', 'admin', expanded=open_metadata, icon='profile-line', class='p-0') }}
- {% include 'admin/owner.html' %}
- {% include 'admin/status.html' %}
- {% include 'admin/storage.html' %}
- {% include 'admin/tag.html' %}
+ {% include 'admin/owner.html' %}
+ {% include 'admin/purchase_location.html' %}
+ {% include 'admin/status.html' %}
+ {% include 'admin/storage.html' %}
+ {% include 'admin/tag.html' %}
{{ accordion.footer() }}
{% include 'admin/database.html' %}
{% include 'admin/configuration.html' %}
diff --git a/templates/admin/purchase_location.html b/templates/admin/purchase_location.html
new file mode 100644
index 00000000..12675768
--- /dev/null
+++ b/templates/admin/purchase_location.html
@@ -0,0 +1,42 @@
+{% import 'macro/accordion.html' as accordion %}
+
+{{ accordion.header('Set purchase locations', 'purchase-location', 'metadata', expanded=open_purchase_location, icon='building-line', class='p-0') }}
+{% if purchase_location_error %}<div class="alert alert-danger m-2" role="alert"><strong>Error:</strong> {{ purchase_location_error }}.</div>{% endif %}
+<ul class="list-group list-group-flush">
+ {% if metadata_purchase_locations | length %}
+ {% for purchase_location in metadata_purchase_locations %}
+ <li class="list-group-item">
+ <form action="{{ url_for('admin_purchase_location.rename', id=purchase_location.fields.id) }}" method="post" class="row row-cols-lg-auto g-3 align-items-center">
+ <div class="col-12 flex-grow-1">
+ <label class="visually-hidden" for="name-{{ purchase_location.fields.id }}">Name</label>
+ <div class="input-group">
+ <div class="input-group-text">Name</div>
+ <input type="text" class="form-control" id="name-{{ purchase_location.fields.id }}" name="name" value="{{ purchase_location.fields.name }}">
+ <button type="submit" class="btn btn-primary"><i class="ri-edit-line"></i> Rename</button>
+ </div>
+ </div>
+ <div class="col-12">
+ <a href="{{ url_for('admin_purchase_location.delete', id=purchase_location.fields.id) }}" class="btn btn-danger" role="button"><i class="ri-delete-bin-2-line"></i> Delete</a>
+ </div>
+ </form>
+ </li>
+ {% endfor %}
+ {% else %}
+ <li class="list-group-item text-center"><i class="ri-error-warning-line"></i> No purchase location found.</li>
+ {% endif %}
+ <li class="list-group-item">
+ <form action="{{ url_for('admin_purchase_location.add') }}" method="post" class="row row-cols-lg-auto g-3 align-items-center">
+ <div class="col-12 flex-grow-1">
+ <label class="visually-hidden" for="name">Name</label>
+ <div class="input-group">
+ <div class="input-group-text">Name</div>
+ <input type="text" class="form-control" id="name" name="name" value="">
+ </div>
+ </div>
+ <div class="col-12">
+ <button type="submit" class="btn btn-primary"><i class="ri-add-circle-line"></i> Add</button>
+ </div>
+ </form>
+ </li>
+</ul>
+{{ accordion.footer() }}
diff --git a/templates/admin/purchase_location/delete.html b/templates/admin/purchase_location/delete.html
new file mode 100644
index 00000000..c53d8c22
--- /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') }}
+<form action="{{ url_for('admin_purchase_location.do_delete', id=purchase_location.fields.id) }}" method="post">
+ {% if purchase_location_error %}<div class="alert alert-danger text-start" role="alert"><strong>Error:</strong> {{ purchase_location_error }}.</div>{% endif %}
+ <div class="alert alert-danger text-center" role="alert">You are about to <strong>delete a set purchase location</strong>. This action is irreversible.</div>
+ <div class="row row-cols-lg-auto g-3 align-items-center">
+ <div class="col-12 flex-grow-1">
+ <div class="input-group">
+ <div class="input-group-text">Name</div>
+ <input type="text" class="form-control" value="{{ purchase_location.fields.name }}" disabled>
+ </div>
+ </div>
+ </div>
+ <hr class="border-bottom">
+ <a class="btn btn-danger" href="{{ url_for('admin.admin', open_purchase_location=true) }}" role="button"><i class="ri-arrow-left-long-line"></i> Back to the admin</a>
+ <button type="submit" class="btn btn-danger"><i class="ri-delete-bin-2-line"></i> Delete <strong>the set purchase location</strong></button>
+</form>
+{{ accordion.footer() }}
diff --git a/templates/macro/badge.html b/templates/macro/badge.html
index ea2be584..f1c9f89f 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 20642066..7c2525d1 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 8f3b4109..24454c25 100644
--- a/templates/set/filter.html
+++ b/templates/set/filter.html
@@ -60,6 +60,22 @@
</div>
</div>
{% endif %}
+ {% if brickset_purchase_locations | length %}
+ <div class="col-12 flex-grow-1">
+ <label class="visually-hidden" for="grid-owner">Purchase location</label>
+ <div class="input-group">
+ <span class="input-group-text"><i class="ri-building-line"></i><span class="ms-1 d-none d-md-inline"> Purchase location</span></span>
+ <select id="grid-purchase-location" class="form-select"
+ data-filter="value" data-filter-attribute="purchase-location"
+ autocomplete="off">
+ <option value="" selected>All</option>
+ {% for purchase_location in brickset_purchase_locations %}
+ <option value="{{ purchase_location.fields.id }}">{{ purchase_location.fields.name }}</option>
+ {% endfor %}
+ </select>
+ </div>
+ </div>
+ {% endif %}
{% if brickset_storages | length %}
<div class="col-12 flex-grow-1">
<label class="visually-hidden" for="grid-owner">Storage</label>
diff --git a/templates/set/management.html b/templates/set/management.html
index 1a8c2b7b..8d675f9a 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') }}
- <ul class="list-group list-group-flush">
- {% if brickset_owners | length %}
- {% for owner in brickset_owners %}
- <li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(item, owner, delete=delete) }}</li>
- {% endfor %}
- {% else %}
- <li class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No owner found.</li>
+ <ul class="list-group list-group-flush">
+ {% if brickset_owners | length %}
+ {% for owner in brickset_owners %}
+ <li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(item, owner, delete=delete) }}</li>
+ {% endfor %}
+ {% else %}
+ <li class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No owner found.</li>
+ {% endif %}
+ </ul>
+ <div class="list-group list-group-flush border-top">
+ <a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_owner=true) }}"><i class="ri-settings-4-line"></i> Manage the set owners</a>
+ </div>
+ {{ accordion.footer() }}
+ {{ accordion.header('Purchase location', 'purchase-location', 'set-management', icon='building-line') }}
+ {% if brickset_purchase_locations | length %}
+ {{ form.select('Purchase location', item, 'purchase_location', brickset_purchase_locations, delete=delete) }}
+ {% else %}
+ <p class="text-center"><i class="ri-error-warning-line"></i> No purchase location found.</p>
{% endif %}
- </ul>
- <div class="list-group list-group-flush border-top">
- <a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_owner=true) }}"><i class="ri-settings-4-line"></i> Manage the set owners</a>
- </div>
- {{ accordion.footer() }}
+ <hr>
+ <a href="{{ url_for('admin.admin', open_purchase_location=true) }}" class="btn btn-primary" role="button"><i class="ri-settings-4-line"></i> Manage the set purchase locations</a>
+ {{ 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 %}
- <p class="text-center"><i class="ri-error-warning-line"></i> No storage found.</p>
- {% endif %}
- <hr>
- <a href="{{ url_for('admin.admin', open_storage=true) }}" class="btn btn-primary" role="button"><i class="ri-settings-4-line"></i> Manage the set storages</a>
- {{ accordion.footer() }}
- {{ accordion.header('Tags', 'tag', 'set-management', icon='price-tag-2-line', class='p-0') }}
- <ul class="list-group list-group-flush">
- {% if brickset_tags | length %}
- {% for tag in brickset_tags %}
- <li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(item, tag, delete=delete) }}</li>
- {% endfor %}
- {% else %}
- <li class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No tag found.</li>
+ {% if brickset_storages | length %}
+ {{ form.select('Storage', item, 'storage', brickset_storages, delete=delete) }}
+ {% else %}
+ <p class="text-center"><i class="ri-error-warning-line"></i> No storage found.</p>
{% endif %}
- </ul>
- <div class="list-group list-group-flush border-top">
- <a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_tag=true) }}"><i class="ri-settings-4-line"></i> Manage the set tags</a>
- </div>
- {{ accordion.footer() }}
+ <hr>
+ <a href="{{ url_for('admin.admin', open_storage=true) }}" class="btn btn-primary" role="button"><i class="ri-settings-4-line"></i> Manage the set storages</a>
+ {{ accordion.footer() }}
+ {{ accordion.header('Tags', 'tag', 'set-management', icon='price-tag-2-line', class='p-0') }}
+ <ul class="list-group list-group-flush">
+ {% if brickset_tags | length %}
+ {% for tag in brickset_tags %}
+ <li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(item, tag, delete=delete) }}</li>
+ {% endfor %}
+ {% else %}
+ <li class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No tag found.</li>
+ {% endif %}
+ </ul>
+ <div class="list-group list-group-flush border-top">
+ <a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_tag=true) }}"><i class="ri-settings-4-line"></i> Manage the set tags</a>
+ </div>
+ {{ accordion.footer() }}
{{ accordion.header('Data', 'data', 'set-management', icon='database-2-line') }}
- <a href="{{ item.url_for_refresh() }}" class="btn btn-primary" role="button"><i class="ri-refresh-line"></i> Refresh the set data</a>
+ <a href="{{ item.url_for_refresh() }}" class="btn btn-primary" role="button"><i class="ri-refresh-line"></i> Refresh the set data</a>
{{ accordion.footer() }}
{{ accordion.footer() }}
{% endif %}
diff --git a/templates/sets.html b/templates/sets.html
index 5af93c93..4832160c 100644
--- a/templates/sets.html
+++ b/templates/sets.html
@@ -10,7 +10,7 @@
<label class="visually-hidden" for="grid-search">Search</label>
<div class="input-group">
<span class="input-group-text"><i class="ri-search-line"></i><span class="ms-1 d-none d-md-inline"> Search</span></span>
- <input id="grid-search" data-search-exact="name,number,parts,searchStorage,theme,year" data-search-list="searchOwner,searchTag" class="form-control form-control-sm" type="text" placeholder="Set, name, number of parts, theme, year, owner, storage, tag" value="">
+ <input id="grid-search" data-search-exact="name,number,parts,searchPurchaseLocation,searchStorage,theme,year" data-search-list="searchOwner,searchTag" class="form-control form-control-sm" type="text" placeholder="Set, name, number of parts, theme, year, owner, purchase location, storage, tag" value="">
</div>
</div>
<div class="col-12">
From f0cec23da9566f044bd8435d5baeebe36b4901db Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 8f129392..f4d6a48b 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 efcdb16d..2bbb26cd 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 729049a9..5452f938 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 fb972092..c397b130 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 7d52d33a..720be769 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 333868d0..02ef7711 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 00000000..cb8516ee
--- /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 00000000..f742a8fe
--- /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 0777f4e9..f1493b8d 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('/<id>/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('/<id>/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('/<id>/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('/<id>/owner/<metadata_id>', methods=['POST'])
@login_required
diff --git a/docs/development.md b/docs/development.md
index 8657590c..dd3beee6 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 4db9cd7a..ffa41ace 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 a82a4eaa..658ef370 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -9,6 +9,7 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.12.0/baguetteBox.css" integrity="sha512-VZ783G3QIpxXpg7tWpzHn+XhjsOCIxFYoSWmyipKCB41OYaB9i4brxAWuY1c8gGCSqKo7uvckzPJhYcdBZQ9gg==" crossorigin="anonymous" referrerpolicy="no-referrer">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/simple-datatables@9.2.1/dist/style.min.css">
<link href="https://cdn.jsdelivr.net/npm/remixicon@4.6.0/fonts/remixicon.css" rel="stylesheet">
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vanillajs-datepicker@1.3.4/dist/css/datepicker-bs5.min.css">
<link href="{{ url_for('static', filename='styles.css') }}" rel="stylesheet">
<link rel="icon" type="image/png" sizes="48x48" href="{{ url_for('static', filename='brick.png') }}">
</head>
@@ -78,6 +79,7 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/baguettebox.js/1.12.0/baguetteBox.min.js" integrity="sha512-HzIuiABxntLbBS8ClRa7drXZI3cqvkAZ5DD0JCAkmRwUtykSGqzA9uItHivDhRUYnW3MMyY5xqk7qVUHOEMbMA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.8.1/socket.io.min.js" integrity="sha512-8ExARjWWkIllMlNzVg7JKq9RKWPlJABQUNq6YvAjE/HobctjH/NA+bSiDMDvouBVjp4Wwnf1VP1OEv7Zgjtuxw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.jsdelivr.net/npm/simple-datatables@9.2.1/dist/umd/simple-datatables.min.js"></script>
+ <script src="https://cdn.jsdelivr.net/npm/vanillajs-datepicker@1.3.4/dist/js/datepicker-full.min.js"></script>
<!-- BrickTracker scripts -->
<script src="{{ url_for('static', filename='scripts/changer.js') }}"></script>
<script src="{{ url_for('static', filename='scripts/grid/filter.js') }}"></script>
diff --git a/templates/macro/badge.html b/templates/macro/badge.html
index f1c9f89f..ee68a69d 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 45ec2b5d..9564f350 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 %}
<label class="visually-hidden" for="{{ prefix }}-{{ id }}">{{ name }}</label>
<div class="input-group">
+ {% if icon %}<span class="input-group-text"><i class="ri-{{ icon }} me-1"></i><span class="ms-1 d-none d-md-inline"> {{ name }}</span></span>{% endif %}
<input class="form-control form-control-sm flex-shrink-1" type="text" id="{{ prefix }}-{{ id }}" value="{% if value %}{{ value }}{% endif %}"
{% if g.login.is_authenticated() %}
data-changer-id="{{ id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ url }}"
+ {% if date %}data-changer-date="true"{% endif %}
{% else %}
disabled
{% endif %}
autocomplete="off">
+ {% if suffix %}<span class="input-group-text d-none d-md-inline">{{ suffix }}</span>{% endif %}
{% if g.login.is_authenticated() %}
<span id="status-{{ prefix }}-{{ id }}" class="input-group-text ri-save-line"></span>
<button id="clear-{{ prefix }}-{{ id }}" type="button" class="btn btn-sm btn-light btn-outline-danger border"><i class="ri-eraser-line"></i></button>
@@ -45,7 +48,7 @@
{% set prefix=metadata_list.as_prefix() %}
<label class="visually-hidden" for="{{ prefix }}-{{ item.fields.id }}">{{ name }}</label>
<div class="input-group">
- {% if icon %}<span class="input-group-text"><i class="ri-{{ icon }}"></i></span>{% endif %}
+ {% if icon %}<span class="input-group-text"><i class="ri-{{ icon }} me-1"></i><span class="ms-1 d-none d-md-inline"> {{ name }}</span></span>{% endif %}
<select id="{{ prefix }}-{{ item.fields.id }}" class="form-select"
{% if not delete %}
data-changer-id="{{ item.fields.id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ metadata_list.url_for_set_value(item.fields.id) }}"
diff --git a/templates/set/card.html b/templates/set/card.html
index 7c2525d1..924bcd75 100644
--- a/templates/set/card.html
+++ b/templates/set/card.html
@@ -17,6 +17,8 @@
{% if not config['HIDE_TABLE_DAMAGED_PARTS'] %}
data-has-damaged="{{ (item.fields.total_damaged > 0) | int }}" data-damaged="{{ item.fields.total_damaged }}"
{% endif %}
+ {% if item.fields.purchase_date is not none %}data-purchase-date="{{ item.fields.purchase_date }}"{% endif %}
+ {% if item.fields.purchase_price is not none %}data-purchase-price="{{ item.fields.purchase_price }}"{% 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 }}"
@@ -68,8 +70,10 @@
{{ 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 %}
+ {{ badge.purchase_date(item.purchase_date(), solo=solo, last=last) }}
+ {{ badge.purchase_location(item, brickset_purchase_locations, solo=solo, last=last) }}
+ {{ badge.purchase_price(item.purchase_price(), solo=solo, last=last) }}
{% if not solo %}
{{ badge.instructions(item, solo=solo, last=last) }}
{% endif %}
diff --git a/templates/set/management.html b/templates/set/management.html
index 8d675f9a..7ed92aff 100644
--- a/templates/set/management.html
+++ b/templates/set/management.html
@@ -1,5 +1,5 @@
{% if g.login.is_authenticated() %}
- {{ accordion.header('Management', 'set-management', 'set-details', icon='settings-4-line', class='p-0') }}
+ {{ accordion.header('Management', 'set-management', 'set-details', icon='settings-4-line', class='p-0', expanded=true) }}
{{ accordion.header('Owners', 'owner', 'set-management', icon='group-line', class='p-0') }}
<ul class="list-group list-group-flush">
{% if brickset_owners | length %}
@@ -14,12 +14,23 @@
<a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_owner=true) }}"><i class="ri-settings-4-line"></i> Manage the set owners</a>
</div>
{{ accordion.footer() }}
- {{ accordion.header('Purchase location', 'purchase-location', 'set-management', icon='building-line') }}
- {% if brickset_purchase_locations | length %}
- {{ form.select('Purchase location', item, 'purchase_location', brickset_purchase_locations, delete=delete) }}
- {% else %}
- <p class="text-center"><i class="ri-error-warning-line"></i> No purchase location found.</p>
- {% endif %}
+ {{ accordion.header('Purchase', 'purchase', 'set-management', icon='wallet-3-line', expanded=true) }}
+ <div class="alert alert-info" role="alert">The expected date format here is <code>yyyy/mm/dd</code> (year/month/day), but you can configured how it is displayed in the set card with the <code>PURCHASE_DATE_FORMAT</code> variable.</div>
+ <div class="row row-cols-lg-auto g-1 justify-content-start align-items-center pb-2">
+ <div class="col-12">
+ {{ form.input('Date', item.fields.id, 'date', item.url_for_purchase_date(), item.purchase_date(standard=true), icon='calendar-line', date=true) }}
+ </div>
+ <div class="col-12 flex-grow-1">
+ {{ form.input('Price', item.fields.id, 'price', item.url_for_purchase_price(), item.fields.purchase_price, suffix=config['PURCHASE_CURRENCY'], icon='wallet-3-line') }}
+ </div>
+ <div class="col-12 flex-grow-1">
+ {% if brickset_purchase_locations | length %}
+ {{ form.select('Location', item, 'purchase_location', brickset_purchase_locations, icon='building-line', delete=delete) }}
+ {% else %}
+ <i class="ri-error-warning-line"></i> No purchase location found.
+ {% endif %}
+ </div>
+ </div>
<hr>
<a href="{{ url_for('admin.admin', open_purchase_location=true) }}" class="btn btn-primary" role="button"><i class="ri-settings-4-line"></i> Manage the set purchase locations</a>
{{ accordion.footer() }}
diff --git a/templates/set/sort.html b/templates/set/sort.html
index 3315a1df..abc37d95 100644
--- a/templates/set/sort.html
+++ b/templates/set/sort.html
@@ -22,6 +22,10 @@
<button id="sort-damaged" type="button" class="btn btn-outline-primary mb-2"
data-sort-attribute="damaged" data-sort-desc="true"><i class="ri-error-warning-line"></i><span class="d-none d-xl-inline"> Damaged</span></button>
{% endif %}
+ <button id="sort-purchase-date" type="button" class="btn btn-outline-primary mb-2"
+ data-sort-attribute="purchase-date" data-sort-desc="true"><i class="ri-calendar-line"></i><span class="d-none d-xl-inline"> Date</span></button>
+ <button id="sort-purchase-price" type="button" class="btn btn-outline-primary mb-2"
+ data-sort-attribute="purchase-price" data-sort-desc="true"><i class="ri-wallet-3-line"></i><span class="d-none d-xl-inline"> Price</span></button>
<button id="sort-clear" type="button" class="btn btn-outline-dark mb-2"
data-sort-clear="true"><i class="ri-close-circle-line"></i><span class="d-none d-xl-inline"> Clear</span></button>
</div>
From 2e995b615d0a91e370aef758ea3eeccd75939db5 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Tue, 4 Feb 2025 17:04:09 +0100
Subject: [PATCH 141/154] Configuration doc
---
.env.sample | 15 ++++++++++-----
CHANGELOG.md | 1 +
bricktracker/config.py | 2 +-
3 files changed, 12 insertions(+), 6 deletions(-)
diff --git a/.env.sample b/.env.sample
index f4d6a48b..65a9d306 100644
--- a/.env.sample
+++ b/.env.sample
@@ -60,6 +60,11 @@
# Legacy name: DOMAIN_NAME
# BK_DOMAIN_NAME=http://localhost:3333
+# Optional: Format of the timestamp for files on disk (instructions, themes)
+# Check https://docs.python.org/3/library/time.html#time.strftime for format details
+# Default: %d/%m/%Y, %H:%M:%S
+# BK_FILE_DATETIME_FORMAT=%m/%d/%Y, %H:%M
+
# Optional: IP address the server will listen on.
# Default: 0.0.0.0
# BK_HOST=0.0.0.0
@@ -91,11 +96,6 @@
# 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
@@ -108,6 +108,11 @@
# Default: false
# BK_HIDE_ALL_PARTS=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 'Sets' entry from the menu. Does not disable the route.
# Default: false
# BK_HIDE_ALL_SETS=true
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2bbb26cd..46c76b54 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@
- 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
+- Documented: `BK_FILE_DATETIME_FORMAT`, date format for files on disk (instructions, theme)
### Code
diff --git a/bricktracker/config.py b/bricktracker/config.py
index 5452f938..e8872e53 100644
--- a/bricktracker/config.py
+++ b/bricktracker/config.py
@@ -28,9 +28,9 @@ CONFIG: Final[list[dict[str, Any]]] = [
{'n': 'HIDE_ALL_INSTRUCTIONS', 'c': bool},
{'n': 'HIDE_ALL_MINIFIGURES', 'c': bool},
{'n': 'HIDE_ALL_PARTS', 'c': bool},
+ {'n': 'HIDE_ALL_PROBLEMS_PARTS', 'e': 'BK_HIDE_MISSING_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},
{'n': 'HIDE_TABLE_MISSING_PARTS', 'c': bool},
From 9642853d8efd378f0dfc517a6e79c71c3c5c1149 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Tue, 4 Feb 2025 17:38:26 +0100
Subject: [PATCH 142/154] Sort metadata lists by name for more consistency
---
bricktracker/metadata_list.py | 5 +++--
bricktracker/set_owner_list.py | 3 ++-
bricktracker/set_purchase_location_list.py | 7 +++++--
bricktracker/set_status_list.py | 3 ++-
bricktracker/set_storage_list.py | 7 +++++--
bricktracker/set_tag_list.py | 5 +++--
bricktracker/sql/set/metadata/owner/base.sql | 6 +++++-
bricktracker/sql/set/metadata/purchase_location/base.sql | 4 ++++
bricktracker/sql/set/metadata/status/base.sql | 6 +++++-
bricktracker/sql/set/metadata/tag/base.sql | 6 +++++-
10 files changed, 39 insertions(+), 13 deletions(-)
diff --git a/bricktracker/metadata_list.py b/bricktracker/metadata_list.py
index 71245483..a7df7521 100644
--- a/bricktracker/metadata_list.py
+++ b/bricktracker/metadata_list.py
@@ -30,8 +30,9 @@ class BrickMetadataList(BrickRecordList[T]):
mapping: dict[str, T]
model: Type[T]
- # Database table
+ # Database
table: str
+ order: str
# Queries
select_query: str
@@ -73,7 +74,7 @@ class BrickMetadataList(BrickRecordList[T]):
self.__class__.mapping = {}
# Load the metadata from the database
- for record in self.select():
+ for record in self.select(order=self.order):
metadata = model(record=record)
self.__class__.records.append(metadata)
diff --git a/bricktracker/set_owner_list.py b/bricktracker/set_owner_list.py
index 7d3b8f5e..74219a73 100644
--- a/bricktracker/set_owner_list.py
+++ b/bricktracker/set_owner_list.py
@@ -11,8 +11,9 @@ logger = logging.getLogger(__name__)
class BrickSetOwnerList(BrickMetadataList[BrickSetOwner]):
kind: str = 'set owners'
- # Database table
+ # Database
table: str = 'bricktracker_set_owners'
+ order: str = '"bricktracker_metadata_owners"."name"'
# Queries
select_query = 'set/metadata/owner/list'
diff --git a/bricktracker/set_purchase_location_list.py b/bricktracker/set_purchase_location_list.py
index 3ffae4bc..d49a1eb9 100644
--- a/bricktracker/set_purchase_location_list.py
+++ b/bricktracker/set_purchase_location_list.py
@@ -15,9 +15,12 @@ class BrickSetPurchaseLocationList(
):
kind: str = 'set purchase locations'
+ # Order
+ order: str = '"bricktracker_metadata_purchase_locations"."name"'
+
# Queries
- select_query = 'set/metadata/purchase_location/list'
- all_query = 'set/metadata/purchase_location/all'
+ select_query: str = 'set/metadata/purchase_location/list'
+ all_query: str = 'set/metadata/purchase_location/all'
# Set value endpoint
set_value_endpoint: str = 'set.update_purchase_location'
diff --git a/bricktracker/set_status_list.py b/bricktracker/set_status_list.py
index e238f62c..c40731c4 100644
--- a/bricktracker/set_status_list.py
+++ b/bricktracker/set_status_list.py
@@ -11,8 +11,9 @@ logger = logging.getLogger(__name__)
class BrickSetStatusList(BrickMetadataList[BrickSetStatus]):
kind: str = 'set statuses'
- # Database table
+ # Database
table: str = 'bricktracker_set_statuses'
+ order: str = '"bricktracker_metadata_statuses"."name"'
# Queries
select_query = 'set/metadata/status/list'
diff --git a/bricktracker/set_storage_list.py b/bricktracker/set_storage_list.py
index 77be7164..7e62333b 100644
--- a/bricktracker/set_storage_list.py
+++ b/bricktracker/set_storage_list.py
@@ -13,9 +13,12 @@ logger = logging.getLogger(__name__)
class BrickSetStorageList(BrickMetadataList[BrickSetStorage]):
kind: str = 'set storages'
+ # Order
+ order: str = '"bricktracker_metadata_storages"."name"'
+
# Queries
- select_query = 'set/metadata/storage/list'
- all_query = 'set/metadata/storage/all'
+ select_query: str = 'set/metadata/storage/list'
+ all_query: str = 'set/metadata/storage/all'
# Set value endpoint
set_value_endpoint: str = 'set.update_storage'
diff --git a/bricktracker/set_tag_list.py b/bricktracker/set_tag_list.py
index 822ac3bf..9ed0d910 100644
--- a/bricktracker/set_tag_list.py
+++ b/bricktracker/set_tag_list.py
@@ -11,11 +11,12 @@ logger = logging.getLogger(__name__)
class BrickSetTagList(BrickMetadataList[BrickSetTag]):
kind: str = 'set tags'
- # Database table
+ # Database
table: str = 'bricktracker_set_tags'
+ order: str = '"bricktracker_metadata_tags"."name"'
# Queries
- select_query = 'set/metadata/tag/list'
+ select_query: str = 'set/metadata/tag/list'
# Instantiate the list with the proper class
@classmethod
diff --git a/bricktracker/sql/set/metadata/owner/base.sql b/bricktracker/sql/set/metadata/owner/base.sql
index 095ae3d6..f36d023a 100644
--- a/bricktracker/sql/set/metadata/owner/base.sql
+++ b/bricktracker/sql/set/metadata/owner/base.sql
@@ -3,4 +3,8 @@ SELECT
"bricktracker_metadata_owners"."name"
FROM "bricktracker_metadata_owners"
-{% block where %}{% endblock %}
\ No newline at end of file
+{% block where %}{% endblock %}
+
+{% if order %}
+ORDER BY {{ order }}
+{% endif %}
diff --git a/bricktracker/sql/set/metadata/purchase_location/base.sql b/bricktracker/sql/set/metadata/purchase_location/base.sql
index 8ac33ca1..c0a0ce06 100644
--- a/bricktracker/sql/set/metadata/purchase_location/base.sql
+++ b/bricktracker/sql/set/metadata/purchase_location/base.sql
@@ -4,3 +4,7 @@ SELECT
FROM "bricktracker_metadata_purchase_locations"
{% block where %}{% endblock %}
+
+{% if order %}
+ORDER BY {{ order }}
+{% endif %}
diff --git a/bricktracker/sql/set/metadata/status/base.sql b/bricktracker/sql/set/metadata/status/base.sql
index b1b2167d..d962cc25 100644
--- a/bricktracker/sql/set/metadata/status/base.sql
+++ b/bricktracker/sql/set/metadata/status/base.sql
@@ -4,4 +4,8 @@ SELECT
"bricktracker_metadata_statuses"."displayed_on_grid"
FROM "bricktracker_metadata_statuses"
-{% block where %}{% endblock %}
\ No newline at end of file
+{% block where %}{% endblock %}
+
+{% if order %}
+ORDER BY {{ order }}
+{% endif %}
diff --git a/bricktracker/sql/set/metadata/tag/base.sql b/bricktracker/sql/set/metadata/tag/base.sql
index 3ec57259..7e6f03bf 100644
--- a/bricktracker/sql/set/metadata/tag/base.sql
+++ b/bricktracker/sql/set/metadata/tag/base.sql
@@ -3,4 +3,8 @@ SELECT
"bricktracker_metadata_tags"."name"
FROM "bricktracker_metadata_tags"
-{% block where %}{% endblock %}
\ No newline at end of file
+{% block where %}{% endblock %}
+
+{% if order %}
+ORDER BY {{ order }}
+{% endif %}
From 9326c06c3e0e1d97365f0f06e0e6b4006b10757e Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Tue, 4 Feb 2025 17:39:47 +0100
Subject: [PATCH 143/154] Remove forced open management accordion
---
templates/set/management.html | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/templates/set/management.html b/templates/set/management.html
index 7ed92aff..744c9616 100644
--- a/templates/set/management.html
+++ b/templates/set/management.html
@@ -1,5 +1,5 @@
{% if g.login.is_authenticated() %}
- {{ accordion.header('Management', 'set-management', 'set-details', icon='settings-4-line', class='p-0', expanded=true) }}
+ {{ 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') }}
<ul class="list-group list-group-flush">
{% if brickset_owners | length %}
@@ -14,7 +14,7 @@
<a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_owner=true) }}"><i class="ri-settings-4-line"></i> Manage the set owners</a>
</div>
{{ accordion.footer() }}
- {{ accordion.header('Purchase', 'purchase', 'set-management', icon='wallet-3-line', expanded=true) }}
+ {{ accordion.header('Purchase', 'purchase', 'set-management', icon='wallet-3-line') }}
<div class="alert alert-info" role="alert">The expected date format here is <code>yyyy/mm/dd</code> (year/month/day), but you can configured how it is displayed in the set card with the <code>PURCHASE_DATE_FORMAT</code> variable.</div>
<div class="row row-cols-lg-auto g-1 justify-content-start align-items-center pb-2">
<div class="col-12">
From 50e6c8bf9ccce6adef0c6a614cdb39bafa6ee54d Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Tue, 4 Feb 2025 18:16:27 +0100
Subject: [PATCH 144/154] Merge delete.html with set.html
---
bricktracker/views/set.py | 3 ++-
templates/delete.html | 15 ---------------
templates/set.html | 4 ++--
3 files changed, 4 insertions(+), 18 deletions(-)
delete mode 100644 templates/delete.html
diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py
index f1493b8d..c174576b 100644
--- a/bricktracker/views/set.py
+++ b/bricktracker/views/set.py
@@ -145,7 +145,8 @@ def update_tag(*, id: str, metadata_id: str) -> Response:
@exception_handler(__file__)
def delete(*, id: str) -> str:
return render_template(
- 'delete.html',
+ 'set.html',
+ delete=True,
item=BrickSet().select_specific(id),
error=request.args.get('error'),
)
diff --git a/templates/delete.html b/templates/delete.html
deleted file mode 100644
index 07c2b082..00000000
--- a/templates/delete.html
+++ /dev/null
@@ -1,15 +0,0 @@
-{% extends 'base.html' %}
-
-{% block title %} - Delete a set {{ item.fields.set }} ({{ item.fields.id }}){% endblock %}
-
-{% block main %}
-<div class="container">
- <div class="row">
- <div class="col-12">
- {% with solo=true, delete=true %}
- {% include 'set/card.html' %}
- {% endwith %}
- </div>
- </div>
-</div>
-{% endblock %}
diff --git a/templates/set.html b/templates/set.html
index 69ba6dd1..12df4918 100644
--- a/templates/set.html
+++ b/templates/set.html
@@ -1,12 +1,12 @@
{% extends 'base.html' %}
-{% block title %} - Set {{ item.fields.name}} ({{ item.fields.set }}){% endblock %}
+{% block title %} - {% if delete %}Delete a set{% else %}Set{% endif %} {{ item.fields.name}} ({{ item.fields.set }}){% endblock %}
{% block main %}
<div class="container">
<div class="row">
<div class="col-12">
- {% with solo=true %}
+ {% with solo=true, delete=delete %}
{% include 'set/card.html' %}
{% endwith %}
</div>
From 90a72130dfe6489e82ae2e8ccc48d12cce148433 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Tue, 4 Feb 2025 19:05:38 +0100
Subject: [PATCH 145/154] Make form.select generic
---
templates/macro/form.html | 17 ++++++++---------
templates/set/management.html | 4 ++--
2 files changed, 10 insertions(+), 11 deletions(-)
diff --git a/templates/macro/form.html b/templates/macro/form.html
index 9564f350..72af87f5 100644
--- a/templates/macro/form.html
+++ b/templates/macro/form.html
@@ -43,26 +43,25 @@
{% endif %}
{% endmacro %}
-{% macro select(name, item, field, metadata_list, nullable=true, icon=none, delete=false) %}
+{% macro select(name, id, prefix, url, value, metadata_list, nullable=true, icon=none, delete=false) %}
{% if g.login.is_authenticated() %}
- {% set prefix=metadata_list.as_prefix() %}
- <label class="visually-hidden" for="{{ prefix }}-{{ item.fields.id }}">{{ name }}</label>
+ <label class="visually-hidden" for="{{ prefix }}-{{ id }}">{{ name }}</label>
<div class="input-group">
{% if icon %}<span class="input-group-text"><i class="ri-{{ icon }} me-1"></i><span class="ms-1 d-none d-md-inline"> {{ name }}</span></span>{% endif %}
- <select id="{{ prefix }}-{{ item.fields.id }}" class="form-select"
+ <select id="{{ prefix }}-{{ id }}" class="form-select"
{% if not delete %}
- data-changer-id="{{ item.fields.id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ metadata_list.url_for_set_value(item.fields.id) }}"
+ data-changer-id="{{ id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ url }}"
{% else %}
disabled
{% endif %}
autocomplete="off">
- {% if nullable %}<option value="" {% if item.fields[field] is none %}selected{% endif %}><i>None</i></option>{% endif %}
+ {% if nullable %}<option value="" {% if value is none %}selected{% endif %}><i>None</i></option>{% endif %}
{% for metadata in metadata_list %}
- <option value="{{ metadata.fields.id }}" {% if metadata.fields.id == item.fields[field] %}selected{% endif %}>{{ metadata.fields.name }}</option>
+ <option value="{{ metadata.fields.id }}" {% if metadata.fields.id == value %}selected{% endif %}>{{ metadata.fields.name }}</option>
{% endfor %}
</select>
- <span id="status-{{ prefix }}-{{ item.fields.id }}" class="input-group-text ri-save-line"></span>
- <button id="clear-{{ prefix }}-{{ item.fields.id }}" type="button" class="btn btn-sm btn-light btn-outline-danger border"><i class="ri-eraser-line"></i></button>
+ <span id="status-{{ prefix }}-{{ id }}" class="input-group-text ri-save-line"></span>
+ <button id="clear-{{ prefix }}-{{ id }}" type="button" class="btn btn-sm btn-light btn-outline-danger border"><i class="ri-eraser-line"></i></button>
</div>
{% endif %}
{% endmacro %}
diff --git a/templates/set/management.html b/templates/set/management.html
index 744c9616..ade6729c 100644
--- a/templates/set/management.html
+++ b/templates/set/management.html
@@ -25,7 +25,7 @@
</div>
<div class="col-12 flex-grow-1">
{% 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 %}
<i class="ri-error-warning-line"></i> 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 %}
<p class="text-center"><i class="ri-error-warning-line"></i> No storage found.</p>
{% endif %}
From e022a6bc1ef32d10866ffa24c064e828e14eaac9 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
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 30d61eef..0ef7d43f 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 8fdd3cb0..ae34b3a6 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 74219a73..ec2af9d4 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 d49a1eb9..65e5f1b3 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 c40731c4..ff4603be 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 7e62333b..7cd9e136 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 9ed0d910..93817bac 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 30dce281..85b995da 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 354782d0..90ac201a 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 c3aa2f23..17ae4f0f 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 d5f15bb5..ca9511a1 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 aa01b79c..e51b66b6 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 <versatile.mailbox@gmail.com>
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 72af87f5..7f7952be 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() %}
- <input class="form-check-input" type="checkbox" id="{{ prefix }}-{{ item.fields.id }}" {% if item.fields[metadata.as_column()] %}checked{% endif %}
+ <input class="form-check-input" type="checkbox" id="{{ prefix }}-{{ id }}" {% if checked %}checked{% endif %}
{% if not delete %}
- data-changer-id="{{ item.fields.id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ metadata.url_for_set_state(item.fields.id) }}" {% if parent %}data-changer-parent="{{ parent }}"{% endif %}
+ data-changer-id="{{ id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ url }}" {% if parent %}data-changer-parent="{{ parent }}"{% endif %}
{% else %}
disabled
{% endif %}
autocomplete="off">
- <label class="form-check-label flex-grow-1 ms-1" for="{{ prefix }}-{{ item.fields.id }}">
- {{ metadata.fields.name }} <i id="status-{{ prefix }}-{{ item.fields.id }}"></i>
+ <label class="form-check-label flex-grow-1 ms-1" for="{{ prefix }}-{{ id }}">
+ {{ name }} <i id="status-{{ prefix }}-{{ id }}"></i>
</label>
{% else %}
- <input class="form-check-input text-reset" type="checkbox" {% if item.fields[metadata.as_column()] %}checked{% endif %} disabled>
- {{ text }}
+ <input class="form-check-input text-reset" type="checkbox" {% if checked %}checked{% endif %} disabled>
+ {{ name }}
{% endif %}
{% endmacro %}
diff --git a/templates/set/card.html b/templates/set/card.html
index 924bcd75..77cf33fa 100644
--- a/templates/set/card.html
+++ b/templates/set/card.html
@@ -83,7 +83,7 @@
{% if not tiny and brickset_statuses | length %}
<ul class="list-group list-group-flush card-check border-bottom-0">
{% for status in brickset_statuses %}
- <li class="d-flex list-group-item {% if not solo %}p-1{% endif %} text-nowrap">{{ form.checkbox(item, status, parent='set', delete=delete) }}</li>
+ <li class="d-flex list-group-item {% if not solo %}p-1{% endif %} text-nowrap">{{ form.checkbox(status.fields.name, item.fields.id, status.as_dataset(), status.url_for_set_state(item.fields.id), item.fields[status.as_column()], parent='set', delete=delete) }}</li>
{% endfor %}
</ul>
{% endif %}
diff --git a/templates/set/management.html b/templates/set/management.html
index ade6729c..ea9b5012 100644
--- a/templates/set/management.html
+++ b/templates/set/management.html
@@ -4,7 +4,7 @@
<ul class="list-group list-group-flush">
{% if brickset_owners | length %}
{% for owner in brickset_owners %}
- <li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(item, owner, delete=delete) }}</li>
+ <li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(owner.fields.name, item.fields.id, owner.as_dataset(), owner.url_for_set_state(item.fields.id), item.fields[owner.as_column()], delete=delete) }}</li>
{% endfor %}
{% else %}
<li class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No owner found.</li>
@@ -47,7 +47,7 @@
<ul class="list-group list-group-flush">
{% if brickset_tags | length %}
{% for tag in brickset_tags %}
- <li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(item, tag, delete=delete) }}</li>
+ <li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(tag.fields.name, item.fields.id, tag.as_dataset(), tag.url_for_set_state(item.fields.id), item.fields[tag.as_column()], delete=delete) }}</li>
{% endfor %}
{% else %}
<li class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No tag found.</li>
From 5d6b373769e250f11fa739a871c66c19db3b9fe8 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Tue, 4 Feb 2025 19:48:39 +0100
Subject: [PATCH 148/154] Add missing metadata when deleting a set
---
bricktracker/views/set.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py
index c174576b..bb9e845c 100644
--- a/bricktracker/views/set.py
+++ b/bricktracker/views/set.py
@@ -149,6 +149,7 @@ def delete(*, id: str) -> str:
delete=True,
item=BrickSet().select_specific(id),
error=request.args.get('error'),
+ **set_metadata_lists(as_class=True)
)
From 56c1a46b371ffc33509c1dbbee8c5644ed6c2fd2 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Tue, 4 Feb 2025 19:55:34 +0100
Subject: [PATCH 149/154] Differentiate between no sort and no sort-and-filter
in tables
---
static/scripts/table.js | 13 +++++++++++--
templates/instructions/table.html | 4 ++--
templates/macro/table.html | 2 +-
templates/wish/table.html | 4 ++--
4 files changed, 16 insertions(+), 7 deletions(-)
diff --git a/static/scripts/table.js b/static/scripts/table.js
index 669afc57..f96f10bb 100644
--- a/static/scripts/table.js
+++ b/static/scripts/table.js
@@ -1,11 +1,16 @@
class BrickTable {
constructor(table, per_page) {
- const columns = []
+ const columns = [];
+ const no_sort_and_filter = [];
const no_sort = [];
const number = [];
// Read the table header for parameters
table.querySelectorAll('th').forEach((th, index) => {
+ if (th.dataset.tableNoSortAndFilter) {
+ no_sort_and_filter.push(index);
+ }
+
if (th.dataset.tableNoSort) {
no_sort.push(index);
}
@@ -15,8 +20,12 @@ class BrickTable {
}
});
+ if (no_sort_and_filter.length) {
+ columns.push({ select: no_sort_and_filter, sortable: false, searchable: false });
+ }
+
if (no_sort.length) {
- columns.push({ select: no_sort, sortable: false, searchable: false });
+ columns.push({ select: no_sort, sortable: false });
}
if (number.length) {
diff --git a/templates/instructions/table.html b/templates/instructions/table.html
index 8ad9f3cf..b2c80077 100644
--- a/templates/instructions/table.html
+++ b/templates/instructions/table.html
@@ -6,9 +6,9 @@
<tr>
<th scope="col"><i class="ri-file-line fw-normal"></i> Filename</th>
<th scope="col"><i class="ri-hashtag fw-normal"></i> Set</th>
- <th data-table-no-sort="true" class="no-sort" scope="col"><i class="ri-image-line fw-normal"></i> Image</th>
+ <th data-table-no-sort-and-search="true" class="no-sort" scope="col"><i class="ri-image-line fw-normal"></i> Image</th>
{% if g.login.is_authenticated() %}
- <th data-table-no-sort="true" class="no-sort" scope="col"><i class="ri-settings-4-line fw-normal"></i> Actions</th>
+ <th data-table-no-sort-and-search="true" class="no-sort" scope="col"><i class="ri-settings-4-line fw-normal"></i> Actions</th>
{% endif %}
</tr>
</thead>
diff --git a/templates/macro/table.html b/templates/macro/table.html
index 0638380f..32cc63c1 100644
--- a/templates/macro/table.html
+++ b/templates/macro/table.html
@@ -2,7 +2,7 @@
<thead>
<tr>
{% if image %}
- <th data-table-no-sort="true" class="no-sort" scope="col"><i class="ri-image-line fw-normal"></i> Image</th>
+ <th data-table-no-sort-and-search="true" class="no-sort" scope="col"><i class="ri-image-line fw-normal"></i> Image</th>
{% endif %}
<th scope="col"><i class="ri-pencil-line fw-normal"></i> Name</th>
{% if color %}
diff --git a/templates/wish/table.html b/templates/wish/table.html
index 3b037824..ca222427 100644
--- a/templates/wish/table.html
+++ b/templates/wish/table.html
@@ -5,7 +5,7 @@
<table data-table="{% if all %}true{% endif %}" class="table table-striped align-middle" id="wish">
<thead>
<tr>
- <th data-table-no-sort="true" class="no-sort" scope="col"><i class="ri-image-line fw-normal"></i> Image</th>
+ <th data-table-no-sort-and-search="true" class="no-sort" scope="col"><i class="ri-image-line fw-normal"></i> Image</th>
<th scope="col"><i class="ri-hashtag fw-normal"></i> Set</th>
<th scope="col"><i class="ri-pencil-line fw-normal"></i> Name</th>
<th scope="col"><i class="price-tag-3-line fw-normal"></i> Theme</th>
@@ -13,7 +13,7 @@
<th scope="col"><i class="ri-shapes-line fw-normal"></i> Parts</th>
<th scope="col"><i class="ri-calendar-close-line fw-normal"></i> Retirement</th>
{% if g.login.is_authenticated() %}
- <th data-table-no-sort="true" class="no-sort" scope="col"><i class="ri-settings-4-line fw-normal"></i> Actions</th>
+ <th data-table-no-sort-and-search="true" class="no-sort" scope="col"><i class="ri-settings-4-line fw-normal"></i> Actions</th>
{% endif %}
</tr>
</thead>
From ad24506ab7e7d3d67453ba63b31b7bd918ed30b3 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Tue, 4 Feb 2025 19:56:33 +0100
Subject: [PATCH 150/154] Fix extra comma
---
bricktracker/sql/set/base/base.sql | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/bricktracker/sql/set/base/base.sql b/bricktracker/sql/set/base/base.sql
index 02ef7711..c19ffc85 100644
--- a/bricktracker/sql/set/base/base.sql
+++ b/bricktracker/sql/set/base/base.sql
@@ -29,7 +29,7 @@ SELECT
NULL AS "total_damaged", -- dummy for order: total_damaged
{% endblock %}
{% block total_quantity %}
- NULL AS "total_quantity", -- dummy for order: total_quantity
+ NULL AS "total_quantity" -- dummy for order: total_quantity
{% endblock %}
FROM "bricktracker_sets"
From 64a9e063ec72816f8955da14f9dfcd6657f5b967 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Tue, 4 Feb 2025 20:07:15 +0100
Subject: [PATCH 151/154] Wish requesters
---
CHANGELOG.md | 6 ++
bricktracker/metadata_list.py | 4 +-
bricktracker/record_list.py | 2 +
bricktracker/reload.py | 4 ++
bricktracker/sql/migrations/0013.sql | 7 ++
.../sql/set/metadata/owner/delete.sql | 4 ++
.../sql/set/metadata/owner/insert.sql | 4 ++
bricktracker/sql/wish/base/base.sql | 8 +++
bricktracker/sql/wish/delete/wish.sql | 12 +++-
.../sql/wish/metadata/owner/update/state.sql | 10 +++
bricktracker/views/wish.py | 71 ++++++++++++++++---
bricktracker/wish.py | 19 +++--
bricktracker/wish_list.py | 4 +-
bricktracker/wish_owner.py | 70 ++++++++++++++++++
bricktracker/wish_owner_list.py | 21 ++++++
templates/wish.html | 15 ++++
templates/wish/card.html | 49 +++++++++++++
templates/wish/table.html | 11 ++-
templates/wishes.html | 2 +-
19 files changed, 300 insertions(+), 23 deletions(-)
create mode 100644 bricktracker/sql/wish/metadata/owner/update/state.sql
create mode 100644 bricktracker/wish_owner.py
create mode 100644 bricktracker/wish_owner_list.py
create mode 100644 templates/wish.html
create mode 100644 templates/wish/card.html
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 46c76b54..a6f89a8d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -55,6 +55,9 @@
- Allow for advanced migration scenarios through companion python files
- Add a bunch of the requested fields into the database for future implementation
+- Wish
+ - Requester
+
### UI
- Add
@@ -101,6 +104,9 @@
- Storage list
- Storage content
+- Wish
+ - Requester
+
## 1.1.1: PDF Instructions Download
### Instructions
diff --git a/bricktracker/metadata_list.py b/bricktracker/metadata_list.py
index a7df7521..44bf18fe 100644
--- a/bricktracker/metadata_list.py
+++ b/bricktracker/metadata_list.py
@@ -11,6 +11,7 @@ from .set_purchase_location import BrickSetPurchaseLocation
from .set_status import BrickSetStatus
from .set_storage import BrickSetStorage
from .set_tag import BrickSetTag
+from .wish_owner import BrickWishOwner
logger = logging.getLogger(__name__)
@@ -20,7 +21,8 @@ T = TypeVar(
BrickSetPurchaseLocation,
BrickSetStatus,
BrickSetStorage,
- BrickSetTag
+ BrickSetTag,
+ BrickWishOwner
)
diff --git a/bricktracker/record_list.py b/bricktracker/record_list.py
index 18dcb10c..8d862ed2 100644
--- a/bricktracker/record_list.py
+++ b/bricktracker/record_list.py
@@ -14,6 +14,7 @@ if TYPE_CHECKING:
from .set_storage import BrickSetStorage
from .set_tag import BrickSetTag
from .wish import BrickWish
+ from .wish_owner import BrickWishOwner
T = TypeVar(
'T',
@@ -26,6 +27,7 @@ T = TypeVar(
'BrickSetStorage',
'BrickSetTag',
'BrickWish',
+ 'BrickWishOwner',
'RebrickableSet'
)
diff --git a/bricktracker/reload.py b/bricktracker/reload.py
index 38929f68..99d95bbf 100644
--- a/bricktracker/reload.py
+++ b/bricktracker/reload.py
@@ -6,6 +6,7 @@ from .set_status_list import BrickSetStatusList
from .set_storage_list import BrickSetStorageList
from .set_tag_list import BrickSetTagList
from .theme_list import BrickThemeList
+from .wish_owner_list import BrickWishOwnerList
# Reload everything related to a database after an operation
@@ -35,5 +36,8 @@ def reload() -> None:
# Reload themes
BrickThemeList(force=True)
+
+ # Reload the wish owners
+ BrickWishOwnerList.new(force=True)
except Exception:
pass
diff --git a/bricktracker/sql/migrations/0013.sql b/bricktracker/sql/migrations/0013.sql
index 33f8a6f0..469649d5 100644
--- a/bricktracker/sql/migrations/0013.sql
+++ b/bricktracker/sql/migrations/0013.sql
@@ -16,4 +16,11 @@ CREATE TABLE "bricktracker_set_owners" (
FOREIGN KEY("id") REFERENCES "bricktracker_sets"("id")
);
+-- Create a table for the wish owners
+CREATE TABLE "bricktracker_wish_owners" (
+ "set" TEXT NOT NULL,
+ PRIMARY KEY("set"),
+ FOREIGN KEY("set") REFERENCES "bricktracker_wishes"("set")
+);
+
COMMIT;
diff --git a/bricktracker/sql/set/metadata/owner/delete.sql b/bricktracker/sql/set/metadata/owner/delete.sql
index e9df18d8..5927bfdb 100644
--- a/bricktracker/sql/set/metadata/owner/delete.sql
+++ b/bricktracker/sql/set/metadata/owner/delete.sql
@@ -3,6 +3,10 @@ BEGIN TRANSACTION;
ALTER TABLE "bricktracker_set_owners"
DROP COLUMN "owner_{{ id }}";
+-- Also drop from wishes
+ALTER TABLE "bricktracker_wish_owners"
+DROP COLUMN "owner_{{ id }}";
+
DELETE FROM "bricktracker_metadata_owners"
WHERE "bricktracker_metadata_owners"."id" IS NOT DISTINCT FROM '{{ id }}';
diff --git a/bricktracker/sql/set/metadata/owner/insert.sql b/bricktracker/sql/set/metadata/owner/insert.sql
index cc54a2a5..6b2de775 100644
--- a/bricktracker/sql/set/metadata/owner/insert.sql
+++ b/bricktracker/sql/set/metadata/owner/insert.sql
@@ -3,6 +3,10 @@ BEGIN TRANSACTION;
ALTER TABLE "bricktracker_set_owners"
ADD COLUMN "owner_{{ id }}" BOOLEAN NOT NULL DEFAULT 0;
+-- Also inject into wishes
+ALTER TABLE "bricktracker_wish_owners"
+ADD COLUMN "owner_{{ id }}" BOOLEAN NOT NULL DEFAULT 0;
+
INSERT INTO "bricktracker_metadata_owners" (
"id",
"name"
diff --git a/bricktracker/sql/wish/base/base.sql b/bricktracker/sql/wish/base/base.sql
index b06c66fd..62f5a7cb 100644
--- a/bricktracker/sql/wish/base/base.sql
+++ b/bricktracker/sql/wish/base/base.sql
@@ -5,9 +5,17 @@ SELECT
"bricktracker_wishes"."theme_id",
"bricktracker_wishes"."number_of_parts",
"bricktracker_wishes"."image",
+ {% block owners %}
+ {% if owners %}{{ owners }},{% endif %}
+ {% endblock %}
"bricktracker_wishes"."url"
FROM "bricktracker_wishes"
+{% if owners %}
+LEFT JOIN "bricktracker_wish_owners"
+ON "bricktracker_wishes"."set" IS NOT DISTINCT FROM "bricktracker_wish_owners"."set"
+{% endif %}
+
{% block where %}{% endblock %}
{% if order %}
diff --git a/bricktracker/sql/wish/delete/wish.sql b/bricktracker/sql/wish/delete/wish.sql
index e60b2e48..1adcfc11 100644
--- a/bricktracker/sql/wish/delete/wish.sql
+++ b/bricktracker/sql/wish/delete/wish.sql
@@ -1,2 +1,12 @@
+-- A bit unsafe as it does not use a prepared statement but it
+-- should not be possible to inject anything through the {{ set }} context
+
+BEGIN TRANSACTION;
+
DELETE FROM "bricktracker_wishes"
-WHERE "bricktracker_wishes"."set" IS NOT DISTINCT FROM :set
\ No newline at end of file
+WHERE "bricktracker_wishes"."set" IS NOT DISTINCT FROM '{{ set }}';
+
+DELETE FROM "bricktracker_wish_owners"
+WHERE "bricktracker_wish_owners"."set" IS NOT DISTINCT FROM '{{ set }}';
+
+COMMIT;
\ No newline at end of file
diff --git a/bricktracker/sql/wish/metadata/owner/update/state.sql b/bricktracker/sql/wish/metadata/owner/update/state.sql
new file mode 100644
index 00000000..9191ca8d
--- /dev/null
+++ b/bricktracker/sql/wish/metadata/owner/update/state.sql
@@ -0,0 +1,10 @@
+INSERT INTO "bricktracker_wish_owners" (
+ "set",
+ "{{name}}"
+) VALUES (
+ :set,
+ :state
+)
+ON CONFLICT("set")
+DO UPDATE SET "{{name}}" = :state
+WHERE "bricktracker_wish_owners"."set" IS NOT DISTINCT FROM :set
diff --git a/bricktracker/views/wish.py b/bricktracker/views/wish.py
index b0c763b2..416b9005 100644
--- a/bricktracker/views/wish.py
+++ b/bricktracker/views/wish.py
@@ -1,4 +1,11 @@
-from flask import Blueprint, redirect, render_template, request, url_for
+from flask import (
+ Blueprint,
+ jsonify,
+ redirect,
+ render_template,
+ request,
+ url_for
+)
from flask_login import login_required
from werkzeug.wrappers.response import Response
@@ -6,8 +13,10 @@ from .exceptions import exception_handler
from ..retired_list import BrickRetiredList
from ..wish import BrickWish
from ..wish_list import BrickWishList
+from ..wish_owner_list import BrickWishOwnerList
-wish_page = Blueprint('wish', __name__, url_prefix='/wishlist')
+
+wish_page = Blueprint('wish', __name__, url_prefix='/wishes')
# Index
@@ -18,7 +27,8 @@ def list() -> str:
'wishes.html',
table_collection=BrickWishList().all(),
retired=BrickRetiredList(),
- error=request.args.get('error')
+ error=request.args.get('error'),
+ owners=BrickWishOwnerList.list(),
)
@@ -27,21 +37,60 @@ def list() -> str:
@login_required
@exception_handler(__file__, post_redirect='wish.list')
def add() -> Response:
- # Grab the set number
- number: str = request.form.get('number', '')
+ # Grab the set
+ set: str = request.form.get('set', '')
- if number != '':
- BrickWishList.add(number)
+ if set != '':
+ BrickWishList.add(set)
return redirect(url_for('wish.list'))
-# Delete a set from the wishlit
-@wish_page.route('/delete/<number>', methods=['POST'])
+# Ask for deletion of a wish
+@wish_page.route('/<set>/delete', methods=['GET'])
+@login_required
+@exception_handler(__file__)
+def delete(*, set: str) -> str:
+ return render_template(
+ 'wish.html',
+ delete=True,
+ item=BrickWish().select_specific(set),
+ error=request.args.get('error'),
+ owners=BrickWishOwnerList.list(),
+ )
+
+
+# Actually delete of a set
+@wish_page.route('/<set>/delete', methods=['POST'])
@login_required
@exception_handler(__file__, post_redirect='wish.list')
-def delete(*, number: str) -> Response:
- brickwish = BrickWish().select_specific(number)
+def do_delete(*, set: str) -> Response:
+ brickwish = BrickWish().select_specific(set)
brickwish.delete()
return redirect(url_for('wish.list'))
+
+
+# Details
+@wish_page.route('/<set>/details', methods=['GET'])
+@exception_handler(__file__)
+def details(*, set: str) -> str:
+ return render_template(
+ 'wish.html',
+ item=BrickWish().select_specific(set),
+ retired=BrickRetiredList(),
+ owners=BrickWishOwnerList.list(),
+ )
+
+
+# Change the state of a owner
+@wish_page.route('/<set>/owner/<metadata_id>', methods=['POST'])
+@login_required
+@exception_handler(__file__, json=True)
+def update_owner(*, set: str, metadata_id: str) -> Response:
+ brickwish = BrickWish().select_specific(set)
+ owner = BrickWishOwnerList.get(metadata_id)
+
+ state = owner.update_wish_state(brickwish, json=request.json)
+
+ return jsonify({'value': state})
diff --git a/bricktracker/wish.py b/bricktracker/wish.py
index def41e28..502792fe 100644
--- a/bricktracker/wish.py
+++ b/bricktracker/wish.py
@@ -5,6 +5,7 @@ from flask import url_for
from .exceptions import NotFoundException
from .rebrickable_set import RebrickableSet
from .sql import BrickSQL
+from .wish_owner_list import BrickWishOwnerList
# Lego brick wished set
@@ -16,11 +17,11 @@ class BrickWish(RebrickableSet):
select_query: str = 'wish/select'
insert_query: str = 'wish/insert'
- # Delete a wished set
+ # Delete a wish
def delete(self, /) -> None:
- BrickSQL().execute_and_commit(
+ BrickSQL().executescript(
'wish/delete/wish',
- parameters=self.sql_parameters()
+ set=self.fields.set
)
# Select a specific part (with a set and an id)
@@ -29,7 +30,7 @@ class BrickWish(RebrickableSet):
self.fields.set = set
# Load from database
- if not self.select():
+ if not self.select(owners=BrickWishOwnerList.as_columns()):
raise NotFoundException(
'Wish for set {set} was not found in the database'.format( # noqa: E501
set=self.fields.set,
@@ -38,6 +39,14 @@ class BrickWish(RebrickableSet):
return self
+ # Self url
+ def url(self, /) -> str:
+ return url_for('wish.details', set=self.fields.set)
+
# Deletion url
def url_for_delete(self, /) -> str:
- return url_for('wish.delete', number=self.fields.set)
+ return url_for('wish.delete', set=self.fields.set)
+
+ # Actual deletion url
+ def url_for_do_delete(self, /) -> str:
+ return url_for('wish.do_delete', set=self.fields.set)
diff --git a/bricktracker/wish_list.py b/bricktracker/wish_list.py
index 880021b7..d3038b8a 100644
--- a/bricktracker/wish_list.py
+++ b/bricktracker/wish_list.py
@@ -9,6 +9,7 @@ from .rebrickable import Rebrickable
from .rebrickable_image import RebrickableImage
from .record_list import BrickRecordList
from .wish import BrickWish
+from .wish_owner_list import BrickWishOwnerList
logger = logging.getLogger(__name__)
@@ -22,7 +23,8 @@ class BrickWishList(BrickRecordList[BrickWish]):
def all(self, /) -> Self:
# Load the wished sets from the database
for record in self.select(
- order=current_app.config['WISHES_DEFAULT_ORDER']
+ order=current_app.config['WISHES_DEFAULT_ORDER'],
+ owners=BrickWishOwnerList.as_columns(),
):
brickwish = BrickWish(record=record)
diff --git a/bricktracker/wish_owner.py b/bricktracker/wish_owner.py
new file mode 100644
index 00000000..c31d2c9f
--- /dev/null
+++ b/bricktracker/wish_owner.py
@@ -0,0 +1,70 @@
+import logging
+from typing import Any, TYPE_CHECKING
+
+from flask import url_for
+
+from .exceptions import DatabaseException
+from .metadata import BrickMetadata
+from .sql import BrickSQL
+if TYPE_CHECKING:
+ from .wish import BrickWish
+
+logger = logging.getLogger(__name__)
+
+
+# Lego wish owner metadata
+class BrickWishOwner(BrickMetadata):
+ kind: str = 'owner'
+
+ # Wish state endpoint
+ wish_state_endpoint: str = 'wish.update_owner'
+
+ # Queries
+ update_wish_state_query: str = 'wish/metadata/owner/update/state'
+
+ # Update the selected state of this metadata item for a wish
+ def update_wish_state(
+ self,
+ brickset: 'BrickWish',
+ /,
+ *,
+ json: Any | None = None,
+ state: Any | None = None
+ ) -> Any:
+ if state is None and json is not None:
+ state = json.get('value', False)
+
+ parameters = self.sql_parameters()
+ parameters['set'] = brickset.fields.set
+ parameters['state'] = state
+
+ rows, _ = BrickSQL().execute_and_commit(
+ self.update_wish_state_query,
+ parameters=parameters,
+ name=self.as_column(),
+ )
+
+ if rows != 1:
+ raise DatabaseException('Could not update the {kind} "{name}" state for wish {set}'.format( # noqa: E501
+ kind=self.kind,
+ name=self.fields.name,
+ set=brickset.fields.set,
+ ))
+
+ # Info
+ logger.info('{kind} "{name}" state changed to "{state}" for wish {set}'.format( # noqa: E501
+ kind=self.kind,
+ name=self.fields.name,
+ state=state,
+ set=brickset.fields.set,
+ ))
+
+ return state
+
+ # URL to change the selected state of this metadata item for a wish
+ def url_for_wish_state(self, set: str, /) -> str:
+ return url_for(
+ self.wish_state_endpoint,
+ set=set,
+ metadata_id=self.fields.id
+ )
diff --git a/bricktracker/wish_owner_list.py b/bricktracker/wish_owner_list.py
new file mode 100644
index 00000000..719fc6f5
--- /dev/null
+++ b/bricktracker/wish_owner_list.py
@@ -0,0 +1,21 @@
+from typing import Self
+
+from .metadata_list import BrickMetadataList
+from .wish_owner import BrickWishOwner
+
+
+# Lego sets owner list
+class BrickWishOwnerList(BrickMetadataList[BrickWishOwner]):
+ kind: str = 'wish owners'
+
+ # Database
+ table: str = 'bricktracker_wish_owners'
+ order: str = '"bricktracker_metadata_owners"."name"'
+
+ # Queries
+ select_query = 'set/metadata/owner/list'
+
+ # Instantiate the list with the proper class
+ @classmethod
+ def new(cls, /, *, force: bool = False) -> Self:
+ return cls(BrickWishOwner, force=force)
diff --git a/templates/wish.html b/templates/wish.html
new file mode 100644
index 00000000..6c0f3990
--- /dev/null
+++ b/templates/wish.html
@@ -0,0 +1,15 @@
+{% extends 'base.html' %}
+
+{% block title %} - {% if delete %}Delete a wish{% else %}Wish{% endif %} {{ item.fields.name}} ({{ item.fields.set }}){% endblock %}
+
+{% block main %}
+<div class="container">
+ <div class="row">
+ <div class="col-12">
+ {% with solo=true, delete=delete %}
+ {% include 'wish/card.html' %}
+ {% endwith %}
+ </div>
+ </div>
+</div>
+{% endblock %}
diff --git a/templates/wish/card.html b/templates/wish/card.html
new file mode 100644
index 00000000..fc16d9b9
--- /dev/null
+++ b/templates/wish/card.html
@@ -0,0 +1,49 @@
+{% import 'macro/accordion.html' as accordion %}
+{% import 'macro/badge.html' as badge %}
+{% import 'macro/card.html' as card %}
+{% import 'macro/form.html' as form %}
+
+<div class="card mb-3 flex-fill {% if solo %}card-solo{% endif %}">
+ {{ 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) }}
+ <div class="card-body border-bottom-0 {% if not solo %}p-1{% endif %}">
+ {{ badge.theme(item.theme.name, solo=solo, last=last) }}
+ {{ badge.parts(item.fields.number_of_parts, solo=solo, last=last) }}
+ {% for owner in owners %}
+ {{ badge.owner(item, owner, solo=solo, last=last) }}
+ {% endfor %}
+ </div>
+ {% if solo and g.login.is_authenticated() %}
+ <div class="accordion accordion-flush border-top" id="wish-details">
+ {% if not delete %}
+ {{ accordion.header('Requester', 'owner', 'wish-details', icon='group-line', class='p-0') }}
+ <ul class="list-group list-group-flush">
+ {% if owners | length %}
+ {% for owner in owners %}
+ <li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(owner.fields.name, item.fields.set, owner.as_dataset(), owner.url_for_wish_state(item.fields.set), item.fields[owner.as_column()], delete=delete) }}</li>
+ {% endfor %}
+ {% else %}
+ <li class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No requester found.</li>
+ {% endif %}
+ </ul>
+ <div class="list-group list-group-flush border-top">
+ <a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_owner=true) }}"><i class="ri-settings-4-line"></i> Manage the set owners</a>
+ </div>
+ {{ accordion.footer() }}
+ {% endif %}
+ {{ accordion.header('Danger zone', 'danger-zone', 'wish-details', expanded=delete, danger=true, class='text-end') }}
+ {% if delete %}
+ <form action="{{ item.url_for_do_delete() }}" method="post">
+ {% if error %}<div class="alert alert-danger text-start" role="alert"><strong>Error:</strong> {{ error }}.</div>{% endif %}
+ <div class="alert alert-danger text-center" role="alert">You are about to delete a wish. This action is irreversible.</div>
+ <a class="btn btn-primary" href="{{ item.url() }}" role="button"><i class="ri-arrow-left-long-line"></i> Back to the wish</a>
+ <button type="submit" class="btn btn-danger"><i class="ri-close-line"></i> Delete the wish</button>
+ </form>
+ {% else %}
+ <a href="{{ item.url_for_delete() }}" class="btn btn-danger" role="button"><i class="ri-close-line"></i> Delete the wish</a>
+ {% endif %}
+ {{ accordion.footer() }}
+ </div>
+ <div class="card-footer"></div>
+ {% endif %}
+</div>
diff --git a/templates/wish/table.html b/templates/wish/table.html
index ca222427..41783ca9 100644
--- a/templates/wish/table.html
+++ b/templates/wish/table.html
@@ -12,6 +12,7 @@
<th scope="col"><i class="ri-calendar-line fw-normal"></i> Year</th>
<th scope="col"><i class="ri-shapes-line fw-normal"></i> Parts</th>
<th scope="col"><i class="ri-calendar-close-line fw-normal"></i> Retirement</th>
+ <th data-table-no-sort="true" class="no-sort" scope="col"><i class="ri-user-line fw-normal"></i> Requesters</th>
{% if g.login.is_authenticated() %}
<th data-table-no-sort-and-search="true" class="no-sort" scope="col"><i class="ri-settings-4-line fw-normal"></i> Actions</th>
{% endif %}
@@ -28,11 +29,15 @@
<td>{{ item.fields.year }}</td>
<td>{{ item.fields.number_of_parts }}</td>
<td>{% if retirement_date %}{{ retirement_date }}{% else %}<span class="badge rounded-pill text-bg-light border">Not found</span>{% endif %}</td>
+ <td>
+ {% for owner in owners %}
+ {{ badge.owner(item, owner) }}
+ {% endfor %}
+ </td>
{% if g.login.is_authenticated() %}
<td>
- <form action="{{ item.url_for_delete() }}" method="post">
- <button type="submit" class="btn btn-sm btn-danger"><i class="ri-delete-bin-2-line"></i> Delete</button>
- </form>
+ <a href="{{ item.url() }}" class="btn btn-sm btn-primary mb-1" role="button"><i class="ri-gift-line"></i> Details</a>
+ <a href="{{ item.url_for_delete() }}" class="btn btn-sm btn-danger mb-1" role="button"><i class="ri-delete-bin-2-line"></i> Delete</a>
</td>
{% endif %}
</tr>
diff --git a/templates/wishes.html b/templates/wishes.html
index eac8255f..2c2b2ab9 100644
--- a/templates/wishes.html
+++ b/templates/wishes.html
@@ -12,7 +12,7 @@
<label class="visually-hidden" for="number">Set number</label>
<div class="input-group">
<div class="input-group-text"><i class="ri-hashtag"></i></div>
- <input type="text" class="form-control" id="number" name="number" placeholder="107-1 or 1642-1 or ..." value="" autocomplete="off">
+ <input type="text" class="form-control" id="set" name="set" placeholder="107-1 or 1642-1 or ..." value="" autocomplete="off">
</div>
</div>
<div class="col-12">
From 6eb09643227ebfbcc488c9391acbdccff0b4b66f Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Tue, 4 Feb 2025 21:38:20 +0100
Subject: [PATCH 152/154] Add clear button to the grid search bar
---
static/scripts/grid/filter.js | 8 ++++++++
templates/sets.html | 1 +
2 files changed, 9 insertions(+)
diff --git a/static/scripts/grid/filter.js b/static/scripts/grid/filter.js
index 2825c53a..6de37a87 100644
--- a/static/scripts/grid/filter.js
+++ b/static/scripts/grid/filter.js
@@ -5,6 +5,7 @@ class BrickGridFilter {
// Grid sort elements (built based on the initial id)
this.html_search = document.getElementById(`${this.grid.id}-search`);
+ this.html_search_clear = document.getElementById(`${this.grid.id}-search-clear`);
this.html_filter = document.getElementById(`${this.grid.id}-filter`);
// Search setup
@@ -25,6 +26,13 @@ class BrickGridFilter {
this.html_search.addEventListener("keyup", ((gridfilter) => () => {
gridfilter.filter();
})(this));
+
+ if (this.html_search_clear) {
+ this.html_search_clear.addEventListener("click", ((gridfilter) => () => {
+ this.html_search.value = '';
+ gridfilter.filter();
+ })(this));
+ }
}
// Filters setup
diff --git a/templates/sets.html b/templates/sets.html
index 4832160c..aa598670 100644
--- a/templates/sets.html
+++ b/templates/sets.html
@@ -11,6 +11,7 @@
<div class="input-group">
<span class="input-group-text"><i class="ri-search-line"></i><span class="ms-1 d-none d-md-inline"> Search</span></span>
<input id="grid-search" data-search-exact="name,number,parts,searchPurchaseLocation,searchStorage,theme,year" data-search-list="searchOwner,searchTag" class="form-control form-control-sm" type="text" placeholder="Set, name, number of parts, theme, year, owner, purchase location, storage, tag" value="">
+ <button id="grid-search-clear" type="button" class="btn btn-light btn-outline-danger border"><i class="ri-eraser-line"></i></button>
</div>
</div>
<div class="col-12">
From b6d69e0f10c853761f8f01863f99e18dc32f7647 Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Tue, 4 Feb 2025 23:03:56 +0100
Subject: [PATCH 153/154] Revert the checked state of a checkbox if an error
occured
---
CHANGELOG.md | 3 +++
static/scripts/changer.js | 5 +++++
2 files changed, 8 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a6f89a8d..9a6ba43a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,6 +22,9 @@
### Code
+- Changer
+ - Revert the checked state of a checkbox if an error occured
+
- Form
- Migrate missing input fields to BrickChanger
diff --git a/static/scripts/changer.js b/static/scripts/changer.js
index ffa41ace..a32af76d 100644
--- a/static/scripts/changer.js
+++ b/static/scripts/changer.js
@@ -174,6 +174,11 @@ class BrickChanger {
console.log(error.message);
this.status_error(error.message);
+
+ // Reverse the checked state
+ if (this.html_type == "checkbox") {
+ this.html_element.checked = !this.html_element.checked;
+ }
}
}
}
From a99669d9dcec61684c1df6b0d704f94c97ed523b Mon Sep 17 00:00:00 2001
From: Gregoo <versatile.mailbox@gmail.com>
Date: Tue, 4 Feb 2025 23:05:36 +0100
Subject: [PATCH 154/154] List of sets to be refreshed
---
CHANGELOG.md | 2 +
bricktracker/app.py | 2 +
bricktracker/rebrickable_set.py | 23 +++++++-
bricktracker/rebrickable_set_list.py | 13 +++++
.../sql/rebrickable/set/need_refresh.sql | 53 +++++++++++++++++++
bricktracker/sql_counter.py | 7 +--
bricktracker/views/admin/set.py | 20 +++++++
bricktracker/views/set.py | 15 +++++-
templates/admin.html | 3 ++
templates/admin/refresh.html | 5 ++
templates/admin/set/refresh.html | 34 ++++++++++++
templates/refresh.html | 6 ++-
12 files changed, 175 insertions(+), 8 deletions(-)
create mode 100644 bricktracker/sql/rebrickable/set/need_refresh.sql
create mode 100644 bricktracker/views/admin/set.py
create mode 100644 templates/admin/refresh.html
create mode 100644 templates/admin/set/refresh.html
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9a6ba43a..baad4bde 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -69,6 +69,7 @@
- Admin
- Grey out legacy tables in the database view
- Checkboxes renamed to Set statuses
+ - List of sets that may need to be refreshed
- Cards
- Use macros for badge in the card header
@@ -102,6 +103,7 @@
- Collapsible controls depending on screen size
- Manually collapsible filters (with configuration variable for default state)
- Manually collapsible sort (with configuration variable for default state)
+ - Clear search bar
- Storage
- Storage list
diff --git a/bricktracker/app.py b/bricktracker/app.py
index f0afe429..b4aad9eb 100644
--- a/bricktracker/app.py
+++ b/bricktracker/app.py
@@ -19,6 +19,7 @@ from bricktracker.views.admin.instructions import admin_instructions_page
from bricktracker.views.admin.owner import admin_owner_page
from bricktracker.views.admin.purchase_location import admin_purchase_location_page # noqa: E501
from bricktracker.views.admin.retired import admin_retired_page
+from bricktracker.views.admin.set import admin_set_page
from bricktracker.views.admin.status import admin_status_page
from bricktracker.views.admin.storage import admin_storage_page
from bricktracker.views.admin.tag import admin_tag_page
@@ -90,6 +91,7 @@ def setup_app(app: Flask) -> None:
app.register_blueprint(admin_retired_page)
app.register_blueprint(admin_owner_page)
app.register_blueprint(admin_purchase_location_page)
+ app.register_blueprint(admin_set_page)
app.register_blueprint(admin_status_page)
app.register_blueprint(admin_storage_page)
app.register_blueprint(admin_tag_page)
diff --git a/bricktracker/rebrickable_set.py b/bricktracker/rebrickable_set.py
index fbf10f1a..6beffc2b 100644
--- a/bricktracker/rebrickable_set.py
+++ b/bricktracker/rebrickable_set.py
@@ -1,9 +1,9 @@
import logging
from sqlite3 import Row
import traceback
-from typing import Any, TYPE_CHECKING
+from typing import Any, Self, TYPE_CHECKING
-from flask import current_app
+from flask import current_app, url_for
from .exceptions import ErrorException, NotFoundException
from .instructions import BrickInstructions
@@ -138,6 +138,21 @@ class RebrickableSet(BrickRecord):
return False
+ # Select a specific set (with a set)
+ def select_specific(self, set: str, /) -> Self:
+ # Save the parameters to the fields
+ self.fields.set = set
+
+ # Load from database
+ if not self.select():
+ raise NotFoundException(
+ 'Set with set {set} was not found in the database'.format(
+ set=self.fields.set,
+ ),
+ )
+
+ return self
+
# Return a short form of the Rebrickable set
def short(self, /, *, from_download: bool = False) -> dict[str, Any]:
return {
@@ -164,6 +179,10 @@ class RebrickableSet(BrickRecord):
return ''
+ # Compute the url for the refresh button
+ def url_for_refresh(self, /) -> str:
+ return url_for('set.refresh', set=self.fields.set)
+
# Normalize from Rebrickable
@staticmethod
def from_rebrickable(data: dict[str, Any], /, **_) -> dict[str, Any]:
diff --git a/bricktracker/rebrickable_set_list.py b/bricktracker/rebrickable_set_list.py
index 8fe7ee92..0db84b76 100644
--- a/bricktracker/rebrickable_set_list.py
+++ b/bricktracker/rebrickable_set_list.py
@@ -9,6 +9,7 @@ class RebrickableSetList(BrickRecordList[RebrickableSet]):
# Queries
select_query: str = 'rebrickable/set/list'
+ refresh_query: str = 'rebrickable/set/need_refresh'
# All the sets
def all(self, /) -> Self:
@@ -19,3 +20,15 @@ class RebrickableSetList(BrickRecordList[RebrickableSet]):
self.records.append(rebrickable_set)
return self
+
+ # Sets needing refresh
+ def need_refresh(self, /) -> Self:
+ # Load the sets from the database
+ for record in self.select(
+ override_query=self.refresh_query
+ ):
+ rebrickable_set = RebrickableSet(record=record)
+
+ self.records.append(rebrickable_set)
+
+ return self
diff --git a/bricktracker/sql/rebrickable/set/need_refresh.sql b/bricktracker/sql/rebrickable/set/need_refresh.sql
new file mode 100644
index 00000000..8060a60c
--- /dev/null
+++ b/bricktracker/sql/rebrickable/set/need_refresh.sql
@@ -0,0 +1,53 @@
+SELECT
+ "rebrickable_sets"."set",
+ "rebrickable_sets"."name",
+ "rebrickable_sets"."number_of_parts",
+ "rebrickable_sets"."image",
+ "rebrickable_sets"."url",
+ "null_join"."null_rgb",
+ "null_join"."null_transparent",
+ "null_join"."null_url"
+FROM "rebrickable_sets"
+
+INNER JOIN (
+ SELECT
+ "null_sums"."set",
+ "null_sums"."null_rgb",
+ "null_sums"."null_transparent",
+ "null_sums"."null_url"
+ FROM (
+ SELECT
+ "unique_set_parts"."set",
+ SUM(CASE WHEN "unique_set_parts"."color_rgb" IS NULL THEN 1 ELSE 0 END) AS "null_rgb",
+ SUM(CASE WHEN "unique_set_parts"."color_transparent" IS NULL THEN 1 ELSE 0 END) AS "null_transparent",
+ SUM(CASE WHEN "unique_set_parts"."url" IS NULL THEN 1 ELSE 0 END) AS "null_url"
+ FROM (
+ SELECT
+ "bricktracker_sets"."set",
+ "rebrickable_parts"."color_rgb",
+ "rebrickable_parts"."color_transparent",
+ "rebrickable_parts"."url"
+ FROM "bricktracker_sets"
+
+ INNER JOIN "bricktracker_parts"
+ ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_parts"."id"
+
+ LEFT JOIN "rebrickable_parts"
+ ON "bricktracker_parts"."part" IS NOT DISTINCT FROM "rebrickable_parts"."part"
+ AND "bricktracker_parts"."color" IS NOT DISTINCT FROM "rebrickable_parts"."color_id"
+
+ GROUP BY
+ "bricktracker_sets"."set",
+ "bricktracker_parts"."part",
+ "bricktracker_parts"."color"
+ ) "unique_set_parts"
+
+ GROUP BY "unique_set_parts"."set"
+
+ ) "null_sums"
+
+ WHERE "null_rgb" > 0
+ OR "null_transparent" > 0
+ OR "null_url" > 0
+) "null_join"
+ON "rebrickable_sets"."set" IS NOT DISTINCT FROM "null_join"."set"
diff --git a/bricktracker/sql_counter.py b/bricktracker/sql_counter.py
index e5b92624..4d7a61e8 100644
--- a/bricktracker/sql_counter.py
+++ b/bricktracker/sql_counter.py
@@ -10,11 +10,12 @@ ALIASES: dict[str, Tuple[str, str]] = {
'bricktracker_minifigures': ('Bricktracker minifigures', 'group-line'),
'bricktracker_parts': ('Bricktracker parts', 'shapes-line'),
'bricktracker_set_checkboxes': ('Bricktracker set checkboxes (legacy)', 'checkbox-line'), # noqa: E501
- 'bricktracker_set_owners': ('Bricktracker set owners', 'checkbox-line'), # noqa: E501
- 'bricktracker_set_statuses': ('Bricktracker set statuses', 'user-line'), # noqa: E501
- 'bricktracker_set_tags': ('Bricktracker set tags', 'price-tag-2-line'), # noqa: E501
+ 'bricktracker_set_owners': ('Bricktracker set owners', 'checkbox-line'),
+ 'bricktracker_set_statuses': ('Bricktracker set statuses', 'user-line'),
+ 'bricktracker_set_tags': ('Bricktracker set tags', 'price-tag-2-line'),
'bricktracker_sets': ('Bricktracker sets', 'hashtag'),
'bricktracker_wishes': ('Bricktracker wishes', 'gift-line'),
+ 'bricktracker_wish_owners': ('Bricktracker wish owners', 'checkbox-line'),
'inventory': ('Parts', 'shapes-line'),
'inventory_old': ('Parts (legacy)', 'shapes-line'),
'minifigures': ('Minifigures', 'group-line'),
diff --git a/bricktracker/views/admin/set.py b/bricktracker/views/admin/set.py
new file mode 100644
index 00000000..6f00910c
--- /dev/null
+++ b/bricktracker/views/admin/set.py
@@ -0,0 +1,20 @@
+from flask import Blueprint, render_template, request
+from flask_login import login_required
+
+from ..exceptions import exception_handler
+from ...rebrickable_set_list import RebrickableSetList
+
+admin_set_page = Blueprint('admin_set', __name__, url_prefix='/admin/set')
+
+
+# Sets that need o be refreshed
+@admin_set_page.route('/refresh', methods=['GET'])
+@login_required
+@exception_handler(__file__)
+def refresh() -> str:
+ return render_template(
+ 'admin.html',
+ refresh_set=True,
+ table_collection=RebrickableSetList().need_refresh(),
+ set_error=request.args.get('set_error')
+ )
diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py
index bb9e845c..1ffec550 100644
--- a/bricktracker/views/set.py
+++ b/bricktracker/views/set.py
@@ -13,8 +13,10 @@ from flask_login import login_required
from werkzeug.wrappers.response import Response
from .exceptions import exception_handler
+from ..exceptions import ErrorException
from ..minifigure import BrickMinifigure
from ..part import BrickPart
+from ..rebrickable_set import RebrickableSet
from ..set import BrickSet
from ..set_list import BrickSetList, set_metadata_lists
from ..set_owner_list import BrickSetOwnerList
@@ -241,13 +243,22 @@ def problem_part(
# Refresh a set
+@set_page.route('/refresh/<set>/', methods=['GET'])
@set_page.route('/<id>/refresh', methods=['GET'])
@login_required
@exception_handler(__file__)
-def refresh(*, id: str) -> str:
+def refresh(*, id: str | None = None, set: str | None = None) -> str:
+ if id is not None:
+ item = BrickSet().select_specific(id)
+ elif set is not None:
+ item = RebrickableSet().select_specific(set)
+ else:
+ raise ErrorException('Could not load any set to refresh')
+
return render_template(
'refresh.html',
- item=BrickSet().select_specific(id),
+ id=id,
+ item=item,
path=current_app.config['SOCKET_PATH'],
namespace=current_app.config['SOCKET_NAMESPACE'],
messages=MESSAGES
diff --git a/templates/admin.html b/templates/admin.html
index 87dbdb4a..2da5000d 100644
--- a/templates/admin.html
+++ b/templates/admin.html
@@ -32,6 +32,8 @@
{% include 'admin/database/import.html' %}
{% elif upgrade_database %}
{% include 'admin/database/upgrade.html' %}
+ {% elif refresh_set %}
+ {% include 'admin/set/refresh.html' %}
{% else %}
{% include 'admin/logout.html' %}
{% include 'admin/instructions.html' %}
@@ -47,6 +49,7 @@
{% include 'admin/storage.html' %}
{% include 'admin/tag.html' %}
{{ accordion.footer() }}
+ {% include 'admin/refresh.html' %}
{% include 'admin/database.html' %}
{% include 'admin/configuration.html' %}
{% endif %}
diff --git a/templates/admin/refresh.html b/templates/admin/refresh.html
new file mode 100644
index 00000000..ab81cc82
--- /dev/null
+++ b/templates/admin/refresh.html
@@ -0,0 +1,5 @@
+{% import 'macro/accordion.html' as accordion %}
+
+{{ accordion.header('Set refresh', 'refresh', 'admin', icon='refresh-line') }}
+<a href="{{ url_for('admin_set.refresh') }}" class="btn btn-primary" role="button"><i class="ri-refresh-line"></i> Check for sets that may need a refresh</a>
+{{ accordion.footer() }}
diff --git a/templates/admin/set/refresh.html b/templates/admin/set/refresh.html
new file mode 100644
index 00000000..0f4befcc
--- /dev/null
+++ b/templates/admin/set/refresh.html
@@ -0,0 +1,34 @@
+{% import 'macro/table.html' as table %}
+{% import 'macro/badge.html' as badge %}
+
+<div class="alert alert-info m-2" role="alert">This page lists the sets that may need a refresh because they have some of their newer fields containing empty values.</div>
+<div class="table-responsive-sm">
+ <table data-table="true" class="table table-striped align-middle" id="wish">
+ <thead>
+ <tr>
+ <th data-table-no-sort-and-search="true" class="no-sort" scope="col"><i class="ri-image-line fw-normal"></i> Image</th>
+ <th scope="col"><i class="ri-hashtag fw-normal"></i> Set</th>
+ <th scope="col"><i class="ri-pencil-line fw-normal"></i> Name</th>
+ <th scope="col"><i class="ri-shapes-line fw-normal"></i> Parts</th>
+ <th data-table-number="true" scope="col"><i class="ri-error-warning-line fw-normal"></i> Empty RGB</th>
+ <th data-table-number="true" scope="col"><i class="ri-error-warning-line fw-normal"></i> Empty transparent</th>
+ <th data-table-number="true" scope="col"><i class="ri-error-warning-line fw-normal"></i> Empty URL</th>
+ <th data-table-no-sort-and-search="true" class="no-sort" scope="col"><i class="ri-settings-4-line fw-normal"></i> Actions</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for item in table_collection %}
+ <tr>
+ {{ table.image(item.url_for_image(), caption=item.fields.name, alt=item.fields.set) }}
+ <td>{{ item.fields.set }} {{ table.rebrickable(item) }}</td>
+ <td>{{ item.fields.name }}</td>
+ <td>{{ item.fields.number_of_parts }}</td>
+ <td>{{ item.fields.null_rgb }}</td>
+ <td>{{ item.fields.null_transparent }}</td>
+ <td>{{ item.fields.null_url }}</td>
+ <td><a href="{{ item.url_for_refresh() }}" class="btn btn-primary" role="button"><i class="ri-refresh-line"></i> Refresh</a></td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+</div>
\ No newline at end of file
diff --git a/templates/refresh.html b/templates/refresh.html
index 5add93d8..3a9804de 100644
--- a/templates/refresh.html
+++ b/templates/refresh.html
@@ -51,7 +51,11 @@
</div>
<div class="card-footer text-end">
<span id="refresh-status-icon" class="me-1"></span><span id="refresh-status" class="me-1"></span>
- <a href="{{ url_for('set.details', id=item.fields.id) }}" class="btn btn-primary" role="button"><i class="ri-hashtag"></i> Back to the set details</a>
+ {% if id %}
+ <a href="{{ url_for('set.details', id=item.fields.id) }}" class="btn btn-primary" role="button"><i class="ri-hashtag"></i> Back to the set details</a>
+ {% else %}
+ <a href="{{ url_for('admin_set.refresh') }}" class="btn btn-danger" role="button"><i class="ri-hashtag"></i> List of sets to be refreshed</a>
+ {% endif %}
<button id="refresh" type="button" class="btn btn-primary"><i class="ri-refresh-line"></i> Refresh</button>
</div>
</div>