mid refactor for improved rebustness and readability

This commit is contained in:
Landon Wark
2023-03-15 15:38:02 -05:00
parent fc334155ff
commit c645d3a9cf
15 changed files with 337 additions and 468 deletions

View File

@@ -21,19 +21,13 @@ from pathlib import Path
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
# The below should allow automatic creation of foreign keys in the database # The below _should_ allow automatic creation of foreign keys in the database
@event.listens_for(Engine, "connect") @event.listens_for(Engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record): def set_sqlite_pragma(dbapi_connection, connection_record):
cursor = dbapi_connection.cursor() cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA foreign_keys=ON") cursor.execute("PRAGMA foreign_keys=ON")
cursor.close() cursor.close()
def get_kits_by_use( ctx:dict, kittype_str:str|None) -> list:
pass
# ctx dict should contain the database session
def store_submission(ctx:dict, base_submission:models.BasicSubmission) -> None|dict: def store_submission(ctx:dict, base_submission:models.BasicSubmission) -> None|dict:
""" """
Upserts submissions into database Upserts submissions into database
@@ -73,21 +67,22 @@ def store_submission(ctx:dict, base_submission:models.BasicSubmission) -> None|d
def store_reagent(ctx:dict, reagent:models.Reagent) -> None|dict: def store_reagent(ctx:dict, reagent:models.Reagent) -> None|dict:
""" """
_summary_ Inserts a reagent into the database.
Args: Args:
ctx (dict): settings passed down from gui ctx (dict): settings passed down from gui
reagent (models.Reagent): Reagent object to be added to db reagent (models.Reagent): Reagent object to be added to db
Returns: Returns:
None|dict: obejct indicating issue to be reported in the gui None|dict: object indicating issue to be reported in the gui
""" """
logger.debug(reagent.__dict__) logger.debug(f"Reagent dictionary: {reagent.__dict__}")
ctx['database_session'].add(reagent) ctx['database_session'].add(reagent)
try: try:
ctx['database_session'].commit() ctx['database_session'].commit()
except (sqlite3.OperationalError, sqlalchemy.exc.OperationalError): except (sqlite3.OperationalError, sqlalchemy.exc.OperationalError):
return {"message":"The database is locked for editing."} return {"message":"The database is locked for editing."}
return None
def construct_submission_info(ctx:dict, info_dict:dict) -> models.BasicSubmission: def construct_submission_info(ctx:dict, info_dict:dict) -> models.BasicSubmission:
@@ -103,12 +98,12 @@ def construct_submission_info(ctx:dict, info_dict:dict) -> models.BasicSubmissio
""" """
# convert submission type into model name # convert submission type into model name
query = info_dict['submission_type'].replace(" ", "") query = info_dict['submission_type'].replace(" ", "")
# check database for existing object # 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"]): if info_dict["rsl_plate_num"] == 'nan' or info_dict["rsl_plate_num"] == None or not check_not_nan(info_dict["rsl_plate_num"]):
code = 2
instance = None instance = None
msg = "A proper RSL plate number is required." msg = "A proper RSL plate number is required."
return instance, {'code': 2, 'message': "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() 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 # get model based on submission type converted above
logger.debug(f"Looking at models for submission type: {query}") logger.debug(f"Looking at models for submission type: {query}")
@@ -142,7 +137,8 @@ def construct_submission_info(ctx:dict, info_dict:dict) -> models.BasicSubmissio
field_value = lookup_org_by_name(ctx=ctx, name=q_str) field_value = lookup_org_by_name(ctx=ctx, name=q_str)
logger.debug(f"Got {field_value} for organization {q_str}") logger.debug(f"Got {field_value} for organization {q_str}")
case "submitter_plate_num": case "submitter_plate_num":
# Because of unique constraint, the submitter plate number cannot be None, so... # Because of unique constraint, there will be problems with
# multiple submissions named 'None', so...
logger.debug(f"Submitter plate id: {info_dict[item]}") logger.debug(f"Submitter plate id: {info_dict[item]}")
if info_dict[item] == None or info_dict[item] == "None": 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.") logger.debug(f"Got None as a submitter plate number, inserting random string to preserve database unique constraint.")
@@ -156,7 +152,8 @@ def construct_submission_info(ctx:dict, info_dict:dict) -> models.BasicSubmissio
except AttributeError: except AttributeError:
logger.debug(f"Could not set attribute: {item} to {info_dict[item]}") logger.debug(f"Could not set attribute: {item} to {info_dict[item]}")
continue continue
# calculate cost of the run: immutable cost + mutable times number of columns # 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: try:
instance.run_cost = instance.extraction_kit.immutable_cost + (instance.extraction_kit.mutable_cost * ((instance.sample_count / 8)/12)) instance.run_cost = instance.extraction_kit.immutable_cost + (instance.extraction_kit.mutable_cost * ((instance.sample_count / 8)/12))
except (TypeError, AttributeError): except (TypeError, AttributeError):
@@ -167,7 +164,7 @@ def construct_submission_info(ctx:dict, info_dict:dict) -> models.BasicSubmissio
logger.debug(f"Constructed instance: {instance.to_string()}") logger.debug(f"Constructed instance: {instance.to_string()}")
except AttributeError as e: except AttributeError as e:
logger.debug(f"Something went wrong constructing instance {info_dict['rsl_plate_num']}: {e}") logger.debug(f"Something went wrong constructing instance {info_dict['rsl_plate_num']}: {e}")
logger.debug(msg) logger.debug(f"Constructed submissions message: {msg}")
return instance, {'code':code, 'message':msg} return instance, {'code':code, 'message':msg}
@@ -194,7 +191,7 @@ def construct_reagent(ctx:dict, info_dict:dict) -> models.Reagent:
case "type": case "type":
reagent.type = lookup_reagenttype_by_name(ctx=ctx, rt_name=info_dict[item].replace(" ", "_").lower()) reagent.type = lookup_reagenttype_by_name(ctx=ctx, rt_name=info_dict[item].replace(" ", "_").lower())
# add end-of-life extension from reagent type to expiry date # add end-of-life extension from reagent type to expiry date
# Edit: this will now be done only in the reporting phase to account for potential changes in end-of-life extensions # NOTE: this will now be done only in the reporting phase to account for potential changes in end-of-life extensions
# try: # try:
# reagent.expiry = reagent.expiry + reagent.type.eol_ext # reagent.expiry = reagent.expiry + reagent.type.eol_ext
# except TypeError as e: # except TypeError as e:
@@ -204,7 +201,6 @@ def construct_reagent(ctx:dict, info_dict:dict) -> models.Reagent:
return reagent return reagent
def lookup_reagent(ctx:dict, reagent_lot:str) -> models.Reagent: def lookup_reagent(ctx:dict, reagent_lot:str) -> models.Reagent:
""" """
Query db for reagent based on lot number Query db for reagent based on lot number
@@ -219,6 +215,7 @@ def lookup_reagent(ctx:dict, reagent_lot:str) -> models.Reagent:
lookedup = ctx['database_session'].query(models.Reagent).filter(models.Reagent.lot==reagent_lot).first() lookedup = ctx['database_session'].query(models.Reagent).filter(models.Reagent.lot==reagent_lot).first()
return lookedup return lookedup
def get_all_reagenttype_names(ctx:dict) -> list[str]: def get_all_reagenttype_names(ctx:dict) -> list[str]:
""" """
Lookup all reagent types and get names Lookup all reagent types and get names
@@ -232,6 +229,7 @@ def get_all_reagenttype_names(ctx:dict) -> list[str]:
lookedup = [item.__str__() for item in ctx['database_session'].query(models.ReagentType).all()] lookedup = [item.__str__() for item in ctx['database_session'].query(models.ReagentType).all()]
return lookedup return lookedup
def lookup_reagenttype_by_name(ctx:dict, rt_name:str) -> models.ReagentType: def lookup_reagenttype_by_name(ctx:dict, rt_name:str) -> models.ReagentType:
""" """
Lookup a single reagent type by name Lookup a single reagent type by name
@@ -251,7 +249,7 @@ def lookup_reagenttype_by_name(ctx:dict, rt_name:str) -> models.ReagentType:
def lookup_kittype_by_use(ctx:dict, used_by:str) -> list[models.KitType]: def lookup_kittype_by_use(ctx:dict, used_by:str) -> list[models.KitType]:
""" """
Lookup a kit by an sample type its used for Lookup kits by a sample type its used for
Args: Args:
ctx (dict): settings passed from gui ctx (dict): settings passed from gui
@@ -262,6 +260,7 @@ def lookup_kittype_by_use(ctx:dict, used_by:str) -> list[models.KitType]:
""" """
return ctx['database_session'].query(models.KitType).filter(models.KitType.used_for.contains(used_by)).all() 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: def lookup_kittype_by_name(ctx:dict, name:str) -> models.KitType:
""" """
Lookup a kit type by name Lookup a kit type by name
@@ -288,7 +287,6 @@ def lookup_regent_by_type_name(ctx:dict, type_name:str) -> list[models.Reagent]:
Returns: Returns:
list[models.Reagent]: list of retrieved reagents list[models.Reagent]: list of retrieved reagents
""" """
# return [item for item in ctx['database_session'].query(models.Reagent).join(models.Reagent.type, aliased=True).filter(models.ReagentType.name==type_name).all()]
return ctx['database_session'].query(models.Reagent).join(models.Reagent.type, aliased=True).filter(models.ReagentType.name==type_name).all() return ctx['database_session'].query(models.Reagent).join(models.Reagent.type, aliased=True).filter(models.ReagentType.name==type_name).all()
@@ -308,8 +306,7 @@ def lookup_regent_by_type_name_and_kit_name(ctx:dict, type_name:str, kit_name:st
# Hang on, this is going to be a long one. # Hang on, this is going to be a long one.
# by_type = ctx['database_session'].query(models.Reagent).join(models.Reagent.type, aliased=True).filter(models.ReagentType.name.endswith(type_name)).all() # 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)) rt_types = ctx['database_session'].query(models.ReagentType).filter(models.ReagentType.name.endswith(type_name))
# add filter for kit name... which I can not get to work. # add filter for kit name...
# add_in = by_type.join(models.ReagentType.kits).filter(models.KitType.name==kit_name)
try: try:
check = not np.isnan(kit_name) check = not np.isnan(kit_name)
except TypeError: except TypeError:
@@ -317,12 +314,10 @@ def lookup_regent_by_type_name_and_kit_name(ctx:dict, type_name:str, kit_name:st
if check: if check:
kit_type = lookup_kittype_by_name(ctx=ctx, name=kit_name) 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}") 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() rt_types = rt_types.join(reagenttypes_kittypes).filter(reagenttypes_kittypes.c.kits_id==kit_type.id).first()
else:
# for item in by_type: rt_types = rt_types.first()
# logger.debug([thing.name for thing in item.type.kits])
# output = [item for item in by_type if kit_name in [thing.name for thing in item.type.kits]]
# else:
output = rt_types.instances output = rt_types.instances
return output return output
@@ -336,7 +331,7 @@ def lookup_all_submissions_by_type(ctx:dict, sub_type:str|None=None) -> list[mod
type (str | None, optional): submission type (should be string in D3 of excel sheet). Defaults to None. type (str | None, optional): submission type (should be string in D3 of excel sheet). Defaults to None.
Returns: Returns:
_type_: list of retrieved submissions list[models.BasicSubmission]: list of retrieved submissions
""" """
if sub_type == None: if sub_type == None:
subs = ctx['database_session'].query(models.BasicSubmission).all() subs = ctx['database_session'].query(models.BasicSubmission).all()
@@ -358,7 +353,7 @@ def lookup_all_orgs(ctx:dict) -> list[models.Organization]:
def lookup_org_by_name(ctx:dict, name:str|None) -> models.Organization: def lookup_org_by_name(ctx:dict, name:str|None) -> models.Organization:
""" """
Lookup organization (lab) by name. Lookup organization (lab) by (startswith) name.
Args: Args:
ctx (dict): settings passed from gui ctx (dict): settings passed from gui
@@ -368,7 +363,6 @@ def lookup_org_by_name(ctx:dict, name:str|None) -> models.Organization:
models.Organization: retrieved organization models.Organization: retrieved organization
""" """
logger.debug(f"Querying organization: {name}") logger.debug(f"Querying organization: {name}")
# return ctx['database_session'].query(models.Organization).filter(models.Organization.name==name).first()
return ctx['database_session'].query(models.Organization).filter(models.Organization.name.startswith(name)).first() 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: def submissions_to_df(ctx:dict, sub_type:str|None=None) -> pd.DataFrame:
@@ -383,10 +377,11 @@ def submissions_to_df(ctx:dict, sub_type:str|None=None) -> pd.DataFrame:
pd.DataFrame: dataframe constructed from retrieved submissions pd.DataFrame: dataframe constructed from retrieved submissions
""" """
logger.debug(f"Type: {sub_type}") logger.debug(f"Type: {sub_type}")
# pass to lookup function # 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)] 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) df = pd.DataFrame.from_records(subs)
# logger.debug(f"Pre: {df['Technician']}") # Exclude sub information
try: try:
df = df.drop("controls", axis=1) df = df.drop("controls", axis=1)
except: except:
@@ -395,7 +390,6 @@ def submissions_to_df(ctx:dict, sub_type:str|None=None) -> pd.DataFrame:
df = df.drop("ext_info", axis=1) df = df.drop("ext_info", axis=1)
except: except:
logger.warning(f"Couldn't drop 'controls' column from submissionsheet df.") logger.warning(f"Couldn't drop 'controls' column from submissionsheet df.")
# logger.debug(f"Post: {df['Technician']}")
return df return df
@@ -413,13 +407,9 @@ def lookup_submission_by_id(ctx:dict, id:int) -> models.BasicSubmission:
return ctx['database_session'].query(models.BasicSubmission).filter(models.BasicSubmission.id==id).first() return ctx['database_session'].query(models.BasicSubmission).filter(models.BasicSubmission.id==id).first()
def create_submission_details(ctx:dict, sub_id:int) -> dict:
pass
def lookup_submissions_by_date_range(ctx:dict, start_date:datetime.date, end_date:datetime.date) -> list[models.BasicSubmission]: def lookup_submissions_by_date_range(ctx:dict, start_date:datetime.date, end_date:datetime.date) -> list[models.BasicSubmission]:
""" """
Lookup submissions by range of submitted dates Lookup submissions greater than start_date and less than end_date
Args: Args:
ctx (dict): settings passed from gui ctx (dict): settings passed from gui
@@ -429,18 +419,21 @@ def lookup_submissions_by_date_range(ctx:dict, start_date:datetime.date, end_dat
Returns: Returns:
list[models.BasicSubmission]: list of retrieved submissions list[models.BasicSubmission]: list of retrieved submissions
""" """
return ctx['database_session'].query(models.BasicSubmission).filter(and_(models.BasicSubmission.submitted_date > start_date, models.BasicSubmission.submitted_date < end_date)).all() # return ctx['database_session'].query(models.BasicSubmission).filter(and_(models.BasicSubmission.submitted_date > start_date, models.BasicSubmission.submitted_date < end_date)).all()
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[models.ControlType]: def get_all_Control_Types_names(ctx:dict) -> list[str]:
""" """
Grabs all control type names from db. Grabs all control type names from db.
Args: Args:
settings (dict): settings passed down from click. Defaults to {}. settings (dict): settings passed down from gui.
Returns: Returns:
list: names list list: list of controltype names
""" """
conTypes = ctx['database_session'].query(models.ControlType).all() conTypes = ctx['database_session'].query(models.ControlType).all()
conTypes = [conType.name for conType in conTypes] conTypes = [conType.name for conType in conTypes]
@@ -451,6 +444,7 @@ def get_all_Control_Types_names(ctx:dict) -> list[models.ControlType]:
def create_kit_from_yaml(ctx:dict, exp:dict) -> dict: def create_kit_from_yaml(ctx:dict, exp:dict) -> dict:
""" """
Create and store a new kit in the database based on a .yml file Create and store a new kit in the database based on a .yml file
TODO: split into create and store functions
Args: Args:
ctx (dict): Context dictionary passed down from frontend ctx (dict): Context dictionary passed down from frontend
@@ -459,18 +453,20 @@ def create_kit_from_yaml(ctx:dict, exp:dict) -> dict:
Returns: Returns:
dict: a dictionary containing results of db addition dict: a dictionary containing results of db addition
""" """
# try: # Don't want just anyone adding kits
# power_users = ctx['power_users']
# except KeyError:
if not check_is_power_user(ctx=ctx): if not check_is_power_user(ctx=ctx):
logger.debug(f"{getuser()} does not have permission to add kits.") 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"} return {'code':1, 'message':"This user does not have permission to add kits.", "status":"warning"}
# iterate through keys in dict
for type in exp: for type in exp:
if type == "password": if type == "password":
continue continue
# A submission type may use multiple kits.
for kt in exp[type]['kits']: for kt in exp[type]['kits']:
kit = models.KitType(name=kt, used_for=[type.replace("_", " ").title()], cost_per_run=exp[type]["kits"][kt]["cost"]) 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']: 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() look_up = ctx['database_session'].query(models.ReagentType).filter(models.ReagentType.name==r).first()
if look_up == None: 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]) rt = models.ReagentType(name=r.replace(" ", "_").lower(), eol_ext=timedelta(30*exp[type]['kits'][kt]['reagenttypes'][r]['eol_ext']), kits=[kit])
@@ -478,15 +474,15 @@ def create_kit_from_yaml(ctx:dict, exp:dict) -> dict:
rt = look_up rt = look_up
rt.kits.append(kit) rt.kits.append(kit)
# add this because I think it's necessary to get proper back population # add this because I think it's necessary to get proper back population
# rt.kit_id.append(kit.id)
kit.reagent_types_id.append(rt.id) kit.reagent_types_id.append(rt.id)
ctx['database_session'].add(rt) ctx['database_session'].add(rt)
logger.debug(rt.__dict__) logger.debug(f"Kit construction reagent type: {rt.__dict__}")
logger.debug(kit.__dict__) logger.debug(f"Kit construction kit: {kit.__dict__}")
ctx['database_session'].add(kit) ctx['database_session'].add(kit)
ctx['database_session'].commit() ctx['database_session'].commit()
return {'code':0, 'message':'Kit has been added', 'status': 'information'} return {'code':0, 'message':'Kit has been added', 'status': 'information'}
def create_org_from_yaml(ctx:dict, org:dict) -> dict: def create_org_from_yaml(ctx:dict, org:dict) -> dict:
""" """
Create and store a new organization based on a .yml file Create and store a new organization based on a .yml file
@@ -498,30 +494,26 @@ def create_org_from_yaml(ctx:dict, org:dict) -> dict:
Returns: Returns:
dict: dictionary containing results of db addition dict: dictionary containing results of db addition
""" """
# try: # Don't want just anyone adding in clients
# power_users = ctx['power_users']
# except KeyError:
# logger.debug("This user does not have permission to add kits.")
# return {'code':1,'message':"This user does not have permission to add organizations."}
# logger.debug(f"Adding organization for user: {getuser()}")
# if getuser() not in power_users:
if not check_is_power_user(ctx=ctx): if not check_is_power_user(ctx=ctx):
logger.debug(f"{getuser()} does not have permission to add kits.") logger.debug(f"{getuser()} does not have permission to add kits.")
return {'code':1, 'message':"This user does not have permission to add organizations."} return {'code':1, 'message':"This user does not have permission to add organizations."}
# the yml can contain multiple clients
for client in org: for client in org:
cli_org = models.Organization(name=client.replace(" ", "_").lower(), cost_centre=org[client]['cost centre']) 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']: for contact in org[client]['contacts']:
cont_name = list(contact.keys())[0] 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() look_up = ctx['database_session'].query(models.Contact).filter(models.Contact.name==cont_name).first()
if look_up == None: if look_up == None:
cli_cont = models.Contact(name=cont_name, phone=contact[cont_name]['phone'], email=contact[cont_name]['email'], organization=[cli_org]) cli_cont = models.Contact(name=cont_name, phone=contact[cont_name]['phone'], email=contact[cont_name]['email'], organization=[cli_org])
else: else:
cli_cont = look_up cli_cont = look_up
cli_cont.organization.append(cli_org) cli_cont.organization.append(cli_org)
# cli_org.contacts.append(cli_cont)
# cli_org.contact_ids.append_foreign_key(cli_cont.id)
ctx['database_session'].add(cli_cont) ctx['database_session'].add(cli_cont)
logger.debug(cli_cont.__dict__) 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'].add(cli_org)
ctx["database_session"].commit() ctx["database_session"].commit()
return {"code":0, "message":"Organization has been added."} return {"code":0, "message":"Organization has been added."}
@@ -538,11 +530,11 @@ def lookup_all_sample_types(ctx:dict) -> list[str]:
list[str]: list of sample type names list[str]: list of sample type names
""" """
uses = [item.used_for for item in ctx['database_session'].query(models.KitType).all()] uses = [item.used_for for item in ctx['database_session'].query(models.KitType).all()]
# flattened list of lists
uses = list(set([item for sublist in uses for item in sublist])) uses = list(set([item for sublist in uses for item in sublist]))
return uses return uses
def get_all_available_modes(ctx:dict) -> list[str]: def get_all_available_modes(ctx:dict) -> list[str]:
""" """
Get types of analysis for controls Get types of analysis for controls
@@ -553,6 +545,7 @@ def get_all_available_modes(ctx:dict) -> list[str]:
Returns: Returns:
list[str]: list of analysis types 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() rel = ctx['database_session'].query(models.Control).first()
try: try:
cols = [item.name for item in list(rel.__table__.columns) if isinstance(item.type, JSON)] cols = [item.name for item in list(rel.__table__.columns) if isinstance(item.type, JSON)]
@@ -562,54 +555,49 @@ def get_all_available_modes(ctx:dict) -> list[str]:
return 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]: 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. Returns a list of control objects that are instances of the input controltype.
Between dates if supplied.
Args: Args:
con_type (str): Name of the control type. ctx (dict): Settings passed down from gui
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: Returns:
list: Control instances. list[models.Control]: list of control samples.
""" """
logger.debug(f"Using dates: {start_date} to {end_date}") logger.debug(f"Using dates: {start_date} to {end_date}")
if start_date != None and end_date != None: if start_date != None and end_date != None:
output = ctx['database_session'].query(models.Control).join(models.ControlType).filter_by(name=con_type).filter(models.Control.submitted_date.between(start_date.strftime("%Y-%m-%d"), end_date.strftime("%Y-%m-%d"))).all() 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: else:
output = ctx['database_session'].query(models.Control).join(models.ControlType).filter_by(name=con_type).all() output = ctx['database_session'].query(models.Control).join(models.ControlType).filter_by(name=con_type).all()
logger.debug(f"Returned controls between dates: {output}") logger.debug(f"Returned controls between dates: {output}")
return output return output
# query = ctx['database_session'].query(models.ControlType).filter_by(name=con_type)
# try:
# output = query.first().instances
# except AttributeError:
# output = None
# # Hacky solution to my not being able to get the sql query to work.
# if start_date != None and end_date != None:
# output = [item for item in output if item.submitted_date.date() > start_date and item.submitted_date.date() < end_date]
# # logger.debug(f"Type {con_type}: {query.first()}")
# return output
def get_control_subtypes(ctx:dict, type:str, mode:str) -> list[str]: def get_control_subtypes(ctx:dict, type:str, mode:str) -> list[str]:
""" """
Get subtypes for a control analysis type Get subtypes for a control analysis mode
Args: Args:
ctx (dict): settings passed from gui ctx (dict): settings passed from gui
type (str): control type name type (str): control type name
mode (str): analysis type name mode (str): analysis mode name
Returns: Returns:
list[str]: list of subtype names list[str]: list of subtype names
""" """
# Only the first control of type is necessary since they all share subtypes
try: try:
outs = get_all_controls_by_type(ctx=ctx, con_type=type)[0] outs = get_all_controls_by_type(ctx=ctx, con_type=type)[0]
except TypeError: except TypeError:
return [] return []
# Get analysis mode data as dict
jsoner = json.loads(getattr(outs, mode)) jsoner = json.loads(getattr(outs, mode))
logger.debug(f"JSON out: {jsoner}") logger.debug(f"JSON out: {jsoner}")
try: try:
@@ -620,11 +608,30 @@ def get_control_subtypes(ctx:dict, type:str, mode:str) -> list[str]:
return subtypes return subtypes
def get_all_controls(ctx:dict): 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() return ctx['database_session'].query(models.Control).all()
def lookup_submission_by_rsl_num(ctx:dict, rsl_num:str): 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() return ctx['database_session'].query(models.BasicSubmission).filter(models.BasicSubmission.rsl_plate_num.startswith(rsl_num)).first()
@@ -641,10 +648,15 @@ def delete_submission_by_id(ctx:dict, id:int) -> None:
id (int): id of submission to be deleted. 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. # 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() 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() backup = sub.to_dict()
with open(Path(ctx['backup_path']).joinpath(f"{sub.rsl_plate_num}-backup({date.today().strftime('%Y%m%d')}).yml"), "w") as f: try:
yaml.dump(backup, f) 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 = [] sub.reagents = []
for sample in sub.samples: for sample in sub.samples:
ctx['database_session'].delete(sample) ctx['database_session'].delete(sample)

View File

@@ -21,7 +21,7 @@ class KitType(Base):
name = Column(String(64), unique=True) #: name of kit name = Column(String(64), unique=True) #: name of kit
submissions = relationship("BasicSubmission", back_populates="extraction_kit") #: submissions this kit was used for submissions = relationship("BasicSubmission", back_populates="extraction_kit") #: submissions this kit was used for
used_for = Column(JSON) #: list of names of sample types this kit can process used_for = Column(JSON) #: list of names of sample types this kit can process
cost_per_run = Column(FLOAT(2)) #: dollar amount for each full run of this kit cost_per_run = Column(FLOAT(2)) #: dollar amount for each full run of this kit NOTE: depreciated, use the constant and mutable costs instead
mutable_cost = Column(FLOAT(2)) #: dollar amount that can change with number of columns (reagents, tips, etc) mutable_cost = Column(FLOAT(2)) #: dollar amount that can change with number of columns (reagents, tips, etc)
constant_cost = Column(FLOAT(2)) #: dollar amount that will remain constant (plates, man hours, etc) constant_cost = Column(FLOAT(2)) #: dollar amount that will remain constant (plates, man hours, etc)
reagent_types = relationship("ReagentType", back_populates="kits", uselist=True, secondary=reagenttypes_kittypes) #: reagent types this kit contains reagent_types = relationship("ReagentType", back_populates="kits", uselist=True, secondary=reagenttypes_kittypes) #: reagent types this kit contains
@@ -81,9 +81,7 @@ class Reagent(Base):
Returns: Returns:
str: string representing this object's type and lot number str: string representing this object's type and lot number
""" """
lot = str(self.lot) return str(self.lot)
r_type = str(self.type)
return f"{r_type} - {lot}"
def to_sub_dict(self) -> dict: def to_sub_dict(self) -> dict:
""" """

View File

@@ -16,6 +16,9 @@ class WWSample(Base):
rsl_plate = relationship("Wastewater", back_populates="samples") #: relationship to parent plate rsl_plate = relationship("Wastewater", back_populates="samples") #: relationship to parent plate
rsl_plate_id = Column(INTEGER, ForeignKey("_submissions.id", ondelete="SET NULL", name="fk_WWS_submission_id")) rsl_plate_id = Column(INTEGER, ForeignKey("_submissions.id", ondelete="SET NULL", name="fk_WWS_submission_id"))
collection_date = Column(TIMESTAMP) #: Date submission received collection_date = Column(TIMESTAMP) #: Date submission received
well_number = Column(String(8)) #: location on plate
# The following are fields from the sample tracking excel sheet Ruth put together.
# I have no idea when they will be implemented or how.
testing_type = Column(String(64)) testing_type = Column(String(64))
site_status = Column(String(64)) site_status = Column(String(64))
notes = Column(String(2000)) notes = Column(String(2000))
@@ -24,7 +27,7 @@ class WWSample(Base):
seq_submitted = Column(BOOLEAN()) seq_submitted = Column(BOOLEAN())
ww_seq_run_id = Column(String(64)) ww_seq_run_id = Column(String(64))
sample_type = Column(String(8)) sample_type = Column(String(8))
well_number = Column(String(8)) #: location on plate
def to_string(self) -> str: def to_string(self) -> str:
""" """

View File

@@ -35,6 +35,7 @@ class BasicSubmission(Base):
run_cost = Column(FLOAT(2)) #: total cost of running the plate. Set from kit costs at time of creation. run_cost = Column(FLOAT(2)) #: total cost of running the plate. Set from kit costs at time of creation.
uploaded_by = Column(String(32)) #: user name of person who submitted the submission to the database. uploaded_by = Column(String(32)) #: user name of person who submitted the submission to the database.
# Allows for subclassing into ex. BacterialCulture, Wastewater, etc.
__mapper_args__ = { __mapper_args__ = {
"polymorphic_identity": "basic_submission", "polymorphic_identity": "basic_submission",
"polymorphic_on": submission_type, "polymorphic_on": submission_type,
@@ -148,23 +149,25 @@ class BasicSubmission(Base):
} }
return output return output
# Below are the custom submission # Below are the custom submission types
class BacterialCulture(BasicSubmission): class BacterialCulture(BasicSubmission):
""" """
derivative submission type from BasicSubmission derivative submission type from BasicSubmission
""" """
# control_id = Column(INTEGER, ForeignKey("_control_samples.id", ondelete="SET NULL", name="fk_BC_control_id"))
controls = relationship("Control", back_populates="submission", uselist=True) #: A control sample added to submission controls = relationship("Control", back_populates="submission", uselist=True) #: A control sample added to submission
samples = relationship("BCSample", back_populates="rsl_plate", uselist=True) samples = relationship("BCSample", back_populates="rsl_plate", uselist=True)
# bc_sample_id = Column(INTEGER, ForeignKey("_bc_samples.id", ondelete="SET NULL", name="fk_BC_sample_id"))
__mapper_args__ = {"polymorphic_identity": "bacterial_culture", "polymorphic_load": "inline"} __mapper_args__ = {"polymorphic_identity": "bacterial_culture", "polymorphic_load": "inline"}
def to_dict(self) -> dict: def to_dict(self) -> dict:
"""
Extends parent class method to add controls to dict
Returns:
dict: dictionary used in submissions summary
"""
output = super().to_dict() output = super().to_dict()
output['controls'] = [item.to_sub_dict() for item in self.controls] output['controls'] = [item.to_sub_dict() for item in self.controls]
# logger.debug(f"{self.rsl_plate_num} technician: {output}")
return output return output

View File

@@ -2,7 +2,6 @@ from pandas import DataFrame
import re 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
@@ -40,3 +39,5 @@ def drop_reruns_from_df(ctx:dict, df: DataFrame) -> DataFrame:
# 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:
return None

View File

@@ -75,15 +75,14 @@ class SheetParser(object):
Returns: Returns:
pd.DataFrame: relevant dataframe from excel sheet pd.DataFrame: relevant dataframe from excel sheet
""" """
# self.xl is a pd.ExcelFile so we need to parse it into a df
submission_info = self.xl.parse(sheet_name=sheet_name, dtype=object) submission_info = self.xl.parse(sheet_name=sheet_name, dtype=object)
self.sub['submitter_plate_num'] = submission_info.iloc[0][1] self.sub['submitter_plate_num'] = submission_info.iloc[0][1]
self.sub['rsl_plate_num'] = submission_info.iloc[10][1] self.sub['rsl_plate_num'] = submission_info.iloc[10][1]
self.sub['submitted_date'] = submission_info.iloc[1][1] self.sub['submitted_date'] = submission_info.iloc[1][1]
self.sub['submitting_lab'] = submission_info.iloc[0][3] self.sub['submitting_lab'] = submission_info.iloc[0][3]
self.sub['sample_count'] = submission_info.iloc[2][3] self.sub['sample_count'] = submission_info.iloc[2][3]
self.sub['extraction_kit'] = submission_info.iloc[3][3] self.sub['extraction_kit'] = submission_info.iloc[3][3]
return submission_info return submission_info
@@ -104,10 +103,6 @@ class SheetParser(object):
if ii == 11: if ii == 11:
continue continue
logger.debug(f"Running reagent parse for {row[1]} with type {type(row[1])} and value: {row[2]} with type {type(row[2])}") logger.debug(f"Running reagent parse for {row[1]} with type {type(row[1])} and value: {row[2]} with type {type(row[2])}")
# try:
# check = not np.isnan(row[1])
# except TypeError:
# check = True
if not isinstance(row[2], float) and check_not_nan(row[1]): if not isinstance(row[2], float) and check_not_nan(row[1]):
# must be prefixed with 'lot_' to be recognized by gui # must be prefixed with 'lot_' to be recognized by gui
try: try:
@@ -122,13 +117,7 @@ class SheetParser(object):
logger.debug(f"Couldn't upperize {row[2]}, must be a number") logger.debug(f"Couldn't upperize {row[2]}, must be a number")
output_var = row[2] output_var = row[2]
logger.debug(f"Output variable is {output_var}") logger.debug(f"Output variable is {output_var}")
# self.sub[f"lot_{reagent_type}"] = output_var
# update 2023-02-10 to above allowing generation of expiry date in adding reagent to db.
logger.debug(f"Expiry date for imported reagent: {row[3]}") logger.debug(f"Expiry date for imported reagent: {row[3]}")
# try:
# check = not np.isnan(row[3])
# except TypeError:
# check = True
if check_not_nan(row[3]): if check_not_nan(row[3]):
expiry = row[3].date() expiry = row[3].date()
else: else:
@@ -146,19 +135,8 @@ class SheetParser(object):
# reagents # reagents
# must be prefixed with 'lot_' to be recognized by gui # must be prefixed with 'lot_' to be recognized by gui
# Todo: find a more adaptable way to read reagents. # Todo: find a more adaptable way to read reagents.
reagent_range = submission_info.iloc[1:13, 4:8] reagent_range = submission_info.iloc[1:13, 4:8]
_parse_reagents(reagent_range) _parse_reagents(reagent_range)
# self.sub['lot_wash_1'] = submission_info.iloc[1][6] #if pd.isnull(submission_info.iloc[1][6]) else string_formatter(submission_info.iloc[1][6])
# self.sub['lot_wash_2'] = submission_info.iloc[2][6] #if pd.isnull(submission_info.iloc[2][6]) else string_formatter(submission_info.iloc[2][6])
# self.sub['lot_binding_buffer'] = submission_info.iloc[3][6] #if pd.isnull(submission_info.iloc[3][6]) else string_formatter(submission_info.iloc[3][6])
# self.sub['lot_magnetic_beads'] = submission_info.iloc[4][6] #if pd.isnull(submission_info.iloc[4][6]) else string_formatter(submission_info.iloc[4][6])
# self.sub['lot_lysis_buffer'] = submission_info.iloc[5][6] #if np.nan(submission_info.iloc[5][6]) else string_formatter(submission_info.iloc[5][6])
# self.sub['lot_elution_buffer'] = submission_info.iloc[6][6] #if pd.isnull(submission_info.iloc[6][6]) else string_formatter(submission_info.iloc[6][6])
# self.sub['lot_isopropanol'] = submission_info.iloc[9][6] #if pd.isnull(submission_info.iloc[9][6]) else string_formatter(submission_info.iloc[9][6])
# self.sub['lot_ethanol'] = submission_info.iloc[10][6] #if pd.isnull(submission_info.iloc[10][6]) else string_formatter(submission_info.iloc[10][6])
# self.sub['lot_positive_control'] = submission_info.iloc[103][1] #if pd.isnull(submission_info.iloc[103][1]) else string_formatter(submission_info.iloc[103][1])
# self.sub['lot_plate'] = submission_info.iloc[12][6] #if pd.isnull(submission_info.iloc[12][6]) else string_formatter(submission_info.iloc[12][6])
# get individual sample info # get individual sample info
sample_parser = SampleParser(submission_info.iloc[15:111]) sample_parser = SampleParser(submission_info.iloc[15:111])
sample_parse = getattr(sample_parser, f"parse_{self.sub['submission_type'].lower()}_samples") sample_parse = getattr(sample_parser, f"parse_{self.sub['submission_type'].lower()}_samples")
@@ -178,12 +156,8 @@ class SheetParser(object):
Args: Args:
df (pd.DataFrame): input sub dataframe df (pd.DataFrame): input sub dataframe
""" """
# logger.debug(df) # iterate through sub-df rows
for ii, row in df.iterrows(): for ii, row in df.iterrows():
# try:
# check = not np.isnan(row[5])
# except TypeError:
# check = True
if not isinstance(row[5], float) and check_not_nan(row[5]): if not isinstance(row[5], float) and check_not_nan(row[5]):
# must be prefixed with 'lot_' to be recognized by gui # must be prefixed with 'lot_' to be recognized by gui
# regex below will remove 80% from 80% ethanol in the Wastewater kit. # regex below will remove 80% from 80% ethanol in the Wastewater kit.
@@ -202,34 +176,26 @@ class SheetParser(object):
else: else:
expiry = date.today() expiry = date.today()
self.sub[f"lot_{output_key}"] = {'lot':output_var, 'exp':expiry} self.sub[f"lot_{output_key}"] = {'lot':output_var, 'exp':expiry}
# parse submission sheet
submission_info = self._parse_generic("WW Submissions (ENTER HERE)") submission_info = self._parse_generic("WW Submissions (ENTER HERE)")
# parse enrichment sheet
enrichment_info = self.xl.parse("Enrichment Worksheet", dtype=object) enrichment_info = self.xl.parse("Enrichment Worksheet", dtype=object)
# set enrichment reagent range
enr_reagent_range = enrichment_info.iloc[0:4, 9:20] enr_reagent_range = enrichment_info.iloc[0:4, 9:20]
# parse extraction sheet
extraction_info = self.xl.parse("Extraction Worksheet", dtype=object) extraction_info = self.xl.parse("Extraction Worksheet", dtype=object)
# set extraction reagent range
ext_reagent_range = extraction_info.iloc[0:5, 9:20] ext_reagent_range = extraction_info.iloc[0:5, 9:20]
# parse qpcr sheet
qprc_info = self.xl.parse("qPCR Worksheet", dtype=object) qprc_info = self.xl.parse("qPCR Worksheet", dtype=object)
# set qpcr reagent range
pcr_reagent_range = qprc_info.iloc[0:5, 9:20] pcr_reagent_range = qprc_info.iloc[0:5, 9:20]
# compile technician info
self.sub['technician'] = f"Enr: {enrichment_info.columns[2]}, Ext: {extraction_info.columns[2]}, PCR: {qprc_info.columns[2]}" self.sub['technician'] = f"Enr: {enrichment_info.columns[2]}, Ext: {extraction_info.columns[2]}, PCR: {qprc_info.columns[2]}"
_parse_reagents(enr_reagent_range) _parse_reagents(enr_reagent_range)
_parse_reagents(ext_reagent_range) _parse_reagents(ext_reagent_range)
_parse_reagents(pcr_reagent_range) _parse_reagents(pcr_reagent_range)
# reagents # parse samples
# logger.debug(qprc_info)
# self.sub['lot_lysis_buffer'] = enrichment_info.iloc[0][14] #if pd.isnull(enrichment_info.iloc[0][14]) else string_formatter(enrichment_info.iloc[0][14])
# self.sub['lot_proteinase_K'] = enrichment_info.iloc[1][14] #if pd.isnull(enrichment_info.iloc[1][14]) else string_formatter(enrichment_info.iloc[1][14])
# self.sub['lot_magnetic_virus_particles'] = enrichment_info.iloc[2][14] #if pd.isnull(enrichment_info.iloc[2][14]) else string_formatter(enrichment_info.iloc[2][14])
# self.sub['lot_enrichment_reagent_1'] = enrichment_info.iloc[3][14] #if pd.isnull(enrichment_info.iloc[3][14]) else string_formatter(enrichment_info.iloc[3][14])
# self.sub['lot_binding_buffer'] = extraction_info.iloc[0][14] #if pd.isnull(extraction_info.iloc[0][14]) else string_formatter(extraction_info.iloc[0][14])
# self.sub['lot_magnetic_beads'] = extraction_info.iloc[1][14] #if pd.isnull(extraction_info.iloc[1][14]) else string_formatter(extraction_info.iloc[1][14])
# self.sub['lot_wash'] = extraction_info.iloc[2][14] #if pd.isnull(extraction_info.iloc[2][14]) else string_formatter(extraction_info.iloc[2][14])
# self.sub['lot_ethanol'] = extraction_info.iloc[3][14] #if pd.isnull(extraction_info.iloc[3][14]) else string_formatter(extraction_info.iloc[3][14])
# self.sub['lot_elution_buffer'] = extraction_info.iloc[4][14] #if pd.isnull(extraction_info.iloc[4][14]) else string_formatter(extraction_info.iloc[4][14])
# self.sub['lot_master_mix'] = qprc_info.iloc[0][14] #if pd.isnull(qprc_info.iloc[0][14]) else string_formatter(qprc_info.iloc[0][14])
# self.sub['lot_pre_mix_1'] = qprc_info.iloc[1][14] #if pd.isnull(qprc_info.iloc[1][14]) else string_formatter(qprc_info.iloc[1][14])
# self.sub['lot_pre_mix_2'] = qprc_info.iloc[2][14] #if pd.isnull(qprc_info.iloc[2][14]) else string_formatter(qprc_info.iloc[2][14])
# self.sub['lot_positive_control'] = qprc_info.iloc[3][14] #if pd.isnull(qprc_info.iloc[3][14]) else string_formatter(qprc_info.iloc[3][14])
# self.sub['lot_ddh2o'] = qprc_info.iloc[4][14] #if pd.isnull(qprc_info.iloc[4][14]) else string_formatter(qprc_info.iloc[4][14])
# get individual sample info
sample_parser = SampleParser(submission_info.iloc[16:40]) sample_parser = SampleParser(submission_info.iloc[16:40])
sample_parse = getattr(sample_parser, f"parse_{self.sub['submission_type'].lower()}_samples") sample_parse = getattr(sample_parser, f"parse_{self.sub['submission_type'].lower()}_samples")
self.sub['samples'] = sample_parse() self.sub['samples'] = sample_parse()
@@ -241,6 +207,12 @@ class SampleParser(object):
""" """
def __init__(self, df:pd.DataFrame) -> None: def __init__(self, df:pd.DataFrame) -> None:
"""
convert sample sub-dataframe to dictionary of records
Args:
df (pd.DataFrame): input sample dataframe
"""
self.samples = df.to_dict("records") self.samples = df.to_dict("records")
@@ -287,6 +259,7 @@ class SampleParser(object):
not_a_nan = not np.isnan(sample['Unnamed: 3']) not_a_nan = not np.isnan(sample['Unnamed: 3'])
except TypeError: except TypeError:
not_a_nan = True not_a_nan = True
# if we don't have a sample full id, make one up
if not_a_nan: if not_a_nan:
new.ww_sample_full_id = sample['Unnamed: 3'] new.ww_sample_full_id = sample['Unnamed: 3']
else: else:

View File

@@ -1,8 +1,6 @@
from pandas import DataFrame, concat from pandas import DataFrame
from operator import itemgetter
# from backend.db import models # from backend.db import models
import json
import logging import logging
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from datetime import date, timedelta from datetime import date, timedelta
@@ -38,13 +36,8 @@ def make_report_xlsx(records:list[dict]) -> DataFrame:
df2 = df.groupby(["Submitting Lab", "Extraction Kit"]).agg({'Extraction Kit':'count', 'Cost': 'sum', 'Sample Count':'sum'}) df2 = df.groupby(["Submitting Lab", "Extraction Kit"]).agg({'Extraction Kit':'count', 'Cost': 'sum', 'Sample Count':'sum'})
df2 = df2.rename(columns={"Extraction Kit": 'Kit Count'}) df2 = df2.rename(columns={"Extraction Kit": 'Kit Count'})
logger.debug(f"Output daftaframe for xlsx: {df2.columns}") logger.debug(f"Output daftaframe for xlsx: {df2.columns}")
# apply formating to cost column
# df2.iloc[:, (df2.columns.get_level_values(1)=='sum') & (df2.columns.get_level_values(0)=='Cost')] = df2.iloc[:, (df2.columns.get_level_values(1)=='sum') & (df2.columns.get_level_values(0)=='Cost')].applymap('${:,.2f}'.format)
return df2 return df2
# def split_row_item(item:str) -> float:
# return item.split(" ")[-1]
def make_report_html(df:DataFrame, start_date:date, end_date:date) -> str: def make_report_html(df:DataFrame, start_date:date, end_date:date) -> str:
@@ -63,23 +56,20 @@ def make_report_html(df:DataFrame, start_date:date, end_date:date) -> str:
output = [] output = []
logger.debug(f"Report DataFrame: {df}") logger.debug(f"Report DataFrame: {df}")
for ii, row in enumerate(df.iterrows()): for ii, row in enumerate(df.iterrows()):
# row = [item for item in row]
logger.debug(f"Row {ii}: {row}") logger.debug(f"Row {ii}: {row}")
lab = row[0][0] lab = row[0][0]
logger.debug(type(row)) logger.debug(type(row))
logger.debug(f"Old lab: {old_lab}, Current lab: {lab}") logger.debug(f"Old lab: {old_lab}, Current lab: {lab}")
logger.debug(f"Name: {row[0][1]}") logger.debug(f"Name: {row[0][1]}")
data = [item for item in row[1]] data = [item for item in row[1]]
# logger.debug(data)
# logger.debug(f"Cost: {split_row_item(data[1])}")
# logger.debug(f"Kit count: {split_row_item(data[0])}")
# logger.debug(f"Sample Count: {split_row_item(data[2])}")
kit = dict(name=row[0][1], cost=data[1], plate_count=int(data[0]), sample_count=int(data[2])) kit = dict(name=row[0][1], cost=data[1], plate_count=int(data[0]), sample_count=int(data[2]))
# if this is the same lab as before add together
if lab == old_lab: if lab == old_lab:
output[-1]['kits'].append(kit) output[-1]['kits'].append(kit)
output[-1]['total_cost'] += kit['cost'] output[-1]['total_cost'] += kit['cost']
output[-1]['total_samples'] += kit['sample_count'] output[-1]['total_samples'] += kit['sample_count']
output[-1]['total_plates'] += kit['plate_count'] output[-1]['total_plates'] += kit['plate_count']
# if not the same lab, make a new one
else: else:
adder = dict(lab=lab, kits=[kit], total_cost=kit['cost'], total_samples=kit['sample_count'], total_plates=kit['plate_count']) adder = dict(lab=lab, kits=[kit], total_cost=kit['cost'], total_samples=kit['sample_count'], total_plates=kit['plate_count'])
output.append(adder) output.append(adder)
@@ -91,83 +81,6 @@ def make_report_html(df:DataFrame, start_date:date, end_date:date) -> str:
return html return html
# def split_controls_dictionary(ctx:dict, input_dict) -> list[dict]:
# # this will be the date in string form
# dict_name = list(input_dict.keys())[0]
# # the data associated with the date key
# sub_dict = input_dict[dict_name]
# # How many "count", "Percent", etc are in the dictionary
# data_size = get_dict_size(sub_dict)
# output = []
# for ii in range(data_size):
# new_dict = {}
# for genus in sub_dict:
# logger.debug(genus)
# sub_name = list(sub_dict[genus].keys())[ii]
# new_dict[genus] = sub_dict[genus][sub_name]
# output.append({"date":dict_name, "name": sub_name, "data": new_dict})
# return output
# def get_dict_size(input:dict):
# return max(len(input[item]) for item in input)
# def convert_all_controls(ctx:dict, data:list) -> dict:
# dfs = {}
# dict_list = [split_controls_dictionary(ctx, datum) for datum in data]
# dict_list = [item for sublist in dict_list for item in sublist]
# names = list(set([datum['name'] for datum in dict_list]))
# for name in names:
# # df = DataFrame()
# # entries = [{item['date']:item['data']} for item in dict_list if item['name']==name]
# # series_list = []
# # df = pd.json_normalize(entries)
# # for entry in entries:
# # col_name = list(entry.keys())[0]
# # col_dict = entry[col_name]
# # series = pd.Series(data=col_dict.values(), index=col_dict.keys(), name=col_name)
# # # df[col_name] = series.values
# # # logger.debug(df.index)
# # series_list.append(series)
# # df = DataFrame(series_list).T.fillna(0)
# # logger.debug(df)
# dfs['name'] = df
# return dfs
# def convert_control_by_mode(ctx:dict, control:models.Control, mode:str) -> list[dict]:
# """
# split control object into analysis types... can I move this into the class itself?
# turns out I can
# Args:
# ctx (dict): settings passed from gui
# control (models.Control): control to be parsed into list
# mode (str): analysis type
# Returns:
# list[dict]: list of records
# """
# output = []
# data = json.loads(getattr(control, mode))
# for genus in data:
# _dict = {}
# _dict['name'] = control.name
# _dict['submitted_date'] = control.submitted_date
# _dict['genus'] = genus
# _dict['target'] = 'Target' if genus.strip("*") in control.controltype.targets else "Off-target"
# for key in data[genus]:
# _dict[key] = data[genus][key]
# output.append(_dict)
# # logger.debug(output)
# return output
def convert_data_list_to_df(ctx:dict, input:list[dict], subtype:str|None=None) -> DataFrame: def convert_data_list_to_df(ctx:dict, input:list[dict], subtype:str|None=None) -> DataFrame:
""" """
Convert list of control records to dataframe Convert list of control records to dataframe

View File

@@ -3,7 +3,6 @@ import sys, os, stat, platform, getpass
import logging import logging
from logging import handlers from logging import handlers
from pathlib import Path from pathlib import Path
# from getpass import getuser
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import create_engine from sqlalchemy import create_engine
@@ -26,9 +25,6 @@ main_aux_dir = Path.home().joinpath(f"{os_config_dir}/submissions")
CONFIGDIR = main_aux_dir.joinpath("config") CONFIGDIR = main_aux_dir.joinpath("config")
LOGDIR = main_aux_dir.joinpath("logs") LOGDIR = main_aux_dir.joinpath("logs")
class GroupWriteRotatingFileHandler(handlers.RotatingFileHandler): class GroupWriteRotatingFileHandler(handlers.RotatingFileHandler):
def doRollover(self): def doRollover(self):
@@ -43,7 +39,6 @@ class GroupWriteRotatingFileHandler(handlers.RotatingFileHandler):
def _open(self): def _open(self):
prevumask=os.umask(0o002) prevumask=os.umask(0o002)
#os.fdopen(os.open('/path/to/file', os.O_WRONLY, 0600))
rtv=handlers.RotatingFileHandler._open(self) rtv=handlers.RotatingFileHandler._open(self)
os.umask(prevumask) os.umask(prevumask)
return rtv return rtv
@@ -74,12 +69,12 @@ def get_config(settings_path: str|None=None) -> dict:
Returns: Returns:
setting: dictionary of settings. setting: dictionary of settings.
""" """
# custom pyyaml constructor to join fields
def join(loader, node): def join(loader, node):
seq = loader.construct_sequence(node) seq = loader.construct_sequence(node)
return ''.join([str(i) for i in seq]) return ''.join([str(i) for i in seq])
## register the tag handler # register the tag handler
yaml.add_constructor('!join', join) yaml.add_constructor('!join', join)
logger.debug(f"Making directory: {CONFIGDIR.__str__()}") logger.debug(f"Making directory: {CONFIGDIR.__str__()}")
# make directories # make directories
try: try:
@@ -97,7 +92,7 @@ def get_config(settings_path: str|None=None) -> dict:
# Check user .config/submissions directory # Check user .config/submissions directory
if CONFIGDIR.joinpath("config.yml").exists(): if CONFIGDIR.joinpath("config.yml").exists():
settings_path = CONFIGDIR.joinpath("config.yml") settings_path = CONFIGDIR.joinpath("config.yml")
# Check user .ozma directory # Check user .submissions directory
elif Path.home().joinpath(".submissions", "config.yml").exists(): elif Path.home().joinpath(".submissions", "config.yml").exists():
settings_path = Path.home().joinpath(".submissions", "config.yml") settings_path = Path.home().joinpath(".submissions", "config.yml")
# finally look in the local config # finally look in the local config
@@ -108,10 +103,11 @@ def get_config(settings_path: str|None=None) -> dict:
settings_path = package_dir.joinpath('config.yml') settings_path = package_dir.joinpath('config.yml')
# Tell program we need to copy the config.yml to the user directory # Tell program we need to copy the config.yml to the user directory
copy_settings_trigger = True copy_settings_trigger = True
# shutil.copyfile(settings_path.__str__(), CONFIGDIR.joinpath("config.yml").__str__())
else: else:
# check if user defined path is directory
if Path(settings_path).is_dir(): if Path(settings_path).is_dir():
settings_path = settings_path.joinpath("config.yml") settings_path = settings_path.joinpath("config.yml")
# check if user defined path is file
elif Path(settings_path).is_file(): elif Path(settings_path).is_file():
settings_path = settings_path settings_path = settings_path
else: else:
@@ -132,7 +128,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 if blank. Get database settings 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.
@@ -140,17 +136,22 @@ def create_database_session(database_path: Path|str|None=None) -> Session:
Returns: Returns:
Session: database session Session: database session
""" """
# convert string to path object
if isinstance(database_path, str): if isinstance(database_path, str):
database_path = Path(database_path) database_path = Path(database_path)
# check if database path defined by user
if database_path == None: if database_path == None:
# check in user's .submissions directory for submissions.db
if Path.home().joinpath(".submissions", "submissions.db").exists(): if Path.home().joinpath(".submissions", "submissions.db").exists():
database_path = Path.home().joinpath(".submissions", "submissions.db") database_path = Path.home().joinpath(".submissions", "submissions.db")
# finally, look in the local dir # finally, look in the local dir
else: else:
database_path = package_dir.joinpath("submissions.db") database_path = package_dir.joinpath("submissions.db")
else: else:
# check if user defined path is directory
if database_path.is_dir(): if database_path.is_dir():
database_path = database_path.joinpath("submissions.db") database_path = database_path.joinpath("submissions.db")
# check if user defined path is a file
elif database_path.is_file(): elif database_path.is_file():
database_path = database_path database_path = database_path
else: else:
@@ -177,17 +178,16 @@ def setup_logger(verbosity:int=3):
# create file handler which logs even debug messages # create file handler which logs even debug messages
try: try:
Path(LOGDIR).mkdir(parents=True) Path(LOGDIR).mkdir(parents=True)
# fh = GroupWriteRotatingFileHandler(LOGDIR.joinpath('submissions.log'), mode='a', maxBytes=100000, backupCount=3, encoding=None, delay=False)
# except FileNotFoundError as e:
except FileExistsError: except FileExistsError:
pass pass
fh = GroupWriteRotatingFileHandler(LOGDIR.joinpath('submissions.log'), mode='a', maxBytes=100000, backupCount=3, encoding=None, delay=False) fh = GroupWriteRotatingFileHandler(LOGDIR.joinpath('submissions.log'), mode='a', maxBytes=100000, backupCount=3, encoding=None, delay=False)
# file logging will always be debug
fh.setLevel(logging.DEBUG) fh.setLevel(logging.DEBUG)
fh.name = "File" fh.name = "File"
# create console handler with a higher log level # create console handler with a higher log level
ch = logging.StreamHandler(stream=sys.stdout)
# create custom logger with STERR -> log # create custom logger with STERR -> log
# ch = StreamToLogger(logger=logger, log_level=verbosity) ch = logging.StreamHandler(stream=sys.stdout)
# set looging level based on verbosity
match verbosity: match verbosity:
case 3: case 3:
ch.setLevel(logging.DEBUG) ch.setLevel(logging.DEBUG)
@@ -203,14 +203,12 @@ def setup_logger(verbosity:int=3):
# add the handlers to the logger # add the handlers to the logger
logger.addHandler(fh) logger.addHandler(fh)
logger.addHandler(ch) logger.addHandler(ch)
# Output exception and traceback to logger
def handle_exception(exc_type, exc_value, exc_traceback): def handle_exception(exc_type, exc_value, exc_traceback):
if issubclass(exc_type, KeyboardInterrupt): if issubclass(exc_type, KeyboardInterrupt):
sys.__excepthook__(exc_type, exc_value, exc_traceback) sys.__excepthook__(exc_type, exc_value, exc_traceback)
return return
logger.critical("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)) logger.critical("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
# sys.exit(f"Uncaught error: {exc_type}, {exc_traceback}, check logs.")
sys.excepthook = handle_exception sys.excepthook = handle_exception
return logger return logger
@@ -233,5 +231,3 @@ def copy_settings(settings_path:Path, settings:dict) -> dict:
with open(settings_path, 'w') as f: with open(settings_path, 'w') as f:
yaml.dump(settings, f) yaml.dump(settings, f)
return settings return settings

View File

@@ -1,12 +1,11 @@
import json import json
import re import re
from typing import Tuple
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QMainWindow, QLabel, QToolBar, QMainWindow, QLabel, QToolBar,
QTabWidget, QWidget, QVBoxLayout, QTabWidget, QWidget, QVBoxLayout,
QPushButton, QFileDialog, QPushButton, QFileDialog,
QLineEdit, QMessageBox, QComboBox, QDateEdit, QHBoxLayout, QLineEdit, QMessageBox, QComboBox, QDateEdit, QHBoxLayout,
QSpinBox, QScrollArea QScrollArea
) )
from PyQt6.QtGui import QAction from PyQt6.QtGui import QAction
from PyQt6.QtCore import QSignalBlocker from PyQt6.QtCore import QSignalBlocker
@@ -27,15 +26,13 @@ from backend.db import (construct_submission_info, lookup_reagent,
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,
get_all_Control_Types_names, create_kit_from_yaml, get_all_available_modes, get_all_controls_by_type, get_all_Control_Types_names, create_kit_from_yaml, get_all_available_modes, get_all_controls_by_type,
get_control_subtypes, lookup_all_submissions_by_type, get_all_controls, lookup_submission_by_rsl_num, get_control_subtypes, lookup_all_submissions_by_type, get_all_controls, lookup_submission_by_rsl_num,
create_org_from_yaml 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 check_kit_integrity from .functions import check_kit_integrity
from tools import check_not_nan from tools import check_not_nan, extract_form_info
from backend.excel.reports import make_report_xlsx, make_report_html from backend.excel.reports import make_report_xlsx, make_report_html
import numpy
from frontend.custom_widgets.sub_details import SubmissionsSheet from frontend.custom_widgets.sub_details import SubmissionsSheet
from frontend.custom_widgets.pop_ups import AlertPop, QuestionAsker from frontend.custom_widgets.pop_ups import AlertPop, QuestionAsker
from frontend.custom_widgets import AddReagentForm, ReportDatePicker, KitAdder, ControlsDatePicker, ImportReagent from frontend.custom_widgets import AddReagentForm, ReportDatePicker, KitAdder, ControlsDatePicker, ImportReagent
@@ -233,12 +230,18 @@ class App(QMainWindow):
# hold samples in 'self' until form submitted # hold samples in 'self' until form submitted
logger.debug(f"{item}: {prsr.sub[item]}") logger.debug(f"{item}: {prsr.sub[item]}")
self.samples = prsr.sub[item] self.samples = prsr.sub[item]
add_widget = None
case _: case _:
# anything else gets added in as a line edit # anything else gets added in as a line edit
self.table_widget.formlayout.addWidget(QLabel(item.replace("_", " ").title())) self.table_widget.formlayout.addWidget(QLabel(item.replace("_", " ").title()))
add_widget = QLineEdit() add_widget = QLineEdit()
add_widget.setText(str(prsr.sub[item]).replace("_", " ")) add_widget.setText(str(prsr.sub[item]).replace("_", " "))
self.table_widget.formlayout.addWidget(add_widget) try:
add_widget.setObjectName(item)
logger.debug(f"Widget name set to: {add_widget.objectName()}")
self.table_widget.formlayout.addWidget(add_widget)
except AttributeError as e:
logger.error(e)
# compare self.reagents with expected reagents in kit # compare self.reagents with expected reagents in kit
if hasattr(self, 'ext_kit'): if hasattr(self, 'ext_kit'):
kit = lookup_kittype_by_name(ctx=self.ctx, name=self.ext_kit) kit = lookup_kittype_by_name(ctx=self.ctx, name=self.ext_kit)
@@ -261,9 +264,10 @@ class App(QMainWindow):
Attempt to add sample to database when 'submit' button clicked Attempt to add sample to database when 'submit' button clicked
""" """
# get info from form # get info from form
labels, values = self.extract_form_info(self.table_widget.tab1) info = extract_form_info(self.table_widget.tab1)
info = {item[0]:item[1] for item in zip(labels, values) if not item[0].startswith("lot_")} reagents = {k:v for k,v in info.items() if k.startswith("lot_")}
reagents = {item[0]:item[1] for item in zip(labels, values) if item[0].startswith("lot_")} info = {k:v for k,v in info.items() if not k.startswith("lot_")}
logger.debug(f"Info: {info}")
logger.debug(f"Reagents: {reagents}") logger.debug(f"Reagents: {reagents}")
parsed_reagents = [] parsed_reagents = []
# compare reagents in form to reagent database # compare reagents in form to reagent database
@@ -308,7 +312,6 @@ class App(QMainWindow):
# add reagents to submission object # add reagents to submission object
for reagent in parsed_reagents: for reagent in parsed_reagents:
base_submission.reagents.append(reagent) base_submission.reagents.append(reagent)
# base_submission.reagents_id = reagent.id
logger.debug("Checking kit integrity...") logger.debug("Checking kit integrity...")
kit_integrity = check_kit_integrity(base_submission) kit_integrity = check_kit_integrity(base_submission)
if kit_integrity != None: if kit_integrity != None:
@@ -317,7 +320,7 @@ class App(QMainWindow):
return return
logger.debug(f"Sending submission: {base_submission.rsl_plate_num} to database.") logger.debug(f"Sending submission: {base_submission.rsl_plate_num} to database.")
result = store_submission(ctx=self.ctx, base_submission=base_submission) result = store_submission(ctx=self.ctx, base_submission=base_submission)
# # check result of storing for issues # check result of storing for issues
if result != None: if result != None:
msg = AlertPop(result['message']) msg = AlertPop(result['message'])
msg.exec() msg.exec()
@@ -345,48 +348,17 @@ class App(QMainWindow):
dlg = AddReagentForm(ctx=self.ctx, reagent_lot=reagent_lot, reagent_type=reagent_type, expiry=expiry) dlg = AddReagentForm(ctx=self.ctx, reagent_lot=reagent_lot, reagent_type=reagent_type, expiry=expiry)
if dlg.exec(): if dlg.exec():
# extract form info # extract form info
labels, values = self.extract_form_info(dlg) info = extract_form_info(dlg)
info = {item[0]:item[1] for item in zip(labels, values)} logger.debug(f"dictionary from form: {info}")
# return None
logger.debug(f"Reagent info: {info}") logger.debug(f"Reagent info: {info}")
# create reagent object # create reagent object
reagent = construct_reagent(ctx=self.ctx, info_dict=info) reagent = construct_reagent(ctx=self.ctx, info_dict=info)
# send reagent to db # send reagent to db
# store_reagent(ctx=self.ctx, reagent=reagent) store_reagent(ctx=self.ctx, reagent=reagent)
return reagent return reagent
def extract_form_info(self, object) -> Tuple[list, list]:
"""
retrieves arbitrary number of labels, values from form
Args:
object (_type_): the form widget
Returns:
_type_: _description_
"""
labels = []
values = []
# grab all widgets in form
prev_item = None
for item in object.layout.parentWidget().findChildren(QWidget):
match item:
case QLabel():
labels.append(item.text().replace(" ", "_").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):
logger.debug(f"Previous: {prev_item}")
logger.debug(f"Item: {item}")
values.append(item.text())
case QComboBox():
values.append(item.currentText())
case QDateEdit():
values.append(item.date().toPyDate())
# value for ad hoc check above
prev_item = item
return labels, values
def generateReport(self): def generateReport(self):
""" """
Action to create a summary of sheet data per client Action to create a summary of sheet data per client
@@ -394,8 +366,10 @@ class App(QMainWindow):
# Custom two date picker for start & end dates # Custom two date picker for start & end dates
dlg = ReportDatePicker() dlg = ReportDatePicker()
if dlg.exec(): if dlg.exec():
labels, values = self.extract_form_info(dlg) # labels, values = extract_form_info(dlg)
info = {item[0]:item[1] for item in zip(labels, values)} # info = {item[0]:item[1] for item in zip(labels, values)}
info = extract_form_info(dlg)
logger.debug(f"Report info: {info}")
# find submissions based on date range # find submissions based on date range
subs = lookup_submissions_by_date_range(ctx=self.ctx, start_date=info['start_date'], end_date=info['end_date']) subs = lookup_submissions_by_date_range(ctx=self.ctx, start_date=info['start_date'], end_date=info['end_date'])
# convert each object to dict # convert each object to dict
@@ -404,7 +378,7 @@ class App(QMainWindow):
html = make_report_html(df=df, start_date=info['start_date'], end_date=info['end_date']) html = make_report_html(df=df, start_date=info['start_date'], end_date=info['end_date'])
# make dataframe from record dictionaries # make dataframe from record dictionaries
# df = make_report_xlsx(records=records) # df = make_report_xlsx(records=records)
# # setup filedialog to handle save location of report # setup filedialog to handle save location of report
home_dir = Path(self.ctx["directory_path"]).joinpath(f"Submissions_Report_{info['start_date']}-{info['end_date']}.pdf").resolve().__str__() home_dir = Path(self.ctx["directory_path"]).joinpath(f"Submissions_Report_{info['start_date']}-{info['end_date']}.pdf").resolve().__str__()
# fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".xlsx")[0]) # fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".xlsx")[0])
fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".pdf")[0]) fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".pdf")[0])

