Attempt to cleanup imports.

This commit is contained in:
lwark
2025-09-12 10:14:53 -05:00
parent ba4912cab7
commit 11abaafcfc
23 changed files with 322 additions and 323 deletions

View File

@@ -1,6 +1,32 @@
""" """
Contains database, validators and excel operations. Contains database, validators and excel operations.
""" """
from .db import * from .db import (
from .excel import * set_sqlite_pragma,
from .validators import * LogMixin, ConfigItem,
AuditLog,
ControlType, Control,
ClientLab, Contact,
ReagentRole, Reagent, ReagentLot, Discount, SubmissionType, ProcedureType, Procedure, ProcedureTypeReagentRoleAssociation,
ProcedureReagentLotAssociation, EquipmentRole, Equipment, EquipmentRoleEquipmentAssociation, Process, ProcessVersion,
Tips, TipsLot, ProcedureEquipmentAssociation,
ProcedureTypeEquipmentRoleAssociation, Results,
ClientSubmission, Run, Sample, ClientSubmissionSampleAssociation, RunSampleAssociation, ProcedureSampleAssociation,
update_log
)
from .excel import (
DefaultParser, DefaultKEYVALUEParser, DefaultTABLEParser, ProcedureInfoParser, ProcedureSampleParser,
ProcedureReagentParser, ProcedureEquipmentParser, DefaultResultsInfoParser, DefaultResultsSampleParser,
PCRSampleParser, PCRInfoParser, ClientSubmissionSampleParser, ClientSubmissionInfoParser, PCRInfoParser,
PCRSampleParser,
DefaultWriter, DefaultKEYVALUEWriter, DefaultTABLEWriter,
ProcedureInfoWriter, ProcedureSampleWriter, ProcedureReagentWriter, ProcedureEquipmentWriter,
PCRInfoWriter, PCRSampleWriter,
ClientSubmissionInfoWriter, ClientSubmissionSampleWriter,
ReportArchetype, ReportMaker, TurnaroundMaker, ConcentrationMaker, ChartReportMaker
)
from .validators import (
DefaultNamer, ClientSubmissionNamer, RSLNamer,
PydRun, PydContact, PydClientLab, PydSample, PydReagent, PydReagentRole, PydEquipment, PydEquipmentRole, PydTips,
PydProcess, PydElastic, PydClientSubmission, PydProcedure, PydResults, PydReagentLot
)

View File

@@ -1,9 +1,14 @@
""" """
All database related operations. All database related operations.
""" """
from datetime import datetime
from getpass import getuser
from sqlalchemy import event, inspect from sqlalchemy import event, inspect
from sqlalchemy.engine import Engine from sqlalchemy.engine import Engine
from tools import ctx from tools import ctx
import logging
logger = logging.getLogger(f"submissions.{__name__}")
@event.listens_for(Engine, "connect") @event.listens_for(Engine, "connect")
@@ -28,7 +33,17 @@ def set_sqlite_pragma(dbapi_connection, connection_record):
cursor.close() cursor.close()
from .models import * from .models import (
LogMixin, ConfigItem,
AuditLog,
ReagentRole, Reagent, ReagentLot, Discount, SubmissionType, ProcedureType, Procedure, ProcedureTypeReagentRoleAssociation,
ProcedureReagentLotAssociation, EquipmentRole, Equipment, EquipmentRoleEquipmentAssociation, Process, ProcessVersion,
Tips, TipsLot, ProcedureEquipmentAssociation,
ProcedureTypeEquipmentRoleAssociation, Results,
ClientSubmission, Run, Sample, ClientSubmissionSampleAssociation, RunSampleAssociation, ProcedureSampleAssociation,
ControlType, Control,
ClientLab, Contact
)
def update_log(mapper, connection, target): def update_log(mapper, connection, target):

View File

