From dffb11f9f2f9e652518d4d0c620491c651da2427 Mon Sep 17 00:00:00 2001 From: "Adriano (daitarn)" Date: Wed, 7 Oct 2026 12:37:14 +0200 Subject: [PATCH] task editor: liste Tipo/Unita e tolleranze generali dalle Impostazioni (#47, #48) Co-Authored-By: Claude Sonnet 5.5 --- src/frontend/flask_app/blueprints/maker.py | 39 ++++- .../templates/maker/task_editor.html | 112 +++++++++--- .../flask_app/tests/test_maker_defaults.py | 164 ++++++++++++++++++ 3 files changed, 294 insertions(+), 21 deletions(-) create mode 100644 src/frontend/flask_app/tests/test_maker_defaults.py diff --git a/src/frontend/flask_app/blueprints/maker.py b/src/frontend/flask_app/blueprints/maker.py index 4eed9c3..574bcf9 100644 --- a/src/frontend/flask_app/blueprints/maker.py +++ b/src/frontend/flask_app/blueprints/maker.py @@ -1,4 +1,7 @@ """Maker blueprint - recipe creation and editing.""" +import json +import math + import requests as http_requests from flask import Blueprint, flash, jsonify, redirect, render_template, request, session, url_for @@ -118,7 +121,41 @@ def task_editor(recipe_id: int): else: recipe["tasks"] = [] - return render_template("maker/task_editor.html", recipe=recipe) + # #47/#48: liste e tolleranze generali dalle Impostazioni; chiave assente, + # vuota o non valida -> None -> il template usa le voci scritte a mano + settings = api_client.get("/api/settings") + if not isinstance(settings, dict) or settings.get("error"): + settings = {} + + return render_template( + "maker/task_editor.html", + recipe=recipe, + list_measurement_types=_settings_string_list(settings.get("list_measurement_types")), + list_units=_settings_string_list(settings.get("list_units")), + general_tolerance_plus=_settings_tolerance(settings.get("general_tolerance_plus")), + general_tolerance_minus=_settings_tolerance(settings.get("general_tolerance_minus")), + ) + + +def _settings_string_list(value): + """#47: array JSON di stringhe non vuote dalle Impostazioni, altrimenti None.""" + try: + items = json.loads(value) if isinstance(value, str) else None + except ValueError: + return None + if not isinstance(items, list): + return None + items = [i.strip() for i in items if isinstance(i, str) and i.strip()] + return items or None + + +def _settings_tolerance(value): + """#48: tolleranza generale (decimale >= 0) dalle Impostazioni, altrimenti None.""" + try: + number = float(str(value).strip()) + except ValueError: + return None + return number if math.isfinite(number) and number >= 0 else None @maker_bp.route("/recipes//tasks//drawing") diff --git a/src/frontend/flask_app/templates/maker/task_editor.html b/src/frontend/flask_app/templates/maker/task_editor.html index 0921dfe..b1fe50a 100644 --- a/src/frontend/flask_app/templates/maker/task_editor.html +++ b/src/frontend/flask_app/templates/maker/task_editor.html @@ -767,18 +767,14 @@
+
@@ -786,24 +782,20 @@
+
@@ -814,6 +806,7 @@ @@ -835,10 +828,14 @@ + +

@@ -1227,6 +1224,27 @@ function taskEditor() { // Uscite numeriche della ricetta visione per task (#46), riempite da vision_recipe.js visionOutputs: {}, + // ---- Liste e tolleranze generali dalle Impostazioni (#47, #48); null = non configurato ---- + configuredTypes: {{ list_measurement_types|default(none)|tojson }}, + configuredUnits: {{ list_units|default(none)|tojson }}, + generalTolPlus: {{ general_tolerance_plus|default(none)|tojson }}, + generalTolMinus: {{ general_tolerance_minus|default(none)|tojson }}, + // #48: true = utl/ltl si riempiono da soli dal nominale (solo quota nuova, finche' non li tocca l'utente) + tolAuto: false, + // Voci scritte a mano usate quando la lista non e' configurata (#47) + defaultTypeOptions: [ + {value: 'linear', label: {{ _('Lineare')|tojson }}}, + {value: 'diameter', label: {{ _('Diametro')|tojson }}}, + {value: 'radius', label: {{ _('Raggio')|tojson }}}, + {value: 'angle', label: {{ _('Angolo')|tojson }}}, + {value: 'roughness', label: {{ _('Rugosita')|tojson }}}, + {value: 'torque', label: {{ _('Coppia')|tojson }}}, + {value: 'force', label: {{ _('Forza')|tojson }}}, + {value: 'weight', label: {{ _('Peso')|tojson }}}, + {value: 'other', label: {{ _('Altro')|tojson }}} + ], + defaultUnits: ['mm', 'cm', 'm', '\u03bcm', 'in', '\u00b0', 'g', 'kg', 'N', 'Nm'], + // ---- Delete modals ---- deleteTaskModal: false, deleteTargetTask: null, @@ -1698,8 +1716,61 @@ function taskEditor() { return Math.max(...task.subtasks.map(s => s.marker_number || 0)) + 1; }, + // #47: opzioni della select Tipo; il valore corrente fuori lista viene aggiunto per non perderlo + measurementTypeOptions() { + const known = v => (this.defaultTypeOptions.find(d => d.value === v) || {label: v}).label; + const opts = this.configuredTypes + ? this.configuredTypes.map(v => ({value: v, label: known(v)})) // un tipo di default noto mantiene l'etichetta tradotta + : this.defaultTypeOptions.slice(); + const cur = this.newSubtask.measurement_type; + if (cur && !opts.some(o => o.value === cur)) opts.push({value: cur, label: known(cur)}); + return opts; + }, + + // #47: opzioni della select Unita, stessa logica del Tipo + unitOptions() { + const opts = (this.configuredUnits || this.defaultUnits).map(v => ({value: v, label: v})); + const cur = this.newSubtask.unit; + if (cur && !opts.some(o => o.value === cur)) opts.push({value: cur, label: cur}); + return opts; + }, + + // #47: unita di partenza di una quota nuova: la prima della lista configurata, altrimenti mm + defaultUnit() { + return this.configuredUnits ? this.configuredUnits[0] : 'mm'; + }, + + // #48: testo "Tolleranze generali: +x / -y" sotto i campi; vuoto se non configurate + generalTolText() { + const parts = []; + if (this.generalTolPlus !== null) parts.push('+' + this.generalTolPlus); + if (this.generalTolMinus !== null) parts.push('\u2212' + this.generalTolMinus); + return parts.length ? {{ _('Tolleranze generali')|tojson }} + ': ' + parts.join(' / ') : ''; + }, + + // #48: su quota nuova, scrivendo il nominale riempie utl = nominale + plus e ltl = nominale - minus + onNominalInput(ev) { + if (!this.tolAuto || this.editingSubtaskId) return; + const raw = String(ev.target.value).trim(); + const nominal = parseFloat(raw); + if (raw === '' || !isFinite(nominal)) { + // nominale svuotato: i limiti riempiti da noi tornano vuoti + this.newSubtask.utl = null; + this.newSubtask.ltl = null; + return; + } + // decimali: quelli del nominale o delle tolleranze generali, al massimo 4 + const dec = s => (String(s).split('.')[1] || '').length; + const decimals = Math.min(4, Math.max(dec(raw), dec(this.generalTolPlus), dec(this.generalTolMinus))); + const round = v => Number(v.toFixed(decimals)); + if (this.generalTolPlus !== null) this.newSubtask.utl = round(nominal + this.generalTolPlus); + if (this.generalTolMinus !== null) this.newSubtask.ltl = round(nominal - this.generalTolMinus); + }, + resetNewSubtask(taskId) { const task = this.tasks.find(t => t.id === taskId); + // #48: automatismo attivo solo se c'e' almeno una tolleranza generale + this.tolAuto = this.generalTolPlus !== null || this.generalTolMinus !== null; this.newSubtask = { marker_number: task ? this.nextMarkerNumber(task) : 1, description: '', @@ -1709,7 +1780,7 @@ function taskEditor() { uwl: null, lwl: null, ltl: null, - unit: 'mm', + unit: this.defaultUnit(), // #47 vision_output: '' }; }, @@ -1779,6 +1850,7 @@ function taskEditor() { // Open the same form used for adding, but pre-filled with existing data this.editingSubtaskId = subtask.id; this.addingSubtaskForTask = taskId; + this.tolAuto = false; // #48: una quota esistente non viene mai ricalcolata this.newSubtask = { marker_number: subtask.marker_number, description: subtask.description || '', diff --git a/src/frontend/flask_app/tests/test_maker_defaults.py b/src/frontend/flask_app/tests/test_maker_defaults.py new file mode 100644 index 0000000..116e035 --- /dev/null +++ b/src/frontend/flask_app/tests/test_maker_defaults.py @@ -0,0 +1,164 @@ +"""#47/#48: liste Tipo/Unita e tolleranze generali dalle Impostazioni nel task editor.""" +import json +import os +import re +import shutil +import subprocess +import tempfile + +import pytest + +from tests.conftest import api_get_router + +_RECIPE_ROUTES = { + "/api/recipes/3": {"id": 3, "code": "R", "name": "R"}, + "/api/recipes/3/tasks": [{ + "id": 11, "title": "T", "task_type": "measure", "subtasks": [], + "file_path": None, "order_index": 1, + }], +} + + +def _render(logged_in_client, mock_api_client, settings): + mock_api_client.get.side_effect = api_get_router({**_RECIPE_ROUTES, "/api/settings": settings}) + resp = logged_in_client.get("/maker/recipes/3/tasks") + assert resp.status_code == 200 + return resp.get_data(as_text=True) + + +class TestTaskEditorSettings: + def test_configured_lists_reach_template(self, logged_in_client, mock_api_client): + html = _render(logged_in_client, mock_api_client, { + "list_measurement_types": json.dumps(["Planarita", "Concentricita"]), + "list_units": json.dumps(["mm", "thou"]), + }) + assert 'configuredTypes: ["Planarita", "Concentricita"]' in html + assert 'configuredUnits: ["mm", "thou"]' in html + # le select leggono le liste, non hanno piu' le option scritte a mano + assert 'x-for="o in measurementTypeOptions()"' in html + assert 'x-for="o in unitOptions()"' in html + assert '