View File

@@ -5,12 +5,11 @@ from PyQt6.QtWidgets import (
QDialogButtonBox, QDateEdit, QSizePolicy, QWidget, QDialogButtonBox, QDateEdit, QSizePolicy, QWidget,
QGridLayout, QPushButton, QSpinBox, QGridLayout, QPushButton, QSpinBox,
QScrollBar, QHBoxLayout, QScrollBar, QHBoxLayout,
QMessageBox
) )
from PyQt6.QtCore import Qt, QDate, QSize from PyQt6.QtCore import Qt, QDate, QSize
# from PyQt6.QtGui import QFontMetrics, QAction # from submissions.backend.db import lookup_regent_by_type_name_and_kit_name
from tools import check_not_nan 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 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 backend.excel.parser import SheetParser
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
import sys import sys
@@ -46,18 +45,19 @@ class AddReagentForm(QDialog):
self.buttonBox = QDialogButtonBox(QBtn) self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept) self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject) self.buttonBox.rejected.connect(self.reject)
# get lot info # widget to get lot info
lot_input = QLineEdit() lot_input = QLineEdit()
lot_input.setObjectName("lot") lot_input.setObjectName("lot")
lot_input.setText(reagent_lot) lot_input.setText(reagent_lot)
# get expiry info # widget to get expiry info
exp_input = QDateEdit(calendarPopup=True) exp_input = QDateEdit(calendarPopup=True)
exp_input.setObjectName('expiry') exp_input.setObjectName('expiry')
# if expiry is not passed in from gui, use today
if expiry == None: if expiry == None:
exp_input.setDate(QDate.currentDate()) exp_input.setDate(QDate.currentDate())
else: else:
exp_input.setDate(expiry) exp_input.setDate(expiry)
# get reagent type info # widget to get reagent type info
type_input = QComboBox() type_input = QComboBox()
type_input.setObjectName('type') type_input.setObjectName('type')
type_input.addItems([item.replace("_", " ").title() for item in get_all_reagenttype_names(ctx=ctx)]) type_input.addItems([item.replace("_", " ").title() for item in get_all_reagenttype_names(ctx=ctx)])
@@ -97,8 +97,10 @@ class ReportDatePicker(QDialog):
self.buttonBox.rejected.connect(self.reject) self.buttonBox.rejected.connect(self.reject)
# widgets to ask for dates # widgets to ask for dates
start_date = QDateEdit(calendarPopup=True) start_date = QDateEdit(calendarPopup=True)
start_date.setObjectName("start_date")
start_date.setDate(QDate.currentDate()) start_date.setDate(QDate.currentDate())
end_date = QDateEdit(calendarPopup=True) end_date = QDateEdit(calendarPopup=True)
end_date.setObjectName("end_date")
end_date.setDate(QDate.currentDate()) end_date.setDate(QDate.currentDate())
self.layout = QVBoxLayout() self.layout = QVBoxLayout()
self.layout.addWidget(QLabel("Start Date")) self.layout.addWidget(QLabel("Start Date"))
@@ -121,28 +123,34 @@ class KitAdder(QWidget):
# insert submit button at top # insert submit button at top
self.submit_btn = QPushButton("Submit") self.submit_btn = QPushButton("Submit")
self.grid.addWidget(self.submit_btn,0,0,1,1) self.grid.addWidget(self.submit_btn,0,0,1,1)
# need to exclude ordinary users to mitigate garbage database entries
# self.grid.addWidget(QLabel("Password:"),1,0)
# self.grid.addWidget(QLineEdit(),1,1)
self.grid.addWidget(QLabel("Kit Name:"),2,0) self.grid.addWidget(QLabel("Kit Name:"),2,0)
self.grid.addWidget(QLineEdit(),2,1) # 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) self.grid.addWidget(QLabel("Used For Sample Type:"),3,0)
# widget to get uses of kit
used_for = QComboBox() used_for = QComboBox()
used_for.setObjectName("used_for")
# Insert all existing sample types # Insert all existing sample types
used_for.addItems(lookup_all_sample_types(ctx=parent_ctx)) used_for.addItems(lookup_all_sample_types(ctx=parent_ctx))
used_for.setEditable(True) used_for.setEditable(True)
self.grid.addWidget(used_for,3,1) self.grid.addWidget(used_for,3,1)
# set cost per run # set cost per run
self.grid.addWidget(QLabel("Constant cost per full plate (plates, work hours, etc.):"),4,0) self.grid.addWidget(QLabel("Constant cost per full plate (plates, work hours, etc.):"),4,0)
cost = QSpinBox() # widget to get constant cost
cost.setMinimum(0) const_cost = QSpinBox()
cost.setMaximum(9999) const_cost.setObjectName("const_cost")
self.grid.addWidget(cost,4,1) 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) self.grid.addWidget(QLabel("Mutable cost per full plate (tips, reagents, etc.):"),5,0)
cost = QSpinBox() # widget to get mutable costs
cost.setMinimum(0) mut_cost = QSpinBox()
cost.setMaximum(9999) mut_cost.setObjectName("mut_cost")
self.grid.addWidget(cost,5,1) mut_cost.setMinimum(0)
mut_cost.setMaximum(9999)
self.grid.addWidget(mut_cost,5,1)
# button to add additional reagent types # button to add additional reagent types
self.add_RT_btn = QPushButton("Add Reagent Type") self.add_RT_btn = QPushButton("Add Reagent Type")
self.grid.addWidget(self.add_RT_btn) self.grid.addWidget(self.add_RT_btn)
@@ -153,8 +161,11 @@ class KitAdder(QWidget):
""" """
insert new reagent type row insert new reagent type row
""" """
# get bottommost row
maxrow = self.grid.rowCount() maxrow = self.grid.rowCount()
self.grid.addWidget(ReagentTypeForm(parent_ctx=self.ctx), maxrow + 1,0,1,2) 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: def submit(self) -> None:
@@ -162,80 +173,65 @@ class KitAdder(QWidget):
send kit to database send kit to database
""" """
# get form info # get form info
labels, values, reagents = self.extract_form_info(self) info, reagents = extract_form_info(self)
info = {item[0]:item[1] for item in zip(labels, values)} logger.debug(f"kit info: {info}")
logger.debug(info)
yml_type = {} yml_type = {}
try: try:
yml_type['password'] = info['password'] yml_type['password'] = info['password']
except KeyError: except KeyError:
pass pass
used = info['used_for_sample_type'].replace(" ", "_").lower() used = info['used_for'].replace(" ", "_").lower()
yml_type[used] = {} yml_type[used] = {}
yml_type[used]['kits'] = {} yml_type[used]['kits'] = {}
yml_type[used]['kits'][info['kit_name']] = {} yml_type[used]['kits'][info['kit_name']] = {}
yml_type[used]['kits'][info['kit_name']]['constant_cost'] = info["Constant cost per full plate (plates, work hours, etc.)"] yml_type[used]['kits'][info['kit_name']]['constant_cost'] = info["const_cost"]
yml_type[used]['kits'][info['kit_name']]['mutable_cost'] = info["Mutable cost per full plate (tips, reagents, etc.)"] yml_type[used]['kits'][info['kit_name']]['mutable_cost'] = info["mut_cost"]
yml_type[used]['kits'][info['kit_name']]['reagenttypes'] = reagents yml_type[used]['kits'][info['kit_name']]['reagenttypes'] = reagents
logger.debug(yml_type) logger.debug(yml_type)
# send to kit constructor # send to kit constructor
result = create_kit_from_yaml(ctx=self.ctx, exp=yml_type) result = create_kit_from_yaml(ctx=self.ctx, exp=yml_type)
# result = create_kit_from_yaml(ctx=self.ctx, exp=exp)
msg = AlertPop(message=result['message'], status=result['status']) msg = AlertPop(message=result['message'], status=result['status'])
# match result['code']:
# case 0:
# msg = AlertPop(message=result['message'], status="information")
# # msg.setText()
# # msg.setInformativeText(result['message'])
# # msg.setWindowTitle("Kit added")
# case 1:
# msg = AlertPop(m)
# msg.setText("Permission Error")
# msg.setInformativeText(result['message'])
# msg.setWindowTitle("Permission Error")
msg.exec() msg.exec()
def extract_form_info(self, object): # def extract_form_info(self, object):
""" # """
retrieves arbitrary number of labels, values from form # 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
# 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): class ReagentTypeForm(QWidget):
""" """
@@ -246,14 +242,17 @@ class ReagentTypeForm(QWidget):
grid = QGridLayout() grid = QGridLayout()
self.setLayout(grid) self.setLayout(grid)
grid.addWidget(QLabel("Name (*Exactly* as it appears in the excel submission form):"),0,0) 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 = QComboBox()
reagent_getter.setObjectName("name")
# lookup all reagent type names from db # lookup all reagent type names from db
reagent_getter.addItems(get_all_reagenttype_names(ctx=parent_ctx)) reagent_getter.addItems(get_all_reagenttype_names(ctx=parent_ctx))
reagent_getter.setEditable(True) reagent_getter.setEditable(True)
grid.addWidget(reagent_getter,0,1) grid.addWidget(reagent_getter,0,1)
grid.addWidget(QLabel("Extension of Life (months):"),0,2) grid.addWidget(QLabel("Extension of Life (months):"),0,2)
# get extension of life # widget toget extension of life
eol = QSpinBox() eol = QSpinBox()
eol.setObjectName('eol')
eol.setMinimum(0) eol.setMinimum(0)
grid.addWidget(eol, 0,3) grid.addWidget(eol, 0,3)
@@ -277,7 +276,6 @@ class ControlsDatePicker(QWidget):
self.layout.addWidget(self.start_date) self.layout.addWidget(self.start_date)
self.layout.addWidget(QLabel("End Date")) self.layout.addWidget(QLabel("End Date"))
self.layout.addWidget(self.end_date) self.layout.addWidget(self.end_date)
self.setLayout(self.layout) self.setLayout(self.layout)
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
@@ -304,15 +302,17 @@ class ImportReagent(QComboBox):
logger.debug(f"Attempting lookup of reagents by type: {query_var}") 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. # 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(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 = [] output_reg = []
for reagent in relevant_reagents: for reagent in relevant_reagents:
# extract strings from any sets.
if isinstance(reagent, set): if isinstance(reagent, set):
for thing in reagent: for thing in reagent:
output_reg.append(thing) output_reg.append(thing)
elif isinstance(reagent, str): elif isinstance(reagent, str):
output_reg.append(reagent) output_reg.append(reagent)
relevant_reagents = output_reg relevant_reagents = output_reg
# if reagent in sheet is not found insert it into items # if reagent in sheet is not found insert it into the front of relevant reagents so it shows
if prsr != None: if prsr != None:
logger.debug(f"Relevant reagents for {prsr.sub[item]}: {relevant_reagents}") 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 str(prsr.sub[item]['lot']) not in relevant_reagents and prsr.sub[item]['lot'] != 'nan':

