Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 124 additions & 14 deletions wadas/domain/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,13 @@ def get_instance(cls):
return None
return DataBase.wadas_db

@classmethod
def get_enabled_db(cls):
"""Method that returns the db instance if enabled"""

db = DataBase.get_instance()
return db if db and db.enabled else None

@classmethod
def destroy_instance(cls):
"""Destroy the current database instance and release resources."""
Expand Down Expand Up @@ -380,17 +387,23 @@ def update_detection_event(cls, detection_event: DetectionEvent):

@classmethod
def update_camera(cls, camera, delete_camera=False):
"""Method to reflect camera fields update in db."""
"""Method to reflect camera fields update in db given a camera object"""

logger.debug("Updating camera db entry...")
camera_db_id = cls.get_camera_id(camera.id)
enabled = camera.enabled

if not camera_db_id:
if not cls.update_camera_by_db_id(
cls.get_camera_id(camera.id), camera.enabled, delete_camera
):
logger.error(
"Unable to update camera. Camera ID %s not found or already deleted.", camera.id
)
return

@classmethod
def update_camera_by_db_id(cls, camera_db_id, enabled, delete_camera=False):
"""Method to reflect camera fields update in db given a camera id"""

logger.debug("Updating camera db entry...")

if not camera_db_id:
return False

if delete_camera:
deletion_date_time = get_precise_timestamp()
Expand All @@ -408,21 +421,28 @@ def update_camera(cls, camera, delete_camera=False):
else:
stmt = update(ORMCamera).where(ORMCamera.db_id == camera_db_id).values(enabled=enabled)
cls.run_query(stmt)
return True

@classmethod
def update_actuator(cls, actuator, delete_actuator=False):
"""Method to reflect actuator fields update in db."""

logger.debug("Updating actuator db entry...")
actuator_db_id = cls.get_actuator_id(actuator.id)
enabled = actuator.enabled
"""Method to reflect actuator fields update in db given an actuator object"""

if not actuator_db_id:
if not cls.update_actuator_by_db_id(
cls.get_actuator_id(actuator.id), actuator.enabled, delete_actuator
):
logger.error(
"Unable to update actuator. Actuator ID %s not found or already deleted.",
actuator.id,
)
return

@classmethod
def update_actuator_by_db_id(cls, actuator_db_id, enabled, delete_actuator=False):
"""Method to reflect actuator fields update in db given an actuator id."""

logger.debug("Updating actuator db entry...")

if not actuator_db_id:
return False

if delete_actuator:
deletion_date_time = get_precise_timestamp()
Expand All @@ -444,6 +464,7 @@ def update_actuator(cls, actuator, delete_actuator=False):
.values(enabled=enabled)
)
cls.run_query(stmt)
return True

@classmethod
def add_actuator_to_camera(cls, camera, actuator):
Expand Down Expand Up @@ -687,6 +708,95 @@ def populate_db(cls, uuid):
for camera in cameras:
cls.insert_into_db(camera)

@classmethod
def sanitize_db(cls):
"""Method to align db tables with domain model"""

if session := cls.create_session():
# Check if actuators in model are reflected into db
for actuator_id in Actuator.actuators:
cur_actuator = Actuator.actuators[actuator_id]
if actuator_db_id := cls.get_actuator_id(actuator_id):
# Check actuator attributes type, enabled
db_actuator = (
session.query(ORMActuator).filter(ORMActuator.db_id == actuator_db_id).one()
)
if db_actuator.type != cur_actuator.type:
# If type does not match, set to deleted the one in db
cls.update_actuator_by_db_id(actuator_db_id, db_actuator.enabled, True)
# Insert new actuator into db
cls.insert_into_db(cur_actuator)
if db_actuator.enabled != cur_actuator.enabled:
cls.update_actuator(cur_actuator, False)
else:
cls.insert_into_db(cur_actuator)

