feat(views): add routes for individual minifigures, parts, and purchase locations
This commit is contained in:
@@ -0,0 +1,405 @@
|
||||
import logging
|
||||
from functools import wraps
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
Response,
|
||||
abort,
|
||||
current_app,
|
||||
jsonify,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
url_for,
|
||||
)
|
||||
from flask_login import login_required
|
||||
|
||||
from .exceptions import exception_handler
|
||||
from ..individual_minifigure import IndividualMinifigure
|
||||
from ..individual_minifigure_list import IndividualMinifigureList
|
||||
from ..part import BrickPart
|
||||
from ..set_list import set_metadata_lists
|
||||
from ..set_owner_list import BrickSetOwnerList
|
||||
from ..set_tag_list import BrickSetTagList
|
||||
from ..set_storage_list import BrickSetStorageList
|
||||
from ..set_purchase_location_list import BrickSetPurchaseLocationList
|
||||
from ..sql import BrickSQL
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
individual_minifigure_page = Blueprint('individual_minifigure', __name__, url_prefix='/individual-minifigures')
|
||||
|
||||
|
||||
def require_individual_minifigures_write(f):
|
||||
"""Decorator to block write operations (add/update/delete) when individual minifigures are disabled.
|
||||
This prevents adding new minifigures but allows viewing existing ones."""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if current_app.config.get('DISABLE_INDIVIDUAL_MINIFIGURES', False):
|
||||
abort(404)
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
|
||||
# List all individual minifigures
|
||||
@individual_minifigure_page.route('/')
|
||||
@exception_handler(__file__)
|
||||
def list() -> str:
|
||||
minifigures = IndividualMinifigureList().all()
|
||||
|
||||
return render_template(
|
||||
'individual_minifigures.html',
|
||||
minifigures=minifigures,
|
||||
**set_metadata_lists(as_class=True)
|
||||
)
|
||||
|
||||
|
||||
# Individual minifigure instance details/edit
|
||||
@individual_minifigure_page.route('/<id>')
|
||||
@exception_handler(__file__)
|
||||
def details(*, id: str) -> str:
|
||||
item = IndividualMinifigure().select_by_id(id)
|
||||
|
||||
return render_template(
|
||||
'individual_minifigure/details.html',
|
||||
item=item,
|
||||
**set_metadata_lists(as_class=True)
|
||||
)
|
||||
|
||||
|
||||
# Update individual minifigure instance
|
||||
@individual_minifigure_page.route('/<id>/update', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_minifigures_write
|
||||
def update(*, id: str):
|
||||
item = IndividualMinifigure().select_by_id(id)
|
||||
|
||||
# Update basic fields
|
||||
item.fields.quantity = int(request.form.get('quantity', 1))
|
||||
item.fields.description = request.form.get('description', '')
|
||||
item.fields.storage = request.form.get('storage') or None
|
||||
item.fields.purchase_location = request.form.get('purchase_location') or None
|
||||
item.fields.purchase_date = request.form.get('purchase_date') or None
|
||||
item.fields.purchase_price = request.form.get('purchase_price') or None
|
||||
|
||||
# Update the individual minifigure
|
||||
from ..sql import BrickSQL
|
||||
BrickSQL().execute(
|
||||
'individual_minifigure/update',
|
||||
parameters={
|
||||
'id': item.fields.id,
|
||||
'quantity': item.fields.quantity,
|
||||
'description': item.fields.description,
|
||||
'storage': item.fields.storage,
|
||||
'purchase_location': item.fields.purchase_location,
|
||||
'purchase_date': item.fields.purchase_date,
|
||||
'purchase_price': item.fields.purchase_price,
|
||||
},
|
||||
commit=False,
|
||||
)
|
||||
|
||||
# Update owners
|
||||
owners = request.form.getlist('owners')
|
||||
for owner in BrickSetOwnerList.list():
|
||||
owner.update_individual_minifigure_state(item, state=(owner.fields.id in owners))
|
||||
|
||||
# Update tags
|
||||
tags = request.form.getlist('tags')
|
||||
for tag in BrickSetTagList.list():
|
||||
tag.update_individual_minifigure_state(item, state=(tag.fields.id in tags))
|
||||
|
||||
BrickSQL().commit()
|
||||
|
||||
return redirect(url_for('individual_minifigure.details', id=id))
|
||||
|
||||
|
||||
# Update quantity
|
||||
@individual_minifigure_page.route('/<id>/update/quantity', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_minifigures_write
|
||||
@login_required
|
||||
def update_quantity(*, id: str):
|
||||
item = IndividualMinifigure().select_by_id(id)
|
||||
item.fields.quantity = int(request.json.get('value', 1))
|
||||
|
||||
BrickSQL().execute_and_commit(
|
||||
'individual_minifigure/update',
|
||||
parameters={
|
||||
'id': item.fields.id,
|
||||
'quantity': item.fields.quantity,
|
||||
'description': item.fields.description,
|
||||
'storage': item.fields.storage,
|
||||
'purchase_location': item.fields.purchase_location if hasattr(item.fields, 'purchase_location') else None,
|
||||
'purchase_date': item.fields.purchase_date if hasattr(item.fields, 'purchase_date') else None,
|
||||
'purchase_price': item.fields.purchase_price if hasattr(item.fields, 'purchase_price') else None,
|
||||
}
|
||||
)
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# Update description
|
||||
@individual_minifigure_page.route('/<id>/update/description', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_minifigures_write
|
||||
@login_required
|
||||
def update_description(*, id: str):
|
||||
item = IndividualMinifigure().select_by_id(id)
|
||||
item.fields.description = request.json.get('value', '')
|
||||
|
||||
BrickSQL().execute_and_commit(
|
||||
'individual_minifigure/update',
|
||||
parameters={
|
||||
'id': item.fields.id,
|
||||
'quantity': item.fields.quantity,
|
||||
'description': item.fields.description,
|
||||
'storage': item.fields.storage,
|
||||
'purchase_location': item.fields.purchase_location if hasattr(item.fields, 'purchase_location') else None,
|
||||
'purchase_date': item.fields.purchase_date if hasattr(item.fields, 'purchase_date') else None,
|
||||
'purchase_price': item.fields.purchase_price if hasattr(item.fields, 'purchase_price') else None,
|
||||
}
|
||||
)
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# Update owner
|
||||
@individual_minifigure_page.route('/<id>/update/owner/<metadata_id>', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_minifigures_write
|
||||
@login_required
|
||||
def update_owner(*, id: str, metadata_id: str):
|
||||
item = IndividualMinifigure().select_by_id(id)
|
||||
owner = BrickSetOwnerList.get(metadata_id)
|
||||
owner.update_individual_minifigure_state(item, json=request.json)
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# Update tag
|
||||
@individual_minifigure_page.route('/<id>/update/tag/<metadata_id>', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_minifigures_write
|
||||
@login_required
|
||||
def update_tag(*, id: str, metadata_id: str):
|
||||
item = IndividualMinifigure().select_by_id(id)
|
||||
tag = BrickSetTagList.get(metadata_id)
|
||||
tag.update_individual_minifigure_state(item, json=request.json)
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# Update status
|
||||
@individual_minifigure_page.route('/<id>/update/status/<metadata_id>', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_minifigures_write
|
||||
@login_required
|
||||
def update_status(*, id: str, metadata_id: str):
|
||||
item = IndividualMinifigure().select_by_id(id)
|
||||
from ..set_status_list import BrickSetStatusList
|
||||
status = BrickSetStatusList.get(metadata_id)
|
||||
status.update_individual_minifigure_state(item, json=request.json)
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# Update storage
|
||||
@individual_minifigure_page.route('/<id>/update/storage', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_minifigures_write
|
||||
@login_required
|
||||
def update_storage(*, id: str):
|
||||
item = IndividualMinifigure().select_by_id(id)
|
||||
storage_id = request.json.get('value')
|
||||
|
||||
BrickSQL().execute_and_commit(
|
||||
'individual_minifigure/update',
|
||||
parameters={
|
||||
'id': item.fields.id,
|
||||
'quantity': item.fields.quantity,
|
||||
'description': item.fields.description,
|
||||
'storage': storage_id if storage_id else None,
|
||||
'purchase_location': item.fields.purchase_location if hasattr(item.fields, 'purchase_location') else None,
|
||||
'purchase_date': item.fields.purchase_date if hasattr(item.fields, 'purchase_date') else None,
|
||||
'purchase_price': item.fields.purchase_price if hasattr(item.fields, 'purchase_price') else None,
|
||||
}
|
||||
)
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# Update purchase location
|
||||
@individual_minifigure_page.route('/<id>/update/purchase_location', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_minifigures_write
|
||||
@login_required
|
||||
def update_purchase_location(*, id: str):
|
||||
item = IndividualMinifigure().select_by_id(id)
|
||||
location_id = request.json.get('value')
|
||||
|
||||
BrickSQL().execute_and_commit(
|
||||
'individual_minifigure/update',
|
||||
parameters={
|
||||
'id': item.fields.id,
|
||||
'quantity': item.fields.quantity,
|
||||
'description': item.fields.description,
|
||||
'storage': item.fields.storage,
|
||||
'purchase_location': location_id if location_id else None,
|
||||
'purchase_date': item.fields.purchase_date if hasattr(item.fields, 'purchase_date') else None,
|
||||
'purchase_price': item.fields.purchase_price if hasattr(item.fields, 'purchase_price') else None,
|
||||
}
|
||||
)
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# Update purchase date
|
||||
@individual_minifigure_page.route('/<id>/update/purchase_date', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_minifigures_write
|
||||
@login_required
|
||||
def update_purchase_date(*, id: str):
|
||||
from datetime import datetime
|
||||
|
||||
item = IndividualMinifigure().select_by_id(id)
|
||||
purchase_date_str = request.json.get('value')
|
||||
|
||||
# Convert date string to Unix timestamp (same as sets)
|
||||
purchase_date = None
|
||||
if purchase_date_str and purchase_date_str != '':
|
||||
try:
|
||||
purchase_date = datetime.strptime(purchase_date_str, '%Y/%m/%d').timestamp()
|
||||
except ValueError as e:
|
||||
logger.error('Invalid date format: {date}'.format(date=purchase_date_str))
|
||||
return jsonify({'error': '{date} is not a valid date'.format(date=purchase_date_str)}), 400
|
||||
|
||||
BrickSQL().execute_and_commit(
|
||||
'individual_minifigure/update',
|
||||
parameters={
|
||||
'id': item.fields.id,
|
||||
'quantity': item.fields.quantity,
|
||||
'description': item.fields.description,
|
||||
'storage': item.fields.storage,
|
||||
'purchase_location': item.fields.purchase_location if hasattr(item.fields, 'purchase_location') else None,
|
||||
'purchase_date': purchase_date,
|
||||
'purchase_price': item.fields.purchase_price if hasattr(item.fields, 'purchase_price') else None,
|
||||
}
|
||||
)
|
||||
|
||||
logger.info('Updated individual minifigure {id} purchase_date to: {date}'.format(
|
||||
id=id,
|
||||
date=purchase_date
|
||||
))
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# Update purchase price
|
||||
@individual_minifigure_page.route('/<id>/update/purchase_price', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_minifigures_write
|
||||
@login_required
|
||||
def update_purchase_price(*, id: str):
|
||||
item = IndividualMinifigure().select_by_id(id)
|
||||
purchase_price = request.json.get('value')
|
||||
|
||||
BrickSQL().execute_and_commit(
|
||||
'individual_minifigure/update',
|
||||
parameters={
|
||||
'id': item.fields.id,
|
||||
'quantity': item.fields.quantity,
|
||||
'description': item.fields.description,
|
||||
'storage': item.fields.storage,
|
||||
'purchase_location': item.fields.purchase_location if hasattr(item.fields, 'purchase_location') else None,
|
||||
'purchase_date': item.fields.purchase_date if hasattr(item.fields, 'purchase_date') else None,
|
||||
'purchase_price': purchase_price if purchase_price else None,
|
||||
}
|
||||
)
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# Update problematic pieces of an individual minifigure
|
||||
@individual_minifigure_page.route('/<id>/parts/<part>/<int:color>/<int:spare>/<problem>', methods=['POST'])
|
||||
@exception_handler(__file__, json=True)
|
||||
@require_individual_minifigures_write
|
||||
@login_required
|
||||
def problem_part(
|
||||
*,
|
||||
id: str,
|
||||
part: str,
|
||||
color: int,
|
||||
spare: int,
|
||||
problem: str,
|
||||
) -> Response:
|
||||
minifigure = IndividualMinifigure().select_by_id(id)
|
||||
|
||||
brickpart = BrickPart().select_specific_individual_minifigure(
|
||||
minifigure,
|
||||
part,
|
||||
color,
|
||||
spare,
|
||||
)
|
||||
|
||||
amount = brickpart.update_problem_individual_minifigure(problem, request.json)
|
||||
|
||||
# Info
|
||||
logger.info('Individual minifigure {figure} ({id}): updated part ({part} color: {color}, spare: {spare}) {problem} count to {amount}'.format(
|
||||
figure=minifigure.fields.figure,
|
||||
id=minifigure.fields.id,
|
||||
part=brickpart.fields.part,
|
||||
color=brickpart.fields.color,
|
||||
spare=brickpart.fields.spare,
|
||||
problem=problem,
|
||||
amount=amount
|
||||
))
|
||||
|
||||
return jsonify({problem: amount})
|
||||
|
||||
|
||||
# Update checked state of parts
|
||||
@individual_minifigure_page.route('/<id>/parts/<part>/<int:color>/<int:spare>/checked', methods=['POST'])
|
||||
@exception_handler(__file__, json=True)
|
||||
@require_individual_minifigures_write
|
||||
@login_required
|
||||
def checked_part(
|
||||
*,
|
||||
id: str,
|
||||
part: str,
|
||||
color: int,
|
||||
spare: int,
|
||||
) -> Response:
|
||||
minifigure = IndividualMinifigure().select_by_id(id)
|
||||
|
||||
brickpart = BrickPart().select_specific_individual_minifigure(
|
||||
minifigure,
|
||||
part,
|
||||
color,
|
||||
spare,
|
||||
)
|
||||
|
||||
checked = brickpart.update_checked_individual_minifigure(request.json)
|
||||
|
||||
# Info
|
||||
logger.info('Individual minifigure {figure} ({id}): updated part ({part} color: {color}, spare: {spare}) checked state to {checked}'.format(
|
||||
figure=minifigure.fields.figure,
|
||||
id=minifigure.fields.id,
|
||||
part=brickpart.fields.part,
|
||||
color=brickpart.fields.color,
|
||||
spare=brickpart.fields.spare,
|
||||
checked=checked
|
||||
))
|
||||
|
||||
return jsonify({'checked': checked})
|
||||
|
||||
|
||||
# Delete individual minifigure instance
|
||||
@individual_minifigure_page.route('/<id>/delete', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_minifigures_write
|
||||
@login_required
|
||||
def delete(*, id: str):
|
||||
item = IndividualMinifigure().select_by_id(id)
|
||||
figure = item.fields.figure
|
||||
item.delete()
|
||||
|
||||
return redirect(url_for('minifigure.details', figure=figure))
|
||||
@@ -0,0 +1,659 @@
|
||||
import logging
|
||||
from functools import wraps
|
||||
|
||||
from flask import (
|
||||
Blueprint,
|
||||
Response,
|
||||
abort,
|
||||
current_app,
|
||||
jsonify,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
url_for,
|
||||
)
|
||||
from flask_login import login_required
|
||||
|
||||
from .exceptions import exception_handler
|
||||
from ..individual_part import IndividualPart
|
||||
from ..individual_part_list import IndividualPartList
|
||||
from ..individual_part_lot import IndividualPartLot
|
||||
from ..individual_part_lot_list import IndividualPartLotList
|
||||
from ..set_list import set_metadata_lists
|
||||
from ..set_owner_list import BrickSetOwnerList
|
||||
from ..set_tag_list import BrickSetTagList
|
||||
from ..set_storage_list import BrickSetStorageList
|
||||
from ..set_purchase_location_list import BrickSetPurchaseLocationList
|
||||
from ..sql import BrickSQL
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
individual_part_page = Blueprint('individual_part', __name__, url_prefix='/individual-parts')
|
||||
|
||||
|
||||
def require_individual_parts_write(f):
|
||||
"""Decorator to block write operations (add/update/delete) when individual parts are disabled.
|
||||
This prevents adding new parts but allows viewing existing ones."""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if current_app.config.get('DISABLE_INDIVIDUAL_PARTS', False):
|
||||
abort(404)
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
|
||||
# List all individual parts
|
||||
@individual_part_page.route('/')
|
||||
@exception_handler(__file__)
|
||||
def list() -> str:
|
||||
parts = IndividualPartList().all()
|
||||
|
||||
return render_template(
|
||||
'individual_parts.html',
|
||||
parts=parts,
|
||||
**set_metadata_lists(as_class=True)
|
||||
)
|
||||
|
||||
|
||||
# Quick add individual part from set parts table
|
||||
@individual_part_page.route('/quick-add', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_parts_write
|
||||
@login_required
|
||||
def quick_add() -> Response:
|
||||
"""Quick add an individual part from a set's parts table"""
|
||||
import uuid
|
||||
|
||||
data = request.get_json()
|
||||
|
||||
# Validate required fields
|
||||
if not data or 'part' not in data or 'color' not in data:
|
||||
return jsonify({'error': 'Missing required fields: part and color'}), 400
|
||||
|
||||
# Generate UUID for new individual part
|
||||
part_id = str(uuid.uuid4())
|
||||
|
||||
# Extract fields with defaults
|
||||
quantity = data.get('quantity', 1)
|
||||
storage = data.get('storage') or None
|
||||
purchase_location = data.get('purchase_location') or None
|
||||
description = data.get('description') or None
|
||||
|
||||
try:
|
||||
sql = BrickSQL()
|
||||
|
||||
# Insert the individual part
|
||||
sql.execute(
|
||||
'individual_part/insert',
|
||||
parameters={
|
||||
'id': part_id,
|
||||
'part': data['part'],
|
||||
'color': data['color'],
|
||||
'quantity': quantity,
|
||||
'missing': 0,
|
||||
'damaged': 0,
|
||||
'checked': False,
|
||||
'description': description,
|
||||
'lot_id': None, # Not part of a lot
|
||||
'storage': storage,
|
||||
'purchase_location': purchase_location,
|
||||
'purchase_date': None,
|
||||
'purchase_price': None
|
||||
}
|
||||
)
|
||||
|
||||
# Initialize metadata rows (owners, tags, statuses) with default values
|
||||
# Uses the same SQL as updating set metadata, consolidated tables accept any entity id
|
||||
|
||||
# Initialize owner metadata (all set to 0 initially)
|
||||
for owner in BrickSetOwnerList.list():
|
||||
sql.execute(
|
||||
'set/metadata/owner/update/state',
|
||||
parameters={
|
||||
'set_id': part_id,
|
||||
'state': 0
|
||||
},
|
||||
name=owner.as_column()
|
||||
)
|
||||
|
||||
# Initialize tag metadata (all set to 0 initially)
|
||||
for tag in BrickSetTagList.list():
|
||||
sql.execute(
|
||||
'set/metadata/tag/update/state',
|
||||
parameters={
|
||||
'set_id': part_id,
|
||||
'state': 0
|
||||
},
|
||||
name=tag.as_column()
|
||||
)
|
||||
|
||||
# Initialize status metadata (all set to 0 initially)
|
||||
# Note: Individual parts (not lots) support statuses
|
||||
from ..set_status_list import BrickSetStatusList
|
||||
for status in BrickSetStatusList.list():
|
||||
sql.execute(
|
||||
'set/metadata/status/update/state',
|
||||
parameters={
|
||||
'set_id': part_id,
|
||||
'state': 0
|
||||
},
|
||||
name=status.as_column()
|
||||
)
|
||||
|
||||
sql.commit()
|
||||
|
||||
logger.info('Quick-added individual part {id}: {part}/{color} x{quantity}'.format(
|
||||
id=part_id,
|
||||
part=data["part"],
|
||||
color=data["color"],
|
||||
quantity=quantity
|
||||
))
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'id': part_id,
|
||||
'message': 'Added {quantity}x {part} to individual parts inventory'.format(
|
||||
quantity=quantity,
|
||||
part=data["part"]
|
||||
)
|
||||
}), 201
|
||||
|
||||
except Exception as e:
|
||||
logger.error('Error quick-adding individual part: {error}'.format(error=e))
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
# Individual part instance details/edit
|
||||
@individual_part_page.route('/<id>')
|
||||
@exception_handler(__file__)
|
||||
def details(*, id: str) -> str:
|
||||
item = IndividualPart().select_by_id(id)
|
||||
|
||||
# Check if this part belongs to a lot
|
||||
lot = None
|
||||
if hasattr(item.fields, 'lot_id') and item.fields.lot_id:
|
||||
try:
|
||||
lot = IndividualPartLot().select_by_id(item.fields.lot_id)
|
||||
except Exception as e:
|
||||
logger.warning('Could not load lot {lot_id} for part {part_id}: {error}'.format(
|
||||
lot_id=item.fields.lot_id,
|
||||
part_id=id,
|
||||
error=e
|
||||
))
|
||||
|
||||
return render_template(
|
||||
'individual_part/details.html',
|
||||
item=item,
|
||||
lot=lot,
|
||||
**set_metadata_lists(as_class=True)
|
||||
)
|
||||
|
||||
|
||||
# Update individual part instance
|
||||
@individual_part_page.route('/<id>/update', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_parts_write
|
||||
def update(*, id: str):
|
||||
item = IndividualPart().select_by_id(id)
|
||||
|
||||
# Update basic fields
|
||||
item.fields.quantity = int(request.form.get('quantity', 1))
|
||||
item.fields.description = request.form.get('description', '')
|
||||
item.fields.storage = request.form.get('storage') or None
|
||||
item.fields.purchase_location = request.form.get('purchase_location') or None
|
||||
item.fields.purchase_date = request.form.get('purchase_date') or None
|
||||
item.fields.purchase_price = request.form.get('purchase_price') or None
|
||||
|
||||
# Update the individual part
|
||||
BrickSQL().execute(
|
||||
'individual_part/update_full',
|
||||
parameters={
|
||||
'id': item.fields.id,
|
||||
'quantity': item.fields.quantity,
|
||||
'description': item.fields.description,
|
||||
'storage': item.fields.storage,
|
||||
'purchase_location': item.fields.purchase_location,
|
||||
'purchase_date': item.fields.purchase_date,
|
||||
'purchase_price': item.fields.purchase_price,
|
||||
},
|
||||
commit=False,
|
||||
)
|
||||
|
||||
# Update owners
|
||||
owners = request.form.getlist('owners')
|
||||
for owner in BrickSetOwnerList.list():
|
||||
owner.update_individual_part_state(item, state=(owner.fields.id in owners))
|
||||
|
||||
# Update tags
|
||||
tags = request.form.getlist('tags')
|
||||
for tag in BrickSetTagList.list():
|
||||
tag.update_individual_part_state(item, state=(tag.fields.id in tags))
|
||||
|
||||
BrickSQL().commit()
|
||||
|
||||
return redirect(url_for('individual_part.details', id=id))
|
||||
|
||||
|
||||
# Update quantity
|
||||
@individual_part_page.route('/<id>/update/quantity', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_parts_write
|
||||
@login_required
|
||||
def update_quantity(*, id: str):
|
||||
item = IndividualPart().select_by_id(id)
|
||||
value = request.json.get('value', '1')
|
||||
|
||||
# Handle empty string or 0 - don't allow it
|
||||
if value == '' or value == '0' or value == 0:
|
||||
return jsonify({'success': False, 'error': 'Quantity cannot be 0. Use the delete button in the menu to remove this part from the lot.'})
|
||||
|
||||
try:
|
||||
quantity = int(value)
|
||||
if quantity < 1:
|
||||
return jsonify({'success': False, 'error': 'Quantity must be at least 1'})
|
||||
except ValueError:
|
||||
return jsonify({'success': False, 'error': 'Invalid quantity value'})
|
||||
|
||||
item.update_field('quantity', quantity)
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# Update description
|
||||
@individual_part_page.route('/<id>/update/description', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_parts_write
|
||||
@login_required
|
||||
def update_description(*, id: str):
|
||||
item = IndividualPart().select_by_id(id)
|
||||
description = request.json.get('value', '')
|
||||
item.update_field('description', description)
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# Update purchase date
|
||||
@individual_part_page.route('/<id>/update/purchase_date', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_parts_write
|
||||
@login_required
|
||||
def update_purchase_date(*, id: str):
|
||||
item = IndividualPart().select_by_id(id)
|
||||
purchase_date = request.json.get('value', '')
|
||||
|
||||
# Convert date string to timestamp if provided, otherwise None
|
||||
if purchase_date and purchase_date.strip():
|
||||
try:
|
||||
from datetime import datetime
|
||||
# Expecting yyyy/mm/dd format
|
||||
date_obj = datetime.strptime(purchase_date, '%Y/%m/%d')
|
||||
timestamp = date_obj.timestamp()
|
||||
item.update_field('purchase_date', timestamp)
|
||||
except ValueError:
|
||||
return jsonify({'success': False, 'error': 'Invalid date format. Expected yyyy/mm/dd'})
|
||||
else:
|
||||
item.update_field('purchase_date', None)
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# Update purchase price
|
||||
@individual_part_page.route('/<id>/update/purchase_price', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_parts_write
|
||||
@login_required
|
||||
def update_purchase_price(*, id: str):
|
||||
item = IndividualPart().select_by_id(id)
|
||||
purchase_price = request.json.get('value', '')
|
||||
|
||||
# Convert to float if provided, otherwise None
|
||||
if purchase_price and str(purchase_price).strip():
|
||||
try:
|
||||
price = float(purchase_price)
|
||||
item.update_field('purchase_price', price)
|
||||
except ValueError:
|
||||
return jsonify({'success': False, 'error': 'Invalid price value'})
|
||||
else:
|
||||
item.update_field('purchase_price', None)
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# Update owner
|
||||
@individual_part_page.route('/<id>/update/owner/<metadata_id>', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_parts_write
|
||||
@login_required
|
||||
def update_owner(*, id: str, metadata_id: str):
|
||||
item = IndividualPart().select_by_id(id)
|
||||
owner = BrickSetOwnerList.get(metadata_id)
|
||||
owner.update_individual_part_state(item, json=request.json)
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# Update tag
|
||||
@individual_part_page.route('/<id>/update/tag/<metadata_id>', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_parts_write
|
||||
@login_required
|
||||
def update_tag(*, id: str, metadata_id: str):
|
||||
item = IndividualPart().select_by_id(id)
|
||||
tag = BrickSetTagList.get(metadata_id)
|
||||
tag.update_individual_part_state(item, json=request.json)
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# Update status
|
||||
@individual_part_page.route('/<id>/update/status/<metadata_id>', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_parts_write
|
||||
@login_required
|
||||
def update_status(*, id: str, metadata_id: str):
|
||||
item = IndividualPart().select_by_id(id)
|
||||
from ..set_status_list import BrickSetStatusList
|
||||
status = BrickSetStatusList.get(metadata_id)
|
||||
status.update_individual_part_state(item, json=request.json)
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# Update storage
|
||||
@individual_part_page.route('/<id>/update/storage', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_parts_write
|
||||
@login_required
|
||||
def update_storage(*, id: str):
|
||||
item = IndividualPart().select_by_id(id)
|
||||
storage_id = request.json.get('value')
|
||||
item.update_field('storage', storage_id if storage_id else None)
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# Update purchase location
|
||||
@individual_part_page.route('/<id>/update/purchase_location', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_parts_write
|
||||
@login_required
|
||||
def update_purchase_location(*, id: str):
|
||||
item = IndividualPart().select_by_id(id)
|
||||
location_id = request.json.get('value')
|
||||
item.update_field('purchase_location', location_id if location_id else None)
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# Update missing count
|
||||
@individual_part_page.route('/<id>/update/missing', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_parts_write
|
||||
@login_required
|
||||
def update_missing(*, id: str):
|
||||
item = IndividualPart().select_by_id(id)
|
||||
amount = item.update_problem('missing', request.json)
|
||||
|
||||
logger.info('Individual part {part} (color: {color}, id: {id}): updated missing count to {amount}'.format(
|
||||
part=item.fields.part,
|
||||
color=item.fields.color,
|
||||
id=item.fields.id,
|
||||
amount=amount
|
||||
))
|
||||
|
||||
return jsonify({'missing': amount})
|
||||
|
||||
|
||||
# Update damaged count
|
||||
@individual_part_page.route('/<id>/update/damaged', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_parts_write
|
||||
@login_required
|
||||
def update_damaged(*, id: str):
|
||||
item = IndividualPart().select_by_id(id)
|
||||
amount = item.update_problem('damaged', request.json)
|
||||
|
||||
logger.info('Individual part {part} (color: {color}, id: {id}): updated damaged count to {amount}'.format(
|
||||
part=item.fields.part,
|
||||
color=item.fields.color,
|
||||
id=item.fields.id,
|
||||
amount=amount
|
||||
))
|
||||
|
||||
return jsonify({'damaged': amount})
|
||||
|
||||
|
||||
# Update checked state
|
||||
@individual_part_page.route('/<id>/update/checked', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_parts_write
|
||||
@login_required
|
||||
def update_checked(*, id: str):
|
||||
item = IndividualPart().select_by_id(id)
|
||||
checked = item.update_checked(request.json)
|
||||
|
||||
logger.info('Individual part {part} (color: {color}, id: {id}): updated checked state to {checked}'.format(
|
||||
part=item.fields.part,
|
||||
color=item.fields.color,
|
||||
id=item.fields.id,
|
||||
checked=checked
|
||||
))
|
||||
|
||||
return jsonify({'checked': checked})
|
||||
|
||||
|
||||
# Delete individual part instance
|
||||
@individual_part_page.route('/<id>/delete', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_parts_write
|
||||
@login_required
|
||||
def delete_part(*, id: str):
|
||||
item = IndividualPart().select_by_id(id)
|
||||
lot_id = item.fields.lot_id if hasattr(item.fields, 'lot_id') else None
|
||||
|
||||
item.delete()
|
||||
|
||||
logger.info('Deleted individual part {part} (color: {color}, id: {id})'.format(
|
||||
part=item.fields.part,
|
||||
color=item.fields.color,
|
||||
id=id
|
||||
))
|
||||
|
||||
# If part was in a lot, redirect back to the lot
|
||||
if lot_id:
|
||||
return redirect(url_for('individual_part.lot_details', lot_id=lot_id))
|
||||
else:
|
||||
return redirect(url_for('individual_part.list'))
|
||||
|
||||
|
||||
# List all lots
|
||||
@individual_part_page.route('/lot/')
|
||||
@exception_handler(__file__)
|
||||
def list_lots() -> str:
|
||||
"""List all individual part lots"""
|
||||
# Use optimized query that includes part_count
|
||||
lots = IndividualPartLotList().all()
|
||||
|
||||
return render_template(
|
||||
'individual_part/lots.html',
|
||||
lots=lots,
|
||||
**set_metadata_lists(as_class=True)
|
||||
)
|
||||
|
||||
|
||||
# Lot detail page
|
||||
@individual_part_page.route('/lot/<lot_id>')
|
||||
@exception_handler(__file__)
|
||||
def lot_details(*, lot_id: str) -> str:
|
||||
"""Display details for an individual part lot (behaves like a set)"""
|
||||
lot = IndividualPartLot().select_by_id(lot_id)
|
||||
|
||||
return render_template(
|
||||
'individual_part/lot_details.html',
|
||||
item=lot, # Pass as 'item' like sets do
|
||||
solo=True,
|
||||
**set_metadata_lists(as_class=True)
|
||||
)
|
||||
|
||||
|
||||
# Update lot name
|
||||
@individual_part_page.route('/lot/<lot_id>/update/name', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_parts_write
|
||||
@login_required
|
||||
def update_lot_name(*, lot_id: str):
|
||||
lot = IndividualPartLot().select_by_id(lot_id)
|
||||
name = request.json.get('value', '')
|
||||
|
||||
from ..sql import BrickSQL
|
||||
sql = BrickSQL()
|
||||
sql.execute_and_commit('individual_part_lot/update/name', parameters={'name': name, 'id': lot_id})
|
||||
|
||||
logger.info('Updated lot {lot_id} name to: {name}'.format(lot_id=lot_id, name=name))
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# Update lot description
|
||||
@individual_part_page.route('/lot/<lot_id>/update/description', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_parts_write
|
||||
@login_required
|
||||
def update_lot_description(*, lot_id: str):
|
||||
lot = IndividualPartLot().select_by_id(lot_id)
|
||||
description = request.json.get('value', '')
|
||||
|
||||
from ..sql import BrickSQL
|
||||
sql = BrickSQL()
|
||||
sql.execute_and_commit('individual_part_lot/update/description', parameters={'description': description, 'id': lot_id})
|
||||
|
||||
logger.info('Updated lot {lot_id} description'.format(lot_id=lot_id))
|
||||
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# Delete lot
|
||||
@individual_part_page.route('/lot/<lot_id>/delete', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_parts_write
|
||||
@login_required
|
||||
def delete_lot(*, lot_id: str):
|
||||
lot = IndividualPartLot().select_by_id(lot_id)
|
||||
lot.delete()
|
||||
|
||||
logger.info('Deleted individual part lot {lot_id}'.format(lot_id=lot_id))
|
||||
|
||||
return redirect(url_for('individual_part.list_lots'))
|
||||
|
||||
|
||||
# Update lot owner
|
||||
@individual_part_page.route('/lot/<lot_id>/update/owner/<metadata_id>', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_parts_write
|
||||
@login_required
|
||||
def update_lot_owner(*, lot_id: str, metadata_id: str):
|
||||
from ..set_owner_list import BrickSetOwnerList
|
||||
lot = IndividualPartLot().select_by_id(lot_id)
|
||||
owner = BrickSetOwnerList.get(metadata_id)
|
||||
owner.update_individual_part_lot_state(lot, json=request.json)
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# Update lot tag
|
||||
@individual_part_page.route('/lot/<lot_id>/update/tag/<metadata_id>', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_parts_write
|
||||
@login_required
|
||||
def update_lot_tag(*, lot_id: str, metadata_id: str):
|
||||
from ..set_tag_list import BrickSetTagList
|
||||
lot = IndividualPartLot().select_by_id(lot_id)
|
||||
tag = BrickSetTagList.get(metadata_id)
|
||||
tag.update_individual_part_lot_state(lot, json=request.json)
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# Update lot status
|
||||
@individual_part_page.route('/lot/<lot_id>/update/status/<metadata_id>', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_parts_write
|
||||
@login_required
|
||||
def update_lot_status(*, lot_id: str, metadata_id: str):
|
||||
from ..set_status_list import BrickSetStatusList
|
||||
lot = IndividualPartLot().select_by_id(lot_id)
|
||||
status = BrickSetStatusList.get(metadata_id)
|
||||
status.update_individual_part_lot_state(lot, json=request.json)
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# Update lot storage
|
||||
@individual_part_page.route('/lot/<lot_id>/update/storage', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_parts_write
|
||||
@login_required
|
||||
def update_lot_storage(*, lot_id: str):
|
||||
from ..set_storage_list import BrickSetStorageList
|
||||
lot = IndividualPartLot().select_by_id(lot_id)
|
||||
storage_id = request.json.get('value', '')
|
||||
|
||||
from ..sql import BrickSQL
|
||||
sql = BrickSQL()
|
||||
sql.execute_and_commit('individual_part_lot/update/storage', parameters={'storage': storage_id if storage_id else None, 'id': lot_id})
|
||||
|
||||
logger.info('Updated lot {lot_id} storage to: {storage}'.format(lot_id=lot_id, storage=storage_id))
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# Update lot purchase location
|
||||
@individual_part_page.route('/lot/<lot_id>/update/purchase_location', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_parts_write
|
||||
@login_required
|
||||
def update_lot_purchase_location(*, lot_id: str):
|
||||
from ..set_purchase_location_list import BrickSetPurchaseLocationList
|
||||
lot = IndividualPartLot().select_by_id(lot_id)
|
||||
location_id = request.json.get('value', '')
|
||||
|
||||
from ..sql import BrickSQL
|
||||
sql = BrickSQL()
|
||||
sql.execute_and_commit('individual_part_lot/update/purchase_location', parameters={'purchase_location': location_id if location_id else None, 'id': lot_id})
|
||||
|
||||
logger.info('Updated lot {lot_id} purchase_location to: {location}'.format(lot_id=lot_id, location=location_id))
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# Update lot purchase date
|
||||
@individual_part_page.route('/lot/<lot_id>/update/purchase_date', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_parts_write
|
||||
@login_required
|
||||
def update_lot_purchase_date(*, lot_id: str):
|
||||
from datetime import datetime
|
||||
|
||||
lot = IndividualPartLot().select_by_id(lot_id)
|
||||
purchase_date_str = request.json.get('value')
|
||||
|
||||
# Convert date string to Unix timestamp (same as sets)
|
||||
purchase_date = None
|
||||
if purchase_date_str and purchase_date_str != '':
|
||||
try:
|
||||
purchase_date = datetime.strptime(purchase_date_str, '%Y/%m/%d').timestamp()
|
||||
except ValueError as e:
|
||||
logger.error('Invalid date format: {date}'.format(date=purchase_date_str))
|
||||
return jsonify({'error': '{date} is not a valid date'.format(date=purchase_date_str)}), 400
|
||||
|
||||
from ..sql import BrickSQL
|
||||
sql = BrickSQL()
|
||||
sql.execute_and_commit('individual_part_lot/update/purchase_date', parameters={'purchase_date': purchase_date, 'id': lot_id})
|
||||
|
||||
logger.info('Updated lot {lot_id} purchase_date to: {date}'.format(lot_id=lot_id, date=purchase_date))
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
# Update lot purchase price
|
||||
@individual_part_page.route('/lot/<lot_id>/update/purchase_price', methods=['POST'])
|
||||
@exception_handler(__file__)
|
||||
@require_individual_parts_write
|
||||
@login_required
|
||||
def update_lot_purchase_price(*, lot_id: str):
|
||||
lot = IndividualPartLot().select_by_id(lot_id)
|
||||
purchase_price = request.json.get('value')
|
||||
|
||||
from ..sql import BrickSQL
|
||||
sql = BrickSQL()
|
||||
sql.execute_and_commit('individual_part_lot/update/purchase_price', parameters={'purchase_price': purchase_price if purchase_price else None, 'id': lot_id})
|
||||
|
||||
logger.info('Updated lot {lot_id} purchase_price to: {price}'.format(lot_id=lot_id, price=purchase_price))
|
||||
return jsonify({'success': True})
|
||||
@@ -0,0 +1,38 @@
|
||||
from flask import Blueprint, render_template
|
||||
|
||||
from .exceptions import exception_handler
|
||||
from ..set_list import BrickSetList, set_metadata_lists
|
||||
from ..set_purchase_location import BrickSetPurchaseLocation
|
||||
from ..set_purchase_location_list import BrickSetPurchaseLocationList
|
||||
from ..individual_minifigure_list import IndividualMinifigureList
|
||||
from ..individual_part_list import IndividualPartList
|
||||
from ..individual_part_lot_list import IndividualPartLotList
|
||||
|
||||
purchase_location_page = Blueprint('purchase_location', __name__, url_prefix='/purchase-locations')
|
||||
|
||||
|
||||
# Index
|
||||
@purchase_location_page.route('/', methods=['GET'])
|
||||
@exception_handler(__file__)
|
||||
def list() -> str:
|
||||
return render_template(
|
||||
'purchase_locations.html',
|
||||
table_collection=BrickSetPurchaseLocationList.all(),
|
||||
)
|
||||
|
||||
|
||||
# Purchase location details
|
||||
@purchase_location_page.route('/<id>/details')
|
||||
@exception_handler(__file__)
|
||||
def details(*, id: str) -> str:
|
||||
purchase_location = BrickSetPurchaseLocation().select_specific(id)
|
||||
|
||||
return render_template(
|
||||
'purchase_location.html',
|
||||
item=purchase_location,
|
||||
sets=BrickSetList().using_purchase_location(purchase_location),
|
||||
individual_minifigures=IndividualMinifigureList().using_purchase_location(purchase_location),
|
||||
individual_parts=IndividualPartList().using_purchase_location(purchase_location),
|
||||
individual_part_lots=IndividualPartLotList().using_purchase_location(purchase_location),
|
||||
**set_metadata_lists(as_class=True)
|
||||
)
|
||||
Reference in New Issue
Block a user