View File

@@ -1,12 +1,7 @@
# from datetime import date
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QLabel, QVBoxLayout, QDialog, QLabel, QVBoxLayout, QDialog,
QDialogButtonBox, QMessageBox QDialogButtonBox, QMessageBox
) )
# from PyQt6.QtCore import Qt, QDate, QSize
# from PyQt6.QtGui import QFontMetrics, QAction
# from backend.db import get_all_reagenttype_names, lookup_all_sample_types, create_kit_from_yaml
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
import sys import sys
from pathlib import Path from pathlib import Path
@@ -14,6 +9,7 @@ import logging
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
# determine if pyinstaller launcher is being used
if getattr(sys, 'frozen', False): if getattr(sys, 'frozen', False):
loader_path = Path(sys._MEIPASS).joinpath("files", "templates") loader_path = Path(sys._MEIPASS).joinpath("files", "templates")
else: else:
@@ -22,50 +18,6 @@ loader = FileSystemLoader(loader_path)
env = Environment(loader=loader) env = Environment(loader=loader)
# class AddReagentQuestion(QDialog):
# """
# dialog to ask about adding a new reagne to db
# """
# def __init__(self, reagent_type:str, reagent_lot:str) -> QDialog:
# super().__init__()
# self.setWindowTitle(f"Add {reagent_lot}?")
# QBtn = QDialogButtonBox.StandardButton.Yes | QDialogButtonBox.StandardButton.No
# self.buttonBox = QDialogButtonBox(QBtn)
# self.buttonBox.accepted.connect(self.accept)
# self.buttonBox.rejected.connect(self.reject)
# self.layout = QVBoxLayout()
# message = QLabel(f"Couldn't find reagent type {reagent_type.replace('_', ' ').title().strip('Lot')}: {reagent_lot} in the database.\n\nWould you like to add it?")
# self.layout.addWidget(message)
# self.layout.addWidget(self.buttonBox)
# self.setLayout(self.layout)
# class OverwriteSubQuestion(QDialog):
# """
# dialog to ask about overwriting existing submission
# """
# def __init__(self, message:str, rsl_plate_num:str) -> QDialog:
# super().__init__()
# self.setWindowTitle(f"Overwrite {rsl_plate_num}?")
# QBtn = QDialogButtonBox.StandardButton.Yes | QDialogButtonBox.StandardButton.No
# self.buttonBox = QDialogButtonBox(QBtn)
# self.buttonBox.accepted.connect(self.accept)
# self.buttonBox.rejected.connect(self.reject)
# self.layout = QVBoxLayout()
# message = QLabel(message)
# self.layout.addWidget(message)
# self.layout.addWidget(self.buttonBox)
# self.setLayout(self.layout)
class QuestionAsker(QDialog): class QuestionAsker(QDialog):
""" """
dialog to ask yes/no questions dialog to ask yes/no questions
@@ -73,23 +25,27 @@ class QuestionAsker(QDialog):
def __init__(self, title:str, message:str) -> QDialog: def __init__(self, title:str, message:str) -> QDialog:
super().__init__() super().__init__()
self.setWindowTitle(title) self.setWindowTitle(title)
# set yes/no buttons
QBtn = QDialogButtonBox.StandardButton.Yes | QDialogButtonBox.StandardButton.No QBtn = QDialogButtonBox.StandardButton.Yes | QDialogButtonBox.StandardButton.No
self.buttonBox = QDialogButtonBox(QBtn) self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept) self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject) self.buttonBox.rejected.connect(self.reject)
self.layout = QVBoxLayout() self.layout = QVBoxLayout()
# Text for the yes/no question
message = QLabel(message) message = QLabel(message)
self.layout.addWidget(message) self.layout.addWidget(message)
self.layout.addWidget(self.buttonBox) self.layout.addWidget(self.buttonBox)
self.setLayout(self.layout) self.setLayout(self.layout)
class AlertPop(QMessageBox): class AlertPop(QMessageBox):
"""
Dialog to show an alert.
"""
def __init__(self, message:str, status:str) -> QMessageBox: def __init__(self, message:str, status:str) -> QMessageBox:
super().__init__() super().__init__()
# select icon by string
icon = getattr(QMessageBox.Icon, status.title()) icon = getattr(QMessageBox.Icon, status.title())
self.setIcon(icon) self.setIcon(icon)
# msg.setText("Error")
self.setInformativeText(message) self.setInformativeText(message)
self.setWindowTitle(status.title()) self.setWindowTitle(status.title())

