Files
ShooterHub/blueprints/equipment.py
2026-03-19 16:42:37 +01:00

193 lines
6.4 KiB
Python

from pathlib import Path
from flask import (
abort, current_app, flash, redirect, render_template,
request, send_from_directory, url_for,
)
from flask import Blueprint
from flask_babel import _
from flask_login import current_user, login_required
from sqlalchemy import select
from extensions import db
from models import EquipmentItem
from storage import rotate_photo, save_equipment_photo
equipment_bp = Blueprint("equipment", __name__, url_prefix="/equipment")
CATEGORIES = [
("rifle", "Rifle"),
("handgun", "Handgun"),
("scope", "Scope"),
("other", "Other"),
]
CATEGORY_KEYS = [k for k, _ in CATEGORIES]
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _own_item(item_id: int) -> EquipmentItem:
item = db.session.get(EquipmentItem, item_id)
if item is None:
abort(404)
if item.user_id != current_user.id:
abort(403)
return item
def _apply_form(item: EquipmentItem) -> str | None:
"""Write request.form fields onto item. Returns an error string or None."""
name = request.form.get("name", "").strip()
category = request.form.get("category", "").strip()
if not name:
return _("Name is required.")
if category not in CATEGORY_KEYS:
return _("Invalid category.")
item.name = name
item.category = category
item.brand = request.form.get("brand", "").strip() or None
item.model = request.form.get("model", "").strip() or None
item.serial_number = request.form.get("serial_number", "").strip() or None
item.notes = request.form.get("notes", "").strip() or None
if category == "scope":
item.magnification = request.form.get("magnification", "").strip() or None
item.reticle = request.form.get("reticle", "").strip() or None
item.unit = request.form.get("unit", "").strip() or None
item.caliber = None
else:
item.caliber = request.form.get("caliber", "").strip() or None
item.magnification = None
item.reticle = None
item.unit = None
return None
# ---------------------------------------------------------------------------
# Routes
# ---------------------------------------------------------------------------
@equipment_bp.route("/")
@login_required
def index():
items = db.session.scalars(
select(EquipmentItem)
.where(EquipmentItem.user_id == current_user.id)
.order_by(EquipmentItem.category, EquipmentItem.name)
).all()
return render_template("equipment/list.html", items=items, categories=CATEGORIES)
@equipment_bp.route("/new", methods=["GET", "POST"])
@login_required
def new():
if request.method == "POST":
item = EquipmentItem(user_id=current_user.id)
db.session.add(item)
error = _apply_form(item)
if error:
db.session.expunge(item)
flash(error, "error")
return render_template("equipment/form.html", item=None,
categories=CATEGORIES, prefill=request.form)
db.session.flush()
_handle_photo(item, is_new=True)
db.session.commit()
flash(_("'%(name)s' added.", name=item.name), "success")
return redirect(url_for("equipment.detail", item_id=item.id))
return render_template("equipment/form.html", item=None, categories=CATEGORIES)
@equipment_bp.route("/<int:item_id>")
@login_required
def detail(item_id: int):
item = _own_item(item_id)
return render_template("equipment/detail.html", item=item, categories=dict(CATEGORIES))
@equipment_bp.route("/<int:item_id>/edit", methods=["GET", "POST"])
@login_required
def edit(item_id: int):
item = _own_item(item_id)
if request.method == "POST":
error = _apply_form(item)
if error:
flash(error, "error")
return render_template("equipment/form.html", item=item,
categories=CATEGORIES, prefill=request.form)
_handle_photo(item, is_new=False)
db.session.commit()
flash(_("'%(name)s' updated.", name=item.name), "success")
return redirect(url_for("equipment.detail", item_id=item.id))
return render_template("equipment/form.html", item=item, categories=CATEGORIES)
@equipment_bp.route("/<int:item_id>/delete", methods=["POST"])
@login_required
def delete(item_id: int):
item = _own_item(item_id)
name = item.name
if item.photo_path:
_remove_photo_file(item.photo_path)
db.session.delete(item)
db.session.commit()
flash(_("'%(name)s' deleted.", name=name), "success")
return redirect(url_for("equipment.index"))
@equipment_bp.route("/<int:item_id>/photo/rotate", methods=["POST"])
@login_required
def rotate_photo_view(item_id: int):
item = _own_item(item_id)
if not item.photo_path:
flash(_("No photo to rotate."), "error")
return redirect(url_for("equipment.detail", item_id=item_id))
try:
degrees = int(request.form.get("degrees", 0))
except ValueError:
abort(400)
if degrees not in (-90, 90, 180):
abort(400)
rotate_photo(item.photo_path, degrees)
return redirect(url_for("equipment.detail", item_id=item_id))
@equipment_bp.route("/photos/<path:filepath>")
@login_required
def photo(filepath: str):
"""Serve equipment photo. Only the owning user may access it."""
try:
owner_id = int(filepath.split("/")[0])
except (ValueError, IndexError):
abort(404)
if owner_id != current_user.id:
abort(403)
storage_root = current_app.config["STORAGE_ROOT"]
return send_from_directory(Path(storage_root) / "equipment_photos", filepath)
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
def _handle_photo(item: EquipmentItem, *, is_new: bool) -> None:
photo_file = request.files.get("photo")
if not (photo_file and photo_file.filename):
return
try:
old_path = item.photo_path
item.photo_path = save_equipment_photo(current_user.id, item.id, photo_file)
if old_path:
_remove_photo_file(old_path)
except ValueError as e:
flash(str(e), "error")
def _remove_photo_file(photo_path: str) -> None:
try:
storage_root = current_app.config["STORAGE_ROOT"]
(Path(storage_root) / photo_path).unlink(missing_ok=True)
except Exception:
pass