Plate map in procedure details.

This commit is contained in:
lwark
2025-09-15 09:21:52 -05:00
parent 11abaafcfc
commit 3862604dfa
14 changed files with 756 additions and 781 deletions

View File

@@ -1,3 +1,7 @@
# 202509.02
- First Useable updated version.
# 202504.04
- Added html links for equipment/processes/tips.

View File

@@ -5,7 +5,6 @@ from __future__ import annotations
import sys, logging, json, inspect
from datetime import datetime, date
from pprint import pformat
from dateutil.parser import parse
from jinja2 import TemplateNotFound, Template
from pandas import DataFrame
@@ -19,7 +18,7 @@ from sqlalchemy.exc import ArgumentError
from typing import Any, List, ClassVar
from pathlib import Path
from sqlalchemy.orm.relationships import _RelationshipDeclared
from tools import report_result, list_sort_dict, jinja_template_loading, Report, Result
from tools import report_result, list_sort_dict, jinja_template_loading, Report, Result, ctx
# NOTE: Load testing environment
if 'pytest' in sys.modules:
@@ -92,10 +91,6 @@ class BaseClass(Base):
Returns:
Session: DB session from ctx settings.
"""
if 'pytest' not in sys.modules:
from tools import ctx
else:
from test_settings import ctx
return ctx.database_session
@classmethod
@@ -107,10 +102,6 @@ class BaseClass(Base):
Returns:
Path: Location of the Submissions directory in Settings object
"""
if 'pytest' not in sys.modules:
from tools import ctx
else:
from test_settings import ctx
return ctx.directory_path
@classmethod
@@ -122,10 +113,6 @@ class BaseClass(Base):
Returns:
Path: Location of the Submissions backup directory in Settings object
"""
if 'pytest' not in sys.modules:
from tools import ctx
else:
from test_settings import ctx
return ctx.backup_path
def __init__(self, *args, **kwargs):
@@ -284,7 +271,7 @@ class BaseClass(Base):
if issubclass(v.__class__, PydBaseClass):
setattr(instance, k, v.to_sql())
instance._misc_info.update(outside_kwargs)
logger.info(f"Instance from query or create: {instance}, new: {new}")
# logger.info(f"Instance from query or create: {instance}, new: {new}")
return instance, new
@classmethod

View File

@@ -87,7 +87,7 @@ class ReagentRole(BaseClass):
new = True
for k, v in sanitized_kwargs.items():
setattr(instance, k, v)
logger.info(f"Instance from query or create: {instance}")
# logger.info(f"Instance from query or create: {instance}")
return instance, new
@classmethod
@@ -785,7 +785,7 @@ class ProcedureType(BaseClass):
)
return PydProcedure(**output)
def construct_plate_map(self, sample_dicts: List["PydSample"]) -> str:
def construct_plate_map(self, sample_dicts: List["PydSample"], creation:bool=True, vw_modifier:float=1.0) -> str:
"""
Constructs an html based plate map for procedure details.
@@ -800,13 +800,13 @@ class ProcedureType(BaseClass):
if self.plate_rows == 0 or self.plate_columns == 0:
return "<br/>"
sample_dicts = self.pad_sample_dicts(sample_dicts=sample_dicts)
vw = round((-0.07 * len(sample_dicts)) + 12.2, 1)
vw = round((-0.07 * len(sample_dicts)) + (12.2 * vw_modifier), 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
env = jinja_template_loading()
template = env.get_template("support/plate_map.html")
html = template.render(plate_rows=self.plate_rows, plate_columns=self.plate_columns, samples=sample_dicts,
vw=vw)
vw=vw, creation=creation)
return html + "<br/>"
def pad_sample_dicts(self, sample_dicts: List["PydSample"]):
@@ -985,6 +985,7 @@ class Procedure(BaseClass):
output['sample_count'] = len(active_samples)
output['clientlab'] = self.run.clientsubmission.clientlab.name
output['cost'] = 0.00
output['platemap'] = self.make_procedure_platemap()
return output
def to_pydantic(self, **kwargs):
@@ -1038,6 +1039,12 @@ class Procedure(BaseClass):
output = {k: v for k, v in dicto.items()}
return output
def make_procedure_platemap(self):
dicto = [sample.to_pydantic() for sample in self.proceduresampleassociation]
html = self.proceduretype.construct_plate_map(sample_dicts=dicto, creation=False, vw_modifier=1.15)
return html
class ProcedureTypeReagentRoleAssociation(BaseClass):
"""
@@ -1143,7 +1150,7 @@ class ProcedureTypeReagentRoleAssociation(BaseClass):
case _:
pass
setattr(instance, k, v)
logger.info(f"Instance from query or create: {instance.__dict__}\nis new: {new}")
# logger.info(f"Instance from query or create: {instance.__dict__}\nis new: {new}")
return instance, new
@classmethod
@@ -1423,7 +1430,7 @@ class EquipmentRole(BaseClass):
new = True
for k, v in sanitized_kwargs.items():
setattr(instance, k, v)
logger.info(f"Instance from query or create: {instance}")
# logger.info(f"Instance from query or create: {instance}")
return instance, new
@classmethod
@@ -1816,6 +1823,9 @@ class Process(BaseClass):
class ProcessVersion(BaseClass):
pyd_model_name = "Process"
id = Column(INTEGER, primary_key=True) #: Process id, primary key
version = Column(FLOAT(2), default=1.00) #: Version number
date_verified = Column(TIMESTAMP) #: Date this version was deemed worthy
@@ -1867,7 +1877,7 @@ class ProcessVersion(BaseClass):
version: str | float | None = None,
name: str | None = None,
limit: int = 0,
**kwargs) -> ReagentLot | List[ReagentLot]:
**kwargs) -> ProcessVersion | List[ProcessVersion]:
query: Query = cls.__database_session__.query(cls)
match name:
case str():
@@ -1881,6 +1891,9 @@ class ProcessVersion(BaseClass):
pass
return cls.execute_query(query=query, limit=limit)
# def to_pydantic(self, pyd_model_name: str | None = None, **kwargs):
# output = super().to_pydantic(pyd_model_name=pyd_model_name, **kwargs)
class Tips(BaseClass):
"""

