diff --git a/bricktracker/app.py b/bricktracker/app.py
index f6054bcb..15cb9a3d 100644
--- a/bricktracker/app.py
+++ b/bricktracker/app.py
@@ -13,11 +13,12 @@ from bricktracker.sql import close
from bricktracker.version import __version__
from bricktracker.views.add import add_page
from bricktracker.views.admin.admin import admin_page
-from bricktracker.views.admin.status import admin_status_page
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.retired import admin_retired_page
+from bricktracker.views.admin.status import admin_status_page
from bricktracker.views.admin.theme import admin_theme_page
from bricktracker.views.error import error_404
from bricktracker.views.index import index_page
@@ -78,11 +79,12 @@ def setup_app(app: Flask) -> None:
# Register admin routes
app.register_blueprint(admin_page)
- app.register_blueprint(admin_status_page)
app.register_blueprint(admin_database_page)
app.register_blueprint(admin_image_page)
app.register_blueprint(admin_instructions_page)
app.register_blueprint(admin_retired_page)
+ app.register_blueprint(admin_owner_page)
+ app.register_blueprint(admin_status_page)
app.register_blueprint(admin_theme_page)
# An helper to make global variables available to the
diff --git a/bricktracker/metadata.py b/bricktracker/metadata.py
index c7a9678b..4b7c54ea 100644
--- a/bricktracker/metadata.py
+++ b/bricktracker/metadata.py
@@ -176,8 +176,16 @@ class BrickMetadata(BrickRecord):
return value
# Update the selected state of this metadata item for a set
- def update_set_state(self, brickset: 'BrickSet', json: Any | None) -> Any:
- state: bool = json.get('value', False) # type: ignore
+ def update_set_state(
+ self,
+ brickset: 'BrickSet',
+ /,
+ *,
+ json: Any | None = None,
+ state: bool | None = None,
+ ) -> Any:
+ if state is None:
+ state = json.get('value', False) # type: ignore
parameters = self.sql_parameters()
parameters['set_id'] = brickset.fields.id
diff --git a/bricktracker/metadata_list.py b/bricktracker/metadata_list.py
index 80bb3df0..bb2e337b 100644
--- a/bricktracker/metadata_list.py
+++ b/bricktracker/metadata_list.py
@@ -1,14 +1,15 @@
import logging
-from typing import Type
+from typing import Type, TypeVar
from .exceptions import NotFoundException
from .fields import BrickRecordFields
from .record_list import BrickRecordList
+from .set_owner import BrickSetOwner
from .set_status import BrickSetStatus
logger = logging.getLogger(__name__)
-T = BrickSetStatus
+T = TypeVar('T', 'BrickSetStatus', 'BrickSetOwner')
# Lego sets metadata list
diff --git a/bricktracker/record_list.py b/bricktracker/record_list.py
index 22752032..8927e717 100644
--- a/bricktracker/record_list.py
+++ b/bricktracker/record_list.py
@@ -8,12 +8,14 @@ if TYPE_CHECKING:
from .part import BrickPart
from .rebrickable_set import RebrickableSet
from .set import BrickSet
+ from .set_owner import BrickSetOwner
from .set_status import BrickSetStatus
from .wish import BrickWish
T = TypeVar(
'T',
'BrickSet',
+ 'BrickSetOwner',
'BrickSetStatus',
'BrickPart',
'BrickMinifigure',
diff --git a/bricktracker/reload.py b/bricktracker/reload.py
index 259cffad..73e9e241 100644
--- a/bricktracker/reload.py
+++ b/bricktracker/reload.py
@@ -1,5 +1,7 @@
from .instructions_list import BrickInstructionsList
from .retired_list import BrickRetiredList
+from .set_owner import BrickSetOwner
+from .set_owner_list import BrickSetOwnerList
from .set_status import BrickSetStatus
from .set_status_list import BrickSetStatusList
from .theme_list import BrickThemeList
@@ -12,6 +14,9 @@ def reload() -> None:
# Reload the instructions
BrickInstructionsList(force=True)
+ # Reload the set owners
+ BrickSetOwnerList(BrickSetOwner, force=True)
+
# Reload the set statuses
BrickSetStatusList(BrickSetStatus, force=True)
diff --git a/bricktracker/set.py b/bricktracker/set.py
index 32bf8daa..f4bf1a26 100644
--- a/bricktracker/set.py
+++ b/bricktracker/set.py
@@ -9,6 +9,8 @@ from .exceptions import NotFoundException
from .minifigure_list import BrickMinifigureList
from .part_list import BrickPartList
from .rebrickable_set import RebrickableSet
+from .set_owner import BrickSetOwner
+from .set_owner_list import BrickSetOwnerList
from .set_status import BrickSetStatus
from .set_status_list import BrickSetStatusList
from .sql import BrickSQL
@@ -68,6 +70,13 @@ class BrickSet(RebrickableSet):
if not BrickMinifigureList.download(socket, self, refresh=refresh):
return False
+ # Save the owners
+ owners: list[str] = list(data.get('owners', []))
+
+ for id in owners:
+ owner = BrickSetOwnerList(BrickSetOwner).get(id)
+ owner.update_set_state(self, state=True)
+
# Commit the transaction to the database
socket.auto_progress(
message='Set {set}: writing to the database'.format(
@@ -162,6 +171,7 @@ class BrickSet(RebrickableSet):
# Load from database
if not self.select(
+ owners=BrickSetOwnerList(BrickSetOwner).as_columns(),
statuses=BrickSetStatusList(BrickSetStatus).as_columns(all=True)
):
raise NotFoundException(
diff --git a/bricktracker/set_list.py b/bricktracker/set_list.py
index 251cfdcd..6a94185b 100644
--- a/bricktracker/set_list.py
+++ b/bricktracker/set_list.py
@@ -3,6 +3,8 @@ from typing import Self
from flask import current_app
from .record_list import BrickRecordList
+from .set_owner import BrickSetOwner
+from .set_owner_list import BrickSetOwnerList
from .set_status import BrickSetStatus
from .set_status_list import BrickSetStatusList
from .set import BrickSet
@@ -38,6 +40,7 @@ class BrickSetList(BrickRecordList[BrickSet]):
# Load the sets from the database
for record in self.select(
order=self.order,
+ owners=BrickSetOwnerList(BrickSetOwner).as_columns(),
statuses=BrickSetStatusList(BrickSetStatus).as_columns()
):
brickset = BrickSet(record=record)
@@ -74,6 +77,7 @@ class BrickSetList(BrickRecordList[BrickSet]):
for record in self.select(
order=order,
limit=limit,
+ owners=BrickSetOwnerList(BrickSetOwner).as_columns(),
statuses=BrickSetStatusList(BrickSetStatus).as_columns()
):
brickset = BrickSet(record=record)
diff --git a/bricktracker/set_owner.py b/bricktracker/set_owner.py
new file mode 100644
index 00000000..3c07647b
--- /dev/null
+++ b/bricktracker/set_owner.py
@@ -0,0 +1,16 @@
+from .metadata import BrickMetadata
+
+
+# Lego set owner metadata
+class BrickSetOwner(BrickMetadata):
+ kind: str = 'owner'
+
+ # Set state endpoint
+ set_state_endpoint: str = 'set.update_owner'
+
+ # Queries
+ delete_query: str = 'set/metadata/owner/delete'
+ insert_query: str = 'set/metadata/owner/insert'
+ select_query: str = 'set/metadata/owner/select'
+ update_field_query: str = 'set/metadata/owner/update/field'
+ update_set_state_query: str = 'set/metadata/owner/update/state'
diff --git a/bricktracker/set_owner_list.py b/bricktracker/set_owner_list.py
new file mode 100644
index 00000000..13097490
--- /dev/null
+++ b/bricktracker/set_owner_list.py
@@ -0,0 +1,17 @@
+import logging
+
+from .metadata_list import BrickMetadataList
+from .set_owner import BrickSetOwner
+
+logger = logging.getLogger(__name__)
+
+
+# Lego sets owner list
+class BrickSetOwnerList(BrickMetadataList[BrickSetOwner]):
+ kind: str = 'set owners'
+
+ # Database table
+ table: str = 'bricktracker_set_owners'
+
+ # Queries
+ select_query = 'set/metadata/owner/list'
diff --git a/bricktracker/sql/migrations/0013.sql b/bricktracker/sql/migrations/0013.sql
new file mode 100644
index 00000000..33f8a6f0
--- /dev/null
+++ b/bricktracker/sql/migrations/0013.sql
@@ -0,0 +1,19 @@
+-- description: Add set owners
+
+BEGIN TRANSACTION;
+
+-- Create a table to define each set owners: an id and a name
+CREATE TABLE "bricktracker_metadata_owners" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ PRIMARY KEY("id")
+);
+
+-- Create a table for the set owners
+CREATE TABLE "bricktracker_set_owners" (
+ "id" TEXT NOT NULL,
+ PRIMARY KEY("id"),
+ FOREIGN KEY("id") REFERENCES "bricktracker_sets"("id")
+);
+
+COMMIT;
diff --git a/bricktracker/sql/set/base/base.sql b/bricktracker/sql/set/base/base.sql
index 8b1f4c88..940dab9f 100644
--- a/bricktracker/sql/set/base/base.sql
+++ b/bricktracker/sql/set/base/base.sql
@@ -9,6 +9,9 @@ SELECT
"rebrickable_sets"."number_of_parts",
"rebrickable_sets"."image",
"rebrickable_sets"."url",
+ {% block owners %}
+ {% if owners %}{{ owners }},{% endif %}
+ {% endblock %}
{% block statuses %}
{% if statuses %}{{ statuses }},{% endif %}
{% endblock %}
diff --git a/bricktracker/sql/set/base/full.sql b/bricktracker/sql/set/base/full.sql
index 70730ff6..725b56dc 100644
--- a/bricktracker/sql/set/base/full.sql
+++ b/bricktracker/sql/set/base/full.sql
@@ -13,6 +13,11 @@ IFNULL("minifigures_join"."total", 0) AS "total_minifigures"
{% endblock %}
{% block join %}
+{% if owners %}
+LEFT JOIN "bricktracker_set_owners"
+ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_owners"."id"
+{% endif %}
+
{% if statuses %}
LEFT JOIN "bricktracker_set_statuses"
ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_statuses"."id"
diff --git a/bricktracker/sql/set/delete/set.sql b/bricktracker/sql/set/delete/set.sql
index 49b0e884..2db140de 100644
--- a/bricktracker/sql/set/delete/set.sql
+++ b/bricktracker/sql/set/delete/set.sql
@@ -6,6 +6,9 @@ BEGIN TRANSACTION;
DELETE FROM "bricktracker_sets"
WHERE "bricktracker_sets"."id" IS NOT DISTINCT FROM '{{ id }}';
+DELETE FROM "bricktracker_set_owners"
+WHERE "bricktracker_set_owners"."id" IS NOT DISTINCT FROM '{{ id }}';
+
DELETE FROM "bricktracker_set_statuses"
WHERE "bricktracker_set_statuses"."id" IS NOT DISTINCT FROM '{{ id }}';
diff --git a/bricktracker/sql/set/metadata/owner/base.sql b/bricktracker/sql/set/metadata/owner/base.sql
new file mode 100644
index 00000000..095ae3d6
--- /dev/null
+++ b/bricktracker/sql/set/metadata/owner/base.sql
@@ -0,0 +1,6 @@
+SELECT
+ "bricktracker_metadata_owners"."id",
+ "bricktracker_metadata_owners"."name"
+FROM "bricktracker_metadata_owners"
+
+{% block where %}{% endblock %}
\ No newline at end of file
diff --git a/bricktracker/sql/set/metadata/owner/delete.sql b/bricktracker/sql/set/metadata/owner/delete.sql
new file mode 100644
index 00000000..e9df18d8
--- /dev/null
+++ b/bricktracker/sql/set/metadata/owner/delete.sql
@@ -0,0 +1,9 @@
+BEGIN TRANSACTION;
+
+ALTER TABLE "bricktracker_set_owners"
+DROP COLUMN "owner_{{ id }}";
+
+DELETE FROM "bricktracker_metadata_owners"
+WHERE "bricktracker_metadata_owners"."id" IS NOT DISTINCT FROM '{{ id }}';
+
+COMMIT;
\ No newline at end of file
diff --git a/bricktracker/sql/set/metadata/owner/insert.sql b/bricktracker/sql/set/metadata/owner/insert.sql
new file mode 100644
index 00000000..cc54a2a5
--- /dev/null
+++ b/bricktracker/sql/set/metadata/owner/insert.sql
@@ -0,0 +1,14 @@
+BEGIN TRANSACTION;
+
+ALTER TABLE "bricktracker_set_owners"
+ADD COLUMN "owner_{{ id }}" BOOLEAN NOT NULL DEFAULT 0;
+
+INSERT INTO "bricktracker_metadata_owners" (
+ "id",
+ "name"
+) VALUES (
+ '{{ id }}',
+ '{{ name }}'
+);
+
+COMMIT;
\ No newline at end of file
diff --git a/bricktracker/sql/set/metadata/owner/list.sql b/bricktracker/sql/set/metadata/owner/list.sql
new file mode 100644
index 00000000..e970cf9c
--- /dev/null
+++ b/bricktracker/sql/set/metadata/owner/list.sql
@@ -0,0 +1 @@
+{% extends 'set/metadata/owner/base.sql' %}
diff --git a/bricktracker/sql/set/metadata/owner/select.sql b/bricktracker/sql/set/metadata/owner/select.sql
new file mode 100644
index 00000000..82245656
--- /dev/null
+++ b/bricktracker/sql/set/metadata/owner/select.sql
@@ -0,0 +1,5 @@
+{% extends 'set/metadata/owner/base.sql' %}
+
+{% block where %}
+WHERE "bricktracker_metadata_owners"."id" IS NOT DISTINCT FROM :id
+{% endblock %}
\ No newline at end of file
diff --git a/bricktracker/sql/set/metadata/owner/update/field.sql b/bricktracker/sql/set/metadata/owner/update/field.sql
new file mode 100644
index 00000000..5f047a33
--- /dev/null
+++ b/bricktracker/sql/set/metadata/owner/update/field.sql
@@ -0,0 +1,3 @@
+UPDATE "bricktracker_metadata_owners"
+SET "{{field}}" = :value
+WHERE "bricktracker_metadata_owners"."id" IS NOT DISTINCT FROM :id
diff --git a/bricktracker/sql/set/metadata/owner/update/state.sql b/bricktracker/sql/set/metadata/owner/update/state.sql
new file mode 100644
index 00000000..24692075
--- /dev/null
+++ b/bricktracker/sql/set/metadata/owner/update/state.sql
@@ -0,0 +1,10 @@
+INSERT INTO "bricktracker_set_owners" (
+ "id",
+ "{{name}}"
+) VALUES (
+ :set_id,
+ :state
+)
+ON CONFLICT("id")
+DO UPDATE SET "{{name}}" = :state
+WHERE "bricktracker_set_owners"."id" IS NOT DISTINCT FROM :set_id
diff --git a/bricktracker/sql_counter.py b/bricktracker/sql_counter.py
index 28c03a32..30c83d6d 100644
--- a/bricktracker/sql_counter.py
+++ b/bricktracker/sql_counter.py
@@ -6,7 +6,8 @@ ALIASES: dict[str, Tuple[str, str]] = {
'bricktracker_minifigures': ('Bricktracker minifigures', 'group-line'),
'bricktracker_parts': ('Bricktracker parts', 'shapes-line'),
'bricktracker_set_checkboxes': ('Bricktracker set checkboxes (legacy)', 'checkbox-line'), # noqa: E501
- 'bricktracker_set_statuses': ('Bricktracker set statuses', 'checkbox-line'), # noqa: E501
+ 'bricktracker_set_owners': ('Bricktracker set owners', 'checkbox-line'), # noqa: E501
+ 'bricktracker_set_statuses': ('Bricktracker set statuses', 'user-line'), # noqa: E501
'bricktracker_set_storages': ('Bricktracker set storages', 'archive-2-line'), # noqa: E501
'bricktracker_sets': ('Bricktracker sets', 'hashtag'),
'bricktracker_wishes': ('Bricktracker wishes', 'gift-line'),
diff --git a/bricktracker/version.py b/bricktracker/version.py
index 11dd9c9a..996b4f61 100644
--- a/bricktracker/version.py
+++ b/bricktracker/version.py
@@ -1,4 +1,4 @@
from typing import Final
__version__: Final[str] = '1.2.0'
-__database_version__: Final[int] = 12
+__database_version__: Final[int] = 13
diff --git a/bricktracker/views/add.py b/bricktracker/views/add.py
index 44f3ddca..20607dcf 100644
--- a/bricktracker/views/add.py
+++ b/bricktracker/views/add.py
@@ -3,6 +3,8 @@ from flask_login import login_required
from ..configuration_list import BrickConfigurationList
from .exceptions import exception_handler
+from ..set_owner import BrickSetOwner
+from ..set_owner_list import BrickSetOwnerList
from ..socket import MESSAGES
add_page = Blueprint('add', __name__, url_prefix='/add')
@@ -17,6 +19,7 @@ def add() -> str:
return render_template(
'add.html',
+ brickset_owners=BrickSetOwnerList(BrickSetOwner).list(),
path=current_app.config['SOCKET_PATH'],
namespace=current_app.config['SOCKET_NAMESPACE'],
messages=MESSAGES
@@ -32,6 +35,7 @@ def bulk() -> str:
return render_template(
'add.html',
+ brickset_owners=BrickSetOwnerList(BrickSetOwner).list(),
path=current_app.config['SOCKET_PATH'],
namespace=current_app.config['SOCKET_NAMESPACE'],
messages=MESSAGES,
diff --git a/bricktracker/views/admin/admin.py b/bricktracker/views/admin/admin.py
index c18a74b2..36037d36 100644
--- a/bricktracker/views/admin/admin.py
+++ b/bricktracker/views/admin/admin.py
@@ -8,6 +8,8 @@ from ..exceptions import exception_handler
from ...instructions_list import BrickInstructionsList
from ...rebrickable_image import RebrickableImage
from ...retired_list import BrickRetiredList
+from ...set_owner import BrickSetOwner
+from ...set_owner_list import BrickSetOwnerList
from ...set_status import BrickSetStatus
from ...set_status_list import BrickSetStatusList
from ...sql_counter import BrickCounter
@@ -28,6 +30,7 @@ def admin() -> str:
database_exception: Exception | None = None
database_upgrade_needed: bool = False
database_version: int = -1
+ metadata_owners: list[BrickSetOwner] = []
metadata_statuses: list[BrickSetStatus] = []
nil_minifigure_name: str = ''
nil_minifigure_url: str = ''
@@ -41,6 +44,7 @@ def admin() -> str:
database_version = database.version
database_counters = BrickSQL().count_records()
+ metadata_owners = BrickSetOwnerList(BrickSetOwner).list()
metadata_statuses = BrickSetStatusList(BrickSetStatus).list(all=True)
except Exception as e:
database_exception = e
@@ -65,6 +69,7 @@ def admin() -> str:
open_image = request.args.get('open_image', None)
open_instructions = request.args.get('open_instructions', None)
open_logout = request.args.get('open_logout', None)
+ open_owner = request.args.get('open_owner', None)
open_retired = request.args.get('open_retired', None)
open_status = request.args.get('open_status', None)
open_theme = request.args.get('open_theme', None)
@@ -73,6 +78,7 @@ def admin() -> str:
open_image is None and
open_instructions is None and
open_logout is None and
+ open_owner is None and
open_retired is None and
open_status is None and
open_theme is None
@@ -81,13 +87,13 @@ def admin() -> str:
return render_template(
'admin.html',
configuration=BrickConfigurationList.list(),
- status_error=request.args.get('status_error'),
database_counters=database_counters,
database_error=request.args.get('database_error'),
database_exception=database_exception,
database_upgrade_needed=database_upgrade_needed,
database_version=database_version,
instructions=BrickInstructionsList(),
+ metadata_owners=metadata_owners,
metadata_statuses=metadata_statuses,
nil_minifigure_name=nil_minifigure_name,
nil_minifigure_url=nil_minifigure_url,
@@ -98,8 +104,11 @@ def admin() -> str:
open_image=open_image,
open_instructions=open_instructions,
open_logout=open_logout,
+ open_owner=open_owner,
open_retired=open_retired,
open_theme=open_theme,
+ owner_error=request.args.get('owner_error'),
+ status_error=request.args.get('status_error'),
retired=BrickRetiredList(),
theme=BrickThemeList(),
)
diff --git a/bricktracker/views/admin/owner.py b/bricktracker/views/admin/owner.py
new file mode 100644
index 00000000..bfa799e3
--- /dev/null
+++ b/bricktracker/views/admin/owner.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_owner import BrickSetOwner
+
+admin_owner_page = Blueprint(
+ 'admin_owner',
+ __name__,
+ url_prefix='/admin/owner'
+)
+
+
+# Add a metadata owner
+@admin_owner_page.route('/add', methods=['POST'])
+@login_required
+@exception_handler(
+ __file__,
+ post_redirect='admin.admin',
+ error_name='owner_error',
+ open_owner=True
+)
+def add() -> Response:
+ BrickSetOwner().from_form(request.form).insert()
+
+ reload()
+
+ return redirect(url_for('admin.admin', open_owner=True))
+
+
+# Delete the metadata owner
+@admin_owner_page.route('<id>/delete', methods=['GET'])
+@login_required
+@exception_handler(__file__)
+def delete(*, id: str) -> str:
+ return render_template(
+ 'admin.html',
+ delete_owner=True,
+ owner=BrickSetOwner().select_specific(id),
+ error=request.args.get('owner_error')
+ )
+
+
+# Actually delete the metadata owner
+@admin_owner_page.route('<id>/delete', methods=['POST'])
+@login_required
+@exception_handler(
+ __file__,
+ post_redirect='admin_owner.delete',
+ error_name='owner_error'
+)
+def do_delete(*, id: str) -> Response:
+ owner = BrickSetOwner().select_specific(id)
+ owner.delete()
+
+ reload()
+
+ return redirect(url_for('admin.admin', open_owner=True))
+
+
+# Rename the metadata owner
+@admin_owner_page.route('<id>/rename', methods=['POST'])
+@login_required
+@exception_handler(
+ __file__,
+ post_redirect='admin.admin',
+ error_name='owner_error',
+ open_owner=True
+)
+def rename(*, id: str) -> Response:
+ owner = BrickSetOwner().select_specific(id)
+ owner.from_form(request.form).rename()
+
+ reload()
+
+ return redirect(url_for('admin.admin', open_owner=True))
diff --git a/bricktracker/views/index.py b/bricktracker/views/index.py
index f8fe7b71..3d8a55ec 100644
--- a/bricktracker/views/index.py
+++ b/bricktracker/views/index.py
@@ -2,6 +2,8 @@ from flask import Blueprint, render_template
from .exceptions import exception_handler
from ..minifigure_list import BrickMinifigureList
+from ..set_owner import BrickSetOwner
+from ..set_owner_list import BrickSetOwnerList
from ..set_status import BrickSetStatus
from ..set_status_list import BrickSetStatusList
from ..set_list import BrickSetList
@@ -16,6 +18,7 @@ def index() -> str:
return render_template(
'index.html',
brickset_collection=BrickSetList().last(),
- minifigure_collection=BrickMinifigureList().last(),
+ brickset_owners=BrickSetOwnerList(BrickSetOwner).list(),
brickset_statuses=BrickSetStatusList(BrickSetStatus).list(),
+ minifigure_collection=BrickMinifigureList().last(),
)
diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py
index 9f1990cc..fd922ec4 100644
--- a/bricktracker/views/set.py
+++ b/bricktracker/views/set.py
@@ -16,6 +16,8 @@ from .exceptions import exception_handler
from ..minifigure import BrickMinifigure
from ..part import BrickPart
from ..set import BrickSet
+from ..set_owner import BrickSetOwner
+from ..set_owner_list import BrickSetOwnerList
from ..set_status import BrickSetStatus
from ..set_status_list import BrickSetStatusList
from ..set_list import BrickSetList
@@ -33,11 +35,25 @@ def list() -> str:
return render_template(
'sets.html',
collection=BrickSetList().all(),
+ brickset_owners=BrickSetOwnerList(BrickSetOwner).list(),
brickset_statuses=BrickSetStatusList(BrickSetStatus).list(),
)
-# Change the status of a status
+# Change the state of a owner
+@set_page.route('/<id>/owner/<metadata_id>', methods=['POST'])
+@login_required
+@exception_handler(__file__, json=True)
+def update_owner(*, id: str, metadata_id: str) -> Response:
+ brickset = BrickSet().select_light(id)
+ owner = BrickSetOwnerList(BrickSetOwner).get(metadata_id)
+
+ state = owner.update_set_state(brickset, json=request.json)
+
+ return jsonify({'value': state})
+
+
+# Change the state of a status
@set_page.route('/<id>/status/<metadata_id>', methods=['POST'])
@login_required
@exception_handler(__file__, json=True)
@@ -98,6 +114,7 @@ def details(*, id: str) -> str:
'set.html',
item=BrickSet().select_specific(id),
open_instructions=request.args.get('open_instructions'),
+ brickset_owners=BrickSetOwnerList(BrickSetOwner).list(),
brickset_statuses=BrickSetStatusList(BrickSetStatus).list(all=True),
)
diff --git a/static/scripts/socket/set.js b/static/scripts/socket/set.js
index 07d7cc7e..6459aa66 100644
--- a/static/scripts/socket/set.js
+++ b/static/scripts/socket/set.js
@@ -15,6 +15,7 @@ class BrickSetSocket extends BrickSocket {
this.html_button = document.getElementById(id);
this.html_input = document.getElementById(`${id}-set`);
this.html_no_confim = document.getElementById(`${id}-no-confirm`);
+ this.html_owners = document.getElementById(`${id}-owners`);
// Card elements
this.html_card = document.getElementById(`${id}-card`);
@@ -139,10 +140,21 @@ class BrickSetSocket extends BrickSocket {
this.set_list_last_set = set;
}
+ // Grab the owners
+ const owners = [];
+ if (this.html_owners) {
+ this.html_owners.querySelectorAll('input').forEach(input => {
+ if (input.checked) {
+ owners.push(input.value);
+ }
+ });
+ }
+
this.spinner(true);
this.socket.emit(this.messages.IMPORT_SET, {
set: (set !== undefined) ? set : this.html_input.value,
+ owners: owners,
refresh: this.refresh
});
} else {
@@ -247,6 +259,10 @@ class BrickSetSocket extends BrickSocket {
this.html_input.disabled = !enabled;
}
+ if (this.html_owners) {
+ this.html_owners.querySelectorAll('input').forEach(input => input.disabled = !enabled);
+ }
+
if (this.html_card_confirm) {
this.html_card_confirm.disabled = !enabled;
}
diff --git a/templates/add.html b/templates/add.html
index 12387398..59c50299 100644
--- a/templates/add.html
+++ b/templates/add.html
@@ -33,6 +33,19 @@
Add without confirmation
</label>
</div>
+ {% if brickset_owners | length %}
+ <h5 class="border-bottom mt-2">Owners</h5>
+ <div id="add-owners">
+ {% for owner in brickset_owners %}
+ {% with id=owner.as_dataset() %}
+ <div class="form-check">
+ <input class="form-check-input" type="checkbox" value="{{ owner.fields.id }}" id="{{ id }}" autocomplete="off">
+ <label class="form-check-label" for="{{ id }}">{{ owner.fields.name }}</label>
+ </div>
+ {% endwith %}
+ {% endfor %}
+ </div>
+ {% endif %}
<hr>
<div class="mb-3">
<p>
diff --git a/templates/admin.html b/templates/admin.html
index b2a54f3a..962730bc 100644
--- a/templates/admin.html
+++ b/templates/admin.html
@@ -12,10 +12,12 @@
<h5 class="mb-0"><i class="ri-settings-4-line"></i> Administration</h5>
</div>
<div class="accordion accordion-flush" id="admin">
- {% if delete_status %}
- {% include 'admin/status/delete.html' %}
- {% elif delete_database %}
+ {% if delete_database %}
{% include 'admin/database/delete.html' %}
+ {% elif delete_owner %}
+ {% include 'admin/owner/delete.html' %}
+ {% elif delete_status %}
+ {% include 'admin/status/delete.html' %}
{% elif drop_database %}
{% include 'admin/database/drop.html' %}
{% elif import_database %}
@@ -30,6 +32,7 @@
{% endif %}
{% include 'admin/theme.html' %}
{% include 'admin/retired.html' %}
+ {% include 'admin/owner.html' %}
{% include 'admin/status.html' %}
{% include 'admin/database.html' %}
{% include 'admin/configuration.html' %}
diff --git a/templates/admin/owner.html b/templates/admin/owner.html
new file mode 100644
index 00000000..7447a6da
--- /dev/null
+++ b/templates/admin/owner.html
@@ -0,0 +1,42 @@
+{% import 'macro/accordion.html' as accordion %}
+
+{{ accordion.header('Set owners', 'owner', 'admin', expanded=open_owner, icon='user-line', class='p-0') }}
+{% if owner_error %}<div class="alert alert-danger m-2" role="alert"><strong>Error:</strong> {{ owner_error }}.</div>{% endif %}
+<ul class="list-group list-group-flush">
+ {% if metadata_owners | length %}
+ {% for owner in metadata_owners %}
+ <li class="list-group-item">
+ <form action="{{ url_for('admin_owner.rename', id=owner.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-{{ owner.fields.id }}">Name</label>
+ <div class="input-group">
+ <div class="input-group-text">Name</div>
+ <input type="text" class="form-control" id="name-{{ owner.fields.id }}" name="name" value="{{ owner.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_owner.delete', id=owner.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"><i class="ri-error-warning-line"></i> No owner found.</li>
+ {% endif %}
+ <li class="list-group-item">
+ <form action="{{ url_for('admin_owner.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/owner/delete.html b/templates/admin/owner/delete.html
new file mode 100644
index 00000000..56821f35
--- /dev/null
+++ b/templates/admin/owner/delete.html
@@ -0,0 +1,19 @@
+{% import 'macro/accordion.html' as accordion %}
+
+{{ accordion.header('Set owners danger zone', 'owner-danger', 'admin', expanded=true, danger=true, class='text-end') }}
+<form action="{{ url_for('admin_owner.do_delete', id=owner.fields.id) }}" method="post">
+ {% if owner_error %}<div class="alert alert-danger text-start" role="alert"><strong>Error:</strong> {{ owner_error }}.</div>{% endif %}
+ <div class="alert alert-danger text-center" role="alert">You are about to <strong>delete a set owner</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="{{ owner.fields.name }}" disabled>
+ </div>
+ </div>
+ </div>
+ <hr class="border-bottom">
+ <a class="btn btn-danger" href="{{ url_for('admin.admin', open_owner=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 owner</strong></button>
+</form>
+{{ accordion.footer() }}
diff --git a/templates/macro/badge.html b/templates/macro/badge.html
index 3d8a5e26..2be95ca8 100644
--- a/templates/macro/badge.html
+++ b/templates/macro/badge.html
@@ -50,9 +50,18 @@
{{ badge(check=quantity, solo=solo, last=last, color='success', icon='close-line', collapsible='Quantity:', text=quantity, alt='Quantity') }}
{% endmacro %}
+{% macro owner(item, owner, solo=false, last=false) %}
+ {% if last %}
+ {% set tooltip=owner.fields.name %}
+ {% else %}
+ {% set text=owner.fields.name %}
+ {% endif %}
+ {{ badge(check=item.fields[owner.as_column()], solo=solo, last=last, color='light text-success-emphasis bg-success-subtle border border-success-subtle', icon='user-line', text=text, alt='Owner', tooltip=tooltip) }}
+{% endmacro %}
+
{% macro print(item, solo=false, last=false, header=false) %}
{% if item.fields.print %}
- {{ badge(url=item.url_for_print(), solo=solo, last=last, color='light border', icon='paint-brush-line', collapsible='Print') }}
+ {{ badge(url=item.url_for_print(), solo=solo, last=last, color='light border', icon='paint-brush-line', collapsible='Print') }}
{% endif %}
{% endmacro %}
diff --git a/templates/set/card.html b/templates/set/card.html
index c9afbdd4..9f2f83a7 100644
--- a/templates/set/card.html
+++ b/templates/set/card.html
@@ -9,6 +9,12 @@
data-year="{{ item.fields.year }}" data-theme="{{ item.theme.name | lower }}" data-minifigures="{{ item.fields.total_minifigures }}" data-has-minifigures="{{ (item.fields.total_minifigures > 0) | int }}"
data-has-missing="{{ (item.fields.total_missing > 0) | int }}" data-has-missing-instructions="{{ (not (item.instructions | length)) | int }}" data-missing="{{ item.fields.total_missing }}"
{% for status in brickset_statuses %}data-{{ status.as_dataset() }}="{{ item.fields[status.as_column()] }}" {% endfor %}
+ {% for owner in brickset_owners %}
+ {% with checked=item.fields[owner.as_column()] %}
+ data-{{ owner.as_dataset() }}="{{ checked }}"
+ {% if checked %} data-owner-{{ loop.index }}="{{ owner.fields.name | lower }}"{% endif %}
+ {% endwith %}
+ {% endfor %}
{% endif %}
>
{{ card.header(item, item.fields.name, solo=solo, identifier=item.fields.set) }}
@@ -19,6 +25,9 @@
{{ badge.parts(item.fields.number_of_parts, solo=solo, last=last) }}
{{ badge.total_minifigures(item.fields.total_minifigures, solo=solo, last=last) }}
{{ badge.total_missing(item.fields.total_missing, solo=solo, last=last) }}
+ {% for owner in brickset_owners %}
+ {{ badge.owner(item, owner, solo=solo, last=last) }}
+ {% endfor %}
{% if not last %}
{% if not solo %}
{{ badge.instructions(item, solo=solo, last=last) }}
diff --git a/templates/set/management.html b/templates/set/management.html
index 38074808..957db816 100644
--- a/templates/set/management.html
+++ b/templates/set/management.html
@@ -1,6 +1,20 @@
{% if g.login.is_authenticated() %}
+{{ accordion.header('Owners', 'owner', 'set-details', 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"><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('Management', 'management', 'set-details', expanded=true, icon='settings-4-line') }}
- <h5 class="border-bottom">Data</h5>
- <a href="{{ item.url_for_refresh() }}" class="btn btn-primary" role="button"><i class="ri-refresh-line"></i> Refresh the set data</a>
+ <h5 class="border-bottom">Data</h5>
+ <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() }}
{% endif %}
diff --git a/templates/sets.html b/templates/sets.html
index 97a7922c..b06c94dd 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-xl-inline"> Search</span></span>
- <input id="grid-search" data-search-exact="name,number,parts,theme,year" class="form-control form-control-sm" type="text" placeholder="Set name, set number, set theme or number of parts..." value="">
+ <input id="grid-search" data-search-exact="name,number,parts,theme,year" data-search-list="owner" class="form-control form-control-sm" type="text" placeholder="Set, name, number of parts, theme, year, owner" value="">
</div>
</div>
<div class="col-12">
@@ -75,6 +75,20 @@
</select>
</div>
</div>
+ <div class="col-12 flex-grow-1">
+ <label class="visually-hidden" for="grid-owner">Owner</label>
+ <div class="input-group">
+ <span class="input-group-text"><i class="ri-user-line"></i><span class="ms-1 d-none d-xl-inline"> Owner</span></span>
+ <select id="grid-owner" class="form-select"
+ data-filter="metadata"
+ autocomplete="off">
+ <option value="" selected>All</option>
+ {% for owner in brickset_owners %}
+ <option value="{{ owner.as_dataset() }}">{{ owner.fields.name }}</option>
+ {% endfor %}
+ </select>
+ </div>
+ </div>
</div>
<div class="row" data-grid="true" id="grid">
{% for item in collection %}