Prior to updating queries to use query alias.

This commit is contained in:
lwark
2025-01-27 08:55:34 -06:00
parent 960fbdb85c
commit 73752cde87
10 changed files with 351 additions and 87 deletions

View File

@@ -6,13 +6,12 @@ import sys, logging
from pandas import DataFrame from pandas import DataFrame
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import Column, INTEGER, String, JSON from sqlalchemy import Column, INTEGER, String, JSON
from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query, Session from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query, Session, InstrumentedAttribute
from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.exc import ArgumentError from sqlalchemy.exc import ArgumentError
from typing import Any, List from typing import Any, List
from pathlib import Path from pathlib import Path
from tools import report_result from tools import report_result, list_sort_dict
# NOTE: Load testing environment # NOTE: Load testing environment
if 'pytest' in sys.modules: if 'pytest' in sys.modules:
@@ -49,6 +48,30 @@ class BaseClass(Base):
__table_args__ = {'extend_existing': True} #: Will only add new columns __table_args__ = {'extend_existing': True} #: Will only add new columns
singles = ['id'] singles = ['id']
omni_removes = ['submissions']
omni_sort = ["name"]
@classproperty
def skip_on_edit(cls):
if "association" in cls.__name__.lower() or cls.__name__.lower() == "discount":
return True
else:
return False
@classproperty
def aliases(cls):
return [cls.__name__.lower()]
@classproperty
def level(cls):
if "association" in cls.__name__.lower() or cls.__name__.lower() == "discount":
return 2
else:
return 1
@classproperty
def query_alias(cls):
return cls.__name__.lower()
@classmethod @classmethod
@declared_attr @declared_attr
@@ -175,7 +198,7 @@ class BaseClass(Base):
try: try:
records = [obj.to_sub_dict(**kwargs) for obj in objects] records = [obj.to_sub_dict(**kwargs) for obj in objects]
except AttributeError: except AttributeError:
records = [obj.omnigui_dict for obj in objects] records = [{k:v['instance_attr'] for k, v in obj.to_omnigui_dict(**kwargs).items()} for obj in objects]
return DataFrame.from_records(records) return DataFrame.from_records(records)
@classmethod @classmethod
@@ -249,11 +272,25 @@ class BaseClass(Base):
Returns: Returns:
dict: Dictionary of object minus _sa_instance_state with id at the front. dict: Dictionary of object minus _sa_instance_state with id at the front.
""" """
dicto = {k: v for k, v in self.__dict__.items() if k not in ["_sa_instance_state"]} # dicto = {k: v for k, v in self.__dict__.items() if k not in ["_sa_instance_state"]}
dicto = {key: dict(class_attr=getattr(self.__class__, key), instance_attr=getattr(self, key))
for key in dir(self.__class__) if
isinstance(getattr(self.__class__, key), InstrumentedAttribute) and key not in self.omni_removes
}
for k, v in dicto.items():
try:
v['instance_attr'] = v['instance_attr'].name
except AttributeError:
continue
try:
dicto = list_sort_dict(input_dict=dicto, sort_list=self.__class__.omni_sort)
except TypeError as e:
logger.error(f"Could not sort {self.__class__.__name__} by list due to :{e}")
try: try:
dicto = {'id': dicto.pop('id'), **dicto} dicto = {'id': dicto.pop('id'), **dicto}
except KeyError: except KeyError:
pass pass
# logger.debug(f"{self.__class__.__name__} omnigui dict:\n\n{pformat(dicto)}")
return dicto return dicto
@classproperty @classproperty
@@ -268,6 +305,7 @@ class BaseClass(Base):
try: try:
model = getattr(pydant, f"Pyd{cls.__name__}") model = getattr(pydant, f"Pyd{cls.__name__}")
except AttributeError: except AttributeError:
logger.warning(f"Couldn't get {cls.__name__} pydantic model.")
return None return None
return model return model
@@ -281,6 +319,13 @@ class BaseClass(Base):
""" """
return dict() return dict()
@classmethod
def relevant_relationships(cls, relationship_instance):
query_kwargs = {relationship_instance.query_alias:relationship_instance}
return cls.query(**query_kwargs)
class ConfigItem(BaseClass): class ConfigItem(BaseClass):
""" """

View File

