Skip to content

Commit 9700813

Browse files
Minor improvement to db (#115)
* Sanitize db function introduced * Fixed actuator id in query * Now mainwindow shows selected database and notification methods * UI bugfixes: WhatsApp enablement checkbox now working at init time when notifier whas provided by config. * Dialogs titles fixed
1 parent c3a2bc1 commit 9700813

20 files changed

+446
-232
lines changed

wadas/domain/database.py

Lines changed: 123 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,12 @@ def get_instance(cls):
178178
return None
179179
return DataBase.wadas_db
180180

181+
@classmethod
182+
def get_enabled_db(cls):
183+
"""Method that returns the db instance if enabled"""
184+
185+
return db if (db := cls.get_instance()) and db.enabled else None
186+
181187
@classmethod
182188
def destroy_instance(cls):
183189
"""Destroy the current database instance and release resources."""
@@ -380,17 +386,23 @@ def update_detection_event(cls, detection_event: DetectionEvent):
380386

381387
@classmethod
382388
def update_camera(cls, camera, delete_camera=False):
383-
"""Method to reflect camera fields update in db."""
389+
"""Method to reflect camera fields update in db given a camera object"""
384390

385-
logger.debug("Updating camera db entry...")
386-
camera_db_id = cls.get_camera_id(camera.id)
387-
enabled = camera.enabled
388-
389-
if not camera_db_id:
391+
if not cls.update_camera_by_db_id(
392+
cls.get_camera_id(camera.id), camera.enabled, delete_camera
393+
):
390394
logger.error(
391395
"Unable to update camera. Camera ID %s not found or already deleted.", camera.id
392396
)
393-
return
397+
398+
@classmethod
399+
def update_camera_by_db_id(cls, camera_db_id, enabled, delete_camera=False):
400+
"""Method to reflect camera fields update in db given a camera id"""
401+
402+
logger.debug("Updating camera db entry...")
403+
404+
if not camera_db_id:
405+
return False
394406

395407
if delete_camera:
396408
deletion_date_time = get_precise_timestamp()
@@ -408,21 +420,28 @@ def update_camera(cls, camera, delete_camera=False):
408420
else:
409421
stmt = update(ORMCamera).where(ORMCamera.db_id == camera_db_id).values(enabled=enabled)
410422
cls.run_query(stmt)
423+
return True
411424

412425
@classmethod
413426
def update_actuator(cls, actuator, delete_actuator=False):
414-
"""Method to reflect actuator fields update in db."""
415-
416-
logger.debug("Updating actuator db entry...")
417-
actuator_db_id = cls.get_actuator_id(actuator.id)
418-
enabled = actuator.enabled
427+
"""Method to reflect actuator fields update in db given an actuator object"""
419428

420-
if not actuator_db_id:
429+
if not cls.update_actuator_by_db_id(
430+
cls.get_actuator_id(actuator.id), actuator.enabled, delete_actuator
431+
):
421432
logger.error(
422433
"Unable to update actuator. Actuator ID %s not found or already deleted.",
423434
actuator.id,
424435
)
425-
return
436+
437+
@classmethod
438+
def update_actuator_by_db_id(cls, actuator_db_id, enabled, delete_actuator=False):
439+
"""Method to reflect actuator fields update in db given an actuator id."""
440+
441+
logger.debug("Updating actuator db entry...")
442+
443+
if not actuator_db_id:
444+
return False
426445

427446
if delete_actuator:
428447
deletion_date_time = get_precise_timestamp()
@@ -444,6 +463,7 @@ def update_actuator(cls, actuator, delete_actuator=False):
444463
.values(enabled=enabled)
445464
)
446465
cls.run_query(stmt)
466+
return True
447467

448468
@classmethod
449469
def add_actuator_to_camera(cls, camera, actuator):
@@ -687,6 +707,95 @@ def populate_db(cls, uuid):
687707
for camera in cameras:
688708
cls.insert_into_db(camera)
689709

