Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jbf7qKuxsnAVjxthRy7dnB
This commit is contained in:
@@ -5,6 +5,7 @@ import math
|
||||
import requests as http_requests
|
||||
|
||||
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_babel import gettext as _
|
||||
@@ -701,6 +702,46 @@ def api_dxf_compare_recipe(task_id: int):
|
||||
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)
|
||||
@maker_bp.route("/api/vision/dxf-preview", methods=["POST"])
|
||||
@login_required
|
||||
|
||||
@@ -45,10 +45,102 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
||||
runPxPerMm: null, // #104: scala usata nell'ultima Prova (null = calibrazione stazione)
|
||||
runBandMm: null, // #104: banda 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() {
|
||||
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() {
|
||||
@@ -109,6 +201,7 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
||||
this.tested = false; // #104
|
||||
this.resPx = null; // #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
|
||||
@@ -140,6 +233,8 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
||||
},
|
||||
onDxf(ev) {
|
||||
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.tested = false; // #104
|
||||
this.loadDxfPreview(); // #101: anteprima appena si sceglie il file
|
||||
@@ -267,6 +362,10 @@ function dxfRecipeWizard(taskId, csrf, msgs) {
|
||||
this.notice = this.msgs.saved || 'Saved';
|
||||
// Il componente visionRecipe ricarica cartella, file e uscite delle quote
|
||||
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) {
|
||||
|
||||
@@ -662,11 +662,16 @@
|
||||
<!-- ========== Crea ricetta da DXF (#83): solo dxf_compare ========== -->
|
||||
<template x-if="task.task_type === 'dxf_compare'">
|
||||
<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 #}
|
||||
<button type="button" class="btn btn-secondary text-xs" @click="toggle()">
|
||||
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 #}
|
||||
{# #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') }}
|
||||
</button>
|
||||
<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 -->
|
||||
<div>
|
||||
<label class="tmf-label text-xs">{{ _('Stazione') }}</label>
|
||||
@@ -714,6 +719,8 @@
|
||||
<div>
|
||||
<label class="tmf-label text-xs">{{ _('Disegno (.dxf)') }}</label>
|
||||
<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 -->
|
||||
<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>
|
||||
|
||||
@@ -167,6 +167,106 @@ const PREVIEW = {
|
||||
w5.$refs.viewport.clientWidth = 0; w5.zoom = 1; w5.fit();
|
||||
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); }
|
||||
console.log('tutto ok');
|
||||
})();
|
||||
|
||||
@@ -253,6 +253,79 @@ class TestDxfPreviewProxy:
|
||||
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:
|
||||
def test_stations_page_has_calibration_cell(self, logged_in_client):
|
||||
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 "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 "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
|
||||
|
||||
@@ -3559,3 +3559,28 @@ msgstr "manual mm/px"
|
||||
#: templates/maker/task_editor.html (#104)
|
||||
msgid "calibrazione stazione"
|
||||
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)
|
||||
msgid "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"
|
||||
|
||||
Reference in New Issue
Block a user