Set owners

This commit is contained in:
Gregoo 2025-01-31 16:34:52 +01:00
parent 2a12889695
commit 7047a28845
36 changed files with 418 additions and 18 deletions

View File

@ -13,11 +13,12 @@ from bricktracker.sql import close
from bricktracker.version import __version__ from bricktracker.version import __version__
from bricktracker.views.add import add_page from bricktracker.views.add import add_page
from bricktracker.views.admin.admin import admin_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.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.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.theme import admin_theme_page from bricktracker.views.admin.theme import admin_theme_page
from bricktracker.views.error import error_404 from bricktracker.views.error import error_404
from bricktracker.views.index import index_page from bricktracker.views.index import index_page
@ -78,11 +79,12 @@ def setup_app(app: Flask) -> None:
# Register admin routes # Register admin routes
app.register_blueprint(admin_page) app.register_blueprint(admin_page)
app.register_blueprint(admin_status_page)
app.register_blueprint(admin_database_page) app.register_blueprint(admin_database_page)
app.register_blueprint(admin_image_page) app.register_blueprint(admin_image_page)
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_status_page)
app.register_blueprint(admin_theme_page) app.register_blueprint(admin_theme_page)
# An helper to make global variables available to the # An helper to make global variables available to the

View File

@ -176,8 +176,16 @@ class BrickMetadata(BrickRecord):
return value return value
# Update the selected state of this metadata item for a set # Update the selected state of this metadata item for a set
def update_set_state(self, brickset: 'BrickSet', json: Any | None) -> Any: def update_set_state(
state: bool = json.get('value', False) # type: ignore 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 = self.sql_parameters()
parameters['set_id'] = brickset.fields.id parameters['set_id'] = brickset.fields.id

View File

@ -1,14 +1,15 @@
import logging import logging
from typing import Type from typing import Type, TypeVar
from .exceptions import NotFoundException 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_status import BrickSetStatus from .set_status import BrickSetStatus
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
T = BrickSetStatus T = TypeVar('T', 'BrickSetStatus', 'BrickSetOwner')
# Lego sets metadata list # Lego sets metadata list

View File

@ -8,12 +8,14 @@ if TYPE_CHECKING:
from .part import BrickPart from .part import BrickPart
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_status import BrickSetStatus from .set_status import BrickSetStatus
from .wish import BrickWish from .wish import BrickWish
T = TypeVar( T = TypeVar(
'T', 'T',
'BrickSet', 'BrickSet',
'BrickSetOwner',
'BrickSetStatus', 'BrickSetStatus',
'BrickPart', 'BrickPart',
'BrickMinifigure', 'BrickMinifigure',

View File

@ -1,5 +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 import BrickSetOwner
from .set_owner_list import BrickSetOwnerList
from .set_status import BrickSetStatus from .set_status import BrickSetStatus
from .set_status_list import BrickSetStatusList from .set_status_list import BrickSetStatusList
from .theme_list import BrickThemeList from .theme_list import BrickThemeList
@ -12,6 +14,9 @@ def reload() -> None:
# Reload the instructions # Reload the instructions
BrickInstructionsList(force=True) BrickInstructionsList(force=True)
# Reload the set owners
BrickSetOwnerList(BrickSetOwner, force=True)
# Reload the set statuses # Reload the set statuses
BrickSetStatusList(BrickSetStatus, force=True) BrickSetStatusList(BrickSetStatus, force=True)

View File

@ -9,6 +9,8 @@ from .exceptions import NotFoundException
from .minifigure_list import BrickMinifigureList 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 import BrickSetOwner
from .set_owner_list import BrickSetOwnerList
from .set_status import BrickSetStatus from .set_status import BrickSetStatus
from .set_status_list import BrickSetStatusList from .set_status_list import BrickSetStatusList
from .sql import BrickSQL from .sql import BrickSQL
@ -68,6 +70,13 @@ class BrickSet(RebrickableSet):
if not BrickMinifigureList.download(socket, self, refresh=refresh): if not BrickMinifigureList.download(socket, self, refresh=refresh):
return False 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 # Commit the transaction to the database
socket.auto_progress( socket.auto_progress(
message='Set {set}: writing to the database'.format( message='Set {set}: writing to the database'.format(
@ -162,6 +171,7 @@ class BrickSet(RebrickableSet):
# Load from database # Load from database
if not self.select( if not self.select(
owners=BrickSetOwnerList(BrickSetOwner).as_columns(),
statuses=BrickSetStatusList(BrickSetStatus).as_columns(all=True) statuses=BrickSetStatusList(BrickSetStatus).as_columns(all=True)
): ):
raise NotFoundException( raise NotFoundException(

View File

@ -3,6 +3,8 @@ from typing import Self
from flask import current_app from flask import current_app
from .record_list import BrickRecordList from .record_list import BrickRecordList
from .set_owner import BrickSetOwner
from .set_owner_list import BrickSetOwnerList
from .set_status import BrickSetStatus from .set_status import BrickSetStatus
from .set_status_list import BrickSetStatusList from .set_status_list import BrickSetStatusList
from .set import BrickSet from .set import BrickSet
@ -38,6 +40,7 @@ class BrickSetList(BrickRecordList[BrickSet]):
# Load the sets from the database # Load the sets from the database
for record in self.select( for record in self.select(
order=self.order, order=self.order,
owners=BrickSetOwnerList(BrickSetOwner).as_columns(),
statuses=BrickSetStatusList(BrickSetStatus).as_columns() statuses=BrickSetStatusList(BrickSetStatus).as_columns()
): ):
brickset = BrickSet(record=record) brickset = BrickSet(record=record)
@ -74,6 +77,7 @@ class BrickSetList(BrickRecordList[BrickSet]):
for record in self.select( for record in self.select(
order=order, order=order,
limit=limit, limit=limit,
owners=BrickSetOwnerList(BrickSetOwner).as_columns(),
statuses=BrickSetStatusList(BrickSetStatus).as_columns() statuses=BrickSetStatusList(BrickSetStatus).as_columns()
): ):
brickset = BrickSet(record=record) brickset = BrickSet(record=record)

16
bricktracker/set_owner.py Normal file
View File

@ -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'

View File

@ -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'

View File

@ -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;

View File

@ -9,6 +9,9 @@ SELECT
"rebrickable_sets"."number_of_parts", "rebrickable_sets"."number_of_parts",
"rebrickable_sets"."image", "rebrickable_sets"."image",
"rebrickable_sets"."url", "rebrickable_sets"."url",
{% block owners %}
{% if owners %}{{ owners }},{% endif %}
{% endblock %}
{% block statuses %} {% block statuses %}
{% if statuses %}{{ statuses }},{% endif %} {% if statuses %}{{ statuses }},{% endif %}
{% endblock %} {% endblock %}

View File

@ -13,6 +13,11 @@ IFNULL("minifigures_join"."total", 0) AS "total_minifigures"
{% endblock %} {% endblock %}
{% block join %} {% block join %}
{% if owners %}
LEFT JOIN "bricktracker_set_owners"
ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_owners"."id"
{% endif %}
{% if statuses %} {% if statuses %}
LEFT JOIN "bricktracker_set_statuses" LEFT JOIN "bricktracker_set_statuses"
ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_statuses"."id" ON "bricktracker_sets"."id" IS NOT DISTINCT FROM "bricktracker_set_statuses"."id"

View File

@ -6,6 +6,9 @@ BEGIN TRANSACTION;
DELETE FROM "bricktracker_sets" DELETE FROM "bricktracker_sets"
WHERE "bricktracker_sets"."id" IS NOT DISTINCT FROM '{{ id }}'; 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" DELETE FROM "bricktracker_set_statuses"
WHERE "bricktracker_set_statuses"."id" IS NOT DISTINCT FROM '{{ id }}'; WHERE "bricktracker_set_statuses"."id" IS NOT DISTINCT FROM '{{ id }}';

View File

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

View File

@ -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;

View File

@ -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;

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -6,7 +6,8 @@ ALIASES: dict[str, Tuple[str, str]] = {
'bricktracker_minifigures': ('Bricktracker minifigures', 'group-line'), 'bricktracker_minifigures': ('Bricktracker minifigures', 'group-line'),
'bricktracker_parts': ('Bricktracker parts', 'shapes-line'), 'bricktracker_parts': ('Bricktracker parts', 'shapes-line'),
'bricktracker_set_checkboxes': ('Bricktracker set checkboxes (legacy)', 'checkbox-line'), # noqa: E501 '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_set_storages': ('Bricktracker set storages', 'archive-2-line'), # noqa: E501
'bricktracker_sets': ('Bricktracker sets', 'hashtag'), 'bricktracker_sets': ('Bricktracker sets', 'hashtag'),
'bricktracker_wishes': ('Bricktracker wishes', 'gift-line'), 'bricktracker_wishes': ('Bricktracker wishes', 'gift-line'),

View File

@ -1,4 +1,4 @@
from typing import Final from typing import Final
__version__: Final[str] = '1.2.0' __version__: Final[str] = '1.2.0'
__database_version__: Final[int] = 12 __database_version__: Final[int] = 13

View File

@ -3,6 +3,8 @@ from flask_login import login_required
from ..configuration_list import BrickConfigurationList from ..configuration_list import BrickConfigurationList
from .exceptions import exception_handler from .exceptions import exception_handler
from ..set_owner import BrickSetOwner
from ..set_owner_list import BrickSetOwnerList
from ..socket import MESSAGES from ..socket import MESSAGES
add_page = Blueprint('add', __name__, url_prefix='/add') add_page = Blueprint('add', __name__, url_prefix='/add')
@ -17,6 +19,7 @@ def add() -> str:
return render_template( return render_template(
'add.html', 'add.html',
brickset_owners=BrickSetOwnerList(BrickSetOwner).list(),
path=current_app.config['SOCKET_PATH'], path=current_app.config['SOCKET_PATH'],
namespace=current_app.config['SOCKET_NAMESPACE'], namespace=current_app.config['SOCKET_NAMESPACE'],
messages=MESSAGES messages=MESSAGES
@ -32,6 +35,7 @@ def bulk() -> str:
return render_template( return render_template(
'add.html', 'add.html',
brickset_owners=BrickSetOwnerList(BrickSetOwner).list(),
path=current_app.config['SOCKET_PATH'], path=current_app.config['SOCKET_PATH'],
namespace=current_app.config['SOCKET_NAMESPACE'], namespace=current_app.config['SOCKET_NAMESPACE'],
messages=MESSAGES, messages=MESSAGES,

View File

@ -8,6 +8,8 @@ from ..exceptions import exception_handler
from ...instructions_list import BrickInstructionsList from ...instructions_list import BrickInstructionsList
from ...rebrickable_image import RebrickableImage from ...rebrickable_image import RebrickableImage
from ...retired_list import BrickRetiredList from ...retired_list import BrickRetiredList
from ...set_owner import BrickSetOwner
from ...set_owner_list import BrickSetOwnerList
from ...set_status import BrickSetStatus from ...set_status import BrickSetStatus
from ...set_status_list import BrickSetStatusList from ...set_status_list import BrickSetStatusList
from ...sql_counter import BrickCounter from ...sql_counter import BrickCounter
@ -28,6 +30,7 @@ def admin() -> str:
database_exception: Exception | None = None database_exception: Exception | None = None
database_upgrade_needed: bool = False database_upgrade_needed: bool = False
database_version: int = -1 database_version: int = -1
metadata_owners: list[BrickSetOwner] = []
metadata_statuses: list[BrickSetStatus] = [] metadata_statuses: list[BrickSetStatus] = []
nil_minifigure_name: str = '' nil_minifigure_name: str = ''
nil_minifigure_url: str = '' nil_minifigure_url: str = ''
@ -41,6 +44,7 @@ def admin() -> str:
database_version = database.version database_version = database.version
database_counters = BrickSQL().count_records() database_counters = BrickSQL().count_records()
metadata_owners = BrickSetOwnerList(BrickSetOwner).list()
metadata_statuses = BrickSetStatusList(BrickSetStatus).list(all=True) metadata_statuses = BrickSetStatusList(BrickSetStatus).list(all=True)
except Exception as e: except Exception as e:
database_exception = e database_exception = e
@ -65,6 +69,7 @@ def admin() -> str:
open_image = request.args.get('open_image', None) open_image = request.args.get('open_image', None)
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_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_theme = request.args.get('open_theme', None) open_theme = request.args.get('open_theme', None)
@ -73,6 +78,7 @@ def admin() -> str:
open_image is None and open_image is None and
open_instructions is None and open_instructions is None and
open_logout is None and open_logout is None and
open_owner is None and
open_retired is None and open_retired is None and
open_status is None and open_status is None and
open_theme is None open_theme is None
@ -81,13 +87,13 @@ def admin() -> str:
return render_template( return render_template(
'admin.html', 'admin.html',
configuration=BrickConfigurationList.list(), configuration=BrickConfigurationList.list(),
status_error=request.args.get('status_error'),
database_counters=database_counters, database_counters=database_counters,
database_error=request.args.get('database_error'), database_error=request.args.get('database_error'),
database_exception=database_exception, database_exception=database_exception,
database_upgrade_needed=database_upgrade_needed, database_upgrade_needed=database_upgrade_needed,
database_version=database_version, database_version=database_version,
instructions=BrickInstructionsList(), instructions=BrickInstructionsList(),
metadata_owners=metadata_owners,
metadata_statuses=metadata_statuses, metadata_statuses=metadata_statuses,
nil_minifigure_name=nil_minifigure_name, nil_minifigure_name=nil_minifigure_name,
nil_minifigure_url=nil_minifigure_url, nil_minifigure_url=nil_minifigure_url,
@ -98,8 +104,11 @@ def admin() -> str:
open_image=open_image, open_image=open_image,
open_instructions=open_instructions, open_instructions=open_instructions,
open_logout=open_logout, open_logout=open_logout,
open_owner=open_owner,
open_retired=open_retired, open_retired=open_retired,
open_theme=open_theme, open_theme=open_theme,
owner_error=request.args.get('owner_error'),
status_error=request.args.get('status_error'),
retired=BrickRetiredList(), retired=BrickRetiredList(),
theme=BrickThemeList(), theme=BrickThemeList(),
) )

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_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))

View File

@ -2,6 +2,8 @@ from flask import Blueprint, render_template
from .exceptions import exception_handler from .exceptions import exception_handler
from ..minifigure_list import BrickMinifigureList from ..minifigure_list import BrickMinifigureList
from ..set_owner import BrickSetOwner
from ..set_owner_list import BrickSetOwnerList
from ..set_status import BrickSetStatus from ..set_status import BrickSetStatus
from ..set_status_list import BrickSetStatusList from ..set_status_list import BrickSetStatusList
from ..set_list import BrickSetList from ..set_list import BrickSetList
@ -16,6 +18,7 @@ def index() -> str:
return render_template( return render_template(
'index.html', 'index.html',
brickset_collection=BrickSetList().last(), brickset_collection=BrickSetList().last(),
minifigure_collection=BrickMinifigureList().last(), brickset_owners=BrickSetOwnerList(BrickSetOwner).list(),
brickset_statuses=BrickSetStatusList(BrickSetStatus).list(), brickset_statuses=BrickSetStatusList(BrickSetStatus).list(),
minifigure_collection=BrickMinifigureList().last(),
) )

View File

@ -16,6 +16,8 @@ from .exceptions import exception_handler
from ..minifigure import BrickMinifigure from ..minifigure import BrickMinifigure
from ..part import BrickPart from ..part import BrickPart
from ..set import BrickSet from ..set import BrickSet
from ..set_owner import BrickSetOwner
from ..set_owner_list import BrickSetOwnerList
from ..set_status import BrickSetStatus from ..set_status import BrickSetStatus
from ..set_status_list import BrickSetStatusList from ..set_status_list import BrickSetStatusList
from ..set_list import BrickSetList from ..set_list import BrickSetList
@ -33,11 +35,25 @@ def list() -> str:
return render_template( return render_template(
'sets.html', 'sets.html',
collection=BrickSetList().all(), collection=BrickSetList().all(),
brickset_owners=BrickSetOwnerList(BrickSetOwner).list(),
brickset_statuses=BrickSetStatusList(BrickSetStatus).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']) @set_page.route('/<id>/status/<metadata_id>', methods=['POST'])
@login_required @login_required
@exception_handler(__file__, json=True) @exception_handler(__file__, json=True)
@ -98,6 +114,7 @@ def details(*, id: str) -> str:
'set.html', 'set.html',
item=BrickSet().select_specific(id), item=BrickSet().select_specific(id),
open_instructions=request.args.get('open_instructions'), open_instructions=request.args.get('open_instructions'),
brickset_owners=BrickSetOwnerList(BrickSetOwner).list(),
brickset_statuses=BrickSetStatusList(BrickSetStatus).list(all=True), brickset_statuses=BrickSetStatusList(BrickSetStatus).list(all=True),
) )

View File

@ -15,6 +15,7 @@ class BrickSetSocket extends BrickSocket {
this.html_button = document.getElementById(id); this.html_button = document.getElementById(id);
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`);
// Card elements // Card elements
this.html_card = document.getElementById(`${id}-card`); this.html_card = document.getElementById(`${id}-card`);
@ -139,10 +140,21 @@ class BrickSetSocket extends BrickSocket {
this.set_list_last_set = set; 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.spinner(true);
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,
refresh: this.refresh refresh: this.refresh
}); });
} else { } else {
@ -247,6 +259,10 @@ class BrickSetSocket extends BrickSocket {
this.html_input.disabled = !enabled; this.html_input.disabled = !enabled;
} }
if (this.html_owners) {
this.html_owners.querySelectorAll('input').forEach(input => input.disabled = !enabled);
}
if (this.html_card_confirm) { if (this.html_card_confirm) {
this.html_card_confirm.disabled = !enabled; this.html_card_confirm.disabled = !enabled;
} }

View File

@ -33,6 +33,19 @@
Add without confirmation Add without confirmation
</label> </label>
</div> </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> <hr>
<div class="mb-3"> <div class="mb-3">
<p> <p>

View File

@ -12,10 +12,12 @@
<h5 class="mb-0"><i class="ri-settings-4-line"></i> Administration</h5> <h5 class="mb-0"><i class="ri-settings-4-line"></i> Administration</h5>
</div> </div>
<div class="accordion accordion-flush" id="admin"> <div class="accordion accordion-flush" id="admin">
{% if delete_status %} {% if delete_database %}
{% include 'admin/status/delete.html' %}
{% elif delete_database %}
{% include 'admin/database/delete.html' %} {% include 'admin/database/delete.html' %}
{% elif delete_owner %}
{% include 'admin/owner/delete.html' %}
{% elif delete_status %}
{% include 'admin/status/delete.html' %}
{% elif drop_database %} {% elif drop_database %}
{% include 'admin/database/drop.html' %} {% include 'admin/database/drop.html' %}
{% elif import_database %} {% elif import_database %}
@ -30,6 +32,7 @@
{% endif %} {% endif %}
{% include 'admin/theme.html' %} {% include 'admin/theme.html' %}
{% include 'admin/retired.html' %} {% include 'admin/retired.html' %}
{% include 'admin/owner.html' %}
{% include 'admin/status.html' %} {% include 'admin/status.html' %}
{% 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 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() }}

View File

@ -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() }}

View File

@ -50,6 +50,15 @@
{{ badge(check=quantity, solo=solo, last=last, color='success', icon='close-line', collapsible='Quantity:', text=quantity, alt='Quantity') }} {{ badge(check=quantity, solo=solo, last=last, color='success', icon='close-line', collapsible='Quantity:', text=quantity, alt='Quantity') }}
{% endmacro %} {% 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) %} {% macro print(item, solo=false, last=false, header=false) %}
{% if item.fields.print %} {% 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') }}

View File

@ -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-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 }}" 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 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 %} {% endif %}
> >
{{ card.header(item, item.fields.name, solo=solo, identifier=item.fields.set) }} {{ 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.parts(item.fields.number_of_parts, solo=solo, last=last) }}
{{ badge.total_minifigures(item.fields.total_minifigures, 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) }} {{ 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 last %}
{% if not solo %} {% if not solo %}
{{ badge.instructions(item, solo=solo, last=last) }} {{ badge.instructions(item, solo=solo, last=last) }}

View File

@ -1,4 +1,18 @@
{% if g.login.is_authenticated() %} {% 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') }} {{ accordion.header('Management', 'management', 'set-details', expanded=true, icon='settings-4-line') }}
<h5 class="border-bottom">Data</h5> <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> <a href="{{ item.url_for_refresh() }}" class="btn btn-primary" role="button"><i class="ri-refresh-line"></i> Refresh the set data</a>

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-xl-inline"> Search</span></span> <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> </div>
<div class="col-12"> <div class="col-12">
@ -75,6 +75,20 @@
</select> </select>
</div> </div>
</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>
<div class="row" data-grid="true" id="grid"> <div class="row" data-grid="true" id="grid">
{% for item in collection %} {% for item in collection %}