@@ -3,11 +3,14 @@ Contains all models for sqlalchemy
""" """
from __future__ import annotations from __future__ import annotations
import sys, logging, json, inspect import sys, logging, json, inspect
from datetime import datetime, date
from pprint import pformat
from dateutil.parser import parse from dateutil.parser import parse
from jinja2 import TemplateNotFound from jinja2 import TemplateNotFound, Template
from pandas import DataFrame from pandas import DataFrame
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import Column, INTEGER, String, JSON, DATETIME from sqlalchemy import Column, INTEGER, String, JSON, TIMESTAMP
from sqlalchemy.ext.associationproxy import AssociationProxy from sqlalchemy.ext.associationproxy import AssociationProxy
from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query, Session, InstrumentedAttribute, ColumnProperty from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query, Session, InstrumentedAttribute, ColumnProperty
from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import declared_attr
@@ -16,7 +19,7 @@ from sqlalchemy.exc import ArgumentError
from typing import Any, List, ClassVar from typing import Any, List, ClassVar
from pathlib import Path from pathlib import Path
from sqlalchemy.orm.relationships import _RelationshipDeclared from sqlalchemy.orm.relationships import _RelationshipDeclared
from tools import report_result, list_sort_dict from tools import report_result, list_sort_dict, jinja_template_loading, Report, Result
# NOTE: Load testing environment # NOTE: Load testing environment
if 'pytest' in sys.modules: if 'pytest' in sys.modules:
@@ -46,7 +49,9 @@ class BaseClass(Base):
except AttributeError: except AttributeError:
return f"<{self.__class__.__name__}(Name Unavailable)>" return f"<{self.__class__.__name__}(Name Unavailable)>"
@classproperty # @classproperty
@classmethod
@declared_attr
def aliases(cls) -> List[str]: def aliases(cls) -> List[str]:
""" """
List of other names this class might be known by. List of other names this class might be known by.
@@ -56,7 +61,8 @@ class BaseClass(Base):
""" """
return [cls.query_alias] return [cls.query_alias]
@classproperty @classmethod
@declared_attr
def query_alias(cls) -> str: def query_alias(cls) -> str:
""" """
What to query this class as. What to query this class as.
@@ -126,7 +132,9 @@ class BaseClass(Base):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._misc_info = dict() self._misc_info = dict()
@classproperty # @classproperty
@classmethod
@declared_attr
def jsons(cls) -> List[str]: def jsons(cls) -> List[str]:
""" """
Get list of JSON db columns Get list of JSON db columns
@@ -139,7 +147,9 @@ class BaseClass(Base):
except AttributeError: except AttributeError:
return [] return []
@classproperty # @classproperty
@classmethod
@declared_attr
def timestamps(cls) -> List[str]: def timestamps(cls) -> List[str]:
""" """
Get list of TIMESTAMP columns Get list of TIMESTAMP columns
@@ -169,9 +179,6 @@ class BaseClass(Base):
continue continue
if not isinstance(item[1].property, ColumnProperty): if not isinstance(item[1].property, ColumnProperty):
continue continue
# if isinstance(item[1], _RelationshipDeclared):
# if "association" in item[0]:
# continue
if len(item[1].foreign_keys) > 0: if len(item[1].foreign_keys) > 0:
continue continue
if item[1].type.__class__.__name__ not in ["String"]: if item[1].type.__class__.__name__ not in ["String"]:
@@ -245,14 +252,15 @@ class BaseClass(Base):
""" """
if not objects: if not objects:
try: try:
records = [obj.details_dict(**kwargs) for obj in cls.query()] q = cls.query()
except AttributeError: except AttributeError:
records = [obj.details_dict(**kwargs) for obj in cls.query(page_size=0)] q = cls.query(page_size=0)
else: else:
try: q = objects
records = [obj.to_sub_dict(**kwargs) for obj in objects] records = []
except AttributeError: for obj in q:
records = [{k: v['instance_attr'] for k, v in obj.omnigui_instance_dict.items()} for obj in objects] dicto = obj.details_dict(**kwargs)
records.append({key: value for key, value in dicto.items() if key not in dicto['excluded']})
return DataFrame.from_records(records) return DataFrame.from_records(records)
@classmethod @classmethod
@@ -397,7 +405,9 @@ class BaseClass(Base):
pass pass
return dicto return dicto
@classproperty # @classproperty
@classmethod
@declared_attr
def pydantic_model(cls) -> BaseModel: def pydantic_model(cls) -> BaseModel:
""" """
Gets the pydantic model corresponding to this object. Gets the pydantic model corresponding to this object.
@@ -410,10 +420,15 @@ class BaseClass(Base):
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.") logger.warning(f"Couldn't get {cls.__name__} pydantic model.")
return pydant.PydElastic try:
model = getattr(pydant, f"Pyd{cls.pyd_model_name}")
except AttributeError:
return pydant.PydElastic
return model return model
@classproperty # @classproperty
@classmethod
@declared_attr
def add_edit_tooltips(cls) -> dict: def add_edit_tooltips(cls) -> dict:
""" """
Gets tooltips for Omni-add-edit Gets tooltips for Omni-add-edit
@@ -423,7 +438,9 @@ class BaseClass(Base):
""" """
return dict() return dict()
@classproperty # @classproperty
@classmethod
@declared_attr
def details_template(cls) -> Template: def details_template(cls) -> Template:
""" """
Get the details jinja template for the correct class Get the details jinja template for the correct class
@@ -609,6 +626,11 @@ class BaseClass(Base):
value = getattr(self, k) value = getattr(self, k)
except AttributeError: except AttributeError:
continue continue
match value:
case str():
value = value.strip('\"')
case _:
pass
output[k.strip("_")] = value output[k.strip("_")] = value
if self._misc_info: if self._misc_info:
for key, value in self._misc_info.items(): for key, value in self._misc_info.items():
@@ -718,12 +740,20 @@ class ConfigItem(BaseClass):
return config_items return config_items
from .controls import *
# NOTE: import order must go: orgs, kittype, run due to circular import issues # NOTE: import order must go: orgs, kittype, run due to circular import issues
from .organizations import *
from .procedures import *
from .submissions import *
from .audit import AuditLog from .audit import AuditLog
from .organizations import (
ClientLab, Contact, BaseClass # NOTE: For some reason I need to import BaseClass at this point for queries to work.
)
from .procedures import (
ReagentRole, Reagent, ReagentLot, Discount, SubmissionType, ProcedureType, Procedure, ProcedureTypeReagentRoleAssociation,
ProcedureReagentLotAssociation, EquipmentRole, Equipment, EquipmentRoleEquipmentAssociation, Process, ProcessVersion,
Tips, TipsLot, ProcedureEquipmentAssociation, ProcedureTypeEquipmentRoleAssociation, Results
)
from .submissions import (
ClientSubmission, Run, Sample, ClientSubmissionSampleAssociation, RunSampleAssociation, ProcedureSampleAssociation
)
from .controls import ControlType, Control
# NOTE: Add a creator to the procedure for reagent association. Assigned here due to circular import constraints. # NOTE: Add a creator to the procedure for reagent association. Assigned here due to circular import constraints.
# https://docs.sqlalchemy.org/en/20/orm/extensions/associationproxy.html#sqlalchemy.ext.associationproxy.association_proxy.params.creator # https://docs.sqlalchemy.org/en/20/orm/extensions/associationproxy.html#sqlalchemy.ext.associationproxy.association_proxy.params.creator

View File

@@ -11,6 +11,7 @@ import logging
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
# NOTE: Need a seperate base for this.
Base: DeclarativeMeta = declarative_base() Base: DeclarativeMeta = declarative_base()
class AuditLog(Base): class AuditLog(Base):

View File

