diff --git a/CHANGELOG.md b/CHANGELOG.md index 123f8a0..de4c4a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 202403.03 + +- Automated version construction. + ## 202403.02 - Moved functions out of submission container to submission form diff --git a/TODO.md b/TODO.md index 5088558..3e79877 100644 --- a/TODO.md +++ b/TODO.md @@ -1,3 +1,4 @@ +- [ ] Merge BasicSubmission.find_subclasses and BasicSubmission.find_polymorphic_subclass - [ ] Fix updating of Extraction Kit in submission form widget. - [x] Fix cropping of gel image. - [ ] Create Tips ... *sigh*. diff --git a/src/submissions/__init__.py b/src/submissions/__init__.py index 59282e5..eefd8e0 100644 --- a/src/submissions/__init__.py +++ b/src/submissions/__init__.py @@ -1,12 +1,24 @@ # __init__.py from pathlib import Path +from datetime import date +import calendar # Version of the realpython-reader package + +year = date.today().year +month = date.today().month +day = date.today().day + +def get_week_of_month() -> int: + for ii, week in enumerate(calendar.monthcalendar(date.today().year, date.today().month)): + if day in week: + return ii + 1 + __project__ = "submissions" -__version__ = "202403.2b" +__version__ = f"{year}{str(month).zfill(2)}.{get_week_of_month()}b" __author__ = {"name":"Landon Wark", "email":"Landon.Wark@phac-aspc.gc.ca"} -__copyright__ = "2022-2024, Government of Canada" +__copyright__ = f"2022-{date.today().year}, Government of Canada" project_path = Path(__file__).parents[2].absolute() diff --git a/src/submissions/backend/db/models/kits.py b/src/submissions/backend/db/models/kits.py index 5aef6a0..e7b23de 100644 --- a/src/submissions/backend/db/models/kits.py +++ b/src/submissions/backend/db/models/kits.py @@ -1252,9 +1252,13 @@ class SubmissionEquipmentAssociation(BaseClass): equipment = relationship(Equipment, back_populates="equipment_submission_associations") #: associated equipment - def __init__(self, submission, equipment): + def __repr__(self): + return f"" + + def __init__(self, submission, equipment, role:str="None"): self.submission = submission self.equipment = equipment + self.role = role def to_sub_dict(self) -> dict: """ @@ -1263,7 +1267,11 @@ class SubmissionEquipmentAssociation(BaseClass): Returns: dict: This SubmissionEquipmentAssociation as a dictionary """ - output = dict(name=self.equipment.name, asset_number=self.equipment.asset_number, comment=self.comments, processes=[self.process.name], role=self.role, nickname=self.equipment.nickname) + try: + process = self.process.name + except AttributeError: + process = "No process found" + output = dict(name=self.equipment.name, asset_number=self.equipment.asset_number, comment=self.comments, processes=[process], role=self.role, nickname=self.equipment.nickname) return output class SubmissionTypeEquipmentRoleAssociation(BaseClass): @@ -1344,7 +1352,7 @@ class Process(BaseClass): Returns: str: Representation of this Process """ - return f"" @classmethod @setup_lookup diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index d092948..238cc99 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -96,9 +96,10 @@ class BasicSubmission(BaseClass): Returns: str: Representation of this BasicSubmission """ - return f"{self.submission_type}Submission({self.rsl_plate_num})" + submission_type = self.submission_type or "Basic" + return f"{submission_type}Submission({self.rsl_plate_num})" - def to_dict(self, full_data:bool=False, backup:bool=False) -> dict: + def to_dict(self, full_data:bool=False, backup:bool=False, report:bool=False) -> dict: """ Constructs dictionary used in submissions summary @@ -126,13 +127,33 @@ class BasicSubmission(BaseClass): ext_kit = None # load scraped extraction info try: - ext_info = json.loads(self.extraction_info) + # ext_info = json.loads(self.extraction_info) + ext_info = self.extraction_info except TypeError: ext_info = None - except JSONDecodeError as e: - ext_info = None - logger.error(f"Json error in {self.rsl_plate_num}: {e}") - # Updated 2023-09 to use the extraction kit to pull reagents. + # except JSONDecodeError as e: + # ext_info = None + # logger.error(f"Json error in {self.rsl_plate_num}: {e}") + output = { + "id": self.id, + "Plate Number": self.rsl_plate_num, + "Submission Type": self.submission_type_name, + # "Submission Category": self.submission_category, + "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, + # "Technician": self.technician, + "Cost": self.run_cost, + # "reagents": reagents, + # "samples": samples, + # "extraction_info": ext_info, + # "comment": comments, + # "equipment": equipment + } + if report: + return output if full_data: logger.debug(f"Attempting reagents.") try: @@ -160,60 +181,27 @@ class BasicSubmission(BaseClass): except Exception as e: logger.error(f"Error setting comment: {self.comment}") comments = None - output = { - "id": self.id, - "Plate Number": self.rsl_plate_num, - "Submission Type": self.submission_type_name, - "Submission Category": self.submission_category, - "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, - "Technician": self.technician, - "Cost": self.run_cost, - "reagents": reagents, - "samples": samples, - "extraction_info": ext_info, - "comment": comments, - "equipment": equipment - } - return output - - def report_dict(self) -> dict: - """ - dictionary used in creating reports - - Returns: - dict: dictionary used in creating reports - """ - # get lab name from nested organization object - try: - sub_lab = self.submitting_lab.name - except AttributeError: - sub_lab = None - try: - sub_lab = sub_lab.replace("_", " ").title() - except AttributeError: - pass - # get extraction kit name from nested kittype object - try: - ext_kit = self.extraction_kit.name - except AttributeError: - ext_kit = None - output = { - "id": self.id, - "Plate Number": self.rsl_plate_num, - "Submission Type": self.submission_type_name.replace("_", " ").title(), - "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 - } + output["Submission Category"] = self.submission_category + output["Technician"] = self.technician + output["reagents"] = reagents + output["samples"] = samples + output["extraction_info"] = ext_info + output["comment"] = comments + output["equipment"] = equipment return output + def calculate_column_count(self) -> int: + """ + Calculate the number of columns in this submission + + Returns: + int: Number of unique columns. + """ + # logger.debug(f"Here's the samples: {self.samples}") + columns = set([assoc.column for assoc in self.submission_sample_associations]) + # logger.debug(f"Here are the columns for {self.rsl_plate_num}: {columns}") + return len(columns) + def calculate_base_cost(self): """ Calculates cost of the plate @@ -225,7 +213,7 @@ class BasicSubmission(BaseClass): logger.error(f"Column count error: {e}") # Get kit associated with this submission assoc = [item for item in self.extraction_kit.kit_submissiontype_associations if item.submission_type == self.submission_type][0] - # logger.debug(f"Came up with association: {assoc}") + logger.debug(f"Came up with association: {assoc}") # If every individual cost is 0 this is probably an old plate. if all(item == 0.0 for item in [assoc.constant_cost, assoc.mutable_cost_column, assoc.mutable_cost_sample]): try: @@ -238,18 +226,6 @@ class BasicSubmission(BaseClass): except Exception as e: logger.error(f"Calculation error: {e}") self.run_cost = round(self.run_cost, 2) - - def calculate_column_count(self) -> int: - """ - Calculate the number of columns in this submission - - Returns: - int: Number of unique columns. - """ - # logger.debug(f"Here's the samples: {self.samples}") - columns = set([assoc.column for assoc in self.submission_sample_associations]) - # logger.debug(f"Here are the columns for {self.rsl_plate_num}: {columns}") - return len(columns) def hitpick_plate(self) -> list: """ @@ -363,9 +339,9 @@ class BasicSubmission(BaseClass): # logger.debug(f"Looking up organization: {value}") field_value = Organization.query(name=value) # logger.debug(f"Got {field_value} for organization {value}") - case "submitter_plate_num": - # logger.debug(f"Submitter plate id: {value}") - field_value = value + # case "submitter_plate_num": + # # logger.debug(f"Submitter plate id: {value}") + # field_value = value case "samples": for sample in value: # logger.debug(f"Parsing {sample} to sql.") @@ -388,7 +364,12 @@ class BasicSubmission(BaseClass): if value == "" or value == None or value == 'null': field_value = None else: - field_value = dict(name="submitter", text=value, time=datetime.now()) + field_value = dict(name=getuser(), text=value, time=datetime.now()) + if self.comment is None: + self.comment = [field_value] + else: + self.comment.append(field_value) + return case _: field_value = value # insert into field @@ -1010,21 +991,23 @@ class BacterialCulture(BasicSubmission): polymorphic_load="inline", inherit_condition=(id == BasicSubmission.id)) - def to_dict(self, full_data:bool=False, backup:bool=False) -> dict: + def to_dict(self, full_data:bool=False, backup:bool=False, report:bool=False) -> dict: """ Extends parent class method to add controls to dict Returns: dict: dictionary used in submissions summary """ - output = super().to_dict(full_data=full_data, backup=backup) + output = super().to_dict(full_data=full_data, backup=backup, report=report) + if report: + return output if full_data: output['controls'] = [item.to_sub_dict() for item in self.controls] return output @classmethod - def get_abbreviation(cls) -> str: - return "BC" + def get_default_info(cls) -> dict: + return dict(abbreviation="BC", submission_type="Bacterial Culture") @classmethod def custom_platemap(cls, xl: pd.ExcelFile, plate_map: pd.DataFrame) -> pd.DataFrame: @@ -1066,20 +1049,27 @@ class BacterialCulture(BasicSubmission): 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()[0:2].upper()) + input_excel["Sample List"].cell(row=15, column=2, value=getuser()) return input_excel @classmethod - def enforce_name(cls, instr:str, data:dict|None=None) -> str: + def enforce_name(cls, instr:str, data:dict|None={}) -> str: """ Extends parent """ from backend.validators import RSLNamer - data['abbreviation'] = cls.get_abbreviation() + defaults = cls.get_default_info() + data['abbreviation'] = defaults['abbreviation'] + if 'submission_type' not in data.keys() or data['submission_type'] in [None, ""]: + data['submission_type'] = defaults['submission_type'] outstr = super().enforce_name(instr=instr, data=data) + if outstr in [None, ""]: + outstr = RSLNamer.construct_new_plate_name(data=data) + if re.search(rf"{data['abbreviation']}", outstr, flags=re.IGNORECASE) is None: + outstr = re.sub(rf"RSL-?", rf"RSL-{data['abbreviation']}-", outstr, flags=re.IGNORECASE) try: outstr = re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\1\2\3", outstr) - outstr = re.sub(r"BC(\d{6})", r"BC-\1", outstr, flags=re.IGNORECASE) + outstr = re.sub(rf"{data['abbreviation']}(\d{6})", rf"{data['abbreviation']}-\1", outstr, flags=re.IGNORECASE).upper() except (AttributeError, TypeError) as e: outstr = RSLNamer.construct_new_plate_name(data=data) try: @@ -1117,14 +1107,14 @@ class BacterialCulture(BasicSubmission): template += "_{{ submitting_lab }}_{{ submitter_plate_num }}" return template - @classmethod - def parse_info(cls, input_dict: dict, xl: pd.ExcelFile | None = None) -> dict: - """ - Extends parent - """ - input_dict = super().parse_info(input_dict, xl) - input_dict['submitted_date']['missing'] = True - return input_dict + # @classmethod + # def parse_info(cls, input_dict: dict, xl: pd.ExcelFile | None = None) -> dict: + # """ + # Extends parent + # """ + # input_dict = super().parse_info(input_dict, xl) + # input_dict['submitted_date']['missing'] = True + # return input_dict @classmethod def custom_sample_autofill_row(cls, sample, worksheet: Worksheet) -> int: @@ -1158,25 +1148,29 @@ class Wastewater(BasicSubmission): polymorphic_load="inline", inherit_condition=(id == BasicSubmission.id)) - def to_dict(self, full_data:bool=False, backup:bool=False) -> dict: + def to_dict(self, full_data:bool=False, backup:bool=False, report:bool=False) -> dict: """ Extends parent class method to add controls to dict Returns: dict: dictionary used in submissions summary """ - output = super().to_dict(full_data=full_data) + output = super().to_dict(full_data=full_data, backup=backup, report=report) + if report: + return output try: - output['pcr_info'] = json.loads(self.pcr_info) + # output['pcr_info'] = json.loads(self.pcr_info) + output['pcr_info'] = self.pcr_info except TypeError as e: pass - output['Technician'] = f"Enr: {self.technician}, Ext: {self.ext_technician}, PCR: {self.pcr_technician}" - + ext_tech = self.ext_technician or self.technician + pcr_tech = self.pcr_technician or self.technician + output['Technician'] = f"Enr: {self.technician}, Ext: {ext_tech}, PCR: {pcr_tech}" return output @classmethod - def get_abbreviation(cls) -> str: - return "WW" + def get_default_info(cls) -> dict: + return dict(abbreviation="WW", submission_type="Wastewater") @classmethod def parse_info(cls, input_dict:dict, xl:pd.ExcelFile|None=None) -> dict: @@ -1212,7 +1206,7 @@ class Wastewater(BasicSubmission): except ValueError: logger.error("Well call number doesn't match sample number") logger.debug(f"Well call df: {well_call_df}") - for ii, row in samples_df.iterrows(): + for _, row in samples_df.iterrows(): try: sample_obj = [sample for sample in samples if sample['sample'] == row[3]][0] except IndexError: @@ -1238,7 +1232,8 @@ class Wastewater(BasicSubmission): Extends parent """ from backend.validators import RSLNamer - data['abbreviation'] = cls.get_abbreviation() + defaults = cls.get_default_info() + data['abbreviation'] = defaults['abbreviation'] outstr = super().enforce_name(instr=instr, data=data) try: outstr = re.sub(r"PCR(-|_)", "", outstr) @@ -1313,14 +1308,16 @@ class WastewaterArtic(BasicSubmission): polymorphic_load="inline", inherit_condition=(id == BasicSubmission.id)) - def to_dict(self, full_data:bool=False, backup:bool=False) -> dict: + def to_dict(self, full_data:bool=False, backup:bool=False, report:bool=False) -> dict: """ Extends parent class method to add controls to dict Returns: dict: dictionary used in submissions summary """ - output = super().to_dict(full_data=full_data) + output = super().to_dict(full_data=full_data, backup=backup, report=report) + if report: + return output output['gel_info'] = self.gel_info output['gel_image'] = self.gel_image output['dna_core_submission_number'] = self.dna_core_submission_number @@ -2140,7 +2137,7 @@ class SubmissionSampleAssociation(BaseClass): tooltip_text += sample['tooltip'] except KeyError: pass - sample.update(dict(name=self.sample.submitter_id[:10], tooltip=tooltip_text)) + sample.update(dict(Name=self.sample.submitter_id[:10], tooltip=tooltip_text)) return sample @classmethod diff --git a/src/submissions/frontend/widgets/app.py b/src/submissions/frontend/widgets/app.py index 2c2c010..6bc47b0 100644 --- a/src/submissions/frontend/widgets/app.py +++ b/src/submissions/frontend/widgets/app.py @@ -124,7 +124,7 @@ class App(QMainWindow): Show the 'about' message """ output = f"Version: {self.ctx.package.__version__}\n\nAuthor: {self.ctx.package.__author__['name']} - {self.ctx.package.__author__['email']}\n\nCopyright: {self.ctx.package.__copyright__}" - about = AlertPop(message=output, status="information") + about = AlertPop(message=output, status="Information") about.exec() def openDocs(self): diff --git a/src/submissions/frontend/widgets/submission_table.py b/src/submissions/frontend/widgets/submission_table.py index 34263ce..38118e7 100644 --- a/src/submissions/frontend/widgets/submission_table.py +++ b/src/submissions/frontend/widgets/submission_table.py @@ -178,12 +178,14 @@ class SubmissionsSheet(QTableView): except AttributeError: continue if sub.extraction_info != None: - existing = json.loads(sub.extraction_info) + # existing = json.loads(sub.extraction_info) + existing = sub.extraction_info else: existing = None # Check if the new info already exists in the imported submission try: - if json.dumps(new_run) in sub.extraction_info: + # if json.dumps(new_run) in sub.extraction_info: + if new_run in sub.extraction_info: logger.debug(f"Looks like we already have that info.") continue except TypeError: @@ -194,13 +196,16 @@ class SubmissionsSheet(QTableView): logger.debug(f"Updating {type(existing)}: {existing} with {type(new_run)}: {new_run}") existing.append(new_run) logger.debug(f"Setting: {existing}") - sub.extraction_info = json.dumps(existing) + # sub.extraction_info = json.dumps(existing) + sub.extraction_info = existing except TypeError: logger.error(f"Error updating!") - sub.extraction_info = json.dumps([new_run]) + # sub.extraction_info = json.dumps([new_run]) + sub.extraction_info = [new_run] logger.debug(f"Final ext info for {sub.rsl_plate_num}: {sub.extraction_info}") else: - sub.extraction_info = json.dumps([new_run]) + # sub.extraction_info = json.dumps([new_run]) + sub.extraction_info = [new_run] sub.save() self.report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information')) @@ -250,30 +255,35 @@ class SubmissionsSheet(QTableView): continue # check if pcr_info already exists if hasattr(sub, 'pcr_info') and sub.pcr_info != None: - existing = json.loads(sub.pcr_info) + # existing = json.loads(sub.pcr_info) + existing = sub.pcr_info else: existing = None # check if this entry already exists in imported submission try: - if json.dumps(new_run) in sub.pcr_info: + # if json.dumps(new_run) in sub.pcr_info: + if new_run in sub.pcr_info: logger.debug(f"Looks like we already have that info.") continue else: count += 1 except TypeError: logger.error(f"No json to dump") - if existing != None: + if existing is not None: try: logger.debug(f"Updating {type(existing)}: {existing} with {type(new_run)}: {new_run}") existing.append(new_run) logger.debug(f"Setting: {existing}") + # sub.pcr_info = json.dumps(existing) sub.pcr_info = json.dumps(existing) except TypeError: logger.error(f"Error updating!") - sub.pcr_info = json.dumps([new_run]) + # sub.pcr_info = json.dumps([new_run]) + sub.pcr_info = [new_run] logger.debug(f"Final ext info for {sub.rsl_plate_num}: {sub.pcr_info}") else: - sub.pcr_info = json.dumps([new_run]) + # sub.pcr_info = json.dumps([new_run]) + sub.pcr_info = [new_run] sub.save() self.report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information')) @@ -305,7 +315,7 @@ class SubmissionsSheet(QTableView): # find submissions based on date range subs = BasicSubmission.query(start_date=info['start_date'], end_date=info['end_date']) # convert each object to dict - records = [item.report_dict() for item in subs] + records = [item.to_dict(report=True) for item in subs] logger.debug(f"Records: {pformat(records)}") # make dataframe from record dictionaries detailed_df, summary_df = make_report_xlsx(records=records) diff --git a/src/submissions/frontend/widgets/submission_widget.py b/src/submissions/frontend/widgets/submission_widget.py index 60bbe08..1482ffc 100644 --- a/src/submissions/frontend/widgets/submission_widget.py +++ b/src/submissions/frontend/widgets/submission_widget.py @@ -151,23 +151,28 @@ class SubmissionFormContainer(QWidget): return # Check if PCR info already exists if hasattr(sub, 'pcr_info') and sub.pcr_info != None: - existing = json.loads(sub.pcr_info) + # existing = json.loads(sub.pcr_info) + existing = sub.pcr_info else: existing = None if existing != None: # update pcr_info try: logger.debug(f"Updating {type(existing)}: {existing} with {type(parser.pcr)}: {parser.pcr}") - if json.dumps(parser.pcr) not in sub.pcr_info: + # if json.dumps(parser.pcr) not in sub.pcr_info: + if parser.pcr not in sub.pcr_info: existing.append(parser.pcr) logger.debug(f"Setting: {existing}") - sub.pcr_info = json.dumps(existing) + # sub.pcr_info = json.dumps(existing) + sub.pcr_info = existing except TypeError: logger.error(f"Error updating!") - sub.pcr_info = json.dumps([parser.pcr]) + # sub.pcr_info = json.dumps([parser.pcr]) + sub.pcr_info = [parser.pcr] logger.debug(f"Final pcr info for {sub.rsl_plate_num}: {sub.pcr_info}") else: - sub.pcr_info = json.dumps([parser.pcr]) + # sub.pcr_info = json.dumps([parser.pcr]) + sub.pcr_info = [parser.pcr] logger.debug(f"Existing {type(sub.pcr_info)}: {sub.pcr_info}") logger.debug(f"Inserting {type(json.dumps(parser.pcr))}: {json.dumps(parser.pcr)}") sub.save(original=False)