diff --git a/bricktracker/minifigure.py b/bricktracker/minifigure.py index e0318a0..1ad6aa6 100644 --- a/bricktracker/minifigure.py +++ b/bricktracker/minifigure.py @@ -20,7 +20,7 @@ class BrickMinifigure(RebrickableMinifigure): select_query: str = 'minifigure/select/specific' # Import a minifigure into the database - def download(self, socket: 'BrickSocket') -> bool: + def download(self, socket: 'BrickSocket', refresh: bool = False) -> bool: if self.brickset is None: raise ErrorException('Importing a minifigure from Rebrickable outside of a set is not supported') # noqa: E501 @@ -33,8 +33,9 @@ class BrickMinifigure(RebrickableMinifigure): ) ) - # Insert into database - self.insert(commit=False) + if not refresh: + # Insert into database + self.insert(commit=False) # Insert the rebrickable set into database self.insert_rebrickable() @@ -43,7 +44,8 @@ class BrickMinifigure(RebrickableMinifigure): if not BrickPartList.download( socket, self.brickset, - minifigure=self + minifigure=self, + refresh=refresh ): return False diff --git a/bricktracker/minifigure_list.py b/bricktracker/minifigure_list.py index a59fee5..790018a 100644 --- a/bricktracker/minifigure_list.py +++ b/bricktracker/minifigure_list.py @@ -134,7 +134,13 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): # Import the minifigures from Rebrickable @staticmethod - def download(socket: 'BrickSocket', brickset: 'BrickSet', /) -> bool: + def download( + socket: 'BrickSocket', + brickset: 'BrickSet', + /, + *, + refresh: bool = False + ) -> bool: try: socket.auto_progress( message='Set {set}: loading minifigures from Rebrickable'.format( # noqa: E501 @@ -157,7 +163,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): # Process each minifigure for minifigure in minifigures: - if not minifigure.download(socket): + if not minifigure.download(socket, refresh=refresh): return False return True diff --git a/bricktracker/part.py b/bricktracker/part.py index 7e82c45..af901b2 100644 --- a/bricktracker/part.py +++ b/bricktracker/part.py @@ -34,7 +34,7 @@ class BrickPart(RebrickablePart): self.kind = 'Set' # Import a part into the database - def download(self, socket: 'BrickSocket') -> bool: + def download(self, socket: 'BrickSocket', refresh: bool = False) -> bool: if self.brickset is None: raise ErrorException('Importing a part from Rebrickable outside of a set is not supported') # noqa: E501 @@ -48,8 +48,9 @@ class BrickPart(RebrickablePart): ) ) - # Insert into database - self.insert(commit=False) + if not refresh: + # Insert into database + self.insert(commit=False) # Insert the rebrickable set into database self.insert_rebrickable() diff --git a/bricktracker/part_list.py b/bricktracker/part_list.py index 0074b9b..667c26e 100644 --- a/bricktracker/part_list.py +++ b/bricktracker/part_list.py @@ -139,6 +139,7 @@ class BrickPartList(BrickRecordList[BrickPart]): /, *, minifigure: 'BrickMinifigure | None' = None, + refresh: bool = False ) -> bool: if minifigure is not None: identifier = minifigure.fields.figure @@ -174,7 +175,7 @@ class BrickPartList(BrickRecordList[BrickPart]): # Process each part for part in inventory: - if not part.download(socket): + if not part.download(socket, refresh=refresh): return False except Exception as e: diff --git a/bricktracker/rebrickable_minifigure.py b/bricktracker/rebrickable_minifigure.py index 973b9fb..30d61ee 100644 --- a/bricktracker/rebrickable_minifigure.py +++ b/bricktracker/rebrickable_minifigure.py @@ -38,27 +38,22 @@ class RebrickableMinifigure(BrickRecord): self.ingest(record) # Insert the minifigure from Rebrickable - def insert_rebrickable(self, /) -> bool: + def insert_rebrickable(self, /) -> None: if self.brickset is None: raise ErrorException('Importing a minifigure from Rebrickable outside of a set is not supported') # noqa: E501 # Insert the Rebrickable minifigure to the database - rows, _ = self.insert( + self.insert( commit=False, no_defer=True, override_query=RebrickableMinifigure.insert_query ) - inserted = rows > 0 - - if inserted: - if not current_app.config['USE_REMOTE_IMAGES']: - RebrickableImage( - self.brickset, - minifigure=self, - ).download() - - return inserted + if not current_app.config['USE_REMOTE_IMAGES']: + RebrickableImage( + self.brickset, + minifigure=self, + ).download() # Return a dict with common SQL parameters for a minifigure def sql_parameters(self, /) -> dict[str, Any]: diff --git a/bricktracker/rebrickable_part.py b/bricktracker/rebrickable_part.py index 93c6b34..704990c 100644 --- a/bricktracker/rebrickable_part.py +++ b/bricktracker/rebrickable_part.py @@ -48,28 +48,23 @@ class RebrickablePart(BrickRecord): self.ingest(record) # Insert the part from Rebrickable - def insert_rebrickable(self, /) -> bool: + def insert_rebrickable(self, /) -> None: if self.brickset is None: raise ErrorException('Importing a part from Rebrickable outside of a set is not supported') # noqa: E501 # Insert the Rebrickable part to the database - rows, _ = self.insert( + self.insert( commit=False, no_defer=True, override_query=RebrickablePart.insert_query ) - inserted = rows > 0 - - if inserted: - if not current_app.config['USE_REMOTE_IMAGES']: - RebrickableImage( - self.brickset, - minifigure=self.minifigure, - part=self, - ).download() - - return inserted + if not current_app.config['USE_REMOTE_IMAGES']: + RebrickableImage( + self.brickset, + minifigure=self.minifigure, + part=self, + ).download() # Return a dict with common SQL parameters for a part def sql_parameters(self, /) -> dict[str, Any]: diff --git a/bricktracker/rebrickable_set.py b/bricktracker/rebrickable_set.py index 1cd4b8d..fbf10f1 100644 --- a/bricktracker/rebrickable_set.py +++ b/bricktracker/rebrickable_set.py @@ -47,21 +47,16 @@ class RebrickableSet(BrickRecord): self.ingest(record) # Insert the set from Rebrickable - def insert_rebrickable(self, /) -> bool: + def insert_rebrickable(self, /) -> None: # Insert the Rebrickable set to the database - rows, _ = self.insert( + self.insert( commit=False, no_defer=True, override_query=RebrickableSet.insert_query ) - inserted = rows > 0 - - if inserted: - if not current_app.config['USE_REMOTE_IMAGES']: - RebrickableImage(self).download() - - return inserted + if not current_app.config['USE_REMOTE_IMAGES']: + RebrickableImage(self).download() # Ingest a set def ingest(self, record: Row | dict[str, Any], /): diff --git a/bricktracker/record.py b/bricktracker/record.py index 08651d2..f7cc889 100644 --- a/bricktracker/record.py +++ b/bricktracker/record.py @@ -1,5 +1,5 @@ from sqlite3 import Row -from typing import Any, ItemsView, Tuple +from typing import Any, ItemsView from .fields import BrickRecordFields from .sql import BrickSQL @@ -31,14 +31,14 @@ class BrickRecord(object): commit=True, no_defer=False, override_query: str | None = None - ) -> Tuple[int, str]: + ) -> None: if override_query: query = override_query else: query = self.insert_query database = BrickSQL() - rows, q = database.execute( + database.execute( query, parameters=self.sql_parameters(), defer=not commit and not no_defer, @@ -47,8 +47,6 @@ class BrickRecord(object): if commit: database.commit() - return rows, q - # Shorthand to field items def items(self, /) -> ItemsView[str, Any]: return self.fields.__dict__.items() diff --git a/bricktracker/set.py b/bricktracker/set.py index 63d4128..28f0341 100644 --- a/bricktracker/set.py +++ b/bricktracker/set.py @@ -47,21 +47,25 @@ class BrickSet(RebrickableSet): increment_total=True, ) + # Grabbing the refresh flag + refresh: bool = bool(data.get('refresh', False)) + # Generate an UUID for self self.fields.id = str(uuid4()) - # Insert into database - self.insert(commit=False) + if not refresh: + # Insert into database + self.insert(commit=False) # Insert the rebrickable set into database self.insert_rebrickable() # Load the inventory - if not BrickPartList.download(socket, self): + if not BrickPartList.download(socket, self, refresh=refresh): return False # Load the minifigures - if not BrickMinifigureList.download(socket, self): + if not BrickMinifigureList.download(socket, self, refresh=refresh): return False # Commit the transaction to the database @@ -74,20 +78,34 @@ class BrickSet(RebrickableSet): BrickSQL().commit() - # Info - logger.info('Set {set}: imported (id: {id})'.format( - set=self.fields.set, - id=self.fields.id, - )) - - # Complete - socket.complete( - message='Set {set}: imported (Go to the set)'.format( # noqa: E501 + if refresh: + # Info + logger.info('Set {set}: imported (id: {id})'.format( set=self.fields.set, - url=self.url() - ), - download=True - ) + id=self.fields.id, + )) + + # Complete + socket.complete( + message='Set {set}: refreshed'.format( # noqa: E501 + set=self.fields.set, + ), + download=True + ) + else: + # Info + logger.info('Set {set}: refreshed'.format( + set=self.fields.set, + )) + + # Complete + socket.complete( + message='Set {set}: imported (Go to the set)'.format( # noqa: E501 + set=self.fields.set, + url=self.url() + ), + download=True + ) except Exception as e: socket.fail( @@ -192,3 +210,10 @@ class BrickSet(RebrickableSet): ) else: return '' + + # Compute the url for the refresh button + def url_for_refresh(self, /) -> str: + return url_for( + 'set.refresh', + id=self.fields.id, + ) diff --git a/bricktracker/set_checkbox.py b/bricktracker/set_checkbox.py index ea6d6d2..38a10f0 100644 --- a/bricktracker/set_checkbox.py +++ b/bricktracker/set_checkbox.py @@ -1,5 +1,5 @@ from sqlite3 import Row -from typing import Any, Self, Tuple +from typing import Any, Self from uuid import uuid4 from flask import url_for @@ -60,7 +60,7 @@ class BrickSetCheckbox(BrickRecord): return self # Insert into database - def insert(self, **_) -> Tuple[int, str]: + def insert(self, **_) -> None: # Generate an ID for the checkbox (with underscores to make it # column name friendly) self.fields.id = str(uuid4()).replace('-', '_') @@ -72,9 +72,6 @@ class BrickSetCheckbox(BrickRecord): displayed_on_grid=self.fields.displayed_on_grid ) - # To accomodate the parent().insert we have overriden - return 0, '' - # Rename the checkbox def rename(self, /) -> None: # Update the name diff --git a/bricktracker/sql/rebrickable/minifigure/insert.sql b/bricktracker/sql/rebrickable/minifigure/insert.sql index 0671925..6c0ac8e 100644 --- a/bricktracker/sql/rebrickable/minifigure/insert.sql +++ b/bricktracker/sql/rebrickable/minifigure/insert.sql @@ -9,3 +9,9 @@ INSERT OR IGNORE INTO "rebrickable_minifigures" ( :name, :image ) +ON CONFLICT("figure") +DO UPDATE SET +"number" = :number, +"name" = :name, +"image" = :image +WHERE "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM :figure diff --git a/bricktracker/sql/rebrickable/part/insert.sql b/bricktracker/sql/rebrickable/part/insert.sql index d989258..fcec4ef 100644 --- a/bricktracker/sql/rebrickable/part/insert.sql +++ b/bricktracker/sql/rebrickable/part/insert.sql @@ -23,3 +23,16 @@ INSERT OR IGNORE INTO "rebrickable_parts" ( :url, :print ) +ON CONFLICT("part", "color_id") +DO UPDATE SET +"color_name" = :color_name, +"color_rgb" = :color_rgb, +"color_transparent" = :color_transparent, +"name" = :name, +"category" = :category, +"image" = :image, +"image_id" = :image_id, +"url" = :url, +"print" = :print +WHERE "rebrickable_parts"."part" IS NOT DISTINCT FROM :part +AND "rebrickable_parts"."color_id" IS NOT DISTINCT FROM :color_id \ No newline at end of file diff --git a/bricktracker/sql/rebrickable/set/insert.sql b/bricktracker/sql/rebrickable/set/insert.sql index 88b2b44..39e6964 100644 --- a/bricktracker/sql/rebrickable/set/insert.sql +++ b/bricktracker/sql/rebrickable/set/insert.sql @@ -21,3 +21,15 @@ INSERT OR IGNORE INTO "rebrickable_sets" ( :url, :last_modified ) +ON CONFLICT("set") +DO UPDATE SET + "number" = :number, + "version" = :version, + "name" = :name, + "year" = :year, + "theme_id" = :theme_id, + "number_of_parts" = :number_of_parts, + "image" = :image, + "url" = :url, + "last_modified" = :last_modified +WHERE "rebrickable_sets"."set" IS NOT DISTINCT FROM :set diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py index 0b8d843..117ff03 100644 --- a/bricktracker/views/set.py +++ b/bricktracker/views/set.py @@ -2,6 +2,7 @@ import logging from flask import ( Blueprint, + current_app, jsonify, render_template, redirect, @@ -17,6 +18,7 @@ from ..part import BrickPart from ..set import BrickSet from ..set_checkbox_list import BrickSetCheckboxList from ..set_list import BrickSetList +from ..socket import MESSAGES logger = logging.getLogger(__name__) @@ -154,3 +156,16 @@ def missing_part( )) return jsonify({'missing': missing}) + + +# Refresh a set +@set_page.route('//refresh', methods=['GET']) +@exception_handler(__file__) +def refresh(*, id: str) -> str: + return render_template( + 'refresh.html', + item=BrickSet().select_specific(id), + path=current_app.config['SOCKET_PATH'], + namespace=current_app.config['SOCKET_NAMESPACE'], + messages=MESSAGES + ) diff --git a/static/scripts/socket/set.js b/static/scripts/socket/set.js index 41056b8..4f6bf97 100644 --- a/static/scripts/socket/set.js +++ b/static/scripts/socket/set.js @@ -1,8 +1,11 @@ // Set Socket class class BrickSetSocket extends BrickSocket { - constructor(id, path, namespace, messages, bulk=false) { + constructor(id, path, namespace, messages, bulk=false, refresh=false) { super(id, path, namespace, messages, bulk); + // Refresh mode + this.refresh = true + // Listeners this.add_listener = undefined; this.input_listener = undefined; @@ -82,7 +85,7 @@ class BrickSetSocket extends BrickSocket { this.read_set_list(); } - if (this.bulk || (this.html_no_confim && this.html_no_confim.checked)) { + if (this.bulk || this.refresh || (this.html_no_confim && this.html_no_confim.checked)) { this.import_set(true); } else { this.load_set(); @@ -140,6 +143,7 @@ class BrickSetSocket extends BrickSocket { this.socket.emit(this.messages.IMPORT_SET, { set: (set !== undefined) ? set : this.html_input.value, + refresh: this.refresh }); } else { this.fail("Could not find the input field for the set number"); diff --git a/templates/add.html b/templates/add.html index 140eec6..5316ea1 100644 --- a/templates/add.html +++ b/templates/add.html @@ -68,5 +68,7 @@ -{% include 'set/socket.html' %} +{% with id='add' %} + {% include 'set/socket.html' %} +{% endwith %} {% endblock %} diff --git a/templates/bulk.html b/templates/bulk.html index 6e6e5d8..00d4779 100644 --- a/templates/bulk.html +++ b/templates/bulk.html @@ -58,7 +58,7 @@ -{% with bulk=true %} +{% with id='add', bulk=true %} {% include 'set/socket.html' %} {% endwith %} {% endblock %} diff --git a/templates/refresh.html b/templates/refresh.html new file mode 100644 index 0000000..5add93d --- /dev/null +++ b/templates/refresh.html @@ -0,0 +1,64 @@ +{% extends 'base.html' %} + +{% block title %} - Refresh set {{ item.fields.set }}{% endblock %} + +{% block main %} + + + Refreshing from Rebrickable + This will refresh all the Rebrickable data (set, minifigures, parts) associated with this set. + + + + + + Refresh a set + + + + + + Set number + + + + + + Progress + + + Loading... + + + + + + + + + + + + {{ item.fields.set }} + {{ item.fields.name }} + + + + + + + + + + + + + +{% with id='refresh', refresh=true %} + {% include 'set/socket.html' %} +{% endwith %} +{% endblock %} diff --git a/templates/set/card.html b/templates/set/card.html index 20bfe4a..02d77b3 100644 --- a/templates/set/card.html +++ b/templates/set/card.html @@ -63,6 +63,9 @@ {% endfor %} {% endif %} {% if g.login.is_authenticated() %} + {{ accordion.header('Management', 'management', 'set-details', icon='settings-4-line', class='text-end') }} + Refresh the set + {{ accordion.footer() }} {{ accordion.header('Danger zone', 'danger-zone', 'set-details', expanded=delete, danger=true, class='text-end') }} {% if delete %} diff --git a/templates/set/socket.html b/templates/set/socket.html index a566a95..c4000a8 100644 --- a/templates/set/socket.html +++ b/templates/set/socket.html @@ -1,12 +1,19 @@
This will refresh all the Rebrickable data (set, minifigures, parts) associated with this set.
+ Progress + + + Loading... + +