@@ -7,6 +7,7 @@ from pprint import pformat
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB
from sqlalchemy.orm import relationship, validates, Query from sqlalchemy.orm import relationship, validates, Query
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.hybrid import hybrid_property
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from tools import check_authorization, setup_lookup, Report, Result, check_regex_match, yaml_regex_creator, timezone from tools import check_authorization, setup_lookup, Report, Result, check_regex_match, yaml_regex_creator, timezone
from typing import List, Literal, Generator, Any, Tuple from typing import List, Literal, Generator, Any, Tuple
@@ -95,6 +96,8 @@ class KitType(BaseClass):
Base of kits used in submission processing Base of kits used in submission processing
""" """
query_alias = "kit_type"
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64), unique=True) #: name of kit name = Column(String(64), unique=True) #: name of kit
submissions = relationship("BasicSubmission", back_populates="extraction_kit") #: submissions this kit was used for submissions = relationship("BasicSubmission", back_populates="extraction_kit") #: submissions this kit was used for
@@ -129,6 +132,10 @@ class KitType(BaseClass):
""" """
return f"<KitType({self.name})>" return f"<KitType({self.name})>"
@classproperty
def aliases(cls):
return super().aliases + [cls.query_alias, "kit_types"]
def get_reagents(self, def get_reagents(self,
required: bool = False, required: bool = False,
submission_type: str | SubmissionType | None = None submission_type: str | SubmissionType | None = None
@@ -402,12 +409,14 @@ class ReagentRole(BaseClass):
name: str | None = None, name: str | None = None,
kit_type: KitType | str | None = None, kit_type: KitType | str | None = None,
reagent: Reagent | str | None = None, reagent: Reagent | str | None = None,
id: int | None = None,
limit: int = 0, limit: int = 0,
) -> ReagentRole | List[ReagentRole]: ) -> ReagentRole | List[ReagentRole]:
""" """
Lookup reagent types in the database. Lookup reagent types in the database.
Args: Args:
id (id | None, optional): Id of the object. Defaults to None.
name (str | None, optional): Reagent type name. Defaults to None. name (str | None, optional): Reagent type name. Defaults to None.
kit_type (KitType | str | None, optional): Kit the type of interest belongs to. Defaults to None. kit_type (KitType | str | None, optional): Kit the type of interest belongs to. Defaults to None.
reagent (Reagent | str | None, optional): Concrete instance of the type of interest. Defaults to None. reagent (Reagent | str | None, optional): Concrete instance of the type of interest. Defaults to None.
@@ -445,6 +454,12 @@ class ReagentRole(BaseClass):
limit = 1 limit = 1
case _: case _:
pass pass
match id:
case int():
query = query.filter(cls.id == id)
limit = 1
case _:
pass
return cls.execute_query(query=query, limit=limit) return cls.execute_query(query=query, limit=limit)
def to_pydantic(self) -> "PydReagent": def to_pydantic(self) -> "PydReagent":
@@ -476,7 +491,7 @@ class Reagent(BaseClass, LogMixin):
Concrete reagent instance Concrete reagent instance
""" """
searchables = [dict(label="Lot", field="lot")]
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
role = relationship("ReagentRole", back_populates="instances", role = relationship("ReagentRole", back_populates="instances",
@@ -504,6 +519,10 @@ class Reagent(BaseClass, LogMixin):
name = f"<Reagent({self.role.name}-{self.lot})>" name = f"<Reagent({self.role.name}-{self.lot})>"
return name return name
@classproperty
def searchables(cls):
return [dict(label="Lot", field="lot")]
def to_sub_dict(self, extraction_kit: KitType = None, full_data: bool = False, **kwargs) -> dict: def to_sub_dict(self, extraction_kit: KitType = None, full_data: bool = False, **kwargs) -> dict:
""" """
dictionary containing values necessary for gui dictionary containing values necessary for gui
@@ -700,6 +719,8 @@ class Discount(BaseClass):
Relationship table for client labs for certain kits. Relationship table for client labs for certain kits.
""" """
skip_on_edit = True
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
kit = relationship("KitType") #: joined parent reagent type kit = relationship("KitType") #: joined parent reagent type
kit_id = Column(INTEGER, ForeignKey("_kittype.id", ondelete='SET NULL', name="fk_kit_type_id")) #: id of joined kit kit_id = Column(INTEGER, ForeignKey("_kittype.id", ondelete='SET NULL', name="fk_kit_type_id")) #: id of joined kit
@@ -812,6 +833,14 @@ class SubmissionType(BaseClass):
""" """
return f"<SubmissionType({self.name})>" return f"<SubmissionType({self.name})>"
@classproperty
def aliases(cls):
return super().aliases + ["submission_types", "submission_type"]
@classproperty
def omni_removes(cls):
return super().omni_removes + ["template_file", "defaults", "instances"]
@classproperty @classproperty
def basic_template(cls) -> bytes: def basic_template(cls) -> bytes:
""" """
@@ -1063,6 +1092,10 @@ class SubmissionTypeKitTypeAssociation(BaseClass):
Abstract of relationship between kits and their submission type. Abstract of relationship between kits and their submission type.
""" """
omni_removes = BaseClass.omni_removes + ["submission_types_id", "kits_id"]
omni_sort = ["submission_type", "kit_type"]
level = 2
submission_types_id = Column(INTEGER, ForeignKey("_submissiontype.id"), submission_types_id = Column(INTEGER, ForeignKey("_submissiontype.id"),
primary_key=True) #: id of joined submission type primary_key=True) #: id of joined submission type
kits_id = Column(INTEGER, ForeignKey("_kittype.id"), primary_key=True) #: id of joined kit kits_id = Column(INTEGER, ForeignKey("_kittype.id"), primary_key=True) #: id of joined kit
@@ -1093,12 +1126,17 @@ class SubmissionTypeKitTypeAssociation(BaseClass):
""" """
return f"<SubmissionTypeKitTypeAssociation({self.submission_type.name}&{self.kit_type.name})>" return f"<SubmissionTypeKitTypeAssociation({self.submission_type.name}&{self.kit_type.name})>"
@property
def name(self):
return f"{self.submission_type.name} -> {self.kit_type.name}"
@classmethod @classmethod
@setup_lookup @setup_lookup
def query(cls, def query(cls,
submission_type: SubmissionType | str | int | None = None, submission_type: SubmissionType | str | int | None = None,
kit_type: KitType | str | int | None = None, kit_type: KitType | str | int | None = None,
limit: int = 0 limit: int = 0,
**kwargs
) -> SubmissionTypeKitTypeAssociation | List[SubmissionTypeKitTypeAssociation]: ) -> SubmissionTypeKitTypeAssociation | List[SubmissionTypeKitTypeAssociation]:
""" """
Lookup SubmissionTypeKitTypeAssociations of interest. Lookup SubmissionTypeKitTypeAssociations of interest.
@@ -1126,7 +1164,9 @@ class SubmissionTypeKitTypeAssociation(BaseClass):
query = query.join(KitType).filter(KitType.name == kit_type) query = query.join(KitType).filter(KitType.name == kit_type)
case int(): case int():
query = query.join(KitType).filter(KitType.id == kit_type) query = query.join(KitType).filter(KitType.id == kit_type)
limit = query.count() if kit_type is not None and submission_type is not None:
limit = 1
# limit = query.count()
return cls.execute_query(query=query, limit=limit) return cls.execute_query(query=query, limit=limit)
def to_export_dict(self): def to_export_dict(self):
@@ -1148,6 +1188,9 @@ class KitTypeReagentRoleAssociation(BaseClass):
DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
""" """
omni_removes = BaseClass.omni_removes + ["submission_type_id", "kits_id", "reagent_roles_id", "last_used"]
omni_sort = ["submission_type", "kit_type", "reagent_role", "required", "uses"]
reagent_roles_id = Column(INTEGER, ForeignKey("_reagentrole.id"), reagent_roles_id = Column(INTEGER, ForeignKey("_reagentrole.id"),
primary_key=True) #: id of associated reagent type primary_key=True) #: id of associated reagent type
kits_id = Column(INTEGER, ForeignKey("_kittype.id"), primary_key=True) #: id of associated reagent type kits_id = Column(INTEGER, ForeignKey("_kittype.id"), primary_key=True) #: id of associated reagent type
@@ -1176,6 +1219,10 @@ class KitTypeReagentRoleAssociation(BaseClass):
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<KitTypeReagentRoleAssociation({self.kit_type} & {self.reagent_role})>" return f"<KitTypeReagentRoleAssociation({self.kit_type} & {self.reagent_role})>"
@property
def name(self):
return f"{self.kit_type.name} -> {self.reagent_role.name}"
@validates('required') @validates('required')
def validate_required(self, key, value): def validate_required(self, key, value):
""" """
@@ -1219,7 +1266,8 @@ class KitTypeReagentRoleAssociation(BaseClass):
def query(cls, def query(cls,
kit_type: KitType | str | None = None, kit_type: KitType | str | None = None,
reagent_role: ReagentRole | str | None = None, reagent_role: ReagentRole | str | None = None,
limit: int = 0 limit: int = 0,
**kwargs
) -> KitTypeReagentRoleAssociation | List[KitTypeReagentRoleAssociation]: ) -> KitTypeReagentRoleAssociation | List[KitTypeReagentRoleAssociation]:
""" """
Lookup junction of ReagentType and KitType Lookup junction of ReagentType and KitType
@@ -1280,6 +1328,12 @@ class KitTypeReagentRoleAssociation(BaseClass):
for rel_reagent in relevant_reagents: for rel_reagent in relevant_reagents:
yield rel_reagent yield rel_reagent
@property
def omnigui_dict(self) -> dict:
dicto = super().omnigui_dict
dicto['required']['instance_attr'] = bool(dicto['required']['instance_attr'])
return dicto
class SubmissionReagentAssociation(BaseClass): class SubmissionReagentAssociation(BaseClass):
""" """
@@ -1498,7 +1552,8 @@ class Equipment(BaseClass, LogMixin):
PydEquipment: pydantic equipment object PydEquipment: pydantic equipment object
""" """
from backend.validators.pydant import PydEquipment from backend.validators.pydant import PydEquipment
processes = self.get_processes(submission_type=submission_type, extraction_kit=extraction_kit, equipment_role=role) processes = self.get_processes(submission_type=submission_type, extraction_kit=extraction_kit,
equipment_role=role)
return PydEquipment(processes=processes, role=role, return PydEquipment(processes=processes, role=role,
**self.to_dict(processes=False)) **self.to_dict(processes=False))
@@ -1718,7 +1773,8 @@ class SubmissionEquipmentAssociation(BaseClass):
@classmethod @classmethod
@setup_lookup @setup_lookup
def query(cls, equipment_id: int|None=None, submission_id: int|None=None, role: str | None = None, limit: int = 0, **kwargs) \ def query(cls, equipment_id: int | None = None, submission_id: int | None = None, role: str | None = None,
limit: int = 0, **kwargs) \
-> Any | List[Any]: -> Any | List[Any]:
query: Query = cls.__database_session__.query(cls) query: Query = cls.__database_session__.query(cls)
query = query.filter(cls.equipment_id == equipment_id) query = query.filter(cls.equipment_id == equipment_id)
@@ -1821,7 +1877,8 @@ class Process(BaseClass):
submission_type: str | SubmissionType | None = None, submission_type: str | SubmissionType | None = None,
extraction_kit: str | KitType | None = None, extraction_kit: str | KitType | None = None,
equipment_role: str | KitType | None = None, equipment_role: str | KitType | None = None,
limit: int = 0) -> Process | List[Process]: limit: int = 0,
**kwargs) -> Process | List[Process]:
""" """
Lookup Processes Lookup Processes
@@ -1876,6 +1933,8 @@ class Process(BaseClass):
def save(self): def save(self):
super().save() super().save()
# @classmethod
class TipRole(BaseClass): class TipRole(BaseClass):
""" """
@@ -2019,7 +2078,6 @@ class SubmissionTipsAssociation(BaseClass):
instance = SubmissionTipsAssociation(submission=submission, tips=tips, role_name=role) instance = SubmissionTipsAssociation(submission=submission, tips=tips, role_name=role)
return instance return instance
def to_pydantic(self): def to_pydantic(self):
from backend.validators import PydTips from backend.validators import PydTips
return PydTips(name=self.tips.name, lot=self.tips.lot, role=self.role_name) return PydTips(name=self.tips.name, lot=self.tips.lot, role=self.role_name)

View File

@@ -28,6 +28,8 @@ class Organization(BaseClass):
Base of organization Base of organization
""" """
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) #: organization name name = Column(String(64)) #: organization name
submissions = relationship("BasicSubmission", submissions = relationship("BasicSubmission",
@@ -124,8 +126,6 @@ class Contact(BaseClass):
Base of Contact Base of Contact
""" """
searchables = []
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) #: contact name name = Column(String(64)) #: contact name
email = Column(String(64)) #: contact email email = Column(String(64)) #: contact email
@@ -137,6 +137,10 @@ class Contact(BaseClass):
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<Contact({self.name})>" return f"<Contact({self.name})>"
@classproperty
def searchables(cls):
return []
@classmethod @classmethod
@setup_lookup @setup_lookup
def query(cls, def query(cls,

View File

@@ -1960,6 +1960,7 @@ class WastewaterArtic(BasicSubmission):
input_dict['rsl_number'] = cls.en_adapter(input_str=input_dict['submitter_id']) input_dict['rsl_number'] = cls.en_adapter(input_str=input_dict['submitter_id'])
# NOTE: Check for extraction negative control (Robotics) # NOTE: Check for extraction negative control (Robotics)
if re.search(rf"^{year}-(RSL)", input_dict['submitter_id']): if re.search(rf"^{year}-(RSL)", input_dict['submitter_id']):
logger.debug(f"Found {year}-(RSL), so we are going to run PBS adapter:")
input_dict['rsl_number'] = cls.pbs_adapter(input_str=input_dict['submitter_id']) input_dict['rsl_number'] = cls.pbs_adapter(input_str=input_dict['submitter_id'])
return input_dict return input_dict
@@ -2019,7 +2020,9 @@ class WastewaterArtic(BasicSubmission):
""" """
# NOTE: Remove letters. # NOTE: Remove letters.
processed = input_str.replace("RSL", "") processed = input_str.replace("RSL", "")
# NOTE: Remove brackets at end
processed = re.sub(r"\(.*\)$", "", processed).strip() processed = re.sub(r"\(.*\)$", "", processed).strip()
# NOTE: Remove any non-R letters at end.
processed = re.sub(r"[A-QS-Z]+\d*", "", processed) processed = re.sub(r"[A-QS-Z]+\d*", "", processed)
# NOTE: Remove trailing '-' if any # NOTE: Remove trailing '-' if any
processed = processed.strip("-") processed = processed.strip("-")
@@ -2037,6 +2040,8 @@ class WastewaterArtic(BasicSubmission):
if repeat_num is None and "R" in plate_num: if repeat_num is None and "R" in plate_num:
repeat_num = "1" repeat_num = "1"
plate_num = re.sub(r"R", rf"R{repeat_num}", plate_num) plate_num = re.sub(r"R", rf"R{repeat_num}", plate_num)
# NOTE: Remove any redundant -digits
processed = re.sub(r"-\d$", "", processed)
day = re.search(r"\d{2}$", processed).group() day = re.search(r"\d{2}$", processed).group()
processed = rreplace(processed, day, "") processed = rreplace(processed, day, "")
month = re.search(r"\d{2}$", processed).group() month = re.search(r"\d{2}$", processed).group()
@@ -2237,8 +2242,6 @@ class BasicSample(BaseClass, LogMixin):
Base of basic sample which polymorphs into BCSample and WWSample Base of basic sample which polymorphs into BCSample and WWSample
""" """
searchables = [dict(label="Submitter ID", field="submitter_id")]
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
submitter_id = Column(String(64), nullable=False, unique=True) #: identification from submitter submitter_id = Column(String(64), nullable=False, unique=True) #: identification from submitter
sample_type = Column(String(32)) #: mode_sub_type of sample sample_type = Column(String(32)) #: mode_sub_type of sample
@@ -2287,6 +2290,10 @@ class BasicSample(BaseClass, LogMixin):
except AttributeError: except AttributeError:
return f"<Sample({self.submitter_id})" return f"<Sample({self.submitter_id})"
@classproperty
def searchables(cls):
return [dict(label="Submitter ID", field="submitter_id")]
@classproperty @classproperty
def timestamps(cls) -> List[str]: def timestamps(cls) -> List[str]:
""" """
@@ -2657,7 +2664,7 @@ class WastewaterSample(BasicSample):
Returns: Returns:
List[str]: List of fields. List[str]: List of fields.
""" """
searchables = super().searchables searchables = deepcopy(super().searchables)
for item in ["ww_processing_num", "ww_full_sample_id", "rsl_number"]: for item in ["ww_processing_num", "ww_full_sample_id", "rsl_number"]:
label = item.strip("ww_").replace("_", " ").replace("rsl", "RSL").title() label = item.strip("ww_").replace("_", " ").replace("rsl", "RSL").title()
searchables.append(dict(label=label, field=item)) searchables.append(dict(label=label, field=item))

View File

@@ -1205,3 +1205,33 @@ class PydIridaControl(BaseModel, extra='ignore'):
if instance.__getattribute__(key) != field_value: if instance.__getattribute__(key) != field_value:
instance.__setattr__(key, field_value) instance.__setattr__(key, field_value)
return instance return instance
class PydProcess(BaseModel, extra="allow"):
name: str
submission_types: List[str]
equipment: List[str]
equipment_roles: List[str]
kit_types: List[str]
tip_roles: List[str]
@field_validator("submission_types", "equipment", "equipment_roles", "kit_types", "tip_roles", mode="before")
@classmethod
def enforce_list(cls, value):
if not isinstance(value, list):
return [value]
return value
def to_sql(self):
instance = Process.query(name=self.name)
if not instance:
instance = Process()
dicto = instance.omnigui_dict
for key in self.model_fields:
# field_value = self.__getattribute__(key)
# if instance.__getattribute__(key) != field_value:
# instance.__setattr__(key, field_value)
test = dicto[key]
print(f"Attribute: {test['class_attr'].property}")
return instance

View File

@@ -6,31 +6,40 @@ from pprint import pformat
from typing import Any, Tuple from typing import Any, Tuple
from pydantic import BaseModel from pydantic import BaseModel
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QLabel, QDialog, QWidget, QLineEdit, QGridLayout, QComboBox, QDialogButtonBox, QDateEdit, QSpinBox, QDoubleSpinBox QLabel, QDialog, QWidget, QLineEdit, QGridLayout, QComboBox, QDialogButtonBox, QDateEdit, QSpinBox, QDoubleSpinBox,
QCheckBox
) )
from sqlalchemy import String, TIMESTAMP, INTEGER, FLOAT from sqlalchemy import String, TIMESTAMP, INTEGER, FLOAT, JSON, BLOB
from sqlalchemy.orm import InstrumentedAttribute, ColumnProperty from sqlalchemy.orm import InstrumentedAttribute, ColumnProperty
import logging import logging
from sqlalchemy.orm.relationships import _RelationshipDeclared from sqlalchemy.orm.relationships import _RelationshipDeclared
from tools import Report, report_result from tools import Report, report_result
from backend import db
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
class AddEdit(QDialog): class AddEdit(QDialog):
def __init__(self, parent, instance: Any | None = None, manager: str = ""): def __init__(self, parent, instance: Any | None = None, managers: set = set()):
super().__init__(parent) super().__init__(parent)
logger.debug(f"Managers: {managers}")
self.instance = instance self.instance = instance
self.object_type = instance.__class__ self.object_type = instance.__class__
self.managers = managers
if instance.level < 2:
try:
self.managers.add(self.parent().instance)
except AttributeError:
pass
logger.debug(f"Managers: {managers}")
self.layout = QGridLayout(self) self.layout = QGridLayout(self)
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn) self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept) self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject) self.buttonBox.rejected.connect(self.reject)
fields = {key: dict(class_attr=getattr(self.object_type, key), instance_attr=getattr(self.instance, key)) logger.debug(f"Fields: {pformat(self.instance.omnigui_dict)}")
for key in dir(self.object_type) if isinstance(getattr(self.object_type, key), InstrumentedAttribute) fields = {k: v for k, v in self.instance.omnigui_dict.items() if "id" not in k}
and "id" not in key and key != manager}
# NOTE: Move 'name' to the front # NOTE: Move 'name' to the front
try: try:
fields = {'name': fields.pop('name'), **fields} fields = {'name': fields.pop('name'), **fields}
@@ -52,15 +61,17 @@ class AddEdit(QDialog):
self.layout.addWidget(widget, self.layout.rowCount(), 0) self.layout.addWidget(widget, self.layout.rowCount(), 0)
height_counter += 1 height_counter += 1
self.layout.addWidget(self.buttonBox) self.layout.addWidget(self.buttonBox)
self.setWindowTitle(f"Add/Edit {self.object_type.__name__}") self.setWindowTitle(f"Add/Edit {self.object_type.__name__} - Manager: {self.managers}")
self.setMinimumSize(600, 50 * height_counter) self.setMinimumSize(600, 50 * height_counter)
self.setLayout(self.layout) self.setLayout(self.layout)
@report_result @report_result
def parse_form(self) -> Tuple[BaseModel, Report]: def parse_form(self) -> Tuple[BaseModel, Report]:
from backend.validators import pydant
report = Report() report = Report()
parsed = {result[0].strip(":"): result[1] for result in [item.parse_form() for item in self.findChildren(EditProperty)] if result[0]} parsed = {result[0].strip(":"): result[1] for result in
# logger.debug(parsed) [item.parse_form() for item in self.findChildren(EditProperty)] if result[0]}
logger.debug(f"Parsed form: {parsed}")
model = self.object_type.pydantic_model model = self.object_type.pydantic_model
# NOTE: Hand-off to pydantic model for validation. # 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. # NOTE: Also, why am I not just using the toSQL method here. I could write one for contacts.
@@ -75,22 +86,41 @@ class EditProperty(QWidget):
self.name = key self.name = key
self.label = QLabel(key.title().replace("_", " ")) self.label = QLabel(key.title().replace("_", " "))
self.layout = QGridLayout() self.layout = QGridLayout()
self.layout.addWidget(self.label, 0, 0, 1, 1)
self.setObjectName(key) 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: match column_type['class_attr'].property:
case ColumnProperty(): case ColumnProperty():
self.column_property_set(column_type, value=value) self.column_property_set(column_type, value=value)
case _RelationshipDeclared(): case _RelationshipDeclared():
if not self.property_class.skip_on_edit:
self.relationship_property_set(column_type, value=value) self.relationship_property_set(column_type, value=value)
else:
return
case _: case _:
logger.error(f"{column_type} not a supported type.") logger.error(f"{column_type} not a supported type.")
return return
# if not self.is_list:
self.layout.addWidget(self.label, 0, 0, 1, 1)
self.layout.addWidget(self.widget, 0, 1, 1, 3) self.layout.addWidget(self.widget, 0, 1, 1, 3)
self.setLayout(self.layout) self.setLayout(self.layout)
def relationship_property_set(self, relationship_property, value=None): def relationship_property_set(self, relationship, value=None):
self.property_class = relationship_property['class_attr'].property.entity.class_ self.widget = QComboBox()
self.is_list = relationship_property['class_attr'].property.uselist 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()] choices = [""] + [item.name for item in self.property_class.query()]
try: try:
instance_value = getattr(self.parent().instance, self.objectName()) instance_value = getattr(self.parent().instance, self.objectName())
@@ -102,18 +132,22 @@ class EditProperty(QWidget):
instance_value = next((item.name for item in instance_value), None) instance_value = next((item.name for item in instance_value), None)
if instance_value: if instance_value:
choices.insert(0, choices.pop(choices.index(instance_value))) choices.insert(0, choices.pop(choices.index(instance_value)))
self.widget = QComboBox()
self.widget.addItems(choices) self.widget.addItems(choices)
def column_property_set(self, column_property, value=None): 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: match column_property['class_attr'].expression.type:
case String(): case String():
if not value: if value is None:
value = "" value = ""
self.widget = QLineEdit(self) self.widget = QLineEdit(self)
self.widget.setText(value) self.widget.setText(value)
case INTEGER(): case INTEGER():
if not value: if isinstance(column_property['instance_attr'], bool):
self.widget = QCheckBox()
self.widget.setChecked(value)
else:
if value is None:
value = 1 value = 1
self.widget = QSpinBox() self.widget = QSpinBox()
self.widget.setValue(value) self.widget.setValue(value)
@@ -127,6 +161,10 @@ class EditProperty(QWidget):
if not value: if not value:
value = date.today() value = date.today()
self.widget.setDate(value) self.widget.setDate(value)
case JSON():
self.widget = QLabel("JSON Under construction")
case BLOB():
self.widget = QLabel("BLOB Under construction")
case _: case _:
logger.error(f"{column_property} not a supported property.") logger.error(f"{column_property} not a supported property.")
self.widget = None self.widget = None
@@ -151,10 +189,10 @@ class EditProperty(QWidget):
value = self.widget.currentText() value = self.widget.currentText()
case QSpinBox() | QDoubleSpinBox(): case QSpinBox() | QDoubleSpinBox():
value = self.widget.value() value = self.widget.value()
# if self.is_list: case QCheckBox():
# value = [self.property_class.query(name=prelim)] value = self.widget.isChecked()
# else:
# value = self.property_class.query(name=prelim)
case _: case _:
value = None value = None
return self.objectName(), value return self.objectName(), value