@@ -4,8 +4,8 @@ All client organization related models.
from __future__ import annotations from __future__ import annotations
import logging import logging
from sqlalchemy import Column, String, INTEGER, ForeignKey, Table from sqlalchemy import Column, String, INTEGER, ForeignKey, Table
from sqlalchemy.orm import relationship, Query from sqlalchemy.orm import relationship, Query, declared_attr
from . import Base, BaseClass from . import BaseClass
from tools import check_authorization, setup_lookup from tools import check_authorization, setup_lookup
from typing import List from typing import List
@@ -14,7 +14,8 @@ logger = logging.getLogger(f"submissions.{__name__}")
# table containing clientlab/contact relationship # table containing clientlab/contact relationship
clientlab_contact = Table( clientlab_contact = Table(
"_clientlab_contact", "_clientlab_contact",
Base.metadata, # Base.metadata,
BaseClass.__base__.metadata,
Column("clientlab_id", INTEGER, ForeignKey("_clientlab.id")), Column("clientlab_id", INTEGER, ForeignKey("_clientlab.id")),
Column("contact_id", INTEGER, ForeignKey("_contact.id")), Column("contact_id", INTEGER, ForeignKey("_contact.id")),
extend_existing=True extend_existing=True
@@ -98,7 +99,9 @@ class Contact(BaseClass):
secondary=clientlab_contact) #: relationship to joined clientlab secondary=clientlab_contact) #: relationship to joined clientlab
clientsubmission = relationship("ClientSubmission", back_populates="contact") #: procedure this contact has submitted clientsubmission = relationship("ClientSubmission", back_populates="contact") #: procedure this contact has submitted
@classproperty # @classproperty
@classmethod
@declared_attr
def searchables(cls): def searchables(cls):
return [] return []

View File

@@ -7,13 +7,13 @@ from operator import itemgetter
from pprint import pformat from pprint import pformat
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, func from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, func
from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import relationship, validates, Query from sqlalchemy.orm import relationship, validates, Query, declared_attr
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from tools import check_authorization, setup_lookup, Report, Result, check_regex_match, timezone, \ from tools import check_authorization, setup_lookup, Report, Result, check_regex_match, timezone, \
jinja_template_loading, flatten_list jinja_template_loading, flatten_list
from typing import List, Literal, Generator, Any, Tuple, TYPE_CHECKING from typing import List, Literal, Generator, Any, Tuple, TYPE_CHECKING
from . import Base, BaseClass, ClientLab, LogMixin from . import BaseClass, ClientLab, LogMixin
from sqlalchemy.exc import OperationalError as AlcOperationalError, IntegrityError as AlcIntegrityError from sqlalchemy.exc import OperationalError as AlcOperationalError, IntegrityError as AlcIntegrityError
from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError
@@ -25,7 +25,7 @@ logger = logging.getLogger(f'submissions.{__name__}')
reagentrole_reagent = Table( reagentrole_reagent = Table(
"_reagentrole_reagent", "_reagentrole_reagent",
Base.metadata, BaseClass.__base__.metadata,
Column("reagent_id", INTEGER, ForeignKey("_reagent.id")), Column("reagent_id", INTEGER, ForeignKey("_reagent.id")),
Column("reagentrole_id", INTEGER, ForeignKey("_reagentrole.id")), Column("reagentrole_id", INTEGER, ForeignKey("_reagentrole.id")),
extend_existing=True extend_existing=True
@@ -33,7 +33,7 @@ reagentrole_reagent = Table(
equipment_process = Table( equipment_process = Table(
"_equipment_process", "_equipment_process",
Base.metadata, BaseClass.__base__.metadata,
Column("process_id", INTEGER, ForeignKey("_process.id")), Column("process_id", INTEGER, ForeignKey("_process.id")),
Column("equipment_id", INTEGER, ForeignKey("_equipment.id")), Column("equipment_id", INTEGER, ForeignKey("_equipment.id")),
extend_existing=True extend_existing=True
@@ -41,7 +41,7 @@ equipment_process = Table(
process_tips = Table( process_tips = Table(
"_process_tips", "_process_tips",
Base.metadata, BaseClass.__base__.metadata,
Column("process_id", INTEGER, ForeignKey("_process.id")), Column("process_id", INTEGER, ForeignKey("_process.id")),
Column("tips_id", INTEGER, ForeignKey("_tips.id")), Column("tips_id", INTEGER, ForeignKey("_tips.id")),
extend_existing=True extend_existing=True
@@ -49,7 +49,7 @@ process_tips = Table(
submissiontype_proceduretype = Table( submissiontype_proceduretype = Table(
"_submissiontype_proceduretype", "_submissiontype_proceduretype",
Base.metadata, BaseClass.__base__.metadata,
Column("submissiontype_id", INTEGER, ForeignKey("_submissiontype.id")), Column("submissiontype_id", INTEGER, ForeignKey("_submissiontype.id")),
Column("proceduretype_id", INTEGER, ForeignKey("_proceduretype.id")), Column("proceduretype_id", INTEGER, ForeignKey("_proceduretype.id")),
extend_existing=True extend_existing=True
@@ -217,7 +217,8 @@ class Reagent(BaseClass, LogMixin):
self.name = name self.name = name
self.eol_ext = eol_ext self.eol_ext = eol_ext
@classproperty @classmethod
@declared_attr
def searchables(cls): def searchables(cls):
return [dict(label="Lot", field="lot")] return [dict(label="Lot", field="lot")]
@@ -322,7 +323,8 @@ class Reagent(BaseClass, LogMixin):
@classproperty @classmethod
@declared_attr
def add_edit_tooltips(self): def add_edit_tooltips(self):
return dict( return dict(
expiry="Use exact date on reagent.\nEOL will be calculated from kittype automatically" expiry="Use exact date on reagent.\nEOL will be calculated from kittype automatically"
@@ -342,6 +344,9 @@ class Reagent(BaseClass, LogMixin):
class ReagentLot(BaseClass): class ReagentLot(BaseClass):
pyd_model_name = "Reagent"
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
lot = Column(String(64), unique=True) #: lot number of reagent lot = Column(String(64), unique=True) #: lot number of reagent
expiry = Column(TIMESTAMP) #: expiry date - extended by eol_ext of parent programmatically expiry = Column(TIMESTAMP) #: expiry date - extended by eol_ext of parent programmatically
@@ -368,7 +373,7 @@ class ReagentLot(BaseClass):
def query(cls, def query(cls,
lot: str | None = None, lot: str | None = None,
name: str | None = None, name: str | None = None,
limit: int = 1, limit: int = 0,
**kwargs) -> ReagentLot | List[ReagentLot]: **kwargs) -> ReagentLot | List[ReagentLot]:
""" """
@@ -386,6 +391,7 @@ class ReagentLot(BaseClass):
match lot: match lot:
case str(): case str():
query = query.filter(cls.lot == lot) query = query.filter(cls.lot == lot)
limit = 1
case _: case _:
pass pass
match name: match name:
@@ -414,13 +420,28 @@ class ReagentLot(BaseClass):
@check_authorization @check_authorization
def edit_from_search(self, obj, **kwargs): def edit_from_search(self, obj, **kwargs):
from frontend.widgets.omni_add_edit import AddEdit from frontend.widgets.omni_add_edit import AddEdit
from backend.validators.pydant import PydElastic
dlg = AddEdit(parent=None, instance=self, disabled=['reagent']) dlg = AddEdit(parent=None, instance=self, disabled=['reagent'])
if dlg.exec(): if dlg.exec():
pyd = dlg.parse_form() pyd = dlg.parse_form()
for field in pyd.model_fields: logger.debug(f"Pydantic returned: {type(pyd)} {pyd.model_fields}")
self.set_attribute(field, pyd.__getattribute__(field)) fields = pyd.model_fields
if isinstance(pyd, PydElastic):
fields.update(pyd.model_extra)
for field in fields:
if field in ['instance']:
continue
field_value = pyd.__getattribute__(field)
logger.debug(f"Setting {field} in Reagent Lot to {field_value}")
self.set_attribute(field, field_value)
self.save() self.save()
def details_dict(self, **kwargs) -> dict:
output = super().details_dict(**kwargs)
output['excluded'] += ["reagentlotprocedureassociation", "procedures"]
output['reagent'] = output['reagent'].name
return output
class Discount(BaseClass): class Discount(BaseClass):
""" """
Relationship table for client labs for certain kits. Relationship table for client labs for certain kits.
@@ -508,7 +529,8 @@ class SubmissionType(BaseClass):
""" """
return f"<SubmissionType({self.name})>" return f"<SubmissionType({self.name})>"
@classproperty @classmethod
@declared_attr
def aliases(cls) -> List[str]: def aliases(cls) -> List[str]:
""" """
Gets other names the sql object of this class might go by. Gets other names the sql object of this class might go by.
@@ -604,12 +626,14 @@ class SubmissionType(BaseClass):
sample_map=self.sample_map sample_map=self.sample_map
) )
@classproperty @classmethod
@declared_attr
def info_map_json_edit_fields(cls): def info_map_json_edit_fields(cls):
dicto = dict() dicto = dict()
return dicto return dicto
@classproperty @classmethod
@declared_attr
def regex(cls) -> re.Pattern: def regex(cls) -> re.Pattern:
""" """
Constructs catchall regex. Constructs catchall regex.
@@ -1182,7 +1206,8 @@ class ProcedureTypeReagentRoleAssociation(BaseClass):
dicto['required']['instance_attr'] = bool(dicto['required']['instance_attr']) dicto['required']['instance_attr'] = bool(dicto['required']['instance_attr'])
return dicto return dicto
@classproperty @classmethod
@declared_attr
def json_edit_fields(cls) -> dict: def json_edit_fields(cls) -> dict:
dicto = dict( dicto = dict(
sheet="str", sheet="str",
@@ -1607,7 +1632,8 @@ class Equipment(BaseClass, LogMixin):
creation_dict['equipmentrole'] = equipmentrole or creation_dict['equipmentrole'] creation_dict['equipmentrole'] = equipmentrole or creation_dict['equipmentrole']
return PydEquipment(**creation_dict) return PydEquipment(**creation_dict)
@classproperty @classmethod
@declared_attr
def manufacturer_regex(cls) -> re.Pattern: def manufacturer_regex(cls) -> re.Pattern:
""" """
Creates regex to determine tip manufacturer Creates regex to determine tip manufacturer

View File

@@ -11,9 +11,9 @@ from pprint import pformat
from pandas import DataFrame from pandas import DataFrame
from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.ext.hybrid import hybrid_property
from frontend.widgets.functions import select_save_file from frontend.widgets.functions import select_save_file
from . import Base, BaseClass, SubmissionType, ClientLab, Contact, LogMixin, Procedure from . import BaseClass, SubmissionType, ClientLab, Contact, LogMixin, Procedure
from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case, func from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case, func
from sqlalchemy.orm import relationship, Query from sqlalchemy.orm import relationship, Query, declared_attr
from sqlalchemy.orm.attributes import flag_modified from sqlalchemy.orm.attributes import flag_modified
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.exc import OperationalError as AlcOperationalError, IntegrityError as AlcIntegrityError, StatementError from sqlalchemy.exc import OperationalError as AlcOperationalError, IntegrityError as AlcIntegrityError, StatementError
@@ -947,7 +947,8 @@ class Run(BaseClass, LogMixin):
# NOTE: Polymorphic functions # NOTE: Polymorphic functions
@classproperty @classmethod
@declared_attr
def regex(cls) -> re.Pattern: def regex(cls) -> re.Pattern:
""" """
Constructs catchall regex. Constructs catchall regex.
@@ -1297,7 +1298,8 @@ class Sample(BaseClass, LogMixin):
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<Sample({self.sample_id})>" return f"<Sample({self.sample_id})>"
@classproperty @classmethod
@declared_attr
def searchables(cls): def searchables(cls):
return [dict(label="Submitter ID", field="sample_id")] return [dict(label="Submitter ID", field="sample_id")]

View File

@@ -2,4 +2,18 @@
Contains pandas and openpyxl convenience functions for interacting with excel workbooks Contains pandas and openpyxl convenience functions for interacting with excel workbooks
""" """
from backend.excel.parsers.clientsubmission_parser import ClientSubmissionInfoParser, ClientSubmissionSampleParser # from backend.excel.parsers.clientsubmission_parser import ClientSubmissionInfoParser, ClientSubmissionSampleParser
from .parsers import (
DefaultParser, DefaultKEYVALUEParser, DefaultTABLEParser,
ProcedureInfoParser, ProcedureSampleParser, ProcedureReagentParser, ProcedureEquipmentParser,
DefaultResultsInfoParser, DefaultResultsSampleParser, PCRSampleParser, PCRInfoParser,
ClientSubmissionSampleParser, ClientSubmissionInfoParser,
PCRInfoParser, PCRSampleParser
)
from .writers import (
DefaultWriter, DefaultKEYVALUEWriter, DefaultTABLEWriter,
ProcedureInfoWriter, ProcedureSampleWriter, ProcedureReagentWriter, ProcedureEquipmentWriter,
PCRInfoWriter, PCRSampleWriter,
ClientSubmissionInfoWriter, ClientSubmissionSampleWriter
)
from .reports import ReportArchetype, ReportMaker, TurnaroundMaker, ConcentrationMaker, ChartReportMaker

View File

@@ -137,5 +137,10 @@ class DefaultTABLEParser(DefaultParser):
return [self._pyd_object(**output) for output in self.parsed_info] return [self._pyd_object(**output) for output in self.parsed_info]
from .procedure_parsers import ProcedureInfoParser, ProcedureSampleParser, ProcedureReagentParser, ProcedureEquipmentParser
from .results_parsers import (
DefaultResultsInfoParser, DefaultResultsSampleParser,
PCRSampleParser, PCRInfoParser
)
from .clientsubmission_parser import ClientSubmissionSampleParser, ClientSubmissionInfoParser from .clientsubmission_parser import ClientSubmissionSampleParser, ClientSubmissionInfoParser
from backend.excel.parsers.results_parsers.pcr_results_parser import PCRInfoParser, PCRSampleParser from .results_parsers.pcr_results_parser import PCRInfoParser, PCRSampleParser

View File

@@ -245,4 +245,8 @@ class DefaultTABLEWriter(DefaultWriter):
return worksheet return worksheet
from .procedure_writers import ProcedureInfoWriter, ProcedureSampleWriter, ProcedureReagentWriter, ProcedureEquipmentWriter
from .results_writers import (
PCRInfoWriter, PCRSampleWriter
)
from .clientsubmission_writer import ClientSubmissionInfoWriter, ClientSubmissionSampleWriter from .clientsubmission_writer import ClientSubmissionInfoWriter, ClientSubmissionSampleWriter

View File

@@ -265,5 +265,7 @@ class RSLNamer(object):
return "" return ""
from .pydant import PydRun, PydContact, PydClientLab, PydSample, PydReagent, PydReagentRole, \ from .pydant import (
PydEquipment, PydEquipmentRole, PydTips, PydProcess, PydElastic, PydClientSubmission, PydProcedure, PydResults PydRun, PydContact, PydClientLab, PydSample, PydReagent, PydReagentRole, PydEquipment, PydEquipmentRole, PydTips,
PydProcess, PydElastic, PydClientSubmission, PydProcedure, PydResults, PydReagentLot
)

View File

@@ -3,6 +3,7 @@ Contains pydantic models and accompanying validators
""" """
from __future__ import annotations from __future__ import annotations
import re, logging, csv, sys, string import re, logging, csv, sys, string
from pprint import pformat
from pydantic import BaseModel, field_validator, Field, model_validator from pydantic import BaseModel, field_validator, Field, model_validator
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from dateutil.parser import parse from dateutil.parser import parse
@@ -11,7 +12,8 @@ from typing import List, Tuple, Literal
from types import GeneratorType from types import GeneratorType
from . import RSLNamer from . import RSLNamer
from pathlib import Path from pathlib import Path
from tools import check_not_nan, convert_nans_to_nones, Report, Result, timezone, sort_dict_by_list, row_keys from tools import check_not_nan, convert_nans_to_nones, Report, Result, timezone, sort_dict_by_list, row_keys, \
flatten_list
from backend.db import models from backend.db import models
from backend.db.models import * from backend.db.models import *
from sqlalchemy.exc import StatementError from sqlalchemy.exc import StatementError
@@ -136,6 +138,12 @@ class PydBaseClass(BaseModel, extra='allow', validate_assignment=True):
return list(set(output)) return list(set(output))
class PydReagentLot(PydBaseClass):
lot: str | None
expiry: date | datetime | Literal['NA'] | None = Field(default=None, validate_default=True)
missing: bool = Field(default=True)
comment: str | None = Field(default="", validate_default=True)
class PydReagent(PydBaseClass): class PydReagent(PydBaseClass):
lot: str | None lot: str | None
reagentrole: str | None reagentrole: str | None
@@ -220,6 +228,7 @@ class PydReagent(PydBaseClass):
else: else:
return values.data['reagentrole'].strip() return values.data['reagentrole'].strip()
def improved_dict(self) -> dict: def improved_dict(self) -> dict:
""" """
Constructs a dictionary consisting of model.fields and model.extras Constructs a dictionary consisting of model.fields and model.extras

View File

@@ -1,5 +1,32 @@
''' """
Constructs main application. Constructs main application.
''' """
from .widgets import * from .widgets import (
from .visualizations import * pandasModel,
App,
Concentrations,
ControlsViewer,
DateTypePicker,
EquipmentUsage, RoleComboBox,
select_open_file, select_save_file, save_pdf,
GelBox, ControlsForm,
InfoPane,
StartEndDatePicker, CheckableComboBox, Pagifier,
AddEdit, EditProperty,
SearchBox, SearchResults, FieldSearch,
QuestionAsker, AlertPop, HTMLPop, ObjectSelector,
ProcedureCreation,
SampleChecker,
SubmissionDetails, SubmissionComment,
SubmissionsTree, ClientSubmissionRunModel,
MyQComboBox, MyQDateEdit, SubmissionFormContainer, SubmissionFormWidget, ClientSubmissionFormWidget,
Summary,
TurnaroundMaker
)
from .visualizations import (
CustomFigure,
IridaFigure,
PCRFigure,
ConcentrationsChart,
TurnaroundChart
)

View File

@@ -1,20 +1,71 @@
""" """
Contains all custom generated PyQT6 derivative widgets. Contains all custom generated PyQT6 derivative widgets.
""" """
from PyQt6.QtCore import QAbstractTableModel, Qt
class pandasModel(QAbstractTableModel):
"""
pandas model for inserting summary sheet into gui
NOTE: Copied from Stack Overflow. I have no idea how it actually works.
"""
def __init__(self, data) -> None:
QAbstractTableModel.__init__(self)
self._data = data
def rowCount(self, parent=None) -> int:
"""
does what it says
Args:
parent (_type_, optional): _description_. Defaults to None.
Returns:
int: number of rows in data
"""
return self._data.shape[0]
def columnCount(self, parent=None) -> int:
"""
does what it says
Args:
parent (_type_, optional): _description_. Defaults to None.
Returns:
int: number of columns in data
"""
return self._data.shape[1]
def data(self, index, role=Qt.ItemDataRole.DisplayRole) -> str | None:
if index.isValid():
if role == Qt.ItemDataRole.DisplayRole:
return str(self._data.iloc[index.row(), index.column()])
return None
def headerData(self, col, orientation, role):
if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
return self._data.columns[col]
return None
from .app import App from .app import App
from .controls_chart import * from .concentrations import Concentrations
from .equipment_usage import * from .controls_chart import ControlsViewer
from .functions import * from .date_type_picker import DateTypePicker
from .gel_checker import * from .equipment_usage import EquipmentUsage, RoleComboBox
from .info_tab import * from .functions import select_open_file, select_save_file, save_pdf
from .misc import * from .gel_checker import GelBox, ControlsForm
from .omni_search import * from .info_tab import InfoPane
from .pop_ups import * from .misc import StartEndDatePicker, CheckableComboBox, Pagifier
from .submission_details import * from .omni_add_edit import AddEdit, EditProperty
from .submission_table import * from .omni_search import SearchBox, SearchResults, FieldSearch
from .submission_widget import * from .pop_ups import QuestionAsker, AlertPop, HTMLPop, ObjectSelector
from .summary import * from .procedure_creation import ProcedureCreation
from .turnaround import * from .sample_checker import SampleChecker
from .omni_add_edit import * from .submission_details import SubmissionDetails, SubmissionComment
from .omni_manager_pydant import * from .submission_table import SubmissionsTree, ClientSubmissionRunModel
from .submission_widget import MyQComboBox, MyQDateEdit, SubmissionFormContainer, SubmissionFormWidget, ClientSubmissionFormWidget
from .summary import Summary
from .turnaround import TurnaroundMaker

View File

@@ -13,7 +13,7 @@ from PyQt6.QtGui import QAction
from pathlib import Path from pathlib import Path
from markdown import markdown from markdown import markdown
from pandas import ExcelWriter from pandas import ExcelWriter
from backend.db.models import ReagentLot # from backend.db.models import ReagentLot
from tools import ( from tools import (
check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size, is_power_user, check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size, is_power_user,
under_development under_development
@@ -24,7 +24,6 @@ from .pop_ups import HTMLPop
from .misc import Pagifier from .misc import Pagifier
from .submission_table import SubmissionsTree, ClientSubmissionRunModel from .submission_table import SubmissionsTree, ClientSubmissionRunModel
from .submission_widget import SubmissionFormContainer from .submission_widget import SubmissionFormContainer
# from .controls_chart import ControlsViewer
from .summary import Summary from .summary import Summary
from .turnaround import TurnaroundTime from .turnaround import TurnaroundTime
from .concentrations import Concentrations from .concentrations import Concentrations
@@ -181,7 +180,8 @@ class App(QMainWindow):
@check_authorization @check_authorization
def edit_reagent(self, *args, **kwargs): def edit_reagent(self, *args, **kwargs):
dlg = SearchBox(parent=self, object_type=ReagentLot, extras=[dict(name='Role', field="reagentrole")]) from backend.db.models import ReagentLot
dlg = SearchBox(parent=self, object_type=ReagentLot, extras=ReagentLot.get_searchables())
dlg.exec() dlg.exec()
def update_data(self): def update_data(self):

View File

@@ -5,7 +5,6 @@ from PyQt6.QtWidgets import (
QVBoxLayout, QDialog, QDialogButtonBox QVBoxLayout, QDialog, QDialogButtonBox
) )
from .misc import CheckableComboBox, StartEndDatePicker from .misc import CheckableComboBox, StartEndDatePicker
from backend.db.models.procedures import SubmissionType
import logging import logging
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -14,6 +13,7 @@ logger = logging.getLogger(f"submissions.{__name__}")
class DateTypePicker(QDialog): class DateTypePicker(QDialog):
def __init__(self, parent): def __init__(self, parent):
from backend.db.models.procedures import SubmissionType
super().__init__(parent) super().__init__(parent)
self.layout = QVBoxLayout() self.layout = QVBoxLayout()
self.setFixedWidth(500) self.setFixedWidth(500)

View File

@@ -34,7 +34,6 @@ class AddEdit(QDialog):
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 = {k: v for k, v in self.instance.omnigui_instance_dict.items() if "id" not in k} fields = {k: v for k, v in self.instance.omnigui_instance_dict.items() if "id" not in k}
logger.debug(f"Fields: {pformat(fields)}")
# 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}

View File

@@ -1,7 +1,6 @@
""" """
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
@@ -10,8 +9,8 @@ from PyQt6.QtWidgets import (
QLabel, QVBoxLayout, QDialog, QLabel, QVBoxLayout, QDialog,
QTableView, QWidget, QLineEdit, QGridLayout, QComboBox, QDialogButtonBox QTableView, QWidget, QLineEdit, QGridLayout, QComboBox, QDialogButtonBox
) )
from .submission_table import pandasModel from . import pandasModel
import logging import logging, sys
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -164,10 +163,10 @@ 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 + [item for item in deepcopy(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
def setData(self, df: DataFrame) -> None: def setData(self, df: DataFrame) -> None:
""" """
@@ -176,10 +175,11 @@ class SearchResults(QTableView):
self.data = df self.data = df
try: try:
self.columns_of_interest = [dict(name=item['field'], column=self.data.columns.get_loc(item['field'])) for self.columns_of_interest = [dict(name=item, column=self.data.columns.get_loc(item)) for
item in self.extras] item in self.object_type.get_searchables()]
except KeyError: except KeyError:
self.columns_of_interest = [] self.columns_of_interest = []
logger.debug(f"Columns of Interest: {pformat(self.columns_of_interest)}")
try: try:
self.data['id'] = self.data['id'].apply(str) self.data['id'] = self.data['id'].apply(str)
self.data['id'] = self.data['id'].str.zfill(3) self.data['id'] = self.data['id'].str.zfill(3)
@@ -204,10 +204,13 @@ class SearchResults(QTableView):
None None
""" """
context = {item['name']: x.sibling(x.row(), item['column']).data() for item in self.columns_of_interest} context = {item['name']: x.sibling(x.row(), item['column']).data() for item in self.columns_of_interest}
logger.debug(f"Context: {pformat(context)}")
try: try:
object = self.object_type.query(**context) object = self.object_type.query(**context)
except KeyError: except KeyError as e:
logger.error(e)
object = None object = None
logger.debug(f"Object: {object}")
try: try:
object.edit_from_search(obj=self.parent, **context) object.edit_from_search(obj=self.parent, **context)
except AttributeError as e: except AttributeError as e:

View File

@@ -69,11 +69,13 @@ class ProcedureCreation(QDialog):
equipment['name'] == relevant_procedure_item.name)) equipment['name'] == relevant_procedure_item.name))
equipmentrole['equipment'].insert(0, equipmentrole['equipment'].pop( equipmentrole['equipment'].insert(0, equipmentrole['equipment'].pop(
equipmentrole['equipment'].index(item_in_er_list))) equipmentrole['equipment'].index(item_in_er_list)))
proceduretype_dict['equipment_section'] = EquipmentUsage.construct_html(procedure=self.procedure, child=True) # proceduretype_dict['equipment_section'] = EquipmentUsage.construct_html(procedure=self.procedure, child=True)
proceduretype_dict['equipment'] = [sanitize_object_for_json(object) for object in proceduretype_dict['equipment']] proceduretype_dict['equipment'] = [sanitize_object_for_json(object) for object in proceduretype_dict['equipment']]
self.update_equipment = EquipmentUsage.update_equipment self.update_equipment = EquipmentUsage.update_equipment
regex = re.compile(r".*R\d$") regex = re.compile(r".*R\d$")
proceduretype_dict['previous'] = [""] + [item.name for item in self.run.procedure if item.proceduretype == self.proceduretype and not bool(regex.match(item.name))] proceduretype_dict['previous'] = [""] + [item.name for item in self.run.procedure if item.proceduretype == self.proceduretype and not bool(regex.match(item.name))]
logger.debug(f"Procedure:\n{pformat(self.procedure.__dict__)}")
logger.debug(f"ProcedureType:\n{pformat(proceduretype_dict)}")
html = render_details_template( html = render_details_template(
template_name="procedure_creation", template_name="procedure_creation",
js_in=["procedure_form", "grid_drag", "context_menu"], js_in=["procedure_form", "grid_drag", "context_menu"],

View File

@@ -1,234 +1,18 @@
""" """
Contains widgets specific to the procedure summary and procedure details. Contains widgets specific to the procedure summary and procedure details.
""" """
import sys, logging
import sys, logging, re
from pprint import pformat from pprint import pformat
from PyQt6.QtWidgets import QTableView, QMenu, QTreeView, QStyledItemDelegate, QStyle, QStyleOptionViewItem, \ from PyQt6.QtWidgets import QMenu, QTreeView, QAbstractItemView
QHeaderView, QAbstractItemView, QWidget, QTreeWidgetItemIterator from PyQt6.QtCore import QModelIndex
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel, pyqtSlot, QModelIndex from PyQt6.QtGui import QAction, QCursor, QStandardItemModel, QStandardItem, QContextMenuEvent
from PyQt6.QtGui import QAction, QCursor, QStandardItemModel, QStandardItem, QIcon, QColor, QContextMenuEvent from typing import List
from typing import Dict, List from backend.db.models import Run, ClientSubmission, Procedure
# from backend import Procedure from tools import get_application_from_parent
from backend.db.models.submissions import Run, ClientSubmission
from backend.db.models.procedures import Procedure
from tools import Report, Result, report_result, get_application_from_parent
from .functions import select_open_file
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
class pandasModel(QAbstractTableModel):
"""
pandas model for inserting summary sheet into gui
NOTE: Copied from Stack Overflow. I have no idea how it actually works.
"""
def __init__(self, data) -> None:
QAbstractTableModel.__init__(self)
self._data = data
def rowCount(self, parent=None) -> int:
"""
does what it says
Args:
parent (_type_, optional): _description_. Defaults to None.
Returns:
int: number of rows in data
"""
return self._data.shape[0]
def columnCount(self, parent=None) -> int:
"""
does what it says
Args:
parent (_type_, optional): _description_. Defaults to None.
Returns:
int: number of columns in data
"""
return self._data.shape[1]
def data(self, index, role=Qt.ItemDataRole.DisplayRole) -> str | None:
if index.isValid():
if role == Qt.ItemDataRole.DisplayRole:
return str(self._data.iloc[index.row(), index.column()])
return None
def headerData(self, col, orientation, role):
if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
return self._data.columns[col]
return None
class SubmissionsSheet(QTableView):
"""
presents procedure summary to user in tab1
"""
def __init__(self, parent) -> None:
super().__init__(parent)
self.app = self.parent()
self.report = Report()
try:
page_size = self.app.page_size
except AttributeError:
page_size = 250
self.set_data(page=1, page_size=page_size)
self.resizeColumnsToContents()
self.resizeRowsToContents()
self.setSortingEnabled(True)
self.doubleClicked.connect(lambda x: Run.query(id=x.sibling(x.row(), 0).data()).show_details(self))
# NOTE: Have to procedure native query here because mine just returns results?
self.total_count = Run.__database_session__.query(Run).count()
def set_data(self, page: int = 1, page_size: int = 250) -> None:
"""
sets data in model
"""
self.data = Run.submissions_to_df(page=page, page_size=page_size)
try:
self.data['Id'] = self.data['Id'].apply(str)
self.data['Id'] = self.data['Id'].str.zfill(4)
except KeyError as e:
logger.error(f"Could not alter id to string due to {e}")
proxyModel = QSortFilterProxyModel()
proxyModel.setSourceModel(pandasModel(self.data))
self.setModel(proxyModel)
def contextMenuEvent(self, event):
"""
Creates actions for right click menu events.
Args:
event (_type_): the item of interest
"""
# NOTE: Get current row index
id = self.selectionModel().currentIndex()
# NOTE: Convert to data in id column (i.e. column 0)
id = id.sibling(id.row(), 0).data()
submission = Run.query(id=id)
self.menu = QMenu(self)
self.con_actions = submission.custom_context_events()
for k in self.con_actions.keys():
action = QAction(k, self)
action.triggered.connect(lambda _, action_name=k: self.triggered_action(action_name=action_name))
self.menu.addAction(action)
# NOTE: add other required actions
self.menu.popup(QCursor.pos())
def triggered_action(self, action_name: str):
"""
Calls the triggered action from the context menu
Args:
action_name (str): name of the action from the menu
"""
func = self.con_actions[action_name]
func(obj=self)
@report_result
def link_extractions(self):
"""
Pull extraction logs into the db
"""
report = Report()
result = self.link_extractions_function()
report.add_result(result)
return report
def link_extractions_function(self):
"""
Link extractions from runlogs to imported procedure
Args:
obj (QMainWindow): original app window
Returns:
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
"""
report = Report()
fname = select_open_file(self, file_extension="csv")
with open(fname.__str__(), 'r') as f:
# NOTE: split csv on commas
runs = [col.strip().split(",") for col in f.readlines()]
count = 0
for run in runs:
new_run = dict(
start_time=run[0].strip(),
rsl_plate_number=run[1].strip(),
sample_count=run[2].strip(),
status=run[3].strip(),
experiment_name=run[4].strip(),
end_time=run[5].strip()
)
# NOTE: elution columns are item 6 in the comma split list to the end
for ii in range(6, len(run)):
new_run[f"column{str(ii - 5)}_vol"] = run[ii]
# NOTE: Lookup imported procedure
sub = Run.query(name=new_run['name'])
# NOTE: If no such procedure exists, move onto the next procedure
if sub is None:
continue
try:
count += 1
except AttributeError:
continue
sub.set_attribute('extraction_info', new_run)
sub.save()
report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information'))
return report
@report_result
def link_pcr(self):
"""
Pull pcr logs into the db
"""
report = Report()
result = self.link_pcr_function()
report.add_result(result)
return report
def link_pcr_function(self):
"""
Link PCR data from procedure logs to an imported procedure
Args:
obj (QMainWindow): original app window
Returns:
Tuple[QMainWindow, dict]: Collection of new main app window and result dict
"""
report = Report()
fname = select_open_file(self, file_extension="csv")
with open(fname.__str__(), 'r') as f:
# NOTE: split csv rows on comma
runs = [col.strip().split(",") for col in f.readlines()]
count = 0
for run in runs:
new_run = dict(
start_time=run[0].strip(),
rsl_plate_number=run[1].strip(),
biomek_status=run[2].strip(),
quant_status=run[3].strip(),
experiment_name=run[4].strip(),
end_time=run[5].strip()
)
# NOTE: lookup imported procedure
sub = Run.query(rsl_number=new_run['name'])
# NOTE: if imported procedure doesn't exist move on to next procedure
if sub is None:
continue
sub.set_attribute('pcr_info', new_run)
# NOTE: check if pcr_info already exists
sub.save()
report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information'))
return report
class SubmissionsTree(QTreeView): class SubmissionsTree(QTreeView):
""" """
https://stackoverflow.com/questions/54385437/how-can-i-make-a-table-that-can-collapse-its-rows-into-categories-in-qt https://stackoverflow.com/questions/54385437/how-can-i-make-a-table-that-can-collapse-its-rows-into-categories-in-qt
@@ -372,10 +156,6 @@ class SubmissionsTree(QTreeView):
class ClientSubmissionRunModel(QStandardItemModel): class ClientSubmissionRunModel(QStandardItemModel):
# def __init__(self, parent=None):
# super(ClientSubmissionRunModel, self).__init__(parent)
#
def add_child(self, parent: QStandardItem, child:dict): def add_child(self, parent: QStandardItem, child:dict):
item = QStandardItem(child['name']) item = QStandardItem(child['name'])
item.setData(dict(item_type=child['item_type'], query_str=child['query_str']), 1) item.setData(dict(item_type=child['item_type'], query_str=child['query_str']), 1)

View File

@@ -11,12 +11,12 @@ from .functions import select_open_file, select_save_file
from pathlib import Path from pathlib import Path
from tools import Report, Result, check_not_nan, main_form_style, report_result, get_application_from_parent from tools import Report, Result, check_not_nan, main_form_style, report_result, get_application_from_parent
from backend.validators import PydReagent, PydClientSubmission, PydSample from backend.validators import PydReagent, PydClientSubmission, PydSample
from backend.db import ( from backend.db.models import (
ClientLab, SubmissionType, Reagent, ReagentLot, ClientLab, SubmissionType, Reagent, ReagentLot,
ReagentRole, ProcedureTypeReagentRoleAssociation, Run, ClientSubmission ReagentRole, ProcedureTypeReagentRoleAssociation, Run, ClientSubmission
) )
from pprint import pformat from pprint import pformat
from .pop_ups import QuestionAsker, AlertPop from .pop_ups import QuestionAsker
from .omni_add_edit import AddEdit from .omni_add_edit import AddEdit
from typing import List, Tuple from typing import List, Tuple
from datetime import date from datetime import date

View File

@@ -3,7 +3,7 @@ Pane to hold information e.g. cost summary.
""" """
from .info_tab import InfoPane from .info_tab import InfoPane
from PyQt6.QtWidgets import QWidget, QLabel, QPushButton from PyQt6.QtWidgets import QWidget, QLabel, QPushButton
from backend.db import ClientLab from backend.db.models import ClientLab
from backend.excel.reports import ReportMaker from backend.excel.reports import ReportMaker
from .misc import CheckableComboBox from .misc import CheckableComboBox
import logging import logging

View File

@@ -4,7 +4,7 @@ Pane showing turnaround time summary.
from PyQt6.QtWidgets import QWidget, QPushButton, QComboBox, QLabel from PyQt6.QtWidgets import QWidget, QPushButton, QComboBox, QLabel
from .info_tab import InfoPane from .info_tab import InfoPane
from backend.excel.reports import TurnaroundMaker from backend.excel.reports import TurnaroundMaker
from backend.db import SubmissionType from backend.db.models import SubmissionType
from frontend.visualizations.turnaround_chart import TurnaroundChart from frontend.visualizations.turnaround_chart import TurnaroundChart
import logging import logging