Set purchase location

This commit is contained in:
Gregoo 2025-02-04 12:52:18 +01:00
parent e7bfa66512
commit 195f18f141
34 changed files with 427 additions and 48 deletions

View File

@ -168,6 +168,12 @@
# Default: 3333 # Default: 3333
# BK_PORT=3333 # BK_PORT=3333
# Optional: Change the default order of purchase locations. By default ordered by insertion order.
# Useful column names for this option are:
# - "bricktracker_metadata_purchase_locations"."name" ASC: storage name
# Default: "bricktracker_metadata_purchase_locations"."name" ASC
# BK_PURCHASE_LOCATION_DEFAULT_ORDER="bricktracker_metadata_purchase_locations"."name" ASC
# Optional: Shuffle the lists on the front page. # Optional: Shuffle the lists on the front page.
# Default: false # Default: false
# Legacy name: RANDOM # Legacy name: RANDOM

View File

@ -14,7 +14,8 @@
- Added: `BK_SHOW_GRID_SORT`, show the sort options on the grid by default - Added: `BK_SHOW_GRID_SORT`, show the sort options on the grid by default
- Added: `BK_SHOW_GRID_FILTERS`, show the filter options on the grid by default - Added: `BK_SHOW_GRID_FILTERS`, show the filter options on the grid by default
- Added: `BK_HIDE_ALL_STORAGES`, hide the "Storages" menu entry - Added: `BK_HIDE_ALL_STORAGES`, hide the "Storages" menu entry
- Added: `BK_MINIFIGURES_DEFAULT_ORDER`, ordering of storages - Added: `BK_STORAGE_DEFAULT_ORDER`, ordering of storages
- Added: `BK_PURCHASE_LOCATION_DEFAULT_ORDER`, ordering of purchase locations
### Code ### Code
@ -39,6 +40,7 @@
- Ownership - Ownership
- Tags - Tags
- Storage - Storage
- Purchase location
- Storage - Storage
- Storage content and list - Storage content and list
@ -85,6 +87,7 @@
- Tags - Tags
- Refresh - Refresh
- Storage - Storage
- Purchase location
- Sets grid - Sets grid
- Collapsible controls depending on screen size - Collapsible controls depending on screen size

View File

@ -17,6 +17,7 @@ from bricktracker.views.admin.database import admin_database_page
from bricktracker.views.admin.image import admin_image_page from bricktracker.views.admin.image import admin_image_page
from bricktracker.views.admin.instructions import admin_instructions_page from bricktracker.views.admin.instructions import admin_instructions_page
from bricktracker.views.admin.owner import admin_owner_page from bricktracker.views.admin.owner import admin_owner_page
from bricktracker.views.admin.purchase_location import admin_purchase_location_page # noqa: E501
from bricktracker.views.admin.retired import admin_retired_page from bricktracker.views.admin.retired import admin_retired_page
from bricktracker.views.admin.status import admin_status_page from bricktracker.views.admin.status import admin_status_page
from bricktracker.views.admin.storage import admin_storage_page from bricktracker.views.admin.storage import admin_storage_page
@ -88,6 +89,7 @@ def setup_app(app: Flask) -> None:
app.register_blueprint(admin_instructions_page) app.register_blueprint(admin_instructions_page)
app.register_blueprint(admin_retired_page) app.register_blueprint(admin_retired_page)
app.register_blueprint(admin_owner_page) app.register_blueprint(admin_owner_page)
app.register_blueprint(admin_purchase_location_page)
app.register_blueprint(admin_status_page) app.register_blueprint(admin_status_page)
app.register_blueprint(admin_storage_page) app.register_blueprint(admin_storage_page)
app.register_blueprint(admin_tag_page) app.register_blueprint(admin_tag_page)

View File

