WIP: Initial work on deduplicating the minifigures and parts #57
17
.env.sample
17
.env.sample
@ -2,7 +2,7 @@
|
|||||||
# If set, it will append a direct ORDER BY <whatever you set> to the SQL query
|
# If set, it will append a direct ORDER BY <whatever you set> to the SQL query
|
||||||
# while listing objects. You can look at the structure of the SQLite database to
|
# while listing objects. You can look at the structure of the SQLite database to
|
||||||
# see the schema and the column names. Some fields are compound and not visible
|
# see the schema and the column names. Some fields are compound and not visible
|
||||||
# directly from the schema (joins). You can check the query in the */list.sql files
|
# directly from the schema (joins). You can check the query in the */list.sql and */base/*.sql files
|
||||||
# in the source to see all column names.
|
# in the source to see all column names.
|
||||||
# The usual syntax for those variables is "<table>"."<column>" [ASC|DESC].
|
# The usual syntax for those variables is "<table>"."<column>" [ASC|DESC].
|
||||||
# For composite fields (CASE, SUM, COUNT) the syntax is <field>, there is no <table> name.
|
# For composite fields (CASE, SUM, COUNT) the syntax is <field>, there is no <table> name.
|
||||||
@ -111,16 +111,21 @@
|
|||||||
# Default: false
|
# Default: false
|
||||||
# BK_HIDE_MISSING_PARTS=true
|
# BK_HIDE_MISSING_PARTS=true
|
||||||
|
|
||||||
|
# Optional: Hide the 'Instructions' entry in a Set card
|
||||||
|
# Default: false
|
||||||
|
# BK_HIDE_SET_INSTRUCTIONS=true
|
||||||
|
|
||||||
# Optional: Hide the 'Wishlist' entry from the menu. Does not disable the route.
|
# Optional: Hide the 'Wishlist' entry from the menu. Does not disable the route.
|
||||||
# Default: false
|
# Default: false
|
||||||
# BK_HIDE_WISHES=true
|
# BK_HIDE_WISHES=true
|
||||||
|
|
||||||
# Optional: Change the default order of minifigures. By default ordered by insertion order.
|
# Optional: Change the default order of minifigures. By default ordered by insertion order.
|
||||||
# Useful column names for this option are:
|
# Useful column names for this option are:
|
||||||
# - "minifigures"."fig_num": minifigure ID (fig-xxxxx)
|
# - "rebrickable_minifigures"."figure": minifigure ID (fig-xxxxx)
|
||||||
# - "minifigures"."name": minifigure name
|
# - "rebrickable_minifigures"."number": minifigure ID as an integer (xxxxx)
|
||||||
# Default: "minifigures"."name" ASC
|
# - "rebrickable_minifigures"."name": minifigure name
|
||||||
# BK_MINIFIGURES_DEFAULT_ORDER="minifigures"."name" ASC
|
# Default: "rebrickable_minifigures"."name" ASC
|
||||||
|
# BK_MINIFIGURES_DEFAULT_ORDER="rebrickable_minifigures"."name" ASC
|
||||||
|
|
||||||
# Optional: Folder where to store the minifigures images, relative to the '/app/static/' folder
|
# Optional: Folder where to store the minifigures images, relative to the '/app/static/' folder
|
||||||
# Default: minifigs
|
# Default: minifigs
|
||||||
@ -171,7 +176,7 @@
|
|||||||
# BK_REBRICKABLE_IMAGE_NIL_MINIFIGURE=
|
# BK_REBRICKABLE_IMAGE_NIL_MINIFIGURE=
|
||||||
|
|
||||||
# Optional: Pattern of the link to Rebrickable for a minifigure. Will be passed to Python .format()
|
# Optional: Pattern of the link to Rebrickable for a minifigure. Will be passed to Python .format()
|
||||||
# Default: https://rebrickable.com/minifigs/{number}
|
# Default: https://rebrickable.com/minifigs/{figure}
|
||||||
# BK_REBRICKABLE_LINK_MINIFIGURE_PATTERN=
|
# BK_REBRICKABLE_LINK_MINIFIGURE_PATTERN=
|
||||||
|
|
||||||
# Optional: Pattern of the link to Rebrickable for a part. Will be passed to Python .format()
|
# Optional: Pattern of the link to Rebrickable for a part. Will be passed to Python .format()
|
||||||
|
27
CHANGELOG.md
27
CHANGELOG.md
@ -1,5 +1,32 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
- General cleanup
|
||||||
|
|
||||||
|
- Minifigure
|
||||||
|
- Deduplicate
|
||||||
|
|
||||||
|
- Socket
|
||||||
|
- Add decorator for rebrickable, authenticated and threaded socket actions
|
||||||
|
|
||||||
|
- SQL
|
||||||
|
- Allow for advanced migration scenarios through companion python files
|
||||||
|
|
||||||
|
### UI
|
||||||
|
|
||||||
|
- Add
|
||||||
|
- Allow adding or bulk adding by pressing Enter in the input field
|
||||||
|
|
||||||
|
- Admin
|
||||||
|
- Grey out legacy tables in the database view
|
||||||
|
|
||||||
|
- Sets
|
||||||
|
- Add a flag to hide instructions in a set
|
||||||
|
|
||||||
|
|
||||||
## 1.1.1: PDF Instructions Download
|
## 1.1.1: PDF Instructions Download
|
||||||
|
|
||||||
### Instructions
|
### Instructions
|
||||||
|
@ -30,8 +30,9 @@ CONFIG: Final[list[dict[str, Any]]] = [
|
|||||||
{'n': 'HIDE_ALL_PARTS', 'c': bool},
|
{'n': 'HIDE_ALL_PARTS', 'c': bool},
|
||||||
{'n': 'HIDE_ALL_SETS', 'c': bool},
|
{'n': 'HIDE_ALL_SETS', 'c': bool},
|
||||||
{'n': 'HIDE_MISSING_PARTS', 'c': bool},
|
{'n': 'HIDE_MISSING_PARTS', 'c': bool},
|
||||||
|
{'n': 'HIDE_SET_INSTRUCTIONS', 'c': bool},
|
||||||
{'n': 'HIDE_WISHES', 'c': bool},
|
{'n': 'HIDE_WISHES', 'c': bool},
|
||||||
{'n': 'MINIFIGURES_DEFAULT_ORDER', 'd': '"minifigures"."name" ASC'},
|
{'n': 'MINIFIGURES_DEFAULT_ORDER', 'd': '"rebrickable_minifigures"."name" ASC'}, # noqa: E501
|
||||||
{'n': 'MINIFIGURES_FOLDER', 'd': 'minifigs', 's': True},
|
{'n': 'MINIFIGURES_FOLDER', 'd': 'minifigs', 's': True},
|
||||||
{'n': 'NO_THREADED_SOCKET', 'c': bool},
|
{'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': '"inventory"."name" ASC, "inventory"."color_name" ASC, "inventory"."is_spare" ASC'}, # noqa: E501
|
||||||
@ -41,7 +42,7 @@ CONFIG: Final[list[dict[str, Any]]] = [
|
|||||||
{'n': 'REBRICKABLE_API_KEY', 'e': 'REBRICKABLE_API_KEY', 'd': ''},
|
{'n': 'REBRICKABLE_API_KEY', 'e': 'REBRICKABLE_API_KEY', 'd': ''},
|
||||||
{'n': 'REBRICKABLE_IMAGE_NIL', 'd': 'https://rebrickable.com/static/img/nil.png'}, # noqa: E501
|
{'n': 'REBRICKABLE_IMAGE_NIL', '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_IMAGE_NIL_MINIFIGURE', 'd': 'https://rebrickable.com/static/img/nil_mf.jpg'}, # noqa: E501
|
||||||
{'n': 'REBRICKABLE_LINK_MINIFIGURE_PATTERN', 'd': 'https://rebrickable.com/minifigs/{number}'}, # noqa: E501
|
{'n': 'REBRICKABLE_LINK_MINIFIGURE_PATTERN', 'd': 'https://rebrickable.com/minifigs/{figure}'}, # noqa: E501
|
||||||
{'n': 'REBRICKABLE_LINK_PART_PATTERN', 'd': 'https://rebrickable.com/parts/{number}/_/{color}'}, # noqa: E501
|
{'n': 'REBRICKABLE_LINK_PART_PATTERN', 'd': 'https://rebrickable.com/parts/{number}/_/{color}'}, # noqa: E501
|
||||||
{'n': 'REBRICKABLE_LINK_INSTRUCTIONS_PATTERN', 'd': 'https://rebrickable.com/instructions/{path}'}, # noqa: E501
|
{'n': 'REBRICKABLE_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_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
|
||||||
|
27
bricktracker/migrations/0007.py
Normal file
27
bricktracker/migrations/0007.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from typing import Any, TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..sql import BrickSQL
|
||||||
|
|
||||||
|
|
||||||
|
# Grab the list of checkboxes to create a list of SQL columns
|
||||||
|
def migration_0007(self: 'BrickSQL') -> dict[str, Any]:
|
||||||
|
records = self.fetchall('checkbox/list')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'sources': ', '.join([
|
||||||
|
'"bricktracker_set_statuses_old"."status_{id}"'.format(id=record['id']) # noqa: E501
|
||||||
|
for record
|
||||||
|
in records
|
||||||
|
]),
|
||||||
|
'targets': ', '.join([
|
||||||
|
'"status_{id}"'.format(id=record['id'])
|
||||||
|
for record
|
||||||
|
in records
|
||||||
|
]),
|
||||||
|
'structure': ', '.join([
|
||||||
|
'"status_{id}" BOOLEAN NOT NULL DEFAULT 0'.format(id=record['id'])
|
||||||
|
for record
|
||||||
|
in records
|
||||||
|
])
|
||||||
|
}
|
0
bricktracker/migrations/__init__.py
Normal file
0
bricktracker/migrations/__init__.py
Normal file
@ -1,48 +1,61 @@
|
|||||||
from sqlite3 import Row
|
import logging
|
||||||
from typing import Any, Self, TYPE_CHECKING
|
import traceback
|
||||||
|
from typing import Self, TYPE_CHECKING
|
||||||
from flask import current_app, url_for
|
|
||||||
|
|
||||||
from .exceptions import ErrorException, NotFoundException
|
from .exceptions import ErrorException, NotFoundException
|
||||||
from .part_list import BrickPartList
|
from .part_list import BrickPartList
|
||||||
from .rebrickable_image import RebrickableImage
|
from .rebrickable_parts import RebrickableParts
|
||||||
from .record import BrickRecord
|
from .rebrickable_minifigure import RebrickableMinifigure
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .set import BrickSet
|
from .set import BrickSet
|
||||||
|
from .socket import BrickSocket
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# Lego minifigure
|
# Lego minifigure
|
||||||
class BrickMinifigure(BrickRecord):
|
class BrickMinifigure(RebrickableMinifigure):
|
||||||
brickset: 'BrickSet | None'
|
|
||||||
|
|
||||||
# Queries
|
# Queries
|
||||||
insert_query: str = 'minifigure/insert'
|
insert_query: str = 'minifigure/insert'
|
||||||
generic_query: str = 'minifigure/select/generic'
|
generic_query: str = 'minifigure/select/generic'
|
||||||
select_query: str = 'minifigure/select/specific'
|
select_query: str = 'minifigure/select/specific'
|
||||||
|
|
||||||
def __init__(
|
def download(self, socket: 'BrickSocket'):
|
||||||
self,
|
if self.brickset is None:
|
||||||
/,
|
raise ErrorException('Importing a minifigure from Rebrickable outside of a set is not supported') # noqa: E501
|
||||||
*,
|
|
||||||
brickset: 'BrickSet | None' = None,
|
|
||||||
record: Row | dict[str, Any] | None = None,
|
|
||||||
):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
# Save the brickset
|
try:
|
||||||
self.brickset = brickset
|
# Insert into the database
|
||||||
|
socket.auto_progress(
|
||||||
|
message='Set {set}: inserting minifigure {figure} into database'.format( # noqa: E501
|
||||||
|
set=self.brickset.fields.set,
|
||||||
|
figure=self.fields.figure
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Ingest the record if it has one
|
# Insert into database
|
||||||
if record is not None:
|
self.insert(commit=False)
|
||||||
self.ingest(record)
|
|
||||||
|
|
||||||
# Return the number just in digits format
|
# Insert the rebrickable set into database
|
||||||
def clean_number(self, /) -> str:
|
self.insert_rebrickable()
|
||||||
number: str = self.fields.fig_num
|
|
||||||
number = number.removeprefix('fig-')
|
|
||||||
number = number.lstrip('0')
|
|
||||||
|
|
||||||
return number
|
# Load the inventory
|
||||||
|
RebrickableParts(
|
||||||
|
socket,
|
||||||
|
self.brickset,
|
||||||
|
minifigure=self,
|
||||||
|
).download()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
socket.fail(
|
||||||
|
message='Error while importing minifigure {figure} from {set}: {error}'.format( # noqa: E501
|
||||||
|
figure=self.fields.figure,
|
||||||
|
set=self.brickset.fields.set,
|
||||||
|
error=e,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(traceback.format_exc())
|
||||||
|
|
||||||
# Parts
|
# Parts
|
||||||
def generic_parts(self, /) -> BrickPartList:
|
def generic_parts(self, /) -> BrickPartList:
|
||||||
@ -51,108 +64,38 @@ class BrickMinifigure(BrickRecord):
|
|||||||
# Parts
|
# Parts
|
||||||
def parts(self, /) -> BrickPartList:
|
def parts(self, /) -> BrickPartList:
|
||||||
if self.brickset is None:
|
if self.brickset is None:
|
||||||
raise ErrorException('Part list for minifigure {number} requires a brickset'.format( # noqa: E501
|
raise ErrorException('Part list for minifigure {figure} requires a brickset'.format( # noqa: E501
|
||||||
number=self.fields.fig_num,
|
figure=self.fields.figure,
|
||||||
))
|
))
|
||||||
|
|
||||||
return BrickPartList().load(self.brickset, minifigure=self)
|
return BrickPartList().load(self.brickset, minifigure=self)
|
||||||
|
|
||||||
# Select a generic minifigure
|
# Select a generic minifigure
|
||||||
def select_generic(self, fig_num: str, /) -> Self:
|
def select_generic(self, figure: str, /) -> Self:
|
||||||
# Save the parameters to the fields
|
# Save the parameters to the fields
|
||||||
self.fields.fig_num = fig_num
|
self.fields.figure = figure
|
||||||
|
|
||||||
if not self.select(override_query=self.generic_query):
|
if not self.select(override_query=self.generic_query):
|
||||||
raise NotFoundException(
|
raise NotFoundException(
|
||||||
'Minifigure with number {number} was not found in the database'.format( # noqa: E501
|
'Minifigure with figure {figure} was not found in the database'.format( # noqa: E501
|
||||||
number=self.fields.fig_num,
|
figure=self.fields.figure,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
# Select a specific minifigure (with a set and an number)
|
# Select a specific minifigure (with a set and a figure)
|
||||||
def select_specific(self, brickset: 'BrickSet', fig_num: str, /) -> Self:
|
def select_specific(self, brickset: 'BrickSet', figure: str, /) -> Self:
|
||||||
# Save the parameters to the fields
|
# Save the parameters to the fields
|
||||||
self.brickset = brickset
|
self.brickset = brickset
|
||||||
self.fields.fig_num = fig_num
|
self.fields.figure = figure
|
||||||
|
|
||||||
if not self.select():
|
if not self.select():
|
||||||
raise NotFoundException(
|
raise NotFoundException(
|
||||||
'Minifigure with number {number} from set {set} was not found in the database'.format( # noqa: E501
|
'Minifigure with figure {figure} from set {set} was not found in the database'.format( # noqa: E501
|
||||||
number=self.fields.fig_num,
|
figure=self.fields.figure,
|
||||||
set=self.brickset.fields.set,
|
set=self.brickset.fields.set,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
# Return a dict with common SQL parameters for a minifigure
|
|
||||||
def sql_parameters(self, /) -> dict[str, Any]:
|
|
||||||
parameters = super().sql_parameters()
|
|
||||||
|
|
||||||
# Supplement from the brickset
|
|
||||||
if self.brickset is not None:
|
|
||||||
if 'u_id' not in parameters:
|
|
||||||
parameters['u_id'] = self.brickset.fields.id
|
|
||||||
|
|
||||||
if 'set_num' not in parameters:
|
|
||||||
parameters['set_num'] = self.brickset.fields.set
|
|
||||||
|
|
||||||
return parameters
|
|
||||||
|
|
||||||
# Self url
|
|
||||||
def url(self, /) -> str:
|
|
||||||
return url_for(
|
|
||||||
'minifigure.details',
|
|
||||||
number=self.fields.fig_num,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Compute the url for minifigure part image
|
|
||||||
def url_for_image(self, /) -> str:
|
|
||||||
if not current_app.config['USE_REMOTE_IMAGES']:
|
|
||||||
if self.fields.set_img_url is None:
|
|
||||||
file = RebrickableImage.nil_minifigure_name()
|
|
||||||
else:
|
|
||||||
file = self.fields.fig_num
|
|
||||||
|
|
||||||
return RebrickableImage.static_url(file, 'MINIFIGURES_FOLDER')
|
|
||||||
else:
|
|
||||||
if self.fields.set_img_url is None:
|
|
||||||
return current_app.config['REBRICKABLE_IMAGE_NIL_MINIFIGURE']
|
|
||||||
else:
|
|
||||||
return self.fields.set_img_url
|
|
||||||
|
|
||||||
# Compute the url for the rebrickable page
|
|
||||||
def url_for_rebrickable(self, /) -> str:
|
|
||||||
if current_app.config['REBRICKABLE_LINKS']:
|
|
||||||
try:
|
|
||||||
return current_app.config['REBRICKABLE_LINK_MINIFIGURE_PATTERN'].format( # noqa: E501
|
|
||||||
number=self.fields.fig_num.lower(),
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return ''
|
|
||||||
|
|
||||||
# Normalize from Rebrickable
|
|
||||||
@staticmethod
|
|
||||||
def from_rebrickable(
|
|
||||||
data: dict[str, Any],
|
|
||||||
/,
|
|
||||||
*,
|
|
||||||
brickset: 'BrickSet | None' = None,
|
|
||||||
**_,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
record = {
|
|
||||||
'fig_num': data['set_num'],
|
|
||||||
'name': data['set_name'],
|
|
||||||
'quantity': data['quantity'],
|
|
||||||
'set_img_url': data['set_img_url'],
|
|
||||||
}
|
|
||||||
|
|
||||||
if brickset is not None:
|
|
||||||
record['set_num'] = brickset.fields.set
|
|
||||||
record['u_id'] = brickset.fields.id
|
|
||||||
|
|
||||||
return record
|
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
|
import logging
|
||||||
|
import traceback
|
||||||
from typing import Any, Self, TYPE_CHECKING
|
from typing import Any, Self, TYPE_CHECKING
|
||||||
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
from .minifigure import BrickMinifigure
|
from .minifigure import BrickMinifigure
|
||||||
|
from .rebrickable import Rebrickable
|
||||||
from .record_list import BrickRecordList
|
from .record_list import BrickRecordList
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .set import BrickSet
|
from .set import BrickSet
|
||||||
|
from .socket import BrickSocket
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# Lego minifigures
|
# Lego minifigures
|
||||||
@ -47,7 +53,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
|
|||||||
if current_app.config['RANDOM']:
|
if current_app.config['RANDOM']:
|
||||||
order = 'RANDOM()'
|
order = 'RANDOM()'
|
||||||
else:
|
else:
|
||||||
order = 'minifigures.rowid DESC'
|
order = '"bricktracker_minifigures"."rowid" DESC'
|
||||||
|
|
||||||
for record in self.select(
|
for record in self.select(
|
||||||
override_query=self.last_query,
|
override_query=self.last_query,
|
||||||
@ -61,7 +67,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
# Load minifigures from a brickset
|
# Load minifigures from a brickset
|
||||||
def load(self, brickset: 'BrickSet', /) -> Self:
|
def from_set(self, brickset: 'BrickSet', /) -> Self:
|
||||||
# Save the brickset
|
# Save the brickset
|
||||||
self.brickset = brickset
|
self.brickset = brickset
|
||||||
|
|
||||||
@ -73,16 +79,6 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
|
|||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
# Return a dict with common SQL parameters for a minifigures list
|
|
||||||
def sql_parameters(self, /) -> dict[str, Any]:
|
|
||||||
parameters: dict[str, Any] = super().sql_parameters()
|
|
||||||
|
|
||||||
if self.brickset is not None:
|
|
||||||
parameters['u_id'] = self.brickset.fields.id
|
|
||||||
parameters['set_num'] = self.brickset.fields.set
|
|
||||||
|
|
||||||
return parameters
|
|
||||||
|
|
||||||
# Minifigures missing a part
|
# Minifigures missing a part
|
||||||
def missing_part(
|
def missing_part(
|
||||||
self,
|
self,
|
||||||
@ -132,3 +128,51 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
|
|||||||
self.records.append(minifigure)
|
self.records.append(minifigure)
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
# Return a dict with common SQL parameters for a minifigures list
|
||||||
|
def sql_parameters(self, /) -> dict[str, Any]:
|
||||||
|
parameters: dict[str, Any] = super().sql_parameters()
|
||||||
|
|
||||||
|
if self.brickset is not None:
|
||||||
|
parameters['id'] = self.brickset.fields.id
|
||||||
|
|
||||||
|
return parameters
|
||||||
|
|
||||||
|
# Import the minifigures from Rebrickable
|
||||||
|
@staticmethod
|
||||||
|
def download(socket: 'BrickSocket', brickset: 'BrickSet', /) -> None:
|
||||||
|
try:
|
||||||
|
socket.auto_progress(
|
||||||
|
message='Set {set}: loading minifigures from Rebrickable'.format( # noqa: E501
|
||||||
|
set=brickset.fields.set,
|
||||||
|
),
|
||||||
|
increment_total=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug('rebrick.lego.get_set_minifigs("{set}")'.format(
|
||||||
|
set=brickset.fields.set,
|
||||||
|
))
|
||||||
|
|
||||||
|
minifigures = Rebrickable[BrickMinifigure](
|
||||||
|
'get_set_minifigs',
|
||||||
|
brickset.fields.set,
|
||||||
|
BrickMinifigure,
|
||||||
|
socket=socket,
|
||||||
|
brickset=brickset,
|
||||||
|
).list()
|
||||||
|
|
||||||
|
# Process each minifigure
|
||||||
|
socket.update_total(len(minifigures), add=True)
|
||||||
|
|
||||||
|
for minifigure in minifigures:
|
||||||
|
minifigure.download(socket)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
socket.fail(
|
||||||
|
message='Error while importing set {set} minifigure list: {error}'.format( # noqa: E501
|
||||||
|
set=brickset.fields.set,
|
||||||
|
error=e,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug(traceback.format_exc())
|
||||||
|
@ -137,7 +137,7 @@ class BrickPart(BrickRecord):
|
|||||||
|
|
||||||
if 'set_num' not in parameters:
|
if 'set_num' not in parameters:
|
||||||
if self.minifigure is not None:
|
if self.minifigure is not None:
|
||||||
parameters['set_num'] = self.minifigure.fields.fig_num
|
parameters['set_num'] = self.minifigure.fields.figure
|
||||||
|
|
||||||
elif self.brickset is not None:
|
elif self.brickset is not None:
|
||||||
parameters['set_num'] = self.brickset.fields.set
|
parameters['set_num'] = self.brickset.fields.set
|
||||||
@ -215,14 +215,14 @@ class BrickPart(BrickRecord):
|
|||||||
return url_for(
|
return url_for(
|
||||||
'set.missing_minifigure_part',
|
'set.missing_minifigure_part',
|
||||||
id=self.fields.u_id,
|
id=self.fields.u_id,
|
||||||
minifigure_id=self.minifigure.fields.fig_num,
|
figure=self.minifigure.fields.figure,
|
||||||
part_id=self.fields.id,
|
part=self.fields.id,
|
||||||
)
|
)
|
||||||
|
|
||||||
return url_for(
|
return url_for(
|
||||||
'set.missing_part',
|
'set.missing_part',
|
||||||
id=self.fields.u_id,
|
id=self.fields.u_id,
|
||||||
part_id=self.fields.id
|
part=self.fields.id
|
||||||
)
|
)
|
||||||
|
|
||||||
# Compute the url for the rebrickable page
|
# Compute the url for the rebrickable page
|
||||||
|
@ -120,7 +120,7 @@ class BrickPartList(BrickRecordList[BrickPart]):
|
|||||||
# Use the minifigure number if present,
|
# Use the minifigure number if present,
|
||||||
# otherwise use the set number
|
# otherwise use the set number
|
||||||
if self.minifigure is not None:
|
if self.minifigure is not None:
|
||||||
parameters['set_num'] = self.minifigure.fields.fig_num
|
parameters['set_num'] = self.minifigure.fields.figure
|
||||||
elif self.brickset is not None:
|
elif self.brickset is not None:
|
||||||
parameters['set_num'] = self.brickset.fields.set
|
parameters['set_num'] = self.brickset.fields.set
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ from shutil import copyfileobj
|
|||||||
|
|
||||||
from .exceptions import DownloadException
|
from .exceptions import DownloadException
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .minifigure import BrickMinifigure
|
from .rebrickable_minifigure import RebrickableMinifigure
|
||||||
from .part import BrickPart
|
from .part import BrickPart
|
||||||
from .rebrickable_set import RebrickableSet
|
from .rebrickable_set import RebrickableSet
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ if TYPE_CHECKING:
|
|||||||
# A set, part or minifigure image from Rebrickable
|
# A set, part or minifigure image from Rebrickable
|
||||||
class RebrickableImage(object):
|
class RebrickableImage(object):
|
||||||
set: 'RebrickableSet'
|
set: 'RebrickableSet'
|
||||||
minifigure: 'BrickMinifigure | None'
|
minifigure: 'RebrickableMinifigure | None'
|
||||||
part: 'BrickPart | None'
|
part: 'BrickPart | None'
|
||||||
|
|
||||||
extension: str | None
|
extension: str | None
|
||||||
@ -26,7 +26,7 @@ class RebrickableImage(object):
|
|||||||
set: 'RebrickableSet',
|
set: 'RebrickableSet',
|
||||||
/,
|
/,
|
||||||
*,
|
*,
|
||||||
minifigure: 'BrickMinifigure | None' = None,
|
minifigure: 'RebrickableMinifigure | None' = None,
|
||||||
part: 'BrickPart | None' = None,
|
part: 'BrickPart | None' = None,
|
||||||
):
|
):
|
||||||
# Save all objects
|
# Save all objects
|
||||||
@ -87,10 +87,10 @@ class RebrickableImage(object):
|
|||||||
return self.part.fields.part_img_url_id
|
return self.part.fields.part_img_url_id
|
||||||
|
|
||||||
if self.minifigure is not None:
|
if self.minifigure is not None:
|
||||||
if self.minifigure.fields.set_img_url is None:
|
if self.minifigure.fields.image is None:
|
||||||
return RebrickableImage.nil_minifigure_name()
|
return RebrickableImage.nil_minifigure_name()
|
||||||
else:
|
else:
|
||||||
return self.minifigure.fields.fig_num
|
return self.minifigure.fields.figure
|
||||||
|
|
||||||
return self.set.fields.set
|
return self.set.fields.set
|
||||||
|
|
||||||
@ -111,10 +111,10 @@ class RebrickableImage(object):
|
|||||||
return self.part.fields.part_img_url
|
return self.part.fields.part_img_url
|
||||||
|
|
||||||
if self.minifigure is not None:
|
if self.minifigure is not None:
|
||||||
if self.minifigure.fields.set_img_url is None:
|
if self.minifigure.fields.image is None:
|
||||||
return current_app.config['REBRICKABLE_IMAGE_NIL_MINIFIGURE']
|
return current_app.config['REBRICKABLE_IMAGE_NIL_MINIFIGURE']
|
||||||
else:
|
else:
|
||||||
return self.minifigure.fields.set_img_url
|
return self.minifigure.fields.image
|
||||||
|
|
||||||
return self.set.fields.image
|
return self.set.fields.image
|
||||||
|
|
||||||
|
129
bricktracker/rebrickable_minifigure.py
Normal file
129
bricktracker/rebrickable_minifigure.py
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import logging
|
||||||
|
from sqlite3 import Row
|
||||||
|
from typing import Any, TYPE_CHECKING
|
||||||
|
|
||||||
|
from flask import current_app, url_for
|
||||||
|
|
||||||
|
from .exceptions import ErrorException
|
||||||
|
from .rebrickable_image import RebrickableImage
|
||||||
|
from .record import BrickRecord
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .set import BrickSet
|
||||||
|
from .socket import BrickSocket
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# A minifigure from Rebrickable
|
||||||
|
class RebrickableMinifigure(BrickRecord):
|
||||||
|
socket: 'BrickSocket'
|
||||||
|
brickset: 'BrickSet | None'
|
||||||
|
|
||||||
|
# Queries
|
||||||
|
select_query: str = 'rebrickable/minifigure/select'
|
||||||
|
insert_query: str = 'rebrickable/minifigure/insert'
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
/,
|
||||||
|
*,
|
||||||
|
brickset: 'BrickSet | None' = None,
|
||||||
|
socket: 'BrickSocket | None' = None,
|
||||||
|
record: Row | dict[str, Any] | None = None
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
# Placeholders
|
||||||
|
self.instructions = []
|
||||||
|
|
||||||
|
# Save the brickset
|
||||||
|
self.brickset = brickset
|
||||||
|
|
||||||
|
# Save the socket
|
||||||
|
if socket is not None:
|
||||||
|
self.socket = socket
|
||||||
|
|
||||||
|
# Ingest the record if it has one
|
||||||
|
if record is not None:
|
||||||
|
self.ingest(record)
|
||||||
|
|
||||||
|
# Insert the minifigure from Rebrickable
|
||||||
|
def insert_rebrickable(self, /) -> bool:
|
||||||
|
if self.brickset is None:
|
||||||
|
raise ErrorException('Importing a minifigure from Rebrickable outside of a set is not supported') # noqa: E501
|
||||||
|
|
||||||
|
# Insert the Rebrickable minifigure to the database
|
||||||
|
rows, _ = self.insert(
|
||||||
|
commit=False,
|
||||||
|
no_defer=True,
|
||||||
|
override_query=RebrickableMinifigure.insert_query
|
||||||
|
)
|
||||||
|
|
||||||
|
inserted = rows > 0
|
||||||
|
|
||||||
|
if inserted:
|
||||||
|
if not current_app.config['USE_REMOTE_IMAGES']:
|
||||||
|
RebrickableImage(
|
||||||
|
self.brickset,
|
||||||
|
minifigure=self,
|
||||||
|
).download()
|
||||||
|
|
||||||
|
return inserted
|
||||||
|
|
||||||
|
# Return a dict with common SQL parameters for a minifigure
|
||||||
|
def sql_parameters(self, /) -> dict[str, Any]:
|
||||||
|
parameters = super().sql_parameters()
|
||||||
|
|
||||||
|
# Supplement from the brickset
|
||||||
|
if self.brickset is not None and 'id' not in parameters:
|
||||||
|
parameters['id'] = self.brickset.fields.id
|
||||||
|
|
||||||
|
return parameters
|
||||||
|
|
||||||
|
# Self url
|
||||||
|
def url(self, /) -> str:
|
||||||
|
return url_for(
|
||||||
|
'minifigure.details',
|
||||||
|
figure=self.fields.figure,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Compute the url for minifigure image
|
||||||
|
def url_for_image(self, /) -> str:
|
||||||
|
if not current_app.config['USE_REMOTE_IMAGES']:
|
||||||
|
if self.fields.image is None:
|
||||||
|
file = RebrickableImage.nil_minifigure_name()
|
||||||
|
else:
|
||||||
|
file = self.fields.figure
|
||||||
|
|
||||||
|
return RebrickableImage.static_url(file, 'MINIFIGURES_FOLDER')
|
||||||
|
else:
|
||||||
|
if self.fields.image is None:
|
||||||
|
return current_app.config['REBRICKABLE_IMAGE_NIL_MINIFIGURE']
|
||||||
|
else:
|
||||||
|
return self.fields.image
|
||||||
|
|
||||||
|
# Compute the url for the rebrickable page
|
||||||
|
def url_for_rebrickable(self, /) -> str:
|
||||||
|
if current_app.config['REBRICKABLE_LINKS']:
|
||||||
|
try:
|
||||||
|
return current_app.config['REBRICKABLE_LINK_MINIFIGURE_PATTERN'].format( # noqa: E501
|
||||||
|
number=self.fields.figure,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return ''
|
||||||
|
|
||||||
|
# Normalize from Rebrickable
|
||||||
|
@staticmethod
|
||||||
|
def from_rebrickable(data: dict[str, Any], /, **_) -> dict[str, Any]:
|
||||||
|
# Extracting number
|
||||||
|
number = int(str(data['set_num'])[5:])
|
||||||
|
|
||||||
|
return {
|
||||||
|
'figure': str(data['set_num']),
|
||||||
|
'number': int(number),
|
||||||
|
'name': str(data['set_name']),
|
||||||
|
'quantity': int(data['quantity']),
|
||||||
|
'image': data['set_img_url'],
|
||||||
|
}
|
@ -1,85 +0,0 @@
|
|||||||
import logging
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from flask import current_app
|
|
||||||
|
|
||||||
from .minifigure import BrickMinifigure
|
|
||||||
from .rebrickable import Rebrickable
|
|
||||||
from .rebrickable_image import RebrickableImage
|
|
||||||
from .rebrickable_parts import RebrickableParts
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .set import BrickSet
|
|
||||||
from .socket import BrickSocket
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
# Minifigures from Rebrickable
|
|
||||||
class RebrickableMinifigures(object):
|
|
||||||
socket: 'BrickSocket'
|
|
||||||
brickset: 'BrickSet'
|
|
||||||
|
|
||||||
def __init__(self, socket: 'BrickSocket', brickset: 'BrickSet', /):
|
|
||||||
# Save the socket
|
|
||||||
self.socket = socket
|
|
||||||
|
|
||||||
# Save the objects
|
|
||||||
self.brickset = brickset
|
|
||||||
|
|
||||||
# Import the minifigures from Rebrickable
|
|
||||||
def download(self, /) -> None:
|
|
||||||
self.socket.auto_progress(
|
|
||||||
message='Set {number}: loading minifigures from Rebrickable'.format( # noqa: E501
|
|
||||||
number=self.brickset.fields.set,
|
|
||||||
),
|
|
||||||
increment_total=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.debug('rebrick.lego.get_set_minifigs("{set}")'.format(
|
|
||||||
set=self.brickset.fields.set,
|
|
||||||
))
|
|
||||||
|
|
||||||
minifigures = Rebrickable[BrickMinifigure](
|
|
||||||
'get_set_minifigs',
|
|
||||||
self.brickset.fields.set,
|
|
||||||
BrickMinifigure,
|
|
||||||
socket=self.socket,
|
|
||||||
brickset=self.brickset,
|
|
||||||
).list()
|
|
||||||
|
|
||||||
# Process each minifigure
|
|
||||||
total = len(minifigures)
|
|
||||||
for index, minifigure in enumerate(minifigures):
|
|
||||||
# Insert into the database
|
|
||||||
self.socket.auto_progress(
|
|
||||||
message='Set {number}: inserting minifigure {current}/{total} into database'.format( # noqa: E501
|
|
||||||
number=self.brickset.fields.set,
|
|
||||||
current=index+1,
|
|
||||||
total=total,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Insert into database
|
|
||||||
minifigure.insert(commit=False)
|
|
||||||
|
|
||||||
# Grab the image
|
|
||||||
self.socket.progress(
|
|
||||||
message='Set {number}: downloading minifigure {current}/{total} image'.format( # noqa: E501
|
|
||||||
number=self.brickset.fields.set,
|
|
||||||
current=index+1,
|
|
||||||
total=total,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not current_app.config['USE_REMOTE_IMAGES']:
|
|
||||||
RebrickableImage(
|
|
||||||
self.brickset,
|
|
||||||
minifigure=minifigure
|
|
||||||
).download()
|
|
||||||
|
|
||||||
# Load the inventory
|
|
||||||
RebrickableParts(
|
|
||||||
self.socket,
|
|
||||||
self.brickset,
|
|
||||||
minifigure=minifigure,
|
|
||||||
).download()
|
|
@ -40,7 +40,7 @@ class RebrickableParts(object):
|
|||||||
self.minifigure = minifigure
|
self.minifigure = minifigure
|
||||||
|
|
||||||
if self.minifigure is not None:
|
if self.minifigure is not None:
|
||||||
self.number = self.minifigure.fields.fig_num
|
self.number = self.minifigure.fields.figure
|
||||||
self.kind = 'Minifigure'
|
self.kind = 'Minifigure'
|
||||||
self.method = 'get_minifig_elements'
|
self.method = 'get_minifig_elements'
|
||||||
else:
|
else:
|
||||||
|
@ -21,7 +21,6 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
# A set from Rebrickable
|
# A set from Rebrickable
|
||||||
class RebrickableSet(BrickRecord):
|
class RebrickableSet(BrickRecord):
|
||||||
socket: 'BrickSocket'
|
|
||||||
theme: 'BrickTheme'
|
theme: 'BrickTheme'
|
||||||
instructions: list[BrickInstructions]
|
instructions: list[BrickInstructions]
|
||||||
|
|
||||||
@ -36,7 +35,6 @@ class RebrickableSet(BrickRecord):
|
|||||||
self,
|
self,
|
||||||
/,
|
/,
|
||||||
*,
|
*,
|
||||||
socket: 'BrickSocket | None' = None,
|
|
||||||
record: Row | dict[str, Any] | None = None
|
record: Row | dict[str, Any] | None = None
|
||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -44,16 +42,12 @@ class RebrickableSet(BrickRecord):
|
|||||||
# Placeholders
|
# Placeholders
|
||||||
self.instructions = []
|
self.instructions = []
|
||||||
|
|
||||||
# Save the socket
|
|
||||||
if socket is not None:
|
|
||||||
self.socket = socket
|
|
||||||
|
|
||||||
# Ingest the record if it has one
|
# Ingest the record if it has one
|
||||||
if record is not None:
|
if record is not None:
|
||||||
self.ingest(record)
|
self.ingest(record)
|
||||||
|
|
||||||
# Import the set from Rebrickable
|
# Insert the set from Rebrickable
|
||||||
def download_rebrickable(self, /) -> None:
|
def insert_rebrickable(self, /) -> bool:
|
||||||
# Insert the Rebrickable set to the database
|
# Insert the Rebrickable set to the database
|
||||||
rows, _ = self.insert(
|
rows, _ = self.insert(
|
||||||
commit=False,
|
commit=False,
|
||||||
@ -61,10 +55,14 @@ class RebrickableSet(BrickRecord):
|
|||||||
override_query=RebrickableSet.insert_query
|
override_query=RebrickableSet.insert_query
|
||||||
)
|
)
|
||||||
|
|
||||||
if rows > 0:
|
inserted = rows > 0
|
||||||
|
|
||||||
|
if inserted:
|
||||||
if not current_app.config['USE_REMOTE_IMAGES']:
|
if not current_app.config['USE_REMOTE_IMAGES']:
|
||||||
RebrickableImage(self).download()
|
RebrickableImage(self).download()
|
||||||
|
|
||||||
|
return inserted
|
||||||
|
|
||||||
# Ingest a set
|
# Ingest a set
|
||||||
def ingest(self, record: Row | dict[str, Any], /):
|
def ingest(self, record: Row | dict[str, Any], /):
|
||||||
super().ingest(record)
|
super().ingest(record)
|
||||||
@ -88,20 +86,21 @@ class RebrickableSet(BrickRecord):
|
|||||||
# Load the set from Rebrickable
|
# Load the set from Rebrickable
|
||||||
def load(
|
def load(
|
||||||
self,
|
self,
|
||||||
|
socket: 'BrickSocket',
|
||||||
data: dict[str, Any],
|
data: dict[str, Any],
|
||||||
/,
|
/,
|
||||||
*,
|
*,
|
||||||
from_download=False,
|
from_download=False,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
# Reset the progress
|
# Reset the progress
|
||||||
self.socket.progress_count = 0
|
socket.progress_count = 0
|
||||||
self.socket.progress_total = 2
|
socket.progress_total = 2
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.socket.auto_progress(message='Parsing set number')
|
socket.auto_progress(message='Parsing set number')
|
||||||
set = parse_set(str(data['set']))
|
set = parse_set(str(data['set']))
|
||||||
|
|
||||||
self.socket.auto_progress(
|
socket.auto_progress(
|
||||||
message='Set {set}: loading from Rebrickable'.format(
|
message='Set {set}: loading from Rebrickable'.format(
|
||||||
set=set,
|
set=set,
|
||||||
),
|
),
|
||||||
@ -118,12 +117,12 @@ class RebrickableSet(BrickRecord):
|
|||||||
instance=self,
|
instance=self,
|
||||||
).get()
|
).get()
|
||||||
|
|
||||||
self.socket.emit('SET_LOADED', self.short(
|
socket.emit('SET_LOADED', self.short(
|
||||||
from_download=from_download
|
from_download=from_download
|
||||||
))
|
))
|
||||||
|
|
||||||
if not from_download:
|
if not from_download:
|
||||||
self.socket.complete(
|
socket.complete(
|
||||||
message='Set {set}: loaded from Rebrickable'.format(
|
message='Set {set}: loaded from Rebrickable'.format(
|
||||||
set=self.fields.set
|
set=self.fields.set
|
||||||
)
|
)
|
||||||
@ -132,7 +131,7 @@ class RebrickableSet(BrickRecord):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.socket.fail(
|
socket.fail(
|
||||||
message='Could not load the set from Rebrickable: {error}. Data: {data}'.format( # noqa: E501
|
message='Could not load the set from Rebrickable: {error}. Data: {data}'.format( # noqa: E501
|
||||||
error=str(e),
|
error=str(e),
|
||||||
data=data,
|
data=data,
|
||||||
|
@ -1,19 +1,20 @@
|
|||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
from typing import Any, Self
|
from typing import Any, Self, TYPE_CHECKING
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from flask import url_for
|
from flask import current_app, url_for
|
||||||
|
|
||||||
from .exceptions import DatabaseException, NotFoundException
|
from .exceptions import DatabaseException, NotFoundException
|
||||||
from .minifigure_list import BrickMinifigureList
|
from .minifigure_list import BrickMinifigureList
|
||||||
from .part_list import BrickPartList
|
from .part_list import BrickPartList
|
||||||
from .rebrickable_minifigures import RebrickableMinifigures
|
|
||||||
from .rebrickable_parts import RebrickableParts
|
from .rebrickable_parts import RebrickableParts
|
||||||
from .rebrickable_set import RebrickableSet
|
from .rebrickable_set import RebrickableSet
|
||||||
from .set_checkbox import BrickSetCheckbox
|
from .set_checkbox import BrickSetCheckbox
|
||||||
from .set_checkbox_list import BrickSetCheckboxList
|
from .set_checkbox_list import BrickSetCheckboxList
|
||||||
from .sql import BrickSQL
|
from .sql import BrickSQL
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .socket import BrickSocket
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -33,16 +34,16 @@ class BrickSet(RebrickableSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Import a set into the database
|
# Import a set into the database
|
||||||
def download(self, data: dict[str, Any], /) -> None:
|
def download(self, socket: 'BrickSocket', data: dict[str, Any], /) -> None:
|
||||||
# Load the set
|
# Load the set
|
||||||
if not self.load(data, from_download=True):
|
if not self.load(socket, data, from_download=True):
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Insert into the database
|
# Insert into the database
|
||||||
self.socket.auto_progress(
|
socket.auto_progress(
|
||||||
message='Set {number}: inserting into database'.format(
|
message='Set {set}: inserting into database'.format(
|
||||||
number=self.fields.set
|
set=self.fields.set
|
||||||
),
|
),
|
||||||
increment_total=True,
|
increment_total=True,
|
||||||
)
|
)
|
||||||
@ -53,19 +54,19 @@ class BrickSet(RebrickableSet):
|
|||||||
# Insert into database
|
# Insert into database
|
||||||
self.insert(commit=False)
|
self.insert(commit=False)
|
||||||
|
|
||||||
# Execute the parent download method
|
# Insert the rebrickable set into database
|
||||||
self.download_rebrickable()
|
self.insert_rebrickable()
|
||||||
|
|
||||||
# Load the inventory
|
# Load the inventory
|
||||||
RebrickableParts(self.socket, self).download()
|
RebrickableParts(socket, self).download()
|
||||||
|
|
||||||
# Load the minifigures
|
# Load the minifigures
|
||||||
RebrickableMinifigures(self.socket, self).download()
|
BrickMinifigureList.download(socket, self)
|
||||||
|
|
||||||
# Commit the transaction to the database
|
# Commit the transaction to the database
|
||||||
self.socket.auto_progress(
|
socket.auto_progress(
|
||||||
message='Set {number}: writing to the database'.format(
|
message='Set {set}: writing to the database'.format(
|
||||||
number=self.fields.set
|
set=self.fields.set
|
||||||
),
|
),
|
||||||
increment_total=True,
|
increment_total=True,
|
||||||
)
|
)
|
||||||
@ -73,37 +74,33 @@ class BrickSet(RebrickableSet):
|
|||||||
BrickSQL().commit()
|
BrickSQL().commit()
|
||||||
|
|
||||||
# Info
|
# Info
|
||||||
logger.info('Set {number}: imported (id: {id})'.format(
|
logger.info('Set {set}: imported (id: {id})'.format(
|
||||||
number=self.fields.set,
|
set=self.fields.set,
|
||||||
id=self.fields.id,
|
id=self.fields.id,
|
||||||
))
|
))
|
||||||
|
|
||||||
# Complete
|
# Complete
|
||||||
self.socket.complete(
|
socket.complete(
|
||||||
message='Set {number}: imported (<a href="{url}">Go to the set</a>)'.format( # noqa: E501
|
message='Set {set}: imported (<a href="{url}">Go to the set</a>)'.format( # noqa: E501
|
||||||
number=self.fields.set,
|
set=self.fields.set,
|
||||||
url=self.url()
|
url=self.url()
|
||||||
),
|
),
|
||||||
download=True
|
download=True
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.socket.fail(
|
socket.fail(
|
||||||
message='Error while importing set {number}: {error}'.format(
|
message='Error while importing set {set}: {error}'.format(
|
||||||
number=self.fields.set,
|
set=self.fields.set,
|
||||||
error=e,
|
error=e,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(traceback.format_exc())
|
logger.debug(traceback.format_exc())
|
||||||
|
|
||||||
# Insert a Rebrickable set
|
|
||||||
def insert_rebrickable(self, /) -> None:
|
|
||||||
self.insert()
|
|
||||||
|
|
||||||
# Minifigures
|
# Minifigures
|
||||||
def minifigures(self, /) -> BrickMinifigureList:
|
def minifigures(self, /) -> BrickMinifigureList:
|
||||||
return BrickMinifigureList().load(self)
|
return BrickMinifigureList().from_set(self)
|
||||||
|
|
||||||
# Parts
|
# Parts
|
||||||
def parts(self, /) -> BrickPartList:
|
def parts(self, /) -> BrickPartList:
|
||||||
@ -159,9 +156,9 @@ class BrickSet(RebrickableSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if rows != 1:
|
if rows != 1:
|
||||||
raise DatabaseException('Could not update the status "{status}" for set {number} ({id})'.format( # noqa: E501
|
raise DatabaseException('Could not update the status "{status}" for set {set} ({id})'.format( # noqa: E501
|
||||||
status=checkbox.fields.name,
|
status=checkbox.fields.name,
|
||||||
number=self.fields.set,
|
set=self.fields.set,
|
||||||
id=self.fields.id,
|
id=self.fields.id,
|
||||||
))
|
))
|
||||||
|
|
||||||
@ -179,7 +176,10 @@ class BrickSet(RebrickableSet):
|
|||||||
|
|
||||||
# Compute the url for the set instructions
|
# Compute the url for the set instructions
|
||||||
def url_for_instructions(self, /) -> str:
|
def url_for_instructions(self, /) -> str:
|
||||||
if len(self.instructions):
|
if (
|
||||||
|
not current_app.config['HIDE_SET_INSTRUCTIONS'] and
|
||||||
|
len(self.instructions)
|
||||||
|
):
|
||||||
return url_for(
|
return url_for(
|
||||||
'set.details',
|
'set.details',
|
||||||
id=self.fields.id,
|
id=self.fields.id,
|
||||||
|
@ -82,13 +82,9 @@ class BrickSetList(BrickRecordList[BrickSet]):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
# Sets missing a minifigure
|
# Sets missing a minifigure
|
||||||
def missing_minifigure(
|
def missing_minifigure(self, figure: str, /) -> Self:
|
||||||
self,
|
|
||||||
fig_num: str,
|
|
||||||
/
|
|
||||||
) -> Self:
|
|
||||||
# Save the parameters to the fields
|
# Save the parameters to the fields
|
||||||
self.fields.fig_num = fig_num
|
self.fields.figure = figure
|
||||||
|
|
||||||
# Load the sets from the database
|
# Load the sets from the database
|
||||||
for record in self.select(
|
for record in self.select(
|
||||||
@ -127,13 +123,9 @@ class BrickSetList(BrickRecordList[BrickSet]):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
# Sets using a minifigure
|
# Sets using a minifigure
|
||||||
def using_minifigure(
|
def using_minifigure(self, figure: str, /) -> Self:
|
||||||
self,
|
|
||||||
fig_num: str,
|
|
||||||
/
|
|
||||||
) -> Self:
|
|
||||||
# Save the parameters to the fields
|
# Save the parameters to the fields
|
||||||
self.fields.fig_num = fig_num
|
self.fields.figure = figure
|
||||||
|
|
||||||
# Load the sets from the database
|
# Load the sets from the database
|
||||||
for record in self.select(
|
for record in self.select(
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any, Final, Tuple
|
from typing import Any, Final, Tuple
|
||||||
|
|
||||||
from flask import copy_current_request_context, Flask, request
|
from flask import Flask, request
|
||||||
from flask_socketio import SocketIO
|
from flask_socketio import SocketIO
|
||||||
|
|
||||||
from .configuration_list import BrickConfigurationList
|
|
||||||
from .instructions import BrickInstructions
|
from .instructions import BrickInstructions
|
||||||
from .instructions_list import BrickInstructionsList
|
from .instructions_list import BrickInstructionsList
|
||||||
from .login import LoginManager
|
|
||||||
from .set import BrickSet
|
from .set import BrickSet
|
||||||
|
from .socket_decorator import authenticated_socket, rebrickable_socket
|
||||||
from .sql import close as sql_close
|
from .sql import close as sql_close
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -87,12 +86,8 @@ class BrickSocket(object):
|
|||||||
self.disconnected()
|
self.disconnected()
|
||||||
|
|
||||||
@self.socket.on(MESSAGES['DOWNLOAD_INSTRUCTIONS'], namespace=self.namespace) # noqa: E501
|
@self.socket.on(MESSAGES['DOWNLOAD_INSTRUCTIONS'], namespace=self.namespace) # noqa: E501
|
||||||
|
@authenticated_socket(self)
|
||||||
def download_instructions(data: dict[str, Any], /) -> None:
|
def download_instructions(data: dict[str, Any], /) -> None:
|
||||||
# Needs to be authenticated
|
|
||||||
if LoginManager.is_not_authenticated():
|
|
||||||
self.fail(message='You need to be authenticated')
|
|
||||||
return
|
|
||||||
|
|
||||||
instructions = BrickInstructions(
|
instructions = BrickInstructions(
|
||||||
'{name}.pdf'.format(name=data.get('alt', '')),
|
'{name}.pdf'.format(name=data.get('alt', '')),
|
||||||
socket=self
|
socket=self
|
||||||
@ -107,71 +102,18 @@ class BrickSocket(object):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Start it in a thread if requested
|
instructions.download(path)
|
||||||
if self.threaded:
|
|
||||||
@copy_current_request_context
|
|
||||||
def do_download() -> None:
|
|
||||||
instructions.download(path)
|
|
||||||
|
|
||||||
BrickInstructionsList(force=True)
|
BrickInstructionsList(force=True)
|
||||||
|
|
||||||
self.socket.start_background_task(do_download)
|
|
||||||
else:
|
|
||||||
instructions.download(path)
|
|
||||||
|
|
||||||
BrickInstructionsList(force=True)
|
|
||||||
|
|
||||||
@self.socket.on(MESSAGES['IMPORT_SET'], namespace=self.namespace)
|
@self.socket.on(MESSAGES['IMPORT_SET'], namespace=self.namespace)
|
||||||
|
@rebrickable_socket(self)
|
||||||
def import_set(data: dict[str, Any], /) -> None:
|
def import_set(data: dict[str, Any], /) -> None:
|
||||||
# Needs to be authenticated
|
BrickSet().download(self, data)
|
||||||
if LoginManager.is_not_authenticated():
|
|
||||||
self.fail(message='You need to be authenticated')
|
|
||||||
return
|
|
||||||
|
|
||||||
# Needs the Rebrickable API key
|
|
||||||
try:
|
|
||||||
BrickConfigurationList.error_unless_is_set('REBRICKABLE_API_KEY') # noqa: E501
|
|
||||||
except Exception as e:
|
|
||||||
self.fail(message=str(e))
|
|
||||||
return
|
|
||||||
|
|
||||||
brickset = BrickSet(socket=self)
|
|
||||||
|
|
||||||
# Start it in a thread if requested
|
|
||||||
if self.threaded:
|
|
||||||
@copy_current_request_context
|
|
||||||
def do_download() -> None:
|
|
||||||
brickset.download(data)
|
|
||||||
|
|
||||||
self.socket.start_background_task(do_download)
|
|
||||||
else:
|
|
||||||
brickset.download(data)
|
|
||||||
|
|
||||||
@self.socket.on(MESSAGES['LOAD_SET'], namespace=self.namespace)
|
@self.socket.on(MESSAGES['LOAD_SET'], namespace=self.namespace)
|
||||||
def load_set(data: dict[str, Any], /) -> None:
|
def load_set(data: dict[str, Any], /) -> None:
|
||||||
# Needs to be authenticated
|
BrickSet().load(self, data)
|
||||||
if LoginManager.is_not_authenticated():
|
|
||||||
self.fail(message='You need to be authenticated')
|
|
||||||
return
|
|
||||||
|
|
||||||
# Needs the Rebrickable API key
|
|
||||||
try:
|
|
||||||
BrickConfigurationList.error_unless_is_set('REBRICKABLE_API_KEY') # noqa: E501
|
|
||||||
except Exception as e:
|
|
||||||
self.fail(message=str(e))
|
|
||||||
return
|
|
||||||
|
|
||||||
brickset = BrickSet(socket=self)
|
|
||||||
|
|
||||||
# Start it in a thread if requested
|
|
||||||
if self.threaded:
|
|
||||||
@copy_current_request_context
|
|
||||||
def do_load() -> None:
|
|
||||||
brickset.load(data)
|
|
||||||
|
|
||||||
self.socket.start_background_task(do_load)
|
|
||||||
else:
|
|
||||||
brickset.load(data)
|
|
||||||
|
|
||||||
# Update the progress auto-incrementing
|
# Update the progress auto-incrementing
|
||||||
def auto_progress(
|
def auto_progress(
|
||||||
|
93
bricktracker/socket_decorator.py
Normal file
93
bricktracker/socket_decorator.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
from functools import wraps
|
||||||
|
from threading import Thread
|
||||||
|
from typing import Callable, ParamSpec, TYPE_CHECKING, Union
|
||||||
|
|
||||||
|
from flask import copy_current_request_context
|
||||||
|
|
||||||
|
from .configuration_list import BrickConfigurationList
|
||||||
|
from .login import LoginManager
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .socket import BrickSocket
|
||||||
|
|
||||||
|
# What a threaded function can return (None or Thread)
|
||||||
|
SocketReturn = Union[None, Thread]
|
||||||
|
|
||||||
|
# Threaded signature (*arg, **kwargs -> (None or Thread)
|
||||||
|
P = ParamSpec('P')
|
||||||
|
SocketCallable = Callable[P, SocketReturn]
|
||||||
|
|
||||||
|
|
||||||
|
# Fail if not authenticated
|
||||||
|
def authenticated_socket(
|
||||||
|
self: 'BrickSocket',
|
||||||
|
/,
|
||||||
|
*,
|
||||||
|
threaded: bool = True,
|
||||||
|
) -> Callable[[SocketCallable], SocketCallable]:
|
||||||
|
def outer(function: SocketCallable, /) -> SocketCallable:
|
||||||
|
@wraps(function)
|
||||||
|
def wrapper(*args, **kwargs) -> SocketReturn:
|
||||||
|
# Needs to be authenticated
|
||||||
|
if LoginManager.is_not_authenticated():
|
||||||
|
self.fail(message='You need to be authenticated')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Apply threading
|
||||||
|
if threaded:
|
||||||
|
return threaded_socket(self)(function)(*args, **kwargs)
|
||||||
|
else:
|
||||||
|
return function(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
return outer
|
||||||
|
|
||||||
|
|
||||||
|
# Fail if not ready for Rebrickable (authenticated, API key)
|
||||||
|
# Automatically makes it threaded
|
||||||
|
def rebrickable_socket(
|
||||||
|
self: 'BrickSocket',
|
||||||
|
/,
|
||||||
|
*,
|
||||||
|
threaded: bool = True,
|
||||||
|
) -> Callable[[SocketCallable], SocketCallable]:
|
||||||
|
def outer(function: SocketCallable, /) -> SocketCallable:
|
||||||
|
@wraps(function)
|
||||||
|
# Automatically authenticated
|
||||||
|
@authenticated_socket(self, threaded=False)
|
||||||
|
def wrapper(*args, **kwargs) -> SocketReturn:
|
||||||
|
# Needs the Rebrickable API key
|
||||||
|
try:
|
||||||
|
BrickConfigurationList.error_unless_is_set('REBRICKABLE_API_KEY') # noqa: E501
|
||||||
|
except Exception as e:
|
||||||
|
self.fail(message=str(e))
|
||||||
|
return
|
||||||
|
|
||||||
|
# Apply threading
|
||||||
|
if threaded:
|
||||||
|
return threaded_socket(self)(function)(*args, **kwargs)
|
||||||
|
else:
|
||||||
|
return function(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
return outer
|
||||||
|
|
||||||
|
|
||||||
|
# Start the function in a thread if the socket is threaded
|
||||||
|
def threaded_socket(
|
||||||
|
self: 'BrickSocket',
|
||||||
|
/
|
||||||
|
) -> Callable[[SocketCallable], SocketCallable]:
|
||||||
|
def outer(function: SocketCallable, /) -> SocketCallable:
|
||||||
|
@wraps(function)
|
||||||
|
def wrapper(*args, **kwargs) -> SocketReturn:
|
||||||
|
# Start it in a thread if requested
|
||||||
|
if self.threaded:
|
||||||
|
@copy_current_request_context
|
||||||
|
def do_function() -> None:
|
||||||
|
function(*args, **kwargs)
|
||||||
|
|
||||||
|
return self.socket.start_background_task(do_function)
|
||||||
|
else:
|
||||||
|
return function(*args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
return outer
|
@ -1,3 +1,4 @@
|
|||||||
|
from importlib import import_module
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sqlite3
|
import sqlite3
|
||||||
@ -301,7 +302,32 @@ class BrickSQL(object):
|
|||||||
version=pending.version)
|
version=pending.version)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.executescript(pending.get_query())
|
# Load context from the migrations if it exists
|
||||||
|
# It looks for a file in migrations/ named after the SQL file
|
||||||
|
# and containing one function named migration_xxxx, also named
|
||||||
|
# after the SQL file, returning a context dict.
|
||||||
|
#
|
||||||
|
# For instance:
|
||||||
|
# - sql/migrations/0007.sql
|
||||||
|
# - migrations/0007.py
|
||||||
|
# - def migration_0007(BrickSQL) -> dict[str, Any]
|
||||||
|
try:
|
||||||
|
module = import_module(
|
||||||
|
'.migrations.{name}'.format(
|
||||||
|
name=pending.name
|
||||||
|
),
|
||||||
|
package='bricktracker'
|
||||||
|
)
|
||||||
|
|
||||||
|
function = getattr(module, 'migration_{name}'.format(
|
||||||
|
name=pending.name
|
||||||
|
))
|
||||||
|
|
||||||
|
context: dict[str, Any] = function(self)
|
||||||
|
except Exception:
|
||||||
|
context: dict[str, Any] = {}
|
||||||
|
|
||||||
|
self.executescript(pending.get_query(), **context)
|
||||||
self.execute('schema/set_version', version=pending.version)
|
self.execute('schema/set_version', version=pending.version)
|
||||||
|
|
||||||
# Tells whether the database needs upgrade
|
# Tells whether the database needs upgrade
|
||||||
|
53
bricktracker/sql/migrations/0007.sql
Normal file
53
bricktracker/sql/migrations/0007.sql
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
-- description: Renaming various complicated field names to something simpler
|
||||||
|
|
||||||
|
PRAGMA foreign_keys = ON;
|
||||||
|
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
-- Rename sets table
|
||||||
|
ALTER TABLE "bricktracker_sets" RENAME TO "bricktracker_sets_old";
|
||||||
|
|
||||||
|
-- Re-Create a Bricktable set table with the simplified name
|
||||||
|
CREATE TABLE "bricktracker_sets" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"set" TEXT NOT NULL,
|
||||||
|
PRIMARY KEY("id"),
|
||||||
|
FOREIGN KEY("set") REFERENCES "rebrickable_sets"("set")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Insert existing sets into the new table
|
||||||
|
INSERT INTO "bricktracker_sets" (
|
||||||
|
"id",
|
||||||
|
"set"
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
"bricktracker_sets_old"."id",
|
||||||
|
"bricktracker_sets_old"."rebrickable_set"
|
||||||
|
FROM "bricktracker_sets_old";
|
||||||
|
|
||||||
|
-- Rename status table
|
||||||
|
ALTER TABLE "bricktracker_set_statuses" RENAME TO "bricktracker_set_statuses_old";
|
||||||
|
|
||||||
|
-- Re-create a table for the status of each checkbox
|
||||||
|
CREATE TABLE "bricktracker_set_statuses" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
{% if structure %}{{ structure }},{% endif %}
|
||||||
|
PRIMARY KEY("id"),
|
||||||
|
FOREIGN KEY("id") REFERENCES "bricktracker_sets"("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Insert existing status into the new table
|
||||||
|
INSERT INTO "bricktracker_set_statuses" (
|
||||||
|
{% if targets %}{{ targets }},{% endif %}
|
||||||
|
"id"
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
{% if sources %}{{ sources }},{% endif %}
|
||||||
|
"bricktracker_set_statuses_old"."bricktracker_set_id"
|
||||||
|
FROM "bricktracker_set_statuses_old";
|
||||||
|
|
||||||
|
-- Delete the original tables
|
||||||
|
DROP TABLE "bricktracker_set_statuses_old";
|
||||||
|
DROP TABLE "bricktracker_sets_old";
|
||||||
|
|
||||||
|
COMMIT;
|
30
bricktracker/sql/migrations/0008.sql
Normal file
30
bricktracker/sql/migrations/0008.sql
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
-- description: Creation of the deduplicated table of Rebrickable minifigures
|
||||||
|
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
-- Create a Rebrickable minifigures table: each unique minifigure imported from Rebrickable
|
||||||
|
CREATE TABLE "rebrickable_minifigures" (
|
||||||
|
"figure" TEXT NOT NULL,
|
||||||
|
"number" INTEGER NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"image" TEXT,
|
||||||
|
PRIMARY KEY("figure")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Insert existing sets into the new table
|
||||||
|
INSERT INTO "rebrickable_minifigures" (
|
||||||
|
"figure",
|
||||||
|
"number",
|
||||||
|
"name",
|
||||||
|
"image"
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
"minifigures"."fig_num",
|
||||||
|
CAST(SUBSTR("minifigures"."fig_num", 5) AS INTEGER),
|
||||||
|
"minifigures"."name",
|
||||||
|
"minifigures"."set_img_url"
|
||||||
|
FROM "minifigures"
|
||||||
|
GROUP BY
|
||||||
|
"minifigures"."fig_num";
|
||||||
|
|
||||||
|
COMMIT;
|
32
bricktracker/sql/migrations/0009.sql
Normal file
32
bricktracker/sql/migrations/0009.sql
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
-- description: Migrate the Bricktracker minifigures
|
||||||
|
|
||||||
|
PRAGMA foreign_keys = ON;
|
||||||
|
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
|
||||||
|
-- Create a Bricktable minifigures table: an amount of minifigures linked to a Bricktracker set
|
||||||
|
CREATE TABLE "bricktracker_minifigures" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"figure" TEXT NOT NULL,
|
||||||
|
"quantity" INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY("id", "figure"),
|
||||||
|
FOREIGN KEY("id") REFERENCES "bricktracker_sets"("id"),
|
||||||
|
FOREIGN KEY("figure") REFERENCES "rebrickable_minifigures"("figure")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Insert existing sets into the new table
|
||||||
|
INSERT INTO "bricktracker_minifigures" (
|
||||||
|
"id",
|
||||||
|
"figure",
|
||||||
|
"quantity"
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
"minifigures"."u_id",
|
||||||
|
"minifigures"."fig_num",
|
||||||
|
"minifigures"."quantity"
|
||||||
|
FROM "minifigures";
|
||||||
|
|
||||||
|
-- Rename the original table (don't delete it yet?)
|
||||||
|
ALTER TABLE "minifigures" RENAME TO "minifigures_old";
|
||||||
|
|
||||||
|
COMMIT;
|
@ -1,10 +1,10 @@
|
|||||||
SELECT
|
SELECT
|
||||||
"minifigures"."fig_num",
|
{% block set %}{% endblock %}
|
||||||
"minifigures"."set_num",
|
"bricktracker_minifigures"."quantity",
|
||||||
"minifigures"."name",
|
"rebrickable_minifigures"."figure",
|
||||||
"minifigures"."quantity",
|
"rebrickable_minifigures"."number",
|
||||||
"minifigures"."set_img_url",
|
"rebrickable_minifigures"."name",
|
||||||
"minifigures"."u_id",
|
"rebrickable_minifigures"."image",
|
||||||
{% block total_missing %}
|
{% block total_missing %}
|
||||||
NULL AS "total_missing", -- dummy for order: total_missing
|
NULL AS "total_missing", -- dummy for order: total_missing
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -14,7 +14,10 @@ SELECT
|
|||||||
{% block total_sets %}
|
{% block total_sets %}
|
||||||
NULL AS "total_sets" -- dummy for order: total_sets
|
NULL AS "total_sets" -- dummy for order: total_sets
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
FROM "minifigures"
|
FROM "bricktracker_minifigures"
|
||||||
|
|
||||||
|
INNER JOIN "rebrickable_minifigures"
|
||||||
|
ON "bricktracker_minifigures"."figure" IS NOT DISTINCT FROM "rebrickable_minifigures"."figure"
|
||||||
|
|
||||||
{% block join %}{% endblock %}
|
{% block join %}{% endblock %}
|
||||||
|
|
@ -1,15 +1,9 @@
|
|||||||
INSERT INTO "minifigures" (
|
INSERT INTO "bricktracker_minifigures" (
|
||||||
"fig_num",
|
"id",
|
||||||
"set_num",
|
"figure",
|
||||||
"name",
|
"quantity"
|
||||||
"quantity",
|
|
||||||
"set_img_url",
|
|
||||||
"u_id"
|
|
||||||
) VALUES (
|
) VALUES (
|
||||||
:fig_num,
|
:id,
|
||||||
:set_num,
|
:figure,
|
||||||
:name,
|
:quantity
|
||||||
:quantity,
|
|
||||||
:set_img_url,
|
|
||||||
:u_id
|
|
||||||
)
|
)
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
{% extends 'minifigure/base/select.sql' %}
|
{% extends 'minifigure/base/base.sql' %}
|
||||||
|
|
||||||
{% block total_missing %}
|
{% block total_missing %}
|
||||||
SUM(IFNULL("missing_join"."total", 0)) AS "total_missing",
|
SUM(IFNULL("missing_join"."total", 0)) AS "total_missing",
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block total_quantity %}
|
{% block total_quantity %}
|
||||||
SUM(IFNULL("minifigures"."quantity", 0)) AS "total_quantity",
|
SUM(IFNULL("bricktracker_minifigures"."quantity", 0)) AS "total_quantity",
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block total_sets %}
|
{% block total_sets %}
|
||||||
COUNT("minifigures"."set_num") AS "total_sets"
|
COUNT("bricktracker_minifigures"."id") AS "total_sets"
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block join %}
|
{% block join %}
|
||||||
@ -24,11 +24,11 @@ LEFT JOIN (
|
|||||||
"missing"."set_num",
|
"missing"."set_num",
|
||||||
"missing"."u_id"
|
"missing"."u_id"
|
||||||
) missing_join
|
) missing_join
|
||||||
ON "minifigures"."u_id" IS NOT DISTINCT FROM "missing_join"."u_id"
|
ON "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "missing_join"."u_id"
|
||||||
AND "minifigures"."fig_num" IS NOT DISTINCT FROM "missing_join"."set_num"
|
AND "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "missing_join"."set_num"
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block group %}
|
{% block group %}
|
||||||
GROUP BY
|
GROUP BY
|
||||||
"minifigures"."fig_num"
|
"rebrickable_minifigures"."figure"
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
{% extends 'minifigure/base/select.sql' %}
|
{% extends 'minifigure/base/base.sql' %}
|
||||||
|
|
||||||
{% block where %}
|
{% block where %}
|
||||||
WHERE "minifigures"."u_id" IS NOT DISTINCT FROM :u_id
|
WHERE "bricktracker_minifigures"."id" IS NOT DISTINCT FROM :id
|
||||||
AND "minifigures"."set_num" IS NOT DISTINCT FROM :set_num
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'minifigure/base/select.sql' %}
|
{% extends 'minifigure/base/base.sql' %}
|
||||||
|
|
||||||
{% block total_missing %}
|
{% block total_missing %}
|
||||||
SUM(IFNULL("missing"."quantity", 0)) AS "total_missing",
|
SUM(IFNULL("missing"."quantity", 0)) AS "total_missing",
|
||||||
@ -6,12 +6,12 @@ SUM(IFNULL("missing"."quantity", 0)) AS "total_missing",
|
|||||||
|
|
||||||
{% block join %}
|
{% block join %}
|
||||||
LEFT JOIN "missing"
|
LEFT JOIN "missing"
|
||||||
ON "minifigures"."fig_num" IS NOT DISTINCT FROM "missing"."set_num"
|
ON "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "missing"."set_num"
|
||||||
AND "minifigures"."u_id" IS NOT DISTINCT FROM "missing"."u_id"
|
AND "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "missing"."u_id"
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block group %}
|
{% block group %}
|
||||||
GROUP BY
|
GROUP BY
|
||||||
"minifigures"."fig_num",
|
"rebrickable_minifigures"."figure",
|
||||||
"minifigures"."u_id"
|
"bricktracker_minifigures"."id"
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
{% extends 'minifigure/base/select.sql' %}
|
{% extends 'minifigure/base/base.sql' %}
|
||||||
|
|
||||||
{% block total_missing %}
|
{% block total_missing %}
|
||||||
SUM(IFNULL("missing"."quantity", 0)) AS "total_missing",
|
SUM(IFNULL("missing"."quantity", 0)) AS "total_missing",
|
||||||
@ -6,12 +6,12 @@ SUM(IFNULL("missing"."quantity", 0)) AS "total_missing",
|
|||||||
|
|
||||||
{% block join %}
|
{% block join %}
|
||||||
LEFT JOIN "missing"
|
LEFT JOIN "missing"
|
||||||
ON "minifigures"."fig_num" IS NOT DISTINCT FROM "missing"."set_num"
|
ON "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "missing"."set_num"
|
||||||
AND "minifigures"."u_id" IS NOT DISTINCT FROM "missing"."u_id"
|
AND "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "missing"."u_id"
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block where %}
|
{% block where %}
|
||||||
WHERE "minifigures"."fig_num" IN (
|
WHERE "rebrickable_minifigures"."figure" IN (
|
||||||
SELECT
|
SELECT
|
||||||
"missing"."set_num"
|
"missing"."set_num"
|
||||||
FROM "missing"
|
FROM "missing"
|
||||||
@ -26,5 +26,5 @@ WHERE "minifigures"."fig_num" IN (
|
|||||||
|
|
||||||
{% block group %}
|
{% block group %}
|
||||||
GROUP BY
|
GROUP BY
|
||||||
"minifigures"."fig_num"
|
"rebrickable_minifigures"."figure"
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
{% extends 'minifigure/base/select.sql' %}
|
{% extends 'minifigure/base/base.sql' %}
|
||||||
|
|
||||||
{% block total_quantity %}
|
{% block total_quantity %}
|
||||||
SUM("minifigures"."quantity") AS "total_quantity",
|
SUM("bricktracker_minifigures"."quantity") AS "total_quantity",
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block where %}
|
{% block where %}
|
||||||
WHERE "minifigures"."fig_num" IN (
|
WHERE "rebrickable_minifigures"."figure" IN (
|
||||||
SELECT
|
SELECT
|
||||||
"inventory"."set_num"
|
"inventory"."set_num"
|
||||||
FROM "inventory"
|
FROM "inventory"
|
||||||
@ -20,5 +20,5 @@ WHERE "minifigures"."fig_num" IN (
|
|||||||
|
|
||||||
{% block group %}
|
{% block group %}
|
||||||
GROUP BY
|
GROUP BY
|
||||||
"minifigures"."fig_num"
|
"rebrickable_minifigures"."figure"
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,38 +1,28 @@
|
|||||||
{% extends 'minifigure/base/select.sql' %}
|
{% extends 'minifigure/base/base.sql' %}
|
||||||
|
|
||||||
{% block total_missing %}
|
{% block total_missing %}
|
||||||
SUM(IFNULL("missing_join"."total", 0)) AS "total_missing",
|
SUM(IFNULL("missing"."quantity", 0)) AS "total_missing",
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block total_quantity %}
|
{% block total_quantity %}
|
||||||
SUM(IFNULL("minifigures"."quantity", 0)) AS "total_quantity",
|
SUM(IFNULL("bricktracker_minifigures"."quantity", 0)) AS "total_quantity",
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block total_sets %}
|
{% block total_sets %}
|
||||||
COUNT("minifigures"."set_num") AS "total_sets"
|
COUNT(DISTINCT "bricktracker_minifigures"."id") AS "total_sets"
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block join %}
|
{% block join %}
|
||||||
-- LEFT JOIN + SELECT to avoid messing the total
|
LEFT JOIN "missing"
|
||||||
LEFT JOIN (
|
ON "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM "missing"."set_num"
|
||||||
SELECT
|
AND "bricktracker_minifigures"."id" IS NOT DISTINCT FROM "missing"."u_id"
|
||||||
"missing"."set_num",
|
|
||||||
"missing"."u_id",
|
|
||||||
SUM("missing"."quantity") AS "total"
|
|
||||||
FROM "missing"
|
|
||||||
GROUP BY
|
|
||||||
"missing"."set_num",
|
|
||||||
"missing"."u_id"
|
|
||||||
) "missing_join"
|
|
||||||
ON "minifigures"."u_id" IS NOT DISTINCT FROM "missing_join"."u_id"
|
|
||||||
AND "minifigures"."fig_num" IS NOT DISTINCT FROM "missing_join"."set_num"
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block where %}
|
{% block where %}
|
||||||
WHERE "minifigures"."fig_num" IS NOT DISTINCT FROM :fig_num
|
WHERE "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM :figure
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block group %}
|
{% block group %}
|
||||||
GROUP BY
|
GROUP BY
|
||||||
"minifigures"."fig_num"
|
"rebrickable_minifigures"."figure"
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
{% extends 'minifigure/base/select.sql' %}
|
{% extends 'minifigure/base/base.sql' %}
|
||||||
|
|
||||||
{% block where %}
|
{% block where %}
|
||||||
WHERE "minifigures"."fig_num" IS NOT DISTINCT FROM :fig_num
|
WHERE "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM :figure
|
||||||
AND "minifigures"."u_id" IS NOT DISTINCT FROM :u_id
|
AND "bricktracker_minifigures"."id" IS NOT DISTINCT FROM :id
|
||||||
AND "minifigures"."set_num" IS NOT DISTINCT FROM :set_num
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -5,15 +5,15 @@ SUM(IFNULL("missing"."quantity", 0)) AS "total_missing",
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block total_quantity %}
|
{% block total_quantity %}
|
||||||
SUM("inventory"."quantity" * IFNULL("minifigures"."quantity", 1)) AS "total_quantity",
|
SUM("inventory"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)) AS "total_quantity",
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block total_sets %}
|
{% block total_sets %}
|
||||||
COUNT(DISTINCT "bricktracker_sets"."id") AS "total_sets",
|
COUNT(DISTINCT "bricktracker_minifigures"."id") AS "total_sets",
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block total_minifigures %}
|
{% block total_minifigures %}
|
||||||
SUM(IFNULL("minifigures"."quantity", 0)) AS "total_minifigures"
|
SUM(IFNULL("bricktracker_minifigures"."quantity", 0)) AS "total_minifigures"
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block join %}
|
{% block join %}
|
||||||
@ -25,12 +25,9 @@ AND "inventory"."color_id" IS NOT DISTINCT FROM "missing"."color_id"
|
|||||||
AND "inventory"."element_id" IS NOT DISTINCT FROM "missing"."element_id"
|
AND "inventory"."element_id" IS NOT DISTINCT FROM "missing"."element_id"
|
||||||
AND "inventory"."u_id" IS NOT DISTINCT FROM "missing"."u_id"
|
AND "inventory"."u_id" IS NOT DISTINCT FROM "missing"."u_id"
|
||||||
|
|
||||||
LEFT JOIN "minifigures"
|
LEFT JOIN "bricktracker_minifigures"
|
||||||
ON "inventory"."set_num" IS NOT DISTINCT FROM "minifigures"."fig_num"
|
ON "inventory"."set_num" IS NOT DISTINCT FROM "bricktracker_minifigures"."figure"
|
||||||
AND "inventory"."u_id" IS NOT DISTINCT FROM "minifigures"."u_id"
|
AND "inventory"."u_id" IS NOT DISTINCT FROM "bricktracker_minifigures"."id"
|
||||||
|
|
||||||
LEFT JOIN "bricktracker_sets"
|
|
||||||
ON "inventory"."u_id" IS NOT DISTINCT FROM "bricktracker_sets"."id"
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block group %}
|
{% block group %}
|
||||||
|
@ -5,11 +5,11 @@ SUM(IFNULL("missing"."quantity", 0)) AS "total_missing",
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block total_sets %}
|
{% block total_sets %}
|
||||||
COUNT("inventory"."u_id") - COUNT("minifigures"."u_id") AS "total_sets",
|
COUNT("inventory"."u_id") - COUNT("bricktracker_minifigures"."id") AS "total_sets",
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block total_minifigures %}
|
{% block total_minifigures %}
|
||||||
SUM(IFNULL("minifigures"."quantity", 0)) AS "total_minifigures"
|
SUM(IFNULL("bricktracker_minifigures"."quantity", 0)) AS "total_minifigures"
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block join %}
|
{% block join %}
|
||||||
@ -21,9 +21,9 @@ AND "missing"."color_id" IS NOT DISTINCT FROM "inventory"."color_id"
|
|||||||
AND "missing"."element_id" IS NOT DISTINCT FROM "inventory"."element_id"
|
AND "missing"."element_id" IS NOT DISTINCT FROM "inventory"."element_id"
|
||||||
AND "missing"."u_id" IS NOT DISTINCT FROM "inventory"."u_id"
|
AND "missing"."u_id" IS NOT DISTINCT FROM "inventory"."u_id"
|
||||||
|
|
||||||
LEFT JOIN "minifigures"
|
LEFT JOIN "bricktracker_minifigures"
|
||||||
ON "missing"."set_num" IS NOT DISTINCT FROM "minifigures"."fig_num"
|
ON "inventory"."set_num" IS NOT DISTINCT FROM "bricktracker_minifigures"."figure"
|
||||||
AND "missing"."u_id" IS NOT DISTINCT FROM "minifigures"."u_id"
|
AND "inventory"."u_id" IS NOT DISTINCT FROM "bricktracker_minifigures"."id"
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block group %}
|
{% block group %}
|
||||||
|
@ -5,11 +5,11 @@ SUM(IFNULL("missing"."quantity", 0)) AS "total_missing",
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block total_quantity %}
|
{% block total_quantity %}
|
||||||
SUM((NOT "inventory"."is_spare") * "inventory"."quantity" * IFNULL("minifigures"."quantity", 1)) AS "total_quantity",
|
SUM((NOT "inventory"."is_spare") * "inventory"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)) AS "total_quantity",
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block total_spare %}
|
{% block total_spare %}
|
||||||
SUM("inventory"."is_spare" * "inventory"."quantity" * IFNULL("minifigures"."quantity", 1)) AS "total_spare",
|
SUM("inventory"."is_spare" * "inventory"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)) AS "total_spare",
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block join %}
|
{% block join %}
|
||||||
@ -21,9 +21,9 @@ AND "inventory"."color_id" IS NOT DISTINCT FROM "missing"."color_id"
|
|||||||
AND "inventory"."element_id" IS NOT DISTINCT FROM "missing"."element_id"
|
AND "inventory"."element_id" IS NOT DISTINCT FROM "missing"."element_id"
|
||||||
AND "inventory"."u_id" IS NOT DISTINCT FROM "missing"."u_id"
|
AND "inventory"."u_id" IS NOT DISTINCT FROM "missing"."u_id"
|
||||||
|
|
||||||
LEFT JOIN "minifigures"
|
LEFT JOIN "bricktracker_minifigures"
|
||||||
ON "inventory"."set_num" IS NOT DISTINCT FROM "minifigures"."fig_num"
|
ON "inventory"."set_num" IS NOT DISTINCT FROM "bricktracker_minifigures"."figure"
|
||||||
AND "inventory"."u_id" IS NOT DISTINCT FROM "minifigures"."u_id"
|
AND "inventory"."u_id" IS NOT DISTINCT FROM "bricktracker_minifigures"."id"
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block where %}
|
{% block where %}
|
||||||
|
11
bricktracker/sql/rebrickable/minifigure/insert.sql
Normal file
11
bricktracker/sql/rebrickable/minifigure/insert.sql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
INSERT OR IGNORE INTO "rebrickable_minifigures" (
|
||||||
|
"figure",
|
||||||
|
"number",
|
||||||
|
"name",
|
||||||
|
"image"
|
||||||
|
) VALUES (
|
||||||
|
:figure,
|
||||||
|
:number,
|
||||||
|
:name,
|
||||||
|
:image
|
||||||
|
)
|
6
bricktracker/sql/rebrickable/minifigure/list.sql
Normal file
6
bricktracker/sql/rebrickable/minifigure/list.sql
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
SELECT
|
||||||
|
"rebrickable_minifigures"."figure",
|
||||||
|
"rebrickable_minifigures"."number",
|
||||||
|
"rebrickable_minifigures"."name",
|
||||||
|
"rebrickable_minifigures"."image"
|
||||||
|
FROM "rebrickable_minifigures"
|
8
bricktracker/sql/rebrickable/minifigure/select.sql
Normal file
8
bricktracker/sql/rebrickable/minifigure/select.sql
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
SELECT
|
||||||
|
"rebrickable_minifigures"."figure",
|
||||||
|
"rebrickable_minifigures"."number",
|
||||||
|
"rebrickable_minifigures"."name",
|
||||||
|
"rebrickable_minifigures"."image"
|
||||||
|
FROM "rebrickable_minifigures"
|
||||||
|
|
||||||
|
WHERE "rebrickable_minifigures"."figure" IS NOT DISTINCT FROM :figure
|
@ -1,12 +1,15 @@
|
|||||||
BEGIN transaction;
|
BEGIN transaction;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS "bricktracker_minifigures";
|
||||||
DROP TABLE IF EXISTS "bricktracker_sets";
|
DROP TABLE IF EXISTS "bricktracker_sets";
|
||||||
DROP TABLE IF EXISTS "bricktracker_set_checkboxes";
|
DROP TABLE IF EXISTS "bricktracker_set_checkboxes";
|
||||||
DROP TABLE IF EXISTS "bricktracker_set_statuses";
|
DROP TABLE IF EXISTS "bricktracker_set_statuses";
|
||||||
DROP TABLE IF EXISTS "bricktracker_wishes";
|
DROP TABLE IF EXISTS "bricktracker_wishes";
|
||||||
DROP TABLE IF EXISTS "inventory";
|
DROP TABLE IF EXISTS "inventory";
|
||||||
DROP TABLE IF EXISTS "minifigures";
|
DROP TABLE IF EXISTS "minifigures";
|
||||||
|
DROP TABLE IF EXISTS "minifigures_old";
|
||||||
DROP TABLE IF EXISTS "missing";
|
DROP TABLE IF EXISTS "missing";
|
||||||
|
DROP TABLE IF EXISTS "rebrickable_minifigures";
|
||||||
DROP TABLE IF EXISTS "rebrickable_sets";
|
DROP TABLE IF EXISTS "rebrickable_sets";
|
||||||
DROP TABLE IF EXISTS "sets";
|
DROP TABLE IF EXISTS "sets";
|
||||||
DROP TABLE IF EXISTS "sets_old";
|
DROP TABLE IF EXISTS "sets_old";
|
||||||
|
@ -21,7 +21,7 @@ SELECT
|
|||||||
FROM "bricktracker_sets"
|
FROM "bricktracker_sets"
|
||||||
|
|
||||||
INNER JOIN "rebrickable_sets"
|
INNER JOIN "rebrickable_sets"
|
||||||
ON "bricktracker_sets"."rebrickable_set" IS NOT DISTINCT FROM "rebrickable_sets"."set"
|
ON "bricktracker_sets"."set" IS NOT DISTINCT FROM "rebrickable_sets"."set"
|
||||||
|
|
||||||
{% block join %}{% endblock %}
|
{% block join %}{% endblock %}
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ IFNULL("minifigures_join"."total", 0) AS "total_minifigures"
|
|||||||
{% block join %}
|
{% block join %}
|
||||||
{% if statuses %}
|
{% if statuses %}
|
||||||
LEFT JOIN "bricktracker_set_statuses"
|
LEFT JOIN "bricktracker_set_statuses"
|
||||||
ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_statuses"."bricktracker_set_id"
|
ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_statuses"."id"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
-- LEFT JOIN + SELECT to avoid messing the total
|
-- LEFT JOIN + SELECT to avoid messing the total
|
||||||
@ -32,11 +32,11 @@ ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "missing_join"."u_id"
|
|||||||
-- LEFT JOIN + SELECT to avoid messing the total
|
-- LEFT JOIN + SELECT to avoid messing the total
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT
|
SELECT
|
||||||
"minifigures"."u_id",
|
"bricktracker_minifigures"."id",
|
||||||
SUM("minifigures"."quantity") AS "total"
|
SUM("bricktracker_minifigures"."quantity") AS "total"
|
||||||
FROM "minifigures"
|
FROM "bricktracker_minifigures"
|
||||||
{% block where_minifigures %}{% endblock %}
|
{% block where_minifigures %}{% endblock %}
|
||||||
GROUP BY "u_id"
|
GROUP BY "bricktracker_minifigures"."id"
|
||||||
) "minifigures_join"
|
) "minifigures_join"
|
||||||
ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "minifigures_join"."u_id"
|
ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "minifigures_join"."id"
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -1,6 +1,6 @@
|
|||||||
SELECT
|
SELECT
|
||||||
"bricktracker_sets"."id",
|
"bricktracker_sets"."id",
|
||||||
"bricktracker_sets"."rebrickable_set" AS "set"
|
"bricktracker_sets"."set"
|
||||||
FROM "bricktracker_sets"
|
FROM "bricktracker_sets"
|
||||||
|
|
||||||
{% block join %}{% endblock %}
|
{% block join %}{% endblock %}
|
||||||
|
@ -7,10 +7,10 @@ DELETE FROM "bricktracker_sets"
|
|||||||
WHERE "bricktracker_sets"."id" IS NOT DISTINCT FROM '{{ id }}';
|
WHERE "bricktracker_sets"."id" IS NOT DISTINCT FROM '{{ id }}';
|
||||||
|
|
||||||
DELETE FROM "bricktracker_set_statuses"
|
DELETE FROM "bricktracker_set_statuses"
|
||||||
WHERE "bricktracker_set_statuses"."bricktracker_set_id" IS NOT DISTINCT FROM '{{ id }}';
|
WHERE "bricktracker_set_statuses"."id" IS NOT DISTINCT FROM '{{ id }}';
|
||||||
|
|
||||||
DELETE FROM "minifigures"
|
DELETE FROM "bricktracker_minifigures"
|
||||||
WHERE "minifigures"."u_id" IS NOT DISTINCT FROM '{{ id }}';
|
WHERE "bricktracker_minifigures"."id" IS NOT DISTINCT FROM '{{ id }}';
|
||||||
|
|
||||||
DELETE FROM "missing"
|
DELETE FROM "missing"
|
||||||
WHERE "missing"."u_id" IS NOT DISTINCT FROM '{{ id }}';
|
WHERE "missing"."u_id" IS NOT DISTINCT FROM '{{ id }}';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
INSERT OR IGNORE INTO "bricktracker_sets" (
|
INSERT OR IGNORE INTO "bricktracker_sets" (
|
||||||
"id",
|
"id",
|
||||||
"rebrickable_set"
|
"set"
|
||||||
) VALUES (
|
) VALUES (
|
||||||
:id,
|
:id,
|
||||||
:set
|
:set
|
||||||
|
@ -2,5 +2,5 @@
|
|||||||
|
|
||||||
{% block group %}
|
{% block group %}
|
||||||
GROUP BY
|
GROUP BY
|
||||||
"bricktracker_sets"."rebrickable_set"
|
"bricktracker_sets"."set"
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -6,7 +6,7 @@ WHERE "bricktracker_sets"."id" IN (
|
|||||||
"missing"."u_id"
|
"missing"."u_id"
|
||||||
FROM "missing"
|
FROM "missing"
|
||||||
|
|
||||||
WHERE "missing"."set_num" IS NOT DISTINCT FROM :fig_num
|
WHERE "missing"."set_num" IS NOT DISTINCT FROM :figure
|
||||||
|
|
||||||
GROUP BY "missing"."u_id"
|
GROUP BY "missing"."u_id"
|
||||||
)
|
)
|
||||||
|
@ -6,7 +6,7 @@ WHERE "bricktracker_sets"."id" IN (
|
|||||||
"inventory"."u_id"
|
"inventory"."u_id"
|
||||||
FROM "inventory"
|
FROM "inventory"
|
||||||
|
|
||||||
WHERE "inventory"."set_num" IS NOT DISTINCT FROM :fig_num
|
WHERE "inventory"."set_num" IS NOT DISTINCT FROM :figure
|
||||||
|
|
||||||
GROUP BY "inventory"."u_id"
|
GROUP BY "inventory"."u_id"
|
||||||
)
|
)
|
||||||
|
@ -5,7 +5,7 @@ WHERE "missing"."u_id" IS NOT DISTINCT FROM :id
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block where_minifigures %}
|
{% block where_minifigures %}
|
||||||
WHERE "minifigures"."u_id" IS NOT DISTINCT FROM :id
|
WHERE "bricktracker_minifigures"."id" IS NOT DISTINCT FROM :id
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block where %}
|
{% block where %}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
INSERT INTO "bricktracker_set_statuses" (
|
INSERT INTO "bricktracker_set_statuses" (
|
||||||
"bricktracker_set_id",
|
"id",
|
||||||
"{{name}}"
|
"{{name}}"
|
||||||
) VALUES (
|
) VALUES (
|
||||||
:id,
|
:id,
|
||||||
:status
|
:status
|
||||||
)
|
)
|
||||||
ON CONFLICT("bricktracker_set_id")
|
ON CONFLICT("id")
|
||||||
DO UPDATE SET "{{name}}" = :status
|
DO UPDATE SET "{{name}}" = :status
|
||||||
WHERE "bricktracker_set_statuses"."bricktracker_set_id" IS NOT DISTINCT FROM :id
|
WHERE "bricktracker_set_statuses"."id" IS NOT DISTINCT FROM :id
|
||||||
|
@ -2,13 +2,16 @@ from typing import Tuple
|
|||||||
|
|
||||||
# Some table aliases to make it look cleaner (id: (name, icon))
|
# Some table aliases to make it look cleaner (id: (name, icon))
|
||||||
ALIASES: dict[str, Tuple[str, str]] = {
|
ALIASES: dict[str, Tuple[str, str]] = {
|
||||||
'bricktracker_set_checkboxes': ('Checkboxes', 'checkbox-line'),
|
'bricktracker_minifigures': ('Bricktracker minifigures', 'group-line'),
|
||||||
|
'bricktracker_set_checkboxes': ('Bricktracker set checkboxes', 'checkbox-line'), # noqa: E501
|
||||||
'bricktracker_set_statuses': ('Bricktracker sets status', 'checkbox-line'),
|
'bricktracker_set_statuses': ('Bricktracker sets status', 'checkbox-line'),
|
||||||
'bricktracker_sets': ('Bricktracker sets', 'hashtag'),
|
'bricktracker_sets': ('Bricktracker sets', 'hashtag'),
|
||||||
'bricktracker_wishes': ('Bricktracker wishes', 'gift-line'),
|
'bricktracker_wishes': ('Bricktracker wishes', 'gift-line'),
|
||||||
'inventory': ('Parts', 'shapes-line'),
|
'inventory': ('Parts', 'shapes-line'),
|
||||||
'minifigures': ('Minifigures', 'group-line'),
|
'minifigures': ('Minifigures', 'group-line'),
|
||||||
|
'minifigures_old': ('Minifigures (legacy)', 'group-line'),
|
||||||
'missing': ('Missing', 'error-warning-line'),
|
'missing': ('Missing', 'error-warning-line'),
|
||||||
|
'rebrickable_minifigures': ('Rebrickable minifigures', 'group-line'),
|
||||||
'rebrickable_sets': ('Rebrickable sets', 'hashtag'),
|
'rebrickable_sets': ('Rebrickable sets', 'hashtag'),
|
||||||
'sets': ('Sets', 'hashtag'),
|
'sets': ('Sets', 'hashtag'),
|
||||||
'sets_old': ('Sets (legacy)', 'hashtag'),
|
'sets_old': ('Sets (legacy)', 'hashtag'),
|
||||||
@ -22,6 +25,7 @@ class BrickCounter(object):
|
|||||||
table: str
|
table: str
|
||||||
icon: str
|
icon: str
|
||||||
count: int
|
count: int
|
||||||
|
legacy: bool
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -44,3 +48,5 @@ class BrickCounter(object):
|
|||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
self.icon = icon
|
self.icon = icon
|
||||||
|
|
||||||
|
self.legacy = '(legacy)' in self.name
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
__version__: Final[str] = '1.1.1'
|
__version__: Final[str] = '1.2.0'
|
||||||
__database_version__: Final[int] = 6
|
__database_version__: Final[int] = 9
|
||||||
|
@ -19,12 +19,12 @@ def list() -> str:
|
|||||||
|
|
||||||
|
|
||||||
# Minifigure details
|
# Minifigure details
|
||||||
@minifigure_page.route('/<number>/details')
|
@minifigure_page.route('/<figure>/details')
|
||||||
@exception_handler(__file__)
|
@exception_handler(__file__)
|
||||||
def details(*, number: str) -> str:
|
def details(*, figure: str) -> str:
|
||||||
return render_template(
|
return render_template(
|
||||||
'minifigure.html',
|
'minifigure.html',
|
||||||
item=BrickMinifigure().select_generic(number),
|
item=BrickMinifigure().select_generic(figure),
|
||||||
using=BrickSetList().using_minifigure(number),
|
using=BrickSetList().using_minifigure(figure),
|
||||||
missing=BrickSetList().missing_minifigure(number),
|
missing=BrickSetList().missing_minifigure(figure),
|
||||||
)
|
)
|
||||||
|
@ -47,8 +47,8 @@ def update_status(*, id: str, checkbox_id: str) -> Response:
|
|||||||
brickset.update_status(checkbox, value)
|
brickset.update_status(checkbox, value)
|
||||||
|
|
||||||
# Info
|
# Info
|
||||||
logger.info('Set {number} ({id}): status "{status}" changed to "{state}"'.format( # noqa: E501
|
logger.info('Set {set} ({id}): status "{status}" changed to "{state}"'.format( # noqa: E501
|
||||||
number=brickset.fields.set,
|
set=brickset.fields.set,
|
||||||
id=brickset.fields.id,
|
id=brickset.fields.id,
|
||||||
status=checkbox.fields.name,
|
status=checkbox.fields.name,
|
||||||
state=value,
|
state=value,
|
||||||
@ -77,8 +77,8 @@ def do_delete(*, id: str) -> Response:
|
|||||||
brickset.delete()
|
brickset.delete()
|
||||||
|
|
||||||
# Info
|
# Info
|
||||||
logger.info('Set {number} ({id}): deleted'.format(
|
logger.info('Set {set} ({id}): deleted'.format(
|
||||||
number=brickset.fields.set,
|
set=brickset.fields.set,
|
||||||
id=brickset.fields.id,
|
id=brickset.fields.id,
|
||||||
))
|
))
|
||||||
|
|
||||||
@ -108,33 +108,28 @@ def details(*, id: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
# Update the missing pieces of a minifig part
|
# Update the missing pieces of a minifig part
|
||||||
@set_page.route('/<id>/minifigures/<minifigure_id>/parts/<part_id>/missing', methods=['POST']) # noqa: E501
|
@set_page.route('/<id>/minifigures/<figure>/parts/<part>/missing', methods=['POST']) # noqa: E501
|
||||||
@login_required
|
@login_required
|
||||||
@exception_handler(__file__, json=True)
|
@exception_handler(__file__, json=True)
|
||||||
def missing_minifigure_part(
|
def missing_minifigure_part(*, id: str, figure: str, part: str) -> Response:
|
||||||
*,
|
|
||||||
id: str,
|
|
||||||
minifigure_id: str,
|
|
||||||
part_id: str
|
|
||||||
) -> Response:
|
|
||||||
brickset = BrickSet().select_specific(id)
|
brickset = BrickSet().select_specific(id)
|
||||||
minifigure = BrickMinifigure().select_specific(brickset, minifigure_id)
|
brickminifigure = BrickMinifigure().select_specific(brickset, figure)
|
||||||
part = BrickPart().select_specific(
|
brickpart = BrickPart().select_specific(
|
||||||
brickset,
|
brickset,
|
||||||
part_id,
|
part,
|
||||||
minifigure=minifigure,
|
minifigure=brickminifigure,
|
||||||
)
|
)
|
||||||
|
|
||||||
missing = request.json.get('missing', '') # type: ignore
|
missing = request.json.get('missing', '') # type: ignore
|
||||||
|
|
||||||
part.update_missing(missing)
|
brickpart.update_missing(missing)
|
||||||
|
|
||||||
# Info
|
# Info
|
||||||
logger.info('Set {number} ({id}): updated minifigure ({minifigure}) part ({part}) missing count to {missing}'.format( # noqa: E501
|
logger.info('Set {set} ({id}): updated minifigure ({figure}) part ({part}) missing count to {missing}'.format( # noqa: E501
|
||||||
number=brickset.fields.set,
|
set=brickset.fields.set,
|
||||||
id=brickset.fields.id,
|
id=brickset.fields.id,
|
||||||
minifigure=minifigure.fields.fig_num,
|
figure=brickminifigure.fields.figure,
|
||||||
part=part.fields.id,
|
part=brickpart.fields.id,
|
||||||
missing=missing,
|
missing=missing,
|
||||||
))
|
))
|
||||||
|
|
||||||
@ -142,22 +137,22 @@ def missing_minifigure_part(
|
|||||||
|
|
||||||
|
|
||||||
# Update the missing pieces of a part
|
# Update the missing pieces of a part
|
||||||
@set_page.route('/<id>/parts/<part_id>/missing', methods=['POST'])
|
@set_page.route('/<id>/parts/<part>/missing', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@exception_handler(__file__, json=True)
|
@exception_handler(__file__, json=True)
|
||||||
def missing_part(*, id: str, part_id: str) -> Response:
|
def missing_part(*, id: str, part: str) -> Response:
|
||||||
brickset = BrickSet().select_specific(id)
|
brickset = BrickSet().select_specific(id)
|
||||||
part = BrickPart().select_specific(brickset, part_id)
|
brickpart = BrickPart().select_specific(brickset, part)
|
||||||
|
|
||||||
missing = request.json.get('missing', '') # type: ignore
|
missing = request.json.get('missing', '') # type: ignore
|
||||||
|
|
||||||
part.update_missing(missing)
|
brickpart.update_missing(missing)
|
||||||
|
|
||||||
# Info
|
# Info
|
||||||
logger.info('Set {number} ({id}): updated part ({part}) missing count to {missing}'.format( # noqa: E501
|
logger.info('Set {set} ({id}): updated part ({part}) missing count to {missing}'.format( # noqa: E501
|
||||||
number=brickset.fields.set,
|
set=brickset.fields.set,
|
||||||
id=brickset.fields.id,
|
id=brickset.fields.id,
|
||||||
part=part.fields.id,
|
part=brickpart.fields.id,
|
||||||
missing=missing,
|
missing=missing,
|
||||||
))
|
))
|
||||||
|
|
||||||
|
@ -31,8 +31,8 @@ class BrickWish(RebrickableSet):
|
|||||||
# Load from database
|
# Load from database
|
||||||
if not self.select():
|
if not self.select():
|
||||||
raise NotFoundException(
|
raise NotFoundException(
|
||||||
'Wish with number {number} was not found in the database'.format( # noqa: E501
|
'Wish for set {set} was not found in the database'.format( # noqa: E501
|
||||||
number=self.fields.set,
|
set=self.fields.set,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ It uses the following Python/pip packages:
|
|||||||
It also uses the following libraries and frameworks:
|
It also uses the following libraries and frameworks:
|
||||||
|
|
||||||
- Boostrap (https://getbootstrap.com/)
|
- Boostrap (https://getbootstrap.com/)
|
||||||
|
- Remixicon (https://remixicon.com/)
|
||||||
- `baguettebox` (https://github.com/feimosi/baguetteBox.js)
|
- `baguettebox` (https://github.com/feimosi/baguetteBox.js)
|
||||||
- `tinysort` (https://github.com/Sjeiti/TinySort)
|
- `tinysort` (https://github.com/Sjeiti/TinySort)
|
||||||
- `sortable` (https://github.com/tofsjonas/sortable)
|
- `sortable` (https://github.com/tofsjonas/sortable)
|
||||||
|
@ -11,15 +11,9 @@ class BrickInstructionsSocket extends BrickSocket {
|
|||||||
this.html_files = document.getElementById(`${id}-files`);
|
this.html_files = document.getElementById(`${id}-files`);
|
||||||
|
|
||||||
if (this.html_button) {
|
if (this.html_button) {
|
||||||
this.download_listener = ((bricksocket) => (e) => {
|
this.download_listener = this.html_button.addEventListener("click", ((bricksocket) => (e) => {
|
||||||
if (!bricksocket.disabled && bricksocket.socket !== undefined && bricksocket.socket.connected) {
|
bricksocket.execute();
|
||||||
bricksocket.toggle(false);
|
})(this));
|
||||||
|
|
||||||
bricksocket.download_instructions();
|
|
||||||
}
|
|
||||||
})(this);
|
|
||||||
|
|
||||||
this.html_button.addEventListener("click", this.download_listener);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.html_card_dismiss && this.html_card) {
|
if (this.html_card_dismiss && this.html_card) {
|
||||||
@ -43,6 +37,15 @@ class BrickInstructionsSocket extends BrickSocket {
|
|||||||
this.download_instructions(true);
|
this.download_instructions(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Execute the action
|
||||||
|
execute() {
|
||||||
|
if (!this.disabled && this.socket !== undefined && this.socket.connected) {
|
||||||
|
this.toggle(false);
|
||||||
|
|
||||||
|
this.download_instructions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get the list of checkboxes describing files
|
// Get the list of checkboxes describing files
|
||||||
get_files(checked=false) {
|
get_files(checked=false) {
|
||||||
let files = [];
|
let files = [];
|
||||||
|
@ -5,6 +5,7 @@ class BrickSetSocket extends BrickSocket {
|
|||||||
|
|
||||||
// Listeners
|
// Listeners
|
||||||
this.add_listener = undefined;
|
this.add_listener = undefined;
|
||||||
|
this.input_listener = undefined;
|
||||||
this.confirm_listener = undefined;
|
this.confirm_listener = undefined;
|
||||||
|
|
||||||
// Form elements (built based on the initial id)
|
// Form elements (built based on the initial id)
|
||||||
@ -23,24 +24,15 @@ class BrickSetSocket extends BrickSocket {
|
|||||||
this.html_card_dismiss = document.getElementById(`${id}-card-dismiss`);
|
this.html_card_dismiss = document.getElementById(`${id}-card-dismiss`);
|
||||||
|
|
||||||
if (this.html_button) {
|
if (this.html_button) {
|
||||||
this.add_listener = ((bricksocket) => (e) => {
|
this.add_listener = this.html_button.addEventListener("click", ((bricksocket) => (e) => {
|
||||||
if (!bricksocket.disabled && bricksocket.socket !== undefined && bricksocket.socket.connected) {
|
bricksocket.execute();
|
||||||
bricksocket.toggle(false);
|
})(this));
|
||||||
|
|
||||||
// Split and save the list if bulk
|
this.input_listener = this.html_input.addEventListener("keyup", ((bricksocket) => (e) => {
|
||||||
if (bricksocket.bulk) {
|
if (e.key === 'Enter') {
|
||||||
bricksocket.read_set_list()
|
bricksocket.execute();
|
||||||
}
|
|
||||||
|
|
||||||
if (bricksocket.bulk || (bricksocket.html_no_confim && bricksocket.html_no_confim.checked)) {
|
|
||||||
bricksocket.import_set(true);
|
|
||||||
} else {
|
|
||||||
bricksocket.load_set();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})(this);
|
})(this))
|
||||||
|
|
||||||
this.html_button.addEventListener("click", this.add_listener);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.html_card_dismiss && this.html_card) {
|
if (this.html_card_dismiss && this.html_card) {
|
||||||
@ -80,6 +72,24 @@ class BrickSetSocket extends BrickSocket {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Execute the action
|
||||||
|
execute() {
|
||||||
|
if (!this.disabled && this.socket !== undefined && this.socket.connected) {
|
||||||
|
this.toggle(false);
|
||||||
|
|
||||||
|
// Split and save the list if bulk
|
||||||
|
if (this.bulk) {
|
||||||
|
this.read_set_list();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.bulk || (this.html_no_confim && this.html_no_confim.checked)) {
|
||||||
|
this.import_set(true);
|
||||||
|
} else {
|
||||||
|
this.load_set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Upon receiving a fail message
|
// Upon receiving a fail message
|
||||||
fail(data) {
|
fail(data) {
|
||||||
super.fail(data);
|
super.fail(data);
|
||||||
|
@ -22,8 +22,8 @@
|
|||||||
<div class="d-flex justify-content-start">
|
<div class="d-flex justify-content-start">
|
||||||
<ul class="list-group me-2">
|
<ul class="list-group me-2">
|
||||||
{% for counter in database_counters %}
|
{% for counter in database_counters %}
|
||||||
<li class="list-group-item d-flex justify-content-between align-items-start">
|
<li class="list-group-item d-flex justify-content-between align-items-start {% if counter.legacy %}list-group-item-dark{% endif %}">
|
||||||
<span><i class="ri-{{ counter.icon }}"></i> {{ counter.name }}</span> <span class="badge text-bg-primary rounded-pill ms-2">{{ counter.count }}</span>
|
<span><i class="ri-{{ counter.icon }}"></i> {{ counter.name }}</span> <span class="badge {% if counter.legacy %}text-bg-light border{% else %}text-bg-primary{% endif %} rounded-pill ms-2">{{ counter.count }}</span>
|
||||||
</li>
|
</li>
|
||||||
{% if not (loop.index % 5) %}
|
{% if not (loop.index % 5) %}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -3,11 +3,11 @@
|
|||||||
{% import 'macro/card.html' as card %}
|
{% import 'macro/card.html' as card %}
|
||||||
|
|
||||||
<div class="card mb-3 flex-fill {% if solo %}card-solo{% endif %}">
|
<div class="card mb-3 flex-fill {% if solo %}card-solo{% endif %}">
|
||||||
{{ card.header(item, item.fields.name, solo=solo, number=item.clean_number(), icon='user-line') }}
|
{{ card.header(item, item.fields.name, solo=solo, number=item.fields.number, icon='user-line') }}
|
||||||
{{ card.image(item, solo=solo, last=last, caption=item.fields.name, alt=item.fields.fig_num, medium=true) }}
|
{{ card.image(item, solo=solo, last=last, caption=item.fields.name, alt=item.fields.figure, medium=true) }}
|
||||||
<div class="card-body border-bottom {% if not solo %}p-1{% endif %}">
|
<div class="card-body border-bottom {% if not solo %}p-1{% endif %}">
|
||||||
{% if last %}
|
{% if last %}
|
||||||
{{ badge.set(item.fields.set_num, solo=solo, last=last, id=item.fields.u_id) }}
|
{{ badge.set(item.fields.set, solo=solo, last=last, id=item.fields.rebrickable_set_id) }}
|
||||||
{{ badge.quantity(item.fields.quantity, solo=solo, last=last) }}
|
{{ badge.quantity(item.fields.quantity, solo=solo, last=last) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ badge.quantity(item.fields.total_quantity, solo=solo, last=last) }}
|
{{ badge.quantity(item.fields.total_quantity, solo=solo, last=last) }}
|
||||||
@ -19,7 +19,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% if solo %}
|
{% if solo %}
|
||||||
<div class="accordion accordion-flush" id="minifigure-details">
|
<div class="accordion accordion-flush" id="minifigure-details">
|
||||||
{{ accordion.table(item.generic_parts(), 'Parts', item.fields.fig_num, 'minifigure-details', 'part/table.html', icon='shapes-line', alt=item.fields.fig_num, read_only_missing=read_only_missing)}}
|
{{ accordion.table(item.generic_parts(), 'Parts', item.fields.figure, 'minifigure-details', 'part/table.html', icon='shapes-line', alt=item.fields.figure, read_only_missing=read_only_missing)}}
|
||||||
{{ accordion.cards(using, 'Sets using this minifigure', 'using-inventory', 'minifigure-details', 'set/card.html', icon='hashtag') }}
|
{{ accordion.cards(using, 'Sets using this minifigure', 'using-inventory', 'minifigure-details', 'set/card.html', icon='hashtag') }}
|
||||||
{{ accordion.cards(missing, 'Sets missing parts of this minifigure', 'missing-inventory', 'minifigure-details', 'set/card.html', icon='error-warning-line') }}
|
{{ accordion.cards(missing, 'Sets missing parts of this minifigure', 'missing-inventory', 'minifigure-details', 'set/card.html', icon='error-warning-line') }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for item in table_collection %}
|
{% for item in table_collection %}
|
||||||
<tr>
|
<tr>
|
||||||
{{ table.image(item.url_for_image(), caption=item.fields.name, alt=item.fields.fig_num) }}
|
{{ table.image(item.url_for_image(), caption=item.fields.name, alt=item.fields.figure) }}
|
||||||
<td >
|
<td >
|
||||||
<a class="text-reset" href="{{ item.url() }}" style="max-width:auto">{{ item.fields.name }}</a>
|
<a class="text-reset" href="{{ item.url() }}" style="max-width:auto">{{ item.fields.name }}</a>
|
||||||
{% if all %}
|
{% if all %}
|
||||||
|
@ -38,26 +38,28 @@
|
|||||||
{% if solo %}
|
{% if solo %}
|
||||||
<div class="accordion accordion-flush border-top" id="set-details">
|
<div class="accordion accordion-flush border-top" id="set-details">
|
||||||
{% if not delete %}
|
{% if not delete %}
|
||||||
{{ accordion.header('Instructions', 'instructions', 'set-details', expanded=open_instructions, quantity=item.instructions | length, icon='file-line', class='p-0') }}
|
{% if not config['HIDE_SET_INSTRUCTIONS'] %}
|
||||||
<div class="list-group list-group-flush">
|
{{ accordion.header('Instructions', 'instructions', 'set-details', expanded=open_instructions, quantity=item.instructions | length, icon='file-line', class='p-0') }}
|
||||||
{% if item.instructions | length %}
|
<div class="list-group list-group-flush">
|
||||||
{% for instruction in item.instructions %}
|
{% if item.instructions | length %}
|
||||||
<a class="list-group-item list-group-item-action" href="{{ instruction.url() }}" target="_blank"><i class="ri-arrow-right-long-line"></i> <i class="ri-{{ instruction.icon() }}"></i> {{ instruction.filename }}</a>
|
{% for instruction in item.instructions %}
|
||||||
{% endfor %}
|
<a class="list-group-item list-group-item-action" href="{{ instruction.url() }}" target="_blank"><i class="ri-arrow-right-long-line"></i> <i class="ri-{{ instruction.icon() }}"></i> {{ instruction.filename }}</a>
|
||||||
{% else %}
|
{% endfor %}
|
||||||
<span class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No instructions file found.</span>
|
{% else %}
|
||||||
{% if g.login.is_authenticated() %}
|
<span class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No instructions file found.</span>
|
||||||
<a class="list-group-item list-group-item-action" href="{{ url_for('instructions.upload') }}"><i class="ri-upload-line"></i> Upload an instructions file</a>
|
{% if g.login.is_authenticated() %}
|
||||||
|
<a class="list-group-item list-group-item-action" href="{{ url_for('instructions.upload') }}"><i class="ri-upload-line"></i> Upload an instructions file</a>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% if g.login.is_authenticated() %}
|
||||||
{% if g.login.is_authenticated() %}
|
<a class="list-group-item list-group-item-action" href="{{ url_for('instructions.download', set=item.fields.set) }}"><i class="ri-download-line"></i> Download instructions from Rebrickable</a>
|
||||||
<a class="list-group-item list-group-item-action" href="{{ url_for('instructions.download', set=item.fields.set) }}"><i class="ri-download-line"></i> Download instructions from Rebrickable</a>
|
{% endif %}
|
||||||
{% endif %}
|
</div>
|
||||||
</div>
|
{{ accordion.footer() }}
|
||||||
{{ accordion.footer() }}
|
{% endif %}
|
||||||
{{ accordion.table(item.parts(), 'Parts', 'parts-inventory', 'set-details', 'part/table.html', icon='shapes-line')}}
|
{{ accordion.table(item.parts(), 'Parts', 'parts-inventory', 'set-details', 'part/table.html', icon='shapes-line')}}
|
||||||
{% for minifigure in item.minifigures() %}
|
{% for minifigure in item.minifigures() %}
|
||||||
{{ accordion.table(minifigure.parts(), minifigure.fields.name, minifigure.fields.fig_num, 'set-details', 'part/table.html', quantity=minifigure.fields.quantity, icon='group-line', image=minifigure.url_for_image(), alt=minifigure.fields.fig_num, details=minifigure.url())}}
|
{{ accordion.table(minifigure.parts(), minifigure.fields.name, minifigure.fields.figure, 'set-details', 'part/table.html', quantity=minifigure.fields.quantity, icon='group-line', image=minifigure.url_for_image(), alt=minifigure.fields.figure, details=minifigure.url())}}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if g.login.is_authenticated() %}
|
{% if g.login.is_authenticated() %}
|
||||||
|
Loading…
Reference in New Issue
Block a user