basic procedure creation working

This commit is contained in:
lwark
2025-05-28 14:26:12 -05:00
parent 4e1e06bb5e
commit fef964fba0
7 changed files with 319 additions and 36 deletions

View File

@@ -6,6 +6,7 @@ import json, zipfile, yaml, logging, re, sys
from operator import itemgetter from operator import itemgetter
from pprint import pformat from pprint import pformat
import numpy as np
from jinja2 import Template, TemplateNotFound from jinja2 import Template, TemplateNotFound
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB
from sqlalchemy.orm import relationship, validates, Query from sqlalchemy.orm import relationship, validates, Query
@@ -539,9 +540,19 @@ 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 get_reagents(self, kittype: str | KitType | None = None):
def reagents(self): if not kittype:
return [f"{reagent.name} - {reagent.lot}" for reagent in self.reagent] return [f"{reagent.name} - {reagent.lot}" 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)
reagents = [reagent for reagent in self.reagent]
if assoc:
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]
class Reagent(BaseClass, LogMixin): class Reagent(BaseClass, LogMixin):
""" """
@@ -1151,21 +1162,25 @@ class ProcedureType(BaseClass):
def as_dict(self): def as_dict(self):
return dict( return dict(
name=self.name, name=self.name,
kittype=[item.name for item in self.kittype] kittype=[item.name for item in self.kittype],
plate_rows=self.plate_rows,
plate_columns=self.plate_columns
) )
def construct_dummy_procedure(self): def construct_dummy_procedure(self, run: Run|None=None):
from backend.validators.pydant import PydProcedure from backend.validators.pydant import PydProcedure
if run:
samples = run.constuct_sample_dicts_for_proceduretype(proceduretype=self)
output = dict( output = dict(
proceduretype=self, proceduretype=self,
#name=dict(value=self.name, missing=True), #name=dict(value=self.name, missing=True),
#possible_kits=[kittype.name for kittype in self.kittype], #possible_kits=[kittype.name for kittype in self.kittype],
repeat=False, repeat=False
plate_map=self.construct_plate_map() # plate_map=plate_map
) )
return PydProcedure(**output) return PydProcedure(**output)
def construct_plate_map(self) -> str: def construct_plate_map(self, sample_dicts: List[dict]) -> str:
""" """
Constructs an html based plate map for procedure details. Constructs an html based plate map for procedure details.
@@ -1179,22 +1194,31 @@ class ProcedureType(BaseClass):
""" """
if self.plate_rows == 0 or self.plate_columns == 0: if self.plate_rows == 0 or self.plate_columns == 0:
return "<br/>" return "<br/>"
plate_rows = range(1, self.plate_rows + 1) # plate_rows = range(1, self.plate_rows + 1)
plate_columns = range(1, self.plate_columns + 1) # plate_columns = range(1, self.plate_columns + 1)
total_wells = self.plate_columns * self.plate_rows # total_wells = self.plate_columns * self.plate_rows
vw = round((-0.07 * total_wells) + 12.2, 1) vw = round((-0.07 * len(sample_dicts)) + 12.2, 1)
# sample_dicts = run.constuct_sample_dicts_for_proceduretype(proceduretype=self)
# output_samples = [next((item for item in sample_dicts if item['row'] == row and item['column'] == column),
wells = [dict(name="", row=row, column=column, background_color="#ffffff") # dict(sample_id="", row=row, column=column, background_color="#ffffff"))
for row in plate_rows # for row in plate_rows
for column in plate_columns] # for column in plate_columns]
# logger.debug(f"Output samples:\n{pformat(output_samples)}")
# NOTE: An overly complicated list comprehension create a list of sample locations # 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 # NOTE: next will return a blank cell if no value found for row/column
env = jinja_template_loading() env = jinja_template_loading()
template = env.get_template("plate_map.html") template = env.get_template("plate_map.html")
html = template.render(plate_rows=self.plate_rows, plate_columns=self.plate_columns, samples=wells, vw=vw) html = template.render(plate_rows=self.plate_rows, plate_columns=self.plate_columns, samples=sample_dicts, vw=vw)
return html + "<br/>" return html + "<br/>"
@property
def ranked_plate(self):
matrix = np.array([[0 for yyy in range(1, self.plate_rows + 1)] for xxx in range(1, self.plate_columns + 1)])
return {iii: (item[0][1] + 1, item[0][0] + 1) for iii, item in enumerate(np.ndenumerate(matrix), start=1)}
@property
def total_wells(self):
return self.plate_rows * self.plate_columns
class Procedure(BaseClass): class Procedure(BaseClass):
id = Column(INTEGER, primary_key=True) id = Column(INTEGER, primary_key=True)