View File

@@ -1,20 +1,17 @@
from datetime import date
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QVBoxLayout, QDialog, QTableView, QVBoxLayout, QDialog, QTableView,
QTextEdit, QPushButton, QScrollArea, QTextEdit, QPushButton, QScrollArea,
QMessageBox, QFileDialog, QMenu QMessageBox, QFileDialog, QMenu
) )
from PyQt6.QtCore import Qt, QAbstractTableModel from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel
from PyQt6.QtGui import QFontMetrics, QAction, QCursor from PyQt6.QtGui import QFontMetrics, QAction, QCursor
from backend.db import submissions_to_df, lookup_submission_by_id, delete_submission_by_id from backend.db import submissions_to_df, lookup_submission_by_id, delete_submission_by_id
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
from xhtml2pdf import pisa from xhtml2pdf import pisa
import sys import sys
from pathlib import Path from pathlib import Path
import logging import logging
from .pop_ups import AlertPop, QuestionAsker from .pop_ups import QuestionAsker
from tools import check_is_power_user
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
@@ -45,7 +42,7 @@ class pandasModel(QAbstractTableModel):
""" """
return self._data.shape[0] return self._data.shape[0]
def columnCount(self, parnet=None) -> int: def columnCount(self, parent=None) -> int:
""" """
does what it says does what it says
@@ -85,7 +82,7 @@ class SubmissionsSheet(QTableView):
self.setData() self.setData()
self.resizeColumnsToContents() self.resizeColumnsToContents()
self.resizeRowsToContents() self.resizeRowsToContents()
# self.clicked.connect(self.test) self.setSortingEnabled(True)
self.doubleClicked.connect(self.show_details) self.doubleClicked.connect(self.show_details)
def setData(self) -> None: def setData(self) -> None:
@@ -93,6 +90,8 @@ class SubmissionsSheet(QTableView):
sets data in model sets data in model
""" """
self.data = submissions_to_df(ctx=self.ctx) self.data = submissions_to_df(ctx=self.ctx)
self.data['id'] = self.data['id'].apply(str)
self.data['id'] = self.data['id'].str.zfill(3)
try: try:
del self.data['samples'] del self.data['samples']
except KeyError: except KeyError:
@@ -101,8 +100,11 @@ class SubmissionsSheet(QTableView):
del self.data['reagents'] del self.data['reagents']
except KeyError: except KeyError:
pass pass
self.model = pandasModel(self.data) proxyModel = QSortFilterProxyModel()
self.setModel(self.model) proxyModel.setSourceModel(pandasModel(self.data))
# self.model = pandasModel(self.data)
# self.setModel(self.model)
self.setModel(proxyModel)
# self.resize(800,600) # self.resize(800,600)
def show_details(self) -> None: def show_details(self) -> None:
@@ -110,22 +112,22 @@ class SubmissionsSheet(QTableView):
creates detailed data to show in seperate window creates detailed data to show in seperate window
""" """
index = (self.selectionModel().currentIndex()) index = (self.selectionModel().currentIndex())
# logger.debug(index)
value = index.sibling(index.row(),0).data() value = index.sibling(index.row(),0).data()
dlg = SubmissionDetails(ctx=self.ctx, id=value) dlg = SubmissionDetails(ctx=self.ctx, id=value)
# dlg.show()
if dlg.exec(): if dlg.exec():
pass pass
def contextMenuEvent(self, event): def contextMenuEvent(self, event):
"""
Creates actions for right click menu events.
Args:
event (_type_): the item of interest
"""
self.menu = QMenu(self) self.menu = QMenu(self)
renameAction = QAction('Delete', self) renameAction = QAction('Delete', self)
detailsAction = QAction('Details', self) detailsAction = QAction('Details', self)
# Originally I intended to limit deletions to power users.
# renameAction.setEnabled(False)
# if check_is_power_user(ctx=self.ctx):
# renameAction.setEnabled(True)
renameAction.triggered.connect(lambda: self.delete_item(event)) renameAction.triggered.connect(lambda: self.delete_item(event))
detailsAction.triggered.connect(lambda: self.show_details()) detailsAction.triggered.connect(lambda: self.show_details())
self.menu.addAction(detailsAction) self.menu.addAction(detailsAction)
@@ -164,12 +166,8 @@ class SubmissionDetails(QDialog):
# get submision from db # get submision from db
data = lookup_submission_by_id(ctx=ctx, id=id) data = lookup_submission_by_id(ctx=ctx, id=id)
self.base_dict = data.to_dict() self.base_dict = data.to_dict()
# logger.debug(f"Base dict: {self.base_dict}")
# don't want id # don't want id
del self.base_dict['id'] del self.base_dict['id']
# convert sub objects to dicts
# self.base_dict['reagents'] = [item.to_sub_dict() for item in data.reagents]
# self.base_dict['samples'] = [item.to_sub_dict() for item in data.samples]
# retrieve jinja template # retrieve jinja template
template = env.get_template("submission_details.txt") template = env.get_template("submission_details.txt")
# render using object dict # render using object dict
@@ -192,24 +190,18 @@ class SubmissionDetails(QDialog):
interior.setWidget(txt_editor) interior.setWidget(txt_editor)
self.layout = QVBoxLayout() self.layout = QVBoxLayout()
self.setFixedSize(w, 900) self.setFixedSize(w, 900)
# button to export a pdf version
btn = QPushButton("Export PDF") btn = QPushButton("Export PDF")
btn.setParent(self) btn.setParent(self)
btn.setFixedWidth(w) btn.setFixedWidth(w)
btn.clicked.connect(self.export) btn.clicked.connect(self.export)
# def _create_actions(self):
# self.exportAction = QAction("Export", self)
def export(self): def export(self):
template = env.get_template("submission_details.html") template = env.get_template("submission_details.html")
html = template.render(sub=self.base_dict) html = template.render(sub=self.base_dict)
# logger.debug(f"Submission details: {self.base_dict}")
home_dir = Path(self.ctx["directory_path"]).joinpath(f"Submission_Details_{self.base_dict['Plate Number']}.pdf").resolve().__str__() home_dir = Path(self.ctx["directory_path"]).joinpath(f"Submission_Details_{self.base_dict['Plate Number']}.pdf").resolve().__str__()
fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".pdf")[0]) fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".pdf")[0])
# logger.debug(f"report output name: {fname}")
# df.to_excel(fname, engine='openpyxl')
if fname.__str__() == ".": if fname.__str__() == ".":
logger.debug("Saving pdf was cancelled.") logger.debug("Saving pdf was cancelled.")
return return
@@ -223,4 +215,3 @@ class SubmissionDetails(QDialog):
msg.setInformativeText(f"Looks like {fname.__str__()} is open.\nPlease close it and try again.") msg.setInformativeText(f"Looks like {fname.__str__()} is open.\nPlease close it and try again.")
msg.setWindowTitle("Permission Error") msg.setWindowTitle("Permission Error")
msg.exec() msg.exec()

