diff --git a/TODO.txt b/TODO.txt
new file mode 100644
index 0000000..28481be
--- /dev/null
+++ b/TODO.txt
@@ -0,0 +1,12 @@
+Funzioni di gestione commesse: creazione, modifica, cancellazione
+Sistema di gestione diversi modelli di etichetta tramite software Zebra Designer
+Controllo tester di prova tenuta Tecna T3 tramite interfaccia USB
+Controllo elettrovalvole di selezione alta/bassa pressione tramite I/O digitali
+
+Ciclo di lavoro ordinario:
+acquisizione del barcode del componente sotto test, se previsto da ricetta
+test tenuta a pressione 1 (es. 5 bar)
+test tenuta a pressione 2 (es. 20 bar)(opzionale per ricetta)
+stampa etichetta test OK
+Salvataggio dati dei test su portale di tracciabilitĂ www.r5portal.it sia OK che scarti
+Visualizzazione locale archivio test effettuati
diff --git a/config/machine_settings/defaults.ini b/config/machine_settings/defaults.ini
index 272948c..18b0d2e 100644
--- a/config/machine_settings/defaults.ini
+++ b/config/machine_settings/defaults.ini
@@ -1,10 +1,10 @@
[test]
parameter: default
-[image_saver]
-location: data/images
-minimum free space gb: 20
-suffix: A
+[vision_saver]
+time_format: %Y-%m-%d_%H-%M-%S
+location: ./data/images
+minimum_disk_free_space_gb: 20
[archive_synchronizer]
archive_endpoint: https://r5portal.it/api/echo/
diff --git a/simulate.sh b/simulate.sh
index d1f5427..37c0c9d 100755
--- a/simulate.sh
+++ b/simulate.sh
@@ -16,6 +16,10 @@ python -B -u "./src/main.py" \
--auto-select \
--style windows \
$* 2> >(sed $'s/.*/\e[31m&\e[m/' >&2) # &
+# --about \
+# --archive \
+# --autotests-archive \
# --sim-archiver \
+# --users-management \
# sudo renice -n -10 $!
# fg
diff --git a/src/components/__init__.py b/src/components/__init__.py
index d333be4..249f7f5 100644
--- a/src/components/__init__.py
+++ b/src/components/__init__.py
@@ -1,3 +1,4 @@
from .archive_synchronizer import ArchiveSynchronizer
from .remote_api import RemoteAPI
from .test_component import TestComponent
+from .vision_saver import VisionSaver
diff --git a/src/components/archive_synchronizer.py b/src/components/archive_synchronizer.py
index ab2cdeb..b08c973 100644
--- a/src/components/archive_synchronizer.py
+++ b/src/components/archive_synchronizer.py
@@ -18,8 +18,8 @@ requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
class ArchiveSynchronizer(Component):
- def __init__(self, config=None, name=None, period=1, lazy=True, paused=False):
- super().__init__(config=config, name=name, period=period, lazy=lazy, paused=paused)
+ def __init__(self, config=None, name=None, period=1, lazy=True, paused=False, threaded=True):
+ super().__init__(config=config, name=name, period=period, lazy=lazy, paused=paused, threaded=threaded)
self.simulate = "--sim-archiver" in sys.argv
def config_changed(self):
@@ -61,6 +61,7 @@ class ArchiveSynchronizer(Component):
"user": record.user.username,
"recipe": record.recipe.name,
"test_data": json.dumps(record.test_data),
+ "result": record.result,
"overridden": record.overridden,
}, timeout=5, verify=False)
if r.status_code != 200:
diff --git a/src/components/component.py b/src/components/component.py
index bdcf521..ffa00cc 100644
--- a/src/components/component.py
+++ b/src/components/component.py
@@ -18,20 +18,25 @@ class Component(QObject):
period=None, # period to call _get
lazy=True, # whether or not accumulate periodic _get calls if falling behind
paused=False,
+ threaded=True,
):
super().__init__()
self.config = config
self.name = name if name is not None else str(id(self))
+ self._threaded = threaded
self._period = period
self._single_shot = lazy
self._paused = paused
self._started = False
self._running = False
self.sources = {}
- self._lock = QSemaphore(1)
- self._lock.acquire(max(self._lock.available(), 1))
+ if self._threaded:
+ self._lock = QSemaphore(1)
+ self._lock.acquire(max(self._lock.available(), 1))
self._timer = None
self.log = logging.getLogger(f"{self.__class__.__name__} ({self.name})")
+ if not threaded:
+ self.start()
def _config_changed(self):
self.log.info("reconfigure")
@@ -51,52 +56,70 @@ class Component(QObject):
self._started = True
if not self._paused:
self._do_resume()
- else:
+ elif self._threaded:
self._lock.release()
self.log.info("started")
@property
def started(self):
- self._lock.acquire(max(self._lock.available(), 1))
+ if self._threaded:
+ self._lock.acquire(max(self._lock.available(), 1))
started = self._started
- self._lock.release()
+ if self._threaded:
+ self._lock.release()
return started
@property
def running(self):
- self._lock.acquire(max(self._lock.available(), 1))
+ if self._threaded:
+ self._lock.acquire(max(self._lock.available(), 1))
running = self._running
- self._lock.release()
+ if self._threaded:
+ self._lock.release()
return running
def wait_ready(self, timeout=5):
- timeout = round(timeout * 1000)
- if self._lock.tryAcquire(max(self._lock.available(), 1), timeout):
- self._lock.release()
- else:
- self._lock.release()
- raise RuntimeError(f"{self.name} was not ready before timeout of {timeout}ms")
+ if self._threaded:
+ timeout = round(timeout * 1000)
+ if self._lock.tryAcquire(max(self._lock.available(), 1), timeout):
+ self._lock.release()
+ else:
+ self._lock.release()
+ raise RuntimeError(f"{self.name} was not ready before timeout of {timeout}ms")
def pause(self):
- self._lock.acquire(max(self._lock.available(), 1))
+ if self._threaded:
+ self._lock.acquire(max(self._lock.available(), 1))
if self._running is False:
- self._lock.release()
+ if self._threaded:
+ self._lock.release()
return
- self._pause.emit()
- self.wait_ready()
+ if self._threaded:
+ self._pause.emit()
+ self.wait_ready()
+ else:
+ self._do_pause()
def resume(self):
- self._lock.acquire(max(self._lock.available(), 1))
+ if self._threaded:
+ self._lock.acquire(max(self._lock.available(), 1))
if self._running is True:
- self._lock.release()
+ if self._threaded:
+ self._lock.release()
return
- self._resume.emit()
- self.wait_ready()
+ if self._threaded:
+ self._resume.emit()
+ self.wait_ready()
+ else:
+ self._do_resume()
def set_sources(self, sources=None): # sources should be {"source_name": signal_to_connect}
- self._lock.acquire(max(self._lock.available(), 1))
- self._set_sources.emit(sources)
- self.wait_ready()
+ if self._threaded:
+ self._lock.acquire(max(self._lock.available(), 1))
+ self._set_sources.emit(sources)
+ self.wait_ready()
+ else:
+ self._do_set_sources(sources)
def _init_periodic(self):
if self._period is not None:
@@ -110,9 +133,12 @@ class Component(QObject):
self.log.debug("no init periodic")
def set_period(self, period=None, lazy=True):
- self._lock.acquire(max(self._lock.available(), 1))
- self._set_sources.emit({"period": period, "lazy": lazy})
- self.wait_ready()
+ if self._threaded:
+ self._lock.acquire(max(self._lock.available(), 1))
+ self._set_sources.emit({"period": period, "lazy": lazy})
+ self.wait_ready()
+ else:
+ self._do_set_period({"period": period, "lazy": lazy})
def _start_periodic(self):
if self._timer is not None:
@@ -157,14 +183,16 @@ class Component(QObject):
self._connect_sources()
self._running = True
self.log.info("resumed")
- self._lock.release()
+ if self._threaded:
+ self._lock.release()
def _do_pause(self):
self._stop_periodic()
self._disconnect_sources()
self._running = False
self.log.info("paused")
- self._lock.release()
+ if self._threaded:
+ self._lock.release()
def _do_set_sources(self, sources):
if self._running:
@@ -173,14 +201,16 @@ class Component(QObject):
if self._running:
self._connect_sources()
self.log.info("set sources")
- self._lock.release()
+ if self._threaded:
+ self._lock.release()
def _do_set_period(self, spec):
self._period = spec.get("period", None)
self._single_shot = spec.get("lazy", True)
self._init_periodic()
self.log.info("set period")
- self._lock.release()
+ if self._threaded:
+ self._lock.release()
def _get(self, data=None):
if data is None:
diff --git a/src/components/remote_api.py b/src/components/remote_api.py
index 040c055..49099a8 100644
--- a/src/components/remote_api.py
+++ b/src/components/remote_api.py
@@ -24,8 +24,8 @@ def aupdate_available_msg():
class RemoteAPI(Component):
api_cmd = pyqtSignal(str)
- def __init__(self, config=None, name=None, period=1, lazy=True, paused=False, main=None):
- super().__init__(config=config, name=name, period=period, lazy=lazy, paused=paused)
+ def __init__(self, config=None, name=None, period=1, lazy=True, paused=False, main=None, threaded=True):
+ super().__init__(config=config, name=name, period=period, lazy=lazy, paused=paused, threaded=threaded)
self.main = main
@pyqtSlot()
diff --git a/src/components/test_component.py b/src/components/test_component.py
index 920f529..3af4c2c 100644
--- a/src/components/test_component.py
+++ b/src/components/test_component.py
@@ -11,6 +11,7 @@ class TestComponent(Component):
period=1,
lazy=True,
paused=False,
+ threaded=True,
):
super().__init__(
config=config,
@@ -18,6 +19,7 @@ class TestComponent(Component):
period=period,
lazy=lazy,
paused=paused,
+ threaded=threaded,
)
self.parameter = self.config["test"]["parameter"]
diff --git a/src/components/vision_saver.py b/src/components/vision_saver.py
new file mode 100755
index 0000000..e112906
--- /dev/null
+++ b/src/components/vision_saver.py
@@ -0,0 +1,70 @@
+import glob
+import os
+import shutil
+from datetime import datetime
+from pathlib import Path
+
+import cv2
+import numpy as np
+
+from .component import Component
+
+
+class VisionSaver(Component):
+ def __init__(self, config=None, name=None):
+ super().__init__(config=config, name=name, threaded=False)
+
+ def config_changed(self):
+ self.location = Path(self.config["vision_saver"]["path"])
+ os.makedirs(self.location, exist_ok=True)
+ self.mask_zones = self.config["vision_saver"].get("mask_zones", None)
+ self.minimum_disk_free_space_gb = self.config["vision_saver"].get("minimum_disk_free_space_gb", None)
+ if self.minimum_disk_free_space_gb is not None:
+ self.minimum_disk_free_space_gb = float(self.minimum_disk_free_space_gb)
+ self.time_format = self.config["vision_saver"]["time_format"]
+
+ def save(self, save_time, img, mask=True):
+ timestamp = datetime.fromtimestamp(save_time).strftime(self.time_format)
+ save_dir = self.location / save_time.strftime("%Y") / save_time.strftime("%m")
+ os.makedirs(save_dir, exist_ok=True)
+ out_path = save_dir / f"{timestamp}.png"
+ self.log.info(f"saving {out_path}")
+ img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
+ if mask:
+ height, width, channels = img.shape
+ out = np.full(
+ [height, width, channels],
+ [0] * channels
+ )
+ for zone_name in self.mask_zones:
+ zone = self.bench.zones[zone_name]["box"]
+ out[zone[1]:zone[3], zone[0]:zone[2]] = img[zone[1]:zone[3], zone[0]:zone[2]]
+ else:
+ out = img
+ cv2.imwrite(out_path, out)
+ return out_path
+
+ def remove_older_images_if_needed(self):
+ if self.minimum_disk_free_space_gb is None:
+ return
+ minimum_disk_free_bytes = self.minimum_disk_free_space_gb * 10**9
+ archive = os.path.abspath(self.location)
+ free = shutil.disk_usage(archive)[-1]
+ if free < minimum_disk_free_bytes:
+ self.log.warning(f"LOW DISK SPACE {(free / 10 ** 9):3.2f}GB/{(minimum_disk_free_bytes / 10 ** 9):3.2f}GB), removing older vision saves")
+ sections = sorted([os.path.dirname(section) for section in glob.glob(f"{archive}/*/")])
+ years = sorted({os.path.basename(os.path.dirname(year)) for section in sections for year in glob.glob(f"{section}/*/")})
+ while free < minimum_disk_free_bytes and len(years) > 0:
+ year = years.pop(0)
+ months = sorted({os.path.basename(os.path.dirname(month)) for section in sections for month in glob.glob(f"{section}/{year}/*/")})
+ while free < minimum_disk_free_bytes and len(months) > 0:
+ month = months.pop(0)
+ for section in sections:
+ self.log.info(f"REMOVING '{section}/{year}/{month}'")
+ shutil.rmtree(f"{section}/{year}/{month}", ignore_errors=True)
+ free = shutil.disk_usage(archive)[-1]
+ if len(months) == 0:
+ for section in sections:
+ self.log.info(f"REMOVING '{section}/{year}'")
+ shutil.rmtree(f"{section}/{year}", ignore_errors=True)
+ free = shutil.disk_usage(archive)[-1]
diff --git a/src/lib/db/__init__.py b/src/lib/db/__init__.py
index 1d6f4bc..f78bb22 100644
--- a/src/lib/db/__init__.py
+++ b/src/lib/db/__init__.py
@@ -5,10 +5,11 @@ import logging
from playhouse.sqlite_ext import JSONField
-from .models import Archive, Log, Recipes, Session, Users, db
+from .models import Archive, Log, Recipes, Session, Users, db, Autotests
models_reference = {
"archive": Archive,
+ "autotests": Autotests,
"log": Log,
"recipes": Recipes,
"users": Users,
diff --git a/src/lib/db/models/__init__.py b/src/lib/db/models/__init__.py
index d26cab3..c450943 100644
--- a/src/lib/db/models/__init__.py
+++ b/src/lib/db/models/__init__.py
@@ -1,4 +1,5 @@
from .archive import Archive
+from .autotests import Autotests
from .base_model import db
from .log import Log
from .recipes import Recipes
diff --git a/src/lib/db/models/archive.py b/src/lib/db/models/archive.py
index 925ebef..7099d56 100644
--- a/src/lib/db/models/archive.py
+++ b/src/lib/db/models/archive.py
@@ -13,18 +13,20 @@ class Archive(BaseModel):
time = DateTimeField(unique=True, null=False, default=datetime.now)
user = ForeignKeyField(Users, Users.username, null=False)
recipe = ForeignKeyField(Recipes, null=False)
+ result = BooleanField(null=False)
+ overridden = BooleanField(null=False)
test_data = JSONField(null=False)
- overridden = BooleanField(null=False, default=False)
archived = BooleanField(null=False, default=False)
uploaded = BooleanField(null=False, default=False)
@classmethod
@db.atomic()
- def archive(cls, recipe, test_data, overridden, vision_duration):
+ def archive(cls, recipe, test_data, result, overridden):
return cls.create(
user=Users.get_session().user,
recipe=recipe,
test_data=test_data,
+ result=result,
overridden=overridden,
)
diff --git a/src/lib/db/models/autotests.py b/src/lib/db/models/autotests.py
new file mode 100755
index 0000000..891f881
--- /dev/null
+++ b/src/lib/db/models/autotests.py
@@ -0,0 +1,39 @@
+from datetime import datetime
+
+from peewee import (AutoField, BooleanField, DateTimeField, ForeignKeyField,
+ TextField, fn)
+from playhouse.sqlite_ext import JSONField
+
+from .base_model import BaseModel, db
+from .recipes import Recipes
+from .users import Users
+
+
+class Autotests(BaseModel):
+ id = AutoField(primary_key=True, unique=True, null=False)
+ time = DateTimeField(unique=True, null=False, default=datetime.now)
+ user = ForeignKeyField(Users, Users.username, null=False)
+ recipe = ForeignKeyField(Recipes, null=False)
+ result = BooleanField(null=False)
+ overridden = BooleanField(null=False)
+ reason = TextField(null=False)
+ test_data = JSONField(null=False)
+
+ @classmethod
+ @db.atomic()
+ def archive(cls, recipe, test_data, result, overridden, reason):
+ return cls.create(
+ user=Users.get_session().user,
+ recipe=recipe,
+ test_data=test_data,
+ result=result,
+ overridden=overridden,
+ reason=reason,
+ )
+
+ @staticmethod
+ def get_last_time():
+ return Autotests.select(fn.MAX(Autotests.time)).scalar()
+
+ class Meta:
+ table_name = "autotests"
diff --git a/src/main.py b/src/main.py
index 0c20ac8..daed934 100644
--- a/src/main.py
+++ b/src/main.py
@@ -29,7 +29,7 @@ logs_dir = Path(".") / "data" / "logs"
os.makedirs(logs_dir, exist_ok=True)
logging.basicConfig(
format="{asctime}:{name}:{levelname}:{message}",
- datefmt="%Y-%m-%dT%H:%M:%S%z",
+ datefmt="%Y-%m-%dT%H-%M-%S%z",
style="{",
level="INFO",
handlers=[
@@ -54,7 +54,8 @@ if True:
from lib.helpers import ConfigReader
from PyQt5.QtCore import QObject, QThread, pyqtSignal
from PyQt5.QtWidgets import QApplication, QMessageBox
- from ui import About, Login, Main_Window, Test, Users_Management
+ from ui import (About, Archive, Autotests_Archive, Login, Main_Window,
+ Test, Users_Management)
class Main(QObject):
@@ -96,9 +97,19 @@ class Main(QObject):
# self.main_window = Main_Window(self.bench)
self.main_window = Main_Window()
# CONNECT MAIN WINDOW ACTIONS
+ self.main_window.archive_a.triggered.connect(self.open_archive)
+ if "--archive" in sys.argv:
+ self.main_window.archive_a.trigger()
+ self.main_window.autotests_archive_a.triggered.connect(self.open_autotests_archive)
+ if "--autotests-archive" in sys.argv:
+ self.main_window.autotests_archive_a.trigger()
self.main_window.about_a.triggered.connect(self.open_about)
+ if "--about" in sys.argv:
+ self.main_window.about_a.trigger()
self.main_window.admin_m.menuAction().setVisible(False) # admin menu should not be visible before an admin logs in
- self.main_window.login_management_a.triggered.connect(self.open_login_management)
+ self.main_window.users_management_a.triggered.connect(self.open_users_management)
+ if "--users-management" in sys.argv:
+ self.main_window.users_management_a.trigger()
# OPEN LOGIN TAB
self.open_login()
# SHOW MAIN WINDOW
@@ -110,15 +121,20 @@ class Main(QObject):
self.main_window.showFullScreen()
else:
self.main_window.show()
- self.main_window.show()
+
+ def open_archive(self):
+ self.main_window.open_dialog(Archive())
+
+ def open_autotests_archive(self):
+ self.main_window.open_dialog(Autotests_Archive())
+
+ def open_users_management(self):
+ self.main_window.open_dialog(Users_Management())
def open_about(self):
about_widget = About()
self.main_window.open_dialog(about_widget)
- def open_login_management(self):
- self.main_window.open_dialog(Users_Management())
-
def open_login(self):
tab = Login()
tab.successful_login.connect(self.logghed_in)
diff --git a/src/test.py b/src/test.py
deleted file mode 100644
index cadb780..0000000
--- a/src/test.py
+++ /dev/null
@@ -1,291 +0,0 @@
-#!/usr/bin/env python3
-import faulthandler
-import signal
-
-from lib.helpers.mergingbuffer import MergingBuffer
-
-faulthandler.enable()
-signal.signal(signal.SIGINT, lambda a, b: quit())
-
-
-def test(buffer, data, expected, expected_last_popped, function, test_name):
- print(test_name, "-" * 25, flush=True)
- print(f"buffer {list(buffer)}", flush=True)
- print(f"last_popped {buffer.last_popped}", flush=True)
- print(f"data {data}", flush=True)
- function(data)
- print(f"merged buffer {list(buffer)}", flush=True)
- print(f"last_popped {buffer.last_popped}", flush=True)
- if list(buffer) != expected:
- print(f"expected {expected}", flush=True)
- print(f"failed {test_name}", flush=True)
- quit()
- if buffer.last_popped != expected_last_popped:
- print(f"expected_last_popped {expected_last_popped}", flush=True)
- print(f"failed {test_name}", flush=True)
- quit()
- print(test_name, "_" * 25, flush=True)
-
-
-buffer = MergingBuffer()
-# insert
-data = [{"time": 0, "a": 0}, ]
-expected = [
- {
- "time": 0,
- "a": 0,
- "changed": {"time", "a"},
- },
-]
-expected_last_popped = {}
-test(buffer, data, expected, expected_last_popped, buffer.merge, "insert")
-
-# merge
-data = [{"time": 0, "b": 0}, ]
-expected = [
- {
- "time": 0,
- "a": 0,
- "b": 0,
- "changed": {"time", "a", "b"},
- },
-]
-expected_last_popped = {}
-test(buffer, data, expected, expected_last_popped, buffer.merge, "merge")
-
-# append
-data = [{"time": 1, "b": 1}, ]
-expected = [
- {
- "time": 0,
- "a": 0,
- "b": 0,
- "changed": {"time", "a", "b"},
- },
- {
- "time": 1,
- "b": 1,
- "changed": {"time", "b"},
- },
-]
-expected_last_popped = {}
-test(buffer, data, expected, expected_last_popped, buffer.merge, "append")
-
-# propagation
-data = [{"time": 2, "a": 2}, ]
-expected = [
- {
- "time": 0,
- "a": 0,
- "b": 0,
- "changed": {"time", "a", "b"},
- },
- {
- "time": 1,
- "a": 0,
- "b": 1,
- "changed": {"time", "b"},
- },
- {
- "time": 2,
- "a": 2,
- "changed": {"time", "a"},
- },
-]
-expected_last_popped = {}
-test(buffer, data, expected, expected_last_popped, buffer.merge, "propagation")
-
-# unmergeable
-data = [{"time": 2, "c": 2}, ]
-expected = [
- {
- "time": 2,
- "a": 2,
- "c": 2,
- "changed": {"time", "a", "c"},
- },
-]
-expected_last_popped = {
- "time": 1,
- "a": 0,
- "b": 1,
-}
-
-test(buffer, data, expected, expected_last_popped, buffer.merge, "unmergeable")
-
-# skip
-data = [{"time": 0, "d": 0}, {"time": 2, "d": 2}, ]
-expected = [
- {
- "time": 2,
- "a": 2,
- "c": 2,
- "d": 2,
- "changed": {"time", "a", "c", "d"},
- },
-]
-expected_last_popped = {
- "time": 1,
- "a": 0,
- "b": 1,
- "d": 0,
-}
-test(buffer, data, expected, expected_last_popped, buffer.merge, "skip")
-
-# extra
-data = [{"time": 2, "b": 2}, ]
-expected = [
- {
- "time": 2,
- "a": 2,
- "b": 2,
- "c": 2,
- "d": 2,
- "changed": {"time", "a", "b", "c", "d"},
- },
-]
-expected_last_popped = {
- "time": 1,
- "a": 0,
- "b": 1,
- "d": 0,
-}
-test(buffer, data, expected, expected_last_popped, buffer.merge, "extra")
-
-# pop_merged
-data = ["a", "b", "c", "d", ]
-expected = []
-expected_last_popped = {
- "time": 2,
- "a": 2,
- "b": 2,
- "c": 2,
- "d": 2,
-}
-test(buffer, data, expected, expected_last_popped, buffer.pop_merged, "pop_merged")
-
-# skip_and_no_update_popped
-data = [{"time": 1, "a": 1}, ]
-expected = []
-expected_last_popped = {
- "time": 2,
- "a": 2,
- "b": 2,
- "c": 2,
- "d": 2,
-}
-test(buffer, data, expected, expected_last_popped, buffer.merge, "skip_and_no_update_popped")
-
-# skip_and_update_popped
-data = [{"time": 2, "a": 2.1}, ]
-expected = []
-expected_last_popped = {
- "time": 2,
- "a": 2.1,
- "b": 2,
- "c": 2,
- "d": 2,
-}
-test(buffer, data, expected, expected_last_popped, buffer.merge, "skip_and_update_popped")
-
-# extra
-data = [{"time": 4, "e": 4}, ]
-expected = [
- {
- "time": 4,
- "e": 4,
- "changed": {"time", "e"},
- },
-]
-expected_last_popped = {
- "time": 2,
- "a": 2.1,
- "b": 2,
- "c": 2,
- "d": 2,
-}
-test(buffer, data, expected, expected_last_popped, buffer.merge, "extra")
-
-# skip_unfillable
-data = [{"time": 3, "a": 3}]
-expected = [
- {
- "time": 4,
- "e": 4,
- "changed": {"time", "e"},
- },
-]
-expected_last_popped = {
- "time": 3,
- "a": 3,
- "b": 2,
- "c": 2,
- "d": 2,
-}
-test(buffer, data, expected, expected_last_popped, buffer.merge, "skip_unfillable")
-
-# skip_fillable
-data = [{"time": 3, "e": 3}, ]
-expected = [
- {
- "time": 4,
- "e": 4,
- "changed": {"time", "e"},
- },
-]
-expected_last_popped = {
- "time": 3,
- "a": 3,
- "b": 2,
- "c": 2,
- "d": 2,
- "e": 3,
-}
-test(buffer, data, expected, expected_last_popped, buffer.merge, "skip_fillable")
-
-# insert_fillable
-data = [{"time": 3.5, "f": 3.5}, ]
-expected = [
- {
- "time": 3.5,
- "e": 3,
- "f": 3.5,
- "changed": {"time", "f"},
- },
- {
- "time": 4,
- "e": 4,
- "changed": {"time", "e"},
- },
-]
-expected_last_popped = {
- "time": 3,
- "a": 3,
- "b": 2,
- "c": 2,
- "d": 2,
- "e": 3,
-}
-test(buffer, data, expected, expected_last_popped, buffer.merge, "insert_fillable")
-
-# unmergeable_2
-data = [{"time": 5, "g": 5}]
-expected = [
- {
- "time": 5,
- "g": 5,
- "changed": {"time", "g"},
- },
-]
-expected_last_popped = {
- "time": 4,
- "a": 3,
- "b": 2,
- "c": 2,
- "d": 2,
- "e": 4,
- "f": 3.5,
-}
-test(buffer, data, expected, expected_last_popped, buffer.merge, "unmergeable_2")
-
-print("DONE, all tests ok")
diff --git a/src/ui/__init__.py b/src/ui/__init__.py
index 30a469a..37c9097 100644
--- a/src/ui/__init__.py
+++ b/src/ui/__init__.py
@@ -1,4 +1,6 @@
from .about import About
+from .archive import Archive
+from .autotests_archive import Autotests_Archive
from .dialog import Dialog
from .login import Login
from .main_window import Main_Window
diff --git a/src/ui/about/about.ui b/src/ui/about/about.ui
index ddcff9e..d446ab9 100644
--- a/src/ui/about/about.ui
+++ b/src/ui/about/about.ui
@@ -10,9 +10,6 @@
262
-
- Form
-
-
diff --git a/src/ui/archive/__init__.py b/src/ui/archive/__init__.py
new file mode 100755
index 0000000..6cc2e3f
--- /dev/null
+++ b/src/ui/archive/__init__.py
@@ -0,0 +1 @@
+from .archive import Archive
diff --git a/src/ui/archive/archive.py b/src/ui/archive/archive.py
new file mode 100755
index 0000000..d706234
--- /dev/null
+++ b/src/ui/archive/archive.py
@@ -0,0 +1,70 @@
+from lib.db import Users
+from PyQt5.QtWidgets import QAbstractItemView
+from ui.crud import Crud, Json_External_Dialog_Cell_Widget
+from ui.helpers import replace_widget
+from ui.widget import Widget
+
+
+class Archive(Widget):
+ def __init__(self, printer=None):
+ super().__init__()
+ self.printer = printer
+ session = Users.get_session()
+ if session is not None and session.is_admin:
+ crud_aliases = {
+ "id": "Id",
+ "time": "Data e ora",
+ "user": "Operatore",
+ "recipe": "Ricetta",
+ "result": "Esito",
+ "overridden": "Esito forzato",
+ "test_data": "Dati del test",
+ "archived": "Archiviato sul portale",
+ "uploaded": "Immagine in cloud",
+ }
+ readonly = ["id"]
+ else:
+ crud_aliases = {
+ "time": "Data e ora",
+ "user": "Operatore",
+ "recipe": "Ricetta",
+ "result": "Esito",
+ "overridden": "Esito forzato",
+ "test_data": "Dati del test",
+ }
+ readonly = True
+ self.crud = Crud(
+ "archive",
+ display_name="Archivio",
+ readonly=readonly,
+ select=list(crud_aliases.keys()),
+ fields_aliases=crud_aliases,
+ widget_classes={
+ "test_data": Json_External_Dialog_Cell_Widget,
+ },
+ )
+ replace_widget(self, "crud_w", self.crud)
+ self.selected = None
+ self.print_b.setEnabled(False)
+ self.crud.db_tw.setSelectionBehavior(QAbstractItemView.SelectRows)
+ self.crud.db_tw.setSelectionMode(QAbstractItemView.SingleSelection)
+ self.crud.db_tw.itemSelectionChanged.connect(self.check)
+ self.print_b.clicked.connect(self.print_label)
+
+ def check(self):
+ if not self.crud.modified:
+ selected = self.crud.get_selected_rows()
+ if len(selected) == 1:
+ selected = selected[0] - 1 # - 1 because rn starts from 1 (filters line)
+ if selected >= 0 and selected < len(self.crud.data_index):
+ selected = self.crud.data_index[selected]
+ self.selected = self.crud.db.table_model.get_by_id(selected)
+ self.print_b.setEnabled(True)
+ return
+ self.selected = None
+ self.print_b.setEnabled(False)
+
+ def print_label(self):
+ self.check()
+ if self.selected is not None and self.printer is not None:
+ self.printer.print_archive_label(self.selected)
diff --git a/src/ui/archive/archive.ui b/src/ui/archive/archive.ui
new file mode 100755
index 0000000..1b18bd1
--- /dev/null
+++ b/src/ui/archive/archive.ui
@@ -0,0 +1,37 @@
+
+
+ Test archive
+
+
+
+ 0
+ 0
+ 98
+ 61
+
+
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ Stampa
+
+
+
+ -
+
+
+
+
+
+ print_b
+
+
+
+
diff --git a/src/ui/autotests_archive/__init__.py b/src/ui/autotests_archive/__init__.py
new file mode 100755
index 0000000..b80d471
--- /dev/null
+++ b/src/ui/autotests_archive/__init__.py
@@ -0,0 +1 @@
+from .autotests_archive import Autotests_Archive
diff --git a/src/ui/autotests_archive/autotests_archive.py b/src/ui/autotests_archive/autotests_archive.py
new file mode 100755
index 0000000..8768564
--- /dev/null
+++ b/src/ui/autotests_archive/autotests_archive.py
@@ -0,0 +1,70 @@
+from lib.db import Users
+from PyQt5.QtWidgets import QAbstractItemView
+from ui.crud import Crud, Json_External_Dialog_Cell_Widget
+from ui.helpers import replace_widget
+from ui.widget import Widget
+
+
+class Autotests_Archive(Widget):
+ def __init__(self, printer=None):
+ super().__init__()
+ self.printer = printer
+ session = Users.get_session()
+ if session is not None and session.is_admin:
+ crud_aliases = {
+ "id": "Id",
+ "time": "Data e ora",
+ "user": "Operatore",
+ "recipe": "Ricetta",
+ "result": "Esito",
+ "reason": "Motivo",
+ "overridden": "Esito forzato",
+ "test_data": "Dati del test",
+ }
+ readonly = ["id"]
+ else:
+ crud_aliases = {
+ "time": "Data e ora",
+ "user": "Operatore",
+ "recipe": "Ricetta",
+ "result": "Esito",
+ "reason": "Motivo",
+ "overridden": "Esito forzato",
+ "test_data": "Dati del test",
+ }
+ readonly = True
+ self.crud = Crud(
+ "autotests",
+ display_name="Archivio autotest",
+ readonly=readonly,
+ select=list(crud_aliases.keys()),
+ fields_aliases=crud_aliases,
+ widget_classes={
+ "test_data": Json_External_Dialog_Cell_Widget,
+ },
+ )
+ replace_widget(self, "crud_w", self.crud)
+ self.selected = None
+ self.print_b.setEnabled(False)
+ self.crud.db_tw.setSelectionBehavior(QAbstractItemView.SelectRows)
+ self.crud.db_tw.setSelectionMode(QAbstractItemView.SingleSelection)
+ self.crud.db_tw.itemSelectionChanged.connect(self.check)
+ self.print_b.clicked.connect(self.print_label)
+
+ def check(self):
+ if not self.crud.modified:
+ selected = self.crud.get_selected_rows()
+ if len(selected) == 1:
+ selected = selected[0] - 1 # - 1 because rn starts from 1 (filters line)
+ if selected >= 0 and selected < len(self.crud.data_index):
+ selected = self.crud.data_index[selected]
+ self.selected = self.crud.db.table_model.get_by_id(selected)
+ self.print_b.setEnabled(True)
+ return
+ self.selected = None
+ self.print_b.setEnabled(False)
+
+ def print_label(self):
+ self.check()
+ if self.selected is not None and self.printer is not None:
+ self.printer.print_autotest_label(self.selected)
diff --git a/src/ui/autotests_archive/autotests_archive.ui b/src/ui/autotests_archive/autotests_archive.ui
new file mode 100755
index 0000000..f4cf414
--- /dev/null
+++ b/src/ui/autotests_archive/autotests_archive.ui
@@ -0,0 +1,37 @@
+
+
+ Autotests archive
+
+
+
+ 0
+ 0
+ 98
+ 61
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Stampa
+
+
+
+ -
+
+
+
+
+
+ print_b
+
+
+
+
diff --git a/src/ui/crud/crud.py b/src/ui/crud/crud.py
index 5f042ca..07e978e 100755
--- a/src/ui/crud/crud.py
+++ b/src/ui/crud/crud.py
@@ -70,7 +70,7 @@ class Cell:
value = self.parse()
fail = False
except Exception:
- traceback.print_exc()
+ self.log.exception(traceback.format_exc())
value = None
fail = True
if fail or value != self.value:
@@ -316,7 +316,7 @@ class Crud(Widget):
try:
r[fn] = w.parse(row_number=rn, crud=self)
except Exception:
- traceback.print_exc()
+ self.log.exception(traceback.format_exc())
self.set_row_color(rn, "red")
fail = True
add_row, r, filter_fail = self.row_filter(r, rn, self)
@@ -332,7 +332,7 @@ class Crud(Widget):
try:
self.db.commit(data, self.deleted_rows)
except Exception as e:
- traceback.print_exc()
+ self.log.exception(traceback.format_exc())
QMessageBox.critical(None, "Errore Salvataggio DB", str(e))
return False
# GET DATA
diff --git a/src/ui/crud/crud.ui b/src/ui/crud/crud.ui
index a2b62b4..525c69f 100755
--- a/src/ui/crud/crud.ui
+++ b/src/ui/crud/crud.ui
@@ -22,9 +22,6 @@
600
-
- Crud
-
-
diff --git a/src/ui/dialog/dialog.py b/src/ui/dialog/dialog.py
index 65a3ae5..a19915d 100644
--- a/src/ui/dialog/dialog.py
+++ b/src/ui/dialog/dialog.py
@@ -1,7 +1,10 @@
+import logging
+
from PyQt5 import uic
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QDialog
+from ui.helpers import replace_widget
dialogs = {}
@@ -18,17 +21,11 @@ class Dialog(QDialog):
self.ui = uic.loadUi(u, self)
# LOGO
self.setWindowIcon(QIcon("src/ui/imgs/neo.ico"))
+ self.log = logging.getLogger(f"{self.__class__.__name__} ({id(self)})")
def setCentralWidget(self, widget):
widget.setParent(self)
- i = self.layout().replaceWidget(self.centralwidget, widget, options=Qt.FindDirectChildrenOnly)
- if i is None:
- raise AssertionError(
- "{}.centralwidget is missing, cannot replace it. Maybe check dialog.ui file".format(__name__))
- self.centralwidget.hide()
- self.centralwidget.deleteLater()
- del i
- self.centralwidget = widget
+ replace_widget(self, "centralWidget", widget)
def centralWidget(self):
return self.centralwidget
diff --git a/src/ui/dialog/dialog.ui b/src/ui/dialog/dialog.ui
index 00ebfc7..775af0a 100644
--- a/src/ui/dialog/dialog.ui
+++ b/src/ui/dialog/dialog.ui
@@ -16,12 +16,9 @@
0
-
-
-
-
-
+
0
@@ -34,4 +31,4 @@
-
\ No newline at end of file
+
diff --git a/src/ui/helpers/replace_widget.py b/src/ui/helpers/replace_widget.py
index 409ac0c..8f989b6 100644
--- a/src/ui/helpers/replace_widget.py
+++ b/src/ui/helpers/replace_widget.py
@@ -1,6 +1,8 @@
def replace_widget(parent, name, new, delete=False):
old = getattr(parent, name)
- old.parentWidget().layout().replaceWidget(old, new)
+ replaced = old.parentWidget().layout().replaceWidget(old, new)
+ if replaced is None:
+ raise AssertionError(f"{name} not found, cannot replace it.")
old.hide()
setattr(parent, name, new)
new.show()
diff --git a/src/ui/login/login.ui b/src/ui/login/login.ui
index 3a81d6d..4e409b6 100755
--- a/src/ui/login/login.ui
+++ b/src/ui/login/login.ui
@@ -10,9 +10,6 @@
294
-
- Login
-
-
diff --git a/src/ui/main_window/main_window.ui b/src/ui/main_window/main_window.ui
index e51e948..2c3f092 100644
--- a/src/ui/main_window/main_window.ui
+++ b/src/ui/main_window/main_window.ui
@@ -6,8 +6,8 @@
0
0
- 320
- 180
+ 94
+ 40
@@ -16,7 +16,7 @@
0
0
- 320
+ 94
24
@@ -30,8 +30,16 @@
Amministrazione
-
+
+
+
@@ -40,11 +48,21 @@
Powered by
-
+
Gestione utenti
+
+
+ Archivio
+
+
+
+
+ Archivio autotest
+
+
diff --git a/src/ui/recipe_selection/recipe_selection.py b/src/ui/recipe_selection/recipe_selection.py
index 282626f..6d6efc5 100755
--- a/src/ui/recipe_selection/recipe_selection.py
+++ b/src/ui/recipe_selection/recipe_selection.py
@@ -1,10 +1,11 @@
import sys
from lib.db import Recipes
-from PyQt5.QtCore import Qt, QTimer, pyqtSignal
+from PyQt5.QtCore import QTimer, pyqtSignal
from PyQt5.QtGui import QKeySequence
from PyQt5.QtWidgets import QShortcut
from ui.crud import Crud
+from ui.helpers import replace_widget
from ui.widget import Widget
@@ -27,9 +28,9 @@ def recipes_row_filter(row, row_number, crud):
class Recipe_Selection(Widget):
ok = pyqtSignal(Recipes)
- def __init__(self, session=None):
+ def __init__(self):
super().__init__()
- self.crud_aliases = {
+ crud_aliases = {
"name": "Ricetta",
"client": "Cliente",
"part_number": "N° disegno",
@@ -40,20 +41,14 @@ class Recipe_Selection(Widget):
"recipes",
display_name="SELEZIONE RICETTA",
readonly=True,
- select=list(self.crud_aliases.keys()),
+ select=list(crud_aliases.keys()),
filters={"archived": False},
- fields_aliases=self.crud_aliases,
+ fields_aliases=crud_aliases,
autocomplete={"archived": False},
row_upgrader=recipes_row_upgrader,
row_filter=recipes_row_filter,
)
- i = self.layout().replaceWidget(self.crud_w, self.crud, options=Qt.FindDirectChildrenOnly)
- if i is None:
- raise AssertionError("{}.crud_w is missing, cannot replace it. Maybe check dialog.ui file".format(__name__))
- self.crud_w.hide()
- self.crud_w.deleteLater()
- del i
- self.crud_w = self.crud
+ replace_widget(self, "crud_w", self.crud)
self.selected = None
self.select_b.setEnabled(False)
QShortcut(QKeySequence("Return"), self).activated.connect(self.select_b.click)
diff --git a/src/ui/recipe_selection/recipe_selection.ui b/src/ui/recipe_selection/recipe_selection.ui
index c788b23..0d798b8 100644
--- a/src/ui/recipe_selection/recipe_selection.ui
+++ b/src/ui/recipe_selection/recipe_selection.ui
@@ -10,9 +10,6 @@
600
-
- Form
-
-
diff --git a/src/ui/test/test.ui b/src/ui/test/test.ui
index f337b23..b6f355d 100755
--- a/src/ui/test/test.ui
+++ b/src/ui/test/test.ui
@@ -10,9 +10,6 @@
85
-
- Test
-
-
diff --git a/src/ui/test_assembly/test_assembly.ui b/src/ui/test_assembly/test_assembly.ui
index 1433860..9db7fa4 100755
--- a/src/ui/test_assembly/test_assembly.ui
+++ b/src/ui/test_assembly/test_assembly.ui
@@ -10,9 +10,6 @@
108
-
- Form
-
-
diff --git a/src/ui/test_autotest/test_autotest.ui b/src/ui/test_autotest/test_autotest.ui
index 9773f77..9ef8444 100755
--- a/src/ui/test_autotest/test_autotest.ui
+++ b/src/ui/test_autotest/test_autotest.ui
@@ -2,9 +2,6 @@
Autotest
-
- Form
-
diff --git a/src/ui/users_management/users_management.py b/src/ui/users_management/users_management.py
index 77bea87..d4ca05d 100644
--- a/src/ui/users_management/users_management.py
+++ b/src/ui/users_management/users_management.py
@@ -38,7 +38,7 @@ class Users_Management(Widget):
def parse(self, row_number=None, crud=None):
return Users.parse_roles(self.text())
- self.crud_aliases = {
+ crud_aliases = {
"id": "Id",
"username": "Nome utente",
"password": "Password",
@@ -48,8 +48,8 @@ class Users_Management(Widget):
"users",
display_name="GESTIONE UTENTI",
readonly=["id"],
- select=list(self.crud_aliases.keys()),
- fields_aliases=self.crud_aliases,
+ select=list(crud_aliases.keys()),
+ fields_aliases=crud_aliases,
autocomplete={"archived": False},
widget_classes={
"username": Username_Line_Edit_Cell_Widget,
@@ -68,7 +68,7 @@ class Users_Management(Widget):
try:
user = Users.generate(username=row["username"], password=row["password"], roles=row["roles"])
except AssertionError as e:
- traceback.print_exc()
+ self.log.exception(traceback.format_exc())
crud.set_row_color(row_number, "red")
QMessageBox.critical(None, "Errore Salvataggio DB", f"Errore alla riga {row_number}:\n{str(e)}")
return False, None, True
diff --git a/src/ui/users_management/users_management.ui b/src/ui/users_management/users_management.ui
index a06d976..34e6e8b 100644
--- a/src/ui/users_management/users_management.ui
+++ b/src/ui/users_management/users_management.ui
@@ -10,9 +10,6 @@
18
-
- Form
-
diff --git a/src/ui/widget/widget.py b/src/ui/widget/widget.py
index 18c414d..25104f2 100644
--- a/src/ui/widget/widget.py
+++ b/src/ui/widget/widget.py
@@ -1,3 +1,5 @@
+import logging
+
from lib.helpers import get_resource
from PyQt5 import uic
from PyQt5.QtCore import Qt
@@ -13,6 +15,7 @@ class Widget(QWidget):
u = get_resource("ui/{0}/{0}.ui".format(me.lower()))
self.ui = uic.loadUi(u, self)
self.setWindowTitle(me)
+ self.log = logging.getLogger(f"{self.__class__.__name__} ({id(self)})")
def setParent(self, parent):
parent._closing.connect(self._parent_closing)
diff --git a/src/ui/window/window.py b/src/ui/window/window.py
index a28e8db..6999475 100644
--- a/src/ui/window/window.py
+++ b/src/ui/window/window.py
@@ -1,3 +1,5 @@
+import logging
+
from lib.helpers import get_resource
from PyQt5 import uic
from PyQt5.QtCore import Qt, pyqtSignal
@@ -18,6 +20,7 @@ class Window(QMainWindow):
self.ui = uic.loadUi(u, self)
# LOGO
self.setWindowIcon(QIcon(get_resource("ui/imgs/neo.ico")))
+ self.log = logging.getLogger(f"{self.__class__.__name__} ({id(self)})")
def setCentralWidget(self, widget):
widget.setParent(self)
diff --git a/src/ui/window/window.ui b/src/ui/window/window.ui
index c4dda26..2866b32 100644
--- a/src/ui/window/window.ui
+++ b/src/ui/window/window.ui
@@ -20,42 +20,8 @@
24
-
-
-
-
-
-
- Powered by
-
-
-
-
- Panel
-
-
-
-
- Exit
-
-
-
-
- Exporter
-
-
-
\ No newline at end of file
+