diff --git a/bricktracker/metadata.py b/bricktracker/metadata.py new file mode 100644 index 0000000..df93c93 --- /dev/null +++ b/bricktracker/metadata.py @@ -0,0 +1,197 @@ +import logging +from sqlite3 import Row +from typing import Any, Self, TYPE_CHECKING +from uuid import uuid4 + +from flask import url_for + +from .exceptions import DatabaseException, ErrorException, NotFoundException +from .record import BrickRecord +from .sql import BrickSQL +if TYPE_CHECKING: + from .set import BrickSet + +logger = logging.getLogger(__name__) + + +# Lego set metadata (customizable list of entries that can be checked) +class BrickMetadata(BrickRecord): + kind: str + prefix: str + + # Set state endpoint + set_state_endpoint: str + + # Queries + delete_query: str + insert_query: str + select_query: str + update_field_query: str + update_set_state_query: str + + def __init__( + self, + /, + *, + record: Row | dict[str, Any] | None = None, + ): + super().__init__() + + # Ingest the record if it has one + if record is not None: + self.ingest(record) + + # SQL column name + def as_column(self, /) -> str: + return '{prefix}_{id}'.format(id=self.fields.id, prefix=self.prefix) + + # HTML dataset name + def as_dataset(self, /) -> str: + return '{id}'.format( + id=self.as_column().replace('_', '-') + ) + + # Delete from database + def delete(self, /) -> None: + BrickSQL().executescript( + self.delete_query, + id=self.fields.id, + ) + + # Insert into database + def insert(self, /, **context) -> None: + self.safe() + + # Generate an ID for the metadata (with underscores to make it + # column name friendly) + self.fields.id = str(uuid4()).replace('-', '_') + + BrickSQL().executescript( + self.insert_query, + id=self.fields.id, + name=self.fields.safe_name, + **context + ) + + # Rename the entry + def rename(self, /) -> None: + self.safe() + + self.update_field('name', value=self.fields.name) + + # Make the name "safe" + # Security: eh. + def safe(self, /) -> None: + # Prevent self-ownage with accidental quote escape + self.fields.safe_name = self.fields.name.replace("'", "''") + + # URL to change the selected state of this metadata item for a set + def url_for_set_state(self, id: str, /) -> str: + return url_for( + self.set_state_endpoint, + id=id, + metadata_id=self.fields.id + ) + + # Select a specific checkbox (with an id) + def select_specific(self, id: str, /) -> Self: + # Save the parameters to the fields + self.fields.id = id + + # Load from database + if not self.select(): + raise NotFoundException( + '{kind} with ID {id} was not found in the database'.format( + kind=self.kind.capitalize(), + id=self.fields.id, + ), + ) + + return self + + # Update a field + def update_field( + self, + field: str, + /, + *, + json: Any | None = None, + value: Any | None = None + ) -> Any: + if value is None: + value = json.get('value', None) # type: ignore + + if value is None: + raise ErrorException('"{field}" of a {kind} cannot be set to an empty value'.format( # noqa: E501 + field=field, + kind=self.kind + )) + + if field == 'id' or not hasattr(self.fields, field): + raise NotFoundException('"{field}" is not a field of a {kind}'.format( # noqa: E501 + kind=self.kind, + field=field + )) + + parameters = self.sql_parameters() + parameters['value'] = value + + # Update the status + rows, _ = BrickSQL().execute_and_commit( + self.update_field_query, + parameters=parameters, + field=field, + ) + + if rows != 1: + raise DatabaseException('Could not update the field "{field}" for {kind} {name} ({id})'.format( # noqa: E501 + field=field, + kind=self.kind, + name=self.fields.name, + id=self.fields.id, + )) + + # Info + logger.info('{kind} "{name}" ({id}): field "{field}" changed to "{value}"'.format( # noqa: E501 + kind=self.kind.capitalize(), + name=self.fields.name, + id=self.fields.id, + field=field, + value=value, + )) + + return value + + # Update the selected state of this metadata item for a set + def update_set_state(self, brickset: 'BrickSet', json: Any | None) -> Any: + state: bool = json.get('value', False) # type: ignore + + 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, + name=self.as_column(), + ) + + if rows != 1: + raise DatabaseException('Could not update the {kind} "{name}" state for set {set} ({id})'.format( # noqa: E501 + kind=self.kind, + name=self.fields.name, + set=brickset.fields.set, + id=brickset.fields.id, + )) + + # Info + logger.info('{kind} "{name}" state change 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 diff --git a/bricktracker/set.py b/bricktracker/set.py index fa05b0b..6b2da47 100644 --- a/bricktracker/set.py +++ b/bricktracker/set.py @@ -5,11 +5,10 @@ from uuid import uuid4 from flask import current_app, url_for -from .exceptions import DatabaseException, NotFoundException +from .exceptions import NotFoundException from .minifigure_list import BrickMinifigureList from .part_list import BrickPartList from .rebrickable_set import RebrickableSet -from .set_checkbox import BrickSetCheckbox from .set_checkbox_list import BrickSetCheckboxList from .sql import BrickSQL if TYPE_CHECKING: @@ -172,30 +171,6 @@ class BrickSet(RebrickableSet): return self - # Update a status - def update_status( - self, - checkbox: BrickSetCheckbox, - status: bool, - / - ) -> None: - parameters = self.sql_parameters() - parameters['status'] = status - - # Update the status - rows, _ = BrickSQL().execute_and_commit( - 'set/update/status', - parameters=parameters, - name=checkbox.as_column(), - ) - - if rows != 1: - raise DatabaseException('Could not update the status "{status}" for set {set} ({id})'.format( # noqa: E501 - status=checkbox.fields.name, - set=self.fields.set, - id=self.fields.id, - )) - # Self url def url(self, /) -> str: return url_for('set.details', id=self.fields.id) diff --git a/bricktracker/set_checkbox.py b/bricktracker/set_checkbox.py index 38a10f0..f38b5f6 100644 --- a/bricktracker/set_checkbox.py +++ b/bricktracker/set_checkbox.py @@ -1,139 +1,39 @@ -from sqlite3 import Row -from typing import Any, Self -from uuid import uuid4 +from typing import Self -from flask import url_for - -from .exceptions import DatabaseException, ErrorException, NotFoundException -from .record import BrickRecord -from .sql import BrickSQL +from .exceptions import ErrorException +from .metadata import BrickMetadata # Lego set checkbox -class BrickSetCheckbox(BrickRecord): +class BrickSetCheckbox(BrickMetadata): + kind: str = 'checkbox' + prefix: str = 'status' + + # Set state endpoint + set_state_endpoint: str = 'set.update_status' + # Queries + delete_query: str = 'checkbox/delete' + insert_query: str = 'checkbox/insert' select_query: str = 'checkbox/select' - - def __init__( - self, - /, - *, - record: Row | dict[str, Any] | None = None, - ): - super().__init__() - - # Ingest the record if it has one - if record is not None: - self.ingest(record) - - # SQL column name - def as_column(self) -> str: - return 'status_{id}'.format(id=self.fields.id) - - # HTML dataset name - def as_dataset(self) -> str: - return '{id}'.format( - id=self.as_column().replace('_', '-') - ) - - # Delete from database - def delete(self) -> None: - BrickSQL().executescript( - 'checkbox/delete', - id=self.fields.id, - ) + update_field_query: str = 'checkbox/update/field' + update_set_state_query: str = 'set/update/status' # Grab data from a form - def from_form(self, form: dict[str, str]) -> Self: + def from_form(self, form: dict[str, str], /) -> Self: name = form.get('name', None) grid = form.get('grid', None) if name is None or name == '': raise ErrorException('Checkbox name cannot be empty') - # Security: eh. - # Prevent self-ownage with accidental quote escape self.fields.name = name - self.fields.safe_name = self.fields.name.replace("'", "''") self.fields.displayed_on_grid = grid == 'on' return self # Insert into database - def insert(self, **_) -> None: - # Generate an ID for the checkbox (with underscores to make it - # column name friendly) - self.fields.id = str(uuid4()).replace('-', '_') - - BrickSQL().executescript( - 'checkbox/add', - id=self.fields.id, - name=self.fields.safe_name, + def insert(self, /, **_) -> None: + super().insert( displayed_on_grid=self.fields.displayed_on_grid ) - - # Rename the checkbox - def rename(self, /) -> None: - # Update the name - rows, _ = BrickSQL().execute_and_commit( - 'checkbox/update/name', - parameters=self.sql_parameters(), - ) - - if rows != 1: - raise DatabaseException('Could not update the name for checkbox {name} ({id})'.format( # noqa: E501 - name=self.fields.name, - id=self.fields.id, - )) - - # URL to change the status - def status_url(self, id: str) -> str: - return url_for( - 'set.update_status', - id=id, - checkbox_id=self.fields.id - ) - - # Select a specific checkbox (with an id) - def select_specific(self, id: str, /) -> Self: - # Save the parameters to the fields - self.fields.id = id - - # Load from database - if not self.select(): - raise NotFoundException( - 'Checkbox with ID {id} was not found in the database'.format( - id=self.fields.id, - ), - ) - - return self - - # Update a status - def update_status( - self, - name: str, - status: bool, - / - ) -> None: - if not hasattr(self.fields, name) or name in ['id', 'name']: - raise NotFoundException('{name} is not a field of a checkbox'.format( # noqa: E501 - name=name - )) - - parameters = self.sql_parameters() - parameters['status'] = status - - # Update the status - rows, _ = BrickSQL().execute_and_commit( - 'checkbox/update/status', - parameters=parameters, - name=name, - ) - - if rows != 1: - raise DatabaseException('Could not update the status "{status}" for checkbox {name} ({id})'.format( # noqa: E501 - status=name, - name=self.fields.name, - id=self.fields.id, - )) diff --git a/bricktracker/sql/checkbox/add.sql b/bricktracker/sql/checkbox/insert.sql similarity index 100% rename from bricktracker/sql/checkbox/add.sql rename to bricktracker/sql/checkbox/insert.sql diff --git a/bricktracker/sql/checkbox/update/name.sql b/bricktracker/sql/checkbox/update/field.sql similarity index 80% rename from bricktracker/sql/checkbox/update/name.sql rename to bricktracker/sql/checkbox/update/field.sql index 19fccc0..a65e3c0 100644 --- a/bricktracker/sql/checkbox/update/name.sql +++ b/bricktracker/sql/checkbox/update/field.sql @@ -1,3 +1,3 @@ UPDATE "bricktracker_set_checkboxes" -SET "name" = :safe_name +SET "{{field}}" = :value WHERE "bricktracker_set_checkboxes"."id" IS NOT DISTINCT FROM :id diff --git a/bricktracker/sql/checkbox/update/status.sql b/bricktracker/sql/checkbox/update/status.sql deleted file mode 100644 index 3c04c22..0000000 --- a/bricktracker/sql/checkbox/update/status.sql +++ /dev/null @@ -1,3 +0,0 @@ -UPDATE "bricktracker_set_checkboxes" -SET "{{name}}" = :status -WHERE "bricktracker_set_checkboxes"."id" IS NOT DISTINCT FROM :id diff --git a/bricktracker/sql/set/update/status.sql b/bricktracker/sql/set/update/status.sql index 4fc78e4..7697ca5 100644 --- a/bricktracker/sql/set/update/status.sql +++ b/bricktracker/sql/set/update/status.sql @@ -2,9 +2,9 @@ INSERT INTO "bricktracker_set_statuses" ( "id", "{{name}}" ) VALUES ( - :id, - :status + :set_id, + :state ) ON CONFLICT("id") -DO UPDATE SET "{{name}}" = :status -WHERE "bricktracker_set_statuses"."id" IS NOT DISTINCT FROM :id +DO UPDATE SET "{{name}}" = :state +WHERE "bricktracker_set_statuses"."id" IS NOT DISTINCT FROM :set_id diff --git a/bricktracker/views/admin/checkbox.py b/bricktracker/views/admin/checkbox.py index 134c886..6db6c5d 100644 --- a/bricktracker/views/admin/checkbox.py +++ b/bricktracker/views/admin/checkbox.py @@ -1,5 +1,3 @@ -import logging - from flask import ( Blueprint, jsonify, @@ -15,8 +13,6 @@ from ..exceptions import exception_handler from ...reload import reload from ...set_checkbox import BrickSetCheckbox -logger = logging.getLogger(__name__) - admin_checkbox_page = Blueprint( 'admin_checkbox', __name__, @@ -71,23 +67,13 @@ def do_delete(*, id: str) -> Response: return redirect(url_for('admin.admin', open_checkbox=True)) -# Change the status of a checkbox -@admin_checkbox_page.route('//status/', methods=['POST']) +# Change the field of a checkbox +@admin_checkbox_page.route('//field/', methods=['POST']) @login_required @exception_handler(__file__, json=True) -def update_status(*, id: str, name: str) -> Response: - value: bool = request.json.get('value', False) # type: ignore - +def update_field(*, id: str, name: str) -> Response: checkbox = BrickSetCheckbox().select_specific(id) - checkbox.update_status(name, value) - - # Info - logger.info('Checkbox {name} ({id}): status "{status}" changed to "{state}"'.format( # noqa: E501 - name=checkbox.fields.name, - id=checkbox.fields.id, - status=name, - state=value, - )) + value = checkbox.update_field(name, json=request.json) reload() diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py index 809d46b..bb3c234 100644 --- a/bricktracker/views/set.py +++ b/bricktracker/views/set.py @@ -37,26 +37,16 @@ def list() -> str: # Change the status of a checkbox -@set_page.route('//status/', methods=['POST']) +@set_page.route('//status/', methods=['POST']) @login_required @exception_handler(__file__, json=True) -def update_status(*, id: str, checkbox_id: str) -> Response: - value: bool = request.json.get('value', False) # type: ignore - +def update_status(*, id: str, metadata_id: str) -> Response: brickset = BrickSet().select_light(id) - checkbox = BrickSetCheckboxList().get(checkbox_id) + checkbox = BrickSetCheckboxList().get(metadata_id) - brickset.update_status(checkbox, value) + state = checkbox.update_set_state(brickset, request.json) - # Info - logger.info('Set {set} ({id}): status "{status}" changed to "{state}"'.format( # noqa: E501 - set=brickset.fields.set, - id=brickset.fields.id, - status=checkbox.fields.name, - state=value, - )) - - return jsonify({'value': value}) + return jsonify({'value': state}) # Ask for deletion of a set diff --git a/templates/admin/checkbox.html b/templates/admin/checkbox.html index 0ba484a..22a3215 100644 --- a/templates/admin/checkbox.html +++ b/templates/admin/checkbox.html @@ -18,7 +18,7 @@