mirror of
https://gitea.baerentsen.space/FrederikBaerentsen/BrickTracker.git
synced 2026-04-29 19:49:25 -05:00
fix(minifigures): fixed metadata format and individual minifigures layout
This commit is contained in:
+14
-9
@@ -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
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 [
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user