feat(individual): implement read-only mode for individual minifigures and parts

This commit is contained in:
FrederikBaerentsen
2026-01-20 07:38:06 +01:00
parent 4f1997305f
commit fbf330705f
11 changed files with 125 additions and 41 deletions

View File

@@ -37,7 +37,9 @@
- Purchase tracking with date, location, and price
- Quick navigation from set minifigures to individual instances
- Filter and search capabilities
- Feature flags: `BK_HIDE_INDIVIDUAL_MINIFIGURES` (hide UI), `BK_DISABLE_INDIVIDUAL_MINIFIGURES` (block writes)
- Feature flags:
- `BK_HIDE_INDIVIDUAL_MINIFIGURES`: Hides individual minifigures UI elements (navbar menu item, links from minifigure detail pages)
- `BK_DISABLE_INDIVIDUAL_MINIFIGURES`: Enables read-only mode - all individual minifigure pages remain accessible but with all editing fields disabled (quantity, parts table, metadata inputs), delete buttons hidden, and write operations blocked.
- **Individual Parts Tracking**
- Track loose parts outside of sets and minifigures
@@ -46,7 +48,9 @@
- Problem tracking (missing/damaged/checked states)
- Purchase tracking with date, location, and price
- Bulk part addition interface
- Feature flags: `BK_HIDE_INDIVIDUAL_PARTS` (hide UI), `BK_DISABLE_INDIVIDUAL_PARTS` (block writes)
- Feature flags:
- `BK_HIDE_INDIVIDUAL_PARTS`: Hides individual parts UI elements (navbar menu item, "Add Parts" button, links from part detail pages)
- `BK_DISABLE_INDIVIDUAL_PARTS`: Enables read-only mode - all individual parts and lot pages remain accessible but with all editing fields disabled (quantity, missing/damaged, parts table, metadata inputs), delete buttons hidden, "Add Parts" menu item removed, and write operations blocked. The /add/ page also hides the "Adding individual parts?" section.
- **Part Lots System**
- Organize individual parts into logical lots/collections

View File

@@ -48,9 +48,12 @@ def require_individual_parts_write(f):
def list() -> str:
parts = IndividualPartList().all()
writes_disabled = current_app.config.get('DISABLE_INDIVIDUAL_PARTS', False)
return render_template(
'individual_parts.html',
parts=parts,
writes_disabled=writes_disabled,
**set_metadata_lists(as_class=True)
)
@@ -181,10 +184,13 @@ def details(*, id: str) -> str:
error=e
))
writes_disabled = current_app.config.get('DISABLE_INDIVIDUAL_PARTS', False)
return render_template(
'individual_part/details.html',
item=item,
lot=lot,
writes_disabled=writes_disabled,
**set_metadata_lists(as_class=True)
)
@@ -481,10 +487,13 @@ def lot_details(*, lot_id: str) -> str:
"""Display details for an individual part lot (behaves like a set)"""
lot = IndividualPartLot().select_by_id(lot_id)
writes_disabled = current_app.config.get('DISABLE_INDIVIDUAL_PARTS', False)
return render_template(
'individual_part/lot_details.html',
item=lot, # Pass as 'item' like sets do
solo=True,
writes_disabled=writes_disabled,
**set_metadata_lists(as_class=True)
)

View File

@@ -99,6 +99,8 @@ def list() -> str:
@minifigure_page.route('/<figure>/details')
@exception_handler(__file__)
def details(*, figure: str) -> str:
writes_disabled = current_app.config.get('DISABLE_INDIVIDUAL_MINIFIGURES', False)
return render_template(
'minifigure.html',
item=BrickMinifigure().select_generic(figure),
@@ -106,5 +108,6 @@ def details(*, figure: str) -> str:
missing=BrickSetList().missing_minifigure(figure),
damaged=BrickSetList().damaged_minifigure(figure),
individual_instances=IndividualMinifigureList().instances_by_figure(figure),
writes_disabled=writes_disabled,
**set_metadata_lists(as_class=True)
)

View File