View File

@@ -2,7 +2,6 @@
from backend.db.models import * from backend.db.models import *
import logging import logging
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 check_kit_integrity(sub:BasicSubmission|KitType, reagenttypes:list|None=None) -> dict|None:
@@ -21,6 +20,7 @@ def check_kit_integrity(sub:BasicSubmission|KitType, reagenttypes:list|None=None
match sub: match sub:
case BasicSubmission(): case BasicSubmission():
ext_kit_rtypes = [reagenttype.name for reagenttype in sub.extraction_kit.reagent_types] ext_kit_rtypes = [reagenttype.name for reagenttype in sub.extraction_kit.reagent_types]
# Overwrite function parameter reagenttypes
reagenttypes = [reagent.type.name for reagent in sub.reagents] reagenttypes = [reagent.type.name for reagent in sub.reagents]
case KitType(): case KitType():
ext_kit_rtypes = [reagenttype.name for reagenttype in sub.reagent_types] ext_kit_rtypes = [reagenttype.name for reagenttype in sub.reagent_types]
@@ -30,13 +30,14 @@ def check_kit_integrity(sub:BasicSubmission|KitType, reagenttypes:list|None=None
check = set(ext_kit_rtypes) == set(reagenttypes) check = set(ext_kit_rtypes) == set(reagenttypes)
logger.debug(f"Checking if reagents match kit contents: {check}") logger.debug(f"Checking if reagents match kit contents: {check}")
# what reagent types are in both lists? # what reagent types are in both lists?
common = list(set(ext_kit_rtypes).intersection(reagenttypes)) # common = list(set(ext_kit_rtypes).intersection(reagenttypes))
logger.debug(f"common reagents types: {common}") missing = list(set(ext_kit_rtypes).difference(reagenttypes))
logger.debug(f"Missing reagents types: {missing}")
# if lists are equal return no problem # if lists are equal return no problem
if check: if len(missing)==0:
result = None result = None
else: else:
missing = [x for x in ext_kit_rtypes if x not in common] # missing = [x for x in ext_kit_rtypes if x not in common]
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} 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}
return result return result

