161 lines
6.6 KiB
Python
161 lines
6.6 KiB
Python
"""
|
|
A widget to handle adding/updating any database object.
|
|
"""
|
|
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
|
|
)
|
|
from sqlalchemy import String, TIMESTAMP, INTEGER, FLOAT
|
|
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, manager: str = ""):
|
|
super().__init__(parent)
|
|
self.instance = instance
|
|
self.object_type = instance.__class__
|
|
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)
|
|
fields = {key: dict(class_attr=getattr(self.object_type, key), instance_attr=getattr(self.instance, key))
|
|
for key in dir(self.object_type) if isinstance(getattr(self.object_type, key), InstrumentedAttribute)
|
|
and "id" not in key and key != manager}
|
|
# 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__}")
|
|
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(parsed)
|
|
model = self.object_type.pydantic_model
|
|
# 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.layout.addWidget(self.label, 0, 0, 1, 1)
|
|
self.setObjectName(key)
|
|
match column_type['class_attr'].property:
|
|
case ColumnProperty():
|
|
self.column_property_set(column_type, value=value)
|
|
case _RelationshipDeclared():
|
|
self.relationship_property_set(column_type, value=value)
|
|
case _:
|
|
logger.error(f"{column_type} not a supported type.")
|
|
return
|
|
self.layout.addWidget(self.widget, 0, 1, 1, 3)
|
|
self.setLayout(self.layout)
|
|
|
|
def relationship_property_set(self, relationship_property, value=None):
|
|
self.property_class = relationship_property['class_attr'].property.entity.class_
|
|
self.is_list = relationship_property['class_attr'].property.uselist
|
|
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 = QComboBox()
|
|
self.widget.addItems(choices)
|
|
|
|
def column_property_set(self, column_property, value=None):
|
|
match column_property['class_attr'].expression.type:
|
|
case String():
|
|
if not value:
|
|
value = ""
|
|
self.widget = QLineEdit(self)
|
|
self.widget.setText(value)
|
|
case INTEGER():
|
|
if not value:
|
|
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 _:
|
|
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()
|
|
# if self.is_list:
|
|
# value = [self.property_class.query(name=prelim)]
|
|
# else:
|
|
# value = self.property_class.query(name=prelim)
|
|
case _:
|
|
value = None
|
|
return self.objectName(), value
|