From f0cec23da9566f044bd8435d5baeebe36b4901db Mon Sep 17 00:00:00 2001 From: Gregoo Date: Tue, 4 Feb 2025 17:03:39 +0100 Subject: [PATCH] Set purchase date and price --- .env.sample | 9 ++ CHANGELOG.md | 6 +- bricktracker/config.py | 2 + bricktracker/set.py | 111 +++++++++++++++++- bricktracker/sql/migrations/0007.sql | 2 +- bricktracker/sql/set/base/base.sql | 2 + bricktracker/sql/set/update/purchase_date.sql | 3 + .../sql/set/update/purchase_price.sql | 3 + bricktracker/views/set.py | 24 ++++ docs/development.md | 1 + static/scripts/changer.js | 17 ++- templates/base.html | 2 + templates/macro/badge.html | 20 +++- templates/macro/form.html | 7 +- templates/set/card.html | 6 +- templates/set/management.html | 25 ++-- templates/set/sort.html | 4 + 17 files changed, 228 insertions(+), 16 deletions(-) create mode 100644 bricktracker/sql/set/update/purchase_date.sql create mode 100644 bricktracker/sql/set/update/purchase_price.sql diff --git a/.env.sample b/.env.sample index 8f12939..f4d6a48 100644 --- a/.env.sample +++ b/.env.sample @@ -168,6 +168,15 @@ # Default: 3333 # BK_PORT=3333 +# Optional: Format of the timestamp for purchase dates +# Check https://docs.python.org/3/library/time.html#time.strftime for format details +# Default: %d/%m/%Y +# BK_PURCHASE_DATE_FORMAT=%m/%d/%Y + +# Optional: Currency to display for purchase prices. +# Default: € +# BK_PURCHASE_CURRENCY=£ + # Optional: Change the default order of purchase locations. By default ordered by insertion order. # Useful column names for this option are: # - "bricktracker_metadata_purchase_locations"."name" ASC: storage name diff --git a/CHANGELOG.md b/CHANGELOG.md index efcdb16..2bbb26c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ - Added: `BK_HIDE_ALL_STORAGES`, hide the "Storages" menu entry - Added: `BK_STORAGE_DEFAULT_ORDER`, ordering of storages - Added: `BK_PURCHASE_LOCATION_DEFAULT_ORDER`, ordering of purchase locations +- Added: `BK_PURCHASE_CURRENCY`, currency to display for purchase prices +- Added: `BK_PURCHASE_DATE_FORMAT`, date format for purchase dates ### Code @@ -40,7 +42,7 @@ - Ownership - Tags - Storage - - Purchase location + - Purchase location, date, price - Storage - Storage content and list @@ -87,7 +89,7 @@ - Tags - Refresh - Storage - - Purchase location + - Purchase location, date, price - Sets grid - Collapsible controls depending on screen size diff --git a/bricktracker/config.py b/bricktracker/config.py index 729049a..5452f93 100644 --- a/bricktracker/config.py +++ b/bricktracker/config.py @@ -41,6 +41,8 @@ CONFIG: Final[list[dict[str, Any]]] = [ {'n': 'PARTS_DEFAULT_ORDER', 'd': '"rebrickable_parts"."name" ASC, "rebrickable_parts"."color_name" ASC, "bricktracker_parts"."spare" ASC'}, # noqa: E501 {'n': 'PARTS_FOLDER', 'd': 'parts', 's': True}, {'n': 'PORT', 'd': 3333, 'c': int}, + {'n': 'PURCHASE_DATE_FORMAT', 'd': '%d/%m/%Y'}, + {'n': 'PURCHASE_CURRENCY', 'd': '€'}, {'n': 'PURCHASE_LOCATION_DEFAULT_ORDER', 'd': '"bricktracker_metadata_purchase_locations"."name" ASC'}, # noqa: E501 {'n': 'RANDOM', 'e': 'RANDOM', 'c': bool}, {'n': 'REBRICKABLE_API_KEY', 'e': 'REBRICKABLE_API_KEY', 'd': ''}, diff --git a/bricktracker/set.py b/bricktracker/set.py index fb97209..c397b13 100644 --- a/bricktracker/set.py +++ b/bricktracker/set.py @@ -1,3 +1,4 @@ +from datetime import datetime import logging import traceback from typing import Any, Self, TYPE_CHECKING @@ -5,7 +6,7 @@ from uuid import uuid4 from flask import current_app, url_for -from .exceptions import NotFoundException +from .exceptions import NotFoundException, DatabaseException, ErrorException from .minifigure_list import BrickMinifigureList from .part_list import BrickPartList from .rebrickable_set import RebrickableSet @@ -27,6 +28,8 @@ class BrickSet(RebrickableSet): select_query: str = 'set/select/full' light_query: str = 'set/select/light' insert_query: str = 'set/insert' + update_purchase_date_query: str = 'set/update/purchase_date' + update_purchase_price_query: str = 'set/update/purchase_price' # Delete a set def delete(self, /) -> None: @@ -152,6 +155,30 @@ class BrickSet(RebrickableSet): return True + # Purchase date + def purchase_date(self, /, *, standard: bool = False) -> str: + if self.fields.purchase_date is not None: + time = datetime.fromtimestamp(self.fields.purchase_date) + + if standard: + return time.strftime('%Y/%m/%d') + else: + return time.strftime( + current_app.config['PURCHASE_DATE_FORMAT'] + ) + else: + return '' + + # Purchase price with currency + def purchase_price(self, /) -> str: + if self.fields.purchase_price is not None: + return '{price}{currency}'.format( + price=self.fields.purchase_price, + currency=current_app.config['PURCHASE_CURRENCY'] + ) + else: + return '' + # Minifigures def minifigures(self, /) -> BrickMinifigureList: return BrickMinifigureList().from_set(self) @@ -194,6 +221,80 @@ class BrickSet(RebrickableSet): return self + # Update the purchase date + def update_purchase_date(self, json: Any | None, /) -> Any: + value = json.get('value', None) # type: ignore + + try: + if value == '': + value = None + + if value is not None: + value = datetime.strptime(value, '%Y/%m/%d').timestamp() + except Exception: + raise ErrorException('{value} is not a date'.format( + value=value, + )) + + self.fields.purchase_date = value + + rows, _ = BrickSQL().execute_and_commit( + self.update_purchase_date_query, + parameters=self.sql_parameters() + ) + + if rows != 1: + raise DatabaseException('Could not update the purchase date for set {set} ({id})'.format( # noqa: E501 + set=self.fields.set, + id=self.fields.id, + )) + + # Info + logger.info('Purchase date changed to "{value}" for set {set} ({id})'.format( # noqa: E501 + value=value, + set=self.fields.set, + id=self.fields.id, + )) + + return value + + # Update the purchase price + def update_purchase_price(self, json: Any | None, /) -> Any: + value = json.get('value', None) # type: ignore + + try: + if value == '': + value = None + + if value is not None: + value = float(value) + except Exception: + raise ErrorException('{value} is not a number or empty'.format( + value=value, + )) + + self.fields.purchase_price = value + + rows, _ = BrickSQL().execute_and_commit( + self.update_purchase_price_query, + parameters=self.sql_parameters() + ) + + if rows != 1: + raise DatabaseException('Could not update the purchase price for set {set} ({id})'.format( # noqa: E501 + set=self.fields.set, + id=self.fields.id, + )) + + # Info + logger.info('Purchase price changed to "{value}" for set {set} ({id})'.format( # noqa: E501 + value=value, + set=self.fields.set, + id=self.fields.id, + )) + + return value + # Self url def url(self, /) -> str: return url_for('set.details', id=self.fields.id) @@ -230,3 +331,11 @@ class BrickSet(RebrickableSet): return url_for('storage.details', id=self.fields.storage) else: return '' + + # Update purchase date url + def url_for_purchase_date(self, /) -> str: + return url_for('set.update_purchase_date', id=self.fields.id) + + # Update purchase price url + def url_for_purchase_price(self, /) -> str: + return url_for('set.update_purchase_price', id=self.fields.id) diff --git a/bricktracker/sql/migrations/0007.sql b/bricktracker/sql/migrations/0007.sql index 7d52d33..720be76 100644 --- a/bricktracker/sql/migrations/0007.sql +++ b/bricktracker/sql/migrations/0007.sql @@ -27,7 +27,7 @@ CREATE TABLE "bricktracker_sets" ( "set" TEXT NOT NULL, "description" TEXT, "storage" TEXT, -- Storage bin location - "purchase_date" INTEGER, -- Purchase data + "purchase_date" REAL, -- Purchase data "purchase_location" TEXT, -- Purchase location "purchase_price" REAL, -- Purchase price PRIMARY KEY("id"), diff --git a/bricktracker/sql/set/base/base.sql b/bricktracker/sql/set/base/base.sql index 333868d..02ef771 100644 --- a/bricktracker/sql/set/base/base.sql +++ b/bricktracker/sql/set/base/base.sql @@ -1,7 +1,9 @@ SELECT {% block id %}{% endblock %} "bricktracker_sets"."storage", + "bricktracker_sets"."purchase_date", "bricktracker_sets"."purchase_location", + "bricktracker_sets"."purchase_price", "rebrickable_sets"."set", "rebrickable_sets"."number", "rebrickable_sets"."version", diff --git a/bricktracker/sql/set/update/purchase_date.sql b/bricktracker/sql/set/update/purchase_date.sql new file mode 100644 index 0000000..cb8516e --- /dev/null +++ b/bricktracker/sql/set/update/purchase_date.sql @@ -0,0 +1,3 @@ +UPDATE "bricktracker_sets" +SET "purchase_date" = :purchase_date +WHERE "bricktracker_sets"."id" IS NOT DISTINCT FROM :id diff --git a/bricktracker/sql/set/update/purchase_price.sql b/bricktracker/sql/set/update/purchase_price.sql new file mode 100644 index 0000000..f742a8f --- /dev/null +++ b/bricktracker/sql/set/update/purchase_price.sql @@ -0,0 +1,3 @@ +UPDATE "bricktracker_sets" +SET "purchase_price" = :purchase_price +WHERE "bricktracker_sets"."id" IS NOT DISTINCT FROM :id diff --git a/bricktracker/views/set.py b/bricktracker/views/set.py index 0777f4e..f1493b8 100644 --- a/bricktracker/views/set.py +++ b/bricktracker/views/set.py @@ -41,6 +41,18 @@ def list() -> str: ) +# Change the value of purchase date +@set_page.route('//purchase_date', methods=['POST']) +@login_required +@exception_handler(__file__, json=True) +def update_purchase_date(*, id: str) -> Response: + brickset = BrickSet().select_light(id) + + value = brickset.update_purchase_date(request.json) + + return jsonify({'value': value}) + + # Change the value of purchase location @set_page.route('//purchase_location', methods=['POST']) @login_required @@ -60,6 +72,18 @@ def update_purchase_location(*, id: str) -> Response: return jsonify({'value': value}) +# Change the value of purchase price +@set_page.route('//purchase_price', methods=['POST']) +@login_required +@exception_handler(__file__, json=True) +def update_purchase_price(*, id: str) -> Response: + brickset = BrickSet().select_light(id) + + value = brickset.update_purchase_price(request.json) + + return jsonify({'value': value}) + + # Change the state of a owner @set_page.route('//owner/', methods=['POST']) @login_required diff --git a/docs/development.md b/docs/development.md index 8657590..dd3beee 100644 --- a/docs/development.md +++ b/docs/development.md @@ -21,6 +21,7 @@ It also uses the following libraries and frameworks: - `tinysort` (https://github.com/Sjeiti/TinySort) - `sortable` (https://github.com/tofsjonas/sortable) - `simple-datatables` (https://github.com/fiduswriter/simple-datatables) +- `vanillajs-datepicker` (https://github.com/mymth/vanillajs-datepicker) The BrickTracker brick logo is part of the Small n' Flat Icons set designed by [Arnaud Chesne](https://iconduck.com/designers/arnaud-chesne). diff --git a/static/scripts/changer.js b/static/scripts/changer.js index 4db9cd7..ffa41ac 100644 --- a/static/scripts/changer.js +++ b/static/scripts/changer.js @@ -1,5 +1,6 @@ // Generic state changer with visual feedback -// Tooltips require boostrap.Tooltip +// Tooltips requires boostrap.Tooltip +// Date requires vanillajs-datepicker class BrickChanger { constructor(prefix, id, url, parent = undefined) { this.prefix = prefix @@ -51,6 +52,20 @@ class BrickChanger { changer.change(); })(this)); } + + // Date picker + this.picker = undefined; + if (this.html_element.dataset.changerDate == "true") { + this.picker = new Datepicker(this.html_element, { + buttonClass: 'btn', + format: 'yyyy/mm/dd', + }); + + // Picker fires a custom "changeDate" event + this.html_element.addEventListener("changeDate", ((changer) => (e) => { + changer.change(); + })(this)); + } } // Clean the status diff --git a/templates/base.html b/templates/base.html index a82a4ea..658ef37 100644 --- a/templates/base.html +++ b/templates/base.html @@ -9,6 +9,7 @@ + @@ -78,6 +79,7 @@ + diff --git a/templates/macro/badge.html b/templates/macro/badge.html index f1c9f89..ee68a69 100644 --- a/templates/macro/badge.html +++ b/templates/macro/badge.html @@ -65,6 +65,15 @@ {% endif %} {% endmacro %} +{% macro purchase_date(date, solo=false, last=false) %} + {% if last %} + {% set tooltip=date %} + {% else %} + {% set text=date %} + {% endif %} + {{ badge(check=date, solo=solo, last=last, color='light border', icon='calendar-line', text=text, tooltip=tooltip, collapsible='Date:') }} +{% endmacro %} + {% macro purchase_location(item, purchase_locations, solo=false, last=false) %} {% if purchase_locations and item.fields.purchase_location in purchase_locations.mapping %} {% set purchase_location = purchase_locations.mapping[item.fields.purchase_location] %} @@ -73,10 +82,19 @@ {% else %} {% set text=purchase_location.fields.name %} {% endif %} - {{ badge(check=purchase_location, solo=solo, last=last, color='light border', icon='building-line', text=text, tooltip=tooltip) }} + {{ badge(check=purchase_location, solo=solo, last=last, color='light border', icon='building-line', text=text, tooltip=tooltip, collapsible='Location:') }} {% endif %} {% endmacro %} +{% macro purchase_price(price, solo=false, last=false) %} + {% if last %} + {% set tooltip=price %} + {% else %} + {% set text=price %} + {% endif %} + {{ badge(check=price, solo=solo, last=last, color='light border', icon='wallet-3-line', text=text, tooltip=tooltip, collapsible='Price:') }} +{% endmacro %} + {% macro set(set, solo=false, last=false, url=None, id=None) %} {% if id %} {% set url=url_for('set.details', id=id) %} diff --git a/templates/macro/form.html b/templates/macro/form.html index 45ec2b5..9564f35 100644 --- a/templates/macro/form.html +++ b/templates/macro/form.html @@ -17,19 +17,22 @@ {% endif %} {% endmacro %} -{% macro input(name, id, prefix, url, value, all=none, read_only=none) %} +{% macro input(name, id, prefix, url, value, all=none, read_only=none, icon=none, suffix=none, date=false) %} {% if all or read_only %} {{ value }} {% else %}
+ {% if icon %} {{ name }}{% endif %} + {% if suffix %}{{ suffix }}{% endif %} {% if g.login.is_authenticated() %} @@ -45,7 +48,7 @@ {% set prefix=metadata_list.as_prefix() %}
- {% if icon %}{% endif %} + {% if icon %} {{ name }}{% endif %}