Major refactor for improved documentation and readability
This commit is contained in:
6
src/submissions/CHANGELOG.md
Normal file
6
src/submissions/CHANGELOG.md
Normal 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.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
**This is the readme 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"
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
'''
|
||||||
|
Contains database and excel operations.
|
||||||
|
'''
|
||||||
File diff suppressed because it is too large
Load Diff
669
src/submissions/backend/db/functions.py
Normal file
669
src/submissions/backend/db/functions.py
Normal 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()
|
||||||
@@ -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
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|
||||||
334
src/submissions/frontend/custom_widgets/misc.py
Normal file
334
src/submissions/frontend/custom_widgets/misc.py
Normal 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)
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
'''
|
||||||
|
Contains all operations for creating charts, graphs and visual effects.
|
||||||
|
'''
|
||||||
|
from .control_charts import *
|
||||||
@@ -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
|
|
||||||
###################################################
|
|
||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user