Wish requesters
This commit is contained in:
parent
ad24506ab7
commit
64a9e063ec
@ -55,6 +55,9 @@
|
||||
- Allow for advanced migration scenarios through companion python files
|
||||
- Add a bunch of the requested fields into the database for future implementation
|
||||
|
||||
- Wish
|
||||
- Requester
|
||||
|
||||
### UI
|
||||
|
||||
- Add
|
||||
@ -101,6 +104,9 @@
|
||||
- Storage list
|
||||
- Storage content
|
||||
|
||||
- Wish
|
||||
- Requester
|
||||
|
||||
## 1.1.1: PDF Instructions Download
|
||||
|
||||
### Instructions
|
||||
|
@ -11,6 +11,7 @@ from .set_purchase_location import BrickSetPurchaseLocation
|
||||
from .set_status import BrickSetStatus
|
||||
from .set_storage import BrickSetStorage
|
||||
from .set_tag import BrickSetTag
|
||||
from .wish_owner import BrickWishOwner
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -20,7 +21,8 @@ T = TypeVar(
|
||||
BrickSetPurchaseLocation,
|
||||
BrickSetStatus,
|
||||
BrickSetStorage,
|
||||
BrickSetTag
|
||||
BrickSetTag,
|
||||
BrickWishOwner
|
||||
)
|
||||
|
||||
|
||||
|
@ -14,6 +14,7 @@ if TYPE_CHECKING:
|
||||
from .set_storage import BrickSetStorage
|
||||
from .set_tag import BrickSetTag
|
||||
from .wish import BrickWish
|
||||
from .wish_owner import BrickWishOwner
|
||||
|
||||
T = TypeVar(
|
||||
'T',
|
||||
@ -26,6 +27,7 @@ T = TypeVar(
|
||||
'BrickSetStorage',
|
||||
'BrickSetTag',
|
||||
'BrickWish',
|
||||
'BrickWishOwner',
|
||||
'RebrickableSet'
|
||||
)
|
||||
|
||||
|
@ -6,6 +6,7 @@ from .set_status_list import BrickSetStatusList
|
||||
from .set_storage_list import BrickSetStorageList
|
||||
from .set_tag_list import BrickSetTagList
|
||||
from .theme_list import BrickThemeList
|
||||
from .wish_owner_list import BrickWishOwnerList
|
||||
|
||||
|
||||
# Reload everything related to a database after an operation
|
||||
@ -35,5 +36,8 @@ def reload() -> None:
|
||||
|
||||
# Reload themes
|
||||
BrickThemeList(force=True)
|
||||
|
||||
# Reload the wish owners
|
||||
BrickWishOwnerList.new(force=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
@ -16,4 +16,11 @@ CREATE TABLE "bricktracker_set_owners" (
|
||||
FOREIGN KEY("id") REFERENCES "bricktracker_sets"("id")
|
||||
);
|
||||
|
||||
-- Create a table for the wish owners
|
||||
CREATE TABLE "bricktracker_wish_owners" (
|
||||
"set" TEXT NOT NULL,
|
||||
PRIMARY KEY("set"),
|
||||
FOREIGN KEY("set") REFERENCES "bricktracker_wishes"("set")
|
||||
);
|
||||
|
||||
COMMIT;
|
||||
|
@ -3,6 +3,10 @@ BEGIN TRANSACTION;
|
||||
ALTER TABLE "bricktracker_set_owners"
|
||||
DROP COLUMN "owner_{{ id }}";
|
||||
|
||||
-- Also drop from wishes
|
||||
ALTER TABLE "bricktracker_wish_owners"
|
||||
DROP COLUMN "owner_{{ id }}";
|
||||
|
||||
DELETE FROM "bricktracker_metadata_owners"
|
||||
WHERE "bricktracker_metadata_owners"."id" IS NOT DISTINCT FROM '{{ id }}';
|
||||
|
||||
|
@ -3,6 +3,10 @@ BEGIN TRANSACTION;
|
||||
ALTER TABLE "bricktracker_set_owners"
|
||||
ADD COLUMN "owner_{{ id }}" BOOLEAN NOT NULL DEFAULT 0;
|
||||
|
||||
-- Also inject into wishes
|
||||
ALTER TABLE "bricktracker_wish_owners"
|
||||
ADD COLUMN "owner_{{ id }}" BOOLEAN NOT NULL DEFAULT 0;
|
||||
|
||||
INSERT INTO "bricktracker_metadata_owners" (
|
||||
"id",
|
||||
"name"
|
||||
|
@ -5,9 +5,17 @@ SELECT
|
||||
"bricktracker_wishes"."theme_id",
|
||||
"bricktracker_wishes"."number_of_parts",
|
||||
"bricktracker_wishes"."image",
|
||||
{% block owners %}
|
||||
{% if owners %}{{ owners }},{% endif %}
|
||||
{% endblock %}
|
||||
"bricktracker_wishes"."url"
|
||||
FROM "bricktracker_wishes"
|
||||
|
||||
{% if owners %}
|
||||
LEFT JOIN "bricktracker_wish_owners"
|
||||
ON "bricktracker_wishes"."set" IS NOT DISTINCT FROM "bricktracker_wish_owners"."set"
|
||||
{% endif %}
|
||||
|
||||
{% block where %}{% endblock %}
|
||||
|
||||
{% if order %}
|
||||
|
@ -1,2 +1,12 @@
|
||||
-- A bit unsafe as it does not use a prepared statement but it
|
||||
-- should not be possible to inject anything through the {{ set }} context
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
DELETE FROM "bricktracker_wishes"
|
||||
WHERE "bricktracker_wishes"."set" IS NOT DISTINCT FROM :set
|
||||
WHERE "bricktracker_wishes"."set" IS NOT DISTINCT FROM '{{ set }}';
|
||||
|
||||
DELETE FROM "bricktracker_wish_owners"
|
||||
WHERE "bricktracker_wish_owners"."set" IS NOT DISTINCT FROM '{{ set }}';
|
||||
|
||||
COMMIT;
|
10
bricktracker/sql/wish/metadata/owner/update/state.sql
Normal file
10
bricktracker/sql/wish/metadata/owner/update/state.sql
Normal file
@ -0,0 +1,10 @@
|
||||
INSERT INTO "bricktracker_wish_owners" (
|
||||
"set",
|
||||
"{{name}}"
|
||||
) VALUES (
|
||||
:set,
|
||||
:state
|
||||
)
|
||||
ON CONFLICT("set")
|
||||
DO UPDATE SET "{{name}}" = :state
|
||||
WHERE "bricktracker_wish_owners"."set" IS NOT DISTINCT FROM :set
|
@ -1,4 +1,11 @@
|
||||
from flask import Blueprint, redirect, render_template, request, url_for
|
||||
from flask import (
|
||||
Blueprint,
|
||||
jsonify,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
url_for
|
||||
)
|
||||
from flask_login import login_required
|
||||
from werkzeug.wrappers.response import Response
|
||||
|
||||
@ -6,8 +13,10 @@ from .exceptions import exception_handler
|
||||
from ..retired_list import BrickRetiredList
|
||||
from ..wish import BrickWish
|
||||
from ..wish_list import BrickWishList
|
||||
from ..wish_owner_list import BrickWishOwnerList
|
||||
|
||||
wish_page = Blueprint('wish', __name__, url_prefix='/wishlist')
|
||||
|
||||
wish_page = Blueprint('wish', __name__, url_prefix='/wishes')
|
||||
|
||||
|
||||
# Index
|
||||
@ -18,7 +27,8 @@ def list() -> str:
|
||||
'wishes.html',
|
||||
table_collection=BrickWishList().all(),
|
||||
retired=BrickRetiredList(),
|
||||
error=request.args.get('error')
|
||||
error=request.args.get('error'),
|
||||
owners=BrickWishOwnerList.list(),
|
||||
)
|
||||
|
||||
|
||||
@ -27,21 +37,60 @@ def list() -> str:
|
||||
@login_required
|
||||
@exception_handler(__file__, post_redirect='wish.list')
|
||||
def add() -> Response:
|
||||
# Grab the set number
|
||||
number: str = request.form.get('number', '')
|
||||
# Grab the set
|
||||
set: str = request.form.get('set', '')
|
||||
|
||||
if number != '':
|
||||
BrickWishList.add(number)
|
||||
if set != '':
|
||||
BrickWishList.add(set)
|
||||
|
||||
return redirect(url_for('wish.list'))
|
||||
|
||||
|
||||
# Delete a set from the wishlit
|
||||
@wish_page.route('/delete/<number>', methods=['POST'])
|
||||
# Ask for deletion of a wish
|
||||
@wish_page.route('/<set>/delete', methods=['GET'])
|
||||
@login_required
|
||||
@exception_handler(__file__)
|
||||
def delete(*, set: str) -> str:
|
||||
return render_template(
|
||||
'wish.html',
|
||||
delete=True,
|
||||
item=BrickWish().select_specific(set),
|
||||
error=request.args.get('error'),
|
||||
owners=BrickWishOwnerList.list(),
|
||||
)
|
||||
|
||||
|
||||
# Actually delete of a set
|
||||
@wish_page.route('/<set>/delete', methods=['POST'])
|
||||
@login_required
|
||||
@exception_handler(__file__, post_redirect='wish.list')
|
||||
def delete(*, number: str) -> Response:
|
||||
brickwish = BrickWish().select_specific(number)
|
||||
def do_delete(*, set: str) -> Response:
|
||||
brickwish = BrickWish().select_specific(set)
|
||||
brickwish.delete()
|
||||
|
||||
return redirect(url_for('wish.list'))
|
||||
|
||||
|
||||
# Details
|
||||
@wish_page.route('/<set>/details', methods=['GET'])
|
||||
@exception_handler(__file__)
|
||||
def details(*, set: str) -> str:
|
||||
return render_template(
|
||||
'wish.html',
|
||||
item=BrickWish().select_specific(set),
|
||||
retired=BrickRetiredList(),
|
||||
owners=BrickWishOwnerList.list(),
|
||||
)
|
||||
|
||||
|
||||
# Change the state of a owner
|
||||
@wish_page.route('/<set>/owner/<metadata_id>', methods=['POST'])
|
||||
@login_required
|
||||
@exception_handler(__file__, json=True)
|
||||
def update_owner(*, set: str, metadata_id: str) -> Response:
|
||||
brickwish = BrickWish().select_specific(set)
|
||||
owner = BrickWishOwnerList.get(metadata_id)
|
||||
|
||||
state = owner.update_wish_state(brickwish, json=request.json)
|
||||
|
||||
return jsonify({'value': state})
|
||||
|
@ -5,6 +5,7 @@ from flask import url_for
|
||||
from .exceptions import NotFoundException
|
||||
from .rebrickable_set import RebrickableSet
|
||||
from .sql import BrickSQL
|
||||
from .wish_owner_list import BrickWishOwnerList
|
||||
|
||||
|
||||
# Lego brick wished set
|
||||
@ -16,11 +17,11 @@ class BrickWish(RebrickableSet):
|
||||
select_query: str = 'wish/select'
|
||||
insert_query: str = 'wish/insert'
|
||||
|
||||
# Delete a wished set
|
||||
# Delete a wish
|
||||
def delete(self, /) -> None:
|
||||
BrickSQL().execute_and_commit(
|
||||
BrickSQL().executescript(
|
||||
'wish/delete/wish',
|
||||
parameters=self.sql_parameters()
|
||||
set=self.fields.set
|
||||
)
|
||||
|
||||
# Select a specific part (with a set and an id)
|
||||
@ -29,7 +30,7 @@ class BrickWish(RebrickableSet):
|
||||
self.fields.set = set
|
||||
|
||||
# Load from database
|
||||
if not self.select():
|
||||
if not self.select(owners=BrickWishOwnerList.as_columns()):
|
||||
raise NotFoundException(
|
||||
'Wish for set {set} was not found in the database'.format( # noqa: E501
|
||||
set=self.fields.set,
|
||||
@ -38,6 +39,14 @@ class BrickWish(RebrickableSet):
|
||||
|
||||
return self
|
||||
|
||||
# Self url
|
||||
def url(self, /) -> str:
|
||||
return url_for('wish.details', set=self.fields.set)
|
||||
|
||||
# Deletion url
|
||||
def url_for_delete(self, /) -> str:
|
||||
return url_for('wish.delete', number=self.fields.set)
|
||||
return url_for('wish.delete', set=self.fields.set)
|
||||
|
||||
# Actual deletion url
|
||||
def url_for_do_delete(self, /) -> str:
|
||||
return url_for('wish.do_delete', set=self.fields.set)
|
||||
|
@ -9,6 +9,7 @@ from .rebrickable import Rebrickable
|
||||
from .rebrickable_image import RebrickableImage
|
||||
from .record_list import BrickRecordList
|
||||
from .wish import BrickWish
|
||||
from .wish_owner_list import BrickWishOwnerList
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -22,7 +23,8 @@ class BrickWishList(BrickRecordList[BrickWish]):
|
||||
def all(self, /) -> Self:
|
||||
# Load the wished sets from the database
|
||||
for record in self.select(
|
||||
order=current_app.config['WISHES_DEFAULT_ORDER']
|
||||
order=current_app.config['WISHES_DEFAULT_ORDER'],
|
||||
owners=BrickWishOwnerList.as_columns(),
|
||||
):
|
||||
brickwish = BrickWish(record=record)
|
||||
|
||||
|
70
bricktracker/wish_owner.py
Normal file
70
bricktracker/wish_owner.py
Normal file
@ -0,0 +1,70 @@
|
||||
import logging
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
from flask import url_for
|
||||
|
||||
from .exceptions import DatabaseException
|
||||
from .metadata import BrickMetadata
|
||||
from .sql import BrickSQL
|
||||
if TYPE_CHECKING:
|
||||
from .wish import BrickWish
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Lego wish owner metadata
|
||||
class BrickWishOwner(BrickMetadata):
|
||||
kind: str = 'owner'
|
||||
|
||||
# Wish state endpoint
|
||||
wish_state_endpoint: str = 'wish.update_owner'
|
||||
|
||||
# Queries
|
||||
update_wish_state_query: str = 'wish/metadata/owner/update/state'
|
||||
|
||||
# Update the selected state of this metadata item for a wish
|
||||
def update_wish_state(
|
||||
self,
|
||||
brickset: 'BrickWish',
|
||||
/,
|
||||
*,
|
||||
json: Any | None = None,
|
||||
state: Any | None = None
|
||||
) -> Any:
|
||||
if state is None and json is not None:
|
||||
state = json.get('value', False)
|
||||
|
||||
parameters = self.sql_parameters()
|
||||
parameters['set'] = brickset.fields.set
|
||||
parameters['state'] = state
|
||||
|
||||
rows, _ = BrickSQL().execute_and_commit(
|
||||
self.update_wish_state_query,
|
||||
parameters=parameters,
|
||||
name=self.as_column(),
|
||||
)
|
||||
|
||||
if rows != 1:
|
||||
raise DatabaseException('Could not update the {kind} "{name}" state for wish {set}'.format( # noqa: E501
|
||||
kind=self.kind,
|
||||
name=self.fields.name,
|
||||
set=brickset.fields.set,
|
||||
))
|
||||
|
||||
# Info
|
||||
logger.info('{kind} "{name}" state changed to "{state}" for wish {set}'.format( # noqa: E501
|
||||
kind=self.kind,
|
||||
name=self.fields.name,
|
||||
state=state,
|
||||
set=brickset.fields.set,
|
||||
))
|
||||
|
||||
return state
|
||||
|
||||
# URL to change the selected state of this metadata item for a wish
|
||||
def url_for_wish_state(self, set: str, /) -> str:
|
||||
return url_for(
|
||||
self.wish_state_endpoint,
|
||||
set=set,
|
||||
metadata_id=self.fields.id
|
||||
)
|
21
bricktracker/wish_owner_list.py
Normal file
21
bricktracker/wish_owner_list.py
Normal file
@ -0,0 +1,21 @@
|
||||
from typing import Self
|
||||
|
||||
from .metadata_list import BrickMetadataList
|
||||
from .wish_owner import BrickWishOwner
|
||||
|
||||
|
||||
# Lego sets owner list
|
||||
class BrickWishOwnerList(BrickMetadataList[BrickWishOwner]):
|
||||
kind: str = 'wish owners'
|
||||
|
||||
# Database
|
||||
table: str = 'bricktracker_wish_owners'
|
||||
order: str = '"bricktracker_metadata_owners"."name"'
|
||||
|
||||
# Queries
|
||||
select_query = 'set/metadata/owner/list'
|
||||
|
||||
# Instantiate the list with the proper class
|
||||
@classmethod
|
||||
def new(cls, /, *, force: bool = False) -> Self:
|
||||
return cls(BrickWishOwner, force=force)
|
15
templates/wish.html
Normal file
15
templates/wish.html
Normal file
@ -0,0 +1,15 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block title %} - {% if delete %}Delete a wish{% else %}Wish{% endif %} {{ item.fields.name}} ({{ item.fields.set }}){% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
{% with solo=true, delete=delete %}
|
||||
{% include 'wish/card.html' %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
49
templates/wish/card.html
Normal file
49
templates/wish/card.html
Normal file
@ -0,0 +1,49 @@
|
||||
{% import 'macro/accordion.html' as accordion %}
|
||||
{% import 'macro/badge.html' as badge %}
|
||||
{% import 'macro/card.html' as card %}
|
||||
{% import 'macro/form.html' as form %}
|
||||
|
||||
<div class="card mb-3 flex-fill {% if solo %}card-solo{% endif %}">
|
||||
{{ card.header(item, item.fields.name, solo=solo, identifier=item.fields.set) }}
|
||||
{{ card.image(item, solo=solo, last=last, caption=item.fields.name, alt=item.fields.set) }}
|
||||
<div class="card-body border-bottom-0 {% if not solo %}p-1{% endif %}">
|
||||
{{ badge.theme(item.theme.name, solo=solo, last=last) }}
|
||||
{{ badge.parts(item.fields.number_of_parts, solo=solo, last=last) }}
|
||||
{% for owner in owners %}
|
||||
{{ badge.owner(item, owner, solo=solo, last=last) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if solo and g.login.is_authenticated() %}
|
||||
<div class="accordion accordion-flush border-top" id="wish-details">
|
||||
{% if not delete %}
|
||||
{{ accordion.header('Requester', 'owner', 'wish-details', icon='group-line', class='p-0') }}
|
||||
<ul class="list-group list-group-flush">
|
||||
{% if owners | length %}
|
||||
{% for owner in owners %}
|
||||
<li class="d-flex list-group-item list-group-item-action text-nowrap">{{ form.checkbox(owner.fields.name, item.fields.set, owner.as_dataset(), owner.url_for_wish_state(item.fields.set), item.fields[owner.as_column()], delete=delete) }}</li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<li class="list-group-item list-group-item-action text-center"><i class="ri-error-warning-line"></i> No requester found.</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<div class="list-group list-group-flush border-top">
|
||||
<a class="list-group-item list-group-item-action" href="{{ url_for('admin.admin', open_owner=true) }}"><i class="ri-settings-4-line"></i> Manage the set owners</a>
|
||||
</div>
|
||||
{{ accordion.footer() }}
|
||||
{% endif %}
|
||||
{{ accordion.header('Danger zone', 'danger-zone', 'wish-details', expanded=delete, danger=true, class='text-end') }}
|
||||
{% if delete %}
|
||||
<form action="{{ item.url_for_do_delete() }}" method="post">
|
||||
{% if error %}<div class="alert alert-danger text-start" role="alert"><strong>Error:</strong> {{ error }}.</div>{% endif %}
|
||||
<div class="alert alert-danger text-center" role="alert">You are about to delete a wish. This action is irreversible.</div>
|
||||
<a class="btn btn-primary" href="{{ item.url() }}" role="button"><i class="ri-arrow-left-long-line"></i> Back to the wish</a>
|
||||
<button type="submit" class="btn btn-danger"><i class="ri-close-line"></i> Delete the wish</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<a href="{{ item.url_for_delete() }}" class="btn btn-danger" role="button"><i class="ri-close-line"></i> Delete the wish</a>
|
||||
{% endif %}
|
||||
{{ accordion.footer() }}
|
||||
</div>
|
||||
<div class="card-footer"></div>
|
||||
{% endif %}
|
||||
</div>
|
@ -12,6 +12,7 @@
|
||||
<th scope="col"><i class="ri-calendar-line fw-normal"></i> Year</th>
|
||||
<th scope="col"><i class="ri-shapes-line fw-normal"></i> Parts</th>
|
||||
<th scope="col"><i class="ri-calendar-close-line fw-normal"></i> Retirement</th>
|
||||
<th data-table-no-sort="true" class="no-sort" scope="col"><i class="ri-user-line fw-normal"></i> Requesters</th>
|
||||
{% if g.login.is_authenticated() %}
|
||||
<th data-table-no-sort-and-search="true" class="no-sort" scope="col"><i class="ri-settings-4-line fw-normal"></i> Actions</th>
|
||||
{% endif %}
|
||||
@ -28,11 +29,15 @@
|
||||
<td>{{ item.fields.year }}</td>
|
||||
<td>{{ item.fields.number_of_parts }}</td>
|
||||
<td>{% if retirement_date %}{{ retirement_date }}{% else %}<span class="badge rounded-pill text-bg-light border">Not found</span>{% endif %}</td>
|
||||
<td>
|
||||
{% for owner in owners %}
|
||||
{{ badge.owner(item, owner) }}
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% if g.login.is_authenticated() %}
|
||||
<td>
|
||||
<form action="{{ item.url_for_delete() }}" method="post">
|
||||
<button type="submit" class="btn btn-sm btn-danger"><i class="ri-delete-bin-2-line"></i> Delete</button>
|
||||
</form>
|
||||
<a href="{{ item.url() }}" class="btn btn-sm btn-primary mb-1" role="button"><i class="ri-gift-line"></i> Details</a>
|
||||
<a href="{{ item.url_for_delete() }}" class="btn btn-sm btn-danger mb-1" role="button"><i class="ri-delete-bin-2-line"></i> Delete</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
|
@ -12,7 +12,7 @@
|
||||
<label class="visually-hidden" for="number">Set number</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group-text"><i class="ri-hashtag"></i></div>
|
||||
<input type="text" class="form-control" id="number" name="number" placeholder="107-1 or 1642-1 or ..." value="" autocomplete="off">
|
||||
<input type="text" class="form-control" id="set" name="set" placeholder="107-1 or 1642-1 or ..." value="" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
|
Loading…
Reference in New Issue
Block a user