View File

@@ -28,11 +28,13 @@ from openpyxl.drawing.image import Image as OpenpyxlImage
from tools import row_map, setup_lookup, jinja_template_loading, rreplace, row_keys, check_key_or_attr, Result, Report, \ from tools import row_map, setup_lookup, jinja_template_loading, rreplace, row_keys, check_key_or_attr, Result, Report, \
report_result, create_holidays_for_year, check_dictionary_inclusion_equality report_result, create_holidays_for_year, check_dictionary_inclusion_equality
from datetime import datetime, date from datetime import datetime, date
from typing import List, Any, Tuple, Literal, Generator, Type from typing import List, Any, Tuple, Literal, Generator, Type, TYPE_CHECKING
from pathlib import Path from pathlib import Path
from jinja2.exceptions import TemplateNotFound from jinja2.exceptions import TemplateNotFound
from jinja2 import Template from jinja2 import Template
from PIL import Image from PIL import Image
if TYPE_CHECKING:
from backend.db.models.kits import ProcedureType
from . import kittype_procedure from . import kittype_procedure
@@ -653,6 +655,10 @@ class Run(BaseClass, LogMixin):
output_list = [assoc.hitpicked for assoc in self.runsampleassociation] output_list = [assoc.hitpicked for assoc in self.runsampleassociation]
return output_list return output_list
@property
def sample_dicts(self) -> List[dict]:
return [dict(sample_id=assoc.sample.sample_id, row=assoc.row, column=assoc.column, background_color="#6ffe1d") for assoc in self.runsampleassociation]
@classmethod @classmethod
def make_plate_map(cls, sample_list: list, plate_rows: int = 8, plate_columns=12) -> str: def make_plate_map(cls, sample_list: list, plate_rows: int = 8, plate_columns=12) -> str:
""" """
@@ -1273,6 +1279,51 @@ class Run(BaseClass, LogMixin):
def allowed_procedures(self): def allowed_procedures(self):
return self.clientsubmission.submissiontype.proceduretype return self.clientsubmission.submissiontype.proceduretype
def get_submission_rank_of_sample(self, sample: Sample|str):
if isinstance(sample, str):
sample = Sample.query(sample_id=sample)
clientsubmissionsampleassoc = next((assoc for assoc in self.clientsubmission.clientsubmissionsampleassociation
if assoc.sample == sample), None)
if clientsubmissionsampleassoc:
return clientsubmissionsampleassoc.submission_rank
else:
return 0
def constuct_sample_dicts_for_proceduretype(self, proceduretype: ProcedureType):
plate_dict = proceduretype.ranked_plate
ranked_samples = []
unranked_samples = []
for sample in self.sample:
submission_rank = self.get_submission_rank_of_sample(sample=sample)
if submission_rank != 0:
row, column = plate_dict[submission_rank]
ranked_samples.append(dict(sample_id=sample.sample_id, row=row, column=column, submission_rank=submission_rank, background_color="#6ffe1d"))
else:
unranked_samples.append(sample)
possible_ranks = (item for item in list(plate_dict.keys()) if item not in [sample['submission_rank'] for sample in ranked_samples])
# logger.debug(possible_ranks)
# possible_ranks = (plate_dict[idx] for idx in possible_ranks)
for sample in unranked_samples:
try:
submission_rank = next(possible_ranks)
except StopIteration:
continue
row, column = plate_dict[submission_rank]
ranked_samples.append(
dict(sample_id=sample.sample_id, row=row, column=column, submission_rank=submission_rank,
background_color="#6ffe1d"))
padded_list = []
for iii in range(1, proceduretype.total_wells+1):
sample = next((item for item in ranked_samples if item['submission_rank']==iii),
dict(sample_id="", row=0, column=0, submission_rank=iii)
)
padded_list.append(sample)
logger.debug(f"Final padded list:\n{pformat(list(sorted(padded_list, key=itemgetter('submission_rank'))))}")
return list(sorted(padded_list, key=itemgetter('submission_rank')))
class SampleType(BaseClass): class SampleType(BaseClass):
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key

View File

