Compare commits

..

10 Commits

Author SHA1 Message Date
lwark
3862604dfa Plate map in procedure details. 2025-09-15 09:21:52 -05:00
lwark
11abaafcfc Attempt to cleanup imports. 2025-09-12 10:14:53 -05:00
lwark
ba4912cab7 ReagentLot add/edit updated. 2025-09-10 12:39:02 -05:00
lwark
c9396d6c41 Frontend code cleanup finished. 2025-09-05 15:23:56 -05:00
lwark
610859d84f Code cleanup for validators complete. 2025-09-04 15:21:50 -05:00
lwark
c8b4762747 Code cleanup for managers complete. 2025-09-04 15:01:02 -05:00
lwark
1a90543639 Code cleanup for excel complete. 2025-09-04 14:49:21 -05:00
lwark
0c20ade65a Code cleanup for excel.writers complete. 2025-09-04 14:46:25 -05:00
lwark
b6e1c0dee2 Code cleanup for excel.parsers complete. 2025-09-04 14:35:44 -05:00
lwark
fcda0d873c Code cleanup for db.models complete. 2025-09-03 14:04:26 -05:00
57 changed files with 3427 additions and 5900 deletions

View File

@@ -1,3 +1,7 @@
# 202509.02
- First Useable updated version.
# 202504.04 # 202504.04
- Added html links for equipment/processes/tips. - Added html links for equipment/processes/tips.

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")
@@ -22,14 +27,23 @@ def set_sqlite_pragma(dbapi_connection, connection_record):
execution_phrase = "PRAGMA foreign_keys=ON" execution_phrase = "PRAGMA foreign_keys=ON"
print(f"Executing '{execution_phrase}' in sql.") print(f"Executing '{execution_phrase}' in sql.")
else: else:
# print("Nothing to execute, returning")
cursor.close() cursor.close()
return return
cursor.execute(execution_phrase) cursor.execute(execution_phrase)
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):
@@ -55,9 +69,6 @@ def update_log(mapper, connection, target):
continue continue
added = [str(item) for item in hist.added] added = [str(item) for item in hist.added]
# NOTE: Attributes left out to save space # NOTE: Attributes left out to save space
# if attr.key in ['artic_technician', 'clientsubmissionsampleassociation', 'submission_reagent_associations',
# 'submission_equipment_associations', 'submission_tips_associations', 'contact_id', 'gel_info',
# 'gel_controls', 'source_plates']:
if attr.key in LogMixin.tracking_exclusion: if attr.key in LogMixin.tracking_exclusion:
continue continue
deleted = [str(item) for item in hist.deleted] deleted = [str(item) for item in hist.deleted]

View File

@@ -2,11 +2,14 @@
Contains all models for sqlalchemy Contains all models for sqlalchemy
""" """
from __future__ import annotations from __future__ import annotations
import sys, logging, json 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, Template
from pandas import DataFrame from pandas import DataFrame
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import Column, INTEGER, String, JSON from sqlalchemy import Column, INTEGER, String, JSON, 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
@@ -15,7 +18,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, ctx
# NOTE: Load testing environment # NOTE: Load testing environment
if 'pytest' in sys.modules: if 'pytest' in sys.modules:
@@ -36,10 +39,6 @@ class BaseClass(Base):
__table_args__ = {'extend_existing': True} #: NOTE Will only add new columns __table_args__ = {'extend_existing': True} #: NOTE Will only add new columns
singles = ['id'] singles = ['id']
# omni_removes = ["id", 'run', "omnigui_class_dict", "omnigui_instance_dict"]
# omni_sort = ["name"]
# omni_inheritable = []
searchables = []
_misc_info = Column(JSON) _misc_info = Column(JSON)
@@ -49,7 +48,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.
@@ -59,7 +60,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.
@@ -89,10 +91,6 @@ class BaseClass(Base):
Returns: Returns:
Session: DB session from ctx settings. Session: DB session from ctx settings.
""" """
if 'pytest' not in sys.modules:
from tools import ctx
else:
from test_settings import ctx
return ctx.database_session return ctx.database_session
@classmethod @classmethod
@@ -104,10 +102,6 @@ class BaseClass(Base):
Returns: Returns:
Path: Location of the Submissions directory in Settings object Path: Location of the Submissions directory in Settings object
""" """
if 'pytest' not in sys.modules:
from tools import ctx
else:
from test_settings import ctx
return ctx.directory_path return ctx.directory_path
@classmethod @classmethod
@@ -119,17 +113,15 @@ class BaseClass(Base):
Returns: Returns:
Path: Location of the Submissions backup directory in Settings object Path: Location of the Submissions backup directory in Settings object
""" """
if 'pytest' not in sys.modules:
from tools import ctx
else:
from test_settings import ctx
return ctx.backup_path return ctx.backup_path
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
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
@@ -142,7 +134,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
@@ -155,6 +149,30 @@ class BaseClass(Base):
except AttributeError: except AttributeError:
return [] return []
@classmethod
def get_omni_sort(cls):
output = [item[0] for item in inspect.getmembers(cls, lambda a: not (inspect.isroutine(a)))
if isinstance(item[1], InstrumentedAttribute)] # and not isinstance(item[1].property, _RelationshipDeclared)]
output = [item for item in output if item not in ['_misc_info']]
return output
@classmethod
def get_searchables(cls):
output = []
for item in inspect.getmembers(cls, lambda a: not (inspect.isroutine(a))):
if item[0] in ["_misc_info"]:
continue
if not isinstance(item[1], InstrumentedAttribute):
continue
if not isinstance(item[1].property, ColumnProperty):
continue
if len(item[1].foreign_keys) > 0:
continue
if item[1].type.__class__.__name__ not in ["String"]:
continue
output.append(item[0])
return output
@classmethod @classmethod
def get_default_info(cls, *args) -> dict | list | str: def get_default_info(cls, *args) -> dict | list | str:
""" """
@@ -164,8 +182,7 @@ class BaseClass(Base):
dict | list | str: Output of key:value dict or single (list, str) desired variable dict | list | str: Output of key:value dict or single (list, str) desired variable
""" """
# NOTE: singles is a list of fields that need to be limited to 1 result. # NOTE: singles is a list of fields that need to be limited to 1 result.
singles = list(set(cls.singles + BaseClass.singles)) return dict(singles=list(set(cls.singles + BaseClass.singles)))
return dict(singles=singles)
@classmethod @classmethod
def find_regular_subclass(cls, name: str | None = None) -> Any: def find_regular_subclass(cls, name: str | None = None) -> Any:
@@ -222,14 +239,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
@@ -237,10 +255,8 @@ class BaseClass(Base):
new = False new = False
allowed = [k for k, v in cls.__dict__.items() if allowed = [k for k, v in cls.__dict__.items() if
isinstance(v, InstrumentedAttribute) or isinstance(v, hybrid_property)] isinstance(v, InstrumentedAttribute) or isinstance(v, hybrid_property)]
# and not isinstance(v.property, _RelationshipDeclared)]
sanitized_kwargs = {k: v for k, v in kwargs.items() if k in allowed} sanitized_kwargs = {k: v for k, v in kwargs.items() if k in allowed}
outside_kwargs = {k: v for k, v in kwargs.items() if k not in allowed} outside_kwargs = {k: v for k, v in kwargs.items() if k not in allowed}
logger.debug(f"Sanitized kwargs: {sanitized_kwargs}")
instance = cls.query(limit=1, **sanitized_kwargs) instance = cls.query(limit=1, **sanitized_kwargs)
if not instance or isinstance(instance, list): if not instance or isinstance(instance, list):
instance = cls() instance = cls()
@@ -254,8 +270,6 @@ class BaseClass(Base):
from backend.validators.pydant import PydBaseClass from backend.validators.pydant import PydBaseClass
if issubclass(v.__class__, PydBaseClass): if issubclass(v.__class__, PydBaseClass):
setattr(instance, k, v.to_sql()) setattr(instance, k, v.to_sql())
# else:
# logger.error(f"Could not set {k} due to {e}")
instance._misc_info.update(outside_kwargs) instance._misc_info.update(outside_kwargs)
# logger.info(f"Instance from query or create: {instance}, new: {new}") # logger.info(f"Instance from query or create: {instance}, new: {new}")
return instance, new return instance, new
@@ -286,17 +300,10 @@ class BaseClass(Base):
Returns: Returns:
Any | List[Any]: Single result if limit = 1 or List if other. Any | List[Any]: Single result if limit = 1 or List if other.
""" """
# logger.debug(f"Kwargs: {kwargs}")
# if model is None:
# model = cls
# logger.debug(f"Model: {model}")
if query is None: if query is None:
query: Query = cls.__database_session__.query(cls) query: Query = cls.__database_session__.query(cls)
# else:
# logger.debug(f"Incoming query: {query}")
singles = cls.get_default_info('singles') singles = cls.get_default_info('singles')
for k, v in kwargs.items(): for k, v in kwargs.items():
# logger.info(f"Using key: {k} with value: {v} against {cls}")
try: try:
attr = getattr(cls, k) attr = getattr(cls, k)
except (ArgumentError, AttributeError) as e: except (ArgumentError, AttributeError) as e:
@@ -308,13 +315,11 @@ class BaseClass(Base):
except AttributeError: except AttributeError:
check = False check = False
if check: if check:
logger.debug("Got uselist")
try: try:
query = query.filter(attr.contains(v)) query = query.filter(attr.contains(v))
except ArgumentError: except ArgumentError:
continue continue
else: else:
# logger.debug("Single item.")
try: try:
query = query.filter(attr == v) query = query.filter(attr == v)
except ArgumentError: except ArgumentError:
@@ -354,9 +359,6 @@ class BaseClass(Base):
try: try:
self.__database_session__.add(self) self.__database_session__.add(self)
self.__database_session__.commit() self.__database_session__.commit()
# except sqlalchemy.exc.IntegrityError as i:
# logger.error(f"Integrity error saving {self} due to: {i}")
# logger.error(pformat(self.__dict__))
except Exception as e: except Exception as e:
logger.critical(f"Problem saving {self} due to: {e}") logger.critical(f"Problem saving {self} due to: {e}")
logger.error(f"Error message: {type(e)}") logger.error(f"Error message: {type(e)}")
@@ -374,16 +376,14 @@ class BaseClass(Base):
dict: Dictionary of object minus _sa_instance_state with id at the front. dict: Dictionary of object minus _sa_instance_state with id at the front.
""" """
dicto = {key: dict(class_attr=getattr(self.__class__, key), instance_attr=getattr(self, key)) dicto = {key: dict(class_attr=getattr(self.__class__, key), instance_attr=getattr(self, key))
for key in dir(self.__class__) if for key in self.get_omni_sort()}
isinstance(getattr(self.__class__, key), InstrumentedAttribute) and key not in self.omni_removes
}
for k, v in dicto.items(): for k, v in dicto.items():
try: try:
v['instance_attr'] = v['instance_attr'].name v['instance_attr'] = v['instance_attr'].name
except AttributeError: except AttributeError:
continue continue
try: try:
dicto = list_sort_dict(input_dict=dicto, sort_list=self.__class__.omni_sort) dicto = list_sort_dict(input_dict=dicto, sort_list=self.__class__.get_omni_sort())
except TypeError as e: except TypeError as e:
logger.error(f"Could not sort {self.__class__.__name__} by list due to :{e}") logger.error(f"Could not sort {self.__class__.__name__} by list due to :{e}")
try: try:
@@ -392,7 +392,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.
@@ -405,10 +407,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.")
try:
model = getattr(pydant, f"Pyd{cls.pyd_model_name}")
except AttributeError:
return pydant.PydElastic 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
@@ -418,7 +425,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
@@ -448,14 +457,11 @@ class BaseClass(Base):
Returns: Returns:
bool: If a single unequivocal value is found will be false, else true. bool: If a single unequivocal value is found will be false, else true.
""" """
# logger.debug(f"Incoming attributes: {attributes}")
for key, value in attributes.items(): for key, value in attributes.items():
if value.lower() == "none": if value.lower() == "none":
value = None value = None
# logger.debug(f"Attempting to grab attribute: {key}")
self_value = getattr(self, key) self_value = getattr(self, key)
class_attr = getattr(self.__class__, key) class_attr = getattr(self.__class__, key)
# logger.debug(f"Self value: {self_value}, class attr: {class_attr} of type: {type(class_attr)}")
if isinstance(class_attr, property): if isinstance(class_attr, property):
filter = "property" filter = "property"
else: else:
@@ -475,7 +481,6 @@ class BaseClass(Base):
case "property": case "property":
pass pass
case _RelationshipDeclared(): case _RelationshipDeclared():
# logger.debug(f"Checking {self_value}")
try: try:
self_value = self_value.name self_value = self_value.name
except AttributeError: except AttributeError:
@@ -483,18 +488,14 @@ class BaseClass(Base):
if class_attr.property.uselist: if class_attr.property.uselist:
self_value = self_value.__str__() self_value = self_value.__str__()
try: try:
# logger.debug(f"Check if {self_value.__class__} is subclass of {self.__class__}")
check = issubclass(self_value.__class__, self.__class__) check = issubclass(self_value.__class__, self.__class__)
except TypeError as e: except TypeError as e:
logger.error(f"Couldn't check if {self_value.__class__} is subclass of {self.__class__} due to {e}") logger.error(f"Couldn't check if {self_value.__class__} is subclass of {self.__class__} due to {e}")
check = False check = False
if check: if check:
# logger.debug(f"Checking for subclass name.")
self_value = self_value.name self_value = self_value.name
# logger.debug(f"Checking self_value {self_value} of type {type(self_value)} against attribute {value} of type {type(value)}")
if self_value != value: if self_value != value:
output = False output = False
# logger.debug(f"Value {key} is False, returning.")
return output return output
return True return True
@@ -502,17 +503,14 @@ class BaseClass(Base):
""" """
Custom dunder method to handle potential list relationship issues. Custom dunder method to handle potential list relationship issues.
""" """
# logger.debug(f"Attempting to set: {key} to {value}")
if key.startswith("_"): if key.startswith("_"):
return super().__setattr__(key, value) return super().__setattr__(key, value)
# try:
check = not hasattr(self, key) check = not hasattr(self, key)
# except:
# return
if check: if check:
try: try:
value = json.dumps(value) value = json.dumps(value)
except TypeError: except TypeError as e:
logger.error(f"Error json dumping value: {e}")
value = str(value) value = str(value)
try: try:
self._misc_info.update({key: value}) self._misc_info.update({key: value})
@@ -524,27 +522,20 @@ class BaseClass(Base):
except AttributeError: except AttributeError:
return super().__setattr__(key, value) return super().__setattr__(key, value)
if isinstance(field_type, InstrumentedAttribute): if isinstance(field_type, InstrumentedAttribute):
# logger.debug(f"{key} is an InstrumentedAttribute.")
match field_type.property: match field_type.property:
case ColumnProperty(): case ColumnProperty():
# logger.debug(f"Setting ColumnProperty to {value}")
return super().__setattr__(key, value) return super().__setattr__(key, value)
case _RelationshipDeclared(): case _RelationshipDeclared():
# logger.debug(f"{self.__class__.__name__} Setting _RelationshipDeclared for {key} to {value}")
if field_type.property.uselist: if field_type.property.uselist:
# logger.debug(f"Setting with uselist")
existing = self.__getattribute__(key) existing = self.__getattribute__(key)
# NOTE: This is causing problems with removal of items from lists. Have to overhaul it. # NOTE: This is causing problems with removal of items from lists. Have to overhaul it.
if existing is not None: if existing is not None:
logger.debug(f"{key} Existing: {existing}, incoming: {value}")
if isinstance(value, list): if isinstance(value, list):
# value = existing + value
value = value value = value
else: else:
value = existing + [value] value = existing + [value]
else: else:
if isinstance(value, list): if isinstance(value, list):
# value = value
pass pass
else: else:
value = [value] value = [value]
@@ -552,7 +543,6 @@ class BaseClass(Base):
value = list(set(value)) value = list(set(value))
except TypeError: except TypeError:
pass pass
# logger.debug(f"Final value for {key}: {value}")
return super().__setattr__(key, value) return super().__setattr__(key, value)
else: else:
if isinstance(value, list): if isinstance(value, list):
@@ -573,7 +563,10 @@ class BaseClass(Base):
case _: case _:
return super().__setattr__(key, value) return super().__setattr__(key, value)
else: else:
try:
return super().__setattr__(key, value) return super().__setattr__(key, value)
except AttributeError:
raise AttributeError(f"Can't set {key} to {value}")
def delete(self, **kwargs): def delete(self, **kwargs):
logger.error(f"Delete has not been implemented for {self.__class__.__name__}") logger.error(f"Delete has not been implemented for {self.__class__.__name__}")
@@ -608,7 +601,6 @@ class BaseClass(Base):
relevant = {k: v for k, v in self.__class__.__dict__.items() if relevant = {k: v for k, v in self.__class__.__dict__.items() if
isinstance(v, InstrumentedAttribute) or isinstance(v, AssociationProxy)} isinstance(v, InstrumentedAttribute) or isinstance(v, AssociationProxy)}
# output = OrderedDict()
output = dict(excluded=["excluded", "misc_info", "_misc_info", "id"]) output = dict(excluded=["excluded", "misc_info", "_misc_info", "id"])
for k, v in relevant.items(): for k, v in relevant.items():
try: try:
@@ -621,15 +613,14 @@ class BaseClass(Base):
value = getattr(self, k) value = getattr(self, k)
except AttributeError: except AttributeError:
continue continue
# try: match value:
# logger.debug(f"Setting {k} to {value} for details dict.") case str():
# except AttributeError as e: value = value.strip('\"')
# logger.error(f"Can't log {k} value due to {type(e)}") case _:
# continue 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():
# logger.debug(f"Misc info key {key}")
output[key] = value output[key] = value
return output return output
@@ -664,33 +655,20 @@ class BaseClass(Base):
from backend.validators import pydant from backend.validators import pydant
if not pyd_model_name: if not pyd_model_name:
pyd_model_name = f"Pyd{self.__class__.__name__}" pyd_model_name = f"Pyd{self.__class__.__name__}"
logger.debug(f"Looking for pydant model {pyd_model_name}") logger.info(f"Looking for pydant model {pyd_model_name}")
try: try:
pyd = getattr(pydant, pyd_model_name) pyd = getattr(pydant, pyd_model_name)
except AttributeError: except AttributeError:
raise AttributeError(f"Could not get pydantic class {pyd_model_name}") raise AttributeError(f"Could not get pydantic class {pyd_model_name}")
# logger.debug(f"Kwargs: {kwargs}")
# logger.debug(f"Dict: {pformat(self.details_dict())}")
return pyd(**self.details_dict(**kwargs)) return pyd(**self.details_dict(**kwargs))
def show_details(self, obj): def show_details(self, obj):
logger.debug("Show Details")
from frontend.widgets.submission_details import SubmissionDetails from frontend.widgets.submission_details import SubmissionDetails
dlg = SubmissionDetails(parent=obj, sub=self) dlg = SubmissionDetails(parent=obj, sub=self)
if dlg.exec(): if dlg.exec():
pass pass
def export(self, obj, output_filepath: str | Path | None = None): def export(self, obj, output_filepath: str | Path | None = None):
# if not hasattr(self, "template_file"):
# logger.error(f"Export not implemented for {self.__class__.__name__}")
# return
# pyd = self.to_pydantic()
# if not output_filepath:
# from frontend import select_save_file
# output_filepath = select_save_file(obj=obj, default_name=pyd.construct_filename(), extension="xlsx")
# Writer = getattr(writers, f"{self.__class__.__name__}Writer")
# writer = Writer(output_filepath=output_filepath, pydant_obj=pyd, range_dict=self.range_dict)
# workbook = writer
from backend import managers from backend import managers
Manager = getattr(managers, f"Default{self.__class__.__name__}") Manager = getattr(managers, f"Default{self.__class__.__name__}")
manager = Manager(parent=obj, input_object=self) manager = Manager(parent=obj, input_object=self)
@@ -749,12 +727,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):
@@ -18,10 +19,10 @@ class AuditLog(Base):
__tablename__ = "_auditlog" __tablename__ = "_auditlog"
id = Column(INTEGER, primary_key=True, autoincrement=True) #: primary key id = Column(INTEGER, primary_key=True, autoincrement=True) #: primary key
user = Column(String(64)) user = Column(String(64)) #: The user who made the change
time = Column(TIMESTAMP) time = Column(TIMESTAMP) #: When the change was made
object = Column(String(64)) object = Column(String(64)) #: What was changed
changes = Column(JSON) changes = Column(JSON) #: List of changes that were made
def __repr__(self): def __repr__(self):
return f"<{self.object}: {self.user} @ {self.time}>" return f"<{self.object}: {self.user} @ {self.time}>"

View File

