This commit is contained in:
matteo porta 2022-05-25 17:28:17 +02:00
parent 825b87bf80
commit 5367cfaa1c
40 changed files with 500 additions and 437 deletions

12
TODO.txt Normal file
View File

@ -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

View File

@ -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/

View File

@ -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

View File

@ -1,3 +1,4 @@
from .archive_synchronizer import ArchiveSynchronizer
from .remote_api import RemoteAPI
from .test_component import TestComponent
from .vision_saver import VisionSaver

View File

@ -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:

View File

@ -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:

View File

@ -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()

View File

@ -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"]

70
src/components/vision_saver.py Executable file
View File

@ -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]

View File

@ -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,

View File

@ -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

View File

@ -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,
)

39
src/lib/db/models/autotests.py Executable file
View File

@ -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"

View File

@ -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)

View File

@ -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")

View File

@ -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

View File

@ -10,9 +10,6 @@
<height>262</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label_6">

1
src/ui/archive/__init__.py Executable file
View File

@ -0,0 +1 @@
from .archive import Archive

70
src/ui/archive/archive.py Executable file
View File

@ -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)

37
src/ui/archive/archive.ui Executable file
View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Test archive</class>
<widget class="QWidget" name="Test archive">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>98</width>
<height>61</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<widget class="QPushButton" name="print_b">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Stampa</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QWidget" name="crud_w" native="true"/>
</item>
</layout>
</widget>
<tabstops>
<tabstop>print_b</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1 @@
from .autotests_archive import Autotests_Archive

View File

@ -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)

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Autotests archive</class>
<widget class="QWidget" name="Autotests archive">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>98</width>
<height>61</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<widget class="QPushButton" name="print_b">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Stampa</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QWidget" name="crud_w" native="true"/>
</item>
</layout>
</widget>
<tabstops>
<tabstop>print_b</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@ -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

View File

@ -22,9 +22,6 @@
<height>600</height>
</size>
</property>
<property name="windowTitle">
<string>Crud</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QGroupBox" name="db_gb">

View File

@ -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

View File

@ -16,12 +16,9 @@
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string></string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QWidget" name="centralwidget" native="true">
<widget class="QWidget" name="centralWidget" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
@ -34,4 +31,4 @@
</widget>
<resources/>
<connections/>
</ui>
</ui>

View File

@ -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()

View File

@ -10,9 +10,6 @@
<height>294</height>
</rect>
</property>
<property name="windowTitle">
<string>Login</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="0">
<spacer name="horizontalSpacer">

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>320</width>
<height>180</height>
<width>94</width>
<height>40</height>
</rect>
</property>
<widget class="QWidget" name="centralwidget"/>
@ -16,7 +16,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>320</width>
<width>94</width>
<height>24</height>
</rect>
</property>
@ -30,8 +30,16 @@
<property name="title">
<string>Amministrazione</string>
</property>
<addaction name="login_management_a"/>
<addaction name="users_management_a"/>
</widget>
<widget class="QMenu" name="menuStrumenti">
<property name="title">
<string>Strumenti</string>
</property>
<addaction name="archive_a"/>
<addaction name="autotests_archive_a"/>
</widget>
<addaction name="menuStrumenti"/>
<addaction name="admin_m"/>
<addaction name="menuAbout"/>
</widget>
@ -40,11 +48,21 @@
<string>Powered by</string>
</property>
</action>
<action name="login_management_a">
<action name="users_management_a">
<property name="text">
<string>Gestione utenti</string>
</property>
</action>
<action name="archive_a">
<property name="text">
<string>Archivio</string>
</property>
</action>
<action name="autotests_archive_a">
<property name="text">
<string>Archivio autotest</string>
</property>
</action>
</widget>
<resources/>
<connections/>

View File

@ -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)

View File

@ -10,9 +10,6 @@
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<widget class="QPushButton" name="select_b">

View File

@ -10,9 +10,6 @@
<height>85</height>
</rect>
</property>
<property name="windowTitle">
<string>Test</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="0">
<widget class="QWidget" name="centralWidget" native="true">

View File

@ -10,9 +10,6 @@
<height>108</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="img_l">

View File

@ -2,9 +2,6 @@
<ui version="4.0">
<class>Autotest</class>
<widget class="QWidget" name="Autotest">
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout"/>
</widget>
<resources/>

View File

@ -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

View File

@ -10,9 +10,6 @@
<height>18</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout"/>
</widget>
<resources/>

View File

@ -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)

View File

@ -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)

View File

@ -20,42 +20,8 @@
<height>24</height>
</rect>
</property>
<widget class="QMenu" name="menuAbout">
<property name="title">
<string>About</string>
</property>
<addaction name="powered_by_m"/>
</widget>
<widget class="QMenu" name="menuRecords">
<property name="title">
<string>Records</string>
</property>
<addaction name="record_exporter_m"/>
</widget>
<addaction name="menuRecords"/>
<addaction name="menuAbout"/>
</widget>
<action name="powered_by_m">
<property name="text">
<string>Powered by</string>
</property>
</action>
<action name="diagnostic_panel_m">
<property name="text">
<string>Panel</string>
</property>
</action>
<action name="actionExit">
<property name="text">
<string>Exit</string>
</property>
</action>
<action name="record_exporter_m">
<property name="text">
<string>Exporter</string>
</property>
</action>
</widget>
<resources/>
<connections/>
</ui>
</ui>