diff --git a/bricktracker/minifigure.py b/bricktracker/minifigure.py
index e0318a05..1ad6aa6a 100644
--- a/bricktracker/minifigure.py
+++ b/bricktracker/minifigure.py
@@ -20,7 +20,7 @@ class BrickMinifigure(RebrickableMinifigure):
select_query: str = 'minifigure/select/specific'
# Import a minifigure into the database
- def download(self, socket: 'BrickSocket') -> bool:
+ def download(self, socket: 'BrickSocket', refresh: bool = False) -> bool:
if self.brickset is None:
raise ErrorException('Importing a minifigure from Rebrickable outside of a set is not supported') # noqa: E501
@@ -33,8 +33,9 @@ class BrickMinifigure(RebrickableMinifigure):
)
)
- # Insert into database
- self.insert(commit=False)
+ if not refresh:
+ # Insert into database
+ self.insert(commit=False)
# Insert the rebrickable set into database
self.insert_rebrickable()
@@ -43,7 +44,8 @@ class BrickMinifigure(RebrickableMinifigure):
if not BrickPartList.download(
socket,
self.brickset,
- minifigure=self
+ minifigure=self,
+ refresh=refresh
):
return False
diff --git a/bricktracker/minifigure_list.py b/bricktracker/minifigure_list.py
index a59fee57..790018a7 100644
--- a/bricktracker/minifigure_list.py
+++ b/bricktracker/minifigure_list.py
@@ -134,7 +134,13 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
# Import the minifigures from Rebrickable
@staticmethod
- def download(socket: 'BrickSocket', brickset: 'BrickSet', /) -> bool:
+ def download(
+ socket: 'BrickSocket',
+ brickset: 'BrickSet',
+ /,
+ *,
+ refresh: bool = False
+ ) -> bool:
try:
socket.auto_progress(
message='Set {set}: loading minifigures from Rebrickable'.format( # noqa: E501
@@ -157,7 +163,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
# Process each minifigure
for minifigure in minifigures:
- if not minifigure.download(socket):
+ if not minifigure.download(socket, refresh=refresh):
return False
return True
diff --git a/bricktracker/part.py b/bricktracker/part.py
index 7e82c454..af901b2b 100644
--- a/bricktracker/part.py
+++ b/bricktracker/part.py
@@ -34,7 +34,7 @@ class BrickPart(RebrickablePart):
self.kind = 'Set'
# Import a part into the database
- def download(self, socket: 'BrickSocket') -> bool:
+ def download(self, socket: 'BrickSocket', refresh: bool = False) -> bool:
if self.brickset is None:
raise ErrorException('Importing a part from Rebrickable outside of a set is not supported') # noqa: E501
@@ -48,8 +48,9 @@ class BrickPart(RebrickablePart):
)
)
- # Insert into database
- self.insert(commit=False)
+ if not refresh:
+ # Insert into database
+ self.insert(commit=False)
# Insert the rebrickable set into database
self.insert_rebrickable()
diff --git a/bricktracker/part_list.py b/bricktracker/part_list.py
index 0074b9bf..667c26e4 100644
--- a/bricktracker/part_list.py
+++ b/bricktracker/part_list.py
@@ -139,6 +139,7 @@ class BrickPartList(BrickRecordList[BrickPart]):
/,
*,
minifigure: 'BrickMinifigure | None' = None,
+ refresh: bool = False
) -> bool:
if minifigure is not None:
identifier = minifigure.fields.figure
@@ -174,7 +175,7 @@ class BrickPartList(BrickRecordList[BrickPart]):
# Process each part
for part in inventory:
- if not part.download(socket):
+ if not part.download(socket, refresh=refresh):
return False
except Exception as e:
diff --git a/bricktracker/rebrickable_minifigure.py b/bricktracker/rebrickable_minifigure.py
index 973b9fb9..30d61eef 100644
--- a/bricktracker/rebrickable_minifigure.py
+++ b/bricktracker/rebrickable_minifigure.py
@@ -38,27 +38,22 @@ class RebrickableMinifigure(BrickRecord):
self.ingest(record)
# Insert the minifigure from Rebrickable
- def insert_rebrickable(self, /) -> bool:
+ def insert_rebrickable(self, /) -> None:
if self.brickset is None:
raise ErrorException('Importing a minifigure from Rebrickable outside of a set is not supported') # noqa: E501
# Insert the Rebrickable minifigure to the database
- rows, _ = self.insert(
+ self.insert(
commit=False,
no_defer=True,
override_query=RebrickableMinifigure.insert_query
)
- inserted = rows > 0
-
- if inserted:
- if not current_app.config['USE_REMOTE_IMAGES']:
- RebrickableImage(
- self.brickset,
- minifigure=self,
- ).download()
-
- return inserted
+ if not current_app.config['USE_REMOTE_IMAGES']:
+ RebrickableImage(
+ self.brickset,
+ minifigure=self,
+ ).download()
# Return a dict with common SQL parameters for a minifigure
def sql_parameters(self, /) -> dict[str, Any]:
diff --git a/bricktracker/rebrickable_part.py b/bricktracker/rebrickable_part.py
index 93c6b34e..704990c6 100644
--- a/bricktracker/rebrickable_part.py
+++ b/bricktracker/rebrickable_part.py
@@ -48,28 +48,23 @@ class RebrickablePart(BrickRecord):
self.ingest(record)
# Insert the part from Rebrickable
- def insert_rebrickable(self, /) -> bool:
+ def insert_rebrickable(self, /) -> None:
if self.brickset is None:
raise ErrorException('Importing a part from Rebrickable outside of a set is not supported') # noqa: E501
# Insert the Rebrickable part to the database
- rows, _ = self.insert(
+ self.insert(
commit=False,
no_defer=True,
override_query=RebrickablePart.insert_query
)
- inserted = rows > 0
-
- if inserted:
- if not current_app.config['USE_REMOTE_IMAGES']:
- RebrickableImage(
- self.brickset,
- minifigure=self.minifigure,
- part=self,
- ).download()
-
- return inserted
+ if not current_app.config['USE_REMOTE_IMAGES']:
+ RebrickableImage(
+ self.brickset,
+ minifigure=self.minifigure,
+ part=self,
+ ).download()
# Return a dict with common SQL parameters for a part
def sql_parameters(self, /) -> dict[str, Any]:
diff --git a/bricktracker/rebrickable_set.py b/bricktracker/rebrickable_set.py
index 1cd4b8d6..fbf10f1a 100644
--- a/bricktracker/rebrickable_set.py
+++ b/bricktracker/rebrickable_set.py
@@ -47,21 +47,16 @@ class RebrickableSet(BrickRecord):
self.ingest(record)
# Insert the set from Rebrickable
- def insert_rebrickable(self, /) -> bool:
+ def insert_rebrickable(self, /) -> None:
# Insert the Rebrickable set to the database
- rows, _ = self.insert(
+ self.insert(
commit=False,
no_defer=True,
override_query=RebrickableSet.insert_query
)
- inserted = rows > 0
-
- if inserted:
- if not current_app.config['USE_REMOTE_IMAGES']:
- RebrickableImage(self).download()
-
- return inserted
+ if not current_app.config['USE_REMOTE_IMAGES']:
+ RebrickableImage(self).download()
# Ingest a set
def ingest(self, record: Row | dict[str, Any], /):
diff --git a/bricktracker/record.py b/bricktracker/record.py
index 08651d2f..f7cc8892 100644
--- a/bricktracker/record.py
+++ b/bricktracker/record.py
@@ -1,5 +1,5 @@
from sqlite3 import Row
-from typing import Any, ItemsView, Tuple
+from typing import Any, ItemsView
from .fields import BrickRecordFields
from .sql import BrickSQL
@@ -31,14 +31,14 @@ class BrickRecord(object):
commit=True,
no_defer=False,
override_query: str | None = None
- ) -> Tuple[int, str]:
+ ) -> None:
if override_query:
query = override_query
else:
query = self.insert_query
database = BrickSQL()
- rows, q = database.execute(
+ database.execute(
query,
parameters=self.sql_parameters(),
defer=not commit and not no_defer,
@@ -47,8 +47,6 @@ class BrickRecord(object):
if commit:
database.commit()
- return rows, q
-
# Shorthand to field items
def items(self, /) -> ItemsView[str, Any]:
return self.fields.__dict__.items()
diff --git a/bricktracker/set.py b/bricktracker/set.py
index 63d41281..28f0341c 100644
--- a/bricktracker/set.py
+++ b/bricktracker/set.py
@@ -47,21 +47,25 @@ class BrickSet(RebrickableSet):
increment_total=True,
)
+ # Grabbing the refresh flag
+ refresh: bool = bool(data.get('refresh', False))
+
# Generate an UUID for self
self.fields.id = str(uuid4())
- # Insert into database
- self.insert(commit=False)
+ if not refresh:
+ # Insert into database
+ self.insert(commit=False)
# Insert the rebrickable set into database
self.insert_rebrickable()
# Load the inventory
- if not BrickPartList.download(socket, self):
+ if not BrickPartList.download(socket, self, refresh=refresh):
return False
# Load the minifigures
- if not BrickMinifigureList.download(socket, self):
+ if not BrickMinifigureList.download(socket, self, refresh=refresh):
return False
# Commit the transaction to the database
@@ -74,20 +78,34 @@ class BrickSet(RebrickableSet):
BrickSQL().commit()
- # Info
- logger.info('Set {set}: imported (id: {id})'.format(
- set=self.fields.set,
- id=self.fields.id,
- ))
-
- # Complete
- socket.complete(
- message='Set {set}: imported (<a href="{url}">Go to the set</a>)'.format( # noqa: E501
+ if refresh:
+ # Info
+ logger.info('Set {set}: imported (id: {id})'.format(
set=self.fields.set,
- url=self.url()
- ),
- download=True
- )
+ id=self.fields.id,
+ ))
+
+ # Complete
+ socket.complete(
+ message='Set {set}: refreshed'.format( # noqa: E501
+ set=self.fields.set,
+ ),
+ download=True
+ )
+ else:
+ # Info
+ logger.info('Set {set}: refreshed'.format(
+ set=self.fields.set,
+ ))
+
+ # Complete
+ socket.complete(
+ message='Set {set}: imported (<a href="{url}">Go to the set</a>)'.format( # noqa: E501
+ set=self.fields.set,
+ url=self.url()
+ ),
+ download=True
+ )
except Exception as e:
socket.fail(
@@ -192,3 +210,10 @@ class BrickSet(RebrickableSet):
)
else:
return ''
+
+ # Compute the url for the refresh button
+ def url_for_refresh(self, /) -> str:
+ return url_for(
+ 'set.refresh',
+ id=self.fields.id,
+ )
diff --git a/bricktracker/set_checkbox.py b/bricktracker/set_checkbox.py
index ea6d6d2b..38a10f0d 100644
--- a/bricktracker/set_checkbox.py
+++ b/bricktracker/set_checkbox.py
@@ -1,5 +1,5 @@
from sqlite3 import Row
-from typing import Any, Self, Tuple
+from typing import Any, Self
from uuid import uuid4
from flask import url_for
@@ -60,7 +60,7 @@ class BrickSetCheckbox(BrickRecord):
return self
# Insert into database
- def insert(self, **_) -> Tuple[int, str]:
+ def insert(self, **_) -> None:
# Generate an ID for the checkbox (with underscores to make it
# column name friendly)
self.fields.id = str(uuid4()).replace('-', '_')
@@ -72,9 +72,6 @@ class BrickSetCheckbox(BrickRecord):
displayed_on_grid=self.fields.displayed_on_grid
)
- # To accomodate the parent().insert we have overriden
- return 0, ''
-
# Rename the checkbox
def rename(self, /) -> None:
# Update the name
diff --git a/bricktracker/sql/rebrickable/minifigure/insert.sql b/bricktracker/sql/rebrickable/minifigure/insert.sql
index 06719257..6c0ac8e8 100644
--- a/bricktracker/sql/rebrickable/minifigure/insert.sql
+++ b/bricktracker/sql/rebrickable/minifigure/insert.sql
@@ -9,3 +9,9 @@ INSERT OR IGNORE INTO "rebrickable_minifigures" (
:name,
:image
)
+ON CONFLICT("figure")
+DO UPDATE SET
+"number" = :number,
+"name" = :name,
+"image" = :image
+WHERE "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM :figure
diff --git a/bricktracker/sql/rebrickable/part/insert.sql b/bricktracker/sql/rebrickable/part/insert.sql
index d989258c..fcec4ef0 100644
--- a/bricktracker/sql/rebrickable/part/insert.sql
+++ b/bricktracker/sql/rebrickable/part/insert.sql
@@ -23,3 +23,16 @@ INSERT OR IGNORE INTO "rebrickable_parts" (
:url,
:print
)
+ON CONFLICT("part", "color_id")
+DO UPDATE SET
+"color_name" = :color_name,
+"color_rgb" = :color_rgb,
+"color_transparent" = :color_transparent,
+"name" = :name,
+"category" = :category,
+"image" = :image,
+"image_id" = :image_id,
+"url" = :url,
+"print" = :print
+WHERE "rebrickable_parts"."part" IS NOT DISTINCT FROM :part
+AND "rebrickable_parts"."color_id" IS NOT DISTINCT FROM :color_id
\ No newline at end of file
diff --git a/bricktracker/sql/rebrickable/set/insert.sql b/bricktracker/sql/rebrickable/set/insert.sql
index 88b2b44f..39e69646 100644
--- a/bricktracker/sql/rebrickable/set/insert.sql
+++ b/bricktracker/sql/rebrickable/set/insert.sql
@@ -21,3 +21,15 @@ INSERT OR IGNORE INTO "rebrickable_sets" (
:url,
:last_modified
)
+ON CONFLICT("set")
+DO UPDATE SET
+ "number" = :number,
+ "version" = :version,
+ "name" = :name,
+ "year" = :year,
+ "theme_id" = :theme_id,
+ "number_of_parts" = :number_of_parts,
+ "image" = :image,
+ "url" = :url,
+ "last_modified" = :last_modified
+WHERE "rebrickable_sets"."set" IS NOT DISTINCT FROM :set
diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py
index 0b8d843a..117ff03b 100644
--- a/bricktracker/views/set.py
+++ b/bricktracker/views/set.py
@@ -2,6 +2,7 @@ import logging
from flask import (
Blueprint,
+ current_app,
jsonify,
render_template,
redirect,
@@ -17,6 +18,7 @@ from ..part import BrickPart
from ..set import BrickSet
from ..set_checkbox_list import BrickSetCheckboxList
from ..set_list import BrickSetList
+from ..socket import MESSAGES
logger = logging.getLogger(__name__)
@@ -154,3 +156,16 @@ def missing_part(
))
return jsonify({'missing': missing})
+
+
+# Refresh a set
+@set_page.route('/<id>/refresh', methods=['GET'])
+@exception_handler(__file__)
+def refresh(*, id: str) -> str:
+ return render_template(
+ 'refresh.html',
+ item=BrickSet().select_specific(id),
+ path=current_app.config['SOCKET_PATH'],
+ namespace=current_app.config['SOCKET_NAMESPACE'],
+ messages=MESSAGES
+ )
diff --git a/static/scripts/socket/set.js b/static/scripts/socket/set.js
index 41056b8b..4f6bf978 100644
--- a/static/scripts/socket/set.js
+++ b/static/scripts/socket/set.js
@@ -1,8 +1,11 @@
// Set Socket class
class BrickSetSocket extends BrickSocket {
- constructor(id, path, namespace, messages, bulk=false) {
+ constructor(id, path, namespace, messages, bulk=false, refresh=false) {
super(id, path, namespace, messages, bulk);
+ // Refresh mode
+ this.refresh = true
+
// Listeners
this.add_listener = undefined;
this.input_listener = undefined;
@@ -82,7 +85,7 @@ class BrickSetSocket extends BrickSocket {
this.read_set_list();
}
- if (this.bulk || (this.html_no_confim && this.html_no_confim.checked)) {
+ if (this.bulk || this.refresh || (this.html_no_confim && this.html_no_confim.checked)) {
this.import_set(true);
} else {
this.load_set();
@@ -140,6 +143,7 @@ class BrickSetSocket extends BrickSocket {
this.socket.emit(this.messages.IMPORT_SET, {
set: (set !== undefined) ? set : this.html_input.value,
+ refresh: this.refresh
});
} else {
this.fail("Could not find the input field for the set number");
diff --git a/templates/add.html b/templates/add.html
index 140eec62..5316ea17 100644
--- a/templates/add.html
+++ b/templates/add.html
@@ -68,5 +68,7 @@
</div>
</div>
</div>
-{% include 'set/socket.html' %}
+{% with id='add' %}
+ {% include 'set/socket.html' %}
+{% endwith %}
{% endblock %}
diff --git a/templates/bulk.html b/templates/bulk.html
index 6e6e5d8a..00d47797 100644
--- a/templates/bulk.html
+++ b/templates/bulk.html
@@ -58,7 +58,7 @@
</div>
</div>
</div>
-{% with bulk=true %}
+{% with id='add', bulk=true %}
{% include 'set/socket.html' %}
{% endwith %}
{% endblock %}
diff --git a/templates/refresh.html b/templates/refresh.html
new file mode 100644
index 00000000..5add93d8
--- /dev/null
+++ b/templates/refresh.html
@@ -0,0 +1,64 @@
+{% extends 'base.html' %}
+
+{% block title %} - Refresh set {{ item.fields.set }}{% endblock %}
+
+{% block main %}
+<div class="container">
+ <div class="alert alert-primary" role="alert">
+ <h4 class="alert-heading">Refreshing from Rebrickable</h4>
+ <p class="mb-0">This will refresh all the Rebrickable data (set, minifigures, parts) associated with this set.</p>
+ </div>
+ <div class="row">
+ <div class="col-12">
+ <div class="card mb-3">
+ <div class="card-header">
+ <h5 class="mb-0"><i class="ri-refresh-line"></i> Refresh a set</h5>
+ </div>
+ <div class="card-body">
+ <div id="refresh-fail" class="alert alert-danger d-none" role="alert"></div>
+ <div id="refresh-complete" class="alert alert-success d-none" role="alert"></div>
+ <div class="mb-3">
+ <label for="refresh-set" class="form-label">Set number</label>
+ <input type="text" class="form-control" id="refresh-set" value="{{ item.fields.set }}">
+ </div>
+ <hr>
+ <div class="mb-3">
+ <p>
+ Progress <span id="refresh-count"></span>
+ <span id="refresh-spinner" class="d-none">
+ <span class="spinner-border spinner-border-sm" aria-hidden="true"></span>
+ <span class="visually-hidden" role="status">Loading...</span>
+ </span>
+ </p>
+ <div id="refresh-progress" class="progress" role="progressbar" aria-label="Refresh a set progress" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
+ <div id="refresh-progress-bar" class="progress-bar" style="width: 0%"></div>
+ </div>
+ <p id="refresh-progress-message" class="text-center d-none"></p>
+ </div>
+ <div id="refresh-card" class="d-flex justify-content-center">
+ <div class="card mb-3 col-6">
+ <div class="card-header">
+ <h5 class="mb-0">
+ <span class="badge text-bg-secondary fw-normal"><i class="ri-hashtag"></i> <span id="refresh-card-set">{{ item.fields.set }}</span></span>
+ <span id="refresh-card-name">{{ item.fields.name }}</span>
+ </h5>
+ </div>
+ <div id="refresh-card-image-container" class="card-img" style="background-image: url({{ item.url_for_image() }})">
+ <img id="refresh-card-image" src="{{ item.url_for_image() }}">
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="card-footer text-end">
+ <span id="refresh-status-icon" class="me-1"></span><span id="refresh-status" class="me-1"></span>
+ <a href="{{ url_for('set.details', id=item.fields.id) }}" class="btn btn-primary" role="button"><i class="ri-hashtag"></i> Back to the set details</a>
+ <button id="refresh" type="button" class="btn btn-primary"><i class="ri-refresh-line"></i> Refresh</button>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+{% with id='refresh', refresh=true %}
+ {% include 'set/socket.html' %}
+{% endwith %}
+{% endblock %}
diff --git a/templates/set/card.html b/templates/set/card.html
index 20bfe4a5..02d77b30 100644
--- a/templates/set/card.html
+++ b/templates/set/card.html
@@ -63,6 +63,9 @@
{% endfor %}
{% endif %}
{% if g.login.is_authenticated() %}
+ {{ accordion.header('Management', 'management', 'set-details', icon='settings-4-line', class='text-end') }}
+ <a href="{{ item.url_for_refresh() }}" class="btn btn-primary" role="button"><i class="ri-refresh-line"></i> Refresh the set</a>
+ {{ accordion.footer() }}
{{ accordion.header('Danger zone', 'danger-zone', 'set-details', expanded=delete, danger=true, class='text-end') }}
{% if delete %}
<form action="{{ item.url_for_do_delete() }}" method="post">
diff --git a/templates/set/socket.html b/templates/set/socket.html
index a566a95d..c4000a82 100644
--- a/templates/set/socket.html
+++ b/templates/set/socket.html
@@ -1,12 +1,19 @@
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", () => {
- new BrickSetSocket('add', '{{ path }}', '{{ namespace }}', {
- COMPLETE: '{{ messages['COMPLETE'] }}',
- FAIL: '{{ messages['FAIL'] }}',
- IMPORT_SET: '{{ messages['IMPORT_SET'] }}',
- LOAD_SET: '{{ messages['LOAD_SET'] }}',
- PROGRESS: '{{ messages['PROGRESS'] }}',
- SET_LOADED: '{{ messages['SET_LOADED'] }}',
- }{% if bulk %}, true{% endif %});
+ new BrickSetSocket(
+ '{{ id }}',
+ '{{ path }}',
+ '{{ namespace }}',
+ {
+ COMPLETE: '{{ messages['COMPLETE'] }}',
+ FAIL: '{{ messages['FAIL'] }}',
+ IMPORT_SET: '{{ messages['IMPORT_SET'] }}',
+ LOAD_SET: '{{ messages['LOAD_SET'] }}',
+ PROGRESS: '{{ messages['PROGRESS'] }}',
+ SET_LOADED: '{{ messages['SET_LOADED'] }}',
+ },
+ {% if bulk %}true{% else %}false{% endif %},
+ {% if refresh %}true{% else %}false{% endif %}
+ );
});
</script>