Addition of turnaround time tracking.

This commit is contained in:
lwark
2024-12-04 12:11:10 -06:00
parent 37c5c1d3eb
commit cc53b894b2
14 changed files with 136 additions and 78 deletions

View File

@@ -69,6 +69,6 @@ def update_log(mapper, connection, target):
else:
logger.info(f"No changes detected, not updating logs.")
if ctx.database_schema == "sqlite":
event.listen(LogMixin, 'after_update', update_log, propagate=True)
event.listen(LogMixin, 'after_insert', update_log, propagate=True)
# if ctx.database_schema == "sqlite":
event.listen(LogMixin, 'after_update', update_log, propagate=True)
event.listen(LogMixin, 'after_insert', update_log, propagate=True)

View File

@@ -279,7 +279,7 @@ class Control(BaseClass):
@classmethod
def make_parent_buttons(cls, parent: QWidget) -> None:
"""
Super that will make buttons in a CustomFigure. Made to be overrided.
Super that will make buttons in a CustomFigure. Made to be overridden.
Args:
parent (QWidget): chart holding widget to add buttons to.
@@ -299,6 +299,10 @@ class Control(BaseClass):
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.
@@ -348,7 +352,7 @@ class PCRControl(Control):
df = df[df.ct > 0.0]
except AttributeError:
df = df
fig = PCRFigure(df=df, modes=[])
fig = PCRFigure(df=df, modes=[], settings=chart_settings)
return report, fig
def to_pydantic(self):
@@ -433,12 +437,12 @@ class IridaControl(Control):
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 for controls graphs
split this instance into analysis types ('kraken', 'matches', 'contains') for controls graphs
Args:
consolidate (bool): whether to merge all off-target genera. Defaults to False
control_sub_type (str): control subtype, 'MCS-NOS', etc.
mode (str): analysis type, 'contains', etc.
mode (Literal['kraken', 'matches', 'contains']): analysis type, 'contains', etc.
Returns:
List[dict]: list of records
@@ -562,7 +566,7 @@ class IridaControl(Control):
df, modes = cls.prep_df(ctx=ctx, df=df)
# logger.debug(f"prepped df: \n {df}")
fig = IridaFigure(df=df, ytitle=title, modes=modes, parent=parent,
months=chart_settings['months'])
settings=chart_settings)
return report, fig
@classmethod
@@ -571,9 +575,8 @@ class IridaControl(Control):
Convert list of control records to dataframe
Args:
ctx (dict): settings passed from gui
input_df (list[dict]): list of dictionaries containing records
sub_type (str | None, optional): sub_type of submission type. Defaults to None.
sub_mode (str | None, optional): sub_type of submission type. Defaults to None.
Returns:
DataFrame: dataframe of controls

View File

@@ -168,7 +168,6 @@ class KitType(BaseClass):
else:
return (item.reagent_role for item in relevant_associations)
# TODO: Move to BasicSubmission?
def construct_xl_map_for_use(self, submission_type: str | SubmissionType) -> Generator[(str, str), None, None]:
"""
Creates map of locations in Excel workbook for a SubmissionType
@@ -274,8 +273,7 @@ class KitType(BaseClass):
for kk, vv in assoc.to_export_dict().items():
v[kk] = vv
base_dict['reagent roles'].append(v)
# for k, v in submission_type.construct_equipment_map():
for k, v in submission_type.contstruct_field_map("equipment"):
for k, v in submission_type.construct_field_map("equipment"):
try:
assoc = next(item for item in submission_type.submissiontype_equipmentrole_associations if
item.equipment_role.name == k)
@@ -428,7 +426,7 @@ class Reagent(BaseClass, LogMixin):
submission=sub)) #: Association proxy to SubmissionSampleAssociation.samples
def __repr__(self):
if self.name is not None:
if self.name:
return f"<Reagent({self.name}-{self.lot})>"
else:
return f"<Reagent({self.role.name}-{self.lot})>"
@@ -447,11 +445,12 @@ class Reagent(BaseClass, LogMixin):
if extraction_kit is not None:
# NOTE: Get the intersection of this reagent's ReagentType and all ReagentTypes in KitType
try:
reagent_role = list(set(self.role).intersection(extraction_kit.reagent_roles))[0]
# NOTE: Most will be able to fall back to first ReagentType in itself because most will only have 1.
except:
reagent_role = self.role[0]
reagent_role = next((item for item in set(self.role).intersection(extraction_kit.reagent_roles)), self.role[0])
# try:
# reagent_role = list(set(self.role).intersection(extraction_kit.reagent_roles))[0]
# # NOTE: Most will be able to fall back to first ReagentType in itself because most will only have 1.
# except:
# reagent_role = self.role[0]
else:
try:
reagent_role = self.role[0]

View File