@@ -243,6 +243,8 @@ class PydSample(PydBaseClass):
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)
enabled: bool = Field(default=True) enabled: bool = Field(default=True)
row: int = Field(default=0)
column: int = Field(default=0)
@field_validator("sample_id", mode="before") @field_validator("sample_id", mode="before")
@classmethod @classmethod
@@ -1336,6 +1338,7 @@ class PydElastic(BaseModel, extra="allow", arbitrary_types_allowed=True):
class PydProcedure(PydBaseClass, arbitrary_types_allowed=True): class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
proceduretype: ProcedureType | None = Field(default=None) proceduretype: ProcedureType | None = Field(default=None)
run: Run | None = Field(default=None)
name: dict = Field(default=dict(value="NA", missing=True), validate_default=True) name: dict = Field(default=dict(value="NA", missing=True), validate_default=True)
technician: dict = Field(default=dict(value="NA", missing=True)) technician: dict = Field(default=dict(value="NA", missing=True))
repeat: bool = Field(default=False) repeat: bool = Field(default=False)
@@ -1344,6 +1347,7 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
plate_map: str | None = Field(default=None) plate_map: str | None = Field(default=None)
reagent: list | None = Field(default=[]) reagent: list | None = Field(default=[])
reagentrole: dict | None = Field(default={}, validate_default=True) reagentrole: dict | None = Field(default={}, validate_default=True)
samples: List[PydSample] = Field(default=[])
@field_validator("name") @field_validator("name")
@classmethod @classmethod
@@ -1379,18 +1383,68 @@ class PydProcedure(PydBaseClass, arbitrary_types_allowed=True):
value = {item.name: item.reagents for item in kittype.reagentrole} value = {item.name: item.reagents for item in kittype.reagentrole}
return value return value
def update_kittype_reagentroles(self, kittype: str | KitType): def update_kittype_reagentroles(self, kittype: str | KitType):
if kittype == self.__class__.model_fields['kittype'].default['value']: if kittype == self.__class__.model_fields['kittype'].default['value']:
return return
if isinstance(kittype, str): if isinstance(kittype, str):
kittype_obj = KitType.query(name=kittype) kittype_obj = KitType.query(name=kittype)
try: try:
self.reagentrole = {item.name: item.reagents for item in self.reagentrole = {item.name: item.get_reagents(kittype=kittype_obj) for item in
kittype_obj.get_reagents(proceduretype=self.proceduretype)} kittype_obj.get_reagents(proceduretype=self.proceduretype)}
except AttributeError: except AttributeError:
self.reagentrole = {} self.reagentrole = {}
self.possible_kits.insert(0, self.possible_kits.pop(self.possible_kits.index(kittype))) self.possible_kits.insert(0, self.possible_kits.pop(self.possible_kits.index(kittype)))
def shuffle_samples(self, source_row: int, source_column: int, destination_row: int, destination_column=int):
logger.debug(f"Attempting sample shuffle.")
try:
source_sample = next(
(sample for sample in self.samples if sample.row == source_row and sample.column == source_column))
except StopIteration:
raise StopIteration("Couldn't find proper sample.")
logger.debug(f"Source Well: {source_row}, {source_column}")
logger.debug(f"Destination Well: {destination_row}, {destination_column}")
updateable_samples = []
if source_row > destination_row and source_column >= destination_column:
logger.debug(f"Sample was moved ahead.")
movement = "pos"
for sample in self.samples:
if sample.row >= destination_row and sample.column >= destination_column:
if sample.row <= source_row and sample.column <= source_column:
updateable_samples.append(sample)
elif source_row < destination_row and source_column <= destination_column:
logger.debug(f"Sample was moved back.")
movement = "neg"
for sample in self.samples:
if sample.row <= destination_row and sample.column <= destination_column:
if sample.row >= source_row and sample.column >= source_column:
updateable_samples.append(sample)
else:
logger.debug(f"Don't know what happened.")
logger.debug(f"Samples to be updated: {pformat(updateable_samples)}")
for sample in updateable_samples:
if sample.row == source_row and sample.column == source_column:
sample.row = destination_row
sample.column = destination_column
else:
match movement:
case "pos":
if sample.row + 1 > 8:
sample.column += 1
sample.row = 1
else:
sample.row += 1
case "neg":
if sample.row - 1 <= 0:
sample.column -= 1
sample.row = 8
else:
sample.row -= 1
class PydClientSubmission(PydBaseClass): class PydClientSubmission(PydBaseClass):
# sql_object: ClassVar = ClientSubmission # sql_object: ClassVar = ClientSubmission

View File

@@ -84,3 +84,29 @@ div.gallery {
width: 100%; width: 100%;
object-fit: contain; object-fit: contain;
} }
.plate {
display: inline-grid;
grid-auto-flow: column;
place-content: center center;
grid-gap: 2px;
}
.well {
border: 1px solid #000;
padding: 20px;
display: flex;
text-align: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
cursor: move;
}
.well:hover {
box-shadow: 0 4px 25px rgba(0, 0, 0, 0.5);
}
.grid-item p {
max-width: 100%;
border-radius: 8px;
}