710+
@classmethod
711+
def sanitize_db(cls):
712+
"""Method to align db tables with domain model"""
713+
714+
if session := cls.create_session():
715+
# Check if actuators in model are reflected into db
716+
for actuator_id in Actuator.actuators:
717+
cur_actuator = Actuator.actuators[actuator_id]
718+
if actuator_db_id := cls.get_actuator_id(actuator_id):
719+
# Check actuator attributes type, enabled
720+
db_actuator = (
721+
session.query(ORMActuator).filter(ORMActuator.db_id == actuator_db_id).one()
722+
)
723+
if db_actuator.type != cur_actuator.type:
724+
# If type does not match, set to deleted the one in db
725+
cls.update_actuator_by_db_id(actuator_db_id, db_actuator.enabled, True)
726+
# Insert new actuator into db
727+
cls.insert_into_db(cur_actuator)
728+
if db_actuator.enabled != cur_actuator.enabled:
729+
cls.update_actuator(cur_actuator, False)
730+
else:
731+
cls.insert_into_db(cur_actuator)
732+
733+
# Check if cameras in model are reflected into db
734+
for camera in cameras:
735+
if camera_db_id := cls.get_camera_id(camera.id):
736+
db_camera = (
737+
session.query(ORMCamera).filter(ORMCamera.db_id == camera_db_id).one()
738+
)
739+
if camera.type != db_camera.type:
740+
cls.update_camera_by_db_id(camera_db_id, db_camera.enabled, True)
741+
cls.insert_into_db(camera)
742+
if camera.enabled != db_camera.enabled:
743+
cls.update_camera(camera, False)
744+
if camera.actuators != db_camera.actuators:
745+
# Delete all associations for camera
746+
stmt = delete(camera_actuator_association).where(
747+
camera_actuator_association.c.camera_id == camera_db_id
748+
)
749+
cls.run_query(stmt)
750+
# Pristine associations for camera
751+
for actuator in camera.actuators:
752+
actuator_db_id = cls.get_actuator_id(actuator.id)
753+
stmt = camera_actuator_association.insert().values(
754+
camera_id=camera_db_id, actuator_id=actuator_db_id
755+
)
756+
cls.run_query(stmt)
757+
else:
758+
cls.insert_into_db(camera)
759+
760+
# Check if actuators in db match the ones in domain
761+
actuator_ids = session.query(ORMActuator.actuator_id).all()
762+
actuator_ids_from_db = {actuator_id[0] for actuator_id in actuator_ids}
763+
db_extra_actuators_ids = actuator_ids_from_db - Actuator.actuators.keys()
764+
for extra_actuator_id in db_extra_actuators_ids:
765+
deletion_date_time = get_precise_timestamp()
766+
actuator_db_id = cls.get_actuator_id(extra_actuator_id)
767+
stmt = (
768+
update(ORMActuator)
769+
.where(ORMActuator.db_id == actuator_db_id)
770+
.values(deletion_date=deletion_date_time)
771+
)
772+
cls.run_query(stmt)
773+
# Delete camera association with actuators, if any
774+
stmt = delete(camera_actuator_association).where(
775+
camera_actuator_association.c.actuator_id == extra_actuator_id
776+
)
777+
cls.run_query(stmt)
778+
779+
# Check if cameras in db match the ones in domain
780+
camera_ids = session.query(ORMCamera.camera_id).all()
781+
camera_ids_from_db = {camera_id[0] for camera_id in camera_ids}
782+
camera_id_from_domain = {camera.id for camera in cameras}
783+
db_extra_camera_ids = camera_ids_from_db - camera_id_from_domain
784+
for extra_camera_id in db_extra_camera_ids:
785+
deletion_date_time = get_precise_timestamp()
786+
camera_db_id = cls.get_camera_id(extra_camera_id)
787+
stmt = (
788+
update(ORMCamera)
789+
.where(ORMCamera.db_id == camera_db_id)
790+
.values(deletion_date=deletion_date_time)
791+
)
792+
cls.run_query(stmt)
793+
# Delete camera association with actuators, if any
794+
stmt = delete(camera_actuator_association).where(
795+
camera_actuator_association.c.camera_id == extra_camera_id
796+
)
797+
cls.run_query(stmt)
798+
690799
@abstractmethod
691800
def get_connection_string(self):
692801
"""Generate the connection string based on the database type."""

