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 - + + + + Strumenti + + + + + @@ -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 - - - About - - - - - - Records - - - - - - - - Powered by - - - - - Panel - - - - - Exit - - - - - Exporter - - - \ No newline at end of file +