Set storage

This commit is contained in:
Gregoo 2025-02-03 16:46:45 +01:00
parent ec7fab2a7a
commit 9aff7e622d
32 changed files with 538 additions and 88 deletions

View File

@ -34,6 +34,7 @@ Parts
- Fix missing @login_required for set deletion - Fix missing @login_required for set deletion
- Ownership - Ownership
- Tags - Tags
- Storage
- Socket - Socket
- Add decorator for rebrickable, authenticated and threaded socket actions - Add decorator for rebrickable, authenticated and threaded socket actions
@ -76,6 +77,7 @@ Parts
- Ownership - Ownership
- Tags - Tags
- Refresh - Refresh
- Storage
- Sets grid - Sets grid
- Collapsible controls depending on screen size - Collapsible controls depending on screen size

View File

@ -19,6 +19,7 @@ from bricktracker.views.admin.instructions import admin_instructions_page
from bricktracker.views.admin.owner import admin_owner_page from bricktracker.views.admin.owner import admin_owner_page
from bricktracker.views.admin.retired import admin_retired_page from bricktracker.views.admin.retired import admin_retired_page
from bricktracker.views.admin.status import admin_status_page from bricktracker.views.admin.status import admin_status_page
from bricktracker.views.admin.storage import admin_storage_page
from bricktracker.views.admin.tag import admin_tag_page from bricktracker.views.admin.tag import admin_tag_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
@ -86,6 +87,7 @@ def setup_app(app: Flask) -> None:
app.register_blueprint(admin_retired_page) app.register_blueprint(admin_retired_page)
app.register_blueprint(admin_owner_page) app.register_blueprint(admin_owner_page)
app.register_blueprint(admin_status_page) app.register_blueprint(admin_status_page)
app.register_blueprint(admin_storage_page)
app.register_blueprint(admin_tag_page) app.register_blueprint(admin_tag_page)
app.register_blueprint(admin_theme_page) app.register_blueprint(admin_theme_page)

View File

