From c4bb3c7607cd8910632438bdbf40f7e0a4d669bf Mon Sep 17 00:00:00 2001 From: Gregoo Date: Tue, 28 Jan 2025 19:18:51 +0100 Subject: [PATCH] Deduplicated parts and missing parts --- .env.sample | 16 +- bricktracker/config.py | 6 +- bricktracker/minifigure.py | 17 +- bricktracker/minifigure_list.py | 33 +- bricktracker/part.py | 319 ++++++------------ bricktracker/part_list.py | 86 ++++- bricktracker/rebrickable_image.py | 14 +- bricktracker/rebrickable_part.py | 203 +++++++++++ bricktracker/rebrickable_parts.py | 113 ------- bricktracker/set.py | 17 +- bricktracker/set_list.py | 26 +- bricktracker/sql/migrations/0010.sql | 42 +++ bricktracker/sql/migrations/0011.sql | 60 ++++ bricktracker/sql/minifigure/base/base.sql | 1 - bricktracker/sql/minifigure/list/all.sql | 17 +- bricktracker/sql/minifigure/list/last.sql | 8 +- .../sql/minifigure/list/missing_part.sql | 24 +- .../sql/minifigure/list/using_part.sql | 15 +- .../sql/minifigure/select/generic.sql | 15 +- .../sql/minifigure/select/specific.sql | 4 +- bricktracker/sql/missing/delete/from_set.sql | 4 - bricktracker/sql/missing/insert.sql | 20 -- bricktracker/sql/missing/update/from_set.sql | 5 - bricktracker/sql/part/base/base.sql | 56 +++ bricktracker/sql/part/base/select.sql | 43 --- bricktracker/sql/part/insert.sql | 34 +- bricktracker/sql/part/list/all.sql | 28 +- .../sql/part/list/from_minifigure.sql | 23 +- bricktracker/sql/part/list/from_set.sql | 21 -- bricktracker/sql/part/list/missing.sql | 30 +- bricktracker/sql/part/list/specific.sql | 11 + bricktracker/sql/part/select/generic.sql | 30 +- bricktracker/sql/part/select/specific.sql | 28 +- bricktracker/sql/part/update/missing.sql | 7 + bricktracker/sql/rebrickable/part/insert.sql | 25 ++ bricktracker/sql/rebrickable/part/list.sql | 13 + bricktracker/sql/rebrickable/part/select.sql | 16 + bricktracker/sql/schema/drop.sql | 5 + bricktracker/sql/set/base/full.sql | 10 +- bricktracker/sql/set/delete/set.sql | 7 +- .../sql/set/list/missing_minifigure.sql | 12 +- bricktracker/sql/set/list/missing_part.sql | 15 +- .../sql/set/list/using_minifigure.sql | 11 +- bricktracker/sql/set/list/using_part.sql | 14 +- bricktracker/sql/set/select/full.sql | 2 +- bricktracker/sql_counter.py | 7 +- bricktracker/version.py | 2 +- bricktracker/views/part.py | 27 +- bricktracker/views/set.py | 59 ++-- templates/macro/card.html | 4 +- templates/minifigure/card.html | 2 +- templates/part/card.html | 4 +- templates/part/table.html | 10 +- templates/set/card.html | 4 +- templates/set/mini.html | 2 +- 55 files changed, 871 insertions(+), 756 deletions(-) create mode 100644 bricktracker/rebrickable_part.py delete mode 100644 bricktracker/rebrickable_parts.py create mode 100644 bricktracker/sql/migrations/0010.sql create mode 100644 bricktracker/sql/migrations/0011.sql delete mode 100644 bricktracker/sql/missing/delete/from_set.sql delete mode 100644 bricktracker/sql/missing/insert.sql delete mode 100644 bricktracker/sql/missing/update/from_set.sql create mode 100644 bricktracker/sql/part/base/base.sql delete mode 100644 bricktracker/sql/part/base/select.sql delete mode 100644 bricktracker/sql/part/list/from_set.sql create mode 100644 bricktracker/sql/part/list/specific.sql create mode 100644 bricktracker/sql/part/update/missing.sql create mode 100644 bricktracker/sql/rebrickable/part/insert.sql create mode 100644 bricktracker/sql/rebrickable/part/list.sql create mode 100644 bricktracker/sql/rebrickable/part/select.sql diff --git a/.env.sample b/.env.sample index 04e84ee..46b2131 100644 --- a/.env.sample +++ b/.env.sample @@ -28,7 +28,7 @@ # BK_AUTHENTICATION_KEY=change-this-to-something-random # Optional: Pattern of the link to Bricklink for a part. Will be passed to Python .format() -# Default: https://www.bricklink.com/v2/catalog/catalogitem.page?P={number} +# Default: https://www.bricklink.com/v2/catalog/catalogitem.page?P={part} # BK_BRICKLINK_LINK_PART_PATTERN= # Optional: Display Bricklink links wherever applicable @@ -139,13 +139,13 @@ # Optional: Change the default order of parts. By default ordered by insertion order. # Useful column names for this option are: -# - "inventory"."part_num": part number -# - "inventory"."name": part name -# - "inventory"."color_name": part color name -# - "inventory"."is_spare": par is a spare part +# - "bricktracker_parts"."part": part number +# - "bricktracker_parts"."spare": part is a spare part +# - "rebrickable_parts"."name": part name +# - "rebrickable_parts"."color_name": part color name # - "total_missing": number of missing parts -# Default: "inventory"."name" ASC, "inventory"."color_name" ASC, "inventory"."is_spare" ASC -# BK_PARTS_DEFAULT_ORDER="total_missing" DESC, "inventory"."name" ASC +# Default: "rebrickable_parts"."name" ASC, "rebrickable_parts"."color_name" ASC, "bricktracker_parts"."spare" ASC +# BK_PARTS_DEFAULT_ORDER="total_missing" DESC, "rebrickable_parts"."name"."name" ASC # Optional: Folder where to store the parts images, relative to the '/app/static/' folder # Default: parts @@ -180,7 +180,7 @@ # BK_REBRICKABLE_LINK_MINIFIGURE_PATTERN= # Optional: Pattern of the link to Rebrickable for a part. Will be passed to Python .format() -# Default: https://rebrickable.com/parts/{number}/_/{color} +# Default: https://rebrickable.com/parts/{part}/_/{color} # BK_REBRICKABLE_LINK_PART_PATTERN= # Optional: Pattern of the link to Rebrickable for instructions. Will be passed to Python .format() diff --git a/bricktracker/config.py b/bricktracker/config.py index 236eb54..83cd99a 100644 --- a/bricktracker/config.py +++ b/bricktracker/config.py @@ -10,7 +10,7 @@ from typing import Any, Final CONFIG: Final[list[dict[str, Any]]] = [ {'n': 'AUTHENTICATION_PASSWORD', 'd': ''}, {'n': 'AUTHENTICATION_KEY', 'd': ''}, - {'n': 'BRICKLINK_LINK_PART_PATTERN', 'd': 'https://www.bricklink.com/v2/catalog/catalogitem.page?P={number}'}, # noqa: E501 + {'n': 'BRICKLINK_LINK_PART_PATTERN', 'd': 'https://www.bricklink.com/v2/catalog/catalogitem.page?P={part}'}, # noqa: E501 {'n': 'BRICKLINK_LINKS', 'c': bool}, {'n': 'DATABASE_PATH', 'd': './app.db'}, {'n': 'DATABASE_TIMESTAMP_FORMAT', 'd': '%Y-%m-%d-%H-%M-%S'}, @@ -35,7 +35,7 @@ CONFIG: Final[list[dict[str, Any]]] = [ {'n': 'MINIFIGURES_DEFAULT_ORDER', 'd': '"rebrickable_minifigures"."name" ASC'}, # noqa: E501 {'n': 'MINIFIGURES_FOLDER', 'd': 'minifigs', 's': True}, {'n': 'NO_THREADED_SOCKET', 'c': bool}, - {'n': 'PARTS_DEFAULT_ORDER', 'd': '"inventory"."name" ASC, "inventory"."color_name" ASC, "inventory"."is_spare" ASC'}, # noqa: E501 + {'n': 'PARTS_DEFAULT_ORDER', 'd': '"rebrickable_parts"."name" ASC, "rebrickable_parts"."color_name" ASC, "bricktracker_parts"."spare" ASC'}, # noqa: E501 {'n': 'PARTS_FOLDER', 'd': 'parts', 's': True}, {'n': 'PORT', 'd': 3333, 'c': int}, {'n': 'RANDOM', 'e': 'RANDOM', 'c': bool}, @@ -43,7 +43,7 @@ CONFIG: Final[list[dict[str, Any]]] = [ {'n': 'REBRICKABLE_IMAGE_NIL', 'd': 'https://rebrickable.com/static/img/nil.png'}, # noqa: E501 {'n': 'REBRICKABLE_IMAGE_NIL_MINIFIGURE', 'd': 'https://rebrickable.com/static/img/nil_mf.jpg'}, # noqa: E501 {'n': 'REBRICKABLE_LINK_MINIFIGURE_PATTERN', 'd': 'https://rebrickable.com/minifigs/{figure}'}, # noqa: E501 - {'n': 'REBRICKABLE_LINK_PART_PATTERN', 'd': 'https://rebrickable.com/parts/{number}/_/{color}'}, # noqa: E501 + {'n': 'REBRICKABLE_LINK_PART_PATTERN', 'd': 'https://rebrickable.com/parts/{part}/_/{color}'}, # noqa: E501 {'n': 'REBRICKABLE_LINK_INSTRUCTIONS_PATTERN', 'd': 'https://rebrickable.com/instructions/{path}'}, # noqa: E501 {'n': 'REBRICKABLE_USER_AGENT', 'd': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'}, # noqa: E501 {'n': 'REBRICKABLE_LINKS', 'e': 'LINKS', 'c': bool}, diff --git a/bricktracker/minifigure.py b/bricktracker/minifigure.py index 76a482e..e0318a0 100644 --- a/bricktracker/minifigure.py +++ b/bricktracker/minifigure.py @@ -4,7 +4,6 @@ from typing import Self, TYPE_CHECKING from .exceptions import ErrorException, NotFoundException from .part_list import BrickPartList -from .rebrickable_parts import RebrickableParts from .rebrickable_minifigure import RebrickableMinifigure if TYPE_CHECKING: from .set import BrickSet @@ -20,7 +19,8 @@ class BrickMinifigure(RebrickableMinifigure): generic_query: str = 'minifigure/select/generic' select_query: str = 'minifigure/select/specific' - def download(self, socket: 'BrickSocket'): + # Import a minifigure into the database + def download(self, socket: 'BrickSocket') -> bool: if self.brickset is None: raise ErrorException('Importing a minifigure from Rebrickable outside of a set is not supported') # noqa: E501 @@ -40,11 +40,12 @@ class BrickMinifigure(RebrickableMinifigure): self.insert_rebrickable() # Load the inventory - RebrickableParts( + if not BrickPartList.download( socket, self.brickset, - minifigure=self, - ).download() + minifigure=self + ): + return False except Exception as e: socket.fail( @@ -57,6 +58,10 @@ class BrickMinifigure(RebrickableMinifigure): logger.debug(traceback.format_exc()) + return False + + return True + # Parts def generic_parts(self, /) -> BrickPartList: return BrickPartList().from_minifigure(self) @@ -68,7 +73,7 @@ class BrickMinifigure(RebrickableMinifigure): figure=self.fields.figure, )) - return BrickPartList().load(self.brickset, minifigure=self) + return BrickPartList().list_specific(self.brickset, minifigure=self) # Select a generic minifigure def select_generic(self, figure: str, /) -> Self: diff --git a/bricktracker/minifigure_list.py b/bricktracker/minifigure_list.py index 24b9933..a59fee5 100644 --- a/bricktracker/minifigure_list.py +++ b/bricktracker/minifigure_list.py @@ -82,16 +82,13 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): # Minifigures missing a part def missing_part( self, - part_num: str, - color_id: int, + part: str, + color: int, /, - *, - element_id: int | None = None, ) -> Self: # Save the parameters to the fields - self.fields.part_num = part_num - self.fields.color_id = color_id - self.fields.element_id = element_id + self.fields.part = part + self.fields.color = color # Load the minifigures from the database for record in self.select( @@ -107,16 +104,13 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): # Minifigure using a part def using_part( self, - part_num: str, - color_id: int, + part: str, + color: int, /, - *, - element_id: int | None = None, ) -> Self: # Save the parameters to the fields - self.fields.part_num = part_num - self.fields.color_id = color_id - self.fields.element_id = element_id + self.fields.part = part + self.fields.color = color # Load the minifigures from the database for record in self.select( @@ -140,7 +134,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): # Import the minifigures from Rebrickable @staticmethod - def download(socket: 'BrickSocket', brickset: 'BrickSet', /) -> None: + def download(socket: 'BrickSocket', brickset: 'BrickSet', /) -> bool: try: socket.auto_progress( message='Set {set}: loading minifigures from Rebrickable'.format( # noqa: E501 @@ -162,10 +156,11 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): ).list() # Process each minifigure - socket.update_total(len(minifigures), add=True) - for minifigure in minifigures: - minifigure.download(socket) + if not minifigure.download(socket): + return False + + return True except Exception as e: socket.fail( @@ -176,3 +171,5 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): ) logger.debug(traceback.format_exc()) + + return False diff --git a/bricktracker/part.py b/bricktracker/part.py index b6dc153..7e82c45 100644 --- a/bricktracker/part.py +++ b/bricktracker/part.py @@ -1,103 +1,104 @@ -import os -from sqlite3 import Row +import logging from typing import Any, Self, TYPE_CHECKING -from urllib.parse import urlparse +import traceback -from flask import current_app, url_for - -from .exceptions import DatabaseException, ErrorException, NotFoundException -from .rebrickable_image import RebrickableImage -from .record import BrickRecord +from .exceptions import ErrorException, NotFoundException +from .rebrickable_part import RebrickablePart from .sql import BrickSQL if TYPE_CHECKING: from .minifigure import BrickMinifigure from .set import BrickSet + from .socket import BrickSocket + +logger = logging.getLogger(__name__) # Lego set or minifig part -class BrickPart(BrickRecord): - brickset: 'BrickSet | None' - minifigure: 'BrickMinifigure | None' +class BrickPart(RebrickablePart): + identifier: str + kind: str # Queries insert_query: str = 'part/insert' generic_query: str = 'part/select/generic' select_query: str = 'part/select/specific' - def __init__( - self, - /, - *, - brickset: 'BrickSet | None' = None, - minifigure: 'BrickMinifigure | None' = None, - record: Row | dict[str, Any] | None = None, - ): - super().__init__() + def __init__(self, /, **kwargs): + super().__init__(**kwargs) - # Save the brickset and minifigure - self.brickset = brickset - self.minifigure = minifigure + if self.minifigure is not None: + self.identifier = self.minifigure.fields.figure + self.kind = 'Minifigure' + elif self.brickset is not None: + self.identifier = self.brickset.fields.set + self.kind = 'Set' - # Ingest the record if it has one - if record is not None: - self.ingest(record) + # Import a part into the database + def download(self, socket: 'BrickSocket') -> bool: + if self.brickset is None: + raise ErrorException('Importing a part from Rebrickable outside of a set is not supported') # noqa: E501 - # Delete missing part - def delete_missing(self, /) -> None: - BrickSQL().execute_and_commit( - 'missing/delete/from_set', - parameters=self.sql_parameters() - ) - - # Set missing part - def set_missing(self, quantity: int, /) -> None: - parameters = self.sql_parameters() - parameters['quantity'] = quantity - - # Can't use UPSERT because the database has no keys - # Try to update - database = BrickSQL() - rows, _ = database.execute( - 'missing/update/from_set', - parameters=parameters, - ) - - # Insert if no row has been affected - if not rows: - rows, _ = database.execute( - 'missing/insert', - parameters=parameters, + try: + # Insert into the database + socket.auto_progress( + message='{kind} {identifier}: inserting part {part} into database'.format( # noqa: E501 + kind=self.kind, + identifier=self.identifier, + part=self.fields.part + ) ) - if rows != 1: - raise DatabaseException( - 'Could not update the missing quantity for part {id}'.format( # noqa: E501 - id=self.fields.id - ) - ) + # Insert into database + self.insert(commit=False) - database.commit() + # Insert the rebrickable set into database + self.insert_rebrickable() + + except Exception as e: + socket.fail( + message='Error while importing part {part} from {kind} {identifier}: {error}'.format( # noqa: E501 + part=self.fields.part, + kind=self.kind, + identifier=self.identifier, + error=e, + ) + ) + + logger.debug(traceback.format_exc()) + + return False + + return True + + # A identifier for HTML component + def html_id(self) -> str: + components: list[str] = [] + + if self.fields.figure is not None: + components.append(self.fields.figure) + + components.append(self.fields.part) + components.append(str(self.fields.color)) + components.append(str(self.fields.spare)) + + return '-'.join(components) # Select a generic part def select_generic( self, - part_num: str, - color_id: int, + part: str, + color: int, /, - *, - element_id: int | None = None ) -> Self: # Save the parameters to the fields - self.fields.part_num = part_num - self.fields.color_id = color_id - self.fields.element_id = element_id + self.fields.part = part + self.fields.color = color if not self.select(override_query=self.generic_query): raise NotFoundException( - 'Part with number {number}, color ID {color} and element ID {element} was not found in the database'.format( # noqa: E501 - number=self.fields.part_num, - color=self.fields.color_id, - element=self.fields.element_id, + 'Part with number {number}, color ID {color} was not found in the database'.format( # noqa: E501 + number=self.fields.part, + color=self.fields.color, ), ) @@ -107,7 +108,9 @@ class BrickPart(BrickRecord): def select_specific( self, brickset: 'BrickSet', - id: str, + part: str, + color: int, + spare: int, /, *, minifigure: 'BrickMinifigure | None' = None, @@ -115,168 +118,48 @@ class BrickPart(BrickRecord): # Save the parameters to the fields self.brickset = brickset self.minifigure = minifigure - self.fields.id = id + self.fields.part = part + self.fields.color = color + self.fields.spare = spare if not self.select(): + if self.minifigure is not None: + figure = self.minifigure.fields.figure + else: + figure = None + raise NotFoundException( - 'Part with ID {id} from set {set} was not found in the database'.format( # noqa: E501 + 'Part {part} with color {color} (spare: {spare}) from set {set} ({id}) (minifigure: {figure}) was not found in the database'.format( # noqa: E501 + part=self.fields.part, + color=self.fields.color, + spare=self.fields.spare, id=self.fields.id, set=self.brickset.fields.set, + figure=figure, ), ) return self - # Return a dict with common SQL parameters for a part - def sql_parameters(self, /) -> dict[str, Any]: - parameters = super().sql_parameters() - - # Supplement from the brickset - if 'u_id' not in parameters and self.brickset is not None: - parameters['u_id'] = self.brickset.fields.id - - if 'set_num' not in parameters: - if self.minifigure is not None: - parameters['set_num'] = self.minifigure.fields.figure - - elif self.brickset is not None: - parameters['set_num'] = self.brickset.fields.set - - return parameters - # Update the missing part def update_missing(self, missing: Any, /) -> None: - # If empty, delete it - if missing == '': - self.delete_missing() + # We need a positive integer + try: + missing = int(missing) - else: - # Try to understand it as a number - try: - missing = int(missing) - except Exception: - raise ErrorException('"{missing}" is not a valid integer'.format( # noqa: E501 - missing=missing - )) + if missing < 0: + missing = 0 + except Exception: + raise ErrorException('"{missing}" is not a valid integer'.format( + missing=missing + )) - # If 0, delete it - if missing == 0: - self.delete_missing() + if missing < 0: + raise ErrorException('Cannot set a negative missing value') - else: - # If negative, it's an error - if missing < 0: - raise ErrorException('Cannot set a negative missing value') + self.fields.missing = missing - # Otherwise upsert it - # Not checking if it is too much, you do you - self.set_missing(missing) - - # Self url - def url(self, /) -> str: - return url_for( - 'part.details', - number=self.fields.part_num, - color=self.fields.color_id, - element=self.fields.element_id, + BrickSQL().execute_and_commit( + 'part/update/missing', + parameters=self.sql_parameters() ) - - # Compute the url for the bricklink page - def url_for_bricklink(self, /) -> str: - if current_app.config['BRICKLINK_LINKS']: - try: - return current_app.config['BRICKLINK_LINK_PART_PATTERN'].format( # noqa: E501 - number=self.fields.part_num, - ) - except Exception: - pass - - return '' - - # Compute the url for the part image - def url_for_image(self, /) -> str: - if not current_app.config['USE_REMOTE_IMAGES']: - if self.fields.part_img_url is None: - file = RebrickableImage.nil_name() - else: - file = self.fields.part_img_url_id - - return RebrickableImage.static_url(file, 'PARTS_FOLDER') - else: - if self.fields.part_img_url is None: - return current_app.config['REBRICKABLE_IMAGE_NIL'] - else: - return self.fields.part_img_url - - # Compute the url for missing part - def url_for_missing(self, /) -> str: - # Different URL for a minifigure part - if self.minifigure is not None: - return url_for( - 'set.missing_minifigure_part', - id=self.fields.u_id, - figure=self.minifigure.fields.figure, - part=self.fields.id, - ) - - return url_for( - 'set.missing_part', - id=self.fields.u_id, - part=self.fields.id - ) - - # Compute the url for the rebrickable page - def url_for_rebrickable(self, /) -> str: - if current_app.config['REBRICKABLE_LINKS']: - try: - return current_app.config['REBRICKABLE_LINK_PART_PATTERN'].format( # noqa: E501 - number=self.fields.part_num, - color=self.fields.color_id, - ) - except Exception: - pass - - return '' - - # Normalize from Rebrickable - @staticmethod - def from_rebrickable( - data: dict[str, Any], - /, - *, - brickset: 'BrickSet | None' = None, - minifigure: 'BrickMinifigure | None' = None, - **_, - ) -> dict[str, Any]: - record = { - 'set_num': data['set_num'], - 'id': data['id'], - 'part_num': data['part']['part_num'], - 'name': data['part']['name'], - 'part_img_url': data['part']['part_img_url'], - 'part_img_url_id': None, - 'color_id': data['color']['id'], - 'color_name': data['color']['name'], - 'quantity': data['quantity'], - 'is_spare': data['is_spare'], - 'element_id': data['element_id'], - } - - if brickset is not None: - record['u_id'] = brickset.fields.id - - if minifigure is not None: - record['set_num'] = data['fig_num'] - - # Extract the file name - if data['part']['part_img_url'] is not None: - part_img_url_file = os.path.basename( - urlparse(data['part']['part_img_url']).path - ) - - part_img_url_id, _ = os.path.splitext(part_img_url_file) - - if part_img_url_id is not None or part_img_url_id != '': - record['part_img_url_id'] = part_img_url_id - - return record diff --git a/bricktracker/part_list.py b/bricktracker/part_list.py index 7805d57..0074b9b 100644 --- a/bricktracker/part_list.py +++ b/bricktracker/part_list.py @@ -1,12 +1,18 @@ +import logging from typing import Any, Self, TYPE_CHECKING +import traceback from flask import current_app from .part import BrickPart +from .rebrickable import Rebrickable from .record_list import BrickRecordList if TYPE_CHECKING: from .minifigure import BrickMinifigure from .set import BrickSet + from .socket import BrickSocket + +logger = logging.getLogger(__name__) # Lego set or minifig parts @@ -20,7 +26,7 @@ class BrickPartList(BrickRecordList[BrickPart]): last_query: str = 'part/list/last' minifigure_query: str = 'part/list/from_minifigure' missing_query: str = 'part/list/missing' - select_query: str = 'part/list/from_set' + select_query: str = 'part/list/specific' def __init__(self, /): super().__init__() @@ -44,8 +50,8 @@ class BrickPartList(BrickRecordList[BrickPart]): return self - # Load parts from a brickset or minifigure - def load( + # List specific parts from a brickset or minifigure + def list_specific( self, brickset: 'BrickSet', /, @@ -64,7 +70,7 @@ class BrickPartList(BrickRecordList[BrickPart]): record=record, ) - if current_app.config['SKIP_SPARE_PARTS'] and part.fields.is_spare: + if current_app.config['SKIP_SPARE_PARTS'] and part.fields.spare: continue self.records.append(part) @@ -90,7 +96,7 @@ class BrickPartList(BrickRecordList[BrickPart]): record=record, ) - if current_app.config['SKIP_SPARE_PARTS'] and part.fields.is_spare: + if current_app.config['SKIP_SPARE_PARTS'] and part.fields.spare: continue self.records.append(part) @@ -115,13 +121,73 @@ class BrickPartList(BrickRecordList[BrickPart]): # Set id if self.brickset is not None: - parameters['u_id'] = self.brickset.fields.id + parameters['id'] = self.brickset.fields.id # Use the minifigure number if present, - # otherwise use the set number if self.minifigure is not None: - parameters['set_num'] = self.minifigure.fields.figure - elif self.brickset is not None: - parameters['set_num'] = self.brickset.fields.set + parameters['figure'] = self.minifigure.fields.figure + else: + parameters['figure'] = None return parameters + + # Import the parts from Rebrickable + @staticmethod + def download( + socket: 'BrickSocket', + brickset: 'BrickSet', + /, + *, + minifigure: 'BrickMinifigure | None' = None, + ) -> bool: + if minifigure is not None: + identifier = minifigure.fields.figure + kind = 'Minifigure' + method = 'get_minifig_elements' + else: + identifier = brickset.fields.set + kind = 'Set' + method = 'get_set_elements' + + try: + socket.auto_progress( + message='{kind} {identifier}: loading parts inventory from Rebrickable'.format( # noqa: E501 + kind=kind, + identifier=identifier, + ), + increment_total=True, + ) + + logger.debug('rebrick.lego.{method}("{identifier}")'.format( + method=method, + identifier=identifier, + )) + + inventory = Rebrickable[BrickPart]( + method, + identifier, + BrickPart, + socket=socket, + brickset=brickset, + minifigure=minifigure, + ).list() + + # Process each part + for part in inventory: + if not part.download(socket): + return False + + except Exception as e: + socket.fail( + message='Error while importing {kind} {identifier} parts list: {error}'.format( # noqa: E501 + kind=kind, + identifier=identifier, + error=e, + ) + ) + + logger.debug(traceback.format_exc()) + + return False + + return True diff --git a/bricktracker/rebrickable_image.py b/bricktracker/rebrickable_image.py index f15a9b4..509e718 100644 --- a/bricktracker/rebrickable_image.py +++ b/bricktracker/rebrickable_image.py @@ -9,7 +9,7 @@ from shutil import copyfileobj from .exceptions import DownloadException if TYPE_CHECKING: from .rebrickable_minifigure import RebrickableMinifigure - from .part import BrickPart + from .rebrickable_part import RebrickablePart from .rebrickable_set import RebrickableSet @@ -17,7 +17,7 @@ if TYPE_CHECKING: class RebrickableImage(object): set: 'RebrickableSet' minifigure: 'RebrickableMinifigure | None' - part: 'BrickPart | None' + part: 'RebrickablePart | None' extension: str | None @@ -27,7 +27,7 @@ class RebrickableImage(object): /, *, minifigure: 'RebrickableMinifigure | None' = None, - part: 'BrickPart | None' = None, + part: 'RebrickablePart | None' = None, ): # Save all objects self.set = set @@ -81,10 +81,10 @@ class RebrickableImage(object): # Return the id depending on the objects provided def id(self, /) -> str: if self.part is not None: - if self.part.fields.part_img_url_id is None: + if self.part.fields.image_id is None: return RebrickableImage.nil_name() else: - return self.part.fields.part_img_url_id + return self.part.fields.image_id if self.minifigure is not None: if self.minifigure.fields.image is None: @@ -105,10 +105,10 @@ class RebrickableImage(object): # Return the url depending on the objects provided def url(self, /) -> str: if self.part is not None: - if self.part.fields.part_img_url is None: + if self.part.fields.image is None: return current_app.config['REBRICKABLE_IMAGE_NIL'] else: - return self.part.fields.part_img_url + return self.part.fields.image if self.minifigure is not None: if self.minifigure.fields.image is None: diff --git a/bricktracker/rebrickable_part.py b/bricktracker/rebrickable_part.py new file mode 100644 index 0000000..93c6b34 --- /dev/null +++ b/bricktracker/rebrickable_part.py @@ -0,0 +1,203 @@ +import os +import logging +from sqlite3 import Row +from typing import Any, TYPE_CHECKING +from urllib.parse import urlparse + +from flask import current_app, url_for + +from .exceptions import ErrorException +from .rebrickable_image import RebrickableImage +from .record import BrickRecord +if TYPE_CHECKING: + from .minifigure import BrickMinifigure + from .set import BrickSet + from .socket import BrickSocket + +logger = logging.getLogger(__name__) + + +# A part from Rebrickable +class RebrickablePart(BrickRecord): + socket: 'BrickSocket' + brickset: 'BrickSet | None' + minifigure: 'BrickMinifigure | None' + + # Queries + select_query: str = 'rebrickable/part/select' + insert_query: str = 'rebrickable/part/insert' + + def __init__( + self, + /, + *, + brickset: 'BrickSet | None' = None, + minifigure: 'BrickMinifigure | None' = None, + record: Row | dict[str, Any] | None = None + ): + super().__init__() + + # Save the brickset + self.brickset = brickset + + # Save the minifigure + self.minifigure = minifigure + + # Ingest the record if it has one + if record is not None: + self.ingest(record) + + # Insert the part from Rebrickable + def insert_rebrickable(self, /) -> bool: + if self.brickset is None: + raise ErrorException('Importing a part from Rebrickable outside of a set is not supported') # noqa: E501 + + # Insert the Rebrickable part to the database + rows, _ = self.insert( + commit=False, + no_defer=True, + override_query=RebrickablePart.insert_query + ) + + inserted = rows > 0 + + if inserted: + if not current_app.config['USE_REMOTE_IMAGES']: + RebrickableImage( + self.brickset, + minifigure=self.minifigure, + part=self, + ).download() + + return inserted + + # Return a dict with common SQL parameters for a part + def sql_parameters(self, /) -> dict[str, Any]: + parameters = super().sql_parameters() + + # Set id + if self.brickset is not None: + parameters['id'] = self.brickset.fields.id + + # Use the minifigure number if present, + if self.minifigure is not None: + parameters['figure'] = self.minifigure.fields.figure + else: + parameters['figure'] = None + + return parameters + + # Self url + def url(self, /) -> str: + return url_for( + 'part.details', + part=self.fields.part, + color=self.fields.color, + ) + + # Compute the url for the bricklink page + def url_for_bricklink(self, /) -> str: + if current_app.config['BRICKLINK_LINKS']: + try: + return current_app.config['BRICKLINK_LINK_PART_PATTERN'].format( # noqa: E501 + part=self.fields.part, + ) + except Exception: + pass + + return '' + + # Compute the url for the part image + def url_for_image(self, /) -> str: + if not current_app.config['USE_REMOTE_IMAGES']: + if self.fields.image is None: + file = RebrickableImage.nil_name() + else: + file = self.fields.image_id + + return RebrickableImage.static_url(file, 'PARTS_FOLDER') + else: + if self.fields.image is None: + return current_app.config['REBRICKABLE_IMAGE_NIL'] + else: + return self.fields.image + + # Compute the url for missing part + def url_for_missing(self, /) -> str: + # Different URL for a minifigure part + if self.minifigure is not None: + figure = self.minifigure.fields.figure + else: + figure = None + + return url_for( + 'set.missing_part', + id=self.fields.id, + figure=figure, + part=self.fields.part, + color=self.fields.color, + spare=self.fields.spare, + ) + + # Compute the url for the rebrickable page + def url_for_rebrickable(self, /) -> str: + if current_app.config['REBRICKABLE_LINKS']: + try: + return current_app.config['REBRICKABLE_LINK_PART_PATTERN'].format( # noqa: E501 + part=self.fields.part, + color=self.fields.color, + ) + except Exception: + pass + + return '' + + # Normalize from Rebrickable + @staticmethod + def from_rebrickable( + data: dict[str, Any], + /, + *, + brickset: 'BrickSet | None' = None, + minifigure: 'BrickMinifigure | None' = None, + **_, + ) -> dict[str, Any]: + record = { + 'id': None, + 'figure': None, + 'part': data['part']['part_num'], + 'color': data['color']['id'], + 'spare': data['is_spare'], + 'quantity': data['quantity'], + 'rebrickable_inventory': data['id'], + 'element': data['element_id'], + 'color_id': data['color']['id'], + 'color_name': data['color']['name'], + 'color_rgb': data['color']['rgb'], + 'color_transparent': data['color']['is_trans'], + 'name': data['part']['name'], + 'category': data['part']['part_cat_id'], + 'image': data['part']['part_img_url'], + 'image_id': None, + 'url': data['part']['part_url'], + 'print': data['part']['print_of'] + } + + if brickset is not None: + record['id'] = brickset.fields.id + + if minifigure is not None: + record['figure'] = minifigure.fields.figure + + # Extract the file name + if record['image'] is not None: + image_id, _ = os.path.splitext( + os.path.basename( + urlparse(record['image']).path + ) + ) + + if image_id is not None or image_id != '': + record['image_id'] = image_id + + return record diff --git a/bricktracker/rebrickable_parts.py b/bricktracker/rebrickable_parts.py deleted file mode 100644 index 9fd2341..0000000 --- a/bricktracker/rebrickable_parts.py +++ /dev/null @@ -1,113 +0,0 @@ -import logging -from typing import TYPE_CHECKING - -from flask import current_app - -from .part import BrickPart -from .rebrickable import Rebrickable -from .rebrickable_image import RebrickableImage -if TYPE_CHECKING: - from .minifigure import BrickMinifigure - from .set import BrickSet - from .socket import BrickSocket - -logger = logging.getLogger(__name__) - - -# A list of parts from Rebrickable -class RebrickableParts(object): - socket: 'BrickSocket' - brickset: 'BrickSet' - minifigure: 'BrickMinifigure | None' - - number: str - kind: str - method: str - - def __init__( - self, - socket: 'BrickSocket', - brickset: 'BrickSet', - /, - *, - minifigure: 'BrickMinifigure | None' = None, - ): - # Save the socket - self.socket = socket - - # Save the objects - self.brickset = brickset - self.minifigure = minifigure - - if self.minifigure is not None: - self.number = self.minifigure.fields.figure - self.kind = 'Minifigure' - self.method = 'get_minifig_elements' - else: - self.number = self.brickset.fields.set - self.kind = 'Set' - self.method = 'get_set_elements' - - # Import the parts from Rebrickable - def download(self, /) -> None: - self.socket.auto_progress( - message='{kind} {number}: loading parts inventory from Rebrickable'.format( # noqa: E501 - kind=self.kind, - number=self.number, - ), - increment_total=True, - ) - - logger.debug('rebrick.lego.{method}("{number}")'.format( - method=self.method, - number=self.number, - )) - - inventory = Rebrickable[BrickPart]( - self.method, - self.number, - BrickPart, - socket=self.socket, - brickset=self.brickset, - minifigure=self.minifigure, - ).list() - - # Process each part - total = len(inventory) - for index, part in enumerate(inventory): - # Skip spare parts - if ( - current_app.config['SKIP_SPARE_PARTS'] and - part.fields.is_spare - ): - continue - - # Insert into the database - self.socket.auto_progress( - message='{kind} {number}: inserting part {current}/{total} into database'.format( # noqa: E501 - kind=self.kind, - number=self.number, - current=index+1, - total=total, - ) - ) - - # Insert into database - part.insert(commit=False) - - # Grab the image - self.socket.progress( - message='{kind} {number}: downloading part {current}/{total} image'.format( # noqa: E501 - kind=self.kind, - number=self.number, - current=index+1, - total=total, - ) - ) - - if not current_app.config['USE_REMOTE_IMAGES']: - RebrickableImage( - self.brickset, - minifigure=self.minifigure, - part=part, - ).download() diff --git a/bricktracker/set.py b/bricktracker/set.py index 52a2ed9..63d4128 100644 --- a/bricktracker/set.py +++ b/bricktracker/set.py @@ -8,7 +8,6 @@ from flask import current_app, url_for from .exceptions import DatabaseException, NotFoundException from .minifigure_list import BrickMinifigureList from .part_list import BrickPartList -from .rebrickable_parts import RebrickableParts from .rebrickable_set import RebrickableSet from .set_checkbox import BrickSetCheckbox from .set_checkbox_list import BrickSetCheckboxList @@ -34,10 +33,10 @@ class BrickSet(RebrickableSet): ) # Import a set into the database - def download(self, socket: 'BrickSocket', data: dict[str, Any], /) -> None: + def download(self, socket: 'BrickSocket', data: dict[str, Any], /) -> bool: # Load the set if not self.load(socket, data, from_download=True): - return + return False try: # Insert into the database @@ -58,10 +57,12 @@ class BrickSet(RebrickableSet): self.insert_rebrickable() # Load the inventory - RebrickableParts(socket, self).download() + if not BrickPartList.download(socket, self): + return False # Load the minifigures - BrickMinifigureList.download(socket, self) + if not BrickMinifigureList.download(socket, self): + return False # Commit the transaction to the database socket.auto_progress( @@ -98,13 +99,17 @@ class BrickSet(RebrickableSet): logger.debug(traceback.format_exc()) + return False + + return True + # Minifigures def minifigures(self, /) -> BrickMinifigureList: return BrickMinifigureList().from_set(self) # Parts def parts(self, /) -> BrickPartList: - return BrickPartList().load(self) + return BrickPartList().list_specific(self) # Select a light set (with an id) def select_light(self, id: str, /) -> Self: diff --git a/bricktracker/set_list.py b/bricktracker/set_list.py index 58ae8ec..349af66 100644 --- a/bricktracker/set_list.py +++ b/bricktracker/set_list.py @@ -100,16 +100,13 @@ class BrickSetList(BrickRecordList[BrickSet]): # Sets missing a part def missing_part( self, - part_num: str, - color_id: int, - /, - *, - element_id: int | None = None, + part: str, + color: int, + / ) -> Self: # Save the parameters to the fields - self.fields.part_num = part_num - self.fields.color_id = color_id - self.fields.element_id = element_id + self.fields.part = part + self.fields.color = color # Load the sets from the database for record in self.select( @@ -141,16 +138,13 @@ class BrickSetList(BrickRecordList[BrickSet]): # Sets using a part def using_part( self, - part_num: str, - color_id: int, - /, - *, - element_id: int | None = None, + part: str, + color: int, + / ) -> Self: # Save the parameters to the fields - self.fields.part_num = part_num - self.fields.color_id = color_id - self.fields.element_id = element_id + self.fields.part = part + self.fields.color = color # Load the sets from the database for record in self.select( diff --git a/bricktracker/sql/migrations/0010.sql b/bricktracker/sql/migrations/0010.sql new file mode 100644 index 0000000..8b4b6e6 --- /dev/null +++ b/bricktracker/sql/migrations/0010.sql @@ -0,0 +1,42 @@ +-- description: Creation of the deduplicated table of Rebrickable parts, and add a bunch of extra fields for later + +BEGIN TRANSACTION; + +-- Create a Rebrickable parts table: each unique part imported from Rebrickable +CREATE TABLE "rebrickable_parts" ( + "part" TEXT NOT NULL, + "color_id" INTEGER NOT NULL, + "color_name" TEXT NOT NULL, + "color_rgb" TEXT, -- can be NULL because it was not saved before + "color_transparent" BOOLEAN, -- can be NULL because it was not saved before + "name" TEXT NOT NULL, + "category" INTEGER, -- can be NULL because it was not saved before + "image" TEXT, + "image_id" TEXT, + "url" TEXT, -- can be NULL because it was not saved before + "print" INTEGER, -- can be NULL, was not saved before + PRIMARY KEY("part", "color_id") +); + +-- Insert existing parts into the new table +INSERT INTO "rebrickable_parts" ( + "part", + "color_id", + "color_name", + "name", + "image", + "image_id" +) +SELECT + "inventory"."part_num", + "inventory"."color_id", + "inventory"."color_name", + "inventory"."name", + "inventory"."part_img_url", + "inventory"."part_img_url_id" +FROM "inventory" +GROUP BY + "inventory"."part_num", + "inventory"."color_id"; + +COMMIT; \ No newline at end of file diff --git a/bricktracker/sql/migrations/0011.sql b/bricktracker/sql/migrations/0011.sql new file mode 100644 index 0000000..54962f3 --- /dev/null +++ b/bricktracker/sql/migrations/0011.sql @@ -0,0 +1,60 @@ +-- description: Migrate the Bricktracker parts (and missing parts), and add a bunch of extra fields for later + +PRAGMA foreign_keys = ON; + +BEGIN TRANSACTION; + +-- Create a Bricktracker parts table: an amount of parts linked to a Bricktracker set +CREATE TABLE "bricktracker_parts" ( + "id" TEXT NOT NULL, + "figure" TEXT, + "part" TEXT NOT NULL, + "color" INTEGER NOT NULL, + "spare" BOOLEAN NOT NULL, + "quantity" INTEGER NOT NULL, + "element" INTEGER, + "rebrickable_inventory" INTEGER NOT NULL, + "missing" INTEGER NOT NULL DEFAULT 0, + "damaged" INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY("id", "figure", "part", "color", "spare"), + FOREIGN KEY("id") REFERENCES "bricktracker_sets"("id"), + FOREIGN KEY("figure") REFERENCES "rebrickable_minifigures"("figure"), + FOREIGN KEY("part", "color") REFERENCES "rebrickable_parts"("part", "color_id") +); + +-- Insert existing parts into the new table +INSERT INTO "bricktracker_parts" ( + "id", + "figure", + "part", + "color", + "spare", + "quantity", + "element", + "rebrickable_inventory", + "missing" +) +SELECT + "inventory"."u_id", + CASE WHEN SUBSTR("inventory"."set_num", 0, 5) = 'fig-' THEN "inventory"."set_num" ELSE NULL END, + "inventory"."part_num", + "inventory"."color_id", + "inventory"."is_spare", + "inventory"."quantity", + "inventory"."element_id", + "inventory"."id", + IFNULL("missing"."quantity", 0) +FROM "inventory" +LEFT JOIN "missing" +ON "inventory"."set_num" IS NOT DISTINCT FROM "missing"."set_num" +AND "inventory"."id" IS NOT DISTINCT FROM "missing"."id" +AND "inventory"."part_num" IS NOT DISTINCT FROM "missing"."part_num" +AND "inventory"."color_id" IS NOT DISTINCT FROM "missing"."color_id" +AND "inventory"."element_id" IS NOT DISTINCT FROM "missing"."element_id" +AND "inventory"."u_id" IS NOT DISTINCT FROM "missing"."u_id"; + +-- Rename the original table (don't delete it yet?) +ALTER TABLE "inventory" RENAME TO "inventory_old"; +ALTER TABLE "missing" RENAME TO "missing_old"; + +COMMIT; \ No newline at end of file diff --git a/bricktracker/sql/minifigure/base/base.sql b/bricktracker/sql/minifigure/base/base.sql index c580b38..dbfc428 100644 --- a/bricktracker/sql/minifigure/base/base.sql +++ b/bricktracker/sql/minifigure/base/base.sql @@ -1,5 +1,4 @@ SELECT - {% block set %}{% endblock %} "bricktracker_minifigures"."quantity", "rebrickable_minifigures"."figure", "rebrickable_minifigures"."number", diff --git a/bricktracker/sql/minifigure/list/all.sql b/bricktracker/sql/minifigure/list/all.sql index 498062d..ca23068 100644 --- a/bricktracker/sql/minifigure/list/all.sql +++ b/bricktracker/sql/minifigure/list/all.sql @@ -16,16 +16,17 @@ COUNT("bricktracker_minifigures"."id") AS "total_sets" -- LEFT JOIN + SELECT to avoid messing the total LEFT JOIN ( SELECT - "missing"."set_num", - "missing"."u_id", - SUM("missing"."quantity") AS total - FROM "missing" + "bricktracker_parts"."id", + "bricktracker_parts"."figure", + SUM("bricktracker_parts"."missing") AS total + FROM "bricktracker_parts" + WHERE "bricktracker_parts"."figure" IS NOT NULL GROUP BY - "missing"."set_num", - "missing"."u_id" + "bricktracker_parts"."id", + "bricktracker_parts"."figure" ) missing_join -ON "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "missing_join"."u_id" -AND "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "missing_join"."set_num" +ON "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "missing_join"."id" +AND "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "missing_join"."figure" {% endblock %} {% block group %} diff --git a/bricktracker/sql/minifigure/list/last.sql b/bricktracker/sql/minifigure/list/last.sql index cacc2f7..372610d 100644 --- a/bricktracker/sql/minifigure/list/last.sql +++ b/bricktracker/sql/minifigure/list/last.sql @@ -1,13 +1,13 @@ {% extends 'minifigure/base/base.sql' %} {% block total_missing %} -SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", +SUM("bricktracker_parts"."missing") AS "total_missing", {% endblock %} {% block join %} -LEFT JOIN "missing" -ON "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "missing"."set_num" -AND "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "missing"."u_id" +LEFT JOIN "bricktracker_parts" +ON "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "bricktracker_parts"."id" +AND "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "bricktracker_parts"."figure" {% endblock %} {% block group %} diff --git a/bricktracker/sql/minifigure/list/missing_part.sql b/bricktracker/sql/minifigure/list/missing_part.sql index 3fe4210..32144bd 100644 --- a/bricktracker/sql/minifigure/list/missing_part.sql +++ b/bricktracker/sql/minifigure/list/missing_part.sql @@ -1,26 +1,24 @@ {% extends 'minifigure/base/base.sql' %} {% block total_missing %} -SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", +SUM("bricktracker_parts"."missing") AS "total_missing", {% endblock %} {% block join %} -LEFT JOIN "missing" -ON "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "missing"."set_num" -AND "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "missing"."u_id" +LEFT JOIN "bricktracker_parts" +ON "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "bricktracker_parts"."id" +AND "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "bricktracker_parts"."figure" {% endblock %} {% block where %} WHERE "rebrickable_minifigures"."figure" IN ( - SELECT - "missing"."set_num" - FROM "missing" - - WHERE "missing"."color_id" IS NOT DISTINCT FROM :color_id - AND "missing"."element_id" IS NOT DISTINCT FROM :element_id - AND "missing"."part_num" IS NOT DISTINCT FROM :part_num - - GROUP BY "missing"."set_num" + SELECT "bricktracker_parts"."figure" + FROM "bricktracker_parts" + WHERE "bricktracker_parts"."part" IS NOT DISTINCT FROM :part + AND "bricktracker_parts"."color" IS NOT DISTINCT FROM :color + AND "bricktracker_parts"."figure" IS NOT NULL + AND "bricktracker_parts"."missing" > 0 + GROUP BY "bricktracker_parts"."figure" ) {% endblock %} diff --git a/bricktracker/sql/minifigure/list/using_part.sql b/bricktracker/sql/minifigure/list/using_part.sql index e701d8d..d6ea0d1 100644 --- a/bricktracker/sql/minifigure/list/using_part.sql +++ b/bricktracker/sql/minifigure/list/using_part.sql @@ -6,15 +6,12 @@ SUM("bricktracker_minifigures"."quantity") AS "total_quantity", {% block where %} WHERE "rebrickable_minifigures"."figure" IN ( - SELECT - "inventory"."set_num" - FROM "inventory" - - WHERE "inventory"."color_id" IS NOT DISTINCT FROM :color_id - AND "inventory"."element_id" IS NOT DISTINCT FROM :element_id - AND "inventory"."part_num" IS NOT DISTINCT FROM :part_num - - GROUP BY "inventory"."set_num" + SELECT "bricktracker_parts"."figure" + FROM "bricktracker_parts" + WHERE "bricktracker_parts"."part" IS NOT DISTINCT FROM :part + AND "bricktracker_parts"."color" IS NOT DISTINCT FROM :color + AND "bricktracker_parts"."figure" IS NOT NULL + GROUP BY "bricktracker_parts"."figure" ) {% endblock %} diff --git a/bricktracker/sql/minifigure/select/generic.sql b/bricktracker/sql/minifigure/select/generic.sql index 16dc56d..f5bacd7 100644 --- a/bricktracker/sql/minifigure/select/generic.sql +++ b/bricktracker/sql/minifigure/select/generic.sql @@ -1,7 +1,7 @@ {% extends 'minifigure/base/base.sql' %} {% block total_missing %} -SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", +IFNULL("missing_join"."total", 0) AS "total_missing", {% endblock %} {% block total_quantity %} @@ -13,9 +13,16 @@ COUNT(DISTINCT "bricktracker_minifigures"."id") AS "total_sets" {% endblock %} {% block join %} -LEFT JOIN "missing" -ON "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "missing"."set_num" -AND "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "missing"."u_id" +-- LEFT JOIN + SELECT to avoid messing the total +LEFT JOIN ( + SELECT + "bricktracker_parts"."figure", + SUM("bricktracker_parts"."missing") AS "total" + FROM "bricktracker_parts" + WHERE "bricktracker_parts"."figure" IS NOT DISTINCT FROM :figure + GROUP BY "bricktracker_parts"."figure" +) "missing_join" +ON "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "missing_join"."figure" {% endblock %} {% block where %} diff --git a/bricktracker/sql/minifigure/select/specific.sql b/bricktracker/sql/minifigure/select/specific.sql index 00a66af..970494f 100644 --- a/bricktracker/sql/minifigure/select/specific.sql +++ b/bricktracker/sql/minifigure/select/specific.sql @@ -1,6 +1,6 @@ {% extends 'minifigure/base/base.sql' %} {% block where %} -WHERE "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM :figure -AND "bricktracker_minifigures"."id" IS NOT DISTINCT FROM :id +WHERE "bricktracker_minifigures"."id" IS NOT DISTINCT FROM :id +AND "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM :figure {% endblock %} diff --git a/bricktracker/sql/missing/delete/from_set.sql b/bricktracker/sql/missing/delete/from_set.sql deleted file mode 100644 index ceedc78..0000000 --- a/bricktracker/sql/missing/delete/from_set.sql +++ /dev/null @@ -1,4 +0,0 @@ -DELETE FROM "missing" -WHERE "missing"."set_num" IS NOT DISTINCT FROM :set_num -AND "missing"."id" IS NOT DISTINCT FROM :id -AND "missing"."u_id" IS NOT DISTINCT FROM :u_id diff --git a/bricktracker/sql/missing/insert.sql b/bricktracker/sql/missing/insert.sql deleted file mode 100644 index c645028..0000000 --- a/bricktracker/sql/missing/insert.sql +++ /dev/null @@ -1,20 +0,0 @@ -INSERT INTO "missing" ( - "set_num", - "id", - "part_num", - "part_img_url_id", - "color_id", - "quantity", - "element_id", - "u_id" -) -VALUES( - :set_num, - :id, - :part_num, - :part_img_url_id, - :color_id, - :quantity, - :element_id, - :u_id -) diff --git a/bricktracker/sql/missing/update/from_set.sql b/bricktracker/sql/missing/update/from_set.sql deleted file mode 100644 index 335dd06..0000000 --- a/bricktracker/sql/missing/update/from_set.sql +++ /dev/null @@ -1,5 +0,0 @@ -UPDATE "missing" -SET "quantity" = :quantity -WHERE "missing"."set_num" IS NOT DISTINCT FROM :set_num -AND "missing"."id" IS NOT DISTINCT FROM :id -AND "missing"."u_id" IS NOT DISTINCT FROM :u_id diff --git a/bricktracker/sql/part/base/base.sql b/bricktracker/sql/part/base/base.sql new file mode 100644 index 0000000..d9226b3 --- /dev/null +++ b/bricktracker/sql/part/base/base.sql @@ -0,0 +1,56 @@ +SELECT + "bricktracker_parts"."id", + "bricktracker_parts"."figure", + "bricktracker_parts"."part", + "bricktracker_parts"."color", + "bricktracker_parts"."spare", + "bricktracker_parts"."quantity", + "bricktracker_parts"."element", + --"bricktracker_parts"."rebrickable_inventory", + "bricktracker_parts"."missing", + "bricktracker_parts"."damaged", + --"rebrickable_parts"."part", + --"rebrickable_parts"."color_id", + "rebrickable_parts"."color_name", + "rebrickable_parts"."color_rgb", + "rebrickable_parts"."color_transparent", + "rebrickable_parts"."name", + --"rebrickable_parts"."category", + "rebrickable_parts"."image", + "rebrickable_parts"."image_id", + "rebrickable_parts"."url", + --"rebrickable_parts"."print", + {% block total_missing %} + NULL AS "total_missing", -- dummy for order: total_missing + {% endblock %} + {% block total_quantity %} + NULL AS "total_quantity", -- dummy for order: total_quantity + {% endblock %} + {% block total_spare %} + NULL AS "total_spare", -- dummy for order: total_spare + {% endblock %} + {% block total_sets %} + NULL AS "total_sets", -- dummy for order: total_sets + {% endblock %} + {% block total_minifigures %} + NULL AS "total_minifigures" -- dummy for order: total_minifigures + {% endblock %} +FROM "bricktracker_parts" + +INNER JOIN "rebrickable_parts" +ON "bricktracker_parts"."part" IS NOT DISTINCT FROM "rebrickable_parts"."part" +AND "bricktracker_parts"."color" IS NOT DISTINCT FROM "rebrickable_parts"."color_id" + +{% block join %}{% endblock %} + +{% block where %}{% endblock %} + +{% block group %}{% endblock %} + +{% if order %} +ORDER BY {{ order }} +{% endif %} + +{% if limit %} +LIMIT {{ limit }} +{% endif %} diff --git a/bricktracker/sql/part/base/select.sql b/bricktracker/sql/part/base/select.sql deleted file mode 100644 index 3966a75..0000000 --- a/bricktracker/sql/part/base/select.sql +++ /dev/null @@ -1,43 +0,0 @@ -SELECT - "inventory"."set_num", - "inventory"."id", - "inventory"."part_num", - "inventory"."name", - "inventory"."part_img_url", - "inventory"."part_img_url_id", - "inventory"."color_id", - "inventory"."color_name", - "inventory"."quantity", - "inventory"."is_spare", - "inventory"."element_id", - "inventory"."u_id", - {% block total_missing %} - NULL AS "total_missing", -- dummy for order: total_missing - {% endblock %} - {% block total_quantity %} - NULL AS "total_quantity", -- dummy for order: total_quantity - {% endblock %} - {% block total_spare %} - NULL AS "total_spare", -- dummy for order: total_spare - {% endblock %} - {% block total_sets %} - NULL AS "total_sets", -- dummy for order: total_sets - {% endblock %} - {% block total_minifigures %} - NULL AS "total_minifigures" -- dummy for order: total_minifigures - {% endblock %} -FROM "inventory" - -{% block join %}{% endblock %} - -{% block where %}{% endblock %} - -{% block group %}{% endblock %} - -{% if order %} -ORDER BY {{ order }} -{% endif %} - -{% if limit %} -LIMIT {{ limit }} -{% endif %} diff --git a/bricktracker/sql/part/insert.sql b/bricktracker/sql/part/insert.sql index 39b2d14..e127386 100644 --- a/bricktracker/sql/part/insert.sql +++ b/bricktracker/sql/part/insert.sql @@ -1,27 +1,19 @@ -INSERT INTO inventory ( - "set_num", +INSERT INTO "bricktracker_parts" ( "id", - "part_num", - "name", - "part_img_url", - "part_img_url_id", - "color_id", - "color_name", + "figure", + "part", + "color", + "spare", "quantity", - "is_spare", - "element_id", - "u_id" + "element", + "rebrickable_inventory" ) VALUES ( - :set_num, :id, - :part_num, - :name, - :part_img_url, - :part_img_url_id, - :color_id, - :color_name, + :figure, + :part, + :color, + :spare, :quantity, - :is_spare, - :element_id, - :u_id + :element, + :rebrickable_inventory ) diff --git a/bricktracker/sql/part/list/all.sql b/bricktracker/sql/part/list/all.sql index c5bbf69..c1d0ed1 100644 --- a/bricktracker/sql/part/list/all.sql +++ b/bricktracker/sql/part/list/all.sql @@ -1,15 +1,15 @@ -{% extends 'part/base/select.sql' %} +{% extends 'part/base/base.sql' %} {% block total_missing %} -SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", +SUM("bricktracker_parts"."missing") AS "total_missing", {% endblock %} {% block total_quantity %} -SUM("inventory"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)) AS "total_quantity", +SUM("bricktracker_parts"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)) AS "total_quantity", {% endblock %} {% block total_sets %} -COUNT(DISTINCT "bricktracker_minifigures"."id") AS "total_sets", +COUNT(DISTINCT "bricktracker_parts"."id") AS "total_sets", {% endblock %} {% block total_minifigures %} @@ -17,24 +17,14 @@ SUM(IFNULL("bricktracker_minifigures"."quantity", 0)) AS "total_minifigures" {% endblock %} {% block join %} -LEFT JOIN "missing" -ON "inventory"."set_num" IS NOT DISTINCT FROM "missing"."set_num" -AND "inventory"."id" IS NOT DISTINCT FROM "missing"."id" -AND "inventory"."part_num" IS NOT DISTINCT FROM "missing"."part_num" -AND "inventory"."color_id" IS NOT DISTINCT FROM "missing"."color_id" -AND "inventory"."element_id" IS NOT DISTINCT FROM "missing"."element_id" -AND "inventory"."u_id" IS NOT DISTINCT FROM "missing"."u_id" - LEFT JOIN "bricktracker_minifigures" -ON "inventory"."set_num" IS NOT DISTINCT FROM "bricktracker_minifigures"."figure" -AND "inventory"."u_id" IS NOT DISTINCT FROM "bricktracker_minifigures"."id" +ON "bricktracker_parts"."id" IS NOT DISTINCT FROM "bricktracker_minifigures"."id" +AND "bricktracker_parts"."figure" IS NOT DISTINCT FROM "bricktracker_minifigures"."figure" {% endblock %} {% block group %} GROUP BY - "inventory"."part_num", - "inventory"."name", - "inventory"."color_id", - "inventory"."is_spare", - "inventory"."element_id" + "bricktracker_parts"."part", + "bricktracker_parts"."color", + "bricktracker_parts"."spare" {% endblock %} diff --git a/bricktracker/sql/part/list/from_minifigure.sql b/bricktracker/sql/part/list/from_minifigure.sql index cf4135f..c840938 100644 --- a/bricktracker/sql/part/list/from_minifigure.sql +++ b/bricktracker/sql/part/list/from_minifigure.sql @@ -1,28 +1,17 @@ -{% extends 'part/base/select.sql' %} +{% extends 'part/base/base.sql' %} {% block total_missing %} -SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", -{% endblock %} - -{% block join %} -LEFT JOIN "missing" -ON "missing"."set_num" IS NOT DISTINCT FROM "inventory"."set_num" -AND "missing"."id" IS NOT DISTINCT FROM "inventory"."id" -AND "missing"."part_num" IS NOT DISTINCT FROM "inventory"."part_num" -AND "missing"."color_id" IS NOT DISTINCT FROM "inventory"."color_id" -AND "missing"."element_id" IS NOT DISTINCT FROM "inventory"."element_id" +SUM("bricktracker_parts"."missing") AS "total_missing", {% endblock %} {% block where %} -WHERE "inventory"."set_num" IS NOT DISTINCT FROM :set_num +WHERE "bricktracker_parts"."figure" IS NOT DISTINCT FROM :figure {% endblock %} {% block group %} GROUP BY - "inventory"."part_num", - "inventory"."name", - "inventory"."color_id", - "inventory"."is_spare", - "inventory"."element_id" + "bricktracker_parts"."part", + "bricktracker_parts"."color", + "bricktracker_parts"."spare" {% endblock %} diff --git a/bricktracker/sql/part/list/from_set.sql b/bricktracker/sql/part/list/from_set.sql deleted file mode 100644 index 2646eeb..0000000 --- a/bricktracker/sql/part/list/from_set.sql +++ /dev/null @@ -1,21 +0,0 @@ - -{% extends 'part/base/select.sql' %} - -{% block total_missing %} -IFNULL("missing"."quantity", 0) AS "total_missing", -{% endblock %} - -{% block join %} -LEFT JOIN "missing" -ON "inventory"."set_num" IS NOT DISTINCT FROM "missing"."set_num" -AND "inventory"."id" IS NOT DISTINCT FROM "missing"."id" -AND "inventory"."part_num" IS NOT DISTINCT FROM "missing"."part_num" -AND "inventory"."color_id" IS NOT DISTINCT FROM "missing"."color_id" -AND "inventory"."element_id" IS NOT DISTINCT FROM "missing"."element_id" -AND "inventory"."u_id" IS NOT DISTINCT FROM "missing"."u_id" -{% endblock %} - -{% block where %} -WHERE "inventory"."u_id" IS NOT DISTINCT FROM :u_id -AND "inventory"."set_num" IS NOT DISTINCT FROM :set_num -{% endblock %} diff --git a/bricktracker/sql/part/list/missing.sql b/bricktracker/sql/part/list/missing.sql index 8f17ae3..9d3446e 100644 --- a/bricktracker/sql/part/list/missing.sql +++ b/bricktracker/sql/part/list/missing.sql @@ -1,11 +1,11 @@ -{% extends 'part/base/select.sql' %} +{% extends 'part/base/base.sql' %} {% block total_missing %} -SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", +SUM("bricktracker_parts"."missing") AS "total_missing", {% endblock %} {% block total_sets %} -COUNT("inventory"."u_id") - COUNT("bricktracker_minifigures"."id") AS "total_sets", +COUNT("bricktracker_parts"."id") - COUNT("bricktracker_parts"."figure") AS "total_sets", {% endblock %} {% block total_minifigures %} @@ -13,24 +13,18 @@ SUM(IFNULL("bricktracker_minifigures"."quantity", 0)) AS "total_minifigures" {% endblock %} {% block join %} -INNER JOIN "missing" -ON "missing"."set_num" IS NOT DISTINCT FROM "inventory"."set_num" -AND "missing"."id" IS NOT DISTINCT FROM "inventory"."id" -AND "missing"."part_num" IS NOT DISTINCT FROM "inventory"."part_num" -AND "missing"."color_id" IS NOT DISTINCT FROM "inventory"."color_id" -AND "missing"."element_id" IS NOT DISTINCT FROM "inventory"."element_id" -AND "missing"."u_id" IS NOT DISTINCT FROM "inventory"."u_id" - LEFT JOIN "bricktracker_minifigures" -ON "inventory"."set_num" IS NOT DISTINCT FROM "bricktracker_minifigures"."figure" -AND "inventory"."u_id" IS NOT DISTINCT FROM "bricktracker_minifigures"."id" +ON "bricktracker_parts"."id" IS NOT DISTINCT FROM "bricktracker_minifigures"."id" +AND "bricktracker_parts"."figure" IS NOT DISTINCT FROM "bricktracker_minifigures"."figure" +{% endblock %} + +{% block where %} +WHERE "bricktracker_parts"."missing" > 0 {% endblock %} {% block group %} GROUP BY - "inventory"."part_num", - "inventory"."name", - "inventory"."color_id", - "inventory"."is_spare", - "inventory"."element_id" + "bricktracker_parts"."part", + "bricktracker_parts"."color", + "bricktracker_parts"."spare" {% endblock %} diff --git a/bricktracker/sql/part/list/specific.sql b/bricktracker/sql/part/list/specific.sql new file mode 100644 index 0000000..d3e291a --- /dev/null +++ b/bricktracker/sql/part/list/specific.sql @@ -0,0 +1,11 @@ + +{% extends 'part/base/base.sql' %} + +{% block total_missing %} +IFNULL("bricktracker_parts"."missing", 0) AS "total_missing", +{% endblock %} + +{% block where %} +WHERE "bricktracker_parts"."id" IS NOT DISTINCT FROM :id +AND "bricktracker_parts"."figure" IS NOT DISTINCT FROM :figure +{% endblock %} diff --git a/bricktracker/sql/part/select/generic.sql b/bricktracker/sql/part/select/generic.sql index eb7e194..a1760d6 100644 --- a/bricktracker/sql/part/select/generic.sql +++ b/bricktracker/sql/part/select/generic.sql @@ -1,40 +1,30 @@ -{% extends 'part/base/select.sql' %} +{% extends 'part/base/base.sql' %} {% block total_missing %} -SUM(IFNULL("missing"."quantity", 0)) AS "total_missing", +SUM("bricktracker_parts"."missing") AS "total_missing", {% endblock %} {% block total_quantity %} -SUM((NOT "inventory"."is_spare") * "inventory"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)) AS "total_quantity", +SUM((NOT "bricktracker_parts"."spare") * "bricktracker_parts"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)) AS "total_quantity", {% endblock %} {% block total_spare %} -SUM("inventory"."is_spare" * "inventory"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)) AS "total_spare", +SUM("bricktracker_parts"."spare" * "bricktracker_parts"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)) AS "total_spare", {% endblock %} {% block join %} -LEFT JOIN "missing" -ON "inventory"."set_num" IS NOT DISTINCT FROM "missing"."set_num" -AND "inventory"."id" IS NOT DISTINCT FROM "missing"."id" -AND "inventory"."part_num" IS NOT DISTINCT FROM "missing"."part_num" -AND "inventory"."color_id" IS NOT DISTINCT FROM "missing"."color_id" -AND "inventory"."element_id" IS NOT DISTINCT FROM "missing"."element_id" -AND "inventory"."u_id" IS NOT DISTINCT FROM "missing"."u_id" - LEFT JOIN "bricktracker_minifigures" -ON "inventory"."set_num" IS NOT DISTINCT FROM "bricktracker_minifigures"."figure" -AND "inventory"."u_id" IS NOT DISTINCT FROM "bricktracker_minifigures"."id" +ON "bricktracker_parts"."id" IS NOT DISTINCT FROM "bricktracker_minifigures"."id" +AND "bricktracker_parts"."figure" IS NOT DISTINCT FROM "bricktracker_minifigures"."figure" {% endblock %} {% block where %} -WHERE "inventory"."part_num" IS NOT DISTINCT FROM :part_num -AND "inventory"."color_id" IS NOT DISTINCT FROM :color_id -AND "inventory"."element_id" IS NOT DISTINCT FROM :element_id +WHERE "bricktracker_parts"."part" IS NOT DISTINCT FROM :part +AND "bricktracker_parts"."color" IS NOT DISTINCT FROM :color {% endblock %} {% block group %} GROUP BY - "inventory"."part_num", - "inventory"."color_id", - "inventory"."element_id" + "bricktracker_parts"."part", + "bricktracker_parts"."color" {% endblock %} diff --git a/bricktracker/sql/part/select/specific.sql b/bricktracker/sql/part/select/specific.sql index ebdd5f5..c74a535 100644 --- a/bricktracker/sql/part/select/specific.sql +++ b/bricktracker/sql/part/select/specific.sql @@ -1,24 +1,18 @@ -{% extends 'part/base/select.sql' %} - -{% block join %} -LEFT JOIN "missing" -ON "inventory"."set_num" IS NOT DISTINCT FROM "missing"."set_num" -AND "inventory"."id" IS NOT DISTINCT FROM "missing"."id" -AND "inventory"."u_id" IS NOT DISTINCT FROM "missing"."u_id" -{% endblock %} +{% extends 'part/base/base.sql' %} {% block where %} -WHERE "inventory"."u_id" IS NOT DISTINCT FROM :u_id -AND "inventory"."set_num" IS NOT DISTINCT FROM :set_num -AND "inventory"."id" IS NOT DISTINCT FROM :id +WHERE "bricktracker_parts"."id" IS NOT DISTINCT FROM :id +AND "bricktracker_parts"."figure" IS NOT DISTINCT FROM :figure +AND "bricktracker_parts"."part" IS NOT DISTINCT FROM :part +AND "bricktracker_parts"."color" IS NOT DISTINCT FROM :color +AND "bricktracker_parts"."spare" IS NOT DISTINCT FROM :spare {% endblock %} {% block group %} GROUP BY - "inventory"."set_num", - "inventory"."id", - "inventory"."part_num", - "inventory"."color_id", - "inventory"."element_id", - "inventory"."u_id" + "bricktracker_parts"."id", + "bricktracker_parts"."figure", + "bricktracker_parts"."part", + "bricktracker_parts"."color", + "bricktracker_parts"."spare" {% endblock %} diff --git a/bricktracker/sql/part/update/missing.sql b/bricktracker/sql/part/update/missing.sql new file mode 100644 index 0000000..c1dd368 --- /dev/null +++ b/bricktracker/sql/part/update/missing.sql @@ -0,0 +1,7 @@ +UPDATE "bricktracker_parts" +SET "missing" = :missing +WHERE "bricktracker_parts"."id" IS NOT DISTINCT FROM :id +AND "bricktracker_parts"."figure" IS NOT DISTINCT FROM :figure +AND "bricktracker_parts"."part" IS NOT DISTINCT FROM :part +AND "bricktracker_parts"."color" IS NOT DISTINCT FROM :color +AND "bricktracker_parts"."spare" IS NOT DISTINCT FROM :spare diff --git a/bricktracker/sql/rebrickable/part/insert.sql b/bricktracker/sql/rebrickable/part/insert.sql new file mode 100644 index 0000000..d989258 --- /dev/null +++ b/bricktracker/sql/rebrickable/part/insert.sql @@ -0,0 +1,25 @@ +INSERT OR IGNORE INTO "rebrickable_parts" ( + "part", + "color_id", + "color_name", + "color_rgb", + "color_transparent", + "name", + "category", + "image", + "image_id", + "url", + "print" +) VALUES ( + :part, + :color_id, + :color_name, + :color_rgb, + :color_transparent, + :name, + :category, + :image, + :image_id, + :url, + :print +) diff --git a/bricktracker/sql/rebrickable/part/list.sql b/bricktracker/sql/rebrickable/part/list.sql new file mode 100644 index 0000000..026f465 --- /dev/null +++ b/bricktracker/sql/rebrickable/part/list.sql @@ -0,0 +1,13 @@ +SELECT + "rebrickable_parts"."part", + "rebrickable_parts"."color_id", + "rebrickable_parts"."color_name", + "rebrickable_parts"."color_rgb", + "rebrickable_parts"."color_transparent", + "rebrickable_parts"."name", + "rebrickable_parts"."category", + "rebrickable_parts"."image", + "rebrickable_parts"."image_id", + "rebrickable_parts"."url", + "rebrickable_parts"."print" +FROM "rebrickable_parts" diff --git a/bricktracker/sql/rebrickable/part/select.sql b/bricktracker/sql/rebrickable/part/select.sql new file mode 100644 index 0000000..54f6305 --- /dev/null +++ b/bricktracker/sql/rebrickable/part/select.sql @@ -0,0 +1,16 @@ +SELECT + "rebrickable_parts"."part", + "rebrickable_parts"."color_id", + "rebrickable_parts"."color_name", + "rebrickable_parts"."color_rgb", + "rebrickable_parts"."color_transparent", + "rebrickable_parts"."name", + "rebrickable_parts"."category", + "rebrickable_parts"."image", + "rebrickable_parts"."image_id", + "rebrickable_parts"."url", + "rebrickable_parts"."print" +FROM "rebrickable_parts" + +WHERE "rebrickable_minifigures"."part" IS NOT DISTINCT FROM :figure +AND "rebrickable_minifigures"."color_id" IS NOT DISTINCT FROM :color diff --git a/bricktracker/sql/schema/drop.sql b/bricktracker/sql/schema/drop.sql index 1d39d99..78ea32c 100644 --- a/bricktracker/sql/schema/drop.sql +++ b/bricktracker/sql/schema/drop.sql @@ -1,15 +1,20 @@ BEGIN transaction; DROP TABLE IF EXISTS "bricktracker_minifigures"; +DROP TABLE IF EXISTS "bricktracker_parts"; DROP TABLE IF EXISTS "bricktracker_sets"; DROP TABLE IF EXISTS "bricktracker_set_checkboxes"; DROP TABLE IF EXISTS "bricktracker_set_statuses"; +DROP TABLE IF EXISTS "bricktracker_set_storages"; DROP TABLE IF EXISTS "bricktracker_wishes"; DROP TABLE IF EXISTS "inventory"; +DROP TABLE IF EXISTS "inventory_old"; DROP TABLE IF EXISTS "minifigures"; DROP TABLE IF EXISTS "minifigures_old"; DROP TABLE IF EXISTS "missing"; +DROP TABLE IF EXISTS "missing_old"; DROP TABLE IF EXISTS "rebrickable_minifigures"; +DROP TABLE IF EXISTS "rebrickable_parts"; DROP TABLE IF EXISTS "rebrickable_sets"; DROP TABLE IF EXISTS "sets"; DROP TABLE IF EXISTS "sets_old"; diff --git a/bricktracker/sql/set/base/full.sql b/bricktracker/sql/set/base/full.sql index 092e487..70730ff 100644 --- a/bricktracker/sql/set/base/full.sql +++ b/bricktracker/sql/set/base/full.sql @@ -21,13 +21,13 @@ ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_statuses"."id -- LEFT JOIN + SELECT to avoid messing the total LEFT JOIN ( SELECT - "missing"."u_id", - SUM("missing"."quantity") AS "total" - FROM "missing" + "bricktracker_parts"."id", + SUM("bricktracker_parts"."missing") AS "total" + FROM "bricktracker_parts" {% block where_missing %}{% endblock %} - GROUP BY "u_id" + GROUP BY "bricktracker_parts"."id" ) "missing_join" -ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "missing_join"."u_id" +ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "missing_join"."id" -- LEFT JOIN + SELECT to avoid messing the total LEFT JOIN ( diff --git a/bricktracker/sql/set/delete/set.sql b/bricktracker/sql/set/delete/set.sql index b477f81..49b0e88 100644 --- a/bricktracker/sql/set/delete/set.sql +++ b/bricktracker/sql/set/delete/set.sql @@ -12,10 +12,7 @@ WHERE "bricktracker_set_statuses"."id" IS NOT DISTINCT FROM '{{ id }}'; DELETE FROM "bricktracker_minifigures" WHERE "bricktracker_minifigures"."id" IS NOT DISTINCT FROM '{{ id }}'; -DELETE FROM "missing" -WHERE "missing"."u_id" IS NOT DISTINCT FROM '{{ id }}'; - -DELETE FROM "inventory" -WHERE "inventory"."u_id" IS NOT DISTINCT FROM '{{ id }}'; +DELETE FROM "bricktracker_parts" +WHERE "bricktracker_parts"."id" IS NOT DISTINCT FROM '{{ id }}'; COMMIT; \ No newline at end of file diff --git a/bricktracker/sql/set/list/missing_minifigure.sql b/bricktracker/sql/set/list/missing_minifigure.sql index 2f19bfe..51a615d 100644 --- a/bricktracker/sql/set/list/missing_minifigure.sql +++ b/bricktracker/sql/set/list/missing_minifigure.sql @@ -2,12 +2,10 @@ {% block where %} WHERE "bricktracker_sets"."id" IN ( - SELECT - "missing"."u_id" - FROM "missing" - - WHERE "missing"."set_num" IS NOT DISTINCT FROM :figure - - GROUP BY "missing"."u_id" + SELECT "bricktracker_parts"."id" + FROM "bricktracker_parts" + WHERE "bricktracker_parts"."figure" IS NOT DISTINCT FROM :figure + AND "bricktracker_parts"."missing" > 0 + GROUP BY "bricktracker_parts"."id" ) {% endblock %} diff --git a/bricktracker/sql/set/list/missing_part.sql b/bricktracker/sql/set/list/missing_part.sql index 781754c..9438b67 100644 --- a/bricktracker/sql/set/list/missing_part.sql +++ b/bricktracker/sql/set/list/missing_part.sql @@ -2,14 +2,11 @@ {% block where %} WHERE "bricktracker_sets"."id" IN ( - SELECT - "missing"."u_id" - FROM "missing" - - WHERE "missing"."color_id" IS NOT DISTINCT FROM :color_id - AND "missing"."element_id" IS NOT DISTINCT FROM :element_id - AND "missing"."part_num" IS NOT DISTINCT FROM :part_num - - GROUP BY "missing"."u_id" + SELECT "bricktracker_parts"."id" + FROM "bricktracker_parts" + WHERE "bricktracker_parts"."part" IS NOT DISTINCT FROM :part + AND "bricktracker_parts"."color" IS NOT DISTINCT FROM :color + AND "bricktracker_parts"."missing" > 0 + GROUP BY "bricktracker_parts"."id" ) {% endblock %} diff --git a/bricktracker/sql/set/list/using_minifigure.sql b/bricktracker/sql/set/list/using_minifigure.sql index 711866b..00a1fb0 100644 --- a/bricktracker/sql/set/list/using_minifigure.sql +++ b/bricktracker/sql/set/list/using_minifigure.sql @@ -2,12 +2,9 @@ {% block where %} WHERE "bricktracker_sets"."id" IN ( - SELECT - "inventory"."u_id" - FROM "inventory" - - WHERE "inventory"."set_num" IS NOT DISTINCT FROM :figure - - GROUP BY "inventory"."u_id" + SELECT "bricktracker_parts"."id" + FROM "bricktracker_parts" + WHERE "bricktracker_parts"."figure" IS NOT DISTINCT FROM :figure + GROUP BY "bricktracker_parts"."id" ) {% endblock %} diff --git a/bricktracker/sql/set/list/using_part.sql b/bricktracker/sql/set/list/using_part.sql index 8877cff..a037173 100644 --- a/bricktracker/sql/set/list/using_part.sql +++ b/bricktracker/sql/set/list/using_part.sql @@ -2,14 +2,10 @@ {% block where %} WHERE "bricktracker_sets"."id" IN ( - SELECT - "inventory"."u_id" - FROM "inventory" - - WHERE "inventory"."color_id" IS NOT DISTINCT FROM :color_id - AND "inventory"."element_id" IS NOT DISTINCT FROM :element_id - AND "inventory"."part_num" IS NOT DISTINCT FROM :part_num - - GROUP BY "inventory"."u_id" + SELECT "bricktracker_parts"."id" + FROM "bricktracker_parts" + WHERE "bricktracker_parts"."part" IS NOT DISTINCT FROM :part + AND "bricktracker_parts"."color" IS NOT DISTINCT FROM :color + GROUP BY "bricktracker_parts"."id" ) {% endblock %} diff --git a/bricktracker/sql/set/select/full.sql b/bricktracker/sql/set/select/full.sql index 80d1161..0d12ae8 100644 --- a/bricktracker/sql/set/select/full.sql +++ b/bricktracker/sql/set/select/full.sql @@ -1,7 +1,7 @@ {% extends 'set/base/full.sql' %} {% block where_missing %} -WHERE "missing"."u_id" IS NOT DISTINCT FROM :id +WHERE "bricktracker_parts"."id" IS NOT DISTINCT FROM :id {% endblock %} {% block where_minifigures %} diff --git a/bricktracker/sql_counter.py b/bricktracker/sql_counter.py index f2d1cc5..2e5c072 100644 --- a/bricktracker/sql_counter.py +++ b/bricktracker/sql_counter.py @@ -3,15 +3,20 @@ from typing import Tuple # Some table aliases to make it look cleaner (id: (name, icon)) ALIASES: dict[str, Tuple[str, str]] = { 'bricktracker_minifigures': ('Bricktracker minifigures', 'group-line'), + 'bricktracker_parts': ('Bricktracker parts', 'shapes-line'), 'bricktracker_set_checkboxes': ('Bricktracker set checkboxes', 'checkbox-line'), # noqa: E501 - 'bricktracker_set_statuses': ('Bricktracker sets status', 'checkbox-line'), + 'bricktracker_set_statuses': ('Bricktracker sets status', 'checkbox-circle-line'), # noqa: E501 + 'bricktracker_set_storages': ('Bricktracker sets storages', 'archive-2-line'), # noqa: E501 'bricktracker_sets': ('Bricktracker sets', 'hashtag'), 'bricktracker_wishes': ('Bricktracker wishes', 'gift-line'), 'inventory': ('Parts', 'shapes-line'), + 'inventory_old': ('Parts (legacy)', 'shapes-line'), 'minifigures': ('Minifigures', 'group-line'), 'minifigures_old': ('Minifigures (legacy)', 'group-line'), 'missing': ('Missing', 'error-warning-line'), + 'missing_old': ('Missing (legacy)', 'error-warning-line'), 'rebrickable_minifigures': ('Rebrickable minifigures', 'group-line'), + 'rebrickable_parts': ('Rebrickable parts', 'shapes-line'), 'rebrickable_sets': ('Rebrickable sets', 'hashtag'), 'sets': ('Sets', 'hashtag'), 'sets_old': ('Sets (legacy)', 'hashtag'), diff --git a/bricktracker/version.py b/bricktracker/version.py index 4424778..172ecf1 100644 --- a/bricktracker/version.py +++ b/bricktracker/version.py @@ -1,4 +1,4 @@ from typing import Final __version__: Final[str] = '1.2.0' -__database_version__: Final[int] = 9 +__database_version__: Final[int] = 11 diff --git a/bricktracker/views/part.py b/bricktracker/views/part.py index 2505122..5f20997 100644 --- a/bricktracker/views/part.py +++ b/bricktracker/views/part.py @@ -30,31 +30,26 @@ def missing() -> str: # Part details -@part_page.route('///details', defaults={'element': None}, methods=['GET']) # noqa: E501 -@part_page.route('////details', methods=['GET']) # noqa: E501 +@part_page.route('///details', methods=['GET']) # noqa: E501 @exception_handler(__file__) -def details(*, number: str, color: int, element: int | None) -> str: +def details(*, part: str, color: int) -> str: return render_template( 'part.html', - item=BrickPart().select_generic(number, color, element_id=element), + item=BrickPart().select_generic(part, color), sets_using=BrickSetList().using_part( - number, - color, - element_id=element + part, + color ), sets_missing=BrickSetList().missing_part( - number, - color, - element_id=element + part, + color ), minifigures_using=BrickMinifigureList().using_part( - number, - color, - element_id=element + part, + color ), minifigures_missing=BrickMinifigureList().missing_part( - number, - color, - element_id=element + part, + color ), ) diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py index 3e2304c..0b8d843 100644 --- a/bricktracker/views/set.py +++ b/bricktracker/views/set.py @@ -107,16 +107,34 @@ def details(*, id: str) -> str: ) -# Update the missing pieces of a minifig part -@set_page.route('//minifigures/
/parts//missing', methods=['POST']) # noqa: E501 +# Update the missing pieces of a part +@set_page.route('//parts////missing', defaults={'figure': None}, methods=['POST']) # noqa: E501 +@set_page.route('//minifigures/
/parts////missing', methods=['POST']) # noqa: E501 @login_required @exception_handler(__file__, json=True) -def missing_minifigure_part(*, id: str, figure: str, part: str) -> Response: +def missing_part( + *, + id: str, + figure: str | None, + part: str, + color: int, + spare: int, +) -> Response: + from pprint import pprint + pprint(locals()) + brickset = BrickSet().select_specific(id) - brickminifigure = BrickMinifigure().select_specific(brickset, figure) + + if figure is not None: + brickminifigure = BrickMinifigure().select_specific(brickset, figure) + else: + brickminifigure = None + brickpart = BrickPart().select_specific( brickset, part, + color, + spare, minifigure=brickminifigure, ) @@ -125,35 +143,14 @@ def missing_minifigure_part(*, id: str, figure: str, part: str) -> Response: brickpart.update_missing(missing) # Info - logger.info('Set {set} ({id}): updated minifigure ({figure}) part ({part}) missing count to {missing}'.format( # noqa: E501 + logger.info('Set {set} ({id}): updated part ({part} color: {color}, spare: {spare}, minifigure: {figure}) missing count to {missing}'.format( # noqa: E501 set=brickset.fields.set, id=brickset.fields.id, - figure=brickminifigure.fields.figure, - part=brickpart.fields.id, - missing=missing, - )) - - return jsonify({'missing': missing}) - - -# Update the missing pieces of a part -@set_page.route('//parts//missing', methods=['POST']) -@login_required -@exception_handler(__file__, json=True) -def missing_part(*, id: str, part: str) -> Response: - brickset = BrickSet().select_specific(id) - brickpart = BrickPart().select_specific(brickset, part) - - missing = request.json.get('missing', '') # type: ignore - - brickpart.update_missing(missing) - - # Info - logger.info('Set {set} ({id}): updated part ({part}) missing count to {missing}'.format( # noqa: E501 - set=brickset.fields.set, - id=brickset.fields.id, - part=brickpart.fields.id, - missing=missing, + figure=figure, + part=brickpart.fields.part, + color=brickpart.fields.color, + spare=brickpart.fields.spare, + missing=brickpart.fields.missing, )) return jsonify({'missing': missing}) diff --git a/templates/macro/card.html b/templates/macro/card.html index 3b52220..1cc92ac 100644 --- a/templates/macro/card.html +++ b/templates/macro/card.html @@ -1,10 +1,10 @@ -{% macro header(item, name, solo=false, number=none, color=none, icon='hashtag') %} +{% macro header(item, name, solo=false, identifier=none, color=none, icon='hashtag') %}
{% if not solo %} {% endif %}
- {% if number %}{{ number }}{% endif %} + {% if identifier %}{{ identifier }}{% endif %} {% if color %} {{ color }}{% endif %} {{ name }}
diff --git a/templates/minifigure/card.html b/templates/minifigure/card.html index b1949ef..1b70193 100644 --- a/templates/minifigure/card.html +++ b/templates/minifigure/card.html @@ -3,7 +3,7 @@ {% import 'macro/card.html' as card %}
- {{ card.header(item, item.fields.name, solo=solo, number=item.fields.number, icon='user-line') }} + {{ card.header(item, item.fields.name, solo=solo, identifier=item.fields.figure, icon='user-line') }} {{ card.image(item, solo=solo, last=last, caption=item.fields.name, alt=item.fields.figure, medium=true) }}
{% if last %} diff --git a/templates/part/card.html b/templates/part/card.html index 2f86664..8c23b59 100644 --- a/templates/part/card.html +++ b/templates/part/card.html @@ -3,8 +3,8 @@ {% import 'macro/card.html' as card %}
- {{ card.header(item, item.fields.name, solo=solo, number=item.fields.part_num, color=item.fields.color_name, icon='shapes-line') }} - {{ card.image(item, solo=solo, last=last, caption=item.fields.name, alt=item.fields.part_img_url_id, medium=true) }} + {{ card.header(item, item.fields.name, solo=solo, identifier=item.fields.part, color=item.fields.color, icon='shapes-line') }} + {{ card.image(item, solo=solo, last=last, caption=item.fields.name, alt=item.fields.image_id, medium=true) }}
{{ badge.total_sets(sets_using | length, solo=solo, last=last) }} {{ badge.total_minifigures(minifigures_using | length, solo=solo, last=last) }} diff --git a/templates/part/table.html b/templates/part/table.html index 1fca264..78d0efe 100644 --- a/templates/part/table.html +++ b/templates/part/table.html @@ -6,10 +6,10 @@ {% for item in table_collection %} - {{ table.image(item.url_for_image(), caption=item.fields.name, alt=item.fields.part_num, accordion=solo) }} + {{ table.image(item.url_for_image(), caption=item.fields.name, alt=item.fields.part, accordion=solo) }} {{ item.fields.name }} - {% if item.fields.is_spare %} Spare{% endif %} + {% if item.fields.spare %} Spare{% endif %} {% if all %} {{ table.rebrickable(item) }} {{ table.bricklink(item) }} @@ -24,15 +24,15 @@ {% endif %} {% endif %} {% if not no_missing %} - + {% if all or read_only_missing %} {{ item.fields.total_missing }} {% else %}
{% if g.login.is_authenticated() %} - + onchange="change_part_missing_amount(this, '{{ item.fields.id }}', '{{ item.html_id() }}', '{{ item.url_for_missing() }}')" autocomplete="off"> + {% else %} diff --git a/templates/set/card.html b/templates/set/card.html index e9612eb..20bfe4a 100644 --- a/templates/set/card.html +++ b/templates/set/card.html @@ -11,8 +11,8 @@ {% for checkbox in brickset_checkboxes %}data-{{ checkbox.as_dataset() }}="{{ item.fields[checkbox.as_column()] }}" {% endfor %} {% endif %} > - {{ card.header(item, item.fields.name, solo=solo, number=item.fields.number) }} - {{ card.image(item, solo=solo, last=last, caption=item.fields.name, alt=item.fields.number) }} + {{ card.header(item, item.fields.name, solo=solo, identifier=item.fields.set) }} + {{ card.image(item, solo=solo, last=last, caption=item.fields.name, alt=item.fields.set) }}
{{ badge.theme(item.theme.name, solo=solo, last=last) }} {{ badge.year(item.fields.year, solo=solo, last=last) }} diff --git a/templates/set/mini.html b/templates/set/mini.html index 6bdc028..6305e0e 100644 --- a/templates/set/mini.html +++ b/templates/set/mini.html @@ -2,7 +2,7 @@ {% import 'macro/card.html' as card %}
- {{ card.header(item, item.fields.name, solo=true, number=item.fields.set) }} + {{ card.header(item, item.fields.name, solo=true, identifier=item.fields.set) }} {{ card.image(item, solo=true, last=false, caption=item.fields.name, alt=item.fields.set) }}
{{ badge.theme(item.theme.name) }}