fix(minifigures): fixed metadata format and individual minifigures layout

This commit is contained in:
FrederikBaerentsen
2025-10-10 08:29:12 +02:00
parent bd32ca5b8f
commit a8d36bc5f1
26 changed files with 390 additions and 108 deletions
+14 -9
View File
@@ -160,12 +160,15 @@
# BK_HIDE_WISHES=true
# Optional: Change the default order of minifigures. By default ordered by insertion order.
# Note: Minifigures are queried from a combined view that merges both set-based and individual minifigures.
# Therefore, column references should use the "combined" table alias.
# Useful column names for this option are:
# - "rebrickable_minifigures"."figure": minifigure ID (fig-xxxxx)
# - "rebrickable_minifigures"."number": minifigure ID as an integer (xxxxx)
# - "rebrickable_minifigures"."name": minifigure name
# Default: "rebrickable_minifigures"."name" ASC
# BK_MINIFIGURES_DEFAULT_ORDER="rebrickable_minifigures"."name" ASC
# - "combined"."figure": minifigure ID (fig-xxxxx)
# - "combined"."number": minifigure ID as an integer (xxxxx)
# - "combined"."name": minifigure name
# - "combined"."rowid": insertion order (for both set and individual minifigures)
# Default: "combined"."name" ASC
# BK_MINIFIGURES_DEFAULT_ORDER="combined"."name" ASC
# Optional: Folder where to store the minifigures images, relative to the '/app/static/' folder
# Default: minifigs
@@ -178,14 +181,16 @@
# BK_NO_THREADED_SOCKET=true
# Optional: Change the default order of parts. By default ordered by insertion order.
# Note: Parts are queried from a combined view that merges both set-based and individual minifigure parts.
# Some columns use the "combined" table alias for fields from the merged view.
# Useful column names for this option are:
# - "bricktracker_parts"."part": part number
# - "bricktracker_parts"."spare": part is a spare part
# - "combined"."part": part number
# - "combined"."spare": part is a spare part (use "combined" not "bricktracker_parts")
# - "rebrickable_parts"."name": part name
# - "rebrickable_parts"."color_name": part color name
# - "total_missing": number of missing parts
# Default: "rebrickable_parts"."name" ASC, "rebrickable_parts"."color_name" ASC, "bricktracker_parts"."spare" ASC
# BK_PARTS_DEFAULT_ORDER="total_missing" DESC, "rebrickable_parts"."name"."name" ASC
# Default: "rebrickable_parts"."name" ASC, "rebrickable_parts"."color_name" ASC, "combined"."spare" ASC
# BK_PARTS_DEFAULT_ORDER="total_missing" DESC, "rebrickable_parts"."name" ASC
# Optional: Folder where to store the parts images, relative to the '/app/static/' folder
# Default: parts
+19 -1
View File
@@ -428,7 +428,17 @@ class IndividualMinifigure(RebrickableMinifigure):
# Save the ID parameter
self.fields.id = id
if not self.select():
# Import status list here to get metadata columns
from .set_status_list import BrickSetStatusList
# Pass metadata columns to the query with correct table names for individual minifigures
context = {
'owners': ', ' + BrickSetOwnerList.as_columns(table='bricktracker_individual_minifigure_owners') if BrickSetOwnerList.list() else '',
'statuses': ', ' + BrickSetStatusList.as_columns(table='bricktracker_individual_minifigure_statuses', all=True) if BrickSetStatusList.list(all=True) else '',
'tags': ', ' + BrickSetTagList.as_columns(table='bricktracker_individual_minifigure_tags') if BrickSetTagList.list() else '',
}
if not self.select(**context):
raise NotFoundException(
'Individual minifigure with ID {id} was not found in the database'.format(
id=id,
@@ -441,6 +451,14 @@ class IndividualMinifigure(RebrickableMinifigure):
def url(self, /) -> str:
return url_for('individual_minifigure.details', id=self.fields.id)
# URL for updating quantity
def url_for_quantity(self, /) -> str:
return url_for('individual_minifigure.update_quantity', id=self.fields.id)
# URL for updating description
def url_for_description(self, /) -> str:
return url_for('individual_minifigure.update_description', id=self.fields.id)
# Override from_rebrickable to handle minifigure data
@staticmethod
def from_rebrickable(data: dict[str, Any], /, **_) -> dict[str, Any]:
+23 -5
View File
@@ -19,17 +19,20 @@ logger = logging.getLogger(__name__)
class BrickMetadata(BrickRecord):
kind: str
# Set state endpoint
set_state_endpoint: str
# Endpoints (optional, not all metadata types use all of these)
set_state_endpoint: str = ''
individual_minifigure_state_endpoint: str = ''
individual_minifigure_value_endpoint: str = ''
# Queries
delete_query: str
insert_query: str
select_query: str
update_field_query: str
update_set_state_query: str
update_set_value_query: str
update_individual_minifigure_state_query: str
update_set_state_query: str = ''
update_set_value_query: str = ''
update_individual_minifigure_state_query: str = ''
update_individual_minifigure_value_query: str = ''
def __init__(
self,
@@ -108,6 +111,21 @@ class BrickMetadata(BrickRecord):
metadata_id=self.fields.id
)
# URL to change the selected state of this metadata item for an individual minifigure
def url_for_individual_minifigure_state(self, id: str, /) -> str:
return url_for(
self.individual_minifigure_state_endpoint,
id=id,
metadata_id=self.fields.id
)
# URL to change the value for an individual minifigure
def url_for_individual_minifigure_value(self, id: str, /) -> str:
return url_for(
self.individual_minifigure_value_endpoint,
id=id
)
# Select a specific metadata (with an id)
def select_specific(self, id: str, /) -> Self:
# Save the parameters to the fields
+17 -5
View File
@@ -39,9 +39,10 @@ class BrickMetadataList(BrickRecordList[T]):
# Queries
select_query: str
# Set endpoints
set_state_endpoint: str
set_value_endpoint: str
# List-specific endpoints (for operations on the list itself)
set_state_endpoint: str = ''
set_value_endpoint: str = ''
individual_minifigure_value_endpoint: str = ''
def __init__(
self,
@@ -99,12 +100,15 @@ class BrickMetadataList(BrickRecordList[T]):
# Return the items as columns for a select
@classmethod
def as_columns(cls, /, **kwargs) -> str:
def as_columns(cls, /, table: str | None = None, **kwargs) -> str:
new = cls.new()
# Use provided table name or default to class table
table_name = table if table is not None else cls.table
return ', '.join([
'"{table}"."{column}"'.format(
table=cls.table,
table=table_name,
column=record.as_column(),
)
for record
@@ -184,3 +188,11 @@ class BrickMetadataList(BrickRecordList[T]):
cls.set_value_endpoint,
id=id,
)
# URL to change the selected value of this metadata item for an individual minifigure
@classmethod
def url_for_individual_minifigure_value(cls, id: str, /) -> str:
return url_for(
cls.individual_minifigure_value_endpoint,
id=id,
)
+1 -1
View File
@@ -113,7 +113,7 @@ class BrickMinifigureList(BrickRecordList[BrickMinifigure]):
if current_app.config['RANDOM']:
order = 'RANDOM()'
else:
order = '"bricktracker_minifigures"."rowid" DESC'
order = '"combined"."rowid" DESC'
self.list(override_query=self.last_query, order=order, limit=limit)
+13 -1
View File
@@ -673,7 +673,8 @@ class BrickSetList(BrickRecordList[BrickSet]):
# Helper to build the metadata lists
def set_metadata_lists(
as_class: bool = False
as_class: bool = False,
hardcoded_statuses_only: bool = False
) -> dict[
str,
Union[
@@ -685,9 +686,20 @@ def set_metadata_lists(
list[BrickSetTag]
]
]:
# Get all statuses
all_statuses = BrickSetStatusList.list(all=True)
# Filter to only hardcoded statuses if requested (for individual minifigures)
if hardcoded_statuses_only:
hardcoded_status_ids = ['minifigures_collected', 'set_checked', 'set_collected']
statuses = [s for s in all_statuses if s.fields.id in hardcoded_status_ids]
else:
statuses = all_statuses
return {
'brickset_owners': BrickSetOwnerList.list(),
'brickset_purchase_locations': BrickSetPurchaseLocationList.list(as_class=as_class), # noqa: E501
'brickset_statuses': statuses,
'brickset_storages': BrickSetStorageList.list(as_class=as_class),
'brickset_tags': BrickSetTagList.list(),
}
+2 -1
View File
@@ -5,8 +5,9 @@ from .metadata import BrickMetadata
class BrickSetOwner(BrickMetadata):
kind: str = 'owner'
# Set state endpoint
# Endpoints
set_state_endpoint: str = 'set.update_owner'
individual_minifigure_state_endpoint: str = 'individual_minifigure.update_owner'
# Queries
delete_query: str = 'set/metadata/owner/delete'
+3
View File
@@ -15,6 +15,9 @@ class BrickSetOwnerList(BrickMetadataList[BrickSetOwner]):
# Queries
select_query = 'set/metadata/owner/list'
# Endpoints
set_state_endpoint: str = 'set.update_owner'
# Instantiate the list with the proper class
@classmethod
def new(cls, /, *, force: bool = False) -> Self:
+4 -3
View File
@@ -5,12 +5,13 @@ from .metadata import BrickMetadata
class BrickSetPurchaseLocation(BrickMetadata):
kind: str = 'purchase location'
# Endpoints
individual_minifigure_value_endpoint: str = 'individual_minifigure.update_purchase_location'
# Queries
delete_query: str = 'set/metadata/purchase_location/delete'
insert_query: str = 'set/metadata/purchase_location/insert'
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'
update_set_state_query: str = '' # Not used for purchase location
update_individual_minifigure_state_query: str = '' # Not used for purchase location
set_state_endpoint: str = '' # Not used for purchase location
update_individual_minifigure_value_query: str = 'individual_minifigure/metadata/purchase_location/update/value'
@@ -22,6 +22,9 @@ class BrickSetPurchaseLocationList(
# Set value endpoint
set_value_endpoint: str = 'set.update_purchase_location'
# Individual minifigure value endpoint
individual_minifigure_value_endpoint: str = 'individual_minifigure.update_purchase_location'
# Load all purchase locations
@classmethod
def all(cls, /) -> Self:
+3 -2
View File
@@ -7,8 +7,9 @@ from .metadata import BrickMetadata
class BrickSetStatus(BrickMetadata):
kind: str = 'status'
# Set state endpoint
# Endpoints
set_state_endpoint: str = 'set.update_status'
individual_minifigure_state_endpoint: str = 'individual_minifigure.update_status'
# Queries
delete_query: str = 'set/metadata/status/delete'
@@ -16,7 +17,7 @@ class BrickSetStatus(BrickMetadata):
select_query: str = 'set/metadata/status/select'
update_field_query: str = 'set/metadata/status/update/field'
update_set_state_query: str = 'set/metadata/status/update/state'
update_individual_minifigure_state_query: str = '' # Not used for status
update_individual_minifigure_state_query: str = 'individual_minifigure/metadata/status/update/state'
# Grab data from a form
def from_form(self, form: dict[str, str], /) -> Self:
+3
View File
@@ -15,6 +15,9 @@ class BrickSetStatusList(BrickMetadataList[BrickSetStatus]):
# Queries
select_query = 'set/metadata/status/list'
# Endpoints
set_state_endpoint: str = 'set.update_status'
# Filter the list of set status
def filter(self, all: bool = False) -> list[BrickSetStatus]:
return [
+4 -3
View File
@@ -7,15 +7,16 @@ from flask import url_for
class BrickSetStorage(BrickMetadata):
kind: str = 'storage'
# Endpoints
individual_minifigure_value_endpoint: str = 'individual_minifigure.update_storage'
# Queries
delete_query: str = 'set/metadata/storage/delete'
insert_query: str = 'set/metadata/storage/insert'
select_query: str = 'set/metadata/storage/select'
update_field_query: str = 'set/metadata/storage/update/field'
update_set_value_query: str = 'set/metadata/storage/update/value'
update_set_state_query: str = '' # Not used for storage
update_individual_minifigure_state_query: str = '' # Not used for storage
set_state_endpoint: str = '' # Not used for storage
update_individual_minifigure_value_query: str = 'individual_minifigure/metadata/storage/update/value'
# Self url
def url(self, /) -> str:
+3
View File
@@ -20,6 +20,9 @@ class BrickSetStorageList(BrickMetadataList[BrickSetStorage]):
# Set value endpoint
set_value_endpoint: str = 'set.update_storage'
# Individual minifigure value endpoint
individual_minifigure_value_endpoint: str = 'individual_minifigure.update_storage'
# Load all storages
@classmethod
def all(cls, /) -> Self:
+2 -1
View File
@@ -5,8 +5,9 @@ from .metadata import BrickMetadata
class BrickSetTag(BrickMetadata):
kind: str = 'tag'
# Set state endpoint
# Endpoints
set_state_endpoint: str = 'set.update_tag'
individual_minifigure_state_endpoint: str = 'individual_minifigure.update_tag'
# Queries
delete_query: str = 'set/metadata/tag/delete'
+3
View File
@@ -15,6 +15,9 @@ class BrickSetTagList(BrickMetadataList[BrickSetTag]):
# Queries
select_query: str = 'set/metadata/tag/list'
# Endpoints
set_state_endpoint: str = 'set.update_tag'
# Instantiate the list with the proper class
@classmethod
def new(cls, /, *, force: bool = False) -> Self:
@@ -0,0 +1,10 @@
INSERT INTO "bricktracker_individual_minifigure_statuses" (
"id",
"{{name}}"
) VALUES (
:id,
:state
)
ON CONFLICT("id")
DO UPDATE SET "{{name}}" = :state
WHERE "bricktracker_individual_minifigure_statuses"."id" IS NOT DISTINCT FROM :id
@@ -11,7 +11,7 @@ SELECT
"rebrickable_minifigures"."image",
"rebrickable_minifigures"."number_of_parts",
"storage_meta"."name" AS "storage_name",
"purchase_meta"."name" AS "purchase_location_name"
"purchase_meta"."name" AS "purchase_location_name"{{ owners }}{{ statuses }}{{ tags }}
FROM "bricktracker_individual_minifigures"
INNER JOIN "rebrickable_minifigures"
@@ -23,4 +23,13 @@ ON "bricktracker_individual_minifigures"."storage" = "storage_meta"."id"
LEFT JOIN "bricktracker_metadata_purchase_locations" AS "purchase_meta"
ON "bricktracker_individual_minifigures"."purchase_location" = "purchase_meta"."id"
LEFT JOIN "bricktracker_individual_minifigure_owners"
ON "bricktracker_individual_minifigures"."id" IS NOT DISTINCT FROM "bricktracker_individual_minifigure_owners"."id"
LEFT JOIN "bricktracker_individual_minifigure_statuses"
ON "bricktracker_individual_minifigures"."id" IS NOT DISTINCT FROM "bricktracker_individual_minifigure_statuses"."id"
LEFT JOIN "bricktracker_individual_minifigure_tags"
ON "bricktracker_individual_minifigures"."id" IS NOT DISTINCT FROM "bricktracker_individual_minifigure_tags"."id"
WHERE "bricktracker_individual_minifigures"."id" = :id
@@ -31,6 +31,7 @@ FROM (
"rebrickable_minifigures"."number_of_parts",
"rebrickable_minifigures"."name",
"rebrickable_minifigures"."image",
"bricktracker_minifigures"."rowid" AS "rowid",
'set' AS "source_type"
FROM "bricktracker_minifigures"
INNER JOIN "rebrickable_minifigures"
@@ -47,6 +48,7 @@ FROM (
"rebrickable_minifigures"."number_of_parts",
"rebrickable_minifigures"."name",
"rebrickable_minifigures"."image",
"bricktracker_individual_minifigures"."rowid" AS "rowid",
'individual' AS "source_type"
FROM "bricktracker_individual_minifigures"
INNER JOIN "rebrickable_minifigures"
+130
View File
@@ -1,10 +1,14 @@
from flask import Blueprint, redirect, render_template, request, url_for
from flask_login import login_required
from .exceptions import exception_handler
from ..individual_minifigure import IndividualMinifigure
from ..set_list import set_metadata_lists
from ..set_owner_list import BrickSetOwnerList
from ..set_tag_list import BrickSetTagList
from ..set_storage_list import BrickSetStorageList
from ..set_purchase_location_list import BrickSetPurchaseLocationList
from ..sql import BrickSQL
individual_minifigure_page = Blueprint('individual_minifigure', __name__, url_prefix='/individual-minifigures')
@@ -63,8 +67,134 @@ def update(*, id: str):
return redirect(url_for('individual_minifigure.details', id=id))
# Update quantity
@individual_minifigure_page.route('/<id>/update/quantity', methods=['POST'])
@login_required
@exception_handler(__file__)
def update_quantity(*, id: str):
item = IndividualMinifigure().select_by_id(id)
item.fields.quantity = int(request.json.get('value', 1))
BrickSQL().execute_and_commit(
'individual_minifigure/update',
parameters={
'id': item.fields.id,
'quantity': item.fields.quantity,
'description': item.fields.description,
'storage': item.fields.storage,
'purchase_location': item.fields.purchase_location,
}
)
return redirect(url_for('individual_minifigure.details', id=id))
# Update description
@individual_minifigure_page.route('/<id>/update/description', methods=['POST'])
@login_required
@exception_handler(__file__)
def update_description(*, id: str):
item = IndividualMinifigure().select_by_id(id)
item.fields.description = request.json.get('value', '')
BrickSQL().execute_and_commit(
'individual_minifigure/update',
parameters={
'id': item.fields.id,
'quantity': item.fields.quantity,
'description': item.fields.description,
'storage': item.fields.storage,
'purchase_location': item.fields.purchase_location,
}
)
return redirect(url_for('individual_minifigure.details', id=id))
# Update owner
@individual_minifigure_page.route('/<id>/update/owner/<metadata_id>', methods=['POST'])
@login_required
@exception_handler(__file__)
def update_owner(*, id: str, metadata_id: str):
item = IndividualMinifigure().select_by_id(id)
owner = BrickSetOwnerList.from_id(metadata_id)
owner.update_individual_minifigure_state(item, json=request.json)
return redirect(url_for('individual_minifigure.details', id=id))
# Update tag
@individual_minifigure_page.route('/<id>/update/tag/<metadata_id>', methods=['POST'])
@login_required
@exception_handler(__file__)
def update_tag(*, id: str, metadata_id: str):
item = IndividualMinifigure().select_by_id(id)
tag = BrickSetTagList.from_id(metadata_id)
tag.update_individual_minifigure_state(item, json=request.json)
return redirect(url_for('individual_minifigure.details', id=id))
# Update status
@individual_minifigure_page.route('/<id>/update/status/<metadata_id>', methods=['POST'])
@login_required
@exception_handler(__file__)
def update_status(*, id: str, metadata_id: str):
item = IndividualMinifigure().select_by_id(id)
from ..set_status_list import BrickSetStatusList
status = BrickSetStatusList.get(metadata_id)
status.update_individual_minifigure_state(item, json=request.json)
return redirect(url_for('individual_minifigure.details', id=id))
# Update storage
@individual_minifigure_page.route('/<id>/update/storage', methods=['POST'])
@login_required
@exception_handler(__file__)
def update_storage(*, id: str):
item = IndividualMinifigure().select_by_id(id)
storage_id = request.json.get('value')
BrickSQL().execute_and_commit(
'individual_minifigure/update',
parameters={
'id': item.fields.id,
'quantity': item.fields.quantity,
'description': item.fields.description,
'storage': storage_id if storage_id else None,
'purchase_location': item.fields.purchase_location,
}
)
return redirect(url_for('individual_minifigure.details', id=id))
# Update purchase location
@individual_minifigure_page.route('/<id>/update/purchase_location', methods=['POST'])
@login_required
@exception_handler(__file__)
def update_purchase_location(*, id: str):
item = IndividualMinifigure().select_by_id(id)
location_id = request.json.get('value')
BrickSQL().execute_and_commit(
'individual_minifigure/update',
parameters={
'id': item.fields.id,
'quantity': item.fields.quantity,
'description': item.fields.description,
'storage': item.fields.storage,
'purchase_location': location_id if location_id else None,
}
)
return redirect(url_for('individual_minifigure.details', id=id))
# Delete individual minifigure instance
@individual_minifigure_page.route('/<id>/delete', methods=['POST'])
@login_required
@exception_handler(__file__)
def delete(*, id: str):
item = IndividualMinifigure().select_by_id(id)
-2
View File
@@ -285,7 +285,6 @@ def details(*, id: str) -> str:
item=item,
all_instances=same_set_instances,
open_instructions=request.args.get('open_instructions'),
brickset_statuses=BrickSetStatusList.list(all=True),
**set_metadata_lists(as_class=True)
)
else:
@@ -294,7 +293,6 @@ def details(*, id: str) -> str:
'set.html',
item=item,
open_instructions=request.args.get('open_instructions'),
brickset_statuses=BrickSetStatusList.list(all=True),
**set_metadata_lists(as_class=True)
)
+6
View File
@@ -25,6 +25,7 @@ class BrickChanger {
switch (this.html_type) {
case "checkbox":
case "text":
case "number":
listener = "change";
break;
@@ -33,6 +34,11 @@ class BrickChanger {
}
break;
case "TEXTAREA":
this.html_type = "textarea";
listener = "change";
break;
case "SELECT":
this.html_type = "select";
listener = "change";
+16
View File
@@ -180,3 +180,19 @@
pointer-events: none;
}/* Duplicate filter support */
.duplicate-filter-hidden { display: none !important; }
/* Remove spinner arrows from number inputs */
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="number"] {
-moz-appearance: textfield;
}
/* Remove resize handle from textareas */
textarea {
resize: none;
}
+16 -71
View File
@@ -1,4 +1,6 @@
{% extends 'base.html' %}
{% import 'macro/accordion.html' as accordion %}
{% import 'macro/form.html' as form %}
{% block title %} - Individual Minifigure {{ item.fields.name }}{% endblock %}
@@ -23,78 +25,21 @@
<img class="card-medium-img" src="{{ item.url_for_image() }}" alt="{{ item.fields.figure }}" loading="lazy">
</a>
</div>
<div class="card-body">
<form id="individual-minifigure-form" method="POST" action="{{ url_for('individual_minifigure.update', id=item.fields.id) }}">
<div class="mb-3">
<label for="quantity" class="form-label">Quantity</label>
<input type="number" class="form-control" id="quantity" name="quantity" value="{{ item.fields.quantity }}" min="1" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description" rows="3">{{ item.fields.description or '' }}</textarea>
</div>
<div class="mb-3">
<label for="storage" class="form-label">Storage</label>
<select class="form-select" id="storage" name="storage">
<option value="">None</option>
{% for storage in brickset_storages %}
<option value="{{ storage.fields.id }}" {% if item.fields.storage == storage.fields.id %}selected{% endif %}>
{{ storage.fields.name }}
</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label for="purchase_location" class="form-label">Purchase Location</label>
<select class="form-select" id="purchase_location" name="purchase_location">
<option value="">None</option>
{% for location in brickset_purchase_locations %}
<option value="{{ location.fields.id }}" {% if item.fields.purchase_location == location.fields.id %}selected{% endif %}>
{{ location.fields.name }}
</option>
{% endfor %}
</select>
</div>
<div class="mb-3">
<label class="form-label">Owners</label>
{% for owner in brickset_owners %}
<div class="form-check">
<input class="form-check-input" type="checkbox" name="owners" value="{{ owner.fields.id }}" id="owner-{{ owner.fields.id }}"
{% if owner.has_individual_minifigure(item) %}checked{% endif %}>
<label class="form-check-label" for="owner-{{ owner.fields.id }}">
{{ owner.fields.name }}
</label>
</div>
{% endfor %}
</div>
<div class="mb-3">
<label class="form-label">Tags</label>
{% for tag in brickset_tags %}
<div class="form-check">
<input class="form-check-input" type="checkbox" name="tags" value="{{ tag.fields.id }}" id="tag-{{ tag.fields.id }}"
{% if tag.has_individual_minifigure(item) %}checked{% endif %}>
<label class="form-check-label" for="tag-{{ tag.fields.id }}">
{{ tag.fields.name }}
</label>
</div>
{% endfor %}
</div>
<div class="d-flex justify-content-between">
<button type="submit" class="btn btn-primary">
<i class="ri-save-line"></i> Save Changes
</button>
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#deleteModal">
<i class="ri-delete-bin-line"></i> Delete Instance
</button>
</div>
</form>
<div class="accordion accordion-flush border-top" id="individual-minifigure-details">
{{ accordion.header('Quantity', 'quantity', 'individual-minifigure-details', icon='functions') }}
{{ form.input('Quantity', item.fields.id, 'quantity', item.url_for_quantity(), item.fields.quantity, icon='functions', type='number') }}
{{ accordion.footer() }}
{{ accordion.header('Description', 'description-section', 'individual-minifigure-details', icon='file-text-line') }}
{{ form.input('Description', item.fields.id, 'description', item.url_for_description(), item.fields.description or '', icon='file-text-line', textarea=true) }}
{{ accordion.footer() }}
{% include 'individual_minifigure/management.html' %}
{% if g.login.is_authenticated() %}
{{ accordion.header('Danger zone', 'danger-zone', 'individual-minifigure-details', danger=true, class='text-end') }}
<a href="{{ url_for('individual_minifigure.delete', id=item.fields.id) }}" class="btn btn-danger" role="button" data-bs-toggle="modal" data-bs-target="#deleteModal"><i class="ri-close-line"></i> Delete this individual minifigure instance</a>
{{ accordion.footer() }}
{% endif %}
</div>
<div class="card-footer"></div>
</div>
</div>
</div>
@@ -0,0 +1,71 @@
{% import 'macro/accordion.html' as accordion %}
{% import 'macro/form.html' as form %}
{% if g.login.is_authenticated() %}
{{ accordion.header('Management', 'individual-minifigure-management', 'individual-minifigure-details', icon='settings-4-line', class='p-0') }}
{{ accordion.header('Owners', 'owner', 'individual-minifigure-management', icon='group-line', class='p-0') }}
<ul class="list-group list-group-flush">
{% if brickset_owners | length %}
{% for owner in brickset_owners %}
<li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(owner.fields.name, item.fields.id, owner.as_dataset(), owner.url_for_individual_minifigure_state(item.fields.id), item.fields[owner.as_column()]) }}</li>
{% endfor %}
{% else %}
<li class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No owner found.</li>
{% endif %}
</ul>
<div class="list-group list-group-flush border-top">
<a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_owner=true) }}"><i class="ri-settings-4-line"></i> Manage the minifigure owners</a>
</div>
{{ accordion.footer() }}
{{ accordion.header('Storage', 'storage', 'individual-minifigure-management', icon='archive-2-line') }}
{% if brickset_storages | length %}
{{ form.select('Storage', item.fields.id, brickset_storages.as_prefix(), brickset_storages.url_for_individual_minifigure_value(item.fields.id), item.fields.storage, brickset_storages, icon='building-line') }}
{% else %}
<p class="text-center"><i class="ri-error-warning-line"></i> No storage found.</p>
{% endif %}
<hr>
<a href="{{ url_for('admin.admin', open_storage=true) }}" class="btn btn-primary" role="button"><i class="ri-settings-4-line"></i> Manage the storages</a>
{{ accordion.footer() }}
{{ accordion.header('Purchase', 'purchase', 'individual-minifigure-management', icon='wallet-3-line') }}
<div class="row row-cols-lg-auto g-1 justify-content-start align-items-center pb-2">
<div class="col-12 flex-grow-1">
{% if brickset_purchase_locations | length %}
{{ form.select('Location', item.fields.id, brickset_purchase_locations.as_prefix(), brickset_purchase_locations.url_for_individual_minifigure_value(item.fields.id), item.fields.purchase_location, brickset_purchase_locations, icon='building-line') }}
{% else %}
<i class="ri-error-warning-line"></i> No purchase location found.
{% endif %}
</div>
</div>
<hr>
<a href="{{ url_for('admin.admin', open_purchase_location=true) }}" class="btn btn-primary" role="button"><i class="ri-settings-4-line"></i> Manage the purchase locations</a>
{{ accordion.footer() }}
{{ accordion.header('Statuses', 'status', 'individual-minifigure-management', icon='checkbox-line', class='p-0') }}
<ul class="list-group list-group-flush">
{% if brickset_statuses | length %}
{% for status in brickset_statuses %}
<li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(status.fields.name, item.fields.id, status.as_dataset(), status.url_for_individual_minifigure_state(item.fields.id), item.fields[status.as_column()]) }}</li>
{% endfor %}
{% else %}
<li class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No status found.</li>
{% endif %}
</ul>
<div class="list-group list-group-flush border-top">
<a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_status=true) }}"><i class="ri-settings-4-line"></i> Manage the statuses</a>
</div>
{{ accordion.footer() }}
{{ accordion.header('Tags', 'tag', 'individual-minifigure-management', icon='price-tag-2-line', class='p-0') }}
<ul class="list-group list-group-flush">
{% if brickset_tags | length %}
{% for tag in brickset_tags %}
<li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(tag.fields.name, item.fields.id, tag.as_dataset(), tag.url_for_individual_minifigure_state(item.fields.id), item.fields[tag.as_column()]) }}</li>
{% endfor %}
{% else %}
<li class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No tag found.</li>
{% endif %}
</ul>
<div class="list-group list-group-flush border-top">
<a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_tag=true) }}"><i class="ri-settings-4-line"></i> Manage the tags</a>
</div>
{{ accordion.footer() }}
{{ accordion.footer() }}
{% endif %}
+12 -2
View File
@@ -16,14 +16,23 @@
{% endif %}
{% endmacro %}
{% macro input(name, id, prefix, url, value, all=none, read_only=none, icon=none, suffix=none, date=false) %}
{% macro input(name, id, prefix, url, value, all=none, read_only=none, icon=none, suffix=none, date=false, type=none, textarea=false) %}
{% if all or read_only %}
{{ value }}
{% else %}
<label class="visually-hidden" for="{{ prefix }}-{{ id }}">{{ name }}</label>
<div class="input-group">
{% if icon %}<span class="input-group-text px-1"><i class="ri-{{ icon }} me-1"></i><span class="ms-1 d-none d-md-inline"> {{ name }}</span></span>{% endif %}
<input class="form-control form-control-sm flex-shrink-1 px-1" type="text" id="{{ prefix }}-{{ id }}" value="{% if value %}{{ value }}{% endif %}"
{% if textarea %}
<textarea class="form-control form-control-sm flex-shrink-1 px-1" id="{{ prefix }}-{{ id }}"
{% if g.login.is_authenticated() %}
data-changer-id="{{ id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ url }}"
{% else %}
disabled
{% endif %}
autocomplete="off" rows="3">{% if value %}{{ value }}{% endif %}</textarea>
{% else %}
<input class="form-control form-control-sm flex-shrink-1 px-1" type="{% if type %}{{ type }}{% else %}text{% endif %}" id="{{ prefix }}-{{ id }}" value="{% if value %}{{ value }}{% endif %}"
{% if g.login.is_authenticated() %}
data-changer-id="{{ id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ url }}"
{% if date %}data-changer-date="true"{% endif %}
@@ -31,6 +40,7 @@
disabled
{% endif %}
autocomplete="off">
{% endif %}
{% if suffix %}<span class="input-group-text d-none d-md-inline px-1">{{ suffix }}</span>{% endif %}
{% if g.login.is_authenticated() %}
<span id="status-{{ prefix }}-{{ id }}" class="input-group-text ri-save-line px-1"></span>