diff --git a/.env.sample b/.env.sample
index 44d52fec..8f129392 100644
--- a/.env.sample
+++ b/.env.sample
@@ -168,6 +168,12 @@
# Default: 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.
# Default: false
# Legacy name: RANDOM
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b50f3427..efcdb16d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,7 +14,8 @@
- 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_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
@@ -39,6 +40,7 @@
- Ownership
- Tags
- Storage
+ - Purchase location
- Storage
- Storage content and list
@@ -85,6 +87,7 @@
- Tags
- Refresh
- Storage
+ - Purchase location
- Sets grid
- Collapsible controls depending on screen size
diff --git a/bricktracker/app.py b/bricktracker/app.py
index af005d96..f0afe429 100644
--- a/bricktracker/app.py
+++ b/bricktracker/app.py
@@ -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.instructions import admin_instructions_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.status import admin_status_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_retired_page)
app.register_blueprint(admin_owner_page)
+ app.register_blueprint(admin_purchase_location_page)
app.register_blueprint(admin_status_page)
app.register_blueprint(admin_storage_page)
app.register_blueprint(admin_tag_page)
diff --git a/bricktracker/config.py b/bricktracker/config.py
index 5b9788fb..729049a9 100644
--- a/bricktracker/config.py
+++ b/bricktracker/config.py
@@ -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_FOLDER', 'd': 'parts', 's': True},
{'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': 'REBRICKABLE_API_KEY', 'e': 'REBRICKABLE_API_KEY', 'd': ''},
{'n': 'REBRICKABLE_IMAGE_NIL', 'd': 'https://rebrickable.com/static/img/nil.png'}, # noqa: E501
diff --git a/bricktracker/metadata.py b/bricktracker/metadata.py
index 0b2f61cd..88d26235 100644
--- a/bricktracker/metadata.py
+++ b/bricktracker/metadata.py
@@ -48,7 +48,7 @@ class BrickMetadata(BrickRecord):
def as_column(self, /) -> str:
return '{kind}_{id}'.format(
id=self.fields.id,
- kind=self.kind.lower()
+ kind=self.kind.lower().replace(' ', '-')
)
# HTML dataset name
@@ -90,8 +90,6 @@ class BrickMetadata(BrickRecord):
# Rename the entry
def rename(self, /) -> None:
- self.safe()
-
self.update_field('name', value=self.fields.name)
# Make the name "safe"
@@ -159,7 +157,7 @@ class BrickMetadata(BrickRecord):
)
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,
kind=self.kind,
name=self.fields.name,
diff --git a/bricktracker/metadata_list.py b/bricktracker/metadata_list.py
index 3b98a132..71245483 100644
--- a/bricktracker/metadata_list.py
+++ b/bricktracker/metadata_list.py
@@ -7,13 +7,21 @@ from .exceptions import NotFoundException
from .fields import BrickRecordFields
from .record_list import BrickRecordList
from .set_owner import BrickSetOwner
+from .set_purchase_location import BrickSetPurchaseLocation
from .set_status import BrickSetStatus
from .set_storage import BrickSetStorage
from .set_tag import BrickSetTag
logger = logging.getLogger(__name__)
-T = TypeVar('T', BrickSetOwner, BrickSetStatus, BrickSetStorage, BrickSetTag)
+T = TypeVar(
+ 'T',
+ BrickSetOwner,
+ BrickSetPurchaseLocation,
+ BrickSetStatus,
+ BrickSetStorage,
+ BrickSetTag
+)
# Lego sets metadata list
diff --git a/bricktracker/record_list.py b/bricktracker/record_list.py
index 23da29bc..18dcb10c 100644
--- a/bricktracker/record_list.py
+++ b/bricktracker/record_list.py
@@ -9,6 +9,7 @@ if TYPE_CHECKING:
from .rebrickable_set import RebrickableSet
from .set import BrickSet
from .set_owner import BrickSetOwner
+ from .set_purchase_location import BrickSetPurchaseLocation
from .set_status import BrickSetStatus
from .set_storage import BrickSetStorage
from .set_tag import BrickSetTag
@@ -20,6 +21,7 @@ T = TypeVar(
'BrickPart',
'BrickSet',
'BrickSetOwner',
+ 'BrickSetPurchaseLocation',
'BrickSetStatus',
'BrickSetStorage',
'BrickSetTag',
diff --git a/bricktracker/reload.py b/bricktracker/reload.py
index b2247ea4..38929f68 100644
--- a/bricktracker/reload.py
+++ b/bricktracker/reload.py
@@ -1,6 +1,7 @@
from .instructions_list import BrickInstructionsList
from .retired_list import BrickRetiredList
from .set_owner_list import BrickSetOwnerList
+from .set_purchase_location_list import BrickSetPurchaseLocationList
from .set_status_list import BrickSetStatusList
from .set_storage_list import BrickSetStorageList
from .set_tag_list import BrickSetTagList
@@ -17,6 +18,9 @@ def reload() -> None:
# Reload the set owners
BrickSetOwnerList.new(force=True)
+ # Reload the set purchase locations
+ BrickSetPurchaseLocationList.new(force=True)
+
# Reload the set statuses
BrickSetStatusList.new(force=True)
diff --git a/bricktracker/set.py b/bricktracker/set.py
index 6368d40c..fb972092 100644
--- a/bricktracker/set.py
+++ b/bricktracker/set.py
@@ -10,6 +10,7 @@ from .minifigure_list import BrickMinifigureList
from .part_list import BrickPartList
from .rebrickable_set import RebrickableSet
from .set_owner_list import BrickSetOwnerList
+from .set_purchase_location_list import BrickSetPurchaseLocationList
from .set_status_list import BrickSetStatusList
from .set_storage_list import BrickSetStorageList
from .set_tag_list import BrickSetTagList
@@ -63,6 +64,13 @@ class BrickSet(RebrickableSet):
)
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
self.insert(commit=False)
diff --git a/bricktracker/set_list.py b/bricktracker/set_list.py
index deaf269b..a8f5faa4 100644
--- a/bricktracker/set_list.py
+++ b/bricktracker/set_list.py
@@ -5,6 +5,8 @@ from flask import current_app
from .record_list import BrickRecordList
from .set_owner import BrickSetOwner
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_storage import BrickSetStorage
from .set_storage_list import BrickSetStorageList
@@ -175,6 +177,8 @@ def set_metadata_lists(
str,
Union[
list[BrickSetOwner],
+ list[BrickSetPurchaseLocation],
+ BrickSetPurchaseLocation,
list[BrickSetStorage],
BrickSetStorageList,
list[BrickSetTag]
@@ -182,6 +186,7 @@ def set_metadata_lists(
]:
return {
'brickset_owners': BrickSetOwnerList.list(),
+ 'brickset_purchase_locations': BrickSetPurchaseLocationList.list(as_class=as_class), # noqa: E501
'brickset_storages': BrickSetStorageList.list(as_class=as_class),
'brickset_tags': BrickSetTagList.list(),
}
diff --git a/bricktracker/set_purchase_location.py b/bricktracker/set_purchase_location.py
new file mode 100644
index 00000000..801ccf82
--- /dev/null
+++ b/bricktracker/set_purchase_location.py
@@ -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'
diff --git a/bricktracker/set_purchase_location_list.py b/bricktracker/set_purchase_location_list.py
new file mode 100644
index 00000000..3ffae4bc
--- /dev/null
+++ b/bricktracker/set_purchase_location_list.py
@@ -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)
diff --git a/bricktracker/sql/set/base/base.sql b/bricktracker/sql/set/base/base.sql
index fbc86e0d..333868d0 100644
--- a/bricktracker/sql/set/base/base.sql
+++ b/bricktracker/sql/set/base/base.sql
@@ -1,6 +1,7 @@
SELECT
{% block id %}{% endblock %}
"bricktracker_sets"."storage",
+ "bricktracker_sets"."purchase_location",
"rebrickable_sets"."set",
"rebrickable_sets"."number",
"rebrickable_sets"."version",
diff --git a/bricktracker/sql/set/insert.sql b/bricktracker/sql/set/insert.sql
index 9a46f88b..bc933c33 100644
--- a/bricktracker/sql/set/insert.sql
+++ b/bricktracker/sql/set/insert.sql
@@ -1,9 +1,11 @@
INSERT OR IGNORE INTO "bricktracker_sets" (
"id",
"set",
- "storage"
+ "storage",
+ "purchase_location"
) VALUES (
:id,
:set,
- :storage
+ :storage,
+ :purchase_location
)
diff --git a/bricktracker/sql/set/metadata/purchase_location/base.sql b/bricktracker/sql/set/metadata/purchase_location/base.sql
new file mode 100644
index 00000000..8ac33ca1
--- /dev/null
+++ b/bricktracker/sql/set/metadata/purchase_location/base.sql
@@ -0,0 +1,6 @@
+SELECT
+ "bricktracker_metadata_purchase_locations"."id",
+ "bricktracker_metadata_purchase_locations"."name"
+FROM "bricktracker_metadata_purchase_locations"
+
+{% block where %}{% endblock %}
diff --git a/bricktracker/sql/set/metadata/purchase_location/delete.sql b/bricktracker/sql/set/metadata/purchase_location/delete.sql
new file mode 100644
index 00000000..489dfd07
--- /dev/null
+++ b/bricktracker/sql/set/metadata/purchase_location/delete.sql
@@ -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;
\ No newline at end of file
diff --git a/bricktracker/sql/set/metadata/purchase_location/insert.sql b/bricktracker/sql/set/metadata/purchase_location/insert.sql
new file mode 100644
index 00000000..22fc5879
--- /dev/null
+++ b/bricktracker/sql/set/metadata/purchase_location/insert.sql
@@ -0,0 +1,11 @@
+BEGIN TRANSACTION;
+
+INSERT INTO "bricktracker_metadata_purchase_locations" (
+ "id",
+ "name"
+) VALUES (
+ '{{ id }}',
+ '{{ name }}'
+);
+
+COMMIT;
\ No newline at end of file
diff --git a/bricktracker/sql/set/metadata/purchase_location/list.sql b/bricktracker/sql/set/metadata/purchase_location/list.sql
new file mode 100644
index 00000000..2a0813b5
--- /dev/null
+++ b/bricktracker/sql/set/metadata/purchase_location/list.sql
@@ -0,0 +1 @@
+{% extends 'set/metadata/purchase_location/base.sql' %}
diff --git a/bricktracker/sql/set/metadata/purchase_location/select.sql b/bricktracker/sql/set/metadata/purchase_location/select.sql
new file mode 100644
index 00000000..a9e6161f
--- /dev/null
+++ b/bricktracker/sql/set/metadata/purchase_location/select.sql
@@ -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 %}
\ No newline at end of file
diff --git a/bricktracker/sql/set/metadata/purchase_location/update/field.sql b/bricktracker/sql/set/metadata/purchase_location/update/field.sql
new file mode 100644
index 00000000..323d98d1
--- /dev/null
+++ b/bricktracker/sql/set/metadata/purchase_location/update/field.sql
@@ -0,0 +1,3 @@
+UPDATE "bricktracker_metadata_purchase_locations"
+SET "{{field}}" = :value
+WHERE "bricktracker_metadata_purchase_locations"."id" IS NOT DISTINCT FROM :id
diff --git a/bricktracker/sql/set/metadata/purchase_location/update/value.sql b/bricktracker/sql/set/metadata/purchase_location/update/value.sql
new file mode 100644
index 00000000..d27469e1
--- /dev/null
+++ b/bricktracker/sql/set/metadata/purchase_location/update/value.sql
@@ -0,0 +1,3 @@
+UPDATE "bricktracker_sets"
+SET "purchase_location" = :value
+WHERE "bricktracker_sets"."id" IS NOT DISTINCT FROM :set_id
diff --git a/bricktracker/views/admin/admin.py b/bricktracker/views/admin/admin.py
index 08cdafcb..749b3df2 100644
--- a/bricktracker/views/admin/admin.py
+++ b/bricktracker/views/admin/admin.py
@@ -10,6 +10,8 @@ from ...rebrickable_image import RebrickableImage
from ...retired_list import BrickRetiredList
from ...set_owner import BrickSetOwner
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_list import BrickSetStorageList
from ...set_status import BrickSetStatus
@@ -36,6 +38,7 @@ def admin() -> str:
database_version: int = -1
instructions: BrickInstructionsList | None = None
metadata_owners: list[BrickSetOwner] = []
+ metadata_purchase_locations: list[BrickSetPurchaseLocation] = []
metadata_statuses: list[BrickSetStatus] = []
metadata_storages: list[BrickSetStorage] = []
metadata_tags: list[BrickSetTag] = []
@@ -54,6 +57,7 @@ def admin() -> str:
instructions = BrickInstructionsList()
metadata_owners = BrickSetOwnerList.list()
+ metadata_purchase_locations = BrickSetPurchaseLocationList.list()
metadata_statuses = BrickSetStatusList.list(all=True)
metadata_storages = BrickSetStorageList.list()
metadata_tags = BrickSetTagList.list()
@@ -81,6 +85,7 @@ def admin() -> str:
open_instructions = request.args.get('open_instructions', None)
open_logout = request.args.get('open_logout', 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_status = request.args.get('open_status', None)
open_storage = request.args.get('open_storage', None)
@@ -89,6 +94,7 @@ def admin() -> str:
open_metadata = (
open_owner or
+ open_purchase_location or
open_status or
open_storage or
open_tag
@@ -113,6 +119,7 @@ def admin() -> str:
database_version=database_version,
instructions=instructions,
metadata_owners=metadata_owners,
+ metadata_purchase_locations=metadata_purchase_locations,
metadata_statuses=metadata_statuses,
metadata_storages=metadata_storages,
metadata_tags=metadata_tags,
@@ -126,12 +133,14 @@ def admin() -> str:
open_logout=open_logout,
open_metadata=open_metadata,
open_owner=open_owner,
+ open_purchase_location=open_purchase_location,
open_retired=open_retired,
open_status=open_status,
open_storage=open_storage,
open_tag=open_tag,
open_theme=open_theme,
owner_error=request.args.get('owner_error'),
+ purchase_location_error=request.args.get('purchase_location_error'),
retired=BrickRetiredList(),
status_error=request.args.get('status_error'),
storage_error=request.args.get('storage_error'),
diff --git a/bricktracker/views/admin/purchase_location.py b/bricktracker/views/admin/purchase_location.py
new file mode 100644
index 00000000..48e7b7d5
--- /dev/null
+++ b/bricktracker/views/admin/purchase_location.py
@@ -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))
diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py
index 7f397da7..0777f4e9 100644
--- a/bricktracker/views/set.py
+++ b/bricktracker/views/set.py
@@ -18,6 +18,7 @@ from ..part import BrickPart
from ..set import BrickSet
from ..set_list import BrickSetList, set_metadata_lists
from ..set_owner_list import BrickSetOwnerList
+from ..set_purchase_location_list import BrickSetPurchaseLocationList
from ..set_status_list import BrickSetStatusList
from ..set_storage_list import BrickSetStorageList
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
@set_page.route('/<id>/owner/<metadata_id>', methods=['POST'])
@login_required
diff --git a/static/scripts/socket/set.js b/static/scripts/socket/set.js
index 311eac50..8a4e5bba 100644
--- a/static/scripts/socket/set.js
+++ b/static/scripts/socket/set.js
@@ -16,6 +16,7 @@ class BrickSetSocket extends BrickSocket {
this.html_input = document.getElementById(`${id}-set`);
this.html_no_confim = document.getElementById(`${id}-no-confirm`);
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_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
let storage = null;
if (this.html_storage) {
@@ -177,6 +184,7 @@ class BrickSetSocket extends BrickSocket {
this.socket.emit(this.messages.IMPORT_SET, {
set: (set !== undefined) ? set : this.html_input.value,
owners: owners,
+ purchase_location: purchase_location,
storage: storage,
tags: tags,
refresh: this.refresh
@@ -293,6 +301,10 @@ class BrickSetSocket extends BrickSocket {
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) {
this.html_storage.disabled = !enabled;
}
diff --git a/templates/add.html b/templates/add.html
index 9a0deebf..d9a94629 100644
--- a/templates/add.html
+++ b/templates/add.html
@@ -51,9 +51,22 @@
</div>
{{ accordion.footer() }}
{% 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 %}
{{ 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">
<select id="add-storage" class="form-select" autocomplete="off">
<option value="" selected><i>None</i></option>
diff --git a/templates/admin.html b/templates/admin.html
index 22a7e1a7..87dbdb4a 100644
--- a/templates/admin.html
+++ b/templates/admin.html
@@ -18,6 +18,8 @@
{% include 'admin/database/delete.html' %}
{% elif delete_owner %}
{% include 'admin/owner/delete.html' %}
+ {% elif delete_purchase_location %}
+ {% include 'admin/purchase_location/delete.html' %}
{% elif delete_status %}
{% include 'admin/status/delete.html' %}
{% elif delete_storage %}
@@ -39,10 +41,11 @@
{% include 'admin/theme.html' %}
{% include 'admin/retired.html' %}
{{ accordion.header('Set metadata', 'metadata', 'admin', expanded=open_metadata, icon='profile-line', class='p-0') }}
- {% include 'admin/owner.html' %}
- {% include 'admin/status.html' %}
- {% include 'admin/storage.html' %}
- {% include 'admin/tag.html' %}
+ {% include 'admin/owner.html' %}
+ {% include 'admin/purchase_location.html' %}
+ {% include 'admin/status.html' %}
+ {% include 'admin/storage.html' %}
+ {% include 'admin/tag.html' %}
{{ accordion.footer() }}
{% include 'admin/database.html' %}
{% include 'admin/configuration.html' %}
diff --git a/templates/admin/purchase_location.html b/templates/admin/purchase_location.html
new file mode 100644
index 00000000..12675768
--- /dev/null
+++ b/templates/admin/purchase_location.html
@@ -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() }}
diff --git a/templates/admin/purchase_location/delete.html b/templates/admin/purchase_location/delete.html
new file mode 100644
index 00000000..c53d8c22
--- /dev/null
+++ b/templates/admin/purchase_location/delete.html
@@ -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() }}
diff --git a/templates/macro/badge.html b/templates/macro/badge.html
index ea2be584..f1c9f89f 100644
--- a/templates/macro/badge.html
+++ b/templates/macro/badge.html
@@ -65,6 +65,18 @@
{% endif %}
{% 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) %}
{% if id %}
{% set url=url_for('set.details', id=id) %}
diff --git a/templates/set/card.html b/templates/set/card.html
index 20642066..7c2525d1 100644
--- a/templates/set/card.html
+++ b/templates/set/card.html
@@ -17,6 +17,11 @@
{% if not config['HIDE_TABLE_DAMAGED_PARTS'] %}
data-has-damaged="{{ (item.fields.total_damaged > 0) | int }}" data-damaged="{{ item.fields.total_damaged }}"
{% 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 }}"
{% if item.fields.storage is not none %}
data-storage="{{ item.fields.storage }}"
@@ -63,6 +68,7 @@
{{ badge.owner(item, owner, solo=solo, last=last) }}
{% endfor %}
{{ 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 solo %}
{{ badge.instructions(item, solo=solo, last=last) }}
diff --git a/templates/set/filter.html b/templates/set/filter.html
index 8f3b4109..24454c25 100644
--- a/templates/set/filter.html
+++ b/templates/set/filter.html
@@ -60,6 +60,22 @@
</div>
</div>
{% 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 %}
<div class="col-12 flex-grow-1">
<label class="visually-hidden" for="grid-owner">Storage</label>
diff --git a/templates/set/management.html b/templates/set/management.html
index 1a8c2b7b..8d675f9a 100644
--- a/templates/set/management.html
+++ b/templates/set/management.html
@@ -1,44 +1,53 @@
{% if g.login.is_authenticated() %}
{{ 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') }}
- <ul class="list-group list-group-flush">
- {% if brickset_owners | length %}
- {% for owner in brickset_owners %}
- <li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(item, owner, delete=delete) }}</li>
- {% endfor %}
- {% else %}
- <li class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No owner found.</li>
+ <ul class="list-group list-group-flush">
+ {% if brickset_owners | length %}
+ {% for owner in brickset_owners %}
+ <li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(item, owner, delete=delete) }}</li>
+ {% endfor %}
+ {% else %}
+ <li class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No owner found.</li>
+ {% endif %}
+ </ul>
+ <div class="list-group list-group-flush border-top">
+ <a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_owner=true) }}"><i class="ri-settings-4-line"></i> Manage the 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 %}
- </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() }}
+ <hr>
+ <a href="{{ url_for('admin.admin', open_purchase_location=true) }}" class="btn btn-primary" role="button"><i class="ri-settings-4-line"></i> Manage the set purchase locations</a>
+ {{ accordion.footer() }}
{{ accordion.header('Storage', 'storage', 'set-management', icon='archive-2-line') }}
- {% if brickset_storages | length %}
- {{ form.select('Storage', item, 'storage', brickset_storages, delete=delete) }}
- {% else %}
- <p class="text-center"><i class="ri-error-warning-line"></i> No storage found.</p>
- {% endif %}
- <hr>
- <a href="{{ url_for('admin.admin', open_storage=true) }}" class="btn btn-primary" role="button"><i class="ri-settings-4-line"></i> Manage the 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>
+ {% if brickset_storages | length %}
+ {{ form.select('Storage', item, 'storage', brickset_storages, delete=delete) }}
+ {% else %}
+ <p class="text-center"><i class="ri-error-warning-line"></i> No storage found.</p>
{% 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() }}
+ <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 %}
+ </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') }}
- <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() }}
{% endif %}
diff --git a/templates/sets.html b/templates/sets.html
index 5af93c93..4832160c 100644
--- a/templates/sets.html
+++ b/templates/sets.html
@@ -10,7 +10,7 @@
<label class="visually-hidden" for="grid-search">Search</label>
<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>
- <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 class="col-12">