diff --git a/alembic.ini b/alembic.ini index fb9aaf6..86ad429 100644 --- a/alembic.ini +++ b/alembic.ini @@ -55,8 +55,8 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne # are written from script.py.mako # output_encoding = utf-8 -sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db -; sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\Archives\DB_backups\submissions-20230130.db +; sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db +sqlalchemy.url = sqlite:///C:\Users\lwark\Documents\Archives\DB_backups\submissions-20230213.db [post_write_hooks] diff --git a/alembic/env.py b/alembic/env.py index 2a2c78f..d7ea4c6 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -7,6 +7,7 @@ from alembic import context import sys from pathlib import Path sys.path.append(Path(__file__).parents[1].joinpath("src").resolve().__str__()) +sys.path.append(Path(__file__).parents[1].joinpath("src", "submissions").resolve().__str__()) print(sys.path) # this is the Alembic Config object, which provides diff --git a/src/submissions/__init__.py b/src/submissions/__init__.py index 373711d..13844c3 100644 --- a/src/submissions/__init__.py +++ b/src/submissions/__init__.py @@ -1,4 +1,4 @@ # __init__.py # Version of the realpython-reader package -__version__ = "1.2.3" +__version__ = "1.3.0" diff --git a/src/submissions/backend/db/__init__.py b/src/submissions/backend/db/__init__.py index 48d062b..bd6a8b6 100644 --- a/src/submissions/backend/db/__init__.py +++ b/src/submissions/backend/db/__init__.py @@ -15,6 +15,7 @@ import json # from dateutil.relativedelta import relativedelta from getpass import getuser import numpy as np +from tools import check_not_nan logger = logging.getLogger(f"submissions.{__name__}") @@ -101,8 +102,13 @@ def construct_submission_info(ctx:dict, info_dict:dict) -> models.BasicSubmissio # convert submission type into model name query = info_dict['submission_type'].replace(" ", "") # check database for existing object + 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 + msg = "A proper RSL plate number is required." + return instance, {'code': 2, 'message': "A proper RSL plate number is required."} instance = ctx['database_session'].query(models.BasicSubmission).filter(models.BasicSubmission.rsl_plate_num==info_dict['rsl_plate_num']).first() - msg = "This submission already exists.\nWould you like to overwrite?" + # get model based on submission type converted above logger.debug(f"Looking at models for submission type: {query}") model = getattr(models, query) @@ -113,6 +119,10 @@ def construct_submission_info(ctx:dict, info_dict:dict) -> models.BasicSubmissio instance = model() logger.debug(f"Submission doesn't exist yet, creating new instance: {instance}") msg = None + code =0 + else: + code = 1 + msg = "This submission already exists.\nWould you like to overwrite?" for item in info_dict: logger.debug(f"Setting {item} to {info_dict[item]}") # set fields based on keys in dictionary @@ -151,9 +161,14 @@ def construct_submission_info(ctx:dict, info_dict:dict) -> models.BasicSubmissio except (TypeError, AttributeError): logger.debug(f"Looks like that kit doesn't have cost breakdown yet, using full plate cost.") instance.run_cost = instance.extraction_kit.cost_per_run - logger.debug(f"Constructed instance: {instance.to_string()}") + # We need to make sure there's a proper rsl plate number + + try: + logger.debug(f"Constructed instance: {instance.to_string()}") + except AttributeError as e: + logger.debug(f"Something went wrong constructing instance {info_dict['rsl_plate_num']}: {e}") logger.debug(msg) - return instance, {'message':msg} + return instance, {'code':code, 'message':msg} def construct_reagent(ctx:dict, info_dict:dict) -> models.Reagent: @@ -244,7 +259,7 @@ def lookup_kittype_by_use(ctx:dict, used_by:str) -> list[models.KitType]: Returns: list[models.KitType]: list of kittypes that have that sample type in their uses """ - return ctx['database_session'].query(models.KitType).filter(models.KitType.used_for.contains(used_by)) + return ctx['database_session'].query(models.KitType).filter(models.KitType.used_for.contains(used_by)).all() def lookup_kittype_by_name(ctx:dict, name:str) -> models.KitType: """ @@ -278,7 +293,7 @@ def lookup_regent_by_type_name(ctx:dict, type_name:str) -> list[models.Reagent]: def lookup_regent_by_type_name_and_kit_name(ctx:dict, type_name:str, kit_name:str) -> list[models.Reagent]: """ - Lookup reagents by their type name and kits they belong to (Broken) + Lookup reagents by their type name and kits they belong to (Broken... maybe cursed, I'm not sure.) Args: ctx (dict): settings pass by gui @@ -292,10 +307,6 @@ 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. # by_type = ctx['database_session'].query(models.Reagent).join(models.Reagent.type, aliased=True).filter(models.ReagentType.name.endswith(type_name)).all() rt_types = ctx['database_session'].query(models.ReagentType).filter(models.ReagentType.name.endswith(type_name)) - - - - # add filter for kit name... which I can not get to work. # add_in = by_type.join(models.ReagentType.kits).filter(models.KitType.name==kit_name) try: @@ -315,7 +326,7 @@ def lookup_regent_by_type_name_and_kit_name(ctx:dict, type_name:str, kit_name:st return output -def lookup_all_submissions_by_type(ctx:dict, type:str|None=None) -> list[models.BasicSubmission]: +def lookup_all_submissions_by_type(ctx:dict, sub_type:str|None=None) -> list[models.BasicSubmission]: """ Get all submissions, filtering by type if given @@ -326,10 +337,10 @@ def lookup_all_submissions_by_type(ctx:dict, type:str|None=None) -> list[models. Returns: _type_: list of retrieved submissions """ - if type == None: + if sub_type == None: subs = ctx['database_session'].query(models.BasicSubmission).all() else: - subs = ctx['database_session'].query(models.BasicSubmission).filter(models.BasicSubmission.submission_type==type.lower().replace(" ", "_")).all() + subs = ctx['database_session'].query(models.BasicSubmission).filter(models.BasicSubmission.submission_type==sub_type.lower().replace(" ", "_")).all() return subs def lookup_all_orgs(ctx:dict) -> list[models.Organization]: @@ -358,7 +369,7 @@ def lookup_org_by_name(ctx:dict, name:str|None) -> models.Organization: logger.debug(f"Querying organization: {name}") return ctx['database_session'].query(models.Organization).filter(models.Organization.name==name).first() -def submissions_to_df(ctx:dict, type:str|None=None) -> pd.DataFrame: +def submissions_to_df(ctx:dict, sub_type:str|None=None) -> pd.DataFrame: """ Convert submissions looked up by type to dataframe @@ -369,9 +380,9 @@ def submissions_to_df(ctx:dict, type:str|None=None) -> pd.DataFrame: Returns: pd.DataFrame: dataframe constructed from retrieved submissions """ - logger.debug(f"Type: {type}") + logger.debug(f"Type: {sub_type}") # pass to lookup function - subs = [item.to_dict() for item in lookup_all_submissions_by_type(ctx=ctx, type=type)] + subs = [item.to_dict() for item in lookup_all_submissions_by_type(ctx=ctx, sub_type=sub_type)] df = pd.DataFrame.from_records(subs) # logger.debug(f"Pre: {df['Technician']}") try: @@ -435,14 +446,17 @@ def get_all_Control_Types_names(ctx:dict) -> list[models.ControlType]: return conTypes -def create_kit_from_yaml(ctx:dict, exp:dict) -> None: +def create_kit_from_yaml(ctx:dict, exp:dict) -> dict: """ Create and store a new kit in the database based on a .yml file Args: ctx (dict): Context dictionary passed down from frontend exp (dict): Experiment dictionary created from yaml file - """ + + Returns: + dict: a dictionary containing results of db addition + """ try: power_users = ctx['power_users'] except KeyError: @@ -474,6 +488,44 @@ def create_kit_from_yaml(ctx:dict, exp:dict) -> None: ctx['database_session'].commit() return {'code':0, 'message':'Kit has been added'} +def create_org_from_yaml(ctx:dict, org:dict) -> dict: + """ + Create and store a new organization based on a .yml file + + Args: + ctx (dict): Context dictionary passed down from frontend + org (dict): Dictionary containing organization info. + + Returns: + dict: dictionary containing results of db addition + """ + try: + 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: + logger.debug(f"{getuser()} does not have permission to add kits.") + return {'code':1, 'message':"This user does not have permission to add organizations."} + for client in org: + cli_org = models.Organization(name=client.replace(" ", "_").lower(), cost_centre=org[client]['cost centre']) + for contact in org[client]['contacts']: + cont_name = list(contact.keys())[0] + look_up = ctx['database_session'].query(models.Contact).filter(models.Contact.name==cont_name).first() + if look_up == None: + cli_cont = models.Contact(name=cont_name, phone=contact[cont_name]['phone'], email=contact[cont_name]['email'], organization=[cli_org]) + else: + cli_cont = look_up + cli_cont.organization.append(cli_org) + # cli_org.contacts.append(cli_cont) + # cli_org.contact_ids.append_foreign_key(cli_cont.id) + ctx['database_session'].add(cli_cont) + logger.debug(cli_cont.__dict__) + ctx['database_session'].add(cli_org) + ctx["database_session"].commit() + return {"code":0, "message":"Organization has been added."} + def lookup_all_sample_types(ctx:dict) -> list[str]: """ @@ -511,7 +563,7 @@ def get_all_available_modes(ctx:dict) -> list[str]: -def get_all_controls_by_type(ctx:dict, con_type:str, start_date:date|None=None, end_date:date|None=None) -> list: +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. @@ -571,4 +623,4 @@ def lookup_submission_by_rsl_num(ctx:dict, rsl_num:str): def lookup_submissions_using_reagent(ctx:dict, reagent:models.Reagent) -> list[models.BasicSubmission]: - return ctx['database_session'].query(models.BasicSubmission).join(reagents_submissions).filter(reagents_submissions.c.reagent_id==reagent.id) \ No newline at end of file + return ctx['database_session'].query(models.BasicSubmission).join(reagents_submissions).filter(reagents_submissions.c.reagent_id==reagent.id).all() \ No newline at end of file diff --git a/src/submissions/backend/db/functions/__init__.py b/src/submissions/backend/db/functions/__init__.py index 4b755c2..ad23b3a 100644 --- a/src/submissions/backend/db/functions/__init__.py +++ b/src/submissions/backend/db/functions/__init__.py @@ -1,21 +1,21 @@ -from ..models import * -import logging +# from ..models import * +# import logging -logger = logging.getLogger(f"submissions.{__name__}") +# logger = logging.getLogger(f"submissions.{__name__}") -def check_kit_integrity(sub:BasicSubmission): - ext_kit_rtypes = [reagenttype.name for reagenttype in sub.extraction_kit.reagent_types] - logger.debug(f"Kit reagents: {ext_kit_rtypes}") - reagenttypes = [reagent.type.name for reagent in sub.reagents] - logger.debug(f"Submission reagents: {reagenttypes}") - check = set(ext_kit_rtypes) == set(reagenttypes) - logger.debug(f"Checking if reagents match kit contents: {check}") - common = list(set(ext_kit_rtypes).intersection(reagenttypes)) - logger.debug(f"common reagents types: {common}") - if check: - result = None - else: - result = {'message' : f"Couldn't verify reagents match listed kit components.\n\nIt looks like you are missing: {[x.upper for x in ext_kit_rtypes if x not in common]}\n\nAlternatively, you may have set the wrong extraction kit."} - return result +# def check_kit_integrity(sub:BasicSubmission): +# ext_kit_rtypes = [reagenttype.name for reagenttype in sub.extraction_kit.reagent_types] +# logger.debug(f"Kit reagents: {ext_kit_rtypes}") +# reagenttypes = [reagent.type.name for reagent in sub.reagents] +# logger.debug(f"Submission reagents: {reagenttypes}") +# check = set(ext_kit_rtypes) == set(reagenttypes) +# logger.debug(f"Checking if reagents match kit contents: {check}") +# common = list(set(ext_kit_rtypes).intersection(reagenttypes)) +# logger.debug(f"common reagents types: {common}") +# if check: +# result = None +# else: +# result = {'message' : f"Couldn't verify reagents match listed kit components.\n\nIt looks like you are missing: {[x.upper for x in ext_kit_rtypes if x not in common]}\n\nAlternatively, you may have set the wrong extraction kit."} +# return result diff --git a/src/submissions/backend/db/models/organizations.py b/src/submissions/backend/db/models/organizations.py index b6b5525..9fd3140 100644 --- a/src/submissions/backend/db/models/organizations.py +++ b/src/submissions/backend/db/models/organizations.py @@ -36,7 +36,7 @@ class Contact(Base): """ __tablename__ = "_contacts" - id = id = Column(INTEGER, primary_key=True) #: primary key + id = Column(INTEGER, primary_key=True) #: primary key name = Column(String(64)) #: contact name email = Column(String(64)) #: contact email phone = Column(String(32)) #: contact phone number diff --git a/src/submissions/backend/db/models/samples.py b/src/submissions/backend/db/models/samples.py index 87aa693..cb78e1e 100644 --- a/src/submissions/backend/db/models/samples.py +++ b/src/submissions/backend/db/models/samples.py @@ -1,5 +1,5 @@ from . import Base -from sqlalchemy import Column, String, TIMESTAMP, text, JSON, INTEGER, ForeignKey, FLOAT, BOOLEAN +from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, FLOAT, BOOLEAN from sqlalchemy.orm import relationship diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index 4da816e..0daf05d 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -17,7 +17,7 @@ class BasicSubmission(Base): __tablename__ = "_submissions" id = Column(INTEGER, primary_key=True) #: primary key - rsl_plate_num = Column(String(32), unique=True) #: RSL name (e.g. RSL-22-0012) + rsl_plate_num = Column(String(32), unique=True, nullable=False) #: RSL name (e.g. RSL-22-0012) submitter_plate_num = Column(String(127), unique=True) #: The number given to the submission by the submitting lab submitted_date = Column(TIMESTAMP) #: Date submission received submitting_lab = relationship("Organization", back_populates="submissions") #: client org diff --git a/src/submissions/backend/excel/parser.py b/src/submissions/backend/excel/parser.py index a3d1148..bc1bded 100644 --- a/src/submissions/backend/excel/parser.py +++ b/src/submissions/backend/excel/parser.py @@ -1,13 +1,13 @@ import pandas as pd from pathlib import Path -from backend.db.models.samples import WWSample, BCSample +from backend.db.models import WWSample, BCSample import logging from collections import OrderedDict import re import numpy as np from datetime import date import uuid -from frontend.functions import check_not_nan +from tools import check_not_nan logger = logging.getLogger(f"submissions.{__name__}") @@ -15,7 +15,7 @@ class SheetParser(object): """ object to pull and contain data from excel file """ - def __init__(self, filepath:Path|None = None, **kwargs) -> None: + def __init__(self, filepath:Path|None = None, **kwargs): """ Args: filepath (Path | None, optional): file path to excel sheet. Defaults to None. @@ -77,12 +77,12 @@ class SheetParser(object): """ submission_info = self.xl.parse(sheet_name=sheet_name, dtype=object) - self.sub['submitter_plate_num'] = submission_info.iloc[0][1] #if pd.isnull(submission_info.iloc[0][1]) else string_formatter(submission_info.iloc[0][1]) - self.sub['rsl_plate_num'] = submission_info.iloc[10][1] #if pd.isnull(submission_info.iloc[10][1]) else string_formatter(submission_info.iloc[10][1]) - self.sub['submitted_date'] = submission_info.iloc[1][1] #if pd.isnull(submission_info.iloc[1][1]) else submission_info.iloc[1][1].date()#.strftime("%Y-%m-%d") - self.sub['submitting_lab'] = submission_info.iloc[0][3] #if pd.isnull(submission_info.iloc[0][3]) else string_formatter(submission_info.iloc[0][3]) - self.sub['sample_count'] = submission_info.iloc[2][3] #if pd.isnull(submission_info.iloc[2][3]) else string_formatter(submission_info.iloc[2][3]) - self.sub['extraction_kit'] = submission_info.iloc[3][3] #if #pd.isnull(submission_info.iloc[3][3]) else string_formatter(submission_info.iloc[3][3]) + self.sub['submitter_plate_num'] = submission_info.iloc[0][1] + self.sub['rsl_plate_num'] = submission_info.iloc[10][1] + self.sub['submitted_date'] = submission_info.iloc[1][1] + self.sub['submitting_lab'] = submission_info.iloc[0][3] + self.sub['sample_count'] = submission_info.iloc[2][3] + self.sub['extraction_kit'] = submission_info.iloc[3][3] return submission_info @@ -93,6 +93,12 @@ class SheetParser(object): """ def _parse_reagents(df:pd.DataFrame) -> None: + """ + Pulls reagents from the bacterial sub-dataframe + + Args: + df (pd.DataFrame): input sub dataframe + """ for ii, row in df.iterrows(): # skip positive control if ii == 11: @@ -119,16 +125,15 @@ class SheetParser(object): # 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]}") - try: - check = not np.isnan(row[3]) - except TypeError: - check = True - if check: + # try: + # check = not np.isnan(row[3]) + # except TypeError: + # check = True + if check_not_nan(row[3]): expiry = row[3].date() else: expiry = date.today() self.sub[f"lot_{reagent_type}"] = {'lot':output_var, 'exp':expiry} - submission_info = self._parse_generic("Sample List") # iloc is [row][column] and the first row is set as header row so -2 tech = str(submission_info.iloc[11][1]) @@ -160,9 +165,6 @@ class SheetParser(object): logger.debug(f"Parser result: {self.sub}") self.sub['samples'] = sample_parse() - - - def _parse_wastewater(self) -> None: """ @@ -170,23 +172,32 @@ class SheetParser(object): """ def _parse_reagents(df:pd.DataFrame) -> None: - logger.debug(df) + """ + Pulls reagents from the bacterial sub-dataframe + + Args: + df (pd.DataFrame): input sub dataframe + """ + # logger.debug(df) 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: + # try: + # check = not np.isnan(row[5]) + # except TypeError: + # check = True + if not isinstance(row[5], float) and check_not_nan(row[5]): # must be prefixed with 'lot_' to be recognized by gui + # regex below will remove 80% from 80% ethanol in the Wastewater kit. output_key = re.sub(r"\d{1,3}%", "", row[0].lower().strip().replace(' ', '_')) try: output_var = row[5].upper() except AttributeError: - logger.debug(f"Couldn't upperize {row[2]}, must be a number") + logger.debug(f"Couldn't upperize {row[5]}, must be a number") output_var = row[5] - self.sub[f"lot_{output_key}"] = output_var - - # submission_info = self.xl.parse("WW Submissions (ENTER HERE)") + if check_not_nan(row[7]): + expiry = row[7].date() + else: + expiry = date.today() + self.sub[f"lot_{output_key}"] = {'lot':output_var, 'exp':expiry} submission_info = self._parse_generic("WW Submissions (ENTER HERE)") enrichment_info = self.xl.parse("Enrichment Worksheet", dtype=object) enr_reagent_range = enrichment_info.iloc[0:4, 9:20] @@ -214,17 +225,11 @@ class SheetParser(object): # self.sub['lot_pre_mix_2'] = qprc_info.iloc[2][14] #if pd.isnull(qprc_info.iloc[2][14]) else string_formatter(qprc_info.iloc[2][14]) # self.sub['lot_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]) - # gt individual sample info + # get individual sample info sample_parser = SampleParser(submission_info.iloc[16:40]) sample_parse = getattr(sample_parser, f"parse_{self.sub['submission_type'].lower()}_samples") self.sub['samples'] = sample_parse() - - - - - - class SampleParser(object): """ @@ -241,7 +246,8 @@ class SampleParser(object): Returns: list[BCSample]: list of sample objects - """ + """ + # logger.debug(f"Samples: {self.samples}") new_list = [] for sample in self.samples: new = BCSample() @@ -297,13 +303,3 @@ class SampleParser(object): new.well_number = sample['Unnamed: 1'] new_list.append(new) return new_list - - -# def string_formatter(input): -# logger.debug(f"{input} : {type(input)}") -# match input: -# case int() | float() | np.float64: -# return "{:0.0f}".format(input) -# case _: -# return input - \ No newline at end of file diff --git a/src/submissions/backend/excel/reports.py b/src/submissions/backend/excel/reports.py index 8ba6220..0ffd4a6 100644 --- a/src/submissions/backend/excel/reports.py +++ b/src/submissions/backend/excel/reports.py @@ -10,6 +10,7 @@ from pathlib import Path logger = logging.getLogger(f"submissions.{__name__}") +# set path of templates depending on pyinstaller/raw python if getattr(sys, 'frozen', False): loader_path = Path(sys._MEIPASS).joinpath("files", "templates") else: diff --git a/src/submissions/frontend/__init__.py b/src/submissions/frontend/__init__.py index 81654ab..a565fe9 100644 --- a/src/submissions/frontend/__init__.py +++ b/src/submissions/frontend/__init__.py @@ -19,21 +19,23 @@ from xhtml2pdf import pisa # import plotly.express as px import yaml import pprint -import numpy as np from backend.excel.parser import SheetParser from backend.excel.reports import convert_control_by_mode, convert_data_list_to_df from backend.db import (construct_submission_info, lookup_reagent, construct_reagent, store_reagent, store_submission, lookup_kittype_by_use, 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_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 ) -from .functions import check_kit_integrity, check_not_nan + +from .functions import check_kit_integrity +from tools import check_not_nan 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.pop_ups import AddReagentQuestion, OverwriteSubQuestion, AlertPop +from frontend.custom_widgets.pop_ups import AlertPop, QuestionAsker from frontend.custom_widgets import AddReagentForm, ReportDatePicker, KitAdder, ControlsDatePicker import logging import difflib @@ -96,6 +98,7 @@ class App(QMainWindow): self.addToolBar(toolbar) toolbar.addAction(self.addReagentAction) toolbar.addAction(self.addKitAction) + toolbar.addAction(self.addOrgAction) def _createActions(self): """ @@ -105,6 +108,7 @@ class App(QMainWindow): self.addReagentAction = QAction("Add Reagent", self) self.generateReportAction = QAction("Make Report", self) self.addKitAction = QAction("Add Kit", self) + self.addOrgAction = QAction("Add Org", self) self.joinControlsAction = QAction("Link Controls") self.joinExtractionAction = QAction("Link Ext Logs") @@ -117,6 +121,7 @@ class App(QMainWindow): self.addReagentAction.triggered.connect(self.add_reagent) self.generateReportAction.triggered.connect(self.generateReport) self.addKitAction.triggered.connect(self.add_kit) + self.addOrgAction.triggered.connect(self.add_org) self.table_widget.control_typer.currentIndexChanged.connect(self._controls_getter) self.table_widget.mode_typer.currentIndexChanged.connect(self._controls_getter) self.table_widget.datepicker.start_date.dateChanged.connect(self._controls_getter) @@ -183,7 +188,7 @@ class App(QMainWindow): # create label self.table_widget.formlayout.addWidget(QLabel(item.replace("_", " ").title())) # if extraction kit not available, all other values fail - if np.isnan(prsr.sub[item]): + if not check_not_nan(prsr.sub[item]): msg = AlertPop(message="Make sure to check your extraction kit!", status="warning") msg.exec() # create combobox to hold looked up kits @@ -231,7 +236,7 @@ class App(QMainWindow): elif isinstance(reagent, str): output_reg.append(reagent) relevant_reagents = output_reg - logger.debug(f"Relevant reagents: {relevant_reagents}") + logger.debug(f"Relevant reagents for {prsr.sub[item]}: {relevant_reagents}") # if reagent in sheet is not found insert it into items if str(prsr.sub[item]['lot']) not in relevant_reagents and prsr.sub[item]['lot'] != 'nan': if check_not_nan(prsr.sub[item]['lot']): @@ -272,11 +277,13 @@ class App(QMainWindow): logger.debug(f"Looked up reagent: {wanted_reagent}") # if reagent not found offer to add to database if wanted_reagent == None: - dlg = AddReagentQuestion(reagent_type=reagent, reagent_lot=reagents[reagent]) + # dlg = AddReagentQuestion(reagent_type=reagent, reagent_lot=reagents[reagent]) + r_lot = reagents[reagent] + dlg = QuestionAsker(title=f"Add {r_lot}?", message=f"Couldn't find reagent type {reagent.replace('_', ' ').title().strip('Lot')}: {r_lot} in the database.\n\nWould you like to add it?") if dlg.exec(): logger.debug(f"checking reagent: {reagent} in self.reagents. Result: {self.reagents[reagent]}") expiry_date = self.reagents[reagent]['exp'] - wanted_reagent = self.add_reagent(reagent_lot=reagents[reagent], reagent_type=reagent.replace("lot_", ""), expiry=expiry_date) + wanted_reagent = self.add_reagent(reagent_lot=r_lot, reagent_type=reagent.replace("lot_", ""), expiry=expiry_date) else: logger.debug("Will not add reagent.") if wanted_reagent != None: @@ -287,14 +294,23 @@ class App(QMainWindow): info['uploaded_by'] = getuser() # construct submission object logger.debug(f"Here is the info_dict: {pprint.pformat(info)}") - base_submission, output = construct_submission_info(ctx=self.ctx, info_dict=info) + base_submission, result = construct_submission_info(ctx=self.ctx, info_dict=info) # check output message for issues - if output['message'] != None: - dlg = OverwriteSubQuestion(output['message'], base_submission.rsl_plate_num) - if dlg.exec(): - base_submission.reagents = [] - else: + match result['code']: + case 1: + # if output['code'] > 0: + # dlg = OverwriteSubQuestion(output['message'], base_submission.rsl_plate_num) + dlg = QuestionAsker(title=f"Review {base_submission.rsl_plate_num}?", message=result['message']) + if dlg.exec(): + base_submission.reagents = [] + else: + return + case 2: + dlg = AlertPop(message=result['message'], status='critical') + dlg.exec() return + case _: + pass # add reagents to submission object for reagent in parsed_reagents: base_submission.reagents.append(reagent) @@ -447,20 +463,59 @@ class App(QMainWindow): return # send to kit creator function result = create_kit_from_yaml(ctx=self.ctx, exp=exp) - msg = QMessageBox() + # msg = QMessageBox() # msg.setIcon(QMessageBox.critical) match result['code']: case 0: - msg.setText("Kit added") - msg.setInformativeText(result['message']) - msg.setWindowTitle("Kit added") + msg = AlertPop(message=result['message'], status='info') + # msg.setText("Kit added") + # msg.setInformativeText(result['message']) + # msg.setWindowTitle("Kit added") case 1: - msg.setText("Permission Error") - msg.setInformativeText(result['message']) - msg.setWindowTitle("Permission Error") + msg = AlertPop(message=result['message'], status='critical') + # msg.setText("Permission Error") + # msg.setInformativeText(result['message']) + # msg.setWindowTitle("Permission Error") msg.exec() + def add_org(self): + """ + Constructs new kit from yaml and adds to DB. + """ + # setup file dialog to find yaml flie + home_dir = str(Path(self.ctx["directory_path"])) + fname = Path(QFileDialog.getOpenFileName(self, 'Open file', home_dir, filter = "yml(*.yml)")[0]) + assert fname.exists() + # read yaml file + try: + with open(fname.__str__(), "r") as stream: + try: + org = yaml.load(stream, Loader=yaml.Loader) + except yaml.YAMLError as exc: + logger.error(f'Error reading yaml file {fname}: {exc}') + return {} + except PermissionError: + return + # send to kit creator function + result = create_org_from_yaml(ctx=self.ctx, org=org) + # msg = QMessageBox() + # msg.setIcon(QMessageBox.critical) + match result['code']: + case 0: + msg = AlertPop(message=result['message'], status='information') + # msg.setText("Organization added") + # msg.setInformativeText(result['message']) + # msg.setWindowTitle("Kit added") + case 1: + msg = AlertPop(message=result['message'], status='critical') + # msg.setText("Permission Error") + # msg.setInformativeText(result['message']) + # msg.setWindowTitle("Permission Error") + msg.exec() + + + def _controls_getter(self): """ diff --git a/src/submissions/frontend/custom_widgets/pop_ups.py b/src/submissions/frontend/custom_widgets/pop_ups.py index 3045068..9f54463 100644 --- a/src/submissions/frontend/custom_widgets/pop_ups.py +++ b/src/submissions/frontend/custom_widgets/pop_ups.py @@ -22,50 +22,67 @@ loader = FileSystemLoader(loader_path) env = Environment(loader=loader) -class AddReagentQuestion(QDialog): +# 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): """ - dialog to ask about adding a new reagne to db + dialog to ask yes/no questions """ - def __init__(self, reagent_type:str, reagent_lot:str) -> QDialog: + def __init__(self, title:str, message:str) -> QDialog: super().__init__() - - self.setWindowTitle(f"Add {reagent_lot}?") - + self.setWindowTitle(title) 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 AlertPop(QMessageBox): def __init__(self, message:str, status:str) -> QMessageBox: diff --git a/src/submissions/frontend/functions.py b/src/submissions/frontend/functions.py index 593cf0d..54b70ac 100644 --- a/src/submissions/frontend/functions.py +++ b/src/submissions/frontend/functions.py @@ -21,11 +21,3 @@ def check_kit_integrity(sub:BasicSubmission): return result -def check_not_nan(cell_contents) -> bool: - try: - return not np.isnan(cell_contents) - except ValueError: - return True - except Exception as e: - logger.debug(f"Check encounteded unknown error: {e}") - return False \ No newline at end of file diff --git a/src/submissions/tools/__init__.py b/src/submissions/tools/__init__.py new file mode 100644 index 0000000..b46c972 --- /dev/null +++ b/src/submissions/tools/__init__.py @@ -0,0 +1,13 @@ +import numpy as np +import logging + +logger = logging.getLogger(f"submissions.{__name__}") + +def check_not_nan(cell_contents) -> bool: + try: + return not np.isnan(cell_contents) + except TypeError: + return True + except Exception as e: + logger.debug(f"Check encounteded unknown error: {type(e).__name__} - {e}") + return False \ No newline at end of file diff --git a/tests/test_database_functions.py b/tests/test_database_functions.py deleted file mode 100644 index be3b4df..0000000 --- a/tests/test_database_functions.py +++ /dev/null @@ -1,8 +0,0 @@ -from sqlalchemy import create_engine -from sqlalchemy.orm import Session -from src.submissions.backend.db.models import * -from src.submissions.backend.db import get_kits_by_use - -engine = create_engine("sqlite+pysqlite:///:memory:", echo=True, future=True) -session = Session(engine) -metadata.create_all(engine) \ No newline at end of file