diff --git a/src/submissions/backend/db/models/__init__.py b/src/submissions/backend/db/models/__init__.py index 356878b..8d13460 100644 --- a/src/submissions/backend/db/models/__init__.py +++ b/src/submissions/backend/db/models/__init__.py @@ -8,7 +8,7 @@ from pydantic import BaseModel from sqlalchemy import Column, INTEGER, String, JSON from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query, Session, InstrumentedAttribute from sqlalchemy.ext.declarative import declared_attr -from sqlalchemy.exc import ArgumentError +from sqlalchemy.exc import ArgumentError, InvalidRequestError from typing import Any, List from pathlib import Path from tools import report_result, list_sort_dict @@ -48,7 +48,7 @@ class BaseClass(Base): __table_args__ = {'extend_existing': True} #: Will only add new columns singles = ['id'] - omni_removes = ['submissions'] + omni_removes = ['submissions', "omnigui_class_dict", "omnigui_instance_dict"] omni_sort = ["name"] @classproperty @@ -60,7 +60,7 @@ class BaseClass(Base): @classproperty def aliases(cls): - return [cls.__name__.lower()] + return [cls.query_alias] @classproperty def level(cls): @@ -198,7 +198,7 @@ class BaseClass(Base): try: records = [obj.to_sub_dict(**kwargs) for obj in objects] except AttributeError: - records = [{k:v['instance_attr'] for k, v in obj.to_omnigui_dict(**kwargs).items()} for obj in objects] + records = [{k:v['instance_attr'] for k, v in obj.omnigui_instance_dict.items()} for obj in objects] return DataFrame.from_records(records) @classmethod @@ -233,7 +233,11 @@ class BaseClass(Base): logger.info(f"Using key: {k} with value: {v}") try: attr = getattr(model, k) - query = query.filter(attr == v) + # NOTE: account for attrs that use list. + if attr.property.uselist: + query = query.filter(attr.contains(v)) + else: + query = query.filter(attr == v) except (ArgumentError, AttributeError) as e: logger.error(f"Attribute {k} unavailable due to:\n\t{e}\nSkipping.") if k in singles: @@ -265,14 +269,13 @@ class BaseClass(Base): return report @property - def omnigui_dict(self) -> dict: + def omnigui_instance_dict(self) -> dict: """ For getting any object in an omni-thing friendly output. Returns: 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 = {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 @@ -306,7 +309,7 @@ class BaseClass(Base): model = getattr(pydant, f"Pyd{cls.__name__}") except AttributeError: logger.warning(f"Couldn't get {cls.__name__} pydantic model.") - return None + return pydant.PydElastic return model @classproperty @@ -324,6 +327,32 @@ class BaseClass(Base): query_kwargs = {relationship_instance.query_alias:relationship_instance} return cls.query(**query_kwargs) + def __setattr__(self, key, value): + try: + field_type = getattr(self.__class__, key) + except AttributeError: + return super().__setattr__(key, value) + try: + check = field_type.property.uselist + except AttributeError: + check = False + if check: + logger.debug(f"Setting with uselist") + if self.__getattribute__(key) is not None: + if isinstance(value, list): + value = self.__getattribute__(key) + value + else: + value = self.__getattribute__(key) + [value] + else: + value = [value] + else: + if isinstance(value, list): + if len(value) == 1: + value = value[0] + else: + raise ValueError("Object is too long to parse a single value.") + return super().__setattr__(key, value) + diff --git a/src/submissions/backend/db/models/controls.py b/src/submissions/backend/db/models/controls.py index 5ef6807..3eff08a 100644 --- a/src/submissions/backend/db/models/controls.py +++ b/src/submissions/backend/db/models/controls.py @@ -146,7 +146,7 @@ class Control(BaseClass): @classmethod @setup_lookup def query(cls, - submission_type: str | None = None, + submissiontype: str | None = None, subtype: str | None = None, start_date: date | str | int | None = None, end_date: date | str | int | None = None, @@ -169,13 +169,13 @@ class Control(BaseClass): """ from backend.db import SubmissionType query: Query = cls.__database_session__.query(cls) - match submission_type: + match submissiontype: case str(): from backend import BasicSubmission, SubmissionType - query = query.join(BasicSubmission).join(SubmissionType).filter(SubmissionType.name == submission_type) + query = query.join(BasicSubmission).join(SubmissionType).filter(SubmissionType.name == submissiontype) case SubmissionType(): from backend import BasicSubmission - query = query.join(BasicSubmission).filter(BasicSubmission.submission_type_name == submission_type.name) + query = query.join(BasicSubmission).filter(BasicSubmission.submission_type_name == submissiontype.name) case _: pass # NOTE: by control type @@ -347,7 +347,7 @@ class PCRControl(Control): parent.mode_typer.clear() parent.mode_typer.setEnabled(False) report = Report() - controls = cls.query(submission_type=chart_settings['sub_type'], start_date=chart_settings['start_date'], + controls = cls.query(submissiontype=chart_settings['sub_type'], start_date=chart_settings['start_date'], end_date=chart_settings['end_date']) data = [control.to_sub_dict() for control in controls] df = DataFrame.from_records(data) diff --git a/src/submissions/backend/db/models/kits.py b/src/submissions/backend/db/models/kits.py index d9ddb5f..cd074cc 100644 --- a/src/submissions/backend/db/models/kits.py +++ b/src/submissions/backend/db/models/kits.py @@ -96,7 +96,8 @@ class KitType(BaseClass): Base of kits used in submission processing """ - query_alias = "kit_type" + # query_alias = "kit_type" + omni_sort = BaseClass.omni_sort + ["kit_submissiontype_associations", "kit_reagentrole_associations", "processes"] id = Column(INTEGER, primary_key=True) #: primary key name = Column(String(64), unique=True) #: name of kit @@ -134,7 +135,12 @@ class KitType(BaseClass): @classproperty def aliases(cls): - return super().aliases + [cls.query_alias, "kit_types"] + return super().aliases + [cls.query_alias, "kit_types", "kit_type"] + + @hybrid_property + def submissiontype(self): + """Alias used_for field to allow query with SubmissionType query alias""" + return self.used_for def get_reagents(self, required: bool = False, @@ -227,7 +233,8 @@ class KitType(BaseClass): name: str = None, used_for: str | SubmissionType | None = None, id: int | None = None, - limit: int = 0 + limit: int = 0, + **kwargs ) -> KitType | List[KitType]: """ Lookup a list of or single KitType. @@ -264,7 +271,7 @@ class KitType(BaseClass): limit = 1 case _: pass - return cls.execute_query(query=query, limit=limit) + return cls.execute_query(query=query, limit=limit, **kwargs) @check_authorization def save(self): @@ -407,10 +414,11 @@ class ReagentRole(BaseClass): @setup_lookup def query(cls, name: str | None = None, - kit_type: KitType | str | None = None, + kittype: KitType | str | None = None, reagent: Reagent | str | None = None, id: int | None = None, limit: int = 0, + **kwargs ) -> ReagentRole | List[ReagentRole]: """ Lookup reagent types in the database. @@ -429,14 +437,14 @@ class ReagentRole(BaseClass): ReagentRole|List[ReagentRole]: ReagentRole or list of ReagentRoles matching filter. """ query: Query = cls.__database_session__.query(cls) - if (kit_type is not None and reagent is None) or (reagent is not None and kit_type is None): + if (kittype is not None and reagent is None) or (reagent is not None and kittype is None): raise ValueError("Cannot filter without both reagent and kit type.") - elif kit_type is None and reagent is None: + elif kittype is None and reagent is None: pass else: - match kit_type: + match kittype: case str(): - kit_type = KitType.query(name=kit_type) + kittype = KitType.query(name=kittype) case _: pass match reagent: @@ -446,7 +454,7 @@ class ReagentRole(BaseClass): pass assert reagent.role # NOTE: Get all roles common to the reagent and the kit. - result = set(kit_type.reagent_roles).intersection(reagent.role) + result = set(kittype.reagent_roles).intersection(reagent.role) return next((item for item in result), None) match name: case str(): @@ -491,8 +499,6 @@ class Reagent(BaseClass, LogMixin): Concrete reagent instance """ - - id = Column(INTEGER, primary_key=True) #: primary key role = relationship("ReagentRole", back_populates="instances", secondary=reagentroles_reagents) #: joined parent reagent type @@ -523,6 +529,11 @@ class Reagent(BaseClass, LogMixin): def searchables(cls): return [dict(label="Lot", field="lot")] + @hybrid_property + def reagentrole(self): + """Alias role field to allow query with ReagentRole query alias""" + return self.role + def to_sub_dict(self, extraction_kit: KitType = None, full_data: bool = False, **kwargs) -> dict: """ dictionary containing values necessary for gui @@ -583,9 +594,9 @@ class Reagent(BaseClass, LogMixin): Report: Result of operation """ report = Report() - rt = ReagentRole.query(kit_type=kit, reagent=self, limit=1) + rt = ReagentRole.query(kittype=kit, reagent=self, limit=1) if rt is not None: - assoc = KitTypeReagentRoleAssociation.query(kit_type=kit, reagent_role=rt) + assoc = KitTypeReagentRoleAssociation.query(kittype=kit, reagentrole=rt) if assoc is not None: if assoc.last_used != self.lot: assoc.last_used = self.lot @@ -621,7 +632,8 @@ class Reagent(BaseClass, LogMixin): role: str | ReagentRole | None = None, lot: str | None = None, name: str | None = None, - limit: int = 0 + limit: int = 0, + **kwargs ) -> Reagent | List[Reagent]: """ Lookup a list of reagents from the database. @@ -643,6 +655,8 @@ class Reagent(BaseClass, LogMixin): limit = 1 case _: pass + # if not role and "reagentrole" in kwargs.keys(): + # role = kwargs['reagentrole'] match role: case str(): query = query.join(cls.role).filter(ReagentRole.name == role) @@ -663,7 +677,7 @@ class Reagent(BaseClass, LogMixin): limit = 1 case _: pass - return cls.execute_query(query=query, limit=limit) + return cls.execute_query(query=query, limit=limit, **kwargs) def set_attribute(self, key, value): match key: @@ -741,7 +755,7 @@ class Discount(BaseClass): @setup_lookup def query(cls, organization: Organization | str | int | None = None, - kit_type: KitType | str | int | None = None, + kittype: KitType | str | int | None = None, ) -> Discount | List[Discount]: """ Lookup discount objects (union of kit and organization) @@ -763,13 +777,13 @@ class Discount(BaseClass): query = query.join(Organization).filter(Organization.id == organization) case _: pass - match kit_type: + match kittype: case KitType(): - query = query.filter(cls.kit == kit_type) + query = query.filter(cls.kit == kittype) case str(): - query = query.join(KitType).filter(KitType.name == kit_type) + query = query.join(KitType).filter(KitType.name == kittype) case int(): - query = query.join(KitType).filter(KitType.id == kit_type) + query = query.join(KitType).filter(KitType.id == kittype) case _: pass return cls.execute_query(query=query) @@ -833,6 +847,14 @@ class SubmissionType(BaseClass): """ return f"" + @hybrid_property + def kittype(self): + return self.kit_types + + @hybrid_property + def process(self): + return self.processes + @classproperty def aliases(cls): return super().aliases + ["submission_types", "submission_type"] @@ -990,7 +1012,8 @@ class SubmissionType(BaseClass): def query(cls, name: str | None = None, key: str | None = None, - limit: int = 0 + limit: int = 0, + **kwargs ) -> SubmissionType | List[SubmissionType]: """ Lookup submission type in the database by a number of parameters @@ -1124,7 +1147,23 @@ class SubmissionTypeKitTypeAssociation(BaseClass): Returns: str: Representation of this object """ - return f"" + try: + submission_type_name = self.submission_type.name + except AttributeError: + submission_type_name = "None" + try: + kit_type_name = self.kit_type.name + except AttributeError: + kit_type_name = "None" + return f"" + + @hybrid_property + def kittype(self): + return self.kit_type + + @hybrid_property + def submissiontype(self): + return self.submission_type @property def name(self): @@ -1133,8 +1172,8 @@ class SubmissionTypeKitTypeAssociation(BaseClass): @classmethod @setup_lookup def query(cls, - submission_type: SubmissionType | str | int | None = None, - kit_type: KitType | str | int | None = None, + submissiontype: SubmissionType | str | int | None = None, + kittype: KitType | str | int | None = None, limit: int = 0, **kwargs ) -> SubmissionTypeKitTypeAssociation | List[SubmissionTypeKitTypeAssociation]: @@ -1150,21 +1189,21 @@ class SubmissionTypeKitTypeAssociation(BaseClass): SubmissionTypeKitTypeAssociation|List[SubmissionTypeKitTypeAssociation]: SubmissionTypeKitTypeAssociation(s) of interest """ query: Query = cls.__database_session__.query(cls) - match submission_type: + match submissiontype: case SubmissionType(): - query = query.filter(cls.submission_type == submission_type) + query = query.filter(cls.submission_type == submissiontype) case str(): - query = query.join(SubmissionType).filter(SubmissionType.name == submission_type) + query = query.join(SubmissionType).filter(SubmissionType.name == submissiontype) case int(): - query = query.join(SubmissionType).filter(SubmissionType.id == submission_type) - match kit_type: + query = query.join(SubmissionType).filter(SubmissionType.id == submissiontype) + match kittype: case KitType(): - query = query.filter(cls.kit_type == kit_type) + query = query.filter(cls.kit_type == kittype) case str(): - query = query.join(KitType).filter(KitType.name == kit_type) + query = query.join(KitType).filter(KitType.name == kittype) case int(): - query = query.join(KitType).filter(KitType.id == kit_type) - if kit_type is not None and submission_type is not None: + query = query.join(KitType).filter(KitType.id == kittype) + if kittype is not None and submissiontype is not None: limit = 1 # limit = query.count() return cls.execute_query(query=query, limit=limit) @@ -1221,7 +1260,10 @@ class KitTypeReagentRoleAssociation(BaseClass): @property def name(self): - return f"{self.kit_type.name} -> {self.reagent_role.name}" + try: + return f"{self.kit_type.name} -> {self.reagent_role.name}" + except AttributeError: + return "Blank KitTypeReagentRole" @validates('required') def validate_required(self, key, value): @@ -1238,6 +1280,8 @@ class KitTypeReagentRoleAssociation(BaseClass): Returns: _type_: value """ + if isinstance(value, bool): + value = int(value) if not 0 <= value < 2: raise ValueError(f'Invalid required value {value}. Must be 0 or 1.') return value @@ -1264,8 +1308,8 @@ class KitTypeReagentRoleAssociation(BaseClass): @classmethod @setup_lookup def query(cls, - kit_type: KitType | str | None = None, - reagent_role: ReagentRole | str | None = None, + kittype: KitType | str | None = None, + reagentrole: ReagentRole | str | None = None, limit: int = 0, **kwargs ) -> KitTypeReagentRoleAssociation | List[KitTypeReagentRoleAssociation]: @@ -1281,21 +1325,21 @@ class KitTypeReagentRoleAssociation(BaseClass): models.KitTypeReagentTypeAssociation|List[models.KitTypeReagentTypeAssociation]: Junction of interest. """ query: Query = cls.__database_session__.query(cls) - match kit_type: + match kittype: case KitType(): - query = query.filter(cls.kit_type == kit_type) + query = query.filter(cls.kit_type == kittype) case str(): - query = query.join(KitType).filter(KitType.name == kit_type) + query = query.join(KitType).filter(KitType.name == kittype) case _: pass - match reagent_role: + match reagentrole: case ReagentRole(): - query = query.filter(cls.reagent_role == reagent_role) + query = query.filter(cls.reagent_role == reagentrole) case str(): - query = query.join(ReagentRole).filter(ReagentRole.name == reagent_role) + query = query.join(ReagentRole).filter(ReagentRole.name == reagentrole) case _: pass - if kit_type is not None and reagent_role is not None: + if kittype is not None and reagentrole is not None: limit = 1 return cls.execute_query(query=query, limit=limit) @@ -1329,8 +1373,8 @@ class KitTypeReagentRoleAssociation(BaseClass): yield rel_reagent @property - def omnigui_dict(self) -> dict: - dicto = super().omnigui_dict + def omnigui_instance_dict(self) -> dict: + dicto = super().omnigui_instance_dict dicto['required']['instance_attr'] = bool(dicto['required']['instance_attr']) return dicto @@ -1801,6 +1845,14 @@ class SubmissionTypeEquipmentRoleAssociation(BaseClass): equipment_role = relationship(EquipmentRole, back_populates="equipmentrole_submissiontype_associations") #: associated equipment + @hybrid_property + def submissiontype(self): + return self.submission_type + + @hybrid_property + def equipmentrole(self): + return self.equipment_role + @validates('static') def validate_static(self, key, value): """ @@ -1847,6 +1899,8 @@ class Process(BaseClass): A Process is a method used by a piece of equipment. """ + level = 2 + id = Column(INTEGER, primary_key=True) #: Process id, primary key name = Column(String(64), unique=True) #: Process name submission_types = relationship("SubmissionType", back_populates='processes', @@ -1869,14 +1923,24 @@ class Process(BaseClass): """ return f"" + def set_attribute(self, key, value): + match key: + case "name": + self.name = value + case _: + field = getattr(self, key) + if value not in field: + field.append(value) + + @classmethod @setup_lookup def query(cls, name: str | None = None, id: int | None = None, - submission_type: str | SubmissionType | None = None, - extraction_kit: str | KitType | None = None, - equipment_role: str | KitType | None = None, + submissiontype: str | SubmissionType | None = None, + kittype: str | KitType | None = None, + equipmentrole: str | KitType | None = None, limit: int = 0, **kwargs) -> Process | List[Process]: """ @@ -1891,28 +1955,28 @@ class Process(BaseClass): Process|List[Process]: Process(es) matching criteria """ query = cls.__database_session__.query(cls) - match submission_type: + match submissiontype: case str(): - submission_type = SubmissionType.query(name=submission_type) - query = query.filter(cls.submission_types.contains(submission_type)) + submissiontype = SubmissionType.query(name=submissiontype) + query = query.filter(cls.submission_types.contains(submissiontype)) case SubmissionType(): - query = query.filter(cls.submission_types.contains(submission_type)) + query = query.filter(cls.submission_types.contains(submissiontype)) case _: pass - match extraction_kit: + match kittype: case str(): - extraction_kit = KitType.query(name=extraction_kit) - query = query.filter(cls.kit_types.contains(extraction_kit)) + kittype = KitType.query(name=kittype) + query = query.filter(cls.kit_types.contains(kittype)) case KitType(): - query = query.filter(cls.kit_types.contains(extraction_kit)) + query = query.filter(cls.kit_types.contains(kittype)) case _: pass - match equipment_role: + match equipmentrole: case str(): - equipment_role = EquipmentRole.query(name=equipment_role) - query = query.filter(cls.equipment_roles.contains(equipment_role)) + equipmentrole = EquipmentRole.query(name=equipmentrole) + query = query.filter(cls.equipment_roles.contains(equipmentrole)) case EquipmentRole(): - query = query.filter(cls.equipment_roles.contains(equipment_role)) + query = query.filter(cls.equipment_roles.contains(equipmentrole)) case _: pass match name: @@ -1983,6 +2047,10 @@ class Tips(BaseClass, LogMixin): submissions = association_proxy("tips_submission_associations", 'submission') + @hybrid_property + def tiprole(self): + return self.role + def __repr__(self): return f"" @@ -2033,6 +2101,14 @@ class SubmissionTypeTipRoleAssociation(BaseClass): tip_role = relationship(TipRole, back_populates="tiprole_submissiontype_associations") #: associated equipment + @hybrid_property + def submissiontype(self): + return self.submission_type + + @hybrid_property + def tiprole(self): + return self.tip_role + @check_authorization def save(self): super().save() diff --git a/src/submissions/backend/db/models/organizations.py b/src/submissions/backend/db/models/organizations.py index ffc563d..94eef6e 100644 --- a/src/submissions/backend/db/models/organizations.py +++ b/src/submissions/backend/db/models/organizations.py @@ -6,6 +6,7 @@ import json, yaml, logging from pathlib import Path from pprint import pformat from sqlalchemy import Column, String, INTEGER, ForeignKey, Table +from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import relationship, Query from . import Base, BaseClass from tools import check_authorization, setup_lookup, yaml_regex_creator @@ -38,6 +39,10 @@ class Organization(BaseClass): contacts = relationship("Contact", back_populates="organization", secondary=orgs_contacts) #: contacts involved with this org + @hybrid_property + def contact(self): + return self.contacts + def __repr__(self) -> str: return f"" diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index ad69e3c..22e450a 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -10,6 +10,9 @@ from zipfile import ZipFile, BadZipfile from tempfile import TemporaryDirectory, TemporaryFile from operator import itemgetter from pprint import pformat + +from sqlalchemy.ext.hybrid import hybrid_property + from . import BaseClass, Reagent, SubmissionType, KitType, Organization, Contact, LogMixin, SubmissionReagentAssociation from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case, func from sqlalchemy.orm import relationship, validates, Query @@ -126,6 +129,14 @@ class BasicSubmission(BaseClass, LogMixin): def __repr__(self) -> str: return f"" + @hybrid_property + def kittype(self): + return self.extraction_kit + + @hybrid_property + def organization(self): + return self.submitting_lab + @classproperty def jsons(cls) -> List[str]: """ @@ -488,7 +499,7 @@ class BasicSubmission(BaseClass, LogMixin): """ # NOTE: use lookup function to create list of dicts subs = [item.to_dict() for item in - cls.query(submission_type=submission_type, limit=limit, chronologic=chronologic, page=page, + cls.query(submissiontype=submission_type, limit=limit, chronologic=chronologic, page=page, page_size=page_size)] df = pd.DataFrame.from_records(subs) # NOTE: Exclude sub information @@ -1056,7 +1067,7 @@ class BasicSubmission(BaseClass, LogMixin): @classmethod @setup_lookup def query(cls, - submission_type: str | SubmissionType | None = None, + submissiontype: str | SubmissionType | None = None, submission_type_name: str | None = None, id: int | str | None = None, rsl_plate_num: str | None = None, @@ -1087,8 +1098,8 @@ class BasicSubmission(BaseClass, LogMixin): """ from ... import SubmissionReagentAssociation # NOTE: if you go back to using 'model' change the appropriate cls to model in the query filters - if submission_type is not None: - model = cls.find_polymorphic_subclass(polymorphic_identity=submission_type) + if submissiontype is not None: + model = cls.find_polymorphic_subclass(polymorphic_identity=submissiontype) elif len(kwargs) > 0: # NOTE: find the subclass containing the relevant attributes model = cls.find_polymorphic_subclass(attrs=kwargs) @@ -1196,7 +1207,7 @@ class BasicSubmission(BaseClass, LogMixin): if kwargs == {}: raise ValueError("Need to narrow down query or the first available instance will be returned.") sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed} - instance = cls.query(submission_type=submission_type, limit=1, **sanitized_kwargs) + instance = cls.query(submissiontype=submission_type, limit=1, **sanitized_kwargs) if instance is None: used_class = cls.find_polymorphic_subclass(attrs=kwargs, polymorphic_identity=submission_type) instance = used_class(**sanitized_kwargs) diff --git a/src/submissions/backend/validators/__init__.py b/src/submissions/backend/validators/__init__.py index 9a92a0b..e08f3c2 100644 --- a/src/submissions/backend/validators/__init__.py +++ b/src/submissions/backend/validators/__init__.py @@ -157,7 +157,7 @@ class RSLNamer(object): if "rsl_plate_num" in data.keys(): plate_number = data['rsl_plate_num'].split("-")[-1][0] else: - previous = BasicSubmission.query(start_date=today, end_date=today, submission_type=data['submission_type']) + previous = BasicSubmission.query(start_date=today, end_date=today, submissiontype=data['submission_type']) plate_number = len(previous) + 1 return f"RSL-{data['abbreviation']}-{today.year}{str(today.month).zfill(2)}{str(today.day).zfill(2)}-{plate_number}" @@ -191,5 +191,5 @@ class RSLNamer(object): return "" -from .pydant import PydSubmission, PydKit, PydContact, PydOrganization, PydSample, PydReagent, PydReagentRole, \ - PydEquipment, PydEquipmentRole, PydTips, PydPCRControl, PydIridaControl +from .pydant import PydSubmission, PydKitType, PydContact, PydOrganization, PydSample, PydReagent, PydReagentRole, \ + PydEquipment, PydEquipmentRole, PydTips, PydPCRControl, PydIridaControl, PydProcess, PydElastic diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py index 84ef286..97440ba 100644 --- a/src/submissions/backend/validators/pydant.py +++ b/src/submissions/backend/validators/pydant.py @@ -14,6 +14,8 @@ from pathlib import Path from tools import check_not_nan, convert_nans_to_nones, Report, Result, timezone from backend.db.models import * from sqlalchemy.exc import StatementError, IntegrityError +from sqlalchemy.orm.properties import ColumnProperty +from sqlalchemy.orm.relationships import _RelationshipDeclared from PyQt6.QtWidgets import QWidget logger = logging.getLogger(f"submissions.{__name__}") @@ -203,6 +205,7 @@ class PydSample(BaseModel, extra='allow'): fields = list(self.model_fields.keys()) + list(self.model_extra.keys()) return {k: getattr(self, k) for k in fields} + @report_result def to_sql(self, submission: BasicSubmission | str = None) -> Tuple[ BasicSample, List[SubmissionSampleAssociation], Result | None]: """ @@ -270,6 +273,7 @@ class PydTips(BaseModel): value = value.name return value + @report_result def to_sql(self, submission: BasicSubmission) -> SubmissionTipsAssociation: """ Convert this object to the SQL version for database storage. @@ -280,12 +284,13 @@ class PydTips(BaseModel): Returns: SubmissionTipsAssociation: Association between queried tips and submission """ + report = Report() tips = Tips.query(name=self.name, limit=1) # logger.debug(f"Tips query has yielded: {tips}") assoc = SubmissionTipsAssociation.query_or_create(tips=tips, submission=submission, role=self.role, limit=1) # if assoc is None: # assoc = SubmissionTipsAssociation(submission=submission, tips=tips, role_name=self.role) - return assoc + return assoc, report class PydEquipment(BaseModel, extra='ignore'): @@ -317,6 +322,7 @@ class PydEquipment(BaseModel, extra='ignore'): pass return value + @report_result def to_sql(self, submission: BasicSubmission | str = None, extraction_kit: KitType | str = None) -> Tuple[Equipment, SubmissionEquipmentAssociation]: """ Creates Equipment and SubmssionEquipmentAssociations for this PydEquipment @@ -327,6 +333,7 @@ class PydEquipment(BaseModel, extra='ignore'): Returns: Tuple[Equipment, SubmissionEquipmentAssociation]: SQL objects """ + report = Report() if isinstance(submission, str): submission = BasicSubmission.query(rsl_plate_num=submission) if isinstance(extraction_kit, str): @@ -349,7 +356,7 @@ class PydEquipment(BaseModel, extra='ignore'): # NOTE: It looks like the way fetching the processes is done in the SQL model, this shouldn't be a problem, but I'll include a failsafe. # NOTE: I need to find a way to filter this by the kit involved. if len(self.processes) > 1: - process = Process.query(submission_type=submission.get_submission_type(), extraction_kit=extraction_kit, equipment_role=self.role) + process = Process.query(submissiontype=submission.get_submission_type(), kittype=extraction_kit, equipmentrole=self.role) else: process = Process.query(name=self.processes[0]) if process is None: @@ -362,7 +369,7 @@ class PydEquipment(BaseModel, extra='ignore'): else: logger.warning(f"No submission found") assoc = None - return equipment, assoc + return equipment, assoc, report def improved_dict(self) -> dict: """ @@ -762,6 +769,7 @@ class PydSubmission(BaseModel, extra='allow'): missing_reagents = [reagent for reagent in self.reagents if reagent.missing] return missing_info, missing_reagents + @report_result def to_sql(self) -> Tuple[BasicSubmission, Report]: """ Converts this instance into a backend.db.models.submissions.BasicSubmission instance @@ -867,7 +875,7 @@ class PydSubmission(BaseModel, extra='allow'): # NOTE: Apply any discounts that are applicable for client and kit. try: discounts = [item.amount for item in - Discount.query(kit_type=instance.extraction_kit, organization=instance.submitting_lab)] + Discount.query(kittype=instance.extraction_kit, organization=instance.submitting_lab)] if len(discounts) > 0: instance.run_cost = instance.run_cost - sum(discounts) except Exception as e: @@ -1047,7 +1055,7 @@ class PydOrganization(BaseModel): return None return value - + @report_result def to_sql(self) -> Organization: """ Converts this instance into a backend.db.models.organization.Organization instance. @@ -1055,6 +1063,7 @@ class PydOrganization(BaseModel): Returns: Organization: Organization instance """ + report = Report() instance = Organization() for field in self.model_fields: match field: @@ -1067,7 +1076,7 @@ class PydOrganization(BaseModel): logger.debug(f"Setting {field} to {value}") if value: setattr(instance, field, value) - return instance + return instance, report class PydReagentRole(BaseModel): @@ -1083,6 +1092,7 @@ class PydReagentRole(BaseModel): return timedelta(days=value) return value + @report_result def to_sql(self, kit: KitType) -> ReagentRole: """ Converts this instance into a backend.db.models.ReagentType instance @@ -1093,23 +1103,25 @@ class PydReagentRole(BaseModel): Returns: ReagentRole: ReagentType instance """ + report = Report() instance: ReagentRole = ReagentRole.query(name=self.name) if instance is None: instance = ReagentRole(name=self.name, eol_ext=self.eol_ext) try: - assoc = KitTypeReagentRoleAssociation.query(reagent_role=instance, kit_type=kit) + assoc = KitTypeReagentRoleAssociation.query(reagentrole=instance, kittype=kit) except StatementError: assoc = None if assoc is None: assoc = KitTypeReagentRoleAssociation(kit_type=kit, reagent_role=instance, uses=self.uses, required=self.required) - return instance + return instance, report -class PydKit(BaseModel): +class PydKitType(BaseModel): name: str reagent_roles: List[PydReagentRole] = [] + @report_result def to_sql(self) -> Tuple[KitType, Report]: """ Converts this instance into a backend.db.models.kits.KitType instance @@ -1163,7 +1175,9 @@ class PydPCRControl(BaseModel): submission_id: int controltype_name: str + @report_result def to_sql(self): + report = Report instance = PCRControl.query(name=self.name) if not instance: instance = PCRControl() @@ -1171,7 +1185,7 @@ class PydPCRControl(BaseModel): field_value = self.__getattribute__(key) if instance.__getattribute__(key) != field_value: instance.__setattr__(key, field_value) - return instance + return instance, report class PydIridaControl(BaseModel, extra='ignore'): @@ -1196,7 +1210,9 @@ class PydIridaControl(BaseModel, extra='ignore'): value = "" return value + @report_result def to_sql(self): + report = Report() instance = IridaControl.query(name=self.name) if not instance: instance = IridaControl() @@ -1204,7 +1220,7 @@ class PydIridaControl(BaseModel, extra='ignore'): field_value = self.__getattribute__(key) if instance.__getattribute__(key) != field_value: instance.__setattr__(key, field_value) - return instance + return instance, report class PydProcess(BaseModel, extra="allow"): @@ -1222,16 +1238,62 @@ class PydProcess(BaseModel, extra="allow"): return [value] return value + @report_result def to_sql(self): + report = Report() 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}") + # dicto = instance.omnigui_instance_dict + fields = [item for item in self.model_fields] + for field in fields: + logger.debug(f"Field: {field}") + try: + field_type = getattr(instance.__class__, field).property + except AttributeError: + logger.error(f"No attribute: {field} in {instance.__class__}") + continue + match field_type: + case _RelationshipDeclared(): + logger.debug(f"{field} is a relationship with {field_type.entity.class_}") + query_str = getattr(self, field) + if isinstance(query_str, list): + query_str = query_str[0] + if query_str in ["", " ", None]: + continue + logger.debug(f"Querying {field_type.entity.class_} with name {query_str}") + field_value = field_type.entity.class_.query(name=query_str) + logger.debug(f"{field} query result: {field_value}") + case ColumnProperty(): + logger.debug(f"{field} is a property.") + field_value = getattr(self, field) + instance.set_attribute(key=field, value=field_value) + return instance, report + + +class PydElastic(BaseModel, extra="allow", arbitrary_types_allowed=True): + """Allows for creation of arbitrary pydantic models""" + instance: BaseClass + + @report_result + def to_sql(self): + print(self.instance) + fields = [item for item in self.model_extra] + for field in fields: + try: + field_type = getattr(self.instance.__class__, field).property + except AttributeError: + logger.error(f"No attribute: {field} in {self.instance.__class__}") + continue + match field_type: + case _RelationshipDeclared(): + logger.debug(f"{field} is a relationship with {field_type.entity.class_}") + field_value = field_type.entity.class_.argument.query(name=getattr(self, field)) + logger.debug(f"{field} query result: {field_value}") + case ColumnProperty(): + logger.debug(f"{field} is a property.") + field_value = getattr(self, field) + self.instance.__setattr__(field, field_value) + return self.instance + - return instance diff --git a/src/submissions/frontend/widgets/omni_add_edit.py b/src/submissions/frontend/widgets/omni_add_edit.py index 96eff45..5983229 100644 --- a/src/submissions/frontend/widgets/omni_add_edit.py +++ b/src/submissions/frontend/widgets/omni_add_edit.py @@ -1,6 +1,7 @@ """ 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 @@ -14,7 +15,6 @@ from sqlalchemy.orm import InstrumentedAttribute, ColumnProperty import logging from sqlalchemy.orm.relationships import _RelationshipDeclared from tools import Report, report_result -from backend import db logger = logging.getLogger(f"submissions.{__name__}") @@ -26,9 +26,11 @@ class AddEdit(QDialog): 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 @@ -38,8 +40,8 @@ class AddEdit(QDialog): self.buttonBox = QDialogButtonBox(QBtn) self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) - logger.debug(f"Fields: {pformat(self.instance.omnigui_dict)}") - fields = {k: v for k, v in self.instance.omnigui_dict.items() if "id" not in k} + # 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} @@ -67,12 +69,15 @@ class AddEdit(QDialog): @report_result def parse_form(self) -> Tuple[BaseModel, Report]: - from backend.validators import pydant 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) diff --git a/src/submissions/frontend/widgets/omni_manager.py b/src/submissions/frontend/widgets/omni_manager.py index fa51212..859eb4c 100644 --- a/src/submissions/frontend/widgets/omni_manager.py +++ b/src/submissions/frontend/widgets/omni_manager.py @@ -1,6 +1,8 @@ """ Provides a screen for managing all attributes of a database object. """ +from copy import deepcopy +from pprint import pformat from typing import Any, List from PyQt6.QtCore import QSortFilterProxyModel, Qt from PyQt6.QtGui import QAction, QCursor @@ -17,6 +19,8 @@ from sqlalchemy.orm.relationships import _RelationshipDeclared from pandas import DataFrame from backend import db import logging + +from tools import check_object_in_managers from .omni_add_edit import AddEdit from .omni_search import SearchBox from frontend.widgets.submission_table import pandasModel @@ -33,6 +37,7 @@ class ManagerWindow(QDialog): super().__init__(parent) self.object_type = self.original_type = object_type self.instance = None + # self.managers = deepcopy(managers) self.managers = managers try: self.managers.add(self.parent().instance) @@ -68,28 +73,35 @@ class ManagerWindow(QDialog): """ Changes form inputs based on sample type """ + # logger.debug(f"Instance: {self.instance}") if self.sub_class: self.object_type = getattr(db, self.sub_class.currentText()) - logger.debug(f"From update options, managers: {self.managers}") + # logger.debug(f"From update options, managers: {self.managers}") 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}") + # logger.debug(f"Couldn't set query kwargs due to: {e}") query_kwargs = {} - logger.debug(f"Query kwargs: {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}") + # logger.debug(f"self.object_type: {self.object_type}") if self.instance: - options.insert(0, options.pop(options.index(self.instance.name))) + try: + inserter = options.pop(options.index(self.instance.name)) + except ValueError: + inserter = self.instance.name + options.insert(0, inserter) self.options.clear() self.options.addItems(options) self.options.setEditable(False) self.options.setMinimumWidth(self.minimumWidth()) self.layout.addWidget(self.options, 1, 0, 1, 1) - self.add_button = QPushButton("Add New") - self.layout.addWidget(self.add_button, 1, 1, 1, 1) + if len(options) > 0: + self.add_button = QPushButton("Add New") + self.layout.addWidget(self.add_button, 1, 1, 1, 1) + self.add_button.clicked.connect(self.add_new) self.options.currentTextChanged.connect(self.update_data) - self.add_button.clicked.connect(self.add_new) + # logger.debug(f"Instance: {self.instance}") self.update_data() def update_data(self) -> None: @@ -105,26 +117,34 @@ class ManagerWindow(QDialog): [item for item in self.findChildren(QDialogButtonBox)] for item in deletes: item.setParent(None) + # logger.debug(f"Current options text lower: {self.options.currentText().lower()}") # NOTE: Find the instance this manager will update - logger.debug(f"Querying with {self.options.currentText()}") - self.instance = self.object_type.query(name=self.options.currentText(), limit=1) - 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}") + if "blank" not in self.options.currentText().lower() and self.options.currentText() != "": + # logger.debug(f"Querying with {self.options.currentText()}") + self.instance = self.object_type.query(name=self.options.currentText(), limit=1) + # logger.debug(f"Instance: {self.instance}") + if not self.instance: + self.instance = self.object_type() + # logger.debug(f"self.instance: {self.instance}") + fields = {k: v for k, v in self.instance.omnigui_instance_dict.items() if + isinstance(v['class_attr'], InstrumentedAttribute) and k != "id"} for key, field in fields.items(): + try: + value = getattr(self.instance, key) + except AttributeError: + value = None match field['class_attr'].property: # NOTE: ColumnProperties will be directly edited. case ColumnProperty(): # NOTE: field.property.expression.type gives db column type eg. STRING or TIMESTAMP widget = EditProperty(self, key=key, column_type=field, - value=getattr(self.instance, key)) + value=value) # NOTE: RelationshipDeclareds will be given a list of existing related objects. case _RelationshipDeclared(): if key != "submissions": # NOTE: field.comparator.entity.class_ gives the relationship class widget = EditRelationship(self, key=key, entity=field['class_attr'].comparator.entity.class_, - value=getattr(self.instance, key)) + value=value) else: continue case _: @@ -144,18 +164,28 @@ class ManagerWindow(QDialog): # TODO: Need Relationship property here too? results = [item.parse_form() for item in self.findChildren(EditProperty)] for result in results: - # logger.debug(result) - self.instance.__setattr__(result[0], result[1]) + logger.debug(f"Incoming result: {result}") + setattr(self.instance, result['field'], result['value']) + logger.debug(f"Set result: {getattr(self.instance, result['field'])}") + results = [item.parse_form() for item in self.findChildren(EditRelationship)] + for result in results: + logger.debug(f"Incoming result: {result}") + setattr(self.instance, result['field'], result['value']) + logger.debug(f"Set result: {getattr(self.instance, result['field'])}") return self.instance def add_new(self): - dlg = AddEdit(parent=self, instance=self.object_type(), managers=self.managers) - if dlg.exec(): - new_pyd = dlg.parse_form() - new_instance = new_pyd.to_sql() - new_instance.save() - self.instance = new_instance - self.update_options() + # dlg = AddEdit(parent=self, instance=self.object_type(), managers=self.managers) + # if dlg.exec(): + # new_pyd = dlg.parse_form() + # new_instance = new_pyd.to_sql() + # # new_instance.save() + # logger.debug(f"New instance: {new_instance}") + # self.instance = new_instance + # self.update_options() + new_instance = self.object_type() + self.instance = new_instance + self.update_options() class EditProperty(QWidget): @@ -165,7 +195,8 @@ class EditProperty(QWidget): self.label = QLabel(key.title().replace("_", " ")) self.layout = QGridLayout() self.layout.addWidget(self.label, 0, 0, 1, 1) - logger.debug(f"Column type: {column_type}") + self.setObjectName(key) + # logger.debug(f"Column type: {column_type}") match column_type['class_attr'].property.expression.type: case String(): self.widget = QLineEdit(self) @@ -203,35 +234,51 @@ class EditProperty(QWidget): value = self.widget.text() case QDateEdit(): value = self.widget.date() + case QSpinBox() | QDoubleSpinBox(): + value = self.widget.value() + case QCheckBox(): + value = self.widget.isChecked() case _: value = None - return self.objectName(), value + return dict(field=self.objectName(), value=value) class EditRelationship(QWidget): def __init__(self, parent, key: str, entity: Any, value): super().__init__(parent) - self.entity = entity #: The class of interest - self.data = value + self.entity = entity #: The class of interest + self.setParent(parent) + # logger.debug(f"Edit relationship entity: {self.entity}") self.label = QLabel(key.title().replace("_", " ")) - self.setObjectName(key) - self.table = QTableView() + self.setObjectName(key) #: key is the name of the relationship this represents + self.relationship = getattr(self.parent().instance.__class__, key) + # logger.debug(f"self.relationship: {self.relationship}") + # logger.debug(f"Relationship uses list: {self.relationship.property.uselist}") + self.data = value + # logger.debug(f"Data for edit relationship: {self.data}") + self.widget = QTableView() + self.set_data() self.add_button = QPushButton("Add New") self.add_button.clicked.connect(self.add_new) self.existing_button = QPushButton("Add Existing") self.existing_button.clicked.connect(self.add_existing) - self.existing_button.setEnabled(self.entity.level == 1) + # self.existing_button.setEnabled(self.entity.level == 1) + if not self.relationship.property.uselist and len(self.data) >= 1: + self.add_button.setEnabled(False) + self.existing_button.setEnabled(False) + logger.debug(f"Checked manager for check: {check_object_in_managers(self.parent().managers, self.objectName())}") + if check_object_in_managers(self.parent().managers, self.objectName()): + self.widget.setEnabled(False) + self.add_button.setEnabled(False) + self.existing_button.setEnabled(False) self.layout = QGridLayout() self.layout.addWidget(self.label, 0, 0, 1, 5) - self.layout.addWidget(self.table, 1, 0, 1, 8) + self.layout.addWidget(self.widget, 1, 0, 1, 8) self.layout.addWidget(self.add_button, 0, 6, 1, 1, alignment=Qt.AlignmentFlag.AlignRight) self.layout.addWidget(self.existing_button, 0, 7, 1, 1, alignment=Qt.AlignmentFlag.AlignRight) self.setLayout(self.layout) - self.set_data() - self.table.resizeColumnsToContents() - self.table.resizeRowsToContents() - self.table.setSortingEnabled(True) + # self.set_data() def parse_row(self, x): context = {item: x.sibling(x.row(), self.data.columns.get_loc(item)).data() for item in self.data.columns} @@ -239,34 +286,42 @@ class EditRelationship(QWidget): object = self.entity.query(**context) except KeyError: object = None - self.table.doubleClicked.disconnect() + self.widget.doubleClicked.disconnect() self.add_edit(instance=object) def add_new(self, instance: Any = None): # NOTE: if an existing instance is not being edited, create a new instance if not instance: instance = self.entity() - # 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 + logger.debug(f"Managers going into add new: {managers}") + # 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 + dlg = ManagerWindow(self.parent(), object_type=instance.__class__, extras=[], managers=managers) if dlg.exec(): new_instance = dlg.parse_form() - new_instance = new_instance.to_sql() - logger.debug(f"New instance: {new_instance}") - addition = getattr(self.parent().instance, self.objectName()) - logger.debug(f"Addition: {addition}") + logger.debug(f"New instance before transformation attempt: {new_instance}") + try: + new_instance = new_instance.to_sql() + except AttributeError as e: + logger.error(f"Couldn't convert {new_instance} to sql due to {e}") + logger.debug(f"New instance after transformation attempt: {new_instance.__dict__}") + # addition = getattr(self.parent().instance, self.objectName()) + # logger.debug(f"Addition: {addition}") + # if self.relationship.property.uselist: + # addition.append(instance) + # else: + # addition = instance + # setattr(self.parent().instance, self.objectName(), new_instance) + logger.debug(f"Parent instance after insert: {getattr(self.parent().instance, self.objectName())}") # NOTE: Saving currently disabled - # if isinstance(addition, InstrumentedList): - # addition.append(new_instance) # self.parent().instance.save() + return new_instance self.parent().update_data() def add_existing(self): @@ -274,16 +329,24 @@ class EditRelationship(QWidget): if dlg.exec(): rows = dlg.return_selected_rows() for row in rows: - logger.debug(f"Querying with {row}") + # logger.debug(f"Querying with {row}") instance = self.entity.query(**row) - logger.debug(f"Queried instance: {instance}") - addition = getattr(self.parent().instance, self.objectName()) - logger.debug(f"Addition: {addition}") + # logger.debug(f"Queried instance: {instance}") + # logger.debug(f"Checking field type: {self.objectName()}") + # addition = getattr(self.parent().instance, self.objectName()) + # logger.debug(f"Instance object: {addition}") # NOTE: Saving currently disabled - # if isinstance(addition, InstrumentedList): + # if self.relationship.property.uselist: # addition.append(instance) + # else: + # addition = instance + setattr(self.parent().instance, self.objectName(), instance) # self.parent().instance.save() - self.parent().update_data() + # self.parent().update_data() + # yield instance + + def set_choices(self) -> None: + pass def set_data(self) -> None: """ @@ -291,26 +354,38 @@ class EditRelationship(QWidget): """ # logger.debug(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) + if self.data is not None: + self.data = [self.data] + else: + self.data = [] + checked_manager = check_object_in_managers(self.parent().managers, self.objectName()) + logger.debug(f"Returned checked_manager: {checked_manager}") + if checked_manager is not None: + if not self.data: + self.data = [checked_manager] + # logger.debug(f"Data: {self.data}") try: - self.columns_of_interest = [dict(name=item, column=self.data.columns.get_loc(item)) for item in self.extras] + records = [{k: v['instance_attr'] for k, v in item.omnigui_instance_dict.items()} for item in self.data] + except AttributeError: + records = [] + # logger.debug(f"Records: {records}") + self.df = DataFrame.from_records(records) + try: + self.columns_of_interest = [dict(name=item, column=self.df.columns.get_loc(item)) for item in self.extras] except (KeyError, AttributeError): self.columns_of_interest = [] try: - self.data['id'] = self.data['id'].apply(str) - self.data['id'] = self.data['id'].str.zfill(4) + self.df['id'] = self.df['id'].apply(str) + self.df['id'] = self.df['id'].str.zfill(4) except KeyError as e: logger.error(f"Could not alter id to string due to KeyError: {e}") proxy_model = QSortFilterProxyModel() - proxy_model.setSourceModel(pandasModel(self.data)) - self.table.setModel(proxy_model) - self.table.resizeColumnsToContents() - self.table.resizeRowsToContents() - self.table.setSortingEnabled(True) - self.table.doubleClicked.connect(self.parse_row) + proxy_model.setSourceModel(pandasModel(self.df)) + self.widget.setModel(proxy_model) + self.widget.resizeColumnsToContents() + self.widget.resizeRowsToContents() + self.widget.setSortingEnabled(True) + self.widget.doubleClicked.connect(self.parse_row) def contextMenuEvent(self, event): """ @@ -319,16 +394,23 @@ class EditRelationship(QWidget): Args: event (_type_): the item of interest """ - id = self.table.selectionModel().currentIndex() + print(self.widget.isEnabled()) + if not self.widget.isEnabled(): + logger.warning(f"{self.objectName()} is disabled.") + return + id = self.widget.selectionModel().currentIndex() # NOTE: the overly complicated {column_name: row_value} dictionary construction - row_data = {self.data.columns[column]: self.table.model().index(id.row(), column).data() for column in - range(self.table.model().columnCount())} + row_data = {self.df.columns[column]: self.widget.model().index(id.row(), column).data() for column in + range(self.widget.model().columnCount())} object = self.entity.query(**row_data) if isinstance(object, list): object = object[0] - logger.debug(object) + # logger.debug(object) self.menu = QMenu(self) - action = QAction(f"Remove {object.name}", self) + try: + action = QAction(f"Remove {object.name}", self) + except AttributeError: + action = QAction(f"Remove object", self) action.triggered.connect(lambda: self.remove_item(object=object)) self.menu.addAction(action) self.menu.popup(QCursor.pos()) @@ -338,3 +420,6 @@ class EditRelationship(QWidget): editor.remove(object) self.parent().instance.save() self.parent().update_data() + + def parse_form(self): + return dict(field=self.objectName(), value=self.data) diff --git a/src/submissions/frontend/widgets/submission_widget.py b/src/submissions/frontend/widgets/submission_widget.py index 4e96756..1339671 100644 --- a/src/submissions/frontend/widgets/submission_widget.py +++ b/src/submissions/frontend/widgets/submission_widget.py @@ -341,7 +341,7 @@ class SubmissionFormWidget(QWidget): # result = self.pyd.check_reagent_expiries(exempt=exempt) if len(result.results) > 0: return report - base_submission, result = self.pyd.to_sql() + base_submission = self.pyd.to_sql() # NOTE: check output message for issues try: trigger = result.results[-1] @@ -693,7 +693,7 @@ class SubmissionFormWidget(QWidget): # NOTE: Since this now gets passed in directly from the parser -> pyd -> form and the parser gets the name from the db, it should no longer be necessary to query the db with reagent/kit, but with rt name directly. rt = ReagentRole.query(name=self.reagent.role) if rt is None: - rt = ReagentRole.query(kit_type=self.extraction_kit, reagent=wanted_reagent) + rt = ReagentRole.query(kittype=self.extraction_kit, reagent=wanted_reagent) final = PydReagent(name=wanted_reagent.name, lot=wanted_reagent.lot, role=rt.name, expiry=wanted_reagent.expiry.date(), missing=False) return final, report @@ -733,8 +733,8 @@ class SubmissionFormWidget(QWidget): def __init__(self, scrollWidget, reagent, extraction_kit: str) -> None: super().__init__(scrollWidget=scrollWidget) self.setEditable(True) - looked_up_rt = KitTypeReagentRoleAssociation.query(reagent_role=reagent.role, - kit_type=extraction_kit) + looked_up_rt = KitTypeReagentRoleAssociation.query(reagentrole=reagent.role, + kittype=extraction_kit) relevant_reagents = [str(item.lot) for item in looked_up_rt.get_all_relevant_reagents()] # NOTE: if reagent in sheet is not found insert it into the front of relevant reagents so it shows if str(reagent.lot) not in relevant_reagents: diff --git a/src/submissions/tools/__init__.py b/src/submissions/tools/__init__.py index 420b1a5..ad89a7a 100644 --- a/src/submissions/tools/__init__.py +++ b/src/submissions/tools/__init__.py @@ -16,11 +16,14 @@ from dateutil.easter import easter from jinja2 import Environment, FileSystemLoader from logging import handlers from pathlib import Path -from sqlalchemy.orm import Session +from sqlalchemy.orm import Session, InstrumentedAttribute from sqlalchemy import create_engine, text, MetaData from pydantic import field_validator, BaseModel, Field from pydantic_settings import BaseSettings, SettingsConfigDict from typing import Any, Tuple, Literal, List, Generator + +from sqlalchemy.orm.relationships import _RelationshipDeclared + from __init__ import project_path from configparser import ConfigParser from tkinter import Tk # NOTE: This is for choosing database path before app is created. @@ -813,6 +816,37 @@ def setup_lookup(func): return wrapper +def check_object_in_managers(managers: list, object_name: object): + for manager in managers: + logger.debug(f"Manager: {manager}, Key: {object_name}") + if object_name in manager.aliases: + return manager + relationships = [getattr(manager.__class__, item) for item in dir(manager.__class__) + if isinstance(getattr(manager.__class__, item), InstrumentedAttribute)] + relationships = [item for item in relationships if isinstance(item.property, _RelationshipDeclared)] + for relationship in relationships: + if relationship.key == object_name: + logger.debug(f"Checking {relationship.key}") + try: + rel_obj = getattr(manager, relationship.key) + if rel_obj is not None: + logger.debug(f"Returning {rel_obj}") + return rel_obj + except AttributeError: + pass + if "association" in relationship.key: + try: + logger.debug(f"Checking association {relationship.key}") + rel_obj = next((getattr(item, object_name) for item in getattr(manager, relationship.key) + if getattr(item, object_name) is not None), None) + if rel_obj is not None: + logger.debug(f"Returning {rel_obj}") + return rel_obj + except AttributeError: + pass + return None + + def get_application_from_parent(widget): try: return widget.app