@@ -24,7 +24,7 @@ from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as S
from openpyxl import Workbook
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
report_result, create_holidays_for_year, ctx
from datetime import datetime, date, timedelta
from typing import List, Any, Tuple, Literal, Generator
from dateutil.parser import parse
@@ -127,7 +127,7 @@ class BasicSubmission(BaseClass, LogMixin):
def __repr__(self) -> str:
submission_type = self.submission_type or "Basic"
return f"<{submission_type}Submission({self.rsl_plate_num})>"
return f"<Submission({self.rsl_plate_num})>"
@classmethod
def jsons(cls) -> List[str]:
@@ -1380,17 +1380,22 @@ class BasicSubmission(BaseClass, LogMixin):
writer = pyd.to_writer()
writer.xl.save(filename=fname.with_suffix(".xlsx"))
def get_turnaround_time(self):
completed = self.completed_date or datetime.now()
return self.calculate_turnaround(start_date=self.submitted_date.date(), end_date=completed.date())
def get_turnaround_time(self) -> Tuple[int|None, bool|None]:
try:
completed = self.completed_date.date()
except AttributeError:
completed = None
return self.calculate_turnaround(start_date=self.submitted_date.date(), end_date=completed)
@classmethod
def calculate_turnaround(cls, start_date:date|None=None, end_date:date|None=None) -> int|None:
def calculate_turnaround(cls, start_date:date|None=None, end_date:date|None=None) -> Tuple[int|None, bool|None]:
if not end_date:
return None, None
try:
delta = np.busday_count(start_date, end_date, holidays=create_holidays_for_year(start_date.year))
delta = np.busday_count(start_date, end_date, holidays=create_holidays_for_year(start_date.year)) + 1
except ValueError:
return None
return delta + 1
return None, None
return delta, delta <= ctx.TaT_threshold
# Below are the custom submission types

View File

@@ -293,20 +293,24 @@ class ReagentParser(object):
report = Report()
if isinstance(submission_type, dict):
submission_type = submission_type['value']
if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type)
reagent_map = {k: v for k, v in self.kit_object.construct_xl_map_for_use(submission_type)}
try:
del reagent_map['info']
except KeyError:
pass
# logger.debug(f"Reagent map: {pformat(reagent_map)}")
logger.debug(f"Reagent map: {pformat(reagent_map)}")
# NOTE: If reagent map is empty, maybe the wrong kit was given, check if there's only one kit for that submission type and use it if so.
if not reagent_map.keys():
if not reagent_map:
temp_kit_object = self.submission_type_obj.get_default_kit()
logger.debug(f"Temp kit: {temp_kit_object}")
if temp_kit_object:
self.kit_object = temp_kit_object
reagent_map = {k: v for k, v in self.kit_object.construct_xl_map_for_use(submission_type)}
logger.warning(f"Attempting to salvage {self.kit_object} with default kit map: {reagent_map}")
if not reagent_map.keys():
# reagent_map = {k: v for k, v in self.kit_object.construct_xl_map_for_use(submission_type)}
logger.warning(f"Attempting to salvage with default kit {self.kit_object} and submission_type: {self.submission_type_obj}")
return self.fetch_kit_info_map(submission_type=self.submission_type_obj)
else:
logger.error(f"Still no reagent map, displaying error.")
try:
ext_kit_loc = self.submission_type_obj.info_map['extraction_kit']['read'][0]

View File

@@ -1,6 +1,8 @@
'''
Contains functions for generating summary reports
'''
from pprint import pformat
from pandas import DataFrame, ExcelWriter
import logging
from pathlib import Path
@@ -137,3 +139,35 @@ class ReportMaker(object):
for cell in worksheet['D']:
if cell.row > 1:
cell.style = 'Currency'
class TurnaroundMaker(object):
def __init__(self, start_date: date, end_date: date):
self.start_date = start_date
self.end_date = end_date
# NOTE: Set page size to zero to override limiting query size.
self.subs = BasicSubmission.query(start_date=start_date, end_date=end_date, page_size=0)
records = [self.build_record(sub) for sub in self.subs]
self.df = DataFrame.from_records(records)
@classmethod
def build_record(cls, sub):
days, tat_ok = sub.get_turnaround_time()
return dict(name=sub.rsl_plate_num, days=days, submitted_date=sub.submitted_date,
completed_date=sub.completed_date, acceptable=tat_ok)
def write_report(self, filename: Path | str, obj: QWidget | None = None):
"""
Writes info to files.
Args:
filename (Path | str): Basename of output file
obj (QWidget | None, optional): Parent object. Defaults to None.
"""
if isinstance(filename, str):
filename = Path(filename)
filename = filename.absolute()
self.writer = ExcelWriter(filename.with_suffix(".xlsx"), engine='openpyxl')
self.df.to_excel(self.writer, sheet_name="Turnaround")
# logger.debug(f"Writing report to: {filename}")
self.writer.close()