Moments before disaster.

This commit is contained in:
lwark
2024-12-06 12:02:39 -06:00
parent 5fc02ffeec
commit 80527355d1
23 changed files with 157 additions and 325 deletions

View File

@@ -3,4 +3,5 @@ database_path: null
database_schema: null database_schema: null
database_user: null database_user: null
database_password: null database_password: null
database_name: null database_name: null
logging_enabled: false

View File

@@ -1,10 +1,8 @@
""" """
All database related operations. All database related operations.
""" """
import sqlalchemy.orm
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
@@ -48,7 +46,11 @@ def update_log(mapper, connection, target):
hist = attr.load_history() hist = attr.load_history()
if not hist.has_changes(): if not hist.has_changes():
continue continue
if attr.key == "custom":
continue
added = [str(item) for item in hist.added] added = [str(item) for item in hist.added]
if attr.key in ['submission_sample_associations', 'submission_reagent_associations']:
added = ['Numbers truncated for space purposes.']
deleted = [str(item) for item in hist.deleted] deleted = [str(item) for item in hist.deleted]
change = dict(field=attr.key, added=added, deleted=deleted) change = dict(field=attr.key, added=added, deleted=deleted)
# logger.debug(f"Adding: {pformat(change)}") # logger.debug(f"Adding: {pformat(change)}")
@@ -69,6 +71,6 @@ def update_log(mapper, connection, target):
else: else:
logger.info(f"No changes detected, not updating logs.") logger.info(f"No changes detected, not updating logs.")
# if ctx.database_schema == "sqlite": # if ctx.logging_enabled:
event.listen(LogMixin, 'after_update', update_log, propagate=True) event.listen(LogMixin, 'after_update', update_log, propagate=True)
event.listen(LogMixin, 'after_insert', update_log, propagate=True) event.listen(LogMixin, 'after_insert', update_log, propagate=True)

View File

@@ -1414,25 +1414,6 @@ class Equipment(BaseClass):
if extraction_kit and extraction_kit not in process.kit_types: if extraction_kit and extraction_kit not in process.kit_types:
continue continue
yield process yield process
# processes = (process for process in self.processes if submission_type in process.submission_types)
# match extraction_kit:
# case str():
# # logger.debug(f"Filtering processes by extraction_kit str {extraction_kit}")
# processes = (process for process in processes if
# extraction_kit in [kit.name for kit in process.kit_types])
# case KitType():
# # logger.debug(f"Filtering processes by extraction_kit KitType {extraction_kit}")
# processes = (process for process in processes if extraction_kit in process.kit_types)
# case _:
# pass
# # NOTE: Convert to strings
# # processes = [process.name for process in processes]
# # assert all([isinstance(process, str) for process in processes])
# # if len(processes) == 0:
# # processes = ['']
# # return processes
# for process in processes:
# yield process.name
@classmethod @classmethod
@setup_lookup @setup_lookup
@@ -1650,25 +1631,6 @@ class EquipmentRole(BaseClass):
if extraction_kit and extraction_kit not in process.kit_types: if extraction_kit and extraction_kit not in process.kit_types:
continue continue
yield process.name yield process.name
# if submission_type is not None:
# # logger.debug("Getting all processes for this EquipmentRole")
# processes = [process for process in self.processes if submission_type in process.submission_types]
# else:
# processes = self.processes
# match extraction_kit:
# case str():
# # logger.debug(f"Filtering processes by extraction_kit str {extraction_kit}")
# processes = [item for item in processes if extraction_kit in [kit.name for kit in item.kit_types]]
# case KitType():
# # logger.debug(f"Filtering processes by extraction_kit KitType {extraction_kit}")
# processes = [item for item in processes if extraction_kit in [kit for kit in item.kit_types]]
# case _:
# pass
# output = [item.name for item in processes]
# if len(output) == 0:
# return ['']
# else:
# return output
def to_export_dict(self, submission_type: SubmissionType, kit_type: KitType): def to_export_dict(self, submission_type: SubmissionType, kit_type: KitType):
""" """
@@ -1730,9 +1692,8 @@ class SubmissionEquipmentAssociation(BaseClass):
@classmethod @classmethod
@setup_lookup @setup_lookup
def query(cls, equipment_id: int, submission_id: int, role: str | None = None, limit: int = 0, **kwargs) -> Any | \ def query(cls, equipment_id: int, submission_id: int, role: str | None = None, limit: int = 0, **kwargs) \
List[ -> Any | List[Any]:
Any]:
query: Query = cls.__database_session__.query(cls) query: Query = cls.__database_session__.query(cls)
query = query.filter(cls.equipment_id == equipment_id) query = query.filter(cls.equipment_id == equipment_id)
query = query.filter(cls.submission_id == submission_id) query = query.filter(cls.submission_id == submission_id)
@@ -1777,44 +1738,22 @@ class SubmissionTypeEquipmentRoleAssociation(BaseClass):
raise ValueError(f'Invalid required value {value}. Must be 0 or 1.') raise ValueError(f'Invalid required value {value}. Must be 0 or 1.')
return value return value
def get_all_processes(self, extraction_kit: KitType | str | None = None) -> List[Process]:
"""
Get all processes associated with this SubmissionTypeEquipmentRole
Args:
extraction_kit (KitType | str | None, optional): KitType of interest. Defaults to None.
Returns:
List[Process]: All associated processes
"""
processes = [equipment.get_processes(self.submission_type) for equipment in self.equipment_role.instances]
# NOTE: flatten list
processes = [item for items in processes for item in items if item is not None]
match extraction_kit:
case str():
# logger.debug(f"Filtering Processes by extraction_kit str {extraction_kit}")
processes = [item for item in processes if extraction_kit in [kit.name for kit in item.kit_type]]
case KitType():
# logger.debug(f"Filtering Processes by extraction_kit KitType {extraction_kit}")
processes = [item for item in processes if extraction_kit in [kit for kit in item.kit_type]]
case _:
pass
return processes
@check_authorization @check_authorization
def save(self): def save(self):
super().save() super().save()
def to_export_dict(self, extraction_kit: KitType) -> dict: def to_export_dict(self, extraction_kit: KitType | str) -> dict:
""" """
Creates dictionary for exporting to yml used in new SubmissionType Construction Creates dictionary for exporting to yml used in new SubmissionType Construction
Args: Args:
kit_type (KitType): KitType of interest. extraction_kit (KitType | str): KitType of interest.
Returns: Returns:
dict: Dictionary containing relevant info for SubmissionType construction dict: Dictionary containing relevant info for SubmissionType construction
""" """
if isinstance(extraction_kit, str):
extraction_kit = KitType.query(name=extraction_kit)
base_dict = {k: v for k, v in self.equipment_role.to_export_dict(submission_type=self.submission_type, base_dict = {k: v for k, v in self.equipment_role.to_export_dict(submission_type=self.submission_type,
kit_type=extraction_kit).items()} kit_type=extraction_kit).items()}
base_dict['static'] = self.static base_dict['static'] = self.static
@@ -2013,8 +1952,8 @@ class SubmissionTipsAssociation(BaseClass):
@classmethod @classmethod
@setup_lookup @setup_lookup
def query(cls, tip_id: int, role: str, submission_id: int | None = None, limit: int = 0, **kwargs) -> Any | List[ def query(cls, tip_id: int, role: str, submission_id: int | None = None, limit: int = 0, **kwargs) \
Any]: -> Any | List[Any]:
query: Query = cls.__database_session__.query(cls) query: Query = cls.__database_session__.query(cls)
query = query.filter(cls.tip_id == tip_id) query = query.filter(cls.tip_id == tip_id)
if submission_id is not None: if submission_id is not None:

View File

