from pathlib import Path from flask import Blueprint, current_app, request from flask_jwt_extended import jwt_required from sqlalchemy import select from extensions import db from models import EquipmentItem from storage import save_equipment_photo from .utils import ( created, err, no_content, ok, current_api_user, serialize_equipment, ) equipment_bp = Blueprint("api_equipment", __name__, url_prefix="/equipment") CATEGORY_KEYS = ["rifle", "handgun", "scope", "other"] # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _read_fields(category: str) -> dict: """Read fields from JSON body or multipart form data.""" if request.is_json: body = request.get_json(silent=True) or {} get = lambda key, default="": body.get(key, default) else: get = lambda key, default="": request.form.get(key, default) fields: dict = {} for key in ("name", "brand", "model", "serial_number", "notes"): val = (get(key) or "").strip() fields[key] = val or None # name is required — caller checks non-None fields["name"] = (get("name") or "").strip() if category == "scope": fields["magnification"] = (get("magnification") or "").strip() or None fields["reticle"] = (get("reticle") or "").strip() or None fields["unit"] = (get("unit") or "").strip() or None else: fields["caliber"] = (get("caliber") or "").strip() or None return fields def _apply_fields(item: EquipmentItem, fields: dict) -> None: for key, val in fields.items(): setattr(item, key, val) def _remove_photo(photo_path: str, storage_root: str) -> None: try: Path(storage_root).joinpath(photo_path).unlink(missing_ok=True) except Exception: pass # --------------------------------------------------------------------------- # Routes # --------------------------------------------------------------------------- @equipment_bp.get("/") @jwt_required() def list_equipment(): user = current_api_user() if not user: return err("User not found.", 404) items = db.session.scalars( select(EquipmentItem) .where(EquipmentItem.user_id == user.id) .order_by(EquipmentItem.category, EquipmentItem.name) ).all() return ok([serialize_equipment(i) for i in items]) @equipment_bp.post("/") @jwt_required() def create_equipment(): user = current_api_user() if not user: return err("User not found.", 404) # Category can come from JSON or form if request.is_json: body = request.get_json(silent=True) or {} category = (body.get("category") or "").strip() else: category = (request.form.get("category") or "").strip() if not category: return err("category is required.", 400) if category not in CATEGORY_KEYS: return err(f"category must be one of: {', '.join(CATEGORY_KEYS)}.", 400) fields = _read_fields(category) if not fields.get("name"): return err("name is required.", 400) item = EquipmentItem(user_id=user.id, category=category) _apply_fields(item, fields) db.session.add(item) db.session.flush() # get item.id before photo upload photo = request.files.get("photo") if photo and photo.filename: try: item.photo_path = save_equipment_photo(user.id, item.id, photo) except ValueError as e: db.session.rollback() return err(str(e), 422) db.session.commit() return created(serialize_equipment(item)) @equipment_bp.get("/") @jwt_required() def get_equipment(item_id: int): user = current_api_user() if not user: return err("User not found.", 404) item = db.session.get(EquipmentItem, item_id) if not item: return err("Equipment item not found.", 404) if item.user_id != user.id: return err("Access denied.", 403) return ok(serialize_equipment(item)) @equipment_bp.patch("/") @jwt_required() def update_equipment(item_id: int): user = current_api_user() if not user: return err("User not found.", 404) item = db.session.get(EquipmentItem, item_id) if not item: return err("Equipment item not found.", 404) if item.user_id != user.id: return err("Access denied.", 403) # Determine category (may be updated or use existing) if request.is_json: body = request.get_json(silent=True) or {} new_category = (body.get("category") or "").strip() or None else: new_category = (request.form.get("category") or "").strip() or None if new_category: if new_category not in CATEGORY_KEYS: return err(f"category must be one of: {', '.join(CATEGORY_KEYS)}.", 400) item.category = new_category category = item.category # Only update fields present in the request if request.is_json: body = request.get_json(silent=True) or {} get = lambda key: body.get(key) has = lambda key: key in body else: get = lambda key: request.form.get(key) has = lambda key: key in request.form for key in ("name", "brand", "model", "serial_number", "notes"): if has(key): val = (get(key) or "").strip() or None if key == "name" and not val: return err("name cannot be empty.", 400) setattr(item, key, val) if category == "scope": for key in ("magnification", "reticle", "unit"): if has(key): setattr(item, key, (get(key) or "").strip() or None) else: if has("caliber"): item.caliber = (get("caliber") or "").strip() or None # Handle photo upload photo = request.files.get("photo") if photo and photo.filename: try: old_path = item.photo_path item.photo_path = save_equipment_photo(user.id, item.id, photo) if old_path: storage_root = current_app.config["STORAGE_ROOT"] _remove_photo(old_path, storage_root) except ValueError as e: return err(str(e), 422) db.session.commit() return ok(serialize_equipment(item)) @equipment_bp.delete("/") @jwt_required() def delete_equipment(item_id: int): user = current_api_user() if not user: return err("User not found.", 404) item = db.session.get(EquipmentItem, item_id) if not item: return err("Equipment item not found.", 404) if item.user_id != user.id: return err("Access denied.", 403) if item.photo_path: storage_root = current_app.config["STORAGE_ROOT"] _remove_photo(item.photo_path, storage_root) db.session.delete(item) db.session.commit() return no_content()