Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fdd0dd977 | ||
|
|
9d5c935fd2 |
|
|
@ -223,7 +223,7 @@ class ArchiveSynchronizer(Component):
|
|||
"result": "OK" if record.result else "KO",
|
||||
"serial": record.id,
|
||||
"time": record.time.isoformat(),
|
||||
"user": record.user.username,
|
||||
"user": (record.user.username if hasattr(record.user, "username") else record.user),
|
||||
"barcode_out": record.barcode if record.barcode else "NA",
|
||||
}, timeout=5, verify=False)
|
||||
else:
|
||||
|
|
@ -234,7 +234,7 @@ class ArchiveSynchronizer(Component):
|
|||
"result": "OK" if record.result else "KO",
|
||||
"serial": record.id,
|
||||
"time": record.time.isoformat(),
|
||||
"user": record.user.username,
|
||||
"user": (record.user.username if hasattr(record.user, "username") else record.user),
|
||||
}, timeout=5, verify=False)
|
||||
|
||||
if r.status_code != 200:
|
||||
|
|
|
|||
|
|
@ -3,11 +3,15 @@ import csv
|
|||
import json
|
||||
import logging
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
|
||||
from playhouse.sqlite_ext import JSONField
|
||||
|
||||
from .models import Archive, Log, Recipes, Session, Users, db
|
||||
from .models import Archive, Log, Recipes, Session, Users, db, db_archive
|
||||
from .models.base_model import handle_fatal_sqlite_error
|
||||
|
||||
# Keep a unified reference for consumers like Crud_DB
|
||||
models_reference = {
|
||||
"archive": Archive,
|
||||
"log": Log,
|
||||
|
|
@ -15,36 +19,94 @@ models_reference = {
|
|||
"users": Users,
|
||||
}
|
||||
|
||||
db.connect()
|
||||
db.create_tables(list(models_reference.values()))
|
||||
# Optional test flag to simulate on-disk SQLite corruption (archive DB only)
|
||||
if "--kill-db" in sys.argv:
|
||||
try:
|
||||
_db = db_archive # Only corrupt the archive database
|
||||
path = getattr(_db, "database", None)
|
||||
if path:
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"NOT A DATABASE - corrupted for test")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Connect both databases and create tables in their respective DBs
|
||||
try:
|
||||
db.connect()
|
||||
db_archive.connect()
|
||||
except Exception as e:
|
||||
msg = str(e).lower()
|
||||
if any(s in msg for s in (
|
||||
"malformed",
|
||||
"not a database",
|
||||
"file is encrypted or is not a database",
|
||||
"database disk image is malformed",
|
||||
)):
|
||||
handle_fatal_sqlite_error(e)
|
||||
raise
|
||||
|
||||
# Create tables for the main database (exclude Archive which lives in db_archive)
|
||||
main_models = [Log, Recipes, Users]
|
||||
db.create_tables(main_models)
|
||||
|
||||
# Create tables for the archive database
|
||||
archive_models = [Archive]
|
||||
db_archive.create_tables(archive_models)
|
||||
|
||||
log = logging.getLogger("db")
|
||||
|
||||
@db.atomic()
|
||||
def init_db():
|
||||
# Import seed data for main DB
|
||||
tables = db.get_tables()
|
||||
tables.sort()
|
||||
for table in tables:
|
||||
count = 0
|
||||
try:
|
||||
with open(f"src/lib/db/imports/{table}.csv", "r") as f:
|
||||
table = models_reference[table]
|
||||
fields = list(table._meta.fields)
|
||||
log.info(f"importing {table._meta.table_name}")
|
||||
table_model = models_reference[table]
|
||||
fields = list(table_model._meta.fields)
|
||||
log.info(f"importing {table_model._meta.table_name}")
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
obj = {}
|
||||
for field in fields:
|
||||
if type(table._meta.fields[field]) is JSONField:
|
||||
if type(table_model._meta.fields[field]) is JSONField:
|
||||
obj[field] = json.loads(row[field])
|
||||
else:
|
||||
try:
|
||||
obj[field] = ast.literal_eval(row[field])
|
||||
except (SyntaxError, ValueError):
|
||||
obj[field] = row[field]
|
||||
table.insert(**obj).on_conflict_replace().execute()
|
||||
table_model.insert(**obj).on_conflict_replace().execute()
|
||||
count += 1
|
||||
log.info(f"{table._meta.table_name}: imported {count} rows.")
|
||||
log.info(f"{table_model._meta.table_name}: imported {count} rows.")
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
# Import seed data for archive DB (only 'archive' table)
|
||||
try:
|
||||
with open("src/lib/db/imports/archive.csv", "r") as f:
|
||||
from playhouse.sqlite_ext import JSONField as _J
|
||||
fields = list(Archive._meta.fields)
|
||||
log.info(f"importing {Archive._meta.table_name} (archive DB)")
|
||||
reader = csv.DictReader(f)
|
||||
count = 0
|
||||
with db_archive.atomic():
|
||||
for row in reader:
|
||||
obj = {}
|
||||
for field in fields:
|
||||
if type(Archive._meta.fields[field]) is JSONField:
|
||||
obj[field] = json.loads(row[field])
|
||||
else:
|
||||
try:
|
||||
obj[field] = ast.literal_eval(row[field])
|
||||
except (SyntaxError, ValueError):
|
||||
obj[field] = row[field]
|
||||
Archive.insert(**obj).on_conflict_replace().execute()
|
||||
count += 1
|
||||
log.info(f"{Archive._meta.table_name}: imported {count} rows into archive DB.")
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from .archive import Archive
|
||||
from .base_model import db
|
||||
from .base_model import db, db_archive
|
||||
from .log import Log
|
||||
from .recipes import Recipes
|
||||
from .users import Session, Users
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
from datetime import datetime
|
||||
|
||||
from peewee import (AutoField, BooleanField, DateTimeField, ForeignKeyField,
|
||||
from peewee import (AutoField, BooleanField, DateTimeField,
|
||||
TextField, IntegerField)
|
||||
from playhouse.sqlite_ext import JSONField
|
||||
|
||||
from .base_model import BaseModel, db
|
||||
from .base_model import BaseModel, db_archive
|
||||
from .users import Users
|
||||
|
||||
|
||||
class Archive(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)
|
||||
time = DateTimeField(unique=True, null=False, default=datetime.now)
|
||||
# Store username directly to avoid cross-database foreign keys
|
||||
user = TextField(null=False)
|
||||
result = BooleanField(null=False)
|
||||
overridden = BooleanField(null=False)
|
||||
test_data = JSONField(null=False)
|
||||
|
|
@ -21,14 +22,14 @@ class Archive(BaseModel):
|
|||
uploaded = BooleanField(null=False, default=False)
|
||||
|
||||
@classmethod
|
||||
@db.atomic()
|
||||
@db_archive.atomic()
|
||||
def archive(cls, test_data, result, overridden):
|
||||
time=datetime.now()
|
||||
test_data["time"]=time.strftime("%d/%m/%Y %H:%M:%S")
|
||||
test_data["user"]=Users.get_session().username
|
||||
time = datetime.now()
|
||||
test_data["time"] = time.strftime("%d/%m/%Y %H:%M:%S")
|
||||
test_data["user"] = Users.get_session().username
|
||||
return cls.create(
|
||||
time=time,
|
||||
user=Users.get_session().user,
|
||||
user=Users.get_session().username,
|
||||
result=result,
|
||||
overridden=overridden,
|
||||
test_data=test_data,
|
||||
|
|
@ -36,3 +37,4 @@ class Archive(BaseModel):
|
|||
|
||||
class Meta:
|
||||
table_name = "archive"
|
||||
database = db_archive
|
||||
|
|
|
|||
|
|
@ -1,12 +1,203 @@
|
|||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import traceback
|
||||
|
||||
from peewee import Model
|
||||
from playhouse.sqlite_ext import SqliteExtDatabase
|
||||
|
||||
# GUI popup support (create minimal app if needed)
|
||||
try:
|
||||
from PyQt5.QtWidgets import QApplication, QMessageBox
|
||||
except Exception:
|
||||
QApplication = None
|
||||
QMessageBox = None
|
||||
|
||||
|
||||
db_path = "./data/database"
|
||||
os.makedirs(db_path, exist_ok=True)
|
||||
|
||||
db = SqliteExtDatabase(
|
||||
|
||||
def _delete_archive_and_folder_and_exit():
|
||||
# Try to detach any attached archive data source alias 'e' if present
|
||||
try:
|
||||
if 'db' in globals() and getattr(globals()['db'], 'is_closed', lambda: True)() is False:
|
||||
try:
|
||||
globals()['db'].execute_sql('DETACH DATABASE e')
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if 'db_archive' in globals() and getattr(globals()['db_archive'], 'is_closed', lambda: True)() is False:
|
||||
try:
|
||||
globals()['db_archive'].execute_sql('DETACH DATABASE e')
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Try closing DBs if already created
|
||||
try:
|
||||
if 'db' in globals() and getattr(globals()['db'], 'is_closed', lambda: True)() is False:
|
||||
try:
|
||||
globals()['db'].close()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
if 'db_archive' in globals() and getattr(globals()['db_archive'], 'is_closed', lambda: True)() is False:
|
||||
try:
|
||||
globals()['db_archive'].close()
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Remove archive DB explicitly (base file and side-car files)
|
||||
try:
|
||||
base = os.path.join(db_path, 'sqlite_archive.db')
|
||||
candidates = [
|
||||
base,
|
||||
os.path.join(db_path, 'sqlite_archive'),
|
||||
base + '-wal',
|
||||
base + '-shm',
|
||||
base + '-journal',
|
||||
]
|
||||
for p in candidates:
|
||||
try:
|
||||
os.remove(p)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Try to recreate the new archive DB and import seed data
|
||||
try:
|
||||
# Reconnect will create the file if missing
|
||||
if 'db_archive' in globals():
|
||||
try:
|
||||
if getattr(globals()['db_archive'], 'is_closed', lambda: True)():
|
||||
globals()['db_archive'].connect()
|
||||
except Exception:
|
||||
# If connect fails, try reopen after ensuring close
|
||||
try:
|
||||
globals()['db_archive'].close()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
globals()['db_archive'].connect()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Lazy import to avoid circular
|
||||
try:
|
||||
from .archive import Archive # type: ignore
|
||||
# Create table
|
||||
try:
|
||||
globals()['db_archive'].create_tables([Archive])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Import from CSV if present
|
||||
try:
|
||||
import csv, json, ast
|
||||
from playhouse.sqlite_ext import JSONField as _JSONField
|
||||
path = "src/lib/db/imports/archive.csv"
|
||||
if os.path.exists(path):
|
||||
with open(path, "r") as f:
|
||||
reader = csv.DictReader(f)
|
||||
fields = list(Archive._meta.fields)
|
||||
count = 0
|
||||
with globals()['db_archive'].atomic():
|
||||
for row in reader:
|
||||
obj = {}
|
||||
for field in fields:
|
||||
if type(Archive._meta.fields[field]) is _JSONField:
|
||||
obj[field] = json.loads(row.get(field, "null"))
|
||||
else:
|
||||
try:
|
||||
obj[field] = ast.literal_eval(row.get(field, ""))
|
||||
except (SyntaxError, ValueError):
|
||||
obj[field] = row.get(field)
|
||||
try:
|
||||
Archive.insert(**obj).on_conflict_replace().execute()
|
||||
count += 1
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Hard exit to ensure restart
|
||||
try:
|
||||
os._exit(1)
|
||||
except Exception:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def handle_fatal_sqlite_error(exc=None):
|
||||
"""Show popup, print error to terminal, delete only the archive DB, try to re-import, then exit."""
|
||||
msg_text = "ATTENZIONE ERRORE FATALE DATABASE DI TEST PEFOVARORE PREMERE OK E RIAVVIAREIL PROGRAMMA"
|
||||
|
||||
# Always print error details to the execution terminal
|
||||
try:
|
||||
sys.stderr.write("FATAL SQLITE ERROR DETECTED\n")
|
||||
if exc is not None:
|
||||
sys.stderr.write(f"Exception: {exc!r}\n")
|
||||
try:
|
||||
traceback.print_exc()
|
||||
except Exception:
|
||||
pass
|
||||
sys.stderr.flush()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# If GUI available, show a critical popup. Create a temp app if needed.
|
||||
app_created = False
|
||||
app = None
|
||||
try:
|
||||
if QApplication is not None:
|
||||
app = QApplication.instance()
|
||||
if app is None:
|
||||
app = QApplication([])
|
||||
app_created = True
|
||||
if QMessageBox is not None:
|
||||
QMessageBox.critical(None, "Errore Database", msg_text)
|
||||
except Exception:
|
||||
# Also echo the user-facing message to stderr in case GUI cannot be shown
|
||||
try:
|
||||
sys.stderr.write(msg_text + "\n")
|
||||
sys.stderr.flush()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
# Ensure resources are cleaned up and exit (with attempted rebuild)
|
||||
_delete_archive_and_folder_and_exit()
|
||||
|
||||
|
||||
class SafeSqliteExtDatabase(SqliteExtDatabase):
|
||||
def execute_sql(self, sql, params=None, commit=True):
|
||||
try:
|
||||
return super().execute_sql(sql, params=params, commit=commit)
|
||||
except Exception as e:
|
||||
msg = str(e).lower()
|
||||
if any(s in msg for s in (
|
||||
"malformed",
|
||||
"not a database",
|
||||
"file is encrypted or is not a database",
|
||||
"database disk image is malformed",
|
||||
)):
|
||||
handle_fatal_sqlite_error(e)
|
||||
raise
|
||||
|
||||
|
||||
# Main application database
|
||||
db = SafeSqliteExtDatabase(
|
||||
db_path + "/sqlite.db",
|
||||
pragmas={ # see https://www.sqlite.org/pragma.html
|
||||
"auto_vacuum": 1,
|
||||
|
|
@ -20,6 +211,21 @@ db = SqliteExtDatabase(
|
|||
timeout=5
|
||||
)
|
||||
|
||||
# Separate database dedicated to archive data
|
||||
db_archive = SafeSqliteExtDatabase(
|
||||
db_path + "/sqlite_archive.db",
|
||||
pragmas={
|
||||
"auto_vacuum": 1,
|
||||
"busy_timeout": 5000,
|
||||
"cache_size": round(-64e3),
|
||||
"foreign_keys": 0, # no FKs across DBs; archive uses simple fields
|
||||
"ignore_check_constraints": 0,
|
||||
"journal_mode": "wal",
|
||||
"synchronous": 1,
|
||||
},
|
||||
timeout=5
|
||||
)
|
||||
|
||||
|
||||
class BaseModel(Model):
|
||||
"""A base model that will use our Sqlite database."""
|
||||
|
|
|
|||
|
|
@ -971,8 +971,8 @@ class Test(Widget):
|
|||
# EXTRA DATA
|
||||
"SHIFT": str(get_shift(archived.time)),
|
||||
"STATION": str(self.config.machine_id),
|
||||
"OPERATOR": str(archived.user.username),
|
||||
"BADGE_NUM": str(archived.user.badge_number),
|
||||
"OPERATOR": str(archived.user.username) if hasattr(archived.user, "username") else str(archived.user),
|
||||
"BADGE_NUM": (str(archived.user.badge_number) if hasattr(archived.user, "badge_number") else str((Users.get_user(archived.user).badge_number if Users.get_user(archived.user) else ""))),
|
||||
# BARCODE
|
||||
"BCODE": str(self.step.spec.get("barcode","")),
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user