Files
ShooterHub/blueprints/api/equipment.py

223 lines
6.8 KiB
Python

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("/<int:item_id>")
@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("/<int:item_id>")
@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("/<int:item_id>")
@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()