diff --git a/src/submissions/backend/db/models/__init__.py b/src/submissions/backend/db/models/__init__.py index 21f510c..dfaa7ca 100644 --- a/src/submissions/backend/db/models/__init__.py +++ b/src/submissions/backend/db/models/__init__.py @@ -457,12 +457,12 @@ class BaseClass(Base): """ match input_date: case datetime() | date(): - output_date = input_date#.strftime("%Y-%m-%d %H:%M:%S") + output_date = input_date case int(): output_date = datetime.fromordinal( - datetime(1900, 1, 1).toordinal() + input_date - 2)#.date().strftime("%Y-%m-%d %H:%M:%S") + datetime(1900, 1, 1).toordinal() + input_date - 2) case _: - output_date = parse(input_date)#.strftime("%Y-%m-%d %H:%M:%S") + output_date = parse(input_date) if eod: addition_time = datetime.max.time() else: diff --git a/src/submissions/backend/db/models/kits.py b/src/submissions/backend/db/models/kits.py index 557def1..2017880 100644 --- a/src/submissions/backend/db/models/kits.py +++ b/src/submissions/backend/db/models/kits.py @@ -126,7 +126,13 @@ class KitType(BaseClass): submission_type=ST)) #: Association proxy to SubmissionTypeKitTypeAssociation @classproperty - def aliases(cls): + def aliases(cls) -> List[str]: + """ + Gets other names the sql object of this class might go by. + + Returns: + List[str]: List of names + """ return super().aliases + [cls.query_alias, "kit_types", "kit_type"] @hybrid_property @@ -1077,7 +1083,13 @@ class SubmissionType(BaseClass): return self.processes @classproperty - def aliases(cls): + def aliases(cls) -> List[str]: + """ + Gets other names the sql object of this class might go by. + + Returns: + List[str]: List of names + """ return super().aliases + ["submission_types", "submission_type"] @classproperty diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index 73fdacc..ed39afa 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -1540,12 +1540,15 @@ class BacterialCulture(BasicSubmission): return report parser = ConcentrationParser(filepath=fname, submission=self) conc_samples = [sample for sample in parser.samples] + # logger.debug(f"Concentration samples: {pformat(conc_samples)}") for sample in self.samples: - logger.debug(f"Sample {sample.submitter_id}") + # logger.debug(f"Sample {sample.submitter_id}") + # logger.debug(f"Match {item['submitter_id']}") try: # NOTE: Fix for ENs which have no rsl_number... - sample_dict = next(item for item in conc_samples if item['submitter_id'] == sample.submitter_id) + sample_dict = next(item for item in conc_samples if str(item['submitter_id']).upper() == sample.submitter_id) except StopIteration: + logger.error(f"Couldn't find sample dict for {sample.submitter_id}") continue logger.debug(f"Sample {sample.submitter_id} conc. = {sample_dict['concentration']}") if sample_dict['concentration']: @@ -1734,12 +1737,10 @@ class Wastewater(BasicSubmission): sample[f"ct_{sample['target'].lower()}"] = sample['ct'] if isinstance(sample['ct'], float) else 0.0 # NOTE: Set assessment # logger.debug(f"Sample assessemnt: {sample['assessment']}") - # sample[f"{sample['target'].lower()}_status"] = sample['assessment'] # NOTE: Get sample having other target other_targets = [s for s in samples if re.sub('-N\\d*$', '', s['sample']) == sample['sample']] for s in other_targets: sample[f"ct_{s['target'].lower()}"] = s['ct'] if isinstance(s['ct'], float) else 0.0 - # sample[f"{s['target'].lower()}_status"] = s['assessment'] try: del sample['ct'] except KeyError: @@ -2177,7 +2178,6 @@ class WastewaterArtic(BasicSubmission): except AttributeError: plate_num = "1" plate_num = plate_num.strip("-") - # repeat_num = re.search(r"R(?P\d)?$", "PBS20240426-2R").groups()[0] try: repeat_num = re.search(r"R(?P\d)?$", processed).groups()[0] except: @@ -2663,16 +2663,6 @@ class BasicSample(BaseClass, LogMixin): def delete(self): raise AttributeError(f"Delete not implemented for {self.__class__}") - # @classmethod - # def get_searchables(cls) -> List[dict]: - # """ - # 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")] - @classmethod def samples_to_df(cls, sample_list: List[BasicSample], **kwargs) -> pd.DataFrame: """ @@ -2729,8 +2719,6 @@ class WastewaterSample(BasicSample): Derivative wastewater sample """ - # searchables = BasicSample.searchables + ['ww_processing_num', 'ww_full_sample_id', 'rsl_number'] - id = Column(INTEGER, ForeignKey('_basicsample.id'), primary_key=True) ww_processing_num = Column(String(64)) #: wastewater processing number ww_full_sample_id = Column(String(64)) #: full id given by entrics diff --git a/src/submissions/backend/validators/omni_gui_objects.py b/src/submissions/backend/validators/omni_gui_objects.py index aaf51b0..9be55af 100644 --- a/src/submissions/backend/validators/omni_gui_objects.py +++ b/src/submissions/backend/validators/omni_gui_objects.py @@ -24,10 +24,25 @@ class BaseOmni(BaseModel): return f"<{self.__class__.__name__}({self.__repr_name__})>" @classproperty - def aliases(cls): + def aliases(cls) -> List[str]: + """ + Gets other names the sql object of this class might go by. + + Returns: + List[str]: List of names + """ return cls.class_object.aliases def check_all_attributes(self, attributes: dict) -> bool: + """ + Compares this pobject to dictionary of attributes to determine equality. + + Args: + attributes (dict): + + Returns: + bool: result + """ # logger.debug(f"Incoming attributes: {attributes}") attributes = {k : v for k, v in attributes.items() if k in self.list_searchables.keys()} for key, value in attributes.items(): @@ -47,7 +62,14 @@ class BaseOmni(BaseModel): # logger.debug("Everything checks out, these are the same object.") return True - def __setattr__(self, key, value): + def __setattr__(self, key: str, value: Any): + """ + Overrides built in dunder method + + Args: + key (str): + value (Any): + """ try: class_value = getattr(self.class_object, key) except AttributeError: @@ -151,12 +173,22 @@ class OmniSubmissionType(BaseOmni): super().__init__(**data) self.instance_object = instance_object - def to_dataframe_dict(self): + @property + def dataframe_dict(self) -> dict: + """ + Dictionary of gui relevant values. + + Returns: + dict: result + """ return dict( name=self.name ) def to_sql(self): + """ + Convert this object to an instance of its class object. + """ instance, new = self.class_object.query_or_create(name=self.name) instance.info_map = self.info_map instance.defaults = self.defaults @@ -191,12 +223,23 @@ class OmniReagentRole(BaseOmni): super().__init__(**data) self.instance_object = instance_object - def to_dataframe_dict(self): + @property + def dataframe_dict(self) -> dict: + """ + Dictionary of gui relevant values. + + Returns: + dict: result + """ return dict( name=self.name ) def to_sql(self): + """ + Convert this object to an instance of its class object. + """ + instance, new = self.class_object.query_or_create(name=self.name) if new: instance.eol_ext = self.eol_ext @@ -252,7 +295,14 @@ class OmniSubmissionTypeKitTypeAssociation(BaseOmni): super().__init__(**data) self.instance_object = instance_object - def to_dataframe_dict(self): + @property + def dataframe_dict(self) -> dict: + """ + Dictionary of gui relevant values. + + Returns: + dict: result + """ if isinstance(self.submissiontype, OmniSubmissionType): submissiontype = self.submissiontype.name else: @@ -270,6 +320,10 @@ class OmniSubmissionTypeKitTypeAssociation(BaseOmni): ) def to_sql(self): + """ + Convert this object to an instance of its class object. + """ + # logger.debug(f"Self kittype: {self.submissiontype}") if issubclass(self.submissiontype.__class__, BaseOmni): submissiontype = SubmissionType.query(name=self.submissiontype.name) @@ -288,7 +342,13 @@ class OmniSubmissionTypeKitTypeAssociation(BaseOmni): return instance @property - def list_searchables(self): + def list_searchables(self) -> dict: + """ + Provides attributes for checking this object against a dictionary. + + Returns: + dict: result + """ if isinstance(self.kittype, OmniKitType): kit = self.kittype.name else: @@ -334,7 +394,14 @@ class OmniKitTypeReagentRoleAssociation(BaseOmni): super().__init__(**data) self.instance_object = instance_object - def to_dataframe_dict(self): + @property + def dataframe_dict(self) -> dict: + """ + Dictionary of gui relevant values. + + Returns: + dict: result + """ if isinstance(self.submission_type, OmniSubmissionType): submission_type = self.submission_type.name else: @@ -349,12 +416,16 @@ class OmniKitTypeReagentRoleAssociation(BaseOmni): else: reagent_role = self.reagent_role return dict( - reagent_role=reagent_role, - submission_type=submission_type, - kit_type=kit_type + reagentrole=reagent_role, + submissiontype=submission_type, + kittype=kit_type ) def to_sql(self): + """ + Convert this object to an instance of its class object. + """ + if isinstance(self.reagent_role, OmniReagentRole): reagent_role = self.reagent_role.name else: @@ -387,7 +458,13 @@ class OmniKitTypeReagentRoleAssociation(BaseOmni): return instance @property - def list_searchables(self): + def list_searchables(self) -> dict: + """ + Provides attributes for checking this object against a dictionary. + + Returns: + dict: result + """ if isinstance(self.kit_type, OmniKitType): kit = self.kit_type.name else: @@ -420,12 +497,23 @@ class OmniEquipmentRole(BaseOmni): super().__init__(**data) self.instance_object = instance_object - def to_dataframe_dict(self): + @property + def dataframe_dict(self) -> dict: + """ + Dictionary of gui relevant values. + + Returns: + dict: result + """ return dict( name=self.name ) def to_sql(self): + """ + Convert this object to an instance of its class object. + """ + instance, new = self.class_object.query_or_create(name=self.name) return instance @@ -447,12 +535,23 @@ class OmniTips(BaseOmni): super().__init__(**data) self.instance_object = instance_object - def to_dataframe_dict(self): + @property + def dataframe_dict(self) -> dict: + """ + Dictionary of gui relevant values. + + Returns: + dict: result + """ return dict( name=self.name ) def to_sql(self): + """ + Convert this object to an instance of its class object. + """ + instance, new = self.class_object.query_or_create(name=self.name) return instance @@ -475,13 +574,24 @@ class OmniTipRole(BaseOmni): super().__init__(**data) self.instance_object = instance_object - def to_dataframe_dict(self): + @property + def dataframe_dict(self) -> dict: + """ + Dictionary of gui relevant values. + + Returns: + dict: result + """ return dict( name=self.name, tips=[item.name for item in self.tips] ) def to_sql(self): + """ + Convert this object to an instance of its class object. + """ + instance, new = self.class_object.query_or_create(name=self.name) for tips in self.tips: tips.to_sql() @@ -504,10 +614,17 @@ class OmniProcess(BaseOmni): super().__init__(**data) self.instance_object = instance_object - def to_dataframe_dict(self): - submissiontypes = [item.name for item in self.submission_types] + @property + def dataframe_dict(self) -> dict: + """ + Dictionary of gui relevant values. + + Returns: + dict: result + """ + submissiontypes = [item if isinstance(item, str) else item.name for item in self.submission_types] logger.debug(f"Submission Types: {submissiontypes}") - equipmentroles = [item.name for item in self.equipment_roles] + equipmentroles = [item if isinstance(item, str) else item.name for item in self.equipment_roles] logger.debug(f"Equipment Roles: {equipmentroles}") return dict( name=self.name, @@ -523,6 +640,10 @@ class OmniProcess(BaseOmni): return value def to_sql(self): + """ + Convert this object to an instance of its class object. + """ + instance, new = self.class_object.query_or_create(name=self.name) for st in self.submission_types: try: @@ -548,7 +669,13 @@ class OmniProcess(BaseOmni): return instance @property - def list_searchables(self): + def list_searchables(self) -> dict: + """ + Provides attributes for checking this object against a dictionary. + + Returns: + dict: result + """ return dict(name=self.name) @@ -572,17 +699,24 @@ class OmniKitType(BaseOmni): super().__init__(**data) self.instance_object = instance_object - def to_dataframe_dict(self): + @property + def dataframe_dict(self) -> dict: + """ + Dictionary of gui relevant values. + + Returns: + dict: result + """ return dict( name=self.name ) def to_sql(self) -> KitType: + """ + Convert this object to an instance of its class object. + """ + kit, is_new = KitType.query_or_create(name=self.name) - # if is_new: - # logger.debug(f"New kit made: {kit}") - # else: - # logger.debug(f"Kit retrieved: {kit}") new_rr = [] for rr_assoc in self.kit_reagentrole_associations: new_assoc = rr_assoc.to_sql() @@ -603,9 +737,6 @@ class OmniKitType(BaseOmni): if new_process not in new_processes: new_processes.append(new_process) kit.processes = new_processes - # logger.debug(f"Kit: {pformat(kit.__dict__)}") - # for item in kit.kit_reagentrole_associations: - # logger.debug(f"KTRRassoc: {item.__dict__}") return kit @@ -622,7 +753,14 @@ class OmniOrganization(BaseOmni): super().__init__(**data) self.instance_object = instance_object - def to_dataframe_dict(self): + @property + def dataframe_dict(self) -> dict: + """ + Dictionary of gui relevant values. + + Returns: + dict: result + """ return dict( name=self.name, cost_centre=self.cost_centre, @@ -639,14 +777,27 @@ class OmniContact(BaseOmni): phone: str = Field(default="", description="property") @property - def list_searchables(self): + def list_searchables(self) -> dict: + """ + Provides attributes for checking this object against a dictionary. + + Returns: + dict: result + """ return dict(name=self.name, email=self.email) def __init__(self, instance_object: Any, **data): super().__init__(**data) self.instance_object = instance_object - def to_dataframe_dict(self): + @property + def dataframe_dict(self) -> dict: + """ + Dictionary of gui relevant values. + + Returns: + dict: result + """ return dict( name=self.name, email=self.email, @@ -654,9 +805,9 @@ class OmniContact(BaseOmni): ) def to_sql(self): + """ + Convert this object to an instance of its class object. + """ + contact, is_new = Contact.query_or_create(name=self.name, email=self.email, phone=self.phone) - # if is_new: - # logger.debug(f"New contact made: {contact}") - # else: - # logger.debug(f"Contact retrieved: {contact}") return contact diff --git a/src/submissions/frontend/visualizations/__init__.py b/src/submissions/frontend/visualizations/__init__.py index b6a5b0f..d96e665 100644 --- a/src/submissions/frontend/visualizations/__init__.py +++ b/src/submissions/frontend/visualizations/__init__.py @@ -4,6 +4,8 @@ Contains all operations for creating charts, graphs and visual effects. from datetime import timedelta, date from pathlib import Path from typing import Generator + +import plotly from PyQt6.QtWidgets import QWidget import pandas as pd, logging from plotly.graph_objects import Figure @@ -126,8 +128,8 @@ class CustomFigure(Figure): html = f'' if self is not None: # NOTE: Just cannot get this load from string to freaking work. - html += self.to_html(include_plotlyjs='cdn', full_html=False) - # html += plotly.offline.plot(self, output_type='div', include_plotlyjs=True) + # html += self.to_html(include_plotlyjs='cdn', full_html=False) + html += plotly.offline.plot(self, output_type='div', include_plotlyjs="cdn") else: html += "

No data was retrieved for the given parameters.

" html += '' diff --git a/src/submissions/frontend/widgets/omni_manager_pydant.py b/src/submissions/frontend/widgets/omni_manager_pydant.py index 2603d18..7c1f8bf 100644 --- a/src/submissions/frontend/widgets/omni_manager_pydant.py +++ b/src/submissions/frontend/widgets/omni_manager_pydant.py @@ -6,7 +6,7 @@ from json.decoder import JSONDecodeError from datetime import datetime, timedelta from pprint import pformat from typing import Any, List, Literal -from PyQt6.QtCore import QSortFilterProxyModel, Qt +from PyQt6.QtCore import QSortFilterProxyModel, Qt, QModelIndex from PyQt6.QtGui import QAction, QCursor from PyQt6.QtWidgets import ( QLabel, QDialog, @@ -114,7 +114,16 @@ class ManagerWindow(QDialog): # logger.debug(f"Instance: {self.instance}") self.update_data() - def update_instance(self, initial: bool = False): + def update_instance(self, initial: bool = False) -> None: + """ + Gets the proper instance of this object's class object. + + Args: + initial (bool): Whether this is the initial creation of this object. + + Returns: + None + """ if self.add_edit == "edit" or initial: try: # logger.debug(f"Querying with {self.options.currentText()}") @@ -146,6 +155,7 @@ class ManagerWindow(QDialog): [item for item in self.findChildren(QDialogButtonBox)] for item in deletes: item.setParent(None) + logger.debug(f"Self.omni_object: {self.omni_object}") fields = self.omni_object.__class__.model_fields for key, info in fields.items(): # logger.debug(f"Attempting to set {key}, {info} widget") @@ -193,14 +203,22 @@ class ManagerWindow(QDialog): # logger.debug(f"Instance coming from parsed form: {self.omni_object.__dict__}") return self.omni_object - def add_new(self): + def add_new(self) -> None: + """ + Creates a new instance of this object's class object. + + Returns: + None + """ new_instance = self.class_object() self.instance = new_instance self.update_options() class EditProperty(QWidget): - + """ + Class to manage info items of SQL objects. + """ def __init__(self, parent: ManagerWindow, key: str, column_type: Any, value): super().__init__(parent) self.label = QLabel(key.title().replace("_", " ")) @@ -245,7 +263,13 @@ class EditProperty(QWidget): self.layout.addWidget(self.widget, 0, 1, 1, 3) self.setLayout(self.layout) - def parse_form(self): + def parse_form(self) -> dict: + """ + Gets values from this EditProperty form. + + Returns: + dict: Dictionary of values. + """ # logger.debug(f"Parsing widget {self.objectName()}: {type(self.widget)}") match self.widget: case QLineEdit(): @@ -269,7 +293,7 @@ class EditRelationship(QWidget): from backend.db import models super().__init__(parent) self.class_object = getattr(models, class_object) - logger.debug(f"Attempt value: {value}") + # logger.debug(f"Attempt value: {value}") # logger.debug(f"Class object: {self.class_object}") self.setParent(parent) # logger.debug(f"Edit relationship class_object: {self.class_object}") @@ -317,7 +341,13 @@ class EditRelationship(QWidget): self.setLayout(self.layout) self.set_data() - def update_buttons(self): + def update_buttons(self) -> None: + """ + Enables/disables buttons based on whether property is a list and has data. + + Returns: + None + """ if not self.relationship.property.uselist and len(self.data) >= 1: # logger.debug(f"Property {self.relationship} doesn't use list and data is of length: {len(self.data)}") self.add_button.setEnabled(False) @@ -326,14 +356,23 @@ class EditRelationship(QWidget): self.add_button.setEnabled(True) self.existing_button.setEnabled(True) - def parse_row(self, x): + def parse_row(self, x: QModelIndex) -> None: + """ + + Args: + x (): + + Returns: + + """ context = {item: x.sibling(x.row(), self.df.columns.get_loc(item)).data() for item in self.df.columns} + # logger.debug(f"Context: {pformat(context)}") try: object = self.class_object.query(**context) except KeyError: object = None self.widget.doubleClicked.disconnect() - self.add_edit(instance=object) + self.add_new(instance=object) def add_new(self, instance: Any = None, add_edit: Literal["add", "edit"] = "add", index: int | None = None): if add_edit == "edit": @@ -388,12 +427,13 @@ class EditRelationship(QWidget): """ sets data in model """ - # logger.debug(f"Self.data: {self.data}") + logger.debug(f"Self.data: {self.data}") try: - records = [item.to_dataframe_dict() for item in self.data] - except AttributeError: + records = [item.dataframe_dict for item in self.data] + except AttributeError as e: + logger.error(e) records = [] - # logger.debug(f"Records: {records}") + logger.debug(f"Records: {records}") self.df = DataFrame.from_records(records) try: self.columns_of_interest = [dict(name=item, column=self.df.columns.get_loc(item)) for item in self.extras]