From 62b826c2d17213af37b75e642d21abefcc0d04ab Mon Sep 17 00:00:00 2001 From: lwark Date: Fri, 14 Jun 2024 10:38:46 -0500 Subject: [PATCH] Created WastewaterArticAssociation, added tip handling. --- alembic/versions/d2b094cfa308_adding_tips.py | 8 +- src/submissions/backend/db/models/kits.py | 2 +- .../backend/db/models/submissions.py | 6 +- src/submissions/backend/excel/parser.py | 2 +- src/submissions/backend/excel/writer.py | 138 ++++++++++++++++-- src/submissions/backend/validators/pydant.py | 3 +- .../frontend/widgets/equipment_usage.py | 63 ++++---- 7 files changed, 163 insertions(+), 59 deletions(-) diff --git a/alembic/versions/d2b094cfa308_adding_tips.py b/alembic/versions/d2b094cfa308_adding_tips.py index 4cf94fb..94e1e7a 100644 --- a/alembic/versions/d2b094cfa308_adding_tips.py +++ b/alembic/versions/d2b094cfa308_adding_tips.py @@ -55,11 +55,11 @@ def upgrade() -> None: op.create_table('_submissiontipsassociation', sa.Column('tip_id', sa.INTEGER(), nullable=False), sa.Column('submission_id', sa.INTEGER(), nullable=False), - sa.Column('role_name', sa.String(), nullable=True), - sa.ForeignKeyConstraint(['role_name'], ['_tiprole.name'], ), - sa.ForeignKeyConstraint(['submission_id'], ['_submissiontype.id'], ), + sa.Column('role_name', sa.String(), nullable=False), + # sa.ForeignKeyConstraint(['role_name'], ['_tiprole.name'], ), + sa.ForeignKeyConstraint(['submission_id'], ['_basicsubmission.id'], ), sa.ForeignKeyConstraint(['tip_id'], ['_tips.id'], ), - sa.PrimaryKeyConstraint('tip_id', 'submission_id') + sa.PrimaryKeyConstraint('tip_id', 'submission_id', 'role_name') ) op.create_table('_tiproles_tips', sa.Column('tiprole_id', sa.INTEGER(), nullable=True), diff --git a/src/submissions/backend/db/models/kits.py b/src/submissions/backend/db/models/kits.py index 19419f1..12b577b 100644 --- a/src/submissions/backend/db/models/kits.py +++ b/src/submissions/backend/db/models/kits.py @@ -1601,7 +1601,7 @@ class SubmissionTipsAssociation(BaseClass): back_populates="submission_tips_associations") #: associated submission tips = relationship(Tips, back_populates="tips_submission_associations") #: associated equipment - role_name = Column(String(32)) #, ForeignKey("_tiprole.name")) + role_name = Column(String(32), primary_key=True) #, ForeignKey("_tiprole.name")) # role = relationship(TipRole) diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index c8b3f36..ecd45c5 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -2095,7 +2095,7 @@ class BasicSample(BaseClass): used_class = cls.find_polymorphic_subclass(attrs=sanitized_kwargs, polymorphic_identity=sample_type) instance = used_class(**sanitized_kwargs) instance.sample_type = sample_type - logger.debug(f"Creating instance: {instance}") + # logger.debug(f"Creating instance: {instance}") return instance @classmethod @@ -2352,7 +2352,7 @@ class SubmissionSampleAssociation(BaseClass): self.id = id else: self.id = self.__class__.autoincrement_id() - logger.debug(f"Looking at kwargs: {pformat(kwargs)}") + # logger.debug(f"Looking at kwargs: {pformat(kwargs)}") for k,v in kwargs.items(): try: self.__setattr__(k, v) @@ -2538,7 +2538,7 @@ class SubmissionSampleAssociation(BaseClass): Returns: SubmissionSampleAssociation: Queried or new association. """ - logger.debug(f"Attempting create or query with {kwargs}") + # logger.debug(f"Attempting create or query with {kwargs}") match submission: case BasicSubmission(): pass diff --git a/src/submissions/backend/excel/parser.py b/src/submissions/backend/excel/parser.py index cb78b18..86f07da 100644 --- a/src/submissions/backend/excel/parser.py +++ b/src/submissions/backend/excel/parser.py @@ -613,7 +613,7 @@ class EquipmentParser(object): previous_asset = asset asset = self.get_asset_number(input=asset) logger.debug(f"asset: {asset}") - eq = Equipment.query(name=asset) + eq = Equipment.query(asset_number=asset) process = ws.cell(row=v['process']['row'], column=v['process']['column']).value try: output.append( diff --git a/src/submissions/backend/excel/writer.py b/src/submissions/backend/excel/writer.py index 74f1bbf..520acdc 100644 --- a/src/submissions/backend/excel/writer.py +++ b/src/submissions/backend/excel/writer.py @@ -15,7 +15,7 @@ logger = logging.getLogger(f"submissions.{__name__}") class SheetWriter(object): """ - object to pull and contain data from excel file + object to manage data placement into excel file """ def __init__(self, submission: PydSubmission, missing_only: bool = False): @@ -60,35 +60,52 @@ class SheetWriter(object): self.write_tips() def write_info(self): + """ + Calls info writer + """ disallowed = ['filepath', 'reagents', 'samples', 'equipment', 'controls'] info_dict = {k: v for k, v in self.sub.items() if k not in disallowed} writer = InfoWriter(xl=self.xl, submission_type=self.submission_type, info_dict=info_dict) self.xl = writer.write_info() def write_reagents(self): + """ + Calls reagent writer + """ reagent_list = self.sub['reagents'] writer = ReagentWriter(xl=self.xl, submission_type=self.submission_type, extraction_kit=self.sub['extraction_kit'], reagent_list=reagent_list) self.xl = writer.write_reagents() def write_samples(self): + """ + Calls sample writer + """ sample_list = self.sub['samples'] writer = SampleWriter(xl=self.xl, submission_type=self.submission_type, sample_list=sample_list) self.xl = writer.write_samples() def write_equipment(self): + """ + Calls equipment writer + """ equipment_list = self.sub['equipment'] writer = EquipmentWriter(xl=self.xl, submission_type=self.submission_type, equipment_list=equipment_list) self.xl = writer.write_equipment() def write_tips(self): + """ + Calls tip writer + """ tips_list = self.sub['tips'] writer = TipWriter(xl=self.xl, submission_type=self.submission_type, tips_list=tips_list) self.xl = writer.write_tips() class InfoWriter(object): - + """ + object to write general submission info into excel file + """ def __init__(self, xl: Workbook, submission_type: SubmissionType | str, info_dict: dict, sub_object:BasicSubmission|None=None): logger.debug(f"Info_dict coming into InfoWriter: {pformat(info_dict)}") if isinstance(submission_type, str): @@ -103,6 +120,16 @@ class InfoWriter(object): # logger.debug(pformat(self.info)) def reconcile_map(self, info_dict: dict, info_map: dict) -> dict: + """ + Merge info with its locations + + Args: + info_dict (dict): dictionary of info items + info_map (dict): dictionary of info locations + + Returns: + dict: merged dictionary + """ output = {} for k, v in info_dict.items(): if v is None: @@ -119,7 +146,13 @@ class InfoWriter(object): # logger.debug(f"Reconciled info: {pformat(output)}") return output - def write_info(self): + def write_info(self) -> Workbook: + """ + Performs write operations + + Returns: + Workbook: workbook with info written. + """ for k, v in self.info.items(): # NOTE: merge all comments to fit in single cell. if k == "comment" and isinstance(v['value'], list): @@ -138,7 +171,9 @@ class InfoWriter(object): class ReagentWriter(object): - + """ + object to write reagent data into excel file + """ def __init__(self, xl: Workbook, submission_type: SubmissionType | str, extraction_kit: KitType | str, reagent_list: list): self.xl = xl @@ -149,7 +184,17 @@ class ReagentWriter(object): reagent_map = kit_type.construct_xl_map_for_use(submission_type) self.reagents = self.reconcile_map(reagent_list=reagent_list, reagent_map=reagent_map) - def reconcile_map(self, reagent_list, reagent_map) -> List[dict]: + def reconcile_map(self, reagent_list:List[dict], reagent_map:dict) -> List[dict]: + """ + Merge reagents with their locations + + Args: + reagent_list (List[dict]): List of reagent dictionaries + reagent_map (dict): Reagent locations + + Returns: + List[dict]: merged dictionary + """ output = [] for reagent in reagent_list: try: @@ -168,7 +213,13 @@ class ReagentWriter(object): output.append(placeholder) return output - def write_reagents(self): + def write_reagents(self) -> Workbook: + """ + Performs write operations + + Returns: + Workbook: Workbook with reagents written + """ for reagent in self.reagents: sheet = self.xl[reagent['sheet']] for k, v in reagent.items(): @@ -181,7 +232,9 @@ class ReagentWriter(object): class SampleWriter(object): - + """ + object to write sample data into excel file + """ def __init__(self, xl: Workbook, submission_type: SubmissionType | str, sample_list: list): if isinstance(submission_type, str): submission_type = SubmissionType.query(name=submission_type) @@ -190,7 +243,16 @@ class SampleWriter(object): self.sample_map = submission_type.construct_sample_map()['lookup_table'] self.samples = self.reconcile_map(sample_list) - def reconcile_map(self, sample_list: list): + def reconcile_map(self, sample_list: list) -> List[dict]: + """ + Merge sample info with locations + + Args: + sample_list (list): List of sample information + + Returns: + List[dict]: List of merged dictionaries + """ output = [] multiples = ['row', 'column', 'assoc_id', 'submission_rank'] for sample in sample_list: @@ -204,10 +266,16 @@ class SampleWriter(object): output.append(new) return sorted(output, key=lambda k: k['submission_rank']) - def write_samples(self): + def write_samples(self) -> Workbook: + """ + Performs writing operations. + + Returns: + Workbook: Workbook with samples written + """ sheet = self.xl[self.sample_map['sheet']] columns = self.sample_map['sample_columns'] - for ii, sample in enumerate(self.samples): + for sample in self.samples: row = self.sample_map['start_row'] + (sample['submission_rank'] - 1) for k, v in sample.items(): try: @@ -219,7 +287,9 @@ class SampleWriter(object): class EquipmentWriter(object): - + """ + object to write equipment data into excel file + """ def __init__(self, xl: Workbook, submission_type: SubmissionType | str, equipment_list: list): if isinstance(submission_type, str): submission_type = SubmissionType.query(name=submission_type) @@ -228,7 +298,17 @@ class EquipmentWriter(object): equipment_map = self.submission_type.construct_equipment_map() self.equipment = self.reconcile_map(equipment_list=equipment_list, equipment_map=equipment_map) - def reconcile_map(self, equipment_list: list, equipment_map: list): + def reconcile_map(self, equipment_list: list, equipment_map: dict) -> List[dict]: + """ + Merges equipment with location data + + Args: + equipment_list (list): List of equipment + equipment_map (dict): Dictionary of equipment locations + + Returns: + List[dict]: List of merged dictionaries + """ output = [] if equipment_list is None: return output @@ -248,6 +328,8 @@ class EquipmentWriter(object): # logger.error(f"Keyerror: {e}") continue placeholder[k] = dicto + if "asset_number" not in mp_info.keys(): + placeholder['name']['value'] = f"{equipment['name']} - {equipment['asset_number']}" try: placeholder['sheet'] = mp_info['sheet'] except KeyError: @@ -256,7 +338,13 @@ class EquipmentWriter(object): output.append(placeholder) return output - def write_equipment(self): + def write_equipment(self) -> Workbook: + """ + Performs write operations + + Returns: + Workbook: Workbook with equipment written + """ for equipment in self.equipment: try: sheet = self.xl[equipment['sheet']] @@ -280,7 +368,9 @@ class EquipmentWriter(object): class TipWriter(object): - + """ + object to write tips data into excel file + """ def __init__(self, xl: Workbook, submission_type: SubmissionType | str, tips_list: list): if isinstance(submission_type, str): submission_type = SubmissionType.query(name=submission_type) @@ -289,7 +379,17 @@ class TipWriter(object): tips_map = self.submission_type.construct_tips_map() self.tips = self.reconcile_map(tips_list=tips_list, tips_map=tips_map) - def reconcile_map(self, tips_list: list, tips_map: list): + def reconcile_map(self, tips_list: List[dict], tips_map: dict) -> List[dict]: + """ + Merges tips with location data + + Args: + tips_list (List[dict]): List of tips + tips_map (dict): Tips locations + + Returns: + List[dict]: List of merged dictionaries + """ output = [] if tips_list is None: return output @@ -317,7 +417,13 @@ class TipWriter(object): output.append(placeholder) return output - def write_tips(self): + def write_tips(self) -> Workbook: + """ + Performs write operations + + Returns: + Workbook: Workbook with tips written + """ for tips in self.tips: try: sheet = self.xl[tips['sheet']] diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py index 1e6ecae..94e6306 100644 --- a/src/submissions/backend/validators/pydant.py +++ b/src/submissions/backend/validators/pydant.py @@ -257,7 +257,7 @@ class PydSample(BaseModel, extra='allow'): sample=instance, row=row, column=column, id=aid, submission_rank=submission_rank, **self.model_extra) - logger.debug(f"Using submission_sample_association: {association}") + # logger.debug(f"Using submission_sample_association: {association}") try: # instance.sample_submission_associations.append(association) out_associations.append(association) @@ -734,6 +734,7 @@ class PydSubmission(BaseModel, extra='allow'): for tips in self.tips: if tips is None: continue + logger.debug(f"Converting tips: {tips} to sql.") association = tips.to_sql(submission=instance) if association is not None: # association.save() diff --git a/src/submissions/frontend/widgets/equipment_usage.py b/src/submissions/frontend/widgets/equipment_usage.py index cccab99..68e95e3 100644 --- a/src/submissions/frontend/widgets/equipment_usage.py +++ b/src/submissions/frontend/widgets/equipment_usage.py @@ -2,8 +2,7 @@ import sys from pprint import pformat from PyQt6.QtCore import Qt from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox, - QLabel, QWidget, QHBoxLayout, - QVBoxLayout, QDialogButtonBox, QGridLayout) + QLabel, QWidget, QVBoxLayout, QDialogButtonBox, QGridLayout) from backend.db.models import Equipment, BasicSubmission, Process from backend.validators.pydant import PydEquipment, PydEquipmentRole, PydTips import logging @@ -11,9 +10,10 @@ from typing import List logger = logging.getLogger(f"submissions.{__name__}") + class EquipmentUsage(QDialog): - def __init__(self, parent, submission:BasicSubmission) -> QDialog: + def __init__(self, parent, submission: BasicSubmission) -> QDialog: super().__init__(parent) self.submission = submission self.setWindowTitle("Equipment Checklist") @@ -29,7 +29,7 @@ class EquipmentUsage(QDialog): def populate_form(self): """ Create form widgets - """ + """ QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel self.buttonBox = QDialogButtonBox(QBtn) self.buttonBox.accepted.connect(self.accept) @@ -49,43 +49,44 @@ class EquipmentUsage(QDialog): Returns: List[PydEquipment]: All equipment pulled from widgets - """ + """ output = [] for widget in self.findChildren(QWidget): match widget: - case RoleComboBox() : + case RoleComboBox(): if widget.check.isChecked(): output.append(widget.parse_form()) case _: pass logger.debug(f"parsed output of Equsage form: {pformat(output)}") return [item for item in output if item is not None] - + class LabelRow(QWidget): def __init__(self, parent) -> None: super().__init__(parent) - self.layout = QHBoxLayout() + self.layout = QGridLayout() self.check = QCheckBox() - self.layout.addWidget(self.check) + self.layout.addWidget(self.check, 0, 0) self.check.stateChanged.connect(self.check_all) - for item in ["Role", "Equipment", "Process", "Tips"]: + for iii, item in enumerate(["Role", "Equipment", "Process", "Tips"], start=1): l = QLabel(item) l.setMaximumWidth(200) l.setMinimumWidth(200) - self.layout.addWidget(l) + self.layout.addWidget(l, 0, iii, alignment=Qt.AlignmentFlag.AlignRight) self.setLayout(self.layout) def check_all(self): """ Toggles all checkboxes in the form - """ + """ for object in self.parent().findChildren(QCheckBox): object.setChecked(self.check.isChecked()) + class RoleComboBox(QWidget): - def __init__(self, parent, role:PydEquipmentRole, used:list) -> None: + def __init__(self, parent, role: PydEquipmentRole, used: list) -> None: super().__init__(parent) # self.layout = QHBoxLayout() self.layout = QGridLayout() @@ -105,27 +106,23 @@ class RoleComboBox(QWidget): self.process.setMinimumWidth(200) self.process.setEditable(False) self.process.currentTextChanged.connect(self.update_tips) - # self.tips = QComboBox() - # self.tips.setMaximumWidth(200) - # self.tips.setMinimumWidth(200) - # self.tips.setEditable(True) - self.layout.addWidget(self.check,0,0) + self.layout.addWidget(self.check, 0, 0) label = QLabel(f"{role.name}:") label.setMinimumWidth(200) label.setMaximumWidth(200) label.setAlignment(Qt.AlignmentFlag.AlignLeft) - self.layout.addWidget(label,0,1) - self.layout.addWidget(self.box,0,2) - self.layout.addWidget(self.process,0,3) + self.layout.addWidget(label, 0, 1) + self.layout.addWidget(self.box, 0, 2) + self.layout.addWidget(self.process, 0, 3) self.setLayout(self.layout) - + def update_processes(self): """ Changes processes when equipment is changed - """ + """ equip = self.box.currentText() # logger.debug(f"Updating equipment: {equip}") - equip2 = [item for item in self.role.equipment if item.name==equip][0] + equip2 = [item for item in self.role.equipment if item.name == equip][0] # logger.debug(f"Using: {equip2}") self.process.clear() self.process.addItems([item for item in equip2.processes if item in self.role.processes]) @@ -143,24 +140,25 @@ class RoleComboBox(QWidget): widget.addItems(tip_choices) # logger.debug(f"Tiprole: {tip_role.__dict__}") widget.setObjectName(f"tips_{tip_role.name}") - widget.setMinimumWidth(100) - widget.setMaximumWidth(100) + widget.setMinimumWidth(200) + widget.setMaximumWidth(200) self.layout.addWidget(widget, iii, 4) else: widget = QLabel("") - widget.setMinimumWidth(100) - widget.setMaximumWidth(100) - self.layout.addWidget(widget,0,4) + widget.setMinimumWidth(200) + widget.setMaximumWidth(200) + self.layout.addWidget(widget, 0, 4) - def parse_form(self) -> PydEquipment|None: + def parse_form(self) -> PydEquipment | None: """ Creates PydEquipment for values in form Returns: PydEquipment|None: PydEquipment matching form - """ + """ eq = Equipment.query(name=self.box.currentText()) - tips = [PydTips(name=item.currentText(), role=item.objectName().lstrip("tips").lstrip("_")) for item in self.findChildren(QComboBox) if item.objectName().startswith("tips")] + tips = [PydTips(name=item.currentText(), role=item.objectName().lstrip("tips").lstrip("_")) for item in + self.findChildren(QComboBox) if item.objectName().startswith("tips")] logger.debug(tips) try: return PydEquipment( @@ -173,4 +171,3 @@ class RoleComboBox(QWidget): ) except Exception as e: logger.error(f"Could create PydEquipment due to: {e}") - \ No newline at end of file