@@ -1,4 +1,4 @@
from flask import Blueprint, render_template, request
from flask import Blueprint, current_app, render_template, request
from .exceptions import exception_handler
from ..individual_part_list import IndividualPartList
@@ -201,6 +201,8 @@ def problem() -> str:
def details(*, part: str, color: int) -> str:
brickpart = BrickPart().select_generic(part, color)
writes_disabled = current_app.config.get('DISABLE_INDIVIDUAL_PARTS', False)
return render_template(
'part.html',
item=brickpart,
@@ -232,5 +234,6 @@ def details(*, part: str, color: int) -> str:
similar_prints=BrickPartList().from_print(brickpart),
individual_parts=IndividualPartList().by_part_and_color(part, color),
individual_lots=IndividualPartLotList().by_part_and_color(part, color),
writes_disabled=writes_disabled,
**set_metadata_lists(as_class=True)
)

View File

@@ -6,17 +6,17 @@
{% block main %}
<div class="container">
{% if not bulk and (not config['HIDE_ADD_BULK_SET'] or not config['HIDE_INDIVIDUAL_PARTS']) %}
{% if not bulk and (not config['HIDE_ADD_BULK_SET'] or (not config['HIDE_INDIVIDUAL_PARTS'] and not config['DISABLE_INDIVIDUAL_PARTS'])) %}
<div class="row g-3 mb-3">
{% if not config['HIDE_ADD_BULK_SET'] %}
<div class="col-12 {% if not config['HIDE_INDIVIDUAL_PARTS'] %}col-md-6{% endif %}">
<div class="col-12 {% if not config['HIDE_INDIVIDUAL_PARTS'] and not config['DISABLE_INDIVIDUAL_PARTS'] %}col-md-6{% endif %}">
<div class="alert alert-primary mb-0 h-100" role="alert">
<h4 class="alert-heading">Too many to add?</h4>
<p class="mb-0">You can import multiple sets at once with <a href="{{ url_for('add.bulk') }}" class="btn btn-primary"><i class="ri-function-add-line"></i> Bulk add</a>.</p>
</div>
</div>
{% endif %}
{% if not config['HIDE_INDIVIDUAL_PARTS'] %}
{% if not config['HIDE_INDIVIDUAL_PARTS'] and not config['DISABLE_INDIVIDUAL_PARTS'] %}
<div class="col-12 {% if not config['HIDE_ADD_BULK_SET'] %}col-md-6{% endif %}">
<div class="alert alert-info mb-0 h-100" role="alert">
<h4 class="alert-heading">Adding individual parts?</h4>

View File

@@ -31,7 +31,7 @@
{% if item.endpoint == 'add.add' %}
{# Add menu item with optional dropdown for Bulk/Parts #}
{% set has_bulk = not config['HIDE_ADD_BULK_SET'] %}
{% set has_parts = not config['HIDE_INDIVIDUAL_PARTS'] %}
{% set has_parts = not config['HIDE_INDIVIDUAL_PARTS'] and not config['DISABLE_INDIVIDUAL_PARTS'] %}
{% if has_bulk or has_parts %}
<li class="nav-item dropdown px-1">
<a {% if request.url_rule.endpoint in ['add.add', 'add.bulk', 'add.parts'] %}class="nav-link active" aria-current="page"{% else %}class="nav-link"{% endif %}

View File

@@ -19,14 +19,22 @@
</div>
{% if g.login.is_authenticated() %}
<div class="card-footer p-1 border-bottom">
{{ form.input('Qty', item.fields.id, 'quantity', item.url_for_quantity(), item.fields.quantity, icon='functions') }}
{% if writes_disabled %}
<div class="input-group">
<span class="input-group-text px-1"><i class="ri-functions me-1"></i><span class="ms-1 d-none d-md-inline"> Qty</span></span>
<input class="form-control form-control-sm flex-shrink-1 px-1" type="text" value="{{ item.fields.quantity }}" disabled autocomplete="off">
<span class="input-group-text ri-prohibited-line px-1"></span>
</div>
{% else %}
{{ form.input('Qty', item.fields.id, 'quantity', item.url_for_quantity(), item.fields.quantity, icon='functions') }}
{% endif %}
</div>
{% endif %}
{% if brickset_statuses | length %}
<ul class="list-group list-group-flush card-check border-bottom-0">
{% for status in brickset_statuses %}
<li class="d-flex list-group-item p-1 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()]) }}
{{ 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()], delete=writes_disabled) }}
</li>
{% endfor %}
</ul>

