Moments before disaster.
This commit is contained in:
@@ -4,3 +4,4 @@ database_schema: null
|
||||
database_user: null
|
||||
database_password: null
|
||||
database_name: null
|
||||
logging_enabled: false
|
||||
@@ -1,10 +1,8 @@
|
||||
"""
|
||||
All database related operations.
|
||||
"""
|
||||
import sqlalchemy.orm
|
||||
from sqlalchemy import event, inspect
|
||||
from sqlalchemy.engine import Engine
|
||||
|
||||
from tools import ctx
|
||||
|
||||
|
||||
@@ -48,7 +46,11 @@ def update_log(mapper, connection, target):
|
||||
hist = attr.load_history()
|
||||
if not hist.has_changes():
|
||||
continue
|
||||
if attr.key == "custom":
|
||||
continue
|
||||
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]
|
||||
change = dict(field=attr.key, added=added, deleted=deleted)
|
||||
# logger.debug(f"Adding: {pformat(change)}")
|
||||
@@ -69,6 +71,6 @@ def update_log(mapper, connection, target):
|
||||
else:
|
||||
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_insert', update_log, propagate=True)
|
||||
|
||||
@@ -1414,25 +1414,6 @@ class Equipment(BaseClass):
|
||||
if extraction_kit and extraction_kit not in process.kit_types:
|
||||
continue
|
||||
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
|
||||
@setup_lookup
|
||||
@@ -1650,25 +1631,6 @@ class EquipmentRole(BaseClass):
|
||||
if extraction_kit and extraction_kit not in process.kit_types:
|
||||
continue
|
||||
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):
|
||||
"""
|
||||
@@ -1730,9 +1692,8 @@ class SubmissionEquipmentAssociation(BaseClass):
|
||||
|
||||
@classmethod
|
||||
@setup_lookup
|
||||
def query(cls, equipment_id: int, submission_id: int, role: str | None = None, limit: int = 0, **kwargs) -> Any | \
|
||||
List[
|
||||
Any]:
|
||||
def query(cls, equipment_id: int, submission_id: int, role: str | None = None, limit: int = 0, **kwargs) \
|
||||
-> Any | List[Any]:
|
||||
query: Query = cls.__database_session__.query(cls)
|
||||
query = query.filter(cls.equipment_id == equipment_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.')
|
||||
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
|
||||
def save(self):
|
||||
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
|
||||
|
||||
Args:
|
||||
kit_type (KitType): KitType of interest.
|
||||
extraction_kit (KitType | str): KitType of interest.
|
||||
|
||||
Returns:
|
||||
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,
|
||||
kit_type=extraction_kit).items()}
|
||||
base_dict['static'] = self.static
|
||||
@@ -2013,8 +1952,8 @@ class SubmissionTipsAssociation(BaseClass):
|
||||
|
||||
@classmethod
|
||||
@setup_lookup
|
||||
def query(cls, tip_id: int, role: str, submission_id: int | None = None, limit: int = 0, **kwargs) -> Any | List[
|
||||
Any]:
|
||||
def query(cls, tip_id: int, role: str, submission_id: int | None = None, limit: int = 0, **kwargs) \
|
||||
-> Any | List[Any]:
|
||||
query: Query = cls.__database_session__.query(cls)
|
||||
query = query.filter(cls.tip_id == tip_id)
|
||||
if submission_id is not None:
|
||||
|
||||
@@ -2,13 +2,10 @@
|
||||
Models for the main submission and sample types.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
# import sys
|
||||
# import types
|
||||
# import zipfile
|
||||
from copy import deepcopy
|
||||
from getpass import getuser
|
||||
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 operator import itemgetter
|
||||
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, \
|
||||
ArgumentError
|
||||
from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError
|
||||
# import pandas as pd
|
||||
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, \
|
||||
@@ -1276,7 +1272,7 @@ class BasicSubmission(BaseClass, LogMixin):
|
||||
if msg.exec():
|
||||
try:
|
||||
self.backup(fname=fname, full_backup=True)
|
||||
except zipfile.BadZipfile:
|
||||
except BadZipfile:
|
||||
logger.error("Couldn't open zipfile for writing.")
|
||||
self.__database_session__.delete(self)
|
||||
try:
|
||||
@@ -2261,7 +2257,7 @@ class WastewaterArtic(BasicSubmission):
|
||||
|
||||
# Sample Classes
|
||||
|
||||
class BasicSample(BaseClass):
|
||||
class BasicSample(BaseClass, LogMixin):
|
||||
"""
|
||||
Base of basic sample which polymorphs into BCSample and WWSample
|
||||
"""
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
'''
|
||||
contains parser objects for pulling values from client generated submission sheets.
|
||||
'''
|
||||
import json
|
||||
import sys
|
||||
import logging
|
||||
from copy import copy
|
||||
from getpass import getuser
|
||||
from pprint import pformat
|
||||
@@ -11,7 +10,6 @@ from openpyxl import load_workbook, Workbook
|
||||
from pathlib import Path
|
||||
from backend.db.models import *
|
||||
from backend.validators import PydSubmission, RSLNamer
|
||||
import logging, re
|
||||
from collections import OrderedDict
|
||||
from tools import check_not_nan, is_missing, check_key_or_attr
|
||||
|
||||
@@ -195,7 +193,7 @@ class InfoParser(object):
|
||||
ws = self.xl[sheet]
|
||||
relevant = []
|
||||
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":
|
||||
continue
|
||||
if isinstance(v, str):
|
||||
@@ -230,7 +228,7 @@ class InfoParser(object):
|
||||
case "submitted_date":
|
||||
value, missing = is_missing(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():
|
||||
value, missing = is_missing(value)
|
||||
if missing: continue
|
||||
@@ -300,11 +298,11 @@ class ReagentParser(object):
|
||||
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:
|
||||
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:
|
||||
self.kit_object = temp_kit_object
|
||||
# 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:
|
||||
ws = self.xl[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 == {}:
|
||||
continue
|
||||
for item in relevant:
|
||||
@@ -499,8 +497,7 @@ class SampleParser(object):
|
||||
yield new
|
||||
else:
|
||||
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'])
|
||||
# lookup_samples = sorted(copy(self.lookup_samples), key=lambda d: d[merge_on_id])
|
||||
logger.info(f"Merging sample info using {merge_on_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))
|
||||
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.")
|
||||
searchables = [(jj, sample) for jj, sample in enumerate(lookup_samples)
|
||||
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
|
||||
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:
|
||||
lookup_samples[jj] = {}
|
||||
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.
|
||||
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):
|
||||
submission_type = SubmissionType.query(name=submission_type)
|
||||
self.submission_type = submission_type
|
||||
@@ -568,7 +554,6 @@ class EquipmentParser(object):
|
||||
Returns:
|
||||
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")}
|
||||
|
||||
def get_asset_number(self, input: str) -> str:
|
||||
@@ -638,7 +623,7 @@ class TipParser(object):
|
||||
xl (Workbook): Openpyxl workbook from submitted excel file.
|
||||
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):
|
||||
submission_type = SubmissionType.query(name=submission_type)
|
||||
self.submission_type = submission_type
|
||||
@@ -652,7 +637,6 @@ class TipParser(object):
|
||||
Returns:
|
||||
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")}
|
||||
|
||||
def parse_tips(self) -> List[dict]:
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
Contains functions for generating summary reports
|
||||
'''
|
||||
from pprint import pformat
|
||||
|
||||
from pandas import DataFrame, ExcelWriter
|
||||
import logging
|
||||
from pathlib import Path
|
||||
@@ -72,7 +71,6 @@ class ReportMaker(object):
|
||||
for row in df.iterrows():
|
||||
# logger.debug(f"Row {ii}: {row}")
|
||||
lab = row[0][0]
|
||||
# logger.debug(type(row))
|
||||
# logger.debug(f"Old lab: {old_lab}, Current lab: {lab}")
|
||||
# logger.debug(f"Name: {row[0][1]}")
|
||||
data = [item for item in row[1]]
|
||||
@@ -151,7 +149,16 @@ class TurnaroundMaker(object):
|
||||
self.df = DataFrame.from_records(records)
|
||||
|
||||
@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()
|
||||
return dict(name=str(sub.rsl_plate_num), days=days, submitted_date=sub.submitted_date,
|
||||
completed_date=sub.completed_date, acceptable=tat_ok)
|
||||
|
||||
@@ -41,21 +41,11 @@ class SheetWriter(object):
|
||||
self.sub[k] = v['value']
|
||||
else:
|
||||
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
|
||||
if not template:
|
||||
logger.error(f"No template file found, falling back to Bacterial Culture")
|
||||
template = SubmissionType.retrieve_template_file()
|
||||
workbook = load_workbook(BytesIO(template))
|
||||
# self.workbook = workbook
|
||||
self.xl = workbook
|
||||
self.write_info()
|
||||
self.write_reagents()
|
||||
@@ -152,11 +142,9 @@ class InfoWriter(object):
|
||||
try:
|
||||
dicto['locations'] = info_map[k]
|
||||
except KeyError:
|
||||
# continue
|
||||
pass
|
||||
dicto['value'] = v
|
||||
if len(dicto) > 0:
|
||||
# output[k] = dicto
|
||||
yield k, dicto
|
||||
|
||||
def write_info(self) -> Workbook:
|
||||
@@ -279,7 +267,6 @@ class SampleWriter(object):
|
||||
self.sample_map = submission_type.construct_sample_map()['lookup_table']
|
||||
# NOTE: exclude any samples without a submission rank.
|
||||
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'))
|
||||
|
||||
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.debug(f"{equipment['role']} map: {mp_info}")
|
||||
placeholder = copy(equipment)
|
||||
if mp_info == {}:
|
||||
# if mp_info == {}:
|
||||
if not mp_info:
|
||||
for jj, (k, v) in enumerate(equipment.items(), start=1):
|
||||
dicto = dict(value=v, row=ii, column=jj)
|
||||
placeholder[k] = dicto
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
Contains pydantic models and accompanying validators
|
||||
'''
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
import uuid, re, logging, csv
|
||||
import uuid, re, logging, csv, sys
|
||||
from pydantic import BaseModel, field_validator, Field, model_validator
|
||||
from datetime import date, datetime, timedelta
|
||||
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"))
|
||||
else:
|
||||
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)
|
||||
# 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
|
||||
|
||||
|
||||
@@ -191,11 +184,7 @@ class PydSample(BaseModel, extra='allow'):
|
||||
for k, v in data.model_extra.items():
|
||||
if k in model.timestamps():
|
||||
if isinstance(v, str):
|
||||
# try:
|
||||
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)
|
||||
# logger.debug(f"Data coming out of validation: {pformat(data)}")
|
||||
return data
|
||||
@@ -379,7 +368,6 @@ class PydEquipment(BaseModel, extra='ignore'):
|
||||
role=self.role, limit=1)
|
||||
except TypeError as e:
|
||||
logger.error(f"Couldn't get association due to {e}, returning...")
|
||||
# return equipment, None
|
||||
assoc = None
|
||||
if assoc is None:
|
||||
assoc = SubmissionEquipmentAssociation(submission=submission, equipment=equipment)
|
||||
@@ -830,11 +818,6 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
logger.debug(f"Checking reagent {reagent.lot}")
|
||||
reagent, _ = reagent.toSQL(submission=instance)
|
||||
# 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":
|
||||
for sample in self.samples:
|
||||
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}")
|
||||
case item if item in instance.timestamps():
|
||||
logger.warning(f"Incoming timestamp key: {item}, with value: {value}")
|
||||
# value = value.replace(tzinfo=timezone)
|
||||
if isinstance(value, date):
|
||||
value = datetime.combine(value, datetime.min.time())
|
||||
value = value.replace(tzinfo=timezone)
|
||||
@@ -903,7 +885,6 @@ class PydSubmission(BaseModel, extra='allow'):
|
||||
if check:
|
||||
try:
|
||||
instance.set_attribute(key=key, value=value)
|
||||
# instance.update({key:value})
|
||||
except AttributeError as e:
|
||||
logger.error(f"Could not set attribute: {key} to {value} due to: \n\n {e}")
|
||||
continue
|
||||
|
||||
@@ -40,7 +40,6 @@ class IridaFigure(CustomFigure):
|
||||
Returns:
|
||||
Figure: output stacked bar chart.
|
||||
"""
|
||||
# fig = Figure()
|
||||
for ii, mode in enumerate(modes):
|
||||
if "count" in mode:
|
||||
df[mode] = pd.to_numeric(df[mode], errors='coerce')
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""
|
||||
Construct turnaround time charts
|
||||
"""
|
||||
from pprint import pformat
|
||||
from . import CustomFigure
|
||||
import plotly.express as px
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from PyQt6.QtWidgets import QWidget
|
||||
import logging
|
||||
|
||||
@@ -25,7 +27,6 @@ class TurnaroundChart(CustomFigure):
|
||||
self.construct_chart()
|
||||
if threshold:
|
||||
self.add_hline(y=threshold)
|
||||
# self.update_xaxes()
|
||||
self.update_layout(showlegend=False)
|
||||
|
||||
def construct_chart(self, df: pd.DataFrame | None = None):
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""
|
||||
Constructs main application.
|
||||
"""
|
||||
import os
|
||||
from pprint import pformat
|
||||
from PyQt6.QtCore import qInstallMessageHandler
|
||||
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 datetime import date
|
||||
from .pop_ups import HTMLPop, AlertPop
|
||||
from .misc import LogParser, Pagifier
|
||||
from .misc import Pagifier
|
||||
import logging, webbrowser, sys, shutil
|
||||
from .submission_table import SubmissionsSheet
|
||||
from .submission_widget import SubmissionFormContainer
|
||||
from .controls_chart import ControlsViewer
|
||||
# from .sample_search import SampleSearchBox
|
||||
from .summary import Summary
|
||||
from .turnaround import TurnaroundTime
|
||||
from .omni_search import SearchBox
|
||||
@@ -84,7 +82,7 @@ class App(QMainWindow):
|
||||
fileMenu.addAction(self.importAction)
|
||||
fileMenu.addAction(self.yamlExportAction)
|
||||
fileMenu.addAction(self.yamlImportAction)
|
||||
methodsMenu.addAction(self.searchLog)
|
||||
# methodsMenu.addAction(self.searchLog)
|
||||
methodsMenu.addAction(self.searchSample)
|
||||
maintenanceMenu.addAction(self.joinExtractionAction)
|
||||
maintenanceMenu.addAction(self.joinPCRAction)
|
||||
@@ -98,8 +96,8 @@ class App(QMainWindow):
|
||||
toolbar = QToolBar("My main toolbar")
|
||||
self.addToolBar(toolbar)
|
||||
toolbar.addAction(self.addReagentAction)
|
||||
toolbar.addAction(self.addKitAction)
|
||||
toolbar.addAction(self.addOrgAction)
|
||||
# toolbar.addAction(self.addKitAction)
|
||||
# toolbar.addAction(self.addOrgAction)
|
||||
|
||||
def _createActions(self):
|
||||
"""
|
||||
@@ -108,13 +106,13 @@ class App(QMainWindow):
|
||||
# logger.debug(f"Creating actions...")
|
||||
self.importAction = QAction("&Import Submission", self)
|
||||
self.addReagentAction = QAction("Add Reagent", self)
|
||||
self.addKitAction = QAction("Import Kit", self)
|
||||
self.addOrgAction = QAction("Import Org", self)
|
||||
# self.addKitAction = QAction("Import Kit", self)
|
||||
# self.addOrgAction = QAction("Import Org", self)
|
||||
self.joinExtractionAction = QAction("Link Extraction Logs")
|
||||
self.joinPCRAction = QAction("Link PCR Logs")
|
||||
self.helpAction = QAction("&About", 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.githubAction = QAction("Github", 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.helpAction.triggered.connect(self.showAbout)
|
||||
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.githubAction.triggered.connect(self.openGithub)
|
||||
self.yamlExportAction.triggered.connect(self.export_ST_yaml)
|
||||
self.yamlImportAction.triggered.connect(self.import_ST_yaml)
|
||||
self.table_widget.pager.current_page.textChanged.connect(self.update_data)
|
||||
self.editReagentAction.triggered.connect(self.edit_reagent)
|
||||
self.destroyed.connect(self.final_commit)
|
||||
|
||||
def showAbout(self):
|
||||
"""
|
||||
@@ -180,15 +177,14 @@ class App(QMainWindow):
|
||||
instr = HTMLPop(html=html, title="Instructions")
|
||||
instr.exec()
|
||||
|
||||
def runSearch(self):
|
||||
dlg = LogParser(self)
|
||||
dlg.exec()
|
||||
# def runSearch(self):
|
||||
# dlg = LogParser(self)
|
||||
# dlg.exec()
|
||||
|
||||
def runSampleSearch(self):
|
||||
"""
|
||||
Create a search for samples.
|
||||
"""
|
||||
# dlg = SampleSearchBox(self)
|
||||
dlg = SearchBox(self, object_type=BasicSample, extras=[])
|
||||
dlg.exec()
|
||||
|
||||
@@ -244,7 +240,6 @@ class App(QMainWindow):
|
||||
st = SubmissionType.import_from_json(filepath=fname)
|
||||
if st:
|
||||
# NOTE: Do not delete the print statement below.
|
||||
# print(pformat(st.to_export_dict()))
|
||||
choice = input("Save the above submission type? [y/N]: ")
|
||||
if choice.lower() == "y":
|
||||
pass
|
||||
@@ -254,9 +249,6 @@ class App(QMainWindow):
|
||||
def update_data(self):
|
||||
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):
|
||||
|
||||
|
||||
@@ -10,9 +10,10 @@ from PyQt6.QtWidgets import (
|
||||
from PyQt6.QtCore import QSignalBlocker
|
||||
from backend.db import ControlType, IridaControl
|
||||
import logging
|
||||
from tools import Report, report_result
|
||||
from tools import Report, report_result, Result
|
||||
from frontend.visualizations import CustomFigure
|
||||
from .misc import StartEndDatePicker
|
||||
from .info_tab import InfoPane
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
@@ -37,11 +38,11 @@ class ControlsViewer(QWidget):
|
||||
# NOTE: fetch types of controls
|
||||
con_sub_types = [item for item in self.archetype.targets.keys()]
|
||||
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()
|
||||
mode_types = IridaControl.get_modes()
|
||||
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.setEnabled(False)
|
||||
# NOTE: add widgets to tab2 layout
|
||||
@@ -83,15 +84,15 @@ class ControlsViewer(QWidget):
|
||||
pass
|
||||
# NOTE: correct start date being more recent than end date and rerun
|
||||
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)
|
||||
# NOTE: block signal that will rerun controls getter and set start date
|
||||
# Without triggering this function again
|
||||
msg = f"Start date after end date is not allowed! Setting to {threemonthsago.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(threemonthsago)
|
||||
self.controls_getter()
|
||||
self.report.add_result(report)
|
||||
return
|
||||
self.controls_getter_function()
|
||||
report.add_result(Result(owner=self.__str__(), msg=msg, status="Warning"))
|
||||
return report
|
||||
# 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()
|
||||
@@ -167,4 +168,3 @@ class ControlsViewer(QWidget):
|
||||
self.webengineview.update()
|
||||
# logger.debug("Figure updated... I hope.")
|
||||
return report
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'''
|
||||
Creates forms that the user can enter equipment info into.
|
||||
'''
|
||||
import time
|
||||
from pprint import pformat
|
||||
from PyQt6.QtCore import Qt, QSignalBlocker
|
||||
from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox,
|
||||
|
||||
@@ -66,7 +66,6 @@ class GelBox(QDialog):
|
||||
layout.addWidget(self.imv, 0, 1, 20, 20)
|
||||
# NOTE: setting this widget as central widget of the main window
|
||||
try:
|
||||
# control_info = sorted(self.submission.gel_controls, key=lambda d: d['location'])
|
||||
control_info = sorted(self.submission.gel_controls, key=itemgetter('location'))
|
||||
except KeyError:
|
||||
control_info = None
|
||||
@@ -79,7 +78,6 @@ class GelBox(QDialog):
|
||||
layout.addWidget(self.buttonBox, 23, 1, 1, 1)
|
||||
self.setLayout(layout)
|
||||
|
||||
|
||||
def parse_form(self) -> Tuple[str, str | Path, list]:
|
||||
"""
|
||||
Get relevant values from self/form
|
||||
@@ -124,7 +122,6 @@ class ControlsForm(QWidget):
|
||||
self.layout.addWidget(label, iii, 1, 1, 1)
|
||||
for iii in range(3):
|
||||
for jjj in range(3):
|
||||
# widge = QLineEdit()
|
||||
widge = QComboBox()
|
||||
widge.addItems(['Neg', 'Pos'])
|
||||
widge.setCurrentIndex(0)
|
||||
|
||||
@@ -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.QtWidgets import QWidget, QGridLayout, QPushButton
|
||||
from tools import Report
|
||||
from PyQt6.QtWidgets import QWidget, QGridLayout
|
||||
from tools import Report, report_result, Result
|
||||
from .misc import StartEndDatePicker, save_pdf
|
||||
from .functions import select_save_file
|
||||
import logging
|
||||
@@ -17,22 +22,29 @@ class InfoPane(QWidget):
|
||||
self.report = Report()
|
||||
self.datepicker = StartEndDatePicker(default_start=-31)
|
||||
self.webview = QWebEngineView()
|
||||
self.datepicker.start_date.dateChanged.connect(self.date_changed)
|
||||
self.datepicker.end_date.dateChanged.connect(self.date_changed)
|
||||
self.datepicker.start_date.dateChanged.connect(self.update_data)
|
||||
self.datepicker.end_date.dateChanged.connect(self.update_data)
|
||||
self.layout = QGridLayout(self)
|
||||
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.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.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):
|
||||
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")
|
||||
@@ -43,3 +55,9 @@ class InfoPane(QWidget):
|
||||
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)
|
||||
|
||||
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")
|
||||
@@ -14,8 +14,6 @@ from PyQt6.QtCore import Qt, QDate, QSize, QMarginsF
|
||||
from tools import jinja_template_loading
|
||||
from backend.db.models import *
|
||||
import logging
|
||||
from .pop_ups import AlertPop
|
||||
from .functions import select_open_file
|
||||
|
||||
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])))
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
custom widget to pick start and end dates for controls graphs
|
||||
|
||||
@@ -16,6 +16,9 @@ logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
|
||||
class SearchBox(QDialog):
|
||||
"""
|
||||
The full search widget.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, object_type: Any, extras: List[str], **kwargs):
|
||||
super().__init__(parent)
|
||||
@@ -36,7 +39,7 @@ class SearchBox(QDialog):
|
||||
else:
|
||||
self.sub_class = None
|
||||
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.setLayout(self.layout)
|
||||
self.setWindowTitle(f"Search {self.object_type.__name__}")
|
||||
@@ -51,6 +54,7 @@ class SearchBox(QDialog):
|
||||
# logger.debug(deletes)
|
||||
for item in deletes:
|
||||
item.setParent(None)
|
||||
# NOTE: Handle any subclasses
|
||||
if not self.sub_class:
|
||||
self.update_data()
|
||||
else:
|
||||
@@ -89,6 +93,9 @@ class SearchBox(QDialog):
|
||||
|
||||
|
||||
class FieldSearch(QWidget):
|
||||
"""
|
||||
Search bar.
|
||||
"""
|
||||
|
||||
def __init__(self, parent, label, field_name):
|
||||
super().__init__(parent)
|
||||
@@ -115,6 +122,9 @@ class FieldSearch(QWidget):
|
||||
|
||||
|
||||
class SearchResults(QTableView):
|
||||
"""
|
||||
Results table.
|
||||
"""
|
||||
|
||||
def __init__(self, parent: SearchBox, object_type: Any, extras: List[str], **kwargs):
|
||||
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}
|
||||
logger.debug(f"Context: {context}")
|
||||
try:
|
||||
# object = self.object_type.query(**{self.object_type.searchables: context[self.object_type.searchables]})
|
||||
object = self.object_type.query(**context)
|
||||
except KeyError:
|
||||
object = None
|
||||
|
||||
@@ -42,7 +42,7 @@ class SubmissionDetails(QDialog):
|
||||
# NOTE: button to export a pdf version
|
||||
self.btn = QPushButton("Export PDF")
|
||||
self.btn.setFixedWidth(775)
|
||||
self.btn.clicked.connect(self.export)
|
||||
self.btn.clicked.connect(self.save_pdf)
|
||||
self.back = QPushButton("Back")
|
||||
self.back.setFixedWidth(100)
|
||||
# self.back.clicked.connect(self.back_function)
|
||||
@@ -181,7 +181,7 @@ class SubmissionDetails(QDialog):
|
||||
submission.save()
|
||||
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.
|
||||
"""
|
||||
|
||||
@@ -83,6 +83,7 @@ class SubmissionsSheet(QTableView):
|
||||
self.resizeRowsToContents()
|
||||
self.setSortingEnabled(True)
|
||||
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()
|
||||
|
||||
def setData(self, page: int = 1, page_size: int = 250) -> None:
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
'''
|
||||
Contains all submission related frontend functions
|
||||
'''
|
||||
import sys
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QPushButton, QVBoxLayout,
|
||||
QComboBox, QDateEdit, QLineEdit, QLabel
|
||||
)
|
||||
from PyQt6.QtCore import pyqtSignal, Qt
|
||||
from . import select_open_file, select_save_file
|
||||
import logging, difflib
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from tools import Report, Result, check_not_nan, main_form_style, report_result
|
||||
from backend.excel.parser import SheetParser
|
||||
@@ -187,6 +185,8 @@ class SubmissionFormContainer(QWidget):
|
||||
|
||||
class SubmissionFormWidget(QWidget):
|
||||
|
||||
update_reagent_fields = ['extraction_kit']
|
||||
|
||||
def __init__(self, parent: QWidget, submission: PydSubmission, disable: list | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
# 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']}")
|
||||
self.layout = QVBoxLayout()
|
||||
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:
|
||||
logger.warning(f"{k} in form_ignore {self.ignore}, not creating widget")
|
||||
continue
|
||||
@@ -225,10 +225,10 @@ class SubmissionFormWidget(QWidget):
|
||||
sub_obj=st, disable=check)
|
||||
if add_widget is not None:
|
||||
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)
|
||||
self.setStyleSheet(main_form_style)
|
||||
# self.scrape_reagents(self.pyd.extraction_kit)
|
||||
self.scrape_reagents(self.extraction_kit)
|
||||
|
||||
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):
|
||||
reagent.setParent(None)
|
||||
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:
|
||||
# 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)
|
||||
self.layout.addWidget(add_widget)
|
||||
report.add_result(integrity_report)
|
||||
@@ -334,10 +333,6 @@ class SubmissionFormWidget(QWidget):
|
||||
query = [widget for widget in query if widget.objectName() == object_name]
|
||||
return query
|
||||
|
||||
# def update_pyd(self):
|
||||
# results = self.parse_form()
|
||||
# logger.debug(pformat(results))
|
||||
|
||||
@report_result
|
||||
def submit_new_sample_function(self, *args) -> Report:
|
||||
"""
|
||||
@@ -538,19 +533,14 @@ class SubmissionFormWidget(QWidget):
|
||||
except (TypeError, KeyError):
|
||||
pass
|
||||
obj = parent.parent().parent()
|
||||
logger.debug(f"Object: {obj}")
|
||||
logger.debug(f"Parent: {parent.parent()}")
|
||||
# logger.debug(f"Object: {obj}")
|
||||
# logger.debug(f"Parent: {parent.parent()}")
|
||||
# logger.debug(f"Creating widget for: {key}")
|
||||
match key:
|
||||
case 'submitting_lab':
|
||||
add_widget = MyQComboBox(scrollWidget=parent)
|
||||
# NOTE: lookup organizations suitable for submitting_lab (ctx: self.InfoItem.SubmissionFormWidget.SubmissionFormContainer.AddSubForm )
|
||||
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):
|
||||
value = value['value']
|
||||
if isinstance(value, Organization):
|
||||
@@ -559,7 +549,7 @@ class SubmissionFormWidget(QWidget):
|
||||
looked_up_lab = Organization.query(name=value, limit=1)
|
||||
except AttributeError:
|
||||
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:
|
||||
try:
|
||||
labs.remove(str(looked_up_lab.name))
|
||||
@@ -579,7 +569,6 @@ class SubmissionFormWidget(QWidget):
|
||||
add_widget = MyQComboBox(scrollWidget=parent)
|
||||
# NOTE: lookup existing kits by 'submission_type' decided on by sheetparser
|
||||
# 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]
|
||||
obj.uses = uses
|
||||
# logger.debug(f"Kits received for {submission_type}: {uses}")
|
||||
|
||||
@@ -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 PyQt6.QtWidgets import QWidget, QGridLayout, QPushButton, QLabel
|
||||
from PyQt6.QtWidgets import QWidget, QLabel, QPushButton
|
||||
from backend.db import Organization
|
||||
from backend.excel import ReportMaker
|
||||
from tools import Report
|
||||
from .misc import StartEndDatePicker, save_pdf, CheckableComboBox
|
||||
from .functions import select_save_file
|
||||
from .misc import CheckableComboBox
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
@@ -16,32 +15,27 @@ class Summary(InfoPane):
|
||||
|
||||
def __init__(self, parent: QWidget) -> None:
|
||||
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.setEditable(False)
|
||||
self.org_select.addItem("Select", header=True)
|
||||
for org in [org.name for org in Organization.query()]:
|
||||
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(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)]
|
||||
if self.datepicker.start_date.date() > self.datepicker.end_date.date():
|
||||
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}")
|
||||
# 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.webview.setHtml(self.report_obj.html)
|
||||
if self.report_obj.subs:
|
||||
@@ -50,13 +44,3 @@ class Summary(InfoPane):
|
||||
else:
|
||||
self.save_pdf_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)
|
||||
|
||||
@@ -15,28 +15,25 @@ class TurnaroundTime(InfoPane):
|
||||
|
||||
def __init__(self, parent: QWidget):
|
||||
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.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.layout.addWidget(self.submission_typer, 1, 1, 1, 3)
|
||||
self.submission_typer.currentTextChanged.connect(self.date_changed)
|
||||
self.date_changed()
|
||||
self.submission_typer.currentTextChanged.connect(self.update_data)
|
||||
self.update_data()
|
||||
|
||||
def date_changed(self):
|
||||
if self.datepicker.start_date.date() > self.datepicker.end_date.date():
|
||||
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()
|
||||
def update_data(self):
|
||||
super().update_data()
|
||||
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
|
||||
subtype_obj = None
|
||||
else:
|
||||
@@ -47,5 +44,5 @@ class TurnaroundTime(InfoPane):
|
||||
threshold = subtype_obj.defaults['turnaround_time'] + 0.5
|
||||
else:
|
||||
threshold = None
|
||||
self.chart = TurnaroundChart(df=self.report_obj.df, settings=chart_settings, modes=[], threshold=threshold)
|
||||
self.webview.setHtml(self.chart.to_html())
|
||||
self.fig = TurnaroundChart(df=self.report_obj.df, settings=chart_settings, modes=[], threshold=threshold)
|
||||
self.webview.setHtml(self.fig.to_html())
|
||||
|
||||
@@ -3,13 +3,11 @@ Contains miscellaenous functions used by both frontend and backend.
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import pprint
|
||||
from datetime import date, datetime, timedelta
|
||||
from json import JSONDecodeError
|
||||
from pprint import pprint
|
||||
import numpy as np
|
||||
import logging, re, yaml, sys, os, stat, platform, getpass, inspect
|
||||
import pandas as pd
|
||||
import logging, re, yaml, sys, os, stat, platform, getpass, inspect, json, pandas as pd
|
||||
from dateutil.easter import easter
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from logging import handlers
|
||||
@@ -20,7 +18,6 @@ from sqlalchemy import create_engine, text, MetaData
|
||||
from pydantic import field_validator, BaseModel, Field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from typing import Any, Tuple, Literal, List
|
||||
# print(inspect.stack()[1])
|
||||
from __init__ import project_path
|
||||
from configparser import ConfigParser
|
||||
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
|
||||
database_session: Session | None = None
|
||||
package: Any | None = None
|
||||
logging_enabled: bool = Field(default=False)
|
||||
|
||||
model_config = SettingsConfigDict(env_file_encoding='utf-8')
|
||||
|
||||
@@ -422,6 +420,7 @@ class Settings(BaseSettings, extra="allow"):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
self.set_from_db()
|
||||
pprint(f"User settings:\n{self.__dict__}")
|
||||
|
||||
def set_from_db(self):
|
||||
if 'pytest' in sys.modules:
|
||||
@@ -819,7 +818,7 @@ class Result(BaseModel, arbitrary_types_allowed=True):
|
||||
self.owner = inspect.stack()[1].function
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user