@@ -2,13 +2,10 @@
Models for the main submission and sample types. Models for the main submission and sample types.
""" """
from __future__ import annotations from __future__ import annotations
# import sys
# import types
# import zipfile
from copy import deepcopy 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, uuid, tempfile, re, base64, numpy as np, pandas as pd, types, sys
from zipfile import ZipFile from zipfile import ZipFile, BadZipfile
from tempfile import TemporaryDirectory, TemporaryFile from tempfile import TemporaryDirectory, TemporaryFile
from operator import itemgetter from operator import itemgetter
from pprint import pformat from pprint import pformat
@@ -20,7 +17,6 @@ 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 ArgumentError
from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError
# import pandas as pd
from openpyxl import Workbook from openpyxl import Workbook
from openpyxl.drawing.image import Image as OpenpyxlImage 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, \ from tools import row_map, setup_lookup, jinja_template_loading, rreplace, row_keys, check_key_or_attr, Result, Report, \
@@ -1276,7 +1272,7 @@ class BasicSubmission(BaseClass, LogMixin):
if msg.exec(): if msg.exec():
try: try:
self.backup(fname=fname, full_backup=True) self.backup(fname=fname, full_backup=True)
except zipfile.BadZipfile: except BadZipfile:
logger.error("Couldn't open zipfile for writing.") logger.error("Couldn't open zipfile for writing.")
self.__database_session__.delete(self) self.__database_session__.delete(self)
try: try:
@@ -2261,7 +2257,7 @@ class WastewaterArtic(BasicSubmission):
# Sample Classes # Sample Classes
class BasicSample(BaseClass): class BasicSample(BaseClass, LogMixin):
""" """
Base of basic sample which polymorphs into BCSample and WWSample Base of basic sample which polymorphs into BCSample and WWSample
""" """

View File

@@ -1,8 +1,7 @@
''' '''
contains parser objects for pulling values from client generated submission sheets. contains parser objects for pulling values from client generated submission sheets.
''' '''
import json import logging
import sys
from copy import copy from copy import copy
from getpass import getuser from getpass import getuser
from pprint import pformat from pprint import pformat
@@ -11,7 +10,6 @@ from openpyxl import load_workbook, Workbook
from pathlib import Path from pathlib import Path
from backend.db.models import * from backend.db.models import *
from backend.validators import PydSubmission, RSLNamer from backend.validators import PydSubmission, RSLNamer
import logging, re
from collections import OrderedDict from collections import OrderedDict
from tools import check_not_nan, is_missing, check_key_or_attr from tools import check_not_nan, is_missing, check_key_or_attr
@@ -195,7 +193,7 @@ class InfoParser(object):
ws = self.xl[sheet] ws = self.xl[sheet]
relevant = [] relevant = []
for k, v in self.map.items(): for k, v in self.map.items():
# NOTE: If the value is hardcoded put it in the dictionary directly. # NOTE: If the value is hardcoded put it in the dictionary directly. Ex. Artic kit
if k == "custom": if k == "custom":
continue continue
if isinstance(v, str): if isinstance(v, str):
@@ -230,7 +228,7 @@ class InfoParser(object):
case "submitted_date": case "submitted_date":
value, missing = is_missing(value) value, missing = is_missing(value)
logger.debug(f"Parsed submitted date: {value}") logger.debug(f"Parsed submitted date: {value}")
# NOTE: is field a JSON? # NOTE: is field a JSON? Includes: Extraction info, PCR info, comment, custom
case thing if thing in self.sub_object.jsons(): case thing if thing in self.sub_object.jsons():
value, missing = is_missing(value) value, missing = is_missing(value)
if missing: continue if missing: continue
@@ -300,11 +298,11 @@ class ReagentParser(object):
del reagent_map['info'] del reagent_map['info']
except KeyError: except KeyError:
pass 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. # 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: if not reagent_map:
temp_kit_object = self.submission_type_obj.get_default_kit() temp_kit_object = self.submission_type_obj.get_default_kit()
logger.debug(f"Temp kit: {temp_kit_object}") # logger.debug(f"Temp kit: {temp_kit_object}")
if temp_kit_object: if temp_kit_object:
self.kit_object = 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)} # reagent_map = {k: v for k, v in self.kit_object.construct_xl_map_for_use(submission_type)}
@@ -333,7 +331,7 @@ class ReagentParser(object):
for sheet in self.xl.sheetnames: for sheet in self.xl.sheetnames:
ws = self.xl[sheet] ws = self.xl[sheet]
relevant = {k.strip(): v for k, v in self.map.items() if sheet in self.map[k]['sheet']} relevant = {k.strip(): v for k, v in self.map.items() if sheet in self.map[k]['sheet']}
logger.debug(f"relevant map for {sheet}: {pformat(relevant)}") # logger.debug(f"relevant map for {sheet}: {pformat(relevant)}")
if relevant == {}: if relevant == {}:
continue continue
for item in relevant: for item in relevant:
@@ -499,8 +497,7 @@ class SampleParser(object):
yield new yield new
else: else:
merge_on_id = self.sample_info_map['lookup_table']['merge_on_id'] merge_on_id = self.sample_info_map['lookup_table']['merge_on_id']
# plate_map_samples = sorted(copy(self.plate_map_samples), key=lambda d: d['id']) logger.info(f"Merging sample info using {merge_on_id}")
# lookup_samples = sorted(copy(self.lookup_samples), key=lambda d: d[merge_on_id])
plate_map_samples = sorted(copy(self.plate_map_samples), key=itemgetter('id')) plate_map_samples = sorted(copy(self.plate_map_samples), key=itemgetter('id'))
lookup_samples = sorted(copy(self.lookup_samples), key=itemgetter(merge_on_id)) lookup_samples = sorted(copy(self.lookup_samples), key=itemgetter(merge_on_id))
for ii, psample in enumerate(plate_map_samples): for ii, psample in enumerate(plate_map_samples):
@@ -517,20 +514,9 @@ class SampleParser(object):
logger.warning(f"Match for {psample['id']} not direct, running search.") logger.warning(f"Match for {psample['id']} not direct, running search.")
searchables = [(jj, sample) for jj, sample in enumerate(lookup_samples) searchables = [(jj, sample) for jj, sample in enumerate(lookup_samples)
if merge_on_id in sample.keys()] if merge_on_id in sample.keys()]
# for jj, lsample in enumerate(lookup_samples):
# try:
# check = lsample[merge_on_id] == psample['id']
# except KeyError:
# check = False
# if check:
# new = lsample | psample
# lookup_samples[jj] = {}
# break
# else:
# new = psample
jj, new = next(((jj, lsample | psample) for jj, lsample in searchables jj, new = next(((jj, lsample | psample) for jj, lsample in searchables
if lsample[merge_on_id] == psample['id']), (-1, psample)) if lsample[merge_on_id] == psample['id']), (-1, psample))
logger.debug(f"Assigning from index {jj} - {new}") # logger.debug(f"Assigning from index {jj} - {new}")
if jj >= 0: if jj >= 0:
lookup_samples[jj] = {} lookup_samples[jj] = {}
if not check_key_or_attr(key='submitter_id', interest=new, check_none=True): if not check_key_or_attr(key='submitter_id', interest=new, check_none=True):
@@ -554,7 +540,7 @@ class EquipmentParser(object):
xl (Workbook): Openpyxl workbook from submitted excel file. xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (str | SubmissionType): Type of submission expected (Wastewater, Bacterial Culture, etc.) submission_type (str | SubmissionType): Type of submission expected (Wastewater, Bacterial Culture, etc.)
""" """
logger.debug("\n\nHello from EquipmentParser!\n\n") logger.info("\n\nHello from EquipmentParser!\n\n")
if isinstance(submission_type, str): if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type) submission_type = SubmissionType.query(name=submission_type)
self.submission_type = submission_type self.submission_type = submission_type
@@ -568,7 +554,6 @@ class EquipmentParser(object):
Returns: Returns:
List[dict]: List of locations List[dict]: List of locations
""" """
# return {k: v for k, v in self.submission_type.construct_equipment_map()}
return {k: v for k, v in self.submission_type.construct_field_map("equipment")} return {k: v for k, v in self.submission_type.construct_field_map("equipment")}
def get_asset_number(self, input: str) -> str: def get_asset_number(self, input: str) -> str:
@@ -638,7 +623,7 @@ class TipParser(object):
xl (Workbook): Openpyxl workbook from submitted excel file. xl (Workbook): Openpyxl workbook from submitted excel file.
submission_type (str | SubmissionType): Type of submission expected (Wastewater, Bacterial Culture, etc.) submission_type (str | SubmissionType): Type of submission expected (Wastewater, Bacterial Culture, etc.)
""" """
logger.debug("\n\nHello from TipParser!\n\n") logger.info("\n\nHello from TipParser!\n\n")
if isinstance(submission_type, str): if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type) submission_type = SubmissionType.query(name=submission_type)
self.submission_type = submission_type self.submission_type = submission_type
@@ -652,7 +637,6 @@ class TipParser(object):
Returns: Returns:
List[dict]: List of locations List[dict]: List of locations
""" """
# return {k: v for k, v in self.submission_type.construct_tips_map()}
return {k: v for k, v in self.submission_type.construct_field_map("tip")} return {k: v for k, v in self.submission_type.construct_field_map("tip")}
def parse_tips(self) -> List[dict]: def parse_tips(self) -> List[dict]:

