Files
Submissions-App/src/submissions/frontend/widgets/omni_add_edit.py
2025-03-26 14:19:13 -05:00

203 lines
8.2 KiB
Python

"""
A widget to handle adding/updating any database object.
"""
from copy import deepcopy
from datetime import date
from pprint import pformat
from typing import Any, Tuple
from pydantic import BaseModel
from PyQt6.QtWidgets import (
QLabel, QDialog, QWidget, QLineEdit, QGridLayout, QComboBox, QDialogButtonBox, QDateEdit, QSpinBox, QDoubleSpinBox,
QCheckBox
)
from sqlalchemy import String, TIMESTAMP, INTEGER, FLOAT, JSON, BLOB
from sqlalchemy.orm import InstrumentedAttribute, ColumnProperty
import logging
from sqlalchemy.orm.relationships import _RelationshipDeclared
from tools import Report, report_result
logger = logging.getLogger(f"submissions.{__name__}")
class AddEdit(QDialog):
def __init__(self, parent, instance: Any | None = None, managers: set = set()):
super().__init__(parent)
logger.debug(f"Managers: {managers}")
self.instance = instance
self.object_type = instance.__class__
# self.managers = deepcopy(managers)
self.managers = managers
# if instance.level < 2:
# try:
# logger.debug(f"Parent instance: {self.parent().instance}")
# self.managers.add(self.parent().instance)
# except AttributeError:
# pass
logger.debug(f"Managers: {managers}")
self.layout = QGridLayout(self)
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
# logger.debug(f"Fields: {pformat(self.instance.omnigui_instance_dict)}")
fields = {k: v for k, v in self.instance.omnigui_instance_dict.items() if "id" not in k}
# NOTE: Move 'name' to the front
try:
fields = {'name': fields.pop('name'), **fields}
except KeyError:
pass
height_counter = 0
for key, field in fields.items():
try:
value = getattr(self.instance, key)
except AttributeError:
value = None
try:
logger.debug(f"{key} property: {type(field['class_attr'].property)}")
widget = EditProperty(self, key=key, column_type=field, value=value)
except AttributeError as e:
logger.error(f"Problem setting widget {key}: {e}")
continue
if widget:
self.layout.addWidget(widget, self.layout.rowCount(), 0)
height_counter += 1
self.layout.addWidget(self.buttonBox)
self.setWindowTitle(f"Add/Edit {self.object_type.__name__} - Manager: {self.managers}")
self.setMinimumSize(600, 50 * height_counter)
self.setLayout(self.layout)
@report_result
def parse_form(self) -> Tuple[BaseModel, Report]:
report = Report()
parsed = {result[0].strip(":"): result[1] for result in
[item.parse_form() for item in self.findChildren(EditProperty)] if result[0]}
logger.debug(f"Parsed form: {parsed}")
model = self.object_type.pydantic_model
logger.debug(f"Model type: {model.__name__}")
if model.__name__ == "PydElastic":
logger.debug(f"We have an elastic model.")
parsed['instance'] = self.instance
# NOTE: Hand-off to pydantic model for validation.
# NOTE: Also, why am I not just using the toSQL method here. I could write one for contacts.
model = model(**parsed)
return model, report
class EditProperty(QWidget):
def __init__(self, parent: AddEdit, key: str, column_type: Any, value):
super().__init__(parent)
self.name = key
self.label = QLabel(key.title().replace("_", " "))
self.layout = QGridLayout()
self.setObjectName(key)
try:
self.property_class = column_type['class_attr'].property.entity.class_
except AttributeError:
self.property_class = None
try:
self.is_list = column_type['class_attr'].property.uselist
except AttributeError:
self.is_list = False
match column_type['class_attr'].property:
case ColumnProperty():
self.column_property_set(column_type, value=value)
case _RelationshipDeclared():
if not self.property_class.skip_on_edit:
self.relationship_property_set(column_type, value=value)
else:
return
case _:
logger.error(f"{column_type} not a supported type.")
return
self.layout.addWidget(self.label, 0, 0, 1, 1)
self.layout.addWidget(self.widget, 0, 1, 1, 3)
self.setLayout(self.layout)
def relationship_property_set(self, relationship, value=None):
self.widget = QComboBox()
# logger.debug(self.parent().managers)
for manager in self.parent().managers:
if self.name in manager.aliases:
logger.debug(f"Name: {self.name} is in aliases: {manager.aliases}")
choices = [manager.name]
self.widget.setEnabled(False)
break
else:
choices = [""] + [item.name for item in self.property_class.query()]
try:
instance_value = getattr(self.parent().instance, self.objectName())
except AttributeError:
logger.error(f"Unable to get instance {self.parent().instance} attribute: {self.objectName()}")
instance_value = None
# NOTE: get the value for the current instance and move it to the front.
if isinstance(instance_value, list):
instance_value = next((item.name for item in instance_value), None)
if instance_value:
choices.insert(0, choices.pop(choices.index(instance_value)))
self.widget.addItems(choices)
def column_property_set(self, column_property, value=None):
logger.debug(f"Column Property: {column_property['class_attr'].expression} {column_property}, Value: {value}")
match column_property['class_attr'].expression.type:
case String():
if value is None:
value = ""
self.widget = QLineEdit(self)
self.widget.setText(value)
case INTEGER():
if isinstance(column_property['instance_attr'], bool):
self.widget = QCheckBox()
self.widget.setChecked(value)
else:
if value is None:
value = 1
self.widget = QSpinBox()
self.widget.setValue(value)
case FLOAT():
if not value:
value = 1.0
self.widget = QDoubleSpinBox()
self.widget.setValue(value)
case TIMESTAMP():
self.widget = QDateEdit(self, calendarPopup=True)
if not value:
value = date.today()
self.widget.setDate(value)
case JSON():
self.widget = QLabel("JSON Under construction")
case BLOB():
self.widget = QLabel("BLOB Under construction")
case _:
logger.error(f"{column_property} not a supported property.")
self.widget = None
try:
tooltip_text = self.parent().object_type.add_edit_tooltips[self.objectName()]
self.widget.setToolTip(tooltip_text)
except KeyError:
pass
def parse_form(self):
# NOTE: Make sure there's a widget.
try:
check = self.widget
except AttributeError:
return None, None
match self.widget:
case QLineEdit():
value = self.widget.text()
case QDateEdit():
value = self.widget.date().toPyDate()
case QComboBox():
value = self.widget.currentText()
case QSpinBox() | QDoubleSpinBox():
value = self.widget.value()
case QCheckBox():
value = self.widget.isChecked()
case _:
value = None
return self.objectName(), value