View File

@@ -25,7 +25,6 @@ def create_charts(ctx:dict, df:pd.DataFrame, ytitle:str|None=None) -> Figure:
genera = [] genera = []
if df.empty: if df.empty:
return None return None
for item in df['genus'].to_list(): for item in df['genus'].to_list():
try: try:
if item[-1] == "*": if item[-1] == "*":
@@ -95,7 +94,6 @@ def generic_figure_markers(fig:Figure, modes:list=[], ytitle:str|None=None) -> F
]) ])
) )
) )
# logger.debug(f"Returning figure {fig}")
assert type(fig) == Figure assert type(fig) == Figure
return fig return fig
@@ -148,8 +146,7 @@ def output_figures(settings:dict, figs:list, group_name:str):
except AttributeError: except AttributeError:
logger.error(f"The following figure was a string: {fig}") logger.error(f"The following figure was a string: {fig}")
# Below are the individual construction functions. They must be named "construct_{mode}_chart" and
# take only json_in and mode to hook into the main processor.
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() fig = Figure()
@@ -186,14 +183,15 @@ def construct_chart(ctx:dict, df:pd.DataFrame, modes:list, ytitle:str|None=None)
# sys.exit(f"number of traces={len(fig.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
# take only json_in and mode to hook into the main processor.
def construct_refseq_chart(settings:dict, df:pd.DataFrame, group_name:str, mode:str) -> Figure: def construct_refseq_chart(settings:dict, df:pd.DataFrame, group_name:str, mode:str) -> Figure:
""" """
Constructs intial refseq chart for both contains and matches (depreciated). Constructs intial refseq chart for both contains and matches (depreciated).
Args: Args:
settings (dict): settings passed down from click. settings (dict): settings passed down from gui.
df (pd.DataFrame): dataframe containing all sample data for the group. df (pd.DataFrame): dataframe containing all sample data for the group.
group_name (str): name of the group being processed. group_name (str): name of the group being processed.
mode (str): contains or matches, overwritten by hardcoding, so don't think about it too hard. mode (str): contains or matches, overwritten by hardcoding, so don't think about it too hard.

