post documentation and code clean-up.
This commit is contained in:
@@ -9,6 +9,7 @@ def set_sqlite_pragma(dbapi_connection, connection_record):
|
|||||||
"""
|
"""
|
||||||
*should* allow automatic creation of foreign keys in the database
|
*should* allow automatic creation of foreign keys in the database
|
||||||
I have no idea how it actually works.
|
I have no idea how it actually works.
|
||||||
|
Listens for connect and then turns on foreign keys?
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
dbapi_connection (_type_): _description_
|
dbapi_connection (_type_): _description_
|
||||||
|
|||||||
@@ -1581,12 +1581,12 @@ 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 report:
|
||||||
|
return output
|
||||||
if self.artic_technician in [None, "None"]:
|
if self.artic_technician in [None, "None"]:
|
||||||
output['artic_technician'] = self.technician
|
output['artic_technician'] = self.technician
|
||||||
else:
|
else:
|
||||||
output['artic_technician'] = self.artic_technician
|
output['artic_technician'] = self.artic_technician
|
||||||
if report:
|
|
||||||
return output
|
|
||||||
output['gel_info'] = self.gel_info
|
output['gel_info'] = self.gel_info
|
||||||
output['gel_image_path'] = self.gel_image
|
output['gel_image_path'] = self.gel_image
|
||||||
output['dna_core_submission_number'] = self.dna_core_submission_number
|
output['dna_core_submission_number'] = self.dna_core_submission_number
|
||||||
@@ -2253,6 +2253,12 @@ class BasicSample(BaseClass):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_searchables(cls):
|
def get_searchables(cls):
|
||||||
|
"""
|
||||||
|
Delivers a list of fields that can be used in fuzzy search.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[str]: List of fields.
|
||||||
|
"""
|
||||||
return [dict(label="Submitter ID", field="submitter_id")]
|
return [dict(label="Submitter ID", field="submitter_id")]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -2381,22 +2387,14 @@ class WastewaterSample(BasicSample):
|
|||||||
output_dict["submitter_id"] = output_dict['ww_full_sample_id']
|
output_dict["submitter_id"] = output_dict['ww_full_sample_id']
|
||||||
return output_dict
|
return output_dict
|
||||||
|
|
||||||
def get_previous_ww_submission(self, current_artic_submission: WastewaterArtic):
|
|
||||||
try:
|
|
||||||
plates = [item['plate'] for item in current_artic_submission.source_plates]
|
|
||||||
except TypeError as e:
|
|
||||||
logger.error(f"source_plates must not be present")
|
|
||||||
plates = [item.rsl_plate_num for item in
|
|
||||||
self.submissions[:self.submissions.index(current_artic_submission)]]
|
|
||||||
subs = [sub for sub in self.submissions if sub.rsl_plate_num in plates]
|
|
||||||
# logger.debug(f"Submissions: {subs}")
|
|
||||||
try:
|
|
||||||
return subs[-1]
|
|
||||||
except IndexError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_searchables(cls):
|
def get_searchables(cls) -> List[str]:
|
||||||
|
"""
|
||||||
|
Delivers a list of fields that can be used in fuzzy search. Extends parent.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[str]: List of fields.
|
||||||
|
"""
|
||||||
searchables = super().get_searchables()
|
searchables = super().get_searchables()
|
||||||
for item in ["ww_processing_num", "ww_full_sample_id", "rsl_number"]:
|
for item in ["ww_processing_num", "ww_full_sample_id", "rsl_number"]:
|
||||||
label = item.strip("ww_").replace("_", " ").replace("rsl", "RSL").title()
|
label = item.strip("ww_").replace("_", " ").replace("rsl", "RSL").title()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'''
|
'''
|
||||||
Contains pandas convenience functions for interacting with excel workbooks
|
Contains pandas and openpyxl convenience functions for interacting with excel workbooks
|
||||||
'''
|
'''
|
||||||
|
|
||||||
from .reports import *
|
from .reports import *
|
||||||
from .parser import *
|
from .parser import *
|
||||||
|
from .writer import *
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
'''
|
'''
|
||||||
contains parser object for pulling values from client generated submission sheets.
|
contains parser objects for pulling values from client generated submission sheets.
|
||||||
'''
|
'''
|
||||||
import sys
|
import sys
|
||||||
from copy import copy
|
from copy import copy
|
||||||
@@ -78,7 +78,7 @@ class SheetParser(object):
|
|||||||
|
|
||||||
def parse_reagents(self, extraction_kit: str | None = None):
|
def parse_reagents(self, extraction_kit: str | None = None):
|
||||||
"""
|
"""
|
||||||
Pulls reagent info from the excel sheet
|
Calls reagent parser class to pull info from the excel sheet
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
extraction_kit (str | None, optional): Relevant extraction kit for reagent map. Defaults to None.
|
extraction_kit (str | None, optional): Relevant extraction kit for reagent map. Defaults to None.
|
||||||
@@ -91,16 +91,22 @@ class SheetParser(object):
|
|||||||
|
|
||||||
def parse_samples(self):
|
def parse_samples(self):
|
||||||
"""
|
"""
|
||||||
Pulls sample info from the excel sheet
|
Calls sample parser to pull info from the excel sheet
|
||||||
"""
|
"""
|
||||||
parser = SampleParser(xl=self.xl, submission_type=self.submission_type)
|
parser = SampleParser(xl=self.xl, submission_type=self.submission_type)
|
||||||
self.sub['samples'] = parser.reconcile_samples()
|
self.sub['samples'] = parser.reconcile_samples()
|
||||||
|
|
||||||
def parse_equipment(self):
|
def parse_equipment(self):
|
||||||
|
"""
|
||||||
|
Calls equipment parser to pull info from the excel sheet
|
||||||
|
"""
|
||||||
parser = EquipmentParser(xl=self.xl, submission_type=self.submission_type)
|
parser = EquipmentParser(xl=self.xl, submission_type=self.submission_type)
|
||||||
self.sub['equipment'] = parser.parse_equipment()
|
self.sub['equipment'] = parser.parse_equipment()
|
||||||
|
|
||||||
def parse_tips(self):
|
def parse_tips(self):
|
||||||
|
"""
|
||||||
|
Calls tips parser to pull info from the excel sheet
|
||||||
|
"""
|
||||||
parser = TipParser(xl=self.xl, submission_type=self.submission_type)
|
parser = TipParser(xl=self.xl, submission_type=self.submission_type)
|
||||||
self.sub['tips'] = parser.parse_tips()
|
self.sub['tips'] = parser.parse_tips()
|
||||||
|
|
||||||
@@ -160,8 +166,16 @@ class SheetParser(object):
|
|||||||
|
|
||||||
|
|
||||||
class InfoParser(object):
|
class InfoParser(object):
|
||||||
|
"""
|
||||||
|
Object to parse generic info from excel sheet.
|
||||||
|
"""
|
||||||
def __init__(self, xl: Workbook, submission_type: str|SubmissionType, sub_object: BasicSubmission|None=None):
|
def __init__(self, xl: Workbook, submission_type: str|SubmissionType, sub_object: BasicSubmission|None=None):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
xl (Workbook): Openpyxl workbook from submitted excel file.
|
||||||
|
submission_type (str | SubmissionType): Type of submission expected (Wastewater, Bacterial Culture, etc.)
|
||||||
|
sub_object (BasicSubmission | None, optional): Submission object holding methods. Defaults to None.
|
||||||
|
"""
|
||||||
logger.info(f"\n\nHello from InfoParser!\n\n")
|
logger.info(f"\n\nHello from InfoParser!\n\n")
|
||||||
if isinstance(submission_type, str):
|
if isinstance(submission_type, str):
|
||||||
submission_type = SubmissionType.query(name=submission_type)
|
submission_type = SubmissionType.query(name=submission_type)
|
||||||
@@ -221,6 +235,7 @@ class InfoParser(object):
|
|||||||
new['name'] = k
|
new['name'] = k
|
||||||
relevant.append(new)
|
relevant.append(new)
|
||||||
# logger.debug(f"relevant map for {sheet}: {pformat(relevant)}")
|
# logger.debug(f"relevant map for {sheet}: {pformat(relevant)}")
|
||||||
|
# NOTE: make sure relevant is not an empty list.
|
||||||
if not relevant:
|
if not relevant:
|
||||||
continue
|
continue
|
||||||
for item in relevant:
|
for item in relevant:
|
||||||
@@ -231,6 +246,7 @@ class InfoParser(object):
|
|||||||
case "submission_type":
|
case "submission_type":
|
||||||
value, missing = is_missing(value)
|
value, missing = is_missing(value)
|
||||||
value = value.title()
|
value = value.title()
|
||||||
|
# NOTE: is field a JSON?
|
||||||
case thing if thing in self.sub_object.jsons():
|
case thing if thing in self.sub_object.jsons():
|
||||||
value, missing = is_missing(value)
|
value, missing = is_missing(value)
|
||||||
if missing: continue
|
if missing: continue
|
||||||
@@ -248,12 +264,23 @@ class InfoParser(object):
|
|||||||
dicto[item['name']] = dict(value=value, missing=missing)
|
dicto[item['name']] = dict(value=value, missing=missing)
|
||||||
except (KeyError, IndexError):
|
except (KeyError, IndexError):
|
||||||
continue
|
continue
|
||||||
|
# Return after running the parser components held in submission object.
|
||||||
return self.sub_object.custom_info_parser(input_dict=dicto, xl=self.xl)
|
return self.sub_object.custom_info_parser(input_dict=dicto, xl=self.xl)
|
||||||
|
|
||||||
|
|
||||||
class ReagentParser(object):
|
class ReagentParser(object):
|
||||||
|
"""
|
||||||
|
Object to pull reagents from excel sheet.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, xl: Workbook, submission_type: str, extraction_kit: str, sub_object:BasicSubmission|None=None):
|
def __init__(self, xl: Workbook, submission_type: str, extraction_kit: str, sub_object:BasicSubmission|None=None):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
xl (Workbook): Openpyxl workbook from submitted excel file.
|
||||||
|
submission_type (str): Type of submission expected (Wastewater, Bacterial Culture, etc.)
|
||||||
|
extraction_kit (str): Extraction kit used.
|
||||||
|
sub_object (BasicSubmission | None, optional): Submission object holding methods. Defaults to None.
|
||||||
|
"""
|
||||||
# logger.debug("\n\nHello from ReagentParser!\n\n")
|
# logger.debug("\n\nHello from ReagentParser!\n\n")
|
||||||
self.submission_type_obj = submission_type
|
self.submission_type_obj = submission_type
|
||||||
self.sub_object = sub_object
|
self.sub_object = sub_object
|
||||||
@@ -284,7 +311,7 @@ class ReagentParser(object):
|
|||||||
pass
|
pass
|
||||||
return reagent_map
|
return reagent_map
|
||||||
|
|
||||||
def parse_reagents(self) -> List[PydReagent]:
|
def parse_reagents(self) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
Extracts reagent information from the excel form.
|
Extracts reagent information from the excel form.
|
||||||
|
|
||||||
@@ -312,7 +339,7 @@ class ReagentParser(object):
|
|||||||
comment = ""
|
comment = ""
|
||||||
except (KeyError, IndexError):
|
except (KeyError, IndexError):
|
||||||
listo.append(
|
listo.append(
|
||||||
PydReagent(role=item.strip(), lot=None, expiry=None, name=None, comment="", missing=True))
|
dict(role=item.strip(), lot=None, expiry=None, name=None, comment="", missing=True))
|
||||||
continue
|
continue
|
||||||
# NOTE: If the cell is blank tell the PydReagent
|
# NOTE: If the cell is blank tell the PydReagent
|
||||||
if check_not_nan(lot):
|
if check_not_nan(lot):
|
||||||
@@ -336,17 +363,17 @@ class ReagentParser(object):
|
|||||||
|
|
||||||
class SampleParser(object):
|
class SampleParser(object):
|
||||||
"""
|
"""
|
||||||
object to pull data for samples in excel sheet and construct individual sample objects
|
Object to pull data for samples in excel sheet and construct individual sample objects
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, xl: Workbook, submission_type: SubmissionType, sample_map: dict | None = None, sub_object:BasicSubmission|None=None) -> None:
|
def __init__(self, xl: Workbook, submission_type: SubmissionType, sample_map: dict | None = None, sub_object:BasicSubmission|None=None) -> None:
|
||||||
"""
|
"""
|
||||||
convert sample sub-dataframe to dictionary of records
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
df (pd.DataFrame): input sample dataframe
|
xl (Workbook): Openpyxl workbook from submitted excel file.
|
||||||
elution_map (pd.DataFrame | None, optional): optional map of elution plate. Defaults to None.
|
submission_type (SubmissionType): Type of submission expected (Wastewater, Bacterial Culture, etc.)
|
||||||
"""
|
sample_map (dict | None, optional): Locations in database where samples are found. Defaults to None.
|
||||||
|
sub_object (BasicSubmission | None, optional): Submission object holding methods. Defaults to None.
|
||||||
|
"""
|
||||||
# logger.debug("\n\nHello from SampleParser!\n\n")
|
# logger.debug("\n\nHello from SampleParser!\n\n")
|
||||||
self.samples = []
|
self.samples = []
|
||||||
self.xl = xl
|
self.xl = xl
|
||||||
@@ -383,10 +410,13 @@ class SampleParser(object):
|
|||||||
sample_info_map = sample_map
|
sample_info_map = sample_map
|
||||||
return sample_info_map
|
return sample_info_map
|
||||||
|
|
||||||
def parse_plate_map(self):
|
def parse_plate_map(self) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
Parse sample location/name from plate map
|
Parse sample location/name from plate map
|
||||||
"""
|
|
||||||
|
Returns:
|
||||||
|
List[dict]: List of sample ids and locations.
|
||||||
|
"""
|
||||||
invalids = [0, "0", "EMPTY"]
|
invalids = [0, "0", "EMPTY"]
|
||||||
smap = self.sample_info_map['plate_map']
|
smap = self.sample_info_map['plate_map']
|
||||||
ws = self.xl[smap['sheet']]
|
ws = self.xl[smap['sheet']]
|
||||||
@@ -412,7 +442,11 @@ class SampleParser(object):
|
|||||||
def parse_lookup_table(self) -> List[dict]:
|
def parse_lookup_table(self) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
Parse misc info from lookup table.
|
Parse misc info from lookup table.
|
||||||
"""
|
|
||||||
|
Returns:
|
||||||
|
List[dict]: List of basic sample info.
|
||||||
|
"""
|
||||||
|
|
||||||
lmap = self.sample_info_map['lookup_table']
|
lmap = self.sample_info_map['lookup_table']
|
||||||
ws = self.xl[lmap['sheet']]
|
ws = self.xl[lmap['sheet']]
|
||||||
lookup_samples = []
|
lookup_samples = []
|
||||||
@@ -460,7 +494,13 @@ class SampleParser(object):
|
|||||||
new_samples.append(PydSample(**translated_dict))
|
new_samples.append(PydSample(**translated_dict))
|
||||||
return result, new_samples
|
return result, new_samples
|
||||||
|
|
||||||
def reconcile_samples(self):
|
def reconcile_samples(self) -> List[dict]:
|
||||||
|
"""
|
||||||
|
Merges sample info from lookup table and plate map.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[dict]: Reconciled samples
|
||||||
|
"""
|
||||||
# TODO: Move to pydantic validator?
|
# TODO: Move to pydantic validator?
|
||||||
if self.plate_map_samples is None or self.lookup_samples is None:
|
if self.plate_map_samples is None or self.lookup_samples is None:
|
||||||
self.samples = self.lookup_samples or self.plate_map_samples
|
self.samples = self.lookup_samples or self.plate_map_samples
|
||||||
@@ -504,8 +544,15 @@ class SampleParser(object):
|
|||||||
|
|
||||||
|
|
||||||
class EquipmentParser(object):
|
class EquipmentParser(object):
|
||||||
|
"""
|
||||||
|
Object to pull data for equipment in excel sheet
|
||||||
|
"""
|
||||||
def __init__(self, xl: Workbook, submission_type: str|SubmissionType) -> None:
|
def __init__(self, xl: Workbook, submission_type: str|SubmissionType) -> None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
xl (Workbook): Openpyxl workbook from submitted excel file.
|
||||||
|
submission_type (str | SubmissionType): Type of submission expected (Wastewater, Bacterial Culture, etc.)
|
||||||
|
"""
|
||||||
if isinstance(submission_type, str):
|
if isinstance(submission_type, str):
|
||||||
submission_type = SubmissionType.query(name=submission_type)
|
submission_type = SubmissionType.query(name=submission_type)
|
||||||
self.submission_type = submission_type
|
self.submission_type = submission_type
|
||||||
@@ -582,8 +629,15 @@ class EquipmentParser(object):
|
|||||||
|
|
||||||
|
|
||||||
class TipParser(object):
|
class TipParser(object):
|
||||||
|
"""
|
||||||
|
Object to pull data for tips in excel sheet
|
||||||
|
"""
|
||||||
def __init__(self, xl: Workbook, submission_type: str|SubmissionType) -> None:
|
def __init__(self, xl: Workbook, submission_type: str|SubmissionType) -> None:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
xl (Workbook): Openpyxl workbook from submitted excel file.
|
||||||
|
submission_type (str | SubmissionType): Type of submission expected (Wastewater, Bacterial Culture, etc.)
|
||||||
|
"""
|
||||||
if isinstance(submission_type, str):
|
if isinstance(submission_type, str):
|
||||||
submission_type = SubmissionType.query(name=submission_type)
|
submission_type = SubmissionType.query(name=submission_type)
|
||||||
self.submission_type = submission_type
|
self.submission_type = submission_type
|
||||||
@@ -644,8 +698,6 @@ class PCRParser(object):
|
|||||||
|
|
||||||
def __init__(self, filepath: Path | None=None, submission: BasicSubmission | None=None) -> None:
|
def __init__(self, filepath: Path | None=None, submission: BasicSubmission | None=None) -> None:
|
||||||
"""
|
"""
|
||||||
Initializes object.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
filepath (Path | None, optional): file to parse. Defaults to None.
|
filepath (Path | None, optional): file to parse. Defaults to None.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -90,6 +90,13 @@ class ReportMaker(object):
|
|||||||
return html
|
return html
|
||||||
|
|
||||||
def write_report(self, filename: Path | str, obj: QWidget | None = None):
|
def write_report(self, filename: Path | str, obj: QWidget | None = None):
|
||||||
|
"""
|
||||||
|
Writes info to files.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename (Path | str): Basename of output file
|
||||||
|
obj (QWidget | None, optional): Parent object. Defaults to None.
|
||||||
|
"""
|
||||||
if isinstance(filename, str):
|
if isinstance(filename, str):
|
||||||
filename = Path(filename)
|
filename = Path(filename)
|
||||||
filename = filename.absolute()
|
filename = filename.absolute()
|
||||||
@@ -108,6 +115,9 @@ class ReportMaker(object):
|
|||||||
self.writer.close()
|
self.writer.close()
|
||||||
|
|
||||||
def fix_up_xl(self):
|
def fix_up_xl(self):
|
||||||
|
"""
|
||||||
|
Handles formatting of xl file.
|
||||||
|
"""
|
||||||
# logger.debug(f"Updating worksheet")
|
# logger.debug(f"Updating worksheet")
|
||||||
worksheet: Worksheet = self.writer.sheets['Report']
|
worksheet: Worksheet = self.writer.sheets['Report']
|
||||||
for idx, col in enumerate(self.summary_df, start=1): # loop through all columns
|
for idx, col in enumerate(self.summary_df, start=1): # loop through all columns
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
'''
|
||||||
|
contains writer objects for pushing values to submission sheet templates.
|
||||||
|
'''
|
||||||
import logging
|
import logging
|
||||||
from copy import copy
|
from copy import copy
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
@@ -27,8 +30,9 @@ class SheetWriter(object):
|
|||||||
def __init__(self, submission: PydSubmission, missing_only: bool = False):
|
def __init__(self, submission: PydSubmission, missing_only: bool = False):
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
filepath (Path | None, optional): file path to excel sheet. Defaults to None.
|
submission (PydSubmission): Object containing submission information.
|
||||||
"""
|
missing_only (bool, optional): Whether to only fill in missing values. Defaults to False.
|
||||||
|
"""
|
||||||
self.sub = OrderedDict(submission.improved_dict())
|
self.sub = OrderedDict(submission.improved_dict())
|
||||||
for k, v in self.sub.items():
|
for k, v in self.sub.items():
|
||||||
match k:
|
match k:
|
||||||
@@ -116,6 +120,13 @@ class InfoWriter(object):
|
|||||||
|
|
||||||
def __init__(self, xl: Workbook, submission_type: SubmissionType | str, info_dict: dict,
|
def __init__(self, xl: Workbook, submission_type: SubmissionType | str, info_dict: dict,
|
||||||
sub_object: BasicSubmission | None = None):
|
sub_object: BasicSubmission | None = None):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
xl (Workbook): Openpyxl workbook from submitted excel file.
|
||||||
|
submission_type (SubmissionType | str): Type of submission expected (Wastewater, Bacterial Culture, etc.)
|
||||||
|
info_dict (dict): Dictionary of information to write.
|
||||||
|
sub_object (BasicSubmission | None, optional): Submission object containing methods. Defaults to None.
|
||||||
|
"""
|
||||||
logger.debug(f"Info_dict coming into InfoWriter: {pformat(info_dict)}")
|
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)
|
||||||
@@ -186,6 +197,13 @@ class ReagentWriter(object):
|
|||||||
|
|
||||||
def __init__(self, xl: Workbook, submission_type: SubmissionType | str, extraction_kit: KitType | str,
|
def __init__(self, xl: Workbook, submission_type: SubmissionType | str, extraction_kit: KitType | str,
|
||||||
reagent_list: list):
|
reagent_list: list):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
xl (Workbook): Openpyxl workbook from submitted excel file.
|
||||||
|
submission_type (SubmissionType | str): Type of submission expected (Wastewater, Bacterial Culture, etc.)
|
||||||
|
extraction_kit (KitType | str): Extraction kit used.
|
||||||
|
reagent_list (list): List of reagent dicts to be written to excel.
|
||||||
|
"""
|
||||||
self.xl = xl
|
self.xl = xl
|
||||||
if isinstance(submission_type, str):
|
if isinstance(submission_type, str):
|
||||||
submission_type = SubmissionType.query(name=submission_type)
|
submission_type = SubmissionType.query(name=submission_type)
|
||||||
@@ -245,8 +263,13 @@ class SampleWriter(object):
|
|||||||
"""
|
"""
|
||||||
object to write sample data into excel file
|
object to write sample data into excel file
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, xl: Workbook, submission_type: SubmissionType | str, sample_list: list):
|
def __init__(self, xl: Workbook, submission_type: SubmissionType | str, sample_list: list):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
xl (Workbook): Openpyxl workbook from submitted excel file.
|
||||||
|
submission_type (SubmissionType | str): Type of submission expected (Wastewater, Bacterial Culture, etc.)
|
||||||
|
sample_list (list): List of sample dictionaries to be written to excel file.
|
||||||
|
"""
|
||||||
if isinstance(submission_type, str):
|
if isinstance(submission_type, str):
|
||||||
submission_type = SubmissionType.query(name=submission_type)
|
submission_type = SubmissionType.query(name=submission_type)
|
||||||
self.submission_type = submission_type
|
self.submission_type = submission_type
|
||||||
@@ -303,6 +326,12 @@ class EquipmentWriter(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, xl: Workbook, submission_type: SubmissionType | str, equipment_list: list):
|
def __init__(self, xl: Workbook, submission_type: SubmissionType | str, equipment_list: list):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
xl (Workbook): Openpyxl workbook from submitted excel file.
|
||||||
|
submission_type (SubmissionType | str): Type of submission expected (Wastewater, Bacterial Culture, etc.)
|
||||||
|
equipment_list (list): List of equipment dictionaries to write to excel file.
|
||||||
|
"""
|
||||||
if isinstance(submission_type, str):
|
if isinstance(submission_type, str):
|
||||||
submission_type = SubmissionType.query(name=submission_type)
|
submission_type = SubmissionType.query(name=submission_type)
|
||||||
self.submission_type = submission_type
|
self.submission_type = submission_type
|
||||||
@@ -385,6 +414,12 @@ class TipWriter(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, xl: Workbook, submission_type: SubmissionType | str, tips_list: list):
|
def __init__(self, xl: Workbook, submission_type: SubmissionType | str, tips_list: list):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
xl (Workbook): Openpyxl workbook from submitted excel file.
|
||||||
|
submission_type (SubmissionType | str): Type of submission expected (Wastewater, Bacterial Culture, etc.)
|
||||||
|
tips_list (list): List of tip dictionaries to write to the excel file.
|
||||||
|
"""
|
||||||
if isinstance(submission_type, str):
|
if isinstance(submission_type, str):
|
||||||
submission_type = SubmissionType.query(name=submission_type)
|
submission_type = SubmissionType.query(name=submission_type)
|
||||||
self.submission_type = submission_type
|
self.submission_type = submission_type
|
||||||
@@ -460,8 +495,15 @@ class TipWriter(object):
|
|||||||
|
|
||||||
|
|
||||||
class DocxWriter(object):
|
class DocxWriter(object):
|
||||||
|
"""
|
||||||
|
Object to render
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, base_dict: dict):
|
def __init__(self, base_dict: dict):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
base_dict (dict): dictionary of info to be written to template.
|
||||||
|
"""
|
||||||
self.sub_obj = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=base_dict['submission_type'])
|
self.sub_obj = BasicSubmission.find_polymorphic_subclass(polymorphic_identity=base_dict['submission_type'])
|
||||||
env = jinja_template_loading()
|
env = jinja_template_loading()
|
||||||
temp_name = f"{base_dict['submission_type'].replace(' ', '').lower()}_subdocument.docx"
|
temp_name = f"{base_dict['submission_type'].replace(' ', '').lower()}_subdocument.docx"
|
||||||
@@ -506,7 +548,13 @@ class DocxWriter(object):
|
|||||||
output.append(contents)
|
output.append(contents)
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def create_merged_template(self, *args):
|
def create_merged_template(self, *args) -> BytesIO:
|
||||||
|
"""
|
||||||
|
Appends submission specific information
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
BytesIO: Merged docx template
|
||||||
|
"""
|
||||||
merged_document = Document()
|
merged_document = Document()
|
||||||
output = BytesIO()
|
output = BytesIO()
|
||||||
for index, file in enumerate(args):
|
for index, file in enumerate(args):
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
'''
|
||||||
|
Contains all validators
|
||||||
|
'''
|
||||||
import logging, re
|
import logging, re
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|||||||
@@ -3,21 +3,18 @@ Contains pydantic models and accompanying validators
|
|||||||
'''
|
'''
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import sys
|
import sys
|
||||||
from operator import attrgetter
|
import uuid, re, logging, csv
|
||||||
import uuid, re, logging
|
from pydantic import BaseModel, field_validator, Field, model_validator
|
||||||
from pydantic import BaseModel, field_validator, Field, model_validator, PrivateAttr
|
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from dateutil.parser import parse
|
from dateutil.parser import parse
|
||||||
from dateutil.parser import ParserError
|
from dateutil.parser import ParserError
|
||||||
from typing import List, Tuple, Literal
|
from typing import List, Tuple, Literal
|
||||||
from . import RSLNamer
|
from . import RSLNamer
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tools import check_not_nan, convert_nans_to_nones, Report, Result, row_map
|
from tools import check_not_nan, convert_nans_to_nones, Report, Result
|
||||||
from backend.db.models import *
|
from backend.db.models import *
|
||||||
from sqlalchemy.exc import StatementError, IntegrityError
|
from sqlalchemy.exc import StatementError, IntegrityError
|
||||||
from PyQt6.QtWidgets import QWidget
|
from PyQt6.QtWidgets import QWidget
|
||||||
from openpyxl import load_workbook, Workbook
|
|
||||||
from io import BytesIO
|
|
||||||
|
|
||||||
logger = logging.getLogger(f"submissions.{__name__}")
|
logger = logging.getLogger(f"submissions.{__name__}")
|
||||||
|
|
||||||
@@ -106,20 +103,17 @@ class PydReagent(BaseModel):
|
|||||||
return values.data['role']
|
return values.data['role']
|
||||||
|
|
||||||
def improved_dict(self) -> dict:
|
def improved_dict(self) -> dict:
|
||||||
|
"""
|
||||||
|
Constructs a dictionary consisting of model.fields and model.extras
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Information dictionary
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
extras = list(self.model_extra.keys())
|
extras = list(self.model_extra.keys())
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
extras = []
|
extras = []
|
||||||
fields = list(self.model_fields.keys()) + extras
|
fields = list(self.model_fields.keys()) + extras
|
||||||
# output = {}
|
|
||||||
# for k in fields:
|
|
||||||
# value = getattr(self, k)
|
|
||||||
# match value:
|
|
||||||
# case date():
|
|
||||||
# value = value.strftime("%Y-%m-%d")
|
|
||||||
# case _:
|
|
||||||
# pass
|
|
||||||
# output[k] = value
|
|
||||||
return {k: getattr(self, k) for k in fields}
|
return {k: getattr(self, k) for k in fields}
|
||||||
|
|
||||||
def toSQL(self, submission: BasicSubmission | str = None) -> Tuple[Reagent, SubmissionReagentAssociation, Report]:
|
def toSQL(self, submission: BasicSubmission | str = None) -> Tuple[Reagent, SubmissionReagentAssociation, Report]:
|
||||||
@@ -142,7 +136,7 @@ class PydReagent(BaseModel):
|
|||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
value = value['value']
|
value = value['value']
|
||||||
# logger.debug(f"Reagent info item for {key}: {value}")
|
# logger.debug(f"Reagent info item for {key}: {value}")
|
||||||
# set fields based on keys in dictionary
|
# NOTE: set fields based on keys in dictionary
|
||||||
match key:
|
match key:
|
||||||
case "lot":
|
case "lot":
|
||||||
reagent.lot = value.upper()
|
reagent.lot = value.upper()
|
||||||
@@ -177,11 +171,9 @@ class PydReagent(BaseModel):
|
|||||||
assoc = None
|
assoc = None
|
||||||
# add end-of-life extension from reagent type to expiry date
|
# add end-of-life extension from reagent type to expiry date
|
||||||
# NOTE: this will now be done only in the reporting phase to account for potential changes in end-of-life extensions
|
# NOTE: this will now be done only in the reporting phase to account for potential changes in end-of-life extensions
|
||||||
|
|
||||||
return reagent, assoc, report
|
return reagent, assoc, report
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class PydSample(BaseModel, extra='allow'):
|
class PydSample(BaseModel, extra='allow'):
|
||||||
submitter_id: str
|
submitter_id: str
|
||||||
sample_type: str
|
sample_type: str
|
||||||
@@ -220,6 +212,12 @@ class PydSample(BaseModel, extra='allow'):
|
|||||||
return str(value)
|
return str(value)
|
||||||
|
|
||||||
def improved_dict(self) -> dict:
|
def improved_dict(self) -> dict:
|
||||||
|
"""
|
||||||
|
Constructs a dictionary consisting of model.fields and model.extras
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Information dictionary
|
||||||
|
"""
|
||||||
fields = list(self.model_fields.keys()) + list(self.model_extra.keys())
|
fields = list(self.model_fields.keys()) + list(self.model_extra.keys())
|
||||||
return {k: getattr(self, k) for k in fields}
|
return {k: getattr(self, k) for k in fields}
|
||||||
|
|
||||||
@@ -249,7 +247,6 @@ class PydSample(BaseModel, extra='allow'):
|
|||||||
if isinstance(submission, str):
|
if isinstance(submission, str):
|
||||||
submission = BasicSubmission.query(rsl_plate_num=submission)
|
submission = BasicSubmission.query(rsl_plate_num=submission)
|
||||||
assoc_type = submission.submission_type_name
|
assoc_type = submission.submission_type_name
|
||||||
# assoc_type = self.sample_type.replace("Sample", "").strip()
|
|
||||||
for row, column, aid, submission_rank in zip(self.row, self.column, self.assoc_id, self.submission_rank):
|
for row, column, aid, submission_rank in zip(self.row, self.column, self.assoc_id, self.submission_rank):
|
||||||
# logger.debug(f"Looking up association with identity: ({submission.submission_type_name} Association)")
|
# logger.debug(f"Looking up association with identity: ({submission.submission_type_name} Association)")
|
||||||
# logger.debug(f"Looking up association with identity: ({assoc_type} Association)")
|
# logger.debug(f"Looking up association with identity: ({assoc_type} Association)")
|
||||||
@@ -268,6 +265,12 @@ class PydSample(BaseModel, extra='allow'):
|
|||||||
return instance, out_associations, report
|
return instance, out_associations, report
|
||||||
|
|
||||||
def improved_dict(self) -> dict:
|
def improved_dict(self) -> dict:
|
||||||
|
"""
|
||||||
|
Constructs a dictionary consisting of model.fields and model.extras
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Information dictionary
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
extras = list(self.model_extra.keys())
|
extras = list(self.model_extra.keys())
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@@ -281,7 +284,16 @@ class PydTips(BaseModel):
|
|||||||
lot: str|None = Field(default=None)
|
lot: str|None = Field(default=None)
|
||||||
role: str
|
role: str
|
||||||
|
|
||||||
def to_sql(self, submission:BasicSubmission):
|
def to_sql(self, submission:BasicSubmission) -> SubmissionTipsAssociation:
|
||||||
|
"""
|
||||||
|
Con
|
||||||
|
|
||||||
|
Args:
|
||||||
|
submission (BasicSubmission): A submission object to associate tips represented here.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
SubmissionTipsAssociation: Association between queried tips and submission
|
||||||
|
"""
|
||||||
tips = Tips.query(name=self.name, lot=self.lot, limit=1)
|
tips = Tips.query(name=self.name, lot=self.lot, limit=1)
|
||||||
assoc = SubmissionTipsAssociation(submission=submission, tips=tips, role_name=self.role)
|
assoc = SubmissionTipsAssociation(submission=submission, tips=tips, role_name=self.role)
|
||||||
return assoc
|
return assoc
|
||||||
@@ -348,6 +360,12 @@ class PydEquipment(BaseModel, extra='ignore'):
|
|||||||
return equipment, assoc
|
return equipment, assoc
|
||||||
|
|
||||||
def improved_dict(self) -> dict:
|
def improved_dict(self) -> dict:
|
||||||
|
"""
|
||||||
|
Constructs a dictionary consisting of model.fields and model.extras
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Information dictionary
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
extras = list(self.model_extra.keys())
|
extras = list(self.model_extra.keys())
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
@@ -619,7 +637,14 @@ class PydSubmission(BaseModel, extra='allow'):
|
|||||||
self.submission_object = BasicSubmission.find_polymorphic_subclass(
|
self.submission_object = BasicSubmission.find_polymorphic_subclass(
|
||||||
polymorphic_identity=self.submission_type['value'])
|
polymorphic_identity=self.submission_type['value'])
|
||||||
|
|
||||||
def set_attribute(self, key, value):
|
def set_attribute(self, key:str, value):
|
||||||
|
"""
|
||||||
|
Better handling of attribute setting.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): Name of field to set
|
||||||
|
value (_type_): Value to set field to.
|
||||||
|
"""
|
||||||
self.__setattr__(name=key, value=value)
|
self.__setattr__(name=key, value=value)
|
||||||
|
|
||||||
def handle_duplicate_samples(self):
|
def handle_duplicate_samples(self):
|
||||||
@@ -775,15 +800,14 @@ class PydSubmission(BaseModel, extra='allow'):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
instance.run_cost = 0
|
instance.run_cost = 0
|
||||||
# logger.debug(f"Calculated base run cost of: {instance.run_cost}")
|
# logger.debug(f"Calculated base run cost of: {instance.run_cost}")
|
||||||
# Apply any discounts that are applicable for client and kit.
|
# NOTE: Apply any discounts that are applicable for client and kit.
|
||||||
try:
|
try:
|
||||||
# logger.debug("Checking and applying discounts...")
|
# logger.debug("Checking and applying discounts...")
|
||||||
discounts = [item.amount for item in
|
discounts = [item.amount for item in
|
||||||
Discount.query(kit_type=instance.extraction_kit, organization=instance.submitting_lab)]
|
Discount.query(kit_type=instance.extraction_kit, organization=instance.submitting_lab)]
|
||||||
# logger.debug(f"We got discounts: {discounts}")
|
# logger.debug(f"We got discounts: {discounts}")
|
||||||
if len(discounts) > 0:
|
if len(discounts) > 0:
|
||||||
discounts = sum(discounts)
|
instance.run_cost = instance.run_cost - sum(discounts)
|
||||||
instance.run_cost = instance.run_cost - discounts
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"An unknown exception occurred when calculating discounts: {e}")
|
logger.error(f"An unknown exception occurred when calculating discounts: {e}")
|
||||||
# We need to make sure there's a proper rsl plate number
|
# We need to make sure there's a proper rsl plate number
|
||||||
@@ -808,7 +832,13 @@ class PydSubmission(BaseModel, extra='allow'):
|
|||||||
from frontend.widgets.submission_widget import SubmissionFormWidget
|
from frontend.widgets.submission_widget import SubmissionFormWidget
|
||||||
return SubmissionFormWidget(parent=parent, submission=self)
|
return SubmissionFormWidget(parent=parent, submission=self)
|
||||||
|
|
||||||
def to_writer(self):
|
def to_writer(self) -> "SheetWriter":
|
||||||
|
"""
|
||||||
|
Sends data here to the sheet writer.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
SheetWriter: Sheetwriter object that will perform writing.
|
||||||
|
"""
|
||||||
from backend.excel.writer import SheetWriter
|
from backend.excel.writer import SheetWriter
|
||||||
return SheetWriter(self)
|
return SheetWriter(self)
|
||||||
|
|
||||||
@@ -866,6 +896,19 @@ class PydSubmission(BaseModel, extra='allow'):
|
|||||||
status="Warning")
|
status="Warning")
|
||||||
report.add_result(result)
|
report.add_result(result)
|
||||||
return output_reagents, report
|
return output_reagents, report
|
||||||
|
|
||||||
|
def export_csv(self, filename:Path|str):
|
||||||
|
try:
|
||||||
|
worksheet = self.csv
|
||||||
|
except AttributeError:
|
||||||
|
logger.error("No csv found.")
|
||||||
|
return
|
||||||
|
if isinstance(filename, str):
|
||||||
|
filename = Path(filename)
|
||||||
|
with open(filename, 'w', newline="") as f:
|
||||||
|
c = csv.writer(f)
|
||||||
|
for r in worksheet.rows:
|
||||||
|
c.writerow([cell.value for cell in r])
|
||||||
|
|
||||||
|
|
||||||
class PydContact(BaseModel):
|
class PydContact(BaseModel):
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class App(QMainWindow):
|
|||||||
# logger.debug(f"Creating menu bar...")
|
# logger.debug(f"Creating menu bar...")
|
||||||
menuBar = self.menuBar()
|
menuBar = self.menuBar()
|
||||||
fileMenu = menuBar.addMenu("&File")
|
fileMenu = menuBar.addMenu("&File")
|
||||||
# Creating menus using a title
|
# NOTE: Creating menus using a title
|
||||||
methodsMenu = menuBar.addMenu("&Methods")
|
methodsMenu = menuBar.addMenu("&Methods")
|
||||||
reportMenu = menuBar.addMenu("&Reports")
|
reportMenu = menuBar.addMenu("&Reports")
|
||||||
maintenanceMenu = menuBar.addMenu("&Monthly")
|
maintenanceMenu = menuBar.addMenu("&Monthly")
|
||||||
@@ -70,7 +70,6 @@ class App(QMainWindow):
|
|||||||
helpMenu.addAction(self.helpAction)
|
helpMenu.addAction(self.helpAction)
|
||||||
helpMenu.addAction(self.docsAction)
|
helpMenu.addAction(self.docsAction)
|
||||||
fileMenu.addAction(self.importAction)
|
fileMenu.addAction(self.importAction)
|
||||||
# fileMenu.addAction(self.importPCRAction)
|
|
||||||
methodsMenu.addAction(self.searchLog)
|
methodsMenu.addAction(self.searchLog)
|
||||||
methodsMenu.addAction(self.searchSample)
|
methodsMenu.addAction(self.searchSample)
|
||||||
reportMenu.addAction(self.generateReportAction)
|
reportMenu.addAction(self.generateReportAction)
|
||||||
@@ -94,7 +93,6 @@ class App(QMainWindow):
|
|||||||
"""
|
"""
|
||||||
# logger.debug(f"Creating actions...")
|
# logger.debug(f"Creating actions...")
|
||||||
self.importAction = QAction("&Import Submission", self)
|
self.importAction = QAction("&Import Submission", self)
|
||||||
# self.importPCRAction = QAction("&Import PCR Results", self)
|
|
||||||
self.addReagentAction = QAction("Add Reagent", self)
|
self.addReagentAction = QAction("Add Reagent", self)
|
||||||
self.generateReportAction = QAction("Make Report", self)
|
self.generateReportAction = QAction("Make Report", self)
|
||||||
self.addKitAction = QAction("Import Kit", self)
|
self.addKitAction = QAction("Import Kit", self)
|
||||||
@@ -112,7 +110,6 @@ class App(QMainWindow):
|
|||||||
"""
|
"""
|
||||||
# logger.debug(f"Connecting actions...")
|
# logger.debug(f"Connecting actions...")
|
||||||
self.importAction.triggered.connect(self.table_widget.formwidget.importSubmission)
|
self.importAction.triggered.connect(self.table_widget.formwidget.importSubmission)
|
||||||
# self.importPCRAction.triggered.connect(self.table_widget.formwidget.import_pcr_results)
|
|
||||||
self.addReagentAction.triggered.connect(self.table_widget.formwidget.add_reagent)
|
self.addReagentAction.triggered.connect(self.table_widget.formwidget.add_reagent)
|
||||||
self.generateReportAction.triggered.connect(self.table_widget.sub_wid.generate_report)
|
self.generateReportAction.triggered.connect(self.table_widget.sub_wid.generate_report)
|
||||||
self.joinExtractionAction.triggered.connect(self.table_widget.sub_wid.link_extractions)
|
self.joinExtractionAction.triggered.connect(self.table_widget.sub_wid.link_extractions)
|
||||||
@@ -166,12 +163,17 @@ class App(QMainWindow):
|
|||||||
dlg.exec()
|
dlg.exec()
|
||||||
|
|
||||||
def runSampleSearch(self):
|
def runSampleSearch(self):
|
||||||
|
"""
|
||||||
|
Create a search for samples.
|
||||||
|
"""
|
||||||
dlg = SearchBox(self)
|
dlg = SearchBox(self)
|
||||||
dlg.exec()
|
dlg.exec()
|
||||||
|
|
||||||
def backup_database(self):
|
def backup_database(self):
|
||||||
|
"""
|
||||||
|
Copies the database into the backup directory the first time it is opened every month.
|
||||||
|
"""
|
||||||
month = date.today().strftime("%Y-%m")
|
month = date.today().strftime("%Y-%m")
|
||||||
# day = date.today().strftime("%Y-%m-%d")
|
|
||||||
# logger.debug(f"Here is the db directory: {self.ctx.database_path}")
|
# logger.debug(f"Here is the db directory: {self.ctx.database_path}")
|
||||||
# logger.debug(f"Here is the backup directory: {self.ctx.backup_path}")
|
# logger.debug(f"Here is the backup directory: {self.ctx.backup_path}")
|
||||||
current_month_bak = Path(self.ctx.backup_path).joinpath(f"submissions_backup-{month}").resolve().with_suffix(".db")
|
current_month_bak = Path(self.ctx.backup_path).joinpath(f"submissions_backup-{month}").resolve().with_suffix(".db")
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
'''
|
||||||
|
Handles display of control charts
|
||||||
|
'''
|
||||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QWidget, QVBoxLayout, QComboBox, QHBoxLayout,
|
QWidget, QVBoxLayout, QComboBox, QHBoxLayout,
|
||||||
@@ -54,43 +57,37 @@ class ControlsViewer(QWidget):
|
|||||||
"""
|
"""
|
||||||
self.controls_getter_function()
|
self.controls_getter_function()
|
||||||
|
|
||||||
def chart_maker(self):
|
|
||||||
"""
|
|
||||||
Creates plotly charts for webview
|
|
||||||
"""
|
|
||||||
self.chart_maker_function()
|
|
||||||
|
|
||||||
def controls_getter_function(self):
|
def controls_getter_function(self):
|
||||||
"""
|
"""
|
||||||
Get controls based on start/end dates
|
Get controls based on start/end dates
|
||||||
"""
|
"""
|
||||||
report = Report()
|
report = Report()
|
||||||
# subtype defaults to disabled
|
# NOTE: subtype defaults to disabled
|
||||||
try:
|
try:
|
||||||
self.sub_typer.disconnect()
|
self.sub_typer.disconnect()
|
||||||
except TypeError:
|
except TypeError:
|
||||||
pass
|
pass
|
||||||
# correct start date being more recent than end date and rerun
|
# NOTE: correct start date being more recent than end date and rerun
|
||||||
if self.datepicker.start_date.date() > self.datepicker.end_date.date():
|
if self.datepicker.start_date.date() > self.datepicker.end_date.date():
|
||||||
logger.warning("Start date after end date is not allowed!")
|
logger.warning("Start date after end date is not allowed!")
|
||||||
threemonthsago = self.datepicker.end_date.date().addDays(-60)
|
threemonthsago = self.datepicker.end_date.date().addDays(-60)
|
||||||
# block signal that will rerun controls getter and set start date
|
# NOTE: block signal that will rerun controls getter and set start date
|
||||||
# Without triggering this function again
|
# Without triggering this function again
|
||||||
with QSignalBlocker(self.datepicker.start_date) as blocker:
|
with QSignalBlocker(self.datepicker.start_date) as blocker:
|
||||||
self.datepicker.start_date.setDate(threemonthsago)
|
self.datepicker.start_date.setDate(threemonthsago)
|
||||||
self.controls_getter()
|
self.controls_getter()
|
||||||
self.report.add_result(report)
|
self.report.add_result(report)
|
||||||
return
|
return
|
||||||
# convert to python useable date objects
|
# NOTE: convert to python useable date objects
|
||||||
self.start_date = self.datepicker.start_date.date().toPyDate()
|
self.start_date = self.datepicker.start_date.date().toPyDate()
|
||||||
self.end_date = self.datepicker.end_date.date().toPyDate()
|
self.end_date = self.datepicker.end_date.date().toPyDate()
|
||||||
self.con_type = self.control_typer.currentText()
|
self.con_type = self.control_typer.currentText()
|
||||||
self.mode = self.mode_typer.currentText()
|
self.mode = self.mode_typer.currentText()
|
||||||
self.sub_typer.clear()
|
self.sub_typer.clear()
|
||||||
# lookup subtypes
|
# NOTE: lookup subtypes
|
||||||
sub_types = ControlType.query(name=self.con_type).get_subtypes(mode=self.mode)
|
sub_types = ControlType.query(name=self.con_type).get_subtypes(mode=self.mode)
|
||||||
if sub_types != []:
|
if sub_types != []:
|
||||||
# block signal that will rerun controls getter and update sub_typer
|
# NOTE: block signal that will rerun controls getter and update sub_typer
|
||||||
with QSignalBlocker(self.sub_typer) as blocker:
|
with QSignalBlocker(self.sub_typer) as blocker:
|
||||||
self.sub_typer.addItems(sub_types)
|
self.sub_typer.addItems(sub_types)
|
||||||
self.sub_typer.setEnabled(True)
|
self.sub_typer.setEnabled(True)
|
||||||
@@ -100,7 +97,13 @@ class ControlsViewer(QWidget):
|
|||||||
self.sub_typer.setEnabled(False)
|
self.sub_typer.setEnabled(False)
|
||||||
self.chart_maker()
|
self.chart_maker()
|
||||||
self.report.add_result(report)
|
self.report.add_result(report)
|
||||||
|
|
||||||
|
def chart_maker(self):
|
||||||
|
"""
|
||||||
|
Creates plotly charts for webview
|
||||||
|
"""
|
||||||
|
self.chart_maker_function()
|
||||||
|
|
||||||
def chart_maker_function(self):
|
def chart_maker_function(self):
|
||||||
"""
|
"""
|
||||||
Create html chart for controls reporting
|
Create html chart for controls reporting
|
||||||
@@ -119,7 +122,7 @@ class ControlsViewer(QWidget):
|
|||||||
else:
|
else:
|
||||||
self.subtype = self.sub_typer.currentText()
|
self.subtype = self.sub_typer.currentText()
|
||||||
# logger.debug(f"Subtype: {self.subtype}")
|
# logger.debug(f"Subtype: {self.subtype}")
|
||||||
# query all controls using the type/start and end dates from the gui
|
# NOTE: query all controls using the type/start and end dates from the gui
|
||||||
controls = Control.query(control_type=self.con_type, start_date=self.start_date, end_date=self.end_date)
|
controls = Control.query(control_type=self.con_type, start_date=self.start_date, end_date=self.end_date)
|
||||||
# NOTE: if no data found from query set fig to none for reporting in webview
|
# NOTE: if no data found from query set fig to none for reporting in webview
|
||||||
if controls is None:
|
if controls is None:
|
||||||
@@ -139,7 +142,7 @@ class ControlsViewer(QWidget):
|
|||||||
title = self.mode
|
title = self.mode
|
||||||
else:
|
else:
|
||||||
title = f"{self.mode} - {self.subtype}"
|
title = f"{self.mode} - {self.subtype}"
|
||||||
# send dataframe to chart maker
|
# NOTE: send dataframe to chart maker
|
||||||
fig = create_charts(ctx=self.app.ctx, df=df, ytitle=title)
|
fig = create_charts(ctx=self.app.ctx, df=df, ytitle=title)
|
||||||
# logger.debug(f"Updating figure...")
|
# logger.debug(f"Updating figure...")
|
||||||
# NOTE: construct html for webview
|
# NOTE: construct html for webview
|
||||||
@@ -157,7 +160,7 @@ class ControlsDatePicker(QWidget):
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.start_date = QDateEdit(calendarPopup=True)
|
self.start_date = QDateEdit(calendarPopup=True)
|
||||||
# start date is two months prior to end date by default
|
# NOTE: start date is two months prior to end date by default
|
||||||
twomonthsago = QDate.currentDate().addDays(-60)
|
twomonthsago = QDate.currentDate().addDays(-60)
|
||||||
self.start_date.setDate(twomonthsago)
|
self.start_date.setDate(twomonthsago)
|
||||||
self.end_date = QDateEdit(calendarPopup=True)
|
self.end_date = QDateEdit(calendarPopup=True)
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import sys
|
'''
|
||||||
|
Creates forms that the user can enter equipment info into.
|
||||||
|
'''
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from PyQt6.QtCore import Qt
|
from PyQt6.QtCore import Qt
|
||||||
from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox,
|
from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox,
|
||||||
@@ -180,6 +182,9 @@ class RoleComboBox(QWidget):
|
|||||||
logger.error(f"Could create PydEquipment due to: {e}")
|
logger.error(f"Could create PydEquipment due to: {e}")
|
||||||
|
|
||||||
def toggle_checked(self):
|
def toggle_checked(self):
|
||||||
|
"""
|
||||||
|
If this equipment is disabled, the input fields will be disabled.
|
||||||
|
"""
|
||||||
for widget in self.findChildren(QWidget):
|
for widget in self.findChildren(QWidget):
|
||||||
match widget:
|
match widget:
|
||||||
case QCheckBox():
|
case QCheckBox():
|
||||||
|
|||||||
@@ -23,32 +23,32 @@ class GelBox(QDialog):
|
|||||||
|
|
||||||
def __init__(self, parent, img_path:str|Path, submission:WastewaterArtic):
|
def __init__(self, parent, img_path:str|Path, submission:WastewaterArtic):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
# setting title
|
# NOTE: setting title
|
||||||
self.setWindowTitle("PyQtGraph")
|
self.setWindowTitle("PyQtGraph")
|
||||||
self.img_path = img_path
|
self.img_path = img_path
|
||||||
self.submission = submission
|
self.submission = submission
|
||||||
# setting geometry
|
# NOTE: setting geometry
|
||||||
self.setGeometry(50, 50, 1200, 900)
|
self.setGeometry(50, 50, 1200, 900)
|
||||||
# icon
|
# NOTE: icon
|
||||||
icon = QIcon("skin.png")
|
icon = QIcon("skin.png")
|
||||||
# setting icon to the window
|
# NOTE: setting icon to the window
|
||||||
self.setWindowIcon(icon)
|
self.setWindowIcon(icon)
|
||||||
# calling method
|
# NOTE: calling method
|
||||||
self.UiComponents()
|
self.UiComponents()
|
||||||
# showing all the widgets
|
# NOTE: showing all the widgets
|
||||||
|
|
||||||
# method for components
|
# method for components
|
||||||
def UiComponents(self):
|
def UiComponents(self):
|
||||||
"""
|
"""
|
||||||
Create widgets in ui
|
Create widgets in ui
|
||||||
"""
|
"""
|
||||||
# setting configuration options
|
# NOTE: setting configuration options
|
||||||
pg.setConfigOptions(antialias=True)
|
pg.setConfigOptions(antialias=True)
|
||||||
# creating image view object
|
# NOTE: creating image view object
|
||||||
self.imv = pg.ImageView()
|
self.imv = pg.ImageView()
|
||||||
# Create image.
|
# NOTE: Create image.
|
||||||
# For some reason, ImageView wants to flip the image, so we have to rotate and flip the array first.
|
# NOTE: For some reason, ImageView wants to flip the image, so we have to rotate and flip the array first.
|
||||||
# Using the Image.rotate function results in cropped image.
|
# NOTE: Using the Image.rotate function results in cropped image, so using np.
|
||||||
img = np.flip(np.rot90(np.array(Image.open(self.img_path)),1),0)
|
img = np.flip(np.rot90(np.array(Image.open(self.img_path)),1),0)
|
||||||
self.imv.setImage(img)
|
self.imv.setImage(img)
|
||||||
layout = QGridLayout()
|
layout = QGridLayout()
|
||||||
@@ -60,10 +60,10 @@ class GelBox(QDialog):
|
|||||||
self.gel_barcode = QLineEdit()
|
self.gel_barcode = QLineEdit()
|
||||||
self.gel_barcode.setText(self.submission.gel_barcode)
|
self.gel_barcode.setText(self.submission.gel_barcode)
|
||||||
layout.addWidget(self.gel_barcode, 0, 4)
|
layout.addWidget(self.gel_barcode, 0, 4)
|
||||||
# setting this layout to the widget
|
# NOTE: setting this layout to the widget
|
||||||
# plot window goes on right side, spanning 3 rows
|
# NOTE: plot window goes on right side, spanning 3 rows
|
||||||
layout.addWidget(self.imv, 1, 1,20,20)
|
layout.addWidget(self.imv, 1, 1,20,20)
|
||||||
# setting this widget as central widget of the main window
|
# NOTE: setting this widget as central widget of the main window
|
||||||
try:
|
try:
|
||||||
control_info = sorted(self.submission.gel_controls, key=lambda d: d['location'])
|
control_info = sorted(self.submission.gel_controls, key=lambda d: d['location'])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -123,16 +123,10 @@ class ControlsForm(QWidget):
|
|||||||
widge.setText("Neg")
|
widge.setText("Neg")
|
||||||
widge.setObjectName(f"{rows[iii]} : {columns[jjj]}")
|
widge.setObjectName(f"{rows[iii]} : {columns[jjj]}")
|
||||||
self.layout.addWidget(widge, iii+1, jjj+2, 1, 1)
|
self.layout.addWidget(widge, iii+1, jjj+2, 1, 1)
|
||||||
# try:
|
|
||||||
# for iii, item in enumerate(control_info, start=1):
|
|
||||||
# self.layout.addWidget(QLabel(f"{item['sample_id']} - {item['location']}"), iii+4, 1)
|
|
||||||
# except TypeError:
|
|
||||||
# pass
|
|
||||||
self.layout.addWidget(QLabel("Comments:"), 0,5,1,1)
|
self.layout.addWidget(QLabel("Comments:"), 0,5,1,1)
|
||||||
self.comment_field = QTextEdit(self)
|
self.comment_field = QTextEdit(self)
|
||||||
self.comment_field.setFixedHeight(50)
|
self.comment_field.setFixedHeight(50)
|
||||||
self.layout.addWidget(self.comment_field, 1,5,4,1)
|
self.layout.addWidget(self.comment_field, 1,5,4,1)
|
||||||
|
|
||||||
self.setLayout(self.layout)
|
self.setLayout(self.layout)
|
||||||
|
|
||||||
def parse_form(self) -> List[dict]:
|
def parse_form(self) -> List[dict]:
|
||||||
|
|||||||
@@ -30,27 +30,27 @@ class KitAdder(QWidget):
|
|||||||
scrollContent = QWidget(scroll)
|
scrollContent = QWidget(scroll)
|
||||||
self.grid = QGridLayout()
|
self.grid = QGridLayout()
|
||||||
scrollContent.setLayout(self.grid)
|
scrollContent.setLayout(self.grid)
|
||||||
# insert submit button at top
|
# NOTE: insert submit button at top
|
||||||
self.submit_btn = QPushButton("Submit")
|
self.submit_btn = QPushButton("Submit")
|
||||||
self.grid.addWidget(self.submit_btn,0,0,1,1)
|
self.grid.addWidget(self.submit_btn,0,0,1,1)
|
||||||
self.grid.addWidget(QLabel("Kit Name:"),2,0)
|
self.grid.addWidget(QLabel("Kit Name:"),2,0)
|
||||||
# widget to get kit name
|
# NOTE: widget to get kit name
|
||||||
kit_name = QLineEdit()
|
kit_name = QLineEdit()
|
||||||
kit_name.setObjectName("kit_name")
|
kit_name.setObjectName("kit_name")
|
||||||
self.grid.addWidget(kit_name,2,1)
|
self.grid.addWidget(kit_name,2,1)
|
||||||
self.grid.addWidget(QLabel("Used For Submission Type:"),3,0)
|
self.grid.addWidget(QLabel("Used For Submission Type:"),3,0)
|
||||||
# widget to get uses of kit
|
# NOTE: widget to get uses of kit
|
||||||
used_for = QComboBox()
|
used_for = QComboBox()
|
||||||
used_for.setObjectName("used_for")
|
used_for.setObjectName("used_for")
|
||||||
# Insert all existing sample types
|
# NOTE: Insert all existing sample types
|
||||||
used_for.addItems([item.name for item in SubmissionType.query()])
|
used_for.addItems([item.name for item in SubmissionType.query()])
|
||||||
used_for.setEditable(True)
|
used_for.setEditable(True)
|
||||||
self.grid.addWidget(used_for,3,1)
|
self.grid.addWidget(used_for,3,1)
|
||||||
# Get all fields in SubmissionTypeKitTypeAssociation
|
# NOTE: Get all fields in SubmissionTypeKitTypeAssociation
|
||||||
self.columns = [item for item in SubmissionTypeKitTypeAssociation.__table__.columns if len(item.foreign_keys) == 0]
|
self.columns = [item for item in SubmissionTypeKitTypeAssociation.__table__.columns if len(item.foreign_keys) == 0]
|
||||||
for iii, column in enumerate(self.columns):
|
for iii, column in enumerate(self.columns):
|
||||||
idx = iii + 4
|
idx = iii + 4
|
||||||
# convert field name to human readable.
|
# NOTE: convert field name to human readable.
|
||||||
field_name = column.name.replace("_", " ").title()
|
field_name = column.name.replace("_", " ").title()
|
||||||
self.grid.addWidget(QLabel(field_name),idx,0)
|
self.grid.addWidget(QLabel(field_name),idx,0)
|
||||||
match column.type:
|
match column.type:
|
||||||
@@ -79,7 +79,7 @@ class KitAdder(QWidget):
|
|||||||
"""
|
"""
|
||||||
insert new reagent type row
|
insert new reagent type row
|
||||||
"""
|
"""
|
||||||
# get bottommost row
|
# NOTE: get bottommost row
|
||||||
maxrow = self.grid.rowCount()
|
maxrow = self.grid.rowCount()
|
||||||
reg_form = ReagentRoleForm(parent=self)
|
reg_form = ReagentRoleForm(parent=self)
|
||||||
reg_form.setObjectName(f"ReagentForm_{maxrow}")
|
reg_form.setObjectName(f"ReagentForm_{maxrow}")
|
||||||
@@ -90,14 +90,14 @@ class KitAdder(QWidget):
|
|||||||
send kit to database
|
send kit to database
|
||||||
"""
|
"""
|
||||||
report = Report()
|
report = Report()
|
||||||
# get form info
|
# NOTE: get form info
|
||||||
info, reagents = self.parse_form()
|
info, reagents = self.parse_form()
|
||||||
info = {k:v for k,v in info.items() if k in [column.name for column in self.columns] + ['kit_name', 'used_for']}
|
info = {k:v for k,v in info.items() if k in [column.name for column in self.columns] + ['kit_name', 'used_for']}
|
||||||
# logger.debug(f"kit info: {pformat(info)}")
|
# logger.debug(f"kit info: {pformat(info)}")
|
||||||
# logger.debug(f"kit reagents: {pformat(reagents)}")
|
# logger.debug(f"kit reagents: {pformat(reagents)}")
|
||||||
info['reagent_roles'] = reagents
|
info['reagent_roles'] = reagents
|
||||||
# logger.debug(pformat(info))
|
# logger.debug(pformat(info))
|
||||||
# send to kit constructor
|
# NOTE: send to kit constructor
|
||||||
kit = PydKit(name=info['kit_name'])
|
kit = PydKit(name=info['kit_name'])
|
||||||
for reagent in info['reagent_roles']:
|
for reagent in info['reagent_roles']:
|
||||||
uses = {
|
uses = {
|
||||||
|
|||||||
@@ -27,15 +27,12 @@ class AddReagentForm(QDialog):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
if reagent_lot is None:
|
if reagent_lot is None:
|
||||||
reagent_lot = reagent_role
|
reagent_lot = reagent_role
|
||||||
|
|
||||||
self.setWindowTitle("Add Reagent")
|
self.setWindowTitle("Add Reagent")
|
||||||
|
|
||||||
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||||
|
|
||||||
self.buttonBox = QDialogButtonBox(QBtn)
|
self.buttonBox = QDialogButtonBox(QBtn)
|
||||||
self.buttonBox.accepted.connect(self.accept)
|
self.buttonBox.accepted.connect(self.accept)
|
||||||
self.buttonBox.rejected.connect(self.reject)
|
self.buttonBox.rejected.connect(self.reject)
|
||||||
# widget to get lot info
|
# NOTE: widget to get lot info
|
||||||
self.name_input = QComboBox()
|
self.name_input = QComboBox()
|
||||||
self.name_input.setObjectName("name")
|
self.name_input.setObjectName("name")
|
||||||
self.name_input.setEditable(True)
|
self.name_input.setEditable(True)
|
||||||
@@ -43,10 +40,10 @@ class AddReagentForm(QDialog):
|
|||||||
self.lot_input = QLineEdit()
|
self.lot_input = QLineEdit()
|
||||||
self.lot_input.setObjectName("lot")
|
self.lot_input.setObjectName("lot")
|
||||||
self.lot_input.setText(reagent_lot)
|
self.lot_input.setText(reagent_lot)
|
||||||
# widget to get expiry info
|
# NOTE: widget to get expiry info
|
||||||
self.exp_input = QDateEdit(calendarPopup=True)
|
self.exp_input = QDateEdit(calendarPopup=True)
|
||||||
self.exp_input.setObjectName('expiry')
|
self.exp_input.setObjectName('expiry')
|
||||||
# if expiry is not passed in from gui, use today
|
# NOTE: if expiry is not passed in from gui, use today
|
||||||
if expiry is None:
|
if expiry is None:
|
||||||
self.exp_input.setDate(QDate.currentDate())
|
self.exp_input.setDate(QDate.currentDate())
|
||||||
else:
|
else:
|
||||||
@@ -54,17 +51,17 @@ class AddReagentForm(QDialog):
|
|||||||
self.exp_input.setDate(expiry)
|
self.exp_input.setDate(expiry)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
self.exp_input.setDate(QDate.currentDate())
|
self.exp_input.setDate(QDate.currentDate())
|
||||||
# widget to get reagent type info
|
# NOTE: widget to get reagent type info
|
||||||
self.type_input = QComboBox()
|
self.type_input = QComboBox()
|
||||||
self.type_input.setObjectName('type')
|
self.type_input.setObjectName('type')
|
||||||
self.type_input.addItems([item.name for item in ReagentRole.query()])
|
self.type_input.addItems([item.name for item in ReagentRole.query()])
|
||||||
# logger.debug(f"Trying to find index of {reagent_type}")
|
# logger.debug(f"Trying to find index of {reagent_type}")
|
||||||
# convert input to user friendly string?
|
# NOTE: convert input to user friendly string?
|
||||||
try:
|
try:
|
||||||
reagent_role = reagent_role.replace("_", " ").title()
|
reagent_role = reagent_role.replace("_", " ").title()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
reagent_role = None
|
reagent_role = None
|
||||||
# set parsed reagent type to top of list
|
# NOTE: set parsed reagent type to top of list
|
||||||
index = self.type_input.findText(reagent_role, Qt.MatchFlag.MatchEndsWith)
|
index = self.type_input.findText(reagent_role, Qt.MatchFlag.MatchEndsWith)
|
||||||
if index >= 0:
|
if index >= 0:
|
||||||
self.type_input.setCurrentIndex(index)
|
self.type_input.setCurrentIndex(index)
|
||||||
@@ -110,12 +107,12 @@ class ReportDatePicker(QDialog):
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.setWindowTitle("Select Report Date Range")
|
self.setWindowTitle("Select Report Date Range")
|
||||||
# make confirm/reject buttons
|
# NOTE: make confirm/reject buttons
|
||||||
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||||
self.buttonBox = QDialogButtonBox(QBtn)
|
self.buttonBox = QDialogButtonBox(QBtn)
|
||||||
self.buttonBox.accepted.connect(self.accept)
|
self.buttonBox.accepted.connect(self.accept)
|
||||||
self.buttonBox.rejected.connect(self.reject)
|
self.buttonBox.rejected.connect(self.reject)
|
||||||
# widgets to ask for dates
|
# NOTE: widgets to ask for dates
|
||||||
self.start_date = QDateEdit(calendarPopup=True)
|
self.start_date = QDateEdit(calendarPopup=True)
|
||||||
self.start_date.setObjectName("start_date")
|
self.start_date.setObjectName("start_date")
|
||||||
self.start_date.setDate(QDate.currentDate())
|
self.start_date.setDate(QDate.currentDate())
|
||||||
@@ -139,48 +136,6 @@ class ReportDatePicker(QDialog):
|
|||||||
"""
|
"""
|
||||||
return dict(start_date=self.start_date.date().toPyDate(), end_date = self.end_date.date().toPyDate())
|
return dict(start_date=self.start_date.date().toPyDate(), end_date = self.end_date.date().toPyDate())
|
||||||
|
|
||||||
|
|
||||||
class FirstStrandSalvage(QDialog):
|
|
||||||
|
|
||||||
def __init__(self, ctx:Settings, submitter_id:str, rsl_plate_num:str|None=None) -> None:
|
|
||||||
super().__init__()
|
|
||||||
if rsl_plate_num is None:
|
|
||||||
rsl_plate_num = ""
|
|
||||||
self.setWindowTitle("Add Reagent")
|
|
||||||
|
|
||||||
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
|
||||||
|
|
||||||
self.buttonBox = QDialogButtonBox(QBtn)
|
|
||||||
self.buttonBox.accepted.connect(self.accept)
|
|
||||||
self.buttonBox.rejected.connect(self.reject)
|
|
||||||
self.submitter_id_input = QLineEdit()
|
|
||||||
self.submitter_id_input.setText(submitter_id)
|
|
||||||
self.rsl_plate_num = QLineEdit()
|
|
||||||
self.rsl_plate_num.setText(rsl_plate_num)
|
|
||||||
self.row_letter = QComboBox()
|
|
||||||
self.row_letter.addItems(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'])
|
|
||||||
self.row_letter.setEditable(False)
|
|
||||||
self.column_number = QComboBox()
|
|
||||||
self.column_number.addItems(['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'])
|
|
||||||
self.column_number.setEditable(False)
|
|
||||||
self.layout = QFormLayout()
|
|
||||||
self.layout.addRow(self.tr("&Sample Number:"), self.submitter_id_input)
|
|
||||||
self.layout.addRow(self.tr("&Plate Number:"), self.rsl_plate_num)
|
|
||||||
self.layout.addRow(self.tr("&Source Row:"), self.row_letter)
|
|
||||||
self.layout.addRow(self.tr("&Source Column:"), self.column_number)
|
|
||||||
self.layout.addWidget(self.buttonBox)
|
|
||||||
self.setLayout(self.layout)
|
|
||||||
|
|
||||||
def parse_form(self) -> dict:
|
|
||||||
"""
|
|
||||||
Pulls first strand info from form.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Output info
|
|
||||||
"""
|
|
||||||
return dict(plate=self.rsl_plate_num.text(), submitter_id=self.submitter_id_input.text(), well=f"{self.row_letter.currentText()}{self.column_number.currentText()}")
|
|
||||||
|
|
||||||
|
|
||||||
class LogParser(QDialog):
|
class LogParser(QDialog):
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
|
|||||||
@@ -22,13 +22,13 @@ class QuestionAsker(QDialog):
|
|||||||
def __init__(self, title:str, message:str) -> QDialog:
|
def __init__(self, title:str, message:str) -> QDialog:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.setWindowTitle(title)
|
self.setWindowTitle(title)
|
||||||
# set yes/no buttons
|
# NOTE: set yes/no buttons
|
||||||
QBtn = QDialogButtonBox.StandardButton.Yes | QDialogButtonBox.StandardButton.No
|
QBtn = QDialogButtonBox.StandardButton.Yes | QDialogButtonBox.StandardButton.No
|
||||||
self.buttonBox = QDialogButtonBox(QBtn)
|
self.buttonBox = QDialogButtonBox(QBtn)
|
||||||
self.buttonBox.accepted.connect(self.accept)
|
self.buttonBox.accepted.connect(self.accept)
|
||||||
self.buttonBox.rejected.connect(self.reject)
|
self.buttonBox.rejected.connect(self.reject)
|
||||||
self.layout = QVBoxLayout()
|
self.layout = QVBoxLayout()
|
||||||
# Text for the yes/no question
|
# NOTE: Text for the yes/no question
|
||||||
self.message = QLabel(message)
|
self.message = QLabel(message)
|
||||||
self.layout.addWidget(self.message)
|
self.layout.addWidget(self.message)
|
||||||
self.layout.addWidget(self.buttonBox)
|
self.layout.addWidget(self.buttonBox)
|
||||||
@@ -41,7 +41,7 @@ class AlertPop(QMessageBox):
|
|||||||
"""
|
"""
|
||||||
def __init__(self, message:str, status:Literal['Information', 'Question', 'Warning', 'Critical'], owner:str|None=None) -> QMessageBox:
|
def __init__(self, message:str, status:Literal['Information', 'Question', 'Warning', 'Critical'], owner:str|None=None) -> QMessageBox:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
# select icon by string
|
# NOTE: select icon by string
|
||||||
icon = getattr(QMessageBox.Icon, status)
|
icon = getattr(QMessageBox.Icon, status)
|
||||||
self.setIcon(icon)
|
self.setIcon(icon)
|
||||||
self.setInformativeText(message)
|
self.setInformativeText(message)
|
||||||
@@ -61,13 +61,13 @@ class ObjectSelector(QDialog):
|
|||||||
items = [item.name for item in obj_type.query()]
|
items = [item.name for item in obj_type.query()]
|
||||||
self.widget.addItems(items)
|
self.widget.addItems(items)
|
||||||
self.widget.setEditable(False)
|
self.widget.setEditable(False)
|
||||||
# set yes/no buttons
|
# NOTE: set yes/no buttons
|
||||||
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||||
self.buttonBox = QDialogButtonBox(QBtn)
|
self.buttonBox = QDialogButtonBox(QBtn)
|
||||||
self.buttonBox.accepted.connect(self.accept)
|
self.buttonBox.accepted.connect(self.accept)
|
||||||
self.buttonBox.rejected.connect(self.reject)
|
self.buttonBox.rejected.connect(self.reject)
|
||||||
self.layout = QVBoxLayout()
|
self.layout = QVBoxLayout()
|
||||||
# Text for the yes/no question
|
# NOTE: Text for the yes/no question
|
||||||
message = QLabel(message)
|
message = QLabel(message)
|
||||||
self.layout.addWidget(message)
|
self.layout.addWidget(message)
|
||||||
self.layout.addWidget(self.widget)
|
self.layout.addWidget(self.widget)
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
'''
|
||||||
|
Search box that performs fuzzy search for samples
|
||||||
|
'''
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
@@ -33,6 +36,9 @@ class SearchBox(QDialog):
|
|||||||
self.update_widgets()
|
self.update_widgets()
|
||||||
|
|
||||||
def update_widgets(self):
|
def update_widgets(self):
|
||||||
|
"""
|
||||||
|
Changes form inputs based on sample type
|
||||||
|
"""
|
||||||
deletes = [item for item in self.findChildren(FieldSearch)]
|
deletes = [item for item in self.findChildren(FieldSearch)]
|
||||||
# logger.debug(deletes)
|
# logger.debug(deletes)
|
||||||
for item in deletes:
|
for item in deletes:
|
||||||
@@ -45,13 +51,21 @@ class SearchBox(QDialog):
|
|||||||
widget = FieldSearch(parent=self, label=item['label'], field_name=item['field'])
|
widget = FieldSearch(parent=self, label=item['label'], field_name=item['field'])
|
||||||
self.layout.addWidget(widget, start_row+iii, 0)
|
self.layout.addWidget(widget, start_row+iii, 0)
|
||||||
|
|
||||||
def parse_form(self):
|
def parse_form(self) -> dict:
|
||||||
|
"""
|
||||||
|
Converts form into dictionary.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Fields dictionary
|
||||||
|
"""
|
||||||
fields = [item.parse_form() for item in self.findChildren(FieldSearch)]
|
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}
|
return {item[0]:item[1] for item in fields if item[1] is not None}
|
||||||
|
|
||||||
def update_data(self):
|
def update_data(self):
|
||||||
|
"""
|
||||||
|
Shows dataframe of relevant samples.
|
||||||
|
"""
|
||||||
fields = self.parse_form()
|
fields = self.parse_form()
|
||||||
# data = self.type.samples_to_df(sample_type=self.type, **fields)
|
|
||||||
data = self.type.fuzzy_search(sample_type=self.type, **fields)
|
data = self.type.fuzzy_search(sample_type=self.type, **fields)
|
||||||
data = self.type.samples_to_df(sample_list=data)
|
data = self.type.samples_to_df(sample_list=data)
|
||||||
# logger.debug(f"Data: {data}")
|
# logger.debug(f"Data: {data}")
|
||||||
@@ -72,6 +86,9 @@ class FieldSearch(QWidget):
|
|||||||
self.search_widget.returnPressed.connect(self.enter_pressed)
|
self.search_widget.returnPressed.connect(self.enter_pressed)
|
||||||
|
|
||||||
def enter_pressed(self):
|
def enter_pressed(self):
|
||||||
|
"""
|
||||||
|
Triggered when enter is pressed on this input field.
|
||||||
|
"""
|
||||||
self.parent().update_data()
|
self.parent().update_data()
|
||||||
|
|
||||||
def parse_form(self) -> Tuple:
|
def parse_form(self) -> Tuple:
|
||||||
@@ -99,4 +116,5 @@ class SearchResults(QTableView):
|
|||||||
logger.error("Couldn't format id string.")
|
logger.error("Couldn't format id string.")
|
||||||
proxy_model = QSortFilterProxyModel()
|
proxy_model = QSortFilterProxyModel()
|
||||||
proxy_model.setSourceModel(pandasModel(self.data))
|
proxy_model.setSourceModel(pandasModel(self.data))
|
||||||
self.setModel(proxy_model)
|
self.setModel(proxy_model)
|
||||||
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
from PyQt6.QtGui import QPageSize
|
'''
|
||||||
from PyQt6.QtPrintSupport import QPrinter
|
Webview to show submission and sample details.
|
||||||
from PyQt6.QtWidgets import (QDialog, QPushButton, QVBoxLayout, QMessageBox,
|
'''
|
||||||
|
from PyQt6.QtWidgets import (QDialog, QPushButton, QVBoxLayout,
|
||||||
QDialogButtonBox, QTextEdit)
|
QDialogButtonBox, QTextEdit)
|
||||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||||
from PyQt6.QtWebChannel import QWebChannel
|
from PyQt6.QtWebChannel import QWebChannel
|
||||||
@@ -9,14 +10,12 @@ from PyQt6.QtCore import Qt, pyqtSlot
|
|||||||
from backend.db.models import BasicSubmission, BasicSample
|
from backend.db.models import BasicSubmission, BasicSample
|
||||||
from tools import is_power_user, html_to_pdf
|
from tools import is_power_user, html_to_pdf
|
||||||
from .functions import select_save_file
|
from .functions import select_save_file
|
||||||
from io import BytesIO
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import logging, base64
|
import logging
|
||||||
from getpass import getuser
|
from getpass import getuser
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from html2image import Html2Image
|
|
||||||
from PIL import Image
|
|
||||||
from typing import List
|
from typing import List
|
||||||
from backend.excel.writer import DocxWriter
|
from backend.excel.writer import DocxWriter
|
||||||
|
|
||||||
@@ -135,7 +134,7 @@ class SubmissionComment(QDialog):
|
|||||||
pass
|
pass
|
||||||
self.submission = submission
|
self.submission = submission
|
||||||
self.setWindowTitle(f"{self.submission.rsl_plate_num} Submission Comment")
|
self.setWindowTitle(f"{self.submission.rsl_plate_num} Submission Comment")
|
||||||
# create text field
|
# NOTE: create text field
|
||||||
self.txt_editor = QTextEdit(self)
|
self.txt_editor = QTextEdit(self)
|
||||||
self.txt_editor.setReadOnly(False)
|
self.txt_editor.setReadOnly(False)
|
||||||
self.txt_editor.setText("Add Comment")
|
self.txt_editor.setText("Add Comment")
|
||||||
|
|||||||
@@ -261,32 +261,32 @@ class SubmissionsSheet(QTableView):
|
|||||||
html = make_report_html(df=summary_df, start_date=info['start_date'], end_date=info['end_date'])
|
html = make_report_html(df=summary_df, start_date=info['start_date'], end_date=info['end_date'])
|
||||||
# NOTE: get save location of report
|
# NOTE: get save location of report
|
||||||
fname = select_save_file(obj=self, default_name=f"Submissions_Report_{info['start_date']}-{info['end_date']}.docx", extension="docx")
|
fname = select_save_file(obj=self, default_name=f"Submissions_Report_{info['start_date']}-{info['end_date']}.docx", extension="docx")
|
||||||
html_to_pdf(html=html, output_file=fname)
|
# html_to_pdf(html=html, output_file=fname)
|
||||||
writer = pd.ExcelWriter(fname.with_suffix(".xlsx"), engine='openpyxl')
|
# writer = pd.ExcelWriter(fname.with_suffix(".xlsx"), engine='openpyxl')
|
||||||
summary_df.to_excel(writer, sheet_name="Report")
|
# summary_df.to_excel(writer, sheet_name="Report")
|
||||||
detailed_df.to_excel(writer, sheet_name="Details", index=False)
|
# detailed_df.to_excel(writer, sheet_name="Details", index=False)
|
||||||
worksheet: Worksheet = writer.sheets['Report']
|
# worksheet: Worksheet = writer.sheets['Report']
|
||||||
for idx, col in enumerate(summary_df, start=1): # loop through all columns
|
# for idx, col in enumerate(summary_df, start=1): # loop through all columns
|
||||||
series = summary_df[col]
|
# series = summary_df[col]
|
||||||
max_len = max((
|
# max_len = max((
|
||||||
series.astype(str).map(len).max(), # len of largest item
|
# series.astype(str).map(len).max(), # len of largest item
|
||||||
len(str(series.name)) # len of column name/header
|
# len(str(series.name)) # len of column name/header
|
||||||
)) + 20 # adding a little extra space
|
# )) + 20 # adding a little extra space
|
||||||
try:
|
# try:
|
||||||
# NOTE: Convert idx to letter
|
# # NOTE: Convert idx to letter
|
||||||
col_letter = chr(ord('@') + idx)
|
# col_letter = chr(ord('@') + idx)
|
||||||
worksheet.column_dimensions[col_letter].width = max_len
|
# worksheet.column_dimensions[col_letter].width = max_len
|
||||||
except ValueError:
|
# except ValueError:
|
||||||
pass
|
# pass
|
||||||
blank_row = get_first_blank_df_row(summary_df) + 1
|
# blank_row = get_first_blank_df_row(summary_df) + 1
|
||||||
# logger.debug(f"Blank row index = {blank_row}")
|
# # logger.debug(f"Blank row index = {blank_row}")
|
||||||
for col in range(3,6):
|
# for col in range(3,6):
|
||||||
col_letter = row_map[col]
|
# col_letter = row_map[col]
|
||||||
worksheet.cell(row=blank_row, column=col, value=f"=SUM({col_letter}2:{col_letter}{str(blank_row-1)})")
|
# worksheet.cell(row=blank_row, column=col, value=f"=SUM({col_letter}2:{col_letter}{str(blank_row-1)})")
|
||||||
for cell in worksheet['D']:
|
# for cell in worksheet['D']:
|
||||||
if cell.row > 1:
|
# if cell.row > 1:
|
||||||
cell.style = 'Currency'
|
# cell.style = 'Currency'
|
||||||
writer.close()
|
# writer.close()
|
||||||
# rp = ReportMaker(start_date=info['start_date'], end_date=info['end_date'])
|
rp = ReportMaker(start_date=info['start_date'], end_date=info['end_date'])
|
||||||
# rp.write_report(filename=fname, obj=self)
|
rp.write_report(filename=fname, obj=self)
|
||||||
self.report.add_result(report)
|
self.report.add_result(report)
|
||||||
|
|||||||
@@ -114,4 +114,5 @@ class InfoWidget(QWidget):
|
|||||||
sheets = self.sheet.text().split(","),
|
sheets = self.sheet.text().split(","),
|
||||||
row = self.row.value(),
|
row = self.row.value(),
|
||||||
column = self.column.value()
|
column = self.column.value()
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
|
'''
|
||||||
|
Contains all submission related frontend functions
|
||||||
|
'''
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QWidget, QPushButton, QVBoxLayout,
|
QWidget, QPushButton, QVBoxLayout,
|
||||||
QComboBox, QDateEdit, QLineEdit, QLabel
|
QComboBox, QDateEdit, QLineEdit, QLabel
|
||||||
@@ -149,8 +151,6 @@ class SubmissionFormContainer(QWidget):
|
|||||||
|
|
||||||
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()
|
||||||
@@ -303,10 +303,10 @@ class SubmissionFormWidget(QWidget):
|
|||||||
# logger.debug(f"Base submission: {base_submission.to_dict()}")
|
# logger.debug(f"Base submission: {base_submission.to_dict()}")
|
||||||
# NOTE: check output message for issues
|
# NOTE: check output message for issues
|
||||||
match result.code:
|
match result.code:
|
||||||
# code 0: everything is fine.
|
# NOTE: code 0: everything is fine.
|
||||||
case 0:
|
case 0:
|
||||||
report.add_result(None)
|
report.add_result(None)
|
||||||
# code 1: ask for overwrite
|
# NOTE: code 1: ask for overwrite
|
||||||
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():
|
||||||
@@ -319,7 +319,7 @@ class SubmissionFormWidget(QWidget):
|
|||||||
self.app.report.add_result(report)
|
self.app.report.add_result(report)
|
||||||
self.app.result_reporter()
|
self.app.result_reporter()
|
||||||
return
|
return
|
||||||
# code 2: No RSL plate number given
|
# NOTE: code 2: No RSL plate number given
|
||||||
case 2:
|
case 2:
|
||||||
report.add_result(result)
|
report.add_result(result)
|
||||||
self.app.report.add_result(report)
|
self.app.report.add_result(report)
|
||||||
@@ -351,9 +351,8 @@ class SubmissionFormWidget(QWidget):
|
|||||||
if isinstance(fname, bool) or fname is None:
|
if isinstance(fname, bool) or fname is None:
|
||||||
fname = select_save_file(obj=self, default_name=self.pyd.construct_filename(), extension="csv")
|
fname = select_save_file(obj=self, default_name=self.pyd.construct_filename(), extension="csv")
|
||||||
try:
|
try:
|
||||||
|
self.pyd.export_csv(fname)
|
||||||
# self.pyd.csv.to_csv(fname.__str__(), index=False)
|
# workbook_2_csv(worksheet=self.pyd.csv, filename=fname)
|
||||||
workbook_2_csv(worksheet=self.pyd.csv, filename=fname)
|
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
logger.warning(f"Could not get permissions to {fname}. Possibly the request was cancelled.")
|
logger.warning(f"Could not get permissions to {fname}. Possibly the request was cancelled.")
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import json
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import logging, re, yaml, sys, os, stat, platform, getpass, inspect, csv
|
import logging, re, yaml, sys, os, stat, platform, getpass, inspect, csv
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from PyQt6.QtWidgets import QWidget
|
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
from logging import handlers
|
from logging import handlers
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -65,6 +64,17 @@ def get_unique_values_in_df_column(df: pd.DataFrame, column_name: str) -> list:
|
|||||||
|
|
||||||
|
|
||||||
def check_key_or_attr(key: str, interest: dict | object, check_none: bool = False) -> bool:
|
def check_key_or_attr(key: str, interest: dict | object, check_none: bool = False) -> bool:
|
||||||
|
"""
|
||||||
|
Checks if key exists in dict or object has attribute.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): key or attribute name
|
||||||
|
interest (dict | object): Dictionary or object to be checked.
|
||||||
|
check_none (bool, optional): Return false if value exists, but is None. Defaults to False.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if exists, else False
|
||||||
|
"""
|
||||||
match interest:
|
match interest:
|
||||||
case dict():
|
case dict():
|
||||||
if key in interest.keys():
|
if key in interest.keys():
|
||||||
@@ -105,10 +115,9 @@ def check_not_nan(cell_contents) -> bool:
|
|||||||
Returns:
|
Returns:
|
||||||
bool: True if cell has value, else, false.
|
bool: True if cell has value, else, false.
|
||||||
"""
|
"""
|
||||||
# check for nan as a string first
|
# NOTE: check for nan as a string first
|
||||||
exclude = ['unnamed:', 'blank', 'void']
|
exclude = ['unnamed:', 'blank', 'void']
|
||||||
try:
|
try:
|
||||||
# if "Unnamed:" in cell_contents or "blank" in cell_contents.lower():
|
|
||||||
if cell_contents.lower() in exclude:
|
if cell_contents.lower() in exclude:
|
||||||
cell_contents = np.nan
|
cell_contents = np.nan
|
||||||
cell_contents = cell_contents.lower()
|
cell_contents = cell_contents.lower()
|
||||||
@@ -158,6 +167,15 @@ def convert_nans_to_nones(input_str) -> str | None:
|
|||||||
|
|
||||||
|
|
||||||
def is_missing(value: Any) -> Tuple[Any, bool]:
|
def is_missing(value: Any) -> Tuple[Any, bool]:
|
||||||
|
"""
|
||||||
|
Checks if a parsed value is missing.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value (Any): Incoming value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple[Any, bool]: Value, True if nan, else False
|
||||||
|
"""
|
||||||
if check_not_nan(value):
|
if check_not_nan(value):
|
||||||
return value, False
|
return value, False
|
||||||
else:
|
else:
|
||||||
@@ -262,19 +280,19 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
else:
|
else:
|
||||||
database_path = values.data['database_path']
|
database_path = values.data['database_path']
|
||||||
if database_path is None:
|
if database_path is None:
|
||||||
# check in user's .submissions directory for submissions.db
|
# NOTE: check in user's .submissions directory for submissions.db
|
||||||
if Path.home().joinpath(".submissions", "submissions.db").exists():
|
if Path.home().joinpath(".submissions", "submissions.db").exists():
|
||||||
database_path = Path.home().joinpath(".submissions", "submissions.db")
|
database_path = Path.home().joinpath(".submissions", "submissions.db")
|
||||||
# finally, look in the local dir
|
# NOTE: finally, look in the local dir
|
||||||
else:
|
else:
|
||||||
database_path = package_dir.joinpath("submissions.db")
|
database_path = package_dir.joinpath("submissions.db")
|
||||||
else:
|
else:
|
||||||
if database_path == ":memory:":
|
if database_path == ":memory:":
|
||||||
pass
|
pass
|
||||||
# check if user defined path is directory
|
# NOTE: check if user defined path is directory
|
||||||
elif database_path.is_dir():
|
elif database_path.is_dir():
|
||||||
database_path = database_path.joinpath("submissions.db")
|
database_path = database_path.joinpath("submissions.db")
|
||||||
# check if user defined path is a file
|
# NOTE: check if user defined path is a file
|
||||||
elif database_path.is_file():
|
elif database_path.is_file():
|
||||||
database_path = database_path
|
database_path = database_path
|
||||||
else:
|
else:
|
||||||
@@ -282,7 +300,6 @@ class Settings(BaseSettings, extra="allow"):
|
|||||||
logger.info(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
|
|
||||||
return session
|
return session
|
||||||
|
|
||||||
@field_validator('package', mode="before")
|
@field_validator('package', mode="before")
|
||||||
@@ -403,7 +420,7 @@ class GroupWriteRotatingFileHandler(handlers.RotatingFileHandler):
|
|||||||
"""
|
"""
|
||||||
# Rotate the file first.
|
# Rotate the file first.
|
||||||
handlers.RotatingFileHandler.doRollover(self)
|
handlers.RotatingFileHandler.doRollover(self)
|
||||||
# Add group write to the current permissions.
|
# NOTE: Add group write to the current permissions.
|
||||||
currMode = os.stat(self.baseFilename).st_mode
|
currMode = os.stat(self.baseFilename).st_mode
|
||||||
os.chmod(self.baseFilename, currMode | stat.S_IWGRP)
|
os.chmod(self.baseFilename, currMode | stat.S_IWGRP)
|
||||||
|
|
||||||
@@ -629,6 +646,12 @@ class Report(BaseModel):
|
|||||||
return f"Report(result_count:{len(self.results)})"
|
return f"Report(result_count:{len(self.results)})"
|
||||||
|
|
||||||
def add_result(self, result: Result | Report | None):
|
def add_result(self, result: Result | Report | None):
|
||||||
|
"""
|
||||||
|
Takes a result object or all results in another report and adds them to this one.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
result (Result | Report | None): Results to be added.
|
||||||
|
"""
|
||||||
match result:
|
match result:
|
||||||
case Result():
|
case Result():
|
||||||
logger.info(f"Adding {result} to results.")
|
logger.info(f"Adding {result} to results.")
|
||||||
@@ -644,15 +667,30 @@ class Report(BaseModel):
|
|||||||
case _:
|
case _:
|
||||||
logger.error(f"Unknown variable type: {type(result)} for <Result> entry into <Report>")
|
logger.error(f"Unknown variable type: {type(result)} for <Result> entry into <Report>")
|
||||||
|
|
||||||
def is_empty(self):
|
|
||||||
return bool(self.results)
|
|
||||||
|
|
||||||
|
def rreplace(s:str, old:str, new:str) -> str:
|
||||||
|
"""
|
||||||
|
Removes rightmost occurence of a substring
|
||||||
|
|
||||||
def rreplace(s, old, new):
|
Args:
|
||||||
|
s (str): input string
|
||||||
|
old (str): original substring
|
||||||
|
new (str): new substring
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: updated string
|
||||||
|
"""
|
||||||
return (s[::-1].replace(old[::-1], new[::-1], 1))[::-1]
|
return (s[::-1].replace(old[::-1], new[::-1], 1))[::-1]
|
||||||
|
|
||||||
|
|
||||||
def html_to_pdf(html, output_file: Path | str):
|
def html_to_pdf(html:str, output_file: Path | str):
|
||||||
|
"""
|
||||||
|
Attempts to print an html string as a PDF. (currently not working)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
html (str): Input html string.
|
||||||
|
output_file (Path | str): Output PDF file path.
|
||||||
|
"""
|
||||||
if isinstance(output_file, str):
|
if isinstance(output_file, str):
|
||||||
output_file = Path(output_file)
|
output_file = Path(output_file)
|
||||||
logger.debug(f"Printing PDF to {output_file}")
|
logger.debug(f"Printing PDF to {output_file}")
|
||||||
@@ -688,6 +726,13 @@ def remove_key_from_list_of_dicts(input: list, key: str) -> list:
|
|||||||
|
|
||||||
|
|
||||||
def workbook_2_csv(worksheet: Worksheet, filename: Path):
|
def workbook_2_csv(worksheet: Worksheet, filename: Path):
|
||||||
|
"""
|
||||||
|
Export an excel worksheet (workbook is not correct) to csv file.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
worksheet (Worksheet): Incoming worksheet
|
||||||
|
filename (Path): Output csv filepath.
|
||||||
|
"""
|
||||||
with open(filename, 'w', newline="") as f:
|
with open(filename, 'w', newline="") as f:
|
||||||
c = csv.writer(f)
|
c = csv.writer(f)
|
||||||
for r in worksheet.rows:
|
for r in worksheet.rows:
|
||||||
@@ -698,6 +743,12 @@ ctx = get_config(None)
|
|||||||
|
|
||||||
|
|
||||||
def is_power_user() -> bool:
|
def is_power_user() -> bool:
|
||||||
|
"""
|
||||||
|
Checks if user is in list of power users
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if yes, False if no.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
check = getpass.getuser() in ctx.power_users
|
check = getpass.getuser() in ctx.power_users
|
||||||
except:
|
except:
|
||||||
|
|||||||
Reference in New Issue
Block a user