View File

@@ -2,7 +2,6 @@
Contains functions for generating summary reports Contains functions for generating summary reports
''' '''
from pprint import pformat from pprint import pformat
from pandas import DataFrame, ExcelWriter from pandas import DataFrame, ExcelWriter
import logging import logging
from pathlib import Path from pathlib import Path
@@ -72,7 +71,6 @@ class ReportMaker(object):
for row in df.iterrows(): for row in df.iterrows():
# logger.debug(f"Row {ii}: {row}") # logger.debug(f"Row {ii}: {row}")
lab = row[0][0] lab = row[0][0]
# logger.debug(type(row))
# logger.debug(f"Old lab: {old_lab}, Current lab: {lab}") # logger.debug(f"Old lab: {old_lab}, Current lab: {lab}")
# logger.debug(f"Name: {row[0][1]}") # logger.debug(f"Name: {row[0][1]}")
data = [item for item in row[1]] data = [item for item in row[1]]
@@ -151,7 +149,16 @@ class TurnaroundMaker(object):
self.df = DataFrame.from_records(records) self.df = DataFrame.from_records(records)
@classmethod @classmethod
def build_record(cls, sub): def build_record(cls, sub: BasicSubmission) -> dict:
"""
Build a turnaround dictionary from a submission
Args:
sub (BasicSubmission): The submission to be processed.
Returns:
"""
days, tat_ok = sub.get_turnaround_time() days, tat_ok = sub.get_turnaround_time()
return dict(name=str(sub.rsl_plate_num), days=days, submitted_date=sub.submitted_date, return dict(name=str(sub.rsl_plate_num), days=days, submitted_date=sub.submitted_date,
completed_date=sub.completed_date, acceptable=tat_ok) completed_date=sub.completed_date, acceptable=tat_ok)
@@ -170,4 +177,4 @@ class TurnaroundMaker(object):
self.writer = ExcelWriter(filename.with_suffix(".xlsx"), engine='openpyxl') self.writer = ExcelWriter(filename.with_suffix(".xlsx"), engine='openpyxl')
self.df.to_excel(self.writer, sheet_name="Turnaround") self.df.to_excel(self.writer, sheet_name="Turnaround")
# logger.debug(f"Writing report to: {filename}") # logger.debug(f"Writing report to: {filename}")
self.writer.close() self.writer.close()

View File

@@ -41,21 +41,11 @@ class SheetWriter(object):
self.sub[k] = v['value'] self.sub[k] = v['value']
else: else:
self.sub[k] = v self.sub[k] = v
# logger.debug(f"\n\nWriting to {submission.filepath.__str__()}\n\n")
# if self.filepath.stem.startswith("tmp"):
# template = self.submission_type.template_file
# workbook = load_workbook(BytesIO(template))
# else:
# try:
# workbook = load_workbook(self.filepath)
# except Exception as e:
# logger.error(f"Couldn't open workbook due to {e}")
template = self.submission_type.template_file template = self.submission_type.template_file
if not template: if not template:
logger.error(f"No template file found, falling back to Bacterial Culture") logger.error(f"No template file found, falling back to Bacterial Culture")
template = SubmissionType.retrieve_template_file() template = SubmissionType.retrieve_template_file()
workbook = load_workbook(BytesIO(template)) workbook = load_workbook(BytesIO(template))
# self.workbook = workbook
self.xl = workbook self.xl = workbook
self.write_info() self.write_info()
self.write_reagents() self.write_reagents()
@@ -152,11 +142,9 @@ class InfoWriter(object):
try: try:
dicto['locations'] = info_map[k] dicto['locations'] = info_map[k]
except KeyError: except KeyError:
# continue
pass pass
dicto['value'] = v dicto['value'] = v
if len(dicto) > 0: if len(dicto) > 0:
# output[k] = dicto
yield k, dicto yield k, dicto
def write_info(self) -> Workbook: def write_info(self) -> Workbook:
@@ -279,7 +267,6 @@ class SampleWriter(object):
self.sample_map = submission_type.construct_sample_map()['lookup_table'] self.sample_map = submission_type.construct_sample_map()['lookup_table']
# NOTE: exclude any samples without a submission rank. # NOTE: exclude any samples without a submission rank.
samples = [item for item in self.reconcile_map(sample_list) if item['submission_rank'] > 0] samples = [item for item in self.reconcile_map(sample_list) if item['submission_rank'] > 0]
# self.samples = sorted(samples, key=lambda k: k['submission_rank'])
self.samples = sorted(samples, key=itemgetter('submission_rank')) self.samples = sorted(samples, key=itemgetter('submission_rank'))
def reconcile_map(self, sample_list: list) -> Generator[dict, None, None]: def reconcile_map(self, sample_list: list) -> Generator[dict, None, None]:
@@ -368,7 +355,8 @@ class EquipmentWriter(object):
logger.error(f"No {equipment['role']} in {pformat(equipment_map)}") logger.error(f"No {equipment['role']} in {pformat(equipment_map)}")
# logger.debug(f"{equipment['role']} map: {mp_info}") # logger.debug(f"{equipment['role']} map: {mp_info}")
placeholder = copy(equipment) placeholder = copy(equipment)
if mp_info == {}: # if mp_info == {}:
if not mp_info:
for jj, (k, v) in enumerate(equipment.items(), start=1): for jj, (k, v) in enumerate(equipment.items(), start=1):
dicto = dict(value=v, row=ii, column=jj) dicto = dict(value=v, row=ii, column=jj)
placeholder[k] = dicto placeholder[k] = dicto

View File

@@ -2,8 +2,7 @@
Contains pydantic models and accompanying validators Contains pydantic models and accompanying validators
''' '''
from __future__ import annotations from __future__ import annotations
import sys import uuid, re, logging, csv, sys
import uuid, re, logging, csv
from pydantic import BaseModel, field_validator, Field, model_validator from pydantic import BaseModel, field_validator, Field, model_validator
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from dateutil.parser import parse from dateutil.parser import parse
@@ -165,13 +164,7 @@ class PydReagent(BaseModel):
report.add_result(Result(owner=__name__, code=0, msg="New reagent created.", status="Information")) report.add_result(Result(owner=__name__, code=0, msg="New reagent created.", status="Information"))
else: else:
if submission is not None and reagent not in submission.reagents: if submission is not None and reagent not in submission.reagents:
# assoc = SubmissionReagentAssociation(reagent=reagent, submission=submission)
# assoc.comments = self.comment
submission.update_reagentassoc(reagent=reagent, role=self.role) submission.update_reagentassoc(reagent=reagent, role=self.role)
# else:
# assoc = None
# add end-of-life extension from reagent type to expiry date
# NOTE: this will now be done only in the reporting phase to account for potential changes in end-of-life extensions
return reagent, report return reagent, report
@@ -191,11 +184,7 @@ class PydSample(BaseModel, extra='allow'):
for k, v in data.model_extra.items(): for k, v in data.model_extra.items():
if k in model.timestamps(): if k in model.timestamps():
if isinstance(v, str): if isinstance(v, str):
# try:
v = datetime.strptime(v, "%Y-%m-%d") v = datetime.strptime(v, "%Y-%m-%d")
# except ValueError:
# logger.warning(f"Attribute {k} value {v} for sample {data.submitter_id} could not be coerced into date. Setting to None.")
# v = None
data.__setattr__(k, v) data.__setattr__(k, v)
# logger.debug(f"Data coming out of validation: {pformat(data)}") # logger.debug(f"Data coming out of validation: {pformat(data)}")
return data return data
@@ -379,7 +368,6 @@ class PydEquipment(BaseModel, extra='ignore'):
role=self.role, limit=1) role=self.role, limit=1)
except TypeError as e: except TypeError as e:
logger.error(f"Couldn't get association due to {e}, returning...") logger.error(f"Couldn't get association due to {e}, returning...")
# return equipment, None
assoc = None assoc = None
if assoc is None: if assoc is None:
assoc = SubmissionEquipmentAssociation(submission=submission, equipment=equipment) assoc = SubmissionEquipmentAssociation(submission=submission, equipment=equipment)
@@ -830,11 +818,6 @@ class PydSubmission(BaseModel, extra='allow'):
logger.debug(f"Checking reagent {reagent.lot}") logger.debug(f"Checking reagent {reagent.lot}")
reagent, _ = reagent.toSQL(submission=instance) reagent, _ = reagent.toSQL(submission=instance)
# logger.debug(f"Association: {assoc}") # logger.debug(f"Association: {assoc}")
# if assoc is not None: # and assoc not in instance.submission_reagent_associations:
# if assoc not in instance.submission_reagent_associations:
# instance.submission_reagent_associations.append(assoc)
# else:
# logger.warning(f"Reagent association {assoc} is already present in {instance.submission_reagent_associations}")
case "samples": case "samples":
for sample in self.samples: for sample in self.samples:
sample, associations, _ = sample.toSQL(submission=instance) sample, associations, _ = sample.toSQL(submission=instance)
@@ -871,7 +854,6 @@ class PydSubmission(BaseModel, extra='allow'):
logger.warning(f"Tips association {association} is already present in {instance}") logger.warning(f"Tips association {association} is already present in {instance}")
case item if item in instance.timestamps(): case item if item in instance.timestamps():
logger.warning(f"Incoming timestamp key: {item}, with value: {value}") logger.warning(f"Incoming timestamp key: {item}, with value: {value}")
# value = value.replace(tzinfo=timezone)
if isinstance(value, date): if isinstance(value, date):
value = datetime.combine(value, datetime.min.time()) value = datetime.combine(value, datetime.min.time())
value = value.replace(tzinfo=timezone) value = value.replace(tzinfo=timezone)
@@ -903,7 +885,6 @@ class PydSubmission(BaseModel, extra='allow'):
if check: if check:
try: try:
instance.set_attribute(key=key, value=value) instance.set_attribute(key=key, value=value)
# instance.update({key:value})
except AttributeError as e: except AttributeError as e:
logger.error(f"Could not set attribute: {key} to {value} due to: \n\n {e}") logger.error(f"Could not set attribute: {key} to {value} due to: \n\n {e}")
continue continue