# Check if cameras in model are reflected into db
for camera in cameras:
if camera_db_id := cls.get_camera_id(camera.id):
db_camera = (
session.query(ORMCamera).filter(ORMCamera.db_id == camera_db_id).one()
)
if camera.type != db_camera.type:
cls.update_camera_by_db_id(camera_db_id, db_camera.enabled, True)
cls.insert_into_db(camera)
if camera.enabled != db_camera.enabled:
cls.update_camera(camera, False)
if camera.actuators != db_camera.actuators:
# Delete all associations for camera
stmt = delete(camera_actuator_association).where(
camera_actuator_association.c.camera_id == camera_db_id
)
cls.run_query(stmt)
# Pristine associations for camera
for actuator in camera.actuators:
actuator_db_id = cls.get_actuator_id(actuator.id)
stmt = camera_actuator_association.insert().values(
camera_id=camera_db_id, actuator_id=actuator_db_id
)
cls.run_query(stmt)
else:
cls.insert_into_db(camera)

# Check if actuators in db match the ones in domain
actuator_ids = session.query(ORMActuator.actuator_id).all()
actuator_ids_from_db = {actuator_id[0] for actuator_id in actuator_ids}
db_extra_actuators_ids = actuator_ids_from_db - Actuator.actuators.keys()
for extra_actuator_id in db_extra_actuators_ids:
deletion_date_time = get_precise_timestamp()
actuator_db_id = cls.get_actuator_id(extra_actuator_id)
stmt = (
update(ORMActuator)
.where(ORMActuator.db_id == actuator_db_id)
.values(deletion_date=deletion_date_time)
)
cls.run_query(stmt)
# Delete camera association with actuators, if any
stmt = delete(camera_actuator_association).where(
camera_actuator_association.c.actuator_id == extra_actuator_id
)
cls.run_query(stmt)

# Check if cameras in db match the ones in domain
camera_ids = session.query(ORMCamera.camera_id).all()
camera_ids_from_db = {camera_id[0] for camera_id in camera_ids}
camera_id_from_domain = {camera.id for camera in cameras}
db_extra_camera_ids = camera_ids_from_db - camera_id_from_domain
for extra_camera_id in db_extra_camera_ids:
deletion_date_time = get_precise_timestamp()
camera_db_id = cls.get_camera_id(extra_camera_id)
stmt = (
update(ORMCamera)
.where(ORMCamera.db_id == camera_db_id)
.values(deletion_date=deletion_date_time)
)
cls.run_query(stmt)
# Delete camera association with actuators, if any
stmt = delete(camera_actuator_association).where(
camera_actuator_association.c.camera_id == extra_camera_id
)
cls.run_query(stmt)

@abstractmethod
def get_connection_string(self):
"""Generate the connection string based on the database type."""
Expand Down
6 changes: 4 additions & 2 deletions wadas/domain/db_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,10 @@ def compile_datetime6_mariadb(element, compiler, **kwargs):
camera_actuator_association = Table(
"camera_actuator_association",
Base.metadata,
Column("camera_id", Integer, ForeignKey("cameras.id"), primary_key=True),
Column("actuator_id", Integer, ForeignKey("actuators.id"), primary_key=True),
Column("camera_id", Integer, ForeignKey("cameras.id", ondelete="CASCADE"), primary_key=True),
Column(
"actuator_id", Integer, ForeignKey("actuators.id", ondelete="CASCADE"), primary_key=True
),
)


Expand Down
6 changes: 3 additions & 3 deletions wadas/domain/operation_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def _detect(self, cur_img):
)
self.last_detection = detected_img_path
# Insert detection event into db, if enabled
if (db := DataBase.get_instance()) and db.enabled:
if db := DataBase.get_enabled_db():
db.insert_into_db(detection_event)
return detection_event
else:
Expand Down Expand Up @@ -158,7 +158,7 @@ def _classify(self, detection_event: DetectionEvent):
detection_event.classified_animals = classified_animals
detection_event.classification_img_path = classified_img_path
# Update detection event into db, if enabled
if (db := DataBase.get_instance()) and db.enabled:
if db := DataBase.get_enabled_db():
db.update_detection_event(detection_event)

def ftp_camera_exist(self):
Expand Down Expand Up @@ -215,7 +215,7 @@ def actuate(self, detection_event: DetectionEvent):
)
actuator.actuate(actuation_event)
# Insert actuation event into db, if enabled
if (db := DataBase.get_instance()) and db.enabled:
if db := DataBase.get_enabled_db():
db.insert_into_db(actuation_event)

