Added html links for equipment/processes/tips.

This commit is contained in:
lwark
2025-04-25 15:22:33 -05:00
parent b9ed4eef94
commit b42a7ab100
12 changed files with 378 additions and 15 deletions

View File

@@ -55,9 +55,10 @@ def update_log(mapper, connection, target):
continue
added = [str(item) for item in hist.added]
# NOTE: Attributes left out to save space
if attr.key in ['artic_technician', 'submission_sample_associations', 'submission_reagent_associations',
'submission_equipment_associations', 'submission_tips_associations', 'contact_id', 'gel_info',
'gel_controls', 'source_plates']:
# if attr.key in ['artic_technician', 'submission_sample_associations', 'submission_reagent_associations',
# 'submission_equipment_associations', 'submission_tips_associations', 'contact_id', 'gel_info',
# 'gel_controls', 'source_plates']:
if attr.key in LogMixin.tracking_exclusion:
continue
deleted = [str(item) for item in hist.deleted]
change = dict(field=attr.key, added=added, deleted=deleted)

View File

@@ -9,7 +9,7 @@ from sqlalchemy import Column, INTEGER, String, JSON
from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query, Session, InstrumentedAttribute, ColumnProperty
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.exc import ArgumentError
from typing import Any, List
from typing import Any, List, ClassVar
from pathlib import Path
from sqlalchemy.orm.relationships import _RelationshipDeclared
from tools import report_result, list_sort_dict
@@ -25,6 +25,12 @@ logger = logging.getLogger(f"submissions.{__name__}")
class LogMixin(Base):
tracking_exclusion: ClassVar = ['artic_technician', 'submission_sample_associations',
'submission_reagent_associations', 'submission_equipment_associations',
'submission_tips_associations', 'contact_id', 'gel_info', 'gel_controls',
'source_plates']
__abstract__ = True
@property

View File

@@ -4,12 +4,15 @@ All kit and reagent related models
from __future__ import annotations
import json, zipfile, yaml, logging, re, sys
from pprint import pformat
from jinja2 import Template, TemplateNotFound
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB
from sqlalchemy.orm import relationship, validates, Query
from sqlalchemy.ext.associationproxy import association_proxy
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
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 pandas import ExcelFile
from pathlib import Path
@@ -2065,6 +2068,53 @@ class Equipment(BaseClass, LogMixin):
output.append(equipment[choice])
return output
def to_sub_dict(self, full_data: bool = False, **kwargs) -> dict:
"""
dictionary containing values necessary for gui
Args:
full_data (bool, optional): Whether to include submissions in data for details. Defaults to False.
Returns:
dict: representation of the equipment's attributes
"""
if self.nickname:
nickname = self.nickname
else:
nickname = self.name
output = dict(
name=self.name,
nickname=nickname,
asset_number=self.asset_number
)
if full_data:
subs = []
output['submissions'] = [dict(plate=item.submission.rsl_plate_num, process=item.process.name)
if item.process else dict(plate=item.submission.rsl_plate_num, process="NA")
for item in self.equipment_submission_associations]
output['excluded'] = ['missing', 'submissions', 'excluded', 'editable']
return output
@classproperty
def details_template(cls) -> Template:
"""
Get the details jinja template for the correct class
Args:
base_dict (dict): incoming dictionary of Submission fields
Returns:
Tuple(dict, Template): (Updated dictionary, Template to be rendered)
"""
env = jinja_template_loading()
temp_name = f"{cls.__name__.lower()}_details.html"
try:
template = env.get_template(temp_name)
except TemplateNotFound as e:
logger.error(f"Couldn't find template {e}")
template = env.get_template("equipment_details.html")
return template
class EquipmentRole(BaseClass):
"""
@@ -2219,6 +2269,10 @@ class SubmissionEquipmentAssociation(BaseClass):
self.equipment = equipment
self.role = role
@property
def process(self):
return Process.query(id=self.process_id)
def to_sub_dict(self) -> dict:
"""
This SubmissionEquipmentAssociation as a dictionary
@@ -2433,6 +2487,44 @@ class Process(BaseClass):
tip_roles=tip_roles
)
def to_sub_dict(self, full_data: bool = False, **kwargs) -> dict:
"""
dictionary containing values necessary for gui
Args:
full_data (bool, optional): Whether to include submissions in data for details. Defaults to False.
Returns:
dict: representation of the equipment's attributes
"""
output = dict(
name=self.name,
)
if full_data:
output['submissions'] = [dict(plate=sub.submission.rsl_plate_num, equipment=sub.equipment.name) for sub in self.submissions]
output['excluded'] = ['missing', 'submissions', 'excluded', 'editable']
return output
@classproperty
def details_template(cls) -> Template:
"""
Get the details jinja template for the correct class
Args:
base_dict (dict): incoming dictionary of Submission fields
Returns:
Tuple(dict, Template): (Updated dictionary, Template to be rendered)
"""
env = jinja_template_loading()
temp_name = f"{cls.__name__.lower()}_details.html"
try:
template = env.get_template(temp_name)
except TemplateNotFound as e:
logger.error(f"Couldn't find template {e}")
template = env.get_template("process_details.html")
return template
class TipRole(BaseClass):
"""
@@ -2576,6 +2668,45 @@ class Tips(BaseClass, LogMixin):
name=self.name
)
def to_sub_dict(self, full_data: bool = False, **kwargs) -> dict:
"""
dictionary containing values necessary for gui
Args:
full_data (bool, optional): Whether to include submissions in data for details. Defaults to False.
Returns:
dict: representation of the equipment's attributes
"""
output = dict(
name=self.name,
lot=self.lot,
)
if full_data:
output['submissions'] = [dict(plate=item.submission.rsl_plate_num, role=item.role_name)
for item in self.tips_submission_associations]
output['excluded'] = ['missing', 'submissions', 'excluded', 'editable']
return output
@classproperty
def details_template(cls) -> Template:
"""
Get the details jinja template for the correct class
Args:
base_dict (dict): incoming dictionary of Submission fields
Returns:
Tuple(dict, Template): (Updated dictionary, Template to be rendered)
"""
env = jinja_template_loading()
temp_name = f"{cls.__name__.lower()}_details.html"
try:
template = env.get_template(temp_name)
except TemplateNotFound as e:
logger.error(f"Couldn't find template {e}")
template = env.get_template("tips_details.html")
return template
class SubmissionTypeTipRoleAssociation(BaseClass):
"""

