forked from FrederikBaerentsen/BrickTracker
feat(views): update existing models to support individual items integration
This commit is contained in:
@@ -20,8 +20,8 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
|
|||||||
order: str
|
order: str
|
||||||
|
|
||||||
# Queries
|
# Queries
|
||||||
all_query: str = 'minifigure/list/all'
|
all_query: str = 'minifigure/list/all_unified'
|
||||||
all_by_owner_query: str = 'minifigure/list/all_by_owner'
|
all_by_owner_query: str = 'minifigure/list/all_by_owner_unified'
|
||||||
damaged_part_query: str = 'minifigure/list/damaged_part'
|
damaged_part_query: str = 'minifigure/list/damaged_part'
|
||||||
last_query: str = 'minifigure/list/last'
|
last_query: str = 'minifigure/list/last'
|
||||||
missing_part_query: str = 'minifigure/list/missing_part'
|
missing_part_query: str = 'minifigure/list/missing_part'
|
||||||
@@ -44,7 +44,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
# Load all minifigures with problems filter
|
# Load all minifigures with problems filter
|
||||||
def all_filtered(self, /, owner_id: str | None = None, problems_filter: str = 'all', theme_id: str = 'all', year: str = 'all') -> Self:
|
def all_filtered(self, /, owner_id: str | None = None, problems_filter: str = 'all', theme_id: str = 'all', year: str = 'all', individuals_filter: str = 'all') -> Self:
|
||||||
# Save the owner_id parameter
|
# Save the owner_id parameter
|
||||||
if owner_id is not None:
|
if owner_id is not None:
|
||||||
self.fields.owner_id = owner_id
|
self.fields.owner_id = owner_id
|
||||||
@@ -56,6 +56,8 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
|
|||||||
context['theme_id'] = theme_id
|
context['theme_id'] = theme_id
|
||||||
if year and year != 'all':
|
if year and year != 'all':
|
||||||
context['year'] = year
|
context['year'] = year
|
||||||
|
if individuals_filter and individuals_filter != 'all':
|
||||||
|
context['individuals_filter'] = individuals_filter
|
||||||
|
|
||||||
# Choose query based on whether owner filtering is needed
|
# Choose query based on whether owner filtering is needed
|
||||||
if owner_id and owner_id != 'all':
|
if owner_id and owner_id != 'all':
|
||||||
@@ -77,7 +79,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
# Load all minifigures by owner with problems filter
|
# Load all minifigures by owner with problems filter
|
||||||
def all_by_owner_filtered(self, /, owner_id: str | None = None, problems_filter: str = 'all', theme_id: str = 'all', year: str = 'all') -> Self:
|
def all_by_owner_filtered(self, /, owner_id: str | None = None, problems_filter: str = 'all', theme_id: str = 'all', year: str = 'all', individuals_filter: str = 'all') -> Self:
|
||||||
# Save the owner_id parameter
|
# Save the owner_id parameter
|
||||||
self.fields.owner_id = owner_id
|
self.fields.owner_id = owner_id
|
||||||
|
|
||||||
@@ -88,6 +90,8 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
|
|||||||
context['theme_id'] = theme_id
|
context['theme_id'] = theme_id
|
||||||
if year and year != 'all':
|
if year and year != 'all':
|
||||||
context['year'] = year
|
context['year'] = year
|
||||||
|
if individuals_filter and individuals_filter != 'all':
|
||||||
|
context['individuals_filter'] = individuals_filter
|
||||||
|
|
||||||
# Load the minifigures from the database
|
# Load the minifigures from the database
|
||||||
self.list(override_query=self.all_by_owner_query, **context)
|
self.list(override_query=self.all_by_owner_query, **context)
|
||||||
@@ -101,6 +105,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
|
|||||||
problems_filter: str = 'all',
|
problems_filter: str = 'all',
|
||||||
theme_id: str = 'all',
|
theme_id: str = 'all',
|
||||||
year: str = 'all',
|
year: str = 'all',
|
||||||
|
individuals_filter: str = 'all',
|
||||||
search_query: str | None = None,
|
search_query: str | None = None,
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
per_page: int = 50,
|
per_page: int = 50,
|
||||||
@@ -127,10 +132,13 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
|
|||||||
if year and year != 'all':
|
if year and year != 'all':
|
||||||
filter_context['year'] = year
|
filter_context['year'] = year
|
||||||
|
|
||||||
# Field mapping for sorting
|
if individuals_filter and individuals_filter != 'all':
|
||||||
|
filter_context['individuals_filter'] = individuals_filter
|
||||||
|
|
||||||
|
# Field mapping for sorting (using column names from the unified query)
|
||||||
field_mapping = {
|
field_mapping = {
|
||||||
'name': '"rebrickable_minifigures"."name"',
|
'name': '"name"',
|
||||||
'parts': '"rebrickable_minifigures"."number_of_parts"',
|
'parts': '"number_of_parts"',
|
||||||
'quantity': '"total_quantity"',
|
'quantity': '"total_quantity"',
|
||||||
'missing': '"total_missing"',
|
'missing': '"total_missing"',
|
||||||
'damaged': '"total_damaged"',
|
'damaged': '"total_damaged"',
|
||||||
|
|||||||
+28
-2
@@ -17,7 +17,7 @@ def parse_set(set: str, /) -> str:
|
|||||||
if version == '':
|
if version == '':
|
||||||
version = '1'
|
version = '1'
|
||||||
|
|
||||||
# Version must be a positive integer
|
# Version must be a valid number (but preserve leading zeros for minifigures)
|
||||||
try:
|
try:
|
||||||
version_int = int(version)
|
version_int = int(version)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -30,4 +30,30 @@ def parse_set(set: str, /) -> str:
|
|||||||
version=version,
|
version=version,
|
||||||
))
|
))
|
||||||
|
|
||||||
return '{number}-{version}'.format(number=number, version=version_int)
|
# Preserve original version string to keep leading zeros (important for minifigures like fig-000484)
|
||||||
|
return '{number}-{version}'.format(number=number, version=version)
|
||||||
|
|
||||||
|
|
||||||
|
# Make sense of string supposed to contain a minifigure ID
|
||||||
|
def parse_minifig(figure: str, /) -> str:
|
||||||
|
# Minifigure format is typically fig-XXXXXX
|
||||||
|
# We'll accept with or without the 'fig-' prefix
|
||||||
|
figure = figure.strip()
|
||||||
|
|
||||||
|
if not figure.startswith('fig-'):
|
||||||
|
# Try to add the prefix if it's just numbers
|
||||||
|
if figure.isdigit():
|
||||||
|
figure = 'fig-{figure}'.format(figure=figure.zfill(6))
|
||||||
|
else:
|
||||||
|
raise ErrorException('Minifigure "{figure}" must start with "fig-"'.format(
|
||||||
|
figure=figure,
|
||||||
|
))
|
||||||
|
|
||||||
|
# Validate format: fig-XXXXXX where X can be digits or letters
|
||||||
|
parts = figure.split('-')
|
||||||
|
if len(parts) != 2 or parts[0] != 'fig':
|
||||||
|
raise ErrorException('Invalid minifigure format "{figure}". Expected format: fig-XXXXXX'.format(
|
||||||
|
figure=figure,
|
||||||
|
))
|
||||||
|
|
||||||
|
return figure
|
||||||
|
|||||||
+142
-27
@@ -9,6 +9,7 @@ from .exceptions import ErrorException, NotFoundException
|
|||||||
from .rebrickable_part import RebrickablePart
|
from .rebrickable_part import RebrickablePart
|
||||||
from .sql import BrickSQL
|
from .sql import BrickSQL
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from .individual_minifigure import IndividualMinifigure
|
||||||
from .minifigure import BrickMinifigure
|
from .minifigure import BrickMinifigure
|
||||||
from .set import BrickSet
|
from .set import BrickSet
|
||||||
from .socket import BrickSocket
|
from .socket import BrickSocket
|
||||||
@@ -33,6 +34,7 @@ class BrickPart(RebrickablePart):
|
|||||||
*,
|
*,
|
||||||
brickset: 'BrickSet | None' = None,
|
brickset: 'BrickSet | None' = None,
|
||||||
minifigure: 'BrickMinifigure | None' = None,
|
minifigure: 'BrickMinifigure | None' = None,
|
||||||
|
individual_minifigure: 'IndividualMinifigure | None' = None,
|
||||||
record: Row | dict[str, Any] | None = None
|
record: Row | dict[str, Any] | None = None
|
||||||
):
|
):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -41,7 +43,12 @@ class BrickPart(RebrickablePart):
|
|||||||
record=record
|
record=record
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.minifigure is not None:
|
self.individual_minifigure = individual_minifigure
|
||||||
|
|
||||||
|
if self.individual_minifigure is not None:
|
||||||
|
self.identifier = self.individual_minifigure.fields.id
|
||||||
|
self.kind = 'Individual Minifigure'
|
||||||
|
elif self.minifigure is not None:
|
||||||
self.identifier = self.minifigure.fields.figure
|
self.identifier = self.minifigure.fields.figure
|
||||||
self.kind = 'Minifigure'
|
self.kind = 'Minifigure'
|
||||||
elif self.brickset is not None:
|
elif self.brickset is not None:
|
||||||
@@ -182,6 +189,33 @@ class BrickPart(RebrickablePart):
|
|||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
# Select a specific part from an individual minifigure instance
|
||||||
|
def select_specific_individual_minifigure(
|
||||||
|
self,
|
||||||
|
individual_minifigure: 'IndividualMinifigure',
|
||||||
|
part: str,
|
||||||
|
color: int,
|
||||||
|
spare: int,
|
||||||
|
/,
|
||||||
|
) -> Self:
|
||||||
|
# Save the parameters to the fields
|
||||||
|
self.individual_minifigure = individual_minifigure
|
||||||
|
self.fields.part = part
|
||||||
|
self.fields.color = color
|
||||||
|
self.fields.spare = spare
|
||||||
|
|
||||||
|
if not self.select(override_query='individual_minifigure/part/select/specific'):
|
||||||
|
raise NotFoundException(
|
||||||
|
'Part {part} with color {color} (spare: {spare}) from individual minifigure {id} was not found in the database'.format( # noqa: E501
|
||||||
|
part=self.fields.part,
|
||||||
|
color=self.fields.color,
|
||||||
|
spare=self.fields.spare,
|
||||||
|
id=individual_minifigure.fields.id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
# Update checked state for part walkthrough
|
# Update checked state for part walkthrough
|
||||||
def update_checked(self, json: Any | None, /) -> bool:
|
def update_checked(self, json: Any | None, /) -> bool:
|
||||||
# Handle both direct 'checked' key and changer.js 'value' key format
|
# Handle both direct 'checked' key and changer.js 'value' key format
|
||||||
@@ -202,22 +236,56 @@ class BrickPart(RebrickablePart):
|
|||||||
|
|
||||||
return checked
|
return checked
|
||||||
|
|
||||||
|
# Update checked state for individual minifigure part
|
||||||
|
def update_checked_individual_minifigure(self, json: Any | None, /) -> bool:
|
||||||
|
# Handle both direct 'checked' key and changer.js 'value' key format
|
||||||
|
if json:
|
||||||
|
checked = json.get('checked', json.get('value', False))
|
||||||
|
else:
|
||||||
|
checked = False
|
||||||
|
|
||||||
|
checked = bool(checked)
|
||||||
|
|
||||||
|
self.fields.checked = checked
|
||||||
|
|
||||||
|
BrickSQL().execute_and_commit(
|
||||||
|
'individual_minifigure/part/update/checked',
|
||||||
|
parameters=self.sql_parameters()
|
||||||
|
)
|
||||||
|
|
||||||
|
return checked
|
||||||
|
|
||||||
# Compute the url for updating checked state
|
# Compute the url for updating checked state
|
||||||
def url_for_checked(self, /) -> str:
|
def url_for_checked(self, /) -> str:
|
||||||
# Different URL for a minifigure part
|
# Different URL for individual minifigure part
|
||||||
if self.minifigure is not None:
|
if self.individual_minifigure is not None:
|
||||||
figure = self.minifigure.fields.figure
|
return url_for(
|
||||||
|
'individual_minifigure.checked_part',
|
||||||
|
id=self.individual_minifigure.fields.id,
|
||||||
|
part=self.fields.part,
|
||||||
|
color=self.fields.color,
|
||||||
|
spare=self.fields.spare,
|
||||||
|
)
|
||||||
|
# Different URL for a set minifigure part
|
||||||
|
elif self.minifigure is not None:
|
||||||
|
return url_for(
|
||||||
|
'set.checked_part',
|
||||||
|
id=self.fields.id,
|
||||||
|
figure=self.minifigure.fields.figure,
|
||||||
|
part=self.fields.part,
|
||||||
|
color=self.fields.color,
|
||||||
|
spare=self.fields.spare,
|
||||||
|
)
|
||||||
|
# Set part
|
||||||
else:
|
else:
|
||||||
figure = None
|
return url_for(
|
||||||
|
'set.checked_part',
|
||||||
return url_for(
|
id=self.fields.id,
|
||||||
'set.checked_part',
|
figure=None,
|
||||||
id=self.fields.id,
|
part=self.fields.part,
|
||||||
figure=figure,
|
color=self.fields.color,
|
||||||
part=self.fields.part,
|
spare=self.fields.spare,
|
||||||
color=self.fields.color,
|
)
|
||||||
spare=self.fields.spare,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update a problematic part
|
# Update a problematic part
|
||||||
def update_problem(self, problem: str, json: Any | None, /) -> int:
|
def update_problem(self, problem: str, json: Any | None, /) -> int:
|
||||||
@@ -249,20 +317,67 @@ class BrickPart(RebrickablePart):
|
|||||||
|
|
||||||
return amount
|
return amount
|
||||||
|
|
||||||
|
# Update a problematic part for individual minifigure
|
||||||
|
def update_problem_individual_minifigure(self, problem: str, json: Any | None, /) -> int:
|
||||||
|
amount: str | int = json.get('value', '') # type: ignore
|
||||||
|
|
||||||
|
# We need a positive integer
|
||||||
|
try:
|
||||||
|
if amount == '':
|
||||||
|
amount = 0
|
||||||
|
|
||||||
|
amount = int(amount)
|
||||||
|
|
||||||
|
if amount < 0:
|
||||||
|
amount = 0
|
||||||
|
except Exception:
|
||||||
|
raise ErrorException('"{amount}" is not a valid integer'.format(
|
||||||
|
amount=amount
|
||||||
|
))
|
||||||
|
|
||||||
|
if amount < 0:
|
||||||
|
raise ErrorException('Cannot set a negative amount')
|
||||||
|
|
||||||
|
setattr(self.fields, problem, amount)
|
||||||
|
|
||||||
|
BrickSQL().execute_and_commit(
|
||||||
|
'individual_minifigure/part/update/{problem}'.format(problem=problem),
|
||||||
|
parameters=self.sql_parameters()
|
||||||
|
)
|
||||||
|
|
||||||
|
return amount
|
||||||
|
|
||||||
# Compute the url for problematic part
|
# Compute the url for problematic part
|
||||||
def url_for_problem(self, problem: str, /) -> str:
|
def url_for_problem(self, problem: str, /) -> str:
|
||||||
# Different URL for a minifigure part
|
# Different URL for individual minifigure part
|
||||||
if self.minifigure is not None:
|
if self.individual_minifigure is not None:
|
||||||
figure = self.minifigure.fields.figure
|
return url_for(
|
||||||
|
'individual_minifigure.problem_part',
|
||||||
|
id=self.individual_minifigure.fields.id,
|
||||||
|
part=self.fields.part,
|
||||||
|
color=self.fields.color,
|
||||||
|
spare=self.fields.spare,
|
||||||
|
problem=problem,
|
||||||
|
)
|
||||||
|
# Different URL for set minifigure part
|
||||||
|
elif self.minifigure is not None:
|
||||||
|
return url_for(
|
||||||
|
'set.problem_part',
|
||||||
|
id=self.fields.id,
|
||||||
|
figure=self.minifigure.fields.figure,
|
||||||
|
part=self.fields.part,
|
||||||
|
color=self.fields.color,
|
||||||
|
spare=self.fields.spare,
|
||||||
|
problem=problem,
|
||||||
|
)
|
||||||
|
# Set part
|
||||||
else:
|
else:
|
||||||
figure = None
|
return url_for(
|
||||||
|
'set.problem_part',
|
||||||
return url_for(
|
id=self.fields.id,
|
||||||
'set.problem_part',
|
figure=None,
|
||||||
id=self.fields.id,
|
part=self.fields.part,
|
||||||
figure=figure,
|
color=self.fields.color,
|
||||||
part=self.fields.part,
|
spare=self.fields.spare,
|
||||||
color=self.fields.color,
|
problem=problem,
|
||||||
spare=self.fields.spare,
|
|
||||||
problem=problem,
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ logger = logging.getLogger(__name__)
|
|||||||
class BrickPartList(BrickRecordList[BrickPart]):
|
class BrickPartList(BrickRecordList[BrickPart]):
|
||||||
brickset: 'BrickSet | None'
|
brickset: 'BrickSet | None'
|
||||||
minifigure: 'BrickMinifigure | None'
|
minifigure: 'BrickMinifigure | None'
|
||||||
|
individual_minifigure: 'IndividualMinifigure | None'
|
||||||
order: str
|
order: str
|
||||||
|
|
||||||
# Queries
|
# Queries
|
||||||
@@ -57,8 +58,8 @@ class BrickPartList(BrickRecordList[BrickPart]):
|
|||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
# Load all parts with filters (owner, color, theme, year)
|
# Load all parts with filters (owner, color, theme, year, individuals)
|
||||||
def all_filtered(self, owner_id: str | None = None, color_id: str | None = None, theme_id: str | None = None, year: str | None = None, /) -> Self:
|
def all_filtered(self, owner_id: str | None = None, color_id: str | None = None, theme_id: str | None = None, year: str | None = None, individuals_filter: str | None = None, /) -> Self:
|
||||||
# Save the filter parameters
|
# Save the filter parameters
|
||||||
if owner_id is not None:
|
if owner_id is not None:
|
||||||
self.fields.owner_id = owner_id
|
self.fields.owner_id = owner_id
|
||||||
@@ -80,6 +81,8 @@ class BrickPartList(BrickRecordList[BrickPart]):
|
|||||||
context['theme_id'] = theme_id
|
context['theme_id'] = theme_id
|
||||||
if year and year != 'all':
|
if year and year != 'all':
|
||||||
context['year'] = year
|
context['year'] = year
|
||||||
|
if individuals_filter and individuals_filter == 'only':
|
||||||
|
context['individuals_filter'] = True
|
||||||
|
|
||||||
# Load the parts from the database
|
# Load the parts from the database
|
||||||
self.list(override_query=query, **context)
|
self.list(override_query=query, **context)
|
||||||
@@ -93,6 +96,7 @@ class BrickPartList(BrickRecordList[BrickPart]):
|
|||||||
color_id: str | None = None,
|
color_id: str | None = None,
|
||||||
theme_id: str | None = None,
|
theme_id: str | None = None,
|
||||||
year: str | None = None,
|
year: str | None = None,
|
||||||
|
individuals_filter: str | None = None,
|
||||||
search_query: str | None = None,
|
search_query: str | None = None,
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
per_page: int = 50,
|
per_page: int = 50,
|
||||||
@@ -113,6 +117,8 @@ class BrickPartList(BrickRecordList[BrickPart]):
|
|||||||
filter_context['theme_id'] = theme_id
|
filter_context['theme_id'] = theme_id
|
||||||
if year and year != 'all':
|
if year and year != 'all':
|
||||||
filter_context['year'] = year
|
filter_context['year'] = year
|
||||||
|
if individuals_filter and individuals_filter == 'only':
|
||||||
|
filter_context['individuals_filter'] = True
|
||||||
if search_query:
|
if search_query:
|
||||||
filter_context['search_query'] = search_query
|
filter_context['search_query'] = search_query
|
||||||
# Hide spare parts from display if configured
|
# Hide spare parts from display if configured
|
||||||
@@ -165,6 +171,11 @@ class BrickPartList(BrickRecordList[BrickPart]):
|
|||||||
else:
|
else:
|
||||||
minifigure = None
|
minifigure = None
|
||||||
|
|
||||||
|
if hasattr(self, 'individual_minifigure'):
|
||||||
|
individual_minifigure = self.individual_minifigure
|
||||||
|
else:
|
||||||
|
individual_minifigure = None
|
||||||
|
|
||||||
# Prepare template context for filtering
|
# Prepare template context for filtering
|
||||||
context_vars = {}
|
context_vars = {}
|
||||||
if hasattr(self.fields, 'owner_id') and self.fields.owner_id is not None:
|
if hasattr(self.fields, 'owner_id') and self.fields.owner_id is not None:
|
||||||
@@ -188,6 +199,7 @@ class BrickPartList(BrickRecordList[BrickPart]):
|
|||||||
part = BrickPart(
|
part = BrickPart(
|
||||||
brickset=brickset,
|
brickset=brickset,
|
||||||
minifigure=minifigure,
|
minifigure=minifigure,
|
||||||
|
individual_minifigure=individual_minifigure,
|
||||||
record=record,
|
record=record,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -234,6 +246,24 @@ class BrickPartList(BrickRecordList[BrickPart]):
|
|||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
# Load parts from an individual minifigure instance
|
||||||
|
def from_individual_minifigure(
|
||||||
|
self,
|
||||||
|
individual_minifigure: 'IndividualMinifigure',
|
||||||
|
/,
|
||||||
|
) -> Self:
|
||||||
|
from .individual_minifigure import IndividualMinifigure
|
||||||
|
|
||||||
|
# Save the individual minifigure reference
|
||||||
|
self.individual_minifigure = individual_minifigure
|
||||||
|
|
||||||
|
# Load the parts for this individual minifigure instance
|
||||||
|
self.list(
|
||||||
|
override_query='individual_minifigure/part/list/from_instance'
|
||||||
|
)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
# Load generic parts from a print
|
# Load generic parts from a print
|
||||||
def from_print(
|
def from_print(
|
||||||
self,
|
self,
|
||||||
@@ -369,6 +399,10 @@ class BrickPartList(BrickRecordList[BrickPart]):
|
|||||||
if self.brickset is not None:
|
if self.brickset is not None:
|
||||||
parameters['id'] = self.brickset.fields.id
|
parameters['id'] = self.brickset.fields.id
|
||||||
|
|
||||||
|
# Use the individual minifigure ID if present
|
||||||
|
if hasattr(self, 'individual_minifigure') and self.individual_minifigure is not None:
|
||||||
|
parameters['id'] = self.individual_minifigure.fields.id
|
||||||
|
|
||||||
# Use the minifigure number if present,
|
# Use the minifigure number if present,
|
||||||
if self.minifigure is not None:
|
if self.minifigure is not None:
|
||||||
parameters['figure'] = self.minifigure.fields.figure
|
parameters['figure'] = self.minifigure.fields.figure
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ if TYPE_CHECKING:
|
|||||||
class RebrickableMinifigure(BrickRecord):
|
class RebrickableMinifigure(BrickRecord):
|
||||||
brickset: 'BrickSet | None'
|
brickset: 'BrickSet | None'
|
||||||
|
|
||||||
# Queries
|
|
||||||
select_query: str = 'rebrickable/minifigure/select'
|
select_query: str = 'rebrickable/minifigure/select'
|
||||||
insert_query: str = 'rebrickable/minifigure/insert'
|
insert_query: str = 'rebrickable/minifigure/insert'
|
||||||
|
|
||||||
@@ -27,10 +26,8 @@ class RebrickableMinifigure(BrickRecord):
|
|||||||
):
|
):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
# Save the brickset
|
|
||||||
self.brickset = brickset
|
self.brickset = brickset
|
||||||
|
|
||||||
# Ingest the record if it has one
|
|
||||||
if record is not None:
|
if record is not None:
|
||||||
self.ingest(record)
|
self.ingest(record)
|
||||||
|
|
||||||
@@ -62,7 +59,6 @@ class RebrickableMinifigure(BrickRecord):
|
|||||||
|
|
||||||
return parameters
|
return parameters
|
||||||
|
|
||||||
# Self url
|
|
||||||
def url(self, /) -> str:
|
def url(self, /) -> str:
|
||||||
return url_for(
|
return url_for(
|
||||||
'minifigure.details',
|
'minifigure.details',
|
||||||
@@ -89,17 +85,24 @@ class RebrickableMinifigure(BrickRecord):
|
|||||||
if current_app.config['REBRICKABLE_LINKS']:
|
if current_app.config['REBRICKABLE_LINKS']:
|
||||||
try:
|
try:
|
||||||
return current_app.config['REBRICKABLE_LINK_MINIFIGURE_PATTERN'].format( # noqa: E501
|
return current_app.config['REBRICKABLE_LINK_MINIFIGURE_PATTERN'].format( # noqa: E501
|
||||||
number=self.fields.figure,
|
figure=self.fields.figure,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
# Compute the url for the bricklink page
|
||||||
|
# Note: BrickLink uses different minifigure IDs than Rebrickable (e.g., 'adv010' vs 'fig-000359')
|
||||||
|
# Rebrickable API doesn't provide BrickLink minifigure IDs, so we can't generate valid links
|
||||||
|
def url_for_bricklink(self, /) -> str:
|
||||||
|
# BrickLink links disabled for minifigures - no ID mapping available
|
||||||
|
# Left function for later, if I find a way to implement it.
|
||||||
|
return ''
|
||||||
|
|
||||||
# Normalize from Rebrickable
|
# Normalize from Rebrickable
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_rebrickable(data: dict[str, Any], /, **_) -> dict[str, Any]:
|
def from_rebrickable(data: dict[str, Any], /, **_) -> dict[str, Any]:
|
||||||
# Extracting number
|
|
||||||
number = int(str(data['set_num'])[5:])
|
number = int(str(data['set_num'])[5:])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -67,8 +67,11 @@ class RebrickablePart(BrickRecord):
|
|||||||
def sql_parameters(self, /) -> dict[str, Any]:
|
def sql_parameters(self, /) -> dict[str, Any]:
|
||||||
parameters = super().sql_parameters()
|
parameters = super().sql_parameters()
|
||||||
|
|
||||||
|
# Individual minifigure id takes precedence
|
||||||
|
if hasattr(self, 'individual_minifigure') and self.individual_minifigure is not None:
|
||||||
|
parameters['id'] = self.individual_minifigure.fields.id
|
||||||
# Set id
|
# Set id
|
||||||
if self.brickset is not None:
|
elif self.brickset is not None:
|
||||||
parameters['id'] = self.brickset.fields.id
|
parameters['id'] = self.brickset.fields.id
|
||||||
|
|
||||||
# Use the minifigure number if present,
|
# Use the minifigure number if present,
|
||||||
|
|||||||
@@ -95,6 +95,18 @@ class RebrickableSet(BrickRecord):
|
|||||||
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']))
|
||||||
|
|
||||||
|
# Check if this is actually a minifigure (starts with fig-)
|
||||||
|
# If so, redirect to the minifigure handler
|
||||||
|
if set.startswith('fig-'):
|
||||||
|
from .individual_minifigure import IndividualMinifigure
|
||||||
|
# Transform data: minifigure handler expects 'figure' key instead of 'set'
|
||||||
|
minifig_data = data.copy()
|
||||||
|
minifig_data['figure'] = minifig_data.pop('set')
|
||||||
|
if from_download:
|
||||||
|
return IndividualMinifigure().download(socket, minifig_data)
|
||||||
|
else:
|
||||||
|
return IndividualMinifigure().load(socket, minifig_data)
|
||||||
|
|
||||||
socket.auto_progress(
|
socket.auto_progress(
|
||||||
message='Set {set}: loading from Rebrickable'.format(
|
message='Set {set}: loading from Rebrickable'.format(
|
||||||
set=set,
|
set=set,
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class BrickSetList(BrickRecordList[BrickSet]):
|
|||||||
using_minifigure_query: str = 'set/list/using_minifigure'
|
using_minifigure_query: str = 'set/list/using_minifigure'
|
||||||
using_part_query: str = 'set/list/using_part'
|
using_part_query: str = 'set/list/using_part'
|
||||||
using_storage_query: str = 'set/list/using_storage'
|
using_storage_query: str = 'set/list/using_storage'
|
||||||
|
using_purchase_location_query: str = 'set/list/using_purchase_location'
|
||||||
|
|
||||||
def __init__(self, /):
|
def __init__(self, /):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -678,6 +679,16 @@ class BrickSetList(BrickRecordList[BrickSet]):
|
|||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
# Sets using a purchase location
|
||||||
|
def using_purchase_location(self, purchase_location: BrickSetPurchaseLocation, /) -> Self:
|
||||||
|
# Save the parameters to the fields
|
||||||
|
self.fields.purchase_location = purchase_location.fields.id
|
||||||
|
|
||||||
|
# Load the sets from the database
|
||||||
|
self.list(override_query=self.using_purchase_location_query)
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
# Helper to build the metadata lists
|
# Helper to build the metadata lists
|
||||||
def set_metadata_lists(
|
def set_metadata_lists(
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from .metadata import BrickMetadata
|
from .metadata import BrickMetadata
|
||||||
|
|
||||||
|
from flask import url_for
|
||||||
|
|
||||||
|
|
||||||
# Lego set purchase location metadata
|
# Lego set purchase location metadata
|
||||||
class BrickSetPurchaseLocation(BrickMetadata):
|
class BrickSetPurchaseLocation(BrickMetadata):
|
||||||
@@ -11,3 +13,10 @@ class BrickSetPurchaseLocation(BrickMetadata):
|
|||||||
select_query: str = 'set/metadata/purchase_location/select'
|
select_query: str = 'set/metadata/purchase_location/select'
|
||||||
update_field_query: str = 'set/metadata/purchase_location/update/field'
|
update_field_query: str = 'set/metadata/purchase_location/update/field'
|
||||||
update_set_value_query: str = 'set/metadata/purchase_location/update/value'
|
update_set_value_query: str = 'set/metadata/purchase_location/update/value'
|
||||||
|
|
||||||
|
# Self url
|
||||||
|
def url(self, /) -> str:
|
||||||
|
return url_for(
|
||||||
|
'purchase_location.details',
|
||||||
|
id=self.fields.id,
|
||||||
|
)
|
||||||
|
|||||||
@@ -18,13 +18,22 @@ logger = logging.getLogger(__name__)
|
|||||||
MESSAGES: Final[dict[str, str]] = {
|
MESSAGES: Final[dict[str, str]] = {
|
||||||
'COMPLETE': 'complete',
|
'COMPLETE': 'complete',
|
||||||
'CONNECT': 'connect',
|
'CONNECT': 'connect',
|
||||||
|
'CREATE_LOT': 'create_lot',
|
||||||
|
'CREATE_BULK_INDIVIDUAL_PARTS': 'create_bulk_individual_parts',
|
||||||
'DISCONNECT': 'disconnect',
|
'DISCONNECT': 'disconnect',
|
||||||
'DOWNLOAD_INSTRUCTIONS': 'download_instructions',
|
'DOWNLOAD_INSTRUCTIONS': 'download_instructions',
|
||||||
'DOWNLOAD_PEERON_PAGES': 'download_peeron_pages',
|
'DOWNLOAD_PEERON_PAGES': 'download_peeron_pages',
|
||||||
'FAIL': 'fail',
|
'FAIL': 'fail',
|
||||||
|
'IMPORT_MINIFIGURE': 'import_minifigure',
|
||||||
'IMPORT_SET': 'import_set',
|
'IMPORT_SET': 'import_set',
|
||||||
|
'LOAD_MINIFIGURE': 'load_minifigure',
|
||||||
|
'LOAD_PART': 'load_part',
|
||||||
|
'LOAD_PART_COLORS': 'load_part_colors',
|
||||||
'LOAD_PEERON_PAGES': 'load_peeron_pages',
|
'LOAD_PEERON_PAGES': 'load_peeron_pages',
|
||||||
'LOAD_SET': 'load_set',
|
'LOAD_SET': 'load_set',
|
||||||
|
'MINIFIGURE_LOADED': 'minifigure_loaded',
|
||||||
|
'PART_COLORS_LOADED': 'part_colors_loaded',
|
||||||
|
'PART_LOADED': 'part_loaded',
|
||||||
'PROGRESS': 'progress',
|
'PROGRESS': 'progress',
|
||||||
'SET_LOADED': 'set_loaded',
|
'SET_LOADED': 'set_loaded',
|
||||||
}
|
}
|
||||||
@@ -228,6 +237,67 @@ class BrickSocket(object):
|
|||||||
|
|
||||||
BrickSet().load(self, data)
|
BrickSet().load(self, data)
|
||||||
|
|
||||||
|
@self.socket.on(MESSAGES['IMPORT_MINIFIGURE'], namespace=self.namespace)
|
||||||
|
@rebrickable_socket(self)
|
||||||
|
def import_minifigure(data: dict[str, Any], /) -> None:
|
||||||
|
logger.debug('Socket: IMPORT_MINIFIGURE={data} (from: {fr})'.format(
|
||||||
|
data=data,
|
||||||
|
fr=request.sid, # type: ignore
|
||||||
|
))
|
||||||
|
|
||||||
|
from .individual_minifigure import IndividualMinifigure
|
||||||
|
IndividualMinifigure().download(self, data)
|
||||||
|
|
||||||
|
@self.socket.on(MESSAGES['LOAD_MINIFIGURE'], namespace=self.namespace)
|
||||||
|
def load_minifigure(data: dict[str, Any], /) -> None:
|
||||||
|
logger.debug('Socket: LOAD_MINIFIGURE={data} (from: {fr})'.format(
|
||||||
|
data=data,
|
||||||
|
fr=request.sid, # type: ignore
|
||||||
|
))
|
||||||
|
|
||||||
|
from .individual_minifigure import IndividualMinifigure
|
||||||
|
IndividualMinifigure().load(self, data)
|
||||||
|
|
||||||
|
@self.socket.on(MESSAGES['LOAD_PART'], namespace=self.namespace)
|
||||||
|
def load_part(data: dict[str, Any], /) -> None:
|
||||||
|
logger.debug('Socket: LOAD_PART={data} (from: {fr})'.format(
|
||||||
|
data=data,
|
||||||
|
fr=request.sid, # type: ignore
|
||||||
|
))
|
||||||
|
|
||||||
|
from .individual_part import IndividualPart
|
||||||
|
IndividualPart().add(self, data)
|
||||||
|
|
||||||
|
@self.socket.on(MESSAGES['LOAD_PART_COLORS'], namespace=self.namespace)
|
||||||
|
def load_part_colors(data: dict[str, Any], /) -> None:
|
||||||
|
logger.debug('Socket: LOAD_PART_COLORS={data} (from: {fr})'.format(
|
||||||
|
data=data,
|
||||||
|
fr=request.sid, # type: ignore
|
||||||
|
))
|
||||||
|
|
||||||
|
from .individual_part import IndividualPart
|
||||||
|
IndividualPart().load_colors(self, data)
|
||||||
|
|
||||||
|
@self.socket.on(MESSAGES['CREATE_LOT'], namespace=self.namespace)
|
||||||
|
@rebrickable_socket(self)
|
||||||
|
def create_lot(data: dict[str, Any], /) -> None:
|
||||||
|
logger.debug('Socket: CREATE_LOT (from: {fr})'.format(
|
||||||
|
fr=request.sid, # type: ignore
|
||||||
|
))
|
||||||
|
|
||||||
|
from .individual_part_lot import IndividualPartLot
|
||||||
|
IndividualPartLot().create(self, data)
|
||||||
|
|
||||||
|
@self.socket.on(MESSAGES['CREATE_BULK_INDIVIDUAL_PARTS'], namespace=self.namespace)
|
||||||
|
@rebrickable_socket(self)
|
||||||
|
def create_bulk_individual_parts(data: dict[str, Any], /) -> None:
|
||||||
|
logger.debug('Socket: CREATE_BULK_INDIVIDUAL_PARTS (from: {fr})'.format(
|
||||||
|
fr=request.sid, # type: ignore
|
||||||
|
))
|
||||||
|
|
||||||
|
from .individual_part import IndividualPart
|
||||||
|
IndividualPart().create_bulk(self, data)
|
||||||
|
|
||||||
# Update the progress auto-incrementing
|
# Update the progress auto-incrementing
|
||||||
def auto_progress(
|
def auto_progress(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -53,17 +53,18 @@ class BrickStatistics:
|
|||||||
return [dict(row) for row in results]
|
return [dict(row) for row in results]
|
||||||
|
|
||||||
def get_financial_summary(self) -> dict[str, Any]:
|
def get_financial_summary(self) -> dict[str, Any]:
|
||||||
"""Get financial summary from overview statistics"""
|
"""Get financial summary from overview statistics (includes all item types)"""
|
||||||
overview = self.get_overview()
|
overview = self.get_overview()
|
||||||
return {
|
return {
|
||||||
'total_cost': overview.get('total_cost') or 0,
|
'total_cost': overview.get('combined_total_cost') or 0,
|
||||||
'average_cost': overview.get('average_cost') or 0,
|
'average_cost': overview.get('combined_average_cost') or 0,
|
||||||
'minimum_cost': overview.get('minimum_cost') or 0,
|
'minimum_cost': overview.get('combined_minimum_cost') or 0,
|
||||||
'maximum_cost': overview.get('maximum_cost') or 0,
|
'maximum_cost': overview.get('combined_maximum_cost') or 0,
|
||||||
|
'items_with_price': overview.get('total_items_with_price') or 0,
|
||||||
'sets_with_price': overview.get('sets_with_price') or 0,
|
'sets_with_price': overview.get('sets_with_price') or 0,
|
||||||
'total_sets': overview.get('total_sets') or 0,
|
'total_sets': overview.get('total_sets') or 0,
|
||||||
'percentage_with_price': round(
|
'percentage_with_price': round(
|
||||||
((overview.get('sets_with_price') or 0) / max((overview.get('total_sets') or 0), 1)) * 100, 1
|
((overview.get('total_items_with_price') or 0) / max((overview.get('total_sets') or 0), 1)) * 100, 1
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user