View File

@@ -1,6 +1,14 @@
import numpy as np import numpy as np
import logging import logging
import getpass import getpass
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__}")
@@ -27,3 +35,45 @@ def check_is_power_user(ctx:dict) -> bool:
def create_reagent_list(in_dict:dict) -> list[str]: 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:
"""
retrieves object names and values from form
Args:
object (_type_): the form widget
Returns:
dict: dictionary of objectName:text items
"""
from frontend.custom_widgets import ReagentTypeForm
dicto = {}
reagents = {}
logger.debug(f"Object type: {type(object)}")
# grab all widgets in form
try:
all_children = object.layout.parentWidget().findChildren(QWidget)
except AttributeError:
all_children = object.layout().parentWidget().findChildren(QWidget)
for item in all_children:
logger.debug(f"Looking at: {item.objectName()}")
match item:
case QLineEdit():
dicto[item.objectName()] = item.text()
case QComboBox():
dicto[item.objectName()] = item.currentText()
case QDateEdit():
dicto[item.objectName()] = item.date().toPyDate()
case QSpinBox():
dicto[item.objectName()] = item.value()
case ReagentTypeForm():
reagent = 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"].strip()] = {'eol_ext':int(reagent['eol'])}
# value for ad hoc check above
if reagents != {}:
return dicto, reagents
return dicto