wadas/domain/db_model.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,10 @@ def compile_datetime6_mariadb(element, compiler, **kwargs):
6161
camera_actuator_association = Table(
6262
"camera_actuator_association",
6363
Base.metadata,
64-
Column("camera_id", Integer, ForeignKey("cameras.id"), primary_key=True),
65-
Column("actuator_id", Integer, ForeignKey("actuators.id"), primary_key=True),
64+
Column("camera_id", Integer, ForeignKey("cameras.id", ondelete="CASCADE"), primary_key=True),
65+
Column(
66+
"actuator_id", Integer, ForeignKey("actuators.id", ondelete="CASCADE"), primary_key=True
67+
),
6668
)
6769

6870

wadas/domain/operation_mode.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def _detect(self, cur_img):
127127
)
128128
self.last_detection = detected_img_path
129129
# Insert detection event into db, if enabled
130-
if (db := DataBase.get_instance()) and db.enabled:
130+
if db := DataBase.get_enabled_db():
131131
db.insert_into_db(detection_event)
132132
return detection_event
133133
else:
@@ -158,7 +158,7 @@ def _classify(self, detection_event: DetectionEvent):
158158
detection_event.classified_animals = classified_animals
159159
detection_event.classification_img_path = classified_img_path
160160
# Update detection event into db, if enabled
161-
if (db := DataBase.get_instance()) and db.enabled:
161+
if db := DataBase.get_enabled_db():
162162
db.update_detection_event(detection_event)
163163

164164
def ftp_camera_exist(self):
@@ -215,7 +215,7 @@ def actuate(self, detection_event: DetectionEvent):
215215
)
216216
actuator.actuate(actuation_event)
217217
# Insert actuation event into db, if enabled
218-
if (db := DataBase.get_instance()) and db.enabled:
218+
if db := DataBase.get_enabled_db():
219219
db.insert_into_db(actuation_event)
220220

221221
def execution_completed(self):

wadas/ui/actuators_management_dialog.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def __init__(self, camera, parent=None):
4040
self.setWindowTitle(f"Manage Actuators for Camera ID: {camera.id}")
4141
self.camera = camera
4242
self.original_actuators = camera.actuators.copy() # Save original list for cancel action
43-
self.db_enabled = (db := DataBase.get_instance()) and db.enabled
43+
self.db_enabled = bool(DataBase.get_enabled_db())
4444

4545
layout = QGridLayout(self)
4646

wadas/ui/configure_actuators_dialog.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def __init__(self):
8080
self.initialize_dialog()
8181

8282
# DB enablement status
83-
self.db_enabled = (db := DataBase.get_instance()) and db.enabled
83+
self.db_enabled = bool(DataBase.get_enabled_db())
8484

8585
def initialize_dialog(self):
8686
"""Method to initialize dialog with existing values (if any)."""

wadas/ui/configure_db_dialog.py

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@
2121

2222
import keyring
2323
import validators
24+
from PySide6.QtCore import QThread, Signal
2425
from PySide6.QtGui import QIcon
25-
from PySide6.QtWidgets import QDialog, QDialogButtonBox, QMessageBox
26+
from PySide6.QtWidgets import QDialog, QDialogButtonBox, QMessageBox, QStatusBar, QProgressBar
2627

2728
from wadas.domain.database import DataBase
2829
from wadas.ui.error_message_dialog import WADASErrorMessage
@@ -31,6 +32,14 @@
3132

3233
module_dir_path = os.path.dirname(os.path.abspath(__file__))
3334

35+
36+
class SanitizeWorker(QThread):
37+
finished = Signal() # Signal emitted when the task is done
38+
39+
def run(self):
40+
DataBase.sanitize_db()
41+
self.finished.emit()
42+
3443
class ConfigureDBDialog(QDialog, Ui_ConfigureDBDialog):
3544
"""Class to insert DB configuration to enable WADAS for database persistency."""
3645

