merge #105 105b: wizard Modifica ricetta da DXF (Refs #105)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jbf7qKuxsnAVjxthRy7dnB
This commit is contained in:
2026-10-25 16:31:10 +01:00
7 changed files with 374 additions and 3 deletions
@@ -5,6 +5,7 @@ import math
import requests as http_requests import requests as http_requests
from functools import wraps from functools import wraps
from urllib.parse import quote # #105: percorso del file ricetta nell'URL del backend
from flask import Blueprint, Response, abort, flash, jsonify, redirect, render_template, request, session, url_for from flask import Blueprint, Response, abort, flash, jsonify, redirect, render_template, request, session, url_for
from flask_babel import gettext as _ from flask_babel import gettext as _
@@ -701,6 +702,46 @@ def api_dxf_compare_recipe(task_id: int):
return jsonify(resp), 200 return jsonify(resp), 200
# #105: parametri della ricetta DXF gia' salvata sul task, per riaprire il wizard compilato
@maker_bp.route("/api/vision/tasks/<int:task_id>/recipe/dxf-compare/source", methods=["GET"])
@login_required
@role_required("Maker")
def api_dxf_compare_source(task_id: int):
"""Proxy: parametri e file della ricetta DXF del task (404 se non ce n'e' una) (#105)."""
resp = api_client.get(f"/api/vision/tasks/{task_id}/recipe/dxf-compare/source")
# Codice e corpo del backend arrivano com'e': il 404 vuol dire «nessuna ricetta da DXF»
if resp.get("error"):
return jsonify(resp), resp.get("status_code", 500)
return jsonify(resp), 200
# #105: un file della cartella ricetta (DXF, immagine di riferimento) come byte, non JSON
@maker_bp.route("/api/vision/tasks/<int:task_id>/recipe/files/<path:path>", methods=["GET"])
@login_required
@role_required("Maker")
def api_vision_recipe_file(task_id: int, path: str):
"""Proxy: byte di un file della cartella ricetta del task, con content-type del backend (#105)."""
# Niente segmenti '..' o backslash: urllib3 normalizzerebbe l'URL fuori da questa rotta del backend
if "\\" in path or ".." in path.split("/"):
return jsonify({"error": True, "detail": _("File non trovato")}), 404
try:
resp = http_requests.get(
f"{Config.API_SERVER_URL.rstrip('/')}/api/vision/tasks/{task_id}/recipe/files/{quote(path)}",
headers={"X-API-Key": session.get("api_key", "")},
timeout=30,
)
except http_requests.RequestException as exc:
return jsonify({
"error": True,
"detail": _("Servizio non raggiungibile: %(detail)s", detail=str(exc)),
}), 502
return Response(
resp.content,
status=resp.status_code,
content_type=resp.headers.get("content-type", "application/octet-stream"),
)
# #101: anteprima del DXF appena scelto nel wizard (contorni letti dal vision worker) # #101: anteprima del DXF appena scelto nel wizard (contorni letti dal vision worker)
@maker_bp.route("/api/vision/dxf-preview", methods=["POST"]) @maker_bp.route("/api/vision/dxf-preview", methods=["POST"])
@login_required @login_required
@@ -45,10 +45,102 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
runPxPerMm: null, // #104: scala usata nell'ultima Prova (null = calibrazione stazione) runPxPerMm: null, // #104: scala usata nell'ultima Prova (null = calibrazione stazione)
runBandMm: null, // #104: banda usata nell'ultima Prova runBandMm: null, // #104: banda usata nell'ultima Prova
runMinPointsPct: null, // #104: soglia usata nell'ultima Prova runMinPointsPct: null, // #104: soglia usata nell'ultima Prova
existing: null, // #105: risposta di /source (ricetta DXF gia' salvata sul task); null = nessuna
existingInfo: false, // #105: «Ricetta esistente caricata...» dopo aver compilato il wizard
existingWarn: '', // #105: avviso (immagine non salvata, file illeggibili)
dxfFromRecipe: false, // #105: il DXF viene dalla ricetta (l'<input type=file> non puo' mostrarne il nome)
_existingApplied: false, // #105: il wizard e' gia' stato compilato dalla ricetta (non sovrascrivere le modifiche)
_existingReady: null, // #105: promessa della prima lettura di /source
_srcSeq: 0, // #105: una risposta di /source arrivata dopo una lettura piu' recente si scarta
_dxfPickSeq: 0, // #105: sale a ogni scelta manuale del DXF; i file della ricetta arrivati dopo si scartano
_imgPickSeq: 0, // #105: idem per l'immagine (_setImage)
// #105: all'avvio legge se il task ha gia' una ricetta da DXF (serve per l'etichetta del bottone)
init() {
if (this.taskId) this._existingReady = this.loadExisting();
},
async toggle() { async toggle() {
this.open = !this.open; this.open = !this.open;
if (this.open && !this.stations.length) await this.loadStations(); if (this.open) {
this.openExisting(); // #105: compila il wizard dalla ricetta esistente, in parallelo alle stazioni
if (!this.stations.length) await this.loadStations();
}
},
// #105: GET /source -> this.existing (null se 404 o errore)
async loadExisting() {
const seq = ++this._srcSeq;
let found = null;
try {
const resp = await fetch(`/maker/api/vision/tasks/${this.taskId}/recipe/dxf-compare/source`);
if (resp.ok) {
const body = await resp.json().catch(() => null);
if (body && typeof body === 'object' && !body.error) found = body;
}
} catch (e) { console.error('dxfRecipeWizard source', e); }
if (seq !== this._srcSeq) return; // lettura piu' recente in corso
this.existing = found;
},
// #105: all'apertura, una sola volta, riempie il wizard con la ricetta esistente
async openExisting() {
if (this._existingReady) await this._existingReady;
if (!this.existing || this._existingApplied) return;
await this.applyExisting();
},
// #105: mm/px da px/mm, fino a 6 cifre significative e virgola decimale; '' se non valido
_mmPxFrom(pxPerMm) {
const v = Number(pxPerMm);
if (!Number.isFinite(v) || v <= 0) return '';
return String(Number((1 / v).toPrecision(6))).replace('.', ',');
},
// #105: un file della cartella ricetta -> File; null se assente o illeggibile (in quel caso avvisa)
async _fetchRecipeFile(name) {
try {
const resp = await fetch(`/maker/api/vision/tasks/${this.taskId}/recipe/files/${encodeURIComponent(name)}`);
if (!resp.ok) throw new Error('HTTP ' + resp.status);
const blob = await resp.blob();
return new File([blob], name, { type: blob.type || '' });
} catch (e) {
console.error('dxfRecipeWizard file', name, e);
this.existingWarn = this.msgs.existingFilesFailed || 'Cannot read the recipe files';
return null;
}
},
// #105: valori salvati nei campi, poi DXF e immagine di riferimento dalla cartella ricetta
async applyExisting() {
const e = this.existing;
if (!e) return;
this._existingApplied = true;
this.error = '';
this.notice = '';
this.existingWarn = '';
const band = Number(e.band_mm), pct = Number(e.min_points_pct);
if (Number.isFinite(band) && band > 0) this.bandMm = band;
if (Number.isFinite(pct) && pct > 0) this.minPointsPct = pct;
this.dark = e.polarity === 'dark';
const mm = this._mmPxFrom(e.px_per_mm);
if (mm) this.mmPx = mm; // il campo compare quando l'immagine e' da file
this.existingInfo = true;
// Sequenze lette prima di scaricare: se l'utente sceglie un file a mano nel frattempo, vince lui
const dxfSeq = this._dxfPickSeq, imgSeq = this._imgPickSeq;
const [dxf, img] = await Promise.all([
e.dxf ? this._fetchRecipeFile(e.dxf) : null,
e.image ? this._fetchRecipeFile(e.image) : null,
]);
if (dxf && dxfSeq === this._dxfPickSeq) {
this.dxfFile = dxf;
this.dxfFromRecipe = true;
this.result = null;
this.tested = false;
this.loadDxfPreview();
}
if (img && imgSeq === this._imgPickSeq) this._setImage(img, 'file'); // 'file': il campo mm/px compare gia' compilato
if (!e.image && !this.existingWarn) this.existingWarn = this.msgs.noReferenceImage || 'Reference image not saved';
}, },
async loadStations() { async loadStations() {
@@ -109,6 +201,7 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
this.tested = false; // #104 this.tested = false; // #104
this.resPx = null; // #104 this.resPx = null; // #104
this.imageSource = file ? (source || '') : ''; // #104 this.imageSource = file ? (source || '') : ''; // #104
this._imgPickSeq++; // #105: una scelta piu' recente batte i file della ricetta ancora in arrivo
}, },
// #102: l'<img> dell'anteprima e' caricata: legge i pixel; se non si carica il formato non e' mostrabile // #102: l'<img> dell'anteprima e' caricata: legge i pixel; se non si carica il formato non e' mostrabile
@@ -140,6 +233,8 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
}, },
onDxf(ev) { onDxf(ev) {
this.dxfFile = (ev.target.files && ev.target.files[0]) || null; this.dxfFile = (ev.target.files && ev.target.files[0]) || null;
this._dxfPickSeq++; // #105: scelta a mano: il DXF della ricetta in arrivo si scarta
this.dxfFromRecipe = false; // #105
this.result = null; this.result = null;
this.tested = false; // #104 this.tested = false; // #104
this.loadDxfPreview(); // #101: anteprima appena si sceglie il file this.loadDxfPreview(); // #101: anteprima appena si sceglie il file
@@ -267,6 +362,10 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
this.notice = this.msgs.saved || 'Saved'; this.notice = this.msgs.saved || 'Saved';
// Il componente visionRecipe ricarica cartella, file e uscite delle quote // Il componente visionRecipe ricarica cartella, file e uscite delle quote
this.$dispatch('dxf-recipe-saved', { taskId: this.taskId }); this.$dispatch('dxf-recipe-saved', { taskId: this.taskId });
// #105: il task ha ora una ricetta (etichetta «Modifica»); il wizard e' gia' allineato, non si ricompila
this._existingApplied = true;
this.existingInfo = false;
await this.loadExisting();
} }
} }
} catch (e) { } catch (e) {
@@ -662,11 +662,16 @@
<!-- ========== Crea ricetta da DXF (#83): solo dxf_compare ========== --> <!-- ========== Crea ricetta da DXF (#83): solo dxf_compare ========== -->
<template x-if="task.task_type === 'dxf_compare'"> <template x-if="task.task_type === 'dxf_compare'">
<div class="mt-4 pt-3 border-t border-[var(--border-color)]" <div class="mt-4 pt-3 border-t border-[var(--border-color)]"
x-data="dxfRecipeWizard(task.id, () => csrfToken(), {{ {'requestFailed': _('Richiesta non riuscita'), 'saved': _('Ricetta salvata'), 'dxfEntities': _('entità'), 'imageField': _('campo'), 'pointsInBand': _('Punti in banda'), 'distAvg': _('Distanza media'), 'distMin': _('Distanza minima'), 'distMax': _('Distanza massima'), 'outcomePass': _('Conforme'), 'outcomeWarning': _('Con avvisi'), 'outcomeFail': _('Non conforme'), 'scaleManual': _('mm/px a mano'), 'scaleStation': _('calibrazione stazione')}|tojson_attr }})">{# #104: etichette del risultato della Prova #} x-data="dxfRecipeWizard(task.id, () => csrfToken(), {{ {'requestFailed': _('Richiesta non riuscita'), 'saved': _('Ricetta salvata'), 'dxfEntities': _('entità'), 'imageField': _('campo'), 'pointsInBand': _('Punti in banda'), 'distAvg': _('Distanza media'), 'distMin': _('Distanza minima'), 'distMax': _('Distanza massima'), 'outcomePass': _('Conforme'), 'outcomeWarning': _('Con avvisi'), 'outcomeFail': _('Non conforme'), 'scaleManual': _('mm/px a mano'), 'scaleStation': _('calibrazione stazione'), 'createLabel': _('Crea ricetta da DXF'), 'editLabel': _('Modifica ricetta da DXF'), 'noReferenceImage': _('Immagine di riferimento non salvata (ricetta creata prima): acquisiscila o caricala'), 'existingFilesFailed': _('Impossibile leggere i file della ricetta esistente')}|tojson_attr }})">{# #104: etichette del risultato della Prova; #105: etichette del bottone e avvisi della ricetta esistente #}
<button type="button" class="btn btn-secondary text-xs" @click="toggle()"> {# #105: «Modifica ricetta da DXF» se il task ha gia' una ricetta da DXF #}
<button type="button" class="btn btn-secondary text-xs" @click="toggle()"
x-text="existing ? msgs.editLabel : msgs.createLabel">
{{ _('Crea ricetta da DXF') }} {{ _('Crea ricetta da DXF') }}
</button> </button>
<div x-show="open" x-cloak class="mt-3 space-y-3"> <div x-show="open" x-cloak class="mt-3 space-y-3">
<!-- #105: ricetta esistente caricata nel wizard -->
<p x-show="existingInfo" class="text-xs text-emerald-600">{{ _('Ricetta esistente caricata: Prova e poi Salva ricetta per sostituirla') }}</p>
<p x-show="existingWarn" class="text-xs text-amber-600" x-text="existingWarn"></p>
<!-- Stazione --> <!-- Stazione -->
<div> <div>
<label class="tmf-label text-xs">{{ _('Stazione') }}</label> <label class="tmf-label text-xs">{{ _('Stazione') }}</label>
@@ -714,6 +719,8 @@
<div> <div>
<label class="tmf-label text-xs">{{ _('Disegno (.dxf)') }}</label> <label class="tmf-label text-xs">{{ _('Disegno (.dxf)') }}</label>
<input type="file" accept=".dxf" class="tmf-input text-xs" @change="onDxf($event)"> <input type="file" accept=".dxf" class="tmf-input text-xs" @change="onDxf($event)">
<!-- #105: DXF preso dalla ricetta esistente (l'input file non puo' mostrarne il nome) -->
<p x-show="dxfFromRecipe && dxfFile" class="mt-1 text-xs text-[var(--text-muted)]" x-text="dxfFile ? dxfFile.name : ''"></p>
<!-- #101: lettura del disegno appena scelto il .dxf --> <!-- #101: lettura del disegno appena scelto il .dxf -->
<p x-show="dxfPreviewBusy" class="mt-1 text-xs text-[var(--text-muted)]">{{ _('Lettura DXF…') }}</p> <p x-show="dxfPreviewBusy" class="mt-1 text-xs text-[var(--text-muted)]">{{ _('Lettura DXF…') }}</p>
<p x-show="dxfPreviewError" class="mt-1 text-xs text-red-500" x-text="dxfPreviewError"></p> <p x-show="dxfPreviewError" class="mt-1 text-xs text-red-500" x-text="dxfPreviewError"></p>
@@ -167,6 +167,106 @@ const PREVIEW = {
w5.$refs.viewport.clientWidth = 0; w5.zoom = 1; w5.fit(); w5.$refs.viewport.clientWidth = 0; w5.zoom = 1; w5.fit();
check('#104 fit con viewport nascosto non cambia', w5.zoom === 1); check('#104 fit con viewport nascosto non cambia', w5.zoom === 1);
// #105: ricetta esistente -> wizard compilato
global.File = class { constructor(parts, name, opts) { this.parts = parts; this.name = name; this.type = (opts && opts.type) || ''; } };
const SRC = { band_mm: 0.3, min_points_pct: 90, polarity: 'light', px_per_mm: 71.1237, dxf: 'disegno.dxf', image: 'riferimento.bmp' };
const blobOf = t => ({ type: t });
const mkFetch = (routes, calls) => async (url) => {
calls.push(url);
for (const k of Object.keys(routes)) if (url.endsWith(k)) return routes[k]();
return { ok: false, status: 404, json: async () => ({ error: true }) };
};
const okJson = body => () => ({ ok: true, json: async () => body });
const okBlob = t => () => ({ ok: true, blob: async () => blobOf(t) });
const w6 = dxfRecipeWizard(8, () => 'tok', { noReferenceImage: 'senza immagine', editLabel: 'Modifica', createLabel: 'Crea' });
w6.$nextTick = f => f();
const calls6 = [];
global.fetch = mkFetch({
'/recipe/dxf-compare/source': okJson(SRC),
'/recipe/files/disegno.dxf': okBlob('application/dxf'),
'/recipe/files/riferimento.bmp': okBlob('image/bmp'),
'/dxf-preview': okJson(PREVIEW),
}, calls6);
global.FormData = class { append() {} };
check('#105 senza ricetta: existing null', w6.existing === null);
w6.init();
await w6._existingReady;
check('#105 init legge /source', w6.existing && w6.existing.band_mm === 0.3 && calls6[0] === '/maker/api/vision/tasks/8/recipe/dxf-compare/source', calls6.join(','));
await w6.toggle();
await w6.openExisting();
check('#105 apertura: banda, soglia, polarita', w6.bandMm === 0.3 && w6.minPointsPct === 90 && w6.dark === false);
check('#105 mm/px = 1/px_per_mm con virgola', w6.mmPx === '0,01406', w6.mmPx);
check('#105 DXF caricato dalla cartella', w6.dxfFile && w6.dxfFile.name === 'disegno.dxf' && w6.dxfFromRecipe === true);
await new Promise(r => setTimeout(r, 0)); // l'anteprima parte senza essere attesa
check('#105 anteprima DXF richiesta', calls6.some(u => u.endsWith('/dxf-preview')) && w6.dxfPreview !== null);
check('#105 immagine con source file: il campo mm/px vale', w6.imageFile && w6.imageFile.name === 'riferimento.bmp'
&& w6.imageSource === 'file' && w6.imageSrc === 'blob:x');
check('#105 info mostrata, nessun avviso', w6.existingInfo === true && w6.existingWarn === '');
const nCalls = calls6.length;
w6.bandMm = 0.2;
await w6.openExisting();
check('#105 seconda apertura non ricompila', w6.bandMm === 0.2 && calls6.length === nCalls);
// #105: ricetta senza immagine salvata -> avviso
const w7 = dxfRecipeWizard(8, () => 'tok', { noReferenceImage: 'senza immagine' });
global.fetch = mkFetch({
'/recipe/dxf-compare/source': okJson({ ...SRC, image: null, px_per_mm: null }),
'/recipe/files/disegno.dxf': okBlob('application/dxf'),
'/dxf-preview': okJson(PREVIEW),
}, []);
w7.init(); await w7._existingReady; await w7.openExisting();
check('#105 senza immagine: avviso e nessun file', w7.existingWarn === 'senza immagine' && w7.imageFile === null && w7.imageSource === '');
check('#105 px_per_mm null: mm/px resta vuoto', w7.mmPx === '');
// #105: 404 -> existing null, etichetta «Crea»
const w8 = dxfRecipeWizard(9, () => 'tok', {});
global.fetch = mkFetch({}, []);
w8.init(); await w8._existingReady; await w8.openExisting();
check('#105 404: existing null e nessuna compilazione', w8.existing === null && w8.existingInfo === false && w8.dxfFile === null);
// #105: file scelti a mano durante il download della ricetta non si sovrascrivono
const w9 = dxfRecipeWizard(8, () => 'tok', {});
const gates = {};
const gate = k => () => new Promise(res => { gates[k] = () => res({ ok: true, blob: async () => blobOf(k) }); });
global.fetch = mkFetch({
'/recipe/dxf-compare/source': okJson(SRC),
'/recipe/files/disegno.dxf': gate('dxf'),
'/recipe/files/riferimento.bmp': gate('bmp'),
'/dxf-preview': okJson(PREVIEW),
}, []);
w9.init(); await w9._existingReady;
const pa = w9.openExisting();
await new Promise(r => setTimeout(r, 0));
w9.onDxf({ target: { files: [{ name: 'mio.dxf' }] } });
w9._setImage({ name: 'mia.bmp' }, 'file');
gates.dxf(); gates.bmp();
await pa;
check('#105 DXF scelto a mano non sovrascritto', w9.dxfFile.name === 'mio.dxf' && w9.dxfFromRecipe === false);
check('#105 immagine scelta a mano non sovrascritta', w9.imageFile.name === 'mia.bmp');
// #105: file della ricetta illeggibile -> avviso
const w10 = dxfRecipeWizard(8, () => 'tok', { existingFilesFailed: 'illeggibili' });
global.fetch = mkFetch({
'/recipe/dxf-compare/source': okJson(SRC),
'/recipe/files/disegno.dxf': () => ({ ok: false, status: 404 }),
'/recipe/files/riferimento.bmp': okBlob('image/bmp'),
}, []);
w10.init(); await w10._existingReady; await w10.openExisting();
check('#105 DXF illeggibile: avviso, immagine comunque caricata', w10.existingWarn === 'illeggibili' && w10.dxfFile === null && w10.imageFile !== null);
// #105: dopo Salva si rilegge /source e il wizard non si ricompila
const w11 = dxfRecipeWizard(8, () => 'tok', {});
w11.$nextTick = f => f(); w11.$dispatch = () => {}; w11.$refs = {};
w11.stationId = '1'; w11.calState = 'calibrated'; w11.calPxMm = 50;
w11.imageFile = { name: 'a.bmp' }; w11.dxfFile = { name: 'd.dxf' };
global.FormData = class { append() {} };
global.fetch = async (url, opts) => url.endsWith('/source')
? { ok: true, json: async () => SRC }
: { ok: true, json: async () => ({ saved: true, result: { outputs: {}, overlay: [], outcome: 'pass' } }) };
await w11.run(true);
check('#105 dopo Salva: existing letto e non ricompilato', w11.existing && w11.existing.band_mm === 0.3 && w11._existingApplied === true);
if (failures) { console.log(failures + ' fallimenti'); process.exit(1); } if (failures) { console.log(failures + ' fallimenti'); process.exit(1); }
console.log('tutto ok'); console.log('tutto ok');
})(); })();
@@ -253,6 +253,79 @@ class TestDxfPreviewProxy:
mock_api_client.post.assert_not_called() mock_api_client.post.assert_not_called()
class TestDxfSourceProxy:
"""#105: parametri della ricetta DXF gia' salvata (proxy verso il backend)."""
URL = "/maker/api/vision/tasks/7/recipe/dxf-compare/source"
OK = {"band_mm": 0.3, "min_points_pct": 95.0, "polarity": "dark", "px_per_mm": 71.12,
"dxf": "disegno.dxf", "image": "riferimento.bmp"}
def test_forwards_body(self, logged_in_client, mock_api_client):
mock_api_client.get.return_value = self.OK
resp = logged_in_client.get(self.URL)
assert resp.status_code == 200
assert resp.get_json() == self.OK
assert mock_api_client.get.call_args.args[0] == "/api/vision/tasks/7/recipe/dxf-compare/source"
def test_404_propagates(self, logged_in_client, mock_api_client):
mock_api_client.get.return_value = {
"error": True, "status_code": 404, "detail": "nessuna ricetta da DXF su questo task"}
resp = logged_in_client.get(self.URL)
assert resp.status_code == 404
assert resp.get_json()["detail"] == "nessuna ricetta da DXF su questo task"
def test_requires_maker_role(self, logged_in_client, mock_api_client):
with logged_in_client.session_transaction() as sess:
sess["user"] = {**sess["user"], "roles": ["MeasurementTec"]}
assert logged_in_client.get(self.URL).status_code == 403
mock_api_client.get.assert_not_called()
class TestRecipeFileProxy:
"""#105: file della cartella ricetta (DXF, immagine di riferimento) passati come byte."""
URL = "/maker/api/vision/tasks/7/recipe/files/riferimento.bmp"
BMP = b"BM 1 fake bmp"
def _fake_file(self, status=200, content=BMP, ctype="image/bmp"):
resp = MagicMock(status_code=status, content=content)
resp.headers = {"content-type": ctype}
return resp
def test_bytes_and_content_type_pass_through(self, logged_in_client, mock_api_client):
with patch("blueprints.maker.http_requests.get", return_value=self._fake_file()) as get:
resp = logged_in_client.get(self.URL)
assert resp.status_code == 200
assert resp.data == self.BMP
assert resp.mimetype == "image/bmp"
args, kwargs = get.call_args
assert args[0].endswith("/api/vision/tasks/7/recipe/files/riferimento.bmp")
assert kwargs["headers"]["X-API-Key"] == "test-api-key-12345"
def test_backend_404_propagates(self, logged_in_client, mock_api_client):
body = b'{"detail":"file non trovato"}'
with patch("blueprints.maker.http_requests.get",
return_value=self._fake_file(404, body, "application/json")):
resp = logged_in_client.get(self.URL)
assert resp.status_code == 404
assert resp.get_json()["detail"] == "file non trovato"
def test_unsafe_path_is_404_without_calling_backend(self, logged_in_client, mock_api_client):
with patch("blueprints.maker.http_requests.get") as get:
for path in ("a/../../x", "..%5Cx", "%2e%2e/x"):
assert logged_in_client.get(f"/maker/api/vision/tasks/7/recipe/files/{path}").status_code == 404
get.assert_not_called()
def test_service_down_is_502(self, logged_in_client, mock_api_client):
with patch("blueprints.maker.http_requests.get", side_effect=requests.ConnectionError("giu")):
assert logged_in_client.get(self.URL).status_code == 502
def test_requires_maker_role(self, logged_in_client, mock_api_client):
with logged_in_client.session_transaction() as sess:
sess["user"] = {**sess["user"], "roles": ["MeasurementTec"]}
with patch("blueprints.maker.http_requests.get") as get:
assert logged_in_client.get(self.URL).status_code == 403
get.assert_not_called()
class TestPages: class TestPages:
def test_stations_page_has_calibration_cell(self, logged_in_client): def test_stations_page_has_calibration_cell(self, logged_in_client):
with patch("blueprints.admin.api_client") as admin_api: with patch("blueprints.admin.api_client") as admin_api:
@@ -282,4 +355,5 @@ class TestPages:
assert "dxfPreviewD()" in html and "view-box.camel" in html # #101: anteprima SVG del DXF assert "dxfPreviewD()" in html and "view-box.camel" in html # #101: anteprima SVG del DXF
assert "imageSource === 'file'" in html and "zoomBy(" in html and "fit()" in html # #104: mm/px solo da file, zoom e Fit assert "imageSource === 'file'" in html and "zoomBy(" in html and "fit()" in html # #104: mm/px solo da file, zoom e Fit
assert "imagePreviewUrl" in html and "imagePreviewSummary" in html # #102: anteprima immagine assert "imagePreviewUrl" in html and "imagePreviewSummary" in html # #102: anteprima immagine
assert "existing ? msgs.editLabel : msgs.createLabel" in html and "Modifica ricetta da DXF" in html # #105: etichetta dinamica
assert "Pezzo scuro su fondo chiaro" in html or "Dark part on light" in html assert "Pezzo scuro su fondo chiaro" in html or "Dark part on light" in html
@@ -3559,3 +3559,28 @@ msgstr "manual mm/px"
#: templates/maker/task_editor.html (#104) #: templates/maker/task_editor.html (#104)
msgid "calibrazione stazione" msgid "calibrazione stazione"
msgstr "station calibration" msgstr "station calibration"
#: templates/maker/task_editor.html blueprints/maker.py (#105)
msgid "Modifica ricetta da DXF"
msgstr "Edit recipe from DXF"
#: templates/maker/task_editor.html blueprints/maker.py (#105)
msgid "Immagine di riferimento non salvata (ricetta creata prima): acquisiscila o caricala"
msgstr "Reference image not saved (recipe created earlier): capture or upload it"
#: templates/maker/task_editor.html blueprints/maker.py (#105)
msgid "Impossibile leggere i file della ricetta esistente"
msgstr "Cannot read the files of the existing recipe"
#: templates/maker/task_editor.html blueprints/maker.py (#105)
msgid "Ricetta esistente caricata: Prova e poi Salva ricetta per sostituirla"
msgstr "Existing recipe loaded: Test, then Save recipe to replace it"
#: templates/maker/task_editor.html blueprints/maker.py (#105)
msgid "File non trovato"
msgstr "File not found"
#: templates/maker/task_editor.html blueprints/maker.py (#105)
#, python-format
msgid "Servizio non raggiungibile: %(detail)s"
msgstr "Service not reachable: %(detail)s"
@@ -3543,3 +3543,28 @@ msgstr "mm/px a mano"
#: templates/maker/task_editor.html (#104) #: templates/maker/task_editor.html (#104)
msgid "calibrazione stazione" msgid "calibrazione stazione"
msgstr "calibrazione stazione" msgstr "calibrazione stazione"
#: templates/maker/task_editor.html blueprints/maker.py (#105)
msgid "Modifica ricetta da DXF"
msgstr "Modifica ricetta da DXF"
#: templates/maker/task_editor.html blueprints/maker.py (#105)
msgid "Immagine di riferimento non salvata (ricetta creata prima): acquisiscila o caricala"
msgstr "Immagine di riferimento non salvata (ricetta creata prima): acquisiscila o caricala"
#: templates/maker/task_editor.html blueprints/maker.py (#105)
msgid "Impossibile leggere i file della ricetta esistente"
msgstr "Impossibile leggere i file della ricetta esistente"
#: templates/maker/task_editor.html blueprints/maker.py (#105)
msgid "Ricetta esistente caricata: Prova e poi Salva ricetta per sostituirla"
msgstr "Ricetta esistente caricata: Prova e poi Salva ricetta per sostituirla"
#: templates/maker/task_editor.html blueprints/maker.py (#105)
msgid "File non trovato"
msgstr "File non trovato"
#: templates/maker/task_editor.html blueprints/maker.py (#105)
#, python-format
msgid "Servizio non raggiungibile: %(detail)s"
msgstr "Servizio non raggiungibile: %(detail)s"