feat(add): set metadata on add — purchase, notes, statuses
Allow setting owners, purchase (date, price, location), notes, statuses, storage, and tags directly from the add set form. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -94,6 +94,36 @@ class BrickSet(RebrickableSet):
|
||||
)
|
||||
self.fields.purchase_location = purchase_location.fields.id
|
||||
|
||||
# Save the purchase date
|
||||
purchase_date = data.get('purchase_date', None)
|
||||
if purchase_date == '':
|
||||
purchase_date = None
|
||||
if purchase_date is not None:
|
||||
try:
|
||||
purchase_date = datetime.strptime(
|
||||
purchase_date, '%Y/%m/%d'
|
||||
).timestamp()
|
||||
except Exception:
|
||||
purchase_date = None
|
||||
self.fields.purchase_date = purchase_date
|
||||
|
||||
# Save the purchase price
|
||||
purchase_price = data.get('purchase_price', None)
|
||||
if purchase_price == '':
|
||||
purchase_price = None
|
||||
if purchase_price is not None:
|
||||
try:
|
||||
purchase_price = float(purchase_price)
|
||||
except Exception:
|
||||
purchase_price = None
|
||||
self.fields.purchase_price = purchase_price
|
||||
|
||||
# Save the description/notes
|
||||
description = data.get('description', None)
|
||||
if description == '':
|
||||
description = None
|
||||
self.fields.description = description
|
||||
|
||||
# Insert into database (deferred - will execute at final commit)
|
||||
# All operations are atomic - if anything fails, nothing is committed
|
||||
self.insert(commit=False)
|
||||
@@ -105,6 +135,13 @@ class BrickSet(RebrickableSet):
|
||||
owner = BrickSetOwnerList.get(id)
|
||||
owner.update_set_state(self, state=True, commit=False)
|
||||
|
||||
# Save the statuses (deferred - will execute at final commit)
|
||||
statuses: list[str] = list(data.get('statuses', []))
|
||||
|
||||
for id in statuses:
|
||||
status = BrickSetStatusList.get(id)
|
||||
status.update_set_state(self, state=True, commit=False)
|
||||
|
||||
# Save the tags (deferred - will execute at final commit)
|
||||
tags: list[str] = list(data.get('tags', []))
|
||||
|
||||
|
||||
@@ -2,10 +2,16 @@ INSERT OR IGNORE INTO "bricktracker_sets" (
|
||||
"id",
|
||||
"set",
|
||||
"storage",
|
||||
"purchase_location"
|
||||
"purchase_location",
|
||||
"purchase_date",
|
||||
"purchase_price",
|
||||
"description"
|
||||
) VALUES (
|
||||
:id,
|
||||
:set,
|
||||
:storage,
|
||||
:purchase_location
|
||||
:purchase_location,
|
||||
:purchase_date,
|
||||
:purchase_price,
|
||||
:description
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ from flask_login import login_required
|
||||
from ..configuration_list import BrickConfigurationList
|
||||
from .exceptions import exception_handler
|
||||
from ..set_list import set_metadata_lists
|
||||
from ..set_status_list import BrickSetStatusList
|
||||
from ..socket import MESSAGES
|
||||
|
||||
add_page = Blueprint('add', __name__, url_prefix='/add')
|
||||
@@ -21,6 +22,7 @@ def add() -> str:
|
||||
path=current_app.config['SOCKET_PATH'],
|
||||
namespace=current_app.config['SOCKET_NAMESPACE'],
|
||||
messages=MESSAGES,
|
||||
brickset_statuses=BrickSetStatusList.list(all=True),
|
||||
**set_metadata_lists()
|
||||
)
|
||||
|
||||
@@ -38,6 +40,7 @@ def bulk() -> str:
|
||||
namespace=current_app.config['SOCKET_NAMESPACE'],
|
||||
messages=MESSAGES,
|
||||
bulk=True,
|
||||
brickset_statuses=BrickSetStatusList.list(all=True),
|
||||
**set_metadata_lists()
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
// Add page - handles both sets and individual minifigures
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// Initialize date pickers
|
||||
document.querySelectorAll('[data-add-date="true"]').forEach(el => {
|
||||
new Datepicker(el, {
|
||||
buttonClass: 'btn',
|
||||
format: 'yyyy/mm/dd',
|
||||
});
|
||||
});
|
||||
|
||||
// Get template data from data attributes
|
||||
const addContainer = document.getElementById('add-set');
|
||||
if (!addContainer) return;
|
||||
|
||||
@@ -16,9 +16,13 @@ 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_purchase_date = document.getElementById(`${id}-purchase-date`);
|
||||
this.html_purchase_location = document.getElementById(`${id}-purchase-location`);
|
||||
this.html_purchase_price = document.getElementById(`${id}-purchase-price`);
|
||||
this.html_statuses = document.getElementById(`${id}-statuses`);
|
||||
this.html_storage = document.getElementById(`${id}-storage`);
|
||||
this.html_tags = document.getElementById(`${id}-tags`);
|
||||
this.html_description = document.getElementById(`${id}-description`);
|
||||
|
||||
// Card elements
|
||||
this.html_card = document.getElementById(`${id}-card`);
|
||||
@@ -181,12 +185,44 @@ class BrickSetSocket extends BrickSocket {
|
||||
this.html_progress_bar.scrollIntoView();
|
||||
}
|
||||
|
||||
// Grab the purchase date
|
||||
let purchase_date = '';
|
||||
if (this.html_purchase_date) {
|
||||
purchase_date = this.html_purchase_date.value;
|
||||
}
|
||||
|
||||
// Grab the purchase price
|
||||
let purchase_price = '';
|
||||
if (this.html_purchase_price) {
|
||||
purchase_price = this.html_purchase_price.value;
|
||||
}
|
||||
|
||||
// Grab the statuses
|
||||
const statuses = [];
|
||||
if (this.html_statuses) {
|
||||
this.html_statuses.querySelectorAll('input').forEach(input => {
|
||||
if (input.checked) {
|
||||
statuses.push(input.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Grab the description/notes
|
||||
let description = '';
|
||||
if (this.html_description) {
|
||||
description = this.html_description.value;
|
||||
}
|
||||
|
||||
this.socket.emit(this.messages.IMPORT_SET, {
|
||||
set: (set !== undefined) ? set : this.html_input.value,
|
||||
owners: owners,
|
||||
purchase_date: purchase_date,
|
||||
purchase_location: purchase_location,
|
||||
purchase_price: purchase_price,
|
||||
statuses: statuses,
|
||||
storage: storage,
|
||||
tags: tags,
|
||||
description: description,
|
||||
refresh: this.refresh
|
||||
});
|
||||
} else {
|
||||
@@ -361,10 +397,22 @@ class BrickSetSocket extends BrickSocket {
|
||||
this.html_owners.querySelectorAll('input').forEach(input => input.disabled = !enabled);
|
||||
}
|
||||
|
||||
if (this.html_purchase_date) {
|
||||
this.html_purchase_date.disabled = !enabled;
|
||||
}
|
||||
|
||||
if (this.html_purchase_location) {
|
||||
this.html_purchase_location.disabled = !enabled;
|
||||
}
|
||||
|
||||
if (this.html_purchase_price) {
|
||||
this.html_purchase_price.disabled = !enabled;
|
||||
}
|
||||
|
||||
if (this.html_statuses) {
|
||||
this.html_statuses.querySelectorAll('input').forEach(input => input.disabled = !enabled);
|
||||
}
|
||||
|
||||
if (this.html_storage) {
|
||||
this.html_storage.disabled = !enabled;
|
||||
}
|
||||
@@ -373,6 +421,10 @@ class BrickSetSocket extends BrickSocket {
|
||||
this.html_tags.querySelectorAll('input').forEach(input => input.disabled = !enabled);
|
||||
}
|
||||
|
||||
if (this.html_description) {
|
||||
this.html_description.disabled = !enabled;
|
||||
}
|
||||
|
||||
if (this.html_card_confirm) {
|
||||
this.html_card_confirm.disabled = !enabled;
|
||||
}
|
||||
|
||||
+81
-50
@@ -63,30 +63,43 @@
|
||||
</div>
|
||||
<h6 class="border-bottom mt-2">Metadata</h6>
|
||||
<div class="accordion accordion" id="metadata">
|
||||
{% if not (brickset_owners | length) and not (brickset_purchase_locations | length) and not (brickset_storages | length) and not (brickset_tags | length) %}
|
||||
<div class="alert alert-warning" role="alert">
|
||||
You have no metadata configured.
|
||||
You can add entries in the <a href="{{ url_for('admin.admin', open_metadata=true) }}" class="btn btn-warning" role="button"><i class="ri-profile-line"></i> Set metadata management</a> section of the Admin panel.
|
||||
{% if brickset_owners | length %}
|
||||
{{ accordion.header('Owners', 'owners', 'metadata', icon='user-line') }}
|
||||
<div id="add-owners">
|
||||
{% for owner in brickset_owners %}
|
||||
{% with id=owner.as_dataset() %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="{{ owner.fields.id }}" id="{{ id }}" autocomplete="off">
|
||||
<label class="form-check-label" for="{{ id }}">{{ owner.fields.name }}</label>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% if brickset_owners | length %}
|
||||
{{ accordion.header('Owners', 'owners', 'metadata', icon='user-line') }}
|
||||
<div id="add-owners">
|
||||
{% for owner in brickset_owners %}
|
||||
{% with id=owner.as_dataset() %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="{{ owner.fields.id }}" id="{{ id }}" autocomplete="off">
|
||||
<label class="form-check-label" for="{{ id }}">{{ owner.fields.name }}</label>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{{ accordion.footer() }}
|
||||
{% endif %}
|
||||
{% if brickset_purchase_locations | length %}
|
||||
{{ accordion.header('Purchase location', 'purchase-location', 'metadata', icon='building-line') }}
|
||||
<label class="visually-hidden" for="add-purchase-location">{{ name }}</label>
|
||||
{{ accordion.footer() }}
|
||||
{% endif %}
|
||||
{{ accordion.header('Purchase', 'purchase', 'metadata', icon='wallet-3-line') }}
|
||||
<div class="alert alert-info" role="alert">The expected date format here is <code>yyyy/mm/dd</code> (year/month/day), but you can configure how it is displayed in the set card with the <code>PURCHASE_DATE_FORMAT</code> variable.</div>
|
||||
<div class="row row-cols-lg-auto g-1 justify-content-start align-items-center pb-2">
|
||||
<div class="col-12">
|
||||
<label for="add-purchase-date" class="form-label visually-hidden">Date</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text px-1"><i class="ri-calendar-line me-1"></i><span class="ms-1 d-none d-md-inline"> Date</span></span>
|
||||
<input class="form-control form-control-sm flex-shrink-1 px-1" type="text" id="add-purchase-date" data-add-date="true" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label for="add-purchase-price" class="form-label visually-hidden">Price</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text px-1"><i class="ri-wallet-3-line me-1"></i><span class="ms-1 d-none d-md-inline"> Price</span></span>
|
||||
<input class="form-control form-control-sm flex-shrink-1 px-1" type="text" id="add-purchase-price" autocomplete="off">
|
||||
{% if config['PURCHASE_CURRENCY'] %}<span class="input-group-text d-none d-md-inline px-1">{{ config['PURCHASE_CURRENCY'] }}</span>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if brickset_purchase_locations | length %}
|
||||
<div class="col-12 flex-grow-1">
|
||||
<label for="add-purchase-location" class="form-label visually-hidden">Location</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text px-1"><i class="ri-building-line me-1"></i><span class="ms-1 d-none d-md-inline"> Location</span></span>
|
||||
<select id="add-purchase-location" class="form-select" autocomplete="off">
|
||||
<option value="" selected><i>None</i></option>
|
||||
{% for purchase_location in brickset_purchase_locations %}
|
||||
@@ -94,35 +107,53 @@
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{{ accordion.footer() }}
|
||||
{% endif %}
|
||||
{% if brickset_storages | length %}
|
||||
{{ accordion.header('Storage', 'storage', 'metadata', icon='archive-2-line') }}
|
||||
<label class="visually-hidden" for="add-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">
|
||||
{% for tag in brickset_tags %}
|
||||
{% with id=tag.as_dataset() %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="{{ tag.fields.id }}" id="{{ id }}" autocomplete="off">
|
||||
<label class="form-check-label" for="{{ id }}">{{ tag.fields.name }}</label>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{{ accordion.footer() }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{{ accordion.footer() }}
|
||||
{{ accordion.header('Notes', 'notes', 'metadata', icon='sticky-note-line') }}
|
||||
<textarea class="form-control" id="add-description" rows="3" placeholder="Optional notes..."></textarea>
|
||||
{{ accordion.footer() }}
|
||||
{% if brickset_storages | length %}
|
||||
{{ accordion.header('Storage', 'storage', 'metadata', icon='archive-2-line') }}
|
||||
<label class="visually-hidden" for="add-storage">Storage</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_statuses | length %}
|
||||
{{ accordion.header('Statuses', 'statuses', 'metadata', icon='checkbox-circle-line') }}
|
||||
<div id="add-statuses">
|
||||
{% for status in brickset_statuses %}
|
||||
{% with id=status.as_dataset() %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="{{ status.fields.id }}" id="{{ id }}" autocomplete="off">
|
||||
<label class="form-check-label" for="{{ id }}">{{ status.fields.name }}</label>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{{ accordion.footer() }}
|
||||
{% endif %}
|
||||
{% if brickset_tags | length %}
|
||||
{{ accordion.header('Tags', 'tags', 'metadata', icon='price-tag-2-line') }}
|
||||
<div id="add-tags">
|
||||
{% for tag in brickset_tags %}
|
||||
{% with id=tag.as_dataset() %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="{{ tag.fields.id }}" id="{{ id }}" autocomplete="off">
|
||||
<label class="form-check-label" for="{{ id }}">{{ tag.fields.name }}</label>
|
||||
</div>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{{ accordion.footer() }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
Reference in New Issue
Block a user