documentation and converted to username based exclusion of adding new kits

This commit is contained in:
Landon Wark
2023-01-30 12:07:38 -06:00
parent bbb65d3fe6
commit 1f832dccf2
16 changed files with 876 additions and 296 deletions

View File

@@ -1,4 +1,4 @@
# __init__.py # __init__.py
# Version of the realpython-reader package # Version of the realpython-reader package
__version__ = "1.0.0" __version__ = "1.1.2"

View File

@@ -1,19 +1,27 @@
import sys import sys
from pathlib import Path from pathlib import Path
import os import os
# must be set to enable qtwebengine in network path
if getattr(sys, 'frozen', False): if getattr(sys, 'frozen', False):
os.environ['QTWEBENGINE_DISABLE_SANDBOX'] = "1" os.environ['QTWEBENGINE_DISABLE_SANDBOX'] = "1"
else : else :
pass pass
from configure import get_config, create_database_session, setup_logger from configure import get_config, create_database_session, setup_logger
# setup custom logger
logger = setup_logger(verbosity=3) logger = setup_logger(verbosity=3)
# import config
ctx = get_config(None) ctx = get_config(None)
from PyQt6.QtWidgets import QApplication from PyQt6.QtWidgets import QApplication
from frontend import App from frontend import App
import __init__ as package
# create database session for use with gui session
ctx["database_session"] = create_database_session(Path(ctx['database'])) ctx["database_session"] = create_database_session(Path(ctx['database']))
# set package information fro __init__
ctx['package'] = package
if __name__ == '__main__': if __name__ == '__main__':
#
app = QApplication(['', '--no-sandbox']) app = QApplication(['', '--no-sandbox'])
ex = App(ctx=ctx) ex = App(ctx=ctx)
sys.exit(app.exec()) sys.exit(app.exec())

View File

