diff --git a/src/submissions/backend/db/models/__init__.py b/src/submissions/backend/db/models/__init__.py
index 9b7dbea..81a870c 100644
--- a/src/submissions/backend/db/models/__init__.py
+++ b/src/submissions/backend/db/models/__init__.py
@@ -555,6 +555,8 @@ class BaseClass(Base):
return output_date
+
+
class LogMixin(Base):
tracking_exclusion: ClassVar = ['artic_technician', 'clientsubmissionsampleassociation',
'submission_reagent_associations', 'submission_equipment_associations',
diff --git a/src/submissions/backend/db/models/kits.py b/src/submissions/backend/db/models/kits.py
index 2f3897c..c09310a 100644
--- a/src/submissions/backend/db/models/kits.py
+++ b/src/submissions/backend/db/models/kits.py
@@ -14,12 +14,15 @@ from sqlalchemy.ext.hybrid import hybrid_property
from datetime import date, datetime, timedelta
from tools import check_authorization, setup_lookup, Report, Result, check_regex_match, yaml_regex_creator, timezone, \
jinja_template_loading
-from typing import List, Literal, Generator, Any, Tuple
+from typing import List, Literal, Generator, Any, Tuple, TYPE_CHECKING
from pandas import ExcelFile
from pathlib import Path
from . import Base, BaseClass, ClientLab, LogMixin
from io import BytesIO
+if TYPE_CHECKING:
+ from backend.db.models.submissions import Run
+
logger = logging.getLogger(f'procedure.{__name__}')
reagentrole_reagent = Table(
@@ -159,7 +162,7 @@ class KitType(BaseClass):
def get_reagents(self,
required_only: bool = False,
- proceduretype: str | SubmissionType | None = None
+ proceduretype: str | ProcedureType | None = None
) -> Generator[ReagentRole, None, None]:
"""
Return ReagentTypes linked to kittype through KitTypeReagentTypeAssociation.
@@ -181,9 +184,9 @@ class KitType(BaseClass):
case _:
relevant_associations = [item for item in self.kittypereagentroleassociation]
if required_only:
- return (item.reagent_role for item in relevant_associations if item.required == 1)
+ return (item.reagentrole for item in relevant_associations if item.required == 1)
else:
- return (item.reagent_role for item in relevant_associations)
+ return (item.reagentrole for item in relevant_associations)
def construct_xl_map_for_use(self, proceduretype: str | SubmissionType) -> Tuple[dict | None, KitType]:
"""
@@ -536,6 +539,9 @@ class ReagentRole(BaseClass):
logger.debug(f"Constructing OmniReagentRole with name {self.name}")
return OmniReagentRole(instance_object=self, name=self.name, eol_ext=self.eol_ext)
+ @property
+ def reagents(self):
+ return [f"{reagent.name} - {reagent.lot}" for reagent in self.reagent]
class Reagent(BaseClass, LogMixin):
"""
@@ -841,11 +847,11 @@ class SubmissionType(BaseClass):
info_map = Column(JSON) #: Where parsable information is found in the excel workbook corresponding to this type.
defaults = Column(JSON) #: Basic information about this procedure type
clientsubmission = relationship("ClientSubmission",
- back_populates="submissiontype") #: Concrete control of this type.
+ back_populates="submissiontype") #: Concrete control of this type.
template_file = Column(BLOB) #: Blank form for this type stored as binary.
sample_map = Column(JSON) #: Where sample information is found in the excel sheet corresponding to this type.
proceduretype = relationship("ProcedureType", back_populates="submissiontype",
- secondary=submissiontype_proceduretype) #: run this kittype was used for
+ secondary=submissiontype_proceduretype) #: run this kittype was used for
def __repr__(self) -> str:
"""
@@ -1034,7 +1040,8 @@ class ProcedureType(BaseClass):
id = Column(INTEGER, primary_key=True)
name = Column(String(64))
reagent_map = Column(JSON)
- plate_size = Column(INTEGER, default=0)
+ plate_columns = Column(INTEGER, default=0)
+ plate_rows = Column(INTEGER, default=0)
procedure = relationship("Procedure",
back_populates="proceduretype") #: Concrete control of this type.
@@ -1140,6 +1147,54 @@ class ProcedureType(BaseClass):
raise TypeError(f"Type {type(equipmentrole)} is not allowed")
return list(set([item for items in relevant for item in items if item is not None]))
+ @property
+ def as_dict(self):
+ return dict(
+ name=self.name,
+ kittype=[item.name for item in self.kittype]
+ )
+
+ def construct_dummy_procedure(self):
+ from backend.validators.pydant import PydProcedure
+ output = dict(
+ proceduretype=self,
+ #name=dict(value=self.name, missing=True),
+ #possible_kits=[kittype.name for kittype in self.kittype],
+ repeat=False,
+ plate_map=self.construct_plate_map()
+ )
+ return PydProcedure(**output)
+
+ def construct_plate_map(self) -> str:
+ """
+ Constructs an html based plate map for procedure details.
+
+ Args:
+ sample_list (list): List of procedure sample
+ plate_rows (int, optional): Number of rows in the plate. Defaults to 8.
+ plate_columns (int, optional): Number of columns in the plate. Defaults to 12.
+
+ Returns:
+ str: html output string.
+ """
+ if self.plate_rows == 0 or self.plate_columns == 0:
+ return "
"
+ plate_rows = range(1, self.plate_rows + 1)
+ plate_columns = range(1, self.plate_columns + 1)
+ total_wells = self.plate_columns * self.plate_rows
+ vw = round((-0.07 * total_wells) + 12.2, 1)
+
+
+ wells = [dict(name="", row=row, column=column, background_color="#ffffff")
+ for row in plate_rows
+ for column in plate_columns]
+ # 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
+ env = jinja_template_loading()
+ template = env.get_template("plate_map.html")
+ html = template.render(plate_rows=self.plate_rows, plate_columns=self.plate_columns, samples=wells, vw=vw)
+ return html + "
"
+
class Procedure(BaseClass):
id = Column(INTEGER, primary_key=True)
@@ -1164,7 +1219,8 @@ class Procedure(BaseClass):
) #: Relation to ProcedureReagentAssociation
reagents = association_proxy("procedurereagentassociation",
- "reagent", creator=lambda reg: ProcedureReagentAssociation(reagent=reg)) #: Association proxy to RunReagentAssociation.reagent
+ "reagent", creator=lambda reg: ProcedureReagentAssociation(
+ reagent=reg)) #: Association proxy to RunReagentAssociation.reagent
procedureequipmentassociation = relationship(
"ProcedureEquipmentAssociation",
@@ -1193,7 +1249,8 @@ class Procedure(BaseClass):
@classmethod
@setup_lookup
- def query(cls, id: int|None = None, name: str | None = None, limit: int = 0, **kwargs) -> Procedure | List[Procedure]:
+ def query(cls, id: int | None = None, name: str | None = None, limit: int = 0, **kwargs) -> Procedure | List[
+ Procedure]:
query: Query = cls.__database_session__.query(cls)
match id:
case int():
@@ -1674,9 +1731,9 @@ class Equipment(BaseClass, LogMixin):
nickname = Column(String(64)) #: equipment nickname
asset_number = Column(String(16)) #: Given asset number (corpo nickname if you will)
equipmentrole = relationship("EquipmentRole", back_populates="equipment",
- secondary=equipmentrole_equipment) #: relation to EquipmentRoles
+ secondary=equipmentrole_equipment) #: relation to EquipmentRoles
process = relationship("Process", back_populates="equipment",
- secondary=equipment_process) #: relation to Processes
+ secondary=equipment_process) #: relation to Processes
tips = relationship("Tips", back_populates="equipment",
secondary=equipment_tips) #: relation to Processes
equipmentprocedureassociation = relationship(
@@ -1686,7 +1743,7 @@ class Equipment(BaseClass, LogMixin):
) #: Association with BasicRun
procedure = association_proxy("equipmentprocedureassociation",
- "procedure") #: proxy to equipmentprocedureassociation.procedure
+ "procedure") #: proxy to equipmentprocedureassociation.procedure
def to_dict(self, processes: bool = False) -> dict:
"""
@@ -1888,7 +1945,7 @@ class EquipmentRole(BaseClass):
equipment = relationship("Equipment", back_populates="equipmentrole",
secondary=equipmentrole_equipment) #: Concrete control (Equipment) of reagentrole
process = relationship("Process", back_populates='equipmentrole',
- secondary=equipmentrole_process) #: Associated Processes
+ secondary=equipmentrole_process) #: Associated Processes
equipmentroleproceduretypeassociation = relationship(
"ProcedureTypeEquipmentRoleAssociation",
@@ -1897,7 +1954,7 @@ class EquipmentRole(BaseClass):
) #: relation to SubmissionTypes
proceduretype = association_proxy("equipmentroleproceduretypeassociation",
- "proceduretype") #: proxy to equipmentroleproceduretypeassociation.proceduretype
+ "proceduretype") #: proxy to equipmentroleproceduretypeassociation.proceduretype
def to_dict(self) -> dict:
"""
@@ -2018,7 +2075,7 @@ class ProcedureEquipmentAssociation(BaseClass):
comments = Column(String(1024)) #: comments about equipment
procedure = relationship(Procedure,
- back_populates="procedureequipmentassociation") #: associated procedure
+ back_populates="procedureequipmentassociation") #: associated procedure
equipment = relationship(Equipment, back_populates="equipmentprocedureassociation") #: associated equipment
@@ -2431,7 +2488,8 @@ class Tips(BaseClass, LogMixin):
)
if full_data:
subs = [
- dict(plate=item.procedure.procedure.rsl_plate_num, role=item.role_name, sub_date=item.procedure.procedure.clientsubmission.submitted_date)
+ dict(plate=item.procedure.procedure.rsl_plate_num, role=item.role_name,
+ sub_date=item.procedure.procedure.clientsubmission.submitted_date)
for item in self.tipsprocedureassociation]
output['procedure'] = sorted(subs, key=itemgetter("sub_date"), reverse=True)
output['excluded'] = ['missing', 'procedure', 'excluded', 'editable']
@@ -2466,7 +2524,8 @@ class ProcedureTypeTipRoleAssociation(BaseClass):
proceduretype_id = Column(INTEGER, ForeignKey("_proceduretype.id"),
primary_key=True) #: id of associated procedure
uses = Column(JSON) #: locations of equipment on the procedure type excel sheet.
- static = Column(INTEGER, default=1) #: if 1 this piece of equipment will always be used, otherwise it will need to be selected from list?
+ static = Column(INTEGER,
+ default=1) #: if 1 this piece of equipment will always be used, otherwise it will need to be selected from list?
proceduretype = relationship(ProcedureType,
back_populates="proceduretypetiproleassociation") #: associated procedure
tiprole = relationship(TipRole,
diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py
index 9e0fcdd..04280a9 100644
--- a/src/submissions/backend/db/models/submissions.py
+++ b/src/submissions/backend/db/models/submissions.py
@@ -15,8 +15,6 @@ from operator import itemgetter
from pprint import pformat
from pandas import DataFrame
from sqlalchemy.ext.hybrid import hybrid_property
-
-
from . import Base, BaseClass, Reagent, SubmissionType, KitType, ClientLab, Contact, LogMixin
from sqlalchemy import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case, func, Table
from sqlalchemy.orm import relationship, validates, Query
@@ -1122,8 +1120,13 @@ class Run(BaseClass, LogMixin):
return output
def add_procedure(self, obj, proceduretype_name: str):
+ from frontend.widgets.procedure_creation import ProcedureCreation
procedure_type = next((proceduretype for proceduretype in self.allowed_procedures if proceduretype.name == proceduretype_name))
logger.debug(f"Got ProcedureType: {procedure_type}")
+ dlg = ProcedureCreation(parent=obj, run=self, proceduretype=procedure_type)
+ if dlg.exec():
+ pass
+
def delete(self, obj=None):
"""
diff --git a/src/submissions/backend/validators/__init__.py b/src/submissions/backend/validators/__init__.py
index 45673b0..a07ef8e 100644
--- a/src/submissions/backend/validators/__init__.py
+++ b/src/submissions/backend/validators/__init__.py
@@ -208,4 +208,4 @@ class RSLNamer(object):
from .pydant import PydSubmission, PydKitType, PydContact, PydOrganization, PydSample, PydReagent, PydReagentRole, \
- PydEquipment, PydEquipmentRole, PydTips, PydProcess, PydElastic, PydClientSubmission
+ PydEquipment, PydEquipmentRole, PydTips, PydProcess, PydElastic, PydClientSubmission, PydProcedure
diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py
index 2b890fb..d67f9cd 100644
--- a/src/submissions/backend/validators/pydant.py
+++ b/src/submissions/backend/validators/pydant.py
@@ -24,7 +24,6 @@ logger = logging.getLogger(f"procedure.{__name__}")
class PydBaseClass(BaseModel, extra='allow', validate_assignment=True):
-
_sql_object: ClassVar = None
@model_validator(mode="before")
@@ -240,7 +239,6 @@ class PydReagent(BaseModel):
class PydSample(PydBaseClass):
-
sample_id: str
sampletype: str | None = Field(default=None)
submission_rank: int | List[int] | None = Field(default=0, validate_default=True)
@@ -1336,8 +1334,65 @@ class PydElastic(BaseModel, extra="allow", arbitrary_types_allowed=True):
# NOTE: Generified objects below:
-class PydClientSubmission(PydBaseClass):
+class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
+ proceduretype: ProcedureType | None = Field(default=None)
+ name: dict = Field(default=dict(value="NA", missing=True), validate_default=True)
+ technician: dict = Field(default=dict(value="NA", missing=True))
+ repeat: bool = Field(default=False)
+ kittype: dict = Field(default=dict(value="NA", missing=True))
+ possible_kits: list | None = Field(default=[], validate_default=True)
+ plate_map: str | None = Field(default=None)
+ reagent: list | None = Field(default=[])
+ reagentrole: dict | None = Field(default={}, validate_default=True)
+ @field_validator("name")
+ @classmethod
+ def rescue_name(cls, value, values):
+ if value['value'] == cls.model_fields['name'].default['value']:
+ if values.data['proceduretype']:
+ value['value'] = values.data['proceduretype'].name
+ return value
+
+ @field_validator("possible_kits")
+ @classmethod
+ def rescue_possible_kits(cls, value, values):
+ if not value:
+ if values.data['proceduretype']:
+ value = [kittype.name for kittype in values.data['proceduretype'].kittype]
+ return value
+
+ @field_validator("name", "technician", "kittype")
+ @classmethod
+ def set_colour(cls, value):
+ if value["missing"]:
+ value["colour"] = "FE441D"
+ else:
+ value["colour"] = "6ffe1d"
+ return value
+
+ @field_validator("reagentrole")
+ @classmethod
+ def rescue_reagentrole(cls, value, values):
+ if not value:
+ if values.data['kittype']['value'] != cls.model_fields['kittype'].default['value']:
+ kittype = KitType.query(name=values.data['kittype']['value'])
+ value = {item.name: item.reagents for item in kittype.reagentrole}
+ return value
+
+ def update_kittype_reagentroles(self, kittype: str | KitType):
+ if kittype == self.__class__.model_fields['kittype'].default['value']:
+ return
+ if isinstance(kittype, str):
+ kittype_obj = KitType.query(name=kittype)
+ try:
+ self.reagentrole = {item.name: item.reagents for item in
+ kittype_obj.get_reagents(proceduretype=self.proceduretype)}
+ except AttributeError:
+ self.reagentrole = {}
+ self.possible_kits.insert(0, self.possible_kits.pop(self.possible_kits.index(kittype)))
+
+
+class PydClientSubmission(PydBaseClass):
# sql_object: ClassVar = ClientSubmission
filepath: Path
@@ -1412,5 +1467,3 @@ class PydClientSubmission(PydBaseClass):
"""
from frontend.widgets.submission_widget import ClientSubmissionFormWidget
return ClientSubmissionFormWidget(parent=parent, clientsubmission=self, samples=samples, disable=disable)
-
-
diff --git a/src/submissions/templates/css/styles.css b/src/submissions/templates/css/styles.css
index c890cd1..a14b6cf 100644
--- a/src/submissions/templates/css/styles.css
+++ b/src/submissions/templates/css/styles.css
@@ -40,4 +40,47 @@
cursor: pointer;
}
+div.procedure_creation_leftright {
+ width:100%;
+ overflow:auto;
+ border-style: solid;
+ border-width: 2px;
+ background-color: #FFFFFF;
+ display: flex;
+}
+div.procedure_creation_leftright div.left {
+ height: 100%;
+ width:15%;
+ float:left;
+ border-style: solid;
+ border-width: 1px;
+ background-color: #FFF4A3;
+ padding: 5px;
+ flex: 0.25;
+}
+
+div.procedure_creation_leftright div.right {
+ height: 100%;
+ width:84%;
+ float:left;
+ border-style: solid;
+ border-width: 1px;
+ background-color:#D9EEE1;
+ padding: 5px;
+ flex: 0.75;
+ object-fit: contain;
+}
+
+.dropdown {
+ width: 100%;
+}
+
+.form_text {
+ width: 97%;
+}
+
+div.gallery {
+ width: 100%;
+ object-fit: contain;
+}
diff --git a/src/submissions/templates/plate_map.html b/src/submissions/templates/plate_map.html
index dbc867c..586db7d 100644
--- a/src/submissions/templates/plate_map.html
+++ b/src/submissions/templates/plate_map.html
@@ -1,6 +1,6 @@
-