Various bug fixes.
This commit is contained in:
@@ -177,6 +177,8 @@ class Control(BaseClass):
|
|||||||
data = self.__getattribute__(mode)
|
data = self.__getattribute__(mode)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
data = {}
|
data = {}
|
||||||
|
if data is None:
|
||||||
|
data = {}
|
||||||
# logger.debug(f"Length of data: {len(data)}")
|
# logger.debug(f"Length of data: {len(data)}")
|
||||||
# logger.debug("dict keys are genera of bacteria, e.g. 'Streptococcus'")
|
# logger.debug("dict keys are genera of bacteria, e.g. 'Streptococcus'")
|
||||||
for genus in data:
|
for genus in data:
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
Models for the main submission and sample types.
|
Models for the main submission and sample types.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
from getpass import getuser
|
from getpass import getuser
|
||||||
import logging, uuid, tempfile, re, yaml, base64
|
import logging, uuid, tempfile, re, yaml, base64
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
@@ -13,7 +15,8 @@ from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLO
|
|||||||
from sqlalchemy.orm import relationship, validates, Query
|
from sqlalchemy.orm import relationship, validates, Query
|
||||||
from sqlalchemy.orm.attributes import flag_modified
|
from sqlalchemy.orm.attributes import flag_modified
|
||||||
from sqlalchemy.ext.associationproxy import association_proxy
|
from sqlalchemy.ext.associationproxy import association_proxy
|
||||||
from sqlalchemy.exc import OperationalError as AlcOperationalError, IntegrityError as AlcIntegrityError, StatementError
|
from sqlalchemy.exc import OperationalError as AlcOperationalError, IntegrityError as AlcIntegrityError, StatementError, \
|
||||||
|
ArgumentError
|
||||||
from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError
|
from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from openpyxl import Workbook, load_workbook
|
from openpyxl import Workbook, load_workbook
|
||||||
@@ -244,14 +247,22 @@ class BasicSubmission(BaseClass):
|
|||||||
ext_info = None
|
ext_info = None
|
||||||
output = {
|
output = {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"Plate Number": self.rsl_plate_num,
|
# "Plate Number": self.rsl_plate_num,
|
||||||
"Submission Type": self.submission_type_name,
|
# "Submission Type": self.submission_type_name,
|
||||||
"Submitter Plate Number": self.submitter_plate_num,
|
# "Submitter Plate Number": self.submitter_plate_num,
|
||||||
"Submitted Date": self.submitted_date.strftime("%Y-%m-%d"),
|
# "Submitted Date": self.submitted_date.strftime("%Y-%m-%d"),
|
||||||
"Submitting Lab": sub_lab,
|
# "Submitting Lab": sub_lab,
|
||||||
"Sample Count": self.sample_count,
|
# "Sample Count": self.sample_count,
|
||||||
"Extraction Kit": ext_kit,
|
# "Extraction Kit": ext_kit,
|
||||||
"Cost": self.run_cost,
|
# "Cost": self.run_cost,
|
||||||
|
"plate_number": self.rsl_plate_num,
|
||||||
|
"submission_type": self.submission_type_name,
|
||||||
|
"submitter_plate_number": self.submitter_plate_num,
|
||||||
|
"submitted_date": self.submitted_date.strftime("%Y-%m-%d"),
|
||||||
|
"submitting_lab": sub_lab,
|
||||||
|
"sample_count": self.sample_count,
|
||||||
|
"extraction_kit": ext_kit,
|
||||||
|
"cost": self.run_cost,
|
||||||
}
|
}
|
||||||
if report:
|
if report:
|
||||||
return output
|
return output
|
||||||
@@ -290,17 +301,26 @@ class BasicSubmission(BaseClass):
|
|||||||
try:
|
try:
|
||||||
comments = self.comment
|
comments = self.comment
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error setting comment: {self.comment}")
|
logger.error(f"Error setting comment: {self.comment}, {e}")
|
||||||
comments = None
|
comments = None
|
||||||
output["Submission Category"] = self.submission_category
|
# output["Submission Category"] = self.submission_category
|
||||||
output["Technician"] = self.technician
|
# output["Technician"] = self.technician
|
||||||
|
# output["reagents"] = reagents
|
||||||
|
# output["samples"] = samples
|
||||||
|
# output["extraction_info"] = ext_info
|
||||||
|
# output["comment"] = comments
|
||||||
|
# output["equipment"] = equipment
|
||||||
|
# output["Cost Centre"] = cost_centre
|
||||||
|
# output["Signed By"] = self.signed_by
|
||||||
|
output["submission_category"] = self.submission_category
|
||||||
|
output["technician"] = self.technician
|
||||||
output["reagents"] = reagents
|
output["reagents"] = reagents
|
||||||
output["samples"] = samples
|
output["samples"] = samples
|
||||||
output["extraction_info"] = ext_info
|
output["extraction_info"] = ext_info
|
||||||
output["comment"] = comments
|
output["comment"] = comments
|
||||||
output["equipment"] = equipment
|
output["equipment"] = equipment
|
||||||
output["Cost Centre"] = cost_centre
|
output["cost_centre"] = cost_centre
|
||||||
output["Signed By"] = self.signed_by
|
output["signed_by"] = self.signed_by
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def calculate_column_count(self) -> int:
|
def calculate_column_count(self) -> int:
|
||||||
@@ -380,7 +400,7 @@ class BasicSubmission(BaseClass):
|
|||||||
for column in range(1, plate_columns + 1):
|
for column in range(1, plate_columns + 1):
|
||||||
for row in range(1, plate_rows + 1):
|
for row in range(1, plate_rows + 1):
|
||||||
try:
|
try:
|
||||||
well = [item for item in sample_list if item['Row'] == row and item['Column'] == column][0]
|
well = [item for item in sample_list if item['row'] == row and item['column'] == column][0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
well = dict(name="", row=row, column=column, background_color="#ffffff")
|
well = dict(name="", row=row, column=column, background_color="#ffffff")
|
||||||
output_samples.append(well)
|
output_samples.append(well)
|
||||||
@@ -416,15 +436,21 @@ class BasicSubmission(BaseClass):
|
|||||||
subs = [item.to_dict() for item in cls.query(submission_type=submission_type, limit=limit, chronologic=chronologic)]
|
subs = [item.to_dict() for item in cls.query(submission_type=submission_type, limit=limit, chronologic=chronologic)]
|
||||||
# logger.debug(f"Got {len(subs)} submissions.")
|
# logger.debug(f"Got {len(subs)} submissions.")
|
||||||
df = pd.DataFrame.from_records(subs)
|
df = pd.DataFrame.from_records(subs)
|
||||||
|
# logger.debug(f"Column names: {df.columns}")
|
||||||
# NOTE: Exclude sub information
|
# NOTE: Exclude sub information
|
||||||
for item in ['controls', 'extraction_info', 'pcr_info', 'comment', 'comments', 'samples', 'reagents',
|
excluded = ['controls', 'extraction_info', 'pcr_info', 'comment', 'comments', 'samples', 'reagents',
|
||||||
'equipment', 'gel_info', 'gel_image', 'dna_core_submission_number', 'gel_controls']:
|
'equipment', 'gel_info', 'gel_image', 'dna_core_submission_number', 'gel_controls',
|
||||||
|
'source_plates', 'pcr_technician', 'ext_technician', 'artic_technician', 'cost_centre',
|
||||||
|
'signed_by']
|
||||||
|
for item in excluded:
|
||||||
try:
|
try:
|
||||||
df = df.drop(item, axis=1)
|
df = df.drop(item, axis=1)
|
||||||
except:
|
except:
|
||||||
logger.warning(f"Couldn't drop '{item}' column from submissionsheet df.")
|
logger.warning(f"Couldn't drop '{item}' column from submissionsheet df.")
|
||||||
if chronologic:
|
if chronologic:
|
||||||
df.sort_values(by="Submitted Date", axis=0, inplace=True, ascending=False)
|
df.sort_values(by="id", axis=0, inplace=True, ascending=False)
|
||||||
|
# NOTE: Human friendly column labels
|
||||||
|
df.columns = [item.replace("_", " ").title() for item in df.columns]
|
||||||
return df
|
return df
|
||||||
|
|
||||||
def set_attribute(self, key: str, value):
|
def set_attribute(self, key: str, value):
|
||||||
@@ -540,9 +566,9 @@ class BasicSubmission(BaseClass):
|
|||||||
new_dict[key] = [PydEquipment(**equipment) for equipment in dicto['equipment']]
|
new_dict[key] = [PydEquipment(**equipment) for equipment in dicto['equipment']]
|
||||||
except TypeError as e:
|
except TypeError as e:
|
||||||
logger.error(f"Possible no equipment error: {e}")
|
logger.error(f"Possible no equipment error: {e}")
|
||||||
case "Plate Number":
|
case "plate_number":
|
||||||
new_dict['rsl_plate_num'] = dict(value=value, missing=missing)
|
new_dict['rsl_plate_num'] = dict(value=value, missing=missing)
|
||||||
case "Submitter Plate Number":
|
case "submitter_plate_number":
|
||||||
new_dict['submitter_plate_num'] = dict(value=value, missing=missing)
|
new_dict['submitter_plate_num'] = dict(value=value, missing=missing)
|
||||||
case "id":
|
case "id":
|
||||||
pass
|
pass
|
||||||
@@ -566,6 +592,10 @@ class BasicSubmission(BaseClass):
|
|||||||
self.uploaded_by = getuser()
|
self.uploaded_by = getuser()
|
||||||
super().save()
|
super().save()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_regex(cls):
|
||||||
|
return cls.construct_regex()
|
||||||
|
|
||||||
# Polymorphic functions
|
# Polymorphic functions
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -598,7 +628,6 @@ class BasicSubmission(BaseClass):
|
|||||||
polymorphic_identity = polymorphic_identity['value']
|
polymorphic_identity = polymorphic_identity['value']
|
||||||
if isinstance(polymorphic_identity, SubmissionType):
|
if isinstance(polymorphic_identity, SubmissionType):
|
||||||
polymorphic_identity = polymorphic_identity.name
|
polymorphic_identity = polymorphic_identity.name
|
||||||
# if polymorphic_identity != None:
|
|
||||||
model = cls
|
model = cls
|
||||||
match polymorphic_identity:
|
match polymorphic_identity:
|
||||||
case str():
|
case str():
|
||||||
@@ -624,22 +653,6 @@ class BasicSubmission(BaseClass):
|
|||||||
return model
|
return model
|
||||||
|
|
||||||
# Child class custom functions
|
# Child class custom functions
|
||||||
|
|
||||||
# @classmethod
|
|
||||||
# def custom_platemap(cls, xl: pd.ExcelFile, plate_map: pd.DataFrame) -> pd.DataFrame:
|
|
||||||
# """
|
|
||||||
# Stupid stopgap solution to there being an issue with the Bacterial Culture plate map
|
|
||||||
#
|
|
||||||
# Args:
|
|
||||||
# xl (pd.ExcelFile): original xl workbook, used for child classes mostly
|
|
||||||
# plate_map (pd.DataFrame): original plate map
|
|
||||||
#
|
|
||||||
# Returns:
|
|
||||||
# pd.DataFrame: updated plate map.
|
|
||||||
# """
|
|
||||||
# logger.info(f"Calling {cls.__mapper_args__['polymorphic_identity']} plate mapper.")
|
|
||||||
# return plate_map
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def custom_info_parser(cls, input_dict: dict, xl: Workbook | None = None) -> dict:
|
def custom_info_parser(cls, input_dict: dict, xl: Workbook | None = None) -> dict:
|
||||||
"""
|
"""
|
||||||
@@ -723,7 +736,6 @@ class BasicSubmission(BaseClass):
|
|||||||
data['abbreviation'] = defaults['abbreviation']
|
data['abbreviation'] = defaults['abbreviation']
|
||||||
if 'submission_type' not in data.keys() or data['submission_type'] in [None, ""]:
|
if 'submission_type' not in data.keys() or data['submission_type'] in [None, ""]:
|
||||||
data['submission_type'] = defaults['submission_type']
|
data['submission_type'] = defaults['submission_type']
|
||||||
# outstr = super().enforce_name(instr=instr, data=data)
|
|
||||||
if instr in [None, ""]:
|
if instr in [None, ""]:
|
||||||
# logger.debug("Sending to RSLNamer to make new plate name.")
|
# logger.debug("Sending to RSLNamer to make new plate name.")
|
||||||
outstr = RSLNamer.construct_new_plate_name(data=data)
|
outstr = RSLNamer.construct_new_plate_name(data=data)
|
||||||
@@ -754,22 +766,7 @@ class BasicSubmission(BaseClass):
|
|||||||
outstr = re.sub(r"(-\dR)\d?", rf"\1 {repeat}", outstr).replace(" ", "")
|
outstr = re.sub(r"(-\dR)\d?", rf"\1 {repeat}", outstr).replace(" ", "")
|
||||||
abb = cls.get_default_info('abbreviation')
|
abb = cls.get_default_info('abbreviation')
|
||||||
return re.sub(rf"{abb}(\d)", rf"{abb}-\1", outstr)
|
return re.sub(rf"{abb}(\d)", rf"{abb}-\1", outstr)
|
||||||
# return outstr
|
|
||||||
|
|
||||||
# @classmethod
|
|
||||||
# def parse_pcr(cls, xl: pd.DataFrame, rsl_number: str) -> list:
|
|
||||||
# """
|
|
||||||
# Perform custom parsing of pcr info.
|
|
||||||
#
|
|
||||||
# Args:
|
|
||||||
# xl (pd.DataFrame): pcr info form
|
|
||||||
# rsl_number (str): rsl plate num of interest
|
|
||||||
#
|
|
||||||
# Returns:
|
|
||||||
# list: _description_
|
|
||||||
# """
|
|
||||||
# logger.debug(f"Hello from {cls.__mapper_args__['polymorphic_identity']} PCR parser!")
|
|
||||||
# return []
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse_pcr(cls, xl: Workbook, rsl_plate_num: str) -> list:
|
def parse_pcr(cls, xl: Workbook, rsl_plate_num: str) -> list:
|
||||||
"""
|
"""
|
||||||
@@ -896,15 +893,10 @@ class BasicSubmission(BaseClass):
|
|||||||
# logger.debug(f"Incoming kwargs: {kwargs}")
|
# logger.debug(f"Incoming kwargs: {kwargs}")
|
||||||
# NOTE: if you go back to using 'model' change the appropriate cls to model in the query filters
|
# NOTE: if you go back to using 'model' change the appropriate cls to model in the query filters
|
||||||
if submission_type is not None:
|
if submission_type is not None:
|
||||||
# if isinstance(submission_type, SubmissionType):
|
|
||||||
# model = cls.find_subclasses(submission_type=submission_type.name)
|
|
||||||
model = cls.find_polymorphic_subclass(polymorphic_identity=submission_type)
|
model = cls.find_polymorphic_subclass(polymorphic_identity=submission_type)
|
||||||
# else:
|
|
||||||
# model = cls.find_subclasses(submission_type=submission_type)
|
|
||||||
elif len(kwargs) > 0:
|
elif len(kwargs) > 0:
|
||||||
# find the subclass containing the relevant attributes
|
# find the subclass containing the relevant attributes
|
||||||
# logger.debug(f"Attributes for search: {kwargs}")
|
# logger.debug(f"Attributes for search: {kwargs}")
|
||||||
# model = cls.find_subclasses(attrs=kwargs)
|
|
||||||
model = cls.find_polymorphic_subclass(attrs=kwargs)
|
model = cls.find_polymorphic_subclass(attrs=kwargs)
|
||||||
else:
|
else:
|
||||||
model = cls
|
model = cls
|
||||||
@@ -979,17 +971,6 @@ class BasicSubmission(BaseClass):
|
|||||||
limit = 1
|
limit = 1
|
||||||
case _:
|
case _:
|
||||||
pass
|
pass
|
||||||
# for k, v in kwargs.items():
|
|
||||||
# logger.debug(f"Looking up attribute: {k}")
|
|
||||||
# attr = getattr(model, k)
|
|
||||||
# logger.debug(f"Got attr: {attr}")
|
|
||||||
# query = query.filter(attr==v)
|
|
||||||
# if len(kwargs) > 0:
|
|
||||||
# limit = 1
|
|
||||||
# query = cls.query_by_keywords(query=query, model=model, **kwargs)
|
|
||||||
# if any(x in kwargs.keys() for x in cls.get_default_info('singles')):
|
|
||||||
# logger.debug(f"There's a singled out item in kwargs")
|
|
||||||
# limit = 1
|
|
||||||
if chronologic:
|
if chronologic:
|
||||||
query.order_by(cls.submitted_date)
|
query.order_by(cls.submitted_date)
|
||||||
return cls.execute_query(query=query, model=model, limit=limit, **kwargs)
|
return cls.execute_query(query=query, model=model, limit=limit, **kwargs)
|
||||||
@@ -1150,7 +1131,6 @@ class BasicSubmission(BaseClass):
|
|||||||
if fname.name == "":
|
if fname.name == "":
|
||||||
# logger.debug(f"export cancelled.")
|
# logger.debug(f"export cancelled.")
|
||||||
return
|
return
|
||||||
# pyd.filepath = fname
|
|
||||||
if full_backup:
|
if full_backup:
|
||||||
backup = self.to_dict(full_data=True)
|
backup = self.to_dict(full_data=True)
|
||||||
try:
|
try:
|
||||||
@@ -1158,11 +1138,7 @@ class BasicSubmission(BaseClass):
|
|||||||
yaml.dump(backup, f)
|
yaml.dump(backup, f)
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
logger.error(f"Problem saving yml backup file: {e}")
|
logger.error(f"Problem saving yml backup file: {e}")
|
||||||
# wb = pyd.autofill_excel()
|
|
||||||
# wb = pyd.autofill_samples(wb)
|
|
||||||
# wb = pyd.autofill_equipment(wb)
|
|
||||||
writer = pyd.to_writer()
|
writer = pyd.to_writer()
|
||||||
# wb.save(filename=fname.with_suffix(".xlsx"))
|
|
||||||
writer.xl.save(filename=fname.with_suffix(".xlsx"))
|
writer.xl.save(filename=fname.with_suffix(".xlsx"))
|
||||||
|
|
||||||
# Below are the custom submission types
|
# Below are the custom submission types
|
||||||
@@ -1192,49 +1168,6 @@ class BacterialCulture(BasicSubmission):
|
|||||||
output['controls'] = [item.to_sub_dict() for item in self.controls]
|
output['controls'] = [item.to_sub_dict() for item in self.controls]
|
||||||
return output
|
return output
|
||||||
|
|
||||||
# @classmethod
|
|
||||||
# def custom_platemap(cls, xl: pd.ExcelFile, plate_map: pd.DataFrame) -> pd.DataFrame:
|
|
||||||
# """
|
|
||||||
# Stupid stopgap solution to there being an issue with the Bacterial Culture plate map. Extends parent.
|
|
||||||
#
|
|
||||||
# Args:
|
|
||||||
# xl (pd.ExcelFile): original xl workbook
|
|
||||||
# plate_map (pd.DataFrame): original plate map
|
|
||||||
#
|
|
||||||
# Returns:
|
|
||||||
# pd.DataFrame: updated plate map.
|
|
||||||
# """
|
|
||||||
# plate_map = super().custom_platemap(xl, plate_map)
|
|
||||||
# num1 = xl.parse("Sample List").iloc[40, 1]
|
|
||||||
# num2 = xl.parse("Sample List").iloc[41, 1]
|
|
||||||
# # logger.debug(f"Broken: {plate_map.iloc[5, 0]}, {plate_map.iloc[6, 0]}")
|
|
||||||
# # logger.debug(f"Replace: {num1}, {num2}")
|
|
||||||
# if not check_not_nan(plate_map.iloc[5, 0]):
|
|
||||||
# plate_map.iloc[5, 0] = num1
|
|
||||||
# if not check_not_nan(plate_map.iloc[6, 0]):
|
|
||||||
# plate_map.iloc[6, 0] = num2
|
|
||||||
# return plate_map
|
|
||||||
|
|
||||||
# @classmethod
|
|
||||||
# def custom_writer(cls, input_excel: Workbook, info: dict | None = None, backup: bool = False) -> Workbook:
|
|
||||||
# """
|
|
||||||
# Stupid stopgap solution to there being an issue with the Bacterial Culture plate map. Extends parent.
|
|
||||||
#
|
|
||||||
# Args:
|
|
||||||
# input_excel (Workbook): Input openpyxl workbook
|
|
||||||
#
|
|
||||||
# Returns:
|
|
||||||
# Workbook: Updated openpyxl workbook
|
|
||||||
# """
|
|
||||||
# input_excel = super().custom_writer(input_excel)
|
|
||||||
# sheet = input_excel['Plate Map']
|
|
||||||
# if sheet.cell(12, 2).value == None:
|
|
||||||
# sheet.cell(row=12, column=2, value="=IF(ISBLANK('Sample List'!$B42),\"\",'Sample List'!$B42)")
|
|
||||||
# if sheet.cell(13, 2).value == None:
|
|
||||||
# sheet.cell(row=13, column=2, value="=IF(ISBLANK('Sample List'!$B43),\"\",'Sample List'!$B43)")
|
|
||||||
# input_excel["Sample List"].cell(row=15, column=2, value=getuser())
|
|
||||||
# return input_excel
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_regex(cls) -> str:
|
def get_regex(cls) -> str:
|
||||||
"""
|
"""
|
||||||
@@ -1336,13 +1269,13 @@ class Wastewater(BasicSubmission):
|
|||||||
except TypeError as e:
|
except TypeError as e:
|
||||||
pass
|
pass
|
||||||
if self.ext_technician is None or self.ext_technician == "None":
|
if self.ext_technician is None or self.ext_technician == "None":
|
||||||
output['Ext Technician'] = self.technician
|
output['ext_technician'] = self.technician
|
||||||
else:
|
else:
|
||||||
output["Ext Technician"] = self.ext_technician
|
output["ext_technician"] = self.ext_technician
|
||||||
if self.pcr_technician is None or self.pcr_technician == "None":
|
if self.pcr_technician is None or self.pcr_technician == "None":
|
||||||
output["PCR Technician"] = self.technician
|
output["pcr_technician"] = self.technician
|
||||||
else:
|
else:
|
||||||
output['PCR Technician'] = self.pcr_technician
|
output['pcr_technician'] = self.pcr_technician
|
||||||
return output
|
return output
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -1361,46 +1294,6 @@ class Wastewater(BasicSubmission):
|
|||||||
input_dict['csv'] = xl["Copy to import file"]
|
input_dict['csv'] = xl["Copy to import file"]
|
||||||
return input_dict
|
return input_dict
|
||||||
|
|
||||||
# @classmethod
|
|
||||||
# def parse_pcr(cls, xl: pd.ExcelFile, rsl_number: str) -> list:
|
|
||||||
# """
|
|
||||||
# Parse specific to wastewater samples.
|
|
||||||
# """
|
|
||||||
# samples = super().parse_pcr(xl=xl, rsl_number=rsl_number)
|
|
||||||
# df = xl.parse(sheet_name="Results", dtype=object).fillna("")
|
|
||||||
# column_names = ["Well", "Well Position", "Omit", "Sample", "Target", "Task", " Reporter", "Quencher",
|
|
||||||
# "Amp Status", "Amp Score", "Curve Quality", "Result Quality Issues", "Cq", "Cq Confidence",
|
|
||||||
# "Cq Mean", "Cq SD", "Auto Threshold", "Threshold", "Auto Baseline", "Baseline Start",
|
|
||||||
# "Baseline End"]
|
|
||||||
# samples_df = df.iloc[23:][0:]
|
|
||||||
# logger.debug(f"Dataframe of PCR results:\n\t{samples_df}")
|
|
||||||
# samples_df.columns = column_names
|
|
||||||
# logger.debug(f"Samples columns: {samples_df.columns}")
|
|
||||||
# well_call_df = xl.parse(sheet_name="Well Call").iloc[24:][0:].iloc[:, -1:]
|
|
||||||
# try:
|
|
||||||
# samples_df['Assessment'] = well_call_df.values
|
|
||||||
# except ValueError:
|
|
||||||
# logger.error("Well call number doesn't match sample number")
|
|
||||||
# logger.debug(f"Well call df: {well_call_df}")
|
|
||||||
# for _, row in samples_df.iterrows():
|
|
||||||
# try:
|
|
||||||
# sample_obj = [sample for sample in samples if sample['sample'] == row[3]][0]
|
|
||||||
# except IndexError:
|
|
||||||
# sample_obj = dict(
|
|
||||||
# sample=row['Sample'],
|
|
||||||
# plate_rsl=rsl_number,
|
|
||||||
# )
|
|
||||||
# logger.debug(f"Got sample obj: {sample_obj}")
|
|
||||||
# if isinstance(row['Cq'], float):
|
|
||||||
# sample_obj[f"ct_{row['Target'].lower()}"] = row['Cq']
|
|
||||||
# else:
|
|
||||||
# sample_obj[f"ct_{row['Target'].lower()}"] = 0.0
|
|
||||||
# try:
|
|
||||||
# sample_obj[f"{row['Target'].lower()}_status"] = row['Assessment']
|
|
||||||
# except KeyError:
|
|
||||||
# logger.error(f"No assessment for {sample_obj['sample']}")
|
|
||||||
# samples.append(sample_obj)
|
|
||||||
# return samples
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse_pcr(cls, xl: Workbook, rsl_plate_num: str) -> list:
|
def parse_pcr(cls, xl: Workbook, rsl_plate_num: str) -> list:
|
||||||
"""
|
"""
|
||||||
@@ -1531,6 +1424,8 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
dict: dictionary used in submissions summary
|
dict: dictionary used in submissions summary
|
||||||
"""
|
"""
|
||||||
output = super().to_dict(full_data=full_data, backup=backup, report=report)
|
output = super().to_dict(full_data=full_data, backup=backup, report=report)
|
||||||
|
if self.artic_technician in [None, "None"]:
|
||||||
|
output['artic_technician'] = self.technician
|
||||||
if report:
|
if report:
|
||||||
return output
|
return output
|
||||||
output['gel_info'] = self.gel_info
|
output['gel_info'] = self.gel_info
|
||||||
@@ -1770,16 +1665,13 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
Workbook: Updated workbook
|
Workbook: Updated workbook
|
||||||
"""
|
"""
|
||||||
input_excel = super().custom_info_writer(input_excel, info, backup)
|
input_excel = super().custom_info_writer(input_excel, info, backup)
|
||||||
# worksheet = input_excel["First Strand List"]
|
logger.debug(f"Info:\n{pformat(info)}")
|
||||||
# samples = cls.query(rsl_number=info['rsl_plate_num']['value']).submission_sample_associations
|
|
||||||
# samples = sorted(samples, key=attrgetter('column', 'row'))
|
|
||||||
# logger.debug(f"Info:\n{pformat(info)}")
|
|
||||||
check = 'source_plates' in info.keys() and info['source_plates'] is not None
|
check = 'source_plates' in info.keys() and info['source_plates'] is not None
|
||||||
if check:
|
if check:
|
||||||
worksheet = input_excel['First Strand List']
|
worksheet = input_excel['First Strand List']
|
||||||
start_row = 8
|
start_row = 8
|
||||||
for iii, plate in enumerate(info['source_plates']['value']):
|
for iii, plate in enumerate(info['source_plates']['value']):
|
||||||
# logger.debug(f"Plate: {plate}")
|
logger.debug(f"Plate: {plate}")
|
||||||
row = start_row + iii
|
row = start_row + iii
|
||||||
try:
|
try:
|
||||||
worksheet.cell(row=row, column=3, value=plate['plate'])
|
worksheet.cell(row=row, column=3, value=plate['plate'])
|
||||||
@@ -1868,12 +1760,23 @@ class WastewaterArtic(BasicSubmission):
|
|||||||
set_plate = None
|
set_plate = None
|
||||||
for assoc in self.submission_sample_associations:
|
for assoc in self.submission_sample_associations:
|
||||||
dicto = assoc.to_sub_dict()
|
dicto = assoc.to_sub_dict()
|
||||||
|
if self.source_plates is None:
|
||||||
|
output.append(dicto)
|
||||||
|
continue
|
||||||
for item in self.source_plates:
|
for item in self.source_plates:
|
||||||
old_plate = WastewaterAssociation.query(submission=item['plate'], sample=assoc.sample, limit=1)
|
old_plate = WastewaterAssociation.query(submission=item['plate'], sample=assoc.sample, limit=1)
|
||||||
if old_plate is not None:
|
if old_plate is not None:
|
||||||
set_plate = old_plate.submission.rsl_plate_num
|
set_plate = old_plate.submission.rsl_plate_num
|
||||||
|
logger.debug(dicto['WW Processing Num'])
|
||||||
|
if dicto['WW Processing Num'].startswith("NTC"):
|
||||||
|
dicto['Well'] = dicto['WW Processing Num']
|
||||||
|
else:
|
||||||
|
dicto['Well'] = f"{row_map[old_plate.row]}{old_plate.column}"
|
||||||
break
|
break
|
||||||
|
elif dicto['WW Processing Num'].startswith("NTC"):
|
||||||
|
dicto['Well'] = dicto['WW Processing Num']
|
||||||
dicto['plate_name'] = set_plate
|
dicto['plate_name'] = set_plate
|
||||||
|
logger.debug(f"Here is our raw sample: {pformat(dicto)}")
|
||||||
output.append(dicto)
|
output.append(dicto)
|
||||||
return output
|
return output
|
||||||
|
|
||||||
@@ -1997,8 +1900,8 @@ class BasicSample(BaseClass):
|
|||||||
# logger.debug(f"Converting {self} to dict.")
|
# logger.debug(f"Converting {self} to dict.")
|
||||||
# start = time()
|
# start = time()
|
||||||
sample = {}
|
sample = {}
|
||||||
sample['Submitter ID'] = self.submitter_id
|
sample['submitter_id'] = self.submitter_id
|
||||||
sample['Sample Type'] = self.sample_type
|
sample['sample_type'] = self.sample_type
|
||||||
if full_data:
|
if full_data:
|
||||||
sample['submissions'] = sorted([item.to_sub_dict() for item in self.sample_submission_associations],
|
sample['submissions'] = sorted([item.to_sub_dict() for item in self.sample_submission_associations],
|
||||||
key=itemgetter('submitted_date'))
|
key=itemgetter('submitted_date'))
|
||||||
@@ -2099,7 +2002,7 @@ class BasicSample(BaseClass):
|
|||||||
@setup_lookup
|
@setup_lookup
|
||||||
def query(cls,
|
def query(cls,
|
||||||
submitter_id: str | None = None,
|
submitter_id: str | None = None,
|
||||||
sample_type: str | None = None,
|
sample_type: str | BasicSample | None = None,
|
||||||
limit: int = 0,
|
limit: int = 0,
|
||||||
**kwargs
|
**kwargs
|
||||||
) -> BasicSample | List[BasicSample]:
|
) -> BasicSample | List[BasicSample]:
|
||||||
@@ -2114,14 +2017,14 @@ class BasicSample(BaseClass):
|
|||||||
Returns:
|
Returns:
|
||||||
models.BasicSample|List[models.BasicSample]: Sample(s) of interest.
|
models.BasicSample|List[models.BasicSample]: Sample(s) of interest.
|
||||||
"""
|
"""
|
||||||
if sample_type is None:
|
match sample_type:
|
||||||
# model = cls.find_subclasses(attrs=kwargs)
|
case str():
|
||||||
model = cls.find_polymorphic_subclass(attrs=kwargs)
|
model = cls.find_polymorphic_subclass(polymorphic_identity=sample_type)
|
||||||
else:
|
case BasicSample():
|
||||||
model = cls.find_polymorphic_subclass(polymorphic_identity=sample_type)
|
model = sample_type
|
||||||
|
case _:
|
||||||
|
model = cls.find_polymorphic_subclass(attrs=kwargs)
|
||||||
# logger.debug(f"Length of kwargs: {len(kwargs)}")
|
# logger.debug(f"Length of kwargs: {len(kwargs)}")
|
||||||
# model = models.BasicSample.find_subclasses(ctx=ctx, attrs=kwargs)
|
|
||||||
# query: Query = setup_lookup(ctx=ctx, locals=locals()).query(model)
|
|
||||||
query: Query = cls.__database_session__.query(model)
|
query: Query = cls.__database_session__.query(model)
|
||||||
match submitter_id:
|
match submitter_id:
|
||||||
case str():
|
case str():
|
||||||
@@ -2130,20 +2033,7 @@ class BasicSample(BaseClass):
|
|||||||
limit = 1
|
limit = 1
|
||||||
case _:
|
case _:
|
||||||
pass
|
pass
|
||||||
# match sample_type:
|
|
||||||
# case str():
|
|
||||||
# logger.warning(f"Looking up samples with sample_type is disabled.")
|
|
||||||
# # query = query.filter(models.BasicSample.sample_type==sample_type)
|
|
||||||
# case _:
|
|
||||||
# pass
|
|
||||||
# for k, v in kwargs.items():
|
|
||||||
# attr = getattr(model, k)
|
|
||||||
# # logger.debug(f"Got attr: {attr}")
|
|
||||||
# query = query.filter(attr==v)
|
|
||||||
# if len(kwargs) > 0:
|
|
||||||
# limit = 1
|
|
||||||
return cls.execute_query(query=query, model=model, limit=limit, **kwargs)
|
return cls.execute_query(query=query, model=model, limit=limit, **kwargs)
|
||||||
# return cls.execute_query(query=query, limit=limit)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def query_or_create(cls, sample_type: str | None = None, **kwargs) -> BasicSample:
|
def query_or_create(cls, sample_type: str | None = None, **kwargs) -> BasicSample:
|
||||||
@@ -2163,10 +2053,6 @@ class BasicSample(BaseClass):
|
|||||||
disallowed = ["id"]
|
disallowed = ["id"]
|
||||||
if kwargs == {}:
|
if kwargs == {}:
|
||||||
raise ValueError("Need to narrow down query or the first available instance will be returned.")
|
raise ValueError("Need to narrow down query or the first available instance will be returned.")
|
||||||
# for key in kwargs.keys():
|
|
||||||
# if key in disallowed:
|
|
||||||
# raise ValueError(
|
|
||||||
# f"{key} is not allowed as a query argument as it could lead to creation of duplicate objects.")
|
|
||||||
sanitized_kwargs = {k:v for k,v in kwargs.items() if k not in disallowed}
|
sanitized_kwargs = {k:v for k,v in kwargs.items() if k not in disallowed}
|
||||||
instance = cls.query(sample_type=sample_type, limit=1, **kwargs)
|
instance = cls.query(sample_type=sample_type, limit=1, **kwargs)
|
||||||
# logger.debug(f"Retrieved instance: {instance}")
|
# logger.debug(f"Retrieved instance: {instance}")
|
||||||
@@ -2177,9 +2063,85 @@ class BasicSample(BaseClass):
|
|||||||
logger.debug(f"Creating instance: {instance}")
|
logger.debug(f"Creating instance: {instance}")
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fuzzy_search(cls,
|
||||||
|
# submitter_id: str | None = None,
|
||||||
|
sample_type: str | BasicSample | None = None,
|
||||||
|
# limit: int = 0,
|
||||||
|
**kwargs
|
||||||
|
) -> List[BasicSample]:
|
||||||
|
match sample_type:
|
||||||
|
case str():
|
||||||
|
model = cls.find_polymorphic_subclass(polymorphic_identity=sample_type)
|
||||||
|
case BasicSample():
|
||||||
|
model = sample_type
|
||||||
|
case _:
|
||||||
|
model = cls.find_polymorphic_subclass(attrs=kwargs)
|
||||||
|
# logger.debug(f"Length of kwargs: {len(kwargs)}")
|
||||||
|
query: Query = cls.__database_session__.query(model)
|
||||||
|
for k, v in kwargs.items():
|
||||||
|
search = f"%{v}%"
|
||||||
|
try:
|
||||||
|
attr = getattr(model, k)
|
||||||
|
query = query.filter(attr.like(search))
|
||||||
|
except (ArgumentError, AttributeError) as e:
|
||||||
|
logger.error(f"Attribute {k} unavailable due to:\n\t{e}\nSkipping.")
|
||||||
|
return query.all()
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
raise AttributeError(f"Delete not implemented for {self.__class__}")
|
raise AttributeError(f"Delete not implemented for {self.__class__}")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_searchables(cls):
|
||||||
|
return [dict(label="Submitter ID", field="submitter_id")]
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def samples_to_df(cls, sample_type: str | None | BasicSample = None, **kwargs):
|
||||||
|
# def samples_to_df(cls, sample_type:str|None|BasicSample=None, searchables:dict={}):
|
||||||
|
logger.debug(f"Checking {sample_type} with type {type(sample_type)}")
|
||||||
|
match sample_type:
|
||||||
|
case str():
|
||||||
|
model = BasicSample.find_polymorphic_subclass(polymorphic_identity=sample_type)
|
||||||
|
case _:
|
||||||
|
try:
|
||||||
|
check = issubclass(sample_type, BasicSample)
|
||||||
|
except TypeError:
|
||||||
|
check = False
|
||||||
|
if check:
|
||||||
|
model = sample_type
|
||||||
|
else:
|
||||||
|
model = cls
|
||||||
|
q_out = model.fuzzy_search(sample_type=sample_type, **kwargs)
|
||||||
|
if not isinstance(q_out, list):
|
||||||
|
q_out = [q_out]
|
||||||
|
try:
|
||||||
|
samples = [sample.to_sub_dict() for sample in q_out]
|
||||||
|
except TypeError as e:
|
||||||
|
logger.error(f"Couldn't find any samples with data: {kwargs}\nDue to {e}")
|
||||||
|
return None
|
||||||
|
df = pd.DataFrame.from_records(samples)
|
||||||
|
# NOTE: Exclude sub information
|
||||||
|
for item in ['concentration', 'organism', 'colour', 'tooltip', 'comments', 'samples', 'reagents',
|
||||||
|
'equipment', 'gel_info', 'gel_image', 'dna_core_submission_number', 'gel_controls']:
|
||||||
|
try:
|
||||||
|
df = df.drop(item, axis=1)
|
||||||
|
except:
|
||||||
|
logger.warning(f"Couldn't drop '{item}' column from submissionsheet df.")
|
||||||
|
return df
|
||||||
|
|
||||||
|
def show_details(self, obj):
|
||||||
|
"""
|
||||||
|
Creates Widget for showing submission details.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj (_type_): parent widget
|
||||||
|
"""
|
||||||
|
# logger.debug("Hello from details")
|
||||||
|
from frontend.widgets.submission_details import SubmissionDetails
|
||||||
|
dlg = SubmissionDetails(parent=obj, sub=self)
|
||||||
|
if dlg.exec():
|
||||||
|
pass
|
||||||
|
|
||||||
#Below are the custom sample types
|
#Below are the custom sample types
|
||||||
|
|
||||||
@@ -2229,10 +2191,10 @@ class WastewaterSample(BasicSample):
|
|||||||
dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above
|
dict: well location and name (sample id, organism) NOTE: keys must sync with WWSample to_sub_dict above
|
||||||
"""
|
"""
|
||||||
sample = super().to_sub_dict(full_data=full_data)
|
sample = super().to_sub_dict(full_data=full_data)
|
||||||
sample['WW Processing Num'] = self.ww_processing_num
|
sample['ww_processing_num'] = self.ww_processing_num
|
||||||
sample['Sample Location'] = self.sample_location
|
sample['sample_location'] = self.sample_location
|
||||||
sample['Received Date'] = self.received_date
|
sample['received_date'] = self.received_date
|
||||||
sample['Collection Date'] = self.collection_date
|
sample['collection_date'] = self.collection_date
|
||||||
return sample
|
return sample
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -2257,28 +2219,9 @@ class WastewaterSample(BasicSample):
|
|||||||
output_dict['rsl_number'] = "RSL-WW-" + output_dict['ww_processing_num']
|
output_dict['rsl_number'] = "RSL-WW-" + output_dict['ww_processing_num']
|
||||||
if output_dict['ww_full_sample_id'] is not None and output_dict["submitter_id"] in disallowed:
|
if output_dict['ww_full_sample_id'] is not None and output_dict["submitter_id"] in disallowed:
|
||||||
output_dict["submitter_id"] = output_dict['ww_full_sample_id']
|
output_dict["submitter_id"] = output_dict['ww_full_sample_id']
|
||||||
# if re.search(r"^NTC", output_dict['submitter_id']):
|
|
||||||
# output_dict['submitter_id'] = "Artic-" + output_dict['submitter_id']
|
|
||||||
# Ad hoc repair method for WW (or possibly upstream) not formatting some dates properly.
|
|
||||||
# NOTE: Should be handled by validator.
|
|
||||||
# match output_dict['collection_date']:
|
|
||||||
# case str():
|
|
||||||
# try:
|
|
||||||
# output_dict['collection_date'] = parse(output_dict['collection_date']).date()
|
|
||||||
# except ParserError:
|
|
||||||
# logger.error(f"Problem parsing collection_date: {output_dict['collection_date']}")
|
|
||||||
# output_dict['collection_date'] = date(1970, 1, 1)
|
|
||||||
# case datetime():
|
|
||||||
# output_dict['collection_date'] = output_dict['collection_date'].date()
|
|
||||||
# case date():
|
|
||||||
# pass
|
|
||||||
# case _:
|
|
||||||
# del output_dict['collection_date']
|
|
||||||
return output_dict
|
return output_dict
|
||||||
|
|
||||||
def get_previous_ww_submission(self, current_artic_submission: WastewaterArtic):
|
def get_previous_ww_submission(self, current_artic_submission: WastewaterArtic):
|
||||||
# assocs = [assoc for assoc in self.sample_submission_associations if assoc.submission.submission_type_name=="Wastewater"]
|
|
||||||
# subs = self.submissions[:self.submissions.index(current_artic_submission)]
|
|
||||||
try:
|
try:
|
||||||
plates = [item['plate'] for item in current_artic_submission.source_plates]
|
plates = [item['plate'] for item in current_artic_submission.source_plates]
|
||||||
except TypeError as e:
|
except TypeError as e:
|
||||||
@@ -2292,6 +2235,13 @@ class WastewaterSample(BasicSample):
|
|||||||
except IndexError:
|
except IndexError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_searchables(cls):
|
||||||
|
searchables = super().get_searchables()
|
||||||
|
for item in ["ww_processing_num", "ww_full_sample_id", "rsl_number"]:
|
||||||
|
label = item.strip("ww_").replace("_", " ").replace("rsl", "RSL").title()
|
||||||
|
searchables.append(dict(label=label, field=item))
|
||||||
|
return searchables
|
||||||
|
|
||||||
class BacterialCultureSample(BasicSample):
|
class BacterialCultureSample(BasicSample):
|
||||||
"""
|
"""
|
||||||
@@ -2314,16 +2264,15 @@ class BacterialCultureSample(BasicSample):
|
|||||||
"""
|
"""
|
||||||
# start = time()
|
# start = time()
|
||||||
sample = super().to_sub_dict(full_data=full_data)
|
sample = super().to_sub_dict(full_data=full_data)
|
||||||
sample['Name'] = self.submitter_id
|
sample['name'] = self.submitter_id
|
||||||
sample['Organism'] = self.organism
|
sample['organism'] = self.organism
|
||||||
sample['Concentration'] = self.concentration
|
sample['concentration'] = self.concentration
|
||||||
if self.control != None:
|
if self.control != None:
|
||||||
sample['colour'] = [0, 128, 0]
|
sample['colour'] = [0, 128, 0]
|
||||||
sample['tooltip'] = f"Control: {self.control.controltype.name} - {self.control.controltype.targets}"
|
sample['tooltip'] = f"Control: {self.control.controltype.name} - {self.control.controltype.targets}"
|
||||||
# logger.debug(f"Done converting to {self} to dict after {time()-start}")
|
# logger.debug(f"Done converting to {self} to dict after {time()-start}")
|
||||||
return sample
|
return sample
|
||||||
|
|
||||||
|
|
||||||
# Submission to Sample Associations
|
# Submission to Sample Associations
|
||||||
|
|
||||||
class SubmissionSampleAssociation(BaseClass):
|
class SubmissionSampleAssociation(BaseClass):
|
||||||
@@ -2387,15 +2336,15 @@ class SubmissionSampleAssociation(BaseClass):
|
|||||||
# logger.debug(f"Running {self.__repr__()}")
|
# logger.debug(f"Running {self.__repr__()}")
|
||||||
sample = self.sample.to_sub_dict()
|
sample = self.sample.to_sub_dict()
|
||||||
# logger.debug("Sample conversion complete.")
|
# logger.debug("Sample conversion complete.")
|
||||||
sample['Name'] = self.sample.submitter_id
|
sample['name'] = self.sample.submitter_id
|
||||||
sample['Row'] = self.row
|
sample['row'] = self.row
|
||||||
sample['Column'] = self.column
|
sample['column'] = self.column
|
||||||
try:
|
try:
|
||||||
sample['Well'] = f"{row_map[self.row]}{self.column}"
|
sample['well'] = f"{row_map[self.row]}{self.column}"
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
logger.error(f"Unable to find row {self.row} in row_map.")
|
logger.error(f"Unable to find row {self.row} in row_map.")
|
||||||
sample['Well'] = None
|
sample['Well'] = None
|
||||||
sample['Plate Name'] = self.submission.rsl_plate_num
|
sample['plate_name'] = self.submission.rsl_plate_num
|
||||||
sample['positive'] = False
|
sample['positive'] = False
|
||||||
sample['submitted_date'] = self.submission.submitted_date
|
sample['submitted_date'] = self.submission.submitted_date
|
||||||
sample['submission_rank'] = self.submission_rank
|
sample['submission_rank'] = self.submission_rank
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class SheetParser(object):
|
|||||||
Args:
|
Args:
|
||||||
filepath (Path | None, optional): file path to excel sheet. Defaults to None.
|
filepath (Path | None, optional): file path to excel sheet. Defaults to None.
|
||||||
"""
|
"""
|
||||||
logger.debug(f"\n\nParsing {filepath.__str__()}\n\n")
|
logger.info(f"\n\nParsing {filepath.__str__()}\n\n")
|
||||||
match filepath:
|
match filepath:
|
||||||
case Path():
|
case Path():
|
||||||
self.filepath = filepath
|
self.filepath = filepath
|
||||||
@@ -651,6 +651,15 @@ class PCRParser(object):
|
|||||||
info_map = self.submission_obj.get_submission_type().sample_map['pcr_general_info']
|
info_map = self.submission_obj.get_submission_type().sample_map['pcr_general_info']
|
||||||
sheet = self.xl[info_map['sheet']]
|
sheet = self.xl[info_map['sheet']]
|
||||||
iter_rows = sheet.iter_rows(min_row=info_map['start_row'], max_row=info_map['end_row'])
|
iter_rows = sheet.iter_rows(min_row=info_map['start_row'], max_row=info_map['end_row'])
|
||||||
pcr = {row[0].value.lower().replace(' ', '_'): row[1].value for row in iter_rows}
|
pcr = {}
|
||||||
|
for row in iter_rows:
|
||||||
|
try:
|
||||||
|
key = row[0].value.lower().replace(' ', '_')
|
||||||
|
except AttributeError as e:
|
||||||
|
logger.error(f"No key: {row[0].value} due to {e}")
|
||||||
|
continue
|
||||||
|
value = row[1].value or ""
|
||||||
|
pcr[key] = value
|
||||||
pcr['imported_by'] = getuser()
|
pcr['imported_by'] = getuser()
|
||||||
|
# logger.debug(f"PCR: {pformat(pcr)}")
|
||||||
return pcr
|
return pcr
|
||||||
|
|||||||
@@ -23,13 +23,13 @@ def make_report_xlsx(records:list[dict]) -> Tuple[DataFrame, DataFrame]:
|
|||||||
"""
|
"""
|
||||||
df = DataFrame.from_records(records)
|
df = DataFrame.from_records(records)
|
||||||
# put submissions with the same lab together
|
# put submissions with the same lab together
|
||||||
df = df.sort_values("Submitting Lab")
|
df = df.sort_values("submitting_lab")
|
||||||
# aggregate cost and sample count columns
|
# aggregate cost and sample count columns
|
||||||
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": 'Run Count'})
|
df2 = df2.rename(columns={"extraction_kit": 'run_count'})
|
||||||
# logger.debug(f"Output daftaframe for xlsx: {df2.columns}")
|
# logger.debug(f"Output daftaframe for xlsx: {df2.columns}")
|
||||||
df = df.drop('id', axis=1)
|
df = df.drop('id', axis=1)
|
||||||
df = df.sort_values(['Submitting Lab', "Submitted Date"])
|
df = df.sort_values(['submitting_lab', "submitted_date"])
|
||||||
return df, df2
|
return df, df2
|
||||||
|
|
||||||
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:
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ class SheetWriter(object):
|
|||||||
class InfoWriter(object):
|
class InfoWriter(object):
|
||||||
|
|
||||||
def __init__(self, xl: Workbook, submission_type: SubmissionType | str, info_dict: dict, sub_object:BasicSubmission|None=None):
|
def __init__(self, xl: Workbook, submission_type: SubmissionType | str, info_dict: dict, sub_object:BasicSubmission|None=None):
|
||||||
|
logger.debug(f"Info_dict coming into InfoWriter: {pformat(info_dict)}")
|
||||||
if isinstance(submission_type, str):
|
if isinstance(submission_type, str):
|
||||||
submission_type = SubmissionType.query(name=submission_type)
|
submission_type = SubmissionType.query(name=submission_type)
|
||||||
if sub_object is None:
|
if sub_object is None:
|
||||||
@@ -110,10 +111,15 @@ class InfoWriter(object):
|
|||||||
dicto['value'] = v
|
dicto['value'] = v
|
||||||
if len(dicto) > 0:
|
if len(dicto) > 0:
|
||||||
output[k] = dicto
|
output[k] = dicto
|
||||||
|
# logger.debug(f"Reconciled info: {pformat(output)}")
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def write_info(self):
|
def write_info(self):
|
||||||
for k, v in self.info.items():
|
for k, v in self.info.items():
|
||||||
|
# NOTE: merge all comments to fit in single cell.
|
||||||
|
if k == "comment" and isinstance(v['value'], list):
|
||||||
|
json_join = [item['text'] for item in v['value'] if 'text' in item.keys()]
|
||||||
|
v['value'] = "\n".join(json_join)
|
||||||
try:
|
try:
|
||||||
locations = v['locations']
|
locations = v['locations']
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -183,6 +189,7 @@ class SampleWriter(object):
|
|||||||
output = []
|
output = []
|
||||||
multiples = ['row', 'column', 'assoc_id', 'submission_rank']
|
multiples = ['row', 'column', 'assoc_id', 'submission_rank']
|
||||||
for sample in sample_list:
|
for sample in sample_list:
|
||||||
|
# logger.debug(f"Writing sample: {sample}")
|
||||||
for assoc in zip(sample['row'], sample['column'], sample['submission_rank']):
|
for assoc in zip(sample['row'], sample['column'], sample['submission_rank']):
|
||||||
new = dict(row=assoc[0], column=assoc[1], submission_rank=assoc[2])
|
new = dict(row=assoc[0], column=assoc[1], submission_rank=assoc[2])
|
||||||
for k, v in sample.items():
|
for k, v in sample.items():
|
||||||
|
|||||||
@@ -100,11 +100,15 @@ class RSLNamer(object):
|
|||||||
regex (str): string to construct pattern
|
regex (str): string to construct pattern
|
||||||
filename (str): string to be parsed
|
filename (str): string to be parsed
|
||||||
"""
|
"""
|
||||||
# logger.debug(f"Input string to be parsed: {filename}")
|
logger.debug(f"Input string to be parsed: {filename}")
|
||||||
if regex is None:
|
if regex is None:
|
||||||
regex = BasicSubmission.construct_regex()
|
regex = BasicSubmission.construct_regex()
|
||||||
else:
|
else:
|
||||||
regex = re.compile(rf'{regex}', re.IGNORECASE | re.VERBOSE)
|
logger.debug(f"Incoming regex: {regex}")
|
||||||
|
try:
|
||||||
|
regex = re.compile(rf'{regex}', re.IGNORECASE | re.VERBOSE)
|
||||||
|
except re.error as e:
|
||||||
|
regex = BasicSubmission.construct_regex()
|
||||||
# logger.debug(f"Using regex: {regex}")
|
# logger.debug(f"Using regex: {regex}")
|
||||||
match filename:
|
match filename:
|
||||||
case Path():
|
case Path():
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ class PydSample(BaseModel, extra='allow'):
|
|||||||
# logger.debug(f"Data for pydsample: {data}")
|
# logger.debug(f"Data for pydsample: {data}")
|
||||||
model = BasicSample.find_polymorphic_subclass(polymorphic_identity=data.sample_type)
|
model = BasicSample.find_polymorphic_subclass(polymorphic_identity=data.sample_type)
|
||||||
for k, v in data.model_extra.items():
|
for k, v in data.model_extra.items():
|
||||||
print(k, v)
|
# print(k, v)
|
||||||
if k in model.timestamps():
|
if k in model.timestamps():
|
||||||
if isinstance(v, str):
|
if isinstance(v, str):
|
||||||
v = datetime.strptime(v, "%Y-%m-%d")
|
v = datetime.strptime(v, "%Y-%m-%d")
|
||||||
@@ -463,7 +463,10 @@ class PydSubmission(BaseModel, extra='allow'):
|
|||||||
return value
|
return value
|
||||||
else:
|
else:
|
||||||
# logger.debug("Constructing plate name.")
|
# logger.debug("Constructing plate name.")
|
||||||
output = RSLNamer(filename=values.data['filepath'].__str__(), sub_type=sub_type,
|
if "pytest" in sys.modules and sub_type.replace(" ", "") == "BasicSubmission":
|
||||||
|
output = "RSL-BS-Test001"
|
||||||
|
else:
|
||||||
|
output = RSLNamer(filename=values.data['filepath'].__str__(), sub_type=sub_type,
|
||||||
data=values.data).parsed_name
|
data=values.data).parsed_name
|
||||||
return dict(value=output, missing=True)
|
return dict(value=output, missing=True)
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from .submission_widget import SubmissionFormContainer
|
|||||||
from .controls_chart import ControlsViewer
|
from .controls_chart import ControlsViewer
|
||||||
from .kit_creator import KitAdder
|
from .kit_creator import KitAdder
|
||||||
from .submission_type_creator import SubmissionTypeAdder
|
from .submission_type_creator import SubmissionTypeAdder
|
||||||
|
from .sample_search import SearchBox
|
||||||
|
|
||||||
logger = logging.getLogger(f'submissions.{__name__}')
|
logger = logging.getLogger(f'submissions.{__name__}')
|
||||||
logger.info("Hello, I am a logger")
|
logger.info("Hello, I am a logger")
|
||||||
@@ -71,6 +72,7 @@ class App(QMainWindow):
|
|||||||
fileMenu.addAction(self.importAction)
|
fileMenu.addAction(self.importAction)
|
||||||
# fileMenu.addAction(self.importPCRAction)
|
# fileMenu.addAction(self.importPCRAction)
|
||||||
methodsMenu.addAction(self.searchLog)
|
methodsMenu.addAction(self.searchLog)
|
||||||
|
methodsMenu.addAction(self.searchSample)
|
||||||
reportMenu.addAction(self.generateReportAction)
|
reportMenu.addAction(self.generateReportAction)
|
||||||
maintenanceMenu.addAction(self.joinExtractionAction)
|
maintenanceMenu.addAction(self.joinExtractionAction)
|
||||||
maintenanceMenu.addAction(self.joinPCRAction)
|
maintenanceMenu.addAction(self.joinPCRAction)
|
||||||
@@ -102,6 +104,7 @@ class App(QMainWindow):
|
|||||||
self.helpAction = QAction("&About", self)
|
self.helpAction = QAction("&About", self)
|
||||||
self.docsAction = QAction("&Docs", self)
|
self.docsAction = QAction("&Docs", self)
|
||||||
self.searchLog = QAction("Search Log", self)
|
self.searchLog = QAction("Search Log", self)
|
||||||
|
self.searchSample = QAction("Search Sample", self)
|
||||||
|
|
||||||
def _connectActions(self):
|
def _connectActions(self):
|
||||||
"""
|
"""
|
||||||
@@ -117,6 +120,7 @@ class App(QMainWindow):
|
|||||||
self.helpAction.triggered.connect(self.showAbout)
|
self.helpAction.triggered.connect(self.showAbout)
|
||||||
self.docsAction.triggered.connect(self.openDocs)
|
self.docsAction.triggered.connect(self.openDocs)
|
||||||
self.searchLog.triggered.connect(self.runSearch)
|
self.searchLog.triggered.connect(self.runSearch)
|
||||||
|
self.searchSample.triggered.connect(self.runSampleSearch)
|
||||||
|
|
||||||
def showAbout(self):
|
def showAbout(self):
|
||||||
"""
|
"""
|
||||||
@@ -161,6 +165,10 @@ class App(QMainWindow):
|
|||||||
dlg = LogParser(self)
|
dlg = LogParser(self)
|
||||||
dlg.exec()
|
dlg.exec()
|
||||||
|
|
||||||
|
def runSampleSearch(self):
|
||||||
|
dlg = SearchBox(self)
|
||||||
|
dlg.exec()
|
||||||
|
|
||||||
def backup_database(self):
|
def backup_database(self):
|
||||||
month = date.today().strftime("%Y-%m")
|
month = date.today().strftime("%Y-%m")
|
||||||
# day = date.today().strftime("%Y-%m-%d")
|
# day = date.today().strftime("%Y-%m-%d")
|
||||||
@@ -171,6 +179,7 @@ class App(QMainWindow):
|
|||||||
logger.info("No backup found for this month, backing up database.")
|
logger.info("No backup found for this month, backing up database.")
|
||||||
shutil.copyfile(self.ctx.database_path, current_month_bak)
|
shutil.copyfile(self.ctx.database_path, current_month_bak)
|
||||||
|
|
||||||
|
|
||||||
class AddSubForm(QWidget):
|
class AddSubForm(QWidget):
|
||||||
|
|
||||||
def __init__(self, parent:QWidget):
|
def __init__(self, parent:QWidget):
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class ControlsViewer(QWidget):
|
|||||||
def __init__(self, parent: QWidget) -> None:
|
def __init__(self, parent: QWidget) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.app = self.parent().parent()
|
self.app = self.parent().parent()
|
||||||
print(f"\n\n{self.app}\n\n")
|
# logger.debug(f"\n\n{self.app}\n\n")
|
||||||
self.report = Report()
|
self.report = Report()
|
||||||
self.datepicker = ControlsDatePicker()
|
self.datepicker = ControlsDatePicker()
|
||||||
self.webengineview = QWebEngineView()
|
self.webengineview = QWebEngineView()
|
||||||
|
|||||||
101
src/submissions/frontend/widgets/sample_search.py
Normal file
101
src/submissions/frontend/widgets/sample_search.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
from pprint import pformat
|
||||||
|
from typing import Tuple
|
||||||
|
from pandas import DataFrame
|
||||||
|
from PyQt6.QtCore import QAbstractTableModel, Qt, QEvent, QSortFilterProxyModel
|
||||||
|
from PyQt6.QtWidgets import (
|
||||||
|
QLabel, QVBoxLayout, QDialog,
|
||||||
|
QDialogButtonBox, QMessageBox, QComboBox, QTableView, QWidget, QLineEdit, QGridLayout
|
||||||
|
)
|
||||||
|
from backend.db.models import BasicSample
|
||||||
|
from .submission_table import pandasModel
|
||||||
|
from .submission_details import SubmissionDetails
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
|
|
||||||
|
|
||||||
|
class SearchBox(QDialog):
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.layout = QGridLayout(self)
|
||||||
|
self.sample_type = QComboBox(self)
|
||||||
|
self.sample_type.setObjectName("sample_type")
|
||||||
|
self.sample_type.currentTextChanged.connect(self.update_widgets)
|
||||||
|
options = [cls.__mapper_args__['polymorphic_identity'] for cls in BasicSample.__subclasses__()]
|
||||||
|
self.sample_type.addItems(options)
|
||||||
|
self.sample_type.setEditable(False)
|
||||||
|
self.setMinimumSize(600, 600)
|
||||||
|
self.sample_type.setMinimumWidth(self.minimumWidth())
|
||||||
|
self.layout.addWidget(self.sample_type, 0, 0)
|
||||||
|
self.results = SearchResults()
|
||||||
|
self.layout.addWidget(self.results, 5, 0)
|
||||||
|
self.setLayout(self.layout)
|
||||||
|
self.update_widgets()
|
||||||
|
|
||||||
|
def update_widgets(self):
|
||||||
|
deletes = [item for item in self.findChildren(FieldSearch)]
|
||||||
|
# logger.debug(deletes)
|
||||||
|
for item in deletes:
|
||||||
|
item.setParent(None)
|
||||||
|
self.type = BasicSample.find_polymorphic_subclass(self.sample_type.currentText())
|
||||||
|
# logger.debug(f"Sample type: {self.type}")
|
||||||
|
searchables = self.type.get_searchables()
|
||||||
|
start_row = 1
|
||||||
|
for iii, item in enumerate(searchables):
|
||||||
|
widget = FieldSearch(parent=self, label=item['label'], field_name=item['field'])
|
||||||
|
self.layout.addWidget(widget, start_row+iii, 0)
|
||||||
|
|
||||||
|
def parse_form(self):
|
||||||
|
fields = [item.parse_form() for item in self.findChildren(FieldSearch)]
|
||||||
|
return {item[0]:item[1] for item in fields if item[1] is not None}
|
||||||
|
|
||||||
|
def update_data(self):
|
||||||
|
fields = self.parse_form()
|
||||||
|
data = self.type.samples_to_df(sample_type=self.type, **fields)
|
||||||
|
# logger.debug(f"Data: {data}")
|
||||||
|
self.results.setData(df=data)
|
||||||
|
|
||||||
|
|
||||||
|
class FieldSearch(QWidget):
|
||||||
|
|
||||||
|
def __init__(self, parent, label, field_name):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.layout = QVBoxLayout(self)
|
||||||
|
label_widget = QLabel(label)
|
||||||
|
self.layout.addWidget(label_widget)
|
||||||
|
self.search_widget = QLineEdit()
|
||||||
|
self.search_widget.setObjectName(field_name)
|
||||||
|
self.layout.addWidget(self.search_widget)
|
||||||
|
self.setLayout(self.layout)
|
||||||
|
self.search_widget.returnPressed.connect(self.enter_pressed)
|
||||||
|
|
||||||
|
def enter_pressed(self):
|
||||||
|
self.parent().update_data()
|
||||||
|
|
||||||
|
def parse_form(self) -> Tuple:
|
||||||
|
field_value = self.search_widget.text()
|
||||||
|
if field_value == "":
|
||||||
|
field_value = None
|
||||||
|
return self.search_widget.objectName(), field_value
|
||||||
|
|
||||||
|
|
||||||
|
class SearchResults(QTableView):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.doubleClicked.connect(lambda x: BasicSample.query(submitter_id=x.sibling(x.row(), 0).data()).show_details(self))
|
||||||
|
|
||||||
|
def setData(self, df:DataFrame) -> None:
|
||||||
|
"""
|
||||||
|
sets data in model
|
||||||
|
"""
|
||||||
|
self.data = df
|
||||||
|
try:
|
||||||
|
self.data['id'] = self.data['id'].apply(str)
|
||||||
|
self.data['id'] = self.data['id'].str.zfill(3)
|
||||||
|
except (TypeError, KeyError):
|
||||||
|
logger.error("Couldn't format id string.")
|
||||||
|
proxy_model = QSortFilterProxyModel()
|
||||||
|
proxy_model.setSourceModel(pandasModel(self.data))
|
||||||
|
self.setModel(proxy_model)
|
||||||
@@ -25,7 +25,7 @@ class SubmissionDetails(QDialog):
|
|||||||
"""
|
"""
|
||||||
a window showing text details of submission
|
a window showing text details of submission
|
||||||
"""
|
"""
|
||||||
def __init__(self, parent, sub:BasicSubmission) -> None:
|
def __init__(self, parent, sub:BasicSubmission|BasicSample) -> None:
|
||||||
|
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
try:
|
try:
|
||||||
@@ -47,19 +47,24 @@ class SubmissionDetails(QDialog):
|
|||||||
# NOTE: setup channel
|
# NOTE: setup channel
|
||||||
self.channel = QWebChannel()
|
self.channel = QWebChannel()
|
||||||
self.channel.registerObject('backend', self)
|
self.channel.registerObject('backend', self)
|
||||||
self.submission_details(submission=sub)
|
match sub:
|
||||||
self.rsl_plate_num = sub.rsl_plate_num
|
case BasicSubmission():
|
||||||
|
self.submission_details(submission=sub)
|
||||||
|
self.rsl_plate_num = sub.rsl_plate_num
|
||||||
|
case BasicSample():
|
||||||
|
self.sample_details(sample=sub)
|
||||||
self.webview.page().setWebChannel(self.channel)
|
self.webview.page().setWebChannel(self.channel)
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def sample_details(self, sample:str):
|
def sample_details(self, sample:str|BasicSample):
|
||||||
"""
|
"""
|
||||||
Changes details view to summary of Sample
|
Changes details view to summary of Sample
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
sample (str): Submitter Id of the sample.
|
sample (str): Submitter Id of the sample.
|
||||||
"""
|
"""
|
||||||
sample = BasicSample.query(submitter_id=sample)
|
if isinstance(sample, str):
|
||||||
|
sample = BasicSample.query(submitter_id=sample)
|
||||||
base_dict = sample.to_sub_dict(full_data=True)
|
base_dict = sample.to_sub_dict(full_data=True)
|
||||||
base_dict, template = sample.get_details_template(base_dict=base_dict)
|
base_dict, template = sample.get_details_template(base_dict=base_dict)
|
||||||
html = template.render(sample=base_dict)
|
html = template.render(sample=base_dict)
|
||||||
@@ -87,6 +92,8 @@ class SubmissionDetails(QDialog):
|
|||||||
self.base_dict, self.template = submission.get_details_template(base_dict=self.base_dict)
|
self.base_dict, self.template = submission.get_details_template(base_dict=self.base_dict)
|
||||||
self.html = self.template.render(sub=self.base_dict, signing_permission=is_power_user())
|
self.html = self.template.render(sub=self.base_dict, signing_permission=is_power_user())
|
||||||
self.webview.setHtml(self.html)
|
self.webview.setHtml(self.html)
|
||||||
|
with open("test.html", "w") as f:
|
||||||
|
f.write(self.html)
|
||||||
self.setWindowTitle(f"Submission Details - {submission.rsl_plate_num}")
|
self.setWindowTitle(f"Submission Details - {submission.rsl_plate_num}")
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
@@ -138,7 +145,7 @@ class SubmissionComment(QDialog):
|
|||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
try:
|
try:
|
||||||
self.app = parent.parent().parent().parent().parent().parent().parent
|
self.app = parent.parent().parent().parent().parent().parent().parent
|
||||||
print(f"App: {self.app}")
|
# logger.debug(f"App: {self.app}")
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
self.submission = submission
|
self.submission = submission
|
||||||
|
|||||||
@@ -87,11 +87,10 @@ class SubmissionsSheet(QTableView):
|
|||||||
"""
|
"""
|
||||||
self.data = BasicSubmission.submissions_to_df()
|
self.data = BasicSubmission.submissions_to_df()
|
||||||
try:
|
try:
|
||||||
self.data['id'] = self.data['id'].apply(str)
|
self.data['Id'] = self.data['Id'].apply(str)
|
||||||
self.data['id'] = self.data['id'].str.zfill(3)
|
self.data['Id'] = self.data['Id'].str.zfill(3)
|
||||||
except KeyError:
|
except KeyError as e:
|
||||||
pass
|
logger.error(f"Could not alter id to string due to {e}")
|
||||||
|
|
||||||
proxyModel = QSortFilterProxyModel()
|
proxyModel = QSortFilterProxyModel()
|
||||||
proxyModel.setSourceModel(pandasModel(self.data))
|
proxyModel.setSourceModel(pandasModel(self.data))
|
||||||
self.setModel(proxyModel)
|
self.setModel(proxyModel)
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ from datetime import date
|
|||||||
|
|
||||||
logger = logging.getLogger(f"submissions.{__name__}")
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
|
|
||||||
class SubmissionFormContainer(QWidget):
|
|
||||||
|
|
||||||
|
class SubmissionFormContainer(QWidget):
|
||||||
# A signal carrying a path
|
# A signal carrying a path
|
||||||
import_drag = pyqtSignal(Path)
|
import_drag = pyqtSignal(Path)
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ class SubmissionFormContainer(QWidget):
|
|||||||
self.app.last_dir = fname.parent
|
self.app.last_dir = fname.parent
|
||||||
self.import_drag.emit(fname)
|
self.import_drag.emit(fname)
|
||||||
|
|
||||||
def importSubmission(self, fname:Path|None=None):
|
def importSubmission(self, fname: Path | None = None):
|
||||||
"""
|
"""
|
||||||
import submission from excel sheet into form
|
import submission from excel sheet into form
|
||||||
"""
|
"""
|
||||||
@@ -68,7 +68,7 @@ class SubmissionFormContainer(QWidget):
|
|||||||
self.report = Report()
|
self.report = Report()
|
||||||
self.app.result_reporter()
|
self.app.result_reporter()
|
||||||
|
|
||||||
def import_submission_function(self, fname:Path|None=None):
|
def import_submission_function(self, fname: Path | None = None):
|
||||||
"""
|
"""
|
||||||
Import a new submission to the app window
|
Import a new submission to the app window
|
||||||
|
|
||||||
@@ -84,10 +84,10 @@ class SubmissionFormContainer(QWidget):
|
|||||||
self.form.setParent(None)
|
self.form.setParent(None)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
pass
|
||||||
# initialize samples
|
# NOTE: initialize samples
|
||||||
self.samples = []
|
self.samples = []
|
||||||
self.missing_info = []
|
self.missing_info = []
|
||||||
# set file dialog
|
# NOTE: set file dialog
|
||||||
if isinstance(fname, bool) or fname == None:
|
if isinstance(fname, bool) or fname == None:
|
||||||
fname = select_open_file(self, file_extension="xlsx")
|
fname = select_open_file(self, file_extension="xlsx")
|
||||||
# logger.debug(f"Attempting to parse file: {fname}")
|
# logger.debug(f"Attempting to parse file: {fname}")
|
||||||
@@ -95,7 +95,7 @@ class SubmissionFormContainer(QWidget):
|
|||||||
report.add_result(Result(msg=f"File {fname.__str__()} not found.", status="critical"))
|
report.add_result(Result(msg=f"File {fname.__str__()} not found.", status="critical"))
|
||||||
self.report.add_result(report)
|
self.report.add_result(report)
|
||||||
return
|
return
|
||||||
# create sheetparser using excel sheet and context from gui
|
# NOTE: create sheetparser using excel sheet and context from gui
|
||||||
try:
|
try:
|
||||||
self.prsr = SheetParser(filepath=fname)
|
self.prsr = SheetParser(filepath=fname)
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
@@ -108,13 +108,12 @@ class SubmissionFormContainer(QWidget):
|
|||||||
# logger.debug(f"Pydantic result: \n\n{pformat(self.pyd)}\n\n")
|
# logger.debug(f"Pydantic result: \n\n{pformat(self.pyd)}\n\n")
|
||||||
self.form = self.pyd.to_form(parent=self)
|
self.form = self.pyd.to_form(parent=self)
|
||||||
self.layout().addWidget(self.form)
|
self.layout().addWidget(self.form)
|
||||||
# if self.prsr.sample_result != None:
|
|
||||||
# report.add_result(msg=self.prsr.sample_result, status="Warning")
|
|
||||||
self.report.add_result(report)
|
self.report.add_result(report)
|
||||||
# logger.debug(f"Outgoing report: {self.report.results}")
|
# logger.debug(f"Outgoing report: {self.report.results}")
|
||||||
# logger.debug(f"All attributes of submission container:\n{pformat(self.__dict__)}")
|
# logger.debug(f"All attributes of submission container:\n{pformat(self.__dict__)}")
|
||||||
|
|
||||||
def add_reagent(self, reagent_lot:str|None=None, reagent_type:str|None=None, expiry:date|None=None, name:str|None=None):
|
def add_reagent(self, reagent_lot: str | None = None, reagent_type: str | None = None, expiry: date | None = None,
|
||||||
|
name: str | None = None):
|
||||||
"""
|
"""
|
||||||
Action to create new reagent in DB.
|
Action to create new reagent in DB.
|
||||||
|
|
||||||
@@ -130,24 +129,25 @@ class SubmissionFormContainer(QWidget):
|
|||||||
report = Report()
|
report = Report()
|
||||||
if isinstance(reagent_lot, bool):
|
if isinstance(reagent_lot, bool):
|
||||||
reagent_lot = ""
|
reagent_lot = ""
|
||||||
# create form
|
# NOTE: create form
|
||||||
dlg = AddReagentForm(reagent_lot=reagent_lot, reagent_type=reagent_type, expiry=expiry, reagent_name=name)
|
dlg = AddReagentForm(reagent_lot=reagent_lot, reagent_type=reagent_type, expiry=expiry, reagent_name=name)
|
||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
# extract form info
|
# extract form info
|
||||||
info = dlg.parse_form()
|
info = dlg.parse_form()
|
||||||
# logger.debug(f"Reagent info: {info}")
|
# logger.debug(f"Reagent info: {info}")
|
||||||
# create reagent object
|
# NOTE: create reagent object
|
||||||
reagent = PydReagent(ctx=self.app.ctx, **info, missing=False)
|
reagent = PydReagent(ctx=self.app.ctx, **info, missing=False)
|
||||||
# send reagent to db
|
# NOTE: send reagent to db
|
||||||
sqlobj, result = reagent.toSQL()
|
sqlobj, result = reagent.toSQL()
|
||||||
sqlobj.save()
|
sqlobj.save()
|
||||||
report.add_result(result)
|
report.add_result(result)
|
||||||
self.app.result_reporter()
|
self.app.result_reporter()
|
||||||
return reagent
|
return reagent
|
||||||
|
|
||||||
|
|
||||||
class SubmissionFormWidget(QWidget):
|
class SubmissionFormWidget(QWidget):
|
||||||
|
|
||||||
def __init__(self, parent: QWidget, submission:PydSubmission) -> None:
|
def __init__(self, parent: QWidget, submission: PydSubmission) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
# self.report = Report()
|
# self.report = Report()
|
||||||
self.app = parent.app
|
self.app = parent.app
|
||||||
@@ -157,10 +157,8 @@ class SubmissionFormWidget(QWidget):
|
|||||||
defaults = st.get_default_info("form_recover", "form_ignore")
|
defaults = st.get_default_info("form_recover", "form_ignore")
|
||||||
self.recover = defaults['form_recover']
|
self.recover = defaults['form_recover']
|
||||||
self.ignore = defaults['form_ignore']
|
self.ignore = defaults['form_ignore']
|
||||||
# self.ignore += self.recover
|
|
||||||
# logger.debug(f"Attempting to extend ignore list with {self.pyd.submission_type['value']}")
|
# logger.debug(f"Attempting to extend ignore list with {self.pyd.submission_type['value']}")
|
||||||
self.layout = QVBoxLayout()
|
self.layout = QVBoxLayout()
|
||||||
# for k, v in kwargs.items():
|
|
||||||
for k in list(self.pyd.model_fields.keys()) + list(self.pyd.model_extra.keys()):
|
for k in list(self.pyd.model_fields.keys()) + list(self.pyd.model_extra.keys()):
|
||||||
if k in self.ignore:
|
if k in self.ignore:
|
||||||
continue
|
continue
|
||||||
@@ -176,7 +174,8 @@ class SubmissionFormWidget(QWidget):
|
|||||||
add_widget.input.currentTextChanged.connect(self.scrape_reagents)
|
add_widget.input.currentTextChanged.connect(self.scrape_reagents)
|
||||||
self.scrape_reagents(self.pyd.extraction_kit)
|
self.scrape_reagents(self.pyd.extraction_kit)
|
||||||
|
|
||||||
def create_widget(self, key:str, value:dict|PydReagent, submission_type:str|None=None, extraction_kit:str|None=None) -> "self.InfoItem":
|
def create_widget(self, key: str, value: dict | PydReagent, submission_type: str | None = None,
|
||||||
|
extraction_kit: str | None = None) -> "self.InfoItem":
|
||||||
"""
|
"""
|
||||||
Make an InfoItem widget to hold a field
|
Make an InfoItem widget to hold a field
|
||||||
|
|
||||||
@@ -200,7 +199,7 @@ class SubmissionFormWidget(QWidget):
|
|||||||
return widget
|
return widget
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def scrape_reagents(self, *args, **kwargs):#extraction_kit:str, caller:str|None=None):
|
def scrape_reagents(self, *args, **kwargs): #extraction_kit:str, caller:str|None=None):
|
||||||
"""
|
"""
|
||||||
Extracted scrape reagents function that will run when
|
Extracted scrape reagents function that will run when
|
||||||
form 'extraction_kit' widget is updated.
|
form 'extraction_kit' widget is updated.
|
||||||
@@ -214,13 +213,12 @@ class SubmissionFormWidget(QWidget):
|
|||||||
"""
|
"""
|
||||||
extraction_kit = args[0]
|
extraction_kit = args[0]
|
||||||
caller = inspect.stack()[1].function.__repr__().replace("'", "")
|
caller = inspect.stack()[1].function.__repr__().replace("'", "")
|
||||||
# self.reagents = []
|
|
||||||
# logger.debug(f"Self.reagents: {self.reagents}")
|
# logger.debug(f"Self.reagents: {self.reagents}")
|
||||||
# logger.debug(f"\n\n{pformat(caller)}\n\n")
|
# logger.debug(f"\n\n{pformat(caller)}\n\n")
|
||||||
# logger.debug(f"SubmissionType: {self.submission_type}")
|
# logger.debug(f"SubmissionType: {self.submission_type}")
|
||||||
report = Report()
|
report = Report()
|
||||||
# logger.debug(f"Extraction kit: {extraction_kit}")
|
# logger.debug(f"Extraction kit: {extraction_kit}")
|
||||||
# Remove previous reagent widgets
|
# NOTE: Remove previous reagent widgets
|
||||||
try:
|
try:
|
||||||
old_reagents = self.find_widgets()
|
old_reagents = self.find_widgets()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@@ -230,19 +228,6 @@ class SubmissionFormWidget(QWidget):
|
|||||||
for reagent in old_reagents:
|
for reagent in old_reagents:
|
||||||
if isinstance(reagent, self.ReagentFormWidget) or isinstance(reagent, QPushButton):
|
if isinstance(reagent, self.ReagentFormWidget) or isinstance(reagent, QPushButton):
|
||||||
reagent.setParent(None)
|
reagent.setParent(None)
|
||||||
# match caller:
|
|
||||||
# case "import_submission_function":
|
|
||||||
# self.reagents = self.prsr.sub['reagents']
|
|
||||||
# case _:
|
|
||||||
# already_have = [reagent for reagent in self.prsr.sub['reagents'] if not reagent.missing]
|
|
||||||
# already_have = [reagent for reagent in self.pyd.reagents if not reagent.missing]
|
|
||||||
# names = list(set([item.type for item in already_have]))
|
|
||||||
# # logger.debug(f"Already have: {already_have}")
|
|
||||||
# reagents = [item.to_pydantic() for item in KitType.query(name=extraction_kit).get_reagents(submission_type=self.pyd.submission_type) if item.name not in names]
|
|
||||||
# # logger.debug(f"Missing: {reagents}")
|
|
||||||
# self.pyd.reagents = already_have + reagents
|
|
||||||
# logger.debug(f"Reagents: {self.reagents}")
|
|
||||||
# self.kit_integrity_completion_function(extraction_kit=extraction_kit)
|
|
||||||
reagents, integrity_report = self.pyd.check_kit_integrity(extraction_kit=extraction_kit)
|
reagents, integrity_report = self.pyd.check_kit_integrity(extraction_kit=extraction_kit)
|
||||||
# logger.debug(f"Missing reagents: {obj.missing_reagents}")
|
# logger.debug(f"Missing reagents: {obj.missing_reagents}")
|
||||||
for reagent in reagents:
|
for reagent in reagents:
|
||||||
@@ -270,7 +255,7 @@ class SubmissionFormWidget(QWidget):
|
|||||||
for item in self.findChildren(QWidget):
|
for item in self.findChildren(QWidget):
|
||||||
item.setParent(None)
|
item.setParent(None)
|
||||||
|
|
||||||
def find_widgets(self, object_name:str|None=None) -> List[QWidget]:
|
def find_widgets(self, object_name: str | None = None) -> List[QWidget]:
|
||||||
"""
|
"""
|
||||||
Gets all widgets filtered by object name
|
Gets all widgets filtered by object name
|
||||||
|
|
||||||
@@ -281,8 +266,8 @@ class SubmissionFormWidget(QWidget):
|
|||||||
List[QWidget]: Widgets matching filter
|
List[QWidget]: Widgets matching filter
|
||||||
"""
|
"""
|
||||||
query = self.findChildren(QWidget)
|
query = self.findChildren(QWidget)
|
||||||
if object_name != None:
|
if object_name is not None:
|
||||||
query = [widget for widget in query if widget.objectName()==object_name]
|
query = [widget for widget in query if widget.objectName() == object_name]
|
||||||
return query
|
return query
|
||||||
|
|
||||||
def submit_new_sample_function(self) -> QWidget:
|
def submit_new_sample_function(self) -> QWidget:
|
||||||
@@ -320,8 +305,7 @@ class SubmissionFormWidget(QWidget):
|
|||||||
case 1:
|
case 1:
|
||||||
dlg = QuestionAsker(title=f"Review {base_submission.rsl_plate_num}?", message=result.msg)
|
dlg = QuestionAsker(title=f"Review {base_submission.rsl_plate_num}?", message=result.msg)
|
||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
# Do not add duplicate reagents.
|
# NOTE: Do not add duplicate reagents.
|
||||||
# base_submission.reagents = []
|
|
||||||
result = None
|
result = None
|
||||||
else:
|
else:
|
||||||
self.app.ctx.database_session.rollback()
|
self.app.ctx.database_session.rollback()
|
||||||
@@ -347,7 +331,7 @@ class SubmissionFormWidget(QWidget):
|
|||||||
self.app.report.add_result(report)
|
self.app.report.add_result(report)
|
||||||
self.app.result_reporter()
|
self.app.result_reporter()
|
||||||
|
|
||||||
def export_csv_function(self, fname:Path|None=None):
|
def export_csv_function(self, fname: Path | None = None):
|
||||||
"""
|
"""
|
||||||
Save the submission's csv file.
|
Save the submission's csv file.
|
||||||
|
|
||||||
@@ -397,7 +381,7 @@ class SubmissionFormWidget(QWidget):
|
|||||||
value = getattr(self, item)
|
value = getattr(self, item)
|
||||||
# logger.debug(f"Setting {item}")
|
# logger.debug(f"Setting {item}")
|
||||||
info[item] = value
|
info[item] = value
|
||||||
for k,v in info.items():
|
for k, v in info.items():
|
||||||
self.pyd.set_attribute(key=k, value=v)
|
self.pyd.set_attribute(key=k, value=v)
|
||||||
# NOTE: return submission
|
# NOTE: return submission
|
||||||
report.add_result(report)
|
report.add_result(report)
|
||||||
@@ -405,20 +389,20 @@ class SubmissionFormWidget(QWidget):
|
|||||||
|
|
||||||
class InfoItem(QWidget):
|
class InfoItem(QWidget):
|
||||||
|
|
||||||
def __init__(self, parent: QWidget, key:str, value:dict, submission_type:str|None=None) -> None:
|
def __init__(self, parent: QWidget, key: str, value: dict, submission_type: str | None = None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
self.label = self.ParsedQLabel(key=key, value=value)
|
self.label = self.ParsedQLabel(key=key, value=value)
|
||||||
self.input: QWidget = self.set_widget(parent=self, key=key, value=value, submission_type=submission_type)
|
self.input: QWidget = self.set_widget(parent=self, key=key, value=value, submission_type=submission_type)
|
||||||
self.setObjectName(key)
|
self.setObjectName(key)
|
||||||
try:
|
try:
|
||||||
self.missing:bool = value['missing']
|
self.missing: bool = value['missing']
|
||||||
except (TypeError, KeyError):
|
except (TypeError, KeyError):
|
||||||
self.missing:bool = True
|
self.missing: bool = True
|
||||||
if self.input != None:
|
if self.input is not None:
|
||||||
layout.addWidget(self.label)
|
layout.addWidget(self.label)
|
||||||
layout.addWidget(self.input)
|
layout.addWidget(self.input)
|
||||||
layout.setContentsMargins(0,0,0,0)
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
match self.input:
|
match self.input:
|
||||||
case QComboBox():
|
case QComboBox():
|
||||||
@@ -446,7 +430,7 @@ class SubmissionFormWidget(QWidget):
|
|||||||
return None, None
|
return None, None
|
||||||
return self.input.objectName(), dict(value=value, missing=self.missing)
|
return self.input.objectName(), dict(value=value, missing=self.missing)
|
||||||
|
|
||||||
def set_widget(self, parent: QWidget, key:str, value:dict, submission_type:str|None=None) -> QWidget:
|
def set_widget(self, parent: QWidget, key: str, value: dict, submission_type: str | None = None) -> QWidget:
|
||||||
"""
|
"""
|
||||||
Creates form widget
|
Creates form widget
|
||||||
|
|
||||||
@@ -480,11 +464,12 @@ class SubmissionFormWidget(QWidget):
|
|||||||
case 'extraction_kit':
|
case 'extraction_kit':
|
||||||
# if extraction kit not available, all other values fail
|
# if extraction kit not available, all other values fail
|
||||||
if not check_not_nan(value):
|
if not check_not_nan(value):
|
||||||
msg = AlertPop(message="Make sure to check your extraction kit in the excel sheet!", status="warning")
|
msg = AlertPop(message="Make sure to check your extraction kit in the excel sheet!",
|
||||||
|
status="warning")
|
||||||
msg.exec()
|
msg.exec()
|
||||||
# create combobox to hold looked up kits
|
# NOTE: create combobox to hold looked up kits
|
||||||
add_widget = QComboBox()
|
add_widget = QComboBox()
|
||||||
# lookup existing kits by 'submission_type' decided on by sheetparser
|
# NOTE: lookup existing kits by 'submission_type' decided on by sheetparser
|
||||||
# logger.debug(f"Looking up kits used for {submission_type}")
|
# logger.debug(f"Looking up kits used for {submission_type}")
|
||||||
uses = [item.name for item in KitType.query(used_for=submission_type)]
|
uses = [item.name for item in KitType.query(used_for=submission_type)]
|
||||||
obj.uses = uses
|
obj.uses = uses
|
||||||
@@ -497,14 +482,13 @@ class SubmissionFormWidget(QWidget):
|
|||||||
logger.error(f"Couldn't find {obj.prsr.sub['extraction_kit']}")
|
logger.error(f"Couldn't find {obj.prsr.sub['extraction_kit']}")
|
||||||
obj.ext_kit = uses[0]
|
obj.ext_kit = uses[0]
|
||||||
add_widget.addItems(uses)
|
add_widget.addItems(uses)
|
||||||
|
|
||||||
case 'submitted_date':
|
case 'submitted_date':
|
||||||
# uses base calendar
|
# NOTE: uses base calendar
|
||||||
add_widget = QDateEdit(calendarPopup=True)
|
add_widget = QDateEdit(calendarPopup=True)
|
||||||
# sets submitted date based on date found in excel sheet
|
# NOTE: sets submitted date based on date found in excel sheet
|
||||||
try:
|
try:
|
||||||
add_widget.setDate(value)
|
add_widget.setDate(value)
|
||||||
# if not found, use today
|
# NOTE: if not found, use today
|
||||||
except:
|
except:
|
||||||
add_widget.setDate(date.today())
|
add_widget.setDate(date.today())
|
||||||
case 'submission_category':
|
case 'submission_category':
|
||||||
@@ -517,11 +501,11 @@ class SubmissionFormWidget(QWidget):
|
|||||||
cats.insert(0, cats.pop(cats.index(submission_type)))
|
cats.insert(0, cats.pop(cats.index(submission_type)))
|
||||||
add_widget.addItems(cats)
|
add_widget.addItems(cats)
|
||||||
case _:
|
case _:
|
||||||
# anything else gets added in as a line edit
|
# NOTE: anything else gets added in as a line edit
|
||||||
add_widget = QLineEdit()
|
add_widget = QLineEdit()
|
||||||
# logger.debug(f"Setting widget text to {str(value).replace('_', ' ')}")
|
# logger.debug(f"Setting widget text to {str(value).replace('_', ' ')}")
|
||||||
add_widget.setText(str(value).replace("_", " "))
|
add_widget.setText(str(value).replace("_", " "))
|
||||||
if add_widget != None:
|
if add_widget is not None:
|
||||||
add_widget.setObjectName(key)
|
add_widget.setObjectName(key)
|
||||||
add_widget.setParent(parent)
|
add_widget.setParent(parent)
|
||||||
return add_widget
|
return add_widget
|
||||||
@@ -535,7 +519,7 @@ class SubmissionFormWidget(QWidget):
|
|||||||
|
|
||||||
class ParsedQLabel(QLabel):
|
class ParsedQLabel(QLabel):
|
||||||
|
|
||||||
def __init__(self, key:str, value:dict, title:bool=True, label_name:str|None=None):
|
def __init__(self, key: str, value: dict, title: bool = True, label_name: str | None = None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
try:
|
try:
|
||||||
check = not value['missing']
|
check = not value['missing']
|
||||||
@@ -546,7 +530,7 @@ class SubmissionFormWidget(QWidget):
|
|||||||
else:
|
else:
|
||||||
self.setObjectName(f"{key}_label")
|
self.setObjectName(f"{key}_label")
|
||||||
if title:
|
if title:
|
||||||
output = key.replace('_', ' ').title()
|
output = key.replace('_', ' ').title().replace("Rsl", "RSL").replace("Pcr", "PCR")
|
||||||
else:
|
else:
|
||||||
output = key.replace('_', ' ')
|
output = key.replace('_', ' ')
|
||||||
if check:
|
if check:
|
||||||
@@ -554,7 +538,7 @@ class SubmissionFormWidget(QWidget):
|
|||||||
else:
|
else:
|
||||||
self.setText(f"MISSING {output}")
|
self.setText(f"MISSING {output}")
|
||||||
|
|
||||||
def updated(self, key:str, title:bool=True):
|
def updated(self, key: str, title: bool = True):
|
||||||
"""
|
"""
|
||||||
Mark widget as updated
|
Mark widget as updated
|
||||||
|
|
||||||
@@ -563,34 +547,29 @@ class SubmissionFormWidget(QWidget):
|
|||||||
title (bool, optional): Use title case. Defaults to True.
|
title (bool, optional): Use title case. Defaults to True.
|
||||||
"""
|
"""
|
||||||
if title:
|
if title:
|
||||||
output = key.replace('_', ' ').title()
|
output = key.replace('_', ' ').title().replace("Rsl", "RSL").replace("Pcr", "PCR")
|
||||||
else:
|
else:
|
||||||
output = key.replace('_', ' ')
|
output = key.replace('_', ' ')
|
||||||
self.setText(f"UPDATED {output}")
|
self.setText(f"UPDATED {output}")
|
||||||
|
|
||||||
class ReagentFormWidget(QWidget):
|
class ReagentFormWidget(QWidget):
|
||||||
|
|
||||||
def __init__(self, parent:QWidget, reagent:PydReagent, extraction_kit:str):
|
def __init__(self, parent: QWidget, reagent: PydReagent, extraction_kit: str):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.app = self.parent().parent().parent().parent().parent().parent().parent().parent()
|
self.app = self.parent().parent().parent().parent().parent().parent().parent().parent()
|
||||||
self.reagent = reagent
|
self.reagent = reagent
|
||||||
self.extraction_kit = extraction_kit
|
self.extraction_kit = extraction_kit
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
# layout = QGridLayout()
|
|
||||||
# self.check_box = QCheckBox(self)
|
|
||||||
# self.check_box.setChecked(True)
|
|
||||||
# self.check_box.stateChanged.connect(self.check_uncheck)
|
|
||||||
# layout.addWidget(self.check_box, 0,0)
|
|
||||||
self.label = self.ReagentParsedLabel(reagent=reagent)
|
self.label = self.ReagentParsedLabel(reagent=reagent)
|
||||||
layout.addWidget(self.label)
|
layout.addWidget(self.label)
|
||||||
self.lot = self.ReagentLot(reagent=reagent, extraction_kit=extraction_kit)
|
self.lot = self.ReagentLot(reagent=reagent, extraction_kit=extraction_kit)
|
||||||
layout.addWidget(self.lot)
|
layout.addWidget(self.lot)
|
||||||
# Remove spacing between reagents
|
# NOTE: Remove spacing between reagents
|
||||||
layout.setContentsMargins(0,0,0,0)
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
self.setObjectName(reagent.name)
|
self.setObjectName(reagent.name)
|
||||||
self.missing = reagent.missing
|
self.missing = reagent.missing
|
||||||
# If changed set self.missing to True and update self.label
|
# NOTE: If changed set self.missing to True and update self.label
|
||||||
self.lot.currentTextChanged.connect(self.updated)
|
self.lot.currentTextChanged.connect(self.updated)
|
||||||
|
|
||||||
def parse_form(self) -> Tuple[PydReagent, dict]:
|
def parse_form(self) -> Tuple[PydReagent, dict]:
|
||||||
@@ -600,28 +579,30 @@ class SubmissionFormWidget(QWidget):
|
|||||||
Returns:
|
Returns:
|
||||||
Tuple[PydReagent, dict]: PydReagent and Report(?)
|
Tuple[PydReagent, dict]: PydReagent and Report(?)
|
||||||
"""
|
"""
|
||||||
# if not self.check_box.isChecked():
|
|
||||||
# return None, None
|
|
||||||
lot = self.lot.currentText()
|
lot = self.lot.currentText()
|
||||||
# logger.debug(f"Using this lot for the reagent {self.reagent}: {lot}")
|
# logger.debug(f"Using this lot for the reagent {self.reagent}: {lot}")
|
||||||
wanted_reagent = Reagent.query(lot_number=lot, reagent_type=self.reagent.type)
|
wanted_reagent = Reagent.query(lot_number=lot, reagent_type=self.reagent.type)
|
||||||
# NOTE: if reagent doesn't exist in database, offer to add it (uses App.add_reagent)
|
# NOTE: if reagent doesn't exist in database, offer to add it (uses App.add_reagent)
|
||||||
if wanted_reagent == None:
|
if wanted_reagent == None:
|
||||||
dlg = QuestionAsker(title=f"Add {lot}?", message=f"Couldn't find reagent type {self.reagent.type}: {lot} in the database.\n\nWould you like to add it?")
|
dlg = QuestionAsker(title=f"Add {lot}?",
|
||||||
|
message=f"Couldn't find reagent type {self.reagent.type}: {lot} in the database.\n\nWould you like to add it?")
|
||||||
if dlg.exec():
|
if dlg.exec():
|
||||||
wanted_reagent = self.parent().parent().add_reagent(reagent_lot=lot, reagent_type=self.reagent.type, expiry=self.reagent.expiry, name=self.reagent.name)
|
wanted_reagent = self.parent().parent().add_reagent(reagent_lot=lot, reagent_type=self.reagent.type,
|
||||||
|
expiry=self.reagent.expiry,
|
||||||
|
name=self.reagent.name)
|
||||||
return wanted_reagent, None
|
return wanted_reagent, None
|
||||||
else:
|
else:
|
||||||
# NOTE: In this case we will have an empty reagent and the submission will fail kit integrity check
|
# NOTE: In this case we will have an empty reagent and the submission will fail kit integrity check
|
||||||
# logger.debug("Will not add reagent.")
|
# logger.debug("Will not add reagent.")
|
||||||
return None, Result(msg="Failed integrity check", status="Critical")
|
return None, Result(msg="Failed integrity check", status="Critical")
|
||||||
else:
|
else:
|
||||||
# Since this now gets passed in directly from the parser -> pyd -> form and the parser gets the name
|
# NOTE: Since this now gets passed in directly from the parser -> pyd -> form and the parser gets the name
|
||||||
# from the db, it should no longer be necessary to query the db with reagent/kit, but with rt name directly.
|
# from the db, it should no longer be necessary to query the db with reagent/kit, but with rt name directly.
|
||||||
rt = ReagentType.query(name=self.reagent.type)
|
rt = ReagentType.query(name=self.reagent.type)
|
||||||
if rt == None:
|
if rt == None:
|
||||||
rt = ReagentType.query(kit_type=self.extraction_kit, reagent=wanted_reagent)
|
rt = ReagentType.query(kit_type=self.extraction_kit, reagent=wanted_reagent)
|
||||||
return PydReagent(name=wanted_reagent.name, lot=wanted_reagent.lot, type=rt.name, expiry=wanted_reagent.expiry, missing=False), None
|
return PydReagent(name=wanted_reagent.name, lot=wanted_reagent.lot, type=rt.name,
|
||||||
|
expiry=wanted_reagent.expiry, missing=False), None
|
||||||
|
|
||||||
def updated(self):
|
def updated(self):
|
||||||
"""
|
"""
|
||||||
@@ -632,7 +613,7 @@ class SubmissionFormWidget(QWidget):
|
|||||||
|
|
||||||
class ReagentParsedLabel(QLabel):
|
class ReagentParsedLabel(QLabel):
|
||||||
|
|
||||||
def __init__(self, reagent:PydReagent):
|
def __init__(self, reagent: PydReagent):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
try:
|
try:
|
||||||
check = not reagent.missing
|
check = not reagent.missing
|
||||||
@@ -644,7 +625,7 @@ class SubmissionFormWidget(QWidget):
|
|||||||
else:
|
else:
|
||||||
self.setText(f"MISSING {reagent.type}")
|
self.setText(f"MISSING {reagent.type}")
|
||||||
|
|
||||||
def updated(self, reagent_type:str):
|
def updated(self, reagent_type: str):
|
||||||
"""
|
"""
|
||||||
Marks widget as updated
|
Marks widget as updated
|
||||||
|
|
||||||
@@ -655,7 +636,7 @@ class SubmissionFormWidget(QWidget):
|
|||||||
|
|
||||||
class ReagentLot(QComboBox):
|
class ReagentLot(QComboBox):
|
||||||
|
|
||||||
def __init__(self, reagent, extraction_kit:str) -> None:
|
def __init__(self, reagent, extraction_kit: str) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.setEditable(True)
|
self.setEditable(True)
|
||||||
# logger.debug(f"Attempting lookup of reagents by type: {reagent.type}")
|
# logger.debug(f"Attempting lookup of reagents by type: {reagent.type}")
|
||||||
@@ -664,7 +645,7 @@ class SubmissionFormWidget(QWidget):
|
|||||||
relevant_reagents = [str(item.lot) for item in lookup]
|
relevant_reagents = [str(item.lot) for item in lookup]
|
||||||
output_reg = []
|
output_reg = []
|
||||||
for rel_reagent in relevant_reagents:
|
for rel_reagent in relevant_reagents:
|
||||||
# extract strings from any sets.
|
# NOTE: extract strings from any sets.
|
||||||
if isinstance(rel_reagent, set):
|
if isinstance(rel_reagent, set):
|
||||||
for thing in rel_reagent:
|
for thing in rel_reagent:
|
||||||
output_reg.append(thing)
|
output_reg.append(thing)
|
||||||
@@ -677,7 +658,8 @@ class SubmissionFormWidget(QWidget):
|
|||||||
if check_not_nan(reagent.lot):
|
if check_not_nan(reagent.lot):
|
||||||
relevant_reagents.insert(0, str(reagent.lot))
|
relevant_reagents.insert(0, str(reagent.lot))
|
||||||
else:
|
else:
|
||||||
looked_up_rt = KitTypeReagentTypeAssociation.query(reagent_type=reagent.type, kit_type=extraction_kit)
|
looked_up_rt = KitTypeReagentTypeAssociation.query(reagent_type=reagent.type,
|
||||||
|
kit_type=extraction_kit)
|
||||||
try:
|
try:
|
||||||
looked_up_reg = Reagent.query(lot_number=looked_up_rt.last_used)
|
looked_up_reg = Reagent.query(lot_number=looked_up_rt.last_used)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
|||||||
@@ -42,11 +42,11 @@
|
|||||||
{% block body %}
|
{% block body %}
|
||||||
<h2><u>Sample Details for {{ sample['Submitter ID'] }}</u></h2>
|
<h2><u>Sample Details for {{ sample['Submitter ID'] }}</u></h2>
|
||||||
<p>{% for key, value in sample.items() if key not in sample['excluded'] %}
|
<p>{% for key, value in sample.items() if key not in sample['excluded'] %}
|
||||||
<b>{{ key }}: </b>{{ value }}<br>
|
<b>{{ key | replace("_", " ") | title }}: </b>{{ value }}<br>
|
||||||
{% endfor %}</p>
|
{% endfor %}</p>
|
||||||
{% if sample['submissions'] %}<h2>Submissions:</h2>
|
{% if sample['submissions'] %}<h2>Submissions:</h2>
|
||||||
{% for submission in sample['submissions'] %}
|
{% for submission in sample['submissions'] %}
|
||||||
<p id="{{ submission['Plate Name'] }}"><b>{{ submission['Plate Name'] }}:</b> {{ submission['Well'] }}</p>
|
<p id="{{ submission['plate_name'] }}"><b>{{ submission['plate_name'] }}:</b> {{ submission['well'] }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -57,8 +57,8 @@
|
|||||||
backend = channel.objects.backend;
|
backend = channel.objects.backend;
|
||||||
});
|
});
|
||||||
{% for submission in sample['submissions'] %}
|
{% for submission in sample['submissions'] %}
|
||||||
document.getElementById("{{ submission['Plate Name'] }}").addEventListener("dblclick", function(){
|
document.getElementById("{{ submission['plate_name'] }}").addEventListener("dblclick", function(){
|
||||||
backend.submission_details("{{ submission['Plate Name'] }}");
|
backend.submission_details("{{ submission['plate_name'] }}");
|
||||||
});
|
});
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
<!-- {% set excluded = ['reagents', 'samples', 'controls', 'extraction_info', 'pcr_info', 'comment', 'barcode', 'platemap', 'export_map', 'equipment'] %} -->
|
<!-- {% set excluded = ['reagents', 'samples', 'controls', 'extraction_info', 'pcr_info', 'comment', 'barcode', 'platemap', 'export_map', 'equipment'] %} -->
|
||||||
<h2><u>Submission Details for {{ sub['Plate Number'] }}</u></h2> {% if sub['barcode'] %}<img align='right' height="30px" width="120px" src="data:image/jpeg;base64,{{ sub['barcode'] | safe }}">{% endif %}
|
<h2><u>Submission Details for {{ sub['Plate Number'] }}</u></h2> {% if sub['barcode'] %}<img align='right' height="30px" width="120px" src="data:image/jpeg;base64,{{ sub['barcode'] | safe }}">{% endif %}
|
||||||
<p>{% for key, value in sub.items() if key not in sub['excluded'] %}
|
<p>{% for key, value in sub.items() if key not in sub['excluded'] %}
|
||||||
<b>{{ key }}: </b>{% if key=='Cost' %}{% if sub['Cost'] %} {{ "${:,.2f}".format(value) }}{% endif %}{% else %}{{ value }}{% endif %}<br>
|
<b>{{ key | replace("_", " ") | title | replace("Pcr", "PCR") }}: </b>{% if key=='Cost' %}{% if sub['cost'] %} {{ "${:,.2f}".format(value) }}{% endif %}{% else %}{{ value }}{% endif %}<br>
|
||||||
{% endfor %}</p>
|
{% endfor %}</p>
|
||||||
<h3><u>Reagents:</u></h3>
|
<h3><u>Reagents:</u></h3>
|
||||||
<p>{% for item in sub['reagents'] %}
|
<p>{% for item in sub['reagents'] %}
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
{% if sub['samples'] %}
|
{% if sub['samples'] %}
|
||||||
<h3><u>Samples:</u></h3>
|
<h3><u>Samples:</u></h3>
|
||||||
<p>{% for item in sub['samples'] %}
|
<p>{% for item in sub['samples'] %}
|
||||||
<b>{{ item['Well'] }}:</b> {% if item['Organism'] %} {{ item['Name'] }} - ({{ item['Organism']|replace('\n\t', '<br> ') }}){% else %} {{ item['Name']|replace('\n\t', '<br> ') }}{% endif %}<br>
|
<b>{{ item['well'] }}:</b> {% if item['organism'] %} {{ item['name'] }} - ({{ item['organism']|replace('\n\t', '<br> ') }}){% else %} {{ item['name']|replace('\n\t', '<br> ') }}{% endif %}<br>
|
||||||
{% endfor %}</p>
|
{% endfor %}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if sub['controls'] %}
|
{% if sub['controls'] %}
|
||||||
@@ -129,12 +129,12 @@
|
|||||||
backend = channel.objects.backend;
|
backend = channel.objects.backend;
|
||||||
});
|
});
|
||||||
{% for sample in sub['samples'] %}
|
{% for sample in sub['samples'] %}
|
||||||
document.getElementById("{{sample['Submitter ID']}}").addEventListener("dblclick", function(){
|
document.getElementById("{{ sample['submitter_id'] }}").addEventListener("dblclick", function(){
|
||||||
backend.sample_details("{{ sample['Submitter ID'] }}");
|
backend.sample_details("{{ sample['submitter_id'] }}");
|
||||||
});
|
});
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
document.getElementById("sign_btn").addEventListener("click", function(){
|
document.getElementById("sign_btn").addEventListener("click", function(){
|
||||||
backend.sign_off("{{ sub['Plate Number'] }}");
|
backend.sign_off("{{ sub['plate_number'] }}");
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<div class="gallery" style="display: grid;grid-template-columns: repeat({{ PLATE_COLUMNS }}, 7.5vw);grid-template-rows: repeat({{ PLATE_ROWS }}, 7.5vw);grid-gap: 2px;">
|
<div class="gallery" style="display: grid;grid-template-columns: repeat({{ PLATE_COLUMNS }}, 7.5vw);grid-template-rows: repeat({{ PLATE_ROWS }}, 7.5vw);grid-gap: 2px;">
|
||||||
{% for sample in samples %}
|
{% for sample in samples %}
|
||||||
<div class="well" id="{{sample['Submitter ID']}}" style="background-color: {{sample['background_color']}};
|
<div class="well" id="{{sample['submitter_id']}}" style="background-color: {{sample['background_color']}};
|
||||||
border: 1px solid #000;
|
border: 1px solid #000;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
grid-column-start: {{sample['Column']}};
|
grid-column-start: {{sample['column']}};
|
||||||
grid-column-end: {{sample['Column']}};
|
grid-column-end: {{sample['column']}};
|
||||||
grid-row-start: {{sample['Row']}};
|
grid-row-start: {{sample['row']}};
|
||||||
grid-row-end: {{sample['Row']}};
|
grid-row-end: {{sample['row']}};
|
||||||
display: flex;
|
display: flex;
|
||||||
">
|
">
|
||||||
<div class="tooltip" style="font-size: 0.5em; text-align: center; word-wrap: break-word;">{{ sample['Name'] }}
|
<div class="tooltip" style="font-size: 0.5em; text-align: center; word-wrap: break-word;">{{ sample['name'] }}
|
||||||
<span class="tooltiptext">{{ sample['tooltip'] }}</span>
|
<span class="tooltiptext">{{ sample['tooltip'] }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Sample name: {{ fields['Submitter ID'] }}<br>
|
Sample name: {{ fields['submitter_id'] }}<br>
|
||||||
{% if fields['Organism'] %}Organism: {{ fields['Organism'] }}<br>{% endif %}
|
{% if fields['organism'] %}Organism: {{ fields['organism'] }}<br>{% endif %}
|
||||||
{% if fields['Concentration'] %}Concentration: {{ fields['Concentration'] }}<br>{% endif %}
|
{% if fields['concentration'] %}Concentration: {{ fields['concentration'] }}<br>{% endif %}
|
||||||
Well: {{ fields['Well'] }}<!--{{ fields['column'] }}-->
|
Well: {{ fields['well'] }}<!--{{ fields['column'] }}-->
|
||||||
@@ -86,7 +86,7 @@ def check_not_nan(cell_contents) -> bool:
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"Check encountered unknown error: {type(e).__name__} - {e}")
|
logger.error(f"Check encountered unknown error: {type(e).__name__} - {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -228,7 +228,7 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
database_path = database_path
|
database_path = database_path
|
||||||
else:
|
else:
|
||||||
raise FileNotFoundError("No database file found. Exiting program.")
|
raise FileNotFoundError("No database file found. Exiting program.")
|
||||||
logger.debug(f"Using {database_path} for database file.")
|
logger.info(f"Using {database_path} for database file.")
|
||||||
engine = create_engine(f"sqlite:///{database_path}") #, echo=True, future=True)
|
engine = create_engine(f"sqlite:///{database_path}") #, echo=True, future=True)
|
||||||
session = Session(engine)
|
session = Session(engine)
|
||||||
# metadata.session = session
|
# metadata.session = session
|
||||||
|
|||||||
Reference in New Issue
Block a user