View File

@@ -133,8 +133,8 @@ class CustomFigure(Figure):
else:
html += "<h1>No data was retrieved for the given parameters.</h1>"
html += '</body></html>'
with open("test.html", "w", encoding="utf-8") as f:
f.write(html)
# with open("test.html", "w", encoding="utf-8") as f:
# f.write(html)
return html

View File

@@ -7,7 +7,7 @@ from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtCore import Qt, pyqtSlot
from jinja2 import TemplateNotFound
from backend.db.models import BasicSubmission, BasicSample, Reagent, KitType
from backend.db.models import BasicSubmission, BasicSample, Reagent, KitType, Equipment, Process, Tips
from tools import is_power_user, jinja_template_loading, timezone, get_application_from_parent
from .functions import select_save_file, save_pdf
from pathlib import Path
@@ -84,6 +84,48 @@ class SubmissionDetails(QDialog):
else:
self.back.setEnabled(True)
@pyqtSlot(str)
def equipment_details(self, equipment: str | Equipment):
logger.debug(f"Equipment details")
if isinstance(equipment, str):
equipment = Equipment.query(name=equipment)
base_dict = equipment.to_sub_dict(full_data=True)
template = equipment.details_template
template_path = Path(template.environment.loader.__getattribute__("searchpath")[0])
with open(template_path.joinpath("css", "styles.css"), "r") as f:
css = f.read()
html = template.render(equipment=base_dict, css=css)
self.webview.setHtml(html)
self.setWindowTitle(f"Equipment Details - {equipment.name}")
@pyqtSlot(str)
def process_details(self, process: str | Process):
logger.debug(f"Equipment details")
if isinstance(process, str):
process = Process.query(name=process)
base_dict = process.to_sub_dict(full_data=True)
template = process.details_template
template_path = Path(template.environment.loader.__getattribute__("searchpath")[0])
with open(template_path.joinpath("css", "styles.css"), "r") as f:
css = f.read()
html = template.render(process=base_dict, css=css)
self.webview.setHtml(html)
self.setWindowTitle(f"Process Details - {process.name}")
@pyqtSlot(str)
def tips_details(self, tips: str | Tips):
logger.debug(f"Equipment details: {tips}")
if isinstance(tips, str):
tips = Tips.query(lot=tips)
base_dict = tips.to_sub_dict(full_data=True)
template = tips.details_template
template_path = Path(template.environment.loader.__getattribute__("searchpath")[0])
with open(template_path.joinpath("css", "styles.css"), "r") as f:
css = f.read()
html = template.render(tips=base_dict, css=css)
self.webview.setHtml(html)
self.setWindowTitle(f"Process Details - {tips.name}")
@pyqtSlot(str)
def sample_details(self, sample: str | BasicSample):
"""
@@ -103,8 +145,8 @@ class SubmissionDetails(QDialog):
with open(template_path.joinpath("css", "styles.css"), "r") as f:
css = f.read()
html = template.render(sample=base_dict, css=css)
with open(f"{sample.submitter_id}.html", 'w') as f:
f.write(html)
# with open(f"{sample.submitter_id}.html", 'w') as f:
# f.write(html)
self.webview.setHtml(html)
self.setWindowTitle(f"Sample Details - {sample.submitter_id}")

View File

@@ -20,19 +20,19 @@
{% if sub['reagents'] %}
<h3><u>Reagents:</u></h3>
<p>{% for item in sub['reagents'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['role'] }}</b>: <a class="data-link reagent" id="{{ item['lot'] }}">{{ item['lot'] }} (EXP: {{ item['expiry'] }})</a><br>
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['role'] }}:</b> <a class="data-link reagent" id="{{ item['lot'] }}">{{ item['lot'] }} (EXP: {{ item['expiry'] }})</a><br>
{% endfor %}</p>
{% endif %}
{% if sub['equipment'] %}
<h3><u>Equipment:</u></h3>
<p>{% for item in sub['equipment'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['role'] }}:</b> {{ item['name'] }} ({{ item['asset_number'] }}): {{ item['processes'][0]|replace('\n\t', '<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;') }}<br>
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['role'] }}:</b> <a class="data-link equipment" id="{{ item['name'] }}"> {{ item['name'] }} ({{ item['asset_number'] }})</a>: <a class="data-link process" id="{{ item['processes'][0]|replace('\n\t', '') }}">{{ item['processes'][0]|replace('\n\t', '<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;') }}</a><br>
{% endfor %}</p>
{% endif %}
{% if sub['tips'] %}
<h3><u>Tips:</u></h3>
<p>{% for item in sub['tips'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['role'] }}:</b> {{ item['name'] }} ({{ item['lot'] }})<br>
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ item['role'] }}:</b> <a class="data-link tips" id="{{ item['lot'] }}">{{ item['name'] }} ({{ item['lot'] }})</a><br>
{% endfor %}</p>
{% endif %}
{% if sub['samples'] %}
@@ -99,6 +99,33 @@
})
}
var equipmentSelection = document.getElementsByClassName('equipment');
for(let i = 0; i < equipmentSelection.length; i++) {
equipmentSelection[i].addEventListener("click", function() {
console.log(equipmentSelection[i].id);
backend.equipment_details(equipmentSelection[i].id);
})
}
var processSelection = document.getElementsByClassName('process');
for(let i = 0; i < processSelection.length; i++) {
processSelection[i].addEventListener("click", function() {
console.log(processSelection[i].id);
backend.process_details(processSelection[i].id);
})
}
var tipsSelection = document.getElementsByClassName('tips');
for(let i = 0; i < tipsSelection.length; i++) {
tipsSelection[i].addEventListener("click", function() {
console.log(tipsSelection[i].id);
backend.tips_details(tipsSelection[i].id);
})
}
document.getElementById("sign_btn").addEventListener("click", function(){
backend.sign_off("{{ sub['plate_number'] }}");
});

View File

@@ -0,0 +1,50 @@
{% extends "details.html" %}
<head>
{% block head %}
{{ super() }}
<title>Equipment Details for {{ equipment['name'] }}</title>
{% endblock %}
</head>
<body>
{% block body %}
<h2><u>Equipment Details for {{ equipment['name'] }}</u></h2>
{{ super() }}
<p>{% for key, value in equipment.items() if key not in equipment['excluded'] %}
<!-- &nbsp;&nbsp;&nbsp;&nbsp;<b>{{ key | replace("_", " ") | title }}: </b>{% if permission and key in reagent['editable']%}<input type={% if key=='expiry' %}"date"{% else %}"text"{% endif %} id="{{ key }}" name="{{ key }}" value="{{ value }}">{% else %}{{ value }}{% endif %}<br>-->
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ key | replace("_", " ") | title }}: </b>{{ value }}<br>
{% endfor %}</p>
<!-- {% if permission %}-->
<!-- <button type="button" id="save_btn">Save</button>-->
<!-- {% endif %}-->
{% if equipment['submissions'] %}<h2>Submissions:</h2>
{% for submission in equipment['submissions'] %}
<p><b><a class="data-link" id="{{ submission['plate'] }}">{{ submission['plate'] }}:</a></b> <a class="data-link process" id="{{ submission['process'] }}">{{ submission['process'] }}</a></p>
{% endfor %}
{% endif %}
{% endblock %}
</body>
<script>
{% block script %}
{{ super() }}
var processSelection = document.getElementsByClassName('process');
for(let i = 0; i < processSelection.length; i++) {
processSelection[i].addEventListener("click", function() {
console.log(processSelection[i].id);
backend.process_details(processSelection[i].id);
})
}
{% for submission in equipment['submissions'] %}
document.getElementById("{{ submission }}").addEventListener("click", function(){
backend.submission_details("{{ submission }}");
});
{% endfor %}
document.addEventListener('DOMContentLoaded', function() {
backend.activate_export(false);
}, false);
{% endblock %}
</script>
</html>

View File

@@ -0,0 +1,49 @@
{% extends "details.html" %}
<head>
{% block head %}
{{ super() }}
<title>Process Details for {{ process['name'] }}</title>
{% endblock %}
</head>
<body>
{% block body %}
<h2><u>Process Details for {{ process['name'] }}</u></h2>
{{ super() }}
<p>{% for key, value in process.items() if key not in process['excluded'] %}
<!-- &nbsp;&nbsp;&nbsp;&nbsp;<b>{{ key | replace("_", " ") | title }}: </b>{% if permission and key in reagent['editable']%}<input type={% if key=='expiry' %}"date"{% else %}"text"{% endif %} id="{{ key }}" name="{{ key }}" value="{{ value }}">{% else %}{{ value }}{% endif %}<br>-->
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ key | replace("_", " ") | title }}: </b>{{ value }}<br>
{% endfor %}</p>
<!-- {% if permission %}-->
<!-- <button type="button" id="save_btn">Save</button>-->
<!-- {% endif %}-->
{% if process['submissions'] %}<h2>Submissions:</h2>
{% for submission in process['submissions'] %}
<p><b><a class="data-link" id="{{ submission['plate'] }}">{{ submission['plate'] }}:</a></b><a class="data-link equipment" id="{{ submission['equipment'] }}">{{ submission['equipment'] }}</a></p>
{% endfor %}
{% endif %}
{% endblock %}
</body>
<script>
{% block script %}
{{ super() }}
var equipmentSelection = document.getElementsByClassName('equipment');
for(let i = 0; i < equipmentSelection.length; i++) {
equipmentSelection[i].addEventListener("click", function() {
console.log(equipmentSelection[i].id);
backend.equipment_details(equipmentSelection[i].id);
})
}
{% for submission in process['submissions'] %}
document.getElementById("{{ submission['plate'] }}").addEventListener("click", function(){
backend.submission_details("{{ submission['plate'] }}");
});
{% endfor %}
document.addEventListener('DOMContentLoaded', function() {
backend.activate_export(false);
}, false);
{% endblock %}
</script>
</html>

View File

@@ -0,0 +1,50 @@
{% extends "details.html" %}
<head>
{% block head %}
{{ super() }}
<title>Tips Details for {{ tips['name'] }}</title>
{% endblock %}
</head>
<body>
{% block body %}
<h2><u>Tips Details for {{ tips['name'] }}</u></h2>
{{ super() }}
<p>{% for key, value in tips.items() if key not in tips['excluded'] %}
<!-- &nbsp;&nbsp;&nbsp;&nbsp;<b>{{ key | replace("_", " ") | title }}: </b>{% if permission and key in reagent['editable']%}<input type={% if key=='expiry' %}"date"{% else %}"text"{% endif %} id="{{ key }}" name="{{ key }}" value="{{ value }}">{% else %}{{ value }}{% endif %}<br>-->
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ key | replace("_", " ") | title }}: </b>{{ value }}<br>
{% endfor %}</p>
<!-- {% if permission %}-->
<!-- <button type="button" id="save_btn">Save</button>-->
<!-- {% endif %}-->
{% if tips['submissions'] %}<h2>Submissions:</h2>
{% for submission in tips['submissions'] %}
<p><b><a class="data-link" id="{{ submission['plate'] }}">{{ submission['plate'] }}:</a></b> <a class="data-link process" id="{{ submission['role'] }}">{{ submission['role'] }}</a></p>
{% endfor %}
{% endif %}
{% endblock %}
</body>
<script>
{% block script %}
{{ super() }}
var processSelection = document.getElementsByClassName('process');
for(let i = 0; i < processSelection.length; i++) {
processSelection[i].addEventListener("click", function() {
console.log(processSelection[i].id);
backend.process_details(processSelection[i].id);
})
}
{% for submission in tips['submissions'] %}
document.getElementById("{{ submission }}").addEventListener("click", function(){
backend.submission_details("{{ submission }}");
});
{% endfor %}
document.addEventListener('DOMContentLoaded', function() {
backend.activate_export(false);
}, false);
{% endblock %}
</script>
</html>