@@ -57,13 +66,20 @@ def __init__(self, project_uuid):
5766
self.ui.lineEdit_db_password.textChanged.connect(self.validate)
5867
self.ui.checkBox_new_db.clicked.connect(self.on_checkbox_new_db_checked)
5968
self.ui.buttonBox.button(QDialogButtonBox.Cancel).clicked.connect(self.on_cancel_clicked)
69+
self.ui.checkBox_enable_db.clicked.connect(self.on_enable_state_changed)
6070

6171
self.init_dialog()
6272
self.uuid = project_uuid
6373
self.db_created = False
6474
self.initial_wadas_db = DataBase.get_instance()
6575
self.ui.plainTextEdit_db_test.setPlainText("Test out your DB before accepting changes!")
6676

77+
# Indefinite progress bar
78+
self.progress_bar = QProgressBar()
79+
self.progress_bar.setRange(0, 0) # Indeterminate range
80+
self.progress_bar.setVisible(False) # Initially hidden
81+
self.ui.gridLayout_mysql.addWidget(self.progress_bar)
82+
6783
def init_dialog(self):
6884
"""Method to initialize dialog with saved configuration data"""
6985

@@ -419,4 +435,46 @@ def on_cancel_clicked(self):
419435
self.initial_wadas_db.enabled,
420436
self.initial_wadas_db.version,
421437
False
422-
)
438+
)
439+
440+
def on_enable_state_changed(self):
441+
"""Method to sanitize db if existing db is re-enabled."""
442+
443+
if self.ui.checkBox_enable_db.isChecked():
444+
if self.initial_wadas_db and not self.initial_wadas_db.enabled and not self.db_created:
445+
reply = QMessageBox.question(
446+
self,
447+
"Confirm database synchronization",
448+
"Re-Enabling the db will cause the synchronization of WADAS data into db.\n"
449+
"This operation cannot be reverted or interrupted. Are you sure you want to continue?",
450+
QMessageBox.Yes | QMessageBox.No,
451+
QMessageBox.No
452+
)
453+
if reply == QMessageBox.Yes:
454+
self.start_sanitization()
455+
self.show_status_dialog("Database synchronization status",
456+
"Database synchronization complete!",
457+
True)
458+
else:
459+
self.ui.checkBox_enable_db.setChecked(False)
460+
461+
def start_sanitization(self):
462+
"""Method that starts the sanitization process in a separate thread."""
463+
464+
self.progress_bar.setVisible(True)
465+
self.ui.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False)
466+
self.ui.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
467+
self.ui.label_error.setText("Synchronizing database...")
468+
469+
# Create and start the worker thread
470+
self.worker = SanitizeWorker()
471+
self.worker.finished.connect(self.on_sanitization_complete)
472+
self.worker.start()
473+
474+
def on_sanitization_complete(self):
475+
"""Method to complete the sanitization process."""
476+
477+
self.progress_bar.setVisible(False)
478+
self.ui.label_error.setText("")
479+
self.ui.buttonBox.button(QDialogButtonBox.Ok).setEnabled(True)
480+
self.ui.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)

wadas/ui/configure_ftp_cameras_dialog.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def __init__(self):
101101
self._setup_logger()
102102

103103
# DB enablement status
104-
self.db_enabled = (db := DataBase.get_instance()) and db.enabled
104+
self.db_enabled = bool(DataBase.get_enabled_db())
105105

106106
def initialize_dialog(self):
107107
"""Method to initialize dialog with existing values (if any)."""

wadas/ui/configure_whatsapp_dialog.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def initialize_form(self):
5959
"""Method to initialize form with existing WhatsApp configuration data (if any)."""
6060

6161
if Notifier.notifiers[Notifier.NotifierTypes.WHATSAPP.value]:
62-
self.ui.checkBox_enableWhatsAppNotifications.setEnabled(self.whatsapp_notifier.enabled)
62+
self.ui.checkBox_enableWhatsAppNotifications.setChecked(self.whatsapp_notifier.enabled)
6363
self.ui.lineEdit_phoneID.setText(self.whatsapp_notifier.sender_id)
6464
credentials = keyring.get_credential("WADAS_WhatsApp", self.whatsapp_notifier.sender_id)
6565
if credentials and credentials.username == self.whatsapp_notifier.sender_id:

0 commit comments

Comments
 (0)