Moments before disaster.
This commit is contained in:
@@ -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
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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]:
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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}")
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user