@ -41,6 +41,7 @@ CONFIG: Final[list[dict[str, Any]]] = [
{'n': 'PARTS_DEFAULT_ORDER', 'd': '"rebrickable_parts"."name" ASC, "rebrickable_parts"."color_name" ASC, "bricktracker_parts"."spare" ASC'}, # noqa: E501 {'n': 'PARTS_DEFAULT_ORDER', 'd': '"rebrickable_parts"."name" ASC, "rebrickable_parts"."color_name" ASC, "bricktracker_parts"."spare" ASC'}, # noqa: E501
{'n': 'PARTS_FOLDER', 'd': 'parts', 's': True}, {'n': 'PARTS_FOLDER', 'd': 'parts', 's': True},
{'n': 'PORT', 'd': 3333, 'c': int}, {'n': 'PORT', 'd': 3333, 'c': int},
{'n': 'PURCHASE_LOCATION_DEFAULT_ORDER', 'd': '"bricktracker_metadata_purchase_locations"."name" ASC'}, # noqa: E501
{'n': 'RANDOM', 'e': 'RANDOM', 'c': bool}, {'n': 'RANDOM', 'e': 'RANDOM', 'c': bool},
{'n': 'REBRICKABLE_API_KEY', 'e': 'REBRICKABLE_API_KEY', 'd': ''}, {'n': 'REBRICKABLE_API_KEY', 'e': 'REBRICKABLE_API_KEY', 'd': ''},
{'n': 'REBRICKABLE_IMAGE_NIL', 'd': 'https://rebrickable.com/static/img/nil.png'}, # noqa: E501 {'n': 'REBRICKABLE_IMAGE_NIL', 'd': 'https://rebrickable.com/static/img/nil.png'}, # noqa: E501

View File

@ -48,7 +48,7 @@ class BrickMetadata(BrickRecord):
def as_column(self, /) -> str: def as_column(self, /) -> str:
return '{kind}_{id}'.format( return '{kind}_{id}'.format(
id=self.fields.id, id=self.fields.id,
kind=self.kind.lower() kind=self.kind.lower().replace(' ', '-')
) )
# HTML dataset name # HTML dataset name
@ -90,8 +90,6 @@ class BrickMetadata(BrickRecord):
# Rename the entry # Rename the entry
def rename(self, /) -> None: def rename(self, /) -> None:
self.safe()
self.update_field('name', value=self.fields.name) self.update_field('name', value=self.fields.name)
# Make the name "safe" # Make the name "safe"
@ -159,7 +157,7 @@ class BrickMetadata(BrickRecord):
) )
if rows != 1: if rows != 1:
raise DatabaseException('Could not update the field "{field}" for {kind} {name} ({id})'.format( # noqa: E501 raise DatabaseException('Could not update the field "{field}" for {kind} "{name}" ({id})'.format( # noqa: E501
field=field, field=field,
kind=self.kind, kind=self.kind,
name=self.fields.name, name=self.fields.name,

View File

@ -7,13 +7,21 @@ from .exceptions import NotFoundException
from .fields import BrickRecordFields from .fields import BrickRecordFields
from .record_list import BrickRecordList from .record_list import BrickRecordList
from .set_owner import BrickSetOwner from .set_owner import BrickSetOwner
from .set_purchase_location import BrickSetPurchaseLocation
from .set_status import BrickSetStatus from .set_status import BrickSetStatus
from .set_storage import BrickSetStorage from .set_storage import BrickSetStorage
from .set_tag import BrickSetTag from .set_tag import BrickSetTag
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
T = TypeVar('T', BrickSetOwner, BrickSetStatus, BrickSetStorage, BrickSetTag) T = TypeVar(
'T',
BrickSetOwner,
BrickSetPurchaseLocation,
BrickSetStatus,
BrickSetStorage,
BrickSetTag
)
# Lego sets metadata list # Lego sets metadata list

View File

@ -9,6 +9,7 @@ if TYPE_CHECKING:
from .rebrickable_set import RebrickableSet from .rebrickable_set import RebrickableSet
from .set import BrickSet from .set import BrickSet
from .set_owner import BrickSetOwner from .set_owner import BrickSetOwner
from .set_purchase_location import BrickSetPurchaseLocation
from .set_status import BrickSetStatus from .set_status import BrickSetStatus
from .set_storage import BrickSetStorage from .set_storage import BrickSetStorage
from .set_tag import BrickSetTag from .set_tag import BrickSetTag
@ -20,6 +21,7 @@ T = TypeVar(
'BrickPart', 'BrickPart',
'BrickSet', 'BrickSet',
'BrickSetOwner', 'BrickSetOwner',
'BrickSetPurchaseLocation',
'BrickSetStatus', 'BrickSetStatus',
'BrickSetStorage', 'BrickSetStorage',
'BrickSetTag', 'BrickSetTag',

View File

@ -1,6 +1,7 @@
from .instructions_list import BrickInstructionsList from .instructions_list import BrickInstructionsList
from .retired_list import BrickRetiredList from .retired_list import BrickRetiredList
from .set_owner_list import BrickSetOwnerList from .set_owner_list import BrickSetOwnerList
from .set_purchase_location_list import BrickSetPurchaseLocationList
from .set_status_list import BrickSetStatusList from .set_status_list import BrickSetStatusList
from .set_storage_list import BrickSetStorageList from .set_storage_list import BrickSetStorageList
from .set_tag_list import BrickSetTagList from .set_tag_list import BrickSetTagList
@ -17,6 +18,9 @@ def reload() -> None:
# Reload the set owners # Reload the set owners
BrickSetOwnerList.new(force=True) BrickSetOwnerList.new(force=True)
# Reload the set purchase locations
BrickSetPurchaseLocationList.new(force=True)
# Reload the set statuses # Reload the set statuses
BrickSetStatusList.new(force=True) BrickSetStatusList.new(force=True)

View File

@ -10,6 +10,7 @@ from .minifigure_list import BrickMinifigureList
from .part_list import BrickPartList from .part_list import BrickPartList
from .rebrickable_set import RebrickableSet from .rebrickable_set import RebrickableSet
from .set_owner_list import BrickSetOwnerList from .set_owner_list import BrickSetOwnerList
from .set_purchase_location_list import BrickSetPurchaseLocationList
from .set_status_list import BrickSetStatusList from .set_status_list import BrickSetStatusList
from .set_storage_list import BrickSetStorageList from .set_storage_list import BrickSetStorageList
from .set_tag_list import BrickSetTagList from .set_tag_list import BrickSetTagList
@ -63,6 +64,13 @@ class BrickSet(RebrickableSet):
) )
self.fields.storage = storage.fields.id self.fields.storage = storage.fields.id
# Save the purchase location
purchase_location = BrickSetPurchaseLocationList.get(
data.get('purchase_location', ''),
allow_none=True
)
self.fields.purchase_location = purchase_location.fields.id
# Insert into database # Insert into database
self.insert(commit=False) self.insert(commit=False)

View File

@ -5,6 +5,8 @@ from flask import current_app
from .record_list import BrickRecordList from .record_list import BrickRecordList
from .set_owner import BrickSetOwner from .set_owner import BrickSetOwner
from .set_owner_list import BrickSetOwnerList from .set_owner_list import BrickSetOwnerList
from .set_purchase_location import BrickSetPurchaseLocation
from .set_purchase_location_list import BrickSetPurchaseLocationList
from .set_status_list import BrickSetStatusList from .set_status_list import BrickSetStatusList
from .set_storage import BrickSetStorage from .set_storage import BrickSetStorage
from .set_storage_list import BrickSetStorageList from .set_storage_list import BrickSetStorageList
@ -175,6 +177,8 @@ def set_metadata_lists(
str, str,
Union[ Union[
list[BrickSetOwner], list[BrickSetOwner],
list[BrickSetPurchaseLocation],
BrickSetPurchaseLocation,
list[BrickSetStorage], list[BrickSetStorage],
BrickSetStorageList, BrickSetStorageList,
list[BrickSetTag] list[BrickSetTag]
@ -182,6 +186,7 @@ def set_metadata_lists(
]: ]:
return { return {
'brickset_owners': BrickSetOwnerList.list(), 'brickset_owners': BrickSetOwnerList.list(),
'brickset_purchase_locations': BrickSetPurchaseLocationList.list(as_class=as_class), # noqa: E501
'brickset_storages': BrickSetStorageList.list(as_class=as_class), 'brickset_storages': BrickSetStorageList.list(as_class=as_class),
'brickset_tags': BrickSetTagList.list(), 'brickset_tags': BrickSetTagList.list(),
} }

View File

@ -0,0 +1,13 @@
from .metadata import BrickMetadata
# Lego set purchase location metadata
class BrickSetPurchaseLocation(BrickMetadata):
kind: str = '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'

View File

@ -0,0 +1,42 @@
import logging
from typing import Self
from flask import current_app
from .metadata_list import BrickMetadataList
from .set_purchase_location import BrickSetPurchaseLocation
logger = logging.getLogger(__name__)
# Lego sets purchase location list
class BrickSetPurchaseLocationList(
BrickMetadataList[BrickSetPurchaseLocation]
):
kind: str = 'set purchase locations'
# Queries
select_query = 'set/metadata/purchase_location/list'
all_query = 'set/metadata/purchase_location/all'
# Set value endpoint
set_value_endpoint: str = 'set.update_purchase_location'
# Load all purchase locations
@classmethod
def all(cls, /) -> Self:
new = cls.new()
new.override()
for record in new.select(
override_query=cls.all_query,
order=current_app.config['PURCHASE_LOCATION_DEFAULT_ORDER']
):
new.records.append(new.model(record=record))
return new
# Instantiate the list with the proper class
@classmethod
def new(cls, /, *, force: bool = False) -> Self:
return cls(BrickSetPurchaseLocation, force=force)

View File

@ -1,6 +1,7 @@
SELECT SELECT
{% block id %}{% endblock %} {% block id %}{% endblock %}
"bricktracker_sets"."storage", "bricktracker_sets"."storage",
"bricktracker_sets"."purchase_location",
"rebrickable_sets"."set", "rebrickable_sets"."set",
"rebrickable_sets"."number", "rebrickable_sets"."number",
"rebrickable_sets"."version", "rebrickable_sets"."version",

View File

@ -1,9 +1,11 @@
INSERT OR IGNORE INTO "bricktracker_sets" ( INSERT OR IGNORE INTO "bricktracker_sets" (
"id", "id",
"set", "set",
"storage" "storage",
"purchase_location"
) VALUES ( ) VALUES (
:id, :id,
:set, :set,
:storage :storage,
:purchase_location
) )

View File

@ -0,0 +1,6 @@
SELECT
"bricktracker_metadata_purchase_locations"."id",
"bricktracker_metadata_purchase_locations"."name"
FROM "bricktracker_metadata_purchase_locations"
{% block where %}{% endblock %}

View File

@ -0,0 +1,10 @@
BEGIN TRANSACTION;
DELETE FROM "bricktracker_metadata_purchase_locations"
WHERE "bricktracker_metadata_purchase_locations"."id" IS NOT DISTINCT FROM '{{ id }}';
UPDATE "bricktracker_sets"
SET "purchase_location" = NULL
WHERE "bricktracker_sets"."purchase_location" IS NOT DISTINCT FROM '{{ id }}';
COMMIT;

View File

@ -0,0 +1,11 @@
BEGIN TRANSACTION;
INSERT INTO "bricktracker_metadata_purchase_locations" (
"id",
"name"
) VALUES (
'{{ id }}',
'{{ name }}'
);
COMMIT;

View File

@ -0,0 +1 @@
{% extends 'set/metadata/purchase_location/base.sql' %}

View File

@ -0,0 +1,5 @@
{% extends 'set/metadata/purchase_location/base.sql' %}
{% block where %}
WHERE "bricktracker_metadata_purchase_locations"."id" IS NOT DISTINCT FROM :id
{% endblock %}

View File

@ -0,0 +1,3 @@
UPDATE "bricktracker_metadata_purchase_locations"
SET "{{field}}" = :value
WHERE "bricktracker_metadata_purchase_locations"."id" IS NOT DISTINCT FROM :id

View File

@ -0,0 +1,3 @@
UPDATE "bricktracker_sets"
SET "purchase_location" = :value
WHERE "bricktracker_sets"."id" IS NOT DISTINCT FROM :set_id

View File

@ -10,6 +10,8 @@ from ...rebrickable_image import RebrickableImage
from ...retired_list import BrickRetiredList from ...retired_list import BrickRetiredList
from ...set_owner import BrickSetOwner from ...set_owner import BrickSetOwner
from ...set_owner_list import BrickSetOwnerList from ...set_owner_list import BrickSetOwnerList
from ...set_purchase_location import BrickSetPurchaseLocation
from ...set_purchase_location_list import BrickSetPurchaseLocationList
from ...set_storage import BrickSetStorage from ...set_storage import BrickSetStorage
from ...set_storage_list import BrickSetStorageList from ...set_storage_list import BrickSetStorageList
from ...set_status import BrickSetStatus from ...set_status import BrickSetStatus
@ -36,6 +38,7 @@ def admin() -> str:
database_version: int = -1 database_version: int = -1
instructions: BrickInstructionsList | None = None instructions: BrickInstructionsList | None = None
metadata_owners: list[BrickSetOwner] = [] metadata_owners: list[BrickSetOwner] = []
metadata_purchase_locations: list[BrickSetPurchaseLocation] = []
metadata_statuses: list[BrickSetStatus] = [] metadata_statuses: list[BrickSetStatus] = []
metadata_storages: list[BrickSetStorage] = [] metadata_storages: list[BrickSetStorage] = []
metadata_tags: list[BrickSetTag] = [] metadata_tags: list[BrickSetTag] = []
@ -54,6 +57,7 @@ def admin() -> str:
instructions = BrickInstructionsList() instructions = BrickInstructionsList()
metadata_owners = BrickSetOwnerList.list() metadata_owners = BrickSetOwnerList.list()
metadata_purchase_locations = BrickSetPurchaseLocationList.list()
metadata_statuses = BrickSetStatusList.list(all=True) metadata_statuses = BrickSetStatusList.list(all=True)
metadata_storages = BrickSetStorageList.list() metadata_storages = BrickSetStorageList.list()
metadata_tags = BrickSetTagList.list() metadata_tags = BrickSetTagList.list()
@ -81,6 +85,7 @@ def admin() -> str:
open_instructions = request.args.get('open_instructions', None) open_instructions = request.args.get('open_instructions', None)
open_logout = request.args.get('open_logout', None) open_logout = request.args.get('open_logout', None)
open_owner = request.args.get('open_owner', None) open_owner = request.args.get('open_owner', None)
open_purchase_location = request.args.get('open_purchase_location', None)
open_retired = request.args.get('open_retired', None) open_retired = request.args.get('open_retired', None)
open_status = request.args.get('open_status', None) open_status = request.args.get('open_status', None)
open_storage = request.args.get('open_storage', None) open_storage = request.args.get('open_storage', None)
@ -89,6 +94,7 @@ def admin() -> str:
open_metadata = ( open_metadata = (
open_owner or open_owner or
open_purchase_location or
open_status or open_status or
open_storage or open_storage or
open_tag open_tag
@ -113,6 +119,7 @@ def admin() -> str:
database_version=database_version, database_version=database_version,
instructions=instructions, instructions=instructions,
metadata_owners=metadata_owners, metadata_owners=metadata_owners,
metadata_purchase_locations=metadata_purchase_locations,
metadata_statuses=metadata_statuses, metadata_statuses=metadata_statuses,
metadata_storages=metadata_storages, metadata_storages=metadata_storages,
metadata_tags=metadata_tags, metadata_tags=metadata_tags,
@ -126,12 +133,14 @@ def admin() -> str:
open_logout=open_logout, open_logout=open_logout,
open_metadata=open_metadata, open_metadata=open_metadata,
open_owner=open_owner, open_owner=open_owner,
open_purchase_location=open_purchase_location,
open_retired=open_retired, open_retired=open_retired,
open_status=open_status, open_status=open_status,
open_storage=open_storage, open_storage=open_storage,
open_tag=open_tag, open_tag=open_tag,
open_theme=open_theme, open_theme=open_theme,
owner_error=request.args.get('owner_error'), owner_error=request.args.get('owner_error'),
purchase_location_error=request.args.get('purchase_location_error'),
retired=BrickRetiredList(), retired=BrickRetiredList(),
status_error=request.args.get('status_error'), status_error=request.args.get('status_error'),
storage_error=request.args.get('storage_error'), storage_error=request.args.get('storage_error'),

View File

@ -0,0 +1,84 @@
from flask import (
Blueprint,
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_purchase_location import BrickSetPurchaseLocation
admin_purchase_location_page = Blueprint(
'admin_purchase_location',
__name__,
url_prefix='/admin/purchase_location'
)
# Add a metadata purchase location
@admin_purchase_location_page.route('/add', methods=['POST'])
@login_required
@exception_handler(
__file__,
post_redirect='admin.admin',
error_name='purchase_location_error',
open_purchase_location=True
)
def add() -> Response:
BrickSetPurchaseLocation().from_form(request.form).insert()
reload()
return redirect(url_for('admin.admin', open_purchase_location=True))
# Delete the metadata purchase location
@admin_purchase_location_page.route('<id>/delete', methods=['GET'])
@login_required
@exception_handler(__file__)
def delete(*, id: str) -> str:
return render_template(
'admin.html',
delete_purchase_location=True,
purchase_location=BrickSetPurchaseLocation().select_specific(id),
error=request.args.get('purchase_location_error')
)
# Actually delete the metadata purchase location
@admin_purchase_location_page.route('<id>/delete', methods=['POST'])
@login_required
@exception_handler(
__file__,
post_redirect='admin_purchase_location.delete',
error_name='purchase_location_error'
)
def do_delete(*, id: str) -> Response:
purchase_location = BrickSetPurchaseLocation().select_specific(id)
purchase_location.delete()
reload()
return redirect(url_for('admin.admin', open_purchase_location=True))
# Rename the metadata purchase location
@admin_purchase_location_page.route('<id>/rename', methods=['POST'])
@login_required
@exception_handler(
__file__,
post_redirect='admin.admin',
error_name='purchase_location_error',
open_purchase_location=True
)
def rename(*, id: str) -> Response:
purchase_location = BrickSetPurchaseLocation().select_specific(id)
purchase_location.from_form(request.form).rename()
reload()
return redirect(url_for('admin.admin', open_purchase_location=True))

View File

@ -18,6 +18,7 @@ from ..part import BrickPart
from ..set import BrickSet from ..set import BrickSet
from ..set_list import BrickSetList, set_metadata_lists from ..set_list import BrickSetList, set_metadata_lists
from ..set_owner_list import BrickSetOwnerList from ..set_owner_list import BrickSetOwnerList
from ..set_purchase_location_list import BrickSetPurchaseLocationList
from ..set_status_list import BrickSetStatusList from ..set_status_list import BrickSetStatusList
from ..set_storage_list import BrickSetStorageList from ..set_storage_list import BrickSetStorageList
from ..set_tag_list import BrickSetTagList from ..set_tag_list import BrickSetTagList
@ -40,6 +41,25 @@ def list() -> str:
) )
# Change the value of purchase location
@set_page.route('/<id>/purchase_location', methods=['POST'])
@login_required
@exception_handler(__file__, json=True)
def update_purchase_location(*, id: str) -> Response:
brickset = BrickSet().select_light(id)
purchase_location = BrickSetPurchaseLocationList.get(
request.json.get('value', ''), # type: ignore
allow_none=True
)
value = purchase_location.update_set_value(
brickset,
value=purchase_location.fields.id
)
return jsonify({'value': value})
# Change the state of a owner # Change the state of a owner
@set_page.route('/<id>/owner/<metadata_id>', methods=['POST']) @set_page.route('/<id>/owner/<metadata_id>', methods=['POST'])
@login_required @login_required

View File

@ -16,6 +16,7 @@ class BrickSetSocket extends BrickSocket {
this.html_input = document.getElementById(`${id}-set`); this.html_input = document.getElementById(`${id}-set`);
this.html_no_confim = document.getElementById(`${id}-no-confirm`); this.html_no_confim = document.getElementById(`${id}-no-confirm`);
this.html_owners = document.getElementById(`${id}-owners`); this.html_owners = document.getElementById(`${id}-owners`);
this.html_purchase_location = document.getElementById(`${id}-purchase-location`);
this.html_storage = document.getElementById(`${id}-storage`); this.html_storage = document.getElementById(`${id}-storage`);
this.html_tags = document.getElementById(`${id}-tags`); this.html_tags = document.getElementById(`${id}-tags`);
@ -152,6 +153,12 @@ class BrickSetSocket extends BrickSocket {
}); });
} }
// Grab the purchase location
let purchase_location = null;
if (this.html_purchase_location) {
purchase_location = this.html_purchase_location.value;
}
// Grab the storage // Grab the storage
let storage = null; let storage = null;
if (this.html_storage) { if (this.html_storage) {
@ -177,6 +184,7 @@ class BrickSetSocket extends BrickSocket {
this.socket.emit(this.messages.IMPORT_SET, { this.socket.emit(this.messages.IMPORT_SET, {
set: (set !== undefined) ? set : this.html_input.value, set: (set !== undefined) ? set : this.html_input.value,
owners: owners, owners: owners,
purchase_location: purchase_location,
storage: storage, storage: storage,
tags: tags, tags: tags,
refresh: this.refresh refresh: this.refresh
@ -293,6 +301,10 @@ class BrickSetSocket extends BrickSocket {
this.html_owners.querySelectorAll('input').forEach(input => input.disabled = !enabled); this.html_owners.querySelectorAll('input').forEach(input => input.disabled = !enabled);
} }
if (this.html_purchase_location) {
this.html_purchase_location.disabled = !enabled;
}
if (this.html_storage) { if (this.html_storage) {
this.html_storage.disabled = !enabled; this.html_storage.disabled = !enabled;
} }

View File

@ -51,9 +51,22 @@
</div> </div>
{{ accordion.footer() }} {{ accordion.footer() }}
{% endif %} {% endif %}
{% if brickset_purchase_locations | length %}
{{ accordion.header('Purchase location', 'purchase-location', 'metadata', icon='building-line') }}
<label class="visually-hidden" for="add-purchase-location">{{ name }}</label>
<div class="input-group">
<select id="add-purchase-location" class="form-select" autocomplete="off">
<option value="" selected><i>None</i></option>
{% for purchase_location in brickset_purchase_locations %}
<option value="{{ purchase_location.fields.id }}">{{ purchase_location.fields.name }}</option>
{% endfor %}
</select>
</div>
{{ accordion.footer() }}
{% endif %}
{% if brickset_storages | length %} {% if brickset_storages | length %}
{{ accordion.header('Storage', 'storage', 'metadata', icon='archive-2-line') }} {{ accordion.header('Storage', 'storage', 'metadata', icon='archive-2-line') }}
<label class="visually-hidden" for="storage">{{ name }}</label> <label class="visually-hidden" for="add-storage">{{ name }}</label>
<div class="input-group"> <div class="input-group">
<select id="add-storage" class="form-select" autocomplete="off"> <select id="add-storage" class="form-select" autocomplete="off">
<option value="" selected><i>None</i></option> <option value="" selected><i>None</i></option>

View File

@ -18,6 +18,8 @@
{% include 'admin/database/delete.html' %} {% include 'admin/database/delete.html' %}
{% elif delete_owner %} {% elif delete_owner %}
{% include 'admin/owner/delete.html' %} {% include 'admin/owner/delete.html' %}
{% elif delete_purchase_location %}
{% include 'admin/purchase_location/delete.html' %}
{% elif delete_status %} {% elif delete_status %}
{% include 'admin/status/delete.html' %} {% include 'admin/status/delete.html' %}
{% elif delete_storage %} {% elif delete_storage %}
@ -39,10 +41,11 @@
{% include 'admin/theme.html' %} {% include 'admin/theme.html' %}
{% include 'admin/retired.html' %} {% include 'admin/retired.html' %}
{{ accordion.header('Set metadata', 'metadata', 'admin', expanded=open_metadata, icon='profile-line', class='p-0') }} {{ accordion.header('Set metadata', 'metadata', 'admin', expanded=open_metadata, icon='profile-line', class='p-0') }}
{% include 'admin/owner.html' %} {% include 'admin/owner.html' %}
{% include 'admin/status.html' %} {% include 'admin/purchase_location.html' %}
{% include 'admin/storage.html' %} {% include 'admin/status.html' %}
{% include 'admin/tag.html' %} {% include 'admin/storage.html' %}
{% include 'admin/tag.html' %}
{{ accordion.footer() }} {{ accordion.footer() }}
{% include 'admin/database.html' %} {% include 'admin/database.html' %}
{% include 'admin/configuration.html' %} {% include 'admin/configuration.html' %}

View File

@ -0,0 +1,42 @@
{% import 'macro/accordion.html' as accordion %}
{{ accordion.header('Set purchase locations', 'purchase-location', 'metadata', expanded=open_purchase_location, icon='building-line', class='p-0') }}
{% if purchase_location_error %}<div class="alert alert-danger m-2" role="alert"><strong>Error:</strong> {{ purchase_location_error }}.</div>{% endif %}
<ul class="list-group list-group-flush">
{% if metadata_purchase_locations | length %}
{% for purchase_location in metadata_purchase_locations %}
<li class="list-group-item">
<form action="{{ url_for('admin_purchase_location.rename', id=purchase_location.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-{{ purchase_location.fields.id }}">Name</label>
<div class="input-group">
<div class="input-group-text">Name</div>
<input type="text" class="form-control" id="name-{{ purchase_location.fields.id }}" name="name" value="{{ purchase_location.fields.name }}">
<button type="submit" class="btn btn-primary"><i class="ri-edit-line"></i> Rename</button>
</div>
</div>
<div class="col-12">
<a href="{{ url_for('admin_purchase_location.delete', id=purchase_location.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 purchase location found.</li>
{% endif %}
<li class="list-group-item">
<form action="{{ url_for('admin_purchase_location.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">
<button type="submit" class="btn btn-primary"><i class="ri-add-circle-line"></i> Add</button>
</div>
</form>
</li>
</ul>
{{ accordion.footer() }}

View File

@ -0,0 +1,19 @@
{% import 'macro/accordion.html' as accordion %}
{{ accordion.header('Set purchase locations danger zone', 'purchase-location-danger', 'admin', expanded=true, danger=true, class='text-end') }}
<form action="{{ url_for('admin_purchase_location.do_delete', id=purchase_location.fields.id) }}" method="post">
{% if purchase_location_error %}<div class="alert alert-danger text-start" role="alert"><strong>Error:</strong> {{ purchase_location_error }}.</div>{% endif %}
<div class="alert alert-danger text-center" role="alert">You are about to <strong>delete a set purchase location</strong>. 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="{{ purchase_location.fields.name }}" disabled>
</div>
</div>
</div>
<hr class="border-bottom">
<a class="btn btn-danger" href="{{ url_for('admin.admin', open_purchase_location=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 set purchase location</strong></button>
</form>
{{ accordion.footer() }}

View File

@ -65,6 +65,18 @@
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
{% macro purchase_location(item, purchase_locations, solo=false, last=false) %}
{% if purchase_locations and item.fields.purchase_location in purchase_locations.mapping %}
{% set purchase_location = purchase_locations.mapping[item.fields.purchase_location] %}
{% if last %}
{% set tooltip=purchase_location.fields.name %}
{% else %}
{% set text=purchase_location.fields.name %}
{% endif %}
{{ badge(check=purchase_location, solo=solo, last=last, color='light border', icon='building-line', text=text, tooltip=tooltip) }}
{% endif %}
{% endmacro %}
{% macro set(set, solo=false, last=false, url=None, id=None) %} {% macro set(set, solo=false, last=false, url=None, id=None) %}
{% if id %} {% if id %}
{% set url=url_for('set.details', id=id) %} {% set url=url_for('set.details', id=id) %}

View File

@ -17,6 +17,11 @@
{% if not config['HIDE_TABLE_DAMAGED_PARTS'] %} {% if not config['HIDE_TABLE_DAMAGED_PARTS'] %}
data-has-damaged="{{ (item.fields.total_damaged > 0) | int }}" data-damaged="{{ item.fields.total_damaged }}" data-has-damaged="{{ (item.fields.total_damaged > 0) | int }}" data-damaged="{{ item.fields.total_damaged }}"
{% endif %} {% endif %}
data-has-purchase-location="{{ item.fields.purchase_location is not none | int }}"
{% if item.fields.purchase_location is not none %}
data-purchase-location="{{ item.fields.purchase_location }}"
{% if item.fields.purchase_location in brickset_purchase_locations.mapping %}data-search-purchase-location="{{ brickset_purchase_locations.mapping[item.fields.purchase_location].fields.name | lower }}"{% endif %}
{% endif %}
data-has-storage="{{ item.fields.storage is not none | int }}" data-has-storage="{{ item.fields.storage is not none | int }}"
{% if item.fields.storage is not none %} {% if item.fields.storage is not none %}
data-storage="{{ item.fields.storage }}" data-storage="{{ item.fields.storage }}"
@ -63,6 +68,7 @@
{{ badge.owner(item, owner, solo=solo, last=last) }} {{ badge.owner(item, owner, solo=solo, last=last) }}
{% endfor %} {% endfor %}
{{ badge.storage(item, brickset_storages, solo=solo, last=last) }} {{ badge.storage(item, brickset_storages, solo=solo, last=last) }}
{{ badge.purchase_location(item, brickset_purchase_locations, solo=solo, last=last) }}
{% if not last %} {% if not last %}
{% if not solo %} {% if not solo %}
{{ badge.instructions(item, solo=solo, last=last) }} {{ badge.instructions(item, solo=solo, last=last) }}

View File

@ -60,6 +60,22 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if brickset_purchase_locations | length %}
<div class="col-12 flex-grow-1">
<label class="visually-hidden" for="grid-owner">Purchase location</label>
<div class="input-group">
<span class="input-group-text"><i class="ri-building-line"></i><span class="ms-1 d-none d-md-inline"> Purchase location</span></span>
<select id="grid-purchase-location" class="form-select"
data-filter="value" data-filter-attribute="purchase-location"
autocomplete="off">
<option value="" selected>All</option>
{% for purchase_location in brickset_purchase_locations %}
<option value="{{ purchase_location.fields.id }}">{{ purchase_location.fields.name }}</option>
{% endfor %}
</select>
</div>
</div>
{% endif %}
{% if brickset_storages | length %} {% if brickset_storages | length %}
<div class="col-12 flex-grow-1"> <div class="col-12 flex-grow-1">
<label class="visually-hidden" for="grid-owner">Storage</label> <label class="visually-hidden" for="grid-owner">Storage</label>

View File

@ -1,44 +1,53 @@
{% if g.login.is_authenticated() %} {% if g.login.is_authenticated() %}
{{ accordion.header('Management', 'set-management', 'set-details', icon='settings-4-line', class='p-0') }} {{ accordion.header('Management', 'set-management', 'set-details', icon='settings-4-line', class='p-0') }}
{{ accordion.header('Owners', 'owner', 'set-management', icon='group-line', class='p-0') }} {{ accordion.header('Owners', 'owner', 'set-management', icon='group-line', class='p-0') }}
<ul class="list-group list-group-flush"> <ul class="list-group list-group-flush">
{% if brickset_owners | length %} {% if brickset_owners | length %}
{% for owner in brickset_owners %} {% for owner in brickset_owners %}
<li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(item, owner, delete=delete) }}</li> <li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(item, owner, delete=delete) }}</li>
{% endfor %} {% endfor %}
{% else %} {% else %}
<li class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No owner found.</li> <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 set owners</a>
</div>
{{ accordion.footer() }}
{{ accordion.header('Purchase location', 'purchase-location', 'set-management', icon='building-line') }}
{% if brickset_purchase_locations | length %}
{{ form.select('Purchase location', item, 'purchase_location', brickset_purchase_locations, delete=delete) }}
{% else %}
<p class="text-center"><i class="ri-error-warning-line"></i> No purchase location found.</p>
{% endif %} {% endif %}
</ul> <hr>
<div class="list-group list-group-flush border-top"> <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 set purchase locations</a>
<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 set owners</a> {{ accordion.footer() }}
</div>
{{ accordion.footer() }}
{{ accordion.header('Storage', 'storage', 'set-management', icon='archive-2-line') }} {{ accordion.header('Storage', 'storage', 'set-management', icon='archive-2-line') }}
{% if brickset_storages | length %} {% if brickset_storages | length %}
{{ form.select('Storage', item, 'storage', brickset_storages, delete=delete) }} {{ form.select('Storage', item, 'storage', brickset_storages, delete=delete) }}
{% else %} {% else %}
<p class="text-center"><i class="ri-error-warning-line"></i> No storage found.</p> <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 set storages</a>
{{ accordion.footer() }}
{{ accordion.header('Tags', 'tag', 'set-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(item, tag, delete=delete) }}</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 %} {% endif %}
</ul> <hr>
<div class="list-group list-group-flush border-top"> <a href="{{ url_for('admin.admin', open_storage=true) }}" class="btn btn-primary" role="button"><i class="ri-settings-4-line"></i> Manage the set storages</a>
<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> {{ accordion.footer() }}
</div> {{ accordion.header('Tags', 'tag', 'set-management', icon='price-tag-2-line', class='p-0') }}
{{ accordion.footer() }} <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(item, tag, delete=delete) }}</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 set tags</a>
</div>
{{ accordion.footer() }}
{{ accordion.header('Data', 'data', 'set-management', icon='database-2-line') }} {{ 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> <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() }} {{ accordion.footer() }}
{{ accordion.footer() }} {{ accordion.footer() }}
{% endif %} {% endif %}

View File

@ -10,7 +10,7 @@
<label class="visually-hidden" for="grid-search">Search</label> <label class="visually-hidden" for="grid-search">Search</label>
<div class="input-group"> <div class="input-group">
<span class="input-group-text"><i class="ri-search-line"></i><span class="ms-1 d-none d-md-inline"> Search</span></span> <span class="input-group-text"><i class="ri-search-line"></i><span class="ms-1 d-none d-md-inline"> Search</span></span>
<input id="grid-search" data-search-exact="name,number,parts,searchStorage,theme,year" data-search-list="searchOwner,searchTag" class="form-control form-control-sm" type="text" placeholder="Set, name, number of parts, theme, year, owner, storage, tag" value=""> <input id="grid-search" data-search-exact="name,number,parts,searchPurchaseLocation,searchStorage,theme,year" data-search-list="searchOwner,searchTag" class="form-control form-control-sm" type="text" placeholder="Set, name, number of parts, theme, year, owner, purchase location, storage, tag" value="">
</div> </div>
</div> </div>
<div class="col-12"> <div class="col-12">