View File

@@ -1,17 +1,15 @@
<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;"> <div class="plate" id="plate-container" style="grid-template-columns: repeat({{ plate_columns }}, {{ vw }}vw);grid-template-rows: repeat({{ plate_rows }}, {{ vw }}vw);">
{% 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" draggable="true" id="sample_{{ sample['submission_rank'] }}" style="background-color: {{ sample['background_color'] }};">
border: 1px solid #000; <p style="font-size: 0.7em; text-align: center; word-wrap: break-word;">{{ sample['sample_id'] }}</p>
padding: 20px; <!-- <div class="tooltip" style="font-size: 0.5em; text-align: center; word-wrap: break-word;">{{ sample['sample_id'] }}-->
<!-- <span class="tooltiptext">{{ sample['tooltip'] }}</span>-->
<!-- </div>--
grid-column-start: {{sample['column']}}; grid-column-start: {{sample['column']}};
grid-column-end: {{sample['column']}}; grid-column-end: {{sample['column']}};
grid-row-start: {{sample['row']}}; grid-row-start: {{sample['row']}};
grid-row-end: {{sample['row']}}; grid-row-end: {{sample['row']}};-->
display: flex;
">
<div class="tooltip" style="font-size: 0.5em; text-align: center; word-wrap: break-word;">{{ sample['name'] }}
<span class="tooltiptext">{{ sample['tooltip'] }}</span>
</div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>

View File

@@ -0,0 +1,124 @@
{% extends "details.html" %}
<html>
<head>
{% block head %}
{{ super() }}
<title> New {{ proceduretype['name'] }} for {{ run['plate_number'] }}</title>
{% endblock %}
</head>
<!-- Is this working? -->
<body>
{% block body %}
<h1>New {{ proceduretype['name'] }} for {{ run['plate_number'] }}</h1><br><br>
<div class="procedure_creation_leftright">
<div class="left">
<form>
<label for="technician">Technician:</label><br>
<input type="text" class="form_text" id="technician" name="technician" width="100%" value="{{ procedure['technician']['value'] }}" background-color="{{ procedure['technician']['colour'] }}"><br><br>
<label for="repeat">Repeat:</label>
<input type="checkbox" class="form_check" id="repeat" name="repeat" value="{{ procedure['repeat'] }}"><br><br>
<label>Kit Type:</label><br>
<select class="dropdown" id="kittype" background-colour="{{ procedure['kittype']['colour'] }}">
{% for kittype in procedure['possible_kits'] %}
<option value="{{ kittype }}">{{ kittype }}</option>
{% endfor %}
</select><br>
{% if procedure['reagentrole'] %}
<br><hr><br>
{% for key, value in procedure['reagentrole'].items() %}
<label for="{{ key }}">{{ key }}:</label><br>
<select class="reagentrole dropdown" id="{{ key }}" name="{{ reagentrole }}"><br>
{% for reagent in value %}
<option value="{{ reagent }}">{{ reagent }}</option>
{% endfor %}
</select>
{% endfor %}
{% endif %}
</form>
</div>
<div class="right">
{% if plate_map %}
<h1>Plate map:</h1>
{{ plate_map }}
{% endif %}
</div>
</div>
{% endblock %}
</body>
<script>
{% block script %}
{{ super() }}
document.getElementById("kittype").addEventListener("change", function() {
backend.update_kit(this.value);
})
var formchecks = document.getElementsByClassName('form_check');
for(let i = 0; i < formchecks.length; i++) {
formchecks[i].addEventListener("change", function() {
backend.check_toggle(formchecks[i].id, formchecks[i].checked);
})
};
var formtexts = document.getElementsByClassName('form_text');
for(let i = 0; i < formtexts.length; i++) {
formtexts[i].addEventListener("input", function() {
backend.text_changed(formtexts[i].id, formtexts[i].value);
})
};
const gridContainer = document.getElementById("plate-container");
let draggedItem = null;
//Handle Drag start
gridContainer.addEventListener("dragstart", (e) => {
draggedItem = e.target;
draggedItem.style.opacity = "0.5";
});
//Handle Drag End
gridContainer.addEventListener("dragend", (e) => {
e.target.style.opacity = "1";
draggedItem = null;
});
//handle dragging ove grid items
gridContainer.addEventListener("dragover", (e) => {
e.preventDefault();
});
//Handle Drop
gridContainer.addEventListener("drop", (e) => {
e.preventDefault();
const targetItem = e.target;
if (
targetItem &&
targetItem !== draggedItem &&
targetItem.classList.contains("well")
) {
const draggedIndex = [...gridContainer.children].indexOf(draggedItem);
const targetIndex = [...gridContainer.children].indexOf(targetItem);
if (draggedIndex < targetIndex) {
backend.log_drag(draggedIndex.toString(), targetIndex.toString() + " Lesser");
gridContainer.insertBefore(draggedItem, targetItem.nextSibling);
} else {
backend.log_drag(draggedIndex.toString(), targetIndex.toString() + " Greater");
gridContainer.insertBefore(draggedItem, targetItem);
}
output = [];
fullGrid = [...gridContainer.children];
fullGrid.forEach(function(item, index) {
output.push({item: item, index: index})
});
backend.replow(output);
}
});
{% endblock %}
</script>
</html>

View File

@@ -27,7 +27,6 @@ from sqlalchemy.exc import IntegrityError as sqlalcIntegrityError
from pytz import timezone as tz from pytz import timezone as tz
from functools import wraps from functools import wraps
timezone = tz("America/Winnipeg") timezone = tz("America/Winnipeg")
logger = logging.getLogger(f"procedure.{__name__}") logger = logging.getLogger(f"procedure.{__name__}")
@@ -249,6 +248,7 @@ def timer(func):
func (__function__): incoming function func (__function__): incoming function
""" """
@wraps(func) @wraps(func)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
start_time = time.perf_counter() start_time = time.perf_counter()
@@ -257,6 +257,7 @@ def timer(func):
run_time = end_time - start_time run_time = end_time - start_time
print(f"Finished {func.__name__}() in {run_time:.4f} secs") print(f"Finished {func.__name__}() in {run_time:.4f} secs")
return value return value
return wrapper return wrapper
@@ -295,7 +296,6 @@ class GroupWriteRotatingFileHandler(handlers.RotatingFileHandler):
class CustomFormatter(logging.Formatter): class CustomFormatter(logging.Formatter):
class bcolors: class bcolors:
HEADER = '\033[95m' HEADER = '\033[95m'
OKBLUE = '\033[94m' OKBLUE = '\033[94m'
@@ -482,6 +482,7 @@ def setup_lookup(func):
elif v is not None: elif v is not None:
sanitized_kwargs[k] = v sanitized_kwargs[k] = v
return func(*args, **sanitized_kwargs) return func(*args, **sanitized_kwargs)
return wrapper return wrapper
@@ -532,7 +533,6 @@ def get_application_from_parent(widget):
class Result(BaseModel, arbitrary_types_allowed=True): class Result(BaseModel, arbitrary_types_allowed=True):
owner: str = Field(default="", validate_default=True) owner: str = Field(default="", validate_default=True)
code: int = Field(default=0) code: int = Field(default=0)
msg: str | Exception msg: str | Exception
@@ -768,6 +768,7 @@ def under_development(func):
Result(owner=func.__str__(), code=1, msg=error_msg, Result(owner=func.__str__(), code=1, msg=error_msg,
status="warning")) status="warning"))
return report return report
return wrapper return wrapper
@@ -856,6 +857,7 @@ def create_holidays_for_year(year: int | None = None) -> List[date]:
offset = -d.weekday() # weekday == 0 means Monday offset = -d.weekday() # weekday == 0 means Monday
output = d + timedelta(offset) output = d + timedelta(offset)
return output.date() return output.date()
if not year: if not year:
year = date.today().year year = date.today().year
# NOTE: Includes New Year's day for next year. # NOTE: Includes New Year's day for next year.
@@ -900,6 +902,11 @@ def flatten_list(input_list: list):
return list(itertools.chain.from_iterable(input_list)) return list(itertools.chain.from_iterable(input_list))
def create_plate_grid(rows: int, columns: int):
matrix = np.array([[0 for yyy in range(1, columns + 1)] for xxx in range(1, rows + 1)])
return {iii: (item[0][1]+1, item[0][0]+1) for iii, item in enumerate(np.ndenumerate(matrix), start=1)}
class classproperty(property): class classproperty(property):
def __get__(self, owner_self, owner_cls): def __get__(self, owner_self, owner_cls):
return self.fget(owner_cls) return self.fget(owner_cls)
@@ -1293,4 +1300,3 @@ class Settings(BaseSettings, extra="allow"):
ctx = Settings() ctx = Settings()