feat(admin): added options to order badges on sets and details page.

This commit is contained in:
FrederikBaerentsen
2025-12-21 20:52:02 -05:00
parent b30deef529
commit 2f1bba475d
7 changed files with 130 additions and 40 deletions
+15 -1
View File
@@ -481,6 +481,20 @@
# BK_STATISTICS_DEFAULT_EXPANDED=false
# Optional: Enable dark mode by default
# When true, the application starts in dark mode.
# When true, the application starts in dark mode.
# Default: false
# BK_DARK_MODE=true
# Optional: Customize badge order for Grid view (set cards on /sets/)
# Comma-separated list of badge keys in the order they should appear
# Available badges: theme, tag, year, parts, instance_count, total_minifigures,
# total_missing, total_damaged, owner, storage, purchase_date, purchase_location,
# purchase_price, instructions, rebrickable, bricklink
# Default: theme,year,parts,total_minifigures,owner
# BK_BADGE_ORDER_GRID=theme,year,parts,total_minifigures,owner,storage
# Optional: Customize badge order for Detail view (individual set details page)
# Comma-separated list of badge keys in the order they should appear
# Use the same badge keys as BK_BADGE_ORDER_GRID
# Default: theme,tag,year,parts,instance_count,total_minifigures,total_missing,total_damaged,owner,storage,purchase_date,purchase_location,purchase_price,instructions,rebrickable,bricklink
# BK_BADGE_ORDER_DETAIL=theme,tag,year,parts,owner,storage,purchase_date,rebrickable,bricklink
+24 -9
View File
@@ -17,15 +17,29 @@
- BrickLink exports use proper BrickLink part numbers and color IDs when available
- Filter support: All part exports accept owner, color, theme, and year query parameters
- Format information displayed in UI for user guidance
- **Database Integrity Check and Cleanup** (from 1.3.2)
- **Badge Order Customization**
- Added customizable badge ordering for set cards and detail pages
- Separate configurations for Grid view (`/sets/` cards) and Detail view (individual set pages)
- Configure via environment variables in `.env` file:
- `BK_BADGE_ORDER_GRID`: Comma-separated badge keys for grid view (default: theme,year,parts,total_minifigures,owner)
- `BK_BADGE_ORDER_DETAIL`: Comma-separated badge keys for detail view (default: all 16 badges)
- Can also be configured via Live Settings page in admin panel under "Default Ordering & Formatting"
- Changes apply immediately without restart when edited via admin panel
- 16 available badge types: theme, tag, year, parts, instance_count, total_minifigures, total_missing, total_damaged, owner, storage, purchase_date, purchase_location, purchase_price, instructions, rebrickable, bricklink
## 1.3.1
### New Functionality
- **Database Integrity Check and Cleanup**
- Added database integrity scanner to detect orphaned records and foreign key violations
- New "Check Database Integrity" button in admin panel scans for issues
- Detects orphaned sets, parts, and parts with missing set references
- Two-step cleanup process with Bootstrap modal confirmation
- Warning prompts users to backup database before cleanup
- Automatic cleanup removes all orphaned records in one operation
- Cleanup removes all orphaned records in one operation
- Detailed scan results show affected records with counts and descriptions
- **Database Optimization** (from 1.3.2)
- **Database Optimization**
- Added "Optimize Database" button to re-create performance indexes
- Safe to run after database imports or restores
- Re-creates all indexes from migration #19 using `CREATE INDEX IF NOT EXISTS`
@@ -35,19 +49,20 @@
### Bug Fixes
- **Fixed foreign key constraint errors during set imports** (from 1.3.1): Resolved `FOREIGN KEY constraint failed` errors when importing sets with parts and minifigures
- **Fixed foreign key constraint errors during set imports**: Resolved `FOREIGN KEY constraint failed` errors when importing sets with parts and minifigures
- Fixed insertion order in `bricktracker/part.py`: Parent records (`rebrickable_parts`) now inserted before child records (`bricktracker_parts`)
- Fixed insertion order in `bricktracker/minifigure.py`: Parent records (`rebrickable_minifigures`) now inserted before child records (`bricktracker_minifigures`)
- Ensures foreign key references are valid when SQLite checks constraints
- **Fixed set metadata updates** (from 1.3.1): Owner, status, and tag checkboxes now properly persist changes on set details page
- **Fixed set metadata updates**: Owner, status, and tag checkboxes now properly persist changes on set details page
- Fixed `update_set_state()` method to commit database transactions (was using deferred execution without commit)
- All metadata updates (owner, status, tags, storage, purchase info) now work consistently
- **Fixed nil image downloads** (from 1.3.1): Placeholder images for parts and minifigures without images now download correctly
- **Fixed nil image downloads**: Placeholder images for parts and minifigures without images now download correctly
- Removed early returns that prevented nil image downloads
- Nil images now properly saved to configured folders (e.g., `/app/data/parts/nil.jpg`)
- **Fixed error logging for missing files** (from 1.3.1): File not found errors now show actual configured folder paths instead of just URL paths
- **Fixed error logging for missing files**: File not found errors now show actual configured folder paths instead of just URL paths
- Added detailed logging showing both file path and configured folder for easier debugging
- **Fixed minifigure filters in client-side pagination mode** (from 1.3.1): Owner and other filters now work correctly when server-side pagination is disabled
- **Fixed minifigure filters in client-side pagination mode**: Owner and other filters now work correctly when server-side pagination is disabled
- Aligned filter behavior with parts page (applies filters server-side, then loads filtered data for client-side search)
## 1.3
+2
View File
@@ -97,4 +97,6 @@ CONFIG: Final[list[dict[str, Any]]] = [
{'n': 'STATISTICS_SHOW_CHARTS', 'd': True, 'c': bool},
{'n': 'STATISTICS_DEFAULT_EXPANDED', 'd': True, 'c': bool},
{'n': 'DARK_MODE', 'c': bool},
{'n': 'BADGE_ORDER_GRID', 'd': ['theme', 'year', 'parts', 'total_minifigures', 'owner'], 'c': list},
{'n': 'BADGE_ORDER_DETAIL', 'd': ['theme', 'tag', 'year', 'parts', 'instance_count', 'total_minifigures', 'total_missing', 'total_damaged', 'owner', 'storage', 'purchase_date', 'purchase_location', 'purchase_price', 'instructions', 'rebrickable', 'bricklink'], 'c': list},
]
+5 -2
View File
@@ -54,6 +54,9 @@ LIVE_CHANGEABLE_VARS: Final[List[str]] = [
'BK_STATISTICS_SHOW_CHARTS',
'BK_STATISTICS_DEFAULT_EXPANDED',
'BK_DARK_MODE',
# Badge order preferences
'BK_BADGE_ORDER_GRID',
'BK_BADGE_ORDER_DETAIL',
# Default ordering and formatting
'BK_INSTRUCTIONS_ALLOWED_EXTENSIONS',
'BK_MINIFIGURES_DEFAULT_ORDER',
@@ -179,8 +182,8 @@ class ConfigManager:
def _cast_value(self, var_name: str, value: Any) -> Any:
"""Cast value to appropriate type based on variable name"""
# List variables (admin sections) - Check this FIRST before boolean check
if 'sections' in var_name.lower():
# List variables (admin sections, badge order) - Check this FIRST before boolean check
if any(keyword in var_name.lower() for keyword in ['sections', 'badge_order']):
if isinstance(value, str):
return [section.strip() for section in value.split(',') if section.strip()]
elif isinstance(value, list):
+16
View File
@@ -560,6 +560,22 @@
<h6 class="fw-bold text-primary border-bottom pb-1 mb-3 mt-4">Default Ordering & Formatting</h6>
<div class="row g-3">
<div class="col-12">
<label for="BK_BADGE_ORDER_GRID" class="form-label">
BK_BADGE_ORDER_GRID {{ config_badges('BK_BADGE_ORDER_GRID') }}
<div class="text-muted small">Badge order for grid view (comma-separated badge keys)</div>
</label>
<input type="text" class="form-control config-text" id="BK_BADGE_ORDER_GRID" data-var="BK_BADGE_ORDER_GRID" {{ is_locked('BK_BADGE_ORDER_GRID') }}>
</div>
<div class="col-12">
<label for="BK_BADGE_ORDER_DETAIL" class="form-label">
BK_BADGE_ORDER_DETAIL {{ config_badges('BK_BADGE_ORDER_DETAIL') }}
<div class="text-muted small">Badge order for detail view (comma-separated badge keys)</div>
</label>
<input type="text" class="form-control config-text" id="BK_BADGE_ORDER_DETAIL" data-var="BK_BADGE_ORDER_DETAIL" {{ is_locked('BK_BADGE_ORDER_DETAIL') }}>
</div>
<div class="col-12">
<label for="BK_INSTRUCTIONS_ALLOWED_EXTENSIONS" class="form-label">
BK_INSTRUCTIONS_ALLOWED_EXTENSIONS {{ config_badges('BK_INSTRUCTIONS_ALLOWED_EXTENSIONS') }}
+66
View File
@@ -218,3 +218,69 @@
{% macro year(year, solo=false, last=false) %}
{{ badge(check=year, solo=solo, last=last, color='secondary', icon='calendar-line', collapsible='Year:', text=year, alt='Year') }}
{% endmacro %}
{% macro render_ordered_badges(item, brickset_tags, brickset_owners, brickset_storages, brickset_purchase_locations, solo=false, last=false, context='grid') %}
{# Get badge order from config based on context (grid or detail) #}
{% if context == 'detail' %}
{% set badge_order = config.get('BADGE_ORDER_DETAIL', ['theme', 'tag', 'year', 'parts', 'instance_count', 'total_minifigures', 'total_missing', 'total_damaged', 'owner', 'storage', 'purchase_date', 'purchase_location', 'purchase_price', 'instructions', 'rebrickable', 'bricklink']) %}
{% else %}
{% set badge_order = config.get('BADGE_ORDER_GRID', ['theme', 'year', 'parts', 'total_minifigures', 'owner']) %}
{% endif %}
{# Render each badge in the configured order #}
{% for badge_key in badge_order %}
{% if badge_key == 'theme' %}
{{ theme(item.theme.name, solo=solo, last=last) }}
{% elif badge_key == 'tag' %}
{% for tag_item in brickset_tags %}
{{ tag(item, tag_item, solo=solo, last=last) }}
{% endfor %}
{% elif badge_key == 'year' %}
{% if not last %}
{{ year(item.fields.year, solo=solo, last=last) }}
{% endif %}
{% elif badge_key == 'parts' %}
{{ parts(item.fields.number_of_parts, solo=solo, last=last) }}
{% elif badge_key == 'instance_count' %}
{% if item.fields.instance_count is defined and item.fields.instance_count > 1 %}
<span class="badge bg-primary"><i class="ri-stack-line"></i> {{ item.fields.instance_count }} copies</span>
{% endif %}
{% elif badge_key == 'total_minifigures' %}
{{ total_minifigures(item.fields.total_minifigures, solo=solo, last=last) }}
{% elif badge_key == 'total_missing' %}
{{ total_missing(item.fields.total_missing, solo=solo, last=last) }}
{% elif badge_key == 'total_damaged' %}
{{ total_damaged(item.fields.total_damaged, solo=solo, last=last) }}
{% elif badge_key == 'owner' %}
{% for owner_item in brickset_owners %}
{{ owner(item, owner_item, solo=solo, last=last) }}
{% endfor %}
{% elif badge_key == 'storage' %}
{{ storage(item, brickset_storages, solo=solo, last=last) }}
{% elif badge_key == 'purchase_date' %}
{% if not last %}
{{ purchase_date(item.purchase_date(), solo=solo, last=last, date_max_formatted=item.purchase_date_max_formatted()) }}
{% endif %}
{% elif badge_key == 'purchase_location' %}
{% if not last %}
{{ purchase_location(item, brickset_purchase_locations, solo=solo, last=last) }}
{% endif %}
{% elif badge_key == 'purchase_price' %}
{% if not last %}
{{ purchase_price(item.purchase_price(), solo=solo, last=last) }}
{% endif %}
{% elif badge_key == 'instructions' %}
{% if not last and not solo %}
{{ instructions(item, solo=solo, last=last) }}
{% endif %}
{% elif badge_key == 'rebrickable' %}
{% if not last %}
{{ rebrickable(item, solo=solo, last=last) }}
{% endif %}
{% elif badge_key == 'bricklink' %}
{% if not last %}
{{ bricklink(item, solo=solo, last=last) }}
{% endif %}
{% endif %}
{% endfor %}
{% endmacro %}
+2 -28
View File
@@ -57,34 +57,8 @@
{{ card.header(item, item.fields.name, solo=solo, identifier=item.fields.set) }}
{{ card.image(item, solo=solo, last=last, caption=item.fields.name, alt=item.fields.set) }}
<div class="card-body border-bottom-0 {% if not solo %}p-1{% endif %}"{% if current_viewing %} style="border-color: var(--bs-border-color) !important; border-width: 1px !important;"{% endif %}>
{{ badge.theme(item.theme.name, solo=solo, last=last) }}
{% for tag in brickset_tags %}
{{ badge.tag(item, tag, solo=solo, last=last) }}
{% endfor %}
{% if not last %}
{{ badge.year(item.fields.year, solo=solo, last=last) }}
{% endif %}
{{ badge.parts(item.fields.number_of_parts, solo=solo, last=last) }}
{% if item.fields.instance_count is defined and item.fields.instance_count > 1 %}
<span class="badge bg-primary"><i class="ri-stack-line"></i> {{ item.fields.instance_count }} copies</span>
{% endif %}
{{ badge.total_minifigures(item.fields.total_minifigures, solo=solo, last=last) }}
{{ badge.total_missing(item.fields.total_missing, solo=solo, last=last) }}
{{ badge.total_damaged(item.fields.total_damaged, solo=solo, last=last) }}
{% for owner in brickset_owners %}
{{ badge.owner(item, owner, solo=solo, last=last) }}
{% endfor %}
{{ badge.storage(item, brickset_storages, solo=solo, last=last) }}
{% if not last %}
{{ badge.purchase_date(item.purchase_date(), solo=solo, last=last, date_max_formatted=item.purchase_date_max_formatted()) }}
{{ badge.purchase_location(item, brickset_purchase_locations, solo=solo, last=last) }}
{{ badge.purchase_price(item.purchase_price(), solo=solo, last=last) }}
{% if not solo %}
{{ badge.instructions(item, solo=solo, last=last) }}
{% endif %}
{{ badge.rebrickable(item, solo=solo, last=last) }}
{{ badge.bricklink(item, solo=solo, last=last) }}
{% endif %}
{# Render badges in configured order based on context (grid view vs detail view) #}
{{ badge.render_ordered_badges(item, brickset_tags, brickset_owners, brickset_storages, brickset_purchase_locations, solo=solo, last=last, context='detail' if solo else 'grid') }}
</div>
{% if not tiny and brickset_statuses | length %}
<ul class="list-group list-group-flush card-check border-bottom-0"{% if current_viewing %} style="border-color: var(--bs-border-color) !important; border-width: 1px !important;"{% endif %}>