@ -36,6 +36,9 @@ class BrickMetadata(BrickRecord):
): ):
super().__init__() super().__init__()
# Defined an empty ID
self.fields.id = None
# Ingest the record if it has one # Ingest the record if it has one
if record is not None: if record is not None:
self.ingest(record) self.ingest(record)
@ -129,8 +132,8 @@ class BrickMetadata(BrickRecord):
json: Any | None = None, json: Any | None = None,
value: Any | None = None value: Any | None = None
) -> Any: ) -> Any:
if value is None: if value is None and json is not None:
value = json.get('value', None) # type: ignore value = json.get('value', None)
if value is None: if value is None:
raise ErrorException('"{field}" of a {kind} cannot be set to an empty value'.format( # noqa: E501 raise ErrorException('"{field}" of a {kind} cannot be set to an empty value'.format( # noqa: E501
@ -180,16 +183,15 @@ class BrickMetadata(BrickRecord):
/, /,
*, *,
json: Any | None = None, json: Any | None = None,
state: bool | None = None, state: Any | None = None
) -> Any: ) -> Any:
if state is None: if state is None and json is not None:
state = json.get('value', False) # type: ignore state = json.get('value', False)
parameters = self.sql_parameters() parameters = self.sql_parameters()
parameters['set_id'] = brickset.fields.id parameters['set_id'] = brickset.fields.id
parameters['state'] = state parameters['state'] = state
# Update the status
rows, _ = BrickSQL().execute_and_commit( rows, _ = BrickSQL().execute_and_commit(
self.update_set_state_query, self.update_set_state_query,
parameters=parameters, parameters=parameters,
@ -205,7 +207,53 @@ class BrickMetadata(BrickRecord):
)) ))
# Info # Info
logger.info('{kind} "{name}" state change to "{state}" for set {set} ({id})'.format( # noqa: E501 logger.info('{kind} "{name}" state changed to "{state}" for set {set} ({id})'.format( # noqa: E501
kind=self.kind,
name=self.fields.name,
state=state,
set=brickset.fields.set,
id=brickset.fields.id,
))
return state
# Update the selected value of this metadata item for a set
def update_set_value(
self,
brickset: 'BrickSet',
/,
*,
json: Any | None = None,
state: Any | None = None,
) -> Any:
if state is None and json is not None:
state = json.get('value', '')
if state == '':
state = None
parameters = self.sql_parameters()
parameters['set_id'] = brickset.fields.id
parameters['state'] = state
rows, _ = BrickSQL().execute_and_commit(
self.update_set_state_query,
parameters=parameters,
)
# Update the status
if state is None and not hasattr(self.fields, 'name'):
self.fields.name = 'None'
if rows != 1:
raise DatabaseException('Could not update the {kind} value for set {set} ({id})'.format( # noqa: E501
kind=self.kind,
set=brickset.fields.set,
id=brickset.fields.id,
))
# Info
logger.info('{kind} value changed to "{name}" ({state}) for set {set} ({id})'.format( # noqa: E501
kind=self.kind, kind=self.kind,
name=self.fields.name, name=self.fields.name,
state=state, state=state,

View File

@ -1,16 +1,19 @@
import logging import logging
from typing import Type, TypeVar from typing import List, overload, Self, Type, TypeVar
from flask import url_for
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_owner import BrickSetOwner
from .set_status import BrickSetStatus from .set_status import BrickSetStatus
from .set_storage import BrickSetStorage
from .set_tag import BrickSetTag from .set_tag import BrickSetTag
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
T = TypeVar('T', BrickSetStatus, BrickSetOwner, BrickSetTag) T = TypeVar('T', BrickSetOwner, BrickSetStatus, BrickSetStorage, BrickSetTag)
# Lego sets metadata list # Lego sets metadata list
@ -25,8 +28,29 @@ class BrickMetadataList(BrickRecordList[T]):
# Queries # Queries
select_query: str select_query: str
def __init__(self, model: Type[T], /, *, force: bool = False): # Set state endpoint
# Load statuses only if there is none already loaded set_state_endpoint: str
def __init__(
self,
model: Type[T],
/,
*,
force: bool = False,
records: list[T] | None = None
):
self.model = model
# Records override (masking the class variables with instance ones)
if records is not None:
self.records = []
self.mapping = {}
for metadata in records:
self.records.append(metadata)
self.mapping[metadata.fields.id] = metadata
else:
# Load metadata only if there is none already loaded
records = getattr(self, 'records', None) records = getattr(self, 'records', None)
if records is None or force: if records is None or force:
@ -40,40 +64,83 @@ class BrickMetadataList(BrickRecordList[T]):
self.__class__.records = [] self.__class__.records = []
self.__class__.mapping = {} self.__class__.mapping = {}
# Load the statuses from the database # Load the metadata from the database
for record in self.select(): for record in self.select():
status = model(record=record) metadata = model(record=record)
self.__class__.records.append(status) self.__class__.records.append(metadata)
self.__class__.mapping[status.fields.id] = status self.__class__.mapping[metadata.fields.id] = metadata
# Return the items as columns for a select # HTML prefix name
def as_columns(self, /, **kwargs) -> str: def as_prefix(self, /) -> str:
return ', '.join([ return self.kind.replace(' ', '-')
'"{table}"."{column}"'.format(
table=self.table,
column=record.as_column(),
)
for record
in self.filter(**kwargs)
])
# Filter the list of records (this one does nothing) # Filter the list of records (this one does nothing)
def filter(self) -> list[T]: def filter(self) -> list[T]:
return self.records return self.records
# Return the items as columns for a select
@classmethod
def as_columns(cls, /, **kwargs) -> str:
new = cls.new()
return ', '.join([
'"{table}"."{column}"'.format(
table=cls.table,
column=record.as_column(),
)
for record
in new.filter(**kwargs)
])
# Grab a specific status # Grab a specific status
def get(self, id: str, /) -> T: @classmethod
if id not in self.mapping: def get(cls, id: str, /, *, allow_none: bool = False) -> T:
new = cls.new()
if allow_none and id == '':
return new.model()
if id not in new.mapping:
raise NotFoundException( raise NotFoundException(
'{kind} with ID {id} was not found in the database'.format( '{kind} with ID {id} was not found in the database'.format(
kind=self.kind.capitalize(), kind=new.kind.capitalize(),
id=id, id=id,
), ),
) )
return self.mapping[id] return new.mapping[id]
# Get the list of statuses depending on the context # Get the list of statuses depending on the context
def list(self, /, **kwargs) -> list[T]: @overload
return self.filter(**kwargs) @classmethod
def list(cls, /, **kwargs) -> List[T]: ...
@overload
@classmethod
def list(cls, /, as_class: bool = False, **kwargs) -> Self: ...
@classmethod
def list(cls, /, as_class: bool = False, **kwargs) -> List[T] | Self:
new = cls.new()
list = new.filter(**kwargs)
if as_class:
print(list)
# Return a copy of the metadata list with overriden records
return cls(new.model, records=list)
else:
return list
# Instantiate the list with the proper class
@classmethod
def new(cls, /, *, force: bool = False) -> Self:
raise Exception('new() is not implemented for BrickMetadataList')
# URL to change the selected state of this metadata item for a set
@classmethod
def url_for_set_state(cls, id: str, /) -> str:
return url_for(
cls.set_state_endpoint,
id=id,
)

View File

@ -10,17 +10,19 @@ if TYPE_CHECKING:
from .set import BrickSet from .set import BrickSet
from .set_owner import BrickSetOwner from .set_owner import BrickSetOwner
from .set_status import BrickSetStatus from .set_status import BrickSetStatus
from .set_storage import BrickSetStorage
from .set_tag import BrickSetTag from .set_tag import BrickSetTag
from .wish import BrickWish from .wish import BrickWish
T = TypeVar( T = TypeVar(
'T', 'T',
'BrickMinifigure',
'BrickPart',
'BrickSet', 'BrickSet',
'BrickSetOwner', 'BrickSetOwner',
'BrickSetStatus', 'BrickSetStatus',
'BrickSetStorage',
'BrickSetTag', 'BrickSetTag',
'BrickPart',
'BrickMinifigure',
'BrickWish', 'BrickWish',
'RebrickableSet' 'RebrickableSet'
) )

View File

@ -2,6 +2,7 @@ from .instructions_list import BrickInstructionsList
from .retired_list import BrickRetiredList from .retired_list import BrickRetiredList
from .set_owner_list import BrickSetOwnerList from .set_owner_list import BrickSetOwnerList
from .set_status_list import BrickSetStatusList from .set_status_list import BrickSetStatusList
from .set_storage_list import BrickSetStorageList
from .set_tag_list import BrickSetTagList from .set_tag_list import BrickSetTagList
from .theme_list import BrickThemeList from .theme_list import BrickThemeList
@ -19,6 +20,9 @@ def reload() -> None:
# Reload the set statuses # Reload the set statuses
BrickSetStatusList.new(force=True) BrickSetStatusList.new(force=True)
# Reload the set storages
BrickSetStorageList.new(force=True)
# Reload the set tags # Reload the set tags
BrickSetTagList.new(force=True) BrickSetTagList.new(force=True)

View File

@ -11,6 +11,7 @@ from .part_list import BrickPartList
from .rebrickable_set import RebrickableSet from .rebrickable_set import RebrickableSet
from .set_owner_list import BrickSetOwnerList from .set_owner_list import BrickSetOwnerList
from .set_status_list import BrickSetStatusList from .set_status_list import BrickSetStatusList
from .set_storage_list import BrickSetStorageList
from .set_tag_list import BrickSetTagList from .set_tag_list import BrickSetTagList
from .sql import BrickSQL from .sql import BrickSQL
if TYPE_CHECKING: if TYPE_CHECKING:
@ -55,9 +56,30 @@ class BrickSet(RebrickableSet):
self.fields.id = str(uuid4()) self.fields.id = str(uuid4())
if not refresh: if not refresh:
# Save the storage
storage = BrickSetStorageList.get(
data.get('storage', ''),
allow_none=True
)
self.fields.storage = storage.fields.id
# Insert into database # Insert into database
self.insert(commit=False) self.insert(commit=False)
# Save the owners
owners: list[str] = list(data.get('owners', []))
for id in owners:
owner = BrickSetOwnerList.get(id)
owner.update_set_state(self, state=True)
# Save the tags
tags: list[str] = list(data.get('tags', []))
for id in tags:
tag = BrickSetTagList.get(id)
tag.update_set_state(self, state=True)
# Insert the rebrickable set into database # Insert the rebrickable set into database
self.insert_rebrickable() self.insert_rebrickable()
@ -69,20 +91,6 @@ 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)
# Save the tags
tags: list[str] = list(data.get('tags', []))
for id in tags:
tag = BrickSetTagList(BrickSetTag).get(id)
tag.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(
@ -166,9 +174,9 @@ class BrickSet(RebrickableSet):
# Load from database # Load from database
if not self.select( if not self.select(
owners=BrickSetOwnerList.new().as_columns(), owners=BrickSetOwnerList.as_columns(),
statuses=BrickSetStatusList.new().as_columns(all=True), statuses=BrickSetStatusList.as_columns(all=True),
tags=BrickSetTagList.new().as_columns(), tags=BrickSetTagList.as_columns(),
): ):
raise NotFoundException( raise NotFoundException(
'Set with ID {id} was not found in the database'.format( 'Set with ID {id} was not found in the database'.format(

View File

@ -41,9 +41,9 @@ 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.new().as_columns(), owners=BrickSetOwnerList.as_columns(),
statuses=BrickSetStatusList.new().as_columns(), statuses=BrickSetStatusList.as_columns(),
tags=BrickSetTagList.new().as_columns(), tags=BrickSetTagList.as_columns(),
): ):
brickset = BrickSet(record=record) brickset = BrickSet(record=record)
@ -112,9 +112,9 @@ class BrickSetList(BrickRecordList[BrickSet]):
for record in self.select( for record in self.select(
order=order, order=order,
limit=limit, limit=limit,
owners=BrickSetOwnerList.new().as_columns(), owners=BrickSetOwnerList.as_columns(),
statuses=BrickSetStatusList.new().as_columns(), statuses=BrickSetStatusList.as_columns(),
tags=BrickSetTagList.new().as_columns(), tags=BrickSetTagList.as_columns(),
): ):
brickset = BrickSet(record=record) brickset = BrickSet(record=record)

View File

@ -0,0 +1,13 @@
from .metadata import BrickMetadata
# Lego set storage metadata
class BrickSetStorage(BrickMetadata):
kind: str = 'storage'
# Queries
delete_query: str = 'set/metadata/storage/delete'
insert_query: str = 'set/metadata/storage/insert'
select_query: str = 'set/metadata/storage/select'
update_field_query: str = 'set/metadata/storage/update/field'
update_set_state_query: str = 'set/metadata/storage/update/state'

View File

@ -0,0 +1,23 @@
import logging
from typing import Self
from .metadata_list import BrickMetadataList
from .set_storage import BrickSetStorage
logger = logging.getLogger(__name__)
# Lego sets storage list
class BrickSetStorageList(BrickMetadataList[BrickSetStorage]):
kind: str = 'set storages'
# Queries
select_query = 'set/metadata/storage/list'
# Set state endpoint
set_state_endpoint: str = 'set.update_storage'
# Instantiate the list with the proper class
@classmethod
def new(cls, /, *, force: bool = False) -> Self:
return cls(BrickSetStorage, force=force)

View File

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

View File

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

View File

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

View File

@ -0,0 +1,6 @@
BEGIN TRANSACTION;
DELETE FROM "bricktracker_metadata_storages"
WHERE "bricktracker_metadata_statuses"."id" IS NOT DISTINCT FROM '{{ id }}';
COMMIT;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ 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_list import BrickSetOwnerList from ..set_owner_list import BrickSetOwnerList
from ..set_storage_list import BrickSetStorageList
from ..set_tag_list import BrickSetTagList from ..set_tag_list import BrickSetTagList
from ..socket import MESSAGES from ..socket import MESSAGES
@ -19,8 +20,9 @@ def add() -> str:
return render_template( return render_template(
'add.html', 'add.html',
brickset_owners=BrickSetOwnerList.new().list(), brickset_owners=BrickSetOwnerList.list(),
brickset_tags=BrickSetTagList.new().list(), brickset_storages=BrickSetStorageList.list(),
brickset_tags=BrickSetTagList.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
@ -36,8 +38,9 @@ def bulk() -> str:
return render_template( return render_template(
'add.html', 'add.html',
brickset_owners=BrickSetOwnerList.new().list(), brickset_owners=BrickSetOwnerList.list(),
brickset_tags=BrickSetTagList.new().list(), brickset_storages=BrickSetStorageList.list(),
brickset_tags=BrickSetTagList.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

@ -10,6 +10,8 @@ from ...rebrickable_image import RebrickableImage
from ...retired_list import BrickRetiredList from ...retired_list import BrickRetiredList
from ...set_owner import BrickSetOwner from ...set_owner import BrickSetOwner
from ...set_owner_list import BrickSetOwnerList from ...set_owner_list import BrickSetOwnerList
from ...set_storage import BrickSetStorage
from ...set_storage_list import BrickSetStorageList
from ...set_status import BrickSetStatus from ...set_status import BrickSetStatus
from ...set_status_list import BrickSetStatusList from ...set_status_list import BrickSetStatusList
from ...set_tag import BrickSetTag from ...set_tag import BrickSetTag
@ -34,6 +36,7 @@ def admin() -> str:
database_version: int = -1 database_version: int = -1
metadata_owners: list[BrickSetOwner] = [] metadata_owners: list[BrickSetOwner] = []
metadata_statuses: list[BrickSetStatus] = [] metadata_statuses: list[BrickSetStatus] = []
metadata_storages: list[BrickSetStorage] = []
metadata_tags: list[BrickSetTag] = [] metadata_tags: list[BrickSetTag] = []
nil_minifigure_name: str = '' nil_minifigure_name: str = ''
nil_minifigure_url: str = '' nil_minifigure_url: str = ''
@ -47,9 +50,10 @@ def admin() -> str:
database_version = database.version database_version = database.version
database_counters = BrickSQL().count_records() database_counters = BrickSQL().count_records()
metadata_owners = BrickSetOwnerList.new().list() metadata_owners = BrickSetOwnerList.list()
metadata_statuses = BrickSetStatusList.new().list(all=True) metadata_statuses = BrickSetStatusList.list(all=True)
metadata_tags = BrickSetTagList.new().list() metadata_storages = BrickSetStorageList.list()
metadata_tags = BrickSetTagList.list()
except Exception as e: except Exception as e:
database_exception = e database_exception = e
@ -76,6 +80,7 @@ def admin() -> str:
open_owner = request.args.get('open_owner', 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_storage = request.args.get('open_storage', None)
open_tag = request.args.get('open_tag', None) open_tag = request.args.get('open_tag', None)
open_theme = request.args.get('open_theme', None) open_theme = request.args.get('open_theme', None)
@ -86,6 +91,7 @@ def admin() -> str:
open_owner 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_storage is None and
open_tag is None and open_tag is None and
open_theme is None open_theme is None
) )
@ -101,6 +107,7 @@ def admin() -> str:
instructions=BrickInstructionsList(), instructions=BrickInstructionsList(),
metadata_owners=metadata_owners, metadata_owners=metadata_owners,
metadata_statuses=metadata_statuses, metadata_statuses=metadata_statuses,
metadata_storages=metadata_storages,
metadata_tags=metadata_tags, metadata_tags=metadata_tags,
nil_minifigure_name=nil_minifigure_name, nil_minifigure_name=nil_minifigure_name,
nil_minifigure_url=nil_minifigure_url, nil_minifigure_url=nil_minifigure_url,
@ -113,11 +120,13 @@ def admin() -> str:
open_owner=open_owner, open_owner=open_owner,
open_retired=open_retired, open_retired=open_retired,
open_status=open_status, open_status=open_status,
open_storage=open_storage,
open_tag=open_tag, open_tag=open_tag,
open_theme=open_theme, open_theme=open_theme,
owner_error=request.args.get('owner_error'), owner_error=request.args.get('owner_error'),
retired=BrickRetiredList(), retired=BrickRetiredList(),
status_error=request.args.get('status_error'), status_error=request.args.get('status_error'),
storage_error=request.args.get('storage_error'),
tag_error=request.args.get('tag_error'), tag_error=request.args.get('tag_error'),
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_storage import BrickSetStorage
admin_storage_page = Blueprint(
'admin_storage',
__name__,
url_prefix='/admin/storage'
)
# Add a metadata storage
@admin_storage_page.route('/add', methods=['POST'])
@login_required
@exception_handler(
__file__,
post_redirect='admin.admin',
error_name='storage_error',
open_storage=True
)
def add() -> Response:
BrickSetStorage().from_form(request.form).insert()
reload()
return redirect(url_for('admin.admin', open_storage=True))
# Delete the metadata storage
@admin_storage_page.route('<id>/delete', methods=['GET'])
@login_required
@exception_handler(__file__)
def delete(*, id: str) -> str:
return render_template(
'admin.html',
delete_storage=True,
storage=BrickSetStorage().select_specific(id),
error=request.args.get('storage_error')
)
# Actually delete the metadata storage
@admin_storage_page.route('<id>/delete', methods=['POST'])
@login_required
@exception_handler(
__file__,
post_redirect='admin_storage.delete',
error_name='storage_error'
)
def do_delete(*, id: str) -> Response:
storage = BrickSetStorage().select_specific(id)
storage.delete()
reload()
return redirect(url_for('admin.admin', open_storage=True))
# Rename the metadata storage
@admin_storage_page.route('<id>/rename', methods=['POST'])
@login_required
@exception_handler(
__file__,
post_redirect='admin.admin',
error_name='storage_error',
open_storage=True
)
def rename(*, id: str) -> Response:
storage = BrickSetStorage().select_specific(id)
storage.from_form(request.form).rename()
reload()
return redirect(url_for('admin.admin', open_storage=True))

View File

@ -4,6 +4,7 @@ from .exceptions import exception_handler
from ..minifigure_list import BrickMinifigureList from ..minifigure_list import BrickMinifigureList
from ..set_owner_list import BrickSetOwnerList from ..set_owner_list import BrickSetOwnerList
from ..set_status_list import BrickSetStatusList from ..set_status_list import BrickSetStatusList
from ..set_storage_list import BrickSetStorageList
from ..set_tag_list import BrickSetTagList from ..set_tag_list import BrickSetTagList
from ..set_list import BrickSetList from ..set_list import BrickSetList
@ -17,8 +18,9 @@ def index() -> str:
return render_template( return render_template(
'index.html', 'index.html',
brickset_collection=BrickSetList().last(), brickset_collection=BrickSetList().last(),
brickset_owners=BrickSetOwnerList.new().list(), brickset_owners=BrickSetOwnerList.list(),
brickset_statuses=BrickSetStatusList.new().list(), brickset_statuses=BrickSetStatusList.list(),
brickset_tags=BrickSetTagList.new().list(), brickset_storages=BrickSetStorageList.list(as_class=True),
brickset_tags=BrickSetTagList.list(),
minifigure_collection=BrickMinifigureList().last(), minifigure_collection=BrickMinifigureList().last(),
) )

View File

@ -19,6 +19,7 @@ from ..set import BrickSet
from ..set_list import BrickSetList from ..set_list import BrickSetList
from ..set_owner_list import BrickSetOwnerList from ..set_owner_list import BrickSetOwnerList
from ..set_status_list import BrickSetStatusList from ..set_status_list import BrickSetStatusList
from ..set_storage_list import BrickSetStorageList
from ..set_tag_list import BrickSetTagList from ..set_tag_list import BrickSetTagList
from ..socket import MESSAGES from ..socket import MESSAGES
@ -34,9 +35,10 @@ def list() -> str:
return render_template( return render_template(
'sets.html', 'sets.html',
collection=BrickSetList().all(), collection=BrickSetList().all(),
brickset_owners=BrickSetOwnerList.new().list(), brickset_owners=BrickSetOwnerList.list(),
brickset_statuses=BrickSetStatusList.new().list(), brickset_statuses=BrickSetStatusList.list(),
brickset_tags=BrickSetTagList.new().list(), brickset_storages=BrickSetStorageList.list(as_class=True),
brickset_tags=BrickSetTagList.list(),
) )
@ -46,7 +48,7 @@ def list() -> str:
@exception_handler(__file__, json=True) @exception_handler(__file__, json=True)
def update_owner(*, id: str, metadata_id: str) -> Response: def update_owner(*, id: str, metadata_id: str) -> Response:
brickset = BrickSet().select_light(id) brickset = BrickSet().select_light(id)
owner = BrickSetOwnerList.new().get(metadata_id) owner = BrickSetOwnerList.get(metadata_id)
state = owner.update_set_state(brickset, json=request.json) state = owner.update_set_state(brickset, json=request.json)
@ -59,20 +61,36 @@ def update_owner(*, id: str, metadata_id: str) -> Response:
@exception_handler(__file__, json=True) @exception_handler(__file__, json=True)
def update_status(*, id: str, metadata_id: str) -> Response: def update_status(*, id: str, metadata_id: str) -> Response:
brickset = BrickSet().select_light(id) brickset = BrickSet().select_light(id)
status = BrickSetStatusList.new().get(metadata_id) status = BrickSetStatusList.get(metadata_id)
state = status.update_set_state(brickset, json=request.json) state = status.update_set_state(brickset, json=request.json)
return jsonify({'value': state}) return jsonify({'value': state})
# Change the state of a storage
@set_page.route('/<id>/storage', methods=['POST'])
@login_required
@exception_handler(__file__, json=True)
def update_storage(*, id: str) -> Response:
brickset = BrickSet().select_light(id)
storage = BrickSetStorageList.get(
request.json.get('value', ''), # type: ignore
allow_none=True
)
state = storage.update_set_value(brickset, state=storage.fields.id)
return jsonify({'value': state})
# Change the state of a tag # Change the state of a tag
@set_page.route('/<id>/tag/<metadata_id>', methods=['POST']) @set_page.route('/<id>/tag/<metadata_id>', methods=['POST'])
@login_required @login_required
@exception_handler(__file__, json=True) @exception_handler(__file__, json=True)
def update_tag(*, id: str, metadata_id: str) -> Response: def update_tag(*, id: str, metadata_id: str) -> Response:
brickset = BrickSet().select_light(id) brickset = BrickSet().select_light(id)
tag = BrickSetTagList.new().get(metadata_id) tag = BrickSetTagList.get(metadata_id)
state = tag.update_set_state(brickset, json=request.json) state = tag.update_set_state(brickset, json=request.json)
@ -127,9 +145,10 @@ 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.new().list(), brickset_owners=BrickSetOwnerList.list(),
brickset_statuses=BrickSetStatusList.new().list(all=True), brickset_statuses=BrickSetStatusList.list(all=True),
brickset_tags=BrickSetTagList.new().list(), brickset_storages=BrickSetStorageList.list(as_class=True),
brickset_tags=BrickSetTagList.list(),
) )

View File

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

View File

@ -51,6 +51,19 @@
</div> </div>
{{ accordion.footer() }} {{ accordion.footer() }}
{% endif %} {% endif %}
{% if brickset_storages | length %}
{{ accordion.header('Storage', 'storage', 'metadata', icon='archive-2-line') }}
<label class="visually-hidden" for="storage">{{ name }}</label>
<div class="input-group">
<select id="add-storage" class="form-select" autocomplete="off">
<option value="" selected><i>None</i></option>
{% for storage in brickset_storages %}
<option value="{{ storage.fields.id }}">{{ storage.fields.name }}</option>
{% endfor %}
</select>
</div>
{{ accordion.footer() }}
{% endif %}
{% if brickset_tags | length %} {% if brickset_tags | length %}
{{ accordion.header('Tags', 'tags', 'metadata', icon='price-tag-2-line') }} {{ accordion.header('Tags', 'tags', 'metadata', icon='price-tag-2-line') }}
<div id="add-tags"> <div id="add-tags">

View File

@ -18,6 +18,8 @@
{% include 'admin/owner/delete.html' %} {% include 'admin/owner/delete.html' %}
{% elif delete_status %} {% elif delete_status %}
{% include 'admin/status/delete.html' %} {% include 'admin/status/delete.html' %}
{% elif delete_storage %}
{% include 'admin/storage/delete.html' %}
{% elif delete_tag %} {% elif delete_tag %}
{% include 'admin/tag/delete.html' %} {% include 'admin/tag/delete.html' %}
{% elif drop_database %} {% elif drop_database %}
@ -36,6 +38,7 @@
{% include 'admin/retired.html' %} {% include 'admin/retired.html' %}
{% include 'admin/owner.html' %} {% include 'admin/owner.html' %}
{% include 'admin/status.html' %} {% include 'admin/status.html' %}
{% include 'admin/storage.html' %}
{% include 'admin/tag.html' %} {% include 'admin/tag.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 storages', 'storage', 'admin', expanded=open_storage, icon='archive-2-line', class='p-0') }}
{% if storage_error %}<div class="alert alert-danger m-2" role="alert"><strong>Error:</strong> {{ storage_error }}.</div>{% endif %}
<ul class="list-group list-group-flush">
{% if metadata_storages | length %}
{% for storage in metadata_storages %}
<li class="list-group-item">
<form action="{{ url_for('admin_storage.rename', id=storage.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-{{ storage.fields.id }}">Name</label>
<div class="input-group">
<div class="input-group-text">Name</div>
<input type="text" class="form-control" id="name-{{ storage.fields.id }}" name="name" value="{{ storage.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_storage.delete', id=storage.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 storage found.</li>
{% endif %}
<li class="list-group-item">
<form action="{{ url_for('admin_storage.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 storages danger zone', 'storage-danger', 'admin', expanded=true, danger=true, class='text-end') }}
<form action="{{ url_for('admin_storage.do_delete', id=storage.fields.id) }}" method="post">
{% if storage_error %}<div class="alert alert-danger text-start" role="alert"><strong>Error:</strong> {{ storage_error }}.</div>{% endif %}
<div class="alert alert-danger text-center" role="alert">You are about to <strong>delete a set storage</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="{{ storage.fields.name }}" disabled>
</div>
</div>
</div>
<hr class="border-bottom">
<a class="btn btn-danger" href="{{ url_for('admin.admin', open_storage=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 storage</strong></button>
</form>
{{ accordion.footer() }}

View File

@ -72,6 +72,18 @@
{{ badge(check=set, url=url, solo=solo, last=last, color='secondary', icon='hashtag', collapsible='Set:', text=set, alt='Set') }} {{ badge(check=set, url=url, solo=solo, last=last, color='secondary', icon='hashtag', collapsible='Set:', text=set, alt='Set') }}
{% endmacro %} {% endmacro %}
{% macro storage(item, storages, solo=false, last=false) %}
{% if item.fields.storage in storages.mapping %}
{% set storage = storages.mapping[item.fields.storage] %}
{% if last %}
{% set tooltip=storage.fields.name %}
{% else %}
{% set text=storage.fields.name %}
{% endif %}
{{ badge(check=storage, solo=solo, last=last, color='light text-warning-emphasis bg-warning-subtle border border-warning-subtle', icon='archive-2-line', text=text, alt='Storage', tooltip=tooltip) }}
{% endif %}
{% endmacro %}
{% macro tag(item, tag, solo=false, last=false) %} {% macro tag(item, tag, solo=false, last=false) %}
{% if last %} {% if last %}
{% set tooltip=tag.fields.name %} {% set tooltip=tag.fields.name %}

View File

@ -39,3 +39,27 @@
</div> </div>
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
{% macro select(name, item, field, metadata_list, nullable=true, icon=none, delete=false) %}
{% if g.login.is_authenticated() %}
{% set prefix=metadata_list.as_prefix() %}
<label class="visually-hidden" for="{{ prefix }}-{{ item.fields.id }}">{{ name }}</label>
<div class="input-group">
{% if icon %}<span class="input-group-text"><i class="ri-{{ icon }}"></i></span>{% endif %}
<select id="{{ prefix }}-{{ item.fields.id }}" class="form-select"
{% if not delete %}
data-changer-id="{{ item.fields.id }}" data-changer-prefix="{{ prefix }}" data-changer-url="{{ metadata_list.url_for_set_state(item.fields.id) }}"
{% else %}
disabled
{% endif %}
autocomplete="off">
{% if nullable %}<option value="" {% if item.fields[field] is none %}selected{% endif %}><i>None</i></option>{% endif %}
{% for metadata in metadata_list %}
<option value="{{ metadata.fields.id }}" {% if metadata.fields.id == item.fields[field] %}selected{% endif %}>{{ metadata.fields.name }}</option>
{% endfor %}
</select>
<span id="status-{{ prefix }}-{{ item.fields.id }}" class="input-group-text ri-save-line"></span>
<button id="clear-{{ prefix }}-{{ item.fields.id }}" type="button" class="btn btn-sm btn-light btn-outline-danger border"><i class="ri-eraser-line"></i></button>
</div>
{% endif %}
{% endmacro %}

View File

@ -49,6 +49,7 @@
{% for owner in brickset_owners %} {% for owner in brickset_owners %}
{{ badge.owner(item, owner, solo=solo, last=last) }} {{ badge.owner(item, owner, solo=solo, last=last) }}
{% endfor %} {% endfor %}
{{ badge.storage(item, brickset_storages, solo=solo, last=last) }}
{% 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) }}