fix(parts): make every filter on the parts and problem pages reach the SQL
This commit is contained in:
+136
-187
@@ -14,6 +14,46 @@ if TYPE_CHECKING:
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# The filters both the parts page and the problem page offer. 'all' or empty means
|
||||
# the filter is off.
|
||||
FILTER_NAMES: tuple[str, ...] = (
|
||||
'owner_id',
|
||||
'color_id',
|
||||
'theme_id',
|
||||
'year',
|
||||
'storage_id',
|
||||
'tag_id',
|
||||
'status_id',
|
||||
)
|
||||
|
||||
# Filters whose value is data and can be bound as a SQL parameter. owner, tag and
|
||||
# status are missing on purpose: those become column names, so they get validated
|
||||
# against the known metadata ids instead.
|
||||
BOUND_FILTERS: tuple[str, ...] = ('color_id', 'theme_id', 'year', 'storage_id')
|
||||
|
||||
# Storage has two sentinel values on top of the real ids
|
||||
STORAGE_SENTINELS: tuple[str, ...] = ('__none__', '-__none__')
|
||||
|
||||
|
||||
# Strip the leading "-" a negated filter carries
|
||||
def without_negation(value: str, /) -> str:
|
||||
return value[1:] if value.startswith('-') else value
|
||||
|
||||
|
||||
# owner, tag and status ids are interpolated into the query as column names, which
|
||||
# no amount of quoting makes safe. Only ids we already know about get through.
|
||||
def known_metadata_id(value: str | None, known: set[str], /) -> str | None:
|
||||
if not value or value == 'all':
|
||||
return None
|
||||
|
||||
if without_negation(value) not in known:
|
||||
logger.warning('Ignoring unknown metadata filter: {value}'.format(
|
||||
value=value,
|
||||
))
|
||||
return None
|
||||
|
||||
return value
|
||||
|
||||
|
||||
# Lego set or minifig parts
|
||||
class BrickPartList(BrickRecordList[BrickPart]):
|
||||
@@ -21,130 +61,139 @@ class BrickPartList(BrickRecordList[BrickPart]):
|
||||
minifigure: 'BrickMinifigure | None'
|
||||
individual_minifigure: 'IndividualMinifigure | None'
|
||||
order: str
|
||||
filter_parameters: dict[str, Any]
|
||||
|
||||
# Queries
|
||||
all_query: str = 'part/list/all'
|
||||
all_by_owner_query: str = 'part/list/all_by_owner'
|
||||
all_query: str = 'part/list/filtered'
|
||||
filtered_query: str = 'part/list/filtered'
|
||||
different_color_query = 'part/list/with_different_color'
|
||||
last_query: str = 'part/list/last'
|
||||
minifigure_query: str = 'part/list/from_minifigure'
|
||||
problem_query: str = 'part/list/problem'
|
||||
print_query: str = 'part/list/from_print'
|
||||
select_query: str = 'part/list/specific'
|
||||
|
||||
# Sortable columns, shared by both pages
|
||||
field_mapping: dict[str, str] = {
|
||||
'name': '"rebrickable_parts"."name"',
|
||||
'color': '"rebrickable_parts"."color_name"',
|
||||
'quantity': '"total_quantity"',
|
||||
'missing': '"total_missing"',
|
||||
'damaged': '"total_damaged"',
|
||||
'sets': '"total_sets"',
|
||||
'minifigures': '"total_minifigures"',
|
||||
}
|
||||
|
||||
def __init__(self, /):
|
||||
super().__init__()
|
||||
|
||||
# Placeholders
|
||||
self.brickset = None
|
||||
self.minifigure = None
|
||||
self.filter_parameters = {}
|
||||
|
||||
# Store the order for this list
|
||||
self.order = current_app.config['PARTS_DEFAULT_ORDER']
|
||||
|
||||
# Load all parts
|
||||
def all(self, /) -> Self:
|
||||
self.list(override_query=self.all_query)
|
||||
# Build the Jinja context for the filtered query, and stash the values that get
|
||||
# bound as SQL parameters. Both pages go through here, so a filter either works
|
||||
# on both or on neither.
|
||||
def filter_context(
|
||||
self,
|
||||
/,
|
||||
*,
|
||||
problem_only: bool = False,
|
||||
individuals_filter: str | None = None,
|
||||
search_query: str | None = None,
|
||||
**filters: str | None,
|
||||
) -> dict[str, Any]:
|
||||
context: dict[str, Any] = {}
|
||||
|
||||
return self
|
||||
if problem_only:
|
||||
context['problem_only'] = True
|
||||
|
||||
# Load all parts by owner
|
||||
def all_by_owner(self, owner_id: str | None = None, /) -> Self:
|
||||
# Save the owner_id parameter
|
||||
self.fields.owner_id = owner_id
|
||||
for name in FILTER_NAMES:
|
||||
value = filters.get(name)
|
||||
if value and value != 'all':
|
||||
context[name] = value
|
||||
|
||||
# Load the parts from the database
|
||||
self.list(override_query=self.all_by_owner_query)
|
||||
if individuals_filter == 'only':
|
||||
context['individuals_filter'] = True
|
||||
|
||||
return self
|
||||
if search_query:
|
||||
context['search_query'] = search_query
|
||||
|
||||
# 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
|
||||
if color_id is not None:
|
||||
self.fields.color_id = color_id
|
||||
|
||||
# Choose query based on whether owner filtering is needed
|
||||
if owner_id and owner_id != 'all':
|
||||
query = self.all_by_owner_query
|
||||
else:
|
||||
query = self.all_query
|
||||
|
||||
# Prepare context for query
|
||||
context = {}
|
||||
# Hide spare parts from display if configured
|
||||
if current_app.config.get('HIDE_SPARE_PARTS', False):
|
||||
context['skip_spare_parts'] = True
|
||||
if theme_id and theme_id != 'all':
|
||||
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)
|
||||
# Everything that is data rather than a column name gets bound
|
||||
self.filter_parameters = {}
|
||||
for name in BOUND_FILTERS:
|
||||
value = context.get(name)
|
||||
if value is None:
|
||||
continue
|
||||
if name == 'storage_id':
|
||||
if value in STORAGE_SENTINELS:
|
||||
continue
|
||||
value = without_negation(value)
|
||||
self.filter_parameters[name] = value
|
||||
|
||||
if search_query:
|
||||
self.filter_parameters['search_query'] = '%{query}%'.format(
|
||||
query=search_query.lower(),
|
||||
)
|
||||
|
||||
return context
|
||||
|
||||
# Load parts with filters. problem_only narrows to parts with something missing
|
||||
# or damaged, which is all the /parts/problem page is.
|
||||
def filtered(
|
||||
self,
|
||||
/,
|
||||
*,
|
||||
problem_only: bool = False,
|
||||
individuals_filter: str | None = None,
|
||||
**filters: str | None,
|
||||
) -> Self:
|
||||
context = self.filter_context(
|
||||
problem_only=problem_only,
|
||||
individuals_filter=individuals_filter,
|
||||
**filters
|
||||
)
|
||||
|
||||
self.list(override_query=self.filtered_query, **context)
|
||||
|
||||
return self
|
||||
|
||||
# Load parts with pagination support
|
||||
def all_filtered_paginated(
|
||||
# Same thing, one page at a time
|
||||
def paginated_filtered(
|
||||
self,
|
||||
owner_id: str | None = None,
|
||||
color_id: str | None = None,
|
||||
theme_id: str | None = None,
|
||||
year: str | None = None,
|
||||
/,
|
||||
*,
|
||||
problem_only: bool = False,
|
||||
individuals_filter: str | None = None,
|
||||
search_query: str | None = None,
|
||||
page: int = 1,
|
||||
per_page: int = 50,
|
||||
sort_field: str | None = None,
|
||||
sort_order: str = 'asc'
|
||||
sort_order: str = 'asc',
|
||||
**filters: str | None,
|
||||
) -> tuple[Self, int]:
|
||||
# Prepare filter context
|
||||
filter_context = {}
|
||||
if owner_id and owner_id != 'all':
|
||||
filter_context['owner_id'] = owner_id
|
||||
list_query = self.all_by_owner_query
|
||||
else:
|
||||
list_query = self.all_query
|
||||
context = self.filter_context(
|
||||
problem_only=problem_only,
|
||||
individuals_filter=individuals_filter,
|
||||
search_query=search_query,
|
||||
**filters
|
||||
)
|
||||
|
||||
if color_id and color_id != 'all':
|
||||
filter_context['color_id'] = color_id
|
||||
if theme_id and theme_id != 'all':
|
||||
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
|
||||
if current_app.config.get('HIDE_SPARE_PARTS', False):
|
||||
filter_context['skip_spare_parts'] = True
|
||||
|
||||
# Field mapping for sorting
|
||||
field_mapping = {
|
||||
'name': '"rebrickable_parts"."name"',
|
||||
'color': '"rebrickable_parts"."color_name"',
|
||||
'quantity': '"total_quantity"',
|
||||
'missing': '"total_missing"',
|
||||
'damaged': '"total_damaged"',
|
||||
'sets': '"total_sets"',
|
||||
'minifigures': '"total_minifigures"'
|
||||
}
|
||||
|
||||
# Use the base pagination method
|
||||
return self.paginate(
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
sort_field=sort_field,
|
||||
sort_order=sort_order,
|
||||
list_query=list_query,
|
||||
field_mapping=field_mapping,
|
||||
**filter_context
|
||||
list_query=self.filtered_query,
|
||||
field_mapping=self.field_mapping,
|
||||
**context
|
||||
)
|
||||
|
||||
# Base part list
|
||||
@@ -176,25 +225,14 @@ class BrickPartList(BrickRecordList[BrickPart]):
|
||||
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:
|
||||
context_vars['owner_id'] = self.fields.owner_id
|
||||
if hasattr(self.fields, 'color_id') and self.fields.color_id is not None:
|
||||
context_vars['color_id'] = self.fields.color_id
|
||||
if hasattr(self.fields, 'search_query') and self.fields.search_query:
|
||||
context_vars['search_query'] = self.fields.search_query
|
||||
|
||||
# Merge with any additional context passed in
|
||||
context_vars.update(context)
|
||||
|
||||
# Load the sets from the database
|
||||
# Load the sets from the database. Filters arrive in context, built by
|
||||
# filter_context, rather than being read back off self.fields.
|
||||
for record in super().select(
|
||||
override_query=override_query,
|
||||
order=order,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
**context_vars
|
||||
**context
|
||||
):
|
||||
part = BrickPart(
|
||||
brickset=brickset,
|
||||
@@ -301,102 +339,13 @@ class BrickPartList(BrickRecordList[BrickPart]):
|
||||
|
||||
return self
|
||||
|
||||
# Load problematic parts
|
||||
def problem(self, /) -> Self:
|
||||
self.list(override_query=self.problem_query)
|
||||
|
||||
return self
|
||||
|
||||
def problem_filtered(self, owner_id: str | None = None, color_id: str | None = None, theme_id: str | None = None, year: str | None = None, storage_id: str | None = None, tag_id: str | None = None, /) -> Self:
|
||||
# Save the filter parameters for client-side filtering
|
||||
if owner_id is not None:
|
||||
self.fields.owner_id = owner_id
|
||||
if color_id is not None:
|
||||
self.fields.color_id = color_id
|
||||
|
||||
# Prepare context for query
|
||||
context = {}
|
||||
if owner_id and owner_id != 'all':
|
||||
context['owner_id'] = owner_id
|
||||
if color_id and color_id != 'all':
|
||||
context['color_id'] = color_id
|
||||
if theme_id and theme_id != 'all':
|
||||
context['theme_id'] = theme_id
|
||||
if year and year != 'all':
|
||||
context['year'] = year
|
||||
if storage_id and storage_id != 'all':
|
||||
context['storage_id'] = storage_id
|
||||
if tag_id and tag_id != 'all':
|
||||
context['tag_id'] = tag_id
|
||||
# Hide spare parts from display if configured
|
||||
if current_app.config.get('HIDE_SPARE_PARTS', False):
|
||||
context['skip_spare_parts'] = True
|
||||
|
||||
# Load the problematic parts from the database
|
||||
self.list(override_query=self.problem_query, **context)
|
||||
|
||||
return self
|
||||
|
||||
def problem_paginated(
|
||||
self,
|
||||
owner_id: str | None = None,
|
||||
color_id: str | None = None,
|
||||
theme_id: str | None = None,
|
||||
year: str | None = None,
|
||||
storage_id: str | None = None,
|
||||
tag_id: str | None = None,
|
||||
search_query: str | None = None,
|
||||
page: int = 1,
|
||||
per_page: int = 50,
|
||||
sort_field: str | None = None,
|
||||
sort_order: str = 'asc'
|
||||
) -> tuple[Self, int]:
|
||||
# Prepare filter context
|
||||
filter_context = {}
|
||||
if owner_id and owner_id != 'all':
|
||||
filter_context['owner_id'] = owner_id
|
||||
if color_id and color_id != 'all':
|
||||
filter_context['color_id'] = color_id
|
||||
if theme_id and theme_id != 'all':
|
||||
filter_context['theme_id'] = theme_id
|
||||
if year and year != 'all':
|
||||
filter_context['year'] = year
|
||||
if storage_id and storage_id != 'all':
|
||||
filter_context['storage_id'] = storage_id
|
||||
if tag_id and tag_id != 'all':
|
||||
filter_context['tag_id'] = tag_id
|
||||
if search_query:
|
||||
filter_context['search_query'] = search_query
|
||||
# Hide spare parts from display if configured
|
||||
if current_app.config.get('HIDE_SPARE_PARTS', False):
|
||||
filter_context['skip_spare_parts'] = True
|
||||
|
||||
# Field mapping for sorting
|
||||
field_mapping = {
|
||||
'name': '"rebrickable_parts"."name"',
|
||||
'color': '"rebrickable_parts"."color_name"',
|
||||
'quantity': '"total_quantity"',
|
||||
'missing': '"total_missing"',
|
||||
'damaged': '"total_damaged"',
|
||||
'sets': '"total_sets"',
|
||||
'minifigures': '"total_minifigures"'
|
||||
}
|
||||
|
||||
# Use the base pagination method with problem query
|
||||
return self.paginate(
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
sort_field=sort_field,
|
||||
sort_order=sort_order,
|
||||
list_query=self.problem_query,
|
||||
field_mapping=field_mapping,
|
||||
**filter_context
|
||||
)
|
||||
|
||||
# Return a dict with common SQL parameters for a parts list
|
||||
def sql_parameters(self, /) -> dict[str, Any]:
|
||||
parameters: dict[str, Any] = super().sql_parameters()
|
||||
|
||||
# Filter values that are data rather than column names
|
||||
parameters.update(self.filter_parameters)
|
||||
|
||||
# Set id
|
||||
if self.brickset is not None:
|
||||
parameters['id'] = self.brickset.fields.id
|
||||
|
||||
@@ -119,7 +119,12 @@ class BrickRecordList(Generic[T]):
|
||||
# Wrap in COUNT(*)
|
||||
wrapped_sql = f"SELECT COUNT(*) as total_count FROM ({count_sql.strip()})"
|
||||
|
||||
count_result = BrickSQL().raw_execute(wrapped_sql, {}).fetchone()
|
||||
# Same bound values as the list query, otherwise any named placeholder
|
||||
# in the filters blows up the count
|
||||
count_result = BrickSQL().raw_execute(
|
||||
wrapped_sql,
|
||||
self.sql_parameters(),
|
||||
).fetchone()
|
||||
total_count = count_result['total_count'] if count_result else 0
|
||||
|
||||
# Prepare sort order
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
{# Colours that actually occur among the parts, so the dropdown never offers one that
|
||||
cannot match. All three part sources are covered: a colour that only exists on an
|
||||
individual part or on an individual minifigure part used to be unselectable.
|
||||
|
||||
problem_only narrows it to parts with something missing or damaged. #}
|
||||
SELECT DISTINCT
|
||||
"color_id",
|
||||
"color_name",
|
||||
"color_rgb"
|
||||
FROM (
|
||||
-- Colors from set-based parts
|
||||
-- Set parts
|
||||
SELECT DISTINCT
|
||||
"rebrickable_parts"."color_id" AS "color_id",
|
||||
"rebrickable_parts"."color_name" AS "color_name",
|
||||
@@ -12,17 +17,24 @@ FROM (
|
||||
INNER JOIN "bricktracker_parts"
|
||||
ON "bricktracker_parts"."part" IS NOT DISTINCT FROM "rebrickable_parts"."part"
|
||||
AND "bricktracker_parts"."color" IS NOT DISTINCT FROM "rebrickable_parts"."color_id"
|
||||
{% if owner_id and owner_id != 'all' %}
|
||||
INNER JOIN "bricktracker_sets"
|
||||
ON "bricktracker_parts"."id" IS NOT DISTINCT FROM "bricktracker_sets"."id"
|
||||
{% if owner_id %}
|
||||
INNER JOIN "bricktracker_set_owners"
|
||||
ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_owners"."id"
|
||||
WHERE "bricktracker_set_owners"."owner_{{ owner_id }}" = 1
|
||||
ON "bricktracker_parts"."id" IS NOT DISTINCT FROM "bricktracker_set_owners"."id"
|
||||
{% endif %}
|
||||
{% set set_conditions = [] %}
|
||||
{% if problem_only %}
|
||||
{% set _ = set_conditions.append('("bricktracker_parts"."missing" > 0 OR "bricktracker_parts"."damaged" > 0)') %}
|
||||
{% endif %}
|
||||
{% if owner_id %}
|
||||
{% set _ = set_conditions.append('"bricktracker_set_owners"."owner_' ~ owner_id ~ '" = 1') %}
|
||||
{% endif %}
|
||||
{% if set_conditions %}
|
||||
WHERE {{ set_conditions | join(' AND ') }}
|
||||
{% endif %}
|
||||
|
||||
UNION
|
||||
|
||||
-- Colors from individual parts
|
||||
-- Individual parts
|
||||
SELECT DISTINCT
|
||||
"rebrickable_parts"."color_id" AS "color_id",
|
||||
"rebrickable_parts"."color_name" AS "color_name",
|
||||
@@ -31,10 +43,45 @@ FROM (
|
||||
INNER JOIN "bricktracker_individual_parts"
|
||||
ON "bricktracker_individual_parts"."part" IS NOT DISTINCT FROM "rebrickable_parts"."part"
|
||||
AND "bricktracker_individual_parts"."color" IS NOT DISTINCT FROM "rebrickable_parts"."color_id"
|
||||
{% if owner_id and owner_id != 'all' %}
|
||||
{% if owner_id %}
|
||||
INNER JOIN "bricktracker_set_owners"
|
||||
ON "bricktracker_individual_parts"."id" IS NOT DISTINCT FROM "bricktracker_set_owners"."id"
|
||||
WHERE "bricktracker_set_owners"."owner_{{ owner_id }}" = 1
|
||||
{% endif %}
|
||||
{% set individual_conditions = [] %}
|
||||
{% if problem_only %}
|
||||
{% set _ = individual_conditions.append('("bricktracker_individual_parts"."missing" > 0 OR "bricktracker_individual_parts"."damaged" > 0)') %}
|
||||
{% endif %}
|
||||
{% if owner_id %}
|
||||
{% set _ = individual_conditions.append('"bricktracker_set_owners"."owner_' ~ owner_id ~ '" = 1') %}
|
||||
{% endif %}
|
||||
{% if individual_conditions %}
|
||||
WHERE {{ individual_conditions | join(' AND ') }}
|
||||
{% endif %}
|
||||
|
||||
UNION
|
||||
|
||||
-- Individual minifigure parts
|
||||
SELECT DISTINCT
|
||||
"rebrickable_parts"."color_id" AS "color_id",
|
||||
"rebrickable_parts"."color_name" AS "color_name",
|
||||
"rebrickable_parts"."color_rgb" AS "color_rgb"
|
||||
FROM "rebrickable_parts"
|
||||
INNER JOIN "bricktracker_individual_minifigure_parts"
|
||||
ON "bricktracker_individual_minifigure_parts"."part" IS NOT DISTINCT FROM "rebrickable_parts"."part"
|
||||
AND "bricktracker_individual_minifigure_parts"."color" IS NOT DISTINCT FROM "rebrickable_parts"."color_id"
|
||||
{% if owner_id %}
|
||||
INNER JOIN "bricktracker_set_owners"
|
||||
ON "bricktracker_individual_minifigure_parts"."id" IS NOT DISTINCT FROM "bricktracker_set_owners"."id"
|
||||
{% endif %}
|
||||
{% set minifigure_conditions = [] %}
|
||||
{% if problem_only %}
|
||||
{% set _ = minifigure_conditions.append('("bricktracker_individual_minifigure_parts"."missing" > 0 OR "bricktracker_individual_minifigure_parts"."damaged" > 0)') %}
|
||||
{% endif %}
|
||||
{% if owner_id %}
|
||||
{% set _ = minifigure_conditions.append('"bricktracker_set_owners"."owner_' ~ owner_id ~ '" = 1') %}
|
||||
{% endif %}
|
||||
{% if minifigure_conditions %}
|
||||
WHERE {{ minifigure_conditions | join(' AND ') }}
|
||||
{% endif %}
|
||||
)
|
||||
ORDER BY "color_name" ASC
|
||||
ORDER BY "color_name" ASC
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
SELECT DISTINCT
|
||||
"rebrickable_parts"."color_id" AS "color_id",
|
||||
"rebrickable_parts"."color_name" AS "color_name",
|
||||
"rebrickable_parts"."color_rgb" AS "color_rgb"
|
||||
FROM "rebrickable_parts"
|
||||
INNER JOIN "bricktracker_parts"
|
||||
ON "bricktracker_parts"."part" IS NOT DISTINCT FROM "rebrickable_parts"."part"
|
||||
AND "bricktracker_parts"."color" IS NOT DISTINCT FROM "rebrickable_parts"."color_id"
|
||||
{% if owner_id and owner_id != 'all' %}
|
||||
INNER JOIN "bricktracker_sets"
|
||||
ON "bricktracker_parts"."id" IS NOT DISTINCT FROM "bricktracker_sets"."id"
|
||||
INNER JOIN "bricktracker_set_owners"
|
||||
ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_owners"."id"
|
||||
{% endif %}
|
||||
WHERE ("bricktracker_parts"."missing" > 0 OR "bricktracker_parts"."damaged" > 0)
|
||||
{% if owner_id and owner_id != 'all' %}
|
||||
AND "bricktracker_set_owners"."owner_{{ owner_id }}" = 1
|
||||
{% endif %}
|
||||
ORDER BY "rebrickable_parts"."color_name" ASC
|
||||
@@ -1,69 +0,0 @@
|
||||
{% extends 'part/base/base.sql' %}
|
||||
|
||||
{% block total_missing %}
|
||||
SUM("combined"."missing") AS "total_missing",
|
||||
{% endblock %}
|
||||
|
||||
{% block total_damaged %}
|
||||
SUM("combined"."damaged") AS "total_damaged",
|
||||
{% endblock %}
|
||||
|
||||
{% block total_quantity %}
|
||||
SUM("combined"."quantity" * IFNULL("minifigure_quantities"."quantity", 1)) AS "total_quantity",
|
||||
{% endblock %}
|
||||
|
||||
{% block total_sets %}
|
||||
IFNULL(COUNT(DISTINCT CASE WHEN "combined"."source_type" = 'set' THEN "combined"."id" ELSE NULL END), 0) AS "total_sets",
|
||||
{% endblock %}
|
||||
|
||||
{% block total_minifigures %}
|
||||
SUM(IFNULL("minifigure_quantities"."quantity", 0)) AS "total_minifigures"
|
||||
{% endblock %}
|
||||
|
||||
{% block join %}
|
||||
-- Join to get minifigure quantities from both set-based and individual minifigures
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
"bricktracker_minifigures"."id",
|
||||
"bricktracker_minifigures"."figure",
|
||||
"bricktracker_minifigures"."quantity"
|
||||
FROM "bricktracker_minifigures"
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
"bricktracker_individual_minifigures"."id",
|
||||
"bricktracker_individual_minifigures"."figure",
|
||||
"bricktracker_individual_minifigures"."quantity"
|
||||
FROM "bricktracker_individual_minifigures"
|
||||
) AS "minifigure_quantities"
|
||||
ON "combined"."id" IS NOT DISTINCT FROM "minifigure_quantities"."id"
|
||||
AND "combined"."figure" IS NOT DISTINCT FROM "minifigure_quantities"."figure"
|
||||
{% endblock %}
|
||||
|
||||
{% block where %}
|
||||
{% set conditions = [] %}
|
||||
{% if color_id and color_id != 'all' %}
|
||||
{% set _ = conditions.append('"combined"."color" = ' ~ color_id) %}
|
||||
{% endif %}
|
||||
{% if search_query %}
|
||||
{% set search_condition = '(LOWER("rebrickable_parts"."name") LIKE LOWER(\'%' ~ search_query ~ '%\') OR LOWER("rebrickable_parts"."color_name") LIKE LOWER(\'%' ~ search_query ~ '%\') OR LOWER("combined"."part") LIKE LOWER(\'%' ~ search_query ~ '%\'))' %}
|
||||
{% set _ = conditions.append(search_condition) %}
|
||||
{% endif %}
|
||||
{% if skip_spare_parts %}
|
||||
{% set _ = conditions.append('"combined"."spare" = 0') %}
|
||||
{% endif %}
|
||||
{% if individuals_filter %}
|
||||
{% set _ = conditions.append('"combined"."source_type" = \'individual_part\'') %}
|
||||
{% endif %}
|
||||
{% if conditions %}
|
||||
WHERE {{ conditions | join(' AND ') }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block group %}
|
||||
GROUP BY
|
||||
"combined"."part",
|
||||
"combined"."color",
|
||||
"combined"."spare"
|
||||
{% endblock %}
|
||||
@@ -1,146 +0,0 @@
|
||||
{% extends 'part/base/base.sql' %}
|
||||
|
||||
{% block total_missing %}
|
||||
{% if owner_id and owner_id != 'all' %}
|
||||
SUM(CASE
|
||||
WHEN "combined"."source_type" = 'set' AND "bricktracker_set_owners"."owner_{{ owner_id }}" = 1 THEN "combined"."missing"
|
||||
WHEN "combined"."source_type" = 'individual_minifigure' AND "individual_minifigure_owners"."owner_{{ owner_id }}" = 1 THEN "combined"."missing"
|
||||
WHEN "combined"."source_type" = 'individual_part' AND ("individual_part_owners"."owner_{{ owner_id }}" = 1 OR "individual_part_lot_owners"."owner_{{ owner_id }}" = 1) THEN "combined"."missing"
|
||||
ELSE 0
|
||||
END) AS "total_missing",
|
||||
{% else %}
|
||||
SUM("combined"."missing") AS "total_missing",
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block total_damaged %}
|
||||
{% if owner_id and owner_id != 'all' %}
|
||||
SUM(CASE
|
||||
WHEN "combined"."source_type" = 'set' AND "bricktracker_set_owners"."owner_{{ owner_id }}" = 1 THEN "combined"."damaged"
|
||||
WHEN "combined"."source_type" = 'individual_minifigure' AND "individual_minifigure_owners"."owner_{{ owner_id }}" = 1 THEN "combined"."damaged"
|
||||
WHEN "combined"."source_type" = 'individual_part' AND ("individual_part_owners"."owner_{{ owner_id }}" = 1 OR "individual_part_lot_owners"."owner_{{ owner_id }}" = 1) THEN "combined"."damaged"
|
||||
ELSE 0
|
||||
END) AS "total_damaged",
|
||||
{% else %}
|
||||
SUM("combined"."damaged") AS "total_damaged",
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block total_quantity %}
|
||||
{% if owner_id and owner_id != 'all' %}
|
||||
SUM(CASE
|
||||
WHEN "combined"."source_type" = 'set' AND "bricktracker_set_owners"."owner_{{ owner_id }}" = 1 THEN "combined"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)
|
||||
WHEN "combined"."source_type" = 'individual_minifigure' AND "individual_minifigure_owners"."owner_{{ owner_id }}" = 1 THEN "combined"."quantity"
|
||||
WHEN "combined"."source_type" = 'individual_part' AND ("individual_part_owners"."owner_{{ owner_id }}" = 1 OR "individual_part_lot_owners"."owner_{{ owner_id }}" = 1) THEN "combined"."quantity"
|
||||
ELSE 0
|
||||
END) AS "total_quantity",
|
||||
{% else %}
|
||||
SUM(CASE
|
||||
WHEN "combined"."source_type" = 'set' THEN "combined"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)
|
||||
ELSE "combined"."quantity"
|
||||
END) AS "total_quantity",
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block total_sets %}
|
||||
{% if owner_id and owner_id != 'all' %}
|
||||
COUNT(DISTINCT CASE WHEN "combined"."source_type" = 'set' AND "bricktracker_set_owners"."owner_{{ owner_id }}" = 1 THEN "combined"."id" ELSE NULL END) AS "total_sets",
|
||||
{% else %}
|
||||
COUNT(DISTINCT CASE WHEN "combined"."source_type" = 'set' THEN "combined"."id" ELSE NULL END) AS "total_sets",
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block total_minifigures %}
|
||||
{% if owner_id and owner_id != 'all' %}
|
||||
SUM(CASE
|
||||
WHEN "combined"."source_type" = 'set' AND "bricktracker_set_owners"."owner_{{ owner_id }}" = 1 THEN IFNULL("bricktracker_minifigures"."quantity", 0)
|
||||
WHEN "combined"."source_type" = 'individual_minifigure' AND "individual_minifigure_owners"."owner_{{ owner_id }}" = 1 THEN 1
|
||||
ELSE 0
|
||||
END) AS "total_minifigures"
|
||||
{% else %}
|
||||
SUM(CASE
|
||||
WHEN "combined"."source_type" = 'set' THEN IFNULL("bricktracker_minifigures"."quantity", 0)
|
||||
WHEN "combined"."source_type" = 'individual_minifigure' THEN 1
|
||||
ELSE 0
|
||||
END) AS "total_minifigures"
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block join %}
|
||||
-- Left join with sets (for set-based parts)
|
||||
LEFT JOIN "bricktracker_sets"
|
||||
ON "combined"."source_type" = 'set'
|
||||
AND "combined"."id" IS NOT DISTINCT FROM "bricktracker_sets"."id"
|
||||
|
||||
-- Left join with set owners (using dynamic columns)
|
||||
LEFT JOIN "bricktracker_set_owners"
|
||||
ON "combined"."source_type" = 'set'
|
||||
AND "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_owners"."id"
|
||||
|
||||
-- Left join with set-based minifigures
|
||||
LEFT JOIN "bricktracker_minifigures"
|
||||
ON "combined"."source_type" = 'set'
|
||||
AND "combined"."id" IS NOT DISTINCT FROM "bricktracker_minifigures"."id"
|
||||
AND "combined"."figure" IS NOT DISTINCT FROM "bricktracker_minifigures"."figure"
|
||||
|
||||
-- Left join with individual minifigures (for individual minifigure parts)
|
||||
LEFT JOIN "bricktracker_individual_minifigures"
|
||||
ON "combined"."source_type" = 'individual_minifigure'
|
||||
AND "combined"."id" IS NOT DISTINCT FROM "bricktracker_individual_minifigures"."id"
|
||||
|
||||
-- Left join with set owners for individual minifigures (using dynamic columns) - reuse set_owners table
|
||||
LEFT JOIN "bricktracker_set_owners" AS "individual_minifigure_owners"
|
||||
ON "combined"."source_type" = 'individual_minifigure'
|
||||
AND "bricktracker_individual_minifigures"."id" IS NOT DISTINCT FROM "individual_minifigure_owners"."id"
|
||||
|
||||
-- Left join with individual parts (for standalone parts and lot parts)
|
||||
LEFT JOIN "bricktracker_individual_parts"
|
||||
ON "combined"."source_type" = 'individual_part'
|
||||
AND "combined"."id" IS NOT DISTINCT FROM "bricktracker_individual_parts"."id"
|
||||
|
||||
-- Left join with set owners for individual parts (using dynamic columns) - for standalone parts
|
||||
LEFT JOIN "bricktracker_set_owners" AS "individual_part_owners"
|
||||
ON "combined"."source_type" = 'individual_part'
|
||||
AND "bricktracker_individual_parts"."id" IS NOT DISTINCT FROM "individual_part_owners"."id"
|
||||
|
||||
-- Left join with individual part lots (for parts belonging to a lot)
|
||||
LEFT JOIN "bricktracker_individual_part_lots"
|
||||
ON "combined"."source_type" = 'individual_part'
|
||||
AND "bricktracker_individual_parts"."lot_id" IS NOT DISTINCT FROM "bricktracker_individual_part_lots"."id"
|
||||
|
||||
-- Left join with set owners for individual part lots (using dynamic columns)
|
||||
LEFT JOIN "bricktracker_set_owners" AS "individual_part_lot_owners"
|
||||
ON "combined"."source_type" = 'individual_part'
|
||||
AND "bricktracker_individual_part_lots"."id" IS NOT DISTINCT FROM "individual_part_lot_owners"."id"
|
||||
{% endblock %}
|
||||
|
||||
{% block where %}
|
||||
{% set conditions = [] %}
|
||||
{% if owner_id and owner_id != 'all' %}
|
||||
{% set owner_condition = '(("combined"."source_type" = \'set\' AND "bricktracker_set_owners"."owner_' ~ owner_id ~ '" = 1) OR ("combined"."source_type" = \'individual_minifigure\' AND "individual_minifigure_owners"."owner_' ~ owner_id ~ '" = 1) OR ("combined"."source_type" = \'individual_part\' AND ("individual_part_owners"."owner_' ~ owner_id ~ '" = 1 OR "individual_part_lot_owners"."owner_' ~ owner_id ~ '" = 1)))' %}
|
||||
{% set _ = conditions.append(owner_condition) %}
|
||||
{% endif %}
|
||||
{% if color_id and color_id != 'all' %}
|
||||
{% set _ = conditions.append('"combined"."color" = ' ~ color_id) %}
|
||||
{% endif %}
|
||||
{% if search_query %}
|
||||
{% set search_condition = '(LOWER("rebrickable_parts"."name") LIKE LOWER(\'%' ~ search_query ~ '%\') OR LOWER("rebrickable_parts"."color_name") LIKE LOWER(\'%' ~ search_query ~ '%\') OR LOWER("combined"."part") LIKE LOWER(\'%' ~ search_query ~ '%\'))' %}
|
||||
{% set _ = conditions.append(search_condition) %}
|
||||
{% endif %}
|
||||
{% if skip_spare_parts %}
|
||||
{% set _ = conditions.append('"combined"."spare" = 0') %}
|
||||
{% endif %}
|
||||
{% if individuals_filter %}
|
||||
{% set _ = conditions.append('"combined"."source_type" = \'individual_part\'') %}
|
||||
{% endif %}
|
||||
{% if conditions %}
|
||||
WHERE {{ conditions | join(' AND ') }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block group %}
|
||||
GROUP BY
|
||||
"combined"."part",
|
||||
"combined"."color",
|
||||
"combined"."spare"
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,193 @@
|
||||
{#
|
||||
The one filtered parts list. Used by /parts and by /parts/problem, which passes
|
||||
problem_only.
|
||||
|
||||
Filters: owner, color, theme, year, storage, tag, status, plus search, spare parts
|
||||
and individuals only. Every one of them is a single WHERE condition. There is no
|
||||
CASE inside the aggregates on purpose: WHERE runs before GROUP BY, so a row that
|
||||
fails a filter never reaches the SUM in the first place.
|
||||
|
||||
Joins are pulled in only when a filter actually needs them, so an unfiltered page
|
||||
costs the same as it did before.
|
||||
|
||||
owner, status and tag become column names, so they cannot be bound as parameters.
|
||||
The view validates them against the known metadata ids before they get here.
|
||||
#}
|
||||
{% extends 'part/base/base.sql' %}
|
||||
|
||||
{% block total_missing %}
|
||||
SUM("combined"."missing") AS "total_missing",
|
||||
{% endblock %}
|
||||
|
||||
{% block total_damaged %}
|
||||
SUM("combined"."damaged") AS "total_damaged",
|
||||
{% endblock %}
|
||||
|
||||
{% block total_quantity %}
|
||||
SUM("combined"."quantity" * IFNULL("minifigure_quantities"."quantity", 1)) AS "total_quantity",
|
||||
{% endblock %}
|
||||
|
||||
{% block total_sets %}
|
||||
IFNULL(COUNT(DISTINCT CASE WHEN "combined"."source_type" = 'set' THEN "combined"."id" ELSE NULL END), 0) AS "total_sets",
|
||||
{% endblock %}
|
||||
|
||||
{% block total_minifigures %}
|
||||
SUM(IFNULL("minifigure_quantities"."quantity", 0)) AS "total_minifigures"
|
||||
{% endblock %}
|
||||
|
||||
{% block join %}
|
||||
-- Minifigure quantities, from set minifigures and individual ones alike
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
"bricktracker_minifigures"."id",
|
||||
"bricktracker_minifigures"."figure",
|
||||
"bricktracker_minifigures"."quantity"
|
||||
FROM "bricktracker_minifigures"
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
"bricktracker_individual_minifigures"."id",
|
||||
"bricktracker_individual_minifigures"."figure",
|
||||
"bricktracker_individual_minifigures"."quantity"
|
||||
FROM "bricktracker_individual_minifigures"
|
||||
) AS "minifigure_quantities"
|
||||
ON "combined"."id" IS NOT DISTINCT FROM "minifigure_quantities"."id"
|
||||
AND "combined"."figure" IS NOT DISTINCT FROM "minifigure_quantities"."figure"
|
||||
|
||||
{% if theme_id or year or storage_id %}
|
||||
-- Sets, for theme, year and set level storage
|
||||
LEFT JOIN "bricktracker_sets"
|
||||
ON "combined"."source_type" = 'set'
|
||||
AND "combined"."id" IS NOT DISTINCT FROM "bricktracker_sets"."id"
|
||||
{% endif %}
|
||||
|
||||
{% if theme_id or year %}
|
||||
LEFT JOIN "rebrickable_sets"
|
||||
ON "bricktracker_sets"."set" IS NOT DISTINCT FROM "rebrickable_sets"."set"
|
||||
{% endif %}
|
||||
|
||||
{% if storage_id %}
|
||||
-- Individual minifigures carry their own storage
|
||||
LEFT JOIN "bricktracker_individual_minifigures"
|
||||
ON "combined"."source_type" = 'individual_minifigure'
|
||||
AND "combined"."id" IS NOT DISTINCT FROM "bricktracker_individual_minifigures"."id"
|
||||
{% endif %}
|
||||
|
||||
{% if storage_id or owner_id %}
|
||||
-- Individual parts, and the lot they may belong to. A part with no storage or owner
|
||||
-- of its own inherits the lot's.
|
||||
LEFT JOIN "bricktracker_individual_parts"
|
||||
ON "combined"."source_type" = 'individual_part'
|
||||
AND "combined"."id" IS NOT DISTINCT FROM "bricktracker_individual_parts"."id"
|
||||
|
||||
LEFT JOIN "bricktracker_individual_part_lots"
|
||||
ON "bricktracker_individual_parts"."lot_id" IS NOT DISTINCT FROM "bricktracker_individual_part_lots"."id"
|
||||
{% endif %}
|
||||
|
||||
{% if owner_id %}
|
||||
-- Owners, statuses and tags all live in the set metadata tables keyed by item id,
|
||||
-- shared by sets, individual minifigures and individual parts, so no source check.
|
||||
LEFT JOIN "bricktracker_set_owners"
|
||||
ON "combined"."id" IS NOT DISTINCT FROM "bricktracker_set_owners"."id"
|
||||
|
||||
LEFT JOIN "bricktracker_set_owners" AS "lot_owners"
|
||||
ON "bricktracker_individual_part_lots"."id" IS NOT DISTINCT FROM "lot_owners"."id"
|
||||
{% endif %}
|
||||
|
||||
{% if status_id %}
|
||||
LEFT JOIN "bricktracker_set_statuses"
|
||||
ON "combined"."id" IS NOT DISTINCT FROM "bricktracker_set_statuses"."id"
|
||||
{% endif %}
|
||||
|
||||
{% if tag_id %}
|
||||
LEFT JOIN "bricktracker_set_tags"
|
||||
ON "combined"."id" IS NOT DISTINCT FROM "bricktracker_set_tags"."id"
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block where %}
|
||||
{% set conditions = [] %}
|
||||
|
||||
{% if problem_only %}
|
||||
{% set _ = conditions.append('("combined"."missing" > 0 OR "combined"."damaged" > 0)') %}
|
||||
{% endif %}
|
||||
|
||||
{% if skip_spare_parts %}
|
||||
{% set _ = conditions.append('"combined"."spare" = 0') %}
|
||||
{% endif %}
|
||||
|
||||
{% if individuals_filter %}
|
||||
{% set _ = conditions.append('"combined"."source_type" = \'individual_part\'') %}
|
||||
{% endif %}
|
||||
|
||||
{% if color_id %}
|
||||
{% set _ = conditions.append('"combined"."color" = :color_id') %}
|
||||
{% endif %}
|
||||
|
||||
{% if search_query %}
|
||||
{% set _ = conditions.append('(LOWER("rebrickable_parts"."name") LIKE :search_query OR LOWER("rebrickable_parts"."color_name") LIKE :search_query OR LOWER("combined"."part") LIKE :search_query)') %}
|
||||
{% endif %}
|
||||
|
||||
{# Theme and year only exist for set sourced parts. The LEFT JOIN gives NULL for
|
||||
everything else, and NULL = x is never true, so individual parts drop out on
|
||||
their own. #}
|
||||
{% if theme_id %}
|
||||
{% set _ = conditions.append('"rebrickable_sets"."theme_id" = :theme_id') %}
|
||||
{% endif %}
|
||||
|
||||
{% if year %}
|
||||
{% set _ = conditions.append('"rebrickable_sets"."year" = :year') %}
|
||||
{% endif %}
|
||||
|
||||
{% if storage_id %}
|
||||
{% set storage_value = 'COALESCE("bricktracker_sets"."storage", "bricktracker_individual_minifigures"."storage", "bricktracker_individual_parts"."storage", "bricktracker_individual_part_lots"."storage")' %}
|
||||
{% if storage_id == '__none__' %}
|
||||
{% set _ = conditions.append(storage_value ~ ' IS NULL') %}
|
||||
{% elif storage_id == '-__none__' %}
|
||||
{% set _ = conditions.append(storage_value ~ ' IS NOT NULL') %}
|
||||
{% elif storage_id.startswith('-') %}
|
||||
{# IS NOT rather than <> so parts with no storage at all still count as "not this one" #}
|
||||
{% set _ = conditions.append(storage_value ~ ' IS NOT :storage_id') %}
|
||||
{% else %}
|
||||
{% set _ = conditions.append(storage_value ~ ' IS :storage_id') %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if owner_id %}
|
||||
{% if owner_id.startswith('-') %}
|
||||
{% set _ = conditions.append('(IFNULL("bricktracker_set_owners"."owner_' ~ owner_id[1:] ~ '", 0) = 0 AND IFNULL("lot_owners"."owner_' ~ owner_id[1:] ~ '", 0) = 0)') %}
|
||||
{% else %}
|
||||
{% set _ = conditions.append('("bricktracker_set_owners"."owner_' ~ owner_id ~ '" = 1 OR "lot_owners"."owner_' ~ owner_id ~ '" = 1)') %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{# Negated forms use IFNULL so an item with no metadata row counts as not having the
|
||||
status or tag, which is what "everything except assembled" means. #}
|
||||
{% if status_id %}
|
||||
{% if status_id.startswith('-') %}
|
||||
{% set _ = conditions.append('IFNULL("bricktracker_set_statuses"."status_' ~ status_id[1:] ~ '", 0) = 0') %}
|
||||
{% else %}
|
||||
{% set _ = conditions.append('"bricktracker_set_statuses"."status_' ~ status_id ~ '" = 1') %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if tag_id %}
|
||||
{% if tag_id.startswith('-') %}
|
||||
{% set _ = conditions.append('IFNULL("bricktracker_set_tags"."tag_' ~ tag_id[1:] ~ '", 0) = 0') %}
|
||||
{% else %}
|
||||
{% set _ = conditions.append('"bricktracker_set_tags"."tag_' ~ tag_id ~ '" = 1') %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if conditions %}
|
||||
WHERE {{ conditions | join(' AND ') }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block group %}
|
||||
GROUP BY
|
||||
"combined"."part",
|
||||
"combined"."color",
|
||||
"combined"."spare"
|
||||
{% endblock %}
|
||||
@@ -1,119 +0,0 @@
|
||||
{% extends 'part/base/base.sql' %}
|
||||
|
||||
{% block total_missing %}
|
||||
{% if owner_id and owner_id != 'all' %}
|
||||
SUM(CASE
|
||||
WHEN "combined"."source_type" = 'set' AND "bricktracker_set_owners"."owner_{{ owner_id }}" = 1 THEN "combined"."missing"
|
||||
WHEN "combined"."source_type" = 'individual' AND "ind_minifig_owners"."owner_{{ owner_id }}" = 1 THEN "combined"."missing"
|
||||
ELSE 0
|
||||
END) AS "total_missing",
|
||||
{% else %}
|
||||
SUM("combined"."missing") AS "total_missing",
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block total_damaged %}
|
||||
{% if owner_id and owner_id != 'all' %}
|
||||
SUM(CASE
|
||||
WHEN "combined"."source_type" = 'set' AND "bricktracker_set_owners"."owner_{{ owner_id }}" = 1 THEN "combined"."damaged"
|
||||
WHEN "combined"."source_type" = 'individual' AND "ind_minifig_owners"."owner_{{ owner_id }}" = 1 THEN "combined"."damaged"
|
||||
ELSE 0
|
||||
END) AS "total_damaged",
|
||||
{% else %}
|
||||
SUM("combined"."damaged") AS "total_damaged",
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block total_quantity %}
|
||||
{% if owner_id and owner_id != 'all' %}
|
||||
SUM(CASE
|
||||
WHEN "combined"."source_type" = 'set' AND "bricktracker_set_owners"."owner_{{ owner_id }}" = 1 THEN "combined"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)
|
||||
WHEN "combined"."source_type" = 'individual' AND "ind_minifig_owners"."owner_{{ owner_id }}" = 1 THEN "combined"."quantity" * IFNULL("bricktracker_individual_minifigures"."quantity", 1)
|
||||
ELSE 0
|
||||
END) AS "total_quantity",
|
||||
{% else %}
|
||||
SUM(CASE
|
||||
WHEN "combined"."source_type" = 'set' THEN "combined"."quantity" * IFNULL("bricktracker_minifigures"."quantity", 1)
|
||||
WHEN "combined"."source_type" = 'individual' THEN "combined"."quantity" * IFNULL("bricktracker_individual_minifigures"."quantity", 1)
|
||||
ELSE "combined"."quantity"
|
||||
END) AS "total_quantity",
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block total_sets %}
|
||||
{% if owner_id and owner_id != 'all' %}
|
||||
COUNT(DISTINCT CASE WHEN "combined"."source_type" = 'set' AND "bricktracker_set_owners"."owner_{{ owner_id }}" = 1 THEN "combined"."id" ELSE NULL END) AS "total_sets",
|
||||
{% else %}
|
||||
COUNT(DISTINCT CASE WHEN "combined"."source_type" = 'set' THEN "combined"."id" ELSE NULL END) AS "total_sets",
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block total_minifigures %}
|
||||
{% if owner_id and owner_id != 'all' %}
|
||||
SUM(CASE
|
||||
WHEN "combined"."source_type" = 'set' AND "bricktracker_set_owners"."owner_{{ owner_id }}" = 1 THEN IFNULL("bricktracker_minifigures"."quantity", 0)
|
||||
WHEN "combined"."source_type" = 'individual' AND "ind_minifig_owners"."owner_{{ owner_id }}" = 1 THEN IFNULL("bricktracker_individual_minifigures"."quantity", 0)
|
||||
ELSE 0
|
||||
END) AS "total_minifigures"
|
||||
{% else %}
|
||||
SUM(CASE
|
||||
WHEN "combined"."source_type" = 'set' THEN IFNULL("bricktracker_minifigures"."quantity", 0)
|
||||
WHEN "combined"."source_type" = 'individual' THEN IFNULL("bricktracker_individual_minifigures"."quantity", 0)
|
||||
ELSE 0
|
||||
END) AS "total_minifigures"
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block join %}
|
||||
-- Left join with sets for set-based parts
|
||||
LEFT JOIN "bricktracker_sets"
|
||||
ON "combined"."source_type" = 'set'
|
||||
AND "combined"."id" IS NOT DISTINCT FROM "bricktracker_sets"."id"
|
||||
|
||||
-- Left join with set owners (using dynamic columns)
|
||||
LEFT JOIN "bricktracker_set_owners"
|
||||
ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_owners"."id"
|
||||
|
||||
-- Left join with set-based minifigures
|
||||
LEFT JOIN "bricktracker_minifigures"
|
||||
ON "combined"."source_type" = 'set'
|
||||
AND "combined"."id" IS NOT DISTINCT FROM "bricktracker_minifigures"."id"
|
||||
AND "combined"."figure" IS NOT DISTINCT FROM "bricktracker_minifigures"."figure"
|
||||
|
||||
-- Left join with individual minifigures
|
||||
LEFT JOIN "bricktracker_individual_minifigures"
|
||||
ON "combined"."source_type" = 'individual'
|
||||
AND "combined"."id" IS NOT DISTINCT FROM "bricktracker_individual_minifigures"."id"
|
||||
|
||||
-- Left join with individual minifigure owners (using consolidated metadata table)
|
||||
LEFT JOIN "bricktracker_set_owners" AS "ind_minifig_owners"
|
||||
ON "bricktracker_individual_minifigures"."id" IS NOT DISTINCT FROM "ind_minifig_owners"."id"
|
||||
{% endblock %}
|
||||
|
||||
{% block where %}
|
||||
{% set conditions = [] %}
|
||||
-- Always filter for problematic parts
|
||||
{% set _ = conditions.append('("combined"."missing" > 0 OR "combined"."damaged" > 0)') %}
|
||||
{% if owner_id and owner_id != 'all' %}
|
||||
{% set owner_condition = '(("combined"."source_type" = \'set\' AND "bricktracker_set_owners"."owner_' ~ owner_id ~ '" = 1) OR ("combined"."source_type" = \'individual\' AND "ind_minifig_owners"."owner_' ~ owner_id ~ '" = 1))' %}
|
||||
{% set _ = conditions.append(owner_condition) %}
|
||||
{% endif %}
|
||||
{% if color_id and color_id != 'all' %}
|
||||
{% set _ = conditions.append('"combined"."color" = ' ~ color_id) %}
|
||||
{% endif %}
|
||||
{% if search_query %}
|
||||
{% set search_condition = '(LOWER("rebrickable_parts"."name") LIKE LOWER(\'%' ~ search_query ~ '%\') OR LOWER("rebrickable_parts"."color_name") LIKE LOWER(\'%' ~ search_query ~ '%\') OR LOWER("combined"."part") LIKE LOWER(\'%' ~ search_query ~ '%\'))' %}
|
||||
{% set _ = conditions.append(search_condition) %}
|
||||
{% endif %}
|
||||
{% if skip_spare_parts %}
|
||||
{% set _ = conditions.append('"combined"."spare" = 0') %}
|
||||
{% endif %}
|
||||
WHERE {{ conditions | join(' AND ') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block group %}
|
||||
GROUP BY
|
||||
"combined"."part",
|
||||
"combined"."color",
|
||||
"combined"."spare"
|
||||
{% endblock %}
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
{# Compute per part+color set/minifigure counts so each sub-card shows its own
|
||||
totals (fixes #159: they used to inherit the parent part's counts). Mirrors
|
||||
part/list/all.sql. #}
|
||||
part/list/filtered.sql. #}
|
||||
{% block total_sets %}
|
||||
IFNULL(COUNT(DISTINCT CASE WHEN "combined"."source_type" = 'set' THEN "combined"."id" ELSE NULL END), 0) AS "total_sets",
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
-- Get distinct storages from problem parts' sets
|
||||
SELECT DISTINCT
|
||||
"bricktracker_sets"."storage" AS "storage_id",
|
||||
"bricktracker_metadata_storages"."name" AS "storage_name",
|
||||
COUNT(DISTINCT "bricktracker_parts"."part") as "part_count"
|
||||
FROM "bricktracker_parts"
|
||||
INNER JOIN "bricktracker_sets"
|
||||
ON "bricktracker_parts"."id" IS NOT DISTINCT FROM "bricktracker_sets"."id"
|
||||
LEFT JOIN "bricktracker_metadata_storages"
|
||||
ON "bricktracker_sets"."storage" IS NOT DISTINCT FROM "bricktracker_metadata_storages"."id"
|
||||
{% if owner_id and owner_id != 'all' %}
|
||||
INNER JOIN "bricktracker_set_owners"
|
||||
ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_owners"."id"
|
||||
{% endif %}
|
||||
WHERE ("bricktracker_parts"."missing" > 0 OR "bricktracker_parts"."damaged" > 0)
|
||||
AND "bricktracker_sets"."storage" IS NOT NULL
|
||||
{% if owner_id and owner_id != 'all' %}
|
||||
AND "bricktracker_set_owners"."owner_{{ owner_id }}" = 1
|
||||
{% endif %}
|
||||
GROUP BY "bricktracker_sets"."storage", "bricktracker_metadata_storages"."name"
|
||||
ORDER BY "bricktracker_metadata_storages"."name" ASC
|
||||
@@ -1,7 +0,0 @@
|
||||
-- Get list of all tags (simplified - filtering happens at application level)
|
||||
-- Tags use dynamic columns in bricktracker_set_tags, making direct SQL filtering complex
|
||||
SELECT
|
||||
"bricktracker_metadata_tags"."id" AS "tag_id",
|
||||
"bricktracker_metadata_tags"."name" AS "tag_name"
|
||||
FROM "bricktracker_metadata_tags"
|
||||
ORDER BY "bricktracker_metadata_tags"."name" ASC
|
||||
@@ -1,4 +1,6 @@
|
||||
-- Get distinct themes from parts' sets
|
||||
{# Themes present among the parts. Only set sourced parts have a theme, so individual
|
||||
parts and individual minifigure parts are out of scope here by definition.
|
||||
problem_only narrows it to parts with something missing or damaged. #}
|
||||
SELECT DISTINCT
|
||||
"rebrickable_sets"."theme_id",
|
||||
COUNT(DISTINCT "bricktracker_parts"."part") as "part_count"
|
||||
@@ -7,10 +9,19 @@ INNER JOIN "bricktracker_sets"
|
||||
ON "bricktracker_parts"."id" IS NOT DISTINCT FROM "bricktracker_sets"."id"
|
||||
INNER JOIN "rebrickable_sets"
|
||||
ON "bricktracker_sets"."set" IS NOT DISTINCT FROM "rebrickable_sets"."set"
|
||||
{% if owner_id and owner_id != 'all' %}
|
||||
{% if owner_id %}
|
||||
INNER JOIN "bricktracker_set_owners"
|
||||
ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_owners"."id"
|
||||
WHERE "bricktracker_set_owners"."owner_{{ owner_id }}" = 1
|
||||
{% endif %}
|
||||
{% set conditions = [] %}
|
||||
{% if problem_only %}
|
||||
{% set _ = conditions.append('("bricktracker_parts"."missing" > 0 OR "bricktracker_parts"."damaged" > 0)') %}
|
||||
{% endif %}
|
||||
{% if owner_id %}
|
||||
{% set _ = conditions.append('"bricktracker_set_owners"."owner_' ~ owner_id ~ '" = 1') %}
|
||||
{% endif %}
|
||||
{% if conditions %}
|
||||
WHERE {{ conditions | join(' AND ') }}
|
||||
{% endif %}
|
||||
GROUP BY "rebrickable_sets"."theme_id"
|
||||
ORDER BY "rebrickable_sets"."theme_id" ASC
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
-- Get distinct themes from problem parts' sets
|
||||
SELECT DISTINCT
|
||||
"rebrickable_sets"."theme_id",
|
||||
COUNT(DISTINCT "bricktracker_parts"."part") as "part_count"
|
||||
FROM "bricktracker_parts"
|
||||
INNER JOIN "bricktracker_sets"
|
||||
ON "bricktracker_parts"."id" IS NOT DISTINCT FROM "bricktracker_sets"."id"
|
||||
INNER JOIN "rebrickable_sets"
|
||||
ON "bricktracker_sets"."set" IS NOT DISTINCT FROM "rebrickable_sets"."set"
|
||||
{% if owner_id and owner_id != 'all' %}
|
||||
INNER JOIN "bricktracker_set_owners"
|
||||
ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_owners"."id"
|
||||
{% endif %}
|
||||
WHERE ("bricktracker_parts"."missing" > 0 OR "bricktracker_parts"."damaged" > 0)
|
||||
{% if owner_id and owner_id != 'all' %}
|
||||
AND "bricktracker_set_owners"."owner_{{ owner_id }}" = 1
|
||||
{% endif %}
|
||||
GROUP BY "rebrickable_sets"."theme_id"
|
||||
ORDER BY "rebrickable_sets"."theme_id" ASC
|
||||
@@ -1,4 +1,6 @@
|
||||
-- Get distinct years from parts' sets
|
||||
{# Years present among the parts. Only set sourced parts have a year, so individual
|
||||
parts and individual minifigure parts are out of scope here by definition.
|
||||
problem_only narrows it to parts with something missing or damaged. #}
|
||||
SELECT DISTINCT
|
||||
"rebrickable_sets"."year",
|
||||
COUNT(DISTINCT "bricktracker_parts"."part") as "part_count"
|
||||
@@ -7,10 +9,19 @@ INNER JOIN "bricktracker_sets"
|
||||
ON "bricktracker_parts"."id" IS NOT DISTINCT FROM "bricktracker_sets"."id"
|
||||
INNER JOIN "rebrickable_sets"
|
||||
ON "bricktracker_sets"."set" IS NOT DISTINCT FROM "rebrickable_sets"."set"
|
||||
{% if owner_id and owner_id != 'all' %}
|
||||
{% if owner_id %}
|
||||
INNER JOIN "bricktracker_set_owners"
|
||||
ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_owners"."id"
|
||||
WHERE "bricktracker_set_owners"."owner_{{ owner_id }}" = 1
|
||||
{% endif %}
|
||||
{% set conditions = [] %}
|
||||
{% if problem_only %}
|
||||
{% set _ = conditions.append('("bricktracker_parts"."missing" > 0 OR "bricktracker_parts"."damaged" > 0)') %}
|
||||
{% endif %}
|
||||
{% if owner_id %}
|
||||
{% set _ = conditions.append('"bricktracker_set_owners"."owner_' ~ owner_id ~ '" = 1') %}
|
||||
{% endif %}
|
||||
{% if conditions %}
|
||||
WHERE {{ conditions | join(' AND ') }}
|
||||
{% endif %}
|
||||
GROUP BY "rebrickable_sets"."year"
|
||||
ORDER BY "rebrickable_sets"."year" DESC
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
-- Get distinct years from problem parts' sets
|
||||
SELECT DISTINCT
|
||||
"rebrickable_sets"."year",
|
||||
COUNT(DISTINCT "bricktracker_parts"."part") as "part_count"
|
||||
FROM "bricktracker_parts"
|
||||
INNER JOIN "bricktracker_sets"
|
||||
ON "bricktracker_parts"."id" IS NOT DISTINCT FROM "bricktracker_sets"."id"
|
||||
INNER JOIN "rebrickable_sets"
|
||||
ON "bricktracker_sets"."set" IS NOT DISTINCT FROM "rebrickable_sets"."set"
|
||||
{% if owner_id and owner_id != 'all' %}
|
||||
INNER JOIN "bricktracker_set_owners"
|
||||
ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_owners"."id"
|
||||
{% endif %}
|
||||
WHERE ("bricktracker_parts"."missing" > 0 OR "bricktracker_parts"."damaged" > 0)
|
||||
{% if owner_id and owner_id != 'all' %}
|
||||
AND "bricktracker_set_owners"."owner_{{ owner_id }}" = 1
|
||||
{% endif %}
|
||||
GROUP BY "rebrickable_sets"."year"
|
||||
ORDER BY "rebrickable_sets"."year" DESC
|
||||
@@ -63,7 +63,9 @@ def export_parts_rebrickable() -> Response:
|
||||
year = request.args.get('year')
|
||||
|
||||
part_list = BrickPartList()
|
||||
part_list.all_filtered(owner_id, color_id, theme_id, year)
|
||||
part_list.filtered(
|
||||
owner_id=owner_id, color_id=color_id, theme_id=theme_id, year=year
|
||||
)
|
||||
|
||||
part_quantities = {}
|
||||
for part in part_list.records:
|
||||
@@ -101,7 +103,9 @@ def export_parts_lego() -> Response:
|
||||
year = request.args.get('year')
|
||||
|
||||
part_list = BrickPartList()
|
||||
part_list.all_filtered(owner_id, color_id, theme_id, year)
|
||||
part_list.filtered(
|
||||
owner_id=owner_id, color_id=color_id, theme_id=theme_id, year=year
|
||||
)
|
||||
|
||||
element_quantities = {}
|
||||
for part in part_list.records:
|
||||
@@ -140,7 +144,9 @@ def export_parts_bricklink() -> Response:
|
||||
year = request.args.get('year')
|
||||
|
||||
part_list = BrickPartList()
|
||||
part_list.all_filtered(owner_id, color_id, theme_id, year)
|
||||
part_list.filtered(
|
||||
owner_id=owner_id, color_id=color_id, theme_id=theme_id, year=year
|
||||
)
|
||||
|
||||
part_quantities = {}
|
||||
for part in part_list.records:
|
||||
@@ -183,7 +189,10 @@ def export_problems_rebrickable() -> Response:
|
||||
year = request.args.get('year')
|
||||
|
||||
part_list = BrickPartList()
|
||||
part_list.problem_filtered(owner_id, color_id, theme_id, year)
|
||||
part_list.filtered(
|
||||
problem_only=True,
|
||||
owner_id=owner_id, color_id=color_id, theme_id=theme_id, year=year
|
||||
)
|
||||
|
||||
part_quantities = {}
|
||||
for part in part_list.records:
|
||||
@@ -223,7 +232,10 @@ def export_problems_lego() -> Response:
|
||||
year = request.args.get('year')
|
||||
|
||||
part_list = BrickPartList()
|
||||
part_list.problem_filtered(owner_id, color_id, theme_id, year)
|
||||
part_list.filtered(
|
||||
problem_only=True,
|
||||
owner_id=owner_id, color_id=color_id, theme_id=theme_id, year=year
|
||||
)
|
||||
|
||||
element_quantities = {}
|
||||
for part in part_list.records:
|
||||
@@ -263,7 +275,10 @@ def export_problems_bricklink() -> Response:
|
||||
year = request.args.get('year')
|
||||
|
||||
part_list = BrickPartList()
|
||||
part_list.problem_filtered(owner_id, color_id, theme_id, year)
|
||||
part_list.filtered(
|
||||
problem_only=True,
|
||||
owner_id=owner_id, color_id=color_id, theme_id=theme_id, year=year
|
||||
)
|
||||
|
||||
part_quantities = {}
|
||||
for part in part_list.records:
|
||||
@@ -318,7 +333,9 @@ def get_combined_parts_data(owner_id, color_id, theme_id, year):
|
||||
"""Get both set-based and individual parts combined."""
|
||||
# Get set-based parts
|
||||
part_list = BrickPartList()
|
||||
part_list.all_filtered(owner_id, color_id, theme_id, year)
|
||||
part_list.filtered(
|
||||
owner_id=owner_id, color_id=color_id, theme_id=theme_id, year=year
|
||||
)
|
||||
|
||||
combined_quantities = {}
|
||||
for part in part_list.records:
|
||||
@@ -439,7 +456,9 @@ def export_parts_combined_lego() -> Response:
|
||||
|
||||
# Get set-based parts
|
||||
part_list = BrickPartList()
|
||||
part_list.all_filtered(owner_id, color_id, theme_id, year)
|
||||
part_list.filtered(
|
||||
owner_id=owner_id, color_id=color_id, theme_id=theme_id, year=year
|
||||
)
|
||||
|
||||
element_quantities = {}
|
||||
for part in part_list.records:
|
||||
@@ -520,7 +539,9 @@ def export_parts_combined_bricklink() -> Response:
|
||||
|
||||
# Get set-based parts
|
||||
part_list = BrickPartList()
|
||||
part_list.all_filtered(owner_id, color_id, theme_id, year)
|
||||
part_list.filtered(
|
||||
owner_id=owner_id, color_id=color_id, theme_id=theme_id, year=year
|
||||
)
|
||||
|
||||
part_quantities = {}
|
||||
for part in part_list.records:
|
||||
|
||||
+139
-167
@@ -6,192 +6,164 @@ from ..individual_part_lot_list import IndividualPartLotList
|
||||
from ..minifigure_list import BrickMinifigureList
|
||||
from ..pagination_helper import get_pagination_config, build_pagination_context, get_request_params
|
||||
from ..part import BrickPart
|
||||
from ..part_list import BrickPartList
|
||||
from ..part_list import BrickPartList, known_metadata_id
|
||||
from ..set_list import BrickSetList, set_metadata_lists
|
||||
from ..set_owner_list import BrickSetOwnerList
|
||||
from ..set_status_list import BrickSetStatusList
|
||||
from ..set_storage_list import BrickSetStorageList
|
||||
from ..set_tag_list import BrickSetTagList
|
||||
from ..sql import BrickSQL
|
||||
from ..theme_list import BrickThemeList
|
||||
|
||||
part_page = Blueprint('part', __name__, url_prefix='/parts')
|
||||
|
||||
|
||||
# Read the seven filters off the query string. owner, tag and status end up as SQL
|
||||
# column names, so they are checked against the ids we know about first. The rest are
|
||||
# bound as parameters, so they can go through as they are.
|
||||
def filters_from_request(
|
||||
owners: list,
|
||||
statuses: list,
|
||||
tags: list,
|
||||
/,
|
||||
) -> dict[str, str | None]:
|
||||
return {
|
||||
'owner_id': known_metadata_id(
|
||||
request.args.get('owner'),
|
||||
{owner.fields.id for owner in owners},
|
||||
),
|
||||
'color_id': request.args.get('color', 'all'),
|
||||
'theme_id': request.args.get('theme', 'all'),
|
||||
'year': request.args.get('year', 'all'),
|
||||
'storage_id': request.args.get('storage', 'all'),
|
||||
'tag_id': known_metadata_id(
|
||||
request.args.get('tag'),
|
||||
{tag.fields.id for tag in tags},
|
||||
),
|
||||
'status_id': known_metadata_id(
|
||||
request.args.get('status'),
|
||||
{status.fields.id for status in statuses},
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# Options for the colour, theme and year dropdowns. They are derived from the parts
|
||||
# on the page, so they narrow down as the owner filter narrows.
|
||||
def filter_options(
|
||||
owner_id: str | None,
|
||||
/,
|
||||
*,
|
||||
problem_only: bool = False,
|
||||
) -> dict[str, list]:
|
||||
context = {}
|
||||
if problem_only:
|
||||
context['problem_only'] = True
|
||||
|
||||
# ponytail: a negated owner would give the options of the owner being excluded,
|
||||
# so it just does not narrow the lists. Fine for a dropdown.
|
||||
if owner_id and not owner_id.startswith('-'):
|
||||
context['owner_id'] = owner_id
|
||||
|
||||
theme_list = BrickThemeList()
|
||||
themes = []
|
||||
for theme_data in BrickSQL().fetchall('part/themes/list', **context):
|
||||
theme = theme_list.get(theme_data['theme_id'])
|
||||
themes.append({
|
||||
'theme_id': theme_data['theme_id'],
|
||||
'theme_name': theme.name if theme else 'Theme {id}'.format(
|
||||
id=theme_data['theme_id'],
|
||||
),
|
||||
})
|
||||
|
||||
return {
|
||||
'colors': BrickSQL().fetchall('part/colors/list', **context),
|
||||
'themes': themes,
|
||||
'years': BrickSQL().fetchall('part/years/list', **context),
|
||||
}
|
||||
|
||||
|
||||
# Everything both parts pages need to render. They only differ by problem_only and
|
||||
# the template, so the filter plumbing lives here once.
|
||||
def parts_page_context(*, problem_only: bool) -> dict:
|
||||
owners = BrickSetOwnerList.list()
|
||||
statuses = BrickSetStatusList.list(all=True)
|
||||
tags = BrickSetTagList.list()
|
||||
storages = BrickSetStorageList.list()
|
||||
|
||||
filters = filters_from_request(owners, statuses, tags)
|
||||
individuals_filter = request.args.get('individuals', 'all')
|
||||
search_query, sort_field, sort_order, page = get_request_params()
|
||||
|
||||
per_page, is_mobile = get_pagination_config(
|
||||
'problems' if problem_only else 'parts'
|
||||
)
|
||||
use_pagination = per_page > 0
|
||||
|
||||
parts = BrickPartList()
|
||||
if use_pagination:
|
||||
parts, total_count = parts.paginated_filtered(
|
||||
problem_only=problem_only,
|
||||
individuals_filter=individuals_filter,
|
||||
search_query=search_query,
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
sort_field=sort_field,
|
||||
sort_order=sort_order,
|
||||
**filters
|
||||
)
|
||||
pagination = build_pagination_context(
|
||||
page, per_page, total_count, is_mobile
|
||||
)
|
||||
else:
|
||||
parts = parts.filtered(
|
||||
problem_only=problem_only,
|
||||
individuals_filter=individuals_filter,
|
||||
**filters
|
||||
)
|
||||
pagination = None
|
||||
|
||||
return {
|
||||
'table_collection': parts,
|
||||
'pagination': pagination,
|
||||
'use_pagination': use_pagination,
|
||||
'search_query': search_query,
|
||||
'sort_field': sort_field,
|
||||
'sort_order': sort_order,
|
||||
'current_sort': sort_field,
|
||||
'current_order': sort_order,
|
||||
'owners': owners,
|
||||
'storages': storages,
|
||||
'tags': tags,
|
||||
'statuses': statuses,
|
||||
'selected_owner': request.args.get('owner', 'all'),
|
||||
'selected_color': filters['color_id'],
|
||||
'selected_theme': filters['theme_id'],
|
||||
'selected_year': filters['year'],
|
||||
'selected_storage': filters['storage_id'],
|
||||
'selected_tag': request.args.get('tag', 'all'),
|
||||
'selected_status': request.args.get('status', 'all'),
|
||||
'selected_individuals': individuals_filter,
|
||||
**filter_options(filters['owner_id'], problem_only=problem_only),
|
||||
}
|
||||
|
||||
|
||||
# Index
|
||||
@part_page.route('/', methods=['GET'])
|
||||
@exception_handler(__file__)
|
||||
def list() -> str:
|
||||
# Get filter parameters from request
|
||||
owner_id = request.args.get('owner', 'all')
|
||||
color_id = request.args.get('color', 'all')
|
||||
theme_id = request.args.get('theme', 'all')
|
||||
year = request.args.get('year', 'all')
|
||||
individuals_filter = request.args.get('individuals', 'all')
|
||||
search_query, sort_field, sort_order, page = get_request_params()
|
||||
|
||||
# Get pagination configuration
|
||||
per_page, is_mobile = get_pagination_config('parts')
|
||||
use_pagination = per_page > 0
|
||||
|
||||
if use_pagination:
|
||||
# PAGINATION MODE - Server-side pagination with search
|
||||
parts, total_count = BrickPartList().all_filtered_paginated(
|
||||
owner_id=owner_id,
|
||||
color_id=color_id,
|
||||
theme_id=theme_id,
|
||||
year=year,
|
||||
individuals_filter=individuals_filter,
|
||||
search_query=search_query,
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
sort_field=sort_field,
|
||||
sort_order=sort_order
|
||||
)
|
||||
|
||||
pagination_context = build_pagination_context(page, per_page, total_count, is_mobile)
|
||||
else:
|
||||
# ORIGINAL MODE - Single page with all data for client-side search
|
||||
parts = BrickPartList().all_filtered(owner_id, color_id, theme_id, year, individuals_filter)
|
||||
pagination_context = None
|
||||
|
||||
# Get list of owners for filter dropdown
|
||||
owners = BrickSetOwnerList.list()
|
||||
|
||||
# Prepare context for dependent filters
|
||||
filter_context = {}
|
||||
if owner_id != 'all' and owner_id:
|
||||
filter_context['owner_id'] = owner_id
|
||||
|
||||
# Get list of colors for filter dropdown
|
||||
colors = BrickSQL().fetchall('part/colors/list', **filter_context)
|
||||
|
||||
# Get list of themes for filter dropdown
|
||||
from ..theme_list import BrickThemeList
|
||||
theme_list = BrickThemeList()
|
||||
themes_data = BrickSQL().fetchall('part/themes/list', **filter_context)
|
||||
themes = []
|
||||
for theme_data in themes_data:
|
||||
theme = theme_list.get(theme_data['theme_id'])
|
||||
themes.append({
|
||||
'theme_id': theme_data['theme_id'],
|
||||
'theme_name': theme.name if theme else f"Theme {theme_data['theme_id']}"
|
||||
})
|
||||
|
||||
# Get list of years for filter dropdown
|
||||
years = BrickSQL().fetchall('part/years/list', **filter_context)
|
||||
|
||||
template_context = {
|
||||
'table_collection': parts,
|
||||
'owners': owners,
|
||||
'selected_owner': owner_id,
|
||||
'colors': colors,
|
||||
'selected_color': color_id,
|
||||
'themes': themes,
|
||||
'selected_theme': theme_id,
|
||||
'years': years,
|
||||
'selected_year': year,
|
||||
'selected_individuals': individuals_filter,
|
||||
'search_query': search_query,
|
||||
'use_pagination': use_pagination,
|
||||
'current_sort': sort_field,
|
||||
'current_order': sort_order
|
||||
}
|
||||
|
||||
if pagination_context:
|
||||
template_context['pagination'] = pagination_context
|
||||
|
||||
return render_template('parts.html', **template_context)
|
||||
|
||||
return render_template(
|
||||
'parts.html',
|
||||
**parts_page_context(problem_only=False)
|
||||
)
|
||||
|
||||
|
||||
# Problem
|
||||
@part_page.route('/problem', methods=['GET'])
|
||||
@exception_handler(__file__)
|
||||
def problem() -> str:
|
||||
# Get filter parameters from request
|
||||
owner_id = request.args.get('owner', 'all')
|
||||
color_id = request.args.get('color', 'all')
|
||||
theme_id = request.args.get('theme', 'all')
|
||||
year = request.args.get('year', 'all')
|
||||
storage_id = request.args.get('storage', 'all')
|
||||
tag_id = request.args.get('tag', 'all')
|
||||
search_query, sort_field, sort_order, page = get_request_params()
|
||||
|
||||
# Get pagination configuration
|
||||
per_page, is_mobile = get_pagination_config('problems')
|
||||
use_pagination = per_page > 0
|
||||
|
||||
if use_pagination:
|
||||
# PAGINATION MODE - Server-side pagination with search and filters
|
||||
parts, total_count = BrickPartList().problem_paginated(
|
||||
owner_id=owner_id,
|
||||
color_id=color_id,
|
||||
theme_id=theme_id,
|
||||
year=year,
|
||||
storage_id=storage_id,
|
||||
tag_id=tag_id,
|
||||
search_query=search_query,
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
sort_field=sort_field,
|
||||
sort_order=sort_order
|
||||
)
|
||||
|
||||
pagination_context = build_pagination_context(page, per_page, total_count, is_mobile)
|
||||
else:
|
||||
# ORIGINAL MODE - Single page with all data for client-side search
|
||||
parts = BrickPartList().problem_filtered(owner_id, color_id, theme_id, year, storage_id, tag_id)
|
||||
pagination_context = None
|
||||
|
||||
# Get list of owners for filter dropdown
|
||||
owners = BrickSetOwnerList.list()
|
||||
|
||||
# Prepare context for dependent filters
|
||||
filter_context = {}
|
||||
if owner_id != 'all' and owner_id:
|
||||
filter_context['owner_id'] = owner_id
|
||||
|
||||
# Get list of colors for filter dropdown (problem parts only)
|
||||
colors = BrickSQL().fetchall('part/colors/list_problem', **filter_context)
|
||||
|
||||
# Get list of themes for filter dropdown (problem parts only)
|
||||
from ..theme_list import BrickThemeList
|
||||
theme_list = BrickThemeList()
|
||||
themes_data = BrickSQL().fetchall('part/themes/list_problem', **filter_context)
|
||||
themes = []
|
||||
for theme_data in themes_data:
|
||||
theme = theme_list.get(theme_data['theme_id'])
|
||||
themes.append({
|
||||
'theme_id': theme_data['theme_id'],
|
||||
'theme_name': theme.name if theme else f"Theme {theme_data['theme_id']}"
|
||||
})
|
||||
|
||||
# Get list of years for filter dropdown (problem parts only)
|
||||
years = BrickSQL().fetchall('part/years/list_problem', **filter_context)
|
||||
|
||||
# Get list of storages for filter dropdown (problem parts only)
|
||||
storages = BrickSQL().fetchall('part/storages/list_problem', **filter_context)
|
||||
|
||||
# Get list of tags for filter dropdown (problem parts only)
|
||||
tags = BrickSQL().fetchall('part/tags/list_problem', **filter_context)
|
||||
|
||||
return render_template(
|
||||
'problem.html',
|
||||
table_collection=parts,
|
||||
pagination=pagination_context,
|
||||
search_query=search_query,
|
||||
sort_field=sort_field,
|
||||
sort_order=sort_order,
|
||||
use_pagination=use_pagination,
|
||||
owners=owners,
|
||||
colors=colors,
|
||||
selected_owner=owner_id,
|
||||
selected_color=color_id,
|
||||
themes=themes,
|
||||
selected_theme=theme_id,
|
||||
years=years,
|
||||
selected_year=year,
|
||||
storages=storages,
|
||||
selected_storage=storage_id,
|
||||
tags=tags,
|
||||
selected_tag=tag_id
|
||||
**parts_page_context(problem_only=True)
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user