View File

@@ -40,7 +40,6 @@ class IridaFigure(CustomFigure):
Returns: Returns:
Figure: output stacked bar chart. Figure: output stacked bar chart.
""" """
# fig = Figure()
for ii, mode in enumerate(modes): for ii, mode in enumerate(modes):
if "count" in mode: if "count" in mode:
df[mode] = pd.to_numeric(df[mode], errors='coerce') df[mode] = pd.to_numeric(df[mode], errors='coerce')

View File

@@ -1,8 +1,10 @@
"""
Construct turnaround time charts
"""
from pprint import pformat from pprint import pformat
from . import CustomFigure from . import CustomFigure
import plotly.express as px import plotly.express as px
import pandas as pd import pandas as pd
import numpy as np
from PyQt6.QtWidgets import QWidget from PyQt6.QtWidgets import QWidget
import logging import logging
@@ -25,7 +27,6 @@ class TurnaroundChart(CustomFigure):
self.construct_chart() self.construct_chart()
if threshold: if threshold:
self.add_hline(y=threshold) self.add_hline(y=threshold)
# self.update_xaxes()
self.update_layout(showlegend=False) self.update_layout(showlegend=False)
def construct_chart(self, df: pd.DataFrame | None = None): def construct_chart(self, df: pd.DataFrame | None = None):

View File

@@ -1,7 +1,6 @@
""" """
Constructs main application. Constructs main application.
""" """
import os
from pprint import pformat from pprint import pformat
from PyQt6.QtCore import qInstallMessageHandler from PyQt6.QtCore import qInstallMessageHandler
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
@@ -18,12 +17,11 @@ from tools import check_if_app, Settings, Report, jinja_template_loading, check_
from .functions import select_save_file, select_open_file from .functions import select_save_file, select_open_file
from datetime import date from datetime import date
from .pop_ups import HTMLPop, AlertPop from .pop_ups import HTMLPop, AlertPop
from .misc import LogParser, Pagifier from .misc import Pagifier
import logging, webbrowser, sys, shutil import logging, webbrowser, sys, shutil
from .submission_table import SubmissionsSheet from .submission_table import SubmissionsSheet
from .submission_widget import SubmissionFormContainer from .submission_widget import SubmissionFormContainer
from .controls_chart import ControlsViewer from .controls_chart import ControlsViewer
# from .sample_search import SampleSearchBox
from .summary import Summary from .summary import Summary
from .turnaround import TurnaroundTime from .turnaround import TurnaroundTime
from .omni_search import SearchBox from .omni_search import SearchBox
@@ -84,7 +82,7 @@ class App(QMainWindow):
fileMenu.addAction(self.importAction) fileMenu.addAction(self.importAction)
fileMenu.addAction(self.yamlExportAction) fileMenu.addAction(self.yamlExportAction)
fileMenu.addAction(self.yamlImportAction) fileMenu.addAction(self.yamlImportAction)
methodsMenu.addAction(self.searchLog) # methodsMenu.addAction(self.searchLog)
methodsMenu.addAction(self.searchSample) methodsMenu.addAction(self.searchSample)
maintenanceMenu.addAction(self.joinExtractionAction) maintenanceMenu.addAction(self.joinExtractionAction)
maintenanceMenu.addAction(self.joinPCRAction) maintenanceMenu.addAction(self.joinPCRAction)
@@ -98,8 +96,8 @@ class App(QMainWindow):
toolbar = QToolBar("My main toolbar") toolbar = QToolBar("My main toolbar")
self.addToolBar(toolbar) self.addToolBar(toolbar)
toolbar.addAction(self.addReagentAction) toolbar.addAction(self.addReagentAction)
toolbar.addAction(self.addKitAction) # toolbar.addAction(self.addKitAction)
toolbar.addAction(self.addOrgAction) # toolbar.addAction(self.addOrgAction)
def _createActions(self): def _createActions(self):
""" """
@@ -108,13 +106,13 @@ class App(QMainWindow):
# logger.debug(f"Creating actions...") # logger.debug(f"Creating actions...")
self.importAction = QAction("&Import Submission", self) self.importAction = QAction("&Import Submission", self)
self.addReagentAction = QAction("Add Reagent", self) self.addReagentAction = QAction("Add Reagent", self)
self.addKitAction = QAction("Import Kit", self) # self.addKitAction = QAction("Import Kit", self)
self.addOrgAction = QAction("Import Org", self) # self.addOrgAction = QAction("Import Org", self)
self.joinExtractionAction = QAction("Link Extraction Logs") self.joinExtractionAction = QAction("Link Extraction Logs")
self.joinPCRAction = QAction("Link PCR Logs") self.joinPCRAction = QAction("Link PCR Logs")
self.helpAction = QAction("&About", self) self.helpAction = QAction("&About", self)
self.docsAction = QAction("&Docs", self) self.docsAction = QAction("&Docs", self)
self.searchLog = QAction("Search Log", self) # self.searchLog = QAction("Search Log", self)
self.searchSample = QAction("Search Sample", self) self.searchSample = QAction("Search Sample", self)
self.githubAction = QAction("Github", self) self.githubAction = QAction("Github", self)
self.yamlExportAction = QAction("Export Type Example", self) self.yamlExportAction = QAction("Export Type Example", self)
@@ -132,14 +130,13 @@ class App(QMainWindow):
self.joinPCRAction.triggered.connect(self.table_widget.sub_wid.link_pcr) self.joinPCRAction.triggered.connect(self.table_widget.sub_wid.link_pcr)
self.helpAction.triggered.connect(self.showAbout) self.helpAction.triggered.connect(self.showAbout)
self.docsAction.triggered.connect(self.openDocs) self.docsAction.triggered.connect(self.openDocs)
self.searchLog.triggered.connect(self.runSearch) # self.searchLog.triggered.connect(self.runSearch)
self.searchSample.triggered.connect(self.runSampleSearch) self.searchSample.triggered.connect(self.runSampleSearch)
self.githubAction.triggered.connect(self.openGithub) self.githubAction.triggered.connect(self.openGithub)
self.yamlExportAction.triggered.connect(self.export_ST_yaml) self.yamlExportAction.triggered.connect(self.export_ST_yaml)
self.yamlImportAction.triggered.connect(self.import_ST_yaml) self.yamlImportAction.triggered.connect(self.import_ST_yaml)
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.destroyed.connect(self.final_commit)
def showAbout(self): def showAbout(self):
""" """
@@ -180,15 +177,14 @@ class App(QMainWindow):
instr = HTMLPop(html=html, title="Instructions") instr = HTMLPop(html=html, title="Instructions")
instr.exec() instr.exec()
def runSearch(self): # def runSearch(self):
dlg = LogParser(self) # dlg = LogParser(self)
dlg.exec() # dlg.exec()
def runSampleSearch(self): def runSampleSearch(self):
""" """
Create a search for samples. Create a search for samples.
""" """
# dlg = SampleSearchBox(self)
dlg = SearchBox(self, object_type=BasicSample, extras=[]) dlg = SearchBox(self, object_type=BasicSample, extras=[])
dlg.exec() dlg.exec()
@@ -244,7 +240,6 @@ class App(QMainWindow):
st = SubmissionType.import_from_json(filepath=fname) st = SubmissionType.import_from_json(filepath=fname)
if st: if st:
# NOTE: Do not delete the print statement below. # NOTE: Do not delete the print statement below.
# print(pformat(st.to_export_dict()))
choice = input("Save the above submission type? [y/N]: ") choice = input("Save the above submission type? [y/N]: ")
if choice.lower() == "y": if choice.lower() == "y":
pass pass
@@ -254,9 +249,6 @@ class App(QMainWindow):
def update_data(self): def update_data(self):
self.table_widget.sub_wid.setData(page=self.table_widget.pager.page_anchor, page_size=page_size) self.table_widget.sub_wid.setData(page=self.table_widget.pager.page_anchor, page_size=page_size)
def final_commit(self):
logger.debug("Running final commit")
self.ctx.database_session.commit()
class AddSubForm(QWidget): class AddSubForm(QWidget):

