forked from FrederikBaerentsen/BrickTracker
Set storage
This commit is contained in:
parent
ec7fab2a7a
commit
9aff7e622d
@ -34,6 +34,7 @@ Parts
|
||||
- Fix missing @login_required for set deletion
|
||||
- Ownership
|
||||
- Tags
|
||||
- Storage
|
||||
|
||||
- Socket
|
||||
- Add decorator for rebrickable, authenticated and threaded socket actions
|
||||
@ -76,6 +77,7 @@ Parts
|
||||
- Ownership
|
||||
- Tags
|
||||
- Refresh
|
||||
- Storage
|
||||
|
||||
- Sets grid
|
||||
- Collapsible controls depending on screen size
|
||||
|
@ -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.retired import admin_retired_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.theme import admin_theme_page
|
||||
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_owner_page)
|
||||
app.register_blueprint(admin_status_page)
|
||||
app.register_blueprint(admin_storage_page)
|
||||
app.register_blueprint(admin_tag_page)
|
||||
app.register_blueprint(admin_theme_page)
|
||||
|
||||
|
@ -36,6 +36,9 @@ class BrickMetadata(BrickRecord):
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
# Defined an empty ID
|
||||
self.fields.id = None
|
||||
|
||||
# Ingest the record if it has one
|
||||
if record is not None:
|
||||
self.ingest(record)
|
||||
@ -129,8 +132,8 @@ class BrickMetadata(BrickRecord):
|
||||
json: Any | None = None,
|
||||
value: Any | None = None
|
||||
) -> Any:
|
||||
if value is None:
|
||||
value = json.get('value', None) # type: ignore
|
||||
if value is None and json is not None:
|
||||
value = json.get('value', None)
|
||||
|
||||
if value is None:
|
||||
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,
|
||||
state: bool | None = None,
|
||||
state: Any | None = None
|
||||
) -> Any:
|
||||
if state is None:
|
||||
state = json.get('value', False) # type: ignore
|
||||
if state is None and json is not None:
|
||||
state = json.get('value', False)
|
||||
|
||||
parameters = self.sql_parameters()
|
||||
parameters['set_id'] = brickset.fields.id
|
||||
parameters['state'] = state
|
||||
|
||||
# Update the status
|
||||
rows, _ = BrickSQL().execute_and_commit(
|
||||
self.update_set_state_query,
|
||||
parameters=parameters,
|
||||
@ -205,7 +207,53 @@ class BrickMetadata(BrickRecord):
|
||||
))
|
||||
|
||||
# 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,
|
||||
name=self.fields.name,
|
||||
state=state,
|
||||
|
@ -1,16 +1,19 @@
|
||||
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 .fields import BrickRecordFields
|
||||
from .record_list import BrickRecordList
|
||||
from .set_owner import BrickSetOwner
|
||||
from .set_status import BrickSetStatus
|
||||
from .set_storage import BrickSetStorage
|
||||
from .set_tag import BrickSetTag
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
T = TypeVar('T', BrickSetStatus, BrickSetOwner, BrickSetTag)
|
||||
T = TypeVar('T', BrickSetOwner, BrickSetStatus, BrickSetStorage, BrickSetTag)
|
||||
|
||||
|
||||
# Lego sets metadata list
|
||||
@ -25,55 +28,119 @@ class BrickMetadataList(BrickRecordList[T]):
|
||||
# Queries
|
||||
select_query: str
|
||||
|
||||
def __init__(self, model: Type[T], /, *, force: bool = False):
|
||||
# Load statuses only if there is none already loaded
|
||||
records = getattr(self, 'records', None)
|
||||
# Set state endpoint
|
||||
set_state_endpoint: str
|
||||
|
||||
if records is None or force:
|
||||
# Don't use super()__init__ as it would mask class variables
|
||||
self.fields = BrickRecordFields()
|
||||
def __init__(
|
||||
self,
|
||||
model: Type[T],
|
||||
/,
|
||||
*,
|
||||
force: bool = False,
|
||||
records: list[T] | None = None
|
||||
):
|
||||
self.model = model
|
||||
|
||||
logger.info('Loading {kind} list'.format(
|
||||
kind=self.kind
|
||||
))
|
||||
# Records override (masking the class variables with instance ones)
|
||||
if records is not None:
|
||||
self.records = []
|
||||
self.mapping = {}
|
||||
|
||||
self.__class__.records = []
|
||||
self.__class__.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)
|
||||
|
||||
# Load the statuses from the database
|
||||
for record in self.select():
|
||||
status = model(record=record)
|
||||
if records is None or force:
|
||||
# Don't use super()__init__ as it would mask class variables
|
||||
self.fields = BrickRecordFields()
|
||||
|
||||
self.__class__.records.append(status)
|
||||
self.__class__.mapping[status.fields.id] = status
|
||||
logger.info('Loading {kind} list'.format(
|
||||
kind=self.kind
|
||||
))
|
||||
|
||||
# Return the items as columns for a select
|
||||
def as_columns(self, /, **kwargs) -> str:
|
||||
return ', '.join([
|
||||
'"{table}"."{column}"'.format(
|
||||
table=self.table,
|
||||
column=record.as_column(),
|
||||
)
|
||||
for record
|
||||
in self.filter(**kwargs)
|
||||
])
|
||||
self.__class__.records = []
|
||||
self.__class__.mapping = {}
|
||||
|
||||
# Load the metadata from the database
|
||||
for record in self.select():
|
||||
metadata = model(record=record)
|
||||
|
||||
self.__class__.records.append(metadata)
|
||||
self.__class__.mapping[metadata.fields.id] = metadata
|
||||
|
||||
# HTML prefix name
|
||||
def as_prefix(self, /) -> str:
|
||||
return self.kind.replace(' ', '-')
|
||||
|
||||
# Filter the list of records (this one does nothing)
|
||||
def filter(self) -> list[T]:
|
||||
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
|
||||
def get(self, id: str, /) -> T:
|
||||
if id not in self.mapping:
|
||||
@classmethod
|
||||
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(
|
||||
'{kind} with ID {id} was not found in the database'.format(
|
||||
kind=self.kind.capitalize(),
|
||||
kind=new.kind.capitalize(),
|
||||
id=id,
|
||||
),
|
||||
)
|
||||
|
||||
return self.mapping[id]
|
||||
return new.mapping[id]
|
||||
|
||||
# Get the list of statuses depending on the context
|
||||
def list(self, /, **kwargs) -> list[T]:
|
||||
return self.filter(**kwargs)
|
||||
@overload
|
||||
@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,
|
||||
)
|
||||
|
@ -10,17 +10,19 @@ if TYPE_CHECKING:
|
||||
from .set import BrickSet
|
||||
from .set_owner import BrickSetOwner
|
||||
from .set_status import BrickSetStatus
|
||||
from .set_storage import BrickSetStorage
|
||||
from .set_tag import BrickSetTag
|
||||
from .wish import BrickWish
|
||||
|
||||
T = TypeVar(
|
||||
'T',
|
||||
'BrickMinifigure',
|
||||
'BrickPart',
|
||||
'BrickSet',
|
||||
'BrickSetOwner',
|
||||
'BrickSetStatus',
|
||||
'BrickSetStorage',
|
||||
'BrickSetTag',
|
||||
'BrickPart',
|
||||
'BrickMinifigure',
|
||||
'BrickWish',
|
||||
'RebrickableSet'
|
||||
)
|
||||
|
@ -2,6 +2,7 @@ from .instructions_list import BrickInstructionsList
|
||||
from .retired_list import BrickRetiredList
|
||||
from .set_owner_list import BrickSetOwnerList
|
||||
from .set_status_list import BrickSetStatusList
|
||||
from .set_storage_list import BrickSetStorageList
|
||||
from .set_tag_list import BrickSetTagList
|
||||
from .theme_list import BrickThemeList
|
||||
|
||||
@ -19,6 +20,9 @@ def reload() -> None:
|
||||
# Reload the set statuses
|
||||
BrickSetStatusList.new(force=True)
|
||||
|
||||
# Reload the set storages
|
||||
BrickSetStorageList.new(force=True)
|
||||
|
||||
# Reload the set tags
|
||||
BrickSetTagList.new(force=True)
|
||||
|
||||
|
@ -11,6 +11,7 @@ from .part_list import BrickPartList
|
||||
from .rebrickable_set import RebrickableSet
|
||||
from .set_owner_list import BrickSetOwnerList
|
||||
from .set_status_list import BrickSetStatusList
|
||||
from .set_storage_list import BrickSetStorageList
|
||||
from .set_tag_list import BrickSetTagList
|
||||
from .sql import BrickSQL
|
||||
if TYPE_CHECKING:
|
||||
@ -55,9 +56,30 @@ class BrickSet(RebrickableSet):
|
||||
self.fields.id = str(uuid4())
|
||||
|
||||
if not refresh:
|
||||
# Save the storage
|
||||
storage = BrickSetStorageList.get(
|
||||
data.get('storage', ''),
|
||||
allow_none=True
|
||||
)
|
||||
self.fields.storage = storage.fields.id
|
||||
|
||||
# Insert into database
|
||||
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
|
||||
self.insert_rebrickable()
|
||||
|
||||
@ -69,20 +91,6 @@ 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)
|
||||
|
||||
# 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
|
||||
socket.auto_progress(
|
||||
message='Set {set}: writing to the database'.format(
|
||||
@ -166,9 +174,9 @@ class BrickSet(RebrickableSet):
|
||||
|
||||
# Load from database
|
||||
if not self.select(
|
||||
owners=BrickSetOwnerList.new().as_columns(),
|
||||
statuses=BrickSetStatusList.new().as_columns(all=True),
|
||||
tags=BrickSetTagList.new().as_columns(),
|
||||
owners=BrickSetOwnerList.as_columns(),
|
||||
statuses=BrickSetStatusList.as_columns(all=True),
|
||||
tags=BrickSetTagList.as_columns(),
|
||||
):
|
||||
raise NotFoundException(
|
||||
'Set with ID {id} was not found in the database'.format(
|
||||
|
@ -41,9 +41,9 @@ class BrickSetList(BrickRecordList[BrickSet]):
|
||||
# Load the sets from the database
|
||||
for record in self.select(
|
||||
order=self.order,
|
||||
owners=BrickSetOwnerList.new().as_columns(),
|
||||
statuses=BrickSetStatusList.new().as_columns(),
|
||||
tags=BrickSetTagList.new().as_columns(),
|
||||
owners=BrickSetOwnerList.as_columns(),
|
||||
statuses=BrickSetStatusList.as_columns(),
|
||||
tags=BrickSetTagList.as_columns(),
|
||||
):
|
||||
brickset = BrickSet(record=record)
|
||||
|
||||
@ -112,9 +112,9 @@ class BrickSetList(BrickRecordList[BrickSet]):
|
||||
for record in self.select(
|
||||
order=order,
|
||||
limit=limit,
|
||||
owners=BrickSetOwnerList.new().as_columns(),
|
||||
statuses=BrickSetStatusList.new().as_columns(),
|
||||
tags=BrickSetTagList.new().as_columns(),
|
||||
owners=BrickSetOwnerList.as_columns(),
|
||||
statuses=BrickSetStatusList.as_columns(),
|
||||
tags=BrickSetTagList.as_columns(),
|
||||
):
|
||||
brickset = BrickSet(record=record)
|
||||
|
||||
|
13
bricktracker/set_storage.py
Normal file
13
bricktracker/set_storage.py
Normal 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'
|
23
bricktracker/set_storage_list.py
Normal file
23
bricktracker/set_storage_list.py
Normal 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)
|
@ -1,5 +1,6 @@
|
||||
SELECT
|
||||
{% block id %}{% endblock %}
|
||||
"bricktracker_sets"."storage",
|
||||
"rebrickable_sets"."set",
|
||||
"rebrickable_sets"."number",
|
||||
"rebrickable_sets"."version",
|
||||
|
@ -1,7 +1,9 @@
|
||||
INSERT OR IGNORE INTO "bricktracker_sets" (
|
||||
"id",
|
||||
"set"
|
||||
"set",
|
||||
"storage"
|
||||
) VALUES (
|
||||
:id,
|
||||
:set
|
||||
:set,
|
||||
:storage
|
||||
)
|
||||
|
6
bricktracker/sql/set/metadata/storage/base.sql
Normal file
6
bricktracker/sql/set/metadata/storage/base.sql
Normal file
@ -0,0 +1,6 @@
|
||||
SELECT
|
||||
"bricktracker_metadata_storages"."id",
|
||||
"bricktracker_metadata_storages"."name"
|
||||
FROM "bricktracker_metadata_storages"
|
||||
|
||||
{% block where %}{% endblock %}
|
6
bricktracker/sql/set/metadata/storage/delete.sql
Normal file
6
bricktracker/sql/set/metadata/storage/delete.sql
Normal file
@ -0,0 +1,6 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
DELETE FROM "bricktracker_metadata_storages"
|
||||
WHERE "bricktracker_metadata_statuses"."id" IS NOT DISTINCT FROM '{{ id }}';
|
||||
|
||||
COMMIT;
|
11
bricktracker/sql/set/metadata/storage/insert.sql
Normal file
11
bricktracker/sql/set/metadata/storage/insert.sql
Normal file
@ -0,0 +1,11 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
INSERT INTO "bricktracker_metadata_storages" (
|
||||
"id",
|
||||
"name"
|
||||
) VALUES (
|
||||
'{{ id }}',
|
||||
'{{ name }}'
|
||||
);
|
||||
|
||||
COMMIT;
|
1
bricktracker/sql/set/metadata/storage/list.sql
Normal file
1
bricktracker/sql/set/metadata/storage/list.sql
Normal file
@ -0,0 +1 @@
|
||||
{% extends 'set/metadata/storage/base.sql' %}
|
5
bricktracker/sql/set/metadata/storage/select.sql
Normal file
5
bricktracker/sql/set/metadata/storage/select.sql
Normal file
@ -0,0 +1,5 @@
|
||||
{% extends 'set/metadata/storage/base.sql' %}
|
||||
|
||||
{% block where %}
|
||||
WHERE "bricktracker_metadata_storages"."id" IS NOT DISTINCT FROM :id
|
||||
{% endblock %}
|
3
bricktracker/sql/set/metadata/storage/update/field.sql
Normal file
3
bricktracker/sql/set/metadata/storage/update/field.sql
Normal file
@ -0,0 +1,3 @@
|
||||
UPDATE "bricktracker_metadata_storages"
|
||||
SET "{{field}}" = :value
|
||||
WHERE "bricktracker_metadata_storages"."id" IS NOT DISTINCT FROM :id
|
3
bricktracker/sql/set/metadata/storage/update/state.sql
Normal file
3
bricktracker/sql/set/metadata/storage/update/state.sql
Normal file
@ -0,0 +1,3 @@
|
||||
UPDATE "bricktracker_sets"
|
||||
SET "storage" = :state
|
||||
WHERE "bricktracker_sets"."id" IS NOT DISTINCT FROM :set_id
|
@ -4,6 +4,7 @@ from flask_login import login_required
|
||||
from ..configuration_list import BrickConfigurationList
|
||||
from .exceptions import exception_handler
|
||||
from ..set_owner_list import BrickSetOwnerList
|
||||
from ..set_storage_list import BrickSetStorageList
|
||||
from ..set_tag_list import BrickSetTagList
|
||||
from ..socket import MESSAGES
|
||||
|
||||
@ -19,8 +20,9 @@ def add() -> str:
|
||||
|
||||
return render_template(
|
||||
'add.html',
|
||||
brickset_owners=BrickSetOwnerList.new().list(),
|
||||
brickset_tags=BrickSetTagList.new().list(),
|
||||
brickset_owners=BrickSetOwnerList.list(),
|
||||
brickset_storages=BrickSetStorageList.list(),
|
||||
brickset_tags=BrickSetTagList.list(),
|
||||
path=current_app.config['SOCKET_PATH'],
|
||||
namespace=current_app.config['SOCKET_NAMESPACE'],
|
||||
messages=MESSAGES
|
||||
@ -36,8 +38,9 @@ def bulk() -> str:
|
||||
|
||||
return render_template(
|
||||
'add.html',
|
||||
brickset_owners=BrickSetOwnerList.new().list(),
|
||||
brickset_tags=BrickSetTagList.new().list(),
|
||||
brickset_owners=BrickSetOwnerList.list(),
|
||||
brickset_storages=BrickSetStorageList.list(),
|
||||
brickset_tags=BrickSetTagList.list(),
|
||||
path=current_app.config['SOCKET_PATH'],
|
||||
namespace=current_app.config['SOCKET_NAMESPACE'],
|
||||
messages=MESSAGES,
|
||||
|
@ -10,6 +10,8 @@ from ...rebrickable_image import RebrickableImage
|
||||
from ...retired_list import BrickRetiredList
|
||||
from ...set_owner import BrickSetOwner
|
||||
from ...set_owner_list import BrickSetOwnerList
|
||||
from ...set_storage import BrickSetStorage
|
||||
from ...set_storage_list import BrickSetStorageList
|
||||
from ...set_status import BrickSetStatus
|
||||
from ...set_status_list import BrickSetStatusList
|
||||
from ...set_tag import BrickSetTag
|
||||
@ -34,6 +36,7 @@ def admin() -> str:
|
||||
database_version: int = -1
|
||||
metadata_owners: list[BrickSetOwner] = []
|
||||
metadata_statuses: list[BrickSetStatus] = []
|
||||
metadata_storages: list[BrickSetStorage] = []
|
||||
metadata_tags: list[BrickSetTag] = []
|
||||
nil_minifigure_name: str = ''
|
||||
nil_minifigure_url: str = ''
|
||||
@ -47,9 +50,10 @@ def admin() -> str:
|
||||
database_version = database.version
|
||||
database_counters = BrickSQL().count_records()
|
||||
|
||||
metadata_owners = BrickSetOwnerList.new().list()
|
||||
metadata_statuses = BrickSetStatusList.new().list(all=True)
|
||||
metadata_tags = BrickSetTagList.new().list()
|
||||
metadata_owners = BrickSetOwnerList.list()
|
||||
metadata_statuses = BrickSetStatusList.list(all=True)
|
||||
metadata_storages = BrickSetStorageList.list()
|
||||
metadata_tags = BrickSetTagList.list()
|
||||
except Exception as e:
|
||||
database_exception = e
|
||||
|
||||
@ -76,6 +80,7 @@ def admin() -> str:
|
||||
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_storage = request.args.get('open_storage', None)
|
||||
open_tag = request.args.get('open_tag', None)
|
||||
open_theme = request.args.get('open_theme', None)
|
||||
|
||||
@ -86,6 +91,7 @@ def admin() -> str:
|
||||
open_owner is None and
|
||||
open_retired is None and
|
||||
open_status is None and
|
||||
open_storage is None and
|
||||
open_tag is None and
|
||||
open_theme is None
|
||||
)
|
||||
@ -101,6 +107,7 @@ def admin() -> str:
|
||||
instructions=BrickInstructionsList(),
|
||||
metadata_owners=metadata_owners,
|
||||
metadata_statuses=metadata_statuses,
|
||||
metadata_storages=metadata_storages,
|
||||
metadata_tags=metadata_tags,
|
||||
nil_minifigure_name=nil_minifigure_name,
|
||||
nil_minifigure_url=nil_minifigure_url,
|
||||
@ -113,11 +120,13 @@ def admin() -> str:
|
||||
open_owner=open_owner,
|
||||
open_retired=open_retired,
|
||||
open_status=open_status,
|
||||
open_storage=open_storage,
|
||||
open_tag=open_tag,
|
||||
open_theme=open_theme,
|
||||
owner_error=request.args.get('owner_error'),
|
||||
retired=BrickRetiredList(),
|
||||
status_error=request.args.get('status_error'),
|
||||
storage_error=request.args.get('storage_error'),
|
||||
tag_error=request.args.get('tag_error'),
|
||||
theme=BrickThemeList(),
|
||||
)
|
||||
|
84
bricktracker/views/admin/storage.py
Normal file
84
bricktracker/views/admin/storage.py
Normal 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))
|
@ -4,6 +4,7 @@ from .exceptions import exception_handler
|
||||
from ..minifigure_list import BrickMinifigureList
|
||||
from ..set_owner_list import BrickSetOwnerList
|
||||
from ..set_status_list import BrickSetStatusList
|
||||
from ..set_storage_list import BrickSetStorageList
|
||||
from ..set_tag_list import BrickSetTagList
|
||||
from ..set_list import BrickSetList
|
||||
|
||||
@ -17,8 +18,9 @@ def index() -> str:
|
||||
return render_template(
|
||||
'index.html',
|
||||
brickset_collection=BrickSetList().last(),
|
||||
brickset_owners=BrickSetOwnerList.new().list(),
|
||||
brickset_statuses=BrickSetStatusList.new().list(),
|
||||
brickset_tags=BrickSetTagList.new().list(),
|
||||
brickset_owners=BrickSetOwnerList.list(),
|
||||
brickset_statuses=BrickSetStatusList.list(),
|
||||
brickset_storages=BrickSetStorageList.list(as_class=True),
|
||||
brickset_tags=BrickSetTagList.list(),
|
||||
minifigure_collection=BrickMinifigureList().last(),
|
||||
)
|
||||
|
@ -19,6 +19,7 @@ from ..set import BrickSet
|
||||
from ..set_list import BrickSetList
|
||||
from ..set_owner_list import BrickSetOwnerList
|
||||
from ..set_status_list import BrickSetStatusList
|
||||
from ..set_storage_list import BrickSetStorageList
|
||||
from ..set_tag_list import BrickSetTagList
|
||||
from ..socket import MESSAGES
|
||||
|
||||
@ -34,9 +35,10 @@ def list() -> str:
|
||||
return render_template(
|
||||
'sets.html',
|
||||
collection=BrickSetList().all(),
|
||||
brickset_owners=BrickSetOwnerList.new().list(),
|
||||
brickset_statuses=BrickSetStatusList.new().list(),
|
||||
brickset_tags=BrickSetTagList.new().list(),
|
||||
brickset_owners=BrickSetOwnerList.list(),
|
||||
brickset_statuses=BrickSetStatusList.list(),
|
||||
brickset_storages=BrickSetStorageList.list(as_class=True),
|
||||
brickset_tags=BrickSetTagList.list(),
|
||||
)
|
||||
|
||||
|
||||
@ -46,7 +48,7 @@ def list() -> str:
|
||||
@exception_handler(__file__, json=True)
|
||||
def update_owner(*, id: str, metadata_id: str) -> Response:
|
||||
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)
|
||||
|
||||
@ -59,20 +61,36 @@ def update_owner(*, id: str, metadata_id: str) -> Response:
|
||||
@exception_handler(__file__, json=True)
|
||||
def update_status(*, id: str, metadata_id: str) -> Response:
|
||||
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)
|
||||
|
||||
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
|
||||
@set_page.route('/<id>/tag/<metadata_id>', methods=['POST'])
|
||||
@login_required
|
||||
@exception_handler(__file__, json=True)
|
||||
def update_tag(*, id: str, metadata_id: str) -> Response:
|
||||
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)
|
||||
|
||||
@ -127,9 +145,10 @@ def details(*, id: str) -> str:
|
||||
'set.html',
|
||||
item=BrickSet().select_specific(id),
|
||||
open_instructions=request.args.get('open_instructions'),
|
||||
brickset_owners=BrickSetOwnerList.new().list(),
|
||||
brickset_statuses=BrickSetStatusList.new().list(all=True),
|
||||
brickset_tags=BrickSetTagList.new().list(),
|
||||
brickset_owners=BrickSetOwnerList.list(),
|
||||
brickset_statuses=BrickSetStatusList.list(all=True),
|
||||
brickset_storages=BrickSetStorageList.list(as_class=True),
|
||||
brickset_tags=BrickSetTagList.list(),
|
||||
)
|
||||
|
||||
|
||||
|
@ -16,6 +16,7 @@ class BrickSetSocket extends BrickSocket {
|
||||
this.html_input = document.getElementById(`${id}-set`);
|
||||
this.html_no_confim = document.getElementById(`${id}-no-confirm`);
|
||||
this.html_owners = document.getElementById(`${id}-owners`);
|
||||
this.html_storage = document.getElementById(`${id}-storage`);
|
||||
this.html_tags = document.getElementById(`${id}-tags`);
|
||||
|
||||
// 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
|
||||
const tags = [];
|
||||
if (this.html_tags) {
|
||||
@ -170,6 +177,7 @@ class BrickSetSocket extends BrickSocket {
|
||||
this.socket.emit(this.messages.IMPORT_SET, {
|
||||
set: (set !== undefined) ? set : this.html_input.value,
|
||||
owners: owners,
|
||||
storage: storage,
|
||||
tags: tags,
|
||||
refresh: this.refresh
|
||||
});
|
||||
@ -285,6 +293,10 @@ class BrickSetSocket extends BrickSocket {
|
||||
this.html_owners.querySelectorAll('input').forEach(input => input.disabled = !enabled);
|
||||
}
|
||||
|
||||
if (this.html_storage) {
|
||||
this.html_storage.disabled = !enabled;
|
||||
}
|
||||
|
||||
if (this.html_tags) {
|
||||
this.html_tags.querySelectorAll('input').forEach(input => input.disabled = !enabled);
|
||||
}
|
||||
|
@ -51,6 +51,19 @@
|
||||
</div>
|
||||
{{ accordion.footer() }}
|
||||
{% endif %}
|
||||
{% if brickset_storages | length %}
|
||||
{{ accordion.header('Storage', 'storage', 'metadata', icon='archive-2-line') }}
|
||||
<label class="visually-hidden" for="storage">{{ name }}</label>
|
||||
<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 %}
|
||||
{{ accordion.header('Tags', 'tags', 'metadata', icon='price-tag-2-line') }}
|
||||
<div id="add-tags">
|
||||
|
@ -18,6 +18,8 @@
|
||||
{% include 'admin/owner/delete.html' %}
|
||||
{% elif delete_status %}
|
||||
{% include 'admin/status/delete.html' %}
|
||||
{% elif delete_storage %}
|
||||
{% include 'admin/storage/delete.html' %}
|
||||
{% elif delete_tag %}
|
||||
{% include 'admin/tag/delete.html' %}
|
||||
{% elif drop_database %}
|
||||
@ -36,6 +38,7 @@
|
||||
{% include 'admin/retired.html' %}
|
||||
{% include 'admin/owner.html' %}
|
||||
{% include 'admin/status.html' %}
|
||||
{% include 'admin/storage.html' %}
|
||||
{% include 'admin/tag.html' %}
|
||||
{% include 'admin/database.html' %}
|
||||
{% include 'admin/configuration.html' %}
|
||||
|
42
templates/admin/storage.html
Normal file
42
templates/admin/storage.html
Normal 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() }}
|
19
templates/admin/storage/delete.html
Normal file
19
templates/admin/storage/delete.html
Normal 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() }}
|
@ -72,6 +72,18 @@
|
||||
{{ badge(check=set, url=url, solo=solo, last=last, color='secondary', icon='hashtag', collapsible='Set:', text=set, alt='Set') }}
|
||||
{% 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) %}
|
||||
{% if last %}
|
||||
{% set tooltip=tag.fields.name %}
|
||||
|
@ -39,3 +39,27 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
|
@ -48,7 +48,8 @@
|
||||
{{ badge.total_damaged(item.fields.total_damaged, solo=solo, last=last) }}
|
||||
{% for owner in brickset_owners %}
|
||||
{{ badge.owner(item, owner, solo=solo, last=last) }}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{{ badge.storage(item, brickset_storages, solo=solo, last=last) }}
|
||||
{% if not last %}
|
||||
{% if not solo %}
|
||||
{{ badge.instructions(item, solo=solo, last=last) }}
|
||||
|
Loading…
x
Reference in New Issue
Block a user