View File

@@ -879,7 +879,7 @@ class Run(BaseClass, LogMixin):
Returns:
PydSubmission: converted object.
"""
from backend.validators import PydRun
from backend.validators import PydClientSubmission, PydRun
dicto = self.details_dict(full_data=True, backup=backup)
new_dict = {}
for key, value in dicto.items():
@@ -1916,6 +1916,8 @@ class ProcedureSampleAssociation(BaseClass):
misc = output['misc_info']
output.update(relevant)
output['misc_info'] = misc
output['row'] = self.row
output['column'] = self.column
output['results'] = [result.details_dict() for result in output['results']]
return output

View File

@@ -2,7 +2,6 @@
Contains pandas and openpyxl convenience functions for interacting with excel workbooks
"""
# from backend.excel.parsers.clientsubmission_parser import ClientSubmissionInfoParser, ClientSubmissionSampleParser
from .parsers import (
DefaultParser, DefaultKEYVALUEParser, DefaultTABLEParser,
ProcedureInfoParser, ProcedureSampleParser, ProcedureReagentParser, ProcedureEquipmentParser,

View File

@@ -8,7 +8,7 @@ from pathlib import Path
from datetime import date
from typing import Tuple, List
from backend.db.models import Procedure, Run
from tools import jinja_template_loading, get_first_blank_df_row, row_map, flatten_list
from tools import jinja_template_loading, get_first_blank_df_row, row_map, flatten_list, ctx
from PyQt6.QtWidgets import QWidget
from openpyxl.worksheet.worksheet import Worksheet
@@ -173,10 +173,6 @@ class TurnaroundMaker(ReportArchetype):
Returns:
"""
if 'pytest' not in sys.modules:
from tools import ctx
else:
from test_settings import ctx
days = sub.turnaround_time
try:
tat = sub.get_default_info("turnaround_time")

View File

@@ -36,7 +36,6 @@ class DefaultWriter(object):
value = value['name']
except (KeyError, ValueError):
return
# logger.debug(f"Value type: {type(value)}")
match value:
case x if issubclass(value.__class__, BaseClass):
value = value.name
@@ -81,7 +80,6 @@ class DefaultWriter(object):
self.worksheet = self.prewrite(self.worksheet, start_row=start_row)
self.start_row = self.delineate_start_row(start_row=start_row)
self.end_row = self.delineate_end_row(start_row=start_row)
logger.debug(f"Rows for {self.__class__.__name__}:\tstart: {self.start_row}, end: {self.end_row}")
return workbook
def delineate_start_row(self, start_row: int = 1) -> int:
@@ -93,7 +91,6 @@ class DefaultWriter(object):
Returns:
int
"""
logger.debug(f"{self.__class__.__name__} will start looking for blank rows at {start_row}")
for iii, row in enumerate(self.worksheet.iter_rows(min_row=start_row), start=start_row):
if all([item.value is None for item in row]):
return iii
@@ -146,7 +143,6 @@ class DefaultKEYVALUEWriter(DefaultWriter):
dictionary = sort_dict_by_list(dictionary=dictionary, order_list=self.key_order)
for ii, (k, v) in enumerate(dictionary.items(), start=self.start_row):
value = self.stringify_value(value=v)
logger.debug(f"{self.__class__.__name__} attempting to write {value}")
if value is None:
continue
self.worksheet.cell(column=1, row=ii, value=self.prettify_key(k))
@@ -172,7 +168,6 @@ class DefaultTABLEWriter(DefaultWriter):
def delineate_end_row(self, start_row: int = 1) -> int:
end_row = start_row + len(self.pydant_obj) + 1
logger.debug(f"End row has been delineated as {start_row} + {len(self.pydant_obj)} + 1 = {end_row}")
return end_row
def pad_samples_to_length(self, row_count,
@@ -220,7 +215,6 @@ class DefaultTABLEWriter(DefaultWriter):
value = object.improved_dict()[header.lower().replace(" ", "_")]
except (AttributeError, KeyError):
value = ""
# logger.debug(f"{self.__class__.__name__} attempting to write {value}")
self.worksheet.cell(row=write_row, column=column, value=self.stringify_value(value))
self.worksheet = self.postwrite(self.worksheet)
return workbook

View File

@@ -1,5 +1,5 @@
"""
Default
Default writers for procedures.
"""
from __future__ import annotations
import logging, sys
@@ -18,9 +18,7 @@ class ProcedureInfoWriter(DefaultKEYVALUEWriter):
'reagentrole', 'results', 'sample', 'tips', 'reagentlot']
def __init__(self, pydant_obj, *args, **kwargs):
super().__init__(pydant_obj=pydant_obj, *args, **kwargs)
self.fill_dictionary = {k: v for k, v in self.fill_dictionary.items() if k not in self.__class__.exclude}
def write_to_workbook(self, workbook: Workbook, sheet: str | None = None,

View File

@@ -1,5 +1,5 @@
"""
Writers for PCR results from Design and Analysis Software
"""
from __future__ import annotations
import logging

File diff suppressed because it is too large Load Diff

View File

@@ -69,13 +69,10 @@ class ProcedureCreation(QDialog):
equipment['name'] == relevant_procedure_item.name))
equipmentrole['equipment'].insert(0, equipmentrole['equipment'].pop(
equipmentrole['equipment'].index(item_in_er_list)))
# proceduretype_dict['equipment_section'] = EquipmentUsage.construct_html(procedure=self.procedure, child=True)
proceduretype_dict['equipment'] = [sanitize_object_for_json(object) for object in proceduretype_dict['equipment']]
self.update_equipment = EquipmentUsage.update_equipment
regex = re.compile(r".*R\d$")
proceduretype_dict['previous'] = [""] + [item.name for item in self.run.procedure if item.proceduretype == self.proceduretype and not bool(regex.match(item.name))]
logger.debug(f"Procedure:\n{pformat(self.procedure.__dict__)}")
logger.debug(f"ProcedureType:\n{pformat(proceduretype_dict)}")
# sys.exit(f"ProcedureDict:\n{pformat(proceduretype_dict)}")
html = render_details_template(
template_name="procedure_creation",
js_in=["procedure_form", "grid_drag", "context_menu"],
@@ -88,8 +85,9 @@ class ProcedureCreation(QDialog):
self.webview.setHtml(html)
@pyqtSlot(str, str, str, str)
def update_equipment(self, equipmentrole: str, equipment: str, process: str, tips: str):
def update_equipment(self, equipmentrole: str, equipment: str, processversion: str, tips: str):
from backend.db.models import Equipment, ProcessVersion, TipsLot
logger.debug(f"\n\nEquipmentRole: {equipmentrole}, Equipment: {equipment}, Process: {processversion}, Tips: {tips}\n\n")
try:
equipment_of_interest = next(
(item for item in self.procedure.equipment if item.equipmentrole == equipmentrole))
@@ -103,9 +101,9 @@ class ProcedureCreation(QDialog):
eoi.name = equipment.name
eoi.asset_number = equipment.asset_number
eoi.nickname = equipment.nickname
process_name, version = process.split("-v")
process = ProcessVersion.query(name=process_name, version=version, limit=1)
eoi.process = process
process_name, version = processversion.split("-v")
processversion = ProcessVersion.query(name=process_name, version=version, limit=1)
eoi.processversion = processversion.to_pydantic()
try:
tips_manufacturer, tipsref, lot = [item if item != "" else None for item in tips.split("-")]
tips = TipsLot.query(manufacturer=tips_manufacturer, ref=tipsref, lot=lot)

View File

@@ -56,10 +56,15 @@
{% endif %}
{% if procedure['sample'] %}
<button type="button"><h3><u>Procedure Samples:</u></h3></button>
{% if procedure['platemap']|length > 5 %}
<br>
{{ procedure['platemap'] }}
{% else %}
<p>{% for sample in procedure['sample'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<a class="{% if sample['active'] %}data-link {% else %}unused {% endif %}sample" id="{{ sample['sample_id'] }}">{{ sample['sample_id']}}</a><br>
<a class="{% if sample['active'] %}data-link {% else %}unused {% endif %}sample" id="{{ sample['sample_id'] }}">{{ sample['sample_id']}}</a><br>
{% endfor %}</p>
{% endif %}
{% endif %}
{% endblock %}
{% if not child %}
</body>

View File

@@ -1,7 +1,11 @@
<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 %}
<div class="well" draggable="true" id="{{ sample['well_id'] }}" style="background-color: {{ sample['background_color'] }};">
<p style="font-size: 0.7em; text-align: center; word-wrap: break-word;">{{ sample['sample_id'] }}</p>
<div class="well" draggable="true" id="{{ sample['sample_id'] }}" style="background-color: {{ sample['background_color'] }};">
{% if creation %}
<p style="font-size: 0.7em; text-align: center; word-wrap: break-word;">{{ sample['sample_id'] }}</p>
{% else %}
<a class="data-link sample" id="{{ sample['sample_id'] }}">{{ sample['sample_id']}}</a><br>
{% endif %}
</div>
{% endfor %}
</div>

View File

@@ -8,6 +8,7 @@ from copy import copy
from collections import OrderedDict
from datetime import date, datetime, timedelta
from json import JSONDecodeError
from pprint import pformat
from threading import Thread
from inspect import getmembers, isfunction, stack
from dateutil.easter import easter
@@ -1039,7 +1040,7 @@ def flatten_list(input_list: list) -> list:
return list(itertools.chain.from_iterable(input_list))
def sanitize_object_for_json(input_dict: dict) -> dict:
def sanitize_object_for_json(input_dict: dict) -> dict | str:
"""
Takes an object and makes sure its components can be converted to JSON
@@ -1057,8 +1058,12 @@ def sanitize_object_for_json(input_dict: dict) -> dict:
try:
input_dict = json.dumps(input_dict)
except TypeError:
input_dict = str(input_dict)
return input_dict
match input_dict:
case str():
pass
case _:
input_dict = str(input_dict)
return input_dict.strip('\"')
output = {}
for key, value in input_dict.items():
match value:
@@ -1070,7 +1075,13 @@ def sanitize_object_for_json(input_dict: dict) -> dict:
try:
value = json.dumps(value)
except TypeError:
value = str(value)
match value:
case str():
pass
case _:
value = str(value)
if isinstance(value, str):
value = value.strip('\"')
output[key] = value
return output