@@ -12,6 +12,7 @@ import base64
from sqlalchemy import JSON from sqlalchemy import JSON
import json import json
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from getpass import getuser
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -20,8 +21,19 @@ def get_kits_by_use( ctx:dict, kittype_str:str|None) -> list:
# ctx dict should contain the database session # ctx dict should contain the database session
def store_submission(ctx:dict, base_submission:models.BasicSubmission) -> None: def store_submission(ctx:dict, base_submission:models.BasicSubmission) -> None|dict:
"""
Upserts submissions into database
Args:
ctx (dict): settings passed down from gui
base_submission (models.BasicSubmission): submission to be add to db
Returns:
None|dict : object that indicates issue raised for reporting in gui
"""
logger.debug(f"Hello from store_submission") logger.debug(f"Hello from store_submission")
# Add all samples to sample table
for sample in base_submission.samples: for sample in base_submission.samples:
sample.rsl_plate = base_submission sample.rsl_plate = base_submission
logger.debug(f"Attempting to add sample: {sample.to_string()}") logger.debug(f"Attempting to add sample: {sample.to_string()}")
@@ -30,6 +42,7 @@ def store_submission(ctx:dict, base_submission:models.BasicSubmission) -> None:
except (sqlite3.IntegrityError, sqlalchemy.exc.IntegrityError) as e: except (sqlite3.IntegrityError, sqlalchemy.exc.IntegrityError) as e:
logger.debug(f"Hit an integrity error : {e}") logger.debug(f"Hit an integrity error : {e}")
continue continue
# Add submission to submission table
ctx['database_session'].add(base_submission) ctx['database_session'].add(base_submission)
logger.debug(f"Attempting to add submission: {base_submission.rsl_plate_num}") logger.debug(f"Attempting to add submission: {base_submission.rsl_plate_num}")
try: try:
@@ -45,26 +58,51 @@ def store_submission(ctx:dict, base_submission:models.BasicSubmission) -> None:
return None return None
def store_reagent(ctx:dict, reagent:models.Reagent) -> None: def store_reagent(ctx:dict, reagent:models.Reagent) -> None|dict:
"""
_summary_
Args:
ctx (dict): settings passed down from gui
reagent (models.Reagent): Reagent object to be added to db
Returns:
None|dict: obejct indicating issue to be reported in the gui
"""
logger.debug(reagent.__dict__) logger.debug(reagent.__dict__)
ctx['database_session'].add(reagent) ctx['database_session'].add(reagent)
try: try:
ctx['database_session'].commit() ctx['database_session'].commit()
except OperationalError: except (sqlite3.OperationalError, sqlalchemy.exc.OperationalError):
return {"message":"The database is locked for editing."} return {"message":"The database is locked for editing."}
def construct_submission_info(ctx:dict, info_dict:dict) -> models.BasicSubmission: def construct_submission_info(ctx:dict, info_dict:dict) -> models.BasicSubmission:
"""
Construct submission obejct from dictionary
Args:
ctx (dict): settings passed down from gui
info_dict (dict): dictionary to be transformed
Returns:
models.BasicSubmission: Constructed submission object
"""
# convert submission type into model name
query = info_dict['submission_type'].replace(" ", "") query = info_dict['submission_type'].replace(" ", "")
# check database for existing object
instance = ctx['database_session'].query(models.BasicSubmission).filter(models.BasicSubmission.rsl_plate_num==info_dict['rsl_plate_num']).first() instance = ctx['database_session'].query(models.BasicSubmission).filter(models.BasicSubmission.rsl_plate_num==info_dict['rsl_plate_num']).first()
msg = "This submission already exists.\nWould you like to overwrite?" msg = "This submission already exists.\nWould you like to overwrite?"
# get model based on submission type converted above
model = getattr(models, query) model = getattr(models, query)
info_dict['submission_type'] = info_dict['submission_type'].replace(" ", "_").lower() info_dict['submission_type'] = info_dict['submission_type'].replace(" ", "_").lower()
# if query return nothing, ie doesn't already exist in db
if instance == None: if instance == None:
instance = model() instance = model()
msg = None msg = None
for item in info_dict: for item in info_dict:
logger.debug(f"Setting {item} to {info_dict[item]}") logger.debug(f"Setting {item} to {info_dict[item]}")
# set fields based on keys in dictionary
match item: match item:
case "extraction_kit": case "extraction_kit":
q_str = info_dict[item] q_str = info_dict[item]
@@ -86,35 +124,34 @@ def construct_submission_info(ctx:dict, info_dict:dict) -> models.BasicSubmissio
logger.debug(f"Got None as a submitter plate number, inserting random string to preserve database unique constraint.") logger.debug(f"Got None as a submitter plate number, inserting random string to preserve database unique constraint.")
info_dict[item] = uuid.uuid4().hex.upper() info_dict[item] = uuid.uuid4().hex.upper()
field_value = info_dict[item] field_value = info_dict[item]
# case "samples":
# for sample in info_dict[item]:
# instance.samples.append(sample)
# continue
case _: case _:
field_value = info_dict[item] field_value = info_dict[item]
# insert into field
try: try:
setattr(instance, item, field_value) setattr(instance, item, field_value)
except AttributeError: except AttributeError:
logger.debug(f"Could not set attribute: {item} to {info_dict[item]}") logger.debug(f"Could not set attribute: {item} to {info_dict[item]}")
continue continue
# logger.debug(instance.__dict__)
logger.debug(f"Constructed instance: {instance.to_string()}") logger.debug(f"Constructed instance: {instance.to_string()}")
logger.debug(msg) logger.debug(msg)
return instance, {'message':msg} return instance, {'message':msg}
# looked_up = []
# for reagent in reagents:
# my_reagent = lookup_reagent(reagent)
# logger.debug(my_reagent)
# looked_up.append(my_reagent)
# logger.debug(looked_up)
# instance.reagents = looked_up
# ctx['database_session'].add(instance)
# ctx['database_session'].commit()
def construct_reagent(ctx:dict, info_dict:dict) -> models.Reagent: def construct_reagent(ctx:dict, info_dict:dict) -> models.Reagent:
"""
Construct reagent object from dictionary
Args:
ctx (dict): settings passed down from gui
info_dict (dict): dictionary to be converted
Returns:
models.Reagent: Constructed reagent object
"""
reagent = models.Reagent() reagent = models.Reagent()
for item in info_dict: for item in info_dict:
logger.debug(f"Reagent info item: {item}") logger.debug(f"Reagent info item: {item}")
# set fields based on keys in dictionary
match item: match item:
case "lot": case "lot":
reagent.lot = info_dict[item].upper() reagent.lot = info_dict[item].upper()
@@ -122,25 +159,55 @@ def construct_reagent(ctx:dict, info_dict:dict) -> models.Reagent:
reagent.expiry = info_dict[item] reagent.expiry = info_dict[item]
case "type": case "type":
reagent.type = lookup_reagenttype_by_name(ctx=ctx, rt_name=info_dict[item].replace(" ", "_").lower()) reagent.type = lookup_reagenttype_by_name(ctx=ctx, rt_name=info_dict[item].replace(" ", "_").lower())
# add end-of-life extension from reagent type to expiry date
try: try:
reagent.expiry = reagent.expiry + reagent.type.eol_ext reagent.expiry = reagent.expiry + reagent.type.eol_ext
except TypeError as e: except TypeError as e:
logger.debug(f"WE got a type error: {e}.") logger.debug(f"We got a type error: {e}.")
except AttributeError: except AttributeError:
pass pass
return reagent return reagent
def lookup_reagent(ctx:dict, reagent_lot:str): def lookup_reagent(ctx:dict, reagent_lot:str) -> models.Reagent:
"""
Query db for reagent based on lot number
Args:
ctx (dict): settings passed down from gui
reagent_lot (str): lot number to query
Returns:
models.Reagent: looked up reagent
"""
lookedup = ctx['database_session'].query(models.Reagent).filter(models.Reagent.lot==reagent_lot).first() lookedup = ctx['database_session'].query(models.Reagent).filter(models.Reagent.lot==reagent_lot).first()
return lookedup return lookedup
def get_all_reagenttype_names(ctx:dict) -> list[str]: def get_all_reagenttype_names(ctx:dict) -> list[str]:
"""
Lookup all reagent types and get names
Args:
ctx (dict): settings passed from gui
Returns:
list[str]: reagent type names
"""
lookedup = [item.__str__() for item in ctx['database_session'].query(models.ReagentType).all()] lookedup = [item.__str__() for item in ctx['database_session'].query(models.ReagentType).all()]
return lookedup return lookedup
def lookup_reagenttype_by_name(ctx:dict, rt_name:str) -> models.ReagentType: def lookup_reagenttype_by_name(ctx:dict, rt_name:str) -> models.ReagentType:
"""
Lookup a single reagent type by name
Args:
ctx (dict): settings passed from gui
rt_name (str): reagent type name to look up
Returns:
models.ReagentType: looked up reagent type
"""
logger.debug(f"Looking up ReagentType by name: {rt_name}") logger.debug(f"Looking up ReagentType by name: {rt_name}")
lookedup = ctx['database_session'].query(models.ReagentType).filter(models.ReagentType.name==rt_name).first() lookedup = ctx['database_session'].query(models.ReagentType).filter(models.ReagentType.name==rt_name).first()
logger.debug(f"Found ReagentType: {lookedup}") logger.debug(f"Found ReagentType: {lookedup}")
@@ -148,27 +215,78 @@ def lookup_reagenttype_by_name(ctx:dict, rt_name:str) -> models.ReagentType:
def lookup_kittype_by_use(ctx:dict, used_by:str) -> list[models.KitType]: def lookup_kittype_by_use(ctx:dict, used_by:str) -> list[models.KitType]:
# return [item for item in """
Lookup a kit by an sample type its used for
Args:
ctx (dict): settings passed from gui
used_by (str): sample type (should be string in D3 of excel sheet)
Returns:
list[models.KitType]: list of kittypes that have that sample type in their uses
"""
return ctx['database_session'].query(models.KitType).filter(models.KitType.used_for.contains(used_by)) return ctx['database_session'].query(models.KitType).filter(models.KitType.used_for.contains(used_by))
def lookup_kittype_by_name(ctx:dict, name:str) -> models.KitType: def lookup_kittype_by_name(ctx:dict, name:str) -> models.KitType:
"""
Lookup a kit type by name
Args:
ctx (dict): settings passed from bui
name (str): name of kit to query
Returns:
models.KitType: retrieved kittype
"""
logger.debug(f"Querying kittype: {name}") logger.debug(f"Querying kittype: {name}")
return ctx['database_session'].query(models.KitType).filter(models.KitType.name==name).first() return ctx['database_session'].query(models.KitType).filter(models.KitType.name==name).first()
def lookup_regent_by_type_name(ctx:dict, type_name:str) -> list[models.ReagentType]: def lookup_regent_by_type_name(ctx:dict, type_name:str) -> list[models.Reagent]:
"""
Lookup reagents by their type name
Args:
ctx (dict): settings passed from gui
type_name (str): reagent type name
Returns:
list[models.Reagent]: list of retrieved reagents
"""
# return [item for item in ctx['database_session'].query(models.Reagent).join(models.Reagent.type, aliased=True).filter(models.ReagentType.name==type_name).all()] # return [item for item in ctx['database_session'].query(models.Reagent).join(models.Reagent.type, aliased=True).filter(models.ReagentType.name==type_name).all()]
return ctx['database_session'].query(models.Reagent).join(models.Reagent.type, aliased=True).filter(models.ReagentType.name==type_name).all() return ctx['database_session'].query(models.Reagent).join(models.Reagent.type, aliased=True).filter(models.ReagentType.name==type_name).all()
def lookup_regent_by_type_name_and_kit_name(ctx:dict, type_name:str, kit_name:str) -> list[models.Reagent]: def lookup_regent_by_type_name_and_kit_name(ctx:dict, type_name:str, kit_name:str) -> list[models.Reagent]:
"""
Lookup reagents by their type name and kits they belong to
Args:
ctx (dict): settings pass by gui
type_name (str): reagent type name
kit_name (str): kit name
Returns:
list[models.Reagent]: list of retrieved reagents
"""
# Hang on, this is going to be a long one. # Hang on, this is going to be a long one.
by_type = ctx['database_session'].query(models.Reagent).join(models.Reagent.type, aliased=True).filter(models.ReagentType.name.endswith(type_name)) by_type = ctx['database_session'].query(models.Reagent).join(models.Reagent.type, aliased=True).filter(models.ReagentType.name.endswith(type_name))
# add filter for kit name
add_in = by_type.join(models.ReagentType.kits).filter(models.KitType.name==kit_name) add_in = by_type.join(models.ReagentType.kits).filter(models.KitType.name==kit_name)
return add_in return add_in
def lookup_all_submissions_by_type(ctx:dict, type:str|None=None): def lookup_all_submissions_by_type(ctx:dict, type:str|None=None) -> list[models.BasicSubmission]:
"""
Get all submissions, filtering by type if given
Args:
ctx (dict): settings pass from gui
type (str | None, optional): submission type (should be string in D3 of excel sheet). Defaults to None.
Returns:
_type_: list of retrieved submissions
"""
if type == None: if type == None:
subs = ctx['database_session'].query(models.BasicSubmission).all() subs = ctx['database_session'].query(models.BasicSubmission).all()
else: else:
@@ -176,20 +294,60 @@ def lookup_all_submissions_by_type(ctx:dict, type:str|None=None):
return subs return subs
def lookup_all_orgs(ctx:dict) -> list[models.Organization]: def lookup_all_orgs(ctx:dict) -> list[models.Organization]:
"""
Lookup all organizations (labs)
Args:
ctx (dict): settings passed from gui
Returns:
list[models.Organization]: list of retrieved organizations
"""
return ctx['database_session'].query(models.Organization).all() return ctx['database_session'].query(models.Organization).all()
def lookup_org_by_name(ctx:dict, name:str|None) -> models.Organization: def lookup_org_by_name(ctx:dict, name:str|None) -> models.Organization:
"""
Lookup organization (lab) by name.
Args:
ctx (dict): settings passed from gui
name (str | None): name of organization
Returns:
models.Organization: retrieved organization
"""
logger.debug(f"Querying organization: {name}") logger.debug(f"Querying organization: {name}")
return ctx['database_session'].query(models.Organization).filter(models.Organization.name==name).first() return ctx['database_session'].query(models.Organization).filter(models.Organization.name==name).first()
def submissions_to_df(ctx:dict, type:str|None=None): def submissions_to_df(ctx:dict, type:str|None=None) -> pd.DataFrame:
"""
Convert submissions looked up by type to dataframe
Args:
ctx (dict): settings passed by gui
type (str | None, optional): submission type (should be string in D3 of excel sheet) Defaults to None.
Returns:
pd.DataFrame: dataframe constructed from retrieved submissions
"""
logger.debug(f"Type: {type}") logger.debug(f"Type: {type}")
# pass to lookup function
subs = [item.to_dict() for item in lookup_all_submissions_by_type(ctx=ctx, type=type)] subs = [item.to_dict() for item in lookup_all_submissions_by_type(ctx=ctx, type=type)]
df = pd.DataFrame.from_records(subs) df = pd.DataFrame.from_records(subs)
return df return df
def lookup_submission_by_id(ctx:dict, id:int) -> models.BasicSubmission: def lookup_submission_by_id(ctx:dict, id:int) -> models.BasicSubmission:
"""
Lookup submission by id number
Args:
ctx (dict): settings passed from gui
id (int): submission id number
Returns:
models.BasicSubmission: retrieved submission
"""
return ctx['database_session'].query(models.BasicSubmission).filter(models.BasicSubmission.id==id).first() return ctx['database_session'].query(models.BasicSubmission).filter(models.BasicSubmission.id==id).first()
@@ -198,6 +356,17 @@ def create_submission_details(ctx:dict, sub_id:int) -> dict:
def lookup_submissions_by_date_range(ctx:dict, start_date:datetime.date, end_date:datetime.date) -> list[models.BasicSubmission]: def lookup_submissions_by_date_range(ctx:dict, start_date:datetime.date, end_date:datetime.date) -> list[models.BasicSubmission]:
"""
Lookup submissions by range of submitted dates
Args:
ctx (dict): settings passed from gui
start_date (datetime.date): date to start looking
end_date (datetime.date): date to end looking
Returns:
list[models.BasicSubmission]: list of retrieved submissions
"""
return ctx['database_session'].query(models.BasicSubmission).filter(and_(models.BasicSubmission.submitted_date > start_date, models.BasicSubmission.submitted_date < end_date)).all() return ctx['database_session'].query(models.BasicSubmission).filter(and_(models.BasicSubmission.submitted_date > start_date, models.BasicSubmission.submitted_date < end_date)).all()
@@ -226,12 +395,13 @@ def create_kit_from_yaml(ctx:dict, exp:dict) -> None:
exp (dict): Experiment dictionary created from yaml file exp (dict): Experiment dictionary created from yaml file
""" """
try: try:
exp['password'].decode() super_users = ctx['super_users']
except (UnicodeDecodeError, AttributeError): except KeyError:
exp['password'] = exp['password'].encode() logger.debug("This user does not have permission to add kits.")
if base64.b64encode(exp['password']) != b'cnNsX3N1Ym1pNTVpb25z': return {'code':1,'message':"This user does not have permission to add kits."}
logger.debug(f"Not the correct password.") if getuser not in super_users:
return logger.debug("This user does not have permission to add kits.")
return {'code':1, 'message':"This user does not have permission to add kits."}
for type in exp: for type in exp:
if type == "password": if type == "password":
continue continue
@@ -249,9 +419,19 @@ def create_kit_from_yaml(ctx:dict, exp:dict) -> None:
logger.debug(kit.__dict__) logger.debug(kit.__dict__)
ctx['database_session'].add(kit) ctx['database_session'].add(kit)
ctx['database_session'].commit() ctx['database_session'].commit()
return {'code':0, 'message':'Kit has been added'}
def lookup_all_sample_types(ctx:dict) -> list[str]: def lookup_all_sample_types(ctx:dict) -> list[str]:
"""
Lookup all sample types and get names
Args:
ctx (dict): settings pass from gui
Returns:
list[str]: list of sample type names
"""
uses = [item.used_for for item in ctx['database_session'].query(models.KitType).all()] uses = [item.used_for for item in ctx['database_session'].query(models.KitType).all()]
uses = list(set([item for sublist in uses for item in sublist])) uses = list(set([item for sublist in uses for item in sublist]))
return uses return uses
@@ -259,6 +439,15 @@ def lookup_all_sample_types(ctx:dict) -> list[str]:
def get_all_available_modes(ctx:dict) -> list[str]: def get_all_available_modes(ctx:dict) -> list[str]:
"""
Get types of analysis for controls
Args:
ctx (dict): settings passed from gui
Returns:
list[str]: list of analysis types
"""
rel = ctx['database_session'].query(models.Control).first() rel = ctx['database_session'].query(models.Control).first()
try: try:
cols = [item.name for item in list(rel.__table__.columns) if isinstance(item.type, JSON)] cols = [item.name for item in list(rel.__table__.columns) if isinstance(item.type, JSON)]
@@ -294,7 +483,18 @@ def get_all_controls_by_type(ctx:dict, con_type:str, start_date:date|None=None,
return output return output
def get_control_subtypes(ctx:dict, type:str, mode:str): def get_control_subtypes(ctx:dict, type:str, mode:str) -> list[str]:
"""
Get subtypes for a control analysis type
Args:
ctx (dict): settings passed from gui
type (str): control type name
mode (str): analysis type name
Returns:
list[str]: list of subtype names
"""
try: try:
outs = get_all_controls_by_type(ctx=ctx, con_type=type)[0] outs = get_all_controls_by_type(ctx=ctx, con_type=type)[0]
except TypeError: except TypeError:

View File

@@ -1,5 +1,5 @@
from . import Base from . import Base
from sqlalchemy import Column, String, TIMESTAMP, text, JSON, INTEGER, ForeignKey, UniqueConstraint from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
class ControlType(Base): class ControlType(Base):
@@ -32,6 +32,6 @@ class Control(Base):
matches = Column(JSON) #: unstructured hashes in matches.tsv for each organism matches = Column(JSON) #: unstructured hashes in matches.tsv for each organism
kraken = Column(JSON) #: unstructured output from kraken_report kraken = Column(JSON) #: unstructured output from kraken_report
# UniqueConstraint('name', name='uq_control_name') # UniqueConstraint('name', name='uq_control_name')
submission_id = Column(INTEGER, ForeignKey("_submissions.id")) submission_id = Column(INTEGER, ForeignKey("_submissions.id")) #: parent submission id
submission = relationship("BacterialCulture", back_populates="controls", foreign_keys=[submission_id]) submission = relationship("BacterialCulture", back_populates="controls", foreign_keys=[submission_id]) #: parent submission

View File

@@ -3,57 +3,88 @@ from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Int
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
# Table containing reagenttype-kittype relationships
reagenttypes_kittypes = Table("_reagentstypes_kittypes", Base.metadata, Column("reagent_types_id", INTEGER, ForeignKey("_reagent_types.id")), Column("kits_id", INTEGER, ForeignKey("_kits.id"))) reagenttypes_kittypes = Table("_reagentstypes_kittypes", Base.metadata, Column("reagent_types_id", INTEGER, ForeignKey("_reagent_types.id")), Column("kits_id", INTEGER, ForeignKey("_kits.id")))
class KitType(Base): class KitType(Base):
"""
Base of kits used in submission processing
"""
__tablename__ = "_kits" __tablename__ = "_kits"
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64), unique=True) name = Column(String(64), unique=True) #: name of kit
submissions = relationship("BasicSubmission", back_populates="extraction_kit") submissions = relationship("BasicSubmission", back_populates="extraction_kit") #: submissions this kit was used for
used_for = Column(JSON) used_for = Column(JSON) #: list of names of sample types this kit can process
cost_per_run = Column(FLOAT(2)) cost_per_run = Column(FLOAT(2)) #: dollar amount for each full run of this kit
reagent_types = relationship("ReagentType", back_populates="kits", uselist=True, secondary=reagenttypes_kittypes) reagent_types = relationship("ReagentType", back_populates="kits", uselist=True, secondary=reagenttypes_kittypes) #: reagent types this kit contains
reagent_types_id = Column(INTEGER, ForeignKey("_reagent_types.id", ondelete='SET NULL', use_alter=True, name="fk_KT_reagentstype_id")) reagent_types_id = Column(INTEGER, ForeignKey("_reagent_types.id", ondelete='SET NULL', use_alter=True, name="fk_KT_reagentstype_id")) #: joined reagent type id
def __str__(self): def __str__(self) -> str:
"""
a string representing this object
Returns:
str: a string representing this object's name
"""
return self.name return self.name
class ReagentType(Base): class ReagentType(Base):
"""
Base of reagent type abstract
"""
__tablename__ = "_reagent_types" __tablename__ = "_reagent_types"
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) name = Column(String(64)) #: name of reagent type
kit_id = Column(INTEGER, ForeignKey("_kits.id", ondelete="SET NULL", use_alter=True, name="fk_RT_kits_id")) kit_id = Column(INTEGER, ForeignKey("_kits.id", ondelete="SET NULL", use_alter=True, name="fk_RT_kits_id")) #: id of joined kit type
kits = relationship("KitType", back_populates="reagent_types", uselist=True, foreign_keys=[kit_id]) kits = relationship("KitType", back_populates="reagent_types", uselist=True, foreign_keys=[kit_id]) #: kits this reagent is used in
instances = relationship("Reagent", back_populates="type") instances = relationship("Reagent", back_populates="type") #: concrete instances of this reagent type
# instances_id = Column(INTEGER, ForeignKey("_reagents.id", ondelete='SET NULL')) # instances_id = Column(INTEGER, ForeignKey("_reagents.id", ondelete='SET NULL'))
eol_ext = Column(Interval()) eol_ext = Column(Interval()) #: extension of life interval
def __str__(self): def __str__(self) -> str:
"""
string representing this object
Returns:
str: string representing this object's name
"""
return self.name return self.name
class Reagent(Base): class Reagent(Base):
"""
Concrete reagent instance
"""
__tablename__ = "_reagents" __tablename__ = "_reagents"
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
type = relationship("ReagentType", back_populates="instances") type = relationship("ReagentType", back_populates="instances") #: joined parent reagent type
type_id = Column(INTEGER, ForeignKey("_reagent_types.id", ondelete='SET NULL', name="fk_reagent_type_id")) type_id = Column(INTEGER, ForeignKey("_reagent_types.id", ondelete='SET NULL', name="fk_reagent_type_id")) #: id of parent reagent type
name = Column(String(64)) name = Column(String(64)) #: reagent name
lot = Column(String(64)) lot = Column(String(64)) #: lot number of reagent
expiry = Column(TIMESTAMP) expiry = Column(TIMESTAMP) #: expiry date - extended by eol_ext of parent programmatically
submissions = relationship("BasicSubmission", back_populates="reagents", uselist=True) submissions = relationship("BasicSubmission", back_populates="reagents", uselist=True) #: submissions this reagent is used in
def __str__(self): def __str__(self) -> str:
"""
string representing this object
Returns:
str: string representing this object's lot number
"""
return self.lot return self.lot
def to_sub_dict(self): def to_sub_dict(self) -> dict:
"""
dictionary containing values necessary for gui
Returns:
dict: gui friendly dictionary
"""
try: try:
type = self.type.name.replace("_", " ").title() type = self.type.name.replace("_", " ").title()
except AttributeError: except AttributeError:
@@ -63,6 +94,3 @@ class Reagent(Base):
"lot": self.lot, "lot": self.lot,
"expiry": self.expiry.strftime("%Y-%m-%d") "expiry": self.expiry.strftime("%Y-%m-%d")
} }

View File

@@ -3,32 +3,43 @@ from sqlalchemy import Column, String, TIMESTAMP, JSON, Float, INTEGER, ForeignK
from sqlalchemy.orm import relationship, validates from sqlalchemy.orm import relationship, validates
# table containing organization/contact relationship
orgs_contacts = Table("_orgs_contacts", Base.metadata, Column("org_id", INTEGER, ForeignKey("_organizations.id")), Column("contact_id", INTEGER, ForeignKey("_contacts.id"))) orgs_contacts = Table("_orgs_contacts", Base.metadata, Column("org_id", INTEGER, ForeignKey("_organizations.id")), Column("contact_id", INTEGER, ForeignKey("_contacts.id")))
class Organization(Base): class Organization(Base):
"""
Base of organization
"""
__tablename__ = "_organizations" __tablename__ = "_organizations"
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) name = Column(String(64)) #: organization name
submissions = relationship("BasicSubmission", back_populates="submitting_lab") submissions = relationship("BasicSubmission", back_populates="submitting_lab") #: submissions this organization has submitted
cost_centre = Column(String()) cost_centre = Column(String()) #: cost centre used by org for payment
contacts = relationship("Contact", back_populates="organization", secondary=orgs_contacts) contacts = relationship("Contact", back_populates="organization", secondary=orgs_contacts) #: contacts involved with this org
contact_ids = Column(INTEGER, ForeignKey("_contacts.id", ondelete="SET NULL", name="fk_org_contact_id")) contact_ids = Column(INTEGER, ForeignKey("_contacts.id", ondelete="SET NULL", name="fk_org_contact_id")) #: contact ids of this organization
def __str__(self): def __str__(self) -> str:
"""
String representing organization
Returns:
str: string representing organization name
"""
return self.name.replace("_", " ").title() return self.name.replace("_", " ").title()
class Contact(Base): class Contact(Base):
"""
Base of Contace
"""
__tablename__ = "_contacts" __tablename__ = "_contacts"
id = id = Column(INTEGER, primary_key=True) #: primary key id = id = Column(INTEGER, primary_key=True) #: primary key
name = Column(String(64)) name = Column(String(64)) #: contact name
email = Column(String(64)) email = Column(String(64)) #: contact email
phone = Column(String(32)) phone = Column(String(32)) #: contact phone number
organization = relationship("Organization", back_populates="contacts", uselist=True) organization = relationship("Organization", back_populates="contacts", uselist=True, secondary=orgs_contacts) #: relationship to joined organization
# organization_id = Column(INTEGER, ForeignKey("_organizations.id")) organization_id = Column(INTEGER, ForeignKey("_organizations.id", ondelete="SET NULL", name="fk_contact_org_id")) #: joined organization ids

View File

@@ -1,18 +1,20 @@
from . import Base from . import Base
from sqlalchemy import Column, String, TIMESTAMP, text, JSON, INTEGER, ForeignKey, FLOAT, BOOLEAN from sqlalchemy import Column, String, TIMESTAMP, text, JSON, INTEGER, ForeignKey, FLOAT, BOOLEAN
from sqlalchemy.orm import relationship, relationships from sqlalchemy.orm import relationship
class WWSample(Base): class WWSample(Base):
"""
Base wastewater sample
"""
__tablename__ = "_ww_samples" __tablename__ = "_ww_samples"
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
ww_processing_num = Column(String(64)) ww_processing_num = Column(String(64))
ww_sample_full_id = Column(String(64), nullable=False) ww_sample_full_id = Column(String(64), nullable=False)
rsl_number = Column(String(64)) rsl_number = Column(String(64)) #: rsl plate identification number
rsl_plate = relationship("Wastewater", back_populates="samples") rsl_plate = relationship("Wastewater", back_populates="samples") #: relationship to parent plate
rsl_plate_id = Column(INTEGER, ForeignKey("_submissions.id", ondelete="SET NULL", name="fk_WWS_sample_id")) rsl_plate_id = Column(INTEGER, ForeignKey("_submissions.id", ondelete="SET NULL", name="fk_WWS_submission_id"))
collection_date = Column(TIMESTAMP) #: Date submission received collection_date = Column(TIMESTAMP) #: Date submission received
testing_type = Column(String(64)) testing_type = Column(String(64))
site_status = Column(String(64)) site_status = Column(String(64))
@@ -22,12 +24,24 @@ class WWSample(Base):
seq_submitted = Column(BOOLEAN()) seq_submitted = Column(BOOLEAN())
ww_seq_run_id = Column(String(64)) ww_seq_run_id = Column(String(64))
sample_type = Column(String(8)) sample_type = Column(String(8))
well_number = Column(String(8)) well_number = Column(String(8)) #: location on plate
def to_string(self): def to_string(self) -> str:
"""
string representing sample object
Returns:
str: string representing location and sample id
"""
return f"{self.well_number}: {self.ww_sample_full_id}" return f"{self.well_number}: {self.ww_sample_full_id}"
def to_sub_dict(self): def to_sub_dict(self) -> dict:
"""
gui friendly dictionary
Returns:
dict: well location and id
"""
return { return {
"well": self.well_number, "well": self.well_number,
"name": self.ww_sample_full_id, "name": self.ww_sample_full_id,
@@ -35,21 +49,35 @@ class WWSample(Base):
class BCSample(Base): class BCSample(Base):
"""
base of bacterial culture sample
"""
__tablename__ = "_bc_samples" __tablename__ = "_bc_samples"
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
well_number = Column(String(8)) well_number = Column(String(8)) #: location on parent plate
sample_id = Column(String(64), nullable=False) sample_id = Column(String(64), nullable=False) #: identification from submitter
organism = Column(String(64)) organism = Column(String(64)) #: bacterial specimen
concentration = Column(String(16)) concentration = Column(String(16)) #:
rsl_plate_id = Column(INTEGER, ForeignKey("_submissions.id", ondelete="SET NULL", name="fk_BCS_sample_id")) rsl_plate_id = Column(INTEGER, ForeignKey("_submissions.id", ondelete="SET NULL", name="fk_BCS_sample_id")) #: id of parent plate
rsl_plate = relationship("BacterialCulture", back_populates="samples") rsl_plate = relationship("BacterialCulture", back_populates="samples") #: relationship to parent plate
def to_string(self): def to_string(self) -> str:
"""
string representing object
Returns:
str: string representing well location, sample id and organism
"""
return f"{self.well_number}: {self.sample_id} - {self.organism}" return f"{self.well_number}: {self.sample_id} - {self.organism}"
def to_sub_dict(self): def to_sub_dict(self) -> dict:
"""
gui friendly dictionary
Returns:
dict: well location and name (sample id, organism)
"""
return { return {
"well": self.well_number, "well": self.well_number,
"name": f"{self.sample_id} - ({self.organism})", "name": f"{self.sample_id} - ({self.organism})",

View File

@@ -3,26 +3,29 @@ from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, Table
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from datetime import datetime as dt from datetime import datetime as dt
# table containing reagents/submission relationships
reagents_submissions = Table("_reagents_submissions", Base.metadata, Column("reagent_id", INTEGER, ForeignKey("_reagents.id")), Column("submission_id", INTEGER, ForeignKey("_submissions.id"))) reagents_submissions = Table("_reagents_submissions", Base.metadata, Column("reagent_id", INTEGER, ForeignKey("_reagents.id")), Column("submission_id", INTEGER, ForeignKey("_submissions.id")))
class BasicSubmission(Base): class BasicSubmission(Base):
"""
Base of basic submission which polymorphs into BacterialCulture and Wastewater
"""
__tablename__ = "_submissions" __tablename__ = "_submissions"
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
rsl_plate_num = Column(String(32), unique=True) #: RSL name (e.g. RSL-22-0012) rsl_plate_num = Column(String(32), unique=True) #: RSL name (e.g. RSL-22-0012)
submitter_plate_num = Column(String(127), unique=True) #: The number given to the submission by the submitting lab submitter_plate_num = Column(String(127), unique=True) #: The number given to the submission by the submitting lab
submitted_date = Column(TIMESTAMP) #: Date submission received submitted_date = Column(TIMESTAMP) #: Date submission received
submitting_lab = relationship("Organization", back_populates="submissions") #: client submitting_lab = relationship("Organization", back_populates="submissions") #: client org
submitting_lab_id = Column(INTEGER, ForeignKey("_organizations.id", ondelete="SET NULL", name="fk_BS_sublab_id")) submitting_lab_id = Column(INTEGER, ForeignKey("_organizations.id", ondelete="SET NULL", name="fk_BS_sublab_id"))
sample_count = Column(INTEGER) #: Number of samples in the submission sample_count = Column(INTEGER) #: Number of samples in the submission
extraction_kit = relationship("KitType", back_populates="submissions") #: The extraction kit used extraction_kit = relationship("KitType", back_populates="submissions") #: The extraction kit used
extraction_kit_id = Column(INTEGER, ForeignKey("_kits.id", ondelete="SET NULL", name="fk_BS_extkit_id")) extraction_kit_id = Column(INTEGER, ForeignKey("_kits.id", ondelete="SET NULL", name="fk_BS_extkit_id"))
submission_type = Column(String(32)) submission_type = Column(String(32)) #: submission type (should be string in D3 of excel sheet)
technician = Column(String(64)) technician = Column(String(64)) #: initials of processing tech
# Move this into custom types? # Move this into custom types?
reagents = relationship("Reagent", back_populates="submissions", secondary=reagents_submissions) reagents = relationship("Reagent", back_populates="submissions", secondary=reagents_submissions) #: relationship to reagents
reagents_id = Column(String, ForeignKey("_reagents.id", ondelete="SET NULL", name="fk_BS_reagents_id")) reagents_id = Column(String, ForeignKey("_reagents.id", ondelete="SET NULL", name="fk_BS_reagents_id")) #: id of used reagents
__mapper_args__ = { __mapper_args__ = {
"polymorphic_identity": "basic_submission", "polymorphic_identity": "basic_submission",
@@ -30,10 +33,23 @@ class BasicSubmission(Base):
"with_polymorphic": "*", "with_polymorphic": "*",
} }
def to_string(self): def to_string(self) -> str:
"""
string presenting basic submission
Returns:
str: string representing rsl plate number and submitter plate number
"""
return f"{self.rsl_plate_num} - {self.submitter_plate_num}" return f"{self.rsl_plate_num} - {self.submitter_plate_num}"
def to_dict(self): def to_dict(self) -> dict:
"""
dictionary used in submissions summary
Returns:
dict: dictionary used in submissions summary
"""
# get lab from nested organization object
try: try:
sub_lab = self.submitting_lab.name sub_lab = self.submitting_lab.name
except AttributeError: except AttributeError:
@@ -42,6 +58,7 @@ class BasicSubmission(Base):
sub_lab = sub_lab.replace("_", " ").title() sub_lab = sub_lab.replace("_", " ").title()
except AttributeError: except AttributeError:
pass pass
# get extraction kit name from nested kit object
try: try:
ext_kit = self.extraction_kit.name ext_kit = self.extraction_kit.name
except AttributeError: except AttributeError:
@@ -60,7 +77,14 @@ class BasicSubmission(Base):
return output return output
def report_dict(self): def report_dict(self) -> dict:
"""
dictionary used in creating reports
Returns:
dict: dictionary used in creating reports
"""
# get lab name from nested organization object
try: try:
sub_lab = self.submitting_lab.name sub_lab = self.submitting_lab.name
except AttributeError: except AttributeError:
@@ -69,10 +93,12 @@ class BasicSubmission(Base):
sub_lab = sub_lab.replace("_", " ").title() sub_lab = sub_lab.replace("_", " ").title()
except AttributeError: except AttributeError:
pass pass
# get extraction kit name from nested kittype object
try: try:
ext_kit = self.extraction_kit.name ext_kit = self.extraction_kit.name
except AttributeError: except AttributeError:
ext_kit = None ext_kit = None
# get extraction kit cost from nested kittype object
try: try:
cost = self.extraction_kit.cost_per_run cost = self.extraction_kit.cost_per_run
except AttributeError: except AttributeError:
@@ -93,6 +119,9 @@ class BasicSubmission(Base):
# Below are the custom submission # Below are the custom submission
class BacterialCulture(BasicSubmission): class BacterialCulture(BasicSubmission):
"""
derivative submission type from BasicSubmission
"""
# control_id = Column(INTEGER, ForeignKey("_control_samples.id", ondelete="SET NULL", name="fk_BC_control_id")) # control_id = Column(INTEGER, ForeignKey("_control_samples.id", ondelete="SET NULL", name="fk_BC_control_id"))
controls = relationship("Control", back_populates="submission", uselist=True) #: A control sample added to submission controls = relationship("Control", back_populates="submission", uselist=True) #: A control sample added to submission
samples = relationship("BCSample", back_populates="rsl_plate", uselist=True) samples = relationship("BCSample", back_populates="rsl_plate", uselist=True)
@@ -101,6 +130,9 @@ class BacterialCulture(BasicSubmission):
class Wastewater(BasicSubmission): class Wastewater(BasicSubmission):
"""
derivative submission type from BasicSubmission
"""
samples = relationship("WWSample", back_populates="rsl_plate", uselist=True) samples = relationship("WWSample", back_populates="rsl_plate", uselist=True)
# ww_sample_id = Column(String, ForeignKey("_ww_samples.id", ondelete="SET NULL", name="fk_WW_sample_id")) # ww_sample_id = Column(String, ForeignKey("_ww_samples.id", ondelete="SET NULL", name="fk_WW_sample_id"))
__mapper_args__ = {"polymorphic_identity": "wastewater", "polymorphic_load": "inline"} __mapper_args__ = {"polymorphic_identity": "wastewater", "polymorphic_load": "inline"}

View File

@@ -1,4 +1,3 @@
from pandas import DataFrame from pandas import DataFrame
import re import re
@@ -6,14 +5,14 @@ import re
def get_unique_values_in_df_column(df: DataFrame, column_name: str) -> list: def get_unique_values_in_df_column(df: DataFrame, column_name: str) -> list:
""" """
_summary_ get all unique values in a dataframe column by name
Args: Args:
df (DataFrame): _description_ df (DataFrame): input dataframe
column_name (str): _description_ column_name (str): name of column of interest
Returns: Returns:
list: _description_ list: sorted list of unique values
""" """
return sorted(df[column_name].unique()) return sorted(df[column_name].unique())
@@ -23,7 +22,7 @@ def drop_reruns_from_df(ctx:dict, df: DataFrame) -> DataFrame:
Removes semi-duplicates from dataframe after finding sequencing repeats. Removes semi-duplicates from dataframe after finding sequencing repeats.
Args: Args:
settings (dict): settings passed down from click settings (dict): settings passed from gui
df (DataFrame): initial dataframe df (DataFrame): initial dataframe
Returns: Returns:

View File

@@ -11,40 +11,68 @@ import uuid
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
class SheetParser(object): class SheetParser(object):
"""
def __init__(self, filepath:Path|None = None, **kwargs): object to pull and contain data from excel file
"""
def __init__(self, filepath:Path|None = None, **kwargs) -> None:
"""
Args:
filepath (Path | None, optional): file path to excel sheet. Defaults to None.
"""
logger.debug(f"Parsing {filepath.__str__()}") logger.debug(f"Parsing {filepath.__str__()}")
# set attributes based on kwargs from gui ctx
for kwarg in kwargs: for kwarg in kwargs:
setattr(self, f"_{kwarg}", kwargs[kwarg]) setattr(self, f"_{kwarg}", kwargs[kwarg])
if filepath == None: if filepath == None:
logger.debug(f"No filepath.") logger.error(f"No filepath given.")
self.xl = None self.xl = None
else: else:
try: try:
self.xl = pd.ExcelFile(filepath.__str__()) self.xl = pd.ExcelFile(filepath.__str__())
except ValueError: except ValueError as e:
logger.error(f"Incorrect value: {e}")
self.xl = None self.xl = None
self.sub = OrderedDict() self.sub = OrderedDict()
# make decision about type of sample we have
self.sub['submission_type'] = self._type_decider() self.sub['submission_type'] = self._type_decider()
# select proper parser based on sample type
parse_sub = getattr(self, f"_parse_{self.sub['submission_type'].lower()}") parse_sub = getattr(self, f"_parse_{self.sub['submission_type'].lower()}")
parse_sub() parse_sub()
def _type_decider(self): def _type_decider(self) -> str:
"""
makes decisions about submission type based on structure of excel file
Returns:
str: submission type name
"""
try: try:
for type in self._submission_types: for type in self._submission_types:
if self.xl.sheet_names == self._submission_types[type]['excel_map']: if self.xl.sheet_names == self._submission_types[type]['excel_map']:
return type.title() return type.title()
return "Unknown" return "Unknown"
except: except Exception as e:
logger.warning(f"We were unable to parse the submission type due to: {e}")
return "Unknown" return "Unknown"
def _parse_unknown(self): def _parse_unknown(self) -> None:
"""
Dummy function to handle unknown excel structures
"""
self.sub = None self.sub = None
def _parse_generic(self, sheet_name:str): def _parse_generic(self, sheet_name:str) -> pd.DataFrame:
"""
Pulls information common to all submission types and passes on dataframe
Args:
sheet_name (str): name of excel worksheet to pull from
Returns:
pd.DataFrame: relevant dataframe from excel sheet
"""
submission_info = self.xl.parse(sheet_name=sheet_name, dtype=object) submission_info = self.xl.parse(sheet_name=sheet_name, dtype=object)
self.sub['submitter_plate_num'] = submission_info.iloc[0][1] #if pd.isnull(submission_info.iloc[0][1]) else string_formatter(submission_info.iloc[0][1]) self.sub['submitter_plate_num'] = submission_info.iloc[0][1] #if pd.isnull(submission_info.iloc[0][1]) else string_formatter(submission_info.iloc[0][1])
@@ -57,7 +85,10 @@ class SheetParser(object):
return submission_info return submission_info
def _parse_bacterial_culture(self): def _parse_bacterial_culture(self) -> None:
"""
pulls info specific to bacterial culture sample type
"""
submission_info = self._parse_generic("Sample List") submission_info = self._parse_generic("Sample List")
# iloc is [row][column] and the first row is set as header row so -2 # iloc is [row][column] and the first row is set as header row so -2
tech = str(submission_info.iloc[11][1]) tech = str(submission_info.iloc[11][1])
@@ -68,7 +99,7 @@ class SheetParser(object):
tech = ", ".join(tech_reg.findall(tech)) tech = ", ".join(tech_reg.findall(tech))
self.sub['technician'] = tech self.sub['technician'] = tech
# reagents # reagents
# must be prefixed with 'lot_' to be recognized by gui
self.sub['lot_wash_1'] = submission_info.iloc[1][6] #if pd.isnull(submission_info.iloc[1][6]) else string_formatter(submission_info.iloc[1][6]) self.sub['lot_wash_1'] = submission_info.iloc[1][6] #if pd.isnull(submission_info.iloc[1][6]) else string_formatter(submission_info.iloc[1][6])
self.sub['lot_wash_2'] = submission_info.iloc[2][6] #if pd.isnull(submission_info.iloc[2][6]) else string_formatter(submission_info.iloc[2][6]) self.sub['lot_wash_2'] = submission_info.iloc[2][6] #if pd.isnull(submission_info.iloc[2][6]) else string_formatter(submission_info.iloc[2][6])
self.sub['lot_binding_buffer'] = submission_info.iloc[3][6] #if pd.isnull(submission_info.iloc[3][6]) else string_formatter(submission_info.iloc[3][6]) self.sub['lot_binding_buffer'] = submission_info.iloc[3][6] #if pd.isnull(submission_info.iloc[3][6]) else string_formatter(submission_info.iloc[3][6])
@@ -79,13 +110,17 @@ class SheetParser(object):
self.sub['lot_ethanol'] = submission_info.iloc[10][6] #if pd.isnull(submission_info.iloc[10][6]) else string_formatter(submission_info.iloc[10][6]) self.sub['lot_ethanol'] = submission_info.iloc[10][6] #if pd.isnull(submission_info.iloc[10][6]) else string_formatter(submission_info.iloc[10][6])
self.sub['lot_positive_control'] = submission_info.iloc[103][1] #if pd.isnull(submission_info.iloc[103][1]) else string_formatter(submission_info.iloc[103][1]) self.sub['lot_positive_control'] = submission_info.iloc[103][1] #if pd.isnull(submission_info.iloc[103][1]) else string_formatter(submission_info.iloc[103][1])
self.sub['lot_plate'] = submission_info.iloc[12][6] #if pd.isnull(submission_info.iloc[12][6]) else string_formatter(submission_info.iloc[12][6]) self.sub['lot_plate'] = submission_info.iloc[12][6] #if pd.isnull(submission_info.iloc[12][6]) else string_formatter(submission_info.iloc[12][6])
# get individual sample info
sample_parser = SampleParser(submission_info.iloc[15:111]) sample_parser = SampleParser(submission_info.iloc[15:111])
sample_parse = getattr(sample_parser, f"parse_{self.sub['submission_type'].lower()}_samples") sample_parse = getattr(sample_parser, f"parse_{self.sub['submission_type'].lower()}_samples")
logger.debug(f"Parser result: {self.sub}") logger.debug(f"Parser result: {self.sub}")
self.sub['samples'] = sample_parse() self.sub['samples'] = sample_parse()
def _parse_wastewater(self): def _parse_wastewater(self) -> None:
"""
pulls info specific to wastewater sample type
"""
# submission_info = self.xl.parse("WW Submissions (ENTER HERE)") # submission_info = self.xl.parse("WW Submissions (ENTER HERE)")
submission_info = self._parse_generic("WW Submissions (ENTER HERE)") submission_info = self._parse_generic("WW Submissions (ENTER HERE)")
enrichment_info = self.xl.parse("Enrichment Worksheet", dtype=object) enrichment_info = self.xl.parse("Enrichment Worksheet", dtype=object)
@@ -108,19 +143,28 @@ class SheetParser(object):
self.sub['lot_pre_mix_2'] = qprc_info.iloc[2][14] #if pd.isnull(qprc_info.iloc[2][14]) else string_formatter(qprc_info.iloc[2][14]) self.sub['lot_pre_mix_2'] = qprc_info.iloc[2][14] #if pd.isnull(qprc_info.iloc[2][14]) else string_formatter(qprc_info.iloc[2][14])
self.sub['lot_positive_control'] = qprc_info.iloc[3][14] #if pd.isnull(qprc_info.iloc[3][14]) else string_formatter(qprc_info.iloc[3][14]) self.sub['lot_positive_control'] = qprc_info.iloc[3][14] #if pd.isnull(qprc_info.iloc[3][14]) else string_formatter(qprc_info.iloc[3][14])
self.sub['lot_ddh2o'] = qprc_info.iloc[4][14] #if pd.isnull(qprc_info.iloc[4][14]) else string_formatter(qprc_info.iloc[4][14]) self.sub['lot_ddh2o'] = qprc_info.iloc[4][14] #if pd.isnull(qprc_info.iloc[4][14]) else string_formatter(qprc_info.iloc[4][14])
# gt individual sample info
sample_parser = SampleParser(submission_info.iloc[16:40]) sample_parser = SampleParser(submission_info.iloc[16:40])
sample_parse = getattr(sample_parser, f"parse_{self.sub['submission_type'].lower()}_samples") sample_parse = getattr(sample_parser, f"parse_{self.sub['submission_type'].lower()}_samples")
self.sub['samples'] = sample_parse() self.sub['samples'] = sample_parse()
class SampleParser(object): class SampleParser(object):
"""
object to pull data for samples in excel sheet and construct individual sample objects
"""
def __init__(self, df:pd.DataFrame) -> None: def __init__(self, df:pd.DataFrame) -> None:
self.samples = df.to_dict("records") self.samples = df.to_dict("records")
def parse_bacterial_culture_samples(self) -> list[BCSample]: def parse_bacterial_culture_samples(self) -> list[BCSample]:
"""
construct bacterial culture specific sample objects
Returns:
list[BCSample]: list of sample objects
"""
new_list = [] new_list = []
for sample in self.samples: for sample in self.samples:
new = BCSample() new = BCSample()
@@ -130,6 +174,7 @@ class SampleParser(object):
new.concentration = sample['Unnamed: 3'] new.concentration = sample['Unnamed: 3']
# logger.debug(f"Sample object: {new.sample_id} = {type(new.sample_id)}") # logger.debug(f"Sample object: {new.sample_id} = {type(new.sample_id)}")
logger.debug(f"Got sample_id: {new.sample_id}") logger.debug(f"Got sample_id: {new.sample_id}")
# need to exclude empties and blanks
try: try:
not_a_nan = not np.isnan(new.sample_id) and str(new.sample_id).lower() != 'blank' not_a_nan = not np.isnan(new.sample_id) and str(new.sample_id).lower() != 'blank'
except TypeError: except TypeError:
@@ -140,10 +185,17 @@ class SampleParser(object):
def parse_wastewater_samples(self) -> list[WWSample]: def parse_wastewater_samples(self) -> list[WWSample]:
"""
construct wastewater specific sample objects
Returns:
list[WWSample]: list of sample objects
"""
new_list = [] new_list = []
for sample in self.samples: for sample in self.samples:
new = WWSample() new = WWSample()
new.ww_processing_num = sample['Unnamed: 2'] new.ww_processing_num = sample['Unnamed: 2']
# need to ensure we have a sample id for database integrity
try: try:
not_a_nan = not np.isnan(sample['Unnamed: 3']) not_a_nan = not np.isnan(sample['Unnamed: 3'])
except TypeError: except TypeError:
@@ -153,6 +205,7 @@ class SampleParser(object):
else: else:
new.ww_sample_full_id = uuid.uuid4().hex.upper() new.ww_sample_full_id = uuid.uuid4().hex.upper()
new.rsl_number = sample['Unnamed: 9'] new.rsl_number = sample['Unnamed: 9']
# need to ensure we get a collection date
try: try:
not_a_nan = not np.isnan(sample['Unnamed: 5']) not_a_nan = not np.isnan(sample['Unnamed: 5'])
except TypeError: except TypeError:
@@ -169,11 +222,11 @@ class SampleParser(object):
return new_list return new_list
def string_formatter(input): # def string_formatter(input):
logger.debug(f"{input} : {type(input)}") # logger.debug(f"{input} : {type(input)}")
match input: # match input:
case int() | float() | np.float64: # case int() | float() | np.float64:
return "{:0.0f}".format(input) # return "{:0.0f}".format(input)
case _: # case _:
return input # return input

View File

@@ -8,13 +8,22 @@ import logging
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
def make_report_xlsx(records:list[dict]) -> DataFrame: def make_report_xlsx(records:list[dict]) -> DataFrame:
"""
create the dataframe for a report
Args:
records (list[dict]): list of dictionaries created from submissions
Returns:
DataFrame: output dataframe
"""
df = DataFrame.from_records(records) df = DataFrame.from_records(records)
# put submissions with the same lab together
df = df.sort_values("Submitting Lab") df = df.sort_values("Submitting Lab")
# table = df.pivot_table(values="Cost", index=["Submitting Lab", "Extraction Kit"], columns=["Cost", "Sample Count"], aggfunc={'Cost':np.sum,'Sample Count':np.sum}) # aggregate cost and sample count columns
df2 = df.groupby(["Submitting Lab", "Extraction Kit"]).agg({'Cost': ['sum', 'count'], 'Sample Count':['sum']}) df2 = df.groupby(["Submitting Lab", "Extraction Kit"]).agg({'Cost': ['sum', 'count'], 'Sample Count':['sum']})
# df2['Cost'] = df2['Cost'].map('${:,.2f}'.format)
logger.debug(df2.columns) logger.debug(df2.columns)
# df2['Cost']['sum'] = df2['Cost']['sum'].apply('${:,.2f}'.format) # apply formating to cost column
df2.iloc[:, (df2.columns.get_level_values(1)=='sum') & (df2.columns.get_level_values(0)=='Cost')] = df2.iloc[:, (df2.columns.get_level_values(1)=='sum') & (df2.columns.get_level_values(0)=='Cost')].applymap('${:,.2f}'.format) df2.iloc[:, (df2.columns.get_level_values(1)=='sum') & (df2.columns.get_level_values(0)=='Cost')] = df2.iloc[:, (df2.columns.get_level_values(1)=='sum') & (df2.columns.get_level_values(0)=='Cost')].applymap('${:,.2f}'.format)
return df2 return df2
@@ -65,7 +74,18 @@ def make_report_xlsx(records:list[dict]) -> DataFrame:
# dfs['name'] = df # dfs['name'] = df
# return dfs # return dfs
def convert_control_by_mode(ctx:dict, control:models.Control, mode:str): def convert_control_by_mode(ctx:dict, control:models.Control, mode:str) -> list[dict]:
"""
split control object into analysis types
Args:
ctx (dict): settings passed from gui
control (models.Control): control to be parsed into list
mode (str): analysis type
Returns:
list[dict]: list of records
"""
output = [] output = []
data = json.loads(getattr(control, mode)) data = json.loads(getattr(control, mode))
for genus in data: for genus in data:
@@ -82,6 +102,17 @@ def convert_control_by_mode(ctx:dict, control:models.Control, mode:str):
def convert_data_list_to_df(ctx:dict, input:list[dict], subtype:str|None=None) -> DataFrame: def convert_data_list_to_df(ctx:dict, input:list[dict], subtype:str|None=None) -> DataFrame:
"""
Convert list of control records to dataframe
Args:
ctx (dict): settings passed from gui
input (list[dict]): list of dictionaries containing records
subtype (str | None, optional): _description_. Defaults to None.
Returns:
DataFrame: _description_
"""
df = DataFrame.from_records(input) df = DataFrame.from_records(input)
safe = ['name', 'submitted_date', 'genus', 'target'] safe = ['name', 'submitted_date', 'genus', 'target']
logger.debug(df) logger.debug(df)

View File

@@ -1,9 +1,9 @@
import yaml import yaml
import sys, os, stat, platform, shutil import sys, os, stat, platform, getpass
import logging import logging
from logging import handlers from logging import handlers
from pathlib import Path from pathlib import Path
# from getpass import getuser
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import create_engine from sqlalchemy import create_engine
@@ -64,8 +64,9 @@ class StreamToLogger(object):
self.logger.log(self.log_level, line.rstrip()) self.logger.log(self.log_level, line.rstrip())
def get_config(settings_path: str|None) -> dict: def get_config(settings_path: str|None=None) -> dict:
"""Get configuration settings from path or default if blank. """
Get configuration settings from path or default if blank.
Args: Args:
settings_path (str, optional): _description_. Defaults to "". settings_path (str, optional): _description_. Defaults to "".
@@ -91,6 +92,7 @@ def get_config(settings_path: str|None) -> dict:
except FileExistsError: except FileExistsError:
pass pass
# if user hasn't defined config path in cli args # if user hasn't defined config path in cli args
copy_settings_trigger = False
if settings_path == None: if settings_path == None:
# Check user .config/submissions directory # Check user .config/submissions directory
if CONFIGDIR.joinpath("config.yml").exists(): if CONFIGDIR.joinpath("config.yml").exists():
@@ -104,7 +106,9 @@ def get_config(settings_path: str|None) -> dict:
settings_path = Path(sys._MEIPASS).joinpath("files", "config.yml") settings_path = Path(sys._MEIPASS).joinpath("files", "config.yml")
else: else:
settings_path = package_dir.joinpath('config.yml') settings_path = package_dir.joinpath('config.yml')
shutil.copyfile(settings_path.__str__(), CONFIGDIR.joinpath("config.yml").__str__()) # Tell program we need to copy the config.yml to the user directory
copy_settings_trigger = True
# shutil.copyfile(settings_path.__str__(), CONFIGDIR.joinpath("config.yml").__str__())
else: else:
if Path(settings_path).is_dir(): if Path(settings_path).is_dir():
settings_path = settings_path.joinpath("config.yml") settings_path = settings_path.joinpath("config.yml")
@@ -120,11 +124,15 @@ def get_config(settings_path: str|None) -> dict:
except yaml.YAMLError as exc: except yaml.YAMLError as exc:
logger.error(f'Error reading yaml file {settings_path}: {exc}') logger.error(f'Error reading yaml file {settings_path}: {exc}')
return {} return {}
# copy settings to config directory
if copy_settings_trigger:
settings = copy_settings(settings_path=CONFIGDIR.joinpath("config.yml"), settings=settings)
return settings return settings
def create_database_session(database_path: Path|None) -> Session: def create_database_session(database_path: Path|None) -> Session:
"""Get database settings from path or default if blank. """
Get database settings from path or default if blank.
Args: Args:
database_path (str, optional): _description_. Defaults to "". database_path (str, optional): _description_. Defaults to "".
@@ -153,7 +161,8 @@ def create_database_session(database_path: Path|None) -> Session:
def setup_logger(verbosity:int=3): def setup_logger(verbosity:int=3):
"""Set logger levels using settings. """
Set logger levels using settings.
Args: Args:
verbose (bool, optional): _description_. Defaults to False. verbose (bool, optional): _description_. Defaults to False.
@@ -203,15 +212,22 @@ def setup_logger(verbosity:int=3):
sys.excepthook = handle_exception sys.excepthook = handle_exception
return logger return logger
# def set_logger_verbosity(verbosity): def copy_settings(settings_path:Path, settings:dict) -> dict:
# """Does what it says. """
# """ copies relevant settings dictionary from the default config.yml to a new directory
# handler = [item for item in logger.parent.handlers if item.name == "Stream"][0]
# match verbosity: Args:
# case 3: settings_path (Path): path to write the file to
# handler.setLevel(logging.DEBUG) settings (dict): settings dictionary obtained from default config.yml
# case 2:
# handler.setLevel(logging.INFO) Returns:
# case 1: dict: output dictionary for use in first run
# handler.setLevel(logging.WARNING) """
# if the current user is not a superuser remove the superusers entry
if not getpass.getuser() in settings['super_users']:
del settings['super_users']
with open(settings_path, 'w') as f:
yaml.dump(settings, f)
return settings

View File

@@ -1,20 +1,20 @@
import re import re
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QMainWindow, QLabel, QToolBar, QStatusBar, QMainWindow, QLabel, QToolBar,
QTabWidget, QWidget, QVBoxLayout, QTabWidget, QWidget, QVBoxLayout,
QPushButton, QMenuBar, QFileDialog, QPushButton, QFileDialog,
QLineEdit, QMessageBox, QComboBox, QDateEdit, QHBoxLayout, QLineEdit, QMessageBox, QComboBox, QDateEdit, QHBoxLayout,
QSpinBox, QScrollArea, QScrollBar, QSizePolicy QSpinBox, QScrollArea
) )
from PyQt6.QtGui import QAction, QIcon from PyQt6.QtGui import QAction
from PyQt6.QtCore import pyqtSlot, QDateTime, QDate, QSignalBlocker, Qt from PyQt6.QtCore import QSignalBlocker
from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebEngineWidgets import QWebEngineView
import pandas as pd # import pandas as pd
from pathlib import Path from pathlib import Path
import plotly import plotly
import plotly.express as px # import plotly.express as px
import yaml import yaml
from backend.excel.parser import SheetParser from backend.excel.parser import SheetParser
@@ -30,41 +30,45 @@ import numpy
from frontend.custom_widgets import AddReagentQuestion, AddReagentForm, SubmissionsSheet, ReportDatePicker, KitAdder, ControlsDatePicker, OverwriteSubQuestion from frontend.custom_widgets import AddReagentQuestion, AddReagentForm, SubmissionsSheet, ReportDatePicker, KitAdder, ControlsDatePicker, OverwriteSubQuestion
import logging import logging
import difflib import difflib
from datetime import date
from frontend.visualizations.charts import create_charts from frontend.visualizations.charts import create_charts
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.info("Hello, I am a logger") logger.info("Hello, I am a logger")
class App(QMainWindow): class App(QMainWindow):
# class App(QScrollArea):
def __init__(self, ctx: dict = {}): def __init__(self, ctx: dict = {}):
super().__init__() super().__init__()
self.ctx = ctx self.ctx = ctx
self.title = 'Submissions App - PyQT6' try:
self.title = f"Submissions App (v{ctx['package'].__version__})"
except AttributeError:
self.title = f"Submissions App"
# set initial app position and size
self.left = 0 self.left = 0
self.top = 0 self.top = 0
self.width = 1300 self.width = 1300
self.height = 1000 self.height = 1000
self.setWindowTitle(self.title) self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height) self.setGeometry(self.left, self.top, self.width, self.height)
# insert tabs into main app
self.table_widget = AddSubForm(self) self.table_widget = AddSubForm(self)
self.setCentralWidget(self.table_widget) self.setCentralWidget(self.table_widget)
# run initial setups
self._createActions() self._createActions()
self._createMenuBar() self._createMenuBar()
self._createToolBar() self._createToolBar()
self._connectActions() self._connectActions()
# self.renderPage()
self.controls_getter() self.controls_getter()
self.show() self.show()
def _createMenuBar(self): def _createMenuBar(self):
"""
adds items to menu bar
"""
menuBar = self.menuBar() menuBar = self.menuBar()
fileMenu = menuBar.addMenu("&File") fileMenu = menuBar.addMenu("&File")
# menuBar.addMenu(fileMenu)
# Creating menus using a title # Creating menus using a title
editMenu = menuBar.addMenu("&Edit") editMenu = menuBar.addMenu("&Edit")
reportMenu = menuBar.addMenu("&Reports") reportMenu = menuBar.addMenu("&Reports")
@@ -73,12 +77,18 @@ class App(QMainWindow):
reportMenu.addAction(self.generateReportAction) reportMenu.addAction(self.generateReportAction)
def _createToolBar(self): def _createToolBar(self):
"""
adds items to toolbar
"""
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)
def _createActions(self): def _createActions(self):
"""
creates actions
"""
self.importAction = QAction("&Import", self) self.importAction = QAction("&Import", self)
self.addReagentAction = QAction("Add Reagent", self) self.addReagentAction = QAction("Add Reagent", self)
self.generateReportAction = QAction("Make Report", self) self.generateReportAction = QAction("Make Report", self)
@@ -86,6 +96,9 @@ class App(QMainWindow):
def _connectActions(self): def _connectActions(self):
"""
connect menu and tool bar item to functions
"""
self.importAction.triggered.connect(self.importSubmission) self.importAction.triggered.connect(self.importSubmission)
self.addReagentAction.triggered.connect(self.add_reagent) self.addReagentAction.triggered.connect(self.add_reagent)
self.generateReportAction.triggered.connect(self.generateReport) self.generateReportAction.triggered.connect(self.generateReport)
@@ -97,21 +110,27 @@ class App(QMainWindow):
def importSubmission(self): def importSubmission(self):
"""
import submission from excel sheet into form
"""
logger.debug(self.ctx) logger.debug(self.ctx)
# initialize samples
self.samples = [] self.samples = []
# set file dialog
home_dir = str(Path(self.ctx["directory_path"])) home_dir = str(Path(self.ctx["directory_path"]))
fname = Path(QFileDialog.getOpenFileName(self, 'Open file', home_dir)[0]) fname = Path(QFileDialog.getOpenFileName(self, 'Open file', home_dir)[0])
logger.debug(f"Attempting to parse file: {fname}") logger.debug(f"Attempting to parse file: {fname}")
assert fname.exists() assert fname.exists()
# create sheetparser using excel sheet and context from gui
try: try:
prsr = SheetParser(fname, **self.ctx) prsr = SheetParser(fname, **self.ctx)
except PermissionError: except PermissionError:
return return
logger.debug(f"prsr.sub = {prsr.sub}") logger.debug(f"prsr.sub = {prsr.sub}")
# replace formlayout with tab1.layout # destroy any widgets from previous imports
# self.form = self.table_widget.formlayout
for item in self.table_widget.formlayout.parentWidget().findChildren(QWidget): for item in self.table_widget.formlayout.parentWidget().findChildren(QWidget):
item.setParent(None) item.setParent(None)
# regex to parser out different variable types for decision making
variable_parser = re.compile(r""" variable_parser = re.compile(r"""
# (?x) # (?x)
(?P<extraction_kit>^extraction_kit$) | (?P<extraction_kit>^extraction_kit$) |
@@ -119,11 +138,10 @@ class App(QMainWindow):
(?P<submitting_lab>)^submitting_lab$ | (?P<submitting_lab>)^submitting_lab$ |
(?P<samples>)^samples$ | (?P<samples>)^samples$ |
(?P<reagent>^lot_.*$) (?P<reagent>^lot_.*$)
""", re.VERBOSE) """, re.VERBOSE)
for item in prsr.sub: for item in prsr.sub:
logger.debug(f"Item: {item}") logger.debug(f"Item: {item}")
# attempt to match variable name to regex group
try: try:
mo = variable_parser.fullmatch(item).lastgroup mo = variable_parser.fullmatch(item).lastgroup
except AttributeError: except AttributeError:
@@ -131,17 +149,23 @@ class App(QMainWindow):
logger.debug(f"Mo: {mo}") logger.debug(f"Mo: {mo}")
match mo: match mo:
case 'submitting_lab': case 'submitting_lab':
# create label
self.table_widget.formlayout.addWidget(QLabel(item.replace("_", " ").title())) self.table_widget.formlayout.addWidget(QLabel(item.replace("_", " ").title()))
logger.debug(f"{item}: {prsr.sub[item]}") logger.debug(f"{item}: {prsr.sub[item]}")
# create combobox to hold looked up submitting labs
add_widget = QComboBox() add_widget = QComboBox()
labs = [item.__str__() for item in lookup_all_orgs(ctx=self.ctx)] labs = [item.__str__() for item in lookup_all_orgs(ctx=self.ctx)]
# try to set closest match to top of list
try: try:
labs = difflib.get_close_matches(prsr.sub[item], labs, len(labs), 0) labs = difflib.get_close_matches(prsr.sub[item], labs, len(labs), 0)
except (TypeError, ValueError): except (TypeError, ValueError):
pass pass
# set combobox values to lookedup values
add_widget.addItems(labs) add_widget.addItems(labs)
case 'extraction_kit': case 'extraction_kit':
# create label
self.table_widget.formlayout.addWidget(QLabel(item.replace("_", " ").title())) self.table_widget.formlayout.addWidget(QLabel(item.replace("_", " ").title()))
# if extraction kit not available, all other values fail
if prsr.sub[item] == 'nan': if prsr.sub[item] == 'nan':
msg = QMessageBox() msg = QMessageBox()
# msg.setIcon(QMessageBox.critical) # msg.setIcon(QMessageBox.critical)
@@ -150,18 +174,27 @@ class App(QMainWindow):
msg.setWindowTitle("Error") msg.setWindowTitle("Error")
msg.exec() msg.exec()
break break
# create combobox to hold looked up kits
add_widget = QComboBox() add_widget = QComboBox()
# lookup existing kits by 'submission_type' decided on by sheetparser
uses = [item.__str__() for item in lookup_kittype_by_use(ctx=self.ctx, used_by=prsr.sub['submission_type'])] uses = [item.__str__() for item in lookup_kittype_by_use(ctx=self.ctx, used_by=prsr.sub['submission_type'])]
if len(uses) > 0: if len(uses) > 0:
add_widget.addItems(uses) add_widget.addItems(uses)
else: else:
add_widget.addItems(['bacterial_culture']) add_widget.addItems(['bacterial_culture'])
case 'submitted_date': case 'submitted_date':
# create label
self.table_widget.formlayout.addWidget(QLabel(item.replace("_", " ").title())) self.table_widget.formlayout.addWidget(QLabel(item.replace("_", " ").title()))
# uses base calendar
add_widget = QDateEdit(calendarPopup=True) add_widget = QDateEdit(calendarPopup=True)
# add_widget.setDateTime(QDateTime.date(prsr.sub[item])) # sets submitted date based on date found in excel sheet
add_widget.setDate(prsr.sub[item]) try:
add_widget.setDate(prsr.sub[item])
# if not found, use today
except:
add_widget.setDate(date.today())
case 'reagent': case 'reagent':
# create label
self.table_widget.formlayout.addWidget(QLabel(item.replace("_", " ").title())) self.table_widget.formlayout.addWidget(QLabel(item.replace("_", " ").title()))
add_widget = QComboBox() add_widget = QComboBox()
add_widget.setEditable(True) add_widget.setEditable(True)
@@ -174,8 +207,10 @@ class App(QMainWindow):
prsr.sub[item] = int(prsr.sub[item]) prsr.sub[item] = int(prsr.sub[item])
except ValueError: except ValueError:
pass pass
# query for reagents using type name from sheet and kit from sheet
relevant_reagents = [item.__str__() for item in lookup_regent_by_type_name_and_kit_name(ctx=self.ctx, type_name=query_var, kit_name=prsr.sub['extraction_kit'])] relevant_reagents = [item.__str__() for item in lookup_regent_by_type_name_and_kit_name(ctx=self.ctx, type_name=query_var, kit_name=prsr.sub['extraction_kit'])]
logger.debug(f"Relevant reagents: {relevant_reagents}") logger.debug(f"Relevant reagents: {relevant_reagents}")
# if reagent in sheet is not found insert it into items
if prsr.sub[item] not in relevant_reagents and prsr.sub[item] != 'nan': if prsr.sub[item] not in relevant_reagents and prsr.sub[item] != 'nan':
try: try:
check = not numpy.isnan(prsr.sub[item]) check = not numpy.isnan(prsr.sub[item])
@@ -187,49 +222,35 @@ class App(QMainWindow):
add_widget.addItems(relevant_reagents) add_widget.addItems(relevant_reagents)
# TODO: make samples not appear in frame. # TODO: make samples not appear in frame.
case 'samples': case 'samples':
# hold samples in 'self' until form submitted
logger.debug(f"{item}: {prsr.sub[item]}") logger.debug(f"{item}: {prsr.sub[item]}")
self.samples = prsr.sub[item] self.samples = prsr.sub[item]
case _: case _:
# anything else gets added in as a line edit
self.table_widget.formlayout.addWidget(QLabel(item.replace("_", " ").title())) self.table_widget.formlayout.addWidget(QLabel(item.replace("_", " ").title()))
add_widget = QLineEdit() add_widget = QLineEdit()
add_widget.setText(str(prsr.sub[item]).replace("_", " ")) add_widget.setText(str(prsr.sub[item]).replace("_", " "))
self.table_widget.formlayout.addWidget(add_widget) self.table_widget.formlayout.addWidget(add_widget)
# create submission button
submit_btn = QPushButton("Submit") submit_btn = QPushButton("Submit")
self.table_widget.formlayout.addWidget(submit_btn) self.table_widget.formlayout.addWidget(submit_btn)
submit_btn.clicked.connect(self.submit_new_sample) submit_btn.clicked.connect(self.submit_new_sample)
# self.table_widget.interior.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.MinimumExpanding)
print(self.table_widget.formwidget.size())
# def renderPage(self):
# """
# Test function for plotly chart rendering
# """
# df = pd.read_excel("C:\\Users\\lwark\\Desktop\\test_df.xlsx", engine="openpyxl")
# fig = px.bar(df, x="submitted_date", y="kraken_percent", color="genus", title="Long-Form Input")
# fig.update_layout(
# xaxis_title="Submitted Date (* - Date parsed from fastq file creation date)",
# yaxis_title="Kraken Percent",
# showlegend=True,
# barmode='stack'
# )
# html = '<html><body>'
# html += plotly.offline.plot(fig, output_type='div', include_plotlyjs='cdn', auto_open=True, image = 'png', image_filename='plot_image')
# html += '</body></html>'
# self.table_widget.webengineview.setHtml(html)
# self.table_widget.webengineview.update()
def submit_new_sample(self): def submit_new_sample(self):
"""
Attempt to add sample to database when 'submit' button clicked
"""
# get info from form
labels, values = self.extract_form_info(self.table_widget.tab1) labels, values = self.extract_form_info(self.table_widget.tab1)
info = {item[0]:item[1] for item in zip(labels, values) if not item[0].startswith("lot_")} info = {item[0]:item[1] for item in zip(labels, values) if not item[0].startswith("lot_")}
reagents = {item[0]:item[1] for item in zip(labels, values) if item[0].startswith("lot_")} reagents = {item[0]:item[1] for item in zip(labels, values) if item[0].startswith("lot_")}
logger.debug(f"Reagents: {reagents}") logger.debug(f"Reagents: {reagents}")
parsed_reagents = [] parsed_reagents = []
# compare reagents in form to reagent database
for reagent in reagents: for reagent in reagents:
wanted_reagent = lookup_reagent(ctx=self.ctx, reagent_lot=reagents[reagent]) wanted_reagent = lookup_reagent(ctx=self.ctx, reagent_lot=reagents[reagent])
logger.debug(wanted_reagent) logger.debug(f"Looked up reagent: {wanted_reagent}")
# if reagent not found offer to add to database
if wanted_reagent == None: if wanted_reagent == None:
dlg = AddReagentQuestion(reagent_type=reagent, reagent_lot=reagents[reagent]) dlg = AddReagentQuestion(reagent_type=reagent, reagent_lot=reagents[reagent])
if dlg.exec(): if dlg.exec():
@@ -239,49 +260,78 @@ class App(QMainWindow):
if wanted_reagent != None: if wanted_reagent != None:
parsed_reagents.append(wanted_reagent) parsed_reagents.append(wanted_reagent)
logger.debug(info) logger.debug(info)
# move samples into preliminary submission dict
info['samples'] = self.samples info['samples'] = self.samples
# construct submission object
base_submission, output = construct_submission_info(ctx=self.ctx, info_dict=info) base_submission, output = construct_submission_info(ctx=self.ctx, info_dict=info)
# check output message for issues
if output['message'] != None: if output['message'] != None:
dlg = OverwriteSubQuestion(output['message'], base_submission.rsl_plate_num) dlg = OverwriteSubQuestion(output['message'], base_submission.rsl_plate_num)
if dlg.exec(): if dlg.exec():
base_submission.reagents = [] base_submission.reagents = []
else: else:
return return
# add reagents to submission object
for reagent in parsed_reagents: for reagent in parsed_reagents:
base_submission.reagents.append(reagent) base_submission.reagents.append(reagent)
logger.debug(f"Sending submission: {base_submission.rsl_plate_num} to database.") logger.debug(f"Sending submission: {base_submission.rsl_plate_num} to database.")
result = store_submission(ctx=self.ctx, base_submission=base_submission) result = store_submission(ctx=self.ctx, base_submission=base_submission)
# check result of storing for issues
if result != None: if result != None:
msg = QMessageBox() msg = QMessageBox()
# msg.setIcon(QMessageBox.critical) # msg.setIcon(QMessageBox.critical)
msg.setText("Error") msg.setText("Error")
msg.setInformativeText(result['message']) msg.setInformativeText(result['message'])
msg.setWindowTitle("Error") msg.setWindowTitle("Error")
msg.show()
msg.exec() msg.exec()
# update summary sheet
self.table_widget.sub_wid.setData() self.table_widget.sub_wid.setData()
# reset form
for item in self.table_widget.formlayout.parentWidget().findChildren(QWidget): for item in self.table_widget.formlayout.parentWidget().findChildren(QWidget):
item.setParent(None) item.setParent(None)
def add_reagent(self, reagent_lot:str|None=None, reagent_type:str|None=None): def add_reagent(self, reagent_lot:str|None=None, reagent_type:str|None=None):
"""
Action to create new reagent in DB.
Args:
reagent_lot (str | None, optional): Parsed reagent from import form. Defaults to None.
reagent_type (str | None, optional): Parsed reagent type from import form. Defaults to None.
Returns:
models.Reagent: the constructed reagent object to add to submission
"""
if isinstance(reagent_lot, bool): if isinstance(reagent_lot, bool):
reagent_lot = "" reagent_lot = ""
# create form
dlg = AddReagentForm(ctx=self.ctx, reagent_lot=reagent_lot, reagent_type=reagent_type) dlg = AddReagentForm(ctx=self.ctx, reagent_lot=reagent_lot, reagent_type=reagent_type)
if dlg.exec(): if dlg.exec():
# extract form info
labels, values = self.extract_form_info(dlg) labels, values = self.extract_form_info(dlg)
info = {item[0]:item[1] for item in zip(labels, values)} info = {item[0]:item[1] for item in zip(labels, values)}
logger.debug(f"Reagent info: {info}") logger.debug(f"Reagent info: {info}")
# create reagent object
reagent = construct_reagent(ctx=self.ctx, info_dict=info) reagent = construct_reagent(ctx=self.ctx, info_dict=info)
# send reagent to db
store_reagent(ctx=self.ctx, reagent=reagent) store_reagent(ctx=self.ctx, reagent=reagent)
return reagent return reagent
def extract_form_info(self, object): def extract_form_info(self, object):
"""
retrieves arbitrary number of labels, values from form
Args:
object (_type_): _description_
Returns:
_type_: _description_
"""
labels = [] labels = []
values = [] values = []
# grab all widgets in form
for item in object.layout.parentWidget().findChildren(QWidget): for item in object.layout.parentWidget().findChildren(QWidget):
match item: match item:
case QLabel(): case QLabel():
labels.append(item.text().replace(" ", "_").lower()) labels.append(item.text().replace(" ", "_").lower())
@@ -295,19 +345,29 @@ class App(QMainWindow):
values.append(item.currentText()) values.append(item.currentText())
case QDateEdit(): case QDateEdit():
values.append(item.date().toPyDate()) values.append(item.date().toPyDate())
# value for ad hoc check above
prev_item = item prev_item = item
return labels, values return labels, values
def generateReport(self): def generateReport(self):
"""
Action to create a summary of sheet data per client
"""
# Custom two date picker for start & end dates
dlg = ReportDatePicker() dlg = ReportDatePicker()
if dlg.exec(): if dlg.exec():
labels, values = self.extract_form_info(dlg) labels, values = self.extract_form_info(dlg)
info = {item[0]:item[1] for item in zip(labels, values)} info = {item[0]:item[1] for item in zip(labels, values)}
# find submissions based on date range
subs = lookup_submissions_by_date_range(ctx=self.ctx, start_date=info['start_date'], end_date=info['end_date']) subs = lookup_submissions_by_date_range(ctx=self.ctx, start_date=info['start_date'], end_date=info['end_date'])
# convert each object to dict
records = [item.report_dict() for item in subs] records = [item.report_dict() for item in subs]
# make dataframe from record dictionaries
df = make_report_xlsx(records=records) df = make_report_xlsx(records=records)
home_dir = Path(self.ctx["directory_path"]).joinpath(f"Submissions_{info['start_date']}-{info['end_date']}.xlsx").resolve().__str__() # setup filedialog to handle save location of report
home_dir = Path(self.ctx["directory_path"]).joinpath(f"Submissions_Report_{info['start_date']}-{info['end_date']}.xlsx").resolve().__str__()
fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".xlsx")[0]) fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".xlsx")[0])
# save file
try: try:
df.to_excel(fname, engine="openpyxl") df.to_excel(fname, engine="openpyxl")
except PermissionError: except PermissionError:
@@ -315,9 +375,14 @@ class App(QMainWindow):
def add_kit(self): def add_kit(self):
"""
Constructs new kit from yaml and adds to DB.
"""
# setup file dialog to find yaml flie
home_dir = str(Path(self.ctx["directory_path"])) home_dir = str(Path(self.ctx["directory_path"]))
fname = Path(QFileDialog.getOpenFileName(self, 'Open file', home_dir, filter = "yml(*.yml)")[0]) fname = Path(QFileDialog.getOpenFileName(self, 'Open file', home_dir, filter = "yml(*.yml)")[0])
assert fname.exists() assert fname.exists()
# read yaml file
try: try:
with open(fname.__str__(), "r") as stream: with open(fname.__str__(), "r") as stream:
try: try:
@@ -327,64 +392,96 @@ class App(QMainWindow):
return {} return {}
except PermissionError: except PermissionError:
return return
create_kit_from_yaml(ctx=self.ctx, exp=exp) # send to kit creator function
result = create_kit_from_yaml(ctx=self.ctx, exp=exp)
msg = QMessageBox()
# msg.setIcon(QMessageBox.critical)
match result['code']:
case 0:
msg.setText("Kit added")
msg.setInformativeText(result['message'])
msg.setWindowTitle("Kit added")
case 1:
msg.setText("Permission Error")
msg.setInformativeText(result['message'])
msg.setWindowTitle("Permission Error")
msg.exec()
def controls_getter(self): def controls_getter(self):
# self.table_widget.webengineview.setHtml("") """
Lookup controls from database and send to chartmaker
"""
# subtype defaults to disabled
try: try:
self.table_widget.sub_typer.disconnect() self.table_widget.sub_typer.disconnect()
except TypeError: except TypeError:
pass pass
# correct start date being more recent than end date and rerun
if self.table_widget.datepicker.start_date.date() > self.table_widget.datepicker.end_date.date(): if self.table_widget.datepicker.start_date.date() > self.table_widget.datepicker.end_date.date():
logger.warning("Start date after end date is not allowed!") logger.warning("Start date after end date is not allowed!")
# self.table_widget.datepicker.start_date.setDate(e_date)
threemonthsago = self.table_widget.datepicker.end_date.date().addDays(-90) threemonthsago = self.table_widget.datepicker.end_date.date().addDays(-90)
# block signal that will rerun controls getter and set start date
with QSignalBlocker(self.table_widget.datepicker.start_date) as blocker: with QSignalBlocker(self.table_widget.datepicker.start_date) as blocker:
self.table_widget.datepicker.start_date.setDate(threemonthsago) self.table_widget.datepicker.start_date.setDate(threemonthsago)
self.controls_getter() self.controls_getter()
return return
# convert to python useable date object
self.start_date = self.table_widget.datepicker.start_date.date().toPyDate() self.start_date = self.table_widget.datepicker.start_date.date().toPyDate()
self.end_date = self.table_widget.datepicker.end_date.date().toPyDate() self.end_date = self.table_widget.datepicker.end_date.date().toPyDate()
self.con_type = self.table_widget.control_typer.currentText() self.con_type = self.table_widget.control_typer.currentText()
self.mode = self.table_widget.mode_typer.currentText() self.mode = self.table_widget.mode_typer.currentText()
self.table_widget.sub_typer.clear() self.table_widget.sub_typer.clear()
# lookup subtypes
sub_types = get_control_subtypes(ctx=self.ctx, type=self.con_type, mode=self.mode) sub_types = get_control_subtypes(ctx=self.ctx, type=self.con_type, mode=self.mode)
if sub_types != []: if sub_types != []:
# block signal that will rerun controls getter and update sub_typer
with QSignalBlocker(self.table_widget.sub_typer) as blocker: with QSignalBlocker(self.table_widget.sub_typer) as blocker:
self.table_widget.sub_typer.addItems(sub_types) self.table_widget.sub_typer.addItems(sub_types)
self.table_widget.sub_typer.setEnabled(True) self.table_widget.sub_typer.setEnabled(True)
self.table_widget.sub_typer.currentTextChanged.connect(self.chart_maker) self.table_widget.sub_typer.currentTextChanged.connect(self.chart_maker)
else: else:
self.table_widget.sub_typer.clear() self.table_widget.sub_typer.clear()
self.table_widget.sub_typer.setEnabled(False) self.table_widget.sub_typer.setEnabled(False)
self.chart_maker() self.chart_maker()
def chart_maker(self): def chart_maker(self):
"""
Creates plotly charts for webview
"""
logger.debug(f"Control getter context: \n\tControl type: {self.con_type}\n\tMode: {self.mode}\n\tStart Date: {self.start_date}\n\tEnd Date: {self.end_date}") logger.debug(f"Control getter context: \n\tControl type: {self.con_type}\n\tMode: {self.mode}\n\tStart Date: {self.start_date}\n\tEnd Date: {self.end_date}")
if self.table_widget.sub_typer.currentText() == "": if self.table_widget.sub_typer.currentText() == "":
self.subtype = None self.subtype = None
else: else:
self.subtype = self.table_widget.sub_typer.currentText() self.subtype = self.table_widget.sub_typer.currentText()
logger.debug(f"Subtype: {self.subtype}") logger.debug(f"Subtype: {self.subtype}")
# query all controls using the type/start and end dates from the gui
controls = get_all_controls_by_type(ctx=self.ctx, con_type=self.con_type, start_date=self.start_date, end_date=self.end_date) controls = get_all_controls_by_type(ctx=self.ctx, con_type=self.con_type, start_date=self.start_date, end_date=self.end_date)
# if no data found from query set fig to none for reporting in webview
if controls == None: if controls == None:
return # return
data = [] fig = None
for control in controls:
dicts = convert_control_by_mode(ctx=self.ctx, control=control, mode=self.mode)
data.append(dicts)
data = [item for sublist in data for item in sublist]
# logger.debug(data)
df = convert_data_list_to_df(ctx=self.ctx, input=data, subtype=self.subtype)
if self.subtype == None:
title = self.mode
else: else:
title = f"{self.mode} - {self.subtype}" data = []
fig = create_charts(ctx=self.ctx, df=df, ytitle=title) for control in controls:
# change each control to list of dicts
dicts = convert_control_by_mode(ctx=self.ctx, control=control, mode=self.mode)
data.append(dicts)
# flatten data to one dimensional list
data = [item for sublist in data for item in sublist]
# logger.debug(data)
# send to dataframe creator
df = convert_data_list_to_df(ctx=self.ctx, input=data, subtype=self.subtype)
if self.subtype == None:
title = self.mode
else:
title = f"{self.mode} - {self.subtype}"
# send dataframe to chart maker
fig = create_charts(ctx=self.ctx, df=df, ytitle=title)
logger.debug(f"Updating figure...") logger.debug(f"Updating figure...")
# construct html for webview
html = '<html><body>' html = '<html><body>'
if fig != None: if fig != None:
html += plotly.offline.plot(fig, output_type='div', include_plotlyjs='cdn')#, image = 'png', auto_open=True, image_filename='plot_image') html += plotly.offline.plot(fig, output_type='div', include_plotlyjs='cdn')#, image = 'png', auto_open=True, image_filename='plot_image')
@@ -393,24 +490,12 @@ class App(QMainWindow):
html += '</body></html>' html += '</body></html>'
# with open("C:\\Users\\lwark\\Desktop\\test.html", "w") as f: # with open("C:\\Users\\lwark\\Desktop\\test.html", "w") as f:
# f.write(html) # f.write(html)
# add html to webview and update.
self.table_widget.webengineview.setHtml(html) self.table_widget.webengineview.setHtml(html)
self.table_widget.webengineview.update() self.table_widget.webengineview.update()
logger.debug("Figure updated... I hope.") logger.debug("Figure updated... I hope.")
# def datechange(self):
# s_date = self.table_widget.datepicker.start_date.date()
# e_date = self.table_widget.datepicker.end_date.date()
# if s_date > e_date:
# logger.debug("that is not allowed!")
# # self.table_widget.datepicker.start_date.setDate(e_date)
# threemonthsago = e_date.addDays(-90)
# self.table_widget.datepicker.start_date.setDate(threemonthsago)
# self.chart_maker()
class AddSubForm(QWidget): class AddSubForm(QWidget):
def __init__(self, parent): def __init__(self, parent):
@@ -428,64 +513,57 @@ class AddSubForm(QWidget):
self.tabs.addTab(self.tab1,"Submissions") self.tabs.addTab(self.tab1,"Submissions")
self.tabs.addTab(self.tab2,"Controls") self.tabs.addTab(self.tab2,"Controls")
self.tabs.addTab(self.tab3, "Add Kit") self.tabs.addTab(self.tab3, "Add Kit")
# Create submission adder form
# Create first tab
# self.scroller = QWidget()
# self.scroller.layout = QVBoxLayout(self)
# self.scroller.setLayout(self.scroller.layout)
# self.tab1.setMaximumHeight(1000)
self.formwidget = QWidget(self) self.formwidget = QWidget(self)
self.formlayout = QVBoxLayout(self) self.formlayout = QVBoxLayout(self)
self.formwidget.setLayout(self.formlayout) self.formwidget.setLayout(self.formlayout)
self.formwidget.setFixedWidth(300) self.formwidget.setFixedWidth(300)
# Make scrollable interior for form
self.interior = QScrollArea(self.tab1) self.interior = QScrollArea(self.tab1)
# self.interior.verticalScrollBar()
# self.interior.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
self.interior.setWidgetResizable(True) self.interior.setWidgetResizable(True)
self.interior.setFixedWidth(325) self.interior.setFixedWidth(325)
# self.interior.setParent(self.tab1)
self.interior.setWidget(self.formwidget) self.interior.setWidget(self.formwidget)
# Create sheet to hold existing submissions
self.sheetwidget = QWidget(self) self.sheetwidget = QWidget(self)
self.sheetlayout = QVBoxLayout(self) self.sheetlayout = QVBoxLayout(self)
self.sheetwidget.setLayout(self.sheetlayout) self.sheetwidget.setLayout(self.sheetlayout)
self.sub_wid = SubmissionsSheet(parent.ctx) self.sub_wid = SubmissionsSheet(parent.ctx)
self.sheetlayout.addWidget(self.sub_wid) self.sheetlayout.addWidget(self.sub_wid)
# Create layout of first tab to hold form and sheet
self.tab1.layout = QHBoxLayout(self) self.tab1.layout = QHBoxLayout(self)
self.tab1.setLayout(self.tab1.layout) self.tab1.setLayout(self.tab1.layout)
# self.tab1.layout.addLayout(self.formlayout)
self.tab1.layout.addWidget(self.interior) self.tab1.layout.addWidget(self.interior)
# self.tab1.layout.addWidget(self.formwidget)
self.tab1.layout.addWidget(self.sheetwidget) self.tab1.layout.addWidget(self.sheetwidget)
# self.tab1.layout.addLayout(self.sheetlayout) # create widgets for tab 2
self.datepicker = ControlsDatePicker() self.datepicker = ControlsDatePicker()
self.webengineview = QWebEngineView() self.webengineview = QWebEngineView()
# set tab2 layout
self.tab2.layout = QVBoxLayout(self) self.tab2.layout = QVBoxLayout(self)
self.control_typer = QComboBox() self.control_typer = QComboBox()
# fetch types of controls
con_types = get_all_Control_Types_names(ctx=parent.ctx) con_types = get_all_Control_Types_names(ctx=parent.ctx)
self.control_typer.addItems(con_types) self.control_typer.addItems(con_types)
# create custom widget to get types of analysis
self.mode_typer = QComboBox() self.mode_typer = QComboBox()
mode_types = get_all_available_modes(ctx=parent.ctx) mode_types = get_all_available_modes(ctx=parent.ctx)
self.mode_typer.addItems(mode_types) self.mode_typer.addItems(mode_types)
# create custom widget to get subtypes of analysis
self.sub_typer = QComboBox() self.sub_typer = QComboBox()
self.sub_typer.setEnabled(False) self.sub_typer.setEnabled(False)
# add widgets to tab2 layout
self.tab2.layout.addWidget(self.datepicker) self.tab2.layout.addWidget(self.datepicker)
self.tab2.layout.addWidget(self.control_typer) self.tab2.layout.addWidget(self.control_typer)
self.tab2.layout.addWidget(self.mode_typer) self.tab2.layout.addWidget(self.mode_typer)
self.tab2.layout.addWidget(self.sub_typer) self.tab2.layout.addWidget(self.sub_typer)
self.tab2.layout.addWidget(self.webengineview) self.tab2.layout.addWidget(self.webengineview)
self.tab2.setLayout(self.tab2.layout) self.tab2.setLayout(self.tab2.layout)
# Add tabs to widget # create custom widget to add new tabs
adder = KitAdder(parent_ctx=parent.ctx) adder = KitAdder(parent_ctx=parent.ctx)
self.tab3.layout = QVBoxLayout(self) self.tab3.layout = QVBoxLayout(self)
self.tab3.layout.addWidget(adder) self.tab3.layout.addWidget(adder)
self.tab3.setLayout(self.tab3.layout) self.tab3.setLayout(self.tab3.layout)
# add tabs to main widget
self.layout.addWidget(self.tabs) self.layout.addWidget(self.tabs)
self.setLayout(self.layout) self.setLayout(self.layout)
print(self.tab1.layout.parentWidget().findChildren(QScrollArea))

View File

@@ -4,7 +4,8 @@ from PyQt6.QtWidgets import (
QDialogButtonBox, QDateEdit, QTableView, QDialogButtonBox, QDateEdit, QTableView,
QTextEdit, QSizePolicy, QWidget, QTextEdit, QSizePolicy, QWidget,
QGridLayout, QPushButton, QSpinBox, QGridLayout, QPushButton, QSpinBox,
QScrollBar, QScrollArea, QHBoxLayout QScrollBar, QScrollArea, QHBoxLayout,
QMessageBox
) )
from PyQt6.QtCore import Qt, QDate, QAbstractTableModel, QSize from PyQt6.QtCore import Qt, QDate, QAbstractTableModel, QSize
from PyQt6.QtGui import QFontMetrics from PyQt6.QtGui import QFontMetrics
@@ -26,7 +27,10 @@ loader = FileSystemLoader(loader_path)
env = Environment(loader=loader) env = Environment(loader=loader)
class AddReagentQuestion(QDialog): class AddReagentQuestion(QDialog):
def __init__(self, reagent_type:str, reagent_lot:str): """
dialog to ask about adding a new reagne to db
"""
def __init__(self, reagent_type:str, reagent_lot:str) -> None:
super().__init__() super().__init__()
self.setWindowTitle(f"Add {reagent_lot}?") self.setWindowTitle(f"Add {reagent_lot}?")
@@ -45,7 +49,10 @@ class AddReagentQuestion(QDialog):
class OverwriteSubQuestion(QDialog): class OverwriteSubQuestion(QDialog):
def __init__(self, message:str, rsl_plate_num:str): """
dialog to ask about overwriting existing submission
"""
def __init__(self, message:str, rsl_plate_num:str) -> None:
super().__init__() super().__init__()
self.setWindowTitle(f"Overwrite {rsl_plate_num}?") self.setWindowTitle(f"Overwrite {rsl_plate_num}?")
@@ -64,7 +71,10 @@ class OverwriteSubQuestion(QDialog):
class AddReagentForm(QDialog): class AddReagentForm(QDialog):
def __init__(self, ctx:dict, reagent_lot:str|None, reagent_type:str|None): """
dialog to add gather info about new reagent
"""
def __init__(self, ctx:dict, reagent_lot:str|None, reagent_type:str|None) -> None:
super().__init__() super().__init__()
if reagent_lot == None: if reagent_lot == None:
@@ -77,20 +87,23 @@ class AddReagentForm(QDialog):
self.buttonBox = QDialogButtonBox(QBtn) self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept) self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject) self.buttonBox.rejected.connect(self.reject)
# get lot info
lot_input = QLineEdit() lot_input = QLineEdit()
lot_input.setText(reagent_lot) lot_input.setText(reagent_lot)
# get expiry info
exp_input = QDateEdit(calendarPopup=True) exp_input = QDateEdit(calendarPopup=True)
exp_input.setDate(QDate.currentDate()) exp_input.setDate(QDate.currentDate())
# get reagent type info
type_input = QComboBox() type_input = QComboBox()
type_input.addItems([item.replace("_", " ").title() for item in get_all_reagenttype_names(ctx=ctx)]) type_input.addItems([item.replace("_", " ").title() for item in get_all_reagenttype_names(ctx=ctx)])
logger.debug(f"Trying to find index of {reagent_type}") logger.debug(f"Trying to find index of {reagent_type}")
# convert input to user friendly string?
try: try:
reagent_type = reagent_type.replace("_", " ").title() reagent_type = reagent_type.replace("_", " ").title()
except AttributeError: except AttributeError:
reagent_type = None reagent_type = None
# set parsed reagent type to top of list
index = type_input.findText(reagent_type, Qt.MatchFlag.MatchEndsWith) index = type_input.findText(reagent_type, Qt.MatchFlag.MatchEndsWith)
if index >= 0: if index >= 0:
type_input.setCurrentIndex(index) type_input.setCurrentIndex(index)
self.layout = QVBoxLayout() self.layout = QVBoxLayout()
@@ -106,18 +119,38 @@ class AddReagentForm(QDialog):
class pandasModel(QAbstractTableModel): class pandasModel(QAbstractTableModel):
"""
def __init__(self, data): pandas model for inserting summary sheet into gui
"""
def __init__(self, data) -> None:
QAbstractTableModel.__init__(self) QAbstractTableModel.__init__(self)
self._data = data self._data = data
def rowCount(self, parent=None): def rowCount(self, parent=None) -> int:
"""
does what it says
Args:
parent (_type_, optional): _description_. Defaults to None.
Returns:
int: number of rows in data
"""
return self._data.shape[0] return self._data.shape[0]
def columnCount(self, parnet=None): def columnCount(self, parnet=None) -> int:
"""
does what it says
Args:
parnet (_type_, optional): _description_. Defaults to None.
Returns:
int: number of columns in data
"""
return self._data.shape[1] return self._data.shape[1]
def data(self, index, role=Qt.ItemDataRole.DisplayRole): def data(self, index, role=Qt.ItemDataRole.DisplayRole) -> str|None:
if index.isValid(): if index.isValid():
if role == Qt.ItemDataRole.DisplayRole: if role == Qt.ItemDataRole.DisplayRole:
return str(self._data.iloc[index.row(), index.column()]) return str(self._data.iloc[index.row(), index.column()])
@@ -130,7 +163,16 @@ class pandasModel(QAbstractTableModel):
class SubmissionsSheet(QTableView): class SubmissionsSheet(QTableView):
def __init__(self, ctx:dict): """
presents submission summary to user in tab1
"""
def __init__(self, ctx:dict) -> None:
"""
initialize
Args:
ctx (dict): settings passed from gui
"""
super().__init__() super().__init__()
self.ctx = ctx self.ctx = ctx
self.setData() self.setData()
@@ -139,20 +181,19 @@ class SubmissionsSheet(QTableView):
# self.clicked.connect(self.test) # self.clicked.connect(self.test)
self.doubleClicked.connect(self.show_details) self.doubleClicked.connect(self.show_details)
def setData(self): def setData(self) -> None:
# horHeaders = [] """
# for n, key in enumerate(sorted(self.data.keys())): sets data in model
# horHeaders.append(key) """
# for m, item in enumerate(self.data[key]):
# newitem = QTableWidgetItem(item)
# self.setItem(m, n, newitem)
# self.setHorizontalHeaderLabels(horHeaders)
self.data = submissions_to_df(ctx=self.ctx) self.data = submissions_to_df(ctx=self.ctx)
self.model = pandasModel(self.data) self.model = pandasModel(self.data)
self.setModel(self.model) self.setModel(self.model)
# self.resize(800,600) # self.resize(800,600)
def show_details(self, item): def show_details(self) -> None:
"""
creates detailed data to show in seperate window
"""
index=(self.selectionModel().currentIndex()) index=(self.selectionModel().currentIndex())
# logger.debug(index) # logger.debug(index)
value=index.sibling(index.row(),0).data() value=index.sibling(index.row(),0).data()
@@ -165,60 +206,65 @@ class SubmissionsSheet(QTableView):
class SubmissionDetails(QDialog): class SubmissionDetails(QDialog):
"""
a window showing text details of submission
"""
def __init__(self, ctx:dict, id:int) -> None: def __init__(self, ctx:dict, id:int) -> None:
super().__init__() super().__init__()
self.setWindowTitle("Submission Details") self.setWindowTitle("Submission Details")
# create scrollable interior
interior = QScrollArea() interior = QScrollArea()
interior.setParent(self) interior.setParent(self)
# get submision from db
data = lookup_submission_by_id(ctx=ctx, id=id) data = lookup_submission_by_id(ctx=ctx, id=id)
base_dict = data.to_dict() base_dict = data.to_dict()
# don't want id
del base_dict['id'] del base_dict['id']
# convert sub objects to dicts
base_dict['reagents'] = [item.to_sub_dict() for item in data.reagents] base_dict['reagents'] = [item.to_sub_dict() for item in data.reagents]
base_dict['samples'] = [item.to_sub_dict() for item in data.samples] base_dict['samples'] = [item.to_sub_dict() for item in data.samples]
# retrieve jinja template
template = env.get_template("submission_details.txt") template = env.get_template("submission_details.txt")
# render using object dict
text = template.render(sub=base_dict) text = template.render(sub=base_dict)
# create text field
txt_editor = QTextEdit(self) txt_editor = QTextEdit(self)
txt_editor.setReadOnly(True) txt_editor.setReadOnly(True)
txt_editor.document().setPlainText(text) txt_editor.document().setPlainText(text)
# resize
font = txt_editor.document().defaultFont() font = txt_editor.document().defaultFont()
fontMetrics = QFontMetrics(font) fontMetrics = QFontMetrics(font)
textSize = fontMetrics.size(0, txt_editor.toPlainText()) textSize = fontMetrics.size(0, txt_editor.toPlainText())
w = textSize.width() + 10 w = textSize.width() + 10
h = textSize.height() + 10 h = textSize.height() + 10
txt_editor.setMinimumSize(w, h) txt_editor.setMinimumSize(w, h)
txt_editor.setMaximumSize(w, h) txt_editor.setMaximumSize(w, h)
txt_editor.resize(w, h) txt_editor.resize(w, h)
interior.resize(w,900) interior.resize(w,900)
# txt_field.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.MinimumExpanding)
# QBtn = QDialogButtonBox.StandardButton.Ok
# self.buttonBox = QDialogButtonBox(QBtn)
# self.buttonBox.accepted.connect(self.accept)
txt_editor.setText(text) txt_editor.setText(text)
# txt_editor.verticalScrollBar()
interior.setWidget(txt_editor) interior.setWidget(txt_editor)
self.layout = QVBoxLayout() self.layout = QVBoxLayout()
self.setFixedSize(w, 900) self.setFixedSize(w, 900)
# self.layout.addWidget(txt_editor)
# self.layout.addStretch()
self.layout.addWidget(interior) self.layout.addWidget(interior)
class ReportDatePicker(QDialog): class ReportDatePicker(QDialog):
"""
custom dialog to ask for report start/stop dates
"""
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
self.setWindowTitle("Select Report Date Range") self.setWindowTitle("Select Report Date Range")
# make confirm/reject buttons
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn) self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept) self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject) self.buttonBox.rejected.connect(self.reject)
# widgets to ask for dates
start_date = QDateEdit(calendarPopup=True) start_date = QDateEdit(calendarPopup=True)
start_date.setDate(QDate.currentDate()) start_date.setDate(QDate.currentDate())
end_date = QDateEdit(calendarPopup=True) end_date = QDateEdit(calendarPopup=True)
@@ -233,46 +279,61 @@ class ReportDatePicker(QDialog):
class KitAdder(QWidget): class KitAdder(QWidget):
def __init__(self, parent_ctx:dict): """
dialog to get information to add kit
"""
def __init__(self, parent_ctx:dict) -> None:
super().__init__() super().__init__()
self.ctx = parent_ctx self.ctx = parent_ctx
self.grid = QGridLayout() self.grid = QGridLayout()
self.setLayout(self.grid) self.setLayout(self.grid)
# insert submit button at top
self.submit_btn = QPushButton("Submit") self.submit_btn = QPushButton("Submit")
self.grid.addWidget(self.submit_btn,0,0,1,1) self.grid.addWidget(self.submit_btn,0,0,1,1)
self.grid.addWidget(QLabel("Password:"),1,0) # need to exclude ordinary users to mitigate garbage database entries
self.grid.addWidget(QLineEdit(),1,1) # self.grid.addWidget(QLabel("Password:"),1,0)
# self.grid.addWidget(QLineEdit(),1,1)
self.grid.addWidget(QLabel("Kit Name:"),2,0) self.grid.addWidget(QLabel("Kit Name:"),2,0)
self.grid.addWidget(QLineEdit(),2,1) self.grid.addWidget(QLineEdit(),2,1)
self.grid.addWidget(QLabel("Used For Sample Type:"),3,0) self.grid.addWidget(QLabel("Used For Sample Type:"),3,0)
used_for = QComboBox() used_for = QComboBox()
# Insert all existing sample types
used_for.addItems(lookup_all_sample_types(ctx=parent_ctx)) used_for.addItems(lookup_all_sample_types(ctx=parent_ctx))
used_for.setEditable(True) used_for.setEditable(True)
self.grid.addWidget(used_for,3,1) self.grid.addWidget(used_for,3,1)
# set cost per run
self.grid.addWidget(QLabel("Cost per run:"),4,0) self.grid.addWidget(QLabel("Cost per run:"),4,0)
cost = QSpinBox() cost = QSpinBox()
cost.setMinimum(0) cost.setMinimum(0)
cost.setMaximum(9999) cost.setMaximum(9999)
self.grid.addWidget(cost,4,1) self.grid.addWidget(cost,4,1)
# button to add additional reagent types
self.add_RT_btn = QPushButton("Add Reagent Type") self.add_RT_btn = QPushButton("Add Reagent Type")
self.grid.addWidget(self.add_RT_btn) self.grid.addWidget(self.add_RT_btn)
self.add_RT_btn.clicked.connect(self.add_RT) self.add_RT_btn.clicked.connect(self.add_RT)
self.submit_btn.clicked.connect(self.submit) self.submit_btn.clicked.connect(self.submit)
def add_RT(self): def add_RT(self) -> None:
"""
insert new reagent type row
"""
maxrow = self.grid.rowCount() maxrow = self.grid.rowCount()
self.grid.addWidget(ReagentTypeForm(parent_ctx=self.ctx), maxrow + 1,0,1,2) self.grid.addWidget(ReagentTypeForm(parent_ctx=self.ctx), maxrow + 1,0,1,2)
def submit(self): def submit(self) -> None:
"""
send kit to database
"""
# get form info
labels, values, reagents = self.extract_form_info(self) labels, values, reagents = self.extract_form_info(self)
info = {item[0]:item[1] for item in zip(labels, values)} info = {item[0]:item[1] for item in zip(labels, values)}
logger.debug(info) logger.debug(info)
# info['reagenttypes'] = reagents
# del info['name']
# del info['extension_of_life_(months)']
yml_type = {} yml_type = {}
yml_type['password'] = info['password'] try:
yml_type['password'] = info['password']
except KeyError:
pass
used = info['used_for_sample_type'].replace(" ", "_").lower() used = info['used_for_sample_type'].replace(" ", "_").lower()
yml_type[used] = {} yml_type[used] = {}
yml_type[used]['kits'] = {} yml_type[used]['kits'] = {}
@@ -280,9 +341,32 @@ class KitAdder(QWidget):
yml_type[used]['kits'][info['kit_name']]['cost'] = info['cost_per_run'] yml_type[used]['kits'][info['kit_name']]['cost'] = info['cost_per_run']
yml_type[used]['kits'][info['kit_name']]['reagenttypes'] = reagents yml_type[used]['kits'][info['kit_name']]['reagenttypes'] = reagents
logger.debug(yml_type) logger.debug(yml_type)
create_kit_from_yaml(ctx=self.ctx, exp=yml_type) # send to kit constructor
result = create_kit_from_yaml(ctx=self.ctx, exp=yml_type)
# result = create_kit_from_yaml(ctx=self.ctx, exp=exp)
msg = QMessageBox()
# msg.setIcon(QMessageBox.critical)
match result['code']:
case 0:
msg.setText("Kit added")
msg.setInformativeText(result['message'])
msg.setWindowTitle("Kit added")
case 1:
msg.setText("Permission Error")
msg.setInformativeText(result['message'])
msg.setWindowTitle("Permission Error")
msg.exec()
def extract_form_info(self, object): def extract_form_info(self, object):
"""
retrieves arbitrary number of labels, values from form
Args:
object (_type_): the object to extract info from
Returns:
_type_: _description_
"""
labels = [] labels = []
values = [] values = []
reagents = {} reagents = {}
@@ -317,26 +401,35 @@ class KitAdder(QWidget):
class ReagentTypeForm(QWidget): class ReagentTypeForm(QWidget):
"""
custom widget to add information about a new reagenttype
"""
def __init__(self, parent_ctx:dict) -> None: def __init__(self, parent_ctx:dict) -> None:
super().__init__() super().__init__()
grid = QGridLayout() grid = QGridLayout()
self.setLayout(grid) self.setLayout(grid)
grid.addWidget(QLabel("Name:"),0,0) grid.addWidget(QLabel("Name:"),0,0)
reagent_getter = QComboBox() reagent_getter = QComboBox()
# lookup all reagent type names from db
reagent_getter.addItems(get_all_reagenttype_names(ctx=parent_ctx)) reagent_getter.addItems(get_all_reagenttype_names(ctx=parent_ctx))
reagent_getter.setEditable(True) reagent_getter.setEditable(True)
grid.addWidget(reagent_getter,0,1) grid.addWidget(reagent_getter,0,1)
grid.addWidget(QLabel("Extension of Life (months):"),0,2) grid.addWidget(QLabel("Extension of Life (months):"),0,2)
# get extension of life
eol = QSpinBox() eol = QSpinBox()
eol.setMinimum(0) eol.setMinimum(0)
grid.addWidget(eol, 0,3) grid.addWidget(eol, 0,3)
class ControlsDatePicker(QWidget): class ControlsDatePicker(QWidget):
"""
custom widget to pick start and end dates for controls graphs
"""
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
self.start_date = QDateEdit(calendarPopup=True) self.start_date = QDateEdit(calendarPopup=True)
# start date is three month prior to end date by default
threemonthsago = QDate.currentDate().addDays(-90) threemonthsago = QDate.currentDate().addDays(-90)
self.start_date.setDate(threemonthsago) self.start_date.setDate(threemonthsago)
self.end_date = QDateEdit(calendarPopup=True) self.end_date = QDateEdit(calendarPopup=True)
@@ -350,5 +443,5 @@ class ControlsDatePicker(QWidget):
self.setLayout(self.layout) self.setLayout(self.layout)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
def sizeHint(self): def sizeHint(self) -> QSize:
return QSize(80,20) return QSize(80,20)

View File

@@ -13,14 +13,15 @@ def create_charts(ctx:dict, df:pd.DataFrame, ytitle:str|None=None) -> Figure:
Constructs figures based on parsed pandas dataframe. Constructs figures based on parsed pandas dataframe.
Args: Args:
settings (dict): settings passed down from click settings (dict): settings passed down from gui
df (pd.DataFrame): input dataframe df (pd.DataFrame): input dataframe
group_name (str): controltype group_name (str): controltype
Returns: Returns:
Figure: _description_ Figure: plotly figure
""" """
from backend.excel import drop_reruns_from_df from backend.excel import drop_reruns_from_df
# converts starred genera to normal and splits off list of starred
genera = [] genera = []
if df.empty: if df.empty:
return None return None
@@ -35,15 +36,17 @@ def create_charts(ctx:dict, df:pd.DataFrame, ytitle:str|None=None) -> Figure:
df['genus'] = df['genus'].replace({'\*':''}, regex=True) df['genus'] = df['genus'].replace({'\*':''}, regex=True)
df['genera'] = genera df['genera'] = genera
df = df.dropna() df = df.dropna()
# remove original runs, using reruns if applicable
df = drop_reruns_from_df(ctx=ctx, df=df) df = drop_reruns_from_df(ctx=ctx, df=df)
# sort by and exclude from
sorts = ['submitted_date', "target", "genus"] sorts = ['submitted_date', "target", "genus"]
exclude = ['name', 'genera'] exclude = ['name', 'genera']
modes = [item for item in df.columns if item not in sorts and item not in exclude and "_hashes" not in item] modes = [item for item in df.columns if item not in sorts and item not in exclude and "_hashes" not in item]
# Set descending for any columns that have "{mode}" in the header. # Set descending for any columns that have "{mode}" in the header.
ascending = [False if item == "target" else True for item in sorts] ascending = [False if item == "target" else True for item in sorts]
df = df.sort_values(by=sorts, ascending=ascending) df = df.sort_values(by=sorts, ascending=ascending)
# actual chart construction is done by
fig = construct_chart(ctx=ctx, df=df, modes=modes, ytitle=ytitle) fig = construct_chart(ctx=ctx, df=df, modes=modes, ytitle=ytitle)
return fig return fig
@@ -186,7 +189,7 @@ def construct_chart(ctx:dict, df:pd.DataFrame, modes:list, ytitle:str|None=None)
def construct_refseq_chart(settings:dict, df:pd.DataFrame, group_name:str, mode:str) -> Figure: def construct_refseq_chart(settings:dict, df:pd.DataFrame, group_name:str, mode:str) -> Figure:
""" """
Constructs intial refseq chart for both contains and matches. Constructs intial refseq chart for both contains and matches (depreciated).
Args: Args:
settings (dict): settings passed down from click. settings (dict): settings passed down from click.
@@ -218,7 +221,7 @@ def construct_refseq_chart(settings:dict, df:pd.DataFrame, group_name:str, mode:
def construct_kraken_chart(settings:dict, df:pd.DataFrame, group_name:str, mode:str) -> Figure: def construct_kraken_chart(settings:dict, df:pd.DataFrame, group_name:str, mode:str) -> Figure:
""" """
Constructs intial refseq chart for each mode in the kraken config settings. Constructs intial refseq chart for each mode in the kraken config settings. (depreciated)
Args: Args:
settings (dict): settings passed down from click. settings (dict): settings passed down from click.

View File

@@ -1,4 +1,4 @@
{# template for constructing submission details #}
{% for key, value in sub.items() if key != 'reagents' and key != 'samples' %} {% for key, value in sub.items() if key != 'reagents' and key != 'samples' %}
{{ key }}: {{ value }} {{ key }}: {{ value }}