From d0630627802c4bf1a169ba2bf9ae6ccbfe13da1d Mon Sep 17 00:00:00 2001 From: Gregoo Date: Fri, 24 Jan 2025 10:36:24 +0100 Subject: [PATCH] Separate bricktracker sets from rebrickable sets (dedup), introduce custom checkboxes --- .env.sample | 32 +-- bricktracker/config.py | 4 +- bricktracker/instructions.py | 10 +- bricktracker/instructions_list.py | 41 ++- bricktracker/minifigure.py | 10 +- bricktracker/minifigure_list.py | 4 +- bricktracker/part.py | 8 +- bricktracker/part_list.py | 4 +- bricktracker/rebrickable.py | 32 ++- bricktracker/rebrickable_image.py | 12 +- bricktracker/rebrickable_minifigures.py | 12 +- bricktracker/rebrickable_parts.py | 2 +- bricktracker/rebrickable_set.py | 237 ++++++++-------- bricktracker/rebrickable_set_list.py | 21 ++ bricktracker/record_list.py | 14 +- bricktracker/reload.py | 4 + bricktracker/set.py | 265 ++++++++---------- bricktracker/set_checkbox.py | 142 ++++++++++ bricktracker/set_checkbox_list.py | 74 +++++ bricktracker/set_list.py | 17 +- bricktracker/socket.py | 6 +- bricktracker/sql/checkbox/add.sql | 16 ++ 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/name.sql | 3 + bricktracker/sql/checkbox/update/status.sql | 3 + bricktracker/sql/migrations/0001.sql | 2 +- bricktracker/sql/migrations/0002.sql | 2 +- bricktracker/sql/migrations/0003.sql | 48 ++++ bricktracker/sql/migrations/0004.sql | 25 ++ bricktracker/sql/migrations/0005.sql | 72 +++++ bricktracker/sql/migrations/0006.sql | 42 +++ .../sql/minifigure/delete/all_from_set.sql | 2 - .../sql/missing/delete/all_from_set.sql | 2 - bricktracker/sql/part/delete/all_from_set.sql | 2 - bricktracker/sql/part/list/all.sql | 6 +- bricktracker/sql/rebrickable/set/insert.sql | 23 ++ bricktracker/sql/rebrickable/set/list.sql | 11 + bricktracker/sql/rebrickable/set/select.sql | 13 + bricktracker/sql/schema/drop.sql | 15 +- bricktracker/sql/set/base/base.sql | 38 +++ bricktracker/sql/set/base/full.sql | 42 +++ bricktracker/sql/set/base/light.sql | 18 ++ bricktracker/sql/set/base/select.sql | 52 ---- bricktracker/sql/set/delete/set.sql | 23 +- bricktracker/sql/set/insert.sql | 30 +- bricktracker/sql/set/list/all.sql | 2 +- bricktracker/sql/set/list/generic.sql | 14 +- .../sql/set/list/missing_minifigure.sql | 4 +- bricktracker/sql/set/list/missing_part.sql | 4 +- .../sql/set/list/using_minifigure.sql | 4 +- bricktracker/sql/set/list/using_part.sql | 4 +- bricktracker/sql/set/select.sql | 13 - bricktracker/sql/set/select/full.sql | 13 + bricktracker/sql/set/select/light.sql | 5 + bricktracker/sql/set/update/status.sql | 10 + bricktracker/sql/set/update_checked.sql | 3 - bricktracker/sql/wish/base/base.sql | 19 ++ bricktracker/sql/wish/base/select.sql | 20 -- bricktracker/sql/wish/delete/wish.sql | 4 +- bricktracker/sql/wish/insert.sql | 20 +- bricktracker/sql/wish/list/all.sql | 2 +- bricktracker/sql/wish/select.sql | 4 +- bricktracker/version.py | 2 +- bricktracker/views/admin/checkbox.py | 98 +++++++ bricktracker/views/index.py | 2 + bricktracker/views/set.py | 87 ++---- bricktracker/wish.py | 54 +--- bricktracker/wish_list.py | 31 +- static/scripts/changer.js | 127 +++++++++ static/scripts/set.js | 54 ---- static/scripts/socket.js | 38 +-- templates/add.html | 2 +- templates/admin.html | 9 +- templates/admin/checkbox.html | 67 +++++ templates/admin/checkbox/delete.html | 25 ++ templates/delete.html | 2 +- templates/instructions/delete.html | 4 +- templates/instructions/rename.html | 4 +- templates/instructions/table.html | 8 +- templates/macro/form.html | 14 +- templates/set.html | 4 +- templates/set/card.html | 40 ++- templates/set/mini.html | 8 +- templates/sets.html | 10 +- templates/wish/table.html | 10 +- 88 files changed, 1560 insertions(+), 748 deletions(-) create mode 100644 bricktracker/rebrickable_set_list.py create mode 100644 bricktracker/set_checkbox.py create mode 100644 bricktracker/set_checkbox_list.py create mode 100644 bricktracker/sql/checkbox/add.sql create mode 100644 bricktracker/sql/checkbox/base.sql create mode 100644 bricktracker/sql/checkbox/delete.sql create mode 100644 bricktracker/sql/checkbox/list.sql create mode 100644 bricktracker/sql/checkbox/select.sql create mode 100644 bricktracker/sql/checkbox/update/name.sql create mode 100644 bricktracker/sql/checkbox/update/status.sql create mode 100644 bricktracker/sql/migrations/0003.sql create mode 100644 bricktracker/sql/migrations/0004.sql create mode 100644 bricktracker/sql/migrations/0005.sql create mode 100644 bricktracker/sql/migrations/0006.sql delete mode 100644 bricktracker/sql/minifigure/delete/all_from_set.sql delete mode 100644 bricktracker/sql/missing/delete/all_from_set.sql delete mode 100644 bricktracker/sql/part/delete/all_from_set.sql create mode 100644 bricktracker/sql/rebrickable/set/insert.sql create mode 100644 bricktracker/sql/rebrickable/set/list.sql create mode 100644 bricktracker/sql/rebrickable/set/select.sql create mode 100644 bricktracker/sql/set/base/base.sql create mode 100644 bricktracker/sql/set/base/full.sql create mode 100644 bricktracker/sql/set/base/light.sql delete mode 100644 bricktracker/sql/set/base/select.sql delete mode 100644 bricktracker/sql/set/select.sql create mode 100644 bricktracker/sql/set/select/full.sql create mode 100644 bricktracker/sql/set/select/light.sql create mode 100644 bricktracker/sql/set/update/status.sql delete mode 100644 bricktracker/sql/set/update_checked.sql create mode 100644 bricktracker/sql/wish/base/base.sql delete mode 100644 bricktracker/sql/wish/base/select.sql create mode 100644 bricktracker/views/admin/checkbox.py create mode 100644 static/scripts/changer.js create mode 100644 templates/admin/checkbox.html create mode 100644 templates/admin/checkbox/delete.html diff --git a/.env.sample b/.env.sample index 5c9144c..f68281e 100644 --- a/.env.sample +++ b/.env.sample @@ -202,16 +202,16 @@ # Optional: Change the default order of sets. By default ordered by insertion order. # Useful column names for this option are: -# - sets.set_num: set number as a string -# - sets.name: set name -# - sets.year: set release year -# - sets.num_parts: set number of parts -# - set_number: the number part of set_num as an integer -# - set_version: the version part of set_num as an integer -# - total_missing: number of missing parts -# - total_minifigures: number of minifigures -# Default: set_number DESC, set_version ASC -# BK_SETS_DEFAULT_ORDER=sets.year ASC +# - "rebrickable_sets"."set": set number as a string +# - "rebrickable_sets"."number": the number part of set as an integer +# - "rebrickable_sets"."version": the version part of set as an integer +# - "rebrickable_sets"."name": set name +# - "rebrickable_sets"."year": set release year +# - "rebrickable_sets"."number_of_parts": set number of parts +# - "total_missing": number of missing parts +# - "total_minifigures": number of minifigures +# Default: "rebrickable_sets"."number" DESC, "rebrickable_sets"."version" ASC +# BK_SETS_DEFAULT_ORDER="rebrickable_sets"."year" ASC # Optional: Folder where to store the sets images, relative to the '/app/static/' folder # Default: sets @@ -250,9 +250,9 @@ # Optional: Change the default order of sets. By default ordered by insertion order. # Useful column names for this option are: -# - wishlist.set_num: set number as a string -# - wishlist.name: set name -# - wishlist.year: set release year -# - wishlist.num_parts: set number of parts -# Default: wishlist.rowid DESC -# BK_WISHES_DEFAULT_ORDER=set_number DESC, set_version ASC +# - "bricktracker_wishes"."set": set number as a string +# - "bricktracker_wishes"."name": set name +# - "bricktracker_wishes"."year": set release year +# - "bricktracker_wishes"."number_of_parts": set number of parts +# Default: "bricktracker_wishes"."rowid" DESC +# BK_WISHES_DEFAULT_ORDER="bricktracker_wishes"."set" DESC diff --git a/bricktracker/config.py b/bricktracker/config.py index 740f7b4..6f5a7c8 100644 --- a/bricktracker/config.py +++ b/bricktracker/config.py @@ -48,7 +48,7 @@ CONFIG: Final[list[dict[str, Any]]] = [ {'n': 'REBRICKABLE_PAGE_SIZE', 'd': 100, 'c': int}, {'n': 'RETIRED_SETS_FILE_URL', 'd': 'https://docs.google.com/spreadsheets/d/1rlYfEXtNKxUOZt2Mfv0H17DvK7bj6Pe0CuYwq6ay8WA/gviz/tq?tqx=out:csv&sheet=Sorted%20by%20Retirement%20Date'}, # noqa: E501 {'n': 'RETIRED_SETS_PATH', 'd': './retired_sets.csv'}, - {'n': 'SETS_DEFAULT_ORDER', 'd': 'set_number DESC, set_version ASC'}, + {'n': 'SETS_DEFAULT_ORDER', 'd': '"rebrickable_sets"."number" DESC, "rebrickable_sets"."version" ASC'}, # noqa: E501 {'n': 'SETS_FOLDER', 'd': 'sets', 's': True}, {'n': 'SKIP_SPARE_PARTS', 'c': bool}, {'n': 'SOCKET_NAMESPACE', 'd': 'bricksocket'}, @@ -57,5 +57,5 @@ CONFIG: Final[list[dict[str, Any]]] = [ {'n': 'THEMES_PATH', 'd': './themes.csv'}, {'n': 'TIMEZONE', 'd': 'Etc/UTC'}, {'n': 'USE_REMOTE_IMAGES', 'c': bool}, - {'n': 'WISHES_DEFAULT_ORDER', 'd': 'wishlist.rowid DESC'}, + {'n': 'WISHES_DEFAULT_ORDER', 'd': '"bricktracker_wishes"."rowid" DESC'}, ] diff --git a/bricktracker/instructions.py b/bricktracker/instructions.py index daf2c35..8f4b4fc 100644 --- a/bricktracker/instructions.py +++ b/bricktracker/instructions.py @@ -10,18 +10,18 @@ from werkzeug.utils import secure_filename from .exceptions import ErrorException if TYPE_CHECKING: - from .set import BrickSet + from .rebrickable_set import RebrickableSet logger = logging.getLogger(__name__) class BrickInstructions(object): allowed: bool - brickset: 'BrickSet | None' + rebrickable: 'RebrickableSet | None' extension: str filename: str mtime: datetime - number: 'str | None' + set: 'str | None' name: str size: int @@ -42,8 +42,8 @@ class BrickInstructions(object): self.allowed = self.extension in current_app.config['INSTRUCTIONS_ALLOWED_EXTENSIONS'] # noqa: E501 # Placeholder - self.brickset = None - self.number = None + self.rebrickable = None + self.set = None # Extract the set number if self.allowed: diff --git a/bricktracker/instructions_list.py b/bricktracker/instructions_list.py index c60b742..6fc364d 100644 --- a/bricktracker/instructions_list.py +++ b/bricktracker/instructions_list.py @@ -1,11 +1,14 @@ import logging import os -from typing import Generator +from typing import Generator, TYPE_CHECKING from flask import current_app from .exceptions import NotFoundException from .instructions import BrickInstructions +from .rebrickable_set_list import RebrickableSetList +if TYPE_CHECKING: + from .rebrickable_set import RebrickableSet logger = logging.getLogger(__name__) @@ -46,46 +49,40 @@ class BrickInstructionsList(object): BrickInstructionsList.all[instruction.filename] = instruction # noqa: E501 if instruction.allowed: - if instruction.number: + if instruction.set: # Instantiate the list if not existing yet - if instruction.number not in BrickInstructionsList.sets: # noqa: E501 - BrickInstructionsList.sets[instruction.number] = [] # noqa: E501 + if instruction.set not in BrickInstructionsList.sets: # noqa: E501 + BrickInstructionsList.sets[instruction.set] = [] # noqa: E501 - BrickInstructionsList.sets[instruction.number].append(instruction) # noqa: E501 + BrickInstructionsList.sets[instruction.set].append(instruction) # noqa: E501 BrickInstructionsList.sets_total += 1 else: BrickInstructionsList.unknown_total += 1 else: BrickInstructionsList.rejected_total += 1 - # Associate bricksets - # Not ideal, to avoid a circular import - from .set import BrickSet - from .set_list import BrickSetList - - # Grab the generic list of sets - bricksets: dict[str, BrickSet] = {} - for brickset in BrickSetList().generic().records: - bricksets[brickset.fields.set_num] = brickset + # List of Rebrickable sets + rebrickable_sets: dict[str, RebrickableSet] = {} + for rebrickable_set in RebrickableSetList().all(): + rebrickable_sets[rebrickable_set.fields.set] = rebrickable_set # noqa: E501 # Inject the brickset if it exists for instruction in self.all.values(): if ( instruction.allowed and - instruction.number is not None and - instruction.brickset is None and - instruction.number in bricksets + instruction.set is not None and + instruction.rebrickable is None and + instruction.set in rebrickable_sets ): - instruction.brickset = bricksets[instruction.number] - + instruction.rebrickable = rebrickable_sets[instruction.set] # noqa: E501 # Ignore errors except Exception: pass # Grab instructions for a set - def get(self, number: str) -> list[BrickInstructions]: - if number in self.sets: - return self.sets[number] + def get(self, set: str) -> list[BrickInstructions]: + if set in self.sets: + return self.sets[set] else: return [] diff --git a/bricktracker/minifigure.py b/bricktracker/minifigure.py index e3b01d8..0ad55b1 100644 --- a/bricktracker/minifigure.py +++ b/bricktracker/minifigure.py @@ -81,7 +81,7 @@ class BrickMinifigure(BrickRecord): raise NotFoundException( 'Minifigure with number {number} from set {set} was not found in the database'.format( # noqa: E501 number=self.fields.fig_num, - set=self.brickset.fields.set_num, + set=self.brickset.fields.set, ), ) @@ -94,10 +94,10 @@ class BrickMinifigure(BrickRecord): # Supplement from the brickset if self.brickset is not None: if 'u_id' not in parameters: - parameters['u_id'] = self.brickset.fields.u_id + parameters['u_id'] = self.brickset.fields.id if 'set_num' not in parameters: - parameters['set_num'] = self.brickset.fields.set_num + parameters['set_num'] = self.brickset.fields.set return parameters @@ -152,7 +152,7 @@ class BrickMinifigure(BrickRecord): } if brickset is not None: - record['set_num'] = brickset.fields.set_num - record['u_id'] = brickset.fields.u_id + 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 dfebc16..04ece73 100644 --- a/bricktracker/minifigure_list.py +++ b/bricktracker/minifigure_list.py @@ -78,8 +78,8 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): parameters: dict[str, Any] = super().sql_parameters() if self.brickset is not None: - parameters['u_id'] = self.brickset.fields.u_id - parameters['set_num'] = self.brickset.fields.set_num + parameters['u_id'] = self.brickset.fields.id + parameters['set_num'] = self.brickset.fields.set return parameters diff --git a/bricktracker/part.py b/bricktracker/part.py index d6b39a1..80a51bd 100644 --- a/bricktracker/part.py +++ b/bricktracker/part.py @@ -121,7 +121,7 @@ class BrickPart(BrickRecord): raise NotFoundException( 'Part with ID {id} from set {set} was not found in the database'.format( # noqa: E501 id=self.fields.id, - set=self.brickset.fields.set_num, + set=self.brickset.fields.set, ), ) @@ -133,14 +133,14 @@ class BrickPart(BrickRecord): # Supplement from the brickset if 'u_id' not in parameters and self.brickset is not None: - parameters['u_id'] = self.brickset.fields.u_id + 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.fig_num elif self.brickset is not None: - parameters['set_num'] = self.brickset.fields.set_num + parameters['set_num'] = self.brickset.fields.set return parameters @@ -263,7 +263,7 @@ class BrickPart(BrickRecord): } if brickset is not None: - record['u_id'] = brickset.fields.u_id + record['u_id'] = brickset.fields.id if minifigure is not None: record['set_num'] = data['fig_num'] diff --git a/bricktracker/part_list.py b/bricktracker/part_list.py index 45a927f..93897f8 100644 --- a/bricktracker/part_list.py +++ b/bricktracker/part_list.py @@ -115,13 +115,13 @@ class BrickPartList(BrickRecordList[BrickPart]): # Set id if self.brickset is not None: - parameters['u_id'] = self.brickset.fields.u_id + parameters['u_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.fig_num elif self.brickset is not None: - parameters['set_num'] = self.brickset.fields.set_num + parameters['set_num'] = self.brickset.fields.set return parameters diff --git a/bricktracker/rebrickable.py b/bricktracker/rebrickable.py index 16b947a..aa69c55 100644 --- a/bricktracker/rebrickable.py +++ b/bricktracker/rebrickable.py @@ -9,11 +9,12 @@ from .exceptions import NotFoundException, ErrorException if TYPE_CHECKING: from .minifigure import BrickMinifigure from .part import BrickPart + from .rebrickable_set import RebrickableSet from .set import BrickSet from .socket import BrickSocket from .wish import BrickWish -T = TypeVar('T', 'BrickSet', 'BrickPart', 'BrickMinifigure', 'BrickWish') +T = TypeVar('T', 'RebrickableSet', 'BrickPart', 'BrickMinifigure', 'BrickWish') # An helper around the rebrick library, autoconverting @@ -23,10 +24,11 @@ class Rebrickable(Generic[T]): number: str model: Type[T] - socket: 'BrickSocket | None' brickset: 'BrickSet | None' - minifigure: 'BrickMinifigure | None' + instance: T | None kind: str + minifigure: 'BrickMinifigure | None' + socket: 'BrickSocket | None' def __init__( self, @@ -35,9 +37,10 @@ class Rebrickable(Generic[T]): model: Type[T], /, *, - socket: 'BrickSocket | None' = None, brickset: 'BrickSet | None' = None, - minifigure: 'BrickMinifigure | None' = None + instance: T | None = None, + minifigure: 'BrickMinifigure | None' = None, + socket: 'BrickSocket | None' = None, ): if not hasattr(lego, method): raise ErrorException('{method} is not a valid method for the rebrick.lego module'.format( # noqa: E501 @@ -49,9 +52,10 @@ class Rebrickable(Generic[T]): self.number = number self.model = model - self.socket = socket self.brickset = brickset + self.instance = instance self.minifigure = minifigure + self.socket = socket if self.minifigure is not None: self.kind = 'Minifigure' @@ -62,13 +66,15 @@ class Rebrickable(Generic[T]): def get(self, /) -> T: model_parameters = self.model_parameters() - return self.model( - **model_parameters, - record=self.model.from_rebrickable( - self.load(), - brickset=self.brickset, - ), - ) + if self.instance is None: + self.instance = self.model(**model_parameters) + + self.instance.ingest(self.model.from_rebrickable( + self.load(), + brickset=self.brickset, + )) + + return self.instance # Get paginated elements from the Rebrickable API def list(self, /) -> list[T]: diff --git a/bricktracker/rebrickable_image.py b/bricktracker/rebrickable_image.py index eee3441..0a0d9f4 100644 --- a/bricktracker/rebrickable_image.py +++ b/bricktracker/rebrickable_image.py @@ -10,12 +10,12 @@ from .exceptions import DownloadException if TYPE_CHECKING: from .minifigure import BrickMinifigure from .part import BrickPart - from .set import BrickSet + from .rebrickable_set import RebrickableSet # A set, part or minifigure image from Rebrickable class RebrickableImage(object): - brickset: 'BrickSet' + set: 'RebrickableSet' minifigure: 'BrickMinifigure | None' part: 'BrickPart | None' @@ -23,14 +23,14 @@ class RebrickableImage(object): def __init__( self, - brickset: 'BrickSet', + set: 'RebrickableSet', /, *, minifigure: 'BrickMinifigure | None' = None, part: 'BrickPart | None' = None, ): # Save all objects - self.brickset = brickset + self.set = set self.minifigure = minifigure self.part = part @@ -92,7 +92,7 @@ class RebrickableImage(object): else: return self.minifigure.fields.fig_num - return self.brickset.fields.set_num + return self.set.fields.set # Return the path depending on the objects provided def path(self, /) -> str: @@ -116,7 +116,7 @@ class RebrickableImage(object): else: return self.minifigure.fields.set_img_url - return self.brickset.fields.set_img_url + return self.set.fields.image # Return the name of the nil image file @staticmethod diff --git a/bricktracker/rebrickable_minifigures.py b/bricktracker/rebrickable_minifigures.py index bcf165b..eb72e06 100644 --- a/bricktracker/rebrickable_minifigures.py +++ b/bricktracker/rebrickable_minifigures.py @@ -30,18 +30,18 @@ class RebrickableMinifigures(object): def download(self, /) -> None: self.socket.auto_progress( message='Set {number}: loading minifigures from Rebrickable'.format( # noqa: E501 - number=self.brickset.fields.set_num, + number=self.brickset.fields.set, ), increment_total=True, ) - logger.debug('rebrick.lego.get_set_minifigs("{set_num}")'.format( - set_num=self.brickset.fields.set_num, + logger.debug('rebrick.lego.get_set_minifigs("{set}")'.format( + set=self.brickset.fields.set, )) minifigures = Rebrickable[BrickMinifigure]( 'get_set_minifigs', - self.brickset.fields.set_num, + self.brickset.fields.set, BrickMinifigure, socket=self.socket, brickset=self.brickset, @@ -53,7 +53,7 @@ class RebrickableMinifigures(object): # 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_num, + number=self.brickset.fields.set, current=index+1, total=total, ) @@ -65,7 +65,7 @@ class RebrickableMinifigures(object): # Grab the image self.socket.progress( message='Set {number}: downloading minifigure {current}/{total} image'.format( # noqa: E501 - number=self.brickset.fields.set_num, + number=self.brickset.fields.set, current=index+1, total=total, ) diff --git a/bricktracker/rebrickable_parts.py b/bricktracker/rebrickable_parts.py index b2f252f..69c42dc 100644 --- a/bricktracker/rebrickable_parts.py +++ b/bricktracker/rebrickable_parts.py @@ -44,7 +44,7 @@ class RebrickableParts(object): self.kind = 'Minifigure' self.method = 'get_minifig_elements' else: - self.number = self.brickset.fields.set_num + self.number = self.brickset.fields.set self.kind = 'Set' self.method = 'get_set_elements' diff --git a/bricktracker/rebrickable_set.py b/bricktracker/rebrickable_set.py index 366bef0..1d3ffb1 100644 --- a/bricktracker/rebrickable_set.py +++ b/bricktracker/rebrickable_set.py @@ -1,103 +1,88 @@ import logging +from sqlite3 import Row import traceback from typing import Any, TYPE_CHECKING -from uuid import uuid4 from flask import current_app from .exceptions import ErrorException, NotFoundException +from .instructions import BrickInstructions from .rebrickable import Rebrickable from .rebrickable_image import RebrickableImage -from .rebrickable_minifigures import RebrickableMinifigures -from .rebrickable_parts import RebrickableParts -from .set import BrickSet -from .sql import BrickSQL -from .wish import BrickWish +from .record import BrickRecord +from .theme_list import BrickThemeList if TYPE_CHECKING: from .socket import BrickSocket + from .theme import BrickTheme logger = logging.getLogger(__name__) # A set from Rebrickable -class RebrickableSet(object): +class RebrickableSet(BrickRecord): socket: 'BrickSocket' + theme: 'BrickTheme' + instructions: list[BrickInstructions] + + # Flags + resolve_instructions: bool = True + + # Queries + select_query: str = 'rebrickable/set/select' + insert_query: str = 'rebrickable/set/insert' + + def __init__( + self, + /, + *, + socket: 'BrickSocket | None' = None, + record: Row | dict[str, Any] | None = None + ): + super().__init__() + + # Placeholders + self.instructions = [] - def __init__(self, socket: 'BrickSocket', /): # Save the socket - self.socket = socket + if socket is not None: + self.socket = socket + + # Ingest the record if it has one + if record is not None: + self.ingest(record) # Import the set from Rebrickable - def download(self, data: dict[str, Any], /) -> None: - # Reset the progress - self.socket.progress_count = 0 - self.socket.progress_total = 0 - - # Load the set - brickset = self.load(data, from_download=True) - - # None brickset means loading failed - if brickset is None: - return - - try: - # Insert into the database - self.socket.auto_progress( - message='Set {number}: inserting into database'.format( - number=brickset.fields.set_num - ), - increment_total=True, - ) - - # Assign a unique ID to the set - brickset.fields.u_id = str(uuid4()) - - # Insert into database - brickset.insert(commit=False) + def download_rebrickable(self, /) -> None: + # Insert the Rebrickable set to the database + rows, _ = self.insert( + commit=False, + no_defer=True, + override_query=RebrickableSet.insert_query + ) + if rows > 0: if not current_app.config['USE_REMOTE_IMAGES']: - RebrickableImage(brickset).download() + RebrickableImage(self).download() - # Load the inventory - RebrickableParts(self.socket, brickset).download() + # Ingest a set + def ingest(self, record: Row | dict[str, Any], /): + super().ingest(record) - # Load the minifigures - RebrickableMinifigures(self.socket, brickset).download() + # Resolve theme + if not hasattr(self.fields, 'theme_id'): + self.fields.theme_id = 0 - # Commit the transaction to the database - self.socket.auto_progress( - message='Set {number}: writing to the database'.format( - number=brickset.fields.set_num - ), - increment_total=True, - ) + self.theme = BrickThemeList().get(self.fields.theme_id) - BrickSQL().commit() + # Resolve instructions + if self.resolve_instructions: + # Not idead, avoiding cyclic import + from .instructions_list import BrickInstructionsList - # Info - logger.info('Set {number}: imported (id: {id})'.format( - number=brickset.fields.set_num, - id=brickset.fields.u_id, - )) - - # Complete - self.socket.complete( - message='Set {number}: imported (Go to the set)'.format( # noqa: E501 - number=brickset.fields.set_num, - url=brickset.url() - ), - download=True - ) - - except Exception as e: - self.socket.fail( - message='Error while importing set {number}: {error}'.format( - number=brickset.fields.set_num, - error=e, + if self.fields.set is not None: + self.instructions = BrickInstructionsList().get( + self.fields.set ) - ) - - logger.debug(traceback.format_exc()) # Load the set from Rebrickable def load( @@ -106,44 +91,45 @@ class RebrickableSet(object): /, *, from_download=False, - ) -> BrickSet | None: + ) -> bool: # Reset the progress self.socket.progress_count = 0 self.socket.progress_total = 2 try: self.socket.auto_progress(message='Parsing set number') - set_num = RebrickableSet.parse_number(str(data['set_num'])) + set = RebrickableSet.parse_number(str(data['set'])) self.socket.auto_progress( - message='Set {num}: loading from Rebrickable'.format( - num=set_num, + message='Set {set}: loading from Rebrickable'.format( + set=set, ), ) - logger.debug('rebrick.lego.get_set("{set_num}")'.format( - set_num=set_num, + logger.debug('rebrick.lego.get_set("{set}")'.format( + set=set, )) - brickset = Rebrickable[BrickSet]( + Rebrickable[RebrickableSet]( 'get_set', - set_num, - BrickSet, + set, + RebrickableSet, + instance=self, ).get() - short = brickset.short() - short['download'] = from_download - - self.socket.emit('SET_LOADED', short) + self.socket.emit('SET_LOADED', self.short( + from_download=from_download + )) if not from_download: self.socket.complete( - message='Set {num}: loaded from Rebrickable'.format( - num=brickset.fields.set_num + message='Set {set}: loaded from Rebrickable'.format( + set=self.fields.set ) ) - return brickset + return True + except Exception as e: self.socket.fail( message='Could not load the set from Rebrickable: {error}. Data: {data}'.format( # noqa: E501 @@ -155,12 +141,62 @@ class RebrickableSet(object): if not isinstance(e, (NotFoundException, ErrorException)): logger.debug(traceback.format_exc()) - return None + return False + + # Return a short form of the Rebrickable set + def short(self, /, *, from_download: bool = False) -> dict[str, Any]: + return { + 'download': from_download, + 'image': self.fields.image, + 'name': self.fields.name, + 'set': self.fields.set, + } + + # Compute the url for the set image + def url_for_image(self, /) -> str: + if not current_app.config['USE_REMOTE_IMAGES']: + return RebrickableImage.static_url( + self.fields.set, + 'SETS_FOLDER' + ) + 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_SET_PATTERN'].format( # noqa: E501 + number=self.fields.number, + ) + except Exception: + pass + + return '' + + # Normalize from Rebrickable + @staticmethod + def from_rebrickable(data: dict[str, Any], /, **_) -> dict[str, Any]: + # Extracting version and number + number, _, version = str(data['set_num']).partition('-') + + return { + 'set': str(data['set_num']), + 'number': int(number), + 'version': int(version), + 'name': str(data['name']), + 'year': int(data['year']), + 'theme_id': int(data['theme_id']), + 'number_of_parts': int(data['num_parts']), + 'image': str(data['set_img_url']), + 'url': str(data['set_url']), + 'last_modified': str(data['last_modified_dt']), + } # Make sense of the number from the data @staticmethod - def parse_number(set_num: str, /) -> str: - number, _, version = set_num.partition('-') + def parse_number(set: str, /) -> str: + number, _, version = set.partition('-') # Making sure both are integers if version == '': @@ -192,24 +228,3 @@ class RebrickableSet(object): )) return '{number}-{version}'.format(number=number, version=version) - - # Wish from Rebrickable - # Redefine this one outside of the socket logic - @staticmethod - def wish(set_num: str) -> None: - set_num = RebrickableSet.parse_number(set_num) - logger.debug('rebrick.lego.get_set("{set_num}")'.format( - set_num=set_num, - )) - - brickwish = Rebrickable[BrickWish]( - 'get_set', - set_num, - BrickWish, - ).get() - - # Insert into database - brickwish.insert() - - if not current_app.config['USE_REMOTE_IMAGES']: - RebrickableImage(brickwish).download() diff --git a/bricktracker/rebrickable_set_list.py b/bricktracker/rebrickable_set_list.py new file mode 100644 index 0000000..8fe7ee9 --- /dev/null +++ b/bricktracker/rebrickable_set_list.py @@ -0,0 +1,21 @@ +from typing import Self + +from .rebrickable_set import RebrickableSet +from .record_list import BrickRecordList + + +# All the rebrickable sets from the database +class RebrickableSetList(BrickRecordList[RebrickableSet]): + + # Queries + select_query: str = 'rebrickable/set/list' + + # All the sets + def all(self, /) -> Self: + # Load the sets from the database + for record in self.select(): + rebrickable_set = RebrickableSet(record=record) + + self.records.append(rebrickable_set) + + return self diff --git a/bricktracker/record_list.py b/bricktracker/record_list.py index ac82e93..0798991 100644 --- a/bricktracker/record_list.py +++ b/bricktracker/record_list.py @@ -6,10 +6,20 @@ from .sql import BrickSQL if TYPE_CHECKING: from .minifigure import BrickMinifigure from .part import BrickPart + from .rebrickable_set import RebrickableSet from .set import BrickSet + from .set_checkbox import BrickSetCheckbox from .wish import BrickWish -T = TypeVar('T', 'BrickSet', 'BrickPart', 'BrickMinifigure', 'BrickWish') +T = TypeVar( + 'T', + 'BrickSet', + 'BrickSetCheckbox', + 'BrickPart', + 'BrickMinifigure', + 'BrickWish', + 'RebrickableSet' +) # SQLite records @@ -65,6 +75,6 @@ class BrickRecordList(Generic[T]): for record in self.records: yield record - # Make the sets measurable + # Make the list measurable def __len__(self, /) -> int: return len(self.records) diff --git a/bricktracker/reload.py b/bricktracker/reload.py index 20ddad2..62564df 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_checkbox_list import BrickSetCheckboxList from .theme_list import BrickThemeList @@ -10,6 +11,9 @@ def reload() -> None: # Reload the instructions BrickInstructionsList(force=True) + # Reload the checkboxes + BrickSetCheckboxList(force=True) + # Reload retired sets BrickRetiredList(force=True) diff --git a/bricktracker/set.py b/bricktracker/set.py index f07b475..aa536b8 100644 --- a/bricktracker/set.py +++ b/bricktracker/set.py @@ -1,71 +1,105 @@ -from sqlite3 import Row +import logging +import traceback from typing import Any, Self +from uuid import uuid4 -from flask import current_app, url_for +from flask import url_for from .exceptions import DatabaseException, NotFoundException -from .instructions import BrickInstructions -from .instructions_list import BrickInstructionsList from .minifigure_list import BrickMinifigureList from .part_list import BrickPartList -from .rebrickable_image import RebrickableImage -from .record import BrickRecord +from .rebrickable_minifigures import RebrickableMinifigures +from .rebrickable_parts import RebrickableParts +from .rebrickable_set import RebrickableSet +from .set_checkbox import BrickSetCheckbox +from .set_checkbox_list import BrickSetCheckboxList from .sql import BrickSQL -from .theme_list import BrickThemeList + +logger = logging.getLogger(__name__) # Lego brick set -class BrickSet(BrickRecord): - instructions: list[BrickInstructions] - theme_name: str - +class BrickSet(RebrickableSet): # Queries - select_query: str = 'set/select' + select_query: str = 'set/select/full' + light_query: str = 'set/select/light' insert_query: str = 'set/insert' - def __init__( - self, - /, - *, - record: Row | dict[str, Any] | None = None, - ): - super().__init__() - - # Placeholders - self.theme_name = '' - self.instructions = [] - - # Ingest the record if it has one - if record is not None: - self.ingest(record) - - # Resolve the theme - self.resolve_theme() - - # Check for the instructions - self.resolve_instructions() - # Delete a set def delete(self, /) -> None: - database = BrickSQL() - parameters = self.sql_parameters() + BrickSQL().executescript( + 'set/delete/set', + id=self.fields.id + ) - # Delete the set - database.execute('set/delete/set', parameters=parameters) + # Import a set into the database + def download(self, data: dict[str, Any], /) -> None: + # Load the set + if not self.load(data, from_download=True): + return - # Delete the minifigures - database.execute( - 'minifigure/delete/all_from_set', parameters=parameters) + try: + # Insert into the database + self.socket.auto_progress( + message='Set {number}: inserting into database'.format( + number=self.fields.set + ), + increment_total=True, + ) - # Delete the parts - database.execute( - 'part/delete/all_from_set', parameters=parameters) + # Generate an UUID for self + self.fields.id = str(uuid4()) - # Delete missing parts - database.execute('missing/delete/all_from_set', parameters=parameters) + # Insert into database + self.insert(commit=False) - # Commit to the database - database.commit() + # Execute the parent download method + self.download_rebrickable() + + # Load the inventory + RebrickableParts(self.socket, self).download() + + # Load the minifigures + RebrickableMinifigures(self.socket, self).download() + + # Commit the transaction to the database + self.socket.auto_progress( + message='Set {number}: writing to the database'.format( + number=self.fields.set + ), + increment_total=True, + ) + + BrickSQL().commit() + + # Info + logger.info('Set {number}: imported (id: {id})'.format( + number=self.fields.set, + id=self.fields.id, + )) + + # Complete + self.socket.complete( + message='Set {number}: imported (Go to the set)'.format( # noqa: E501 + number=self.fields.set, + url=self.url() + ), + download=True + ) + + except Exception as e: + self.socket.fail( + message='Error while importing set {number}: {error}'.format( + number=self.fields.set, + error=e, + ) + ) + + logger.debug(traceback.format_exc()) + + # Insert a Rebrickable set + def insert_rebrickable(self, /) -> None: + self.insert() # Minifigures def minifigures(self, /) -> BrickMinifigureList: @@ -75,140 +109,81 @@ class BrickSet(BrickRecord): def parts(self, /) -> BrickPartList: return BrickPartList().load(self) - # Add instructions to the set - def resolve_instructions(self, /) -> None: - if self.fields.set_num is not None: - self.instructions = BrickInstructionsList().get( - self.fields.set_num - ) - - # Add a theme to the set - def resolve_theme(self, /) -> None: - try: - id = self.fields.theme_id - except Exception: - id = 0 - - theme = BrickThemeList().get(id) - self.theme_name = theme.name - - # Return a short form of the set - def short(self, /) -> dict[str, Any]: - return { - 'name': self.fields.name, - 'set_img_url': self.fields.set_img_url, - 'set_num': self.fields.set_num, - } - - # Select a specific part (with a set and an id) - def select_specific(self, u_id: str, /) -> Self: + # Select a light set (with an id) + def select_light(self, id: str, /) -> Self: # Save the parameters to the fields - self.fields.u_id = u_id + self.fields.id = id # Load from database - if not self.select(): + if not self.select(override_query=self.light_query): raise NotFoundException( 'Set with ID {id} was not found in the database'.format( - id=self.fields.u_id, + id=self.fields.id, ), ) - # Resolve the theme - self.resolve_theme() + return self - # Check for the instructions - self.resolve_instructions() + # Select a specific set (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( + statuses=BrickSetCheckboxList().as_columns(solo=True) + ): + raise NotFoundException( + 'Set with ID {id} was not found in the database'.format( + id=self.fields.id, + ), + ) return self - # Update a checked state - def update_checked(self, name: str, status: bool, /) -> None: + # Update a status + def update_status( + self, + checkbox: BrickSetCheckbox, + status: bool, + / + ) -> None: parameters = self.sql_parameters() parameters['status'] = status - # Update the checked status + # Update the status rows, _ = BrickSQL().execute_and_commit( - 'set/update_checked', + 'set/update/status', parameters=parameters, - name=name, + name=checkbox.as_column(), ) if rows != 1: - raise DatabaseException('Could not update the status {status} for set {number}'.format( # noqa: E501 - status=name, - number=self.fields.set_num, + raise DatabaseException('Could not update the status "{status}" for set {number} ({id})'.format( # noqa: E501 + status=checkbox.fields.name, + number=self.fields.set, + id=self.fields.id, )) # Self url def url(self, /) -> str: - return url_for('set.details', id=self.fields.u_id) + return url_for('set.details', id=self.fields.id) # Deletion url def url_for_delete(self, /) -> str: - return url_for('set.delete', id=self.fields.u_id) + return url_for('set.delete', id=self.fields.id) # Actual deletion url def url_for_do_delete(self, /) -> str: - return url_for('set.do_delete', id=self.fields.u_id) - - # Compute the url for the set image - def url_for_image(self, /) -> str: - if not current_app.config['USE_REMOTE_IMAGES']: - return RebrickableImage.static_url( - self.fields.set_num, - 'SETS_FOLDER' - ) - else: - return self.fields.set_img_url + return url_for('set.do_delete', id=self.fields.id) # Compute the url for the set instructions def url_for_instructions(self, /) -> str: if len(self.instructions): return url_for( 'set.details', - id=self.fields.u_id, + id=self.fields.id, open_instructions=True ) else: return '' - - # Check minifigure collected url - def url_for_minifigures_collected(self, /) -> str: - return url_for('set.minifigures_collected', id=self.fields.u_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_SET_PATTERN'].format( # noqa: E501 - number=self.fields.set_num.lower(), - ) - except Exception: - pass - - return '' - - # Check set checked url - def url_for_set_checked(self, /) -> str: - return url_for('set.set_checked', id=self.fields.u_id) - - # Check set collected url - def url_for_set_collected(self, /) -> str: - return url_for('set.set_collected', id=self.fields.u_id) - - # Normalize from Rebrickable - @staticmethod - def from_rebrickable(data: dict[str, Any], /, **_) -> dict[str, Any]: - return { - 'set_num': data['set_num'], - 'name': data['name'], - 'year': data['year'], - 'theme_id': data['theme_id'], - 'num_parts': data['num_parts'], - 'set_img_url': data['set_img_url'], - 'set_url': data['set_url'], - 'last_modified_dt': data['last_modified_dt'], - 'mini_col': False, - 'set_col': False, - 'set_check': False, - } diff --git a/bricktracker/set_checkbox.py b/bricktracker/set_checkbox.py new file mode 100644 index 0000000..ea6d6d2 --- /dev/null +++ b/bricktracker/set_checkbox.py @@ -0,0 +1,142 @@ +from sqlite3 import Row +from typing import Any, Self, Tuple +from uuid import uuid4 + +from flask import url_for + +from .exceptions import DatabaseException, ErrorException, NotFoundException +from .record import BrickRecord +from .sql import BrickSQL + + +# Lego set checkbox +class BrickSetCheckbox(BrickRecord): + # Queries + 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, + ) + + # Grab data from a form + 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, **_) -> Tuple[int, str]: + # 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, + 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 + 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/set_checkbox_list.py b/bricktracker/set_checkbox_list.py new file mode 100644 index 0000000..0f32240 --- /dev/null +++ b/bricktracker/set_checkbox_list.py @@ -0,0 +1,74 @@ +import logging + +from .exceptions import NotFoundException +from .fields import BrickRecordFields +from .record_list import BrickRecordList +from .set_checkbox import BrickSetCheckbox + +logger = logging.getLogger(__name__) + + +# Lego sets checkbox list +class BrickSetCheckboxList(BrickRecordList[BrickSetCheckbox]): + checkboxes: dict[str, BrickSetCheckbox] + + # Queries + select_query = 'checkbox/list' + + def __init__(self, /, *, force: bool = False): + # Load checkboxes 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') + + BrickSetCheckboxList.records = [] + BrickSetCheckboxList.checkboxes = {} + + # Load the checkboxes from the database + for record in self.select(): + checkbox = BrickSetCheckbox(record=record) + + BrickSetCheckboxList.records.append(checkbox) + BrickSetCheckboxList.checkboxes[checkbox.fields.id] = checkbox + + # Return the checkboxes 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 checkbox + def get(self, id: str, /) -> BrickSetCheckbox: + if id not in self.checkboxes: + raise NotFoundException( + 'Checkbox with ID {id} was not found in the database'.format( + id=self.fields.id, + ), + ) + + return self.checkboxes[id] + + # Get the list of checkboxes depending on the context + def list(self, /, *, all: bool = False) -> list[BrickSetCheckbox]: + return [ + record + for record + in self.records + if all or record.fields.displayed_on_grid + ] diff --git a/bricktracker/set_list.py b/bricktracker/set_list.py index e543b5b..3b229e8 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_checkbox_list import BrickSetCheckboxList from .set import BrickSet @@ -13,6 +14,7 @@ class BrickSetList(BrickRecordList[BrickSet]): # Queries generic_query: str = 'set/list/generic' + light_query: str = 'set/list/light' missing_minifigure_query: str = 'set/list/missing_minifigure' missing_part_query: str = 'set/list/missing_part' select_query: str = 'set/list/all' @@ -33,11 +35,14 @@ class BrickSetList(BrickRecordList[BrickSet]): themes = set() # Load the sets from the database - for record in self.select(order=self.order): + for record in self.select( + order=self.order, + statuses=BrickSetCheckboxList().as_columns() + ): brickset = BrickSet(record=record) self.records.append(brickset) - themes.add(brickset.theme_name) + themes.add(brickset.theme.name) # Convert the set into a list and sort it self.themes = list(themes) @@ -63,9 +68,13 @@ class BrickSetList(BrickRecordList[BrickSet]): if current_app.config['RANDOM']: order = 'RANDOM()' else: - order = 'sets.rowid DESC' + order = '"bricktracker_sets"."rowid" DESC' - for record in self.select(order=order, limit=limit): + for record in self.select( + order=order, + limit=limit, + statuses=BrickSetCheckboxList().as_columns() + ): brickset = BrickSet(record=record) self.records.append(brickset) diff --git a/bricktracker/socket.py b/bricktracker/socket.py index 52bb4f6..4351592 100644 --- a/bricktracker/socket.py +++ b/bricktracker/socket.py @@ -6,7 +6,7 @@ from flask_socketio import SocketIO from .configuration_list import BrickConfigurationList from .login import LoginManager -from .rebrickable_set import RebrickableSet +from .set import BrickSet from .sql import close as sql_close logger = logging.getLogger(__name__) @@ -98,7 +98,7 @@ class BrickSocket(object): self.fail(message=str(e)) return - brickset = RebrickableSet(self) + brickset = BrickSet(socket=self) # Start it in a thread if requested if self.threaded: @@ -124,7 +124,7 @@ class BrickSocket(object): self.fail(message=str(e)) return - brickset = RebrickableSet(self) + brickset = BrickSet(socket=self) # Start it in a thread if requested if self.threaded: diff --git a/bricktracker/sql/checkbox/add.sql b/bricktracker/sql/checkbox/add.sql new file mode 100644 index 0000000..5de9c17 --- /dev/null +++ b/bricktracker/sql/checkbox/add.sql @@ -0,0 +1,16 @@ +BEGIN TRANSACTION; + +ALTER TABLE "bricktracker_set_statuses" +ADD COLUMN "status_{{ id }}" BOOLEAN NOT NULL DEFAULT 0; + +INSERT INTO "bricktracker_set_checkboxes" ( + "id", + "name", + "displayed_on_grid" +) VALUES ( + '{{ id }}', + '{{ name }}', + {{ displayed_on_grid }} +); + +COMMIT; \ No newline at end of file diff --git a/bricktracker/sql/checkbox/base.sql b/bricktracker/sql/checkbox/base.sql new file mode 100644 index 0000000..9726a6c --- /dev/null +++ b/bricktracker/sql/checkbox/base.sql @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000..6eae9d0 --- /dev/null +++ b/bricktracker/sql/checkbox/delete.sql @@ -0,0 +1,9 @@ +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 new file mode 100644 index 0000000..7420eb3 --- /dev/null +++ b/bricktracker/sql/checkbox/list.sql @@ -0,0 +1 @@ +{% extends 'checkbox/base.sql' %} diff --git a/bricktracker/sql/checkbox/select.sql b/bricktracker/sql/checkbox/select.sql new file mode 100644 index 0000000..76557a8 --- /dev/null +++ b/bricktracker/sql/checkbox/select.sql @@ -0,0 +1,5 @@ +{% 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/name.sql b/bricktracker/sql/checkbox/update/name.sql new file mode 100644 index 0000000..19fccc0 --- /dev/null +++ b/bricktracker/sql/checkbox/update/name.sql @@ -0,0 +1,3 @@ +UPDATE "bricktracker_set_checkboxes" +SET "name" = :safe_name +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 new file mode 100644 index 0000000..3c04c22 --- /dev/null +++ b/bricktracker/sql/checkbox/update/status.sql @@ -0,0 +1,3 @@ +UPDATE "bricktracker_set_checkboxes" +SET "{{name}}" = :status +WHERE "bricktracker_set_checkboxes"."id" IS NOT DISTINCT FROM :id diff --git a/bricktracker/sql/migrations/0001.sql b/bricktracker/sql/migrations/0001.sql index 06de28f..6b71f7e 100644 --- a/bricktracker/sql/migrations/0001.sql +++ b/bricktracker/sql/migrations/0001.sql @@ -1,6 +1,6 @@ -- description: Original database initialization -- FROM sqlite3 app.db .schema > init.sql with extra IF NOT EXISTS, transaction and quotes -BEGIN transaction; +BEGIN TRANSACTION; CREATE TABLE IF NOT EXISTS "wishlist" ( "set_num" TEXT, diff --git a/bricktracker/sql/migrations/0002.sql b/bricktracker/sql/migrations/0002.sql index d719d4e..1987812 100644 --- a/bricktracker/sql/migrations/0002.sql +++ b/bricktracker/sql/migrations/0002.sql @@ -3,7 +3,7 @@ -- Set the journal mode to WAL PRAGMA journal_mode = WAL; -BEGIN transaction; +BEGIN TRANSACTION; -- Fix a bug where 'None' was inserted in missing instead of NULL UPDATE "missing" diff --git a/bricktracker/sql/migrations/0003.sql b/bricktracker/sql/migrations/0003.sql new file mode 100644 index 0000000..8871a63 --- /dev/null +++ b/bricktracker/sql/migrations/0003.sql @@ -0,0 +1,48 @@ +-- description: Creation of the deduplicated table of Rebrickable sets + +BEGIN TRANSACTION; + +-- Create a Rebrickable set table: each unique set imported from Rebrickable +CREATE TABLE "rebrickable_sets" ( + "set" TEXT NOT NULL, + "number" INTEGER NOT NULL, + "version" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "year" INTEGER NOT NULL, + "theme_id" INTEGER NOT NULL, + "number_of_parts" INTEGER NOT NULL, + "image" TEXT, + "url" TEXT, + "last_modified" TEXT, + PRIMARY KEY("set") +); + +-- Insert existing sets into the new table +INSERT INTO "rebrickable_sets" ( + "set", + "number", + "version", + "name", + "year", + "theme_id", + "number_of_parts", + "image", + "url", + "last_modified" +) +SELECT + "sets"."set_num", + CAST(SUBSTR("sets"."set_num", 1, INSTR("sets"."set_num", '-') - 1) AS INTEGER), + CAST(SUBSTR("sets"."set_num", INSTR("sets"."set_num", '-') + 1) AS INTEGER), + "sets"."name", + "sets"."year", + "sets"."theme_id", + "sets"."num_parts", + "sets"."set_img_url", + "sets"."set_url", + "sets"."last_modified_dt" +FROM "sets" +GROUP BY + "sets"."set_num"; + +COMMIT; \ No newline at end of file diff --git a/bricktracker/sql/migrations/0004.sql b/bricktracker/sql/migrations/0004.sql new file mode 100644 index 0000000..3828204 --- /dev/null +++ b/bricktracker/sql/migrations/0004.sql @@ -0,0 +1,25 @@ +-- description: Migrate the Bricktracker sets + +PRAGMA foreign_keys = ON; + +BEGIN TRANSACTION; + +-- Create a Bricktable 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, + PRIMARY KEY("id"), + FOREIGN KEY("rebrickable_set") REFERENCES "rebrickable_sets"("set") +); + +-- Insert existing sets into the new table +INSERT INTO "bricktracker_sets" ( + "id", + "rebrickable_set" +) +SELECT + "sets"."u_id", + "sets"."set_num" +FROM "sets"; + +COMMIT; \ No newline at end of file diff --git a/bricktracker/sql/migrations/0005.sql b/bricktracker/sql/migrations/0005.sql new file mode 100644 index 0000000..564f875 --- /dev/null +++ b/bricktracker/sql/migrations/0005.sql @@ -0,0 +1,72 @@ +-- description: Creation of the configurable set checkboxes + +PRAGMA foreign_keys = ON; + +BEGIN TRANSACTION; + +-- Create a table to define each set checkbox: with an ID, a name and if they should be displayed on the grid cards +CREATE TABLE "bricktracker_set_checkboxes" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "displayed_on_grid" BOOLEAN NOT NULL DEFAULT 0, + PRIMARY KEY("id") +); + +-- Seed our checkbox with the 3 original ones +INSERT INTO "bricktracker_set_checkboxes" ( + "id", + "name", + "displayed_on_grid" +) VALUES ( + "minifigures_collected", + "Minifigures are collected", + 1 +); + +INSERT INTO "bricktracker_set_checkboxes" ( + "id", + "name", + "displayed_on_grid" +) VALUES ( + "set_checked", + "Set is checked", + 1 +); + +INSERT INTO "bricktracker_set_checkboxes" ( + "id", + "name", + "displayed_on_grid" +) VALUES ( + "set_collected", + "Set is collected and boxed", + 1 +); + +-- Create a table for the status of each checkbox: with the 3 first status +CREATE TABLE "bricktracker_set_statuses" ( + "bricktracker_set_id" TEXT NOT NULL, + "status_minifigures_collected" BOOLEAN NOT NULL DEFAULT 0, + "status_set_checked" BOOLEAN NOT NULL DEFAULT 0, + "status_set_collected" BOOLEAN NOT NULL DEFAULT 0, + PRIMARY KEY("bricktracker_set_id"), + FOREIGN KEY("bricktracker_set_id") REFERENCES "bricktracker_sets"("id") +); + +INSERT INTO "bricktracker_set_statuses" ( + "bricktracker_set_id", + "status_minifigures_collected", + "status_set_checked", + "status_set_collected" +) +SELECT + "sets"."u_id", + "sets"."mini_col", + "sets"."set_check", + "sets"."set_col" +FROM "sets"; + +-- Rename the original table (don't delete it yet?) +ALTER TABLE "sets" RENAME TO "sets_old"; + +COMMIT; diff --git a/bricktracker/sql/migrations/0006.sql b/bricktracker/sql/migrations/0006.sql new file mode 100644 index 0000000..7d4b9a0 --- /dev/null +++ b/bricktracker/sql/migrations/0006.sql @@ -0,0 +1,42 @@ +-- description: Migrate the whislist to have a Rebrickable sets structure + +BEGIN TRANSACTION; + +-- Create a Rebrickable wish table: each unique (light) set imported from Rebrickable +CREATE TABLE "bricktracker_wishes" ( + "set" TEXT NOT NULL, + "name" TEXT NOT NULL, + "year" INTEGER NOT NULL, + "theme_id" INTEGER NOT NULL, + "number_of_parts" INTEGER NOT NULL, + "image" TEXT, + "url" TEXT, + PRIMARY KEY("set") +); + +-- Insert existing wishes into the new table +INSERT INTO "bricktracker_wishes" ( + "set", + "name", + "year", + "theme_id", + "number_of_parts", + "image", + "url" +) +SELECT + "wishlist"."set_num", + "wishlist"."name", + "wishlist"."year", + "wishlist"."theme_id", + "wishlist"."num_parts", + "wishlist"."set_img_url", + "wishlist"."set_url" +FROM "wishlist" +GROUP BY + "wishlist"."set_num"; + +-- Rename the original table (don't delete it yet?) +ALTER TABLE "wishlist" RENAME TO "wishlist_old"; + +COMMIT; \ No newline at end of file diff --git a/bricktracker/sql/minifigure/delete/all_from_set.sql b/bricktracker/sql/minifigure/delete/all_from_set.sql deleted file mode 100644 index e0b8446..0000000 --- a/bricktracker/sql/minifigure/delete/all_from_set.sql +++ /dev/null @@ -1,2 +0,0 @@ -DELETE FROM "minifigures" -WHERE "minifigures"."u_id" IS NOT DISTINCT FROM :u_id \ No newline at end of file diff --git a/bricktracker/sql/missing/delete/all_from_set.sql b/bricktracker/sql/missing/delete/all_from_set.sql deleted file mode 100644 index 612b102..0000000 --- a/bricktracker/sql/missing/delete/all_from_set.sql +++ /dev/null @@ -1,2 +0,0 @@ -DELETE FROM "missing" -WHERE "missing"."u_id" IS NOT DISTINCT FROM :u_id \ No newline at end of file diff --git a/bricktracker/sql/part/delete/all_from_set.sql b/bricktracker/sql/part/delete/all_from_set.sql deleted file mode 100644 index 59d44c3..0000000 --- a/bricktracker/sql/part/delete/all_from_set.sql +++ /dev/null @@ -1,2 +0,0 @@ -DELETE FROM "inventory" -WHERE "inventory"."u_id" IS NOT DISTINCT FROM :u_id \ No newline at end of file diff --git a/bricktracker/sql/part/list/all.sql b/bricktracker/sql/part/list/all.sql index ded1297..b1ff2ac 100644 --- a/bricktracker/sql/part/list/all.sql +++ b/bricktracker/sql/part/list/all.sql @@ -9,7 +9,7 @@ SUM("inventory"."quantity" * IFNULL("minifigures"."quantity", 1)) AS "total_quan {% endblock %} {% block total_sets %} -COUNT(DISTINCT "sets"."u_id") AS "total_sets", +COUNT(DISTINCT "bricktracker_sets"."id") AS "total_sets", {% endblock %} {% block total_minifigures %} @@ -29,8 +29,8 @@ 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 sets -ON "inventory"."u_id" IS NOT DISTINCT FROM "sets"."u_id" +LEFT JOIN "bricktracker_sets" +ON "inventory"."u_id" IS NOT DISTINCT FROM "bricktracker_sets"."id" {% endblock %} {% block group %} diff --git a/bricktracker/sql/rebrickable/set/insert.sql b/bricktracker/sql/rebrickable/set/insert.sql new file mode 100644 index 0000000..88b2b44 --- /dev/null +++ b/bricktracker/sql/rebrickable/set/insert.sql @@ -0,0 +1,23 @@ +INSERT OR IGNORE INTO "rebrickable_sets" ( + "set", + "number", + "version", + "name", + "year", + "theme_id", + "number_of_parts", + "image", + "url", + "last_modified" +) VALUES ( + :set, + :number, + :version, + :name, + :year, + :theme_id, + :number_of_parts, + :image, + :url, + :last_modified +) diff --git a/bricktracker/sql/rebrickable/set/list.sql b/bricktracker/sql/rebrickable/set/list.sql new file mode 100644 index 0000000..53f4886 --- /dev/null +++ b/bricktracker/sql/rebrickable/set/list.sql @@ -0,0 +1,11 @@ +SELECT + "rebrickable_sets"."set", + "rebrickable_sets"."number", + "rebrickable_sets"."version", + "rebrickable_sets"."name", + "rebrickable_sets"."year", + "rebrickable_sets"."theme_id", + "rebrickable_sets"."number_of_parts", + "rebrickable_sets"."image", + "rebrickable_sets"."url" +FROM "rebrickable_sets" diff --git a/bricktracker/sql/rebrickable/set/select.sql b/bricktracker/sql/rebrickable/set/select.sql new file mode 100644 index 0000000..f760bb6 --- /dev/null +++ b/bricktracker/sql/rebrickable/set/select.sql @@ -0,0 +1,13 @@ +SELECT + "rebrickable_sets"."set", + "rebrickable_sets"."number", + "rebrickable_sets"."version", + "rebrickable_sets"."name", + "rebrickable_sets"."year", + "rebrickable_sets"."theme_id", + "rebrickable_sets"."number_of_parts", + "rebrickable_sets"."image", + "rebrickable_sets"."url" +FROM "rebrickable_sets" + +WHERE "rebrickable_sets"."set" IS NOT DISTINCT FROM :set diff --git a/bricktracker/sql/schema/drop.sql b/bricktracker/sql/schema/drop.sql index 353ce03..b961b28 100644 --- a/bricktracker/sql/schema/drop.sql +++ b/bricktracker/sql/schema/drop.sql @@ -1,9 +1,18 @@ BEGIN transaction; -DROP TABLE IF EXISTS "wishlist"; -DROP TABLE IF EXISTS "sets"; +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 "missing"; +DROP TABLE IF EXISTS "rebrickable_sets"; +DROP TABLE IF EXISTS "sets"; +DROP TABLE IF EXISTS "sets_old"; +DROP TABLE IF EXISTS "wishlist"; +DROP TABLE IF EXISTS "wishlist_old"; -COMMIT; \ No newline at end of file +COMMIT; + +PRAGMA user_version = 0; \ No newline at end of file diff --git a/bricktracker/sql/set/base/base.sql b/bricktracker/sql/set/base/base.sql new file mode 100644 index 0000000..2f4d683 --- /dev/null +++ b/bricktracker/sql/set/base/base.sql @@ -0,0 +1,38 @@ +SELECT + {% block id %}{% endblock %} + "rebrickable_sets"."set", + "rebrickable_sets"."number", + "rebrickable_sets"."version", + "rebrickable_sets"."name", + "rebrickable_sets"."year", + "rebrickable_sets"."theme_id", + "rebrickable_sets"."number_of_parts", + "rebrickable_sets"."image", + "rebrickable_sets"."url", + {% block statuses %} + {% if statuses %}{{ statuses }},{% endif %} + {% endblock %} + {% 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 %} +FROM "bricktracker_sets" + +INNER JOIN "rebrickable_sets" +ON "bricktracker_sets"."rebrickable_set" IS NOT DISTINCT FROM "rebrickable_sets"."set" + +{% 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/set/base/full.sql b/bricktracker/sql/set/base/full.sql new file mode 100644 index 0000000..c169c7a --- /dev/null +++ b/bricktracker/sql/set/base/full.sql @@ -0,0 +1,42 @@ +{% extends 'set/base/base.sql' %} + +{% block id %} +"bricktracker_sets"."id", +{% endblock %} + +{% block total_missing %} +IFNULL("missing_join"."total", 0) AS "total_missing", +{% endblock %} + +{% block total_quantity %} +IFNULL("minifigures_join"."total", 0) AS "total_minifigures" +{% endblock %} + +{% block join %} +{% if statuses %} +LEFT JOIN "bricktracker_set_statuses" +ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_statuses"."bricktracker_set_id" +{% endif %} + +-- LEFT JOIN + SELECT to avoid messing the total +LEFT JOIN ( + SELECT + "missing"."u_id", + SUM("missing"."quantity") AS "total" + FROM "missing" + {% block where_missing %}{% endblock %} + GROUP BY "u_id" +) "missing_join" +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" + {% block where_minifigures %}{% endblock %} + GROUP BY "u_id" +) "minifigures_join" +ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "minifigures_join"."u_id" +{% endblock %} \ No newline at end of file diff --git a/bricktracker/sql/set/base/light.sql b/bricktracker/sql/set/base/light.sql new file mode 100644 index 0000000..b599a87 --- /dev/null +++ b/bricktracker/sql/set/base/light.sql @@ -0,0 +1,18 @@ +SELECT + "bricktracker_sets"."id", + "bricktracker_sets"."rebrickable_set" AS "set" +FROM "bricktracker_sets" + +{% 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/set/base/select.sql b/bricktracker/sql/set/base/select.sql deleted file mode 100644 index 92c3c29..0000000 --- a/bricktracker/sql/set/base/select.sql +++ /dev/null @@ -1,52 +0,0 @@ -SELECT - "sets"."set_num", - "sets"."name", - "sets"."year", - "sets"."theme_id", - "sets"."num_parts", - "sets"."set_img_url", - "sets"."set_url", - "sets"."last_modified_dt", - "sets"."mini_col", - "sets"."set_check", - "sets"."set_col", - "sets"."u_id", - {% block number %} - CAST(SUBSTR("sets"."set_num", 1, INSTR("sets"."set_num", '-') - 1) AS INTEGER) AS "set_number", - CAST(SUBSTR("sets"."set_num", INSTR("sets"."set_num", '-') + 1) AS INTEGER) AS "set_version", - {% endblock %} - IFNULL("missing_join"."total", 0) AS "total_missing", - IFNULL("minifigures_join"."total", 0) AS "total_minifigures" -FROM sets - --- LEFT JOIN + SELECT to avoid messing the total -LEFT JOIN ( - SELECT - "missing"."u_id", - SUM("missing"."quantity") AS "total" - FROM "missing" - {% block where_missing %}{% endblock %} - GROUP BY "u_id" -) "missing_join" -ON "sets"."u_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" - {% block where_minifigures %}{% endblock %} - GROUP BY "u_id" -) "minifigures_join" -ON "sets"."u_id" IS NOT DISTINCT FROM "minifigures_join"."u_id" - -{% block where %}{% endblock %} - -{% if order %} -ORDER BY {{ order }} -{% endif %} - -{% if limit %} -LIMIT {{ limit }} -{% endif %} diff --git a/bricktracker/sql/set/delete/set.sql b/bricktracker/sql/set/delete/set.sql index 0bf32b9..dd2c856 100644 --- a/bricktracker/sql/set/delete/set.sql +++ b/bricktracker/sql/set/delete/set.sql @@ -1,2 +1,21 @@ -DELETE FROM "sets" -WHERE "sets"."u_id" IS NOT DISTINCT FROM :u_id \ No newline at end of file +-- A bit unsafe as it does not use a prepared statement but it +-- should not be possible to inject anything through the {{ id }} context + +BEGIN TRANSACTION; + +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 }}'; + +DELETE FROM "minifigures" +WHERE "minifigures"."u_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 }}'; + +COMMIT; \ No newline at end of file diff --git a/bricktracker/sql/set/insert.sql b/bricktracker/sql/set/insert.sql index eb19329..2462ac5 100644 --- a/bricktracker/sql/set/insert.sql +++ b/bricktracker/sql/set/insert.sql @@ -1,27 +1,7 @@ -INSERT INTO sets ( - "set_num", - "name", - "year", - "theme_id", - "num_parts", - "set_img_url", - "set_url", - "last_modified_dt", - "mini_col", - "set_check", - "set_col", - "u_id" +INSERT OR IGNORE INTO "bricktracker_sets" ( + "id", + "rebrickable_set" ) VALUES ( - :set_num, - :name, - :year, - :theme_id, - :num_parts, - :set_img_url, - :set_url, - :last_modified_dt, - :mini_col, - :set_check, - :set_col, - :u_id + :id, + :set ) diff --git a/bricktracker/sql/set/list/all.sql b/bricktracker/sql/set/list/all.sql index 66e3549..28629cc 100644 --- a/bricktracker/sql/set/list/all.sql +++ b/bricktracker/sql/set/list/all.sql @@ -1 +1 @@ -{% extends 'set/base/select.sql' %} +{% extends 'set/base/full.sql' %} diff --git a/bricktracker/sql/set/list/generic.sql b/bricktracker/sql/set/list/generic.sql index 84ea5c1..0177c2b 100644 --- a/bricktracker/sql/set/list/generic.sql +++ b/bricktracker/sql/set/list/generic.sql @@ -1,12 +1,6 @@ -SELECT - "sets"."set_num", - "sets"."name", - "sets"."year", - "sets"."theme_id", - "sets"."num_parts", - "sets"."set_img_url", - "sets"."set_url" -FROM "sets" +{% extends 'set/base/base.sql' %} +{% block group %} GROUP BY - "sets"."set_num" + "bricktracker_sets"."rebrickable_set" +{% endblock %} diff --git a/bricktracker/sql/set/list/missing_minifigure.sql b/bricktracker/sql/set/list/missing_minifigure.sql index 7dda72b..5f27088 100644 --- a/bricktracker/sql/set/list/missing_minifigure.sql +++ b/bricktracker/sql/set/list/missing_minifigure.sql @@ -1,7 +1,7 @@ -{% extends 'set/base/select.sql' %} +{% extends 'set/base/full.sql' %} {% block where %} -WHERE "sets"."u_id" IN ( +WHERE "bricktracker_sets"."id" IN ( SELECT "missing"."u_id" FROM "missing" diff --git a/bricktracker/sql/set/list/missing_part.sql b/bricktracker/sql/set/list/missing_part.sql index 2b52e22..781754c 100644 --- a/bricktracker/sql/set/list/missing_part.sql +++ b/bricktracker/sql/set/list/missing_part.sql @@ -1,7 +1,7 @@ -{% extends 'set/base/select.sql' %} +{% extends 'set/base/full.sql' %} {% block where %} -WHERE "sets"."u_id" IN ( +WHERE "bricktracker_sets"."id" IN ( SELECT "missing"."u_id" FROM "missing" diff --git a/bricktracker/sql/set/list/using_minifigure.sql b/bricktracker/sql/set/list/using_minifigure.sql index 0c97794..f08a5d7 100644 --- a/bricktracker/sql/set/list/using_minifigure.sql +++ b/bricktracker/sql/set/list/using_minifigure.sql @@ -1,7 +1,7 @@ -{% extends 'set/base/select.sql' %} +{% extends 'set/base/full.sql' %} {% block where %} -WHERE "sets"."u_id" IN ( +WHERE "bricktracker_sets"."id" IN ( SELECT "inventory"."u_id" FROM "inventory" diff --git a/bricktracker/sql/set/list/using_part.sql b/bricktracker/sql/set/list/using_part.sql index cd8a1db..8877cff 100644 --- a/bricktracker/sql/set/list/using_part.sql +++ b/bricktracker/sql/set/list/using_part.sql @@ -1,7 +1,7 @@ -{% extends 'set/base/select.sql' %} +{% extends 'set/base/full.sql' %} {% block where %} -WHERE "sets"."u_id" IN ( +WHERE "bricktracker_sets"."id" IN ( SELECT "inventory"."u_id" FROM "inventory" diff --git a/bricktracker/sql/set/select.sql b/bricktracker/sql/set/select.sql deleted file mode 100644 index 485a733..0000000 --- a/bricktracker/sql/set/select.sql +++ /dev/null @@ -1,13 +0,0 @@ -{% extends 'set/base/select.sql' %} - -{% block where_missing %} -WHERE "missing"."u_id" IS NOT DISTINCT FROM :u_id -{% endblock %} - -{% block where_minifigures %} -WHERE "minifigures"."u_id" IS NOT DISTINCT FROM :u_id -{% endblock %} - -{% block where %} -WHERE "sets"."u_id" IS NOT DISTINCT FROM :u_id -{% endblock %} diff --git a/bricktracker/sql/set/select/full.sql b/bricktracker/sql/set/select/full.sql new file mode 100644 index 0000000..4b19136 --- /dev/null +++ b/bricktracker/sql/set/select/full.sql @@ -0,0 +1,13 @@ +{% extends 'set/base/full.sql' %} + +{% block where_missing %} +WHERE "missing"."u_id" IS NOT DISTINCT FROM :id +{% endblock %} + +{% block where_minifigures %} +WHERE "minifigures"."u_id" IS NOT DISTINCT FROM :id +{% endblock %} + +{% block where %} +WHERE "bricktracker_sets"."id" IS NOT DISTINCT FROM :id +{% endblock %} diff --git a/bricktracker/sql/set/select/light.sql b/bricktracker/sql/set/select/light.sql new file mode 100644 index 0000000..61dce04 --- /dev/null +++ b/bricktracker/sql/set/select/light.sql @@ -0,0 +1,5 @@ +{% extends 'set/base/light.sql' %} + +{% block where %} +WHERE "bricktracker_sets"."id" IS NOT DISTINCT FROM :id +{% endblock %} diff --git a/bricktracker/sql/set/update/status.sql b/bricktracker/sql/set/update/status.sql new file mode 100644 index 0000000..d72616e --- /dev/null +++ b/bricktracker/sql/set/update/status.sql @@ -0,0 +1,10 @@ +INSERT INTO "bricktracker_set_statuses" ( + "bricktracker_set_id", + "{{name}}" +) VALUES ( + :id, + :status +) +ON CONFLICT("bricktracker_set_id") +DO UPDATE SET "{{name}}" = :status +WHERE "bricktracker_set_statuses"."bricktracker_set_id" IS NOT DISTINCT FROM :id diff --git a/bricktracker/sql/set/update_checked.sql b/bricktracker/sql/set/update_checked.sql deleted file mode 100644 index 2fe6852..0000000 --- a/bricktracker/sql/set/update_checked.sql +++ /dev/null @@ -1,3 +0,0 @@ -UPDATE "sets" -SET "{{name}}" = :status -WHERE "sets"."u_id" IS NOT DISTINCT FROM :u_id diff --git a/bricktracker/sql/wish/base/base.sql b/bricktracker/sql/wish/base/base.sql new file mode 100644 index 0000000..b06c66f --- /dev/null +++ b/bricktracker/sql/wish/base/base.sql @@ -0,0 +1,19 @@ +SELECT + "bricktracker_wishes"."set", + "bricktracker_wishes"."name", + "bricktracker_wishes"."year", + "bricktracker_wishes"."theme_id", + "bricktracker_wishes"."number_of_parts", + "bricktracker_wishes"."image", + "bricktracker_wishes"."url" +FROM "bricktracker_wishes" + +{% block where %}{% endblock %} + +{% if order %} +ORDER BY {{ order }} +{% endif %} + +{% if limit %} +LIMIT {{ limit }} +{% endif %} diff --git a/bricktracker/sql/wish/base/select.sql b/bricktracker/sql/wish/base/select.sql deleted file mode 100644 index 5ce91af..0000000 --- a/bricktracker/sql/wish/base/select.sql +++ /dev/null @@ -1,20 +0,0 @@ -SELECT - "wishlist"."set_num", - "wishlist"."name", - "wishlist"."year", - "wishlist"."theme_id", - "wishlist"."num_parts", - "wishlist"."set_img_url", - "wishlist"."set_url", - "wishlist"."last_modified_dt" -FROM "wishlist" - -{% block where %}{% endblock %} - -{% if order %} -ORDER BY {{ order }} -{% endif %} - -{% if limit %} -LIMIT {{ limit }} -{% endif %} diff --git a/bricktracker/sql/wish/delete/wish.sql b/bricktracker/sql/wish/delete/wish.sql index 2f33e9c..e60b2e4 100644 --- a/bricktracker/sql/wish/delete/wish.sql +++ b/bricktracker/sql/wish/delete/wish.sql @@ -1,2 +1,2 @@ -DELETE FROM "wishlist" -WHERE "wishlist"."set_num" IS NOT DISTINCT FROM :set_num \ No newline at end of file +DELETE FROM "bricktracker_wishes" +WHERE "bricktracker_wishes"."set" IS NOT DISTINCT FROM :set \ No newline at end of file diff --git a/bricktracker/sql/wish/insert.sql b/bricktracker/sql/wish/insert.sql index e002ab8..3a4fdec 100644 --- a/bricktracker/sql/wish/insert.sql +++ b/bricktracker/sql/wish/insert.sql @@ -1,19 +1,17 @@ -INSERT INTO "wishlist" ( - "set_num", +INSERT OR IGNORE INTO "bricktracker_wishes" ( + "set", "name", "year", "theme_id", - "num_parts", - "set_img_url", - "set_url", - "last_modified_dt" + "number_of_parts", + "image", + "url" ) VALUES ( - :set_num, + :set, :name, :year, :theme_id, - :num_parts, - :set_img_url, - :set_url, - :last_modified_dt + :number_of_parts, + :image, + :url ) diff --git a/bricktracker/sql/wish/list/all.sql b/bricktracker/sql/wish/list/all.sql index e1e10c5..0d56355 100644 --- a/bricktracker/sql/wish/list/all.sql +++ b/bricktracker/sql/wish/list/all.sql @@ -1 +1 @@ -{% extends 'wish/base/select.sql' %} +{% extends 'wish/base/base.sql' %} diff --git a/bricktracker/sql/wish/select.sql b/bricktracker/sql/wish/select.sql index 326e507..2c399c8 100644 --- a/bricktracker/sql/wish/select.sql +++ b/bricktracker/sql/wish/select.sql @@ -1,5 +1,5 @@ -{% extends 'wish/base/select.sql' %} +{% extends 'wish/base/base.sql' %} {% block where %} -WHERE "wishlist"."set_num" IS NOT DISTINCT FROM :set_num +WHERE "bricktracker_wishes"."set" IS NOT DISTINCT FROM :set {% endblock %} diff --git a/bricktracker/version.py b/bricktracker/version.py index cc0dac8..f421248 100644 --- a/bricktracker/version.py +++ b/bricktracker/version.py @@ -1,4 +1,4 @@ from typing import Final __version__: Final[str] = '1.0.0' -__database_version__: Final[int] = 2 +__database_version__: Final[int] = 6 diff --git a/bricktracker/views/admin/checkbox.py b/bricktracker/views/admin/checkbox.py new file mode 100644 index 0000000..1dd58bb --- /dev/null +++ b/bricktracker/views/admin/checkbox.py @@ -0,0 +1,98 @@ +import logging + +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 + +logger = logging.getLogger(__name__) + +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', 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('/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('error') + ) + + +# Actually delete the checkbox +@admin_checkbox_page.route('/delete', methods=['POST']) +@login_required +@exception_handler(__file__, post_redirect='admin_checkbox.delete') +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 status of a checkbox +@admin_checkbox_page.route('//status/', 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 + + 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, + )) + + reload() + + return jsonify({'value': value}) + + +# Rename the checkbox +@admin_checkbox_page.route('/rename', methods=['POST']) +@login_required +@exception_handler(__file__, post_redirect='admin.admin', 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/index.py b/bricktracker/views/index.py index 511e858..c1f0811 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_checkbox_list import BrickSetCheckboxList from ..set_list import BrickSetList index_page = Blueprint('index', __name__) @@ -15,4 +16,5 @@ def index() -> str: 'index.html', brickset_collection=BrickSetList().last(), minifigure_collection=BrickMinifigureList().last(), + brickset_checkboxes=BrickSetCheckboxList().list(), ) diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py index 2b589b0..b36f7e1 100644 --- a/bricktracker/views/set.py +++ b/bricktracker/views/set.py @@ -15,6 +15,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_list import BrickSetList logger = logging.getLogger(__name__) @@ -26,47 +27,34 @@ set_page = Blueprint('set', __name__, url_prefix='/sets') @set_page.route('/', methods=['GET']) @exception_handler(__file__) def list() -> str: - return render_template('sets.html', collection=BrickSetList().all()) + return render_template( + 'sets.html', + collection=BrickSetList().all(), + brickset_checkboxes=BrickSetCheckboxList().list(), + ) -# Change the set checked status of one set -@set_page.route('//checked', methods=['POST']) +# Change the status of a checkbox +@set_page.route('//status/', methods=['POST']) @login_required @exception_handler(__file__, json=True) -def set_checked(*, id: str) -> Response: - state: bool = request.json.get('state', False) # type: ignore +def update_status(*, id: str, checkbox_id: str) -> Response: + value: bool = request.json.get('value', False) # type: ignore - brickset = BrickSet().select_specific(id) - brickset.update_checked('set_check', state) + brickset = BrickSet().select_light(id) + checkbox = BrickSetCheckboxList().get(checkbox_id) + + brickset.update_status(checkbox, value) # Info - logger.info('Set {number} ({id}): changed set checked status to {state}'.format( # noqa: E501 - number=brickset.fields.set_num, - id=brickset.fields.u_id, - state=state, + logger.info('Set {number} ({id}): status "{status}" changed to "{state}"'.format( # noqa: E501 + number=brickset.fields.set, + id=brickset.fields.id, + status=checkbox.fields.name, + state=value, )) - return jsonify({'state': state}) - - -# Change the set collected status of one set -@set_page.route('//collected', methods=['POST']) -@login_required -@exception_handler(__file__, json=True) -def set_collected(*, id: str) -> Response: - state: bool = request.json.get('state', False) # type: ignore - - brickset = BrickSet().select_specific(id) - brickset.update_checked('set_col', state) - - # Info - logger.info('Set {number} ({id}): changed set collected status to {state}'.format( # noqa: E501 - number=brickset.fields.set_num, - id=brickset.fields.u_id, - state=state, - )) - - return jsonify({'state': state}) + return jsonify({'value': value}) # Ask for deletion of a set @@ -85,13 +73,13 @@ def delete(*, id: str) -> str: @set_page.route('//delete', methods=['POST']) @exception_handler(__file__, post_redirect='set.delete') def do_delete(*, id: str) -> Response: - brickset = BrickSet().select_specific(id) + brickset = BrickSet().select_light(id) brickset.delete() # Info logger.info('Set {number} ({id}): deleted'.format( - number=brickset.fields.set_num, - id=brickset.fields.u_id, + number=brickset.fields.set, + id=brickset.fields.id, )) return redirect(url_for('set.deleted', id=id)) @@ -115,29 +103,10 @@ 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), ) -# Change the minifigures collected status of one set -@set_page.route('/sets//minifigures/collected', methods=['POST']) -@login_required -@exception_handler(__file__, json=True) -def minifigures_collected(*, id: str) -> Response: - state: bool = request.json.get('state', False) # type: ignore - - brickset = BrickSet().select_specific(id) - brickset.update_checked('mini_col', state) - - # Info - logger.info('Set {number} ({id}): changed minifigures collected status to {state}'.format( # noqa: E501 - number=brickset.fields.set_num, - id=brickset.fields.u_id, - state=state, - )) - - return jsonify({'state': state}) - - # Update the missing pieces of a minifig part @set_page.route('//minifigures//parts//missing', methods=['POST']) # noqa: E501 @login_required @@ -162,8 +131,8 @@ def missing_minifigure_part( # Info logger.info('Set {number} ({id}): updated minifigure ({minifigure}) part ({part}) missing count to {missing}'.format( # noqa: E501 - number=brickset.fields.set_num, - id=brickset.fields.u_id, + number=brickset.fields.set, + id=brickset.fields.id, minifigure=minifigure.fields.fig_num, part=part.fields.id, missing=missing, @@ -186,8 +155,8 @@ def missing_part(*, id: str, part_id: str) -> Response: # Info logger.info('Set {number} ({id}): updated part ({part}) missing count to {missing}'.format( # noqa: E501 - number=brickset.fields.set_num, - id=brickset.fields.u_id, + number=brickset.fields.set, + id=brickset.fields.id, part=part.fields.id, missing=missing, )) diff --git a/bricktracker/wish.py b/bricktracker/wish.py index 30643dc..1e301fa 100644 --- a/bricktracker/wish.py +++ b/bricktracker/wish.py @@ -1,38 +1,21 @@ -from sqlite3 import Row -from typing import Any, Self +from typing import Self from flask import url_for from .exceptions import NotFoundException -from .set import BrickSet +from .rebrickable_set import RebrickableSet from .sql import BrickSQL # Lego brick wished set -class BrickWish(BrickSet): +class BrickWish(RebrickableSet): + # Flags + resolve_instructions: bool = False + # Queries select_query: str = 'wish/select' insert_query: str = 'wish/insert' - def __init__( - self, - /, - *, - record: Row | dict[str, Any] | None = None, - ): - # Don't init BrickSet, init the parent of BrickSet directly - super(BrickSet, self).__init__() - - # Placeholders - self.theme_name = '' - - # Ingest the record if it has one - if record is not None: - self.ingest(record) - - # Resolve the theme - self.resolve_theme() - # Delete a wished set def delete(self, /) -> None: BrickSQL().execute_and_commit( @@ -41,37 +24,20 @@ class BrickWish(BrickSet): ) # Select a specific part (with a set and an id) - def select_specific(self, set_num: str, /) -> Self: + def select_specific(self, set: str, /) -> Self: # Save the parameters to the fields - self.fields.set_num = set_num + self.fields.set = set # 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_num, + number=self.fields.set, ), ) - # Resolve the theme - self.resolve_theme() - return self # Deletion url def url_for_delete(self, /) -> str: - return url_for('wish.delete', number=self.fields.set_num) - - # Normalize from Rebrickable - @staticmethod - def from_rebrickable(data: dict[str, Any], /, **_) -> dict[str, Any]: - return { - 'set_num': data['set_num'], - 'name': data['name'], - 'year': data['year'], - 'theme_id': data['theme_id'], - 'num_parts': data['num_parts'], - 'set_img_url': data['set_img_url'], - 'set_url': data['set_url'], - 'last_modified_dt': data['last_modified_dt'], - } + return url_for('wish.delete', number=self.fields.set) diff --git a/bricktracker/wish_list.py b/bricktracker/wish_list.py index 8c55408..dfba800 100644 --- a/bricktracker/wish_list.py +++ b/bricktracker/wish_list.py @@ -1,13 +1,17 @@ +import logging from typing import Self from flask import current_app -from bricktracker.exceptions import NotFoundException - +from .exceptions import NotFoundException +from .rebrickable import Rebrickable +from .rebrickable_image import RebrickableImage from .rebrickable_set import RebrickableSet from .record_list import BrickRecordList from .wish import BrickWish +logger = logging.getLogger(__name__) + # All the wished sets from the database class BrickWishList(BrickRecordList[BrickWish]): @@ -28,10 +32,23 @@ class BrickWishList(BrickRecordList[BrickWish]): # Add a set to the wishlist @staticmethod - def add(set_num: str, /) -> None: - # Check if it already exists + def add(set: str, /) -> None: try: - set_num = RebrickableSet.parse_number(set_num) - BrickWish().select_specific(set_num) + set = RebrickableSet.parse_number(set) + BrickWish().select_specific(set) except NotFoundException: - RebrickableSet.wish(set_num) + logger.debug('rebrick.lego.get_set("{set}")'.format( + set=set, + )) + + brickwish = Rebrickable[BrickWish]( + 'get_set', + set, + BrickWish, + ).get() + + # Insert into database + brickwish.insert() + + if not current_app.config['USE_REMOTE_IMAGES']: + RebrickableImage(brickwish).download() diff --git a/static/scripts/changer.js b/static/scripts/changer.js new file mode 100644 index 0000000..31177d5 --- /dev/null +++ b/static/scripts/changer.js @@ -0,0 +1,127 @@ +// Generic state changer with visual feedback +class BrickChanger { + constructor(prefix, id, url, parent = undefined) { + this.prefix = prefix + this.html_element = document.getElementById(`${prefix}-${id}`); + this.html_status = document.getElementById(`status-${prefix}-${id}`); + this.html_type = this.html_element.getAttribute("type"); + this.url = url; + + if (parent) { + this.html_parent = document.getElementById(`${parent}-${id}`); + this.parent_dataset = `data-${prefix}` + } + + // Register an event depending on the type + if (this.html_type == "checkbox") { + var listener = "change"; + } else { + var listener = "click"; + } + + this.html_element.addEventListener(listener, ((changer) => (e) => { + changer.change(); + })(this)); + } + + // Clean the status + status_clean() { + if (this.html_status) { + const to_remove = Array.from( + this.html_status.classList.values() + ).filter( + (name) => name.startsWith('ri-') || name.startsWith('text-') || name.startsWith('bg-') + ); + + if (to_remove.length) { + this.html_status.classList.remove(...to_remove); + } + } + } + + // Set the status to Error + status_error() { + if (this.html_status) { + this.status_clean(); + this.html_status.classList.add("ri-alert-line", "text-danger"); + } + } + + // Set the status to OK + status_ok() { + if (this.html_status) { + this.status_clean(); + this.html_status.classList.add("ri-checkbox-circle-line", "text-success"); + } + } + + // Set the status to Unknown + status_unknown() { + if (this.html_status) { + this.status_clean(); + this.html_status.classList.add("ri-question-line", "text-warning"); + } + } + + async change() { + try { + this.status_unknown(); + + // Grab the value depending on the type + if (this.html_type == "checkbox") { + var value = this.html_element.checked; + } else { + var value = this.html_element.value; + } + + const response = await fetch(this.url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: this.prefix, + value: value, + }) + }); + + if (!response.ok) { + throw new Error(`Response status: ${response.status}`); + } + + const json = await response.json(); + + if ("error" in json) { + throw new Error(`Error received: ${json.error}`) + } + + this.status_ok(); + + // Update the parent + if (this.html_parent) { + if (this.html_type == "checkbox") { + value = Number(value) + } + + // Not going through dataset to avoid converting + this.html_parent.setAttribute(this.parent_dataset, value); + } + } catch (error) { + console.log(error.message); + + this.status_error(); + } + } +} + +// Helper to setup the changer +const setup_changers = () => { + document.querySelectorAll("*[data-changer-id]").forEach(el => { + new BrickChanger( + el.dataset.changerPrefix, + el.dataset.changerId, + el.dataset.changerUrl, + el.dataset.changerParent + ); + }); +} \ No newline at end of file diff --git a/static/scripts/set.js b/static/scripts/set.js index 88966d2..25ba15d 100644 --- a/static/scripts/set.js +++ b/static/scripts/set.js @@ -7,60 +7,6 @@ const clean_status = (status) => { } } -// Change the status of a set checkbox -const change_set_checkbox_status = async (el, kind, id, url) => { - const status = document.getElementById(`status-${kind}-${id}`); - - try { - // Set the status to unknown - if (status) { - clean_status(status) - status.classList.add("ri-question-line", "text-warning"); - } - - const response = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - state: el.checked - }) - }); - - if (!response.ok) { - throw new Error(`Response status: ${response.status}`); - } - - const json = await response.json(); - - if ("error" in json) { - throw new Error(`Error received: ${json.error}`) - } - - // Set the status to OK - if (status) { - clean_status(status) - status.classList.add("ri-checkbox-circle-line", "text-success"); - } - - // Update the card - const card = document.getElementById(`set-${id}`); - if (card) { - // Not going through dataset to avoid converting - card.setAttribute(`data-${kind}`, Number(el.checked)); - } - } catch (error) { - console.log(error.message); - - // Set the status to not OK - if (status) { - clean_status(status) - status.classList.add("ri-alert-line", "text-danger"); - } - } -} - // Change the amount of missing parts const change_part_missing_amount = async (el, set_id, part_id, url) => { const status = document.getElementById(`status-part-${set_id}-${part_id}`); diff --git a/static/scripts/socket.js b/static/scripts/socket.js index 581b09d..5a24d06 100644 --- a/static/scripts/socket.js +++ b/static/scripts/socket.js @@ -30,7 +30,7 @@ class BrickSocket { // Card elements this.html_card = document.getElementById(`${id}-card`); - this.html_card_number = document.getElementById(`${id}-card-number`); + this.html_card_set = document.getElementById(`${id}-card-set`); this.html_card_name = document.getElementById(`${id}-card-name`); this.html_card_image_container = document.getElementById(`${id}-card-image-container`); this.html_card_image = document.getElementById(`${id}-card-image`); @@ -190,9 +190,9 @@ class BrickSocket { } if (this.bulk && this.html_input) { - if (this.set_list_last_number !== undefined) { - this.set_list.unshift(this.set_list_last_number); - this.set_list_last_number = undefined; + if (this.set_list_last_set !== undefined) { + this.set_list.unshift(this.set_list_last_set); + this.set_list_last_set = undefined; } this.html_input.value = this.set_list.join(', '); @@ -200,7 +200,7 @@ class BrickSocket { } // Import a set - import_set(no_confirm, number, from_complete=false) { + import_set(no_confirm, set, from_complete=false) { if (this.html_input) { if (!this.bulk || !from_complete) { // Reset the progress @@ -213,10 +213,10 @@ class BrickSocket { // Grab from the list if bulk if (this.bulk) { - number = this.set_list.shift() + set = this.set_list.shift() // Abort if nothing left to process - if (number === undefined) { + if (set === undefined) { // Clear the input this.html_input.value = ""; @@ -227,14 +227,14 @@ class BrickSocket { return; } - // Save the pulled number - this.set_list_last_number = number; + // Save the pulled set + this.set_list_last_set = set; } this.spinner(true); this.socket.emit(this.messages.IMPORT_SET, { - set_num: (number !== undefined) ? number : this.html_input.value, + set: (set !== undefined) ? set : this.html_input.value, }); } else { this.fail("Could not find the input field for the set number"); @@ -249,7 +249,7 @@ class BrickSocket { this.spinner(true); this.socket.emit(this.messages.LOAD_SET, { - set_num: this.html_input.value + set: this.html_input.value }); } else { this.fail("Could not find the input field for the set number"); @@ -319,8 +319,8 @@ class BrickSocket { if (this.html_card) { this.html_card.classList.remove("d-none"); - if (this.html_card_number) { - this.html_card_number.textContent = data["set_num"]; + if (this.html_card_set) { + this.html_card_set.textContent = data["set"]; } if (this.html_card_name) { @@ -328,12 +328,12 @@ class BrickSocket { } if (this.html_card_image_container) { - this.html_card_image_container.setAttribute("style", `background-image: url(${data["set_img_url"]})`); + this.html_card_image_container.setAttribute("style", `background-image: url(${data["image"]})`); } if (this.html_card_image) { - this.html_card_image.setAttribute("src", data["set_img_url"]); - this.html_card_image.setAttribute("alt", data["set_num"]); + this.html_card_image.setAttribute("src", data["image"]); + this.html_card_image.setAttribute("alt", data["set"]); } if (this.html_card_footer) { @@ -347,12 +347,12 @@ class BrickSocket { this.html_card_confirm.removeEventListener("click", this.confirm_listener); } - this.confirm_listener = ((bricksocket, number) => (e) => { + this.confirm_listener = ((bricksocket, set) => (e) => { if (!bricksocket.disabled) { bricksocket.toggle(false); - bricksocket.import_set(false, number); + bricksocket.import_set(false, set); } - })(this, data["set_num"]); + })(this, data["set"]); this.html_card_confirm.addEventListener("click", this.confirm_listener); } diff --git a/templates/add.html b/templates/add.html index 1251525..8fff618 100644 --- a/templates/add.html +++ b/templates/add.html @@ -47,7 +47,7 @@
- +
diff --git a/templates/admin.html b/templates/admin.html index d54efc5..860d99f 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -12,7 +12,9 @@
Administration
- {% if delete_database %} + {% if delete_checkbox %} + {% include 'admin/checkbox/delete.html' %} + {% elif delete_database %} {% include 'admin/database/delete.html' %} {% elif drop_database %} {% include 'admin/database/drop.html' %} @@ -28,6 +30,7 @@ {% endif %} {% include 'admin/theme.html' %} {% include 'admin/retired.html' %} + {% include 'admin/checkbox.html' %} {% include 'admin/database.html' %} {% include 'admin/configuration.html' %} {% endif %} @@ -38,4 +41,8 @@
+{% endblock %} + +{% block scripts %} + {% endblock %} \ No newline at end of file diff --git a/templates/admin/checkbox.html b/templates/admin/checkbox.html new file mode 100644 index 0000000..7cbd59d --- /dev/null +++ b/templates/admin/checkbox.html @@ -0,0 +1,67 @@ +{% import 'macro/accordion.html' as accordion %} + +{{ accordion.header('Checkboxes', 'checkbox', 'admin', expanded=open_checkbox, icon='checkbox-line', class='p-0') }} +{% if error %}{% endif %} +{% if database_error %}{% endif %} +
    + {% if brickset_checkboxes | length %} + {% for checkbox in brickset_checkboxes %} +
  • +
    +
    + +
    +
    Name
    + + +
    +
    +
    +
    + + +
    +
    +
    + Delete +
    +
    +
  • + {% endfor %} + {% else %} +
  • No checkbox found.
  • + {% endif %} +
  • +
    +
    + +
    +
    Name
    + +
    +
    +
    +
    + + +
    +
    +
    + +
    +
    +
  • +
