Fixed filtering on /sets page.

This commit is contained in:
2025-09-21 17:26:57 +02:00
parent 4b3aef577a
commit 8a0a7837dc
+155 -37
View File
@@ -105,17 +105,16 @@ class BrickSetList(BrickRecordList[BrickSet]):
# Choose query based on whether filters are applied
query_to_use = 'set/list/all_filtered' if has_filters else self.all_query
# Handle instructions filtering separately (post-SQL filtering)
instructions_filter = None
# Handle instructions filtering
if status_filter in ['has-missing-instructions', '-has-missing-instructions']:
instructions_filter = status_filter
# Remove from SQL context to avoid SQL errors
filter_context['status_filter'] = None
# Recalculate has_filters without instructions
has_filters = any([theme_id_filter, owner_filter, purchase_location_filter, storage_filter, tag_filter])
query_to_use = 'set/list/all_filtered' if has_filters else self.all_query
# For instructions filter, we need to load all sets first, then filter and paginate
return self._all_filtered_paginated_with_instructions(
search_query, page, per_page, sort_field, sort_order,
status_filter, theme_id_filter, owner_filter,
purchase_location_filter, storage_filter, tag_filter
)
# Use the base pagination method with custom list method
# Normal SQL-based filtering and pagination
result, total_count = self.paginate(
page=page,
per_page=per_page,
@@ -126,12 +125,8 @@ class BrickSetList(BrickRecordList[BrickSet]):
**filter_context
)
# Apply instructions filtering after SQL query
if instructions_filter:
result, total_count = self._filter_by_instructions(result, total_count, instructions_filter, page, per_page)
# Populate themes for filter dropdown (always needed)
result._populate_themes()
# Populate themes for filter dropdown from ALL sets, not just current page
result._populate_themes_global()
return result, total_count
@@ -157,42 +152,165 @@ class BrickSetList(BrickRecordList[BrickSet]):
# If themes can't be loaded, return None to disable theme filtering
return None
def _filter_by_instructions(self, result_list: Self, total_count: int, instructions_filter: str, page: int, per_page: int) -> tuple[Self, int]:
"""Filter sets by instruction file existence (post-SQL filtering)"""
def _all_filtered_paginated_with_instructions(
self,
search_query: str | None,
page: int,
per_page: int,
sort_field: str | None,
sort_order: str,
status_filter: str,
theme_id_filter: str | None,
owner_filter: str | None,
purchase_location_filter: str | None,
storage_filter: str | None,
tag_filter: str | None
) -> tuple[Self, int]:
"""Handle filtering when instructions filter is involved"""
try:
# Load all sets first (without pagination) with full metadata
all_sets = BrickSetList()
filter_context = {
'owners': BrickSetOwnerList.as_columns(),
'statuses': BrickSetStatusList.as_columns(),
'tags': BrickSetTagList.as_columns(),
}
all_sets.list(do_theme=True, **filter_context)
# Load instructions list
instructions_list = BrickInstructionsList()
instruction_sets = set(instructions_list.sets.keys())
# Filter the records
# Apply all filters manually
filtered_records = []
for record in result_list.records:
for record in all_sets.records:
# Apply instructions filter
set_id = record.fields.set
has_instructions = set_id in instruction_sets
if instructions_filter == 'has-missing-instructions':
# Show sets that are MISSING instructions
if not has_instructions:
filtered_records.append(record)
elif instructions_filter == '-has-missing-instructions':
# Show sets that HAVE instructions
if has_instructions:
filtered_records.append(record)
if status_filter == 'has-missing-instructions' and has_instructions:
continue # Skip sets that have instructions
elif status_filter == '-has-missing-instructions' and not has_instructions:
continue # Skip sets that don't have instructions
# Create new result with filtered records
new_result = BrickSetList()
new_result.records = filtered_records
# Apply other filters manually
if search_query and not self._matches_search(record, search_query):
continue
if theme_id_filter and not self._matches_theme(record, theme_id_filter):
continue
if owner_filter and not self._matches_owner(record, owner_filter):
continue
if purchase_location_filter and not self._matches_purchase_location(record, purchase_location_filter):
continue
if storage_filter and not self._matches_storage(record, storage_filter):
continue
if tag_filter and not self._matches_tag(record, tag_filter):
continue
# Note: This breaks proper pagination since we're filtering after SQL
# The total_count and pagination will be approximate
# For proper pagination, we'd need a database table for instructions
# This will be implemented in future versions
filtered_records.append(record)
return new_result, len(filtered_records)
# Apply sorting
if sort_field:
filtered_records = self._sort_records(filtered_records, sort_field, sort_order)
# Calculate pagination
total_count = len(filtered_records)
start_index = (page - 1) * per_page
end_index = start_index + per_page
paginated_records = filtered_records[start_index:end_index]
# Create result
result = BrickSetList()
result.records = paginated_records
# Copy themes from the source that has all sets
result.themes = all_sets.themes if hasattr(all_sets, 'themes') else []
# If themes weren't populated, populate them globally
if not result.themes:
result._populate_themes_global()
return result, total_count
except Exception:
# If instructions can't be loaded, return original results
return result_list, total_count
# Fall back to normal pagination without instructions filter
return self.all_filtered_paginated(
search_query, page, per_page, sort_field, sort_order,
None, theme_id_filter, owner_filter,
purchase_location_filter, storage_filter, tag_filter
)
def _populate_themes_global(self) -> None:
"""Populate themes list from ALL sets, not just current page"""
try:
# Load all sets to get all possible themes
all_sets = BrickSetList().all()
themes = set()
for record in all_sets.records:
if hasattr(record, 'theme') and hasattr(record.theme, 'name'):
themes.add(record.theme.name)
self.themes = list(themes)
self.themes.sort()
except Exception:
# Fall back to current page themes
self._populate_themes()
def _matches_search(self, record, search_query: str) -> bool:
"""Check if record matches search query"""
search_lower = search_query.lower()
return (search_lower in record.fields.name.lower() or
search_lower in record.fields.set.lower())
def _matches_theme(self, record, theme_id: str) -> bool:
"""Check if record matches theme filter"""
return str(record.fields.theme_id) == theme_id
def _matches_owner(self, record, owner_filter: str) -> bool:
"""Check if record matches owner filter"""
if not owner_filter.startswith('owner-'):
return True
# Convert owner-uuid format to owner_uuid column name
owner_column = owner_filter.replace('-', '_')
# Check if record has this owner attribute set to 1
return hasattr(record.fields, owner_column) and getattr(record.fields, owner_column) == 1
def _matches_purchase_location(self, record, location_filter: str) -> bool:
"""Check if record matches purchase location filter"""
return record.fields.purchase_location == location_filter
def _matches_storage(self, record, storage_filter: str) -> bool:
"""Check if record matches storage filter"""
return record.fields.storage == storage_filter
def _matches_tag(self, record, tag_filter: str) -> bool:
"""Check if record matches tag filter"""
if not tag_filter.startswith('tag-'):
return True
# Convert tag-uuid format to tag_uuid column name
tag_column = tag_filter.replace('-', '_')
# Check if record has this tag attribute set to 1
return hasattr(record.fields, tag_column) and getattr(record.fields, tag_column) == 1
def _sort_records(self, records, sort_field: str, sort_order: str):
"""Sort records manually"""
reverse = sort_order == 'desc'
if sort_field == 'set':
return sorted(records, key=lambda r: r.fields.set, reverse=reverse)
elif sort_field == 'name':
return sorted(records, key=lambda r: r.fields.name, reverse=reverse)
elif sort_field == 'year':
return sorted(records, key=lambda r: r.fields.year, reverse=reverse)
elif sort_field == 'parts':
return sorted(records, key=lambda r: r.fields.number_of_parts, reverse=reverse)
# Add more sort fields as needed
return records
# Sets with a minifigure part damaged
def damaged_minifigure(self, figure: str, /) -> Self: