From ea24a8ffd4ad18cc8b7f574ef97dd54413bcfdd2 Mon Sep 17 00:00:00 2001 From: lwark Date: Wed, 19 Mar 2025 13:50:37 -0500 Subject: [PATCH] Beginning prelim code cleanup. --- CHANGELOG.md | 9 + src/submissions/backend/db/models/__init__.py | 8 +- src/submissions/backend/db/models/kits.py | 206 +++++++++++++++- .../backend/db/models/submissions.py | 57 ++++- src/submissions/backend/excel/parser.py | 29 ++- .../backend/validators/omni_gui_objects.py | 222 +++++++++++++----- src/submissions/backend/validators/pydant.py | 1 + .../visualizations/turnaround_chart.py | 22 +- .../frontend/widgets/omni_manager_pydant.py | 82 +++++-- .../frontend/widgets/omni_search.py | 3 +- 10 files changed, 529 insertions(+), 110 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d10a4b3..72082e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# 202503.04 + +- Kit editor debugging. +- Fixed missing search bar in Edit Reagent. + +# 202503.03 + +- Kit editor pre-release. + # 202501.02 - Fixed bug where Wastewater ENs were not receiving rsl_number and therefore not getting PCR data. diff --git a/src/submissions/backend/db/models/__init__.py b/src/submissions/backend/db/models/__init__.py index 8dc93ae..f04a2fb 100644 --- a/src/submissions/backend/db/models/__init__.py +++ b/src/submissions/backend/db/models/__init__.py @@ -46,7 +46,7 @@ class BaseClass(Base): """ Abstract class to pass ctx values to all SQLAlchemy objects. """ - __abstract__ = True #: NOTE: Will not be added to DB + __abstract__ = True #: NOTE: Will not be added to DB as a table __table_args__ = {'extend_existing': True} #: Will only add new columns @@ -54,6 +54,7 @@ class BaseClass(Base): omni_removes = ["id", 'submissions', "omnigui_class_dict", "omnigui_instance_dict"] omni_sort = ["name"] omni_inheritable = [] + searchables = [] @classproperty def skip_on_edit(cls): @@ -416,7 +417,10 @@ class BaseClass(Base): else: value = existing + [value] else: - value = [value] + if isinstance(value, list): + value = value + else: + value = [value] value = list(set(value)) logger.debug(f"Final value for {key}: {value}") return super().__setattr__(key, value) diff --git a/src/submissions/backend/db/models/kits.py b/src/submissions/backend/db/models/kits.py index 1e7168d..df1577c 100644 --- a/src/submissions/backend/db/models/kits.py +++ b/src/submissions/backend/db/models/kits.py @@ -858,7 +858,7 @@ class SubmissionType(BaseClass): id = Column(INTEGER, primary_key=True) #: primary key name = Column(String(128), unique=True) #: name of submission type - info_map = Column(JSON) #: Where basic information is found in the excel workbook corresponding to this type. + info_map = Column(JSON) #: Where parsable information is found in the excel workbook corresponding to this type. defaults = Column(JSON) #: Basic information about this submission type instances = relationship("BasicSubmission", backref="submission_type") #: Concrete instances of this type. template_file = Column(BLOB) #: Blank form for this type stored as binary. @@ -866,6 +866,200 @@ class SubmissionType(BaseClass): secondary=submissiontypes_processes) #: Relation to equipment processes used for this type. sample_map = Column(JSON) #: Where sample information is found in the excel sheet corresponding to this type. + """ + Example info_map (Bacterial Culture) + NOTE: read locations will be appended to write locations. + + { + "comment": { + "read": [ + { + "column": 2, + "row": 34, + "sheet": "Sample List" + } + ], + "write": [] + }, + "contact": { + "read": [ + { + "column": 2, + "row": 4, + "sheet": "Sample List" + } + ], + "write": [] + }, + "contact_phone": { + "read": [], + "write": [ + { + "column": 2, + "row": 5, + "sheet": "Sample List" + } + ] + }, + "cost_centre": { + "read": [ + { + "column": 2, + "row": 6, + "sheet": "Sample List" + } + ], + "write": [] + }, + "custom": {}, + "extraction_kit": { + "read": [ + { + "column": 4, + "row": 5, + "sheet": "Sample List" + } + ], + "write": [] + }, + "rsl_plate_num": { + "read": [ + { + "column": 2, + "row": 13, + "sheet": "Sample List" + } + ], + "write": [] + }, + "sample_count": { + "read": [ + { + "column": 4, + "row": 4, + "sheet": "Sample List" + } + ], + "write": [] + }, + "signed_by": { + "read": [], + "write": [ + { + "column": 2, + "row": 15, + "sheet": "Sample List" + } + ] + }, + "submission_category": { + "read": [ + { + "column": 4, + "row": 6, + "sheet": "Sample List" + } + ], + "write": [] + }, + "submission_type": { + "read": [ + { + "column": 4, + "row": 3, + "sheet": "Sample List" + } + ], + "write": [] + }, + "submitted_date": { + "read": [ + { + "column": 2, + "row": 3, + "sheet": "Sample List" + } + ], + "write": [] + }, + "submitter_plate_num": { + "read": [ + { + "column": 2, + "row": 2, + "sheet": "Sample List" + } + ], + "write": [] + }, + "submitting_lab": { + "read": [ + { + "column": 4, + "row": 2, + "sheet": "Sample List" + } + ], + "write": [] + }, + "technician": { + "read": [ + { + "column": 2, + "row": 14, + "sheet": "Sample List" + } + ], + "write": [] + } + } + """ + + """ + Example defaults (for Bacterial Culture) + + { + "abbreviation": "BC", + "details_ignore": [ + "controls" + ], + "form_ignore": [ + "controls", + "cost_centre" + ], + "regex": "(?PRSL(?:-|_)?BC(?:-|_)?20\\d{2}-?\\d{2}-?\\d{2}(?:(_|-)?\\d?([^_0123456789\\sA-QS-Z]|$)?R?\\d?)?)", + "sample_type": "Bacterial Culture Sample", + "turnaround_time": 3 + } + """ + + """ + Example sample_map (Bacterial Culture) + + { + "lookup_table": { + "end_row": 132, + "merge_on_id": "submitter_id", + "sample_columns": { + "column": 6, + "concentration": 4, + "organism": 3, + "row": 5, + "submitter_id": 2 + }, + "sheet": "Sample List", + "start_row": 37 + }, + "plate_map": { + "end_column": 13, + "end_row": 14, + "sheet": "Plate Map", + "start_column": 2, + "start_row": 7 + } + } + """ + submissiontype_kit_associations = relationship( "SubmissionTypeKitTypeAssociation", back_populates="submission_type", @@ -1218,6 +1412,11 @@ class SubmissionType(BaseClass): sample_map=self.sample_map ) + @classproperty + def info_map_json_edit_fields(cls): + dicto = dict() + return dicto + class SubmissionTypeKitTypeAssociation(BaseClass): """ @@ -1519,7 +1718,7 @@ class KitTypeReagentRoleAssociation(BaseClass): case _: pass setattr(instance, k, v) - logger.info(f"Instance from query or create: {instance.__dict__}") + logger.info(f"Instance from query or create: {instance.__dict__}\nis new: {new}") # sys.exit() return instance, new @@ -2196,6 +2395,7 @@ class Process(BaseClass): id = Column(INTEGER, primary_key=True) #: Process id, primary key name = Column(String(64), unique=True) #: Process name + # version = Column(String(32)) submission_types = relationship("SubmissionType", back_populates='processes', secondary=submissiontypes_processes) #: relation to SubmissionType equipment = relationship("Equipment", back_populates='processes', @@ -2209,6 +2409,7 @@ class Process(BaseClass): tip_roles = relationship("TipRole", back_populates='processes', secondary=process_tiprole) #: relation to KitType + def __repr__(self) -> str: """ Returns: @@ -2308,6 +2509,7 @@ class Process(BaseClass): return OmniProcess( instance_object=self, name=self.name, + # version=self.version, submission_types=[item.to_omni() for item in self.submission_types], equipment_roles=[item.to_omni() for item in self.equipment_roles], tip_roles=[item.to_omni() for item in self.tip_roles] diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index 925484b..e663198 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -32,7 +32,6 @@ from jinja2 import Template from PIL import Image - logger = logging.getLogger(f"submissions.{__name__}") @@ -460,7 +459,7 @@ class BasicSubmission(BaseClass, LogMixin): """ rows = range(1, plate_rows + 1) columns = range(1, plate_columns + 1) - logger.debug(f"sample list for plate map: {pformat(sample_list)}") + # logger.debug(f"sample list for plate map: {pformat(sample_list)}") # NOTE: An overly complicated list comprehension create a list of sample locations # NOTE: next will return a blank cell if no value found for row/column output_samples = [next((item for item in sample_list if item['row'] == row and item['column'] == column), @@ -1340,8 +1339,7 @@ class BasicSubmission(BaseClass, LogMixin): for equip in equipment: logger.debug(f"Parsed equipment: {equip}") _, assoc = equip.to_sql(submission=self) - logger.debug(f"Got equipment association: {assoc.__dict__}") - + logger.debug(f"Got equipment association: {assoc} for {equip}") try: assoc.save() except AttributeError as e: @@ -1488,6 +1486,55 @@ class BacterialCulture(BasicSubmission): input_dict = super().custom_info_parser(input_dict=input_dict, xl=xl, custom_fields=custom_fields) return input_dict + def custom_context_events(self) -> dict: + """ + Sets context events for main widget + + Returns: + dict: Context menu items for this instance. + """ + events = super().custom_context_events() + events['Import Concentration'] = self.import_concentration + return events + + @report_result + def import_concentration(self, obj) -> Report: + from frontend.widgets import select_open_file + from backend.excel.parser import ConcentrationParser + report = Report() + fname = select_open_file(obj=obj, file_extension="xlsx") + if not fname: + report.add_result(Result(msg="No file selected, cancelling.", status="Warning")) + return report + parser = ConcentrationParser(filepath=fname, submission=self) + conc_samples = [sample for sample in parser.samples] + for sample in self.samples: + logger.debug(f"Sample {sample.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) + except StopIteration: + continue + logger.debug(f"Sample {sample.submitter_id} conc. = {sample_dict['concentration']}") + if sample_dict['concentration']: + sample.concentration = sample_dict['concentration'] + else: + continue + sample.save() + # logger.debug(conc_samples) + return report + + @classmethod + def parse_concentration(cls, xl: Workbook, rsl_plate_num: str) -> Generator[dict, None, None]: + lookup_table = cls.get_submission_type().sample_map['lookup_table'] + logger.debug(lookup_table) + main_sheet = xl[lookup_table['sheet']] + for row in main_sheet.iter_rows(min_row=lookup_table['start_row'], max_row=lookup_table['end_row']): + idx = row[0].row + sample = dict(submitter_id=main_sheet.cell(row=idx, column=lookup_table['sample_columns']['submitter_id']).value) + sample['concentration'] = main_sheet.cell(row=idx, column=lookup_table['sample_columns']['concentration']).value + yield sample + class Wastewater(BasicSubmission): """ @@ -2598,7 +2645,7 @@ class BasicSample(BaseClass, LogMixin): self.show_details(obj) -# Below are the custom sample types +# NOTE: Below are the custom sample types class WastewaterSample(BasicSample): """ diff --git a/src/submissions/backend/excel/parser.py b/src/submissions/backend/excel/parser.py index a5168d7..f70de73 100644 --- a/src/submissions/backend/excel/parser.py +++ b/src/submissions/backend/excel/parser.py @@ -677,10 +677,7 @@ class PCRParser(object): def pcr_info(self) -> dict: """ Parse general info rows for all types of PCR results - - Args: - sheet_name (str): Name of sheet in excel workbook that holds info. - """ + """ info_map = self.submission_obj.get_submission_type().sample_map['pcr_general_info'] sheet = self.xl[info_map['sheet']] iter_rows = sheet.iter_rows(min_row=info_map['start_row'], max_row=info_map['end_row']) @@ -695,3 +692,27 @@ class PCRParser(object): pcr[key] = value pcr['imported_by'] = getuser() return pcr + + +class ConcentrationParser(object): + + def __init__(self, filepath: Path | None = None, submission: BasicSubmission | None = None) -> None: + if filepath is None: + logger.error('No filepath given.') + self.xl = None + else: + try: + self.xl = load_workbook(filepath) + except ValueError as e: + logger.error(f'Incorrect value: {e}') + self.xl = None + except PermissionError: + logger.error(f"Couldn't get permissions for {filepath.__str__()}. Operation might have been cancelled.") + return None + if submission is None: + self.submission_obj = BacterialCulture + rsl_plate_num = None + else: + self.submission_obj = submission + rsl_plate_num = self.submission_obj.rsl_plate_num + self.samples = self.submission_obj.parse_concentration(xl=self.xl, rsl_plate_num=rsl_plate_num) diff --git a/src/submissions/backend/validators/omni_gui_objects.py b/src/submissions/backend/validators/omni_gui_objects.py index 6618803..285faba 100644 --- a/src/submissions/backend/validators/omni_gui_objects.py +++ b/src/submissions/backend/validators/omni_gui_objects.py @@ -10,7 +10,6 @@ logger = logging.getLogger(f"submissions.{__name__}") class BaseOmni(BaseModel): - instance_object: Any | None = Field(default=None) def __repr__(self): @@ -23,67 +22,107 @@ class BaseOmni(BaseModel): def aliases(cls): return cls.class_object.aliases + # NOTE: Okay, this will not work for editing, since by definition not all attributes will line up. + # def check_all_attributes(self, attributes: dict) -> bool: + # """ + # Checks this instance against a dictionary of attributes to determine if they are a match. + # + # Args: + # attributes (dict): A dictionary of attributes to be check for equivalence + # + # Returns: + # bool: If a single unequivocal value is found will be false, else true. + # """ + # logger.debug(f"Incoming attributes: {attributes}") + # for key, value in attributes.items(): + # logger.debug(f"Comparing value class: {value.__class__} to omni class") + # if isinstance(value, str): + # try: + # check = value.lower() == "none" + # except AttributeError: + # continue + # if check: + # value = None + # logger.debug(f"Attempting to grab attribute: {key}") + # try: + # self_value = getattr(self, key) + # class_attr = getattr(self.class_object, key) + # except AttributeError: + # continue + # try: + # logger.debug(f"Check if {self_value.__class__} is subclass of {BaseOmni}") + # check = issubclass(self_value.__class__, BaseOmni) + # except TypeError as e: + # logger.error(f"Couldn't check if {self_value.__class__} is subclass of {BaseOmni} due to {e}") + # check = False + # if check: + # logger.debug(f"Checking for subclass name.") + # self_value = self_value.name + # try: + # logger.debug(f"Check if {value.__class__} is subclass of {BaseOmni}") + # check = issubclass(value.__class__, BaseOmni) + # except TypeError as e: + # logger.error(f"Couldn't check if {value.__class__} is subclass of {BaseOmni} due to {e}") + # check = False + # if check: + # logger.debug(f"Checking for subclass name.") + # value = value.name + # logger.debug(f"Self value: {self_value}, class attr: {class_attr} of type: {type(class_attr)}") + # if isinstance(class_attr, property): + # filter = "property" + # else: + # filter = class_attr.property + # match filter: + # case ColumnProperty(): + # match class_attr.type: + # case INTEGER(): + # if value.lower() == "true": + # value = 1 + # elif value.lower() == "false": + # value = 0 + # else: + # value = int(value) + # case FLOAT(): + # value = float(value) + # case "property": + # pass + # case _RelationshipDeclared(): + # logger.debug(f"Checking relationship value: {self_value}") + # try: + # self_value = self_value.name + # except AttributeError: + # pass + # if class_attr.property.uselist: + # self_value = self_value.__str__() + # logger.debug( + # f"Checking self_value {self_value} of type {type(self_value)} against attribute {value} of type {type(value)}") + # if self_value != value: + # output = False + # logger.debug(f"Value {key} is False, returning.") + # return output + # return True + def check_all_attributes(self, attributes: dict) -> bool: - """ - Checks this instance against a dictionary of attributes to determine if they are a match. - - Args: - attributes (dict): A dictionary of attributes to be check for equivalence - - Returns: - bool: If a single unequivocal value is found will be false, else true. - """ 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(): - if value.lower() == "none": - value = None - logger.debug(f"Attempting to grab attribute: {key}") - self_value = getattr(self, key) - class_attr = getattr(self.class_object, key) - # logger.debug(f"Self value: {self_value}, class attr: {class_attr} of type: {type(class_attr)}") - if isinstance(class_attr, property): - filter = "property" - else: - filter = class_attr.property - match filter: - case ColumnProperty(): - match class_attr.type: - case INTEGER(): - if value.lower() == "true": - value = 1 - elif value.lower() == "false": - value = 0 - else: - value = int(value) - case FLOAT(): - value = float(value) - case "property": - pass - case _RelationshipDeclared(): - logger.debug(f"Checking {self_value}") - try: - self_value = self_value.name - except AttributeError: - pass - if class_attr.property.uselist: - self_value = self_value.__str__() try: - logger.debug(f"Check if {self_value.__class__} is subclass of {self.__class__}") - check = issubclass(self_value.__class__, self.__class__) + logger.debug(f"Check if {value.__class__} is subclass of {BaseOmni}") + check = issubclass(value.__class__, BaseOmni) except TypeError as e: - logger.error(f"Couldn't check if {self_value.__class__} is subclass of {self.__class__} due to {e}") + logger.error(f"Couldn't check if {value.__class__} is subclass of {BaseOmni} due to {e}") check = False if check: logger.debug(f"Checking for subclass name.") - self_value = self_value.name - logger.debug( - f"Checking self_value {self_value} of type {type(self_value)} against attribute {value} of type {type(value)}") - if self_value != value: - output = False - logger.debug(f"Value {key} is False, returning.") - return output + value = value.name + self_value = self.list_searchables[key] + if value != self_value: + logger.debug(f"Value {key} is False, these are not the same object.") + return False + logger.debug("Everything checks out, these are the same object.") return True + def __setattr__(self, key, value): try: class_value = getattr(self.class_object, key) @@ -176,6 +215,13 @@ class OmniSubmissionType(BaseOmni): return {} return value + @field_validator("template_file", mode="before") + @classmethod + def provide_blank_template_file(cls, value): + if value is None: + value = bytes() + return value + def __init__(self, instance_object: Any, **data): super().__init__(**data) self.instance_object = instance_object @@ -296,7 +342,6 @@ class OmniSubmissionTypeKitTypeAssociation(BaseOmni): constant_cost=self.constant_cost ) - def to_sql(self): logger.debug(f"Self kittype: {self.submissiontype}") if issubclass(self.submissiontype.__class__, BaseOmni): @@ -315,6 +360,18 @@ class OmniSubmissionTypeKitTypeAssociation(BaseOmni): instance.constant_cost = self.constant_cost return instance + @property + def list_searchables(self): + if isinstance(self.kittype, OmniKitType): + kit = self.kittype.name + else: + kit = self.kittype + if isinstance(self.submissiontype, OmniSubmissionType): + subtype = self.submissiontype.name + else: + subtype = self.submissiontype + return dict(kittype=kit, submissiontype=subtype) + class OmniKitTypeReagentRoleAssociation(BaseOmni): class_object: ClassVar[Any] = KitTypeReagentRoleAssociation @@ -325,6 +382,11 @@ class OmniKitTypeReagentRoleAssociation(BaseOmni): submission_type: str | OmniSubmissionType = Field(default="", description="relationship", title="SubmissionType") kit_type: str | OmniKitType = Field(default="", description="relationship", title="KitType") + def __repr__(self): + try: + return f"" + except AttributeError: + return f"" @field_validator("uses", mode="before") @classmethod @@ -362,19 +424,46 @@ class OmniKitTypeReagentRoleAssociation(BaseOmni): reagent_role = self.reagent_role.name else: reagent_role = self.reagent_role + if issubclass(self.submission_type.__class__, BaseOmni): + submissiontype = self.submission_type.name + else: + submissiontype = self.submission_type + if issubclass(self.kit_type.__class__, BaseOmni): + kittype = self.kit_type.name + else: + kittype = self.kit_type instance, new = self.class_object.query_or_create( reagentrole=reagent_role, - kittype=self.kit_type, - submissiontype=self.submission_type + kittype=kittype, + submissiontype=submissiontype ) + logger.debug(f"KitTypeReagentRoleAssociation coming out of query_or_create: {instance.__dict__}\nnew: {new}") if new: + logger.warning(f"This is a new instance: {instance.__dict__}") reagent_role = self.reagent_role.to_sql() instance.reagent_role = reagent_role logger.debug(f"KTRRAssoc uses: {self.uses}") instance.uses = self.uses + instance.required = int(self.required) logger.debug(f"KitTypeReagentRoleAssociation: {pformat(instance.__dict__)}") return instance + @property + def list_searchables(self): + if isinstance(self.kit_type, OmniKitType): + kit = self.kit_type.name + else: + kit = self.kit_type + if isinstance(self.submission_type, OmniSubmissionType): + subtype = self.submission_type.name + else: + subtype = self.submission_type + if isinstance(self.reagent_role, OmniReagentRole): + reagentrole = self.reagent_role.name + else: + reagentrole = self.reagent_role + return dict(kit_type=kit, submission_type=subtype, reagent_role=reagentrole) + class OmniEquipmentRole(BaseOmni): class_object: ClassVar[Any] = EquipmentRole @@ -463,6 +552,7 @@ class OmniProcess(BaseOmni): # NOTE: How am I going to figure out relatioinships without getting into recursion issues? name: str = Field(default="", description="property") #: Process name + # version: str = Field(default="", description="property") #: Version (string to account for "in_use" or whatever) submission_types: List[OmniSubmissionType] | List[str] = Field(default=[], description="relationship", title="SubmissionType") equipment_roles: List[OmniEquipmentRole] | List[str] = Field(default=[], description="relationship", @@ -491,6 +581,13 @@ class OmniProcess(BaseOmni): return "" return value + # @field_validator("version", mode="before") + # @classmethod + # def rescue_name_none(cls, value): + # if not value: + # return "1" + # return value + def to_sql(self): instance, new = self.class_object.query_or_create(name=self.name) for st in self.submission_types: @@ -507,17 +604,21 @@ class OmniProcess(BaseOmni): instance.tip_roles.append(new_assoc) return instance + @property + def list_searchables(self): + return dict(name=self.name) + class OmniKitType(BaseOmni): class_object: ClassVar[Any] = KitType name: str = Field(default="", description="property") kit_submissiontype_associations: List[OmniSubmissionTypeKitTypeAssociation] | List[str] = Field(default=[], - description="relationship", - title="SubmissionTypeKitTypeAssociation") + description="relationship", + title="SubmissionTypeKitTypeAssociation") kit_reagentrole_associations: List[OmniKitTypeReagentRoleAssociation] | List[str] = Field(default=[], - description="relationship", - title="KitTypeReagentRoleAssociation") + description="relationship", + title="KitTypeReagentRoleAssociation") processes: List[OmniProcess] | List[str] = Field(default=[], description="relationship", title="Process") @field_validator("name", mode="before") @@ -548,8 +649,9 @@ class OmniKitType(BaseOmni): if new_assoc not in new_rr: logger.debug(f"Adding {new_assoc} to kit_reagentrole_associations") new_rr.append(new_assoc) - logger.debug(f"Setting kit_reagentrole_associations to {new_rr}") + logger.debug(f"Setting kit_reagentrole_associations to {pformat([item.__dict__ for item in new_rr])}") kit.kit_reagentrole_associations = new_rr + # sys.exit() new_st = [] for st_assoc in self.kit_submissiontype_associations: new_assoc = st_assoc.to_sql() diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py index 9055f0d..d4500e7 100644 --- a/src/submissions/backend/validators/pydant.py +++ b/src/submissions/backend/validators/pydant.py @@ -1244,6 +1244,7 @@ class PydIridaControl(BaseModel, extra='ignore'): class PydProcess(BaseModel, extra="allow"): name: str + version: str = Field(default="1") submission_types: List[str] equipment: List[str] equipment_roles: List[str] diff --git a/src/submissions/frontend/visualizations/turnaround_chart.py b/src/submissions/frontend/visualizations/turnaround_chart.py index 42501e9..6d1d28b 100644 --- a/src/submissions/frontend/visualizations/turnaround_chart.py +++ b/src/submissions/frontend/visualizations/turnaround_chart.py @@ -19,10 +19,6 @@ class TurnaroundChart(CustomFigure): months: int = 6): super().__init__(df=df, modes=modes, settings=settings) self.df = df - # try: - # months = int(settings['months']) - # except KeyError: - # months = 6 self.construct_chart() if threshold: self.add_hline(y=threshold) @@ -31,20 +27,26 @@ class TurnaroundChart(CustomFigure): def construct_chart(self, df: pd.DataFrame | None = None): if df: self.df = df - self.df = self.df[self.df.days.notnull()] - self.df = self.df.sort_values(['submitted_date', 'name'], ascending=[True, True]).reset_index(drop=True) - self.df = self.df.reset_index().rename(columns={"index": "idx"}) try: + self.df = self.df[self.df.days.notnull()] + self.df = self.df.sort_values(['submitted_date', 'name'], ascending=[True, True]).reset_index(drop=True) + self.df = self.df.reset_index().rename(columns={"index": "idx"}) scatter = px.scatter(data_frame=self.df, x='idx', y="days", hover_data=["name", "submitted_date", "completed_date", "days"], color="acceptable", color_discrete_map={True: "green", False: "red"} ) - except ValueError: + except (ValueError, AttributeError): scatter = px.scatter() self.add_traces(scatter.data) self.update_traces(marker={'size': 15}) - tickvals = self.df['idx'].tolist() - ticklabels = self.df['name'].tolist() + try: + tickvals = self.df['idx'].tolist() + except KeyError: + tickvals = [] + try: + ticklabels = self.df['name'].tolist() + except KeyError: + ticklabels = [] self.update_layout( xaxis=dict( tickmode='array', diff --git a/src/submissions/frontend/widgets/omni_manager_pydant.py b/src/submissions/frontend/widgets/omni_manager_pydant.py index bc40ddb..604ea2f 100644 --- a/src/submissions/frontend/widgets/omni_manager_pydant.py +++ b/src/submissions/frontend/widgets/omni_manager_pydant.py @@ -37,7 +37,7 @@ class ManagerWindow(QDialog): super().__init__(parent) # NOTE: Should I pass in an instance? self.instance = instance - logger.debug(f"Setting instance: {self.instance}") + # logger.debug(f"Setting instance: {self.instance}") if not self.instance: self.class_object = self.original_type = object_type else: @@ -131,7 +131,7 @@ class ManagerWindow(QDialog): self.omni_object = self.instance.to_omni(expand=True) else: self.omni_object = self.instance - logger.debug(f"Created omni_object: {self.omni_object.__dict__}") + # logger.debug(f"Created omni_object: {self.omni_object.__dict__}") self.update_data() def update_data(self) -> None: @@ -154,6 +154,7 @@ class ManagerWindow(QDialog): value = getattr(self.omni_object, key) except AttributeError: value = None + # logger.debug(f"Got value {value} for key {key}") match info.description: # NOTE: ColumnProperties will be directly edited. case "property": @@ -163,7 +164,11 @@ class ManagerWindow(QDialog): # NOTE: RelationshipDeclareds will be given a list of existing related objects. case "relationship": # NOTE: field.comparator.class_object.class_ gives the relationship class - # logger.debug(f"Creating relationship widget with value: {value}") + # try: + # logger.debug( + # f"Creating relationship widget with value: {[pformat(item.__dict__) for item in value]}") + # except AttributeError: + # logger.debug(f"Creating relationship widget with value: {value}") widget = EditRelationship(self, key=key, class_object=info.title, value=value) case _: continue @@ -182,13 +187,13 @@ class ManagerWindow(QDialog): # TODO: Need Relationship property here too? results = [item.parse_form() for item in self.findChildren(EditProperty)] for result in results: - logger.debug(f"Incoming property result: {result}") + # logger.debug(f"Incoming property result: {result}") setattr(self.omni_object, result['field'], result['value']) # NOTE: Getting 'None' back here. - logger.debug(f"Set result: {getattr(self.instance, result['field'])}") + # logger.debug(f"Set result: {getattr(self.instance, result['field'])}") results = [item.parse_form() for item in self.findChildren(EditRelationship)] for result in results: - logger.debug(f"Incoming relationship result: {result}") + # logger.debug(f"Incoming relationship result: {result}") setattr(self.omni_object, result['field'], result['value']) # logger.debug(f"Set result: {getattr(self.omni_object, result['field'])}") # logger.debug(f"Instance coming from parsed form: {self.omni_object.__dict__}") @@ -270,12 +275,12 @@ class EditRelationship(QWidget): from backend.db import models super().__init__(parent) self.class_object = getattr(models, class_object) - logger.debug(f"Class object: {self.class_object}") + # logger.debug(f"Class object: {self.class_object}") self.setParent(parent) # logger.debug(f"Edit relationship class_object: {self.class_object}") self.label = QLabel(key.title().replace("_", " ")) self.setObjectName(key) #: key is the name of the relationship this represents - logger.debug(f"Checking relationship for {self.parent().class_object}: {key}") + # logger.debug(f"Checking relationship for {self.parent().class_object}: {key}") self.relationship = getattr(self.parent().class_object, key) self.widget = QTableView() self.add_button = QPushButton("Add New") @@ -288,15 +293,15 @@ class EditRelationship(QWidget): else: value = [] self.data = value - logger.debug(f"Set data: {self.data}") + # logger.debug(f"Set data: {self.data}") # self.update_buttons() - logger.debug(f"Parent manager: {self.parent().manager}") + # logger.debug(f"Parent manager: {self.parent().manager}") checked_manager, is_primary = check_object_in_manager(self.parent().manager, self.objectName()) if checked_manager: if not self.data: self.data = [checked_manager] try: - logger.debug(f"Relationship {key} uses list: {self.relationship.property.uselist}") + # logger.debug(f"Relationship {key} uses list: {self.relationship.property.uselist}") check = not self.relationship.property.uselist and len(self.data) >= 1 except AttributeError: check = True @@ -336,9 +341,9 @@ class EditRelationship(QWidget): self.widget.doubleClicked.disconnect() self.add_edit(instance=object) - def add_new(self, instance: Any = None, add_edit: Literal["add", "edit"] = "add"): + def add_new(self, instance: Any = None, add_edit: Literal["add", "edit"] = "add", index: int | None = None): if add_edit == "edit": - logger.debug(f"Editing instance: {instance.__dict__}") + logger.debug(f"\n\nEditing instance: {instance.__dict__}\n\n") # NOTE: if an existing instance is not being edited, create a new instance if not instance: # logger.debug(f"Creating new instance of {self.class_object}") @@ -352,10 +357,27 @@ class EditRelationship(QWidget): logger.debug(f"New instance: {pformat(new_instance.__dict__)}") # NOTE: Somewhere between this and the next logger, I'm losing the uses data. if add_edit == "add": + logger.debug("Setting as new object") self.parent().omni_object.__setattr__(self.objectName(), new_instance) else: - instance.__dict__.update(new_instance.__dict__) - logger.debug(f"Final instance: {pformat(instance.__dict__)}") + logger.debug("Updating dictionary") + obj = getattr(self.parent().omni_object, self.objectName()) + if isinstance(obj, list): + logger.debug(f"This is a list") + # obj = obj[index] + try: + # NOTE: Okay, this will not work for editing, since by definition not all attributes will line up. + # NOTE: Set items to search by in the Omni object itself? + obj = next((item for item in obj if item.check_all_attributes(new_instance.__dict__))) + except StopIteration: + logger.error(f"Couldn't find object in list.") + return + logger.debug(f"Updating \n{pformat(obj)} with \n{pformat(new_instance.__dict__)}") + obj.__dict__.update(new_instance.__dict__) + # # self.parent().omni_object.__setattr__(self.objectName(), obj) + # # instance.__dict__.update(new_instance.__dict__) + logger.debug(f"Final instance: {pformat(self.parent().omni_object.__dict__)}") + # NOTE: somewhere in the update_data I'm losing changes. self.parent().update_data() def add_existing(self): @@ -415,9 +437,9 @@ class EditRelationship(QWidget): row_data = {self.df.columns[column]: self.widget.model().index(id.row(), column).data() for column in range(self.widget.model().columnCount())} # logger.debug(f"Row data: {row_data}") - logger.debug(f"Attempting to grab {self.objectName()} from {self.parent().omni_object}") + # logger.debug(f"Attempting to grab {self.objectName()} from {self.parent().omni_object}") object = getattr(self.parent().omni_object, self.objectName()) - logger.debug(f"Initial object: {object}") + # logger.debug(f"Initial object: {object}") if isinstance(object, list): try: object = next((item for item in object if item.check_all_attributes(attributes=row_data))) @@ -437,14 +459,15 @@ class EditRelationship(QWidget): edit_action = QAction(f"Edit {object.name}", self) except AttributeError: edit_action = QAction(f"Edit object", self) - edit_action.triggered.connect(lambda: self.add_new(instance=object.instance_object, add_edit="edit")) + edit_action.triggered.connect( + lambda: self.add_new(instance=object.instance_object, add_edit="edit", index=id.row())) self.menu.addAction(edit_action) self.menu.popup(QCursor.pos()) def remove_item(self, object): - logger.debug(f"Attempting to remove {object} from {self.parent().instance.__dict__}") + # logger.debug(f"Attempting to remove {object} from {self.parent().instance.__dict__}") editor = getattr(self.parent().omni_object, self.objectName().lower()) - logger.debug(f"Editor: {editor}") + # logger.debug(f"Editor: {editor}") try: # logger.debug(f"Using remove technique") editor.remove(object) @@ -453,9 +476,9 @@ class EditRelationship(QWidget): setattr(self.parent().omni_object, self.objectName().lower(), None) except ValueError as e: logger.error(f"Remove failed for {self.objectName().lower()} due to {e}.") - logger.debug(f"Setting {self.objectName()} to {editor}") + # logger.debug(f"Setting {self.objectName()} to {editor}") setattr(self.parent().omni_object, self.objectName().lower(), editor) - logger.debug(f"After set: {getattr(self.parent().omni_object, self.objectName().lower())}") + # logger.debug(f"After set: {getattr(self.parent().omni_object, self.objectName().lower())}") self.set_data() self.update_buttons() @@ -466,7 +489,10 @@ class EditRelationship(QWidget): except AttributeError: check = False if check and isinstance(self.data, list): - output_data = self.data[0] + try: + output_data = self.data[0] + except IndexError: + output_data = [] else: output_data = self.data return dict(field=self.objectName(), value=output_data) @@ -476,7 +502,7 @@ class JsonEditButton(QWidget): def __init__(self, parent, key: str, value: str = ""): super().__init__(parent) - logger.debug(f"Setting jsonedit data to: {value}") + # logger.debug(f"Setting jsonedit data to: {value}") self.data = value self.setParent(parent) self.setObjectName(key) @@ -495,7 +521,7 @@ class JsonEditButton(QWidget): self.edit_box.widget.textChanged.connect(self.set_json_to_text) def set_json_to_text(self): - logger.debug(self.edit_box.widget.toPlainText()) + # logger.debug(self.edit_box.widget.toPlainText()) text = self.edit_box.widget.toPlainText() try: jsoner = json.loads(text) @@ -529,7 +555,11 @@ class JsonEditScreen(QDialog): try: self.json_field = getattr(self.class_obj, f"{parameter}_json_edit_fields") except AttributeError: - self.json_field = self.class_obj.json_edit_fields + try: + self.json_field = self.class_obj.json_edit_fields + except AttributeError: + logger.error(f"No json fields to edit.") + return match self.json_field: case dict(): for key, value in self.json_field.items(): diff --git a/src/submissions/frontend/widgets/omni_search.py b/src/submissions/frontend/widgets/omni_search.py index f9a5de9..b679a85 100644 --- a/src/submissions/frontend/widgets/omni_search.py +++ b/src/submissions/frontend/widgets/omni_search.py @@ -60,6 +60,7 @@ class SearchBox(QDialog): Changes form inputs based on sample type """ search_fields = [] + # search_fields = self.object_type.searchables logger.debug(f"Search fields: {search_fields}") deletes = [item for item in self.findChildren(FieldSearch)] for item in deletes: @@ -68,7 +69,7 @@ class SearchBox(QDialog): if not self.sub_class: logger.warning(f"No subclass selected.") self.update_data() - return + # return else: if self.sub_class.currentText() == "Any": self.object_type = self.original_type