View File

@@ -26,7 +26,15 @@
</div>
{% if g.login.is_authenticated() %}
<div class="card-footer p-1">
{{ form.input('Qty', item.fields.id, 'quantity', item.url_for_quantity(), item.fields.quantity, icon='functions') }}
{% if writes_disabled %}
<div class="input-group">
<span class="input-group-text px-1"><i class="ri-functions me-1"></i><span class="ms-1 d-none d-md-inline"> Qty</span></span>
<input class="form-control form-control-sm flex-shrink-1 px-1" type="text" value="{{ item.fields.quantity }}" disabled autocomplete="off">
<span class="input-group-text ri-prohibited-line px-1"></span>
</div>
{% else %}
{{ form.input('Qty', item.fields.id, 'quantity', item.url_for_quantity(), item.fields.quantity, icon='functions') }}
{% endif %}
</div>
{% endif %}
</div>

View File

@@ -31,17 +31,25 @@
</div>
{% endif %}
<div class="card-body border-bottom">
<div class="row g-2">
<div class="col-12 col-lg-4">
{{ form.input('Quantity', item.fields.id, 'quantity', item.url_for_quantity(), item.fields.quantity, icon='functions') }}
{% if writes_disabled %}
<div class="alert alert-secondary mb-0">
<i class="ri-functions"></i> Quantity: {{ item.fields.quantity }} |
<i class="ri-question-line"></i> Missing: {{ item.fields.missing or 0 }} |
<i class="ri-error-warning-line"></i> Damaged: {{ item.fields.damaged or 0 }}
</div>
<div class="col-12 col-lg-4">
{{ form.input('Missing', item.fields.id, 'missing', item.url_for_problem('missing'), item.fields.missing, icon='question-line') }}
{% else %}
<div class="row g-2">
<div class="col-12 col-lg-4">
{{ form.input('Quantity', item.fields.id, 'quantity', item.url_for_quantity(), item.fields.quantity, icon='functions') }}
</div>
<div class="col-12 col-lg-4">
{{ form.input('Missing', item.fields.id, 'missing', item.url_for_problem('missing'), item.fields.missing, icon='question-line') }}
</div>
<div class="col-12 col-lg-4">
{{ form.input('Damaged', item.fields.id, 'damaged', item.url_for_problem('damaged'), item.fields.damaged, icon='error-warning-line') }}
</div>
</div>
<div class="col-12 col-lg-4">
{{ form.input('Damaged', item.fields.id, 'damaged', item.url_for_problem('damaged'), item.fields.damaged, icon='error-warning-line') }}
</div>
</div>
{% endif %}
</div>
<div class="accordion accordion-flush border-top" id="individual-part-details-{{ item.fields.id }}">
{% if lot %}
@@ -61,9 +69,9 @@
{{ accordion.footer() }}
{% else %}
{# Only show management accordion if NOT part of a lot #}
{% include 'individual_part/management.html' %}
{% set management_read_only = writes_disabled %}{% include 'individual_part/management.html' %}
{% endif %}
{% if g.login.is_authenticated() %}
{% if g.login.is_authenticated() and not writes_disabled %}
{{ accordion.header('Danger zone', 'accordion-danger-zone-' ~ item.fields.id, 'individual-part-details-' ~ item.fields.id, danger=true, class='text-end') }}
<a href="{{ url_for('individual_part.delete_part', 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 part instance</a>
{{ accordion.footer() }}

View File

@@ -51,9 +51,9 @@
{% if solo %}
<div class="accordion accordion-flush border-top" id="lot-details">
{{ accordion.table(item.parts(), 'Parts', 'parts-inventory', 'lot-details', 'part/lot_table.html', icon='shapes-line', hamburger_menu=g.login.is_authenticated())}}
{% include 'individual_part/management.html' %}
{% if g.login.is_authenticated() %}
{{ accordion.table(item.parts(), 'Parts', 'parts-inventory', 'lot-details', 'part/lot_table.html', icon='shapes-line', hamburger_menu=g.login.is_authenticated(), read_only=writes_disabled)}}
{% set management_read_only = writes_disabled %}{% include 'individual_part/management.html' %}
{% if g.login.is_authenticated() and not writes_disabled %}
{{ accordion.header('Danger zone', 'danger-zone', 'lot-details', danger=true, class='text-end') }}
<a href="{{ url_for('individual_part.delete_lot', lot_id=item.fields.id) }}" class="btn btn-danger" role="button" data-bs-toggle="modal" data-bs-target="#deleteModal">
<i class="ri-delete-bin-line"></i> Delete entire lot and all parts
@@ -66,7 +66,7 @@
<div class="card-footer"></div>
</div>
{% if solo and g.login.is_authenticated() %}
{% if solo and g.login.is_authenticated() and not writes_disabled %}
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog">

View File

@@ -6,7 +6,14 @@
{% if item.__class__.__name__ == 'IndividualPartLot' %}
{{ accordion.header('Title', 'accordion-title-' ~ item.fields.id, 'accordion-management-' ~ item.fields.id, icon='file-text-line') }}
<div class="alert alert-info" role="alert">Give this part lot a descriptive name to help identify it later. This is shown in the lot card header and list views.</div>
{{ form.input('', item.fields.id, 'lot-name-' ~ item.fields.id, url_for('individual_part.update_lot_name', lot_id=item.fields.id), item.fields.name) }}
{% if management_read_only %}
<div class="input-group">
<input class="form-control form-control-sm flex-shrink-1 px-1" type="text" value="{{ item.fields.name or '' }}" disabled autocomplete="off">
<span class="input-group-text ri-prohibited-line px-1"></span>
</div>
{% else %}
{{ form.input('', item.fields.id, 'lot-name-' ~ item.fields.id, url_for('individual_part.update_lot_name', lot_id=item.fields.id), item.fields.name) }}
{% endif %}
{{ accordion.footer() }}
{% endif %}
{{ accordion.header('Owners', 'accordion-owners-' ~ item.fields.id, 'accordion-management-' ~ item.fields.id, icon='group-line', class='p-0') }}
@@ -14,9 +21,9 @@
{% if brickset_owners | length %}
{% for owner in brickset_owners %}
{% if item.__class__.__name__ == 'IndividualPartLot' %}
<li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(owner.fields.name, item.fields.id, owner.as_dataset(), url_for('individual_part.update_lot_owner', lot_id=item.fields.id, metadata_id=owner.fields.id), item.fields[owner.as_column()] | default(false)) }}</li>
<li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(owner.fields.name, item.fields.id, owner.as_dataset(), url_for('individual_part.update_lot_owner', lot_id=item.fields.id, metadata_id=owner.fields.id), item.fields[owner.as_column()] | default(false), delete=management_read_only) }}</li>
{% else %}
<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_part_state(item.fields.id), item.fields[owner.as_column()]) }}</li>
<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_part_state(item.fields.id), item.fields[owner.as_column()], delete=management_read_only) }}</li>
{% endif %}
{% endfor %}
{% else %}
@@ -32,14 +39,31 @@
<div class="alert alert-info" role="alert">The expected date format here is <code>yyyy/mm/dd</code> (year/month/day), but you can configured how it is displayed in the part lot card with the <code>PURCHASE_DATE_FORMAT</code> variable.</div>
<div class="row row-cols-lg-auto g-1 justify-content-start align-items-center pb-2">
<div class="col-12">
{{ form.input('Date', item.fields.id, 'purchase_date', url_for('individual_part.update_lot_purchase_date', lot_id=item.fields.id), item.fields.purchase_date or '', date=true, icon='calendar-line') }}
{% if management_read_only %}
<div class="input-group">
<span class="input-group-text px-1"><i class="ri-calendar-line me-1"></i><span class="ms-1 d-none d-md-inline"> Date</span></span>
<input class="form-control form-control-sm flex-shrink-1 px-1" type="text" value="{{ item.fields.purchase_date or '' }}" disabled autocomplete="off">
<span class="input-group-text ri-prohibited-line px-1"></span>
</div>
{% else %}
{{ form.input('Date', item.fields.id, 'purchase_date', url_for('individual_part.update_lot_purchase_date', lot_id=item.fields.id), item.fields.purchase_date or '', date=true, icon='calendar-line') }}
{% endif %}
</div>
<div class="col-12 flex-grow-1">
{{ form.input('Price', item.fields.id, 'purchase_price', url_for('individual_part.update_lot_purchase_price', lot_id=item.fields.id), item.fields.purchase_price or '', suffix=config['PURCHASE_CURRENCY'], icon='wallet-3-line') }}
{% if management_read_only %}
<div class="input-group">
<span class="input-group-text px-1"><i class="ri-wallet-3-line me-1"></i><span class="ms-1 d-none d-md-inline"> Price</span></span>
<input class="form-control form-control-sm flex-shrink-1 px-1" type="text" value="{{ item.fields.purchase_price or '' }}" disabled autocomplete="off">
<span class="input-group-text d-none d-md-inline px-1">{{ config['PURCHASE_CURRENCY'] }}</span>
<span class="input-group-text ri-prohibited-line px-1"></span>
</div>
{% else %}
{{ form.input('Price', item.fields.id, 'purchase_price', url_for('individual_part.update_lot_purchase_price', lot_id=item.fields.id), item.fields.purchase_price or '', suffix=config['PURCHASE_CURRENCY'], icon='wallet-3-line') }}
{% endif %}
</div>
<div class="col-12 flex-grow-1">
{% if brickset_purchase_locations | length %}
{{ form.select('Location', item.fields.id, brickset_purchase_locations.as_prefix(), url_for('individual_part.update_lot_purchase_location', lot_id=item.fields.id), item.fields.purchase_location, brickset_purchase_locations, icon='building-line') }}
{{ form.select('Location', item.fields.id, brickset_purchase_locations.as_prefix(), url_for('individual_part.update_lot_purchase_location', lot_id=item.fields.id), item.fields.purchase_location, brickset_purchase_locations, icon='building-line', delete=management_read_only) }}
{% else %}
<i class="ri-error-warning-line"></i> No purchase location found.
{% endif %}
@@ -49,11 +73,11 @@
<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 part lot purchase locations</a>
{{ accordion.footer() }}
{{ accordion.header('Notes', 'accordion-notes-' ~ item.fields.id, 'accordion-management-' ~ item.fields.id, icon='sticky-note-line') }}
{{ form.textarea('', item.fields.id, 'lot-description-' ~ item.fields.id, url_for('individual_part.update_lot_description', lot_id=item.fields.id), item.fields.description, rows=4) }}
{{ form.textarea('', item.fields.id, 'lot-description-' ~ item.fields.id, url_for('individual_part.update_lot_description', lot_id=item.fields.id), item.fields.description, rows=4, delete=management_read_only) }}
{{ accordion.footer() }}
{{ accordion.header('Storage', 'accordion-storage-' ~ item.fields.id, 'accordion-management-' ~ item.fields.id, icon='archive-2-line') }}
{% if brickset_storages | length %}
{{ form.select('Storage', item.fields.id, brickset_storages.as_prefix(), url_for('individual_part.update_lot_storage', lot_id=item.fields.id), item.fields.storage, brickset_storages, icon='building-line') }}
{{ form.select('Storage', item.fields.id, brickset_storages.as_prefix(), url_for('individual_part.update_lot_storage', lot_id=item.fields.id), item.fields.storage, brickset_storages, icon='building-line', delete=management_read_only) }}
{% else %}
<p class="text-center"><i class="ri-error-warning-line"></i> No storage found.</p>
{% endif %}
@@ -63,7 +87,7 @@
{% else %}
{{ accordion.header('Storage', 'accordion-storage-' ~ item.fields.id, 'accordion-management-' ~ item.fields.id, icon='archive-2-line') }}
{% if brickset_storages | length %}
{{ form.select('Storage', item.fields.id, brickset_storages.as_prefix(), brickset_storages.url_for_individual_part_value(item.fields.id), item.fields.storage, brickset_storages, icon='building-line') }}
{{ form.select('Storage', item.fields.id, brickset_storages.as_prefix(), brickset_storages.url_for_individual_part_value(item.fields.id), item.fields.storage, brickset_storages, icon='building-line', delete=management_read_only) }}
{% else %}
<p class="text-center"><i class="ri-error-warning-line"></i> No storage found.</p>
{% endif %}
@@ -74,14 +98,31 @@
<div class="alert alert-info" role="alert">The expected date format here is <code>yyyy/mm/dd</code> (year/month/day), but you can configured how it is displayed in the part card with the <code>PURCHASE_DATE_FORMAT</code> variable.</div>
<div class="row row-cols-lg-auto g-1 justify-content-start align-items-center pb-2">
<div class="col-12">
{{ form.input('Date', item.fields.id, 'purchase_date', item.url_for_purchase_date(), item.fields.purchase_date or '', date=true, icon='calendar-line') }}
{% if management_read_only %}
<div class="input-group">
<span class="input-group-text px-1"><i class="ri-calendar-line me-1"></i><span class="ms-1 d-none d-md-inline"> Date</span></span>
<input class="form-control form-control-sm flex-shrink-1 px-1" type="text" value="{{ item.fields.purchase_date or '' }}" disabled autocomplete="off">
<span class="input-group-text ri-prohibited-line px-1"></span>
</div>
{% else %}
{{ form.input('Date', item.fields.id, 'purchase_date', item.url_for_purchase_date(), item.fields.purchase_date or '', date=true, icon='calendar-line') }}
{% endif %}
</div>
<div class="col-12 flex-grow-1">
{{ form.input('Price', item.fields.id, 'purchase_price', item.url_for_purchase_price(), item.fields.purchase_price or '', suffix=config['PURCHASE_CURRENCY'], icon='wallet-3-line') }}
{% if management_read_only %}
<div class="input-group">
<span class="input-group-text px-1"><i class="ri-wallet-3-line me-1"></i><span class="ms-1 d-none d-md-inline"> Price</span></span>
<input class="form-control form-control-sm flex-shrink-1 px-1" type="text" value="{{ item.fields.purchase_price or '' }}" disabled autocomplete="off">
<span class="input-group-text d-none d-md-inline px-1">{{ config['PURCHASE_CURRENCY'] }}</span>
<span class="input-group-text ri-prohibited-line px-1"></span>
</div>
{% else %}
{{ form.input('Price', item.fields.id, 'purchase_price', item.url_for_purchase_price(), item.fields.purchase_price or '', suffix=config['PURCHASE_CURRENCY'], icon='wallet-3-line') }}
{% endif %}
</div>
<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_part_value(item.fields.id), item.fields.purchase_location, brickset_purchase_locations, icon='building-line') }}
{{ form.select('Location', item.fields.id, brickset_purchase_locations.as_prefix(), brickset_purchase_locations.url_for_individual_part_value(item.fields.id), item.fields.purchase_location, brickset_purchase_locations, icon='building-line', delete=management_read_only) }}
{% else %}
<i class="ri-error-warning-line"></i> No purchase location found.
{% endif %}
@@ -91,7 +132,7 @@
<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('Notes', 'accordion-notes-' ~ item.fields.id, 'accordion-management-' ~ item.fields.id, icon='sticky-note-line') }}
{{ form.textarea('', item.fields.id, 'description', item.url_for_description(), item.fields.description or '', rows=4) }}
{{ form.textarea('', item.fields.id, 'description', item.url_for_description(), item.fields.description or '', rows=4, delete=management_read_only) }}
{{ accordion.footer() }}
{% endif %}
{% if item.__class__.__name__ != 'IndividualPartLot' %}
@@ -99,7 +140,7 @@
<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_part_state(item.fields.id), item.fields[status.as_column()]) }}</li>
<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_part_state(item.fields.id), item.fields[status.as_column()], delete=management_read_only) }}</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>
@@ -115,9 +156,9 @@
{% if brickset_tags | length %}
{% for tag in brickset_tags %}
{% if item.__class__.__name__ == 'IndividualPartLot' %}
<li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(tag.fields.name, item.fields.id, tag.as_dataset(), url_for('individual_part.update_lot_tag', lot_id=item.fields.id, metadata_id=tag.fields.id), item.fields[tag.as_column()] | default(false)) }}</li>
<li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(tag.fields.name, item.fields.id, tag.as_dataset(), url_for('individual_part.update_lot_tag', lot_id=item.fields.id, metadata_id=tag.fields.id), item.fields[tag.as_column()] | default(false), delete=management_read_only) }}</li>
{% else %}
<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_part_state(item.fields.id), item.fields[tag.as_column()]) }}</li>
<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_part_state(item.fields.id), item.fields[tag.as_column()], delete=management_read_only) }}</li>
{% endif %}
{% endfor %}
{% else %}