View File

@@ -10,9 +10,10 @@ from PyQt6.QtWidgets import (
from PyQt6.QtCore import QSignalBlocker from PyQt6.QtCore import QSignalBlocker
from backend.db import ControlType, IridaControl from backend.db import ControlType, IridaControl
import logging import logging
from tools import Report, report_result from tools import Report, report_result, Result
from frontend.visualizations import CustomFigure from frontend.visualizations import CustomFigure
from .misc import StartEndDatePicker from .misc import StartEndDatePicker
from .info_tab import InfoPane
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -37,11 +38,11 @@ class ControlsViewer(QWidget):
# NOTE: fetch types of controls # NOTE: fetch types of controls
con_sub_types = [item for item in self.archetype.targets.keys()] con_sub_types = [item for item in self.archetype.targets.keys()]
self.control_sub_typer.addItems(con_sub_types) self.control_sub_typer.addItems(con_sub_types)
# NOTE: create custom widget to get types of analysis # NOTE: create custom widget to get types of analysis -- disabled by PCR control
self.mode_typer = QComboBox() self.mode_typer = QComboBox()
mode_types = IridaControl.get_modes() mode_types = IridaControl.get_modes()
self.mode_typer.addItems(mode_types) self.mode_typer.addItems(mode_types)
# NOTE: create custom widget to get subtypes of analysis # NOTE: create custom widget to get subtypes of analysis -- disabled by PCR control
self.mode_sub_typer = QComboBox() self.mode_sub_typer = QComboBox()
self.mode_sub_typer.setEnabled(False) self.mode_sub_typer.setEnabled(False)
# NOTE: add widgets to tab2 layout # NOTE: add widgets to tab2 layout
@@ -83,15 +84,15 @@ class ControlsViewer(QWidget):
pass pass
# NOTE: correct start date being more recent than end date and rerun # NOTE: correct start date being more recent than end date and rerun
if self.datepicker.start_date.date() > self.datepicker.end_date.date(): if self.datepicker.start_date.date() > self.datepicker.end_date.date():
logger.warning("Start date after end date is not allowed!")
threemonthsago = self.datepicker.end_date.date().addDays(-60) threemonthsago = self.datepicker.end_date.date().addDays(-60)
# NOTE: block signal that will rerun controls getter and set start date msg = f"Start date after end date is not allowed! Setting to {threemonthsago.toString()}."
# Without triggering this function again logger.warning(msg)
# NOTE: block signal that will rerun controls getter and set start date Without triggering this function again
with QSignalBlocker(self.datepicker.start_date) as blocker: with QSignalBlocker(self.datepicker.start_date) as blocker:
self.datepicker.start_date.setDate(threemonthsago) self.datepicker.start_date.setDate(threemonthsago)
self.controls_getter() self.controls_getter_function()
self.report.add_result(report) report.add_result(Result(owner=self.__str__(), msg=msg, status="Warning"))
return return report
# NOTE: convert to python useable date objects # NOTE: convert to python useable date objects
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()
@@ -167,4 +168,3 @@ class ControlsViewer(QWidget):
self.webengineview.update() self.webengineview.update()
# logger.debug("Figure updated... I hope.") # logger.debug("Figure updated... I hope.")
return report return report

View File

@@ -1,7 +1,6 @@
''' '''
Creates forms that the user can enter equipment info into. Creates forms that the user can enter equipment info into.
''' '''
import time
from pprint import pformat from pprint import pformat
from PyQt6.QtCore import Qt, QSignalBlocker from PyQt6.QtCore import Qt, QSignalBlocker
from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox, from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox,

View File

@@ -66,7 +66,6 @@ class GelBox(QDialog):
layout.addWidget(self.imv, 0, 1, 20, 20) layout.addWidget(self.imv, 0, 1, 20, 20)
# NOTE: setting this widget as central widget of the main window # NOTE: setting this widget as central widget of the main window
try: try:
# control_info = sorted(self.submission.gel_controls, key=lambda d: d['location'])
control_info = sorted(self.submission.gel_controls, key=itemgetter('location')) control_info = sorted(self.submission.gel_controls, key=itemgetter('location'))
except KeyError: except KeyError:
control_info = None control_info = None
@@ -79,7 +78,6 @@ class GelBox(QDialog):
layout.addWidget(self.buttonBox, 23, 1, 1, 1) layout.addWidget(self.buttonBox, 23, 1, 1, 1)
self.setLayout(layout) self.setLayout(layout)
def parse_form(self) -> Tuple[str, str | Path, list]: def parse_form(self) -> Tuple[str, str | Path, list]:
""" """
Get relevant values from self/form Get relevant values from self/form
@@ -124,7 +122,6 @@ class ControlsForm(QWidget):
self.layout.addWidget(label, iii, 1, 1, 1) self.layout.addWidget(label, iii, 1, 1, 1)
for iii in range(3): for iii in range(3):
for jjj in range(3): for jjj in range(3):
# widge = QLineEdit()
widge = QComboBox() widge = QComboBox()
widge.addItems(['Neg', 'Pos']) widge.addItems(['Neg', 'Pos'])
widge.setCurrentIndex(0) widge.setCurrentIndex(0)

View File

@@ -1,6 +1,11 @@
"""
A pane to show info e.g. cost reports and turnaround times.
TODO: Can I merge this with the controls chart pane?
"""
from PyQt6.QtCore import QSignalBlocker
from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import QWidget, QGridLayout, QPushButton from PyQt6.QtWidgets import QWidget, QGridLayout
from tools import Report from tools import Report, report_result, Result
from .misc import StartEndDatePicker, save_pdf from .misc import StartEndDatePicker, save_pdf
from .functions import select_save_file from .functions import select_save_file
import logging import logging
@@ -17,22 +22,29 @@ class InfoPane(QWidget):
self.report = Report() self.report = Report()
self.datepicker = StartEndDatePicker(default_start=-31) self.datepicker = StartEndDatePicker(default_start=-31)
self.webview = QWebEngineView() self.webview = QWebEngineView()
self.datepicker.start_date.dateChanged.connect(self.date_changed) self.datepicker.start_date.dateChanged.connect(self.update_data)
self.datepicker.end_date.dateChanged.connect(self.date_changed) self.datepicker.end_date.dateChanged.connect(self.update_data)
self.layout = QGridLayout(self) self.layout = QGridLayout(self)
self.layout.addWidget(self.datepicker, 0, 0, 1, 2) self.layout.addWidget(self.datepicker, 0, 0, 1, 2)
self.save_excel_button = QPushButton("Save Excel", parent=self)
self.save_excel_button.pressed.connect(self.save_excel)
self.save_pdf_button = QPushButton("Save PDF", parent=self)
self.save_pdf_button.pressed.connect(self.save_pdf)
self.layout.addWidget(self.save_excel_button, 0, 2, 1, 1)
self.layout.addWidget(self.save_pdf_button, 0, 3, 1, 1)
self.layout.addWidget(self.webview, 2, 0, 1, 4) self.layout.addWidget(self.webview, 2, 0, 1, 4)
self.setLayout(self.layout) self.setLayout(self.layout)
def date_changed(self): @report_result
def update_data(self, *args, **kwargs):
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()
if self.datepicker.start_date.date() > self.datepicker.end_date.date():
lastmonth = self.datepicker.end_date.date().addDays(-31)
msg = f"Start date after end date is not allowed! Setting to {lastmonth.toString()}."
logger.warning(msg)
# NOTE: block signal that will rerun controls getter and set start date
# Without triggering this function again
with QSignalBlocker(self.datepicker.start_date) as blocker:
self.datepicker.start_date.setDate(lastmonth)
self.update_data()
report.add_result(Result(owner=self.__str__(), msg=msg, status="Warning"))
return report
def save_excel(self): def save_excel(self):
fname = select_save_file(self, default_name=f"Report {self.start_date.strftime('%Y%m%d')} - {self.end_date.strftime('%Y%m%d')}", extension="xlsx") fname = select_save_file(self, default_name=f"Report {self.start_date.strftime('%Y%m%d')} - {self.end_date.strftime('%Y%m%d')}", extension="xlsx")
@@ -42,4 +54,10 @@ class InfoPane(QWidget):
fname = select_save_file(obj=self, fname = select_save_file(obj=self,
default_name=f"Report {self.start_date.strftime('%Y%m%d')} - {self.end_date.strftime('%Y%m%d')}", default_name=f"Report {self.start_date.strftime('%Y%m%d')} - {self.end_date.strftime('%Y%m%d')}",
extension="pdf") extension="pdf")
save_pdf(obj=self.webview, filename=fname) save_pdf(obj=self.webview, filename=fname)
def save_png(self):
fname = select_save_file(obj=self,
default_name=f"Plotly {self.start_date.strftime('%Y%m%d')} - {self.end_date.strftime('%Y%m%d')}",
extension="png")
self.fig.write_image(fname.absolute().__str__(), engine="kaleido")

View File

@@ -14,8 +14,6 @@ from PyQt6.QtCore import Qt, QDate, QSize, QMarginsF
from tools import jinja_template_loading from tools import jinja_template_loading
from backend.db.models import * from backend.db.models import *
import logging import logging
from .pop_ups import AlertPop
from .functions import select_open_file
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -114,55 +112,6 @@ class AddReagentForm(QDialog):
self.name_input.addItems(list(set([item.name for item in lookup]))) self.name_input.addItems(list(set([item.name for item in lookup])))
class LogParser(QDialog):
def __init__(self, parent):
super().__init__(parent)
self.app = self.parent()
self.filebutton = QPushButton(self)
self.filebutton.setText("Import File")
self.phrase_looker = QComboBox(self)
self.phrase_looker.setEditable(True)
self.btn = QPushButton(self)
self.btn.setText("Search")
self.layout = QFormLayout(self)
self.layout.addRow(self.tr("&File:"), self.filebutton)
self.layout.addRow(self.tr("&Search Term:"), self.phrase_looker)
self.layout.addRow(self.btn)
self.filebutton.clicked.connect(self.filelookup)
self.btn.clicked.connect(self.runsearch)
self.setMinimumWidth(400)
def filelookup(self):
"""
Select file to search
"""
self.fname = select_open_file(self, "tabular")
def runsearch(self):
"""
Gets total/percent occurences of string in tabular file.
"""
count: int = 0
total: int = 0
# logger.debug(f"Current search term: {self.phrase_looker.currentText()}")
try:
with open(self.fname, "r") as f:
for chunk in readInChunks(fileObj=f):
total += len(chunk)
for line in chunk:
if self.phrase_looker.currentText().lower() in line.lower():
count += 1
percent = (count / total) * 100
msg = f"I found {count} instances of the search phrase out of {total} = {percent:.2f}%."
status = "Information"
except AttributeError:
msg = f"No file was selected."
status = "Error"
dlg = AlertPop(message=msg, status=status)
dlg.exec()
class StartEndDatePicker(QWidget): class StartEndDatePicker(QWidget):
""" """
custom widget to pick start and end dates for controls graphs custom widget to pick start and end dates for controls graphs

View File

@@ -16,6 +16,9 @@ logger = logging.getLogger(f"submissions.{__name__}")
class SearchBox(QDialog): class SearchBox(QDialog):
"""
The full search widget.
"""
def __init__(self, parent, object_type: Any, extras: List[str], **kwargs): def __init__(self, parent, object_type: Any, extras: List[str], **kwargs):
super().__init__(parent) super().__init__(parent)
@@ -36,7 +39,7 @@ class SearchBox(QDialog):
else: else:
self.sub_class = None self.sub_class = None
self.results = SearchResults(parent=self, object_type=self.object_type, extras=self.extras, **kwargs) self.results = SearchResults(parent=self, object_type=self.object_type, extras=self.extras, **kwargs)
logger.debug(f"results: {self.results}") # logger.debug(f"results: {self.results}")
self.layout.addWidget(self.results, 5, 0) self.layout.addWidget(self.results, 5, 0)
self.setLayout(self.layout) self.setLayout(self.layout)
self.setWindowTitle(f"Search {self.object_type.__name__}") self.setWindowTitle(f"Search {self.object_type.__name__}")
@@ -51,6 +54,7 @@ class SearchBox(QDialog):
# logger.debug(deletes) # logger.debug(deletes)
for item in deletes: for item in deletes:
item.setParent(None) item.setParent(None)
# NOTE: Handle any subclasses
if not self.sub_class: if not self.sub_class:
self.update_data() self.update_data()
else: else:
@@ -89,6 +93,9 @@ class SearchBox(QDialog):
class FieldSearch(QWidget): class FieldSearch(QWidget):
"""
Search bar.
"""
def __init__(self, parent, label, field_name): def __init__(self, parent, label, field_name):
super().__init__(parent) super().__init__(parent)
@@ -115,6 +122,9 @@ class FieldSearch(QWidget):
class SearchResults(QTableView): class SearchResults(QTableView):
"""
Results table.
"""
def __init__(self, parent: SearchBox, object_type: Any, extras: List[str], **kwargs): def __init__(self, parent: SearchBox, object_type: Any, extras: List[str], **kwargs):
super().__init__() super().__init__()
@@ -146,7 +156,6 @@ class SearchResults(QTableView):
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: {context}") logger.debug(f"Context: {context}")
try: try:
# object = self.object_type.query(**{self.object_type.searchables: context[self.object_type.searchables]})
object = self.object_type.query(**context) object = self.object_type.query(**context)
except KeyError: except KeyError:
object = None object = None

View File

@@ -42,7 +42,7 @@ class SubmissionDetails(QDialog):
# NOTE: button to export a pdf version # NOTE: button to export a pdf version
self.btn = QPushButton("Export PDF") self.btn = QPushButton("Export PDF")
self.btn.setFixedWidth(775) self.btn.setFixedWidth(775)
self.btn.clicked.connect(self.export) self.btn.clicked.connect(self.save_pdf)
self.back = QPushButton("Back") self.back = QPushButton("Back")
self.back.setFixedWidth(100) self.back.setFixedWidth(100)
# self.back.clicked.connect(self.back_function) # self.back.clicked.connect(self.back_function)
@@ -181,7 +181,7 @@ class SubmissionDetails(QDialog):
submission.save() submission.save()
self.submission_details(submission=self.rsl_plate_num) self.submission_details(submission=self.rsl_plate_num)
def export(self): def save_pdf(self):
""" """
Renders submission to html, then creates and saves .pdf file to user selected file. Renders submission to html, then creates and saves .pdf file to user selected file.
""" """

View File

@@ -83,6 +83,7 @@ class SubmissionsSheet(QTableView):
self.resizeRowsToContents() self.resizeRowsToContents()
self.setSortingEnabled(True) self.setSortingEnabled(True)
self.doubleClicked.connect(lambda x: BasicSubmission.query(id=x.sibling(x.row(), 0).data()).show_details(self)) self.doubleClicked.connect(lambda x: BasicSubmission.query(id=x.sibling(x.row(), 0).data()).show_details(self))
# NOTE: Have to run native query here because mine just returns results?
self.total_count = BasicSubmission.__database_session__.query(BasicSubmission).count() self.total_count = BasicSubmission.__database_session__.query(BasicSubmission).count()
def setData(self, page: int = 1, page_size: int = 250) -> None: def setData(self, page: int = 1, page_size: int = 250) -> None:

View File

@@ -1,15 +1,13 @@
''' '''
Contains all submission related frontend functions Contains all submission related frontend functions
''' '''
import sys
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QWidget, QPushButton, QVBoxLayout, QWidget, QPushButton, QVBoxLayout,
QComboBox, QDateEdit, QLineEdit, QLabel QComboBox, QDateEdit, QLineEdit, QLabel
) )
from PyQt6.QtCore import pyqtSignal, Qt from PyQt6.QtCore import pyqtSignal, Qt
from . import select_open_file, select_save_file from . import select_open_file, select_save_file
import logging, difflib import logging
from pathlib import Path from pathlib import Path
from tools import Report, Result, check_not_nan, main_form_style, report_result from tools import Report, Result, check_not_nan, main_form_style, report_result
from backend.excel.parser import SheetParser from backend.excel.parser import SheetParser
@@ -187,6 +185,8 @@ class SubmissionFormContainer(QWidget):
class SubmissionFormWidget(QWidget): class SubmissionFormWidget(QWidget):
update_reagent_fields = ['extraction_kit']
def __init__(self, parent: QWidget, submission: PydSubmission, disable: list | None = None) -> None: def __init__(self, parent: QWidget, submission: PydSubmission, disable: list | None = None) -> None:
super().__init__(parent) super().__init__(parent)
# logger.debug(f"Disable: {disable}") # logger.debug(f"Disable: {disable}")
@@ -203,7 +203,7 @@ class SubmissionFormWidget(QWidget):
# logger.debug(f"Attempting to extend ignore list with {self.pyd.submission_type['value']}") # logger.debug(f"Attempting to extend ignore list with {self.pyd.submission_type['value']}")
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"Creating widget: {k}") # logger.debug(f"Creating widget: {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
@@ -225,10 +225,10 @@ class SubmissionFormWidget(QWidget):
sub_obj=st, disable=check) sub_obj=st, disable=check)
if add_widget is not None: if add_widget is not None:
self.layout.addWidget(add_widget) self.layout.addWidget(add_widget)
if k == "extraction_kit": # if k == "extraction_kit":
if k in self.__class__.update_reagent_fields:
add_widget.input.currentTextChanged.connect(self.scrape_reagents) add_widget.input.currentTextChanged.connect(self.scrape_reagents)
self.setStyleSheet(main_form_style) self.setStyleSheet(main_form_style)
# self.scrape_reagents(self.pyd.extraction_kit)
self.scrape_reagents(self.extraction_kit) self.scrape_reagents(self.extraction_kit)
def create_widget(self, key: str, value: dict | PydReagent, submission_type: str | SubmissionType | None = None, def create_widget(self, key: str, value: dict | PydReagent, submission_type: str | SubmissionType | None = None,
@@ -293,9 +293,8 @@ 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 = self.pyd.check_kit_integrity(extraction_kit=self.extraction_kit) reagents, integrity_report = self.pyd.check_kit_integrity(extraction_kit=self.extraction_kit)
logger.debug(f"Got reagents: {pformat(reagents)}") # logger.debug(f"Got reagents: {pformat(reagents)}")
for reagent in reagents: for reagent in reagents:
# add_widget = self.ReagentFormWidget(parent=self, reagent=reagent, extraction_kit=self.pyd.extraction_kit)
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)
self.layout.addWidget(add_widget) self.layout.addWidget(add_widget)
report.add_result(integrity_report) report.add_result(integrity_report)
@@ -334,10 +333,6 @@ class SubmissionFormWidget(QWidget):
query = [widget for widget in query if widget.objectName() == object_name] query = [widget for widget in query if widget.objectName() == object_name]
return query return query
# def update_pyd(self):
# results = self.parse_form()
# logger.debug(pformat(results))
@report_result @report_result
def submit_new_sample_function(self, *args) -> Report: def submit_new_sample_function(self, *args) -> Report:
""" """
@@ -538,19 +533,14 @@ class SubmissionFormWidget(QWidget):
except (TypeError, KeyError): except (TypeError, KeyError):
pass pass
obj = parent.parent().parent() obj = parent.parent().parent()
logger.debug(f"Object: {obj}") # logger.debug(f"Object: {obj}")
logger.debug(f"Parent: {parent.parent()}") # logger.debug(f"Parent: {parent.parent()}")
# logger.debug(f"Creating widget for: {key}") # logger.debug(f"Creating widget for: {key}")
match key: match key:
case 'submitting_lab': case 'submitting_lab':
add_widget = MyQComboBox(scrollWidget=parent) add_widget = MyQComboBox(scrollWidget=parent)
# NOTE: lookup organizations suitable for submitting_lab (ctx: self.InfoItem.SubmissionFormWidget.SubmissionFormContainer.AddSubForm ) # NOTE: lookup organizations suitable for submitting_lab (ctx: self.InfoItem.SubmissionFormWidget.SubmissionFormContainer.AddSubForm )
labs = [item.name for item in Organization.query()] labs = [item.name for item in Organization.query()]
# NOTE: try to set closest match to top of list
# try:
# labs = difflib.get_close_matches(value, labs, len(labs), 0)
# except (TypeError, ValueError):
# pass
if isinstance(value, dict): if isinstance(value, dict):
value = value['value'] value = value['value']
if isinstance(value, Organization): if isinstance(value, Organization):
@@ -559,7 +549,7 @@ class SubmissionFormWidget(QWidget):
looked_up_lab = Organization.query(name=value, limit=1) looked_up_lab = Organization.query(name=value, limit=1)
except AttributeError: except AttributeError:
looked_up_lab = None looked_up_lab = None
logger.debug(f"\n\nLooked up lab: {looked_up_lab}") # logger.debug(f"\n\nLooked up lab: {looked_up_lab}")
if looked_up_lab: if looked_up_lab:
try: try:
labs.remove(str(looked_up_lab.name)) labs.remove(str(looked_up_lab.name))
@@ -579,7 +569,6 @@ class SubmissionFormWidget(QWidget):
add_widget = MyQComboBox(scrollWidget=parent) add_widget = MyQComboBox(scrollWidget=parent)
# NOTE: lookup existing kits by 'submission_type' decided on by sheetparser # NOTE: lookup existing kits by 'submission_type' decided on by sheetparser
# logger.debug(f"Looking up kits used for {submission_type}") # logger.debug(f"Looking up kits used for {submission_type}")
# uses = [item.name for item in KitType.query(used_for=submission_type)]
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
# logger.debug(f"Kits received for {submission_type}: {uses}") # logger.debug(f"Kits received for {submission_type}: {uses}")

View File

@@ -1,12 +1,11 @@
from PyQt6.QtCore import QSignalBlocker """
from PyQt6.QtWebEngineWidgets import QWebEngineView Pane to hold information e.g. cost summary.
"""
from .info_tab import InfoPane from .info_tab import InfoPane
from PyQt6.QtWidgets import QWidget, QGridLayout, QPushButton, QLabel from PyQt6.QtWidgets import QWidget, QLabel, QPushButton
from backend.db import Organization from backend.db import Organization
from backend.excel import ReportMaker from backend.excel import ReportMaker
from tools import Report from .misc import CheckableComboBox
from .misc import StartEndDatePicker, save_pdf, CheckableComboBox
from .functions import select_save_file
import logging import logging
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -16,32 +15,27 @@ class Summary(InfoPane):
def __init__(self, parent: QWidget) -> None: def __init__(self, parent: QWidget) -> None:
super().__init__(parent) super().__init__(parent)
self.save_excel_button = QPushButton("Save Excel", parent=self)
self.save_excel_button.pressed.connect(self.save_excel)
self.save_pdf_button = QPushButton("Save PDF", parent=self)
self.save_pdf_button.pressed.connect(self.save_pdf)
self.layout.addWidget(self.save_excel_button, 0, 2, 1, 1)
self.layout.addWidget(self.save_pdf_button, 0, 3, 1, 1)
self.org_select = CheckableComboBox() self.org_select = CheckableComboBox()
self.org_select.setEditable(False) self.org_select.setEditable(False)
self.org_select.addItem("Select", header=True) self.org_select.addItem("Select", header=True)
for org in [org.name for org in Organization.query()]: for org in [org.name for org in Organization.query()]:
self.org_select.addItem(org) self.org_select.addItem(org)
self.org_select.model().itemChanged.connect(self.date_changed) self.org_select.model().itemChanged.connect(self.update_data)
self.layout.addWidget(QLabel("Client"), 1, 0, 1, 1) self.layout.addWidget(QLabel("Client"), 1, 0, 1, 1)
self.layout.addWidget(self.org_select, 1, 1, 1, 3) self.layout.addWidget(self.org_select, 1, 1, 1, 3)
self.date_changed() self.update_data()
def date_changed(self):
def update_data(self):
super().update_data()
orgs = [self.org_select.itemText(i) for i in range(self.org_select.count()) if self.org_select.itemChecked(i)] orgs = [self.org_select.itemText(i) for i in range(self.org_select.count()) if self.org_select.itemChecked(i)]
if self.datepicker.start_date.date() > self.datepicker.end_date.date(): # logger.debug(f"Getting report from {self.start_date} to {self.end_date} using {orgs}")
logger.warning("Start date after end date is not allowed!")
lastmonth = self.datepicker.end_date.date().addDays(-31)
# NOTE: block signal that will rerun controls getter and set start date
# Without triggering this function again
with QSignalBlocker(self.datepicker.start_date) as blocker:
self.datepicker.start_date.setDate(lastmonth)
self.date_changed()
return
# NOTE: convert to python useable date objects
# self.start_date = self.datepicker.start_date.date().toPyDate()
# self.end_date = self.datepicker.end_date.date().toPyDate()
super().date_changed()
logger.debug(f"Getting report from {self.start_date} to {self.end_date} using {orgs}")
self.report_obj = ReportMaker(start_date=self.start_date, end_date=self.end_date, organizations=orgs) self.report_obj = ReportMaker(start_date=self.start_date, end_date=self.end_date, organizations=orgs)
self.webview.setHtml(self.report_obj.html) self.webview.setHtml(self.report_obj.html)
if self.report_obj.subs: if self.report_obj.subs:
@@ -50,13 +44,3 @@ class Summary(InfoPane):
else: else:
self.save_pdf_button.setEnabled(False) self.save_pdf_button.setEnabled(False)
self.save_excel_button.setEnabled(False) self.save_excel_button.setEnabled(False)
# def save_excel(self):
# fname = select_save_file(self, default_name=f"Report {self.start_date.strftime('%Y%m%d')} - {self.end_date.strftime('%Y%m%d')}", extension="xlsx")
# self.report_obj.write_report(fname, obj=self)
#
# def save_pdf(self):
# fname = select_save_file(obj=self,
# default_name=f"Report {self.start_date.strftime('%Y%m%d')} - {self.end_date.strftime('%Y%m%d')}",
# extension="pdf")
# save_pdf(obj=self.webview, filename=fname)

View File

@@ -15,28 +15,25 @@ class TurnaroundTime(InfoPane):
def __init__(self, parent: QWidget): def __init__(self, parent: QWidget):
super().__init__(parent) super().__init__(parent)
self.chart = None self.save_button = QPushButton("Save Chart", parent=self)
self.save_button.pressed.connect(self.save_png)
self.layout.addWidget(self.save_button, 0, 2, 1, 1)
self.export_button = QPushButton("Save Data", parent=self)
self.export_button.pressed.connect(self.save_excel)
self.layout.addWidget(self.export_button, 0, 3, 1, 1)
self.fig = None
self.report_object = None self.report_object = None
self.submission_typer = QComboBox(self) self.submission_typer = QComboBox(self)
subs = ["Any"] + [item.name for item in SubmissionType.query()] subs = ["All"] + [item.name for item in SubmissionType.query()]
self.submission_typer.addItems(subs) self.submission_typer.addItems(subs)
self.layout.addWidget(self.submission_typer, 1, 1, 1, 3) self.layout.addWidget(self.submission_typer, 1, 1, 1, 3)
self.submission_typer.currentTextChanged.connect(self.date_changed) self.submission_typer.currentTextChanged.connect(self.update_data)
self.date_changed() self.update_data()
def date_changed(self): def update_data(self):
if self.datepicker.start_date.date() > self.datepicker.end_date.date(): super().update_data()
logger.warning("Start date after end date is not allowed!")
lastmonth = self.datepicker.end_date.date().addDays(-31)
# NOTE: block signal that will rerun controls getter and set start date
# Without triggering this function again
with QSignalBlocker(self.datepicker.start_date) as blocker:
self.datepicker.start_date.setDate(lastmonth)
self.date_changed()
return
super().date_changed()
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)
if self.submission_typer.currentText() == "Any": if self.submission_typer.currentText() == "All":
submission_type = None submission_type = None
subtype_obj = None subtype_obj = None
else: else:
@@ -47,5 +44,5 @@ class TurnaroundTime(InfoPane):
threshold = subtype_obj.defaults['turnaround_time'] + 0.5 threshold = subtype_obj.defaults['turnaround_time'] + 0.5
else: else:
threshold = None threshold = None
self.chart = TurnaroundChart(df=self.report_obj.df, settings=chart_settings, modes=[], threshold=threshold) self.fig = TurnaroundChart(df=self.report_obj.df, settings=chart_settings, modes=[], threshold=threshold)
self.webview.setHtml(self.chart.to_html()) self.webview.setHtml(self.fig.to_html())

