From c93a14f70b84be769fe4df0029bbaa946f9d804f Mon Sep 17 00:00:00 2001 From: FrederikBaerentsen Date: Fri, 26 Jun 2026 22:18:09 +0200 Subject: [PATCH] feat(set): admin-defined typed custom fields per set (#146) --- bricktracker/app.py | 2 + bricktracker/metadata_list.py | 2 + bricktracker/reload.py | 4 + bricktracker/set.py | 2 + bricktracker/set_custom_field.py | 99 +++++++++++++++++++ bricktracker/set_custom_field_list.py | 24 +++++ bricktracker/set_list.py | 2 + bricktracker/sql/migrations/0029.sql | 23 +++++ bricktracker/sql/set/base/base.sql | 3 + bricktracker/sql/set/base/full.sql | 5 + .../sql/set/metadata/custom_field/base.sql | 11 +++ .../sql/set/metadata/custom_field/delete.sql | 9 ++ .../sql/set/metadata/custom_field/insert.sql | 17 ++++ .../sql/set/metadata/custom_field/list.sql | 1 + .../sql/set/metadata/custom_field/select.sql | 5 + .../metadata/custom_field/update/field.sql | 3 + .../metadata/custom_field/update/value.sql | 10 ++ bricktracker/version.py | 2 +- bricktracker/views/admin/admin.py | 10 ++ bricktracker/views/admin/custom_field.py | 98 ++++++++++++++++++ bricktracker/views/set.py | 14 +++ templates/admin.html | 3 + templates/admin/custom_field.html | 65 ++++++++++++ templates/admin/custom_field/delete.html | 19 ++++ templates/set/management.html | 15 +++ 25 files changed, 447 insertions(+), 1 deletion(-) create mode 100644 bricktracker/set_custom_field.py create mode 100644 bricktracker/set_custom_field_list.py create mode 100644 bricktracker/sql/migrations/0029.sql create mode 100644 bricktracker/sql/set/metadata/custom_field/base.sql create mode 100644 bricktracker/sql/set/metadata/custom_field/delete.sql create mode 100644 bricktracker/sql/set/metadata/custom_field/insert.sql create mode 100644 bricktracker/sql/set/metadata/custom_field/list.sql create mode 100644 bricktracker/sql/set/metadata/custom_field/select.sql create mode 100644 bricktracker/sql/set/metadata/custom_field/update/field.sql create mode 100644 bricktracker/sql/set/metadata/custom_field/update/value.sql create mode 100644 bricktracker/views/admin/custom_field.py create mode 100644 templates/admin/custom_field.html create mode 100644 templates/admin/custom_field/delete.html diff --git a/bricktracker/app.py b/bricktracker/app.py index 43db194..cecb512 100644 --- a/bricktracker/app.py +++ b/bricktracker/app.py @@ -17,6 +17,7 @@ from bricktracker.template_filters import replace_query_filter from bricktracker.version import __version__ from bricktracker.views.add import add_page from bricktracker.views.admin.admin import admin_page +from bricktracker.views.admin.custom_field import admin_custom_field_page from bricktracker.views.admin.database import admin_database_page from bricktracker.views.admin.export import admin_export_page from bricktracker.views.admin.image import admin_image_page @@ -156,6 +157,7 @@ def setup_app(app: Flask) -> None: # Register admin routes app.register_blueprint(admin_page) + app.register_blueprint(admin_custom_field_page) app.register_blueprint(admin_database_page) app.register_blueprint(admin_export_page) app.register_blueprint(admin_image_page) diff --git a/bricktracker/metadata_list.py b/bricktracker/metadata_list.py index 197fb34..86603e8 100644 --- a/bricktracker/metadata_list.py +++ b/bricktracker/metadata_list.py @@ -6,6 +6,7 @@ from flask import url_for from .exceptions import ErrorException, NotFoundException from .fields import BrickRecordFields from .record_list import BrickRecordList +from .set_custom_field import BrickSetCustomField from .set_owner import BrickSetOwner from .set_purchase_location import BrickSetPurchaseLocation from .set_status import BrickSetStatus @@ -17,6 +18,7 @@ logger = logging.getLogger(__name__) T = TypeVar( 'T', + BrickSetCustomField, BrickSetOwner, BrickSetPurchaseLocation, BrickSetStatus, diff --git a/bricktracker/reload.py b/bricktracker/reload.py index 99d95bb..a1b1a8e 100644 --- a/bricktracker/reload.py +++ b/bricktracker/reload.py @@ -1,5 +1,6 @@ from .instructions_list import BrickInstructionsList from .retired_list import BrickRetiredList +from .set_custom_field_list import BrickSetCustomFieldList from .set_owner_list import BrickSetOwnerList from .set_purchase_location_list import BrickSetPurchaseLocationList from .set_status_list import BrickSetStatusList @@ -31,6 +32,9 @@ def reload() -> None: # Reload the set tags BrickSetTagList.new(force=True) + # Reload the set custom fields + BrickSetCustomFieldList.new(force=True) + # Reload retired sets BrickRetiredList(force=True) diff --git a/bricktracker/set.py b/bricktracker/set.py index 1475875..eb9232c 100644 --- a/bricktracker/set.py +++ b/bricktracker/set.py @@ -10,6 +10,7 @@ from .exceptions import NotFoundException, DatabaseException, ErrorException from .minifigure_list import BrickMinifigureList from .part_list import BrickPartList from .rebrickable_set import RebrickableSet +from .set_custom_field_list import BrickSetCustomFieldList from .set_owner_list import BrickSetOwnerList from .set_purchase_location_list import BrickSetPurchaseLocationList from .set_status_list import BrickSetStatusList @@ -334,6 +335,7 @@ class BrickSet(RebrickableSet): # Load from database if not self.select( + custom_fields=BrickSetCustomFieldList.as_columns(), owners=BrickSetOwnerList.as_columns(), statuses=BrickSetStatusList.as_columns(all=True), tags=BrickSetTagList.as_columns(), diff --git a/bricktracker/set_custom_field.py b/bricktracker/set_custom_field.py new file mode 100644 index 0000000..35e3bfa --- /dev/null +++ b/bricktracker/set_custom_field.py @@ -0,0 +1,99 @@ +import logging +from typing import Any, Self + +from .exceptions import DatabaseException, ErrorException +from .metadata import BrickMetadata +from .sql import BrickSQL + +logger = logging.getLogger(__name__) + +# Allowed custom field types. The type only drives the input widget and light +# validation; the value is always stored as TEXT. +CUSTOM_FIELD_TYPES = ('text', 'number', 'date') + + +# Lego set custom field metadata (a typed, admin-defined per-set value) +class BrickSetCustomField(BrickMetadata): + kind: str = 'custom field' + + # Set value endpoint + set_state_endpoint: str = 'set.update_custom_field' + + # Queries + delete_query: str = 'set/metadata/custom_field/delete' + insert_query: str = 'set/metadata/custom_field/insert' + select_query: str = 'set/metadata/custom_field/select' + update_field_query: str = 'set/metadata/custom_field/update/field' + update_set_value_query: str = 'set/metadata/custom_field/update/value' + + # SQL column name uses an underscore (kind has a space), so override the + # default which would produce "custom-field_{id}". + def as_column(self, /) -> str: + return 'custom_field_{id}'.format(id=self.fields.id) + + # Grab data from a form (name + type) + def from_form(self, form: dict[str, str], /) -> Self: + super().from_form(form) + + field_type = form.get('type', 'text') + + if field_type not in CUSTOM_FIELD_TYPES: + raise ErrorException( + 'Unknown custom field type: {type}'.format(type=field_type) + ) + + self.fields.type = field_type + + return self + + # Insert into database (carry the type along) + def insert(self, /, **_) -> None: + super().insert( + type=self.fields.type + ) + + # Update the per-set value of this custom field. Unlike the base + # update_set_value (which targets a fixed column), the column name is + # dynamic, so it is injected into the query like update_set_state does. + def update_value( + self, + brickset: Any, + /, + *, + json: Any | None = None, + value: Any | None = None, + ) -> Any: + if value is None and json is not None: + value = json.get('value', '') + + if value == '': + value = None + + parameters = self.sql_parameters() + parameters['set_id'] = brickset.fields.id + parameters['value'] = value + + rows, _ = BrickSQL().execute_and_commit( + self.update_set_value_query, + parameters=parameters, + name=self.as_column(), + ) + + if rows != 1: + raise DatabaseException( + 'Could not update the custom field value for set {set} ({id})'.format( # noqa: E501 + set=brickset.fields.set, + id=brickset.fields.id, + ) + ) + + logger.info( + 'Custom field "{name}" value changed to "{value}" for set {set} ({id})'.format( # noqa: E501 + name=self.fields.name, + value=value, + set=brickset.fields.set, + id=brickset.fields.id, + ) + ) + + return value diff --git a/bricktracker/set_custom_field_list.py b/bricktracker/set_custom_field_list.py new file mode 100644 index 0000000..80bdd80 --- /dev/null +++ b/bricktracker/set_custom_field_list.py @@ -0,0 +1,24 @@ +from typing import Self + +from .metadata_list import BrickMetadataList +from .set_custom_field import BrickSetCustomField + + +# Lego sets custom field list +class BrickSetCustomFieldList(BrickMetadataList[BrickSetCustomField]): + kind: str = 'set custom fields' + + # Database + table: str = 'bricktracker_set_custom_fields' + order: str = '"bricktracker_metadata_custom_fields"."name"' + + # Queries + select_query: str = 'set/metadata/custom_field/list' + + # Set value endpoint + set_value_endpoint: str = 'set.update_custom_field' + + # Instantiate the list with the proper class + @classmethod + def new(cls, /, *, force: bool = False) -> Self: + return cls(BrickSetCustomField, force=force) diff --git a/bricktracker/set_list.py b/bricktracker/set_list.py index a61b295..1becad2 100644 --- a/bricktracker/set_list.py +++ b/bricktracker/set_list.py @@ -3,6 +3,7 @@ from typing import Any, Self, Union from flask import current_app from .record_list import BrickRecordList +from .set_custom_field_list import BrickSetCustomFieldList from .set_owner import BrickSetOwner from .set_owner_list import BrickSetOwnerList from .set_purchase_location import BrickSetPurchaseLocation @@ -705,6 +706,7 @@ def set_metadata_lists( ] ]: return { + 'brickset_custom_fields': BrickSetCustomFieldList.list(), 'brickset_owners': BrickSetOwnerList.list(), 'brickset_purchase_locations': BrickSetPurchaseLocationList.list(as_class=as_class), # noqa: E501 'brickset_storages': BrickSetStorageList.list(as_class=as_class), diff --git a/bricktracker/sql/migrations/0029.sql b/bricktracker/sql/migrations/0029.sql new file mode 100644 index 0000000..bead5d9 --- /dev/null +++ b/bricktracker/sql/migrations/0029.sql @@ -0,0 +1,23 @@ +-- description: Add typed custom fields (admin-defined per-set fields) + +BEGIN TRANSACTION; + +-- Defines each custom field: an id, a name and a type (text / number / date). +-- The type only drives the input widget + light validation; the per-set value +-- is always stored as TEXT in a dynamic column on bricktracker_set_custom_fields. +CREATE TABLE "bricktracker_metadata_custom_fields" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "type" TEXT NOT NULL DEFAULT 'text', + PRIMARY KEY("id") +); + +-- Holds the per-set value of each custom field. A "custom_field_{id}" TEXT +-- column is added for every field created (mirroring the tag/status pattern). +CREATE TABLE "bricktracker_set_custom_fields" ( + "id" TEXT NOT NULL, + PRIMARY KEY("id"), + FOREIGN KEY("id") REFERENCES "bricktracker_sets"("id") +); + +COMMIT; diff --git a/bricktracker/sql/set/base/base.sql b/bricktracker/sql/set/base/base.sql index 1c793a5..1d26637 100644 --- a/bricktracker/sql/set/base/base.sql +++ b/bricktracker/sql/set/base/base.sql @@ -23,6 +23,9 @@ SELECT {% block statuses %} {% if statuses %}{{ statuses }},{% endif %} {% endblock %} + {% block custom_fields %} + {% if custom_fields %}{{ custom_fields }},{% endif %} + {% endblock %} {% block total_missing %} NULL AS "total_missing", -- dummy for order: total_missing {% endblock %} diff --git a/bricktracker/sql/set/base/full.sql b/bricktracker/sql/set/base/full.sql index 92612df..5580221 100644 --- a/bricktracker/sql/set/base/full.sql +++ b/bricktracker/sql/set/base/full.sql @@ -32,6 +32,11 @@ LEFT JOIN "bricktracker_set_tags" ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_tags"."id" {% endif %} +{% if custom_fields %} +LEFT JOIN "bricktracker_set_custom_fields" +ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_custom_fields"."id" +{% endif %} + -- LEFT JOIN + SELECT to avoid messing the total LEFT JOIN ( SELECT diff --git a/bricktracker/sql/set/metadata/custom_field/base.sql b/bricktracker/sql/set/metadata/custom_field/base.sql new file mode 100644 index 0000000..47ba610 --- /dev/null +++ b/bricktracker/sql/set/metadata/custom_field/base.sql @@ -0,0 +1,11 @@ +SELECT + "bricktracker_metadata_custom_fields"."id", + "bricktracker_metadata_custom_fields"."name", + "bricktracker_metadata_custom_fields"."type" +FROM "bricktracker_metadata_custom_fields" + +{% block where %}{% endblock %} + +{% if order %} +ORDER BY {{ order }} +{% endif %} diff --git a/bricktracker/sql/set/metadata/custom_field/delete.sql b/bricktracker/sql/set/metadata/custom_field/delete.sql new file mode 100644 index 0000000..9cca690 --- /dev/null +++ b/bricktracker/sql/set/metadata/custom_field/delete.sql @@ -0,0 +1,9 @@ +BEGIN TRANSACTION; + +ALTER TABLE "bricktracker_set_custom_fields" +DROP COLUMN "custom_field_{{ id }}"; + +DELETE FROM "bricktracker_metadata_custom_fields" +WHERE "bricktracker_metadata_custom_fields"."id" IS NOT DISTINCT FROM '{{ id }}'; + +COMMIT; diff --git a/bricktracker/sql/set/metadata/custom_field/insert.sql b/bricktracker/sql/set/metadata/custom_field/insert.sql new file mode 100644 index 0000000..cd31dfc --- /dev/null +++ b/bricktracker/sql/set/metadata/custom_field/insert.sql @@ -0,0 +1,17 @@ +BEGIN TRANSACTION; + +-- Add the value column for this custom field (per-set TEXT value). +ALTER TABLE "bricktracker_set_custom_fields" +ADD COLUMN "custom_field_{{ id }}" TEXT; + +INSERT INTO "bricktracker_metadata_custom_fields" ( + "id", + "name", + "type" +) VALUES ( + '{{ id }}', + '{{ name }}', + '{{ type }}' +); + +COMMIT; diff --git a/bricktracker/sql/set/metadata/custom_field/list.sql b/bricktracker/sql/set/metadata/custom_field/list.sql new file mode 100644 index 0000000..362c0c9 --- /dev/null +++ b/bricktracker/sql/set/metadata/custom_field/list.sql @@ -0,0 +1 @@ +{% extends 'set/metadata/custom_field/base.sql' %} diff --git a/bricktracker/sql/set/metadata/custom_field/select.sql b/bricktracker/sql/set/metadata/custom_field/select.sql new file mode 100644 index 0000000..07dff9a --- /dev/null +++ b/bricktracker/sql/set/metadata/custom_field/select.sql @@ -0,0 +1,5 @@ +{% extends 'set/metadata/custom_field/base.sql' %} + +{% block where %} +WHERE "bricktracker_metadata_custom_fields"."id" IS NOT DISTINCT FROM :id +{% endblock %} diff --git a/bricktracker/sql/set/metadata/custom_field/update/field.sql b/bricktracker/sql/set/metadata/custom_field/update/field.sql new file mode 100644 index 0000000..8c229cf --- /dev/null +++ b/bricktracker/sql/set/metadata/custom_field/update/field.sql @@ -0,0 +1,3 @@ +UPDATE "bricktracker_metadata_custom_fields" +SET "{{field}}" = :value +WHERE "bricktracker_metadata_custom_fields"."id" IS NOT DISTINCT FROM :id diff --git a/bricktracker/sql/set/metadata/custom_field/update/value.sql b/bricktracker/sql/set/metadata/custom_field/update/value.sql new file mode 100644 index 0000000..51bc58e --- /dev/null +++ b/bricktracker/sql/set/metadata/custom_field/update/value.sql @@ -0,0 +1,10 @@ +INSERT INTO "bricktracker_set_custom_fields" ( + "id", + "{{name}}" +) VALUES ( + :set_id, + :value +) +ON CONFLICT("id") +DO UPDATE SET "{{name}}" = :value +WHERE "bricktracker_set_custom_fields"."id" IS NOT DISTINCT FROM :set_id diff --git a/bricktracker/version.py b/bricktracker/version.py index 7202eb4..0a03ee1 100644 --- a/bricktracker/version.py +++ b/bricktracker/version.py @@ -1,4 +1,4 @@ from typing import Final __version__: Final[str] = '1.5.0' -__database_version__: Final[int] = 28 +__database_version__: Final[int] = 29 diff --git a/bricktracker/views/admin/admin.py b/bricktracker/views/admin/admin.py index 7578083..6c204bf 100644 --- a/bricktracker/views/admin/admin.py +++ b/bricktracker/views/admin/admin.py @@ -11,6 +11,8 @@ from ..exceptions import exception_handler from ...instructions_list import BrickInstructionsList from ...rebrickable_image import RebrickableImage from ...retired_list import BrickRetiredList +from ...set_custom_field import BrickSetCustomField +from ...set_custom_field_list import BrickSetCustomFieldList from ...set_owner import BrickSetOwner from ...set_owner_list import BrickSetOwnerList from ...set_purchase_location import BrickSetPurchaseLocation @@ -129,6 +131,7 @@ def admin() -> str: database_upgrade_needed: bool = False database_version: int = -1 instructions: BrickInstructionsList | None = None + metadata_custom_fields: list[BrickSetCustomField] = [] metadata_owners: list[BrickSetOwner] = [] metadata_purchase_locations: list[BrickSetPurchaseLocation] = [] metadata_statuses: list[BrickSetStatus] = [] @@ -148,6 +151,7 @@ def admin() -> str: instructions = BrickInstructionsList() + metadata_custom_fields = BrickSetCustomFieldList.list() metadata_owners = BrickSetOwnerList.list() metadata_purchase_locations = BrickSetPurchaseLocationList.list() metadata_statuses = BrickSetStatusList.list(all=True) @@ -176,6 +180,7 @@ def admin() -> str: open_image = request.args.get('open_image', None) open_instructions = request.args.get('open_instructions', None) open_logout = request.args.get('open_logout', None) + open_custom_field = request.args.get('open_custom_field', None) open_metadata = request.args.get('open_metadata', None) open_owner = request.args.get('open_owner', None) open_purchase_location = request.args.get('open_purchase_location', None) @@ -187,6 +192,7 @@ def admin() -> str: open_metadata = ( open_metadata or + open_custom_field or open_owner or open_purchase_location or open_status or @@ -215,6 +221,7 @@ def admin() -> str: open_value = should_expand('value', request.args.get('open_value', None)) # Metadata sub-sections + open_custom_field = should_expand('custom_field', open_custom_field) open_owner = should_expand('owner', open_owner) open_purchase_location = should_expand('purchase_location', open_purchase_location) open_status = should_expand('status', open_status) @@ -224,6 +231,7 @@ def admin() -> str: # Recalculate metadata section based on sub-sections or direct config open_metadata = ( should_expand('metadata', open_metadata) or + open_custom_field or open_owner or open_purchase_location or open_status or @@ -267,6 +275,7 @@ def admin() -> str: database_upgrade_needed=database_upgrade_needed, database_version=database_version, instructions=instructions, + metadata_custom_fields=metadata_custom_fields, metadata_owners=metadata_owners, metadata_purchase_locations=metadata_purchase_locations, metadata_statuses=metadata_statuses, @@ -280,6 +289,7 @@ def admin() -> str: open_image=open_image, open_instructions=open_instructions, open_logout=open_logout, + open_custom_field=open_custom_field, open_metadata=open_metadata, open_owner=open_owner, open_purchase_location=open_purchase_location, diff --git a/bricktracker/views/admin/custom_field.py b/bricktracker/views/admin/custom_field.py new file mode 100644 index 0000000..a5ac934 --- /dev/null +++ b/bricktracker/views/admin/custom_field.py @@ -0,0 +1,98 @@ +from flask import ( + Blueprint, + jsonify, + redirect, + request, + render_template, + url_for, +) +from flask_login import login_required +from werkzeug.wrappers.response import Response + +from ..exceptions import exception_handler +from ...reload import reload +from ...set_custom_field import BrickSetCustomField + +admin_custom_field_page = Blueprint( + 'admin_custom_field', + __name__, + url_prefix='/admin/custom_field' +) + + +# Add a metadata custom field +@admin_custom_field_page.route('/add', methods=['POST']) +@login_required +@exception_handler( + __file__, + post_redirect='admin.admin', + error_name='custom_field_error', + open_custom_field=True +) +def add() -> Response: + BrickSetCustomField().from_form(request.form).insert() + + reload() + + return redirect(url_for('admin.admin', open_custom_field=True)) + + +# Delete the metadata custom field +@admin_custom_field_page.route('/delete', methods=['GET']) +@login_required +@exception_handler(__file__) +def delete(*, id: str) -> str: + return render_template( + 'admin.html', + delete_custom_field=True, + custom_field=BrickSetCustomField().select_specific(id), + custom_field_error=request.args.get('custom_field_error') + ) + + +# Actually delete the metadata custom field +@admin_custom_field_page.route('/delete', methods=['POST']) +@login_required +@exception_handler( + __file__, + post_redirect='admin_custom_field.delete', + error_name='custom_field_error' +) +def do_delete(*, id: str) -> Response: + custom_field = BrickSetCustomField().select_specific(id) + custom_field.delete() + + reload() + + return redirect(url_for('admin.admin', open_custom_field=True)) + + +# Change a field of a metadata custom field (name or type) +@admin_custom_field_page.route('//field/', methods=['POST']) +@login_required +@exception_handler(__file__, json=True) +def update_field(*, id: str, name: str) -> Response: + custom_field = BrickSetCustomField().select_specific(id) + value = custom_field.update_field(name, json=request.json) + + reload() + + return jsonify({'value': value}) + + +# Rename the metadata custom field +@admin_custom_field_page.route('/rename', methods=['POST']) +@login_required +@exception_handler( + __file__, + post_redirect='admin.admin', + error_name='custom_field_error', + open_custom_field=True +) +def rename(*, id: str) -> Response: + custom_field = BrickSetCustomField().select_specific(id) + custom_field.from_form(request.form).rename() + + reload() + + return redirect(url_for('admin.admin', open_custom_field=True)) diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py index f692c41..ec8ea21 100644 --- a/bricktracker/views/set.py +++ b/bricktracker/views/set.py @@ -23,6 +23,7 @@ from ..rebrickable_set import RebrickableSet from ..set import BrickSet from ..sidecar import BrickSidecar from ..sidecar_set import summarize as sidecar_summarize +from ..set_custom_field_list import BrickSetCustomFieldList from ..set_list import BrickSetList, set_metadata_lists from ..set_owner_list import BrickSetOwnerList from ..set_purchase_location_list import BrickSetPurchaseLocationList @@ -229,6 +230,19 @@ def update_tag(*, id: str, metadata_id: str) -> Response: return jsonify({'value': state}) +# Change the value of a custom field +@set_page.route('//custom_field/', methods=['POST']) +@login_required +@exception_handler(__file__, json=True) +def update_custom_field(*, id: str, metadata_id: str) -> Response: + brickset = BrickSet().select_light(id) + custom_field = BrickSetCustomFieldList.get(metadata_id) + + value = custom_field.update_value(brickset, json=request.json) + + return jsonify({'value': value}) + + # Ask for deletion of a set @set_page.route('//delete', methods=['GET']) @login_required diff --git a/templates/admin.html b/templates/admin.html index 66e1789..d193f4c 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -20,6 +20,8 @@ {% include 'admin/owner/delete.html' %} {% elif delete_purchase_location %} {% include 'admin/purchase_location/delete.html' %} + {% elif delete_custom_field %} + {% include 'admin/custom_field/delete.html' %} {% elif delete_status %} {% include 'admin/status/delete.html' %} {% elif delete_storage %} @@ -52,6 +54,7 @@ {% include 'admin/status.html' %} {% include 'admin/storage.html' %} {% include 'admin/tag.html' %} + {% include 'admin/custom_field.html' %} {{ accordion.footer() }} {% include 'admin/refresh.html' %} {% include 'admin/database.html' %} diff --git a/templates/admin/custom_field.html b/templates/admin/custom_field.html new file mode 100644 index 0000000..ad71bce --- /dev/null +++ b/templates/admin/custom_field.html @@ -0,0 +1,65 @@ +{% import 'macro/accordion.html' as accordion %} + +{{ accordion.header('Set custom fields', 'custom_field', 'metadata', expanded=open_custom_field, icon='edit-line', class='p-0') }} +{% if custom_field_error %}{% endif %} +
    + {% if metadata_custom_fields | length %} + {% for custom_field in metadata_custom_fields %} +
  • +
    +
    + +
    +
    Name
    + + +
    +
    +
    +
    +
    Type
    + + +
    +
    +
    + Delete +
    +
    +
  • + {% endfor %} + {% else %} +
  • No custom field found.
  • + {% endif %} +
  • +
    +
    + +
    +
    Name
    + +
    +
    +
    +
    +
    Type
    + +
    +
    +
    + +
    +
    +
  • +
+{{ accordion.footer() }} diff --git a/templates/admin/custom_field/delete.html b/templates/admin/custom_field/delete.html new file mode 100644 index 0000000..752484a --- /dev/null +++ b/templates/admin/custom_field/delete.html @@ -0,0 +1,19 @@ +{% import 'macro/accordion.html' as accordion %} + +{{ accordion.header('Set custom fields danger zone', 'custom-field-danger', 'admin', expanded=true, danger=true, class='text-end') }} +
+ {% if custom_field_error %}{% endif %} + +
+
+
+
Name
+ +
+
+
+
+ Back to the admin + +
+{{ accordion.footer() }} diff --git a/templates/set/management.html b/templates/set/management.html index a59ea3c..e9e1674 100644 --- a/templates/set/management.html +++ b/templates/set/management.html @@ -60,6 +60,21 @@ Manage the set tags {{ accordion.footer() }} + {{ accordion.header('Custom fields', 'custom-field', 'set-management', icon='edit-line') }} + {% if brickset_custom_fields | length %} +
+ {% for custom_field in brickset_custom_fields %} +
+ {{ form.input(custom_field.fields.name, item.fields.id, custom_field.as_dataset(), custom_field.url_for_set_state(item.fields.id), item.fields[custom_field.as_column()], icon=('calendar-line' if custom_field.fields.type == 'date' else ('hashtag' if custom_field.fields.type == 'number' else 'edit-line')), date=(custom_field.fields.type == 'date')) }} +
+ {% endfor %} +
+
+ {% else %} +

No custom field found.

+ {% endif %} + Manage the custom fields + {{ accordion.footer() }} {{ accordion.header('Data', 'data', 'set-management', icon='database-2-line') }} Refresh the set data {{ accordion.footer() }}