Basic Procedure addition working.
This commit is contained in:
@@ -555,6 +555,8 @@ class BaseClass(Base):
|
|||||||
return output_date
|
return output_date
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class LogMixin(Base):
|
class LogMixin(Base):
|
||||||
tracking_exclusion: ClassVar = ['artic_technician', 'clientsubmissionsampleassociation',
|
tracking_exclusion: ClassVar = ['artic_technician', 'clientsubmissionsampleassociation',
|
||||||
'submission_reagent_associations', 'submission_equipment_associations',
|
'submission_reagent_associations', 'submission_equipment_associations',
|
||||||
|
|||||||
@@ -14,12 +14,15 @@ from sqlalchemy.ext.hybrid import hybrid_property
|
|||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from tools import check_authorization, setup_lookup, Report, Result, check_regex_match, yaml_regex_creator, timezone, \
|
from tools import check_authorization, setup_lookup, Report, Result, check_regex_match, yaml_regex_creator, timezone, \
|
||||||
jinja_template_loading
|
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 pandas import ExcelFile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from . import Base, BaseClass, ClientLab, LogMixin
|
from . import Base, BaseClass, ClientLab, LogMixin
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from backend.db.models.submissions import Run
|
||||||
|
|
||||||
logger = logging.getLogger(f'procedure.{__name__}')
|
logger = logging.getLogger(f'procedure.{__name__}')
|
||||||
|
|
||||||
reagentrole_reagent = Table(
|
reagentrole_reagent = Table(
|
||||||
@@ -159,7 +162,7 @@ class KitType(BaseClass):
|
|||||||
|
|
||||||
def get_reagents(self,
|
def get_reagents(self,
|
||||||
required_only: bool = False,
|
required_only: bool = False,
|
||||||
proceduretype: str | SubmissionType | None = None
|
proceduretype: str | ProcedureType | None = None
|
||||||
) -> Generator[ReagentRole, None, None]:
|
) -> Generator[ReagentRole, None, None]:
|
||||||
"""
|
"""
|
||||||
Return ReagentTypes linked to kittype through KitTypeReagentTypeAssociation.
|
Return ReagentTypes linked to kittype through KitTypeReagentTypeAssociation.
|
||||||
@@ -181,9 +184,9 @@ class KitType(BaseClass):
|
|||||||
case _:
|
case _:
|
||||||
relevant_associations = [item for item in self.kittypereagentroleassociation]
|
relevant_associations = [item for item in self.kittypereagentroleassociation]
|
||||||
if required_only:
|
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:
|
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]:
|
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}")
|
logger.debug(f"Constructing OmniReagentRole with name {self.name}")
|
||||||
return OmniReagentRole(instance_object=self, name=self.name, eol_ext=self.eol_ext)
|
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):
|
class Reagent(BaseClass, LogMixin):
|
||||||
"""
|
"""
|
||||||
@@ -1034,7 +1040,8 @@ class ProcedureType(BaseClass):
|
|||||||
id = Column(INTEGER, primary_key=True)
|
id = Column(INTEGER, primary_key=True)
|
||||||
name = Column(String(64))
|
name = Column(String(64))
|
||||||
reagent_map = Column(JSON)
|
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",
|
procedure = relationship("Procedure",
|
||||||
back_populates="proceduretype") #: Concrete control of this type.
|
back_populates="proceduretype") #: Concrete control of this type.
|
||||||
@@ -1140,6 +1147,54 @@ class ProcedureType(BaseClass):
|
|||||||
raise TypeError(f"Type {type(equipmentrole)} is not allowed")
|
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]))
|
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 "<br/>"
|
||||||
|
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 + "<br/>"
|
||||||
|
|
||||||
|
|
||||||
class Procedure(BaseClass):
|
class Procedure(BaseClass):
|
||||||
id = Column(INTEGER, primary_key=True)
|
id = Column(INTEGER, primary_key=True)
|
||||||
@@ -1164,7 +1219,8 @@ class Procedure(BaseClass):
|
|||||||
) #: Relation to ProcedureReagentAssociation
|
) #: Relation to ProcedureReagentAssociation
|
||||||
|
|
||||||
reagents = association_proxy("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 = relationship(
|
||||||
"ProcedureEquipmentAssociation",
|
"ProcedureEquipmentAssociation",
|
||||||
@@ -1193,7 +1249,8 @@ class Procedure(BaseClass):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@setup_lookup
|
@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)
|
query: Query = cls.__database_session__.query(cls)
|
||||||
match id:
|
match id:
|
||||||
case int():
|
case int():
|
||||||
@@ -2431,7 +2488,8 @@ class Tips(BaseClass, LogMixin):
|
|||||||
)
|
)
|
||||||
if full_data:
|
if full_data:
|
||||||
subs = [
|
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]
|
for item in self.tipsprocedureassociation]
|
||||||
output['procedure'] = sorted(subs, key=itemgetter("sub_date"), reverse=True)
|
output['procedure'] = sorted(subs, key=itemgetter("sub_date"), reverse=True)
|
||||||
output['excluded'] = ['missing', 'procedure', 'excluded', 'editable']
|
output['excluded'] = ['missing', 'procedure', 'excluded', 'editable']
|
||||||
@@ -2466,7 +2524,8 @@ class ProcedureTypeTipRoleAssociation(BaseClass):
|
|||||||
proceduretype_id = Column(INTEGER, ForeignKey("_proceduretype.id"),
|
proceduretype_id = Column(INTEGER, ForeignKey("_proceduretype.id"),
|
||||||
primary_key=True) #: id of associated procedure
|
primary_key=True) #: id of associated procedure
|
||||||
uses = Column(JSON) #: locations of equipment on the procedure type excel sheet.
|
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,
|
proceduretype = relationship(ProcedureType,
|
||||||
back_populates="proceduretypetiproleassociation") #: associated procedure
|
back_populates="proceduretypetiproleassociation") #: associated procedure
|
||||||
tiprole = relationship(TipRole,
|
tiprole = relationship(TipRole,
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ from operator import itemgetter
|
|||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
from sqlalchemy.ext.hybrid import hybrid_property
|
from sqlalchemy.ext.hybrid import hybrid_property
|
||||||
|
|
||||||
|
|
||||||
from . import Base, BaseClass, Reagent, SubmissionType, KitType, ClientLab, Contact, LogMixin
|
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 import Column, String, TIMESTAMP, INTEGER, ForeignKey, JSON, FLOAT, case, func, Table
|
||||||
from sqlalchemy.orm import relationship, validates, Query
|
from sqlalchemy.orm import relationship, validates, Query
|
||||||
@@ -1122,8 +1120,13 @@ class Run(BaseClass, LogMixin):
|
|||||||
return output
|
return output
|
||||||
|
|
||||||
def add_procedure(self, obj, proceduretype_name: str):
|
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))
|
procedure_type = next((proceduretype for proceduretype in self.allowed_procedures if proceduretype.name == proceduretype_name))
|
||||||
logger.debug(f"Got ProcedureType: {procedure_type}")
|
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):
|
def delete(self, obj=None):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -208,4 +208,4 @@ class RSLNamer(object):
|
|||||||
|
|
||||||
|
|
||||||
from .pydant import PydSubmission, PydKitType, PydContact, PydOrganization, PydSample, PydReagent, PydReagentRole, \
|
from .pydant import PydSubmission, PydKitType, PydContact, PydOrganization, PydSample, PydReagent, PydReagentRole, \
|
||||||
PydEquipment, PydEquipmentRole, PydTips, PydProcess, PydElastic, PydClientSubmission
|
PydEquipment, PydEquipmentRole, PydTips, PydProcess, PydElastic, PydClientSubmission, PydProcedure
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ logger = logging.getLogger(f"procedure.{__name__}")
|
|||||||
|
|
||||||
|
|
||||||
class PydBaseClass(BaseModel, extra='allow', validate_assignment=True):
|
class PydBaseClass(BaseModel, extra='allow', validate_assignment=True):
|
||||||
|
|
||||||
_sql_object: ClassVar = None
|
_sql_object: ClassVar = None
|
||||||
|
|
||||||
@model_validator(mode="before")
|
@model_validator(mode="before")
|
||||||
@@ -240,7 +239,6 @@ class PydReagent(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class PydSample(PydBaseClass):
|
class PydSample(PydBaseClass):
|
||||||
|
|
||||||
sample_id: str
|
sample_id: str
|
||||||
sampletype: str | None = Field(default=None)
|
sampletype: str | None = Field(default=None)
|
||||||
submission_rank: int | List[int] | None = Field(default=0, validate_default=True)
|
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:
|
# 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
|
# sql_object: ClassVar = ClientSubmission
|
||||||
|
|
||||||
filepath: Path
|
filepath: Path
|
||||||
@@ -1412,5 +1467,3 @@ class PydClientSubmission(PydBaseClass):
|
|||||||
"""
|
"""
|
||||||
from frontend.widgets.submission_widget import ClientSubmissionFormWidget
|
from frontend.widgets.submission_widget import ClientSubmissionFormWidget
|
||||||
return ClientSubmissionFormWidget(parent=parent, clientsubmission=self, samples=samples, disable=disable)
|
return ClientSubmissionFormWidget(parent=parent, clientsubmission=self, samples=samples, disable=disable)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -40,4 +40,47 @@
|
|||||||
cursor: pointer;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="gallery" style="display: grid;grid-template-columns: repeat({{ PLATE_COLUMNS }}, 7.5vw);grid-template-rows: repeat({{ PLATE_ROWS }}, 7.5vw);grid-gap: 2px;">
|
<div class="gallery" style="display: grid;grid-template-columns: repeat({{ plate_columns }}, {{ vw }}vw);grid-template-rows: repeat({{ plate_rows }}, {{ vw }}vw);grid-gap: 2px;">
|
||||||
{% for sample in samples %}
|
{% for sample in samples %}
|
||||||
<div class="well data-link sample" id="{{sample['submitter_id']}}" style="background-color: {{sample['background_color']}};
|
<div class="well data-link sample" id="{{sample['submitter_id']}}" style="background-color: {{ sample['background_color'] }};
|
||||||
border: 1px solid #000;
|
border: 1px solid #000;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
grid-column-start: {{sample['column']}};
|
grid-column-start: {{sample['column']}};
|
||||||
|
|||||||
Reference in New Issue
Block a user