223 lines
6.8 KiB
Python
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()
|