diff --git a/src/submissions/backend/db/models/__init__.py b/src/submissions/backend/db/models/__init__.py index a848504..e980c45 100644 --- a/src/submissions/backend/db/models/__init__.py +++ b/src/submissions/backend/db/models/__init__.py @@ -603,7 +603,7 @@ class BaseClass(Base): pyd = getattr(pydant, pyd_model_name) except AttributeError: raise AttributeError(f"Could not get pydantic class {pyd_model_name}") - return pyd(**self.details_dict()) + return pyd(**self.details_dict(**kwargs)) def show_details(self, obj): logger.debug("Show Details") diff --git a/src/submissions/backend/db/models/kits.py b/src/submissions/backend/db/models/kits.py index edeba6d..5f7cb54 100644 --- a/src/submissions/backend/db/models/kits.py +++ b/src/submissions/backend/db/models/kits.py @@ -5,13 +5,11 @@ from __future__ import annotations import zipfile, logging, re from operator import itemgetter from pprint import pformat - import numpy as np from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB from sqlalchemy.orm import relationship, validates, Query from sqlalchemy.ext.associationproxy import association_proxy from datetime import date, datetime, timedelta - from tools import check_authorization, setup_lookup, Report, Result, check_regex_match, timezone, \ jinja_template_loading, flatten_list from typing import List, Literal, Generator, Any, Tuple, TYPE_CHECKING @@ -562,7 +560,8 @@ class ReagentRole(BaseClass): def get_reagents(self, kittype: str | KitType | None = None): if not kittype: - return [f"{reagent.name} - {reagent.lot}" for reagent in self.reagent] + # return [f"{reagent.name} - {reagent.lot} - {reagent.expiry}" for reagent in self.reagent] + return [reagent.to_pydantic() for reagent in self.reagent] if isinstance(kittype, str): kittype = KitType.query(name=kittype) assoc = next((item for item in self.reagentrolekittypeassociation if item.kittype == kittype), None) @@ -571,7 +570,8 @@ class ReagentRole(BaseClass): last_used = Reagent.query(name=assoc.last_used) if last_used: reagents.insert(0, reagents.pop(reagents.index(last_used))) - return [f"{reagent.name} - {reagent.lot}" for reagent in reagents] + # return [f"{reagent.name} - {reagent.lot} - {reagent.expiry}" for reagent in reagents] + return [reagent.to_pydantic(reagentrole=self.name) for reagent in reagents] class Reagent(BaseClass, LogMixin): @@ -680,24 +680,24 @@ class Reagent(BaseClass, LogMixin): report.add_result(Result(msg=f"Updating last used {rt} was not performed.", status="Information")) return report - @classmethod - def query_or_create(cls, **kwargs) -> Reagent: - from backend.validators.pydant import PydReagent - new = False - disallowed = ['expiry'] - sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed} - instance = cls.query(**sanitized_kwargs) - if not instance or isinstance(instance, list): - if "reagentrole" not in kwargs: - try: - kwargs['reagentrole'] = kwargs['name'] - except KeyError: - pass - instance = PydReagent(**kwargs) - new = True - instance = instance.to_sql() - logger.info(f"Instance from query or create: {instance}") - return instance, new + # @classmethod + # def query_or_create(cls, **kwargs) -> Reagent: + # from backend.validators.pydant import PydReagent + # new = False + # disallowed = ['expiry'] + # sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed} + # instance = cls.query(**sanitized_kwargs) + # if not instance or isinstance(instance, list): + # if "reagentrole" not in kwargs: + # try: + # kwargs['reagentrole'] = kwargs['name'] + # except KeyError: + # pass + # instance = PydReagent(**kwargs) + # new = True + # instance = instance.to_sql() + # logger.info(f"Instance from query or create: {instance}") + # return instance, new @classmethod @setup_lookup @@ -800,6 +800,12 @@ class Reagent(BaseClass, LogMixin): expiry="Use exact date on reagent.\nEOL will be calculated from kittype automatically" ) + def details_dict(self, reagentrole:str|None=None, **kwargs): + output = super().details_dict() + if reagentrole: + output['reagentrole'] = reagentrole + return output + class Discount(BaseClass): """ @@ -1278,7 +1284,7 @@ class ProcedureType(BaseClass): if self.plate_rows == 0 or self.plate_columns == 0: return "
" sample_dicts = self.pad_sample_dicts(sample_dicts=sample_dicts) - logger.debug(f"Sample dicts: {pformat(sample_dicts)}") + # logger.debug(f"Sample dicts: {pformat(sample_dicts)}") vw = round((-0.07 * len(sample_dicts)) + 12.2, 1) # 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 @@ -1291,14 +1297,14 @@ class ProcedureType(BaseClass): def pad_sample_dicts(self, sample_dicts: List["PydSample"]): from backend.validators.pydant import PydSample output = [] - logger.debug(f"Rows: {self.plate_rows}") - logger.debug(f"Columns: {self.plate_columns}") + # logger.debug(f"Rows: {self.plate_rows}") + # logger.debug(f"Columns: {self.plate_columns}") for row, column in self.ranked_plate.values(): sample = next((sample for sample in sample_dicts if sample.row == row and sample.column == column), PydSample(**dict(sample_id="", row=row, column=column, enabled=False))) sample.background_color = "#6ffe1d" if sample.enabled else "#ffffff" output.append(sample) - logger.debug(f"Appending {sample} at row {row}, column {column}") + # logger.debug(f"Appending {sample} at row {row}, column {column}") return output @@ -1453,6 +1459,8 @@ class Procedure(BaseClass): dlg = ProcedureCreation(parent=obj, procedure=self.to_pydantic(), edit=True) if dlg.exec(): logger.debug("Edited") + sql, _ = dlg.return_sql() + sql.save() def add_comment(self, obj): logger.debug("Add Comment!") diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py index 6ef9037..9cdaaa1 100644 --- a/src/submissions/backend/validators/pydant.py +++ b/src/submissions/backend/validators/pydant.py @@ -183,7 +183,7 @@ class PydReagent(PydBaseClass): case _: return convert_nans_to_nones(str(value)) if value is None: - value = date.today() + value = datetime.combine(date.today(), datetime.max.time()) return value @field_validator("expiry") @@ -201,6 +201,11 @@ class PydReagent(PydBaseClass): else: return values.data['reagentrole'].strip() + # @field_validator("reagentrole", mode="before") + # @classmethod + # def rescue_reagentrole(cls, value): + # if + def improved_dict(self) -> dict: """ Constructs a dictionary consisting of model.fields and model.extras @@ -226,23 +231,26 @@ class PydReagent(PydBaseClass): report = Report() if self.model_extra is not None: self.__dict__.update(self.model_extra) - reagent = Reagent.query(lot=self.lot, name=self.name) + reagent, new = Reagent.query_or_create(lot=self.lot, name=self.name) # logger.debug(f"Reagent: {reagent}") - if reagent is None: - reagent = Reagent() - for key, value in self.__dict__.items(): - if isinstance(value, dict): - value = value['value'] - # NOTE: reagent method sets fields based on keys in dictionary - reagent.set_attribute(key, value) - if procedure is not None and reagent not in procedure.reagents: - assoc = ProcedureReagentAssociation(reagent=reagent, procedure=procedure) - assoc.comments = self.comment - else: - assoc = None - else: - if submission is not None and reagent not in submission.reagents: - submission.update_reagentassoc(reagent=reagent, role=self.role) + # if reagent is None: + # reagent = Reagent() + # for key, value in self.__dict__.items(): + # if isinstance(value, dict): + # if key == "misc_info": + # value = value + # else: + # value = value['value'] + # # NOTE: reagent method sets fields based on keys in dictionary + # reagent.set_attribute(key, value) + # if procedure is not None and reagent not in procedure.reagents: + # assoc = ProcedureReagentAssociation(reagent=reagent, procedure=procedure) + # assoc.comments = self.comment + # else: + # assoc = None + # else: + # if submission is not None and reagent not in submission.reagents: + # submission.update_reagentassoc(reagent=reagent, role=self.role) return reagent, report @@ -1486,8 +1494,10 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True): if isinstance(kittype, str): kittype_obj = KitType.query(name=kittype) try: - self.reagentrole = {item.name: item.get_reagents(kittype=kittype_obj) + ["New"] for item in - kittype_obj.get_reagents(proceduretype=self.proceduretype)} + self.reagentrole = { + item.name: item.get_reagents(kittype=kittype_obj) + [PydReagent(name="--New--", lot="", reagentrole="")] + for item in + kittype_obj.get_reagents(proceduretype=self.proceduretype)} except AttributeError: self.reagentrole = {} @@ -1511,19 +1521,19 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True): logger.debug(f"Incoming sample_list:\n{pformat(sample_list)}") for sample_dict in sample_list: if sample_dict['sample_id'].startswith("blank_"): - continue + sample_dict['sample_id'] = "" row, column = self.proceduretype.ranked_plate[sample_dict['index']] logger.debug(f"Row: {row}, Column: {column}") try: sample = next( - (item for item in self.samples if item.sample_id.upper() == sample_dict['sample_id'].upper())) + (item for item in self.sample if item.sample_id.upper() == sample_dict['sample_id'].upper())) except StopIteration: # NOTE: Code to check for added controls. logger.debug( f"Sample not found by name: {sample_dict['sample_id']}, checking row {row} column {column}") try: sample = next( - (item for item in self.samples if item.row == row and item.column == column)) + (item for item in self.sample if item.row == row and item.column == column)) except StopIteration: logger.error(f"Couldn't find sample: {pformat(sample_dict)}") continue @@ -1532,7 +1542,23 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True): sample.well_id = sample_dict['sample_id'] sample.row = row sample.column = column - logger.debug(f"Updated samples:\n{pformat(self.samples)}") + # logger.debug(f"Updated samples:\n{pformat(self.sample)}") + + def update_reagents(self, reagentrole: str, name: str, lot: str, expiry: str): + removable = next((item for item in self.reagent if item.reagentrole == reagentrole), None) + if removable: + idx = self.reagent.index(removable) + self.reagent.remove(removable) + else: + idx = 0 + insertable = PydReagent(reagentrole=reagentrole, name=name, lot=lot, expiry=expiry) + self.reagent.insert(idx, insertable) + logger.debug(self.reagent) + + @classmethod + def update_new_reagents(cls, reagent: PydReagent): + reg = reagent.to_sql() + reg.save() def to_sql(self): from backend.db.models import RunSampleAssociation, ProcedureSampleAssociation @@ -1540,12 +1566,24 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True): # for result in self.results: # result, _ = result.to_sql() sql = super().to_sql() - logger.debug(f"Initial PYD: {pformat(self.__dict__)}") + # logger.debug(f"Initial PYD: {pformat(self.__dict__)}") # sql.results = [result.to_sql() for result in self.results] if self.run: sql.run = self.run if self.proceduretype: sql.proceduretype = self.proceduretype + # Note: convert any new reagents to sql and save + for reagentrole, reagents in self.reagentrole.items(): + for reagent in reagents: + if not reagent.lot or reagent.name == "--New--": + continue + self.update_new_reagents(reagent) + # NOTE: reset reagent associations. + sql.procedurereagentassociation = [] + for reagent in self.reagent: + reagent = reagent.to_sql() + if reagent not in sql.reagent: + reagent_assoc = ProcedureReagentAssociation(reagent=reagent, procedure=sql) try: start_index = max([item.id for item in ProcedureSampleAssociation.query()]) + 1 except ValueError: @@ -1571,13 +1609,15 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True): kittype = KitType.query(name=self.kittype['value'], limit=1) if kittype: sql.kittype = kittype + logger.debug(self.reagent) for equipment in self.equipment: equip = Equipment.query(name=equipment.name) if equip not in sql.equipment: - equip_assoc = ProcedureEquipmentAssociation(equipment=equip, procedure=sql, equipmentrole=equip.equipmentrole[0]) + equip_assoc = ProcedureEquipmentAssociation(equipment=equip, procedure=sql, + equipmentrole=equip.equipmentrole[0]) process = equipment.process.to_sql() equip_assoc.process = process - logger.debug(f"Output sql: {[pformat(item.__dict__) for item in sql.procedureequipmentassociation]}") + # logger.debug(f"Output sql: {[pformat(item.__dict__) for item in sql.procedureequipmentassociation]}") return sql, None diff --git a/src/submissions/frontend/widgets/procedure_creation.py b/src/submissions/frontend/widgets/procedure_creation.py index bc33805..37c06cf 100644 --- a/src/submissions/frontend/widgets/procedure_creation.py +++ b/src/submissions/frontend/widgets/procedure_creation.py @@ -3,6 +3,7 @@ """ from __future__ import annotations +import datetime import os import sys, logging from pathlib import Path @@ -122,6 +123,24 @@ class ProcedureCreation(QDialog): def log(self, logtext: str): logger.debug(logtext) + @pyqtSlot(str, str, str, str) + def add_new_reagent(self, reagentrole: str, name: str, lot: str, expiry: str): + from backend.validators.pydant import PydReagent + expiry = datetime.datetime.strptime(expiry, "%Y-%m-%d") + pyd = PydReagent(reagentrole=reagentrole, name=name, lot=lot, expiry=expiry) + logger.debug(pyd) + self.procedure.reagentrole[reagentrole].insert(0, pyd) + logger.debug(pformat(self.procedure.__dict__)) + self.set_html() + + @pyqtSlot(str, str) + def update_reagent(self, reagentrole:str, name_lot_expiry:str): + try: + name, lot, expiry = name_lot_expiry.split(" - ") + except ValueError: + return + self.procedure.update_reagents(reagentrole=reagentrole, name=name, lot=lot, expiry=expiry) + def return_sql(self): return self.procedure.to_sql() diff --git a/src/submissions/frontend/widgets/submission_table.py b/src/submissions/frontend/widgets/submission_table.py index 3a3501e..f80b90c 100644 --- a/src/submissions/frontend/widgets/submission_table.py +++ b/src/submissions/frontend/widgets/submission_table.py @@ -296,7 +296,7 @@ class SubmissionsTree(QTreeView): self.setAlternatingRowColors(True) self.setIndentation(20) self.setItemsExpandable(True) - self.expanded.connect(self.expand_item) + # self.expanded.connect(self.expand_item) for ii in range(2): self.resizeColumnToContents(ii) diff --git a/src/submissions/templates/js/grid_drag.js b/src/submissions/templates/js/grid_drag.js index 49a4538..40c76e2 100644 --- a/src/submissions/templates/js/grid_drag.js +++ b/src/submissions/templates/js/grid_drag.js @@ -32,24 +32,24 @@ gridContainer.addEventListener("drop", (e) => { targetItem !== draggedItem //&& //targetItem.classList.contains("well") ) { - backend.log(targetItem.id); +// backend.log(targetItem.id); const draggedIndex = [...gridContainer.children].indexOf(draggedItem); const targetIndex = [...gridContainer.children].indexOf(targetItem); if (draggedIndex < targetIndex) { - backend.log(draggedIndex.toString() + " " + targetIndex.toString() + " Lesser"); +// backend.log(draggedIndex.toString() + " " + targetIndex.toString() + " Lesser"); gridContainer.insertBefore(draggedItem, targetItem.nextSibling); } else { - backend.log(draggedIndex.toString() + " " + targetIndex.toString() + " Greater"); +// backend.log(draggedIndex.toString() + " " + targetIndex.toString() + " Greater"); gridContainer.insertBefore(draggedItem, targetItem); } -// output = []; -// fullGrid = [...gridContainer.children]; -// fullGrid.forEach(function(item, index) { -// output.push({sample_id: item.id, index: index + 1}) -// }); -// backend.rearrange_plate(output); - rearrange_plate(); + output = []; + fullGrid = [...gridContainer.children]; + fullGrid.forEach(function(item, index) { + output.push({sample_id: item.id, index: index + 1}) + }); + backend.rearrange_plate(output); +// rearrange_plate(); } }); \ No newline at end of file diff --git a/src/submissions/templates/js/procedure_form.js b/src/submissions/templates/js/procedure_form.js index feff608..15aa5e9 100644 --- a/src/submissions/templates/js/procedure_form.js +++ b/src/submissions/templates/js/procedure_form.js @@ -21,45 +21,39 @@ for(let i = 0; i < formtexts.length; i++) { }) }; +var changed_it = new Event('change'); + var reagentRoles = document.getElementsByClassName("reagentrole"); for(let i = 0; i < reagentRoles.length; i++) { reagentRoles[i].addEventListener("change", function() { - if (reagentRoles[i].value === "New") { - + if (reagentRoles[i].value.includes("--New--")) { var br = document.createElement("br"); var new_reg = document.getElementById("new_" + reagentRoles[i].id); - console.log(new_reg.id); var new_form = document.createElement("form"); - + new_form.setAttribute("class", "new_reagent_form") + new_form.setAttribute("id", reagentRoles[i].id + "_addition") var rr_name = document.createElement("input"); rr_name.setAttribute("type", "text"); rr_name.setAttribute("id", "new_" + reagentRoles[i].id + "_name"); - var rr_name_label = document.createElement("label"); rr_name_label.setAttribute("for", "new_" + reagentRoles[i].id + "_name"); rr_name_label.innerHTML = "Name:"; - var rr_lot = document.createElement("input"); rr_lot.setAttribute("type", "text"); rr_lot.setAttribute("id", "new_" + reagentRoles[i].id + "_lot"); - var rr_lot_label = document.createElement("label"); rr_lot_label.setAttribute("for", "new_" + reagentRoles[i].id + "_lot"); rr_lot_label.innerHTML = "Lot:"; - var rr_expiry = document.createElement("input"); rr_expiry.setAttribute("type", "date"); rr_expiry.setAttribute("id", "new_" + reagentRoles[i].id + "_expiry"); - var rr_expiry_label = document.createElement("label"); rr_expiry_label.setAttribute("for", "new_" + reagentRoles[i].id + "_expiry"); rr_expiry_label.innerHTML = "Expiry:"; - var submit_btn = document.createElement("input"); submit_btn.setAttribute("type", "submit"); submit_btn.setAttribute("value", "Submit"); - new_form.appendChild(br.cloneNode()); new_form.appendChild(rr_name_label); new_form.appendChild(rr_name); @@ -72,19 +66,27 @@ for(let i = 0; i < reagentRoles.length; i++) { new_form.appendChild(br.cloneNode()); new_form.appendChild(submit_btn); new_form.appendChild(br.cloneNode()); - new_form.onsubmit = function(event) { event.preventDefault(); - alert(reagentRoles[i].id); name = document.getElementById("new_" + reagentRoles[i].id + "_name").value; lot = document.getElementById("new_" + reagentRoles[i].id + "_lot").value; expiry = document.getElementById("new_" + reagentRoles[i].id + "_expiry").value; - alert("Submitting: " + name + ", " + lot); - backend.log(name + " " + lot + " " + expiry); + backend.add_new_reagent(reagentRoles[i].id, name, lot, expiry); + new_form.remove(); + reagentRoles[i].dispatchEvent(changed_it); } - new_reg.appendChild(new_form); + } else { + newregform = document.getElementById(reagentRoles[i].id + "_addition"); + newregform.remove(); + backend.update_reagent(reagentRoles[i].id, reagentRoles[i].value) } - }) + }); }; +window.onload = function() { + for(let i = 0; i < reagentRoles.length; i++) { + backend.update_reagent(reagentRoles[i].id, reagentRoles[i].value); + } +} + diff --git a/src/submissions/templates/procedure_creation.html b/src/submissions/templates/procedure_creation.html index ef4a65c..35c8b05 100644 --- a/src/submissions/templates/procedure_creation.html +++ b/src/submissions/templates/procedure_creation.html @@ -31,12 +31,14 @@ {% endfor %}
{% if procedure['reagentrole'] %} -


+
+

Reagents

+
{% for key, value in procedure['reagentrole'].items() %}