+{{ accordion.footer() }} + diff --git a/templates/admin/checkbox/delete.html b/templates/admin/checkbox/delete.html new file mode 100644 index 0000000..49d507a --- /dev/null +++ b/templates/admin/checkbox/delete.html @@ -0,0 +1,25 @@ +{% import 'macro/accordion.html' as accordion %} + +{{ accordion.header('Checkbox danger zone', 'checkbox-danger', 'admin', expanded=true, danger=true, class='text-end') }} +
+ {% if error %}{% endif %} + +
+
+
+
Name
+ +
+
+
+
+ + Displayed on the Set Grid +
+
+
+
+ Back to the admin + +
+{{ accordion.footer() }} diff --git a/templates/delete.html b/templates/delete.html index 12848a9..d61821a 100644 --- a/templates/delete.html +++ b/templates/delete.html @@ -1,6 +1,6 @@ {% extends 'base.html' %} -{% block title %} - Delete a set {{ item.fields.set_num }} ({{ item.fields.u_id }}){% endblock %} +{% block title %} - Delete a set {{ item.fields.set }} ({{ item.fields.id }}){% endblock %} {% block main %}
diff --git a/templates/instructions/delete.html b/templates/instructions/delete.html index cc1ba69..9443dd0 100644 --- a/templates/instructions/delete.html +++ b/templates/instructions/delete.html @@ -8,9 +8,9 @@
{{ accordion.header('Instructions danger zone', 'instructions-delete', 'instructions', expanded=true, danger=true) }} - {% if item.brickset %} + {% if item.rebrickable %}
- {% with item=item.brickset %} + {% with item=item.rebrickable %} {% include 'set/mini.html' %} {% endwith %}
diff --git a/templates/instructions/rename.html b/templates/instructions/rename.html index cfc5626..0be3dbe 100644 --- a/templates/instructions/rename.html +++ b/templates/instructions/rename.html @@ -8,9 +8,9 @@
{{ accordion.header('Management', 'instructions-rename', 'instructions', expanded=true) }} - {% if item.brickset %} + {% if item.rebrickable %}
- {% with item=item.brickset %} + {% with item=item.rebrickable %} {% include 'set/mini.html' %} {% endwith %}
diff --git a/templates/instructions/table.html b/templates/instructions/table.html index 8f7c505..ba8f5f8 100644 --- a/templates/instructions/table.html +++ b/templates/instructions/table.html @@ -27,11 +27,11 @@ {{ item.human_time() }} - {% if item.number %} {{ item.number }}{% endif %} - {% if item.brickset %}{{ item.brickset.fields.name }}{% endif %} + {% if item.set %} {{ item.set }}{% endif %} + {% if item.rebrickable %}{{ item.rebrickable.fields.name }}{% endif %} - {% if item.brickset %} - {{ table.image(item.brickset.url_for_image(), caption=item.brickset.fields.name, alt=item.brickset.fields.set_num) }} + {% if item.rebrickable %} + {{ table.image(item.rebrickable.url_for_image(), caption=item.rebrickable.fields.name, alt=item.rebrickable.fields.set) }} {% else %} {% endif %} diff --git a/templates/macro/form.html b/templates/macro/form.html index 2e2f8bb..f9c5a5d 100644 --- a/templates/macro/form.html +++ b/templates/macro/form.html @@ -1,10 +1,14 @@ -{% macro checkbox(kind, id, text, url, checked, delete=false) %} +{% macro checkbox(prefix, id, text, url, checked, delete=false) %} {% if g.login.is_authenticated() %} - -