maker: camera e modo di analisi nel task visione (Refs #97)
Rotte /maker/api/camera/servizi e /maker/api/camera/sorgenti (url ammesso solo se e' un servizio delle stazioni), sezione «Acquisizione» nell'editor task (servizio, sorgente, analisi impulsiva/in linea) salvata come acquisition_json. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jbf7qKuxsnAVjxthRy7dnB
This commit is contained in:
@@ -541,6 +541,55 @@ def api_list_stations_for_maker():
|
|||||||
return jsonify(out), 200
|
return jsonify(out), 200
|
||||||
|
|
||||||
|
|
||||||
|
def _camera_services() -> list[dict]:
|
||||||
|
"""#97: servizi di acquisizione distinti delle stazioni che hanno una camera_url."""
|
||||||
|
services: dict[str, list[str]] = {}
|
||||||
|
for st in _stations_for_maker():
|
||||||
|
url = (st.get("camera_url") or "").strip().rstrip("/")
|
||||||
|
if not url:
|
||||||
|
continue
|
||||||
|
label = f"{st.get('code') or ''} - {st.get('name') or ''}"
|
||||||
|
services.setdefault(url, []).append(label)
|
||||||
|
return [{"camera_url": u, "stazioni": s} for u, s in services.items()]
|
||||||
|
|
||||||
|
|
||||||
|
@maker_bp.route("/api/camera/servizi", methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
@maker_or_admin_required
|
||||||
|
def api_camera_services():
|
||||||
|
"""#97: servizi di acquisizione fra cui scegliere la camera di un task."""
|
||||||
|
return jsonify({"servizi": _camera_services()}), 200
|
||||||
|
|
||||||
|
|
||||||
|
@maker_bp.route("/api/camera/sorgenti", methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
@maker_or_admin_required
|
||||||
|
def api_camera_sources():
|
||||||
|
"""#97: proxy di GET {url}/camera/sources; solo per i servizi delle stazioni."""
|
||||||
|
url = (request.args.get("url") or "").strip().rstrip("/")
|
||||||
|
# Niente richieste verso indirizzi arbitrari: url ammesso solo se e' un servizio noto
|
||||||
|
if not url or url not in {s["camera_url"] for s in _camera_services()}:
|
||||||
|
return jsonify({
|
||||||
|
"error": True,
|
||||||
|
"detail": _("Servizio di acquisizione non ammesso"),
|
||||||
|
}), 400
|
||||||
|
try:
|
||||||
|
resp = http_requests.get(f"{url}/camera/sources", timeout=15)
|
||||||
|
body = resp.json() if resp.status_code == 200 else None
|
||||||
|
except (http_requests.RequestException, ValueError) as exc:
|
||||||
|
return jsonify({
|
||||||
|
"error": True,
|
||||||
|
"detail": _("Servizio camera non raggiungibile: %(detail)s", detail=str(exc)),
|
||||||
|
}), 502
|
||||||
|
sources = body.get("sources") if isinstance(body, dict) else None
|
||||||
|
if not isinstance(sources, list):
|
||||||
|
return jsonify({
|
||||||
|
"error": True,
|
||||||
|
"detail": _("Risposta del servizio camera non valida (%(status)s)", status=resp.status_code),
|
||||||
|
}), 502
|
||||||
|
return jsonify({"sources": sources}), 200
|
||||||
|
|
||||||
|
|
||||||
# Copia minima della logica di measure.api_vision_grab, ma per una stazione scelta per id
|
# Copia minima della logica di measure.api_vision_grab, ma per una stazione scelta per id
|
||||||
@maker_bp.route("/api/stations/<int:station_id>/camera/grab", methods=["GET", "POST"])
|
@maker_bp.route("/api/stations/<int:station_id>/camera/grab", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
// «Acquisizione» dell'editor task (#97): camera (servizio + sorgente) e modo di analisi
|
||||||
|
// («impulsiva» / «in linea») dei task camera_measure e dxf_compare.
|
||||||
|
// Legge /maker/api/camera/servizi e /maker/api/camera/sorgenti?url=. Lo stato scelto sta
|
||||||
|
// nel componente padre (editTaskData.acquisition): questo componente carica solo gli elenchi.
|
||||||
|
// Uso: x-data="taskAcquisition(() => editTaskData.acquisition, msgs)". Dipende da Alpine.js.
|
||||||
|
function taskAcquisition(getAcq, msgs) {
|
||||||
|
return {
|
||||||
|
msgs: msgs || {},
|
||||||
|
servizi: [],
|
||||||
|
sources: [],
|
||||||
|
loading: false,
|
||||||
|
error: '',
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
await this.loadServizi();
|
||||||
|
// Task con camera gia' scelta: elenco sorgenti del suo servizio
|
||||||
|
const acq = getAcq();
|
||||||
|
if (acq && acq.camera_url) await this.loadSources(acq.camera_url);
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadServizi() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/maker/api/camera/servizi');
|
||||||
|
if (resp.ok) this.servizi = (await resp.json()).servizi || [];
|
||||||
|
} catch (e) { console.error('taskAcquisition servizi', e); }
|
||||||
|
},
|
||||||
|
|
||||||
|
// Servizio salvato nel task ma non piu' fra quelli delle stazioni: resta selezionabile
|
||||||
|
serviziConCorrente() {
|
||||||
|
const acq = getAcq();
|
||||||
|
const out = this.servizi.slice();
|
||||||
|
if (acq && acq.camera_url && !out.some(s => s.camera_url === acq.camera_url)) {
|
||||||
|
out.push({ camera_url: acq.camera_url, stazioni: [] });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Cambio servizio: la sorgente scelta non vale piu'
|
||||||
|
async onServizio() {
|
||||||
|
const acq = getAcq();
|
||||||
|
acq.camera_source = '';
|
||||||
|
this.sources = [];
|
||||||
|
this.error = '';
|
||||||
|
if (acq.camera_url) await this.loadSources(acq.camera_url);
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadSources(url) {
|
||||||
|
this.loading = true;
|
||||||
|
this.error = '';
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/maker/api/camera/sorgenti?url=' + encodeURIComponent(url));
|
||||||
|
const data = await resp.json().catch(() => ({}));
|
||||||
|
if (resp.ok) this.sources = data.sources || [];
|
||||||
|
else this.error = data.detail || this.msgs.genericError || '';
|
||||||
|
} catch (e) {
|
||||||
|
this.error = this.msgs.genericError || String(e);
|
||||||
|
} finally { this.loading = false; }
|
||||||
|
},
|
||||||
|
|
||||||
|
// Sorgente salvata ma non piu' elencata dal servizio: resta visibile
|
||||||
|
sourcesConCorrente() {
|
||||||
|
const acq = getAcq();
|
||||||
|
const out = this.sources.slice();
|
||||||
|
if (acq && acq.camera_source && !out.some(s => s.name === acq.camera_source)) {
|
||||||
|
out.push({ name: acq.camera_source, label: acq.camera_source });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -547,6 +547,71 @@
|
|||||||
|
|
||||||
<div class="tmf-card-body space-y-4">
|
<div class="tmf-card-body space-y-4">
|
||||||
|
|
||||||
|
<!-- ========== Acquisizione (#97): camera e modo di analisi, solo camera_measure / dxf_compare ========== -->
|
||||||
|
<template x-if="task.task_type === 'camera_measure' || task.task_type === 'dxf_compare'">
|
||||||
|
<div class="p-3 rounded-lg bg-[var(--bg-secondary)] border border-[var(--border-color)]">
|
||||||
|
<p class="text-xs font-semibold text-[var(--text-secondary)] uppercase tracking-wide mb-2">
|
||||||
|
{{ _('Acquisizione') }}
|
||||||
|
</p>
|
||||||
|
<!-- Lettura: camera e modo salvati nel task -->
|
||||||
|
<template x-if="editingTask !== task.id">
|
||||||
|
<div class="text-xs space-y-1">
|
||||||
|
<template x-if="task.acquisition_json && task.acquisition_json.camera_url">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<p><span class="text-[var(--text-secondary)]">{{ _('Servizio di acquisizione') }}:</span>
|
||||||
|
<span class="font-mono" x-text="task.acquisition_json.camera_url"></span></p>
|
||||||
|
<p><span class="text-[var(--text-secondary)]">{{ _('Sorgente') }}:</span>
|
||||||
|
<span class="font-mono" x-text="task.acquisition_json.camera_source"></span></p>
|
||||||
|
<p><span class="text-[var(--text-secondary)]">{{ _('Modo di analisi') }}:</span>
|
||||||
|
<span x-text="task.acquisition_json.modo === 'in_linea' ? {{ _('Analisi in linea')|tojson_attr }} : {{ _('Analisi impulsiva')|tojson_attr }}"></span></p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<p x-show="!(task.acquisition_json && task.acquisition_json.camera_url)" class="text-[var(--text-muted)]">
|
||||||
|
{{ _("Camera della stazione dell'operatore") }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<!-- Modifica: servizio, sorgente, modo (salvati con «Salva» del task) -->
|
||||||
|
<template x-if="editingTask === task.id">
|
||||||
|
<div class="space-y-3"
|
||||||
|
x-data="taskAcquisition(() => editTaskData.acquisition, {{ {'genericError': _('Errore durante il caricamento delle sorgenti')}|tojson_attr }})">
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="tmf-label text-xs">{{ _('Servizio di acquisizione') }}</label>
|
||||||
|
<select class="tmf-input text-xs" x-model="editTaskData.acquisition.camera_url" @change="onServizio()">
|
||||||
|
<option value="">{{ _("Camera della stazione dell'operatore") }}</option>
|
||||||
|
<template x-for="sv in serviziConCorrente()" :key="sv.camera_url">
|
||||||
|
<option :value="sv.camera_url"
|
||||||
|
x-text="sv.camera_url + (sv.stazioni.length ? ' (' + sv.stazioni.join(', ') + ')' : '')"></option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div x-show="editTaskData.acquisition.camera_url">
|
||||||
|
<label class="tmf-label text-xs">{{ _('Sorgente') }}</label>
|
||||||
|
<select class="tmf-input text-xs" x-model="editTaskData.acquisition.camera_source" :disabled="loading">
|
||||||
|
<option value="">{{ _('Scegli la sorgente') }}</option>
|
||||||
|
<template x-for="src in sourcesConCorrente()" :key="src.name">
|
||||||
|
<option :value="src.name" x-text="src.label || src.name"></option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p x-show="error" class="text-xs text-red-500" x-text="error"></p>
|
||||||
|
<div x-show="editTaskData.acquisition.camera_url" class="flex flex-wrap items-center gap-4 text-xs">
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<input type="radio" :name="'acq-modo-' + task.id" value="impulsiva" x-model="editTaskData.acquisition.modo">
|
||||||
|
{{ _('Analisi impulsiva') }}
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<input type="radio" :name="'acq-modo-' + task.id" value="in_linea" x-model="editTaskData.acquisition.modo">
|
||||||
|
{{ _('Analisi in linea') }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- ========== Ricetta visione (#46): solo camera_measure / dxf_compare ========== -->
|
<!-- ========== Ricetta visione (#46): solo camera_measure / dxf_compare ========== -->
|
||||||
<template x-if="task.task_type === 'camera_measure' || task.task_type === 'dxf_compare'">
|
<template x-if="task.task_type === 'camera_measure' || task.task_type === 'dxf_compare'">
|
||||||
<div class="p-3 rounded-lg bg-[var(--bg-secondary)] border border-[var(--border-color)]"
|
<div class="p-3 rounded-lg bg-[var(--bg-secondary)] border border-[var(--border-color)]"
|
||||||
@@ -1335,6 +1400,7 @@
|
|||||||
<script src="{{ url_for('static', filename='js/vision_recipe.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/vision_recipe.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/station_calibration.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/station_calibration.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/dxf_recipe_wizard.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/dxf_recipe_wizard.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/task_acquisition.js') }}"></script>{# #97 #}
|
||||||
<script src="{{ url_for('static', filename='vendor/pdf-3.11.174.min.js') }}"></script>
|
<script src="{{ url_for('static', filename='vendor/pdf-3.11.174.min.js') }}"></script>
|
||||||
<script>if(typeof pdfjsLib!=='undefined')pdfjsLib.GlobalWorkerOptions.workerSrc='{{ url_for('static', filename='vendor/pdf.worker-3.11.174.min.js') }}';</script>
|
<script>if(typeof pdfjsLib!=='undefined')pdfjsLib.GlobalWorkerOptions.workerSrc='{{ url_for('static', filename='vendor/pdf.worker-3.11.174.min.js') }}';</script>
|
||||||
<script src="{{ url_for('static', filename='js/annotation-viewer.js') }}?v=6"></script>
|
<script src="{{ url_for('static', filename='js/annotation-viewer.js') }}?v=6"></script>
|
||||||
@@ -1389,7 +1455,7 @@ function taskEditor() {
|
|||||||
|
|
||||||
// ---- Task CRUD state ----
|
// ---- Task CRUD state ----
|
||||||
editingTask: null,
|
editingTask: null,
|
||||||
editTaskData: { title: '', directive: '', description: '', task_type: 'note', in_loop: false, in_loop_touched: false, at_close: false, fields: [] },
|
editTaskData: { title: '', directive: '', description: '', task_type: 'note', in_loop: false, in_loop_touched: false, at_close: false, fields: [], acquisition: { camera_url: '', camera_source: '', modo: 'impulsiva' } }, // #97
|
||||||
newTask: { title: '', directive: '', description: '', task_type: 'note', in_loop: false, in_loop_touched: false, at_close: false, fields: [] },
|
newTask: { title: '', directive: '', description: '', task_type: 'note', in_loop: false, in_loop_touched: false, at_close: false, fields: [] },
|
||||||
MAX_NOTE_FIELDS: 20, // #55: limite campi per nota (stesso del backend)
|
MAX_NOTE_FIELDS: 20, // #55: limite campi per nota (stesso del backend)
|
||||||
_fieldUid: 0,
|
_fieldUid: 0,
|
||||||
@@ -1575,6 +1641,18 @@ function taskEditor() {
|
|||||||
this.newTask = { title: '', directive: '', description: '', task_type: 'note', in_loop: false, in_loop_touched: false, at_close: false, fields: [] };
|
this.newTask = { title: '', directive: '', description: '', task_type: 'note', in_loop: false, in_loop_touched: false, at_close: false, fields: [] };
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// #97: acquisition_json del task; null = camera della stazione (voce vuota o tipo senza camera)
|
||||||
|
_acquisitionPayload(obj) {
|
||||||
|
const a = obj.acquisition || {};
|
||||||
|
const usesCamera = obj.task_type === 'camera_measure' || obj.task_type === 'dxf_compare';
|
||||||
|
if (!usesCamera || !a.camera_url) return { ok: true, value: null };
|
||||||
|
if (!a.camera_source) {
|
||||||
|
this.errorMessage = {{ _("Scegli la sorgente della camera")|tojson }};
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
return { ok: true, value: { camera_url: a.camera_url, camera_source: a.camera_source, modo: a.modo === 'in_linea' ? 'in_linea' : 'impulsiva' } };
|
||||||
|
},
|
||||||
|
|
||||||
// ---- #53 #55: ciclo di misura e campi della nota ----
|
// ---- #53 #55: ciclo di misura e campi della nota ----
|
||||||
|
|
||||||
// #53: default di "nel ciclo" per tipo (stessa regola del backend)
|
// #53: default di "nel ciclo" per tipo (stessa regola del backend)
|
||||||
@@ -1785,7 +1863,13 @@ function taskEditor() {
|
|||||||
in_loop: task.in_loop != null ? !!task.in_loop : this.loopDefault(task.task_type || 'note'),
|
in_loop: task.in_loop != null ? !!task.in_loop : this.loopDefault(task.task_type || 'note'),
|
||||||
in_loop_touched: false,
|
in_loop_touched: false,
|
||||||
at_close: !!task.at_close,
|
at_close: !!task.at_close,
|
||||||
fields: this._fieldsToState(task.fields)
|
fields: this._fieldsToState(task.fields),
|
||||||
|
// #97: camera e modo di analisi del task (acquisition_json), vuoti = camera della stazione
|
||||||
|
acquisition: {
|
||||||
|
camera_url: (task.acquisition_json && task.acquisition_json.camera_url) || '',
|
||||||
|
camera_source: (task.acquisition_json && task.acquisition_json.camera_source) || '',
|
||||||
|
modo: (task.acquisition_json && task.acquisition_json.modo) || 'impulsiva'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// Ensure expanded
|
// Ensure expanded
|
||||||
this.expandedTask = task.id;
|
this.expandedTask = task.id;
|
||||||
@@ -1793,7 +1877,7 @@ function taskEditor() {
|
|||||||
|
|
||||||
cancelEditTask() {
|
cancelEditTask() {
|
||||||
this.editingTask = null;
|
this.editingTask = null;
|
||||||
this.editTaskData = { title: '', directive: '', description: '', task_type: 'note', in_loop: false, in_loop_touched: false, at_close: false, fields: [] };
|
this.editTaskData = { title: '', directive: '', description: '', task_type: 'note', in_loop: false, in_loop_touched: false, at_close: false, fields: [], acquisition: { camera_url: '', camera_source: '', modo: 'impulsiva' } }; // #97
|
||||||
},
|
},
|
||||||
|
|
||||||
async updateTask(taskId) {
|
async updateTask(taskId) {
|
||||||
@@ -1801,6 +1885,8 @@ function taskEditor() {
|
|||||||
this.errorMessage = '';
|
this.errorMessage = '';
|
||||||
const fp = this._fieldsPayload(this.editTaskData); // #55
|
const fp = this._fieldsPayload(this.editTaskData); // #55
|
||||||
if (!fp.ok) return;
|
if (!fp.ok) return;
|
||||||
|
const acq = this._acquisitionPayload(this.editTaskData); // #97
|
||||||
|
if (!acq.ok) return;
|
||||||
this.saving = true;
|
this.saving = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1818,7 +1904,8 @@ function taskEditor() {
|
|||||||
// #53 #55: ciclo di misura, compilazione a fine produzione, campi della nota
|
// #53 #55: ciclo di misura, compilazione a fine produzione, campi della nota
|
||||||
in_loop: !!this.editTaskData.in_loop,
|
in_loop: !!this.editTaskData.in_loop,
|
||||||
at_close: this.editTaskData.task_type === 'note' && !!this.editTaskData.at_close,
|
at_close: this.editTaskData.task_type === 'note' && !!this.editTaskData.at_close,
|
||||||
fields: fp.fields
|
fields: fp.fields,
|
||||||
|
acquisition_json: acq.value // #97
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
"""gitea:Adriano/TieMeasureFlow#97 - camera e modo di analisi nel task visione (lato Maker).
|
||||||
|
|
||||||
|
api_client e requests sono finti: nessun servizio camera vero.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from tests.conftest import api_get_router
|
||||||
|
|
||||||
|
ST1 = {"id": 4, "code": "ST-4", "name": "Banco", "camera_url": "http://acq:8765/", "camera_source": "camera:1"}
|
||||||
|
ST2 = {"id": 5, "code": "ST-5", "name": "Linea", "camera_url": "http://acq:8765", "camera_source": "camera:2"}
|
||||||
|
ST3 = {"id": 6, "code": "ST-6", "name": "Altra", "camera_url": "http://altro:9000", "camera_source": "x"}
|
||||||
|
NO_CAMERA = {"id": 7, "code": "ST-7", "name": "Senza", "camera_url": None, "camera_source": None}
|
||||||
|
SOURCES = {"sources": [
|
||||||
|
{"name": "camera:601020", "kind": "camera", "label": "Basler 601020"},
|
||||||
|
{"name": "pezzo.png", "kind": "image", "label": "immagine - pezzo.png"},
|
||||||
|
]}
|
||||||
|
|
||||||
|
|
||||||
|
def _stations(mock_api_client, stations=(ST1, ST2, ST3, NO_CAMERA)):
|
||||||
|
mock_api_client.get.side_effect = api_get_router({"/api/stations": list(stations)})
|
||||||
|
|
||||||
|
|
||||||
|
def _fake(status=200, body=None):
|
||||||
|
resp = MagicMock(status_code=status, text="")
|
||||||
|
if body is None:
|
||||||
|
resp.json.side_effect = ValueError("no json")
|
||||||
|
else:
|
||||||
|
resp.json.return_value = body
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
class TestCameraServizi:
|
||||||
|
def test_distinct_urls_with_stations(self, logged_in_client, mock_api_client):
|
||||||
|
_stations(mock_api_client)
|
||||||
|
resp = logged_in_client.get("/maker/api/camera/servizi")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.get_json() == {"servizi": [
|
||||||
|
{"camera_url": "http://acq:8765", "stazioni": ["ST-4 - Banco", "ST-5 - Linea"]},
|
||||||
|
{"camera_url": "http://altro:9000", "stazioni": ["ST-6 - Altra"]},
|
||||||
|
]}
|
||||||
|
|
||||||
|
def test_no_stations_with_camera(self, logged_in_client, mock_api_client):
|
||||||
|
_stations(mock_api_client, [NO_CAMERA])
|
||||||
|
assert logged_in_client.get("/maker/api/camera/servizi").get_json() == {"servizi": []}
|
||||||
|
|
||||||
|
|
||||||
|
class TestCameraSorgenti:
|
||||||
|
def test_lists_sources(self, logged_in_client, mock_api_client):
|
||||||
|
_stations(mock_api_client)
|
||||||
|
with patch("blueprints.maker.http_requests.get", return_value=_fake(200, SOURCES)) as get:
|
||||||
|
resp = logged_in_client.get("/maker/api/camera/sorgenti?url=http://acq:8765/")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.get_json() == SOURCES
|
||||||
|
args, kwargs = get.call_args
|
||||||
|
assert args[0] == "http://acq:8765/camera/sources"
|
||||||
|
assert kwargs["timeout"] == 15
|
||||||
|
|
||||||
|
def test_url_not_a_station_service_is_400(self, logged_in_client, mock_api_client):
|
||||||
|
_stations(mock_api_client)
|
||||||
|
with patch("blueprints.maker.http_requests.get") as get:
|
||||||
|
resp = logged_in_client.get("/maker/api/camera/sorgenti?url=http://evil:1")
|
||||||
|
assert resp.status_code == 400
|
||||||
|
get.assert_not_called()
|
||||||
|
|
||||||
|
def test_missing_url_is_400(self, logged_in_client, mock_api_client):
|
||||||
|
_stations(mock_api_client)
|
||||||
|
with patch("blueprints.maker.http_requests.get") as get:
|
||||||
|
assert logged_in_client.get("/maker/api/camera/sorgenti").status_code == 400
|
||||||
|
get.assert_not_called()
|
||||||
|
|
||||||
|
def test_service_down_is_502(self, logged_in_client, mock_api_client):
|
||||||
|
_stations(mock_api_client)
|
||||||
|
with patch("blueprints.maker.http_requests.get",
|
||||||
|
side_effect=requests.ConnectionError("refused")):
|
||||||
|
resp = logged_in_client.get("/maker/api/camera/sorgenti?url=http://acq:8765")
|
||||||
|
assert resp.status_code == 502
|
||||||
|
assert "refused" in resp.get_json()["detail"]
|
||||||
|
|
||||||
|
def test_service_error_status_is_502(self, logged_in_client, mock_api_client):
|
||||||
|
_stations(mock_api_client)
|
||||||
|
with patch("blueprints.maker.http_requests.get", return_value=_fake(500, {"detail": "x"})):
|
||||||
|
resp = logged_in_client.get("/maker/api/camera/sorgenti?url=http://acq:8765")
|
||||||
|
assert resp.status_code == 502
|
||||||
|
|
||||||
|
|
||||||
|
class TestTaskProxyForwardsAcquisition:
|
||||||
|
def test_put_forwards_acquisition_json(self, logged_in_client, mock_api_client):
|
||||||
|
acq = {"camera_url": "http://acq:8765", "camera_source": "camera:1", "modo": "in_linea"}
|
||||||
|
mock_api_client.put.return_value = {"id": 11, "title": "T", "acquisition_json": acq}
|
||||||
|
resp = logged_in_client.put("/maker/api/tasks/11", json={"title": "T", "acquisition_json": acq})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert mock_api_client.put.call_args.kwargs["data"]["acquisition_json"] == acq
|
||||||
|
|
||||||
|
def test_put_forwards_null(self, logged_in_client, mock_api_client):
|
||||||
|
mock_api_client.put.return_value = {"id": 11, "title": "T", "acquisition_json": None}
|
||||||
|
logged_in_client.put("/maker/api/tasks/11", json={"title": "T", "acquisition_json": None})
|
||||||
|
assert mock_api_client.put.call_args.kwargs["data"]["acquisition_json"] is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestTaskEditorAcquisitionUi:
|
||||||
|
def _html(self, client, mock_api_client, task_type="camera_measure"):
|
||||||
|
mock_api_client.get.side_effect = api_get_router({
|
||||||
|
"/api/recipes/3": {"id": 3, "code": "R", "name": "R"},
|
||||||
|
"/api/recipes/3/tasks": [{
|
||||||
|
"id": 11, "title": "T", "task_type": task_type, "subtasks": [],
|
||||||
|
"file_path": None, "order_index": 1,
|
||||||
|
"acquisition_json": {"camera_url": "http://acq:8765", "camera_source": "camera:1",
|
||||||
|
"modo": "in_linea"},
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
return client.get("/maker/recipes/3/tasks").get_data(as_text=True)
|
||||||
|
|
||||||
|
def test_section_and_fields(self, logged_in_client, mock_api_client):
|
||||||
|
html = self._html(logged_in_client, mock_api_client)
|
||||||
|
assert "taskAcquisition(" in html
|
||||||
|
assert "js/task_acquisition.js" in html
|
||||||
|
assert "Servizio di acquisizione" in html
|
||||||
|
assert "Sorgente" in html
|
||||||
|
assert "Analisi impulsiva" in html and "Analisi in linea" in html
|
||||||
|
assert 'value="impulsiva"' in html and 'value="in_linea"' in html
|
||||||
|
assert "Camera della stazione dell'operatore" in html or \
|
||||||
|
"Camera della stazione dell'operatore" in html
|
||||||
|
|
||||||
|
def test_payload_carries_acquisition_json(self, logged_in_client, mock_api_client):
|
||||||
|
html = self._html(logged_in_client, mock_api_client)
|
||||||
|
assert "acquisition_json: acq.value" in html
|
||||||
|
assert "_acquisitionPayload(this.editTaskData)" in html
|
||||||
|
# il task salvato riempie lo stato di modifica
|
||||||
|
assert "task.acquisition_json.camera_url" in html
|
||||||
|
|
||||||
|
def test_static_js_endpoints(self):
|
||||||
|
import os
|
||||||
|
path = os.path.join(os.path.dirname(__file__), "..", "static", "js", "task_acquisition.js")
|
||||||
|
src = open(path, encoding="utf-8").read()
|
||||||
|
assert "/maker/api/camera/servizi" in src
|
||||||
|
assert "/maker/api/camera/sorgenti?url=" in src
|
||||||
Reference in New Issue
Block a user