Compare commits
10 Commits
b2225ef731
...
3862604dfa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3862604dfa | ||
|
|
11abaafcfc | ||
|
|
ba4912cab7 | ||
|
|
c9396d6c41 | ||
|
|
610859d84f | ||
|
|
c8b4762747 | ||
|
|
1a90543639 | ||
|
|
0c20ade65a | ||
|
|
b6e1c0dee2 | ||
|
|
fcda0d873c |
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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.")
|
||||||
return pydant.PydElastic
|
try:
|
||||||
|
model = getattr(pydant, f"Pyd{cls.pyd_model_name}")
|
||||||
|
except AttributeError:
|
||||||
|
return pydant.PydElastic
|
||||||
return model
|
return model
|
||||||
|
|
||||||
@classproperty
|
# @classproperty
|
||||||
|
@classmethod
|
||||||
|
@declared_attr
|
||||||
def add_edit_tooltips(cls) -> dict:
|
def add_edit_tooltips(cls) -> dict:
|
||||||
"""
|
"""
|
||||||
Gets tooltips for Omni-add-edit
|
Gets tooltips for Omni-add-edit
|
||||||
@@ -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
|
||||||
@@ -434,7 +443,7 @@ class BaseClass(Base):
|
|||||||
try:
|
try:
|
||||||
template = env.get_template(temp_name)
|
template = env.get_template(temp_name)
|
||||||
except TemplateNotFound as e:
|
except TemplateNotFound as e:
|
||||||
# logger.error(f"Couldn't find template {e}")
|
# logger.error(f"Couldn't find template {e}")
|
||||||
template = env.get_template("details.html")
|
template = env.get_template("details.html")
|
||||||
return template
|
return template
|
||||||
|
|
||||||
@@ -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:
|
||||||
return super().__setattr__(key, value)
|
try:
|
||||||
|
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
|
||||||
|
|||||||
@@ -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}>"
|
||||||
|
|||||||
@@ -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")
|
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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']]
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,19 +22,15 @@ 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
|
||||||
from .procedures import DefaultProcedureManager
|
from .procedures import DefaultProcedureManager
|
||||||
from .results import DefaultResultsManager
|
from .results import DefaultResultsManager
|
||||||
from .runs import DefaultRunManager
|
from .runs import DefaultRunManager
|
||||||
|
|||||||
@@ -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,24 +43,16 @@ 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)
|
||||||
return workbook
|
return workbook
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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__}")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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])
|
||||||
@@ -180,7 +185,7 @@ class RoleComboBox(QWidget):
|
|||||||
def toggle_checked(self):
|
def toggle_checked(self):
|
||||||
"""
|
"""
|
||||||
If this equipment is disabled, the input fields will be disabled.
|
If this equipment is disabled, the input fields will be disabled.
|
||||||
"""
|
"""
|
||||||
for widget in self.findChildren(QWidget):
|
for widget in self.findChildren(QWidget):
|
||||||
match widget:
|
match widget:
|
||||||
case QCheckBox():
|
case QCheckBox():
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()}."
|
||||||
|
|||||||
@@ -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__}")
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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())
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -453,4 +164,4 @@ class ClientSubmissionRunModel(QStandardItemModel):
|
|||||||
return item
|
return item
|
||||||
|
|
||||||
def edit_item(self):
|
def edit_item(self):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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'] %}
|
||||||
<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>
|
||||||
|
|||||||
@@ -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'] }};">
|
||||||
<p style="font-size: 0.7em; text-align: center; word-wrap: break-word;">{{ sample['sample_id'] }}</p>
|
{% if creation %}
|
||||||
|
<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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
@@ -534,11 +536,9 @@ def copy_cells(source_sheet, target_sheet):
|
|||||||
source_cell = cell
|
source_cell = cell
|
||||||
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,15 +546,13 @@ 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)
|
||||||
|
|
||||||
|
|
||||||
def list_str_comparator(input_str:str, listy: List[str], mode: Literal["starts_with", "contains"]) -> bool:
|
def list_str_comparator(input_str: str, listy: List[str], mode: Literal["starts_with", "contains"]) -> bool:
|
||||||
match mode:
|
match mode:
|
||||||
case "starts_with":
|
case "starts_with":
|
||||||
if any([input_str.startswith(item) for item in listy]):
|
if any([input_str.startswith(item) for item in listy]):
|
||||||
@@ -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:
|
||||||
input_dict = str(input_dict)
|
match input_dict:
|
||||||
return input_dict
|
case str():
|
||||||
# return input_dict
|
pass
|
||||||
|
case _:
|
||||||
|
input_dict = str(input_dict)
|
||||||
|
return input_dict.strip('\"')
|
||||||
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:
|
||||||
value = str(value)
|
match value:
|
||||||
|
case str():
|
||||||
|
pass
|
||||||
|
case _:
|
||||||
|
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)])
|
"""
|
||||||
return {iii: (item[0][1]+1, item[0][0]+1) for iii, item in enumerate(np.ndenumerate(matrix), start=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)}
|
||||||
|
|
||||||
|
|
||||||
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']
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user