Stazioni: sorgenti camera lette dall'acquisitore, link Acquisizione (#88)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jbf7qKuxsnAVjxthRy7dnB
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
"""Admin blueprint - user management."""
|
"""Admin blueprint - user management."""
|
||||||
|
from urllib.parse import urlparse # #88: validazione url camera
|
||||||
|
import requests as http_requests # #88: lettura sorgenti dall'acquisitore (come maker.py)
|
||||||
from flask import Blueprint, flash, jsonify, redirect, render_template, request, session, url_for
|
from flask import Blueprint, flash, jsonify, redirect, render_template, request, session, url_for
|
||||||
from flask_babel import gettext as _
|
from flask_babel import gettext as _
|
||||||
|
|
||||||
@@ -170,6 +172,39 @@ def api_toggle_active(user_id: int):
|
|||||||
|
|
||||||
# --- Stations ---
|
# --- Stations ---
|
||||||
|
|
||||||
|
@admin_bp.route("/api/camera/sources", methods=["GET"])
|
||||||
|
@login_required
|
||||||
|
@admin_required
|
||||||
|
def api_camera_sources():
|
||||||
|
"""Proxy (#88): elenco sorgenti di un acquisitore, GET <url>/camera/sources."""
|
||||||
|
url = (request.args.get("url") or "").strip()
|
||||||
|
parsed = urlparse(url)
|
||||||
|
if parsed.scheme not in ("http", "https") or not parsed.netloc:
|
||||||
|
return jsonify({"error": True, "detail": _("Indirizzo della camera non valido")}), 400
|
||||||
|
try:
|
||||||
|
resp = http_requests.get(f"{url.rstrip('/')}/camera/sources", timeout=5)
|
||||||
|
except http_requests.RequestException as exc:
|
||||||
|
return jsonify({
|
||||||
|
"error": True,
|
||||||
|
"detail": _("Servizio camera non raggiungibile: %(detail)s", detail=str(exc)),
|
||||||
|
}), 502
|
||||||
|
invalid = (jsonify({"error": True, "detail": _("Risposta non valida dal servizio camera")}), 502)
|
||||||
|
if resp.status_code != 200:
|
||||||
|
return invalid
|
||||||
|
try:
|
||||||
|
sources = resp.json().get("sources")
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
return invalid
|
||||||
|
if not isinstance(sources, list):
|
||||||
|
return invalid
|
||||||
|
# solo i tre campi che la UI usa, sempre stringhe
|
||||||
|
clean = [
|
||||||
|
{k: str(s.get(k, "")) for k in ("name", "label", "kind")}
|
||||||
|
for s in sources if isinstance(s, dict)
|
||||||
|
]
|
||||||
|
return jsonify({"sources": clean}), 200
|
||||||
|
|
||||||
|
|
||||||
@admin_bp.route("/api/stations", methods=["POST"])
|
@admin_bp.route("/api/stations", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
@admin_required
|
@admin_required
|
||||||
|
|||||||
@@ -598,7 +598,7 @@ def api_station_camera_calibration(station_id: int):
|
|||||||
}), 409
|
}), 409
|
||||||
camera_url, camera_source = camera
|
camera_url, camera_source = camera
|
||||||
base = camera_url.rstrip("/")
|
base = camera_url.rstrip("/")
|
||||||
link = f"{base}/calibrazione" # pagina di calibrazione dell'acquisitore (C4)
|
link = f"{base}/acquisizione" # pagina di acquisizione dell'acquisitore (#88; era /calibrazione, C4)
|
||||||
try:
|
try:
|
||||||
resp = http_requests.get(
|
resp = http_requests.get(
|
||||||
f"{base}/camera/calibration",
|
f"{base}/camera/calibration",
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ function stationCalibrationCell(station, msgs) {
|
|||||||
if (!this.hasCamera) return;
|
if (!this.hasCamera) return;
|
||||||
const r = await fetchStationCalibration(station.id);
|
const r = await fetchStationCalibration(station.id);
|
||||||
this.state = r.state || 'unreachable';
|
this.state = r.state || 'unreachable';
|
||||||
// Link «Calibra»: dal proxy, altrimenti costruito dall'indirizzo della camera
|
// Link «Acquisizione» (#88, era «Calibra»): dal proxy, altrimenti costruito dall'indirizzo della camera
|
||||||
this.calibrationUrl = r.calibration_url || (this.cameraUrl.replace(/\/+$/, '') + '/calibrazione');
|
this.calibrationUrl = r.calibration_url || (this.cameraUrl.replace(/\/+$/, '') + '/acquisizione');
|
||||||
if (this.state === 'calibrated') this.label = formatCalibration(r.calibration || {});
|
if (this.state === 'calibrated') this.label = formatCalibration(r.calibration || {});
|
||||||
else if (this.state === 'uncalibrated') this.label = this.msgs.uncalibrated || 'not calibrated';
|
else if (this.state === 'uncalibrated') this.label = this.msgs.uncalibrated || 'not calibrated';
|
||||||
else this.label = this.msgs.unreachable || 'camera service unreachable';
|
else this.label = this.msgs.unreachable || 'camera service unreachable';
|
||||||
|
|||||||
@@ -97,7 +97,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<a :href="calibrationUrl" target="_blank" rel="noopener"
|
<a :href="calibrationUrl" target="_blank" rel="noopener"
|
||||||
x-show="calibrationUrl"
|
x-show="calibrationUrl"
|
||||||
class="font-medium text-primary hover:underline">{{ _('Calibra') }}</a>
|
class="font-medium text-primary hover:underline">{{ _('Acquisizione') }}</a> {# #88: era «Calibra» #}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
@@ -165,7 +165,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal: Create / Edit Station -->
|
<!-- Modal: Create / Edit Station -->
|
||||||
<div x-show="showModal" x-cloak
|
<div x-show="showModal"
|
||||||
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||||
@keydown.escape.window="closeModal()">
|
@keydown.escape.window="closeModal()">
|
||||||
<div class="absolute inset-0 bg-black/50" @click="closeModal()"></div>
|
<div class="absolute inset-0 bg-black/50" @click="closeModal()"></div>
|
||||||
@@ -238,21 +238,42 @@
|
|||||||
{# Camera della stazione (#72): servizio camera (vs-gui) e sorgente da cui prendere il frame #}
|
{# Camera della stazione (#72): servizio camera (vs-gui) e sorgente da cui prendere il frame #}
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-[var(--text-primary)] mb-1">{{ _('Camera (URL)') }}</label>
|
<label class="block text-sm font-medium text-[var(--text-primary)] mb-1">{{ _('Camera (URL)') }}</label>
|
||||||
<input type="text" x-model="form.camera_url"
|
<input type="text" x-model="form.camera_url" @change="loadCameraSources()"
|
||||||
class="w-full px-3 py-2 rounded-lg border border-[var(--border-color)] bg-[var(--bg-card)]
|
class="w-full px-3 py-2 rounded-lg border border-[var(--border-color)] bg-[var(--bg-card)]
|
||||||
text-sm font-mono text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-colors"
|
text-sm font-mono text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-colors"
|
||||||
placeholder="host:8765"
|
placeholder="host:8765"
|
||||||
maxlength="500">
|
maxlength="500">
|
||||||
<p class="mt-1 text-xs text-[var(--text-secondary)]">{{ _('Vuoto = nessuna camera. Indirizzo del servizio camera, con http o https, senza / finale') }}</p>
|
<p class="mt-1 text-xs text-[var(--text-secondary)]">{{ _('Vuoto = nessuna camera. Indirizzo del servizio camera, con http o https, senza / finale') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
{# Sorgente camera (#88): testo libero con suggerimenti letti dall'acquisitore (datalist) #}
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-[var(--text-primary)] mb-1">{{ _('Sorgente camera') }}</label>
|
<label class="block text-sm font-medium text-[var(--text-primary)] mb-1">{{ _('Sorgente camera') }}</label>
|
||||||
<input type="text" x-model="form.camera_source"
|
<div class="flex gap-2">
|
||||||
class="w-full px-3 py-2 rounded-lg border border-[var(--border-color)] bg-[var(--bg-card)]
|
<input type="text" x-model="form.camera_source" list="cameraSourcesList"
|
||||||
|
class="flex-1 min-w-0 px-3 py-2 rounded-lg border border-[var(--border-color)] bg-[var(--bg-card)]
|
||||||
text-sm font-mono text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-colors"
|
text-sm font-mono text-[var(--text-primary)] focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary transition-colors"
|
||||||
placeholder="camera:folder"
|
placeholder="camera:folder"
|
||||||
maxlength="200">
|
maxlength="200">
|
||||||
<p class="mt-1 text-xs text-[var(--text-secondary)]">{{ _('Es. camera:<seriale> oppure camera:folder') }}</p>
|
<datalist id="cameraSourcesList">
|
||||||
|
<template x-for="s in cameraSources" :key="s.name">
|
||||||
|
<option :value="s.name" :label="s.label"></option>
|
||||||
|
</template>
|
||||||
|
</datalist>
|
||||||
|
<button type="button" @click="loadCameraSources()"
|
||||||
|
:disabled="!(form.camera_url || '').trim() || cameraSourcesLoading"
|
||||||
|
class="px-3 py-2 rounded-lg border border-[var(--border-color)] bg-[var(--bg-card)] text-sm font-medium
|
||||||
|
text-[var(--text-primary)] hover:bg-[var(--bg-secondary)] disabled:opacity-50 disabled:cursor-not-allowed transition-colors whitespace-nowrap">
|
||||||
|
{{ _('Leggi sorgenti') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="mt-1 text-xs"
|
||||||
|
:class="cameraSourcesError ? 'text-red-600 dark:text-red-400' : 'text-[var(--text-secondary)]'"
|
||||||
|
x-text="cameraSourcesStatus || {{ _('Es. camera:<seriale> oppure camera:folder')|tojson_attr }}"></p>
|
||||||
|
<p x-show="cameraSourceMissing" class="mt-1 text-xs text-amber-700 dark:text-amber-300">{{ _("Sorgente non presente nell'acquisitore") }}</p>
|
||||||
|
<a x-show="(form.camera_url || '').trim()"
|
||||||
|
:href="(form.camera_url || '').trim().replace(/\/+$/, '') + '/acquisizione'"
|
||||||
|
target="_blank" rel="noopener"
|
||||||
|
class="mt-1 inline-block text-xs font-medium text-primary hover:underline">{{ _('Apri pagina acquisizione') }}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -289,14 +310,14 @@
|
|||||||
class="px-4 py-2 bg-primary text-white text-sm font-medium rounded-lg
|
class="px-4 py-2 bg-primary text-white text-sm font-medium rounded-lg
|
||||||
hover:bg-primary-700 transition-colors shadow-sm disabled:opacity-50">
|
hover:bg-primary-700 transition-colors shadow-sm disabled:opacity-50">
|
||||||
<span x-show="!saving" x-text="isEditing ? {{ _('Salva Modifiche')|tojson_attr }} : {{ _('Crea Stazione')|tojson_attr }}"></span>
|
<span x-show="!saving" x-text="isEditing ? {{ _('Salva Modifiche')|tojson_attr }} : {{ _('Crea Stazione')|tojson_attr }}"></span>
|
||||||
<span x-show="saving" x-cloak>{{ _('Salvataggio...') }}</span>
|
<span x-show="saving">{{ _('Salvataggio...') }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal: Recipe Assignments -->
|
<!-- Modal: Recipe Assignments -->
|
||||||
<div x-show="showAssignments" x-cloak
|
<div x-show="showAssignments"
|
||||||
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||||
@keydown.escape.window="closeAssignmentsModal()">
|
@keydown.escape.window="closeAssignmentsModal()">
|
||||||
<div class="absolute inset-0 bg-black/50" @click="closeAssignmentsModal()"></div>
|
<div class="absolute inset-0 bg-black/50" @click="closeAssignmentsModal()"></div>
|
||||||
@@ -417,7 +438,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Confirm Reset Modal -->
|
<!-- Confirm Reset Modal -->
|
||||||
<div x-show="showResetConfirm" x-cloak
|
<div x-show="showResetConfirm"
|
||||||
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||||
@keydown.escape.window="showResetConfirm = false">
|
@keydown.escape.window="showResetConfirm = false">
|
||||||
<div class="absolute inset-0 bg-black/50" @click="showResetConfirm = false"></div>
|
<div class="absolute inset-0 bg-black/50" @click="showResetConfirm = false"></div>
|
||||||
@@ -449,7 +470,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Confirm Delete Modal -->
|
<!-- Confirm Delete Modal -->
|
||||||
<div x-show="showDeleteConfirm" x-cloak
|
<div x-show="showDeleteConfirm"
|
||||||
class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
<div class="absolute inset-0 bg-black/50" @click="showDeleteConfirm = false"></div>
|
<div class="absolute inset-0 bg-black/50" @click="showDeleteConfirm = false"></div>
|
||||||
<div x-show="showDeleteConfirm"
|
<div x-show="showDeleteConfirm"
|
||||||
@@ -509,6 +530,12 @@ function stationManagement(initialStations, initialRecipes) {
|
|||||||
camera_source: '', // #72
|
camera_source: '', // #72
|
||||||
active: true,
|
active: true,
|
||||||
},
|
},
|
||||||
|
// #88: sorgenti lette dall'acquisitore per il datalist della sorgente camera
|
||||||
|
cameraSources: [],
|
||||||
|
cameraSourcesLoaded: false,
|
||||||
|
cameraSourcesLoading: false,
|
||||||
|
cameraSourcesStatus: '',
|
||||||
|
cameraSourcesError: false,
|
||||||
|
|
||||||
get csrfToken() {
|
get csrfToken() {
|
||||||
const meta = document.querySelector('meta[name=csrf-token]');
|
const meta = document.querySelector('meta[name=csrf-token]');
|
||||||
@@ -556,11 +583,46 @@ function stationManagement(initialStations, initialRecipes) {
|
|||||||
return '{{ _("Nessuna ricetta assegnata") }}';
|
return '{{ _("Nessuna ricetta assegnata") }}';
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// #88: legge le sorgenti dall'acquisitore (proxy admin) e riempie il datalist
|
||||||
|
async loadCameraSources() {
|
||||||
|
const url = (this.form.camera_url || '').trim();
|
||||||
|
this.cameraSources = [];
|
||||||
|
this.cameraSourcesLoaded = false;
|
||||||
|
this.cameraSourcesError = false;
|
||||||
|
if (!url) { this.cameraSourcesStatus = ''; return; }
|
||||||
|
this.cameraSourcesLoading = true;
|
||||||
|
this.cameraSourcesStatus = '{{ _("Lettura...") }}';
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/admin/api/camera/sources?url=' + encodeURIComponent(url));
|
||||||
|
const result = await resp.json();
|
||||||
|
if (!resp.ok) {
|
||||||
|
this.cameraSourcesError = true;
|
||||||
|
this.cameraSourcesStatus = result.detail || '{{ _("Risposta non valida dal servizio camera") }}';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.cameraSources = result.sources || [];
|
||||||
|
this.cameraSourcesLoaded = true;
|
||||||
|
this.cameraSourcesStatus = '{{ _("%(n)s sorgenti trovate", n="__N__") }}'.replace('__N__', this.cameraSources.length);
|
||||||
|
} catch (e) {
|
||||||
|
this.cameraSourcesError = true;
|
||||||
|
this.cameraSourcesStatus = '{{ _("Risposta non valida dal servizio camera") }}';
|
||||||
|
} finally {
|
||||||
|
this.cameraSourcesLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// #88: avviso se la sorgente scritta non e' fra quelle lette
|
||||||
|
get cameraSourceMissing() {
|
||||||
|
const src = (this.form.camera_source || '').trim();
|
||||||
|
return this.cameraSourcesLoaded && src !== '' && !this.cameraSources.some(s => s.name === src);
|
||||||
|
},
|
||||||
|
|
||||||
openCreateModal() {
|
openCreateModal() {
|
||||||
this.isEditing = false;
|
this.isEditing = false;
|
||||||
this.editingId = null;
|
this.editingId = null;
|
||||||
this.errorMsg = '';
|
this.errorMsg = '';
|
||||||
this.form = { code: '', name: '', location: '', notes: '', vision_worker_url: '', camera_url: '', camera_source: '', active: true };
|
this.form = { code: '', name: '', location: '', notes: '', vision_worker_url: '', camera_url: '', camera_source: '', active: true };
|
||||||
|
this.loadCameraSources(); // #88: svuota lo stato delle sorgenti
|
||||||
this.showModal = true;
|
this.showModal = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -578,6 +640,7 @@ function stationManagement(initialStations, initialRecipes) {
|
|||||||
camera_source: station.camera_source || '', // #72
|
camera_source: station.camera_source || '', // #72
|
||||||
active: station.active,
|
active: station.active,
|
||||||
};
|
};
|
||||||
|
this.loadCameraSources(); // #88: se la camera e' valorizzata legge subito le sorgenti
|
||||||
this.showModal = true;
|
this.showModal = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -254,3 +254,74 @@ def test_create_station_proxy_forwards_the_camera_fields(logged_in_client, mock_
|
|||||||
resp = logged_in_client.post("/admin/api/stations", json=body)
|
resp = logged_in_client.post("/admin/api/stations", json=body)
|
||||||
assert resp.status_code == 201
|
assert resp.status_code == 201
|
||||||
mock_admin_api.post.assert_called_once_with("/api/stations", data=body)
|
mock_admin_api.post.assert_called_once_with("/api/stations", data=body)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Sorgenti camera lette dall'acquisitore (#88) ---
|
||||||
|
|
||||||
|
def _fake_sources_response(status=200, payload=None):
|
||||||
|
resp = MagicMock()
|
||||||
|
resp.status_code = status
|
||||||
|
resp.json.return_value = payload
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
def test_camera_sources_proxy_keeps_only_the_three_fields(logged_in_client, mock_admin_api):
|
||||||
|
payload = {"sources": [
|
||||||
|
{"name": "camera:folder", "label": "Cartella", "kind": "folder", "extra": 1},
|
||||||
|
{"name": "camera:123", "label": "Basler", "kind": "gige"},
|
||||||
|
]}
|
||||||
|
with patch("blueprints.admin.http_requests.get",
|
||||||
|
return_value=_fake_sources_response(200, payload)) as get:
|
||||||
|
resp = logged_in_client.get("/admin/api/camera/sources?url=http://acq:8765/")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.get_json()["sources"][0] == {
|
||||||
|
"name": "camera:folder", "label": "Cartella", "kind": "folder"}
|
||||||
|
assert len(resp.get_json()["sources"]) == 2
|
||||||
|
assert get.call_args.args[0] == "http://acq:8765/camera/sources"
|
||||||
|
assert get.call_args.kwargs["timeout"] == 5
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("url", ["ftp://x", "", "http://", "acq:8765"])
|
||||||
|
def test_camera_sources_proxy_rejects_invalid_url(logged_in_client, mock_admin_api, url):
|
||||||
|
with patch("blueprints.admin.http_requests.get") as get:
|
||||||
|
resp = logged_in_client.get("/admin/api/camera/sources", query_string={"url": url})
|
||||||
|
assert resp.status_code == 400
|
||||||
|
assert resp.get_json()["error"] is True
|
||||||
|
get.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_camera_sources_proxy_unreachable_is_502(logged_in_client, mock_admin_api):
|
||||||
|
import requests
|
||||||
|
with patch("blueprints.admin.http_requests.get",
|
||||||
|
side_effect=requests.ConnectionError("rifiutata")):
|
||||||
|
resp = logged_in_client.get("/admin/api/camera/sources?url=http://acq:8765")
|
||||||
|
assert resp.status_code == 502
|
||||||
|
assert "rifiutata" in resp.get_json()["detail"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("status,payload", [(200, {"altro": 1}), (200, {"sources": "x"}), (500, {"sources": []})])
|
||||||
|
def test_camera_sources_proxy_invalid_response_is_502(logged_in_client, mock_admin_api, status, payload):
|
||||||
|
with patch("blueprints.admin.http_requests.get",
|
||||||
|
return_value=_fake_sources_response(status, payload)):
|
||||||
|
resp = logged_in_client.get("/admin/api/camera/sources?url=http://acq:8765")
|
||||||
|
assert resp.status_code == 502
|
||||||
|
|
||||||
|
|
||||||
|
def test_camera_sources_proxy_non_admin_is_redirected(client, mock_admin_api):
|
||||||
|
with client.session_transaction() as sess:
|
||||||
|
sess["api_key"] = "test-key"
|
||||||
|
sess["user"] = {"id": 2, "username": "operator", "roles": ["MeasurementTec"],
|
||||||
|
"is_admin": False, "active": True}
|
||||||
|
sess["user_id"] = 2
|
||||||
|
with patch("blueprints.admin.http_requests.get") as get:
|
||||||
|
resp = client.get("/admin/api/camera/sources?url=http://acq:8765")
|
||||||
|
assert resp.status_code in (301, 302)
|
||||||
|
get.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_station_form_has_the_camera_sources_picker(logged_in_client, mock_admin_api):
|
||||||
|
mock_admin_api.get.return_value = []
|
||||||
|
html = logged_in_client.get("/admin/stations").get_data(as_text=True)
|
||||||
|
assert "cameraSourcesList" in html
|
||||||
|
assert "loadCameraSources" in html
|
||||||
|
assert "/acquisizione" in html
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ class TestStationCameraCalibration:
|
|||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert body["state"] == "calibrated"
|
assert body["state"] == "calibrated"
|
||||||
assert body["calibration"]["px_per_mm"] == 20.03
|
assert body["calibration"]["px_per_mm"] == 20.03
|
||||||
assert body["calibration_url"] == "http://acq:8765/calibrazione"
|
assert body["calibration_url"] == "http://acq:8765/acquisizione"
|
||||||
args, kwargs = get.call_args
|
args, kwargs = get.call_args
|
||||||
assert args[0] == "http://acq:8765/camera/calibration"
|
assert args[0] == "http://acq:8765/camera/calibration"
|
||||||
assert kwargs["params"] == {"source": "camera:folder"}
|
assert kwargs["params"] == {"source": "camera:folder"}
|
||||||
@@ -130,7 +130,7 @@ class TestStationCameraCalibration:
|
|||||||
resp, _ = self._get(logged_in_client, _fake(404, b"{}", {"detail": "no"}))
|
resp, _ = self._get(logged_in_client, _fake(404, b"{}", {"detail": "no"}))
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert resp.get_json()["state"] == "uncalibrated"
|
assert resp.get_json()["state"] == "uncalibrated"
|
||||||
assert resp.get_json()["calibration_url"] == "http://acq:8765/calibrazione"
|
assert resp.get_json()["calibration_url"] == "http://acq:8765/acquisizione"
|
||||||
|
|
||||||
def test_unreachable(self, logged_in_client, mock_api_client):
|
def test_unreachable(self, logged_in_client, mock_api_client):
|
||||||
_stations(mock_api_client)
|
_stations(mock_api_client)
|
||||||
|
|||||||
@@ -3452,3 +3452,36 @@ msgstr "Try"
|
|||||||
#: templates/maker/task_editor.html templates/admin/stations.html blueprints/maker.py
|
#: templates/maker/task_editor.html templates/admin/stations.html blueprints/maker.py
|
||||||
msgid "Salva ricetta"
|
msgid "Salva ricetta"
|
||||||
msgstr "Save recipe"
|
msgstr "Save recipe"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html blueprints/admin.py (#88)
|
||||||
|
msgid "Acquisizione"
|
||||||
|
msgstr "Acquisition"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html blueprints/admin.py (#88)
|
||||||
|
msgid "Leggi sorgenti"
|
||||||
|
msgstr "Read sources"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html blueprints/admin.py (#88)
|
||||||
|
msgid "Lettura..."
|
||||||
|
msgstr "Reading..."
|
||||||
|
|
||||||
|
#: templates/admin/stations.html blueprints/admin.py (#88)
|
||||||
|
#, python-format
|
||||||
|
msgid "%(n)s sorgenti trovate"
|
||||||
|
msgstr "%(n)s sources found"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html blueprints/admin.py (#88)
|
||||||
|
msgid "Sorgente non presente nell'acquisitore"
|
||||||
|
msgstr "Source not present in the acquisition service"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html blueprints/admin.py (#88)
|
||||||
|
msgid "Apri pagina acquisizione"
|
||||||
|
msgstr "Open acquisition page"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html blueprints/admin.py (#88)
|
||||||
|
msgid "Indirizzo della camera non valido"
|
||||||
|
msgstr "Invalid camera address"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html blueprints/admin.py (#88)
|
||||||
|
msgid "Risposta non valida dal servizio camera"
|
||||||
|
msgstr "Invalid response from the camera service"
|
||||||
|
|||||||
@@ -3440,3 +3440,36 @@ msgstr "Prova"
|
|||||||
#: templates/maker/task_editor.html templates/admin/stations.html blueprints/maker.py
|
#: templates/maker/task_editor.html templates/admin/stations.html blueprints/maker.py
|
||||||
msgid "Salva ricetta"
|
msgid "Salva ricetta"
|
||||||
msgstr "Salva ricetta"
|
msgstr "Salva ricetta"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html blueprints/admin.py (#88)
|
||||||
|
msgid "Acquisizione"
|
||||||
|
msgstr "Acquisizione"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html blueprints/admin.py (#88)
|
||||||
|
msgid "Leggi sorgenti"
|
||||||
|
msgstr "Leggi sorgenti"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html blueprints/admin.py (#88)
|
||||||
|
msgid "Lettura..."
|
||||||
|
msgstr "Lettura..."
|
||||||
|
|
||||||
|
#: templates/admin/stations.html blueprints/admin.py (#88)
|
||||||
|
#, python-format
|
||||||
|
msgid "%(n)s sorgenti trovate"
|
||||||
|
msgstr "%(n)s sorgenti trovate"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html blueprints/admin.py (#88)
|
||||||
|
msgid "Sorgente non presente nell'acquisitore"
|
||||||
|
msgstr "Sorgente non presente nell'acquisitore"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html blueprints/admin.py (#88)
|
||||||
|
msgid "Apri pagina acquisizione"
|
||||||
|
msgstr "Apri pagina acquisizione"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html blueprints/admin.py (#88)
|
||||||
|
msgid "Indirizzo della camera non valido"
|
||||||
|
msgstr "Indirizzo della camera non valido"
|
||||||
|
|
||||||
|
#: templates/admin/stations.html blueprints/admin.py (#88)
|
||||||
|
msgid "Risposta non valida dal servizio camera"
|
||||||
|
msgstr "Risposta non valida dal servizio camera"
|
||||||
|
|||||||
Reference in New Issue
Block a user