View File

@@ -6,9 +6,10 @@ from PyQt6.QtCore import QSortFilterProxyModel, Qt
from PyQt6.QtGui import QAction, QCursor from PyQt6.QtGui import QAction, QCursor
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QLabel, QDialog, QLabel, QDialog,
QTableView, QWidget, QLineEdit, QGridLayout, QComboBox, QPushButton, QDialogButtonBox, QDateEdit, QMenu QTableView, QWidget, QLineEdit, QGridLayout, QComboBox, QPushButton, QDialogButtonBox, QDateEdit, QMenu,
QDoubleSpinBox, QSpinBox, QCheckBox
) )
from sqlalchemy import String, TIMESTAMP from sqlalchemy import String, TIMESTAMP, FLOAT, INTEGER, JSON, BLOB
from sqlalchemy.orm import InstrumentedAttribute from sqlalchemy.orm import InstrumentedAttribute
from sqlalchemy.orm.collections import InstrumentedList from sqlalchemy.orm.collections import InstrumentedList
from sqlalchemy.orm.properties import ColumnProperty from sqlalchemy.orm.properties import ColumnProperty
@@ -28,10 +29,16 @@ class ManagerWindow(QDialog):
Initially this is a window to manage Organization Contacts, but hope to abstract it more later. Initially this is a window to manage Organization Contacts, but hope to abstract it more later.
""" """
def __init__(self, parent, object_type: Any, extras: List[str], **kwargs): def __init__(self, parent, object_type: Any, extras: List[str], managers: set = set(), **kwargs):
super().__init__(parent) super().__init__(parent)
self.object_type = self.original_type = object_type self.object_type = self.original_type = object_type
self.instance = None self.instance = None
self.managers = managers
try:
self.managers.add(self.parent().instance)
except AttributeError:
pass
logger.debug(f"Managers: {managers}")
self.extras = extras self.extras = extras
self.context = kwargs self.context = kwargs
self.layout = QGridLayout(self) self.layout = QGridLayout(self)
@@ -55,7 +62,7 @@ class ManagerWindow(QDialog):
self.options.setObjectName("options") self.options.setObjectName("options")
self.update_options() self.update_options()
self.setLayout(self.layout) self.setLayout(self.layout)
self.setWindowTitle(f"Manage {self.object_type.__name__}") self.setWindowTitle(f"Manage {self.object_type.__name__} - Managers: {self.managers}")
def update_options(self) -> None: def update_options(self) -> None:
""" """
@@ -63,8 +70,15 @@ class ManagerWindow(QDialog):
""" """
if self.sub_class: if self.sub_class:
self.object_type = getattr(db, self.sub_class.currentText()) self.object_type = getattr(db, self.sub_class.currentText())
options = [item.name for item in self.object_type.query()] logger.debug(f"From update options, managers: {self.managers}")
logger.debug(f"self.instance: {self.instance}") try:
query_kwargs = {self.parent().instance.query_alias: self.parent().instance}
except AttributeError as e:
logger.debug(f"Couldn't set query kwargs due to: {e}")
query_kwargs = {}
logger.debug(f"Query kwargs: {query_kwargs}")
options = [item.name for item in self.object_type.query(**query_kwargs)]
logger.debug(f"self.object_type: {self.object_type}")
if self.instance: if self.instance:
options.insert(0, options.pop(options.index(self.instance.name))) options.insert(0, options.pop(options.index(self.instance.name)))
self.options.clear() self.options.clear()
@@ -92,21 +106,24 @@ class ManagerWindow(QDialog):
for item in deletes: for item in deletes:
item.setParent(None) item.setParent(None)
# NOTE: Find the instance this manager will update # NOTE: Find the instance this manager will update
self.instance = self.object_type.query(name=self.options.currentText()) logger.debug(f"Querying with {self.options.currentText()}")
fields = {k: v for k, v in self.object_type.__dict__.items() if self.instance = self.object_type.query(name=self.options.currentText(), limit=1)
isinstance(v, InstrumentedAttribute) and k != "id"} logger.debug(f"Instance: {self.instance}")
fields = {k: v for k, v in self.instance.omnigui_dict.items() if
isinstance(v['class_attr'], InstrumentedAttribute) and k != "id"}
# logger.debug(f"Instance fields: {fields}")
for key, field in fields.items(): for key, field in fields.items():
match field.property: match field['class_attr'].property:
# NOTE: ColumnProperties will be directly edited. # NOTE: ColumnProperties will be directly edited.
case ColumnProperty(): case ColumnProperty():
# NOTE: field.property.expression.type gives db column type eg. STRING or TIMESTAMP # NOTE: field.property.expression.type gives db column type eg. STRING or TIMESTAMP
widget = EditProperty(self, key=key, column_type=field.property.expression.type, widget = EditProperty(self, key=key, column_type=field,
value=getattr(self.instance, key)) value=getattr(self.instance, key))
# NOTE: RelationshipDeclareds will be given a list of existing related objects. # NOTE: RelationshipDeclareds will be given a list of existing related objects.
case _RelationshipDeclared(): case _RelationshipDeclared():
if key != "submissions": if key != "submissions":
# NOTE: field.comparator.entity.class_ gives the relationship class # NOTE: field.comparator.entity.class_ gives the relationship class
widget = EditRelationship(self, key=key, entity=field.comparator.entity.class_, widget = EditRelationship(self, key=key, entity=field['class_attr'].comparator.entity.class_,
value=getattr(self.instance, key)) value=getattr(self.instance, key))
else: else:
continue continue
@@ -132,7 +149,7 @@ class ManagerWindow(QDialog):
return self.instance return self.instance
def add_new(self): def add_new(self):
dlg = AddEdit(parent=self, instance=self.object_type(), manager=self.object_type.__name__.lower()) dlg = AddEdit(parent=self, instance=self.object_type(), managers=self.managers)
if dlg.exec(): if dlg.exec():
new_pyd = dlg.parse_form() new_pyd = dlg.parse_form()
new_instance = new_pyd.to_sql() new_instance = new_pyd.to_sql()
@@ -148,13 +165,33 @@ class EditProperty(QWidget):
self.label = QLabel(key.title().replace("_", " ")) self.label = QLabel(key.title().replace("_", " "))
self.layout = QGridLayout() self.layout = QGridLayout()
self.layout.addWidget(self.label, 0, 0, 1, 1) self.layout.addWidget(self.label, 0, 0, 1, 1)
match column_type: logger.debug(f"Column type: {column_type}")
match column_type['class_attr'].property.expression.type:
case String(): case String():
self.widget = QLineEdit(self) self.widget = QLineEdit(self)
self.widget.setText(value) self.widget.setText(value)
case INTEGER():
if isinstance(column_type['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.setMaximum(999.99)
self.widget.setValue(value)
case TIMESTAMP(): case TIMESTAMP():
self.widget = QDateEdit(self) self.widget = QDateEdit(self)
self.widget.setDate(value) self.widget.setDate(value)
case JSON():
self.widget = QLabel("JSON Under construction")
case BLOB():
self.widget = QLabel("BLOB Under construction")
case _: case _:
self.widget = None self.widget = None
self.layout.addWidget(self.widget, 0, 1, 1, 3) self.layout.addWidget(self.widget, 0, 1, 1, 3)
@@ -175,7 +212,7 @@ class EditRelationship(QWidget):
def __init__(self, parent, key: str, entity: Any, value): def __init__(self, parent, key: str, entity: Any, value):
super().__init__(parent) super().__init__(parent)
self.entity = entity self.entity = entity #: The class of interest
self.data = value self.data = value
self.label = QLabel(key.title().replace("_", " ")) self.label = QLabel(key.title().replace("_", " "))
self.setObjectName(key) self.setObjectName(key)
@@ -184,6 +221,7 @@ class EditRelationship(QWidget):
self.add_button.clicked.connect(self.add_new) self.add_button.clicked.connect(self.add_new)
self.existing_button = QPushButton("Add Existing") self.existing_button = QPushButton("Add Existing")
self.existing_button.clicked.connect(self.add_existing) self.existing_button.clicked.connect(self.add_existing)
self.existing_button.setEnabled(self.entity.level == 1)
self.layout = QGridLayout() self.layout = QGridLayout()
self.layout.addWidget(self.label, 0, 0, 1, 5) self.layout.addWidget(self.label, 0, 0, 1, 5)
self.layout.addWidget(self.table, 1, 0, 1, 8) self.layout.addWidget(self.table, 1, 0, 1, 8)
@@ -205,17 +243,30 @@ class EditRelationship(QWidget):
self.add_edit(instance=object) self.add_edit(instance=object)
def add_new(self, instance: Any = None): def add_new(self, instance: Any = None):
# NOTE: if an existing instance is not being edited, create a new instance
if not instance: if not instance:
instance = self.entity() instance = self.entity()
dlg = AddEdit(self, instance=instance, manager=self.parent().object_type.__name__.lower()) # if self.parent().object_type.level == 2:
managers = self.parent().managers
# else:
# managers = self.parent().managers + [self.parent().instance]
match instance.level:
case 1:
dlg = AddEdit(self.parent(), instance=instance, managers=managers)
case 2:
dlg = ManagerWindow(self.parent(), object_type=instance.__class__, extras=[], managers=managers)
case _:
return
if dlg.exec(): if dlg.exec():
new_instance = dlg.parse_form() new_instance = dlg.parse_form()
new_instance, result = new_instance.to_sql() new_instance = new_instance.to_sql()
logger.debug(f"New instance: {new_instance}") logger.debug(f"New instance: {new_instance}")
addition = getattr(self.parent().instance, self.objectName()) addition = getattr(self.parent().instance, self.objectName())
if isinstance(addition, InstrumentedList): logger.debug(f"Addition: {addition}")
addition.append(new_instance) # NOTE: Saving currently disabled
self.parent().instance.save() # if isinstance(addition, InstrumentedList):
# addition.append(new_instance)
# self.parent().instance.save()
self.parent().update_data() self.parent().update_data()
def add_existing(self): def add_existing(self):
@@ -223,11 +274,15 @@ class EditRelationship(QWidget):
if dlg.exec(): if dlg.exec():
rows = dlg.return_selected_rows() rows = dlg.return_selected_rows()
for row in rows: for row in rows:
logger.debug(f"Querying with {row}")
instance = self.entity.query(**row) instance = self.entity.query(**row)
logger.debug(f"Queried instance: {instance}")
addition = getattr(self.parent().instance, self.objectName()) addition = getattr(self.parent().instance, self.objectName())
if isinstance(addition, InstrumentedList): logger.debug(f"Addition: {addition}")
addition.append(instance) # NOTE: Saving currently disabled
self.parent().instance.save() # if isinstance(addition, InstrumentedList):
# addition.append(instance)
# self.parent().instance.save()
self.parent().update_data() self.parent().update_data()
def set_data(self) -> None: def set_data(self) -> None:
@@ -235,7 +290,11 @@ class EditRelationship(QWidget):
sets data in model sets data in model
""" """
# logger.debug(self.data) # logger.debug(self.data)
self.data = DataFrame.from_records([item.omnigui_dict for item in self.data]) if not isinstance(self.data, list):
self.data = [self.data]
records = [{k: v['instance_attr'] for k, v in item.omnigui_dict.items()} for item in self.data]
# logger.debug(f"Records: {records}")
self.data = DataFrame.from_records(records)
try: try:
self.columns_of_interest = [dict(name=item, column=self.data.columns.get_loc(item)) for item in self.extras] self.columns_of_interest = [dict(name=item, column=self.data.columns.get_loc(item)) for item in self.extras]
except (KeyError, AttributeError): except (KeyError, AttributeError):
@@ -261,8 +320,13 @@ class EditRelationship(QWidget):
event (_type_): the item of interest event (_type_): the item of interest
""" """
id = self.table.selectionModel().currentIndex() id = self.table.selectionModel().currentIndex()
id = int(id.sibling(id.row(), 0).data()) # NOTE: the overly complicated {column_name: row_value} dictionary construction
object = self.entity.query(id=id) row_data = {self.data.columns[column]: self.table.model().index(id.row(), column).data() for column in
range(self.table.model().columnCount())}
object = self.entity.query(**row_data)
if isinstance(object, list):
object = object[0]
logger.debug(object)
self.menu = QMenu(self) self.menu = QMenu(self)
action = QAction(f"Remove {object.name}", self) action = QAction(f"Remove {object.name}", self)
action.triggered.connect(lambda: self.remove_item(object=object)) action.triggered.connect(lambda: self.remove_item(object=object))

View File

@@ -1,6 +1,7 @@
""" """
Search box that performs fuzzy search for various object types Search box that performs fuzzy search for various object types
""" """
from copy import deepcopy
from pprint import pformat from pprint import pformat
from typing import Tuple, Any, List, Generator from typing import Tuple, Any, List, Generator
from pandas import DataFrame from pandas import DataFrame
@@ -22,7 +23,8 @@ class SearchBox(QDialog):
def __init__(self, parent, object_type: Any, extras: List[dict], returnable: bool = False, **kwargs): def __init__(self, parent, object_type: Any, extras: List[dict], returnable: bool = False, **kwargs):
super().__init__(parent) super().__init__(parent)
self.object_type = self.original_type = object_type self.object_type = object_type
self.original_type = object_type
self.extras = extras self.extras = extras
self.context = kwargs self.context = kwargs
self.layout = QGridLayout(self) self.layout = QGridLayout(self)
@@ -43,7 +45,7 @@ class SearchBox(QDialog):
self.setLayout(self.layout) self.setLayout(self.layout)
self.setWindowTitle(f"Search {self.object_type.__name__}") self.setWindowTitle(f"Search {self.object_type.__name__}")
self.update_widgets() self.update_widgets()
self.update_data() # self.update_data()
if returnable: if returnable:
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn) self.buttonBox = QDialogButtonBox(QBtn)
@@ -57,21 +59,30 @@ class SearchBox(QDialog):
""" """
Changes form inputs based on sample type Changes form inputs based on sample type
""" """
search_fields = []
logger.debug(f"Search fields: {search_fields}")
deletes = [item for item in self.findChildren(FieldSearch)] deletes = [item for item in self.findChildren(FieldSearch)]
for item in deletes: for item in deletes:
item.setParent(None) item.setParent(None)
# NOTE: Handle any subclasses # NOTE: Handle any subclasses
if not self.sub_class: if not self.sub_class:
logger.warning(f"No subclass selected.")
self.update_data() self.update_data()
return
else: else:
if self.sub_class.currentText() == "Any": if self.sub_class.currentText() == "Any":
self.object_type = self.original_type self.object_type = self.original_type
else: else:
self.object_type = self.original_type.find_regular_subclass(self.sub_class.currentText()) self.object_type = self.original_type.find_regular_subclass(self.sub_class.currentText())
try: # logger.debug(f"Object type: {self.object_type} - {self.object_type.searchables}")
search_fields = self.object_type.searchables # logger.debug(f"Original type: {self.original_type} - {self.original_type.searchables}")
except AttributeError: for item in self.object_type.searchables:
search_fields = [] if item['field'] in [item['field'] for item in search_fields]:
logger.debug(f"Already have {item['field']}")
continue
else:
search_fields.append(item)
logger.debug(f"Search fields: {search_fields}")
for iii, searchable in enumerate(search_fields): for iii, searchable in enumerate(search_fields):
widget = FieldSearch(parent=self, label=searchable['label'], field_name=searchable['field']) widget = FieldSearch(parent=self, label=searchable['label'], field_name=searchable['field'])
widget.setObjectName(searchable['field']) widget.setObjectName(searchable['field'])
@@ -120,6 +131,7 @@ class FieldSearch(QWidget):
def __init__(self, parent, label, field_name): def __init__(self, parent, label, field_name):
super().__init__(parent) super().__init__(parent)
self.setParent(parent)
self.layout = QVBoxLayout(self) self.layout = QVBoxLayout(self)
label_widget = QLabel(label) label_widget = QLabel(label)
self.layout.addWidget(label_widget) self.layout.addWidget(label_widget)
@@ -158,9 +170,8 @@ class SearchResults(QTableView):
self.context = kwargs self.context = kwargs
self.parent = parent self.parent = parent
self.object_type = object_type self.object_type = object_type
try: try:
self.extras = extras + self.object_type.searchables self.extras = extras + [item for item in deepcopy(self.object_type.searchables)]
except AttributeError: except AttributeError:
self.extras = extras self.extras = extras
logger.debug(f"Extras: {self.extras}") logger.debug(f"Extras: {self.extras}")

View File

@@ -684,7 +684,7 @@ class SubmissionFormWidget(QWidget):
message=f"Couldn't find reagent type {self.reagent.role}: {lot} in the database.\n\nWould you like to add it?") message=f"Couldn't find reagent type {self.reagent.role}: {lot} in the database.\n\nWould you like to add it?")
if dlg.exec(): if dlg.exec():
wanted_reagent = self.parent().parent().new_add_reagent(instance=wanted_reagent) wanted_reagent = self.parent.parent().add_reagent(instance=wanted_reagent)
return wanted_reagent, report return wanted_reagent, report
else: else:
# NOTE: In this case we will have an empty reagent and the submission will fail kit integrity check # NOTE: In this case we will have an empty reagent and the submission will fail kit integrity check