@@ -2,17 +2,13 @@
All control related models. All control related models.
""" """
from __future__ import annotations from __future__ import annotations
import itertools
from pprint import pformat from pprint import pformat
from PyQt6.QtWidgets import QWidget, QCheckBox, QLabel from PyQt6.QtWidgets import QWidget
from pandas import DataFrame from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, case
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, case, FLOAT from sqlalchemy.orm import relationship, Query
from sqlalchemy.orm import relationship, Query, validates
import logging, re import logging, re
from operator import itemgetter
from . import BaseClass from . import BaseClass
from tools import setup_lookup, report_result, Result, Report, Settings, get_unique_values_in_df_column, super_splitter, \ from tools import setup_lookup, Report, Settings, super_splitter
flatten_list, timer
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from typing import List, Literal, Tuple, Generator from typing import List, Literal, Tuple, Generator
from re import Pattern from re import Pattern
@@ -131,16 +127,6 @@ class Control(BaseClass):
procedure = relationship("Procedure", back_populates="control", procedure = relationship("Procedure", back_populates="control",
foreign_keys=[procedure_id]) #: parent procedure foreign_keys=[procedure_id]) #: parent procedure
# __mapper_args__ = {
# "polymorphic_identity": "Basic Control",
# "polymorphic_on": case(
# (controltype_name == "PCR Control", "PCR Control"),
# (controltype_name == "Irida Control", "Irida Control"),
# else_="Basic Control"
# ),
# "with_polymorphic": "*",
# }
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<{self.controltype_name}({self.name})>" return f"<{self.controltype_name}({self.name})>"
@@ -282,450 +268,3 @@ class Control(BaseClass):
def delete(self): def delete(self):
self.__database_session__.delete(self) self.__database_session__.delete(self)
self.__database_session__.commit() self.__database_session__.commit()
# class PCRControl(Control):
# """
# Class made to hold info from Design & Analysis software.
# """
#
# id = Column(INTEGER, ForeignKey('_control.id'), primary_key=True)
# subtype = Column(String(16)) #: PC or NC
# target = Column(String(16)) #: N1, N2, etc.
# ct = Column(FLOAT) #: PCR result
# reagent_lot = Column(String(64), ForeignKey("_reagent.lot", ondelete="SET NULL",
# name="fk_reagent_lot"))
# reagent = relationship("Reagent", foreign_keys=reagent_lot) #: reagent used for this control
#
# __mapper_args__ = dict(polymorphic_identity="PCR Control",
# polymorphic_load="inline",
# inherit_condition=(id == Control.id))
#
# def to_sub_dict(self) -> dict:
# """
# Creates dictionary of fields for this object.
#
# Returns:
# dict: Output dict of name, ct, subtype, target, reagent_lot and submitted_date
# """
# return dict(
# name=self.name,
# ct=self.ct,
# subtype=self.subtype,
# target=self.target,
# reagent_lot=self.reagent_lot,
# submitted_date=self.submitted_date.date()
# )
#
# @classmethod
# @report_result
# def make_chart(cls, parent, chart_settings: dict, ctx: Settings) -> Tuple[Report, "PCRFigure"]:
# """
# Creates a PCRFigure. Overrides parent
#
# Args:
# parent (__type__): Widget to contain the chart.
# chart_settings (dict): settings passed down from chart widget
# ctx (Settings): settings passed down from gui. Not used here.
#
# Returns:
# Tuple[Report, "PCRFigure"]: Report of status and resulting figure.
# """
# from frontend.visualizations.pcr_charts import PCRFigure
# parent.mode_typer.clear()
# parent.mode_typer.setEnabled(False)
# report = Report()
# control = cls.query(proceduretype=chart_settings['submissiontype'], start_date=chart_settings['start_date'],
# end_date=chart_settings['end_date'])
# data = [control.to_sub_dict() for control in control]
# df = DataFrame.from_records(data)
# # NOTE: Get all PCR control with ct over 0
# try:
# df = df[df.ct > 0.0]
# except AttributeError:
# df = df
# fig = PCRFigure(df=df, modes=[], settings=chart_settings)
# return report, fig
#
# def to_pydantic(self):
# from backend.validators import PydPCRControl
# return PydPCRControl(**self.to_sub_dict(),
# controltype_name=self.controltype_name,
# clientsubmission_id=self.clientsubmission_id)
#
#
# class IridaControl(Control):
# subtyping_allowed = ['kraken']
#
# id = Column(INTEGER, ForeignKey('_control.id'), primary_key=True)
# contains = Column(JSON) #: unstructured hashes in contains.tsv for each organism
# matches = Column(JSON) #: unstructured hashes in matches.tsv for each organism
# kraken = Column(JSON) #: unstructured output from kraken_report
# subtype = Column(String(16), nullable=False) #: EN-NOS, MCS-NOS, etc
# refseq_version = Column(String(16)) #: version of refseq used in fastq parsing
# kraken2_version = Column(String(16)) #: version of kraken2 used in fastq parsing
# kraken2_db_version = Column(String(32)) #: folder name of kraken2 db
# sample_id = Column(INTEGER,
# ForeignKey("_basicsample.id", ondelete="SET NULL", name="cont_BCS_id")) #: sample id key
#
# __mapper_args__ = dict(polymorphic_identity="Irida Control",
# polymorphic_load="inline",
# inherit_condition=(id == Control.id))
#
# @property
# def targets(self):
# if self.controltype.targets:
# return list(itertools.chain.from_iterable([value for key, value in self.controltype.targets.items()
# if key == self.subtype]))
# else:
# return ["None"]
#
# @validates("subtype")
# def enforce_subtype_literals(self, key: str, value: str) -> str:
# """
# Validates submissiontype field with acceptable values
#
# Args:
# key (str): Field name
# value (str): Field Value
#
# Raises:
# KeyError: Raised if value is not in the acceptable list.
#
# Returns:
# str: Validated string.
# """
# acceptables = ['ATCC49226', 'ATCC49619', 'EN-NOS', "EN-SSTI", "MCS-NOS", "MCS-SSTI", "SN-NOS", "SN-SSTI"]
# if value.upper() not in acceptables:
# raise KeyError(f"Sub-type must be in {acceptables}")
# return value
#
# def to_sub_dict(self) -> dict:
# """
# Converts object into convenient dictionary for use in procedure summary
#
# Returns:
# dict: output dictionary containing: Name, Type, Targets, Top Kraken results
# """
# try:
# kraken = self.kraken
# except TypeError:
# kraken = {}
# try:
# kraken_cnt_total = sum([item['kraken_count'] for item in kraken.values()])
# except AttributeError:
# kraken_cnt_total = 0
# try:
# new_kraken = [dict(name=key, kraken_count=value['kraken_count'],
# kraken_percent=f"{value['kraken_count'] / kraken_cnt_total:0.2%}",
# target=key in self.controltype.targets)
# for key, value in kraken.items()]
# new_kraken = sorted(new_kraken, key=itemgetter('kraken_count'), reverse=True)[0:10]
# except (AttributeError, ZeroDivisionError):
# new_kraken = []
# output = dict(
# name=self.name,
# type=self.controltype.name,
# targets=", ".join(self.targets),
# kraken=new_kraken
# )
# return output
#
# def convert_by_mode(self, control_sub_type: str, mode: Literal['kraken', 'matches', 'contains'],
# consolidate: bool = False) -> Generator[dict, None, None]:
# """
# split this instance into analysis types ('kraken', 'matches', 'contains') for control graphs
#
# Args:
# consolidate (bool): whether to merge all off-target genera. Defaults to False
# control_sub_type (str): control subtype, 'MCS-NOS', etc.
# mode (Literal['kraken', 'matches', 'contains']): analysis type, 'contains', etc.
#
# Returns:
# List[dict]: list of records
# """
# try:
# data = self.__getattribute__(mode)
# except TypeError:
# data = {}
# if data is None:
# data = {}
# # NOTE: Data truncation and consolidation.
# if "kraken" in mode:
# data = {k: v for k, v in sorted(data.items(), key=lambda d: d[1][f"{mode}_count"], reverse=True)[:50]}
# else:
# if consolidate:
# on_tar = {k: v for k, v in data.items() if k.strip("*") in self.controltype.targets[control_sub_type]}
# off_tar = sum(v[f'{mode}_ratio'] for k, v in data.items() if
# k.strip("*") not in self.controltype.targets[control_sub_type])
# on_tar['Off-target'] = {f"{mode}_ratio": off_tar}
# data = on_tar
# for genus in data:
# _dict = dict(
# name=self.name,
# submitted_date=self.submitted_date,
# genus=genus,
# target='Target' if genus.strip("*") in self.controltype.targets[control_sub_type] else "Off-target"
# )
# for key in data[genus]:
# _dict[key] = data[genus][key]
# yield _dict
#
# @classproperty
# def modes(cls) -> List[str]:
# """
# Get all control modes from database
#
# Returns:
# List[str]: List of control mode names.
# """
# try:
# cols = [item.name for item in list(cls.__table__.columns) if isinstance(item.type, JSON)]
# except AttributeError as e:
# logger.error(f"Failed to get available modes from db: {e}")
# cols = []
# return cols
#
# @classmethod
# def make_parent_buttons(cls, parent: QWidget) -> None:
# """
# Creates buttons for controlling
#
# Args:
# parent (QWidget): chart holding widget to add buttons to.
#
# """
# super().make_parent_buttons(parent=parent)
# rows = parent.layout.rowCount() - 2
# # NOTE: check box for consolidating off-target items
# checker = QCheckBox(parent)
# checker.setChecked(True)
# checker.setObjectName("irida_check")
# checker.setToolTip("Pools off-target genera to save time.")
# parent.layout.addWidget(QLabel("Consolidate Off-targets"), rows, 0, 1, 1)
# parent.layout.addWidget(checker, rows, 1, 1, 2)
# checker.checkStateChanged.connect(parent.update_data)
#
# @classmethod
# @report_result
# def make_chart(cls, chart_settings: dict, parent, ctx) -> Tuple[Report, "IridaFigure" | None]:
# """
# Creates a IridaFigure. Overrides parent
#
# Args:
# parent (__type__): Widget to contain the chart.
# chart_settings (dict): settings passed down from chart widget
# ctx (Settings): settings passed down from gui.
#
# Returns:
# Tuple[Report, "IridaFigure"]: Report of status and resulting figure.
# """
# from frontend.visualizations import IridaFigure
# try:
# checker = parent.findChild(QCheckBox, name="irida_check")
# if chart_settings['mode'] == "kraken":
# checker.setEnabled(False)
# checker.setChecked(False)
# else:
# checker.setEnabled(True)
# consolidate = checker.isChecked()
# except AttributeError:
# consolidate = False
# report = Report()
# control = cls.query(subtype=chart_settings['submissiontype'], start_date=chart_settings['start_date'],
# end_date=chart_settings['end_date'])
# if not control:
# report.add_result(Result(status="Critical", msg="No control found in given date range."))
# return report, None
# # NOTE: change each control to list of dictionaries
# data = [control.convert_by_mode(control_sub_type=chart_settings['submissiontype'], mode=chart_settings['mode'],
# consolidate=consolidate) for
# control in control]
# # NOTE: flatten data to one dimensional list
# # data = [item for sublist in data for item in sublist]
# data = flatten_list(data)
# if not data:
# report.add_result(Result(status="Critical", msg="No data found for control in given date range."))
# return report, None
# df = cls.convert_data_list_to_df(input_df=data, sub_mode=chart_settings['sub_mode'])
# if chart_settings['sub_mode'] is None:
# title = chart_settings['sub_mode']
# else:
# title = f"{chart_settings['mode']} - {chart_settings['sub_mode']}"
# # NOTE: send dataframe to chart maker
# df, modes = cls.prep_df(ctx=ctx, df=df)
# fig = IridaFigure(df=df, ytitle=title, modes=modes, parent=parent,
# settings=chart_settings)
# return report, fig
#
# @classmethod
# def convert_data_list_to_df(cls, input_df: list[dict], sub_mode) -> DataFrame:
# """
# Convert list of control records to dataframe
#
# Args:
# input_df (list[dict]): list of dictionaries containing records
# sub_mode (str | None, optional): submissiontype of procedure type. Defaults to None.
#
# Returns:
# DataFrame: dataframe of control
# """
# df = DataFrame.from_records(input_df)
# safe = ['name', 'submitted_date', 'genus', 'target']
# for column in df.columns:
# if column not in safe:
# if sub_mode is not None and column != sub_mode:
# continue
# else:
# safe.append(column)
# if "percent" in column:
# try:
# count_col = next(item for item in df.columns if "count" in item)
# except StopIteration:
# continue
# # NOTE: The actual percentage from kraken was off due to exclusion of NaN, recalculating.
# df[column] = 100 * df[count_col] / df.groupby('name')[count_col].transform('sum')
# df = df[[c for c in df.columns if c in safe]]
# # NOTE: move date of sample submitted on same date as previous ahead one.
# df = cls.displace_date(df=df)
# # NOTE: ad hoc method to make data labels more accurate.
# df = cls.df_column_renamer(df=df)
# return df
#
# @classmethod
# def df_column_renamer(cls, df: DataFrame) -> DataFrame:
# """
# Ad hoc function I created to clarify some fields
#
# Args:
# df (DataFrame): input dataframe
#
# Returns:
# DataFrame: dataframe with 'clarified' column names
# """
# df = df[df.columns.drop(list(df.filter(regex='_hashes')))]
# return df.rename(columns={
# "contains_ratio": "contains_shared_hashes_ratio",
# "matches_ratio": "matches_shared_hashes_ratio",
# "kraken_count": "kraken2_read_count_(top_50)",
# "kraken_percent": "kraken2_read_percent_(top_50)"
# })
#
# @classmethod
# def displace_date(cls, df: DataFrame) -> DataFrame:
# """
# This function serves to split sample that were submitted on the same date by incrementing dates.
# It will shift the date forward by one day if it is the same day as an existing date in a list.
#
# Args:
# df (DataFrame): input dataframe composed of control records
#
# Returns:
# DataFrame: output dataframe with dates incremented.
# """
# # NOTE: get submitted dates for each control
# dict_list = [dict(name=item, date=df[df.name == item].iloc[0]['submitted_date']) for item in
# sorted(df['name'].unique())]
# previous_dates = set()
# for item in dict_list:
# df, previous_dates = cls.check_date(df=df, item=item, previous_dates=previous_dates)
# return df
#
# @classmethod
# def check_date(cls, df: DataFrame, item: dict, previous_dates: set) -> Tuple[DataFrame, list]:
# """
# Checks if an items date is already present in df and adjusts df accordingly
#
# Args:
# df (DataFrame): input dataframe
# item (dict): control for checking
# previous_dates (list): list of dates found in previous control
#
# Returns:
# Tuple[DataFrame, list]: Output dataframe and appended list of previous dates
# """
# try:
# check = item['date'] in previous_dates
# except IndexError:
# check = False
# previous_dates.add(item['date'])
# if check:
# # NOTE: get df locations where name == item name
# mask = df['name'] == item['name']
# # NOTE: increment date in dataframe
# df.loc[mask, 'submitted_date'] = df.loc[mask, 'submitted_date'].apply(lambda x: x + timedelta(days=1))
# item['date'] += timedelta(days=1)
# passed = False
# else:
# passed = True
# # NOTE: if procedure didn't lead to changed date, return values
# if passed:
# return df, previous_dates
# # NOTE: if date was changed, rerun with new date
# else:
# logger.warning(f"Date check failed, running recursion.")
# df, previous_dates = cls.check_date(df, item, previous_dates)
# return df, previous_dates
#
# @classmethod
# def prep_df(cls, ctx: Settings, df: DataFrame) -> Tuple[DataFrame | None, list]:
# """
# Constructs figures based on parsed pandas dataframe.
#
# Args:
# ctx (Settings): settings passed down from gui
# df (pd.DataFrame): input dataframe
# ytitle (str | None, optional): title for the y-axis. Defaults to None.
#
# Returns:
# Figure: Plotly figure
# """
# # NOTE: converts starred genera to normal and splits off list of starred
# if df.empty:
# return None, []
# df['genus'] = df['genus'].replace({'\*': ''}, regex=True).replace({"NaN": "Unknown"})
# df['genera'] = [item[-1] if item and item[-1] == "*" else "" for item in df['genus'].to_list()]
# # NOTE: remove original run, using reruns if applicable
# df = cls.drop_reruns_from_df(ctx=ctx, df=df)
# # NOTE: sort by and exclude from
# sorts = ['submitted_date', "target", "genus"]
# exclude = ['name', 'genera']
# modes = [item for item in df.columns if item not in sorts and item not in exclude]
# # NOTE: Set descending for any columns that have "{mode}" in the header.
# ascending = [False if item == "target" else True for item in sorts]
# df = df.sort_values(by=sorts, ascending=ascending)
# # NOTE: actual chart construction is done by
# return df, modes
#
# @classmethod
# def drop_reruns_from_df(cls, ctx: Settings, df: DataFrame) -> DataFrame:
# """
# Removes semi-duplicates from dataframe after finding sequencing repeats.
#
# Args:
# ctx (Settings): settings passed from gui
# df (DataFrame): initial dataframe
#
# Returns:
# DataFrame: dataframe with originals removed in favour of repeats.
# """
# if 'rerun_regex' in ctx.model_extra:
# sample_names = get_unique_values_in_df_column(df, column_name="name")
# rerun_regex = re.compile(fr"{ctx.rerun_regex}")
# exclude = [re.sub(rerun_regex, "", sample) for sample in sample_names if rerun_regex.search(sample)]
# df = df[~df.name.isin(exclude)]
# return df
#
# def to_pydantic(self) -> "PydIridaControl":
# """
# Constructs a pydantic version of this object.
#
# Returns:
# PydIridaControl: This object as a pydantic model.
# """
# from backend.validators import PydIridaControl
# return PydIridaControl(**self.__dict__)
#
# @property
# def is_positive_control(self):
# return not self.subtype.lower().startswith("en")

View File

@@ -3,21 +3,19 @@ All client organization related models.
''' '''
from __future__ import annotations from __future__ import annotations
import logging import logging
from pathlib import Path
from pprint import pformat
from sqlalchemy import Column, String, INTEGER, ForeignKey, Table from sqlalchemy import Column, String, INTEGER, ForeignKey, Table
from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import relationship, Query, declared_attr
from sqlalchemy.orm import relationship, Query from . import BaseClass
from . import Base, BaseClass
from tools import check_authorization, setup_lookup from tools import check_authorization, setup_lookup
from typing import List, Tuple from typing import List
logger = logging.getLogger(f"submissions.{__name__}") 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
@@ -31,7 +29,7 @@ class ClientLab(BaseClass):
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) #: clientlab name name = Column(String(64)) #: clientlab name
clientsubmission = relationship("ClientSubmission", back_populates="clientlab") #: procedure this clientlab has submitted clientsubmission = relationship("ClientSubmission", back_populates="clientlab") #: submission this clientlab has submitted
cost_centre = Column(String()) #: cost centre used by org for payment cost_centre = Column(String()) #: cost centre used by org for payment
contact = relationship("Contact", back_populates="clientlab", contact = relationship("Contact", back_populates="clientlab",
secondary=clientlab_contact) #: contact involved with this org secondary=clientlab_contact) #: contact involved with this org
@@ -47,6 +45,7 @@ class ClientLab(BaseClass):
Lookup clientlabs in the database by a number of parameters. Lookup clientlabs in the database by a number of parameters.
Args: Args:
id (int | None, optional): id integer of the clientlab. Defaults to None.
name (str | None, optional): Name of the clientlab. Defaults to None. name (str | None, optional): Name of the clientlab. Defaults to None.
limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0. limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
@@ -100,24 +99,12 @@ 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 []
# @classmethod
# def query_or_create(cls, **kwargs) -> Tuple[Contact, bool]:
# new = False
# disallowed = []
# sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed}
# instance = cls.query(**sanitized_kwargs)
# if not instance or isinstance(instance, list):
# instance = cls()
# new = True
# for k, v in sanitized_kwargs.items():
# setattr(instance, k, v)
# logger.info(f"Instance from contact query or create: {instance}")
# return instance, new
@classmethod @classmethod
@setup_lookup @setup_lookup
def query(cls, def query(cls,
@@ -131,6 +118,7 @@ class Contact(BaseClass):
Lookup contact in the database by a number of parameters. Lookup contact in the database by a number of parameters.
Args: Args:
id (int | None, optional): id integer of the contact. Defaults to None.
name (str | None, optional): Name of the contact. Defaults to None. name (str | None, optional): Name of the contact. Defaults to None.
email (str | None, optional): Email of the contact. Defaults to None. email (str | None, optional): Email of the contact. Defaults to None.
phone (str | None, optional): Phone number of the contact. Defaults to None. phone (str | None, optional): Phone number of the contact. Defaults to None.

File diff suppressed because it is too large Load Diff

View File

@@ -2,43 +2,26 @@
Models for the main procedure and sample types. Models for the main procedure and sample types.
""" """
from __future__ import annotations from __future__ import annotations
import itertools
import pickle
from copy import deepcopy
from getpass import getuser from getpass import getuser
import logging, uuid, tempfile, re, base64, numpy as np, pandas as pd, types, sys import logging, tempfile, re, numpy as np, pandas as pd, types, sys, itertools
from inspect import isclass from inspect import isclass
from io import BytesIO from zipfile import BadZipfile
from zipfile import ZipFile, BadZipfile
from tempfile import TemporaryDirectory, TemporaryFile
from operator import itemgetter from operator import itemgetter
from pprint import pformat from pprint import pformat
import openpyxl
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, Reagent, 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, Table, Sequence from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case, func
from sqlalchemy.orm import relationship, validates, 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
ArgumentError
from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError
from openpyxl import Workbook from tools import setup_lookup, jinja_template_loading, create_holidays_for_year, check_dictionary_inclusion_equality, is_power_user
from openpyxl.drawing.image import Image as OpenpyxlImage
from tools import row_map, setup_lookup, jinja_template_loading, rreplace, row_keys, check_key_or_attr, Result, Report, \
report_result, create_holidays_for_year, check_dictionary_inclusion_equality, is_power_user
from datetime import datetime, date from datetime import datetime, date
from typing import List, Any, Tuple, Literal, Generator, Type, TYPE_CHECKING from typing import List, Literal, Generator, TYPE_CHECKING
from pathlib import Path from pathlib import Path
from jinja2.exceptions import TemplateNotFound
from jinja2 import Template
from PIL import Image
if TYPE_CHECKING: if TYPE_CHECKING:
from backend.db.models.procedures import ProcedureType, Procedure from backend.db.models.procedures import ProcedureType, Procedure
@@ -51,21 +34,21 @@ class ClientSubmission(BaseClass, LogMixin):
""" """
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
submitter_plate_id = Column(String(127), unique=True) #: The number given to the procedure by the submitting lab submitter_plate_id = Column(String(127), unique=True) #: The number given to the submission by the submitting lab
submitted_date = Column(TIMESTAMP) #: Date procedure received submitted_date = Column(TIMESTAMP) #: Date submission received
clientlab = relationship("ClientLab", back_populates="clientsubmission") #: client org clientlab = relationship("ClientLab", back_populates="clientsubmission") #: client org
clientlab_id = Column(INTEGER, ForeignKey("_clientlab.id", ondelete="SET NULL", clientlab_id = Column(INTEGER, ForeignKey("_clientlab.id", ondelete="SET NULL",
name="fk_BS_sublab_id")) #: client lab id from _organizations name="fk_BS_sublab_id")) #: client lab id from _organizations
submission_category = Column(String(64)) submission_category = Column(String(64)) #: i.e. Surveillance
sample_count = Column(INTEGER) #: Number of sample in the procedure sample_count = Column(INTEGER) #: Number of sample in the procedure
full_batch_size = Column(INTEGER) #: Number of wells in provided plate. 0 if no plate. full_batch_size = Column(INTEGER) #: Number of wells in provided plate. 0 if no plate.
comment = Column(JSON) comment = Column(JSON) #: comment objects from users.
run = relationship("Run", back_populates="clientsubmission") #: many-to-one relationship run = relationship("Run", back_populates="clientsubmission") #: many-to-one relationship
contact = relationship("Contact", back_populates="clientsubmission") #: client org contact = relationship("Contact", back_populates="clientsubmission") #: contact representing submitting lab.
contact_id = Column(INTEGER, ForeignKey("_contact.id", ondelete="SET NULL", contact_id = Column(INTEGER, ForeignKey("_contact.id", ondelete="SET NULL",
name="fk_BS_contact_id")) #: client lab id from _organizations name="fk_BS_contact_id")) #: contact id from _organizations
submissiontype_name = Column(String, ForeignKey("_submissiontype.name", ondelete="SET NULL", submissiontype_name = Column(String, ForeignKey("_submissiontype.name", ondelete="SET NULL",
name="fk_BS_subtype_name")) #: name of joined procedure type name="fk_BS_subtype_name")) #: name of joined submission type
submissiontype = relationship("SubmissionType", back_populates="clientsubmission") #: archetype of this procedure submissiontype = relationship("SubmissionType", back_populates="clientsubmission") #: archetype of this procedure
cost_centre = Column( cost_centre = Column(
String(64)) #: Permanent storage of used cost centre in case organization field changed in the future. String(64)) #: Permanent storage of used cost centre in case organization field changed in the future.
@@ -93,7 +76,7 @@ class ClientSubmission(BaseClass, LogMixin):
@setup_lookup @setup_lookup
def query(cls, def query(cls,
submissiontype: str | SubmissionType | None = None, submissiontype: str | SubmissionType | None = None,
submissiontype_name: str | None = None, # submissiontype_name: str | None = None,
id: int | str | None = None, id: int | str | None = None,
submitter_plate_id: str | None = None, submitter_plate_id: str | None = None,
start_date: date | datetime | str | int | None = None, start_date: date | datetime | str | int | None = None,
@@ -108,7 +91,7 @@ class ClientSubmission(BaseClass, LogMixin):
Lookup procedure based on a number of parameters. Overrides parent. Lookup procedure based on a number of parameters. Overrides parent.
Args: Args:
submission_type (str | models.SubmissionType | None, optional): Submission type of interest. Defaults to None. submissiontype (str | models.SubmissionType | None, optional): Submission type of interest. Defaults to None.
id (int | str | None, optional): Submission id in the database (limits results to 1). Defaults to None. id (int | str | None, optional): Submission id in the database (limits results to 1). Defaults to None.
rsl_plate_number (str | None, optional): Submission name in the database (limits results to 1). Defaults to None. rsl_plate_number (str | None, optional): Submission name in the database (limits results to 1). Defaults to None.
start_date (date | str | int | None, optional): Beginning date to search by. Defaults to None. start_date (date | str | int | None, optional): Beginning date to search by. Defaults to None.
@@ -133,7 +116,6 @@ class ClientSubmission(BaseClass, LogMixin):
if start_date is not None: if start_date is not None:
start_date = cls.rectify_query_date(start_date) start_date = cls.rectify_query_date(start_date)
end_date = cls.rectify_query_date(end_date, eod=True) end_date = cls.rectify_query_date(end_date, eod=True)
logger.debug(f"Start date: {start_date}, end date: {end_date}")
query = query.filter(cls.submitted_date.between(start_date, end_date)) query = query.filter(cls.submitted_date.between(start_date, end_date))
# NOTE: by rsl number (returns only a single value) # NOTE: by rsl number (returns only a single value)
match submitter_plate_id: match submitter_plate_id:
@@ -142,9 +124,11 @@ class ClientSubmission(BaseClass, LogMixin):
limit = 1 limit = 1
case _: case _:
pass pass
match submissiontype_name: match submissiontype:
case SubmissionType():
query = query.filter(cls.submissiontype == submissiontype)
case str(): case str():
query = query.filter(cls.submissiontype_name == submissiontype_name) query = query.filter(cls.submissiontype_name == submissiontype)
case _: case _:
pass pass
# NOTE: by id (returns only a single value) # NOTE: by id (returns only a single value)
@@ -157,7 +141,6 @@ class ClientSubmission(BaseClass, LogMixin):
limit = 1 limit = 1
case _: case _:
pass pass
# query = query.order_by(cls.submitted_date.desc())
# NOTE: Split query results into pages of size {page_size} # NOTE: Split query results into pages of size {page_size}
if page_size > 0 and limit == 0: if page_size > 0 and limit == 0:
limit = page_size limit = page_size
@@ -249,11 +232,8 @@ class ClientSubmission(BaseClass, LogMixin):
if report: if report:
return output return output
if full_data: if full_data:
# dicto, _ = self.kittype.construct_xl_map_for_use(self.proceduretype)
# sample = self.generate_associations(name="clientsubmissionsampleassociation")
samples = None samples = None
runs = [item.to_dict(full_data=True) for item in self.run] runs = [item.to_dict(full_data=True) for item in self.run]
# custom = self.custom
else: else:
samples = None samples = None
custom = None custom = None
@@ -280,7 +260,6 @@ class ClientSubmission(BaseClass, LogMixin):
output["comment"] = comments output["comment"] = comments
output["contact"] = contact output["contact"] = contact
output["contact_phone"] = contact_phone output["contact_phone"] = contact_phone
# output["custom"] = custom
output["run"] = runs output["run"] = runs
output['name'] = self.name output['name'] = self.name
return output return output
@@ -291,7 +270,6 @@ class ClientSubmission(BaseClass, LogMixin):
except AssertionError: except AssertionError:
logger.warning(f"Converting {sample} to sql.") logger.warning(f"Converting {sample} to sql.")
sample = sample.to_sql() sample = sample.to_sql()
# logger.debug(sample.__dict__)
try: try:
row = sample._misc_info['row'] row = sample._misc_info['row']
except (KeyError, AttributeError): except (KeyError, AttributeError):
@@ -300,7 +278,6 @@ class ClientSubmission(BaseClass, LogMixin):
column = sample._misc_info['column'] column = sample._misc_info['column']
except KeyError: except KeyError:
column = 0 column = 0
# logger.debug(f"Sample: {sample}")
submission_rank = sample._misc_info['submission_rank'] submission_rank = sample._misc_info['submission_rank']
if sample in self.sample: if sample in self.sample:
return return
@@ -311,7 +288,6 @@ class ClientSubmission(BaseClass, LogMixin):
row=row, row=row,
column=column column=column
) )
# assoc.save()
return assoc return assoc
@property @property
@@ -326,20 +302,16 @@ class ClientSubmission(BaseClass, LogMixin):
return {item: self.__getattribute__(item.lower().replace(" ", "_")) for item in names} return {item: self.__getattribute__(item.lower().replace(" ", "_")) for item in names}
def add_run(self, obj): def add_run(self, obj):
logger.debug("Add Run")
from frontend.widgets.sample_checker import SampleChecker from frontend.widgets.sample_checker import SampleChecker
samples = [sample.to_pydantic() for sample in self.clientsubmissionsampleassociation] samples = [sample.to_pydantic() for sample in self.clientsubmissionsampleassociation]
checker = SampleChecker(parent=None, title="Create Run", samples=samples, clientsubmission=self) checker = SampleChecker(parent=None, title="Create Run", samples=samples, clientsubmission=self)
if checker.exec(): if checker.exec():
run = Run(clientsubmission=self, rsl_plate_number=checker.rsl_plate_number) run = Run(clientsubmission=self, rsl_plate_number=checker.rsl_plate_number)
active_samples = [sample for sample in samples if sample.enabled] active_samples = [sample for sample in samples if sample.enabled]
logger.debug(active_samples)
for sample in active_samples: for sample in active_samples:
sample = sample.to_sql() sample = sample.to_sql()
logger.debug(f"Sample: {sample.id}")
if sample not in run.sample: if sample not in run.sample:
assoc = run.add_sample(sample) assoc = run.add_sample(sample)
# assoc.save()
run.save() run.save()
else: else:
logger.warning("Run cancelled.") logger.warning("Run cancelled.")
@@ -351,13 +323,6 @@ class ClientSubmission(BaseClass, LogMixin):
def add_comment(self, obj): def add_comment(self, obj):
logger.debug("Add Comment") logger.debug("Add Comment")
# def show_details(self, obj):
# logger.debug("Show Details")
# from frontend.widgets.submission_details import SubmissionDetails
# dlg = SubmissionDetails(parent=obj, sub=self)
# if dlg.exec():
# pass
def details_dict(self, **kwargs): def details_dict(self, **kwargs):
output = super().details_dict(**kwargs) output = super().details_dict(**kwargs)
output['clientlab'] = output['clientlab'].details_dict() output['clientlab'] = output['clientlab'].details_dict()
@@ -377,7 +342,6 @@ class ClientSubmission(BaseClass, LogMixin):
def to_pydantic(self, filepath: Path | str | None = None, **kwargs): def to_pydantic(self, filepath: Path | str | None = None, **kwargs):
output = super().to_pydantic(filepath=filepath, **kwargs) output = super().to_pydantic(filepath=filepath, **kwargs)
# output.template_file = self.template_file
return output return output
@@ -389,18 +353,16 @@ class Run(BaseClass, LogMixin):
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
rsl_plate_number = Column(String(32), unique=True, nullable=False) #: RSL name (e.g. RSL-22-0012) rsl_plate_number = Column(String(32), unique=True, nullable=False) #: RSL name (e.g. RSL-22-0012)
clientsubmission_id = Column(INTEGER, ForeignKey("_clientsubmission.id", ondelete="SET NULL", clientsubmission_id = Column(INTEGER, ForeignKey("_clientsubmission.id", ondelete="SET NULL",
name="fk_BS_clientsub_id")) #: client lab id from _organizations) name="fk_BS_clientsub_id")) #: id of parent clientsubmission
clientsubmission = relationship("ClientSubmission", back_populates="run") clientsubmission = relationship("ClientSubmission", back_populates="run") #: parent clientsubmission
_started_date = Column(TIMESTAMP) #: Date this procedure was started. _started_date = Column(TIMESTAMP) #: Date this procedure was started.
run_cost = Column( run_cost = Column(
FLOAT(2)) #: total cost of running the plate. Set from constant and mutable kittype costs at time of creation. FLOAT(2)) #: total cost of running the plate. Set from constant and mutable kittype costs at time of creation.
signed_by = Column(String(32)) #: user name of person who submitted the procedure to the database. signed_by = Column(String(32)) #: user name of person who submitted the procedure to the database.
comment = Column(JSON) #: user notes comment = Column(JSON) #: user notes
custom = Column(JSON) custom = Column(JSON) #: unknown
_completed_date = Column(TIMESTAMP) #: Date this procedure was finished.
_completed_date = Column(TIMESTAMP) procedure = relationship("Procedure", back_populates="run", uselist=True) #: children procedures
procedure = relationship("Procedure", back_populates="run", uselist=True)
runsampleassociation = relationship( runsampleassociation = relationship(
"RunSampleAssociation", "RunSampleAssociation",
@@ -412,20 +374,6 @@ class Run(BaseClass, LogMixin):
"sample", creator=lambda sample: RunSampleAssociation( "sample", creator=lambda sample: RunSampleAssociation(
sample=sample)) #: Association proxy to ClientSubmissionSampleAssociation.sample sample=sample)) #: Association proxy to ClientSubmissionSampleAssociation.sample
# NOTE: Allows for subclassing into ex. BacterialCulture, Wastewater, etc.
# __mapper_args__ = {
# "polymorphic_identity": "Basic Submission",
# "polymorphic_on": case(
#
# (submissiontype_name == "Wastewater", "Wastewater"),
# (submissiontype_name == "Wastewater Artic", "Wastewater Artic"),
# (submissiontype_name == "Bacterial Culture", "Bacterial Culture"),
#
# else_="Basic Submission"
# ),
# "with_polymorphic": "*",
# }
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<Submission({self.name})>" return f"<Submission({self.name})>"
@@ -505,13 +453,14 @@ class Run(BaseClass, LogMixin):
output = {k: v for k, v in dicto.items() if k in args} output = {k: v for k, v in dicto.items() if k in args}
else: else:
output = {k: v for k, v in dicto.items()} output = {k: v for k, v in dicto.items()}
logger.debug(f"Submission type for get default info: {submissiontype}") # logger.debug(f"Submission type for get default info: {submissiontype}")
if isinstance(submissiontype, SubmissionType): if isinstance(submissiontype, SubmissionType):
st = submissiontype st = submissiontype
else: else:
st = cls.get_submission_type(submissiontype) st = cls.get_submission_type(submissiontype)
if st is None: if st is None:
logger.error("No default info for Run.") # logger.error("No default info for Run.")
pass
else: else:
output['submissiontype'] = st.name output['submissiontype'] = st.name
for k, v in st.defaults.items(): for k, v in st.defaults.items():
@@ -556,7 +505,6 @@ class Run(BaseClass, LogMixin):
case SubmissionType(): case SubmissionType():
return submissiontype return submissiontype
case _: case _:
# return SubmissionType.query(cls.__mapper_args__['polymorphic_identity'])
return None return None
@classmethod @classmethod
@@ -697,7 +645,7 @@ class Run(BaseClass, LogMixin):
'permission', "clientsubmission"] 'permission', "clientsubmission"]
output['sample_count'] = self.sample_count output['sample_count'] = self.sample_count
output['clientsubmission'] = self.clientsubmission.name output['clientsubmission'] = self.clientsubmission.name
output['clientlab'] = self.clientsubmission.clientlab # output['clientlab'] = self.clientsubmission.clientlab
output['started_date'] = self.started_date output['started_date'] = self.started_date
output['completed_date'] = self.completed_date output['completed_date'] = self.completed_date
return output return output
@@ -712,14 +660,12 @@ class Run(BaseClass, LogMixin):
query_out = [] query_out = []
for sub_type in submissiontype: for sub_type in submissiontype:
subs = cls.query(page_size=0, start_date=start_date, end_date=end_date, submissiontype=sub_type) subs = cls.query(page_size=0, start_date=start_date, end_date=end_date, submissiontype=sub_type)
# logger.debug(f"Sub results: {run}")
query_out.append(subs) query_out.append(subs)
query_out = list(itertools.chain.from_iterable(query_out)) query_out = list(itertools.chain.from_iterable(query_out))
else: else:
query_out = cls.query(page_size=0, start_date=start_date, end_date=end_date) query_out = cls.query(page_size=0, start_date=start_date, end_date=end_date)
records = [] records = []
for sub in query_out: for sub in query_out:
# output = sub.to_dict(full_data=True)
output = sub.details_dict() output = sub.details_dict()
for k, v in output.items(): for k, v in output.items():
if isinstance(v, types.GeneratorType): if isinstance(v, types.GeneratorType):
@@ -746,29 +692,6 @@ class Run(BaseClass, LogMixin):
Calculates cost of the plate Calculates cost of the plate
""" """
# NOTE: Calculate number of columns based on largest column number # NOTE: Calculate number of columns based on largest column number
# try:
# cols_count_96 = self.column_count
# except Exception as e:
# logger.error(f"Column count error: {e}")
# # NOTE: Get kittype associated with this procedure
# # logger.debug(f"Checking associations with procedure type: {self.submissiontype_name}")
# assoc = next((item for item in self.kittype.kit_submissiontype_associations if
# item.proceduretype == self.submission_type),
# None)
# # logger.debug(f"Got association: {assoc}")
# # NOTE: If every individual cost is 0 this is probably an old plate.
# if all(item == 0.0 for item in [assoc.constant_cost, assoc.mutable_cost_column, assoc.mutable_cost_sample]):
# try:
# self.run_cost = self.kittype.cost_per_run
# except Exception as e:
# logger.error(f"Calculation error: {e}")
# else:
# try:
# self.run_cost = assoc.constant_cost + (assoc.mutable_cost_column * cols_count_96) + (
# assoc.mutable_cost_sample * int(self.sample_count))
# except Exception as e:
# logger.error(f"Calculation error: {e}")
# self.run_cost = round(self.run_cost, 2)
pass pass
@property @property
@@ -802,7 +725,6 @@ class Run(BaseClass, LogMixin):
""" """
rows = range(1, plate_rows + 1) rows = range(1, plate_rows + 1)
columns = range(1, plate_columns + 1) columns = range(1, plate_columns + 1)
# logger.debug(f"sample list for plate map: {pformat(sample_list)}")
# NOTE: An overly complicated list comprehension create a list of sample locations # NOTE: An overly complicated list comprehension create a list of sample locations
# NOTE: next will return a blank cell if no value found for row/column # NOTE: next will return a blank cell if no value found for row/column
output_samples = [next((item for item in sample_list if item['row'] == row and item['column'] == column), output_samples = [next((item for item in sample_list if item['row'] == row and item['column'] == column),
@@ -841,7 +763,6 @@ class Run(BaseClass, LogMixin):
pd.DataFrame: Pandas Dataframe of all relevant procedure pd.DataFrame: Pandas Dataframe of all relevant procedure
""" """
# NOTE: use lookup function to create list of dicts # NOTE: use lookup function to create list of dicts
# subs = [item.to_dict() for item in
subs = [item.details_dict() for item in subs = [item.details_dict() for item in
cls.query(submissiontype=submission_type, limit=limit, chronologic=chronologic, page=page, cls.query(submissiontype=submission_type, limit=limit, chronologic=chronologic, page=page,
page_size=page_size)] page_size=page_size)]
@@ -872,8 +793,6 @@ class Run(BaseClass, LogMixin):
value (_type_): value of attribute value (_type_): value of attribute
""" """
match key: match key:
# case "kittype":
# field_value = KitType.query(name=value)
case "clientlab": case "clientlab":
field_value = ClientLab.query(name=value) field_value = ClientLab.query(name=value)
case "contact": case "contact":
@@ -900,13 +819,11 @@ class Run(BaseClass, LogMixin):
existing = value existing = value
case _: case _:
existing = self.__getattribute__(key) existing = self.__getattribute__(key)
logger.debug(f"Existing value is {pformat(existing)}")
if value in ['', 'null', None]: if value in ['', 'null', None]:
logger.error(f"No value given, not setting.") logger.error(f"No value given, not setting.")
return return
if existing is None: if existing is None:
existing = [] existing = []
# if value in existing:
if check_dictionary_inclusion_equality(existing, value): if check_dictionary_inclusion_equality(existing, value):
logger.warning("Value already exists. Preventing duplicate addition.") logger.warning("Value already exists. Preventing duplicate addition.")
return return
@@ -955,17 +872,6 @@ class Run(BaseClass, LogMixin):
pass pass
return assoc return assoc
# def update_reagentassoc(self, reagent: Reagent, role: str):
# # NOTE: get the first reagent assoc that fills the given reagentrole.
# try:
# assoc = next(item for item in self.submission_reagent_associations if
# item.reagent and role in [role.name for role in item.reagent.equipmentrole])
# assoc.reagent = reagent
# except StopIteration as e:
# logger.error(f"Association for {role} not found, creating new association.")
# assoc = ProcedureReagentAssociation(procedure=self, reagent=reagent)
# self.submission_reagent_associations.append(assoc)
def to_pydantic(self, backup: bool = False) -> "PydSubmission": def to_pydantic(self, backup: bool = False) -> "PydSubmission":
""" """
Converts this instance into a PydSubmission Converts this instance into a PydSubmission
@@ -973,7 +879,7 @@ class Run(BaseClass, LogMixin):
Returns: Returns:
PydSubmission: converted object. PydSubmission: converted object.
""" """
from backend.validators import PydRun from backend.validators import PydClientSubmission, PydRun
dicto = self.details_dict(full_data=True, backup=backup) dicto = self.details_dict(full_data=True, backup=backup)
new_dict = {} new_dict = {}
for key, value in dicto.items(): for key, value in dicto.items():
@@ -1028,7 +934,6 @@ class Run(BaseClass, LogMixin):
Returns: Returns:
str: String from which regex will be compiled. str: String from which regex will be compiled.
""" """
# logger.debug(f"Class for regex: {cls}")
try: try:
regex = cls.get_submission_type(submission_type).defaults['regex'] regex = cls.get_submission_type(submission_type).defaults['regex']
except AttributeError as e: except AttributeError as e:
@@ -1038,12 +943,12 @@ class Run(BaseClass, LogMixin):
regex = re.compile(rf"{regex}", flags=re.IGNORECASE | re.VERBOSE) regex = re.compile(rf"{regex}", flags=re.IGNORECASE | re.VERBOSE)
except re.error as e: except re.error as e:
regex = cls.construct_regex() regex = cls.construct_regex()
# logger.debug(f"Returning regex: {regex}")
return regex return regex
# 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.
@@ -1089,15 +994,6 @@ class Run(BaseClass, LogMixin):
Returns: Returns:
models.Run | List[models.Run]: Run(s) of interest models.Run | List[models.Run]: Run(s) of interest
""" """
# from ... import RunReagentAssociation
# NOTE: if you go back to using 'model' change the appropriate cls to model in the query filters
# if submissiontype is not None:
# model = cls.find_polymorphic_subclass(polymorphic_identity=submissiontype)
# elif len(kwargs) > 0:
# # NOTE: find the subclass containing the relevant attributes
# model = cls.find_polymorphic_subclass(attrs=kwargs)
# else:
# model = cls
query: Query = cls.__database_session__.query(cls) query: Query = cls.__database_session__.query(cls)
if start_date is not None and end_date is None: if start_date is not None and end_date is None:
logger.warning(f"Start date with no end date, using today.") logger.warning(f"Start date with no end date, using today.")
@@ -1107,38 +1003,8 @@ class Run(BaseClass, LogMixin):
start_date = cls.__database_session__.query(cls, func.min(cls.submitted_date)).first()[1] start_date = cls.__database_session__.query(cls, func.min(cls.submitted_date)).first()[1]
logger.warning(f"End date with no start date, using first procedure date: {start_date}") logger.warning(f"End date with no start date, using first procedure date: {start_date}")
if start_date is not None: if start_date is not None:
# match start_date:
# case date():
# pass
# case datetime():
# start_date = start_date.date()
# case int():
# start_date = datetime.fromordinal(
# datetime(1900, 1, 1).toordinal() + start_date - 2).date()
# case _:
# start_date = parse(start_date).date()
# # start_date = start_date.strftime("%Y-%m-%d")
# match end_date:
# case date():
# pass
# case datetime():
# end_date = end_date # + timedelta(days=1)
# # pass
# case int():
# end_date = datetime.fromordinal(datetime(1900, 1, 1).toordinal() + end_date - 2).date() # \
# # + timedelta(days=1)
# case _:
# end_date = parse(end_date).date() # + timedelta(days=1)
# # end_date = end_date.strftime("%Y-%m-%d")
# start_date = datetime.combine(start_date, datetime.min.time()).strftime("%Y-%m-%d %H:%M:%S.%f")
# end_date = datetime.combine(end_date, datetime.max.time()).strftime("%Y-%m-%d %H:%M:%S.%f")
# # if start_date == end_date:
# # start_date = start_date.strftime("%Y-%m-%d %H:%M:%S.%f")
# # query = query.filter(model.submitted_date == start_date)
# # else:
start_date = cls.rectify_query_date(start_date) start_date = cls.rectify_query_date(start_date)
end_date = cls.rectify_query_date(end_date, eod=True) end_date = cls.rectify_query_date(end_date, eod=True)
logger.debug(f"Start date: {start_date}, end date: {end_date}")
query = query.join(ClientSubmission).filter(ClientSubmission.submitted_date.between(start_date, end_date)) query = query.join(ClientSubmission).filter(ClientSubmission.submitted_date.between(start_date, end_date))
# NOTE: by rsl number (returns only a single value) # NOTE: by rsl number (returns only a single value)
match name: match name:
@@ -1164,7 +1030,6 @@ class Run(BaseClass, LogMixin):
limit = 1 limit = 1
case _: case _:
pass pass
# query = query.order_by(cls.submitted_date.desc())
# NOTE: Split query results into pages of size {page_size} # NOTE: Split query results into pages of size {page_size}
if page_size > 0: if page_size > 0:
query = query.limit(page_size) query = query.limit(page_size)
@@ -1173,58 +1038,6 @@ class Run(BaseClass, LogMixin):
query = query.offset(page * page_size) query = query.offset(page * page_size)
return cls.execute_query(query=query, limit=limit, **kwargs) return cls.execute_query(query=query, limit=limit, **kwargs)
# @classmethod
# def query_or_create(cls, submissiontype: str | SubmissionType | None = None, **kwargs) -> Run:
# """
# Returns object from db if exists, else, creates new. Due to need for user input, doesn't see much use ATM.
#
# Args:
# submissiontype (str | SubmissionType | None, optional): Submission type to be created. Defaults to None.
#
# Raises:
# ValueError: Raised if no kwargs passed.
# ValueError: Raised if disallowed key is passed.
#
# Returns:
# cls: A Run subclass instance.
# """
# code = 0
# msg = ""
# report = Report()
# disallowed = ["id"]
# if kwargs == {}:
# raise ValueError("Need to narrow down query or the first available instance will be returned.")
# sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed}
# instance = cls.query(submissiontype=submissiontype, limit=1, **sanitized_kwargs)
# if instance is None:
# used_class = cls.find_polymorphic_subclass(attrs=kwargs, polymorphic_identity=submissiontype)
# instance = used_class(**sanitized_kwargs)
# match submissiontype:
# case str():
# submissiontype = SubmissionType.query(name=submissiontype)
# case _:
# pass
# instance.proceduretype = submissiontype
# instance.submissiontype_name = submissiontype.name
# if "submitted_date" not in kwargs.keys():
# instance.submitted_date = date.today()
# else:
# from frontend.widgets.pop_ups import QuestionAsker
# logger.warning(f"Found existing instance: {instance}, asking to overwrite.")
# # code = 1
# # msg = "This procedure already exists.\nWould you like to overwrite?"
# # report.add_result(Result(msg=msg, code=code))
# dlg = QuestionAsker(title="Overwrite?",
# message="This procedure already exists.\nWould you like to overwrite?")
# if dlg.exec():
# pass
# else:
# code = 1
# msg = "This procedure already exists.\nWould you like to overwrite?"
# report.add_result(Result(msg=msg, code=code))
# return None, report
# return instance, report
# NOTE: Custom context events for the ui # NOTE: Custom context events for the ui
@property @property
@@ -1237,18 +1050,15 @@ class Run(BaseClass, LogMixin):
""" """
names = ["Add Procedure", "Edit", "Export", "Add Comment", "Show Details", "Delete"] names = ["Add Procedure", "Edit", "Export", "Add Comment", "Show Details", "Delete"]
output = {item: self.__getattribute__(item.lower().replace(" ", "_")) for item in names} output = {item: self.__getattribute__(item.lower().replace(" ", "_")) for item in names}
logger.debug(output)
return output return output
def add_procedure(self, obj, proceduretype_name: str): def add_procedure(self, obj, proceduretype_name: str):
from frontend.widgets.procedure_creation import ProcedureCreation from frontend.widgets.procedure_creation import ProcedureCreation
procedure_type: ProcedureType = next( procedure_type: ProcedureType = next(
(proceduretype for proceduretype in self.allowed_procedures if proceduretype.name == proceduretype_name)) (proceduretype for proceduretype in self.allowed_procedures if proceduretype.name == proceduretype_name))
logger.debug(f"Got ProcedureType: {procedure_type}")
dlg = ProcedureCreation(parent=obj, procedure=procedure_type.construct_dummy_procedure(run=self)) dlg = ProcedureCreation(parent=obj, procedure=procedure_type.construct_dummy_procedure(run=self))
if dlg.exec(): if dlg.exec():
sql, _ = dlg.return_sql(new=True) sql, _ = dlg.return_sql(new=True)
# sys.exit(pformat(sql.__dict__))
sql.save() sql.save()
obj.set_data() obj.set_data()
@@ -1282,18 +1092,6 @@ class Run(BaseClass, LogMixin):
except AttributeError: except AttributeError:
logger.error("App will not refresh data at this time.") logger.error("App will not refresh data at this time.")
# def show_details(self, obj):
# """
# Creates Widget for showing procedure details.
#
# Args:
# obj (Widget): Parent widget
# """
# from frontend.widgets.submission_details import SubmissionDetails
# dlg = SubmissionDetails(parent=obj, sub=self)
# if dlg.exec():
# pass
def edit(self, obj): def edit(self, obj):
""" """
Return procedure to form widget for updating Return procedure to form widget for updating
@@ -1315,7 +1113,6 @@ class Run(BaseClass, LogMixin):
Args: Args:
obj (_type_): parent widget obj (_type_): parent widget
""" """
logger.debug(obj)
from frontend.widgets.submission_details import SubmissionComment from frontend.widgets.submission_details import SubmissionComment
dlg = SubmissionComment(parent=obj, submission=self) dlg = SubmissionComment(parent=obj, submission=self)
if dlg.exec(): if dlg.exec():
@@ -1437,8 +1234,6 @@ class Run(BaseClass, LogMixin):
unranked_samples.append(sample) unranked_samples.append(sample)
possible_ranks = (item for item in list(plate_dict.keys()) if possible_ranks = (item for item in list(plate_dict.keys()) if
item not in [sample['submission_rank'] for sample in ranked_samples]) item not in [sample['submission_rank'] for sample in ranked_samples])
# logger.debug(possible_ranks)
# possible_ranks = (plate_dict[idx] for idx in possible_ranks)
for sample in unranked_samples: for sample in unranked_samples:
try: try:
submission_rank = next(possible_ranks) submission_rank = next(possible_ranks)
@@ -1457,17 +1252,9 @@ class Run(BaseClass, LogMixin):
background_color="#ffffff", enabled=False) background_color="#ffffff", enabled=False)
) )
padded_list.append(sample) padded_list.append(sample)
# logger.debug(f"Final padded list:\n{pformat(list(sorted(padded_list, key=itemgetter('submission_rank'))))}")
return list(sorted(padded_list, key=itemgetter('submission_rank'))) return list(sorted(padded_list, key=itemgetter('submission_rank')))
# class SampleType(BaseClass):
# id = Column(INTEGER, primary_key=True) #: primary key
# name = Column(String(64), nullable=False, unique=True) #: identification from submitter
#
# sample = relationship("Sample", back_populates="sampletype", uselist=True)
# NOTE: Sample Classes # NOTE: Sample Classes
class Sample(BaseClass, LogMixin): class Sample(BaseClass, LogMixin):
@@ -1477,11 +1264,7 @@ class Sample(BaseClass, LogMixin):
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
sample_id = Column(String(64), nullable=False, unique=True) #: identification from submitter sample_id = Column(String(64), nullable=False, unique=True) #: identification from submitter
# sampletype_id = Column(INTEGER, ForeignKey("_sampletype.id", ondelete="SET NULL", control = relationship("Control", back_populates="sample", uselist=False) #: Control function this sample fills.
# name="fk_SAMP_sampletype_id"))
# sampletype = relationship("SampleType", back_populates="sample")
# misc_info = Column(JSON)
control = relationship("Control", back_populates="sample", uselist=False)
sampleclientsubmissionassociation = relationship( sampleclientsubmissionassociation = relationship(
"ClientSubmissionSampleAssociation", "ClientSubmissionSampleAssociation",
@@ -1515,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")]
@@ -1529,13 +1313,8 @@ class Sample(BaseClass, LogMixin):
Returns: Returns:
dict: submitter id and sample type and linked procedure if full data dict: submitter id and sample type and linked procedure if full data
""" """
# try:
# sample_type = self.sampletype.name
# except AttributeError:
# sample_type = "NA"
sample = dict( sample = dict(
sample_id=self.sample_id sample_id=self.sample_id
# sampletype=sample_type
) )
if full_data: if full_data:
sample['clientsubmission'] = sorted([item.to_sub_dict() for item in self.sampleclientsubmissionassociation], sample['clientsubmission'] = sorted([item.to_sub_dict() for item in self.sampleclientsubmissionassociation],
@@ -1563,7 +1342,6 @@ class Sample(BaseClass, LogMixin):
@setup_lookup @setup_lookup
def query(cls, def query(cls,
sample_id: str | None = None, sample_id: str | None = None,
# sampletype: str | SampleType | None = None,
limit: int = 0, limit: int = 0,
**kwargs **kwargs
) -> Sample | List[Sample]: ) -> Sample | List[Sample]:
@@ -1578,13 +1356,6 @@ class Sample(BaseClass, LogMixin):
models.Sample|List[models.Sample]: Sample(s) of interest. models.Sample|List[models.Sample]: Sample(s) of interest.
""" """
query = cls.__database_session__.query(cls) query = cls.__database_session__.query(cls)
# match sampletype:
# case str():
# query = query.join(SampleType).filter(SampleType.name == sampletype)
# case SampleType():
# query = query.filter(cls.sampletype == sampletype)
# case _:
# pass
match sample_id: match sample_id:
case str(): case str():
query = query.filter(cls.sample_id == sample_id) query = query.filter(cls.sample_id == sample_id)
@@ -1593,38 +1364,6 @@ class Sample(BaseClass, LogMixin):
pass pass
return cls.execute_query(query=query, limit=limit, **kwargs) return cls.execute_query(query=query, limit=limit, **kwargs)
# @classmethod
# def fuzzy_search(cls,
# sampletype: str | Sample | None = None,
# **kwargs
# ) -> List[Sample]:
# """
# Allows for fuzzy search of sample.
#
# Args:
# sampletype (str | BasicSample | None, optional): Type of sample. Defaults to None.
#
# Returns:
# List[Sample]: List of sample that match kwarg search parameters.
# """
# query: Query = cls.__database_session__.query(cls)
# match sampletype:
# case str():
# query = query.join(SampleType).filter(SampleType.name == sampletype)
# case SampleType():
# query = query.filter(cls.sampletype == sampletype)
# case _:
# pass
# for k, v in kwargs.items():
# search = f"%{v}%"
# try:
# attr = getattr(cls, k)
# # NOTE: the secret sauce is in attr.like
# query = query.filter(attr.like(search))
# except (ArgumentError, AttributeError) as e:
# logger.error(f"Attribute {k} unavailable due to:\n\t{e}\nSkipping.")
# return query.limit(50).all()
def delete(self): def delete(self):
raise AttributeError(f"Delete not implemented for {self.__class__}") raise AttributeError(f"Delete not implemented for {self.__class__}")
@@ -1686,12 +1425,9 @@ class ClientSubmissionSampleAssociation(BaseClass):
DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
""" """
# id = Column(INTEGER, unique=True, nullable=False, autoincrement=True) #: id to be used for inheriting purposes
sample_id = Column(INTEGER, ForeignKey("_sample.id"), primary_key=True) #: id of associated sample sample_id = Column(INTEGER, ForeignKey("_sample.id"), primary_key=True) #: id of associated sample
clientsubmission_id = Column(INTEGER, ForeignKey("_clientsubmission.id"), clientsubmission_id = Column(INTEGER, ForeignKey("_clientsubmission.id"),
primary_key=True) #: id of associated procedure primary_key=True) #: id of associated client submission
# row = Column(INTEGER)
# column = Column(INTEGER)
submission_rank = Column(INTEGER, primary_key=True, default=0) #: Location in sample list submission_rank = Column(INTEGER, primary_key=True, default=0) #: Location in sample list
# NOTE: reference to the Submission object # NOTE: reference to the Submission object
clientsubmission = relationship("ClientSubmission", clientsubmission = relationship("ClientSubmission",
@@ -1708,10 +1444,6 @@ class ClientSubmissionSampleAssociation(BaseClass):
self.row = row self.row = row
self.column = column self.column = column
self.submission_rank = submission_rank self.submission_rank = submission_rank
# if id is not None:
# self.id = id
# else:
# self.id = self.__class__.autoincrement_id()
for k, v in kwargs.items(): for k, v in kwargs.items():
try: try:
self.__setattr__(k, v) self.__setattr__(k, v)
@@ -1735,13 +1467,6 @@ class ClientSubmissionSampleAssociation(BaseClass):
# NOTE: Get associated sample info # NOTE: Get associated sample info
sample = self.sample.to_sub_dict() sample = self.sample.to_sub_dict()
sample['sample_id'] = self.sample.sample_id sample['sample_id'] = self.sample.sample_id
# sample['row'] = self.row
# sample['column'] = self.column
# try:
# sample['well'] = f"{row_map[self.row]}{self.column}"
# except (KeyError, AttributeError) as e:
# logger.error(f"Unable to find row {self.row} in row_map.")
# sample['Well'] = None
sample['plate_name'] = self.clientsubmission.submitter_plate_id sample['plate_name'] = self.clientsubmission.submitter_plate_id
sample['positive'] = False sample['positive'] = False
sample['submitted_date'] = self.clientsubmission.submitted_date sample['submitted_date'] = self.clientsubmission.submitted_date
@@ -1752,10 +1477,8 @@ class ClientSubmissionSampleAssociation(BaseClass):
output = super().details_dict() output = super().details_dict()
# NOTE: Figure out how to merge the misc_info if doing .update instead. # NOTE: Figure out how to merge the misc_info if doing .update instead.
relevant = {k: v for k, v in output.items() if k not in ['sample']} relevant = {k: v for k, v in output.items() if k not in ['sample']}
# logger.debug(f"Relevant info from assoc output: {pformat(relevant)}")
output = output['sample'].details_dict() output = output['sample'].details_dict()
misc = output['misc_info'] misc = output['misc_info']
# # logger.debug(f"Output from sample: {pformat(output)}")
output.update(relevant) output.update(relevant)
output['misc_info'] = misc output['misc_info'] = misc
return output return output
@@ -1798,48 +1521,6 @@ class ClientSubmissionSampleAssociation(BaseClass):
sample.update(dict(Name=self.sample.sample_id[:10], tooltip=tooltip_text, background_color=background)) sample.update(dict(Name=self.sample.sample_id[:10], tooltip=tooltip_text, background_color=background))
return sample return sample
# @classmethod
# def autoincrement_id(cls) -> int:
# """
# Increments the association id automatically
#
# Returns:
# int: incremented id
# """
# if cls.__name__ == "ClientSubmissionSampleAssociation":
# model = cls
# else:
# model = next((base for base in cls.__bases__ if base.__name__ == "ClientSubmissionSampleAssociation"),
# ClientSubmissionSampleAssociation)
# try:
# return max([item.id for item in model.query()]) + 1
# except ValueError as e:
# logger.error(f"Problem incrementing id: {e}")
# return 1
# @classmethod
# def find_polymorphic_subclass(cls, polymorphic_identity: str | None = None) -> ClientSubmissionSampleAssociation:
# """
# Retrieves subclasses of ClientSubmissionSampleAssociation based on type name.
#
# Args:
# polymorphic_identity (str | None, optional): Name of subclass fed to polymorphic identity. Defaults to None.
#
# Returns:
# ClientSubmissionSampleAssociation: Subclass of interest.
# """
# if isinstance(polymorphic_identity, dict):
# polymorphic_identity = polymorphic_identity['value']
# if polymorphic_identity is None:
# model = cls
# else:
# try:
# model = cls.__mapper__.polymorphic_map[polymorphic_identity].class_
# except Exception as e:
# logger.error(f"Could not get polymorph {polymorphic_identity} of {cls} due to {e}")
# model = cls
# return model
@classmethod @classmethod
@setup_lookup @setup_lookup
def query(cls, def query(cls,
@@ -1857,12 +1538,14 @@ class ClientSubmissionSampleAssociation(BaseClass):
Lookup junction of Submission and Sample in the database Lookup junction of Submission and Sample in the database
Args: Args:
run (models.Run | str | None, optional): Submission of interest. Defaults to None. clientsubmission (models.ClientSubmission | str | None, optional): Submission of interest. Defaults to None.
exclude_submission_type ( str | None, optional): Name of submissiontype to exclude. Defaults to None.
sample (models.Sample | str | None, optional): Sample of interest. Defaults to None. sample (models.Sample | str | None, optional): Sample of interest. Defaults to None.
row (int, optional): Row of the sample location on procedure plate. Defaults to 0. row (int, optional): Row of the sample location on procedure plate. Defaults to 0.
column (int, optional): Column of the sample location on the procedure plate. Defaults to 0. column (int, optional): Column of the sample location on the procedure plate. Defaults to 0.
limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0. limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
chronologic (bool, optional): Return results in chronologic order. Defaults to False. chronologic (bool, optional): Return results in chronologic order. Defaults to False.
reverse (bool, optional): Whether or not to reverse order of list. Defaults to False.
Returns: Returns:
models.ClientSubmissionSampleAssociation|List[models.ClientSubmissionSampleAssociation]: Junction(s) of interest models.ClientSubmissionSampleAssociation|List[models.ClientSubmissionSampleAssociation]: Junction(s) of interest
@@ -1960,12 +1643,8 @@ class RunSampleAssociation(BaseClass):
DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
""" """
# id = Column(INTEGER, unique=True, nullable=False) #: id to be used for inheriting purposes
sample_id = Column(INTEGER, ForeignKey("_sample.id"), primary_key=True) #: id of associated sample sample_id = Column(INTEGER, ForeignKey("_sample.id"), primary_key=True) #: id of associated sample
run_id = Column(INTEGER, ForeignKey("_run.id"), primary_key=True) #: id of associated procedure run_id = Column(INTEGER, ForeignKey("_run.id"), primary_key=True) #: id of associated procedure
# row = Column(INTEGER) #: row on the 96 well plate
# column = Column(INTEGER) #: column on the 96 well plate
# misc_info = Column(JSON)
# NOTE: reference to the Submission object # NOTE: reference to the Submission object
@@ -2003,13 +1682,6 @@ class RunSampleAssociation(BaseClass):
# NOTE: Get associated sample info # NOTE: Get associated sample info
sample = self.sample.to_sub_dict() sample = self.sample.to_sub_dict()
sample['name'] = self.sample.sample_id sample['name'] = self.sample.sample_id
# sample['row'] = self.row
# sample['column'] = self.column
# try:
# sample['well'] = f"{row_map[self.row]}{self.column}"
# except KeyError as e:
# logger.error(f"Unable to find row {self.row} in row_map.")
# sample['Well'] = None
sample['plate_name'] = self.run.rsl_plate_number sample['plate_name'] = self.run.rsl_plate_number
sample['positive'] = False sample['positive'] = False
return sample return sample
@@ -2070,11 +1742,13 @@ class RunSampleAssociation(BaseClass):
Args: Args:
run (models.Run | str | None, optional): Submission of interest. Defaults to None. run (models.Run | str | None, optional): Submission of interest. Defaults to None.
exclude_submission_type ( str | None, optional): Name of submissiontype to exclude. Defaults to None.
sample (models.Sample | str | None, optional): Sample of interest. Defaults to None. sample (models.Sample | str | None, optional): Sample of interest. Defaults to None.
row (int, optional): Row of the sample location on procedure plate. Defaults to 0. row (int, optional): Row of the sample location on procedure plate. Defaults to 0.
column (int, optional): Column of the sample location on the procedure plate. Defaults to 0. column (int, optional): Column of the sample location on the procedure plate. Defaults to 0.
limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0. limit (int, optional): Maximum number of results to return (0 = all). Defaults to 0.
chronologic (bool, optional): Return results in chronologic order. Defaults to False. chronologic (bool, optional): Return results in chronologic order. Defaults to False.
reverse (bool, optional): Whether or not to reverse order of list. Defaults to False.
Returns: Returns:
models.ClientSubmissionSampleAssociation|List[models.ClientSubmissionSampleAssociation]: Junction(s) of interest models.ClientSubmissionSampleAssociation|List[models.ClientSubmissionSampleAssociation]: Junction(s) of interest
@@ -2169,13 +1843,10 @@ class RunSampleAssociation(BaseClass):
output = super().details_dict() output = super().details_dict()
# NOTE: Figure out how to merge the misc_info if doing .update instead. # NOTE: Figure out how to merge the misc_info if doing .update instead.
relevant = {k: v for k, v in output.items() if k not in ['sample']} relevant = {k: v for k, v in output.items() if k not in ['sample']}
# logger.debug(f"Relevant info from assoc output: {pformat(relevant)}")
output = output['sample'].details_dict() output = output['sample'].details_dict()
misc = output['misc_info'] misc = output['misc_info']
# logger.debug(f"Output from sample: {pformat(output)}")
output.update(relevant) output.update(relevant)
output['misc_info'] = misc output['misc_info'] = misc
return output return output
@@ -2192,7 +1863,7 @@ class ProcedureSampleAssociation(BaseClass):
sample = relationship(Sample, back_populates="sampleprocedureassociation") #: associated equipment sample = relationship(Sample, back_populates="sampleprocedureassociation") #: associated equipment
results = relationship("Results", back_populates="sampleprocedureassociation") results = relationship("Results", back_populates="sampleprocedureassociation") #: associated results
@classmethod @classmethod
def query(cls, sample: Sample | str | None = None, procedure: Procedure | str | None = None, limit: int = 0, def query(cls, sample: Sample | str | None = None, procedure: Procedure | str | None = None, limit: int = 0,
@@ -2242,12 +1913,11 @@ class ProcedureSampleAssociation(BaseClass):
# NOTE: Figure out how to merge the misc_info if doing .update instead. # NOTE: Figure out how to merge the misc_info if doing .update instead.
relevant = {k: v for k, v in output.items() if k not in ['sample']} relevant = {k: v for k, v in output.items() if k not in ['sample']}
output = output['sample'].details_dict() output = output['sample'].details_dict()
logger.debug(f"Output: {pformat(output)}")
logger.debug(f"Relevant: {pformat(relevant)}")
# relevant['submission_rank'] = output['misc_info']['submission_rank']
misc = output['misc_info'] misc = output['misc_info']
output.update(relevant) output.update(relevant)
output['misc_info'] = misc output['misc_info'] = misc
output['row'] = self.row
output['column'] = self.column
output['results'] = [result.details_dict() for result in output['results']] output['results'] = [result.details_dict() for result in output['results']]
return output return output

View File

@@ -1,8 +1,18 @@
''' """
Contains pandas and openpyxl convenience functions for interacting with excel workbooks Contains pandas and openpyxl convenience functions for interacting with excel workbooks
''' """
# from .parser import * from .parsers import (
from backend.excel.parsers.clientsubmission_parser import ClientSubmissionInfoParser, ClientSubmissionSampleParser DefaultParser, DefaultKEYVALUEParser, DefaultTABLEParser,
# from .reports import * ProcedureInfoParser, ProcedureSampleParser, ProcedureReagentParser, ProcedureEquipmentParser,
# from .writer import * 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

@@ -1,5 +1,5 @@
""" """
Default Parser archetypes.
""" """
from __future__ import annotations from __future__ import annotations
import logging, re import logging, re
@@ -43,7 +43,8 @@ class DefaultParser(object):
*args (): *args ():
**kwargs (): **kwargs ():
""" """
logger.debug(f"\n\nHello from {self.__class__.__name__}\n\n") logger.info(f"\n\nHello from {self.__class__.__name__}\n\n")
self.filepath = filepath
self.proceduretype = proceduretype self.proceduretype = proceduretype
try: try:
self._pyd_object = getattr(pydant, self._pyd_object = getattr(pydant,
@@ -61,10 +62,8 @@ class DefaultParser(object):
self.worksheet = self.workbook[self.sheet] self.worksheet = self.workbook[self.sheet]
self.start_row = self.delineate_start_row(start_row=start_row) self.start_row = self.delineate_start_row(start_row=start_row)
self.end_row = self.delineate_end_row(start_row=self.start_row) self.end_row = self.delineate_end_row(start_row=self.start_row)
logger.debug(f"Start row: {self.start_row}, End row: {self.end_row}")
def to_pydantic(self): def to_pydantic(self):
# data = {key: value['value'] for key, value in self.parsed_info.items()}
data = self.parsed_info data = self.parsed_info
data['filepath'] = self.filepath data['filepath'] = self.filepath
return self._pyd_object(**data) return self._pyd_object(**data)
@@ -100,7 +99,6 @@ class DefaultKEYVALUEParser(DefaultParser):
rows = range(self.start_row, self.end_row) rows = range(self.start_row, self.end_row)
for row in rows: for row in rows:
check_row = [item for item in self.worksheet.rows][row-1] check_row = [item for item in self.worksheet.rows][row-1]
logger.debug(f"Checking row {row-1}, {check_row} for merged cells.")
if any([isinstance(cell, MergedCell) for cell in check_row]): if any([isinstance(cell, MergedCell) for cell in check_row]):
continue continue
key = self.worksheet.cell(row, 1).value key = self.worksheet.cell(row, 1).value
@@ -110,9 +108,7 @@ class DefaultKEYVALUEParser(DefaultParser):
key = key.lower().replace(":", "").strip().replace(" ", "_") key = key.lower().replace(":", "").strip().replace(" ", "_")
value = self.worksheet.cell(row, 2).value value = self.worksheet.cell(row, 2).value
missing = False if value else True missing = False if value else True
# location_map = dict(row=row, key_column=1, value_column=2, sheet=self.worksheet.title)
value = dict(value=value, missing=missing)#, location=location_map) value = dict(value=value, missing=missing)#, location=location_map)
logger.debug(f"Yielding {value} for {key}")
yield key, value yield key, value
@@ -123,7 +119,6 @@ class DefaultTABLEParser(DefaultParser):
@property @property
def parsed_info(self) -> Generator[dict, None, None]: def parsed_info(self) -> Generator[dict, None, None]:
logger.debug(f"creating dataframe from {self.start_row} to {self.end_row}")
df = DataFrame( df = DataFrame(
[item for item in self.worksheet.values][self.start_row - 1:self.end_row - 1]) [item for item in self.worksheet.values][self.start_row - 1:self.end_row - 1])
df.columns = df.iloc[0] df.columns = df.iloc[0]
@@ -131,12 +126,10 @@ class DefaultTABLEParser(DefaultParser):
df = df.dropna(axis=1, how='all') df = df.dropna(axis=1, how='all')
for ii, row in enumerate(df.iterrows()): for ii, row in enumerate(df.iterrows()):
output = {} output = {}
# for key, value in row[1].to_dict().items(): for key, value in row[1].to_dict().items():
for key, value in row[1].details_dict().items():
if isinstance(key, str): if isinstance(key, str):
key = key.lower().replace(" ", "_") key = key.lower().replace(" ", "_")
key = re.sub(r"_(\(.*\)|#)", "", key) key = re.sub(r"_(\(.*\)|#)", "", key)
# logger.debug(f"Row {ii} values: {key}: {value}")
output[key] = value output[key] = value
yield output yield output
@@ -144,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

@@ -1,11 +1,11 @@
""" """
Module for clientsubmission parsing
""" """
from __future__ import annotations from __future__ import annotations
import logging import logging
from pathlib import Path from pathlib import Path
from string import ascii_lowercase from string import ascii_lowercase
from typing import Generator, TYPE_CHECKING, Literal from typing import Generator, TYPE_CHECKING
from openpyxl.reader.excel import load_workbook from openpyxl.reader.excel import load_workbook
from tools import row_keys from tools import row_keys
from . import DefaultKEYVALUEParser, DefaultTABLEParser from . import DefaultKEYVALUEParser, DefaultTABLEParser
@@ -122,20 +122,6 @@ class ClientSubmissionInfoParser(DefaultKEYVALUEParser, SubmissionTyperMixin):
else: else:
self.submissiontype = submissiontype self.submissiontype = submissiontype
super().__init__(filepath=filepath, sheet="Client Info", start_row=1, **kwargs) super().__init__(filepath=filepath, sheet="Client Info", start_row=1, **kwargs)
# NOTE: move to the manager class.
# allowed_procedure_types = [item.name for item in self.submissiontype.proceduretype]
# for name in allowed_procedure_types:
# if name in self.workbook.sheetnames:
# # TODO: check if run with name already exists
# add_run = QuestionAsker(title="Add Run?", message="We've detected a sheet corresponding to an associated procedure type.\nWould you like to add a new run?")
# if add_run.accepted:
# # NOTE: recruit parser.
# try:
# manager = getattr(procedure_managers, name)
# except AttributeError:
# manager = procedure_managers.DefaultManager
# self.manager = manager(proceduretype=name)
# pass
@property @property
def parsed_info(self): def parsed_info(self):
@@ -144,13 +130,11 @@ class ClientSubmissionInfoParser(DefaultKEYVALUEParser, SubmissionTyperMixin):
output['clientlab'] = output['client_lab'] output['clientlab'] = output['client_lab']
except KeyError: except KeyError:
pass pass
# output['submissiontype'] = dict(value=self.submissiontype.name.title())
try: try:
output['submissiontype'] = output['submission_type'] output['submissiontype'] = output['submission_type']
output['submissiontype']['value'] = self.submissiontype.name.title() output['submissiontype']['value'] = self.submissiontype.name.title()
except KeyError: except KeyError:
pass pass
logger.debug(f"Data: {output}")
return output return output
@@ -173,8 +157,6 @@ class ClientSubmissionSampleParser(DefaultTABLEParser, SubmissionTyperMixin):
def parsed_info(self) -> Generator[dict, None, None]: def parsed_info(self) -> Generator[dict, None, None]:
output = super().parsed_info output = super().parsed_info
for ii, sample in enumerate(output, start=1): for ii, sample in enumerate(output, start=1):
# logger.debug(f"Parsed info sample: {sample}")
if isinstance(sample["row"], str) and sample["row"].lower() in ascii_lowercase[0:8]: if isinstance(sample["row"], str) and sample["row"].lower() in ascii_lowercase[0:8]:
try: try:
sample["row"] = row_keys[sample["row"]] sample["row"] = row_keys[sample["row"]]
@@ -184,5 +166,4 @@ class ClientSubmissionSampleParser(DefaultTABLEParser, SubmissionTyperMixin):
yield sample yield sample
def to_pydantic(self): def to_pydantic(self):
logger.debug(f"Attempting to pydantify: {self._pyd_object}")
return [self._pyd_object(**sample) for sample in self.parsed_info if sample['sample_id']] return [self._pyd_object(**sample) for sample in self.parsed_info if sample['sample_id']]

View File

@@ -8,6 +8,13 @@ if TYPE_CHECKING:
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
"""
TODO
- range dicts should hopefully not be necessary in this type of parser. Hopefully all procedure parsers are the same.
"""
class ProcedureInfoParser(DefaultKEYVALUEParser): class ProcedureInfoParser(DefaultKEYVALUEParser):
default_range_dict = [dict( default_range_dict = [dict(

View File

@@ -1,5 +1,5 @@
""" """
Parser for pcr results from Design and Analysis Studio
""" """
from __future__ import annotations from __future__ import annotations
import logging import logging
@@ -15,7 +15,7 @@ logger = logging.getLogger(f"submissions.{__name__}")
class PCRInfoParser(DefaultResultsInfoParser): class PCRInfoParser(DefaultResultsInfoParser):
def __init__(self, filepath: Path | str, sheet: str | None = None, start_row: int = 1, procedure=None, **kwargs): def __init__(self, filepath: Path | str, procedure=None, **kwargs):
self.results_type = "PCR" self.results_type = "PCR"
self.procedure = procedure self.procedure = procedure
super().__init__(filepath=filepath, proceduretype=self.procedure.proceduretype) super().__init__(filepath=filepath, proceduretype=self.procedure.proceduretype)

View File

@@ -7,10 +7,8 @@ from pandas import DataFrame, ExcelWriter
from pathlib import Path from pathlib import Path
from datetime import date from datetime import date
from typing import Tuple, List from typing import Tuple, List
# from backend import Procedure
from backend.db.models import Procedure, Run from backend.db.models import Procedure, Run
from tools import jinja_template_loading, get_first_blank_df_row, row_map, flatten_list from tools import jinja_template_loading, get_first_blank_df_row, row_map, flatten_list, ctx
from PyQt6.QtWidgets import QWidget from PyQt6.QtWidgets import QWidget
from openpyxl.worksheet.worksheet import Worksheet from openpyxl.worksheet.worksheet import Worksheet
@@ -47,7 +45,6 @@ class ReportMaker(object):
self.start_date = start_date self.start_date = start_date
self.end_date = end_date self.end_date = end_date
# NOTE: Set page size to zero to override limiting query size. # NOTE: Set page size to zero to override limiting query size.
# self.runs = Run.query(start_date=start_date, end_date=end_date, page_size=0)
self.procedures = Procedure.query(start_date=start_date, end_date=end_date, page_size=0) self.procedures = Procedure.query(start_date=start_date, end_date=end_date, page_size=0)
if organizations is not None: if organizations is not None:
self.procedures = [procedure for procedure in self.procedures if procedure.run.clientsubmission.clientlab.name in organizations] self.procedures = [procedure for procedure in self.procedures if procedure.run.clientsubmission.clientlab.name in organizations]
@@ -63,9 +60,7 @@ class ReportMaker(object):
""" """
if not self.procedures: if not self.procedures:
return DataFrame(), DataFrame() return DataFrame(), DataFrame()
# df = DataFrame.from_records([item.to_dict(report=True) for item in self.runs])
df = DataFrame.from_records([item.details_dict() for item in self.procedures]) df = DataFrame.from_records([item.details_dict() for item in self.procedures])
logger.debug(df.columns)
# NOTE: put procedure with the same lab together # NOTE: put procedure with the same lab together
df = df.sort_values("clientlab") df = df.sort_values("clientlab")
# NOTE: aggregate cost and sample count columns # NOTE: aggregate cost and sample count columns
@@ -178,10 +173,6 @@ class TurnaroundMaker(ReportArchetype):
Returns: Returns:
""" """
if 'pytest' not in sys.modules:
from tools import ctx
else:
from test_settings import ctx
days = sub.turnaround_time days = sub.turnaround_time
try: try:
tat = sub.get_default_info("turnaround_time") tat = sub.get_default_info("turnaround_time")
@@ -203,14 +194,12 @@ class TurnaroundMaker(ReportArchetype):
class ConcentrationMaker(ReportArchetype): class ConcentrationMaker(ReportArchetype):
def __init__(self, start_date: date, end_date: date, submission_type: str = "Bacterial Culture", def __init__(self, start_date: date, end_date: date, submission_type: str = "Bacterial Culture",
# controls_only: bool = True):
include: List[str] = []): include: List[str] = []):
self.start_date = start_date self.start_date = start_date
self.end_date = end_date self.end_date = end_date
# NOTE: Set page size to zero to override limiting query size. # NOTE: Set page size to zero to override limiting query size.
self.subs = Run.query(start_date=start_date, end_date=end_date, self.subs = Run.query(start_date=start_date, end_date=end_date,
submissiontype_name=submission_type, page_size=0) submissiontype_name=submission_type, page_size=0)
# self.sample = flatten_list([sub.get_provisional_controls(controls_only=controls_only) for sub in self.run])
try: try:
self.samples = flatten_list([sub.get_provisional_controls(include=include) for sub in self.subs]) self.samples = flatten_list([sub.get_provisional_controls(include=include) for sub in self.subs])
except AttributeError: except AttributeError:

View File

@@ -1,11 +1,12 @@
"""
Module for default excel writers
"""
from __future__ import annotations
import logging, sys import logging, sys
from datetime import datetime, date from datetime import datetime, date
from pprint import pformat from pprint import pformat
from types import NoneType
from typing import Any, Literal from typing import Any, Literal
from openpyxl.styles import Alignment, PatternFill from openpyxl.styles import Alignment, PatternFill
from openpyxl.utils import get_column_letter
from openpyxl.workbook.workbook import Workbook from openpyxl.workbook.workbook import Workbook
from openpyxl.worksheet.worksheet import Worksheet from openpyxl.worksheet.worksheet import Worksheet
from pandas import DataFrame from pandas import DataFrame
@@ -39,15 +40,14 @@ class DefaultWriter(object):
case x if issubclass(value.__class__, BaseClass): case x if issubclass(value.__class__, BaseClass):
value = value.name value = value.name
case x if issubclass(value.__class__, PydBaseClass): case x if issubclass(value.__class__, PydBaseClass):
logger.warning(f"PydBaseClass: {value}")
value = value.name value = value.name
case bytes() | list(): case bytes() | list():
value = None value = None
case datetime() | date(): case datetime() | date():
value = value.strftime("%Y-%m-%d") value = value.strftime("%Y-%m-%d")
case _: case _:
value = str(value) value = str(value)
# logger.debug(f"Returning value: {value}")
return value return value
@classmethod @classmethod
@@ -60,7 +60,6 @@ class DefaultWriter(object):
def write_to_workbook(self, workbook: Workbook, sheet: str | None = None, def write_to_workbook(self, workbook: Workbook, sheet: str | None = None,
start_row: int | None = None, *args, **kwargs): start_row: int | None = None, *args, **kwargs):
logger.debug(f"Writing to workbook with {self.__class__.__name__}")
if not start_row: if not start_row:
try: try:
start_row = self.__class__.start_row start_row = self.__class__.start_row
@@ -81,14 +80,19 @@ class DefaultWriter(object):
self.worksheet = self.prewrite(self.worksheet, start_row=start_row) self.worksheet = self.prewrite(self.worksheet, start_row=start_row)
self.start_row = self.delineate_start_row(start_row=start_row) self.start_row = self.delineate_start_row(start_row=start_row)
self.end_row = self.delineate_end_row(start_row=start_row) self.end_row = self.delineate_end_row(start_row=start_row)
logger.debug(f"{self.__class__.__name__} Start row: {self.start_row}, end row: {self.end_row}")
return workbook return workbook
def delineate_start_row(self, start_row: int = 1): def delineate_start_row(self, start_row: int = 1) -> int:
logger.debug(f"Attempting to find start row from {start_row}") """
Gets the first black row.
Args:
start_row (int): row to start looking at.
Returns:
int
"""
for iii, row in enumerate(self.worksheet.iter_rows(min_row=start_row), start=start_row): for iii, row in enumerate(self.worksheet.iter_rows(min_row=start_row), start=start_row):
if all([item.value is None for item in row]): if all([item.value is None for item in row]):
logger.debug(f"Returning {iii} for start row.")
return iii return iii
if self.worksheet.max_row == 1: if self.worksheet.max_row == 1:
return self.worksheet.max_row + 1 return self.worksheet.max_row + 1
@@ -109,7 +113,7 @@ class DefaultWriter(object):
if len(str(cell.value)) > setlen: if len(str(cell.value)) > setlen:
setlen = len(str(cell.value)) setlen = len(str(cell.value))
set_col_width = setlen + 5 set_col_width = setlen + 5
# Setting the column width # Note: Setting the column width
worksheet.column_dimensions[column].width = set_col_width worksheet.column_dimensions[column].width = set_col_width
return worksheet return worksheet
@@ -130,7 +134,6 @@ class DefaultKEYVALUEWriter(DefaultWriter):
@classmethod @classmethod
def check_location(cls, locations: list, sheet: str): def check_location(cls, locations: list, sheet: str):
logger.debug(f"Checking for location against {sheet}")
return any([item['sheet'] == sheet for item in locations]) return any([item['sheet'] == sheet for item in locations])
def write_to_workbook(self, workbook: Workbook, sheet: str | None = None, def write_to_workbook(self, workbook: Workbook, sheet: str | None = None,
@@ -164,7 +167,8 @@ class DefaultTABLEWriter(DefaultWriter):
return row_count return row_count
def delineate_end_row(self, start_row: int = 1) -> int: def delineate_end_row(self, start_row: int = 1) -> int:
return start_row + len(self.pydant_obj) + 1 end_row = start_row + len(self.pydant_obj) + 1
return end_row
def pad_samples_to_length(self, row_count, def pad_samples_to_length(self, row_count,
mode: Literal["submission", "procedure"] = "submission"): #, column_names): mode: Literal["submission", "procedure"] = "submission"): #, column_names):
@@ -193,7 +197,6 @@ class DefaultTABLEWriter(DefaultWriter):
start_row: int | None = None, *args, **kwargs) -> Workbook: start_row: int | None = None, *args, **kwargs) -> Workbook:
workbook = super().write_to_workbook(workbook=workbook, sheet=sheet, start_row=start_row, *args, **kwargs) workbook = super().write_to_workbook(workbook=workbook, sheet=sheet, start_row=start_row, *args, **kwargs)
self.header_list = self.sort_header_row(list(set(flatten_list([item.fields for item in self.pydant_obj])))) self.header_list = self.sort_header_row(list(set(flatten_list([item.fields for item in self.pydant_obj]))))
logger.debug(f"Header row: {self.header_list}")
self.worksheet = self.write_header_row(worksheet=self.worksheet) self.worksheet = self.write_header_row(worksheet=self.worksheet)
for iii, object in enumerate(self.pydant_obj, start=1): for iii, object in enumerate(self.pydant_obj, start=1):
write_row = self.start_row + iii write_row = self.start_row + iii
@@ -219,7 +222,6 @@ class DefaultTABLEWriter(DefaultWriter):
@classmethod @classmethod
def sort_header_row(cls, header_list: list) -> list: def sort_header_row(cls, header_list: list) -> list:
output = [] output = []
logger.debug(cls.exclude)
for item in cls.header_order: for item in cls.header_order:
if item in [header for header in header_list if header not in cls.exclude]: if item in [header for header in header_list if header not in cls.exclude]:
output.append(header_list.pop(header_list.index(item))) output.append(header_list.pop(header_list.index(item)))
@@ -237,4 +239,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

@@ -1,3 +1,6 @@
"""
Module for ClientSubmission writing
"""
from __future__ import annotations from __future__ import annotations
import logging import logging
from pprint import pformat from pprint import pformat
@@ -17,10 +20,8 @@ class ClientSubmissionInfoWriter(DefaultKEYVALUEWriter):
def __init__(self, pydant_obj, *args, **kwargs): def __init__(self, pydant_obj, *args, **kwargs):
super().__init__(pydant_obj=pydant_obj, *args, **kwargs) super().__init__(pydant_obj=pydant_obj, *args, **kwargs)
logger.debug(f"{self.__class__.__name__} recruited!")
def prewrite(self, worksheet: Worksheet, start_row: int) -> Worksheet: def prewrite(self, worksheet: Worksheet, start_row: int) -> Worksheet:
# worksheet.merge_cells(start_row=start_row, start_column=1, end_row=start_row, end_column=4)
worksheet.cell(row=start_row, column=1, value="Submitter Info") worksheet.cell(row=start_row, column=1, value="Submitter Info")
worksheet.cell(row=start_row, column=1).alignment = Alignment(horizontal="center") worksheet.cell(row=start_row, column=1).alignment = Alignment(horizontal="center")
return worksheet return worksheet

View File

@@ -1,14 +1,11 @@
"""
Default writers for procedures.
"""
from __future__ import annotations from __future__ import annotations
import logging import logging, sys
import sys
from pprint import pformat from pprint import pformat
from openpyxl.workbook import Workbook from openpyxl.workbook import Workbook
from backend.excel.writers import DefaultKEYVALUEWriter, DefaultTABLEWriter from backend.excel.writers import DefaultKEYVALUEWriter, DefaultTABLEWriter
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from backend.db.models import ProcedureType
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -18,38 +15,31 @@ class ProcedureInfoWriter(DefaultKEYVALUEWriter):
header_order = [] header_order = []
exclude = ['control', 'equipment', 'excluded', 'id', 'misc_info', 'plate_map', 'possible_kits', exclude = ['control', 'equipment', 'excluded', 'id', 'misc_info', 'plate_map', 'possible_kits',
'procedureequipmentassociation', 'procedurereagentassociation', 'proceduresampleassociation', 'proceduretipsassociation', 'reagent', 'procedureequipmentassociation', 'procedurereagentassociation', 'proceduresampleassociation', 'proceduretipsassociation', 'reagent',
'reagentrole', 'results', 'sample', 'tips'] 'reagentrole', 'results', 'sample', 'tips', 'reagentlot']
def __init__(self, pydant_obj, *args, **kwargs): def __init__(self, pydant_obj, *args, **kwargs):
super().__init__(pydant_obj=pydant_obj, *args, **kwargs) super().__init__(pydant_obj=pydant_obj, *args, **kwargs)
self.fill_dictionary = {k: v for k, v in self.fill_dictionary.items() if k not in self.__class__.exclude} self.fill_dictionary = {k: v for k, v in self.fill_dictionary.items() if k not in self.__class__.exclude}
# logger.debug(pformat(self.fill_dictionary))
# for rng in self.range_dict:
# if "sheet" not in rng or rng['sheet'] == "":
# rng['sheet'] = f"{pydant_obj.proceduretype.name} Quality"
def write_to_workbook(self, workbook: Workbook, sheet: str | None = None, def write_to_workbook(self, workbook: Workbook, sheet: str | None = None,
start_row: int = 1, *args, **kwargs) -> Workbook: start_row: int = 1, *args, **kwargs) -> Workbook:
workbook = super().write_to_workbook(workbook=workbook, sheet=f"{self.pydant_obj.proceduretype.name} Quality") workbook = super().write_to_workbook(workbook=workbook, sheet=f"{self.pydant_obj.proceduretype.name[:20]} Quality")
return workbook return workbook
class ProcedureReagentWriter(DefaultTABLEWriter): class ProcedureReagentWriter(DefaultTABLEWriter):
exclude = ["id", "comments", "missing"] exclude = ["id", "comments", "missing", "active", "name"]
header_order = ["reagentrole", "name", "lot", "expiry"] header_order = ["reagentrole", "reagent_name", "lot", "expiry"]
def __init__(self, pydant_obj, *args, **kwargs): def __init__(self, pydant_obj, *args, **kwargs):
super().__init__(pydant_obj=pydant_obj, *args, **kwargs) super().__init__(pydant_obj=pydant_obj, *args, **kwargs)
self.sheet = f"{self.pydant_obj.proceduretype.name} Quality" self.sheet = f"{self.pydant_obj.proceduretype.name[:20]} Quality"
self.pydant_obj = self.pydant_obj.reagent self.pydant_obj = self.pydant_obj.reagent
def write_to_workbook(self, workbook: Workbook, sheet: str | None = None, def write_to_workbook(self, workbook: Workbook, sheet: str | None = None,
start_row: int = 1, *args, **kwargs) -> Workbook: start_row: int = 1, *args, **kwargs) -> Workbook:
logger.debug(self.pydant_obj) workbook = super().write_to_workbook(workbook=workbook, sheet=self.sheet, start_row=start_row)
workbook = super().write_to_workbook(workbook=workbook, sheet=self.sheet)
return workbook return workbook
@@ -60,13 +50,12 @@ class ProcedureEquipmentWriter(DefaultTABLEWriter):
def __init__(self, pydant_obj, range_dict: dict | None = None, *args, **kwargs): def __init__(self, pydant_obj, range_dict: dict | None = None, *args, **kwargs):
super().__init__(pydant_obj=pydant_obj, range_dict=range_dict, *args, **kwargs) super().__init__(pydant_obj=pydant_obj, range_dict=range_dict, *args, **kwargs)
self.sheet = f"{self.pydant_obj.proceduretype.name} Quality" self.sheet = f"{self.pydant_obj.proceduretype.name[:20]} Quality"
self.pydant_obj = self.pydant_obj.equipment self.pydant_obj = self.pydant_obj.equipment
def write_to_workbook(self, workbook: Workbook, sheet: str | None = None, def write_to_workbook(self, workbook: Workbook, sheet: str | None = None,
start_row: int = 1, *args, **kwargs) -> Workbook: start_row: int = 1, *args, **kwargs) -> Workbook:
logger.debug(self.pydant_obj) workbook = super().write_to_workbook(workbook=workbook, sheet=self.sheet, start_row=start_row)
workbook = super().write_to_workbook(workbook=workbook, sheet=self.sheet)
return workbook return workbook
@@ -77,12 +66,10 @@ class ProcedureSampleWriter(DefaultTABLEWriter):
def __init__(self, pydant_obj, range_dict: dict | None = None, *args, **kwargs): def __init__(self, pydant_obj, range_dict: dict | None = None, *args, **kwargs):
super().__init__(pydant_obj=pydant_obj, range_dict=range_dict, *args, **kwargs) super().__init__(pydant_obj=pydant_obj, range_dict=range_dict, *args, **kwargs)
self.sheet = f"{self.pydant_obj.proceduretype.name} Quality" self.sheet = f"{self.pydant_obj.proceduretype.name[:20]} Quality"
# self.pydant_obj = self.pydant_obj.sample
self.pydant_obj = self.pad_samples_to_length(row_count=pydant_obj.max_sample_rank, mode="procedure") self.pydant_obj = self.pad_samples_to_length(row_count=pydant_obj.max_sample_rank, mode="procedure")
def write_to_workbook(self, workbook: Workbook, sheet: str | None = None, def write_to_workbook(self, workbook: Workbook, sheet: str | None = None,
start_row: int = 1, *args, **kwargs) -> Workbook: start_row: int = 1, *args, **kwargs) -> Workbook:
logger.debug(self.pydant_obj) workbook = super().write_to_workbook(workbook=workbook, sheet=self.sheet, start_row=start_row)
workbook = super().write_to_workbook(workbook=workbook, sheet=self.sheet)
return workbook return workbook

View File

@@ -1,12 +1,12 @@
"""
Writers for PCR results from Design and Analysis Software
"""
from __future__ import annotations from __future__ import annotations
import logging import logging
from pathlib import Path
from pprint import pformat from pprint import pformat
from typing import Generator, TYPE_CHECKING from typing import Generator, TYPE_CHECKING
from openpyxl import Workbook from openpyxl import Workbook
from openpyxl.styles import Alignment from openpyxl.styles import Alignment
from backend.excel.writers import DefaultKEYVALUEWriter, DefaultTABLEWriter from backend.excel.writers import DefaultKEYVALUEWriter, DefaultTABLEWriter
from tools import flatten_list from tools import flatten_list
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -21,26 +21,10 @@ class PCRInfoWriter(DefaultKEYVALUEWriter):
def __init__(self, pydant_obj, proceduretype: "ProcedureType" | None = None, *args, **kwargs): def __init__(self, pydant_obj, proceduretype: "ProcedureType" | None = None, *args, **kwargs):
super().__init__(pydant_obj=pydant_obj, proceduretype=proceduretype, *args, **kwargs) super().__init__(pydant_obj=pydant_obj, proceduretype=proceduretype, *args, **kwargs)
self.fill_dictionary = self.pydant_obj.improved_dict()['result'] self.fill_dictionary = self.pydant_obj.improved_dict()['result']
logger.debug(pformat(self.fill_dictionary))
def write_to_workbook(self, workbook: Workbook, sheet: str | None = None, def write_to_workbook(self, workbook: Workbook, sheet: str | None = None,
start_row: int | None = None, *args, **kwargs) -> Workbook: start_row: int | None = None, *args, **kwargs) -> Workbook:
workbook = super().write_to_workbook(workbook=workbook, sheet=f"{self.proceduretype.name} Results") workbook = super().write_to_workbook(workbook=workbook, sheet=f"{self.proceduretype.name} Results")
# if not start_row:
# try:
# start_row = self.__class__.start_row
# except AttributeError as e:
# logger.error(f"Couldn't get start row due to {e}")
# start_row = 1
# # worksheet = workbook[f"{self.proceduretype.name} Results"]
# self.worksheet = workbook.create_sheet(f"{self.proceduretype.name} Results")
# self.worksheet = self.prewrite(self.worksheet, start_row=start_row)
# # self.start_row = self.delineate_start_row(start_row=start_row)
# # self.end_row = self.delineate_end_row(start_row=start_row)
# # for key, value in self.fill_dictionary['result'].items():
# # # logger.debug(f"Filling in {key} with {value}")
# # self.worksheet.cell(value['location']['row'], value['location']['key_column'], value=key.replace("_", " ").title())
# # self.worksheet.cell(value['location']['row'], value['location']['value_column'], value=value['value'])
return workbook return workbook
@@ -56,7 +40,6 @@ class PCRSampleWriter(DefaultTABLEWriter):
columns.append((iii, header)) columns.append((iii, header))
columns = sorted(columns, key=lambda x: x[0]) columns = sorted(columns, key=lambda x: x[0])
columns = proto_columns + columns columns = proto_columns + columns
# logger.debug(columns)
all_results = flatten_list([[item for item in self.rearrange_results(result)] for result in self.pydant_obj]) all_results = flatten_list([[item for item in self.rearrange_results(result)] for result in self.pydant_obj])
if len(all_results) > 0 : if len(all_results) > 0 :
worksheet.cell(row=header_row, column=1, value="Sample") worksheet.cell(row=header_row, column=1, value="Sample")
@@ -83,21 +66,10 @@ class PCRSampleWriter(DefaultTABLEWriter):
def column_headers(self): def column_headers(self):
output = [] output = []
for item in self.pydant_obj: for item in self.pydant_obj:
# logger.debug(item)
dicto: dict = item.result dicto: dict = item.result
for value in dicto.values(): for value in dicto.values():
if not isinstance(value, dict): if not isinstance(value, dict):
# logger.debug(f"Will not include {value} in column headers.")
continue continue
for key in value.keys(): for key in value.keys():
output.append(key) output.append(key)
return sorted(list(set(output))) return sorted(list(set(output)))

View File

@@ -1,8 +1,8 @@
"""
Module for manager defaults.
"""
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Literal
from backend.db.models import ProcedureType
from frontend.widgets.functions import select_open_file from frontend.widgets.functions import select_open_file
from tools import get_application_from_parent from tools import get_application_from_parent
from backend.validators.pydant import PydBaseClass from backend.validators.pydant import PydBaseClass
@@ -13,7 +13,6 @@ logger = logging.getLogger(f"submissions.{__name__}")
class DefaultManager(object): class DefaultManager(object):
def __init__(self, parent, input_object: Path | str | None = None): def __init__(self, parent, input_object: Path | str | None = None):
logger.debug(f"FName before correction: {type(input_object)}")
self.parent = parent self.parent = parent
match input_object: match input_object:
case str(): case str():
@@ -23,16 +22,12 @@ class DefaultManager(object):
self.input_object = input_object self.input_object = input_object
self.pyd = self.to_pydantic() self.pyd = self.to_pydantic()
case x if issubclass(input_object.__class__, PydBaseClass): case x if issubclass(input_object.__class__, PydBaseClass):
# logger.debug("Subclass of PydBaseClass")
self.pyd = input_object self.pyd = input_object
case x if issubclass(input_object.__class__, BaseClass): case x if issubclass(input_object.__class__, BaseClass):
# logger.debug("Subclass of BaseClass")
self.pyd = input_object.to_pydantic() self.pyd = input_object.to_pydantic()
case _: case _:
self.input_object = select_open_file(file_extension="xlsx", obj=get_application_from_parent(parent)) self.input_object = select_open_file(file_extension="xlsx", obj=get_application_from_parent(parent))
self.pyd = self.to_pydantic() self.pyd = self.to_pydantic()
# logger.debug(f"FName after correction: {input_object}")
from .clientsubmissions import DefaultClientSubmissionManager from .clientsubmissions import DefaultClientSubmissionManager

View File

@@ -1,9 +1,10 @@
"""
Module for manager of ClientSubmission object
"""
from __future__ import annotations from __future__ import annotations
import logging import logging, sys
import sys
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from pathlib import Path from pathlib import Path
from openpyxl.reader.excel import load_workbook
from openpyxl.workbook import Workbook from openpyxl.workbook import Workbook
from backend.validators import RSLNamer from backend.validators import RSLNamer
from backend.managers import DefaultManager from backend.managers import DefaultManager
@@ -42,23 +43,15 @@ class DefaultClientSubmissionManager(DefaultManager):
self.sample_parser = ClientSubmissionSampleParser(filepath=self.input_object, self.sample_parser = ClientSubmissionSampleParser(filepath=self.input_object,
submissiontype=self.submissiontype, submissiontype=self.submissiontype,
start_row=self.info_parser.end_row) start_row=self.info_parser.end_row)
logger.debug(self.sample_parser.__dict__)
self.clientsubmission = self.info_parser.to_pydantic() self.clientsubmission = self.info_parser.to_pydantic()
self.clientsubmission.full_batch_size = self.sample_parser.end_row - self.sample_parser.start_row self.clientsubmission.full_batch_size = self.sample_parser.end_row - self.sample_parser.start_row
self.clientsubmission.sample = self.sample_parser.to_pydantic() self.clientsubmission.sample = self.sample_parser.to_pydantic()
return self.clientsubmission return self.clientsubmission
# def to_pydantic(self):
# self.clientsubmission = self.info_parser.to_pydantic()
# self.clientsubmission.sample = self.sample_parser.to_pydantic()
def write(self, workbook: Workbook) -> Workbook: def write(self, workbook: Workbook) -> Workbook:
# workbook: Workbook = load_workbook(BytesIO(self.submissiontype.template_file))
self.info_writer = ClientSubmissionInfoWriter(pydant_obj=self.pyd) self.info_writer = ClientSubmissionInfoWriter(pydant_obj=self.pyd)
assert isinstance(self.info_writer, ClientSubmissionInfoWriter) assert isinstance(self.info_writer, ClientSubmissionInfoWriter)
logger.debug("Attempting write.")
workbook = self.info_writer.write_to_workbook(workbook) workbook = self.info_writer.write_to_workbook(workbook)
self.sample_writer = ClientSubmissionSampleWriter(pydant_obj=self.pyd) self.sample_writer = ClientSubmissionSampleWriter(pydant_obj=self.pyd)
workbook = self.sample_writer.write_to_workbook(workbook, start_row=self.info_writer.worksheet.max_row + 1) workbook = self.sample_writer.write_to_workbook(workbook, start_row=self.info_writer.worksheet.max_row + 1)

View File

@@ -1,12 +1,10 @@
"""
Module for manager of Procedure object.
"""
from __future__ import annotations from __future__ import annotations
import logging import logging, sys
from io import BytesIO
from pprint import pformat
from openpyxl.reader.excel import load_workbook
from openpyxl.workbook import Workbook from openpyxl.workbook import Workbook
from backend.managers import DefaultManager
from backend.managers import DefaultManager, results
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from pathlib import Path from pathlib import Path
from backend.excel.parsers import procedure_parsers from backend.excel.parsers import procedure_parsers
@@ -57,7 +55,6 @@ class DefaultProcedureManager(DefaultManager):
self.equipment = self.equipment_parser.to_pydantic() self.equipment = self.equipment_parser.to_pydantic()
def write(self, workbook: Workbook) -> Workbook: def write(self, workbook: Workbook) -> Workbook:
# workbook = load_workbook(BytesIO(self.proceduretype.template_file))
try: try:
info_writer = getattr(procedure_writers, f"{self.proceduretype.name.replace(' ', '')}InfoWriter") info_writer = getattr(procedure_writers, f"{self.proceduretype.name.replace(' ', '')}InfoWriter")
except AttributeError: except AttributeError:
@@ -69,29 +66,22 @@ class DefaultProcedureManager(DefaultManager):
except AttributeError: except AttributeError:
reagent_writer = procedure_writers.ProcedureReagentWriter reagent_writer = procedure_writers.ProcedureReagentWriter
self.reagent_writer = reagent_writer(pydant_obj=self.pyd) self.reagent_writer = reagent_writer(pydant_obj=self.pyd)
workbook = self.reagent_writer.write_to_workbook(workbook) workbook = self.reagent_writer.write_to_workbook(workbook, start_row=self.info_writer.end_row)
try: try:
equipment_writer = getattr(procedure_writers, f"{self.proceduretype.name.replace(' ', '')}EquipmentWriter") equipment_writer = getattr(procedure_writers, f"{self.proceduretype.name.replace(' ', '')}EquipmentWriter")
except AttributeError: except AttributeError:
equipment_writer = procedure_writers.ProcedureEquipmentWriter equipment_writer = procedure_writers.ProcedureEquipmentWriter
self.equipment_writer = equipment_writer(pydant_obj=self.pyd) self.equipment_writer = equipment_writer(pydant_obj=self.pyd)
workbook = self.equipment_writer.write_to_workbook(workbook) workbook = self.equipment_writer.write_to_workbook(workbook, start_row=self.reagent_writer.end_row)
try: try:
sample_writer = getattr(procedure_writers, f"{self.proceduretype.name.replace(' ', '')}SampleWriter") sample_writer = getattr(procedure_writers, f"{self.proceduretype.name.replace(' ', '')}SampleWriter")
except AttributeError: except AttributeError:
sample_writer = procedure_writers.ProcedureSampleWriter sample_writer = procedure_writers.ProcedureSampleWriter
self.sample_writer = sample_writer(pydant_obj=self.pyd) self.sample_writer = sample_writer(pydant_obj=self.pyd)
workbook = self.sample_writer.write_to_workbook(workbook) workbook = self.sample_writer.write_to_workbook(workbook, start_row=self.equipment_writer.end_row)
# # logger.debug(self.pyd.result)
# # TODO: Find way to group results by result_type. # # TODO: Find way to group results by result_type.
for result in self.pyd.result: for result in self.pyd.result:
logger.debug(f"Writing {result.result_type}")
Writer = getattr(results_writers, f"{result.result_type}InfoWriter") Writer = getattr(results_writers, f"{result.result_type}InfoWriter")
res_info_writer = Writer(pydant_obj=result, proceduretype=self.proceduretype) res_info_writer = Writer(pydant_obj=result, proceduretype=self.proceduretype)
workbook = res_info_writer.write_to_workbook(workbook=workbook) workbook = res_info_writer.write_to_workbook(workbook=workbook)
# # sample_results = [sample.result for sample in self.pyd.sample]
# # logger.debug(pformat(self.pyd.sample_results))
# Writer = getattr(results_writers, "PCRSampleWriter")
# res_sample_writer = Writer(pydant_obj=self.pyd.sample_results, proceduretype=self.proceduretype)
# workbook = res_sample_writer.write_to_workbook(workbook=workbook)
return workbook return workbook

View File

@@ -1,4 +1,6 @@
"""
Module for default results manager
"""
from __future__ import annotations from __future__ import annotations
import logging import logging
from .. import DefaultManager from .. import DefaultManager
@@ -7,7 +9,6 @@ from pathlib import Path
from frontend.widgets.functions import select_open_file from frontend.widgets.functions import select_open_file
from tools import get_application_from_parent from tools import get_application_from_parent
from typing import TYPE_CHECKING, List from typing import TYPE_CHECKING, List
if TYPE_CHECKING: if TYPE_CHECKING:
from backend.validators.pydant import PydResults from backend.validators.pydant import PydResults
@@ -17,13 +18,11 @@ logger = logging.getLogger(f"submission.{__name__}")
class DefaultResultsManager(DefaultManager): class DefaultResultsManager(DefaultManager):
def __init__(self, procedure: Procedure, parent, fname: Path | str | None = None): def __init__(self, procedure: Procedure, parent, fname: Path | str | None = None):
logger.debug(f"FName before correction: {fname}")
self.procedure = procedure self.procedure = procedure
if not fname: if not fname:
self.fname = select_open_file(file_extension="xlsx", obj=get_application_from_parent(parent)) self.fname = select_open_file(file_extension="xlsx", obj=get_application_from_parent(parent))
elif isinstance(fname, str): elif isinstance(fname, str):
self.fname = Path(fname) self.fname = Path(fname)
logger.debug(f"FName after correction: {fname}")
def procedure_to_pydantic(self) -> PydResults: def procedure_to_pydantic(self) -> PydResults:
info = self.info_parser.to_pydantic() info = self.info_parser.to_pydantic()

View File

@@ -1,14 +1,12 @@
""" """
Module for pcr results from Design and Analysis Studio
""" """
from __future__ import annotations from __future__ import annotations
import logging import logging
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Tuple, List, TYPE_CHECKING from typing import TYPE_CHECKING
from openpyxl.reader.excel import load_workbook from openpyxl.reader.excel import load_workbook
from backend.db.models import Procedure from backend.db.models import Procedure
from backend.excel.parsers.results_parsers.pcr_results_parser import PCRSampleParser, PCRInfoParser from backend.excel.parsers.results_parsers.pcr_results_parser import PCRSampleParser, PCRInfoParser
from backend.excel.writers.results_writers.pcr_results_writer import PCRInfoWriter, PCRSampleWriter from backend.excel.writers.results_writers.pcr_results_writer import PCRInfoWriter, PCRSampleWriter
@@ -33,9 +31,3 @@ class PCRManager(DefaultResultsManager):
self.info_writer = PCRInfoWriter(pydant_obj=self.procedure.to_pydantic(), proceduretype=self.procedure.proceduretype) self.info_writer = PCRInfoWriter(pydant_obj=self.procedure.to_pydantic(), proceduretype=self.procedure.proceduretype)
workbook = self.info_writer.write_to_workbook(workbook) workbook = self.info_writer.write_to_workbook(workbook)
self.sample_writer = PCRSampleWriter(pydant_obj=self.procedure.to_pydantic(), proceduretype=self.procedure.proceduretype) self.sample_writer = PCRSampleWriter(pydant_obj=self.procedure.to_pydantic(), proceduretype=self.procedure.proceduretype)

View File

@@ -1,13 +1,10 @@
"""
Module for managing Runs object
"""
from __future__ import annotations from __future__ import annotations
import logging import logging, sys
from pathlib import Path
from pprint import pformat from pprint import pformat
from openpyxl import load_workbook
from openpyxl.workbook.workbook import Workbook from openpyxl.workbook.workbook import Workbook
from tools import copy_xl_sheet
from backend.managers import DefaultManager from backend.managers import DefaultManager
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -16,16 +13,11 @@ class DefaultRunManager(DefaultManager):
def write(self) -> Workbook: def write(self) -> Workbook:
from backend.managers import DefaultClientSubmissionManager, DefaultProcedureManager from backend.managers import DefaultClientSubmissionManager, DefaultProcedureManager
logger.debug(f"Initializing write") logger.info(f"Initializing write")
clientsubmission = DefaultClientSubmissionManager(parent=self.parent, input_object=self.pyd.clientsubmission, submissiontype=self.pyd.clientsubmission.submissiontype) clientsubmission = DefaultClientSubmissionManager(parent=self.parent, input_object=self.pyd.clientsubmission, submissiontype=self.pyd.clientsubmission.submissiontype)
workbook = Workbook() workbook = Workbook()
workbook = clientsubmission.write(workbook=workbook) workbook = clientsubmission.write(workbook=workbook)
for procedure in self.pyd.procedure: for procedure in self.pyd.procedure:
# # logger.debug(f"Running procedure: {pformat(procedure.__dict__)}")
procedure = DefaultProcedureManager(proceduretype=procedure.proceduretype, parent=self.parent, input_object=procedure) procedure = DefaultProcedureManager(proceduretype=procedure.proceduretype, parent=self.parent, input_object=procedure)
workbook: Workbook = procedure.write(workbook=workbook) workbook: Workbook = procedure.write(workbook=workbook)
# for sheetname in wb.sheetnames:
# source_sheet = wb[sheetname]
# ws = workbook.create_sheet(sheetname)
# copy_xl_sheet(source_sheet, ws)
return workbook return workbook

View File

@@ -48,8 +48,6 @@ class ClientSubmissionNamer(DefaultNamer):
if not sub_type: if not sub_type:
logger.warning(f"Getting submissiontype from regex failed, using default submissiontype.") logger.warning(f"Getting submissiontype from regex failed, using default submissiontype.")
sub_type = SubmissionType.query(name="Default") sub_type = SubmissionType.query(name="Default")
logger.debug(f"Submission Type: {sub_type}")
sys.exit()
return sub_type return sub_type
def get_subtype_from_regex(self) -> SubmissionType: def get_subtype_from_regex(self) -> SubmissionType:
@@ -84,9 +82,6 @@ class ClientSubmissionNamer(DefaultNamer):
return sub_type return sub_type
class RSLNamer(object): class RSLNamer(object):
""" """
Object that will enforce proper formatting on RSL plate names. Object that will enforce proper formatting on RSL plate names.
@@ -98,17 +93,10 @@ class RSLNamer(object):
self.submission_type = submission_type self.submission_type = submission_type
if not self.submission_type: if not self.submission_type:
self.submission_type = self.retrieve_submission_type(filename=filename) self.submission_type = self.retrieve_submission_type(filename=filename)
# logger.info(f"got submission type: {self.submission_type}")
if self.submission_type: if self.submission_type:
# self.sub_object = BasicRun.find_polymorphic_subclass(polymorphic_identity=self.submission_type)
self.sub_object = SubmissionType.query(name=self.submission_type['name'], limit=1) self.sub_object = SubmissionType.query(name=self.submission_type['name'], limit=1)
self.parsed_name = self.retrieve_rsl_number(filename=filename, regex=self.sub_object.get_regex( self.parsed_name = self.retrieve_rsl_number(filename=filename, regex=self.sub_object.get_regex(
submission_type=self.submission_type)) submission_type=self.submission_type))
# if not data:
# data = dict(submission_type=self.submission_type)
# if "proceduretype" not in data.keys():
# data['proceduretype'] = self.submission_type
# self.parsed_name = self.sub_object.enforce_name(instr=self.parsed_name, data=data)
logger.info(f"Parsed name: {self.parsed_name}") logger.info(f"Parsed name: {self.parsed_name}")
@classmethod @classmethod
@@ -227,7 +215,6 @@ class RSLNamer(object):
Returns: Returns:
str: Output filename str: Output filename
""" """
logger.debug(data)
if "submitted_date" in data.keys(): if "submitted_date" in data.keys():
if isinstance(data['submitted_date'], dict): if isinstance(data['submitted_date'], dict):
if data['submitted_date']['value'] is not None: if data['submitted_date']['value'] is not None:
@@ -244,13 +231,8 @@ class RSLNamer(object):
today = datetime.now() today = datetime.now()
if isinstance(today, str): if isinstance(today, str):
today = datetime.strptime(today, "%Y-%m-%d") today = datetime.strptime(today, "%Y-%m-%d")
# if "name" in data.keys():
# logger.debug(f"Found name: {data['name']}")
# plate_number = data['name'].split("-")[-1][0]
# else:
previous = Run.query(start_date=today, end_date=today, submissiontype=data['submissiontype']) previous = Run.query(start_date=today, end_date=today, submissiontype=data['submissiontype'])
plate_number = len(previous) + 1 plate_number = len(previous) + 1
logger.debug(f"Using plate number: {plate_number}")
return f"RSL-{data['abbreviation']}-{today.year}{str(today.month).zfill(2)}{str(today.day).zfill(2)}-{plate_number}" return f"RSL-{data['abbreviation']}-{today.year}{str(today.month).zfill(2)}{str(today.day).zfill(2)}-{plate_number}"
@classmethod @classmethod
@@ -283,5 +265,7 @@ class RSLNamer(object):
return "" return ""
from .pydant import PydRun, PydKitType, PydContact, PydOrganization, 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
)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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,10 +1,9 @@
''' """
Contains all operations for creating charts, graphs and visual effects. Contains all operations for creating charts, graphs and visual effects.
''' """
from datetime import timedelta, date from datetime import timedelta, date
from pathlib import Path from pathlib import Path
from typing import Generator from typing import Generator
import plotly import plotly
from PyQt6.QtWidgets import QWidget from PyQt6.QtWidgets import QWidget
import pandas as pd, logging import pandas as pd, logging
@@ -128,13 +127,10 @@ class CustomFigure(Figure):
html = f'<html><body>' html = f'<html><body>'
if self is not None: if self is not None:
# NOTE: Just cannot get this load from string to freaking work. # NOTE: Just cannot get this load from string to freaking work.
# html += self.to_html(include_plotlyjs='cdn', full_html=False)
html += plotly.offline.plot(self, output_type='div', include_plotlyjs="cdn") html += plotly.offline.plot(self, output_type='div', include_plotlyjs="cdn")
else: else:
html += "<h1>No data was retrieved for the given parameters.</h1>" html += "<h1>No data was retrieved for the given parameters.</h1>"
html += '</body></html>' html += '</body></html>'
# with open("test.html", "w", encoding="utf-8") as f:
# f.write(html)
return html return html

View File

@@ -3,10 +3,9 @@ Construct BC control concentration charts
""" """
from pprint import pformat from pprint import pformat
from . import CustomFigure from . import CustomFigure
import plotly.express as px import logging, sys, plotly.express as px
import pandas as pd import pandas as pd
from PyQt6.QtWidgets import QWidget from PyQt6.QtWidgets import QWidget
import logging
from operator import itemgetter from operator import itemgetter
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -31,13 +30,12 @@ class ConcentrationsChart(CustomFigure):
self.df = self.df.sort_values(['submitted_date', 'procedure'], ascending=[True, True]).reset_index( self.df = self.df.sort_values(['submitted_date', 'procedure'], ascending=[True, True]).reset_index(
drop=True) drop=True)
self.df = self.df.reset_index().rename(columns={"index": "idx"}) self.df = self.df.reset_index().rename(columns={"index": "idx"})
# logger.debug(f"DF after changes:\n{self.df}")
scatter = px.scatter(data_frame=self.df, x='procedure', y="concentration", scatter = px.scatter(data_frame=self.df, x='procedure', y="concentration",
hover_data=["name", "procedure", "submitted_date", "concentration"], hover_data=["name", "procedure", "submitted_date", "concentration"],
color="positive", color_discrete_map={"positive": "red", "negative": "green", "sample":"orange"} color="positive", color_discrete_map={"positive": "red", "negative": "green", "sample":"orange"}
) )
except (ValueError, AttributeError) as e: except (ValueError, AttributeError) as e:
logger.error(f"Error constructing chart: {e}") # logger.error(f"Error constructing chart: {e}")
scatter = px.scatter() scatter = px.scatter()
# NOTE: For some reason if data is allowed to sort itself it leads to wrong ordering of x axis. # NOTE: For some reason if data is allowed to sort itself it leads to wrong ordering of x axis.
traces = sorted(scatter.data, key=itemgetter("name")) traces = sorted(scatter.data, key=itemgetter("name"))

View File

@@ -3,11 +3,9 @@ Functions for constructing irida control graphs using plotly.
""" """
from datetime import date from datetime import date
from pprint import pformat from pprint import pformat
import plotly.express as px import logging, plotly.express as px, pandas as pd
import pandas as pd
from PyQt6.QtWidgets import QWidget from PyQt6.QtWidgets import QWidget
from . import CustomFigure from . import CustomFigure
import logging
from tools import get_unique_values_in_df_column from tools import get_unique_values_in_df_column
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")

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 Reagent # 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
@@ -22,9 +22,8 @@ from .date_type_picker import DateTypePicker
from .functions import select_save_file from .functions import select_save_file
from .pop_ups import HTMLPop from .pop_ups import HTMLPop
from .misc import Pagifier from .misc import Pagifier
from .submission_table import SubmissionsSheet, 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
@@ -132,7 +131,7 @@ class App(QMainWindow):
self.table_widget.pager.current_page.textChanged.connect(self.update_data) self.table_widget.pager.current_page.textChanged.connect(self.update_data)
self.editReagentAction.triggered.connect(self.edit_reagent) self.editReagentAction.triggered.connect(self.edit_reagent)
self.manageOrgsAction.triggered.connect(self.manage_orgs) self.manageOrgsAction.triggered.connect(self.manage_orgs)
self.manageKitsAction.triggered.connect(self.manage_kits) # self.manageKitsAction.triggered.connect(self.manage_kits)
def showAbout(self): def showAbout(self):
""" """
@@ -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=Reagent, 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):
@@ -195,24 +195,23 @@ class App(QMainWindow):
new_org = dlg.parse_form() new_org = dlg.parse_form()
new_org.save() new_org.save()
def manage_kits(self, *args, **kwargs): # def manage_kits(self, *args, **kwargs):
from frontend.widgets.omni_manager_pydant import ManagerWindow as ManagerWindowPyd # from frontend.widgets.omni_manager_pydant import ManagerWindow as ManagerWindowPyd
dlg = ManagerWindowPyd(parent=self, object_type=KitType, extras=[], add_edit='edit', managers=set()) # dlg = ManagerWindowPyd(parent=self, object_type=KitType, extras=[], add_edit='edit', managers=set())
if dlg.exec(): # if dlg.exec():
# logger.debug("\n\nBeginning parsing\n\n") # output = dlg.parse_form()
output = dlg.parse_form() # sql = output.to_sql()
# logger.debug(f"Kit output: {pformat(output.__dict__)}") # assert isinstance(sql, KitType)
# logger.debug("\n\nBeginning transformation\n\n") # sql.save()
sql = output.to_sql()
assert isinstance(sql, KitType)
sql.save()
@under_development @under_development
def submissions_to_excel(self, *args, **kwargs): def submissions_to_excel(self, *args, **kwargs):
from backend.db.models import Run
dlg = DateTypePicker(self) dlg = DateTypePicker(self)
if dlg.exec(): if dlg.exec():
output = dlg.parse_form() output = dlg.parse_form()
df = BasicRun.archive_submissions(**output) # TODO: Move to ClientSubmissions
df = Run.archive_submissions(**output)
filepath = select_save_file(self, f"Submissions {output['start_date']}-{output['end_date']}", "xlsx") filepath = select_save_file(self, f"Submissions {output['start_date']}-{output['end_date']}", "xlsx")
writer = ExcelWriter(filepath, "openpyxl") writer = ExcelWriter(filepath, "openpyxl")
df.to_excel(writer) df.to_excel(writer)
@@ -254,7 +253,6 @@ class AddSubForm(QWidget):
self.sheetwidget = QWidget(self) self.sheetwidget = QWidget(self)
self.sheetlayout = QVBoxLayout(self) self.sheetlayout = QVBoxLayout(self)
self.sheetwidget.setLayout(self.sheetlayout) self.sheetwidget.setLayout(self.sheetlayout)
# self.sub_wid = SubmissionsSheet(parent=parent)
self.sub_wid = SubmissionsTree(parent=parent, model=ClientSubmissionRunModel(self)) self.sub_wid = SubmissionsTree(parent=parent, model=ClientSubmissionRunModel(self))
self.pager = Pagifier(page_max=self.sub_wid.total_count / page_size) self.pager = Pagifier(page_max=self.sub_wid.total_count / page_size)
self.sheetlayout.addWidget(self.sub_wid) self.sheetlayout.addWidget(self.sub_wid)
@@ -265,12 +263,10 @@ class AddSubForm(QWidget):
self.tab1.layout.addWidget(self.interior) self.tab1.layout.addWidget(self.interior)
self.tab1.layout.addWidget(self.sheetwidget) self.tab1.layout.addWidget(self.sheetwidget)
self.tab2.layout = QVBoxLayout(self) self.tab2.layout = QVBoxLayout(self)
# self.irida_viewer = ControlsViewer(self, archetype="Irida Control")
self.irida_viewer = None self.irida_viewer = None
self.tab2.layout.addWidget(self.irida_viewer) self.tab2.layout.addWidget(self.irida_viewer)
self.tab2.setLayout(self.tab2.layout) self.tab2.setLayout(self.tab2.layout)
self.tab3.layout = QVBoxLayout(self) self.tab3.layout = QVBoxLayout(self)
# self.pcr_viewer = ControlsViewer(self, archetype="PCR Control")
self.pcr_viewer = None self.pcr_viewer = None
self.tab3.layout.addWidget(self.pcr_viewer) self.tab3.layout.addWidget(self.pcr_viewer)
self.tab3.setLayout(self.tab3.layout) self.tab3.setLayout(self.tab3.layout)

View File

@@ -43,10 +43,8 @@ class Concentrations(InfoPane):
None None
""" """
include = self.pos_neg.get_checked() include = self.pos_neg.get_checked()
# logger.debug(f"Include: {include}")
super().update_data() super().update_data()
months = self.diff_month(self.start_date, self.end_date) months = self.diff_month(self.start_date, self.end_date)
# logger.debug(f"Box checked: {self.all_box.isChecked()}")
chart_settings = dict(start_date=self.start_date, end_date=self.end_date, chart_settings = dict(start_date=self.start_date, end_date=self.end_date,
include=include) include=include)
self.report_obj = ConcentrationMaker(**chart_settings) self.report_obj = ConcentrationMaker(**chart_settings)

View File

@@ -108,7 +108,6 @@ class ControlsViewer(InfoPane):
parent=self, parent=self,
months=months months=months
) )
# logger.debug(f"Chart settings: {chart_settings}")
self.fig = self.archetype.instance_class.make_chart(chart_settings=chart_settings, parent=self, ctx=self.app.ctx) self.fig = self.archetype.instance_class.make_chart(chart_settings=chart_settings, parent=self, ctx=self.app.ctx)
self.report_obj = ChartReportMaker(df=self.fig.df, sheet_name=self.archetype.name) self.report_obj = ChartReportMaker(df=self.fig.df, sheet_name=self.archetype.name)
if issubclass(self.fig.__class__, CustomFigure): if issubclass(self.fig.__class__, CustomFigure):

View File

@@ -1,13 +1,19 @@
"""
"""
from PyQt6.QtWidgets import ( 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
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)
@@ -27,10 +33,7 @@ class DateTypePicker(QDialog):
self.setLayout(self.layout) self.setLayout(self.layout)
def parse_form(self): def parse_form(self):
# sub_types = [self.typepicker.itemText(i) for i in range(self.typepicker.count()) if self.typepicker.itemChecked(i)]
sub_types = self.typepicker.get_checked() sub_types = self.typepicker.get_checked()
start_date = self.datepicker.start_date.date().toPyDate() start_date = self.datepicker.start_date.date().toPyDate()
end_date = self.datepicker.end_date.date().toPyDate() end_date = self.datepicker.end_date.date().toPyDate()
return dict(submissiontype=sub_types, start_date=start_date, end_date=end_date) return dict(submissiontype=sub_types, start_date=start_date, end_date=end_date)

View File

@@ -1,91 +1,97 @@
''' """
Creates forms that the user can enter equipment info into. Creates forms that the user can enter equipment info into.
''' """
import sys, logging
from pprint import pformat from pprint import pformat
from PyQt6.QtCore import Qt, QSignalBlocker
from PyQt6.QtWidgets import (
QDialog, QComboBox, QCheckBox, QLabel, QWidget, QVBoxLayout, QDialogButtonBox, QGridLayout
)
from backend.db.models import Equipment, Run, Process, Procedure
from backend.validators.pydant import PydEquipment, PydEquipmentRole, PydTips
import logging
from typing import Generator from typing import Generator
from PyQt6.QtCore import Qt, pyqtSlot, QSignalBlocker
from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import (
QDialog, QVBoxLayout, QDialogButtonBox, QGridLayout, QWidget, QCheckBox, QComboBox, QLabel
)
from backend import Process
from backend.db.models import Equipment
from backend.validators.pydant import PydProcedure, PydEquipmentRole, PydTips, PydEquipment
from tools import get_application_from_parent, render_details_template
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
class EquipmentUsage(QDialog): class EquipmentUsage(QDialog):
def __init__(self, parent, procedure: Procedure): def __init__(self, parent, procedure: PydProcedure):
super().__init__(parent) super().__init__(parent)
self.procedure = procedure self.procedure = procedure
self.setWindowTitle(f"Equipment Checklist - {procedure.name}") self.setWindowTitle(f"Equipment Checklist - {procedure.name}")
self.used_equipment = self.procedure.equipment self.used_equipment = self.procedure.equipment
# self.kit = self.procedure.kittype self.kit = self.procedure.kittype
self.opt_equipment = procedure.proceduretype.get_equipment() self.opt_equipment = procedure.proceduretype.get_equipment()
self.layout = QVBoxLayout() self.layout = QVBoxLayout()
self.app = get_application_from_parent(parent)
self.webview = QWebEngineView(parent=self)
self.webview.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
self.webview.setMinimumSize(1200, 800)
self.webview.setMaximumWidth(1200)
# NOTE: Decide if exporting should be allowed.
self.layout = QGridLayout()
# NOTE: button to export a pdf version
self.layout.addWidget(self.webview, 1, 0, 10, 10)
self.setLayout(self.layout) self.setLayout(self.layout)
self.populate_form() self.setFixedWidth(self.webview.width() + 20)
# NOTE: setup channel
def populate_form(self): self.channel = QWebChannel()
""" self.channel.registerObject('backend', self)
Create form widgets html = self.construct_html(procedure=procedure)
""" self.webview.setHtml(html)
self.webview.page().setWebChannel(self.channel)
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn) self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept) self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject) self.buttonBox.rejected.connect(self.reject)
label = self.LabelRow(parent=self) self.layout.addWidget(self.buttonBox, 11, 1, 1, 1)
self.layout.addWidget(label)
for equipment in self.opt_equipment:
widg = equipment.to_form(parent=self, used=self.used_equipment)
self.layout.addWidget(widg)
widg.update_processes()
self.layout.addWidget(self.buttonBox)
def parse_form(self) -> Generator[PydEquipment, None, None]: @classmethod
""" def construct_html(cls, procedure: PydProcedure, child: bool = False):
Pull info from all RoleComboBox widgets proceduretype = procedure.proceduretype
proceduretype_dict = proceduretype.details_dict()
run = procedure.run
html = render_details_template(
template_name="support/equipment_usage",
css_in=[],
js_in=[],
proceduretype=proceduretype_dict,
run=run.details_dict(),
procedure=procedure.__dict__,
child=child
)
return html
Returns: @pyqtSlot(str, str, str, str)
Generator[PydEquipment, None, None]: All equipment pulled from widgets def update_equipment(self, equipmentrole: str, equipment: str, process: str, tips: str):
""" try:
for widget in self.findChildren(QWidget): equipment_of_interest = next(
match widget: (item for item in self.procedure.equipment if item.equipmentrole == equipmentrole))
case RoleComboBox(): except StopIteration:
if widget.check.isChecked(): equipment_of_interest = None
item = widget.parse_form() equipment = Equipment.query(name=equipment)
if item: if equipment_of_interest:
yield item eoi = self.procedure.equipment.pop(self.procedure.equipment.index(equipment_of_interest))
else: else:
continue eoi = equipment.to_pydantic(proceduretype=self.procedure.proceduretype)
else: eoi.name = equipment.name
continue eoi.asset_number = equipment.asset_number
case _: eoi.nickname = equipment.nickname
continue process = next((prcss for prcss in equipment.process if prcss.name == process))
eoi.process = process.to_pydantic()
tips = next((tps for tps in equipment.tips if tps.name == tips))
eoi.tips = tips.to_pydantic()
self.procedure.equipment.append(eoi)
logger.debug(f"Updated equipment: {self.procedure.equipment}")
class LabelRow(QWidget): def save_procedure(self):
"""Provides column headers""" sql, _ = self.procedure.to_sql()
sql.save()
def __init__(self, parent) -> None:
super().__init__(parent)
self.layout = QGridLayout()
self.check = QCheckBox()
self.layout.addWidget(self.check, 0, 0)
self.check.stateChanged.connect(self.check_all)
for iii, item in enumerate(["Role", "Equipment", "Process", "Tips"], start=1):
label = QLabel(item)
label.setMaximumWidth(200)
label.setMinimumWidth(200)
self.layout.addWidget(label, 0, iii, alignment=Qt.AlignmentFlag.AlignRight)
self.setLayout(self.layout)
def check_all(self):
"""
Toggles all checkboxes in the form
"""
for object in self.parent().findChildren(QCheckBox):
object.setChecked(self.check.isChecked())
class RoleComboBox(QWidget): class RoleComboBox(QWidget):
@@ -124,7 +130,6 @@ class RoleComboBox(QWidget):
""" """
equip = self.box.currentText() equip = self.box.currentText()
equip2 = next((item for item in self.role.equipment if item.name == equip), self.role.equipment[0]) equip2 = next((item for item in self.role.equipment if item.name == equip), self.role.equipment[0])
logger.debug(f"Equip2: {equip2}")
with QSignalBlocker(self.process) as blocker: with QSignalBlocker(self.process) as blocker:
self.process.clear() self.process.clear()
self.process.addItems([item for item in equip2.process if item in self.role.process]) self.process.addItems([item for item in equip2.process if item in self.role.process])

View File

@@ -1,131 +0,0 @@
'''
Creates forms that the user can enter equipment info into.
'''
import sys
from pprint import pformat
from PyQt6.QtCore import Qt, QSignalBlocker, pyqtSlot
from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import (
QDialog, QComboBox, QCheckBox, QLabel, QWidget, QVBoxLayout, QDialogButtonBox, QGridLayout
)
from backend.db.models import Equipment, Run, Process, Procedure, Tips
from backend.validators.pydant import PydEquipment, PydEquipmentRole, PydTips, PydProcedure
import logging
from typing import Generator
from tools import get_application_from_parent, render_details_template, flatten_list
logger = logging.getLogger(f"submissions.{__name__}")
class EquipmentUsage(QDialog):
def __init__(self, parent, procedure: PydProcedure):
super().__init__(parent)
self.procedure = procedure
self.setWindowTitle(f"Equipment Checklist - {procedure.name}")
self.used_equipment = self.procedure.equipment
self.kit = self.procedure.kittype
self.opt_equipment = procedure.proceduretype.get_equipment()
self.layout = QVBoxLayout()
self.app = get_application_from_parent(parent)
self.webview = QWebEngineView(parent=self)
self.webview.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
self.webview.setMinimumSize(1200, 800)
self.webview.setMaximumWidth(1200)
# NOTE: Decide if exporting should be allowed.
# self.webview.loadFinished.connect(self.activate_export)
self.layout = QGridLayout()
# NOTE: button to export a pdf version
self.layout.addWidget(self.webview, 1, 0, 10, 10)
self.setLayout(self.layout)
self.setFixedWidth(self.webview.width() + 20)
# NOTE: setup channel
self.channel = QWebChannel()
self.channel.registerObject('backend', self)
html = self.construct_html(procedure=procedure)
self.webview.setHtml(html)
self.webview.page().setWebChannel(self.channel)
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.layout.addWidget(self.buttonBox, 11, 1, 1, 1)
@classmethod
def construct_html(cls, procedure: PydProcedure, child: bool = False):
proceduretype = procedure.proceduretype
proceduretype_dict = proceduretype.details_dict()
run = procedure.run
# proceduretype_dict['equipment_json'] = flatten_list([item['equipment_json'] for item in proceduretype_dict['equipment']])
# proceduretype_dict['equipment_json'] = [
# {'name': 'Liquid Handler', 'equipment': [
# {'name': 'Other', 'asset_number': 'XXX', 'processes': [
# {'name': 'Trust Me', 'tips': ['Blah']},
# {'name': 'No Me', 'tips': ['Blah', 'Crane']}
# ]
# },
# {'name': 'Biomek', 'asset_number': '5015530', 'processes': [
# {'name': 'Sample Addition', 'tips': ['Axygen 20uL']
# }
# ]
# }
# ]
# }
# ]
# if procedure.equipment:
# for equipmentrole in proceduretype_dict['equipment']:
# # NOTE: Check if procedure equipment is present and move to head of the list if so.
# try:
# relevant_procedure_item = next((equipment for equipment in procedure.equipment if
# equipment.equipmentrole == equipmentrole['name']))
# except StopIteration:
# continue
# item_in_er_list = next((equipment for equipment in equipmentrole['equipment'] if
# equipment['name'] == relevant_procedure_item.name))
# equipmentrole['equipment'].insert(0, equipmentrole['equipment'].pop(
# equipmentrole['equipment'].index(item_in_er_list)))
html = render_details_template(
template_name="support/equipment_usage",
css_in=[],
js_in=[],
proceduretype=proceduretype_dict,
run=run.details_dict(),
procedure=procedure.__dict__,
child=child
)
return html
@pyqtSlot(str, str, str, str)
def update_equipment(self, equipmentrole: str, equipment: str, process: str, tips: str):
try:
equipment_of_interest = next(
(item for item in self.procedure.equipment if item.equipmentrole == equipmentrole))
except StopIteration:
equipment_of_interest = None
equipment = Equipment.query(name=equipment)
if equipment_of_interest:
eoi = self.procedure.equipment.pop(self.procedure.equipment.index(equipment_of_interest))
else:
eoi = equipment.to_pydantic(proceduretype=self.procedure.proceduretype)
eoi.name = equipment.name
eoi.asset_number = equipment.asset_number
eoi.nickname = equipment.nickname
process = next((prcss for prcss in equipment.process if prcss.name == process))
eoi.process = process.to_pydantic()
tips = next((tps for tps in equipment.tips if tps.name == tips))
eoi.tips = tips.to_pydantic()
self.procedure.equipment.append(eoi)
logger.debug(f"Updated equipment: {self.procedure.equipment}")
def save_procedure(self):
sql, _ = self.procedure.to_sql()
logger.debug(pformat(sql.__dict__))
# import pickle
# with open("sql.pickle", "wb") as f:
# pickle.dump(sql, f)
# with open("pyd.pickle", "wb") as f:
# pickle.dump(self.procedure, f)
sql.save()

View File

@@ -39,7 +39,7 @@ def select_open_file(obj: QMainWindow, file_extension: str | None = None) -> Pat
logger.warning(f"No file selected, cancelling.") logger.warning(f"No file selected, cancelling.")
return return
obj.last_dir = fname.parent obj.last_dir = fname.parent
logger.debug(f"File selected: {fname}") logger.info(f"File selected: {fname}")
return fname return fname

View File

@@ -5,10 +5,9 @@ from operator import itemgetter
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QWidget, QDialog, QGridLayout, QLabel, QLineEdit, QDialogButtonBox, QTextEdit, QComboBox QWidget, QDialog, QGridLayout, QLabel, QLineEdit, QDialogButtonBox, QTextEdit, QComboBox
) )
import pyqtgraph as pg
from PyQt6.QtGui import QIcon from PyQt6.QtGui import QIcon
from PIL import Image from PIL import Image
import logging, numpy as np import logging, numpy as np, pyqtgraph as pg
from pprint import pformat from pprint import pformat
from typing import Tuple, List from typing import Tuple, List
from pathlib import Path from pathlib import Path

View File

@@ -34,7 +34,6 @@ class InfoPane(QWidget):
report = Report() report = Report()
self.start_date = self.datepicker.start_date.date().toPyDate() self.start_date = self.datepicker.start_date.date().toPyDate()
self.end_date = self.datepicker.end_date.date().toPyDate() self.end_date = self.datepicker.end_date.date().toPyDate()
# logger.debug(f"Start date: {self.start_date}, End date: {self.end_date}")
if self.datepicker.start_date.date() > self.datepicker.end_date.date(): if self.datepicker.start_date.date() > self.datepicker.end_date.date():
lastmonth = self.datepicker.end_date.date().addDays(-31) lastmonth = self.datepicker.end_date.date().addDays(-31)
msg = f"Start date after end date is not allowed! Setting to {lastmonth.toString()}." msg = f"Start date after end date is not allowed! Setting to {lastmonth.toString()}."

View File

@@ -1,7 +1,7 @@
""" """
Contains miscellaneous widgets for frontend functions Contains miscellaneous widgets for frontend functions
""" """
import math import math, logging
from PyQt6.QtGui import QStandardItem, QIcon from PyQt6.QtGui import QStandardItem, QIcon
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QLabel, QLineEdit, QComboBox, QDateEdit, QPushButton, QWidget, QLabel, QLineEdit, QComboBox, QDateEdit, QPushButton, QWidget,
@@ -10,7 +10,6 @@ from PyQt6.QtWidgets import (
from PyQt6.QtCore import Qt, QDate, QSize from PyQt6.QtCore import Qt, QDate, QSize
from tools import jinja_template_loading from tools import jinja_template_loading
from backend.db.models import * from backend.db.models import *
import logging
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")

View File

@@ -3,7 +3,7 @@ A widget to handle adding/updating any database object.
""" """
from datetime import date from datetime import date
from pprint import pformat from pprint import pformat
from typing import Any, Tuple from typing import Any, Tuple, List
from pydantic import BaseModel from pydantic import BaseModel
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QLabel, QDialog, QWidget, QLineEdit, QGridLayout, QComboBox, QDialogButtonBox, QDateEdit, QSpinBox, QDoubleSpinBox, QLabel, QDialog, QWidget, QLineEdit, QGridLayout, QComboBox, QDialogButtonBox, QDateEdit, QSpinBox, QDoubleSpinBox,
@@ -13,6 +13,8 @@ from sqlalchemy import String, TIMESTAMP, INTEGER, FLOAT, JSON, BLOB
from sqlalchemy.orm import ColumnProperty from sqlalchemy.orm import ColumnProperty
import logging import logging
from sqlalchemy.orm.relationships import _RelationshipDeclared from sqlalchemy.orm.relationships import _RelationshipDeclared
from backend.db.models import BaseClass
from backend.validators.pydant import PydBaseClass
from tools import Report, report_result from tools import Report, report_result
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -20,19 +22,17 @@ logger = logging.getLogger(f"submissions.{__name__}")
class AddEdit(QDialog): class AddEdit(QDialog):
def __init__(self, parent, instance: Any | None = None, managers: set = set()): def __init__(self, parent, instance: Any | None = None, managers: set = set(), disabled: List[str] = []):
super().__init__(parent) super().__init__(parent)
# logger.debug(f"Managers: {managers}") logger.debug(f"Disable = {disabled}")
self.instance = instance self.instance = instance
self.object_type = instance.__class__ self.object_type = instance.__class__
self.managers = managers self.managers = managers
# logger.debug(f"Managers: {managers}")
self.layout = QGridLayout(self) self.layout = QGridLayout(self)
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn) self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept) self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject) self.buttonBox.rejected.connect(self.reject)
# logger.debug(f"Fields: {pformat(self.instance.omnigui_instance_dict)}")
fields = {k: v for k, v in self.instance.omnigui_instance_dict.items() if "id" not in k} fields = {k: v for k, v in self.instance.omnigui_instance_dict.items() if "id" not in k}
# NOTE: Move 'name' to the front # NOTE: Move 'name' to the front
try: try:
@@ -41,13 +41,13 @@ class AddEdit(QDialog):
pass pass
height_counter = 0 height_counter = 0
for key, field in fields.items(): for key, field in fields.items():
disable = key in disabled
try: try:
value = getattr(self.instance, key) value = getattr(self.instance, key)
except AttributeError: except AttributeError:
value = None value = None
try: try:
logger.debug(f"{key} property: {type(field['class_attr'].property)}") widget = EditProperty(self, key=key, column_type=field, value=value, disable=disable)
widget = EditProperty(self, key=key, column_type=field, value=value)
except AttributeError as e: except AttributeError as e:
logger.error(f"Problem setting widget {key}: {e}") logger.error(f"Problem setting widget {key}: {e}")
continue continue
@@ -55,7 +55,7 @@ class AddEdit(QDialog):
self.layout.addWidget(widget, self.layout.rowCount(), 0) self.layout.addWidget(widget, self.layout.rowCount(), 0)
height_counter += 1 height_counter += 1
self.layout.addWidget(self.buttonBox) self.layout.addWidget(self.buttonBox)
self.setWindowTitle(f"Add/Edit {self.object_type.__name__} - Manager: {self.managers}") self.setWindowTitle(f"Add/Edit {self.object_type.__name__}")# - Manager: {self.managers}")
self.setMinimumSize(600, 50 * height_counter) self.setMinimumSize(600, 50 * height_counter)
self.setLayout(self.layout) self.setLayout(self.layout)
@@ -64,11 +64,8 @@ class AddEdit(QDialog):
report = Report() report = Report()
parsed = {result[0].strip(":"): result[1] for result in parsed = {result[0].strip(":"): result[1] for result in
[item.parse_form() for item in self.findChildren(EditProperty)] if result[0]} [item.parse_form() for item in self.findChildren(EditProperty)] if result[0]}
# logger.debug(f"Parsed form: {parsed}")
model = self.object_type.pydantic_model model = self.object_type.pydantic_model
# logger.debug(f"Model type: {model.__name__}")
if model.__name__ == "PydElastic": if model.__name__ == "PydElastic":
# logger.debug(f"We have an elastic model.")
parsed['instance'] = self.instance parsed['instance'] = self.instance
# NOTE: Hand-off to pydantic model for validation. # NOTE: Hand-off to pydantic model for validation.
# NOTE: Also, why am I not just using the toSQL method here. I could write one for contact. # NOTE: Also, why am I not just using the toSQL method here. I could write one for contact.
@@ -78,8 +75,9 @@ class AddEdit(QDialog):
class EditProperty(QWidget): class EditProperty(QWidget):
def __init__(self, parent: AddEdit, key: str, column_type: Any, value): def __init__(self, parent: AddEdit, key: str, column_type: Any, value, disable: bool):
super().__init__(parent) super().__init__(parent)
logger.debug(f"Widget column type for {key}: {column_type}")
self.name = key self.name = key
self.label = QLabel(key.title().replace("_", " ")) self.label = QLabel(key.title().replace("_", " "))
self.layout = QGridLayout() self.layout = QGridLayout()
@@ -88,6 +86,7 @@ class EditProperty(QWidget):
self.property_class = column_type['class_attr'].property.entity.class_ self.property_class = column_type['class_attr'].property.entity.class_
except AttributeError: except AttributeError:
self.property_class = None self.property_class = None
logger.debug(f"Property class: {self.property_class}")
try: try:
self.is_list = column_type['class_attr'].property.uselist self.is_list = column_type['class_attr'].property.uselist
except AttributeError: except AttributeError:
@@ -96,23 +95,26 @@ class EditProperty(QWidget):
case ColumnProperty(): case ColumnProperty():
self.column_property_set(column_type, value=value) self.column_property_set(column_type, value=value)
case _RelationshipDeclared(): case _RelationshipDeclared():
if not self.property_class.skip_on_edit: try:
check = self.property_class.skip_on_edit
except AttributeError:
check = False
if not check:
self.relationship_property_set(column_type, value=value) self.relationship_property_set(column_type, value=value)
else: else:
return return
case _: case _:
logger.error(f"{column_type} not a supported type.") logger.error(f"{column_type} not a supported type.")
return return
self.widget.setDisabled(disable)
self.layout.addWidget(self.label, 0, 0, 1, 1) self.layout.addWidget(self.label, 0, 0, 1, 1)
self.layout.addWidget(self.widget, 0, 1, 1, 3) self.layout.addWidget(self.widget, 0, 1, 1, 3)
self.setLayout(self.layout) self.setLayout(self.layout)
def relationship_property_set(self, relationship, value=None): def relationship_property_set(self, relationship, value=None):
self.widget = QComboBox() self.widget = QComboBox()
# logger.debug(self.parent().managers)
for manager in self.parent().managers: for manager in self.parent().managers:
if self.name in manager.aliases: if self.name in manager.aliases:
# logger.debug(f"Name: {self.name} is in aliases: {manager.aliases}")
choices = [manager.name] choices = [manager.name]
self.widget.setEnabled(False) self.widget.setEnabled(False)
break break
@@ -127,11 +129,17 @@ class EditProperty(QWidget):
if isinstance(instance_value, list): if isinstance(instance_value, list):
instance_value = next((item.name for item in instance_value), None) instance_value = next((item.name for item in instance_value), None)
if instance_value: if instance_value:
match instance_value:
case x if issubclass(instance_value.__class__, BaseClass):
instance_value = instance_value.name
case x if issubclass(instance_value.__class__, PydBaseClass):
instance_value = instance_value.name
case _:
pass
choices.insert(0, choices.pop(choices.index(instance_value))) choices.insert(0, choices.pop(choices.index(instance_value)))
self.widget.addItems(choices) self.widget.addItems(choices)
def column_property_set(self, column_property, value=None): def column_property_set(self, column_property, value=None):
# logger.debug(f"Column Property: {column_property['class_attr'].expression} {column_property}, Value: {value}")
match column_property['class_attr'].expression.type: match column_property['class_attr'].expression.type:
case String(): case String():
if value is None: if value is None:
@@ -176,7 +184,6 @@ class EditProperty(QWidget):
check = self.widget check = self.widget
except AttributeError: except AttributeError:
return None, None return None, None
# match self.widget
match check: match check:
case QLineEdit(): case QLineEdit():
value = self.widget.text() value = self.widget.text()

File diff suppressed because it is too large Load Diff

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__}")
@@ -72,18 +71,14 @@ class SearchBox(QDialog):
self.object_type = self.original_type self.object_type = self.original_type
else: else:
self.object_type = self.original_type.find_regular_subclass(self.sub_class.currentText()) self.object_type = self.original_type.find_regular_subclass(self.sub_class.currentText())
# logger.debug(f"Object type: {self.object_type} - {self.object_type.searchables}") for item in self.object_type.get_searchables():
# logger.debug(f"Original type: {self.original_type} - {self.original_type.searchables}") if item in [thing for thing in search_fields]:
for item in self.object_type.searchables:
if item['field'] in [item['field'] for item in search_fields]:
logger.debug(f"Already have {item['field']}")
continue continue
else: else:
search_fields.append(item) search_fields.append(item)
logger.debug(f"Search fields: {search_fields}")
for iii, searchable in enumerate(search_fields): for iii, searchable in enumerate(search_fields):
widget = FieldSearch(parent=self, label=searchable['label'], field_name=searchable['field']) widget = FieldSearch(parent=self, label=searchable, field_name=searchable)
widget.setObjectName(searchable['field']) widget.setObjectName(searchable)
self.layout.addWidget(widget, 1 + iii, 0) self.layout.addWidget(widget, 1 + iii, 0)
widget.search_widget.textChanged.connect(self.update_data) widget.search_widget.textChanged.connect(self.update_data)
self.update_data() self.update_data()
@@ -168,11 +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
# logger.debug(f"Extras: {self.extras}")
def setData(self, df: DataFrame) -> None: def setData(self, df: DataFrame) -> None:
""" """
@@ -181,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)
@@ -209,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

@@ -1,18 +1,15 @@
""" """
Main module to construct the procedure form
""" """
from __future__ import annotations from __future__ import annotations
import sys, logging, os, re, datetime import sys, logging, re, datetime
from pathlib import Path
from pprint import pformat from pprint import pformat
from PyQt6.QtCore import pyqtSlot, Qt from PyQt6.QtCore import pyqtSlot, Qt
from PyQt6.QtGui import QContextMenuEvent, QAction
from PyQt6.QtWebChannel import QWebChannel from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import QDialog, QGridLayout, QMenu, QDialogButtonBox from PyQt6.QtWidgets import QDialog, QGridLayout, QDialogButtonBox
from typing import TYPE_CHECKING, Any, List from typing import TYPE_CHECKING, List
if TYPE_CHECKING: if TYPE_CHECKING:
from backend.db.models import Run, Procedure
from backend.validators import PydProcedure, PydEquipment from backend.validators import PydProcedure, PydEquipment
from tools import get_application_from_parent, render_details_template, sanitize_object_for_json from tools import get_application_from_parent, render_details_template, sanitize_object_for_json
@@ -26,7 +23,6 @@ class ProcedureCreation(QDialog):
self.edit = edit self.edit = edit
self.run = procedure.run self.run = procedure.run
self.procedure = procedure self.procedure = procedure
# logger.debug(f"procedure: {pformat(self.procedure.__dict__)}")
self.proceduretype = procedure.proceduretype self.proceduretype = procedure.proceduretype
self.setWindowTitle(f"New {self.proceduretype.name} for {self.run.rsl_plate_number}") self.setWindowTitle(f"New {self.proceduretype.name} for {self.run.rsl_plate_number}")
self.plate_map = self.proceduretype.construct_plate_map(sample_dicts=self.procedure.sample) self.plate_map = self.proceduretype.construct_plate_map(sample_dicts=self.procedure.sample)
@@ -56,7 +52,7 @@ class ProcedureCreation(QDialog):
def set_html(self): def set_html(self):
from .equipment_usage_2 import EquipmentUsage from .equipment_usage import EquipmentUsage
proceduretype_dict = self.proceduretype.details_dict() proceduretype_dict = self.proceduretype.details_dict()
# NOTE: Add --New-- as an option for reagents. # NOTE: Add --New-- as an option for reagents.
for key, value in self.procedure.reagentrole.items(): for key, value in self.procedure.reagentrole.items():
@@ -73,19 +69,15 @@ 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'] = [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']]
logger.debug(proceduretype_dict['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))]
# sys.exit(f"ProcedureDict:\n{pformat(proceduretype_dict)}")
html = render_details_template( html = render_details_template(
template_name="procedure_creation", template_name="procedure_creation",
# css_in=['new_context_menu'],
js_in=["procedure_form", "grid_drag", "context_menu"], js_in=["procedure_form", "grid_drag", "context_menu"],
proceduretype=proceduretype_dict, proceduretype=proceduretype_dict,
run=self.run.details_dict(), run=self.run.details_dict(),
# procedure=self.procedure.__dict__,
procedure=self.procedure, procedure=self.procedure,
plate_map=self.plate_map, plate_map=self.plate_map,
edit=self.edit edit=self.edit
@@ -93,9 +85,9 @@ class ProcedureCreation(QDialog):
self.webview.setHtml(html) self.webview.setHtml(html)
@pyqtSlot(str, str, str, str) @pyqtSlot(str, str, str, str)
def update_equipment(self, equipmentrole: str, equipment: str, process: str, tips: str): def update_equipment(self, equipmentrole: str, equipment: str, processversion: str, tips: str):
from backend.db.models import Equipment from backend.db.models import Equipment, ProcessVersion, TipsLot
# logger.debug("Updating equipment") logger.debug(f"\n\nEquipmentRole: {equipmentrole}, Equipment: {equipment}, Process: {processversion}, Tips: {tips}\n\n")
try: try:
equipment_of_interest = next( equipment_of_interest = next(
(item for item in self.procedure.equipment if item.equipmentrole == equipmentrole)) (item for item in self.procedure.equipment if item.equipmentrole == equipmentrole))
@@ -109,17 +101,26 @@ class ProcedureCreation(QDialog):
eoi.name = equipment.name eoi.name = equipment.name
eoi.asset_number = equipment.asset_number eoi.asset_number = equipment.asset_number
eoi.nickname = equipment.nickname eoi.nickname = equipment.nickname
# logger.warning("Setting processes.") process_name, version = processversion.split("-v")
eoi.process = [process for process in equipment.get_processes(equipmentrole=equipmentrole)] processversion = ProcessVersion.query(name=process_name, version=version, limit=1)
eoi.processversion = processversion.to_pydantic()
try:
tips_manufacturer, tipsref, lot = [item if item != "" else None for item in tips.split("-")]
tips = TipsLot.query(manufacturer=tips_manufacturer, ref=tipsref, lot=lot)
eoi.tips = tips
except ValueError:
logger.warning(f"No tips info to unpack")
self.procedure.equipment.append(eoi) self.procedure.equipment.append(eoi)
# logger.debug(f"Updated equipment: {pformat(self.procedure.equipment)}")
@pyqtSlot(str, str) @pyqtSlot(str, str)
def text_changed(self, key: str, new_value: str): def text_changed(self, key: str, new_value: str):
logger.debug(f"New value for {key}: {new_value}")
match key: match key:
case "rsl_plate_num": case "rsl_plate_num":
setattr(self.procedure.run, key, new_value) setattr(self.procedure.run, key, new_value)
case "repeat_of":
from backend.db.models import Procedure
parent = Procedure.query(name=new_value, limit=1)
self.procedure.repeat_of = parent
case _: case _:
attribute = getattr(self.procedure, key) attribute = getattr(self.procedure, key)
match attribute: match attribute:
@@ -127,19 +128,14 @@ class ProcedureCreation(QDialog):
attribute['value'] = new_value.strip('\"') attribute['value'] = new_value.strip('\"')
case _: case _:
setattr(self.procedure, key, new_value.strip('\"')) setattr(self.procedure, key, new_value.strip('\"'))
logger.debug(f"Set value for {key}: {getattr(self.procedure, key)}")
@pyqtSlot(str, bool) @pyqtSlot(str, bool)
def check_toggle(self, key: str, ischecked: bool): def check_toggle(self, key: str, ischecked: bool):
logger.debug(f"{key} is checked: {ischecked}")
setattr(self.procedure, key, ischecked) setattr(self.procedure, key, ischecked)
@pyqtSlot(str) @pyqtSlot(str)
def update_kit(self, kittype): def update_kit(self, kittype):
self.procedure.update_kittype_reagentroles(kittype=kittype) self.procedure.update_kittype_reagentroles(kittype=kittype)
logger.debug({k: v for k, v in self.procedure.__dict__.items() if k != "plate_map"})
self.set_html() self.set_html()
@pyqtSlot(list) @pyqtSlot(list)
@@ -155,33 +151,17 @@ class ProcedureCreation(QDialog):
from backend.validators.pydant import PydReagent from backend.validators.pydant import PydReagent
expiry = datetime.datetime.strptime(expiry, "%Y-%m-%d") expiry = datetime.datetime.strptime(expiry, "%Y-%m-%d")
pyd = PydReagent(reagentrole=reagentrole, name=name, lot=lot, expiry=expiry) pyd = PydReagent(reagentrole=reagentrole, name=name, lot=lot, expiry=expiry)
logger.debug(pyd)
self.procedure.reagentrole[reagentrole].insert(0, pyd) self.procedure.reagentrole[reagentrole].insert(0, pyd)
logger.debug(pformat(self.procedure.__dict__))
self.set_html() self.set_html()
@pyqtSlot(str, str) @pyqtSlot(str, str)
def update_reagent(self, reagentrole: str, name_lot_expiry: str): def update_reagent(self, reagentrole: str, name_lot_expiry: str):
logger.debug(f"{reagentrole}: {name_lot_expiry}")
try: try:
name, lot, expiry = name_lot_expiry.split(" - ") name, lot, expiry = name_lot_expiry.split(" - ")
except ValueError as e: except ValueError as e:
logger.debug(f"Couldn't perform split due to {e}")
return return
self.procedure.update_reagents(reagentrole=reagentrole, name=name, lot=lot, expiry=expiry) self.procedure.update_reagents(reagentrole=reagentrole, name=name, lot=lot, expiry=expiry)
def return_sql(self, new: bool = False): def return_sql(self, new: bool = False):
return self.procedure.to_sql(new=new) output = self.procedure.to_sql(new=new)
return output
# class ProcedureWebViewer(QWebEngineView):
#
# def __init__(self, *args, **kwargs):
# super().__init__(*args, **kwargs)
#
# def contextMenuEvent(self, event: QContextMenuEvent):
# self.menu = self.page().createStandardContextMenu()
# self.menu = self.createStandardContextMenu()
# add_sample = QAction("Add Sample")
# self.menu = QMenu()
# self.menu.addAction(add_sample)
# self.menu.popup(event.globalPos())

View File

@@ -1,5 +1,7 @@
"""
"""
import logging import logging
from pathlib import Path
from typing import List from typing import List
from PyQt6.QtCore import Qt, pyqtSlot from PyQt6.QtCore import Qt, pyqtSlot
from PyQt6.QtWebChannel import QWebChannel from PyQt6.QtWebChannel import QWebChannel
@@ -22,7 +24,6 @@ class SampleChecker(QDialog):
self.rsl_plate_number = RSLNamer.construct_new_plate_name(clientsubmission.to_dict()) self.rsl_plate_number = RSLNamer.construct_new_plate_name(clientsubmission.to_dict())
else: else:
self.rsl_plate_number = clientsubmission self.rsl_plate_number = clientsubmission
logger.debug(f"RSL Plate number: {self.rsl_plate_number}")
self.samples = samples self.samples = samples
self.setWindowTitle(title) self.setWindowTitle(title)
self.app = get_application_from_parent(parent) self.app = get_application_from_parent(parent)
@@ -35,16 +36,11 @@ class SampleChecker(QDialog):
self.channel = QWebChannel() self.channel = QWebChannel()
self.channel.registerObject('backend', self) self.channel.registerObject('backend', self)
# NOTE: Used to maintain javascript functions. # NOTE: Used to maintain javascript functions.
# template = env.get_template("sample_checker.html")
# template_path = Path(template.environment.loader.__getattribute__("searchpath")[0])
# with open(template_path.joinpath("css", "styles.css"), "r") as f:
# css = [f.read()]
try: try:
samples = self.formatted_list samples = self.formatted_list
except AttributeError as e: except AttributeError as e:
logger.error(f"Problem getting sample list: {e}") logger.error(f"Problem getting sample list: {e}")
samples = [] samples = []
# html = template.render(samples=samples, css=css, rsl_plate_number=self.rsl_plate_number)
html = render_details_template(template_name="sample_checker", samples=samples, rsl_plate_number=self.rsl_plate_number) html = render_details_template(template_name="sample_checker", samples=samples, rsl_plate_number=self.rsl_plate_number)
self.webview.setHtml(html) self.webview.setHtml(html)
self.webview.page().setWebChannel(self.channel) self.webview.page().setWebChannel(self.channel)
@@ -55,13 +51,8 @@ class SampleChecker(QDialog):
self.layout.addWidget(self.buttonBox, 11, 9, 1, 1, alignment=Qt.AlignmentFlag.AlignRight) self.layout.addWidget(self.buttonBox, 11, 9, 1, 1, alignment=Qt.AlignmentFlag.AlignRight)
self.setLayout(self.layout) self.setLayout(self.layout)
# with open("sample_checker_rendered.html", "w") as f:
# f.write(html)
logger.debug(f"HTML sample checker written!")
@pyqtSlot(str, str, str) @pyqtSlot(str, str, str)
def text_changed(self, submission_rank: str, key: str, new_value: str): def text_changed(self, submission_rank: str, key: str, new_value: str):
logger.debug(f"Name: {submission_rank}, Key: {key}, Value: {new_value}")
try: try:
item = next((sample for sample in self.samples if int(submission_rank) == sample.submission_rank)) item = next((sample for sample in self.samples if int(submission_rank) == sample.submission_rank))
except StopIteration: except StopIteration:
@@ -71,7 +62,6 @@ class SampleChecker(QDialog):
@pyqtSlot(int, bool) @pyqtSlot(int, bool)
def enable_sample(self, submission_rank: int, enabled: bool): def enable_sample(self, submission_rank: int, enabled: bool):
logger.debug(f"Name: {submission_rank}, Enabled: {enabled}")
try: try:
item = next((sample for sample in self.samples if int(submission_rank) == sample.submission_rank)) item = next((sample for sample in self.samples if int(submission_rank) == sample.submission_rank))
except StopIteration: except StopIteration:
@@ -81,14 +71,12 @@ class SampleChecker(QDialog):
@pyqtSlot(str) @pyqtSlot(str)
def set_rsl_plate_number(self, rsl_plate_number: str): def set_rsl_plate_number(self, rsl_plate_number: str):
logger.debug(f"RSL plate num: {rsl_plate_number}")
self.rsl_plate_number = rsl_plate_number self.rsl_plate_number = rsl_plate_number
@property @property
def formatted_list(self) -> List[dict]: def formatted_list(self) -> List[dict]:
output = [] output = []
for sample in self.samples: for sample in self.samples:
# logger.debug(sample)
s = sample.improved_dict(dictionaries=False) s = sample.improved_dict(dictionaries=False)
if s['sample_id'] in [item['sample_id'] for item in output]: if s['sample_id'] in [item['sample_id'] for item in output]:
s['color'] = "red" s['color'] = "red"

View File

@@ -62,14 +62,9 @@ class SubmissionDetails(QDialog):
css = f.read() css = f.read()
key = object.__class__.__name__.lower() key = object.__class__.__name__.lower()
d = {key: details} d = {key: details}
logger.debug(f"Using details: {pformat(d['procedure']['equipment'])}")
html = template.render(**d, css=[css]) html = template.render(**d, css=[css])
self.webview.setHtml(html) self.webview.setHtml(html)
self.setWindowTitle(f"{object.__class__.__name__} Details - {object.name}") self.setWindowTitle(f"{object.__class__.__name__} Details - {object.name}")
# with open(f"{object.__class__.__name__}_details_rendered.html", "w") as f:
# f.write(html)
# pass
def activate_export(self) -> None: def activate_export(self) -> None:
""" """
@@ -96,10 +91,10 @@ class SubmissionDetails(QDialog):
@pyqtSlot(str) @pyqtSlot(str)
def equipment_details(self, equipment: str | Equipment): def equipment_details(self, equipment: str | Equipment):
logger.debug(f"Equipment details")
if isinstance(equipment, str): if isinstance(equipment, str):
equipment = Equipment.query(name=equipment) equipment = Equipment.query(name=equipment)
base_dict = equipment.to_sub_dict(full_data=True) # base_dict = equipment.to_sub_dict(full_data=True)
base_dict = equipment.details_dict()
template = equipment.details_template template = equipment.details_template
template_path = Path(template.environment.loader.__getattribute__("searchpath")[0]) template_path = Path(template.environment.loader.__getattribute__("searchpath")[0])
with open(template_path.joinpath("css", "styles.css"), "r") as f: with open(template_path.joinpath("css", "styles.css"), "r") as f:
@@ -110,10 +105,10 @@ class SubmissionDetails(QDialog):
@pyqtSlot(str) @pyqtSlot(str)
def process_details(self, process: str | Process): def process_details(self, process: str | Process):
logger.debug(f"Process details")
if isinstance(process, str): if isinstance(process, str):
process = Process.query(name=process) process = Process.query(name=process)
base_dict = process.to_sub_dict(full_data=True) # base_dict = process.to_sub_dict(full_data=True)
base_dict = process.details_dict()
template = process.details_template template = process.details_template
template_path = Path(template.environment.loader.__getattribute__("searchpath")[0]) template_path = Path(template.environment.loader.__getattribute__("searchpath")[0])
with open(template_path.joinpath("css", "styles.css"), "r") as f: with open(template_path.joinpath("css", "styles.css"), "r") as f:
@@ -124,10 +119,10 @@ class SubmissionDetails(QDialog):
@pyqtSlot(str) @pyqtSlot(str)
def tips_details(self, tips: str | Tips): def tips_details(self, tips: str | Tips):
logger.debug(f"Equipment details: {tips}")
if isinstance(tips, str): if isinstance(tips, str):
tips = Tips.query(lot=tips) tips = Tips.query(lot=tips)
base_dict = tips.to_sub_dict(full_data=True) # base_dict = tips.to_sub_dict(full_data=True)
base_dict = tips.details_dict()
template = tips.details_template template = tips.details_template
template_path = Path(template.environment.loader.__getattribute__("searchpath")[0]) template_path = Path(template.environment.loader.__getattribute__("searchpath")[0])
with open(template_path.joinpath("css", "styles.css"), "r") as f: with open(template_path.joinpath("css", "styles.css"), "r") as f:
@@ -144,10 +139,10 @@ class SubmissionDetails(QDialog):
Args: Args:
sample (str): Submitter Id of the sample. sample (str): Submitter Id of the sample.
""" """
logger.debug(f"Sample details.")
if isinstance(sample, str): if isinstance(sample, str):
sample = Sample.query(sample_id=sample) sample = Sample.query(sample_id=sample)
base_dict = sample.to_sub_dict(full_data=True) # base_dict = sample.to_sub_dict(full_data=True)
base_dict = sample.details_dict()
exclude = ['procedure', 'excluded', 'colour', 'tooltip'] exclude = ['procedure', 'excluded', 'colour', 'tooltip']
base_dict['excluded'] = exclude base_dict['excluded'] = exclude
template = sample.details_template template = sample.details_template
@@ -155,8 +150,6 @@ class SubmissionDetails(QDialog):
with open(template_path.joinpath("css", "styles.css"), "r") as f: with open(template_path.joinpath("css", "styles.css"), "r") as f:
css = f.read() css = f.read()
html = template.render(sample=base_dict, css=css) html = template.render(sample=base_dict, css=css)
# with open(f"{sample.sample_id}.html", 'w') as f:
# f.write(html)
self.webview.setHtml(html) self.webview.setHtml(html)
self.setWindowTitle(f"Sample Details - {sample.sample_id}") self.setWindowTitle(f"Sample Details - {sample.sample_id}")
@@ -169,13 +162,13 @@ class SubmissionDetails(QDialog):
kit (str | KitType): Name of kittype. kit (str | KitType): Name of kittype.
reagent (str | Reagent): Lot number of the reagent reagent (str | Reagent): Lot number of the reagent
""" """
logger.debug(f"Reagent details.")
if isinstance(reagent, str): if isinstance(reagent, str):
reagent = Reagent.query(lot=reagent) reagent = Reagent.query(lot=reagent)
if isinstance(proceduretype, str): if isinstance(proceduretype, str):
self.proceduretype = ProcedureType.query(name=proceduretype) self.proceduretype = ProcedureType.query(name=proceduretype)
base_dict = reagent.to_sub_dict(proceduretype=self.proceduretype, full_data=True) # base_dict = reagent.to_sub_dict(proceduretype=self.proceduretype, full_data=True)
# base_dict = reagent.details_dict(proceduretype=self.proceduretype, full_data=True) # base_dict = reagent.details_dict(proceduretype=self.proceduretype, full_data=True)
base_dict = reagent.details_dict()
env = jinja_template_loading() env = jinja_template_loading()
temp_name = "reagent_details.html" temp_name = "reagent_details.html"
try: try:
@@ -221,7 +214,6 @@ class SubmissionDetails(QDialog):
Args: Args:
run (str | BasicRun): Submission of interest. run (str | BasicRun): Submission of interest.
""" """
logger.debug(f"Run details.")
if isinstance(run, str): if isinstance(run, str):
run = Run.query(name=run) run = Run.query(name=run)
self.rsl_plate_number = run.rsl_plate_number self.rsl_plate_number = run.rsl_plate_number
@@ -234,7 +226,6 @@ class SubmissionDetails(QDialog):
template_path = Path(self.template.environment.loader.__getattribute__("searchpath")[0]) template_path = Path(self.template.environment.loader.__getattribute__("searchpath")[0])
with open(template_path.joinpath("css", "styles.css"), "r") as f: with open(template_path.joinpath("css", "styles.css"), "r") as f:
css = f.read() css = f.read()
# logger.debug(f"Base dictionary of procedure {self.name}: {pformat(self.base_dict)}")
self.html = self.template.render(sub=self.base_dict, permission=is_power_user(), css=css) self.html = self.template.render(sub=self.base_dict, permission=is_power_user(), css=css)
self.webview.setHtml(self.html) self.webview.setHtml(self.html)
@@ -273,7 +264,6 @@ class SubmissionComment(QDialog):
""" """
def __init__(self, parent, submission: Run) -> None: def __init__(self, parent, submission: Run) -> None:
logger.debug(parent)
super().__init__(parent) super().__init__(parent)
self.app = get_application_from_parent(parent) self.app = get_application_from_parent(parent)
self.submission = submission self.submission = submission
@@ -293,7 +283,7 @@ class SubmissionComment(QDialog):
self.layout.addWidget(self.buttonBox, alignment=Qt.AlignmentFlag.AlignBottom) self.layout.addWidget(self.buttonBox, alignment=Qt.AlignmentFlag.AlignBottom)
self.setLayout(self.layout) self.setLayout(self.layout)
def parse_form(self) -> List[dict]: def parse_form(self) -> dict:
""" """
Adds comment to procedure object. Adds comment to procedure object.
""" """

View File

@@ -1,260 +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 QMenu, QTreeView, QAbstractItemView
from PyQt6.QtWidgets import QTableView, QMenu, QTreeView, QStyledItemDelegate, QStyle, QStyleOptionViewItem, \ from PyQt6.QtCore import QModelIndex
QHeaderView, QAbstractItemView, QWidget, QTreeWidgetItemIterator from PyQt6.QtGui import QAction, QCursor, QStandardItemModel, QStandardItem, QContextMenuEvent
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel, pyqtSlot, QModelIndex from typing import List
from PyQt6.QtGui import QAction, QCursor, QStandardItemModel, QStandardItem, QIcon, QColor, QContextMenuEvent from backend.db.models import Run, ClientSubmission, Procedure
from typing import Dict, List from tools import get_application_from_parent
# 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
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 = ClientSubmission.submissions_to_df(page=page, page_size=page_size)
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 ClientSubmissionDelegate(QStyledItemDelegate):
#
# def __init__(self, parent=None):
# super(ClientSubmissionDelegate, self).__init__(parent)
# pixmapi = QStyle.StandardPixmap.SP_ToolBarHorizontalExtensionButton
# icon1 = QWidget().style().standardIcon(pixmapi)
# pixmapi = QStyle.StandardPixmap.SP_ToolBarVerticalExtensionButton
# icon2 = QWidget().style().standardIcon(pixmapi)
# self._plus_icon = icon1
# self._minus_icon = icon2
#
# def initStyleOption(self, option, index):
# super(ClientSubmissionDelegate, self).initStyleOption(option, index)
# if not index.parent().isValid():
# is_open = bool(option.state & QStyle.StateFlag.State_Open)
# option.features |= QStyleOptionViewItem.ViewItemFeature.HasDecoration
# option.icon = self._minus_icon if is_open else self._plus_icon
# class RunDelegate(ClientSubmissionDelegate):
# pass
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
@@ -264,20 +22,12 @@ class SubmissionsTree(QTreeView):
super(SubmissionsTree, self).__init__(parent) super(SubmissionsTree, self).__init__(parent)
self.app = get_application_from_parent(parent) self.app = get_application_from_parent(parent)
self.total_count = ClientSubmission.__database_session__.query(ClientSubmission).count() self.total_count = ClientSubmission.__database_session__.query(ClientSubmission).count()
# self.setIndentation(0)
self.setExpandsOnDoubleClick(False) self.setExpandsOnDoubleClick(False)
# self.clicked.connect(self.on_clicked)
# delegate1 = ClientSubmissionDelegate(self)
# self.setItemDelegateForColumn(0, delegate1)
self.model = model self.model = model
self.setModel(self.model) self.setModel(self.model)
# self.header().setSectionResizeMode(0, QHeaderView.sectionResizeMode(self,0).ResizeToContents)
self.setSelectionBehavior(QAbstractItemView.selectionBehavior(self).SelectRows) self.setSelectionBehavior(QAbstractItemView.selectionBehavior(self).SelectRows)
# self.setStyleSheet("background-color: #0D1225;")
self.set_data() self.set_data()
self.doubleClicked.connect(self.show_details) self.doubleClicked.connect(self.show_details)
# self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
# self.customContextMenuRequested.connect(self.open_menu)
self.setStyleSheet(""" self.setStyleSheet("""
QTreeView { QTreeView {
background-color: #f5f5f5; background-color: #f5f5f5;
@@ -294,20 +44,13 @@ class SubmissionsTree(QTreeView):
} }
""") """)
# Enable alternating row colors # Note: Enable alternating row colors
self.setAlternatingRowColors(True) self.setAlternatingRowColors(True)
self.setIndentation(20) self.setIndentation(20)
self.setItemsExpandable(True) self.setItemsExpandable(True)
# self.expanded.connect(self.expand_item)
for ii in range(2): for ii in range(2):
self.resizeColumnToContents(ii) self.resizeColumnToContents(ii)
# @pyqtSlot(QModelIndex)
# def on_clicked(self, index):
# if not index.parent().isValid() and index.column() == 0:
# self.setExpanded(index, not self.isExpanded(index))
def expand_item(self, event: QModelIndex): def expand_item(self, event: QModelIndex):
logger.debug(f"Data: {event.data()}") logger.debug(f"Data: {event.data()}")
logger.debug(f"Parent {event.parent().data()}") logger.debug(f"Parent {event.parent().data()}")
@@ -327,18 +70,11 @@ class SubmissionsTree(QTreeView):
""" """
indexes = self.selectedIndexes() indexes = self.selectedIndexes()
dicto = next((item.data(1) for item in indexes if item.data(1))) dicto = next((item.data(1) for item in indexes if item.data(1)))
logger.debug(f"Dicto: {pformat(dicto)}")
query_obj = dicto['item_type'].query(name=dicto['query_str'], limit=1) query_obj = dicto['item_type'].query(name=dicto['query_str'], limit=1)
logger.debug(f"Querying: {query_obj}")
# NOTE: Convert to data in id column (i.e. column 0) # NOTE: Convert to data in id column (i.e. column 0)
# id = id.sibling(id.row(), 0).data()
# logger.debug(id.model().query_group_object(id.row()))
# clientsubmission = id.model().query_group_object(id.row())
self.menu = QMenu(self) self.menu = QMenu(self)
self.con_actions = query_obj.custom_context_events self.con_actions = query_obj.custom_context_events
logger.debug(f"Context menu actions: {self.con_actions}")
for key in self.con_actions.keys(): for key in self.con_actions.keys():
logger.debug(key)
match key.lower(): match key.lower():
case "add procedure": case "add procedure":
action = QMenu(self.menu) action = QMenu(self.menu)
@@ -362,7 +98,7 @@ class SubmissionsTree(QTreeView):
action = QAction(key, self) action = QAction(key, self)
action.triggered.connect(lambda _, action_name=key: self.con_actions[action_name](obj=self)) action.triggered.connect(lambda _, action_name=key: self.con_actions[action_name](obj=self))
self.menu.addAction(action) self.menu.addAction(action)
# # NOTE: add other required actions # NOTE: add other required actions
self.menu.popup(QCursor.pos()) self.menu.popup(QCursor.pos())
def set_data(self, page: int = 1, page_size: int = 250) -> None: def set_data(self, page: int = 1, page_size: int = 250) -> None:
@@ -372,8 +108,6 @@ class SubmissionsTree(QTreeView):
self.clear() self.clear()
self.data = [item.to_dict(full_data=True) for item in self.data = [item.to_dict(full_data=True) for item in
ClientSubmission.query(chronologic=True, page=page, page_size=page_size)] ClientSubmission.query(chronologic=True, page=page, page_size=page_size)]
logger.debug(f"setting data:\n {pformat(self.data)}")
# sys.exit()
root = self.model.invisibleRootItem() root = self.model.invisibleRootItem()
for submission in self.data: for submission in self.data:
group_str = f"{submission['submissiontype']}-{submission['submitter_plate_id']}-{submission['submitted_date']}" group_str = f"{submission['submissiontype']}-{submission['submitter_plate_id']}-{submission['submitted_date']}"
@@ -382,26 +116,21 @@ class SubmissionsTree(QTreeView):
query_str=submission['submitter_plate_id'], query_str=submission['submitter_plate_id'],
item_type=ClientSubmission item_type=ClientSubmission
)) ))
# logger.debug(f"Added {submission_item}")
for run in submission['run']: for run in submission['run']:
# self.model.append_element_to_group(group_item=group_item, element=run)
run_item = self.model.add_child(parent=submission_item, child=dict( run_item = self.model.add_child(parent=submission_item, child=dict(
name=run['plate_number'], name=run['plate_number'],
query_str=run['plate_number'], query_str=run['plate_number'],
item_type=Run item_type=Run
)) ))
# logger.debug(f"Added {run_item}")
for procedure in run['procedures']: for procedure in run['procedures']:
procedure_item = self.model.add_child(parent=run_item, child=dict( procedure_item = self.model.add_child(parent=run_item, child=dict(
name=procedure['name'], name=procedure['name'],
query_str=procedure['name'], query_str=procedure['name'],
item_type=Procedure item_type=Procedure
)) ))
# logger.debug(f"Added {procedure_item}")
def _populateTree(self, children, parent): def _populateTree(self, children, parent):
for child in children: for child in children:
logger.debug(child)
child_item = QStandardItem(child['name']) child_item = QStandardItem(child['name'])
parent.appendRow(child_item) parent.appendRow(child_item)
if isinstance(children, List): if isinstance(children, List):
@@ -409,22 +138,13 @@ class SubmissionsTree(QTreeView):
def clear(self): def clear(self):
if self.model != None: if self.model != None:
# self.model.clear() # works
self.model.setRowCount(0) # works self.model.setRowCount(0) # works
def show_details(self, sel: QModelIndex): def show_details(self, sel: QModelIndex):
# id = self.selectionModel().currentIndex()
# NOTE: Convert to data in id column (i.e. column 0) # NOTE: Convert to data in id column (i.e. column 0)
# id = id.sibling(id.row(), 1)
indexes = self.selectedIndexes() indexes = self.selectedIndexes()
dicto = next((item.data(1) for item in indexes if item.data(1))) dicto = next((item.data(1) for item in indexes if item.data(1)))
# try:
# id = int(id.data())
# except ValueError:
# return
# Run.query(id=id).show_details(self)
obj = dicto['item_type'].query(name=dicto['query_str'], limit=1) obj = dicto['item_type'].query(name=dicto['query_str'], limit=1)
logger.debug(obj)
obj.show_details(self) obj.show_details(self)
def link_extractions(self): def link_extractions(self):
@@ -436,15 +156,6 @@ class SubmissionsTree(QTreeView):
class ClientSubmissionRunModel(QStandardItemModel): class ClientSubmissionRunModel(QStandardItemModel):
def __init__(self, parent=None):
super(ClientSubmissionRunModel, self).__init__(parent)
# headers = ["", "id", "Plate Number", "Started Date", "Completed Date", "Signed By"]
# self.setColumnCount(len(headers))
# self.setHorizontalHeaderLabels(headers)
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

@@ -1,24 +1,22 @@
""" """
Contains all procedure related frontend functions Contains all procedure related frontend functions
""" """
import sys import sys, logging
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QWidget, QPushButton, QVBoxLayout, QWidget, QPushButton, QVBoxLayout,
QComboBox, QDateEdit, QLineEdit, QLabel, QCheckBox, QHBoxLayout, QGridLayout QComboBox, QDateEdit, QLineEdit, QLabel, QCheckBox, QHBoxLayout, QGridLayout
) )
from PyQt6.QtCore import pyqtSignal, Qt, QSignalBlocker from PyQt6.QtCore import pyqtSignal, Qt, QSignalBlocker
from .functions import select_open_file, select_save_file from .functions import select_open_file, select_save_file
import logging
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, 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
@@ -121,37 +119,16 @@ class SubmissionFormContainer(QWidget):
report.add_result(Result(msg=f"File {fname.__str__()} not found.", status="critical")) report.add_result(Result(msg=f"File {fname.__str__()} not found.", status="critical"))
return report return report
# NOTE: create sheetparser using excel sheet and context from gui # NOTE: create sheetparser using excel sheet and context from gui
# try:
# self.clientsubmissionparser = ClientSubmissionInfoParser(filepath=fname)
# except PermissionError:
# logger.error(f"Couldn't get permission to access file: {fname}")
# return
# except AttributeError:
# self.clientsubmissionparser = ClientSubmissionInfoParser(filepath=fname)
# try:
# # self.prsr = SheetParser(filepath=fname)
# self.sampleparser = ClientSubmissionSampleParser(filepath=fname)
# except PermissionError:
# logger.error(f"Couldn't get permission to access file: {fname}")
# return
# except AttributeError:
# self.sampleparser = ClientSubmissionSampleParser(filepath=fname)
# self.pydclientsubmission = self.clientsubmissionparser.to_pydantic()
# self.pydsamples = self.sampleparser.to_pydantic()
# logger.debug(f"Samples: {pformat(self.pydclientsubmission.sample)}")
self.clientsubmission_manager = DefaultClientSubmissionManager(parent=self, input_object=fname) self.clientsubmission_manager = DefaultClientSubmissionManager(parent=self, input_object=fname)
self.pydclientsubmission = self.clientsubmission_manager.to_pydantic() self.pydclientsubmission = self.clientsubmission_manager.to_pydantic()
checker = SampleChecker(self, "Sample Checker", self.pydclientsubmission.sample) checker = SampleChecker(self, "Sample Checker", self.pydclientsubmission.sample)
if checker.exec(): if checker.exec():
# logger.debug(pformat(self.pydclientsubmission.sample))
try: try:
assert isinstance(self.pydclientsubmission, PydClientSubmission) assert isinstance(self.pydclientsubmission, PydClientSubmission)
except AssertionError as e: except AssertionError as e:
logger.error(f"Got wrong type for {self.pydclientsubmission}: {type(self.pydclientsubmission)}") logger.error(f"Got wrong type for {self.pydclientsubmission}: {type(self.pydclientsubmission)}")
raise e raise e
self.form = self.pydclientsubmission.to_form(parent=self) self.form = self.pydclientsubmission.to_form(parent=self)
# self.form.samples = self.pydsamples
self.layout().addWidget(self.form) self.layout().addWidget(self.form)
else: else:
message = "Submission cancelled." message = "Submission cancelled."
@@ -160,7 +137,7 @@ class SubmissionFormContainer(QWidget):
return report return report
@report_result @report_result
def add_reagent(self, instance: Reagent | None = None): def add_reagent(self, instance: ReagentLot | None = None):
""" """
Action to create new reagent in DB. Action to create new reagent in DB.
@@ -172,7 +149,7 @@ class SubmissionFormContainer(QWidget):
""" """
report = Report() report = Report()
if not instance: if not instance:
instance = Reagent() instance = ReagentLot()
dlg = AddEdit(parent=self, instance=instance) dlg = AddEdit(parent=self, instance=instance)
if dlg.exec(): if dlg.exec():
reagent = dlg.parse_form() reagent = dlg.parse_form()
@@ -195,14 +172,11 @@ class SubmissionFormWidget(QWidget):
self.pyd = pyd self.pyd = pyd
self.missing_info = [] self.missing_info = []
self.submissiontype = SubmissionType.query(name=self.pyd.submissiontype['value']) self.submissiontype = SubmissionType.query(name=self.pyd.submissiontype['value'])
# basic_submission_class = self.submission_type.submission_class
# logger.debug(f"Basic procedure class: {basic_submission_class}")
defaults = Run.get_default_info("form_recover", "form_ignore", submissiontype=self.pyd.submissiontype['value']) defaults = Run.get_default_info("form_recover", "form_ignore", submissiontype=self.pyd.submissiontype['value'])
self.recover = defaults['form_recover'] self.recover = defaults['form_recover']
self.ignore = defaults['form_ignore'] self.ignore = defaults['form_ignore']
self.layout = QVBoxLayout() self.layout = QVBoxLayout()
for k in list(self.pyd.model_fields.keys()):# + list(self.pyd.model_extra.keys()): for k in list(self.pyd.model_fields.keys()):# + list(self.pyd.model_extra.keys()):
logger.debug(f"Pydantic field: {k}")
if k in self.ignore: if k in self.ignore:
logger.warning(f"{k} in form_ignore {self.ignore}, not creating widget") logger.warning(f"{k} in form_ignore {self.ignore}, not creating widget")
continue continue
@@ -218,7 +192,6 @@ class SubmissionFormWidget(QWidget):
value = self.pyd.model_extra[k] value = self.pyd.model_extra[k]
except KeyError: except KeyError:
value = dict(value=None, missing=True) value = dict(value=None, missing=True)
logger.debug(f"Pydantic value: {value}")
add_widget = self.create_widget(key=k, value=value, submission_type=self.submissiontype, add_widget = self.create_widget(key=k, value=value, submission_type=self.submissiontype,
run_object=Run(), disable=check) run_object=Run(), disable=check)
if add_widget is not None: if add_widget is not None:
@@ -230,7 +203,6 @@ class SubmissionFormWidget(QWidget):
self.layout.addWidget(self.disabler) self.layout.addWidget(self.disabler)
self.disabler.checkbox.checkStateChanged.connect(self.disable_reagents) self.disabler.checkbox.checkStateChanged.connect(self.disable_reagents)
self.setStyleSheet(main_form_style) self.setStyleSheet(main_form_style)
# self.scrape_reagents(self.kittype)
self.setLayout(self.layout) self.setLayout(self.layout)
def disable_reagents(self): def disable_reagents(self):
@@ -298,7 +270,6 @@ class SubmissionFormWidget(QWidget):
if isinstance(reagent, self.ReagentFormWidget) or isinstance(reagent, QPushButton): if isinstance(reagent, self.ReagentFormWidget) or isinstance(reagent, QPushButton):
reagent.setParent(None) reagent.setParent(None)
reagents, integrity_report, missing_reagents = self.pyd.check_kit_integrity(extraction_kit=self.extraction_kit) reagents, integrity_report, missing_reagents = self.pyd.check_kit_integrity(extraction_kit=self.extraction_kit)
# logger.debug(f"Reagents: {reagents}")
expiry_report = self.pyd.check_reagent_expiries(exempt=missing_reagents) expiry_report = self.pyd.check_reagent_expiries(exempt=missing_reagents)
for reagent in reagents: for reagent in reagents:
add_widget = self.ReagentFormWidget(parent=self, reagent=reagent, extraction_kit=self.extraction_kit) add_widget = self.ReagentFormWidget(parent=self, reagent=reagent, extraction_kit=self.extraction_kit)
@@ -364,34 +335,6 @@ class SubmissionFormWidget(QWidget):
return report return report
base_submission = self.pyd.to_sql() base_submission = self.pyd.to_sql()
# NOTE: check output message for issues # NOTE: check output message for issues
# try:
# trigger = result.results[-1]
# code = trigger.code
# except IndexError as e:
# logger.error(result.results)
# logger.error(f"Problem getting error code: {e}")
# code = 0
# match code:
# # NOTE: code 0: everything is fine.
# case 0:
# pass
# # NOTE: code 1: ask for overwrite
# case 1:
# dlg = QuestionAsker(title=f"Review {base_submission.rsl_plate_number}?", message=trigger.msg)
# if dlg.exec():
# # NOTE: Do not add duplicate reagents.
# pass
# else:
# self.app.ctx.database_session.rollback()
# report.add_result(Result(msg="Overwrite cancelled", status="Information"))
# return report
# # NOTE: code 2: No RSL plate number given
# case 2:
# report.add_result(result)
# return report
# case _:
# pass
# NOTE: add reagents to procedure object
if base_submission is None: if base_submission is None:
return return
for reagent in base_submission.reagents: for reagent in base_submission.reagents:
@@ -450,7 +393,6 @@ class SubmissionFormWidget(QWidget):
if field is not None: if field is not None:
info[field] = value info[field] = value
self.pyd.reagents = reagents self.pyd.reagents = reagents
# logger.debug(f"Reagents from form: {reagents}")
for item in self.recover: for item in self.recover:
if hasattr(self, item): if hasattr(self, item):
value = getattr(self, item) value = getattr(self, item)
@@ -558,29 +500,29 @@ class SubmissionFormWidget(QWidget):
# NOTE: set combobox values to lookedup values # NOTE: set combobox values to lookedup values
add_widget.addItems(labs) add_widget.addItems(labs)
add_widget.setToolTip("Select submitting lab.") add_widget.setToolTip("Select submitting lab.")
case 'kittype': # case 'kittype':
# NOTE: if extraction kittype not available, all other values fail # # NOTE: if extraction kittype not available, all other values fail
if not check_not_nan(value): # if not check_not_nan(value):
msg = AlertPop(message="Make sure to check your extraction kittype in the excel sheet!", # msg = AlertPop(message="Make sure to check your extraction kittype in the excel sheet!",
status="warning") # status="warning")
msg.exec() # msg.exec()
# NOTE: create combobox to hold looked up kits # # NOTE: create combobox to hold looked up kits
add_widget = MyQComboBox(scrollWidget=parent) # add_widget = MyQComboBox(scrollWidget=parent)
# NOTE: lookup existing kits by 'proceduretype' decided on by sheetparser # # NOTE: lookup existing kits by 'proceduretype' decided on by sheetparser
uses = [item.name for item in submission_type.kit_types] # uses = [item.name for item in submission_type.kit_types]
obj.uses = uses # obj.uses = uses
if check_not_nan(value): # if check_not_nan(value):
try: # try:
uses.insert(0, uses.pop(uses.index(value))) # uses.insert(0, uses.pop(uses.index(value)))
except ValueError: # except ValueError:
logger.warning(f"Couldn't find kittype in list, skipping move to top of list.") # logger.warning(f"Couldn't find kittype in list, skipping move to top of list.")
obj.ext_kit = value # obj.ext_kit = value
else: # else:
logger.error(f"Couldn't find {obj.prsr.sub['kittype']}") # logger.error(f"Couldn't find {obj.prsr.sub['kittype']}")
obj.ext_kit = uses[0] # obj.ext_kit = uses[0]
add_widget.addItems(uses) # add_widget.addItems(uses)
add_widget.setToolTip("Select extraction kittype.") # add_widget.setToolTip("Select extraction kittype.")
parent.extraction_kit = add_widget.currentText() # parent.extraction_kit = add_widget.currentText()
case 'submission_category': case 'submission_category':
add_widget = MyQComboBox(scrollWidget=parent) add_widget = MyQComboBox(scrollWidget=parent)
categories = ['Diagnostic', "Surveillance", "Research"] categories = ['Diagnostic', "Surveillance", "Research"]
@@ -813,11 +755,8 @@ class ClientSubmissionFormWidget(SubmissionFormWidget):
self.disabler.setHidden(True) self.disabler.setHidden(True)
except AttributeError: except AttributeError:
pass pass
# save_btn = QPushButton("Save")
self.sample = samples self.sample = samples
logger.debug(f"Samples: {self.sample}")
start_run_btn = QPushButton("Save") start_run_btn = QPushButton("Save")
# self.layout.addWidget(save_btn)
self.layout.addWidget(start_run_btn) self.layout.addWidget(start_run_btn)
start_run_btn.clicked.connect(self.create_new_submission) start_run_btn.clicked.connect(self.create_new_submission)
@@ -846,7 +785,6 @@ class ClientSubmissionFormWidget(SubmissionFormWidget):
field, value = widget.parse_form() field, value = widget.parse_form()
if field is not None: if field is not None:
info[field] = value info[field] = value
# logger.debug(f"Reagents from form: {reagents}")
for item in self.recover: for item in self.recover:
if hasattr(self, item): if hasattr(self, item):
value = getattr(self, item) value = getattr(self, item)
@@ -865,7 +803,6 @@ class ClientSubmissionFormWidget(SubmissionFormWidget):
@report_result @report_result
def create_new_submission(self, *args) -> Report: def create_new_submission(self, *args) -> Report:
pyd = self.to_pydantic() pyd = self.to_pydantic()
logger.debug(f"Pydantic: {pyd}")
sql = pyd.to_sql() sql = pyd.to_sql()
for sample in pyd.sample: for sample in pyd.sample:
if isinstance(sample, PydSample): if isinstance(sample, PydSample):
@@ -874,9 +811,7 @@ class ClientSubmissionFormWidget(SubmissionFormWidget):
if sample.sample_id.lower() in ["", "blank"]: if sample.sample_id.lower() in ["", "blank"]:
continue continue
sample.save() sample.save()
# if sample not in sql.sample:
sql.add_sample(sample=sample) sql.add_sample(sample=sample)
logger.debug(pformat(sql.__dict__))
try: try:
del sql._misc_info['sample'] del sql._misc_info['sample']
except KeyError: except KeyError:

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

View File

@@ -56,10 +56,15 @@
{% endif %} {% endif %}
{% if procedure['sample'] %} {% if procedure['sample'] %}
<button type="button"><h3><u>Procedure Samples:</u></h3></button> <button type="button"><h3><u>Procedure Samples:</u></h3></button>
{% if procedure['platemap']|length > 5 %}
<br>
{{ procedure['platemap'] }}
{% else %}
<p>{% for sample in procedure['sample'] %} <p>{% for sample in procedure['sample'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<a class="{% if sample['active'] %}data-link {% else %}unused {% endif %}sample" id="{{ sample['sample_id'] }}">{{ sample['sample_id']}}</a><br> <a class="{% if sample['active'] %}data-link {% else %}unused {% endif %}sample" id="{{ sample['sample_id'] }}">{{ sample['sample_id']}}</a><br>
{% endfor %}</p> {% endfor %}</p>
{% endif %} {% endif %}
{% endif %}
{% endblock %} {% endblock %}
{% if not child %} {% if not child %}
</body> </body>

View File

@@ -1,7 +1,11 @@
<div class="plate" id="plate-container" style="grid-template-columns: repeat({{ plate_columns }}, {{ vw }}vw);grid-template-rows: repeat({{ plate_rows }}, {{ vw }}vw);"> <div class="plate" id="plate-container" style="grid-template-columns: repeat({{ plate_columns }}, {{ vw }}vw);grid-template-rows: repeat({{ plate_rows }}, {{ vw }}vw);">
{% for sample in samples %} {% for sample in samples %}
<div class="well" draggable="true" id="{{ sample['well_id'] }}" style="background-color: {{ sample['background_color'] }};"> <div class="well" draggable="true" id="{{ sample['sample_id'] }}" style="background-color: {{ sample['background_color'] }};">
{% if creation %}
<p style="font-size: 0.7em; text-align: center; word-wrap: break-word;">{{ sample['sample_id'] }}</p> <p style="font-size: 0.7em; text-align: center; word-wrap: break-word;">{{ sample['sample_id'] }}</p>
{% else %}
<a class="data-link sample" id="{{ sample['sample_id'] }}">{{ sample['sample_id']}}</a><br>
{% endif %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>

View File

@@ -1,18 +1,17 @@
''' """
Contains miscellaenous functions used by both frontend and backend. Contains miscellaenous functions used by both frontend and backend.
''' """
from __future__ import annotations from __future__ import annotations
import builtins, importlib, time, logging, re, yaml, sys, os, stat, platform, getpass, json, numpy as np, pandas as pd import builtins, importlib, time, logging, re, yaml, sys, os, stat, platform, getpass, json, numpy as np, pandas as pd, \
import itertools itertools, openpyxl
from copy import copy
from collections import OrderedDict from collections import OrderedDict
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from json import JSONDecodeError from json import JSONDecodeError
from pprint import pformat
from threading import Thread from threading import Thread
from inspect import getmembers, isfunction, stack from inspect import getmembers, isfunction, stack
from types import NoneType
from dateutil.easter import easter from dateutil.easter import easter
from dateutil.parser import parse
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from logging import handlers, Logger from logging import handlers, Logger
from pathlib import Path from pathlib import Path
@@ -60,7 +59,6 @@ main_form_style = '''
page_size = 250 page_size = 250
# micro_char = uni_char = "\u03BC"
def divide_chunks(input_list: list, chunk_count: int) -> Generator[Any, Any, None]: def divide_chunks(input_list: list, chunk_count: int) -> Generator[Any, Any, None]:
""" """
@@ -447,16 +445,18 @@ def jinja_template_loading() -> Environment:
return env return env
def render_details_template(template_name:str, css_in:List[str]|str=[], js_in:List[str]|str=[], **kwargs) -> str: def render_details_template(template_name: str, css_in: List[str] | str = [], js_in: List[str] | str = [],
**kwargs) -> str:
if isinstance(css_in, str): if isinstance(css_in, str):
css_in = [css_in] css_in = [css_in]
env = jinja_template_loading()
html_folder = Path(env.loader.__getattribute__("searchpath")[0])
css_in = ["styles"] + css_in css_in = ["styles"] + css_in
css_in = [project_path.joinpath("src", "submissions", "templates", "css", f"{c}.css") for c in css_in] css_in = [html_folder.joinpath("css", f"{c}.css") for c in css_in]
if isinstance(js_in, str): if isinstance(js_in, str):
js_in = [js_in] js_in = [js_in]
js_in = ["details"] + js_in js_in = ["details"] + js_in
js_in = [project_path.joinpath("src", "submissions", "templates", "js", f"{j}.js") for j in js_in] js_in = [html_folder.joinpath("js", f"{j}.js") for j in js_in]
env = jinja_template_loading()
template = env.get_template(f"{template_name}.html") template = env.get_template(f"{template_name}.html")
# template_path = Path(template.environment.loader.__getattribute__("searchpath")[0]) # template_path = Path(template.environment.loader.__getattribute__("searchpath")[0])
css_out = [] css_out = []
@@ -467,7 +467,6 @@ def render_details_template(template_name:str, css_in:List[str]|str=[], js_in:Li
for js in js_in: for js in js_in:
with open(js, "r") as f: with open(js, "r") as f:
js_out.append(f.read()) js_out.append(f.read())
# logger.debug(f"Kwargs: {kwargs}")
return template.render(css=css_out, js=js_out, **kwargs) return template.render(css=css_out, js=js_out, **kwargs)
@@ -489,10 +488,10 @@ def convert_well_to_row_column(input_str: str) -> Tuple[int, int]:
return None, None return None, None
return row, column return row, column
# Copy a sheet with style, format, layout, ect. from one Excel file to another Excel file # Copy a sheet with style, format, layout, ect. from one Excel file to another Excel file
# Please add the ..path\\+\\file.. and ..sheet_name.. according to your desire. # Please add the ..path\\+\\file.. and ..sheet_name.. according to your desire.
import openpyxl
from copy import copy
def copy_xl_sheet(source_sheet, target_sheet): def copy_xl_sheet(source_sheet, target_sheet):
@@ -509,8 +508,8 @@ def copy_sheet_attributes(source_sheet, target_sheet):
target_sheet.page_margins = copy(source_sheet.page_margins) target_sheet.page_margins = copy(source_sheet.page_margins)
target_sheet.freeze_panes = copy(source_sheet.freeze_panes) target_sheet.freeze_panes = copy(source_sheet.freeze_panes)
# set row dimensions # NOTE: set row dimensions
# So you cannot copy the row_dimensions attribute. Does not work (because of meta data in the attribute I think). So we copy every row's row_dimensions. That seems to work. # NOTE: So you cannot copy the row_dimensions attribute. Does not work (because of meta data in the attribute I think). So we copy every row's row_dimensions. That seems to work.
for rn in range(len(source_sheet.row_dimensions)): for rn in range(len(source_sheet.row_dimensions)):
target_sheet.row_dimensions[rn] = copy(source_sheet.row_dimensions[rn]) target_sheet.row_dimensions[rn] = copy(source_sheet.row_dimensions[rn])
@@ -519,12 +518,15 @@ def copy_sheet_attributes(source_sheet, target_sheet):
else: else:
target_sheet.sheet_format.defaultColWidth = copy(source_sheet.sheet_format.defaultColWidth) target_sheet.sheet_format.defaultColWidth = copy(source_sheet.sheet_format.defaultColWidth)
# set specific column width and hidden property # NOTE: set specific column width and hidden property
# we cannot copy the entire column_dimensions attribute so we copy selected attributes # NOTE: we cannot copy the entire column_dimensions attribute so we copy selected attributes
for key, value in source_sheet.column_dimensions.items(): for key, value in source_sheet.column_dimensions.items():
target_sheet.column_dimensions[key].min = copy(source_sheet.column_dimensions[key].min) # Excel actually groups multiple columns under 1 key. Use the min max attribute to also group the columns in the targetSheet target_sheet.column_dimensions[key].min = copy(source_sheet.column_dimensions[
target_sheet.column_dimensions[key].max = copy(source_sheet.column_dimensions[key].max) # https://stackoverflow.com/questions/36417278/openpyxl-can-not-read-consecutive-hidden-columns discussed the issue. Note that this is also the case for the width, not onl;y the hidden property key].min) # Excel actually groups multiple columns under 1 key. Use the min max attribute to also group the columns in the targetSheet
target_sheet.column_dimensions[key].width = copy(source_sheet.column_dimensions[key].width) # set width for every column target_sheet.column_dimensions[key].max = copy(source_sheet.column_dimensions[
key].max) # https://stackoverflow.com/questions/36417278/openpyxl-can-not-read-consecutive-hidden-columns discussed the issue. Note that this is also the case for the width, not onl;y the hidden property
target_sheet.column_dimensions[key].width = copy(
source_sheet.column_dimensions[key].width) # set width for every column
target_sheet.column_dimensions[key].hidden = copy(source_sheet.column_dimensions[key].hidden) target_sheet.column_dimensions[key].hidden = copy(source_sheet.column_dimensions[key].hidden)
@@ -535,10 +537,8 @@ def copy_cells(source_sheet, target_sheet):
if isinstance(source_cell, openpyxl.cell.read_only.EmptyCell): if isinstance(source_cell, openpyxl.cell.read_only.EmptyCell):
continue continue
target_cell = target_sheet.cell(column=c + 1, row=r + 1) target_cell = target_sheet.cell(column=c + 1, row=r + 1)
target_cell._value = source_cell._value target_cell._value = source_cell._value
target_cell.data_type = source_cell.data_type target_cell.data_type = source_cell.data_type
if source_cell.has_style: if source_cell.has_style:
target_cell.font = copy(source_cell.font) target_cell.font = copy(source_cell.font)
target_cell.border = copy(source_cell.border) target_cell.border = copy(source_cell.border)
@@ -546,10 +546,8 @@ def copy_cells(source_sheet, target_sheet):
target_cell.number_format = copy(source_cell.number_format) target_cell.number_format = copy(source_cell.number_format)
target_cell.protection = copy(source_cell.protection) target_cell.protection = copy(source_cell.protection)
target_cell.alignment = copy(source_cell.alignment) target_cell.alignment = copy(source_cell.alignment)
if not isinstance(source_cell, openpyxl.cell.ReadOnlyCell) and source_cell.hyperlink: if not isinstance(source_cell, openpyxl.cell.ReadOnlyCell) and source_cell.hyperlink:
target_cell._hyperlink = copy(source_cell.hyperlink) target_cell._hyperlink = copy(source_cell.hyperlink)
if not isinstance(source_cell, openpyxl.cell.ReadOnlyCell) and source_cell.comment: if not isinstance(source_cell, openpyxl.cell.ReadOnlyCell) and source_cell.comment:
target_cell.comment = copy(source_cell.comment) target_cell.comment = copy(source_cell.comment)
@@ -567,6 +565,7 @@ def list_str_comparator(input_str:str, listy: List[str], mode: Literal["starts_w
else: else:
return False return False
def sort_dict_by_list(dictionary: dict, order_list: list) -> dict: def sort_dict_by_list(dictionary: dict, order_list: list) -> dict:
output = OrderedDict() output = OrderedDict()
for item in order_list: for item in order_list:
@@ -602,14 +601,12 @@ def setup_lookup(func):
elif v is not None: elif v is not None:
sanitized_kwargs[k] = v sanitized_kwargs[k] = v
return func(*args, **sanitized_kwargs) return func(*args, **sanitized_kwargs)
return wrapper return wrapper
def check_object_in_manager(manager: list, object_name: object) -> Tuple[Any, bool]: def check_object_in_manager(manager: list, object_name: object) -> Tuple[Any, bool]:
if manager is None: if manager is None:
return None, False return None, False
# logger.debug(f"Manager: {manager}, aliases: {manager.aliases}, Key: {object_name}")
if object_name in manager.aliases: if object_name in manager.aliases:
return manager, True return manager, True
relationships = [getattr(manager.__class__, item) for item in dir(manager.__class__) relationships = [getattr(manager.__class__, item) for item in dir(manager.__class__)
@@ -617,21 +614,17 @@ def check_object_in_manager(manager: list, object_name: object) -> Tuple[Any, bo
relationships = [item for item in relationships if isinstance(item.property, _RelationshipDeclared)] relationships = [item for item in relationships if isinstance(item.property, _RelationshipDeclared)]
for relationship in relationships: for relationship in relationships:
if relationship.key == object_name and "association" not in relationship.key: if relationship.key == object_name and "association" not in relationship.key:
logger.debug(f"Checking {relationship.key}")
try: try:
rel_obj = getattr(manager, relationship.key) rel_obj = getattr(manager, relationship.key)
if rel_obj is not None: if rel_obj is not None:
logger.debug(f"Returning {rel_obj}")
return rel_obj, False return rel_obj, False
except AttributeError: except AttributeError:
pass pass
if "association" in relationship.key: if "association" in relationship.key:
try: try:
logger.debug(f"Checking association {relationship.key}")
rel_obj = next((getattr(item, object_name) for item in getattr(manager, relationship.key) rel_obj = next((getattr(item, object_name) for item in getattr(manager, relationship.key)
if getattr(item, object_name) is not None), None) if getattr(item, object_name) is not None), None)
if rel_obj is not None: if rel_obj is not None:
logger.debug(f"Returning {rel_obj}")
return rel_obj, False return rel_obj, False
except AttributeError: except AttributeError:
pass pass
@@ -862,7 +855,6 @@ def check_authorization(func):
report.add_result( report.add_result(
Result(owner=func.__str__(), code=1, msg=error_msg, status="warning")) Result(owner=func.__str__(), code=1, msg=error_msg, status="warning"))
return report, kwargs return report, kwargs
return wrapper return wrapper
@@ -888,7 +880,6 @@ def under_development(func):
Result(owner=func.__str__(), code=1, msg=error_msg, Result(owner=func.__str__(), code=1, msg=error_msg,
status="warning")) status="warning"))
return report return report
return wrapper return wrapper
@@ -906,7 +897,6 @@ def report_result(func):
@wraps(func) @wraps(func)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
# logger.info(f"Report result being called by {func.__name__}")
output = func(*args, **kwargs) output = func(*args, **kwargs)
match output: match output:
case Report(): case Report():
@@ -931,6 +921,7 @@ def report_result(func):
logger.error(f"Problem reporting due to {e}") logger.error(f"Problem reporting due to {e}")
logger.error(result.msg) logger.error(result.msg)
if output: if output:
logger.info(f"Report result being called by {func.__name__}")
if is_list_etc(output): if is_list_etc(output):
true_output = tuple(item for item in output if not isinstance(item, Report)) true_output = tuple(item for item in output if not isinstance(item, Report))
if len(true_output) == 1: if len(true_output) == 1:
@@ -943,7 +934,6 @@ def report_result(func):
else: else:
true_output = None true_output = None
return true_output return true_output
return wrapper return wrapper
@@ -962,11 +952,32 @@ def is_list_etc(object):
def create_holidays_for_year(year: int | None = None) -> List[date]: def create_holidays_for_year(year: int | None = None) -> List[date]:
def find_nth_monday(year, month, occurence: int | None = None, day: int | None = None): """
if not occurence: Gives stat holidays for the input year.
occurence = 1
Args:
year (int | None, optional): The input year as an integer. Defaults to None.
Returns:
List[date]
"""
def find_nth_monday(year, month, occurrence: int | None = None, day: int | None = None) -> date:
"""
Gets the nth (eg 2nd) monday of the given month.
Args:
year (int): The year the month occurs in.
month (int): The month of interest.
occurrence (int): The n in nth.
day (int): The day of the month to start after.
Returns:
date
"""
if not occurrence:
occurrence = 1
if not day: if not day:
day = occurence * 7 day = occurrence * 7
max_days = (date(2012, month + 1, 1) - date(2012, month, 1)).days max_days = (date(2012, month + 1, 1) - date(2012, month, 1)).days
if day > max_days: if day > max_days:
day = max_days day = max_days
@@ -977,17 +988,16 @@ def create_holidays_for_year(year: int | None = None) -> List[date]:
offset = -d.weekday() # weekday == 0 means Monday offset = -d.weekday() # weekday == 0 means Monday
output = d + timedelta(offset) output = d + timedelta(offset)
return output.date() return output.date()
if not year: if not year:
year = date.today().year year = date.today().year
# NOTE: Includes New Year's day for next year. # NOTE: Static holidays. Includes New Year's day for next year.
holidays = [date(year, 1, 1), date(year, 7, 1), date(year, 9, 30), holidays = [date(year, 1, 1), date(year, 7, 1), date(year, 9, 30),
date(year, 11, 11), date(year, 12, 25), date(year, 12, 26), date(year, 11, 11), date(year, 12, 25), date(year, 12, 26),
date(year + 1, 1, 1)] date(year + 1, 1, 1)]
# NOTE: Labour Day # NOTE: Labour Day
holidays.append(find_nth_monday(year, 9)) holidays.append(find_nth_monday(year, 9))
# NOTE: Thanksgiving # NOTE: Thanksgiving
holidays.append(find_nth_monday(year, 10, occurence=2)) holidays.append(find_nth_monday(year, 10, occurrence=2))
# NOTE: Victoria Day # NOTE: Victoria Day
holidays.append(find_nth_monday(year, 5, day=25)) holidays.append(find_nth_monday(year, 5, day=25))
# NOTE: Easter, etc # NOTE: Easter, etc
@@ -1007,7 +1017,6 @@ def check_dictionary_inclusion_equality(listo: List[dict] | dict, dicto: dict) -
Returns: Returns:
bool: True if dicto is equal to any dictionary in the list. bool: True if dicto is equal to any dictionary in the list.
""" """
# logger.debug(f"Comparing: {listo} and {dicto}")
if isinstance(dicto, list) and isinstance(listo, list): if isinstance(dicto, list) and isinstance(listo, list):
return listo == dicto return listo == dicto
elif isinstance(dicto, dict) and isinstance(listo, dict): elif isinstance(dicto, dict) and isinstance(listo, dict):
@@ -1018,22 +1027,43 @@ def check_dictionary_inclusion_equality(listo: List[dict] | dict, dicto: dict) -
raise TypeError(f"Unsupported variable: {type(listo)}") raise TypeError(f"Unsupported variable: {type(listo)}")
def flatten_list(input_list: list): def flatten_list(input_list: list) -> list:
"""
Takes nested lists and returns a single flat list.
Args:
input_list (list): input nested list.
Returns:
list:
"""
return list(itertools.chain.from_iterable(input_list)) return list(itertools.chain.from_iterable(input_list))
def sanitize_object_for_json(input_dict: dict) -> dict: def sanitize_object_for_json(input_dict: dict) -> dict | str:
"""
Takes an object and makes sure its components can be converted to JSON
Args:
input_dict (dict): Dictionary of interest
Returns:
dict:
"""
if not isinstance(input_dict, dict): if not isinstance(input_dict, dict):
match input_dict: match input_dict:
case int() | float() | bool(): case int() | float() | bool():
pass pass
case _: case _:
try: try:
js = json.dumps(input_dict) input_dict = json.dumps(input_dict)
except TypeError: except TypeError:
match input_dict:
case str():
pass
case _:
input_dict = str(input_dict) input_dict = str(input_dict)
return input_dict return input_dict.strip('\"')
# return input_dict
output = {} output = {}
for key, value in input_dict.items(): for key, value in input_dict.items():
match value: match value:
@@ -1041,22 +1071,43 @@ def sanitize_object_for_json(input_dict: dict) -> dict:
value = [sanitize_object_for_json(object) for object in value] value = [sanitize_object_for_json(object) for object in value]
case dict(): case dict():
value = sanitize_object_for_json(value) value = sanitize_object_for_json(value)
case _: case _:
try: try:
js = json.dumps(value) value = json.dumps(value)
except TypeError: except TypeError:
match value:
case str():
pass
case _:
value = str(value) value = str(value)
if isinstance(value, str):
value = value.strip('\"')
output[key] = value output[key] = value
return output return output
def create_plate_grid(rows: int, columns: int): def create_plate_grid(rows: int, columns: int) -> dict:
matrix = np.array([[0 for yyy in range(1, columns + 1)] for xxx in range(1, rows + 1)]) """
Makes an x by y array to represent a plate.
Args:
rows (int): Number of rows.
columns (int): Number of columns
Returns:
dict: cell number : (row, column)
"""
# NOTE: columns/rows
# matrix = np.array([[0 for yyy in range(1, columns + 1)] for xxx in range(1, rows + 1)])
# NOTE: rows/columns
matrix = np.array([[0 for xxx in range(1, rows + 1)] for yyy in range(1, columns + 1)])
return {iii: (item[0][1] + 1, item[0][0] + 1) for iii, item in enumerate(np.ndenumerate(matrix), start=1)} return {iii: (item[0][1] + 1, item[0][0] + 1) for iii, item in enumerate(np.ndenumerate(matrix), start=1)}
class classproperty(property): class classproperty(property):
"""
Allows for properties on classes as well as objects.
"""
def __get__(self, owner_self, owner_cls): def __get__(self, owner_self, owner_cls):
return self.fget(owner_cls) return self.fget(owner_cls)
@@ -1396,6 +1447,16 @@ class Settings(BaseSettings, extra="allow"):
@classmethod @classmethod
def get_alembic_db_path(cls, alembic_path, mode=Literal['path', 'schema', 'user', 'pass']) -> Path | str: def get_alembic_db_path(cls, alembic_path, mode=Literal['path', 'schema', 'user', 'pass']) -> Path | str:
"""
Retrieves database variables from alembic.ini file.
Args:
alembic_path (Any): Path of the alembic.ini file.
mode (Literal['path', 'schema', 'user', 'pass']): Variable of interest.
Returns:
Path | str
"""
c = ConfigParser() c = ConfigParser()
c.read(alembic_path) c.read(alembic_path)
url = c['alembic']['sqlalchemy.url'] url = c['alembic']['sqlalchemy.url']

View File

@@ -32,8 +32,10 @@ a = Analysis(
binaries=[], binaries=[],
datas=[ datas=[
("src\\config.yml", "files"), ("src\\config.yml", "files"),
("src\\submissions\\templates\\*", "files\\templates"), ("src\\submissions\\templates\\*.html", "files\\templates"),
("src\\submissions\\templates\\css\\*", "files\\templates\\css"), ("src\\submissions\\templates\\css\\*.css", "files\\templates\\css"),
("src\\submissions\\templates\\js\\*.js", "files\\templates\\js"),
("src\\submissions\\templates\\support\\*", "files\\templates\\support"),
("docs\\build", "files\\docs"), ("docs\\build", "files\\docs"),
("src\\submissions\\resources\\*", "files\\resources"), ("src\\submissions\\resources\\*", "files\\resources"),
("alembic.ini", "files"), ("alembic.ini", "files"),
@@ -51,12 +53,32 @@ a = Analysis(
) )
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
#exe = EXE(
# pyz,
# a.scripts,
# a.binaries,
# a.datas,
# [],
# name=f"{__project__}_{__version__}",
# debug=True,
# bootloader_ignore_signals=False,
# strip=False,
# upx=True,
# upx_exclude=[],
# runtime_tmpdir=None,
# console=True,
# disable_windowed_traceback=False,
# argv_emulation=False,
# target_arch=None,
# codesign_identity=None,
# entitlements_file=None,
#)
exe = EXE( exe = EXE(
pyz, pyz,
a.scripts, a.scripts,
[], [],
exclude_binaries=True, exclude_binaries=True,
name=f"{__project__}_{__version__}", name=f"{__project__}_{__version__}_2",
debug=True, debug=True,
bootloader_ignore_signals=False, bootloader_ignore_signals=False,
strip=False, strip=False,