feat(set): admin-defined typed custom fields per set (#146)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
{% extends 'set/metadata/custom_field/base.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 %}
|
||||
@@ -0,0 +1,3 @@
|
||||
UPDATE "bricktracker_metadata_custom_fields"
|
||||
SET "{{field}}" = :value
|
||||
WHERE "bricktracker_metadata_custom_fields"."id" IS NOT DISTINCT FROM :id
|
||||
@@ -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
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Final
|
||||
|
||||
__version__: Final[str] = '1.5.0'
|
||||
__database_version__: Final[int] = 28
|
||||
__database_version__: Final[int] = 29
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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('<id>/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('<id>/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('/<id>/field/<name>', 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('<id>/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))
|
||||
@@ -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('/<id>/custom_field/<metadata_id>', 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('/<id>/delete', methods=['GET'])
|
||||
@login_required
|
||||
|
||||
@@ -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' %}
|
||||
|
||||
@@ -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 %}<div class="alert alert-danger m-2" role="alert"><strong>Error:</strong> {{ custom_field_error }}.</div>{% endif %}
|
||||
<ul class="list-group list-group-flush">
|
||||
{% if metadata_custom_fields | length %}
|
||||
{% for custom_field in metadata_custom_fields %}
|
||||
<li class="list-group-item">
|
||||
<form action="{{ url_for('admin_custom_field.rename', id=custom_field.fields.id) }}" method="post" class="row row-cols-lg-auto g-3 align-items-center">
|
||||
<div class="col-12 flex-grow-1">
|
||||
<label class="visually-hidden" for="name-{{ custom_field.fields.id }}">Name</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-text">Name</div>
|
||||
<input type="text" class="form-control" id="name-{{ custom_field.fields.id }}" name="name" value="{{ custom_field.fields.name }}">
|
||||
<button type="submit" class="btn btn-primary"><i class="ri-edit-line"></i> Rename</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="input-group">
|
||||
<div class="input-group-text"><i class="ri-shape-line"></i> Type</div>
|
||||
<select class="form-select" id="type-{{ custom_field.fields.id }}"
|
||||
data-changer-id="{{ custom_field.fields.id }}" data-changer-prefix="type" data-changer-url="{{ url_for('admin_custom_field.update_field', id=custom_field.fields.id, name='type') }}"
|
||||
autocomplete="off">
|
||||
<option value="text" {% if custom_field.fields.type == 'text' %}selected{% endif %}>Text</option>
|
||||
<option value="number" {% if custom_field.fields.type == 'number' %}selected{% endif %}>Number</option>
|
||||
<option value="date" {% if custom_field.fields.type == 'date' %}selected{% endif %}>Date</option>
|
||||
</select>
|
||||
<span id="status-type-{{ custom_field.fields.id }}" class="input-group-text ri-save-line px-1"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<a href="{{ url_for('admin_custom_field.delete', id=custom_field.fields.id) }}" class="btn btn-danger" role="button"><i class="ri-delete-bin-2-line"></i> Delete</a>
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<li class="list-group-item text-center"><i class="ri-error-warning-line"></i> No custom field found.</li>
|
||||
{% endif %}
|
||||
<li class="list-group-item">
|
||||
<form action="{{ url_for('admin_custom_field.add') }}" method="post" class="row row-cols-lg-auto g-3 align-items-center">
|
||||
<div class="col-12 flex-grow-1">
|
||||
<label class="visually-hidden" for="name">Name</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-text">Name</div>
|
||||
<input type="text" class="form-control" id="name" name="name" value="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="input-group">
|
||||
<div class="input-group-text"><i class="ri-shape-line"></i> Type</div>
|
||||
<select class="form-select" id="type" name="type" autocomplete="off">
|
||||
<option value="text" selected>Text</option>
|
||||
<option value="number">Number</option>
|
||||
<option value="date">Date</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<button type="submit" class="btn btn-primary"><i class="ri-add-circle-line"></i> Add</button>
|
||||
</div>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
{{ accordion.footer() }}
|
||||
@@ -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') }}
|
||||
<form action="{{ url_for('admin_custom_field.do_delete', id=custom_field.fields.id) }}" method="post">
|
||||
{% if custom_field_error %}<div class="alert alert-danger text-start" role="alert"><strong>Error:</strong> {{ custom_field_error }}.</div>{% endif %}
|
||||
<div class="alert alert-danger text-center" role="alert">You are about to <strong>delete a custom field</strong> and every value stored in it. This action is irreversible.</div>
|
||||
<div class="row row-cols-lg-auto g-3 align-items-center">
|
||||
<div class="col-12 flex-grow-1">
|
||||
<div class="input-group">
|
||||
<div class="input-group-text">Name</div>
|
||||
<input type="text" class="form-control" value="{{ custom_field.fields.name }}" disabled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="border-bottom">
|
||||
<a class="btn btn-danger" href="{{ url_for('admin.admin', open_custom_field=true) }}" role="button"><i class="ri-arrow-left-long-line"></i> Back to the admin</a>
|
||||
<button type="submit" class="btn btn-danger"><i class="ri-delete-bin-2-line"></i> Delete <strong>the custom field</strong></button>
|
||||
</form>
|
||||
{{ accordion.footer() }}
|
||||
@@ -60,6 +60,21 @@
|
||||
<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 set tags</a>
|
||||
</div>
|
||||
{{ accordion.footer() }}
|
||||
{{ accordion.header('Custom fields', 'custom-field', 'set-management', icon='edit-line') }}
|
||||
{% if brickset_custom_fields | length %}
|
||||
<div class="row row-cols-1 g-2 pb-2">
|
||||
{% for custom_field in brickset_custom_fields %}
|
||||
<div class="col-12">
|
||||
{{ 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')) }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<hr>
|
||||
{% else %}
|
||||
<p class="text-center"><i class="ri-error-warning-line"></i> No custom field found.</p>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('admin.admin', open_custom_field=true) }}" class="btn btn-primary" role="button"><i class="ri-settings-4-line"></i> Manage the custom fields</a>
|
||||
{{ accordion.footer() }}
|
||||
{{ accordion.header('Data', 'data', 'set-management', icon='database-2-line') }}
|
||||
<a href="{{ item.url_for_refresh() }}" class="btn btn-primary" role="button"><i class="ri-refresh-line"></i> Refresh the set data</a>
|
||||
{{ accordion.footer() }}
|
||||
|
||||
Reference in New Issue
Block a user