2453e552fb
- Extract JSON data to <script> tags instead of tojson_attr in x-data attributes
- Remove literal " from CSS selector in x-data (meta[name=csrf-token])
- Move Alpine.js defer script after extra_js block in base.html
- Add isinstance(resp, dict) guard before .get("error") in measure.py and maker.py
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
381 lines
12 KiB
Python
381 lines
12 KiB
Python
"""Maker blueprint - recipe creation and editing."""
|
|
from flask import Blueprint, flash, jsonify, redirect, render_template, request, url_for
|
|
from flask_babel import gettext as _
|
|
|
|
from blueprints.auth import login_required, role_required
|
|
from services.api_client import api_client
|
|
|
|
maker_bp = Blueprint("maker", __name__, url_prefix="/maker")
|
|
|
|
|
|
# ============================================================================
|
|
# PAGINE (GET)
|
|
# ============================================================================
|
|
|
|
@maker_bp.route("/recipes")
|
|
@login_required
|
|
@role_required("Maker")
|
|
def recipe_list():
|
|
"""List all recipes with filters."""
|
|
page = request.args.get("page", 1, type=int)
|
|
per_page = request.args.get("per_page", 100, type=int)
|
|
search = request.args.get("search", "", type=str)
|
|
|
|
params = {"page": page, "per_page": per_page}
|
|
if search:
|
|
params["search"] = search
|
|
|
|
resp = api_client.get("/api/recipes", params=params)
|
|
|
|
if isinstance(resp, dict) and resp.get("error"):
|
|
flash(_("Errore nel caricamento delle ricette: %(error)s", error=resp.get("detail", "")), "error")
|
|
recipes = []
|
|
total = 0
|
|
pages = 0
|
|
else:
|
|
recipes = resp.get("items", []) if isinstance(resp, dict) else resp
|
|
total = resp.get("total", 0) if isinstance(resp, dict) else len(recipes)
|
|
pages = resp.get("pages", 1) if isinstance(resp, dict) else 1
|
|
|
|
return render_template(
|
|
"maker/recipe_list.html",
|
|
recipes=recipes,
|
|
page=page,
|
|
per_page=per_page,
|
|
total=total,
|
|
pages=pages,
|
|
search=search
|
|
)
|
|
|
|
|
|
@maker_bp.route("/recipes/new")
|
|
@login_required
|
|
@role_required("Maker")
|
|
def recipe_new():
|
|
"""Create new recipe."""
|
|
return render_template("maker/recipe_editor.html", recipe=None)
|
|
|
|
|
|
@maker_bp.route("/recipes/<int:recipe_id>/edit")
|
|
@login_required
|
|
@role_required("Maker")
|
|
def recipe_edit(recipe_id: int):
|
|
"""Edit existing recipe."""
|
|
# Carica recipe con dettagli completi
|
|
resp = api_client.get(f"/api/recipes/{recipe_id}")
|
|
|
|
if resp.get("error"):
|
|
flash(_("Errore nel caricamento della ricetta: %(error)s", error=resp.get("detail", "")), "error")
|
|
return redirect(url_for("maker.recipe_list"))
|
|
|
|
recipe = resp
|
|
|
|
# Carica anche le versioni
|
|
versions_resp = api_client.get(f"/api/recipes/{recipe_id}/versions")
|
|
if isinstance(versions_resp, list):
|
|
recipe["versions"] = versions_resp
|
|
else:
|
|
recipe["versions"] = []
|
|
|
|
return render_template("maker/recipe_editor.html", recipe=recipe)
|
|
|
|
|
|
@maker_bp.route("/recipes/<int:recipe_id>/tasks")
|
|
@login_required
|
|
@role_required("Maker")
|
|
def task_editor(recipe_id: int):
|
|
"""Task/subtask editor with tolerances."""
|
|
# Carica recipe con task/subtask
|
|
resp = api_client.get(f"/api/recipes/{recipe_id}")
|
|
|
|
if resp.get("error"):
|
|
flash(_("Errore nel caricamento della ricetta: %(error)s", error=resp.get("detail", "")), "error")
|
|
return redirect(url_for("maker.recipe_list"))
|
|
|
|
recipe = resp
|
|
|
|
# Carica tasks con subtasks
|
|
tasks_resp = api_client.get(f"/api/recipes/{recipe_id}/tasks")
|
|
if isinstance(tasks_resp, list):
|
|
recipe["tasks"] = tasks_resp
|
|
else:
|
|
recipe["tasks"] = []
|
|
|
|
return render_template("maker/task_editor.html", recipe=recipe)
|
|
|
|
|
|
@maker_bp.route("/recipes/<int:recipe_id>/preview")
|
|
@login_required
|
|
@role_required("Maker")
|
|
def recipe_preview(recipe_id: int):
|
|
"""Preview recipe as MeasurementTec would see it."""
|
|
# Carica recipe completo
|
|
resp = api_client.get(f"/api/recipes/{recipe_id}")
|
|
|
|
if resp.get("error"):
|
|
flash(_("Errore nel caricamento della ricetta: %(error)s", error=resp.get("detail", "")), "error")
|
|
return redirect(url_for("maker.recipe_list"))
|
|
|
|
recipe = resp
|
|
|
|
# Carica tasks con subtasks
|
|
tasks_resp = api_client.get(f"/api/recipes/{recipe_id}/tasks")
|
|
if isinstance(tasks_resp, list):
|
|
recipe["tasks"] = tasks_resp
|
|
else:
|
|
recipe["tasks"] = []
|
|
|
|
return render_template("maker/recipe_preview.html", recipe=recipe)
|
|
|
|
|
|
@maker_bp.route("/recipes/<int:recipe_id>/versions")
|
|
@login_required
|
|
@role_required("Maker")
|
|
def version_history(recipe_id: int):
|
|
"""Version history with diff."""
|
|
# Carica recipe base
|
|
resp = api_client.get(f"/api/recipes/{recipe_id}")
|
|
|
|
if resp.get("error"):
|
|
flash(_("Errore nel caricamento della ricetta: %(error)s", error=resp.get("detail", "")), "error")
|
|
return redirect(url_for("maker.recipe_list"))
|
|
|
|
recipe = resp
|
|
|
|
# Carica versioni
|
|
versions_resp = api_client.get(f"/api/recipes/{recipe_id}/versions")
|
|
if isinstance(versions_resp, list):
|
|
versions = versions_resp
|
|
else:
|
|
versions = []
|
|
flash(_("Errore nel caricamento delle versioni: %(error)s",
|
|
error=versions_resp.get("detail", "") if isinstance(versions_resp, dict) else ""), "warning")
|
|
|
|
return render_template("maker/version_history.html", recipe=recipe, versions=versions)
|
|
|
|
|
|
# ============================================================================
|
|
# API PROXY AJAX (JSON)
|
|
# ============================================================================
|
|
|
|
@maker_bp.route("/api/recipes", methods=["POST"])
|
|
@login_required
|
|
@role_required("Maker")
|
|
def api_create_recipe():
|
|
"""Proxy: Create new recipe."""
|
|
data = request.get_json(silent=True) or {}
|
|
|
|
resp = api_client.post("/api/recipes", data=data)
|
|
|
|
if resp.get("error"):
|
|
return jsonify(resp), resp.get("status_code", 500)
|
|
|
|
return jsonify(resp), 201
|
|
|
|
|
|
@maker_bp.route("/api/recipes/<int:recipe_id>", methods=["PUT"])
|
|
@login_required
|
|
@role_required("Maker")
|
|
def api_update_recipe(recipe_id: int):
|
|
"""Proxy: Update recipe (creates new version)."""
|
|
data = request.get_json(silent=True) or {}
|
|
|
|
resp = api_client.put(f"/api/recipes/{recipe_id}", data=data)
|
|
|
|
if resp.get("error"):
|
|
return jsonify(resp), resp.get("status_code", 500)
|
|
|
|
return jsonify(resp), 200
|
|
|
|
|
|
@maker_bp.route("/api/recipes/<int:recipe_id>", methods=["DELETE"])
|
|
@login_required
|
|
@role_required("Maker")
|
|
def api_delete_recipe(recipe_id: int):
|
|
"""Proxy: Delete recipe (soft delete)."""
|
|
resp = api_client.delete(f"/api/recipes/{recipe_id}")
|
|
|
|
if resp.get("error"):
|
|
return jsonify(resp), resp.get("status_code", 500)
|
|
|
|
return jsonify(resp), 200
|
|
|
|
|
|
# ============================================================================
|
|
# TASK API PROXY
|
|
# ============================================================================
|
|
|
|
@maker_bp.route("/api/recipes/<int:recipe_id>/tasks", methods=["POST"])
|
|
@login_required
|
|
@role_required("Maker")
|
|
def api_create_task(recipe_id: int):
|
|
"""Proxy: Create new task."""
|
|
data = request.get_json(silent=True) or {}
|
|
|
|
resp = api_client.post(f"/api/recipes/{recipe_id}/tasks", data=data)
|
|
|
|
if resp.get("error"):
|
|
return jsonify(resp), resp.get("status_code", 500)
|
|
|
|
return jsonify(resp), 201
|
|
|
|
|
|
@maker_bp.route("/api/tasks/<int:task_id>", methods=["PUT"])
|
|
@login_required
|
|
@role_required("Maker")
|
|
def api_update_task(task_id: int):
|
|
"""Proxy: Update task."""
|
|
data = request.get_json(silent=True) or {}
|
|
|
|
resp = api_client.put(f"/api/tasks/{task_id}", data=data)
|
|
|
|
if resp.get("error"):
|
|
return jsonify(resp), resp.get("status_code", 500)
|
|
|
|
return jsonify(resp), 200
|
|
|
|
|
|
@maker_bp.route("/api/tasks/<int:task_id>", methods=["DELETE"])
|
|
@login_required
|
|
@role_required("Maker")
|
|
def api_delete_task(task_id: int):
|
|
"""Proxy: Delete task."""
|
|
resp = api_client.delete(f"/api/tasks/{task_id}")
|
|
|
|
if resp.get("error"):
|
|
return jsonify(resp), resp.get("status_code", 500)
|
|
|
|
# 204 No Content returns empty dict
|
|
return jsonify(resp), 204
|
|
|
|
|
|
@maker_bp.route("/api/tasks/reorder", methods=["PUT"])
|
|
@login_required
|
|
@role_required("Maker")
|
|
def api_reorder_tasks():
|
|
"""Proxy: Reorder tasks."""
|
|
data = request.get_json(silent=True) or {}
|
|
|
|
resp = api_client.put("/api/tasks/reorder", data=data)
|
|
|
|
if resp.get("error"):
|
|
return jsonify(resp), resp.get("status_code", 500)
|
|
|
|
return jsonify(resp), 200
|
|
|
|
|
|
# ============================================================================
|
|
# SUBTASK API PROXY
|
|
# ============================================================================
|
|
|
|
@maker_bp.route("/api/tasks/<int:task_id>/subtasks", methods=["POST"])
|
|
@login_required
|
|
@role_required("Maker")
|
|
def api_create_subtask(task_id: int):
|
|
"""Proxy: Create new subtask."""
|
|
data = request.get_json(silent=True) or {}
|
|
|
|
resp = api_client.post(f"/api/tasks/{task_id}/subtasks", data=data)
|
|
|
|
if resp.get("error"):
|
|
return jsonify(resp), resp.get("status_code", 500)
|
|
|
|
return jsonify(resp), 201
|
|
|
|
|
|
@maker_bp.route("/api/subtasks/<int:subtask_id>", methods=["PUT"])
|
|
@login_required
|
|
@role_required("Maker")
|
|
def api_update_subtask(subtask_id: int):
|
|
"""Proxy: Update subtask."""
|
|
data = request.get_json(silent=True) or {}
|
|
|
|
resp = api_client.put(f"/api/subtasks/{subtask_id}", data=data)
|
|
|
|
if resp.get("error"):
|
|
return jsonify(resp), resp.get("status_code", 500)
|
|
|
|
return jsonify(resp), 200
|
|
|
|
|
|
@maker_bp.route("/api/subtasks/<int:subtask_id>", methods=["DELETE"])
|
|
@login_required
|
|
@role_required("Maker")
|
|
def api_delete_subtask(subtask_id: int):
|
|
"""Proxy: Delete subtask."""
|
|
resp = api_client.delete(f"/api/subtasks/{subtask_id}")
|
|
|
|
if resp.get("error"):
|
|
return jsonify(resp), resp.get("status_code", 500)
|
|
|
|
# 204 No Content returns empty dict
|
|
return jsonify(resp), 204
|
|
|
|
|
|
# ============================================================================
|
|
# FILE API PROXY
|
|
# ============================================================================
|
|
|
|
@maker_bp.route("/api/upload", methods=["POST"])
|
|
@login_required
|
|
@role_required("Maker")
|
|
def api_upload_file():
|
|
"""Proxy: Upload file (multipart forward)."""
|
|
# Controlla se c'è un file
|
|
if "file" not in request.files:
|
|
return jsonify({"error": True, "detail": _("Nessun file caricato")}), 400
|
|
|
|
file = request.files["file"]
|
|
if file.filename == "":
|
|
return jsonify({"error": True, "detail": _("Nome file vuoto")}), 400
|
|
|
|
# Prepara multipart data
|
|
recipe_id = request.form.get("recipe_id")
|
|
version_id = request.form.get("version_id")
|
|
|
|
files = {"file": (file.filename, file.stream, file.content_type)}
|
|
data = {}
|
|
if recipe_id:
|
|
data["recipe_id"] = recipe_id
|
|
if version_id:
|
|
data["version_id"] = version_id
|
|
|
|
# Forward multipart request
|
|
resp = api_client.post("/api/files/upload", data=data, files=files)
|
|
|
|
if resp.get("error"):
|
|
return jsonify(resp), resp.get("status_code", 500)
|
|
|
|
return jsonify(resp), 201
|
|
|
|
|
|
@maker_bp.route("/api/files/<path:file_path>", methods=["DELETE"])
|
|
@login_required
|
|
@role_required("Maker")
|
|
def api_delete_file(file_path: str):
|
|
"""Proxy: Delete file."""
|
|
resp = api_client.delete(f"/api/files/{file_path}")
|
|
|
|
if resp.get("error"):
|
|
return jsonify(resp), resp.get("status_code", 500)
|
|
|
|
# 204 No Content returns empty dict
|
|
return jsonify(resp), 204
|
|
|
|
|
|
# ============================================================================
|
|
# VERSION API PROXY
|
|
# ============================================================================
|
|
|
|
@maker_bp.route("/api/recipes/<int:recipe_id>/versions/<int:version_number>/measurement-count", methods=["GET"])
|
|
@login_required
|
|
@role_required("Maker")
|
|
def api_get_measurement_count(recipe_id: int, version_number: int):
|
|
"""Proxy: Get measurement count for a specific version."""
|
|
resp = api_client.get(f"/api/recipes/{recipe_id}/versions/{version_number}/measurement-count")
|
|
|
|
if resp.get("error"):
|
|
return jsonify(resp), resp.get("status_code", 500)
|
|
|
|
return jsonify(resp), 200
|