def execution_completed(self):
Expand Down
2 changes: 1 addition & 1 deletion wadas/ui/actuators_management_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def __init__(self, camera, parent=None):
self.setWindowTitle(f"Manage Actuators for Camera ID: {camera.id}")
self.camera = camera
self.original_actuators = camera.actuators.copy() # Save original list for cancel action
self.db_enabled = (db := DataBase.get_instance()) and db.enabled
self.db_enabled = bool(DataBase.get_enabled_db())

layout = QGridLayout(self)

Expand Down
2 changes: 1 addition & 1 deletion wadas/ui/configure_actuators_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def __init__(self):
self.initialize_dialog()

# DB enablement status
self.db_enabled = (db := DataBase.get_instance()) and db.enabled
self.db_enabled = bool(DataBase.get_enabled_db())

def initialize_dialog(self):
"""Method to initialize dialog with existing values (if any)."""
Expand Down
62 changes: 60 additions & 2 deletions wadas/ui/configure_db_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@

import keyring
import validators
from PySide6.QtCore import QThread, Signal
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import QDialog, QDialogButtonBox, QMessageBox
from PySide6.QtWidgets import QDialog, QDialogButtonBox, QMessageBox, QStatusBar, QProgressBar

from wadas.domain.database import DataBase
from wadas.ui.error_message_dialog import WADASErrorMessage
Expand All @@ -31,6 +32,14 @@

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


class SanitizeWorker(QThread):
finished = Signal() # Signal emitted when the task is done

def run(self):
DataBase.sanitize_db()
self.finished.emit()

class ConfigureDBDialog(QDialog, Ui_ConfigureDBDialog):
"""Class to insert DB configuration to enable WADAS for database persistency."""

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

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

# Indefinite progress bar
self.progress_bar = QProgressBar()
self.progress_bar.setRange(0, 0) # Indeterminate range
self.progress_bar.setVisible(False) # Initially hidden
self.ui.gridLayout_mysql.addWidget(self.progress_bar)

def init_dialog(self):
"""Method to initialize dialog with saved configuration data"""

Expand Down Expand Up @@ -419,4 +435,46 @@ def on_cancel_clicked(self):
self.initial_wadas_db.enabled,
self.initial_wadas_db.version,
False
)
)

def on_enable_state_changed(self):
"""Method to sanitize db if existing db is re-enabled."""

if self.ui.checkBox_enable_db.isChecked():
if self.initial_wadas_db and not self.initial_wadas_db.enabled and not self.db_created:
reply = QMessageBox.question(
self,
"Confirm database synchronization",
"Re-Enabling the db will cause the synchronization of WADAS data into db.\n"
"This operation cannot be reverted or interrupted. Are you sure you want to continue?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.Yes:
self.start_sanitization()
self.show_status_dialog("Database synchronization status",
"Database synchronization complete!",
True)
else:
self.ui.checkBox_enable_db.setChecked(False)

def start_sanitization(self):
"""Method that starts the sanitization process in a separate thread."""

self.progress_bar.setVisible(True)
self.ui.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False)
self.ui.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
self.ui.label_error.setText("Synchronizing database...")

# Create and start the worker thread
self.worker = SanitizeWorker()
self.worker.finished.connect(self.on_sanitization_complete)
self.worker.start()

def on_sanitization_complete(self):
"""Method to complete the sanitization process."""

self.progress_bar.setVisible(False)
self.ui.label_error.setText("")
self.ui.buttonBox.button(QDialogButtonBox.Ok).setEnabled(True)
self.ui.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
2 changes: 1 addition & 1 deletion wadas/ui/configure_ftp_cameras_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def __init__(self):
self._setup_logger()

# DB enablement status
self.db_enabled = (db := DataBase.get_instance()) and db.enabled
self.db_enabled = bool(DataBase.get_enabled_db())

def initialize_dialog(self):
"""Method to initialize dialog with existing values (if any)."""
Expand Down
Loading