Attempt to cleanup imports.
This commit is contained in:
@@ -1,6 +1,32 @@
|
||||
"""
|
||||
Contains database, validators and excel operations.
|
||||
"""
|
||||
from .db import *
|
||||
from .excel import *
|
||||
from .validators import *
|
||||
from .db import (
|
||||
set_sqlite_pragma,
|
||||
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
|
||||
)
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
"""
|
||||
All database related operations.
|
||||
"""
|
||||
from datetime import datetime
|
||||
from getpass import getuser
|
||||
from sqlalchemy import event, inspect
|
||||
from sqlalchemy.engine import Engine
|
||||
from tools import ctx
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
|
||||
@event.listens_for(Engine, "connect")
|
||||
@@ -28,7 +33,17 @@ def set_sqlite_pragma(dbapi_connection, connection_record):
|
||||
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):
|
||||
|
||||
@@ -3,11 +3,14 @@ Contains all models for sqlalchemy
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys, logging, json, inspect
|
||||
from datetime import datetime, date
|
||||
from pprint import pformat
|
||||
|
||||
from dateutil.parser import parse
|
||||
from jinja2 import TemplateNotFound
|
||||
from jinja2 import TemplateNotFound, Template
|
||||
from pandas import DataFrame
|
||||
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.orm import DeclarativeMeta, declarative_base, Query, Session, InstrumentedAttribute, ColumnProperty
|
||||
from sqlalchemy.ext.declarative import declared_attr
|
||||
@@ -16,7 +19,7 @@ from sqlalchemy.exc import ArgumentError
|
||||
from typing import Any, List, ClassVar
|
||||
from pathlib import Path
|
||||
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
|
||||
if 'pytest' in sys.modules:
|
||||
@@ -46,7 +49,9 @@ class BaseClass(Base):
|
||||
except AttributeError:
|
||||
return f"<{self.__class__.__name__}(Name Unavailable)>"
|
||||
|
||||
@classproperty
|
||||
# @classproperty
|
||||
@classmethod
|
||||
@declared_attr
|
||||
def aliases(cls) -> List[str]:
|
||||
"""
|
||||
List of other names this class might be known by.
|
||||
@@ -56,7 +61,8 @@ class BaseClass(Base):
|
||||
"""
|
||||
return [cls.query_alias]
|
||||
|
||||
@classproperty
|
||||
@classmethod
|
||||
@declared_attr
|
||||
def query_alias(cls) -> str:
|
||||
"""
|
||||
What to query this class as.
|
||||
@@ -126,7 +132,9 @@ class BaseClass(Base):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._misc_info = dict()
|
||||
|
||||
@classproperty
|
||||
# @classproperty
|
||||
@classmethod
|
||||
@declared_attr
|
||||
def jsons(cls) -> List[str]:
|
||||
"""
|
||||
Get list of JSON db columns
|
||||
@@ -139,7 +147,9 @@ class BaseClass(Base):
|
||||
except AttributeError:
|
||||
return []
|
||||
|
||||
@classproperty
|
||||
# @classproperty
|
||||
@classmethod
|
||||
@declared_attr
|
||||
def timestamps(cls) -> List[str]:
|
||||
"""
|
||||
Get list of TIMESTAMP columns
|
||||
@@ -169,9 +179,6 @@ class BaseClass(Base):
|
||||
continue
|
||||
if not isinstance(item[1].property, ColumnProperty):
|
||||
continue
|
||||
# if isinstance(item[1], _RelationshipDeclared):
|
||||
# if "association" in item[0]:
|
||||
# continue
|
||||
if len(item[1].foreign_keys) > 0:
|
||||
continue
|
||||
if item[1].type.__class__.__name__ not in ["String"]:
|
||||
@@ -245,14 +252,15 @@ class BaseClass(Base):
|
||||
"""
|
||||
if not objects:
|
||||
try:
|
||||
records = [obj.details_dict(**kwargs) for obj in cls.query()]
|
||||
q = cls.query()
|
||||
except AttributeError:
|
||||
records = [obj.details_dict(**kwargs) for obj in cls.query(page_size=0)]
|
||||
q = cls.query(page_size=0)
|
||||
else:
|
||||
try:
|
||||
records = [obj.to_sub_dict(**kwargs) for obj in objects]
|
||||
except AttributeError:
|
||||
records = [{k: v['instance_attr'] for k, v in obj.omnigui_instance_dict.items()} for obj in objects]
|
||||
q = objects
|
||||
records = []
|
||||
for obj in q:
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
@@ -397,7 +405,9 @@ class BaseClass(Base):
|
||||
pass
|
||||
return dicto
|
||||
|
||||
@classproperty
|
||||
# @classproperty
|
||||
@classmethod
|
||||
@declared_attr
|
||||
def pydantic_model(cls) -> BaseModel:
|
||||
"""
|
||||
Gets the pydantic model corresponding to this object.
|
||||
@@ -410,10 +420,15 @@ class BaseClass(Base):
|
||||
model = getattr(pydant, f"Pyd{cls.__name__}")
|
||||
except AttributeError:
|
||||
logger.warning(f"Couldn't get {cls.__name__} pydantic model.")
|
||||
try:
|
||||
model = getattr(pydant, f"Pyd{cls.pyd_model_name}")
|
||||
except AttributeError:
|
||||
return pydant.PydElastic
|
||||
return model
|
||||
|
||||
@classproperty
|
||||
# @classproperty
|
||||
@classmethod
|
||||
@declared_attr
|
||||
def add_edit_tooltips(cls) -> dict:
|
||||
"""
|
||||
Gets tooltips for Omni-add-edit
|
||||
@@ -423,7 +438,9 @@ class BaseClass(Base):
|
||||
"""
|
||||
return dict()
|
||||
|
||||
@classproperty
|
||||
# @classproperty
|
||||
@classmethod
|
||||
@declared_attr
|
||||
def details_template(cls) -> Template:
|
||||
"""
|
||||
Get the details jinja template for the correct class
|
||||
@@ -609,6 +626,11 @@ class BaseClass(Base):
|
||||
value = getattr(self, k)
|
||||
except AttributeError:
|
||||
continue
|
||||
match value:
|
||||
case str():
|
||||
value = value.strip('\"')
|
||||
case _:
|
||||
pass
|
||||
output[k.strip("_")] = value
|
||||
if self._misc_info:
|
||||
for key, value in self._misc_info.items():
|
||||
@@ -718,12 +740,20 @@ class ConfigItem(BaseClass):
|
||||
return config_items
|
||||
|
||||
|
||||
from .controls import *
|
||||
# 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 .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.
|
||||
# https://docs.sqlalchemy.org/en/20/orm/extensions/associationproxy.html#sqlalchemy.ext.associationproxy.association_proxy.params.creator
|
||||
|
||||
@@ -11,6 +11,7 @@ import logging
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
# NOTE: Need a seperate base for this.
|
||||
Base: DeclarativeMeta = declarative_base()
|
||||
|
||||
class AuditLog(Base):
|
||||
|
||||
@@ -4,8 +4,8 @@ All client organization related models.
|
||||
from __future__ import annotations
|
||||
import logging
|
||||
from sqlalchemy import Column, String, INTEGER, ForeignKey, Table
|
||||
from sqlalchemy.orm import relationship, Query
|
||||
from . import Base, BaseClass
|
||||
from sqlalchemy.orm import relationship, Query, declared_attr
|
||||
from . import BaseClass
|
||||
from tools import check_authorization, setup_lookup
|
||||
from typing import List
|
||||
|
||||
@@ -14,7 +14,8 @@ logger = logging.getLogger(f"submissions.{__name__}")
|
||||
# table containing clientlab/contact relationship
|
||||
clientlab_contact = Table(
|
||||
"_clientlab_contact",
|
||||
Base.metadata,
|
||||
# Base.metadata,
|
||||
BaseClass.__base__.metadata,
|
||||
Column("clientlab_id", INTEGER, ForeignKey("_clientlab.id")),
|
||||
Column("contact_id", INTEGER, ForeignKey("_contact.id")),
|
||||
extend_existing=True
|
||||
@@ -98,7 +99,9 @@ class Contact(BaseClass):
|
||||
secondary=clientlab_contact) #: relationship to joined clientlab
|
||||
clientsubmission = relationship("ClientSubmission", back_populates="contact") #: procedure this contact has submitted
|
||||
|
||||
@classproperty
|
||||
# @classproperty
|
||||
@classmethod
|
||||
@declared_attr
|
||||
def searchables(cls):
|
||||
return []
|
||||
|
||||
|
||||
@@ -7,13 +7,13 @@ from operator import itemgetter
|
||||
from pprint import pformat
|
||||
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, func
|
||||
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 datetime import date, datetime, timedelta
|
||||
from tools import check_authorization, setup_lookup, Report, Result, check_regex_match, timezone, \
|
||||
jinja_template_loading, flatten_list
|
||||
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 sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError
|
||||
|
||||
@@ -25,7 +25,7 @@ logger = logging.getLogger(f'submissions.{__name__}')
|
||||
|
||||
reagentrole_reagent = Table(
|
||||
"_reagentrole_reagent",
|
||||
Base.metadata,
|
||||
BaseClass.__base__.metadata,
|
||||
Column("reagent_id", INTEGER, ForeignKey("_reagent.id")),
|
||||
Column("reagentrole_id", INTEGER, ForeignKey("_reagentrole.id")),
|
||||
extend_existing=True
|
||||
@@ -33,7 +33,7 @@ reagentrole_reagent = Table(
|
||||
|
||||
equipment_process = Table(
|
||||
"_equipment_process",
|
||||
Base.metadata,
|
||||
BaseClass.__base__.metadata,
|
||||
Column("process_id", INTEGER, ForeignKey("_process.id")),
|
||||
Column("equipment_id", INTEGER, ForeignKey("_equipment.id")),
|
||||
extend_existing=True
|
||||
@@ -41,7 +41,7 @@ equipment_process = Table(
|
||||
|
||||
process_tips = Table(
|
||||
"_process_tips",
|
||||
Base.metadata,
|
||||
BaseClass.__base__.metadata,
|
||||
Column("process_id", INTEGER, ForeignKey("_process.id")),
|
||||
Column("tips_id", INTEGER, ForeignKey("_tips.id")),
|
||||
extend_existing=True
|
||||
@@ -49,7 +49,7 @@ process_tips = Table(
|
||||
|
||||
submissiontype_proceduretype = Table(
|
||||
"_submissiontype_proceduretype",
|
||||
Base.metadata,
|
||||
BaseClass.__base__.metadata,
|
||||
Column("submissiontype_id", INTEGER, ForeignKey("_submissiontype.id")),
|
||||
Column("proceduretype_id", INTEGER, ForeignKey("_proceduretype.id")),
|
||||
extend_existing=True
|
||||
@@ -217,7 +217,8 @@ class Reagent(BaseClass, LogMixin):
|
||||
self.name = name
|
||||
self.eol_ext = eol_ext
|
||||
|
||||
@classproperty
|
||||
@classmethod
|
||||
@declared_attr
|
||||
def searchables(cls):
|
||||
return [dict(label="Lot", field="lot")]
|
||||
|
||||
@@ -322,7 +323,8 @@ class Reagent(BaseClass, LogMixin):
|
||||
|
||||
|
||||
|
||||
@classproperty
|
||||
@classmethod
|
||||
@declared_attr
|
||||
def add_edit_tooltips(self):
|
||||
return dict(
|
||||
expiry="Use exact date on reagent.\nEOL will be calculated from kittype automatically"
|
||||
@@ -342,6 +344,9 @@ class Reagent(BaseClass, LogMixin):
|
||||
|
||||
|
||||
class ReagentLot(BaseClass):
|
||||
|
||||
pyd_model_name = "Reagent"
|
||||
|
||||
id = Column(INTEGER, primary_key=True) #: primary key
|
||||
lot = Column(String(64), unique=True) #: lot number of reagent
|
||||
expiry = Column(TIMESTAMP) #: expiry date - extended by eol_ext of parent programmatically
|
||||
@@ -368,7 +373,7 @@ class ReagentLot(BaseClass):
|
||||
def query(cls,
|
||||
lot: str | None = None,
|
||||
name: str | None = None,
|
||||
limit: int = 1,
|
||||
limit: int = 0,
|
||||
**kwargs) -> ReagentLot | List[ReagentLot]:
|
||||
"""
|
||||
|
||||
@@ -386,6 +391,7 @@ class ReagentLot(BaseClass):
|
||||
match lot:
|
||||
case str():
|
||||
query = query.filter(cls.lot == lot)
|
||||
limit = 1
|
||||
case _:
|
||||
pass
|
||||
match name:
|
||||
@@ -414,13 +420,28 @@ class ReagentLot(BaseClass):
|
||||
@check_authorization
|
||||
def edit_from_search(self, obj, **kwargs):
|
||||
from frontend.widgets.omni_add_edit import AddEdit
|
||||
from backend.validators.pydant import PydElastic
|
||||
dlg = AddEdit(parent=None, instance=self, disabled=['reagent'])
|
||||
if dlg.exec():
|
||||
pyd = dlg.parse_form()
|
||||
for field in pyd.model_fields:
|
||||
self.set_attribute(field, pyd.__getattribute__(field))
|
||||
logger.debug(f"Pydantic returned: {type(pyd)} {pyd.model_fields}")
|
||||
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()
|
||||
|
||||
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):
|
||||
"""
|
||||
Relationship table for client labs for certain kits.
|
||||
@@ -508,7 +529,8 @@ class SubmissionType(BaseClass):
|
||||
"""
|
||||
return f"<SubmissionType({self.name})>"
|
||||
|
||||
@classproperty
|
||||
@classmethod
|
||||
@declared_attr
|
||||
def aliases(cls) -> List[str]:
|
||||
"""
|
||||
Gets other names the sql object of this class might go by.
|
||||
@@ -604,12 +626,14 @@ class SubmissionType(BaseClass):
|
||||
sample_map=self.sample_map
|
||||
)
|
||||
|
||||
@classproperty
|
||||
@classmethod
|
||||
@declared_attr
|
||||
def info_map_json_edit_fields(cls):
|
||||
dicto = dict()
|
||||
return dicto
|
||||
|
||||
@classproperty
|
||||
@classmethod
|
||||
@declared_attr
|
||||
def regex(cls) -> re.Pattern:
|
||||
"""
|
||||
Constructs catchall regex.
|
||||
@@ -1182,7 +1206,8 @@ class ProcedureTypeReagentRoleAssociation(BaseClass):
|
||||
dicto['required']['instance_attr'] = bool(dicto['required']['instance_attr'])
|
||||
return dicto
|
||||
|
||||
@classproperty
|
||||
@classmethod
|
||||
@declared_attr
|
||||
def json_edit_fields(cls) -> dict:
|
||||
dicto = dict(
|
||||
sheet="str",
|
||||
@@ -1607,7 +1632,8 @@ class Equipment(BaseClass, LogMixin):
|
||||
creation_dict['equipmentrole'] = equipmentrole or creation_dict['equipmentrole']
|
||||
return PydEquipment(**creation_dict)
|
||||
|
||||
@classproperty
|
||||
@classmethod
|
||||
@declared_attr
|
||||
def manufacturer_regex(cls) -> re.Pattern:
|
||||
"""
|
||||
Creates regex to determine tip manufacturer
|
||||
|
||||
@@ -11,9 +11,9 @@ from pprint import pformat
|
||||
from pandas import DataFrame
|
||||
from sqlalchemy.ext.hybrid import hybrid_property
|
||||
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.orm import relationship, Query
|
||||
from sqlalchemy.orm import relationship, Query, declared_attr
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
from sqlalchemy.exc import OperationalError as AlcOperationalError, IntegrityError as AlcIntegrityError, StatementError
|
||||
@@ -947,7 +947,8 @@ class Run(BaseClass, LogMixin):
|
||||
|
||||
# NOTE: Polymorphic functions
|
||||
|
||||
@classproperty
|
||||
@classmethod
|
||||
@declared_attr
|
||||
def regex(cls) -> re.Pattern:
|
||||
"""
|
||||
Constructs catchall regex.
|
||||
@@ -1297,7 +1298,8 @@ class Sample(BaseClass, LogMixin):
|
||||
def __repr__(self) -> str:
|
||||
return f"<Sample({self.sample_id})>"
|
||||
|
||||
@classproperty
|
||||
@classmethod
|
||||
@declared_attr
|
||||
def searchables(cls):
|
||||
return [dict(label="Submitter ID", field="sample_id")]
|
||||
|
||||
|
||||
@@ -2,4 +2,18 @@
|
||||
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
|
||||
|
||||
@@ -137,5 +137,10 @@ class DefaultTABLEParser(DefaultParser):
|
||||
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 backend.excel.parsers.results_parsers.pcr_results_parser import PCRInfoParser, PCRSampleParser
|
||||
from .results_parsers.pcr_results_parser import PCRInfoParser, PCRSampleParser
|
||||
|
||||
@@ -245,4 +245,8 @@ class DefaultTABLEWriter(DefaultWriter):
|
||||
return worksheet
|
||||
|
||||
|
||||
from .procedure_writers import ProcedureInfoWriter, ProcedureSampleWriter, ProcedureReagentWriter, ProcedureEquipmentWriter
|
||||
from .results_writers import (
|
||||
PCRInfoWriter, PCRSampleWriter
|
||||
)
|
||||
from .clientsubmission_writer import ClientSubmissionInfoWriter, ClientSubmissionSampleWriter
|
||||
|
||||
@@ -265,5 +265,7 @@ class RSLNamer(object):
|
||||
return ""
|
||||
|
||||
|
||||
from .pydant import PydRun, PydContact, PydClientLab, PydSample, PydReagent, PydReagentRole, \
|
||||
PydEquipment, PydEquipmentRole, PydTips, PydProcess, PydElastic, PydClientSubmission, PydProcedure, PydResults
|
||||
from .pydant import (
|
||||
PydRun, PydContact, PydClientLab, PydSample, PydReagent, PydReagentRole, PydEquipment, PydEquipmentRole, PydTips,
|
||||
PydProcess, PydElastic, PydClientSubmission, PydProcedure, PydResults, PydReagentLot
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ Contains pydantic models and accompanying validators
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import re, logging, csv, sys, string
|
||||
from pprint import pformat
|
||||
from pydantic import BaseModel, field_validator, Field, model_validator
|
||||
from datetime import date, datetime, timedelta
|
||||
from dateutil.parser import parse
|
||||
@@ -11,7 +12,8 @@ from typing import List, Tuple, Literal
|
||||
from types import GeneratorType
|
||||
from . import RSLNamer
|
||||
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.models import *
|
||||
from sqlalchemy.exc import StatementError
|
||||
@@ -136,6 +138,12 @@ class PydBaseClass(BaseModel, extra='allow', validate_assignment=True):
|
||||
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):
|
||||
lot: str | None
|
||||
reagentrole: str | None
|
||||
@@ -220,6 +228,7 @@ class PydReagent(PydBaseClass):
|
||||
else:
|
||||
return values.data['reagentrole'].strip()
|
||||
|
||||
|
||||
def improved_dict(self) -> dict:
|
||||
"""
|
||||
Constructs a dictionary consisting of model.fields and model.extras
|
||||
|
||||
@@ -1,5 +1,32 @@
|
||||
'''
|
||||
"""
|
||||
Constructs main application.
|
||||
'''
|
||||
from .widgets import *
|
||||
from .visualizations import *
|
||||
"""
|
||||
from .widgets 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
|
||||
)
|
||||
|
||||
@@ -1,20 +1,71 @@
|
||||
"""
|
||||
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 .controls_chart import *
|
||||
from .equipment_usage import *
|
||||
from .functions import *
|
||||
from .gel_checker import *
|
||||
from .info_tab import *
|
||||
from .misc import *
|
||||
from .omni_search import *
|
||||
from .pop_ups import *
|
||||
from .submission_details import *
|
||||
from .submission_table import *
|
||||
from .submission_widget import *
|
||||
from .summary import *
|
||||
from .turnaround import *
|
||||
from .omni_add_edit import *
|
||||
from .omni_manager_pydant import *
|
||||
from .concentrations import Concentrations
|
||||
from .controls_chart import ControlsViewer
|
||||
from .date_type_picker import DateTypePicker
|
||||
from .equipment_usage import EquipmentUsage, RoleComboBox
|
||||
from .functions import select_open_file, select_save_file, save_pdf
|
||||
from .gel_checker import GelBox, ControlsForm
|
||||
from .info_tab import InfoPane
|
||||
from .misc import StartEndDatePicker, CheckableComboBox, Pagifier
|
||||
from .omni_add_edit import AddEdit, EditProperty
|
||||
from .omni_search import SearchBox, SearchResults, FieldSearch
|
||||
from .pop_ups import QuestionAsker, AlertPop, HTMLPop, ObjectSelector
|
||||
from .procedure_creation import ProcedureCreation
|
||||
from .sample_checker import SampleChecker
|
||||
from .submission_details import SubmissionDetails, SubmissionComment
|
||||
from .submission_table import SubmissionsTree, ClientSubmissionRunModel
|
||||
from .submission_widget import MyQComboBox, MyQDateEdit, SubmissionFormContainer, SubmissionFormWidget, ClientSubmissionFormWidget
|
||||
from .summary import Summary
|
||||
from .turnaround import TurnaroundMaker
|
||||
|
||||
@@ -13,7 +13,7 @@ from PyQt6.QtGui import QAction
|
||||
from pathlib import Path
|
||||
from markdown import markdown
|
||||
from pandas import ExcelWriter
|
||||
from backend.db.models import ReagentLot
|
||||
# from backend.db.models import ReagentLot
|
||||
from tools import (
|
||||
check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size, is_power_user,
|
||||
under_development
|
||||
@@ -24,7 +24,6 @@ from .pop_ups import HTMLPop
|
||||
from .misc import Pagifier
|
||||
from .submission_table import SubmissionsTree, ClientSubmissionRunModel
|
||||
from .submission_widget import SubmissionFormContainer
|
||||
# from .controls_chart import ControlsViewer
|
||||
from .summary import Summary
|
||||
from .turnaround import TurnaroundTime
|
||||
from .concentrations import Concentrations
|
||||
@@ -181,7 +180,8 @@ class App(QMainWindow):
|
||||
|
||||
@check_authorization
|
||||
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()
|
||||
|
||||
def update_data(self):
|
||||
|
||||
@@ -5,7 +5,6 @@ from PyQt6.QtWidgets import (
|
||||
QVBoxLayout, QDialog, QDialogButtonBox
|
||||
)
|
||||
from .misc import CheckableComboBox, StartEndDatePicker
|
||||
from backend.db.models.procedures import SubmissionType
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
@@ -14,6 +13,7 @@ logger = logging.getLogger(f"submissions.{__name__}")
|
||||
class DateTypePicker(QDialog):
|
||||
|
||||
def __init__(self, parent):
|
||||
from backend.db.models.procedures import SubmissionType
|
||||
super().__init__(parent)
|
||||
self.layout = QVBoxLayout()
|
||||
self.setFixedWidth(500)
|
||||
|
||||
@@ -34,7 +34,6 @@ class AddEdit(QDialog):
|
||||
self.buttonBox.accepted.connect(self.accept)
|
||||
self.buttonBox.rejected.connect(self.reject)
|
||||
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
|
||||
try:
|
||||
fields = {'name': fields.pop('name'), **fields}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
Search box that performs fuzzy search for various object types
|
||||
"""
|
||||
from copy import deepcopy
|
||||
from pprint import pformat
|
||||
from typing import Tuple, Any, List, Generator
|
||||
from pandas import DataFrame
|
||||
@@ -10,8 +9,8 @@ from PyQt6.QtWidgets import (
|
||||
QLabel, QVBoxLayout, QDialog,
|
||||
QTableView, QWidget, QLineEdit, QGridLayout, QComboBox, QDialogButtonBox
|
||||
)
|
||||
from .submission_table import pandasModel
|
||||
import logging
|
||||
from . import pandasModel
|
||||
import logging, sys
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
@@ -164,10 +163,10 @@ class SearchResults(QTableView):
|
||||
self.context = kwargs
|
||||
self.parent = parent
|
||||
self.object_type = object_type
|
||||
try:
|
||||
self.extras = extras + [item for item in deepcopy(self.object_type.searchables)]
|
||||
except AttributeError:
|
||||
self.extras = extras
|
||||
# try:
|
||||
# self.extras = extras + [item for item in deepcopy(self.object_type.searchables)]
|
||||
# except AttributeError:
|
||||
# self.extras = extras
|
||||
|
||||
def setData(self, df: DataFrame) -> None:
|
||||
"""
|
||||
@@ -176,10 +175,11 @@ class SearchResults(QTableView):
|
||||
|
||||
self.data = df
|
||||
try:
|
||||
self.columns_of_interest = [dict(name=item['field'], column=self.data.columns.get_loc(item['field'])) for
|
||||
item in self.extras]
|
||||
self.columns_of_interest = [dict(name=item, column=self.data.columns.get_loc(item)) for
|
||||
item in self.object_type.get_searchables()]
|
||||
except KeyError:
|
||||
self.columns_of_interest = []
|
||||
logger.debug(f"Columns of Interest: {pformat(self.columns_of_interest)}")
|
||||
try:
|
||||
self.data['id'] = self.data['id'].apply(str)
|
||||
self.data['id'] = self.data['id'].str.zfill(3)
|
||||
@@ -204,10 +204,13 @@ class SearchResults(QTableView):
|
||||
None
|
||||
"""
|
||||
context = {item['name']: x.sibling(x.row(), item['column']).data() for item in self.columns_of_interest}
|
||||
logger.debug(f"Context: {pformat(context)}")
|
||||
try:
|
||||
object = self.object_type.query(**context)
|
||||
except KeyError:
|
||||
except KeyError as e:
|
||||
logger.error(e)
|
||||
object = None
|
||||
logger.debug(f"Object: {object}")
|
||||
try:
|
||||
object.edit_from_search(obj=self.parent, **context)
|
||||
except AttributeError as e:
|
||||
|
||||
@@ -69,11 +69,13 @@ class ProcedureCreation(QDialog):
|
||||
equipment['name'] == relevant_procedure_item.name))
|
||||
equipmentrole['equipment'].insert(0, equipmentrole['equipment'].pop(
|
||||
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']]
|
||||
self.update_equipment = EquipmentUsage.update_equipment
|
||||
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))]
|
||||
logger.debug(f"Procedure:\n{pformat(self.procedure.__dict__)}")
|
||||
logger.debug(f"ProcedureType:\n{pformat(proceduretype_dict)}")
|
||||
html = render_details_template(
|
||||
template_name="procedure_creation",
|
||||
js_in=["procedure_form", "grid_drag", "context_menu"],
|
||||
|
||||
@@ -1,234 +1,18 @@
|
||||
"""
|
||||
Contains widgets specific to the procedure summary and procedure details.
|
||||
"""
|
||||
|
||||
import sys, logging, re
|
||||
import sys, logging
|
||||
from pprint import pformat
|
||||
from PyQt6.QtWidgets import QTableView, QMenu, QTreeView, QStyledItemDelegate, QStyle, QStyleOptionViewItem, \
|
||||
QHeaderView, QAbstractItemView, QWidget, QTreeWidgetItemIterator
|
||||
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel, pyqtSlot, QModelIndex
|
||||
from PyQt6.QtGui import QAction, QCursor, QStandardItemModel, QStandardItem, QIcon, QColor, QContextMenuEvent
|
||||
from typing import Dict, List
|
||||
# from backend import Procedure
|
||||
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
|
||||
from PyQt6.QtWidgets import QMenu, QTreeView, QAbstractItemView
|
||||
from PyQt6.QtCore import QModelIndex
|
||||
from PyQt6.QtGui import QAction, QCursor, QStandardItemModel, QStandardItem, QContextMenuEvent
|
||||
from typing import List
|
||||
from backend.db.models import Run, ClientSubmission, Procedure
|
||||
from tools import get_application_from_parent
|
||||
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
|
||||
|
||||
# def __init__(self, parent=None):
|
||||
# super(ClientSubmissionRunModel, self).__init__(parent)
|
||||
#
|
||||
def add_child(self, parent: QStandardItem, child:dict):
|
||||
item = QStandardItem(child['name'])
|
||||
item.setData(dict(item_type=child['item_type'], query_str=child['query_str']), 1)
|
||||
|
||||
@@ -11,12 +11,12 @@ from .functions import select_open_file, select_save_file
|
||||
from pathlib import Path
|
||||
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.db import (
|
||||
from backend.db.models import (
|
||||
ClientLab, SubmissionType, Reagent, ReagentLot,
|
||||
ReagentRole, ProcedureTypeReagentRoleAssociation, Run, ClientSubmission
|
||||
)
|
||||
from pprint import pformat
|
||||
from .pop_ups import QuestionAsker, AlertPop
|
||||
from .pop_ups import QuestionAsker
|
||||
from .omni_add_edit import AddEdit
|
||||
from typing import List, Tuple
|
||||
from datetime import date
|
||||
|
||||
@@ -3,7 +3,7 @@ Pane to hold information e.g. cost summary.
|
||||
"""
|
||||
from .info_tab import InfoPane
|
||||
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 .misc import CheckableComboBox
|
||||
import logging
|
||||
|
||||
@@ -4,7 +4,7 @@ Pane showing turnaround time summary.
|
||||
from PyQt6.QtWidgets import QWidget, QPushButton, QComboBox, QLabel
|
||||
from .info_tab import InfoPane
|
||||
from backend.excel.reports import TurnaroundMaker
|
||||
from backend.db import SubmissionType
|
||||
from backend.db.models import SubmissionType
|
||||
from frontend.visualizations.turnaround_chart import TurnaroundChart
|
||||
import logging
|
||||
|
||||
|
||||
Reference in New Issue
Block a user