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 # 202504.04
- Added html links for equipment/processes/tips. - Added html links for equipment/processes/tips.

View File

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

View File

@@ -87,7 +87,7 @@ class ReagentRole(BaseClass):
new = True new = True
for k, v in sanitized_kwargs.items(): for k, v in sanitized_kwargs.items():
setattr(instance, k, v) 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 return instance, new
@classmethod @classmethod
@@ -785,7 +785,7 @@ class ProcedureType(BaseClass):
) )
return PydProcedure(**output) 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. 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: if self.plate_rows == 0 or self.plate_columns == 0:
return "<br/>" return "<br/>"
sample_dicts = self.pad_sample_dicts(sample_dicts=sample_dicts) 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: 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("support/plate_map.html") template = env.get_template("support/plate_map.html")
html = template.render(plate_rows=self.plate_rows, plate_columns=self.plate_columns, samples=sample_dicts, 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/>" return html + "<br/>"
def pad_sample_dicts(self, sample_dicts: List["PydSample"]): def pad_sample_dicts(self, sample_dicts: List["PydSample"]):
@@ -985,6 +985,7 @@ class Procedure(BaseClass):
output['sample_count'] = len(active_samples) output['sample_count'] = len(active_samples)
output['clientlab'] = self.run.clientsubmission.clientlab.name output['clientlab'] = self.run.clientsubmission.clientlab.name
output['cost'] = 0.00 output['cost'] = 0.00
output['platemap'] = self.make_procedure_platemap()
return output return output
def to_pydantic(self, **kwargs): def to_pydantic(self, **kwargs):
@@ -1038,6 +1039,12 @@ class Procedure(BaseClass):
output = {k: v for k, v in dicto.items()} output = {k: v for k, v in dicto.items()}
return output 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): class ProcedureTypeReagentRoleAssociation(BaseClass):
""" """
@@ -1143,7 +1150,7 @@ class ProcedureTypeReagentRoleAssociation(BaseClass):
case _: case _:
pass pass
setattr(instance, k, v) 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 return instance, new
@classmethod @classmethod
@@ -1423,7 +1430,7 @@ class EquipmentRole(BaseClass):
new = True new = True
for k, v in sanitized_kwargs.items(): for k, v in sanitized_kwargs.items():
setattr(instance, k, v) 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 return instance, new
@classmethod @classmethod
@@ -1816,6 +1823,9 @@ class Process(BaseClass):
class ProcessVersion(BaseClass): class ProcessVersion(BaseClass):
pyd_model_name = "Process"
id = Column(INTEGER, primary_key=True) #: Process id, primary key id = Column(INTEGER, primary_key=True) #: Process id, primary key
version = Column(FLOAT(2), default=1.00) #: Version number version = Column(FLOAT(2), default=1.00) #: Version number
date_verified = Column(TIMESTAMP) #: Date this version was deemed worthy date_verified = Column(TIMESTAMP) #: Date this version was deemed worthy
@@ -1867,7 +1877,7 @@ class ProcessVersion(BaseClass):
version: str | float | None = None, version: str | float | None = None,
name: str | None = None, name: str | None = None,
limit: int = 0, limit: int = 0,
**kwargs) -> ReagentLot | List[ReagentLot]: **kwargs) -> ProcessVersion | List[ProcessVersion]:
query: Query = cls.__database_session__.query(cls) query: Query = cls.__database_session__.query(cls)
match name: match name:
case str(): case str():
@@ -1881,6 +1891,9 @@ class ProcessVersion(BaseClass):
pass pass
return cls.execute_query(query=query, limit=limit) 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): class Tips(BaseClass):
""" """

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
""" """
Default Default writers for procedures.
""" """
from __future__ import annotations from __future__ import annotations
import logging, sys import logging, sys
@@ -18,9 +18,7 @@ class ProcedureInfoWriter(DefaultKEYVALUEWriter):
'reagentrole', 'results', 'sample', 'tips', 'reagentlot'] 'reagentrole', 'results', 'sample', 'tips', 'reagentlot']
def __init__(self, pydant_obj, *args, **kwargs): def __init__(self, pydant_obj, *args, **kwargs):
super().__init__(pydant_obj=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} 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, 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 from __future__ import annotations
import logging 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)) equipment['name'] == relevant_procedure_item.name))
equipmentrole['equipment'].insert(0, equipmentrole['equipment'].pop( equipmentrole['equipment'].insert(0, equipmentrole['equipment'].pop(
equipmentrole['equipment'].index(item_in_er_list))) 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']] 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$") 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))] 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__)}") # sys.exit(f"ProcedureDict:\n{pformat(proceduretype_dict)}")
logger.debug(f"ProcedureType:\n{pformat(proceduretype_dict)}")
html = render_details_template( html = render_details_template(
template_name="procedure_creation", template_name="procedure_creation",
js_in=["procedure_form", "grid_drag", "context_menu"], js_in=["procedure_form", "grid_drag", "context_menu"],
@@ -88,8 +85,9 @@ class ProcedureCreation(QDialog):
self.webview.setHtml(html) self.webview.setHtml(html)
@pyqtSlot(str, str, str, str) @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 from backend.db.models import Equipment, ProcessVersion, TipsLot
logger.debug(f"\n\nEquipmentRole: {equipmentrole}, Equipment: {equipment}, Process: {processversion}, Tips: {tips}\n\n")
try: try:
equipment_of_interest = next( equipment_of_interest = next(
(item for item in self.procedure.equipment if item.equipmentrole == equipmentrole)) (item for item in self.procedure.equipment if item.equipmentrole == equipmentrole))
@@ -103,9 +101,9 @@ class ProcedureCreation(QDialog):
eoi.name = equipment.name eoi.name = equipment.name
eoi.asset_number = equipment.asset_number eoi.asset_number = equipment.asset_number
eoi.nickname = equipment.nickname eoi.nickname = equipment.nickname
process_name, version = process.split("-v") process_name, version = processversion.split("-v")
process = ProcessVersion.query(name=process_name, version=version, limit=1) processversion = ProcessVersion.query(name=process_name, version=version, limit=1)
eoi.process = process eoi.processversion = processversion.to_pydantic()
try: try:
tips_manufacturer, tipsref, lot = [item if item != "" else None for item in tips.split("-")] tips_manufacturer, tipsref, lot = [item if item != "" else None for item in tips.split("-")]
tips = TipsLot.query(manufacturer=tips_manufacturer, ref=tipsref, lot=lot) tips = TipsLot.query(manufacturer=tips_manufacturer, ref=tipsref, lot=lot)

View File

@@ -56,10 +56,15 @@
{% endif %} {% endif %}
{% if procedure['sample'] %} {% if procedure['sample'] %}
<button type="button"><h3><u>Procedure Samples:</u></h3></button> <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'] %} <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> {% endfor %}</p>
{% endif %} {% endif %}
{% endif %}
{% endblock %} {% endblock %}
{% if not child %} {% if not child %}
</body> </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);"> <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" draggable="true" id="{{ sample['well_id'] }}" style="background-color: {{ sample['background_color'] }};"> <div class="well" draggable="true" id="{{ sample['sample_id'] }}" style="background-color: {{ sample['background_color'] }};">
<p style="font-size: 0.7em; text-align: center; word-wrap: break-word;">{{ sample['sample_id'] }}</p> {% 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> </div>
{% endfor %} {% endfor %}
</div> </div>

View File

@@ -8,6 +8,7 @@ from copy import copy
from collections import OrderedDict from collections import OrderedDict
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from json import JSONDecodeError from json import JSONDecodeError
from pprint import pformat
from threading import Thread from threading import Thread
from inspect import getmembers, isfunction, stack from inspect import getmembers, isfunction, stack
from dateutil.easter import easter from dateutil.easter import easter
@@ -1039,7 +1040,7 @@ def flatten_list(input_list: list) -> list:
return list(itertools.chain.from_iterable(input_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 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: try:
input_dict = json.dumps(input_dict) input_dict = json.dumps(input_dict)
except TypeError: except TypeError:
input_dict = str(input_dict) match input_dict:
return input_dict case str():
pass
case _:
input_dict = str(input_dict)
return input_dict.strip('\"')
output = {} output = {}
for key, value in input_dict.items(): for key, value in input_dict.items():
match value: match value:
@@ -1070,7 +1075,13 @@ def sanitize_object_for_json(input_dict: dict) -> dict:
try: try:
value = json.dumps(value) value = json.dumps(value)
except TypeError: except TypeError:
value = str(value) match value:
case str():
pass
case _:
value = str(value)
if isinstance(value, str):
value = value.strip('\"')
output[key] = value output[key] = value
return output return output