193 lines
6.4 KiB
Python
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
|