From fa053055a3dc53ca7efc7f495fdcafcdbdc25162 Mon Sep 17 00:00:00 2001 From: FrederikBaerentsen Date: Mon, 19 Jan 2026 17:19:21 +0100 Subject: [PATCH] feat(views): update existing models to support individual items integration --- bricktracker/minifigure_list.py | 22 +++- bricktracker/parser.py | 30 ++++- bricktracker/part.py | 169 +++++++++++++++++++++---- bricktracker/part_list.py | 38 +++++- bricktracker/rebrickable_minifigure.py | 15 ++- bricktracker/rebrickable_part.py | 5 +- bricktracker/rebrickable_set.py | 12 ++ bricktracker/set_list.py | 11 ++ bricktracker/set_purchase_location.py | 9 ++ bricktracker/socket.py | 70 ++++++++++ bricktracker/statistics.py | 13 +- 11 files changed, 343 insertions(+), 51 deletions(-) diff --git a/bricktracker/minifigure_list.py b/bricktracker/minifigure_list.py index 35bafad..287b133 100644 --- a/bricktracker/minifigure_list.py +++ b/bricktracker/minifigure_list.py @@ -20,8 +20,8 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): order: str # Queries - all_query: str = 'minifigure/list/all' - all_by_owner_query: str = 'minifigure/list/all_by_owner' + all_query: str = 'minifigure/list/all_unified' + all_by_owner_query: str = 'minifigure/list/all_by_owner_unified' damaged_part_query: str = 'minifigure/list/damaged_part' last_query: str = 'minifigure/list/last' missing_part_query: str = 'minifigure/list/missing_part' @@ -44,7 +44,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): return self # 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 if owner_id is not None: self.fields.owner_id = owner_id @@ -56,6 +56,8 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): context['theme_id'] = theme_id if year and year != 'all': context['year'] = year + if individuals_filter and individuals_filter != 'all': + context['individuals_filter'] = individuals_filter # Choose query based on whether owner filtering is needed if owner_id and owner_id != 'all': @@ -77,7 +79,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): return self # 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 self.fields.owner_id = owner_id @@ -88,6 +90,8 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): context['theme_id'] = theme_id if year and year != 'all': context['year'] = year + if individuals_filter and individuals_filter != 'all': + context['individuals_filter'] = individuals_filter # Load the minifigures from the database self.list(override_query=self.all_by_owner_query, **context) @@ -101,6 +105,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): problems_filter: str = 'all', theme_id: str = 'all', year: str = 'all', + individuals_filter: str = 'all', search_query: str | None = None, page: int = 1, per_page: int = 50, @@ -127,10 +132,13 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]): if year and year != 'all': 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 = { - 'name': '"rebrickable_minifigures"."name"', - 'parts': '"rebrickable_minifigures"."number_of_parts"', + 'name': '"name"', + 'parts': '"number_of_parts"', 'quantity': '"total_quantity"', 'missing': '"total_missing"', 'damaged': '"total_damaged"', diff --git a/bricktracker/parser.py b/bricktracker/parser.py index 5764bfe..c1444f4 100644 --- a/bricktracker/parser.py +++ b/bricktracker/parser.py @@ -17,7 +17,7 @@ def parse_set(set: str, /) -> str: if version == '': version = '1' - # Version must be a positive integer + # Version must be a valid number (but preserve leading zeros for minifigures) try: version_int = int(version) except Exception: @@ -30,4 +30,30 @@ def parse_set(set: str, /) -> str: 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 diff --git a/bricktracker/part.py b/bricktracker/part.py index ed60714..ff9d56a 100644 --- a/bricktracker/part.py +++ b/bricktracker/part.py @@ -9,6 +9,7 @@ from .exceptions import ErrorException, NotFoundException from .rebrickable_part import RebrickablePart from .sql import BrickSQL if TYPE_CHECKING: + from .individual_minifigure import IndividualMinifigure from .minifigure import BrickMinifigure from .set import BrickSet from .socket import BrickSocket @@ -33,6 +34,7 @@ class BrickPart(RebrickablePart): *, brickset: 'BrickSet | None' = None, minifigure: 'BrickMinifigure | None' = None, + individual_minifigure: 'IndividualMinifigure | None' = None, record: Row | dict[str, Any] | None = None ): super().__init__( @@ -41,7 +43,12 @@ class BrickPart(RebrickablePart): 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.kind = 'Minifigure' elif self.brickset is not None: @@ -182,6 +189,33 @@ class BrickPart(RebrickablePart): 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 def update_checked(self, json: Any | None, /) -> bool: # Handle both direct 'checked' key and changer.js 'value' key format @@ -202,22 +236,56 @@ class BrickPart(RebrickablePart): 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 def url_for_checked(self, /) -> str: - # Different URL for a minifigure part - if self.minifigure is not None: - figure = self.minifigure.fields.figure + # Different URL for individual minifigure part + if self.individual_minifigure is not None: + 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: - figure = None - - return url_for( - 'set.checked_part', - id=self.fields.id, - figure=figure, - part=self.fields.part, - color=self.fields.color, - spare=self.fields.spare, - ) + return url_for( + 'set.checked_part', + id=self.fields.id, + figure=None, + part=self.fields.part, + color=self.fields.color, + spare=self.fields.spare, + ) # Update a problematic part def update_problem(self, problem: str, json: Any | None, /) -> int: @@ -249,20 +317,67 @@ class BrickPart(RebrickablePart): 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 def url_for_problem(self, problem: str, /) -> str: - # Different URL for a minifigure part - if self.minifigure is not None: - figure = self.minifigure.fields.figure + # Different URL for individual minifigure part + if self.individual_minifigure is not None: + 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: - figure = None - - return url_for( - 'set.problem_part', - id=self.fields.id, - figure=figure, - part=self.fields.part, - color=self.fields.color, - spare=self.fields.spare, - problem=problem, + return url_for( + 'set.problem_part', + id=self.fields.id, + figure=None, + part=self.fields.part, + color=self.fields.color, + spare=self.fields.spare, + problem=problem, ) diff --git a/bricktracker/part_list.py b/bricktracker/part_list.py index 28f971d..32e738e 100644 --- a/bricktracker/part_list.py +++ b/bricktracker/part_list.py @@ -19,6 +19,7 @@ logger = logging.getLogger(__name__) class BrickPartList(BrickRecordList[BrickPart]): brickset: 'BrickSet | None' minifigure: 'BrickMinifigure | None' + individual_minifigure: 'IndividualMinifigure | None' order: str # Queries @@ -57,8 +58,8 @@ class BrickPartList(BrickRecordList[BrickPart]): return self - # Load all parts with filters (owner, color, theme, year) - def all_filtered(self, owner_id: str | None = None, color_id: str | None = None, theme_id: str | None = None, year: str | None = None, /) -> Self: + # 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, individuals_filter: str | None = None, /) -> Self: # Save the filter parameters if owner_id is not None: self.fields.owner_id = owner_id @@ -80,6 +81,8 @@ class BrickPartList(BrickRecordList[BrickPart]): context['theme_id'] = theme_id if year and year != 'all': context['year'] = year + if individuals_filter and individuals_filter == 'only': + context['individuals_filter'] = True # Load the parts from the database self.list(override_query=query, **context) @@ -93,6 +96,7 @@ class BrickPartList(BrickRecordList[BrickPart]): color_id: str | None = None, theme_id: str | None = None, year: str | None = None, + individuals_filter: str | None = None, search_query: str | None = None, page: int = 1, per_page: int = 50, @@ -113,6 +117,8 @@ class BrickPartList(BrickRecordList[BrickPart]): filter_context['theme_id'] = theme_id if year and year != 'all': filter_context['year'] = year + if individuals_filter and individuals_filter == 'only': + filter_context['individuals_filter'] = True if search_query: filter_context['search_query'] = search_query # Hide spare parts from display if configured @@ -165,6 +171,11 @@ class BrickPartList(BrickRecordList[BrickPart]): else: minifigure = None + if hasattr(self, 'individual_minifigure'): + individual_minifigure = self.individual_minifigure + else: + individual_minifigure = None + # Prepare template context for filtering context_vars = {} if hasattr(self.fields, 'owner_id') and self.fields.owner_id is not None: @@ -188,6 +199,7 @@ class BrickPartList(BrickRecordList[BrickPart]): part = BrickPart( brickset=brickset, minifigure=minifigure, + individual_minifigure=individual_minifigure, record=record, ) @@ -234,6 +246,24 @@ class BrickPartList(BrickRecordList[BrickPart]): 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 def from_print( self, @@ -369,6 +399,10 @@ class BrickPartList(BrickRecordList[BrickPart]): if self.brickset is not None: 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, if self.minifigure is not None: parameters['figure'] = self.minifigure.fields.figure diff --git a/bricktracker/rebrickable_minifigure.py b/bricktracker/rebrickable_minifigure.py index 0ef7d43..c6f11df 100644 --- a/bricktracker/rebrickable_minifigure.py +++ b/bricktracker/rebrickable_minifigure.py @@ -14,7 +14,6 @@ if TYPE_CHECKING: class RebrickableMinifigure(BrickRecord): brickset: 'BrickSet | None' - # Queries select_query: str = 'rebrickable/minifigure/select' insert_query: str = 'rebrickable/minifigure/insert' @@ -27,10 +26,8 @@ class RebrickableMinifigure(BrickRecord): ): super().__init__() - # Save the brickset self.brickset = brickset - # Ingest the record if it has one if record is not None: self.ingest(record) @@ -62,7 +59,6 @@ class RebrickableMinifigure(BrickRecord): return parameters - # Self url def url(self, /) -> str: return url_for( 'minifigure.details', @@ -89,17 +85,24 @@ class RebrickableMinifigure(BrickRecord): if current_app.config['REBRICKABLE_LINKS']: try: return current_app.config['REBRICKABLE_LINK_MINIFIGURE_PATTERN'].format( # noqa: E501 - number=self.fields.figure, + figure=self.fields.figure, ) except Exception: pass 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 @staticmethod def from_rebrickable(data: dict[str, Any], /, **_) -> dict[str, Any]: - # Extracting number number = int(str(data['set_num'])[5:]) return { diff --git a/bricktracker/rebrickable_part.py b/bricktracker/rebrickable_part.py index 09a3761..90974f0 100644 --- a/bricktracker/rebrickable_part.py +++ b/bricktracker/rebrickable_part.py @@ -67,8 +67,11 @@ class RebrickablePart(BrickRecord): def sql_parameters(self, /) -> dict[str, Any]: 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 - if self.brickset is not None: + elif self.brickset is not None: parameters['id'] = self.brickset.fields.id # Use the minifigure number if present, diff --git a/bricktracker/rebrickable_set.py b/bricktracker/rebrickable_set.py index f98785f..da0bd6b 100644 --- a/bricktracker/rebrickable_set.py +++ b/bricktracker/rebrickable_set.py @@ -95,6 +95,18 @@ class RebrickableSet(BrickRecord): socket.auto_progress(message='Parsing set number') 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( message='Set {set}: loading from Rebrickable'.format( set=set, diff --git a/bricktracker/set_list.py b/bricktracker/set_list.py index 9f23494..a61b295 100644 --- a/bricktracker/set_list.py +++ b/bricktracker/set_list.py @@ -36,6 +36,7 @@ class BrickSetList(BrickRecordList[BrickSet]): using_minifigure_query: str = 'set/list/using_minifigure' using_part_query: str = 'set/list/using_part' using_storage_query: str = 'set/list/using_storage' + using_purchase_location_query: str = 'set/list/using_purchase_location' def __init__(self, /): super().__init__() @@ -678,6 +679,16 @@ class BrickSetList(BrickRecordList[BrickSet]): 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 def set_metadata_lists( diff --git a/bricktracker/set_purchase_location.py b/bricktracker/set_purchase_location.py index 801ccf8..f32e52b 100644 --- a/bricktracker/set_purchase_location.py +++ b/bricktracker/set_purchase_location.py @@ -1,5 +1,7 @@ from .metadata import BrickMetadata +from flask import url_for + # Lego set purchase location metadata class BrickSetPurchaseLocation(BrickMetadata): @@ -11,3 +13,10 @@ class BrickSetPurchaseLocation(BrickMetadata): select_query: str = 'set/metadata/purchase_location/select' update_field_query: str = 'set/metadata/purchase_location/update/field' 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, + ) diff --git a/bricktracker/socket.py b/bricktracker/socket.py index 1c13b6e..a123912 100644 --- a/bricktracker/socket.py +++ b/bricktracker/socket.py @@ -18,13 +18,22 @@ logger = logging.getLogger(__name__) MESSAGES: Final[dict[str, str]] = { 'COMPLETE': 'complete', 'CONNECT': 'connect', + 'CREATE_LOT': 'create_lot', + 'CREATE_BULK_INDIVIDUAL_PARTS': 'create_bulk_individual_parts', 'DISCONNECT': 'disconnect', 'DOWNLOAD_INSTRUCTIONS': 'download_instructions', 'DOWNLOAD_PEERON_PAGES': 'download_peeron_pages', 'FAIL': 'fail', + 'IMPORT_MINIFIGURE': 'import_minifigure', '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_SET': 'load_set', + 'MINIFIGURE_LOADED': 'minifigure_loaded', + 'PART_COLORS_LOADED': 'part_colors_loaded', + 'PART_LOADED': 'part_loaded', 'PROGRESS': 'progress', 'SET_LOADED': 'set_loaded', } @@ -228,6 +237,67 @@ class BrickSocket(object): 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 def auto_progress( self, diff --git a/bricktracker/statistics.py b/bricktracker/statistics.py index e446205..fe7baf7 100644 --- a/bricktracker/statistics.py +++ b/bricktracker/statistics.py @@ -53,17 +53,18 @@ class BrickStatistics: return [dict(row) for row in results] 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() return { - 'total_cost': overview.get('total_cost') or 0, - 'average_cost': overview.get('average_cost') or 0, - 'minimum_cost': overview.get('minimum_cost') or 0, - 'maximum_cost': overview.get('maximum_cost') or 0, + 'total_cost': overview.get('combined_total_cost') or 0, + 'average_cost': overview.get('combined_average_cost') or 0, + 'minimum_cost': overview.get('combined_minimum_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, 'total_sets': overview.get('total_sets') or 0, '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 ) }