View File

@@ -931,6 +931,17 @@ def rreplace(s: str, old: str, new: str) -> str:
return (s[::-1].replace(old[::-1], new[::-1], 1))[::-1] return (s[::-1].replace(old[::-1], new[::-1], 1))[::-1]
def list_sort_dict(input_dict: dict, sort_list: list) -> dict:
# sort_list.reverse()
sort_list = reversed(sort_list)
for item in sort_list:
try:
input_dict = {item: input_dict.pop(item), **input_dict}
except KeyError:
continue
return input_dict
def remove_key_from_list_of_dicts(input_list: list, key: str) -> list: def remove_key_from_list_of_dicts(input_list: list, key: str) -> list:
""" """
Removes a key from all dictionaries in a list of dictionaries Removes a key from all dictionaries in a list of dictionaries
@@ -1067,7 +1078,6 @@ def report_result(func):
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
logger.info(f"Report result being called by {func.__name__}") logger.info(f"Report result being called by {func.__name__}")
output = func(*args, **kwargs) output = func(*args, **kwargs)
print(f"Function output: {output}")
match output: match output:
case Report(): case Report():
report = output report = output
@@ -1091,14 +1101,11 @@ def report_result(func):
logger.error(f"Problem reporting due to {e}") logger.error(f"Problem reporting due to {e}")
logger.error(result.msg) logger.error(result.msg)
if output: if output:
print(f"Output going into checking: {output}")
if is_list_etc(output): if is_list_etc(output):
print(f"Output of type {type(output)} is iterable")
true_output = tuple(item for item in output if not isinstance(item, Report)) true_output = tuple(item for item in output if not isinstance(item, Report))
if len(true_output) == 1: if len(true_output) == 1:
true_output = true_output[0] true_output = true_output[0]
else: else:
print(f"Output is of type {type(output)}")
if isinstance(output, Report): if isinstance(output, Report):
true_output = None true_output = None
else: else: