Major refactor for improved documentation and readability

This commit is contained in:
Landon Wark
2023-03-17 15:23:22 -05:00
parent c645d3a9cf
commit 9c9c373830
29 changed files with 1828 additions and 1236 deletions

View File

@@ -0,0 +1,6 @@
**202303.03**
- Increased robustness by utilizing PyQT6 widget names to pull data from forms instead of previously used label/input zip.
- Above allowed for creation of more helpful prompts.
- Added sorting feature to Submission summary.
- Reagent import dropdowns will now prioritize lot number found in a parsed sheet, moving it to the top of the list.

View File

@@ -0,0 +1 @@
**This is the readme file**

View File

@@ -1,6 +1,6 @@
# __init__.py # __init__.py
# Version of the realpython-reader package # Version of the realpython-reader package
__version__ = "202303.2b" __version__ = "202303.3b"
__author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"} __author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"}
__copyright__ = "2022-2023, Government of Canada" __copyright__ = "2022-2023, Government of Canada"

View File

@@ -17,7 +17,7 @@ import __init__ as package
# create database session for use with gui session # 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__ # set package information from __init__
ctx['package'] = package ctx['package'] = package
if __name__ == '__main__': if __name__ == '__main__':
@@ -25,85 +25,3 @@ 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())
# from pathlib import Path
# from tkinter import *
# from tkinter import filedialog as fd
# from tkinter import ttk
# from tkinterhtml import HtmlFrame
# from xl_parser import SheetParser
# class Window(Frame):
# def __init__(self, master=None):
# Frame.__init__(self, master)
# self.master = master
# # Frame.pack_propagate(False)
# menu = Menu(self.master)
# self.master.config(menu=menu)
# fileMenu = Menu(menu)
# fileMenu.add_command(label="Import", command=self.import_callback)
# fileMenu.add_command(label="Exit", command=self.exitProgram)
# menu.add_cascade(label="File", menu=fileMenu)
# editMenu = Menu(menu)
# editMenu.add_command(label="Undo")
# editMenu.add_command(label="Redo")
# menu.add_cascade(label="Edit", menu=editMenu)
# tab_parent = ttk.Notebook(self.master)
# self.add_sample_tab = ttk.Frame(tab_parent)
# self.control_view_tab = HtmlFrame(tab_parent)
# tab_parent.add(self.add_sample_tab, text="Add Sample")
# tab_parent.add(self.control_view_tab, text="Controls View")
# tab_parent.pack()
# with open("L:\Robotics Laboratory Support\Quality\Robotics Support Laboratory Extraction Controls\MCS-SSTI.html", "r") as f:
# data = f.read()
# # frame =
# # frame.set_content(data)
# # self.control_view_tab.set_content("""
# # <html>
# # <body>
# # <h1>Hello world!</h1>
# # <p>First para</p>
# # <ul>
# # <li>first list item</li>
# # <li>second list item</li>
# # </ul>
# # <img src="http://findicons.com/files/icons/638/magic_people/128/magic_ball.png"/>
# # </body>
# # </html>
# # """)
# def exitProgram(self):
# exit()
# def import_callback(self):
# name= fd.askopenfilename()
# prsr = SheetParser(Path(name), **ctx)
# for item in prsr.sub:
# lbl=Label(self.add_sample_tab, text=item, fg='red', font=("Helvetica", 16))
# lbl.pack()
# txtfld=Entry(self.add_sample_tab, text="Data not set", bd=2)
# txtfld.pack()
# txtfld.delete(0,END)
# txtfld.insert(0,prsr.sub[item])
# root = Tk()
# app = Window(root)
# # for item in test_data:
# # lbl=Label(root, text=item, fg='red', font=("Helvetica", 16))
# # lbl.pack()
# # txtfld=Entry(root, text="", bd=2)
# # txtfld.pack()
# # txtfld.delete(0,END)
# # txtfld.insert(0,test_data[item])
# root.wm_title("Tkinter window")
# root.mainloop()

View File

@@ -0,0 +1,3 @@
'''
Contains database and excel operations.
'''

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,669 @@
'''
Convenience functions for interacting with the database.
'''
from . import models
from .models.kits import reagenttypes_kittypes
from .models.submissions import reagents_submissions
import pandas as pd
import sqlalchemy.exc
import sqlite3
import logging
from datetime import date, datetime, timedelta
from sqlalchemy import and_
import uuid
from sqlalchemy import JSON, event
from sqlalchemy.engine import Engine
import json
from getpass import getuser
import numpy as np
import yaml
from pathlib import Path
logger = logging.getLogger(f"submissions.{__name__}")
# The below _should_ allow automatic creation of foreign keys in the database
@event.listens_for(Engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()
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")
# Add all samples to sample table
for sample in base_submission.samples:
sample.rsl_plate = base_submission
logger.debug(f"Attempting to add sample: {sample.to_string()}")
try:
ctx['database_session'].add(sample)
except (sqlite3.IntegrityError, sqlalchemy.exc.IntegrityError) as e:
logger.debug(f"Hit an integrity error : {e}")
continue
# Add submission to submission table
ctx['database_session'].add(base_submission)
logger.debug(f"Attempting to add submission: {base_submission.rsl_plate_num}")
try:
ctx['database_session'].commit()
except (sqlite3.IntegrityError, sqlalchemy.exc.IntegrityError) as e:
logger.debug(f"Hit an integrity error : {e}")
ctx['database_session'].rollback()
return {"message":"This plate number already exists, so we can't add it."}
except (sqlite3.OperationalError, sqlalchemy.exc.IntegrityError) as e:
logger.debug(f"Hit an operational error: {e}")
ctx['database_session'].rollback()
return {"message":"The database is locked for editing."}
return None
def store_reagent(ctx:dict, reagent:models.Reagent) -> None|dict:
"""
Inserts a reagent into the database.
Args:
ctx (dict): settings passed down from gui
reagent (models.Reagent): Reagent object to be added to db
Returns:
None|dict: object indicating issue to be reported in the gui
"""
logger.debug(f"Reagent dictionary: {reagent.__dict__}")
ctx['database_session'].add(reagent)
try:
ctx['database_session'].commit()
except (sqlite3.OperationalError, sqlalchemy.exc.OperationalError):
return {"message":"The database is locked for editing."}
return None
def construct_submission_info(ctx:dict, info_dict:dict) -> models.BasicSubmission:
"""
Construct submission object from dictionary
Args:
ctx (dict): settings passed down from gui
info_dict (dict): dictionary to be transformed
Returns:
models.BasicSubmission: Constructed submission object
"""
from tools import check_not_nan
# convert submission type into model name
query = info_dict['submission_type'].replace(" ", "")
# Ensure an rsl plate number exists for the plate
if info_dict["rsl_plate_num"] == 'nan' or info_dict["rsl_plate_num"] == None or not check_not_nan(info_dict["rsl_plate_num"]):
instance = None
msg = "A proper RSL plate number is required."
return instance, {'code': 2, 'message': "A proper RSL plate number is required."}
# check database for existing object
instance = ctx['database_session'].query(models.BasicSubmission).filter(models.BasicSubmission.rsl_plate_num==info_dict['rsl_plate_num']).first()
# get model based on submission type converted above
logger.debug(f"Looking at models for submission type: {query}")
model = getattr(models, query)
logger.debug(f"We've got the model: {type(model)}")
info_dict['submission_type'] = info_dict['submission_type'].replace(" ", "_").lower()
# if query return nothing, ie doesn't already exist in db
if instance == None:
instance = model()
logger.debug(f"Submission doesn't exist yet, creating new instance: {instance}")
msg = None
code = 0
else:
code = 1
msg = "This submission already exists.\nWould you like to overwrite?"
for item in info_dict:
logger.debug(f"Setting {item} to {info_dict[item]}")
# set fields based on keys in dictionary
match item:
case "extraction_kit":
q_str = info_dict[item]
logger.debug(f"Looking up kit {q_str}")
try:
field_value = lookup_kittype_by_name(ctx=ctx, name=q_str)
except (sqlite3.IntegrityError, sqlalchemy.exc.IntegrityError) as e:
logger.error(f"Hit an integrity error: {e}")
logger.debug(f"Got {field_value} for kit {q_str}")
case "submitting_lab":
q_str = info_dict[item].replace(" ", "_").lower()
logger.debug(f"Looking up organization: {q_str}")
field_value = lookup_org_by_name(ctx=ctx, name=q_str)
logger.debug(f"Got {field_value} for organization {q_str}")
case "submitter_plate_num":
# Because of unique constraint, there will be problems with
# multiple submissions named 'None', so...
logger.debug(f"Submitter plate id: {info_dict[item]}")
if info_dict[item] == None or info_dict[item] == "None":
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()
field_value = info_dict[item]
case _:
field_value = info_dict[item]
# insert into field
try:
setattr(instance, item, field_value)
except AttributeError:
logger.debug(f"Could not set attribute: {item} to {info_dict[item]}")
continue
# calculate cost of the run: immutable cost + mutable times number of columns
# This is now attached to submission upon creation to preserve at-run costs incase of cost increase in the future.
try:
instance.run_cost = instance.extraction_kit.immutable_cost + (instance.extraction_kit.mutable_cost * ((instance.sample_count / 8)/12))
except (TypeError, AttributeError):
logger.debug(f"Looks like that kit doesn't have cost breakdown yet, using full plate cost.")
instance.run_cost = instance.extraction_kit.cost_per_run
# We need to make sure there's a proper rsl plate number
try:
logger.debug(f"Constructed instance: {instance.to_string()}")
except AttributeError as e:
logger.debug(f"Something went wrong constructing instance {info_dict['rsl_plate_num']}: {e}")
logger.debug(f"Constructed submissions message: {msg}")
return instance, {'code':code, 'message':msg}
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()
for item in info_dict:
logger.debug(f"Reagent info item: {item}")
# set fields based on keys in dictionary
match item:
case "lot":
reagent.lot = info_dict[item].upper()
case "expiry":
reagent.expiry = info_dict[item]
case "type":
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
# NOTE: this will now be done only in the reporting phase to account for potential changes in end-of-life extensions
# try:
# reagent.expiry = reagent.expiry + reagent.type.eol_ext
# except TypeError as e:
# logger.debug(f"We got a type error: {e}.")
# except AttributeError:
# pass
return reagent
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()
return lookedup
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()]
return lookedup
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}")
lookedup = ctx['database_session'].query(models.ReagentType).filter(models.ReagentType.name==rt_name).first()
logger.debug(f"Found ReagentType: {lookedup}")
return lookedup
def lookup_kittype_by_use(ctx:dict, used_by:str) -> list[models.KitType]:
"""
Lookup kits by a 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)).all()
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}")
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.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 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]:
"""
Lookup reagents by their type name and kits they belong to (Broken... maybe cursed, I'm not sure.)
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
"""
# What I want to do is get the reagent type by name
# 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)).all()
rt_types = ctx['database_session'].query(models.ReagentType).filter(models.ReagentType.name.endswith(type_name))
# add filter for kit name...
try:
check = not np.isnan(kit_name)
except TypeError:
check = True
if check:
kit_type = lookup_kittype_by_name(ctx=ctx, name=kit_name)
logger.debug(f"reagenttypes: {[item.name for item in rt_types.all()]}, kit: {kit_type.name}")
# add in lookup for related kit_id
rt_types = rt_types.join(reagenttypes_kittypes).filter(reagenttypes_kittypes.c.kits_id==kit_type.id).first()
else:
rt_types = rt_types.first()
output = rt_types.instances
return output
def lookup_all_submissions_by_type(ctx:dict, sub_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:
list[models.BasicSubmission]: list of retrieved submissions
"""
if sub_type == None:
subs = ctx['database_session'].query(models.BasicSubmission).all()
else:
subs = ctx['database_session'].query(models.BasicSubmission).filter(models.BasicSubmission.submission_type==sub_type.lower().replace(" ", "_")).all()
return subs
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()
def lookup_org_by_name(ctx:dict, name:str|None) -> models.Organization:
"""
Lookup organization (lab) by (startswith) 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}")
return ctx['database_session'].query(models.Organization).filter(models.Organization.name.startswith(name)).first()
def submissions_to_df(ctx:dict, sub_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: {sub_type}")
# use lookup function to create list of dicts
subs = [item.to_dict() for item in lookup_all_submissions_by_type(ctx=ctx, sub_type=sub_type)]
# make df from dicts (records) in list
df = pd.DataFrame.from_records(subs)
# Exclude sub information
try:
df = df.drop("controls", axis=1)
except:
logger.warning(f"Couldn't drop 'controls' column from submissionsheet df.")
try:
df = df.drop("ext_info", axis=1)
except:
logger.warning(f"Couldn't drop 'controls' column from submissionsheet df.")
return df
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()
def lookup_submissions_by_date_range(ctx:dict, start_date:datetime.date, end_date:datetime.date) -> list[models.BasicSubmission]:
"""
Lookup submissions greater than start_date and less than end_date
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()
start_date = start_date.strftime("%Y-%m-%d")
end_date = end_date.strftime("%Y-%m-%d")
return ctx['database_session'].query(models.BasicSubmission).filter(models.BasicSubmission.submitted_date.between(start_date, end_date)).all()
def get_all_Control_Types_names(ctx:dict) -> list[str]:
"""
Grabs all control type names from db.
Args:
settings (dict): settings passed down from gui.
Returns:
list: list of controltype names
"""
conTypes = ctx['database_session'].query(models.ControlType).all()
conTypes = [conType.name for conType in conTypes]
logger.debug(f"Control Types: {conTypes}")
return conTypes
def create_kit_from_yaml(ctx:dict, exp:dict) -> dict:
"""
Create and store a new kit in the database based on a .yml file
TODO: split into create and store functions
Args:
ctx (dict): Context dictionary passed down from frontend
exp (dict): Experiment dictionary created from yaml file
Returns:
dict: a dictionary containing results of db addition
"""
from tools import check_is_power_user
# Don't want just anyone adding kits
if not check_is_power_user(ctx=ctx):
logger.debug(f"{getuser()} does not have permission to add kits.")
return {'code':1, 'message':"This user does not have permission to add kits.", "status":"warning"}
# iterate through keys in dict
for type in exp:
if type == "password":
continue
# A submission type may use multiple kits.
for kt in exp[type]['kits']:
kit = models.KitType(name=kt, used_for=[type.replace("_", " ").title()], constant_cost=exp[type]["kits"][kt]["constant_cost"], mutable_cost=exp[type]["kits"][kt]["mutable_cost"])
# A kit contains multiple reagent types.
for r in exp[type]['kits'][kt]['reagenttypes']:
# check if reagent type already exists.
look_up = ctx['database_session'].query(models.ReagentType).filter(models.ReagentType.name==r).first()
if look_up == None:
rt = models.ReagentType(name=r.replace(" ", "_").lower(), eol_ext=timedelta(30*exp[type]['kits'][kt]['reagenttypes'][r]['eol_ext']), kits=[kit])
else:
rt = look_up
rt.kits.append(kit)
# add this because I think it's necessary to get proper back population
kit.reagent_types_id.append(rt.id)
ctx['database_session'].add(rt)
logger.debug(f"Kit construction reagent type: {rt.__dict__}")
logger.debug(f"Kit construction kit: {kit.__dict__}")
ctx['database_session'].add(kit)
ctx['database_session'].commit()
return {'code':0, 'message':'Kit has been added', 'status': 'information'}
def create_org_from_yaml(ctx:dict, org:dict) -> dict:
"""
Create and store a new organization based on a .yml file
Args:
ctx (dict): Context dictionary passed down from frontend
org (dict): Dictionary containing organization info.
Returns:
dict: dictionary containing results of db addition
"""
from tools import check_is_power_user
# Don't want just anyone adding in clients
if not check_is_power_user(ctx=ctx):
logger.debug(f"{getuser()} does not have permission to add kits.")
return {'code':1, 'message':"This user does not have permission to add organizations."}
# the yml can contain multiple clients
for client in org:
cli_org = models.Organization(name=client.replace(" ", "_").lower(), cost_centre=org[client]['cost centre'])
# a client can contain multiple contacts
for contact in org[client]['contacts']:
cont_name = list(contact.keys())[0]
# check if contact already exists
look_up = ctx['database_session'].query(models.Contact).filter(models.Contact.name==cont_name).first()
if look_up == None:
cli_cont = models.Contact(name=cont_name, phone=contact[cont_name]['phone'], email=contact[cont_name]['email'], organization=[cli_org])
else:
cli_cont = look_up
cli_cont.organization.append(cli_org)
ctx['database_session'].add(cli_cont)
logger.debug(f"Client creation contact: {cli_cont.__dict__}")
logger.debug(f"Client creation client: {cli_org.__dict__}")
ctx['database_session'].add(cli_org)
ctx["database_session"].commit()
return {"code":0, "message":"Organization has been added."}
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()]
# flattened list of lists
uses = list(set([item for sublist in uses for item in sublist]))
return uses
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
"""
# Only one control is necessary since they all share the same control types.
rel = ctx['database_session'].query(models.Control).first()
try:
cols = [item.name for item in list(rel.__table__.columns) if isinstance(item.type, JSON)]
except AttributeError as e:
logger.debug(f"Failed to get available modes from db: {e}")
cols = []
return cols
def get_all_controls_by_type(ctx:dict, con_type:str, start_date:date|None=None, end_date:date|None=None) -> list[models.Control]:
"""
Returns a list of control objects that are instances of the input controltype.
Between dates if supplied.
Args:
ctx (dict): Settings passed down from gui
con_type (str): Name of control type.
start_date (date | None, optional): Start date of query. Defaults to None.
end_date (date | None, optional): End date of query. Defaults to None.
Returns:
list[models.Control]: list of control samples.
"""
logger.debug(f"Using dates: {start_date} to {end_date}")
if start_date != None and end_date != None:
start_date = start_date.strftime("%Y-%m-%d")
end_date = end_date.strftime("%Y-%m-%d")
output = ctx['database_session'].query(models.Control).join(models.ControlType).filter_by(name=con_type).filter(models.Control.submitted_date.between(start_date, end_date)).all()
else:
output = ctx['database_session'].query(models.Control).join(models.ControlType).filter_by(name=con_type).all()
logger.debug(f"Returned controls between dates: {output}")
return output
def get_control_subtypes(ctx:dict, type:str, mode:str) -> list[str]:
"""
Get subtypes for a control analysis mode
Args:
ctx (dict): settings passed from gui
type (str): control type name
mode (str): analysis mode name
Returns:
list[str]: list of subtype names
"""
# Only the first control of type is necessary since they all share subtypes
try:
outs = get_all_controls_by_type(ctx=ctx, con_type=type)[0]
except TypeError:
return []
# Get analysis mode data as dict
jsoner = json.loads(getattr(outs, mode))
logger.debug(f"JSON out: {jsoner}")
try:
genera = list(jsoner.keys())[0]
except IndexError:
return []
subtypes = [item for item in jsoner[genera] if "_hashes" not in item and "_ratio" not in item]
return subtypes
def get_all_controls(ctx:dict) -> list[models.Control]:
"""
Retrieve a list of all controls from the database
Args:
ctx (dict): settings passed down from the gui.
Returns:
list[models.Control]: list of all control objects
"""
return ctx['database_session'].query(models.Control).all()
def lookup_submission_by_rsl_num(ctx:dict, rsl_num:str) -> models.BasicSubmission:
"""
Retrieve a submission from the database based on rsl plate number
Args:
ctx (dict): settings passed down from gui
rsl_num (str): rsl plate number
Returns:
models.BasicSubmission: Submissions object retrieved from database
"""
return ctx['database_session'].query(models.BasicSubmission).filter(models.BasicSubmission.rsl_plate_num.startswith(rsl_num)).first()
def lookup_submissions_using_reagent(ctx:dict, reagent:models.Reagent) -> list[models.BasicSubmission]:
return ctx['database_session'].query(models.BasicSubmission).join(reagents_submissions).filter(reagents_submissions.c.reagent_id==reagent.id).all()
def delete_submission_by_id(ctx:dict, id:int) -> None:
"""
Deletes a submission and its associated samples from the database.
Args:
ctx (dict): settings passed down from gui
id (int): id of submission to be deleted.
"""
# In order to properly do this Im' going to have to delete all of the secondary table stuff as well.
# Retrieve submission
sub = ctx['database_session'].query(models.BasicSubmission).filter(models.BasicSubmission.id==id).first()
# Convert to dict for storing backup as a yml
backup = sub.to_dict()
try:
with open(Path(ctx['backup_path']).joinpath(f"{sub.rsl_plate_num}-backup({date.today().strftime('%Y%m%d')}).yml"), "w") as f:
yaml.dump(backup, f)
except KeyError:
pass
sub.reagents = []
for sample in sub.samples:
ctx['database_session'].delete(sample)
ctx["database_session"].delete(sub)
ctx["database_session"].commit()

View File

@@ -1,21 +0,0 @@
# from ..models import *
# import logging
# logger = logging.getLogger(f"submissions.{__name__}")
# def check_kit_integrity(sub:BasicSubmission):
# ext_kit_rtypes = [reagenttype.name for reagenttype in sub.extraction_kit.reagent_types]
# logger.debug(f"Kit reagents: {ext_kit_rtypes}")
# reagenttypes = [reagent.type.name for reagent in sub.reagents]
# logger.debug(f"Submission reagents: {reagenttypes}")
# check = set(ext_kit_rtypes) == set(reagenttypes)
# logger.debug(f"Checking if reagents match kit contents: {check}")
# common = list(set(ext_kit_rtypes).intersection(reagenttypes))
# logger.debug(f"common reagents types: {common}")
# if check:
# result = None
# else:
# result = {'message' : f"Couldn't verify reagents match listed kit components.\n\nIt looks like you are missing: {[x.upper for x in ext_kit_rtypes if x not in common]}\n\nAlternatively, you may have set the wrong extraction kit."}
# return result

View File

@@ -1,5 +1,7 @@
'''
Contains all models for sqlalchemy
'''
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base() Base = declarative_base()
metadata = Base.metadata metadata = Base.metadata

View File

@@ -1,3 +1,6 @@
'''
All control related models.
'''
from . import Base from . import Base
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship

View File

@@ -1,4 +1,6 @@
from copy import deepcopy '''
All kit and reagent related models
'''
from . import Base from . import Base
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship

View File

@@ -1,3 +1,6 @@
'''
All client organization related models.
'''
from . import Base from . import Base
from sqlalchemy import Column, String, INTEGER, ForeignKey, Table from sqlalchemy import Column, String, INTEGER, ForeignKey, Table
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship

View File

@@ -1,3 +1,6 @@
'''
All models for individual samples.
'''
from . import Base from . import Base
from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, FLOAT, BOOLEAN from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, FLOAT, BOOLEAN
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship

View File

@@ -1,3 +1,6 @@
'''
Models for the main submission types.
'''
from . import Base from . import Base
from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, Table, JSON, FLOAT from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, Table, JSON, FLOAT
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship

View File

@@ -1,43 +1,50 @@
from pandas import DataFrame '''
import re Contains pandas convenience functions for interacting with excel workbooks
'''
from .reports import *
from .parser import *
# from pandas import DataFrame
# 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:
""" # """
get all unique values in a dataframe column by name # get all unique values in a dataframe column by name
Args: # Args:
df (DataFrame): input dataframe # df (DataFrame): input dataframe
column_name (str): name of column of interest # column_name (str): name of column of interest
Returns: # Returns:
list: sorted list of unique values # list: sorted list of unique values
""" # """
return sorted(df[column_name].unique()) # return sorted(df[column_name].unique())
def drop_reruns_from_df(ctx:dict, df: DataFrame) -> DataFrame: # 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 from gui # settings (dict): settings passed from gui
df (DataFrame): initial dataframe # df (DataFrame): initial dataframe
Returns: # Returns:
DataFrame: dataframe with originals removed in favour of repeats. # DataFrame: dataframe with originals removed in favour of repeats.
""" # """
sample_names = get_unique_values_in_df_column(df, column_name="name") # sample_names = get_unique_values_in_df_column(df, column_name="name")
if 'rerun_regex' in ctx: # if 'rerun_regex' in ctx:
# logger.debug(f"Compiling regex from: {settings['rerun_regex']}") # # logger.debug(f"Compiling regex from: {settings['rerun_regex']}")
rerun_regex = re.compile(fr"{ctx['rerun_regex']}") # rerun_regex = re.compile(fr"{ctx['rerun_regex']}")
for sample in sample_names: # for sample in sample_names:
# logger.debug(f'Running search on {sample}') # # logger.debug(f'Running search on {sample}')
if rerun_regex.search(sample): # if rerun_regex.search(sample):
# logger.debug(f'Match on {sample}') # # logger.debug(f'Match on {sample}')
first_run = re.sub(rerun_regex, "", sample) # first_run = re.sub(rerun_regex, "", sample)
# logger.debug(f"First run: {first_run}") # # logger.debug(f"First run: {first_run}")
df = df.drop(df[df.name == first_run].index) # df = df.drop(df[df.name == first_run].index)
return df # return df
else: # else:
return None # return None

View File

@@ -1,3 +1,6 @@
'''
contains parser object for pulling values from client generated submission sheets.
'''
import pandas as pd import pandas as pd
from pathlib import Path from pathlib import Path
from backend.db.models import WWSample, BCSample from backend.db.models import WWSample, BCSample

View File

@@ -1,11 +1,13 @@
'''
Contains functions for generating summary reports
'''
from pandas import DataFrame from pandas import DataFrame
# from backend.db import models
import logging import logging
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from datetime import date, timedelta from datetime import date, timedelta
import sys import sys
from pathlib import Path from pathlib import Path
import re
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -93,30 +95,22 @@ def convert_data_list_to_df(ctx:dict, input:list[dict], subtype:str|None=None) -
Returns: Returns:
DataFrame: _description_ DataFrame: _description_
""" """
# copy = input
# for item in copy:
# item['submitted_date'] = item['submitted_date'].strftime("%Y-%m-%d")
# with open("controls.json", "w") as f:
# f.write(json.dumps(copy))
# for item in input:
# logger.debug(item.keys())
df = DataFrame.from_records(input) df = DataFrame.from_records(input)
df.to_excel("test.xlsx", engine="openpyxl") df.to_excel("test.xlsx", engine="openpyxl")
safe = ['name', 'submitted_date', 'genus', 'target'] safe = ['name', 'submitted_date', 'genus', 'target']
# logger.debug(df)
for column in df.columns: for column in df.columns:
if "percent" in column: if "percent" in column:
count_col = [item for item in df.columns if "count" in item][0] count_col = [item for item in df.columns if "count" in item][0]
# The actual percentage from kraken was off due to exclusion of NaN, recalculating. # The actual percentage from kraken was off due to exclusion of NaN, recalculating.
# df[column] = 100 * df[count_col] / df.groupby('submitted_date')[count_col].transform('sum')
df[column] = 100 * df[count_col] / df.groupby('name')[count_col].transform('sum') df[column] = 100 * df[count_col] / df.groupby('name')[count_col].transform('sum')
if column not in safe: if column not in safe:
if subtype != None and column != subtype: if subtype != None and column != subtype:
del df[column] del df[column]
# logger.debug(df) # move date of sample submitted on same date as previous ahead one.
# df.sort_values('submitted_date').to_excel("controls.xlsx", engine="openpyxl")
df = displace_date(df) df = displace_date(df)
df.sort_values('submitted_date').to_excel("controls.xlsx", engine="openpyxl") df.sort_values('submitted_date').to_excel("controls.xlsx", engine="openpyxl")
# ad hoc method to make data labels more accurate.
df = df_column_renamer(df=df) df = df_column_renamer(df=df)
return df return df
@@ -150,24 +144,59 @@ def displace_date(df:DataFrame) -> DataFrame:
Returns: Returns:
DataFrame: output dataframe with dates incremented. DataFrame: output dataframe with dates incremented.
""" """
# dict_list = []
# for item in df['name'].unique():
# dict_list.append(dict(name=item, date=df[df.name == item].iloc[0]['submitted_date']))
logger.debug(f"Unique items: {df['name'].unique()}") logger.debug(f"Unique items: {df['name'].unique()}")
# logger.debug(df.to_string()) # get submitted dates for each control
# the assumption is that closest names will have closest dates...
dict_list = [dict(name=item, date=df[df.name == item].iloc[0]['submitted_date']) for item in sorted(df['name'].unique())] dict_list = [dict(name=item, date=df[df.name == item].iloc[0]['submitted_date']) for item in sorted(df['name'].unique())]
for ii, item in enumerate(dict_list): for ii, item in enumerate(dict_list):
# if ii > 0:
try: try:
check = item['date'] == dict_list[ii-1]['date'] check = item['date'] == dict_list[ii-1]['date']
except IndexError: except IndexError:
check = False check = False
if check: if check:
logger.debug(f"We found one! Increment date!\n{item['date'] - timedelta(days=1)}") logger.debug(f"We found one! Increment date!\n\t{item['date'] - timedelta(days=1)}")
# get df locations where name == item name
mask = df['name'] == item['name'] mask = df['name'] == item['name']
# logger.debug(f"We will increment dates in: {df.loc[mask, 'submitted_date']}") # increment date in dataframe
df.loc[mask, 'submitted_date'] = df.loc[mask, 'submitted_date'].apply(lambda x: x + timedelta(days=1)) df.loc[mask, 'submitted_date'] = df.loc[mask, 'submitted_date'].apply(lambda x: x + timedelta(days=1))
# logger.debug(f"Do these look incremented: {df.loc[mask, 'submitted_date']}")
return df return df
def get_unique_values_in_df_column(df: DataFrame, column_name: str) -> list:
"""
get all unique values in a dataframe column by name
Args:
df (DataFrame): input dataframe
column_name (str): name of column of interest
Returns:
list: sorted list of unique values
"""
return sorted(df[column_name].unique())
def drop_reruns_from_df(ctx:dict, df: DataFrame) -> DataFrame:
"""
Removes semi-duplicates from dataframe after finding sequencing repeats.
Args:
settings (dict): settings passed from gui
df (DataFrame): initial dataframe
Returns:
DataFrame: dataframe with originals removed in favour of repeats.
"""
sample_names = get_unique_values_in_df_column(df, column_name="name")
if 'rerun_regex' in ctx:
# logger.debug(f"Compiling regex from: {settings['rerun_regex']}")
rerun_regex = re.compile(fr"{ctx['rerun_regex']}")
for sample in sample_names:
# logger.debug(f'Running search on {sample}')
if rerun_regex.search(sample):
# logger.debug(f'Match on {sample}')
first_run = re.sub(rerun_regex, "", sample)
# logger.debug(f"First run: {first_run}")
df = df.drop(df[df.name == first_run].index)
return df
else:
return None

View File

@@ -1,3 +1,6 @@
'''
Contains functions for setting up program from config.yml and database.
'''
import yaml import yaml
import sys, os, stat, platform, getpass import sys, os, stat, platform, getpass
import logging import logging
@@ -59,16 +62,18 @@ 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=None) -> dict: def get_config(settings_path: 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 (Path | str | None, optional): Path to config.yml Defaults to None.
Returns: Returns:
setting: dictionary of settings. dict: Dictionary of settings.
""" """
if isinstance(settings_path, str):
settings_path = Path(settings_path)
# custom pyyaml constructor to join fields # custom pyyaml constructor to join fields
def join(loader, node): def join(loader, node):
seq = loader.construct_sequence(node) seq = loader.construct_sequence(node)
@@ -105,10 +110,10 @@ def get_config(settings_path: str|None=None) -> dict:
copy_settings_trigger = True copy_settings_trigger = True
else: else:
# check if user defined path is directory # check if user defined path is directory
if Path(settings_path).is_dir(): if settings_path.is_dir():
settings_path = settings_path.joinpath("config.yml") settings_path = settings_path.joinpath("config.yml")
# check if user defined path is file # check if user defined path is file
elif Path(settings_path).is_file(): elif settings_path.is_file():
settings_path = settings_path settings_path = settings_path
else: else:
logger.error("No config.yml file found. Using empty dictionary.") logger.error("No config.yml file found. Using empty dictionary.")
@@ -128,7 +133,7 @@ def get_config(settings_path: str|None=None) -> dict:
def create_database_session(database_path: Path|str|None=None) -> Session: def create_database_session(database_path: Path|str|None=None) -> Session:
""" """
Get database settings from path or default database if database_path is blank. Creates a session to sqlite3 database from path or default database if database_path is blank.
Args: Args:
database_path (Path | str | None, optional): path to sqlite database. Defaults to None. database_path (Path | str | None, optional): path to sqlite database. Defaults to None.

View File

@@ -1,3 +1,6 @@
'''
Operations for all user interactions.
'''
import json import json
import re import re
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
@@ -19,8 +22,7 @@ from xhtml2pdf import pisa
# import plotly.express as px # import plotly.express as px
import yaml import yaml
import pprint import pprint
from backend.excel.parser import SheetParser from backend.excel import convert_data_list_to_df, make_report_xlsx, make_report_html, SheetParser
from backend.excel.reports import convert_data_list_to_df
from backend.db import (construct_submission_info, lookup_reagent, from backend.db import (construct_submission_info, lookup_reagent,
construct_reagent, store_submission, lookup_kittype_by_use, construct_reagent, store_submission, lookup_kittype_by_use,
lookup_regent_by_type_name, lookup_all_orgs, lookup_submissions_by_date_range, lookup_regent_by_type_name, lookup_all_orgs, lookup_submissions_by_date_range,
@@ -29,18 +31,15 @@ from backend.db import (construct_submission_info, lookup_reagent,
create_org_from_yaml, store_reagent create_org_from_yaml, store_reagent
) )
from backend.db import lookup_kittype_by_name from backend.db import lookup_kittype_by_name
from .functions import extract_form_info
from .functions import check_kit_integrity from tools import check_not_nan, check_kit_integrity
from tools import check_not_nan, extract_form_info # from backend.excel.reports import
from backend.excel.reports import make_report_xlsx, make_report_html from frontend.custom_widgets import SubmissionsSheet, AlertPop, QuestionAsker, AddReagentForm, ReportDatePicker, KitAdder, ControlsDatePicker, ImportReagent
from frontend.custom_widgets.sub_details import SubmissionsSheet
from frontend.custom_widgets.pop_ups import AlertPop, QuestionAsker
from frontend.custom_widgets import AddReagentForm, ReportDatePicker, KitAdder, ControlsDatePicker, ImportReagent
import logging import logging
import difflib import difflib
from getpass import getuser from getpass import getuser
from datetime import date from datetime import date
from frontend.visualizations.charts import create_charts from frontend.visualizations import create_charts
logger = logging.getLogger(f'submissions.{__name__}') logger = logging.getLogger(f'submissions.{__name__}')
logger.info("Hello, I am a logger") logger.info("Hello, I am a logger")

View File

@@ -1,323 +1,7 @@
from datetime import date '''
from PyQt6.QtWidgets import ( Contains all custom generated PyQT6 derivative widgets.
QLabel, QVBoxLayout, '''
QLineEdit, QComboBox, QDialog,
QDialogButtonBox, QDateEdit, QSizePolicy, QWidget,
QGridLayout, QPushButton, QSpinBox,
QScrollBar, QHBoxLayout,
)
from PyQt6.QtCore import Qt, QDate, QSize
# from submissions.backend.db import lookup_regent_by_type_name_and_kit_name
from tools import check_not_nan, extract_form_info
from backend.db import get_all_reagenttype_names, lookup_all_sample_types, create_kit_from_yaml, lookup_regent_by_type_name, lookup_regent_by_type_name_and_kit_name
from backend.excel.parser import SheetParser
from jinja2 import Environment, FileSystemLoader
import sys
from pathlib import Path
import logging
import numpy as np
from .pop_ups import AlertPop
logger = logging.getLogger(f"submissions.{__name__}") from .misc import *
from .pop_ups import *
if getattr(sys, 'frozen', False): from .sub_details import *
loader_path = Path(sys._MEIPASS).joinpath("files", "templates")
else:
loader_path = Path(__file__).parents[2].joinpath('templates').absolute().__str__()
loader = FileSystemLoader(loader_path)
env = Environment(loader=loader)
class AddReagentForm(QDialog):
"""
dialog to add gather info about new reagent
"""
def __init__(self, ctx:dict, reagent_lot:str|None, reagent_type:str|None, expiry:date|None=None) -> None:
super().__init__()
if reagent_lot == None:
reagent_lot = ""
self.setWindowTitle("Add Reagent")
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
# widget to get lot info
lot_input = QLineEdit()
lot_input.setObjectName("lot")
lot_input.setText(reagent_lot)
# widget to get expiry info
exp_input = QDateEdit(calendarPopup=True)
exp_input.setObjectName('expiry')
# if expiry is not passed in from gui, use today
if expiry == None:
exp_input.setDate(QDate.currentDate())
else:
exp_input.setDate(expiry)
# widget to get reagent type info
type_input = QComboBox()
type_input.setObjectName('type')
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}")
# convert input to user friendly string?
try:
reagent_type = reagent_type.replace("_", " ").title()
except AttributeError:
reagent_type = None
# set parsed reagent type to top of list
index = type_input.findText(reagent_type, Qt.MatchFlag.MatchEndsWith)
if index >= 0:
type_input.setCurrentIndex(index)
self.layout = QVBoxLayout()
self.layout.addWidget(QLabel("Lot"))
self.layout.addWidget(lot_input)
self.layout.addWidget(QLabel("Expiry"))
self.layout.addWidget(exp_input)
self.layout.addWidget(QLabel("Type"))
self.layout.addWidget(type_input)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
class ReportDatePicker(QDialog):
"""
custom dialog to ask for report start/stop dates
"""
def __init__(self) -> None:
super().__init__()
self.setWindowTitle("Select Report Date Range")
# make confirm/reject buttons
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
# widgets to ask for dates
start_date = QDateEdit(calendarPopup=True)
start_date.setObjectName("start_date")
start_date.setDate(QDate.currentDate())
end_date = QDateEdit(calendarPopup=True)
end_date.setObjectName("end_date")
end_date.setDate(QDate.currentDate())
self.layout = QVBoxLayout()
self.layout.addWidget(QLabel("Start Date"))
self.layout.addWidget(start_date)
self.layout.addWidget(QLabel("End Date"))
self.layout.addWidget(end_date)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
class KitAdder(QWidget):
"""
dialog to get information to add kit
"""
def __init__(self, parent_ctx:dict) -> None:
super().__init__()
self.ctx = parent_ctx
self.grid = QGridLayout()
self.setLayout(self.grid)
# insert submit button at top
self.submit_btn = QPushButton("Submit")
self.grid.addWidget(self.submit_btn,0,0,1,1)
self.grid.addWidget(QLabel("Kit Name:"),2,0)
# widget to get kit name
kit_name = QLineEdit()
kit_name.setObjectName("kit_name")
self.grid.addWidget(kit_name,2,1)
self.grid.addWidget(QLabel("Used For Sample Type:"),3,0)
# widget to get uses of kit
used_for = QComboBox()
used_for.setObjectName("used_for")
# Insert all existing sample types
used_for.addItems(lookup_all_sample_types(ctx=parent_ctx))
used_for.setEditable(True)
self.grid.addWidget(used_for,3,1)
# set cost per run
self.grid.addWidget(QLabel("Constant cost per full plate (plates, work hours, etc.):"),4,0)
# widget to get constant cost
const_cost = QSpinBox()
const_cost.setObjectName("const_cost")
const_cost.setMinimum(0)
const_cost.setMaximum(9999)
self.grid.addWidget(const_cost,4,1)
self.grid.addWidget(QLabel("Mutable cost per full plate (tips, reagents, etc.):"),5,0)
# widget to get mutable costs
mut_cost = QSpinBox()
mut_cost.setObjectName("mut_cost")
mut_cost.setMinimum(0)
mut_cost.setMaximum(9999)
self.grid.addWidget(mut_cost,5,1)
# button to add additional reagent types
self.add_RT_btn = QPushButton("Add Reagent Type")
self.grid.addWidget(self.add_RT_btn)
self.add_RT_btn.clicked.connect(self.add_RT)
self.submit_btn.clicked.connect(self.submit)
def add_RT(self) -> None:
"""
insert new reagent type row
"""
# get bottommost row
maxrow = self.grid.rowCount()
reg_form = ReagentTypeForm(parent_ctx=self.ctx)
reg_form.setObjectName(f"ReagentForm_{maxrow}")
self.grid.addWidget(reg_form, maxrow + 1,0,1,2)
def submit(self) -> None:
"""
send kit to database
"""
# get form info
info, reagents = extract_form_info(self)
logger.debug(f"kit info: {info}")
yml_type = {}
try:
yml_type['password'] = info['password']
except KeyError:
pass
used = info['used_for'].replace(" ", "_").lower()
yml_type[used] = {}
yml_type[used]['kits'] = {}
yml_type[used]['kits'][info['kit_name']] = {}
yml_type[used]['kits'][info['kit_name']]['constant_cost'] = info["const_cost"]
yml_type[used]['kits'][info['kit_name']]['mutable_cost'] = info["mut_cost"]
yml_type[used]['kits'][info['kit_name']]['reagenttypes'] = reagents
logger.debug(yml_type)
# send to kit constructor
result = create_kit_from_yaml(ctx=self.ctx, exp=yml_type)
msg = AlertPop(message=result['message'], status=result['status'])
msg.exec()
# 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 = []
# values = []
# reagents = {}
# for item in object.findChildren(QWidget):
# logger.debug(item.parentWidget())
# # if not isinstance(item.parentWidget(), ReagentTypeForm):
# match item:
# case QLabel():
# labels.append(item.text().replace(" ", "_").strip(":").lower())
# case QLineEdit():
# # ad hoc check to prevent double reporting of qdatedit under lineedit for some reason
# if not isinstance(prev_item, QDateEdit) and not isinstance(prev_item, QComboBox) and not isinstance(prev_item, QSpinBox) and not isinstance(prev_item, QScrollBar):
# logger.debug(f"Previous: {prev_item}")
# logger.debug(f"Item: {item}, {item.text()}")
# values.append(item.text().strip())
# case QComboBox():
# values.append(item.currentText().strip())
# case QDateEdit():
# values.append(item.date().toPyDate())
# case QSpinBox():
# values.append(item.value())
# case ReagentTypeForm():
# re_labels, re_values, _ = self.extract_form_info(item)
# reagent = {item[0]:item[1] for item in zip(re_labels, re_values)}
# logger.debug(reagent)
# # reagent = {reagent['name:']:{'eol':reagent['extension_of_life_(months):']}}
# reagents[reagent["name_(*exactly*_as_it_appears_in_the_excel_submission_form)"].strip()] = {'eol_ext':int(reagent['extension_of_life_(months)'])}
# prev_item = item
# return labels, values, reagents
class ReagentTypeForm(QWidget):
"""
custom widget to add information about a new reagenttype
"""
def __init__(self, parent_ctx:dict) -> None:
super().__init__()
grid = QGridLayout()
self.setLayout(grid)
grid.addWidget(QLabel("Name (*Exactly* as it appears in the excel submission form):"),0,0)
# Widget to get reagent info
reagent_getter = QComboBox()
reagent_getter.setObjectName("name")
# lookup all reagent type names from db
reagent_getter.addItems(get_all_reagenttype_names(ctx=parent_ctx))
reagent_getter.setEditable(True)
grid.addWidget(reagent_getter,0,1)
grid.addWidget(QLabel("Extension of Life (months):"),0,2)
# widget toget extension of life
eol = QSpinBox()
eol.setObjectName('eol')
eol.setMinimum(0)
grid.addWidget(eol, 0,3)
class ControlsDatePicker(QWidget):
"""
custom widget to pick start and end dates for controls graphs
"""
def __init__(self) -> None:
super().__init__()
self.start_date = QDateEdit(calendarPopup=True)
# start date is three month prior to end date by default
# edit: 2 month, but the variable name is the same cause I'm lazy
threemonthsago = QDate.currentDate().addDays(-60)
self.start_date.setDate(threemonthsago)
self.end_date = QDateEdit(calendarPopup=True)
self.end_date.setDate(QDate.currentDate())
self.layout = QHBoxLayout()
self.layout.addWidget(QLabel("Start Date"))
self.layout.addWidget(self.start_date)
self.layout.addWidget(QLabel("End Date"))
self.layout.addWidget(self.end_date)
self.setLayout(self.layout)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
def sizeHint(self) -> QSize:
return QSize(80,20)
class ImportReagent(QComboBox):
def __init__(self, ctx:dict, item:str, prsr:SheetParser|None=None):
super().__init__()
self.setEditable(True)
# Ensure that all reagenttypes have a name that matches the items in the excel parser
query_var = item.replace("lot_", "")
logger.debug(f"Query for: {query_var}")
if prsr != None:
if isinstance(prsr.sub[item], np.float64):
logger.debug(f"{prsr.sub[item]['lot']} is a numpy float!")
try:
prsr.sub[item] = int(prsr.sub[item]['lot'])
except ValueError:
pass
# query for reagents using type name from sheet and kit from sheet
logger.debug(f"Attempting lookup of reagents by type: {query_var}")
# below was lookup_reagent_by_type_name_and_kit_name, but I couldn't get it to work.
relevant_reagents = [item.__str__() for item in lookup_regent_by_type_name(ctx=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=ctx, type_name=query_var, kit_name=prsr.sub['extraction_kit'])]
output_reg = []
for reagent in relevant_reagents:
# extract strings from any sets.
if isinstance(reagent, set):
for thing in reagent:
output_reg.append(thing)
elif isinstance(reagent, str):
output_reg.append(reagent)
relevant_reagents = output_reg
# if reagent in sheet is not found insert it into the front of relevant reagents so it shows
if prsr != None:
logger.debug(f"Relevant reagents for {prsr.sub[item]}: {relevant_reagents}")
if str(prsr.sub[item]['lot']) not in relevant_reagents and prsr.sub[item]['lot'] != 'nan':
if check_not_nan(prsr.sub[item]['lot']):
relevant_reagents.insert(0, str(prsr.sub[item]['lot']))
logger.debug(f"New relevant reagents: {relevant_reagents}")
self.addItems(relevant_reagents)

View File

@@ -0,0 +1,334 @@
'''
Contains miscellaneous widgets for frontend functions
'''
from datetime import date
from PyQt6.QtWidgets import (
QLabel, QVBoxLayout,
QLineEdit, QComboBox, QDialog,
QDialogButtonBox, QDateEdit, QSizePolicy, QWidget,
QGridLayout, QPushButton, QSpinBox,
QHBoxLayout,
)
from PyQt6.QtCore import Qt, QDate, QSize
# from submissions.backend.db import lookup_regent_by_type_name_and_kit_name
from tools import check_not_nan
from ..functions import extract_form_info
from backend.db import get_all_reagenttype_names, lookup_all_sample_types, create_kit_from_yaml, lookup_regent_by_type_name#, lookup_regent_by_type_name_and_kit_name
from backend.excel.parser import SheetParser
from jinja2 import Environment, FileSystemLoader
import sys
from pathlib import Path
import logging
import numpy as np
from .pop_ups import AlertPop
logger = logging.getLogger(f"submissions.{__name__}")
if getattr(sys, 'frozen', False):
loader_path = Path(sys._MEIPASS).joinpath("files", "templates")
else:
loader_path = Path(__file__).parents[2].joinpath('templates').absolute().__str__()
loader = FileSystemLoader(loader_path)
env = Environment(loader=loader)
class AddReagentForm(QDialog):
"""
dialog to add gather info about new reagent
"""
def __init__(self, ctx:dict, reagent_lot:str|None, reagent_type:str|None, expiry:date|None=None) -> None:
super().__init__()
if reagent_lot == None:
reagent_lot = ""
self.setWindowTitle("Add Reagent")
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
# widget to get lot info
lot_input = QLineEdit()
lot_input.setObjectName("lot")
lot_input.setText(reagent_lot)
# widget to get expiry info
exp_input = QDateEdit(calendarPopup=True)
exp_input.setObjectName('expiry')
# if expiry is not passed in from gui, use today
if expiry == None:
exp_input.setDate(QDate.currentDate())
else:
exp_input.setDate(expiry)
# widget to get reagent type info
type_input = QComboBox()
type_input.setObjectName('type')
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}")
# convert input to user friendly string?
try:
reagent_type = reagent_type.replace("_", " ").title()
except AttributeError:
reagent_type = None
# set parsed reagent type to top of list
index = type_input.findText(reagent_type, Qt.MatchFlag.MatchEndsWith)
if index >= 0:
type_input.setCurrentIndex(index)
self.layout = QVBoxLayout()
self.layout.addWidget(QLabel("Lot:"))
self.layout.addWidget(lot_input)
self.layout.addWidget(QLabel("Expiry:\n(use exact date on reagent.\nEOL will be calculated from kit automatically)"))
self.layout.addWidget(exp_input)
self.layout.addWidget(QLabel("Type:"))
self.layout.addWidget(type_input)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
class ReportDatePicker(QDialog):
"""
custom dialog to ask for report start/stop dates
"""
def __init__(self) -> None:
super().__init__()
self.setWindowTitle("Select Report Date Range")
# make confirm/reject buttons
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
# widgets to ask for dates
start_date = QDateEdit(calendarPopup=True)
start_date.setObjectName("start_date")
start_date.setDate(QDate.currentDate())
end_date = QDateEdit(calendarPopup=True)
end_date.setObjectName("end_date")
end_date.setDate(QDate.currentDate())
self.layout = QVBoxLayout()
self.layout.addWidget(QLabel("Start Date"))
self.layout.addWidget(start_date)
self.layout.addWidget(QLabel("End Date"))
self.layout.addWidget(end_date)
self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout)
class KitAdder(QWidget):
"""
dialog to get information to add kit
"""
def __init__(self, parent_ctx:dict) -> None:
super().__init__()
self.ctx = parent_ctx
self.grid = QGridLayout()
self.setLayout(self.grid)
# insert submit button at top
self.submit_btn = QPushButton("Submit")
self.grid.addWidget(self.submit_btn,0,0,1,1)
self.grid.addWidget(QLabel("Kit Name:"),2,0)
# widget to get kit name
kit_name = QLineEdit()
kit_name.setObjectName("kit_name")
self.grid.addWidget(kit_name,2,1)
self.grid.addWidget(QLabel("Used For Sample Type:"),3,0)
# widget to get uses of kit
used_for = QComboBox()
used_for.setObjectName("used_for")
# Insert all existing sample types
used_for.addItems(lookup_all_sample_types(ctx=parent_ctx))
used_for.setEditable(True)
self.grid.addWidget(used_for,3,1)
# set cost per run
self.grid.addWidget(QLabel("Constant cost per full plate (plates, work hours, etc.):"),4,0)
# widget to get constant cost
const_cost = QSpinBox()
const_cost.setObjectName("const_cost")
const_cost.setMinimum(0)
const_cost.setMaximum(9999)
self.grid.addWidget(const_cost,4,1)
self.grid.addWidget(QLabel("Mutable cost per full plate (tips, reagents, etc.):"),5,0)
# widget to get mutable costs
mut_cost = QSpinBox()
mut_cost.setObjectName("mut_cost")
mut_cost.setMinimum(0)
mut_cost.setMaximum(9999)
self.grid.addWidget(mut_cost,5,1)
# button to add additional reagent types
self.add_RT_btn = QPushButton("Add Reagent Type")
self.grid.addWidget(self.add_RT_btn)
self.add_RT_btn.clicked.connect(self.add_RT)
self.submit_btn.clicked.connect(self.submit)
def add_RT(self) -> None:
"""
insert new reagent type row
"""
# get bottommost row
maxrow = self.grid.rowCount()
reg_form = ReagentTypeForm(parent_ctx=self.ctx)
reg_form.setObjectName(f"ReagentForm_{maxrow}")
self.grid.addWidget(reg_form, maxrow + 1,0,1,2)
def submit(self) -> None:
"""
send kit to database
"""
# get form info
info, reagents = extract_form_info(self)
logger.debug(f"kit info: {info}")
yml_type = {}
try:
yml_type['password'] = info['password']
except KeyError:
pass
used = info['used_for'].replace(" ", "_").lower()
yml_type[used] = {}
yml_type[used]['kits'] = {}
yml_type[used]['kits'][info['kit_name']] = {}
yml_type[used]['kits'][info['kit_name']]['constant_cost'] = info["const_cost"]
yml_type[used]['kits'][info['kit_name']]['mutable_cost'] = info["mut_cost"]
yml_type[used]['kits'][info['kit_name']]['reagenttypes'] = reagents
logger.debug(yml_type)
# send to kit constructor
result = create_kit_from_yaml(ctx=self.ctx, exp=yml_type)
msg = AlertPop(message=result['message'], status=result['status'])
msg.exec()
# 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 = []
# values = []
# reagents = {}
# for item in object.findChildren(QWidget):
# logger.debug(item.parentWidget())
# # if not isinstance(item.parentWidget(), ReagentTypeForm):
# match item:
# case QLabel():
# labels.append(item.text().replace(" ", "_").strip(":").lower())
# case QLineEdit():
# # ad hoc check to prevent double reporting of qdatedit under lineedit for some reason
# if not isinstance(prev_item, QDateEdit) and not isinstance(prev_item, QComboBox) and not isinstance(prev_item, QSpinBox) and not isinstance(prev_item, QScrollBar):
# logger.debug(f"Previous: {prev_item}")
# logger.debug(f"Item: {item}, {item.text()}")
# values.append(item.text().strip())
# case QComboBox():
# values.append(item.currentText().strip())
# case QDateEdit():
# values.append(item.date().toPyDate())
# case QSpinBox():
# values.append(item.value())
# case ReagentTypeForm():
# re_labels, re_values, _ = self.extract_form_info(item)
# reagent = {item[0]:item[1] for item in zip(re_labels, re_values)}
# logger.debug(reagent)
# # reagent = {reagent['name:']:{'eol':reagent['extension_of_life_(months):']}}
# reagents[reagent["name_(*exactly*_as_it_appears_in_the_excel_submission_form)"].strip()] = {'eol_ext':int(reagent['extension_of_life_(months)'])}
# prev_item = item
# return labels, values, reagents
class ReagentTypeForm(QWidget):
"""
custom widget to add information about a new reagenttype
"""
def __init__(self, parent_ctx:dict) -> None:
super().__init__()
grid = QGridLayout()
self.setLayout(grid)
grid.addWidget(QLabel("Name (*Exactly* as it appears in the excel submission form):"),0,0)
# Widget to get reagent info
reagent_getter = QComboBox()
reagent_getter.setObjectName("name")
# lookup all reagent type names from db
reagent_getter.addItems(get_all_reagenttype_names(ctx=parent_ctx))
reagent_getter.setEditable(True)
grid.addWidget(reagent_getter,0,1)
grid.addWidget(QLabel("Extension of Life (months):"),0,2)
# widget toget extension of life
eol = QSpinBox()
eol.setObjectName('eol')
eol.setMinimum(0)
grid.addWidget(eol, 0,3)
class ControlsDatePicker(QWidget):
"""
custom widget to pick start and end dates for controls graphs
"""
def __init__(self) -> None:
super().__init__()
self.start_date = QDateEdit(calendarPopup=True)
# start date is three month prior to end date by default
# NOTE: 2 month, but the variable name is the same cause I'm lazy
threemonthsago = QDate.currentDate().addDays(-60)
self.start_date.setDate(threemonthsago)
self.end_date = QDateEdit(calendarPopup=True)
self.end_date.setDate(QDate.currentDate())
self.layout = QHBoxLayout()
self.layout.addWidget(QLabel("Start Date"))
self.layout.addWidget(self.start_date)
self.layout.addWidget(QLabel("End Date"))
self.layout.addWidget(self.end_date)
self.setLayout(self.layout)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
def sizeHint(self) -> QSize:
return QSize(80,20)
class ImportReagent(QComboBox):
def __init__(self, ctx:dict, item:str, prsr:SheetParser|None=None):
super().__init__()
self.setEditable(True)
# Ensure that all reagenttypes have a name that matches the items in the excel parser
query_var = item.replace("lot_", "")
logger.debug(f"Import Reagent is looking at: {prsr.sub[item]} for {item}")
logger.debug(f"Query for: {query_var}")
if prsr != None:
if isinstance(prsr.sub[item], np.float64):
logger.debug(f"{prsr.sub[item]['lot']} is a numpy float!")
try:
prsr.sub[item] = int(prsr.sub[item]['lot'])
except ValueError:
pass
# query for reagents using type name from sheet and kit from sheet
logger.debug(f"Attempting lookup of reagents by type: {query_var}")
# below was lookup_reagent_by_type_name_and_kit_name, but I couldn't get it to work.
relevant_reagents = [item.__str__() for item in lookup_regent_by_type_name(ctx=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=ctx, type_name=query_var, kit_name=prsr.sub['extraction_kit'])]
output_reg = []
for reagent in relevant_reagents:
# extract strings from any sets.
if isinstance(reagent, set):
for thing in reagent:
output_reg.append(thing)
elif isinstance(reagent, str):
output_reg.append(reagent)
relevant_reagents = output_reg
# if reagent in sheet is not found insert it into the front of relevant reagents so it shows
if prsr != None:
logger.debug(f"Relevant reagents for {prsr.sub[item]}: {relevant_reagents}")
if str(prsr.sub[item]['lot']) not in relevant_reagents:
if check_not_nan(prsr.sub[item]['lot']):
relevant_reagents.insert(0, str(prsr.sub[item]['lot']))
else:
if len(relevant_reagents) > 1:
logger.debug(f"Found {prsr.sub[item]['lot']} in relevant reagents: {relevant_reagents}. Moving to front of list.")
relevant_reagents.insert(0, relevant_reagents.pop(relevant_reagents.index(prsr.sub[item]['lot'])))
else:
logger.debug(f"Found {prsr.sub[item]['lot']} in relevant reagents: {relevant_reagents}. But no need to move due to short list.")
logger.debug(f"New relevant reagents: {relevant_reagents}")
self.addItems(relevant_reagents)

View File

@@ -1,3 +1,6 @@
'''
Contains dialogs for notification and prompting.
'''
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QLabel, QVBoxLayout, QDialog, QLabel, QVBoxLayout, QDialog,
QDialogButtonBox, QMessageBox QDialogButtonBox, QMessageBox

View File

@@ -1,3 +1,6 @@
'''
Contains widgets specific to the submission summary and submission details.
'''
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QVBoxLayout, QDialog, QTableView, QVBoxLayout, QDialog, QTableView,
QTextEdit, QPushButton, QScrollArea, QTextEdit, QPushButton, QScrollArea,

View File

@@ -1,80 +1,53 @@
# from ..models import *
from backend.db.models import * from backend.db.models import *
import logging import logging
from PyQt6.QtWidgets import (
QMainWindow, QLabel, QToolBar,
QTabWidget, QWidget, QVBoxLayout,
QPushButton, QFileDialog,
QLineEdit, QMessageBox, QComboBox, QDateEdit, QHBoxLayout,
QSpinBox, QScrollArea
)
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
def check_kit_integrity(sub:BasicSubmission|KitType, reagenttypes:list|None=None) -> dict|None: def extract_form_info(object) -> dict:
""" """
Ensures all reagents expected in kit are listed in Submission retrieves object names and values from form
Args: Args:
sub (BasicSubmission | KitType): Object containing complete list of reagent types. object (_type_): the form widget
reagenttypes (list | None, optional): List to check against complete list. Defaults to None.
Returns: Returns:
dict|None: Result object containing a message and any missing components. dict: dictionary of objectName:text items
""" """
logger.debug(type(sub)) from frontend.custom_widgets import ReagentTypeForm
# What type is sub? dicto = {}
match sub: reagents = {}
case BasicSubmission(): logger.debug(f"Object type: {type(object)}")
ext_kit_rtypes = [reagenttype.name for reagenttype in sub.extraction_kit.reagent_types] # grab all widgets in form
# Overwrite function parameter reagenttypes try:
reagenttypes = [reagent.type.name for reagent in sub.reagents] all_children = object.layout.parentWidget().findChildren(QWidget)
case KitType(): except AttributeError:
ext_kit_rtypes = [reagenttype.name for reagenttype in sub.reagent_types] all_children = object.layout().parentWidget().findChildren(QWidget)
logger.debug(f"Kit reagents: {ext_kit_rtypes}") for item in all_children:
logger.debug(f"Submission reagents: {reagenttypes}") logger.debug(f"Looking at: {item.objectName()}")
# check if lists are equal match item:
check = set(ext_kit_rtypes) == set(reagenttypes) case QLineEdit():
logger.debug(f"Checking if reagents match kit contents: {check}") dicto[item.objectName()] = item.text()
# what reagent types are in both lists? case QComboBox():
# common = list(set(ext_kit_rtypes).intersection(reagenttypes)) dicto[item.objectName()] = item.currentText()
missing = list(set(ext_kit_rtypes).difference(reagenttypes)) case QDateEdit():
logger.debug(f"Missing reagents types: {missing}") dicto[item.objectName()] = item.date().toPyDate()
# if lists are equal return no problem case QSpinBox():
if len(missing)==0: dicto[item.objectName()] = item.value()
result = None case ReagentTypeForm():
else: reagent = extract_form_info(item)
# missing = [x for x in ext_kit_rtypes if x not in common] # reagent = {item[0]:item[1] for item in zip(re_labels, re_values)}
result = {'message' : f"Couldn't verify reagents match listed kit components.\n\nIt looks like you are missing: {[item.upper() for item in missing]}\n\nAlternatively, you may have set the wrong extraction kit.", 'missing': missing} logger.debug(reagent)
return result # reagent = {reagent['name:']:{'eol':reagent['extension_of_life_(months):']}}
reagents[reagent["name"].strip()] = {'eol_ext':int(reagent['eol'])}
# value for ad hoc check above
# Below is depreciated: if reagents != {}:
# def insert_reagent_import(ctx:dict, item:str, prsr:SheetParser|None=None) -> QComboBox: return dicto, reagents
# add_widget = QComboBox() return dicto
# add_widget.setEditable(True)
# # Ensure that all reagenttypes have a name that matches the items in the excel parser
# query_var = item.replace("lot_", "")
# logger.debug(f"Query for: {query_var}")
# if prsr != None:
# if isinstance(prsr.sub[item], np.float64):
# logger.debug(f"{prsr.sub[item]['lot']} is a numpy float!")
# try:
# prsr.sub[item] = int(prsr.sub[item]['lot'])
# except ValueError:
# pass
# # query for reagents using type name from sheet and kit from sheet
# logger.debug(f"Attempting lookup of reagents by type: {query_var}")
# # below was lookup_reagent_by_type_name_and_kit_name, but I couldn't get it to work.
# relevant_reagents = [item.__str__() for item in lookup_regent_by_type_name(ctx=ctx, type_name=query_var)]#, kit_name=prsr.sub['extraction_kit'])]
# output_reg = []
# for reagent in relevant_reagents:
# if isinstance(reagent, set):
# for thing in reagent:
# output_reg.append(thing)
# elif isinstance(reagent, str):
# output_reg.append(reagent)
# relevant_reagents = output_reg
# # if reagent in sheet is not found insert it into items
# if prsr != None:
# logger.debug(f"Relevant reagents for {prsr.sub[item]}: {relevant_reagents}")
# if str(prsr.sub[item]['lot']) not in relevant_reagents and prsr.sub[item]['lot'] != 'nan':
# if check_not_nan(prsr.sub[item]['lot']):
# relevant_reagents.insert(0, str(prsr.sub[item]['lot']))
# logger.debug(f"New relevant reagents: {relevant_reagents}")
# add_widget.addItems(relevant_reagents)
# return add_widget

View File

@@ -1,40 +0,0 @@
Screen {
}
.box {
height: 3;
border: solid green;
}
#tree-view {
display: none;
scrollbar-gutter: stable;
overflow: auto;
width: auto;
height: 100%;
dock: left;
}
DataBrowser.-show-tree #tree-view {
display: block;
max-width: 50%;
}
#code-view {
overflow: auto scroll;
min-width: 100%;
}
#code {
width: auto;
}
FormField {
width: 70%;
height: 3;
padding: 1 2;
background: $primary;
border: $secondary tall;
content-align: center middle;
}

View File

@@ -0,0 +1,4 @@
'''
Contains all operations for creating charts, graphs and visual effects.
'''
from .control_charts import *

View File

@@ -1,3 +1,6 @@
'''
Functions for constructing controls graphs using plotly.
'''
import plotly.express as px import plotly.express as px
import pandas as pd import pandas as pd
from pathlib import Path from pathlib import Path
@@ -149,8 +152,19 @@ def output_figures(settings:dict, figs:list, group_name:str):
def construct_chart(ctx:dict, df:pd.DataFrame, modes:list, ytitle:str|None=None) -> Figure: def construct_chart(ctx:dict, df:pd.DataFrame, modes:list, ytitle:str|None=None) -> Figure:
fig = Figure() """
Creates a plotly chart for controls from a pandas dataframe
Args:
ctx (dict): settings passed down from gui
df (pd.DataFrame): input dataframe of controls
modes (list): analysis modes to construct charts for
ytitle (str | None, optional): title on the y-axis. Defaults to None.
Returns:
Figure: output stacked bar chart.
"""
fig = Figure()
for ii, mode in enumerate(modes): for ii, mode in enumerate(modes):
if "count" in mode: if "count" in mode:
df[mode] = pd.to_numeric(df[mode],errors='coerce') df[mode] = pd.to_numeric(df[mode],errors='coerce')
@@ -161,7 +175,6 @@ def construct_chart(ctx:dict, df:pd.DataFrame, modes:list, ytitle:str|None=None)
color_discrete_sequence=None color_discrete_sequence=None
else: else:
color = "target" color = "target"
# print(get_unique_values_in_df_column(df, 'target'))
match get_unique_values_in_df_column(df, 'target'): match get_unique_values_in_df_column(df, 'target'):
case ['Target']: case ['Target']:
color_discrete_sequence=["blue"] color_discrete_sequence=["blue"]
@@ -180,7 +193,6 @@ def construct_chart(ctx:dict, df:pd.DataFrame, modes:list, ytitle:str|None=None)
) )
bar.update_traces(visible = ii == 0) bar.update_traces(visible = ii == 0)
fig.add_traces(bar.data) fig.add_traces(bar.data)
# sys.exit(f"number of traces={len(fig.data)}")
return generic_figure_markers(fig=fig, modes=modes, ytitle=ytitle) return generic_figure_markers(fig=fig, modes=modes, ytitle=ytitle)
# Below are the individual construction functions. They must be named "construct_{mode}_chart" and # Below are the individual construction functions. They must be named "construct_{mode}_chart" and
@@ -264,17 +276,3 @@ def divide_chunks(input_list:list, chunk_count:int):
""" """
k, m = divmod(len(input_list), chunk_count) k, m = divmod(len(input_list), chunk_count)
return (input_list[i*k+min(i, m):(i+1)*k+min(i+1, m)] for i in range(chunk_count)) return (input_list[i*k+min(i, m):(i+1)*k+min(i+1, m)] for i in range(chunk_count))
########This must be at bottom of module###########
function_map = {}
for item in dict(locals().items()):
try:
if dict(locals().items())[item].__module__ == __name__:
try:
function_map[item] = dict(locals().items())[item]
except KeyError:
pass
except AttributeError:
pass
###################################################

View File

@@ -1,18 +1,14 @@
import numpy as np import numpy as np
import logging import logging
import getpass import getpass
from PyQt6.QtWidgets import ( from backend.db.models import BasicSubmission, KitType
QMainWindow, QLabel, QToolBar,
QTabWidget, QWidget, QVBoxLayout,
QPushButton, QFileDialog,
QLineEdit, QMessageBox, QComboBox, QDateEdit, QHBoxLayout,
QSpinBox, QScrollArea
)
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
def check_not_nan(cell_contents) -> bool: def check_not_nan(cell_contents) -> bool:
# check for nan as a string first
if cell_contents == 'nan':
cell_contents = np.nan
try: try:
return not np.isnan(cell_contents) return not np.isnan(cell_contents)
except TypeError: except TypeError:
@@ -28,7 +24,7 @@ def check_is_power_user(ctx:dict) -> bool:
except KeyError as e: except KeyError as e:
check = False check = False
except Exception as e: except Exception as e:
logger.debug(f"Check encounteded unknown error: {type(e).__name__} - {e}") logger.debug(f"Check encountered unknown error: {type(e).__name__} - {e}")
check = False check = False
return check return check
@@ -37,43 +33,39 @@ def create_reagent_list(in_dict:dict) -> list[str]:
return [item.strip("lot_") for item in in_dict.keys()] return [item.strip("lot_") for item in in_dict.keys()]
def extract_form_info(object) -> dict: def check_kit_integrity(sub:BasicSubmission|KitType, reagenttypes:list|None=None) -> dict|None:
""" """
retrieves object names and values from form Ensures all reagents expected in kit are listed in Submission
Args: Args:
object (_type_): the form widget sub (BasicSubmission | KitType): Object containing complete list of reagent types.
reagenttypes (list | None, optional): List to check against complete list. Defaults to None.
Returns: Returns:
dict: dictionary of objectName:text items dict|None: Result object containing a message and any missing components.
""" """
from frontend.custom_widgets import ReagentTypeForm logger.debug(type(sub))
dicto = {} # What type is sub?
reagents = {} match sub:
logger.debug(f"Object type: {type(object)}") case BasicSubmission():
# grab all widgets in form ext_kit_rtypes = [reagenttype.name for reagenttype in sub.extraction_kit.reagent_types]
try: # Overwrite function parameter reagenttypes
all_children = object.layout.parentWidget().findChildren(QWidget) reagenttypes = [reagent.type.name for reagent in sub.reagents]
except AttributeError: case KitType():
all_children = object.layout().parentWidget().findChildren(QWidget) ext_kit_rtypes = [reagenttype.name for reagenttype in sub.reagent_types]
for item in all_children: logger.debug(f"Kit reagents: {ext_kit_rtypes}")
logger.debug(f"Looking at: {item.objectName()}") logger.debug(f"Submission reagents: {reagenttypes}")
match item: # check if lists are equal
case QLineEdit(): check = set(ext_kit_rtypes) == set(reagenttypes)
dicto[item.objectName()] = item.text() logger.debug(f"Checking if reagents match kit contents: {check}")
case QComboBox(): # what reagent types are in both lists?
dicto[item.objectName()] = item.currentText() # common = list(set(ext_kit_rtypes).intersection(reagenttypes))
case QDateEdit(): missing = list(set(ext_kit_rtypes).difference(reagenttypes))
dicto[item.objectName()] = item.date().toPyDate() logger.debug(f"Missing reagents types: {missing}")
case QSpinBox(): # if lists are equal return no problem
dicto[item.objectName()] = item.value() if len(missing)==0:
case ReagentTypeForm(): result = None
reagent = extract_form_info(item) else:
# reagent = {item[0]:item[1] for item in zip(re_labels, re_values)} # missing = [x for x in ext_kit_rtypes if x not in common]
logger.debug(reagent) result = {'message' : f"Couldn't verify reagents match listed kit components.\n\nIt looks like you are missing: {[item.upper() for item in missing]}\n\nAlternatively, you may have set the wrong extraction kit.", 'missing': missing}
# reagent = {reagent['name:']:{'eol':reagent['extension_of_life_(months):']}} return result
reagents[reagent["name"].strip()] = {'eol_ext':int(reagent['eol'])}
# value for ad hoc check above
if reagents != {}:
return dicto, reagents
return dicto