Various bug fixes.

This commit is contained in:
lwark
2024-05-30 13:54:35 -05:00
parent ecfc71bcc5
commit 5c52c02f7e
18 changed files with 447 additions and 375 deletions

View File

@@ -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:

View File

@@ -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)
else:
model = cls.find_polymorphic_subclass(polymorphic_identity=sample_type) 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)}") # 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

View File

@@ -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

View File

@@ -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:

View File

@@ -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():

View File

@@ -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:
logger.debug(f"Incoming regex: {regex}")
try:
regex = re.compile(rf'{regex}', re.IGNORECASE | re.VERBOSE) 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():

View File

@@ -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,6 +463,9 @@ class PydSubmission(BaseModel, extra='allow'):
return value return value
else: else:
# logger.debug("Constructing plate name.") # logger.debug("Constructing plate name.")
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, 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)

View File

@@ -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):

View File

@@ -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()

View 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)

View File

@@ -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,18 +47,23 @@ 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)
match sub:
case BasicSubmission():
self.submission_details(submission=sub) self.submission_details(submission=sub)
self.rsl_plate_num = sub.rsl_plate_num 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.
""" """
if isinstance(sample, str):
sample = BasicSample.query(submitter_id=sample) 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)
@@ -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

View File

@@ -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)

View File

@@ -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)
@@ -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,21 +129,22 @@ 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:
@@ -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
@@ -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:
@@ -281,7 +266,7 @@ 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
@@ -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()
@@ -415,7 +399,7 @@ class SubmissionFormWidget(QWidget):
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)
@@ -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
@@ -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:
@@ -563,7 +547,7 @@ 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}")
@@ -576,21 +560,16 @@ class SubmissionFormWidget(QWidget):
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):
""" """
@@ -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:

View File

@@ -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'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ key }}: </b>{{ value }}<br> &nbsp;&nbsp;&nbsp;&nbsp;<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>

View File

@@ -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>&nbsp;&nbsp;&nbsp;{% 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>&nbsp;&nbsp;&nbsp;{% 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'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ key }}: </b>{% if key=='Cost' %}{% if sub['Cost'] %} {{ "${:,.2f}".format(value) }}{% endif %}{% else %}{{ value }}{% endif %}<br> &nbsp;&nbsp;&nbsp;&nbsp;<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'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['Well'] }}:</b> {% if item['Organism'] %} {{ item['Name'] }} - ({{ item['Organism']|replace('\n\t', '<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;') }}){% else %} {{ item['Name']|replace('\n\t', '<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;') }}{% endif %}<br> &nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['well'] }}:</b> {% if item['organism'] %} {{ item['name'] }} - ({{ item['organism']|replace('\n\t', '<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;') }}){% else %} {{ item['name']|replace('\n\t', '<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;') }}{% 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>

View File

@@ -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>

View File

@@ -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'] }}-->

View File

@@ -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