View File

@@ -3,13 +3,11 @@ Contains miscellaenous functions used by both frontend and backend.
''' '''
from __future__ import annotations from __future__ import annotations
import json
import pprint
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from json import JSONDecodeError from json import JSONDecodeError
from pprint import pprint
import numpy as np import numpy as np
import logging, re, yaml, sys, os, stat, platform, getpass, inspect import logging, re, yaml, sys, os, stat, platform, getpass, inspect, json, pandas as pd
import pandas as pd
from dateutil.easter import easter from dateutil.easter import easter
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from logging import handlers from logging import handlers
@@ -20,7 +18,6 @@ from sqlalchemy import create_engine, text, MetaData
from pydantic import field_validator, BaseModel, Field from pydantic import field_validator, BaseModel, Field
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import Any, Tuple, Literal, List from typing import Any, Tuple, Literal, List
# print(inspect.stack()[1])
from __init__ import project_path from __init__ import project_path
from configparser import ConfigParser from configparser import ConfigParser
from tkinter import Tk # NOTE: This is for choosing database path before app is created. from tkinter import Tk # NOTE: This is for choosing database path before app is created.
@@ -261,6 +258,7 @@ class Settings(BaseSettings, extra="allow"):
submission_types: dict | None = None submission_types: dict | None = None
database_session: Session | None = None database_session: Session | None = None
package: Any | None = None package: Any | None = None
logging_enabled: bool = Field(default=False)
model_config = SettingsConfigDict(env_file_encoding='utf-8') model_config = SettingsConfigDict(env_file_encoding='utf-8')
@@ -422,6 +420,7 @@ class Settings(BaseSettings, extra="allow"):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.set_from_db() self.set_from_db()
pprint(f"User settings:\n{self.__dict__}")
def set_from_db(self): def set_from_db(self):
if 'pytest' in sys.modules: if 'pytest' in sys.modules:
@@ -819,7 +818,7 @@ class Result(BaseModel, arbitrary_types_allowed=True):
self.owner = inspect.stack()[1].function self.owner = inspect.stack()[1].function
def report(self): def report(self):
from frontend.widgets.misc import AlertPop from frontend.widgets.pop_ups import AlertPop
return AlertPop(message=self.msg, status=self.status, owner=self.owner) return AlertPop(message=self.msg, status=self.status, owner=self.owner)