Ui interface updates.

This commit is contained in:
lwark
2025-07-04 12:31:55 -05:00
parent 51b193b4db
commit 0472afd9a5
11 changed files with 157 additions and 107 deletions

View File

@@ -13,6 +13,7 @@ from sqlalchemy import Column, INTEGER, String, JSON
from sqlalchemy.ext.associationproxy import AssociationProxy
from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query, Session, InstrumentedAttribute, ColumnProperty
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.exc import ArgumentError
from typing import Any, List, ClassVar
from pathlib import Path
@@ -237,10 +238,10 @@ class BaseClass(Base):
@classmethod
def query_or_create(cls, **kwargs) -> Tuple[Any, bool]:
new = False
allowed = [k for k, v in cls.__dict__.items() if isinstance(v, InstrumentedAttribute)]
allowed = [k for k, v in cls.__dict__.items() if isinstance(v, InstrumentedAttribute) or isinstance(v, hybrid_property)]
# and not isinstance(v.property, _RelationshipDeclared)]
sanitized_kwargs = {k: v for k, v in kwargs.items() if k in allowed}
# logger.debug(f"Sanitized kwargs: {sanitized_kwargs}")
logger.debug(f"Sanitized kwargs: {sanitized_kwargs}")
instance = cls.query(**sanitized_kwargs)
if not instance or isinstance(instance, list):
instance = cls()
@@ -273,7 +274,7 @@ class BaseClass(Base):
return cls.execute_query(**kwargs)
@classmethod
def execute_query(cls, query: Query = None, model=None, limit: int = 0, **kwargs) -> Any | List[Any]:
def execute_query(cls, query: Query = None, model=None, limit: int = 0, offset:int|None=None, **kwargs) -> Any | List[Any]:
"""
Execute sqlalchemy query with relevant defaults.
@@ -291,22 +292,32 @@ class BaseClass(Base):
# logger.debug(f"Model: {model}")
if query is None:
query: Query = cls.__database_session__.query(cls)
else:
logger.debug(f"Incoming query: {query}")
singles = cls.get_default_info('singles')
for k, v in kwargs.items():
logger.info(f"Using key: {k} with value: {v}")
logger.info(f"Using key: {k} with value: {v} against {cls}")
try:
attr = getattr(cls, k)
except (ArgumentError, AttributeError) as e:
logger.error(f"Attribute {k} unavailable due to:\n\t{e}\n.")
continue
# NOTE: account for attrs that use list.
if attr.property.uselist:
try:
check = attr.property.uselist
except AttributeError:
check = False
if check:
logger.debug("Got uselist")
query = query.filter(attr.contains(v))
else:
logger.debug("Single item.")
query = query.filter(attr == v)
except (ArgumentError, AttributeError) as e:
logger.error(f"Attribute {k} unavailable due to:\n\t{e}\nSkipping.")
if k in singles:
logger.warning(f"{k} is in singles. Returning only one value.")
limit = 1
if offset:
query.offset(offset)
with query.session.no_autoflush:
match limit:
case 0:
@@ -476,13 +487,13 @@ class BaseClass(Base):
# logger.debug(f"Attempting to set: {key} to {value}")
if key.startswith("_"):
return super().__setattr__(key, value)
try:
# try:
check = not hasattr(self, key)
except:
return
# except:
# return
if check:
try:
json.dumps(value)
value = json.dumps(value)
except TypeError:
value = str(value)
self._misc_info.update({key: value})
@@ -612,6 +623,7 @@ class BaseClass(Base):
if dlg.exec():
pass
class LogMixin(Base):
tracking_exclusion: ClassVar = ['artic_technician', 'clientsubmissionsampleassociation',
'submission_reagent_associations', 'submission_equipment_associations',

View File

@@ -149,12 +149,14 @@ class ClientSubmission(BaseClass, LogMixin):
pass
# query = query.order_by(cls.submitted_date.desc())
# NOTE: Split query results into pages of size {page_size}
if page_size > 0:
query = query.limit(page_size)
if page_size > 0 and limit == 0:
limit = page_size
page = page - 1
if page is not None:
query = query.offset(page * page_size)
return cls.execute_query(query=query, limit=limit, **kwargs)
offset = page * page_size
else:
offset = None
return cls.execute_query(query=query, limit=limit, offset=offset, **kwargs)
@classmethod
def submissions_to_df(cls, submissiontype: str | None = None, limit: int = 0,
@@ -269,7 +271,9 @@ class ClientSubmission(BaseClass, LogMixin):
try:
assert isinstance(sample, Sample)
except AssertionError:
logger.warning(f"Converting {sample} to sql.")
sample = sample.to_sql()
logger.debug(sample.__dict__)
try:
row = sample._misc_info['row']
except (KeyError, AttributeError):
@@ -278,10 +282,12 @@ class ClientSubmission(BaseClass, LogMixin):
column = sample._misc_info['column']
except KeyError:
column = 0
logger.debug(f"Sample: {sample}")
submission_rank = sample._misc_info['submission_rank']
assoc = ClientSubmissionSampleAssociation(
sample=sample,
submission=self,
submission_rank=sample._misc_info['submission_rank'],
submission_rank=submission_rank,
row=row,
column=column
)
@@ -310,6 +316,7 @@ class ClientSubmission(BaseClass, LogMixin):
for sample in active_samples:
sample = sample.to_sql()
logger.debug(f"Sample: {sample.id}")
if sample not in run.sample:
assoc = run.add_sample(sample)
assoc.save()
else:

View File

@@ -43,9 +43,10 @@ class DefaultParser(object):
"""
self.proceduretype = proceduretype
try:
self._pyd_object = getattr(pydant, f"Pyd{self.__class__.__name__.replace('Parser', '')}")
except AttributeError:
self._pyd_object = pydant.PydResults
self._pyd_object = getattr(pydant, f"Pyd{self.__class__.__name__.replace('Parser', '').replace('Info', '')}")
except AttributeError as e:
logger.error(f"Couldn't get pyd object: Pyd{self.__class__.__name__.replace('Parser', '').replace('Info', '')}")
self._pyd_object = getattr(pydant, self.__class__.pyd_name)
self.workbook = load_workbook(self.filepath, data_only=True)
if not range_dict:
self.range_dict = self.__class__.default_range_dict
@@ -118,7 +119,7 @@ class DefaultTABLEParser(DefaultParser):
if isinstance(key, str):
key = key.lower().replace(" ", "_")
key = re.sub(r"_(\(.*\)|#)", "", key)
logger.debug(f"Row {ii} values: {key}: {value}")
# logger.debug(f"Row {ii} values: {key}: {value}")
output[key] = value
yield output
@@ -126,4 +127,4 @@ class DefaultTABLEParser(DefaultParser):
return [self._pyd_object(**output) for output in self.parsed_info]
from .clientsubmission_parser import *
from backend.excel.parsers.results_parsers.pcr_results_parser import *
from backend.excel.parsers.results_parsers.pcr_results_parser import PCRInfoParser, PCRSampleParser

View File

@@ -74,6 +74,8 @@ class ClientSubmissionInfoParser(DefaultKEYVALUEParser, SubmissionTyperMixin):
Object for retrieving submitter info from "sample list" sheet
"""
pyd_name = "PydClientSubmission"
default_range_dict = [dict(
start_row=2,
end_row=18,
@@ -110,6 +112,8 @@ class ClientSubmissionSampleParser(DefaultTABLEParser, SubmissionTyperMixin):
Object for retrieving submitter samples from "sample list" sheet
"""
pyd_name = "PydSample"
default_range_dict = [dict(
header_row=19,
end_row=115,
@@ -126,7 +130,7 @@ class ClientSubmissionSampleParser(DefaultTABLEParser, SubmissionTyperMixin):
def parsed_info(self) -> Generator[dict, None, None]:
output = super().parsed_info
for ii, sample in enumerate(output):
logger.debug(f"Parsed info sample: {sample}")
# logger.debug(f"Parsed info sample: {sample}")
if isinstance(sample["row"], str) and sample["row"].lower() in ascii_lowercase[0:8]:
try:
sample["row"] = row_keys[sample["row"]]

View File

@@ -290,6 +290,16 @@ class PydSample(PydBaseClass):
value = row_keys[value]
return value
def improved_dict(self, dictionaries: bool = True) -> dict:
output = super().improved_dict(dictionaries=dictionaries)
output['name'] = self.sample_id
del output['sampletype']
return output
def to_sql(self):
sql = super().to_sql()
sql._misc_info["submission_rank"] = self.submission_rank
return sql
class PydTips(BaseModel):
name: str

View File

@@ -90,7 +90,7 @@ class ProcedureCreation(QDialog):
plate_map=self.plate_map,
edit=self.edit
)
with open("web.html", "w") as f:
with open("procedure_creation_rendered.html", "w") as f:
f.write(html)
self.webview.setHtml(html)

View File

@@ -5,11 +5,9 @@ from PyQt6.QtCore import Qt, pyqtSlot
from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QGridLayout
from backend.db.models import ClientSubmission
from backend.validators import PydSample, RSLNamer
from tools import get_application_from_parent, jinja_template_loading
from tools import get_application_from_parent, jinja_template_loading, render_details_template
env = jinja_template_loading()
@@ -24,6 +22,7 @@ class SampleChecker(QDialog):
self.rsl_plate_number = RSLNamer.construct_new_plate_name(clientsubmission.to_dict())
else:
self.rsl_plate_number = clientsubmission
logger.debug(f"RSL Plate number: {self.rsl_plate_number}")
self.samples = samples
self.setWindowTitle(title)
self.app = get_application_from_parent(parent)
@@ -38,22 +37,27 @@ class SampleChecker(QDialog):
# NOTE: Used to maintain javascript functions.
template = env.get_template("sample_checker.html")
template_path = Path(template.environment.loader.__getattribute__("searchpath")[0])
with open(template_path.joinpath("css", "styles.css"), "r") as f:
css = f.read()
# with open(template_path.joinpath("css", "styles.css"), "r") as f:
# css = [f.read()]
try:
samples = self.formatted_list
except AttributeError as e:
logger.error(f"Problem getting sample list: {e}")
samples = []
html = template.render(samples=samples, css=css, rsl_plate_number=self.rsl_plate_number)
# html = template.render(samples=samples, css=css, rsl_plate_number=self.rsl_plate_number)
html = render_details_template(template_name="sample_checker", samples=samples, rsl_plate_number=self.rsl_plate_number)
self.webview.setHtml(html)
self.webview.page().setWebChannel(self.channel)
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
self.layout.addWidget(self.buttonBox, 11, 9, 1, 1, alignment=Qt.AlignmentFlag.AlignRight)
self.setLayout(self.layout)
self.webview.page().setWebChannel(self.channel)
with open("sample_checker_rendered.html", "w") as f:
f.write(html)
logger.debug(f"HTML sample checker written!")
@pyqtSlot(str, str, str)
def text_changed(self, submission_rank: str, key: str, new_value: str):
@@ -65,8 +69,8 @@ class SampleChecker(QDialog):
return
item.__setattr__(key, new_value)
@pyqtSlot(str, bool)
def enable_sample(self, submission_rank: str, enabled: bool):
@pyqtSlot(int, bool)
def enable_sample(self, submission_rank: int, enabled: bool):
logger.debug(f"Name: {submission_rank}, Enabled: {enabled}")
try:
item = next((sample for sample in self.samples if int(submission_rank) == sample.submission_rank))

View File

@@ -325,8 +325,9 @@ class SubmissionsTree(QTreeView):
"""
indexes = self.selectedIndexes()
dicto = next((item.data(1) for item in indexes if item.data(1)))
logger.debug(f"Dicto: {pformat(dicto)}")
query_obj = dicto['item_type'].query(name=dicto['query_str'], limit=1)
logger.debug(query_obj)
logger.debug(f"Querying: {query_obj}")
# NOTE: Convert to data in id column (i.e. column 0)
# id = id.sibling(id.row(), 0).data()
# logger.debug(id.model().query_group_object(id.row()))

View File

@@ -141,6 +141,11 @@ class SubmissionFormContainer(QWidget):
checker = SampleChecker(self, "Sample Checker", self.pydsamples)
if checker.exec():
# logger.debug(pformat(self.pydclientsubmission.sample))
try:
assert isinstance(self.pydclientsubmission, PydClientSubmission)
except AssertionError as e:
logger.error(f"Got wrong type for {self.pydclientsubmission}: {type(self.pydclientsubmission)}")
raise e
self.form = self.pydclientsubmission.to_form(parent=self)
self.form.samples = self.pydsamples
self.layout().addWidget(self.form)

View File

@@ -24,23 +24,23 @@
{% block script %}
{% if not child %}
<script>
var coll = document.getElementsByClassName("collapsible");
var i;
<!--<script>-->
<!--var coll = document.getElementsByClassName("collapsible");-->
<!--var i;-->
for (i = 0; i < coll.length; i++) {
coll[i].addEventListener("click", function() {
this.classList.toggle("active");
var content = this.nextElementSibling;
if (content.style.display === "block") {
content.style.display = "none";
} else {
content.style.display = "block";
}
});
}
</script>
{% endif %}
<!--for (i = 0; i < coll.length; i++) {-->
<!-- coll[i].addEventListener("click", function() {-->
<!-- this.classList.toggle("active");-->
<!-- var content = this.nextElementSibling;-->
<!-- if (content.style.display === "block") {-->
<!-- content.style.display = "none";-->
<!-- } else {-->
<!-- content.style.display = "block";-->
<!-- }-->
<!-- });-->
<!--}-->
<!--</script>-->
<!--{% endif %}-->
{% for j in js%}
<script>

View File

@@ -1,4 +1,5 @@
{% extends "details.html" %}
<html>
<head>
{% block head %}
{{ super() }}
@@ -9,10 +10,9 @@
{% block body %}
<h2><u>Sample Checker</u></h2>
<br>
{% if rsl_plate_num %}
{% if rsl_plate_number %}
<label for="rsl_plate_number">RSL Plate Number:</label><br>
<input type="text" id="rsl_plate_number" name="sample_id" value="{{ rsl_plate_number }}" size="40">
{% endif %}
<br>
<p>Take a moment to verify sample names.</p>
@@ -20,11 +20,9 @@
<form>
&emsp;&emsp;Submitter ID<br/><!--&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;Row&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp; Column<br/>-->
{% for sample in samples %}
{% if rsl_plate_num %}<input type="checkbox" id="{{ sample['submission_rank'] }}_enabled" name="vehicle1" value="Bike" {% if sample['enabled'] %}checked{% endif %}>{% endif %}
{% if rsl_plate_number %}<input type="checkbox" id="{{ sample['submission_rank'] }}_enabled" name="vehicle1" value="Bike" {% if sample['enabled'] %}checked{% endif %}>{% endif %}
{{ '%02d' % sample['submission_rank'] }}
<input type="text" id="{{ sample['submission_rank'] }}_id" name="sample_id" value="{{ sample['sample_id'] }}" size="40" style="color:{{ sample['color'] }};" {% if rsl_plate_num %}disabled{% endif %}>
<!-- <input type="number" id="{{ sample['submission_rank'] }}_row" name="row" value="{{ sample['row'] }}" size="5", min="1">-->
<!-- <input type="number" id="{{ sample['submission_rank'] }}_col" name="column" value="{{ sample['column'] }}" size="5", min="1">-->
<input type="text" id="{{ sample['submission_rank'] }}_id" name="sample_id" value="{{ sample['sample_id'] }}" size="40" style="color:{{ sample['color'] }};" {% if rsl_plate_number %}disabled{% endif %}>
<br/>
{% endfor %}
</form>
@@ -32,23 +30,31 @@
</body>
{% block script %}
{{ super() }}
<script>
{% for sample in samples %}
document.getElementById("{{ sample['submission_rank'] }}_id").addEventListener("input", function(){
backend.text_changed("{{ sample['submission_rank'] }}", this.name, this.value);
});
{% if rsl_plate_num %}
document.getElementById("{{ sample['submission_rank'] }}_enabled").addEventListener("input", function(){
backend.enable_sample("{{ sample['submission_rank'] }}", this.checked);
{% if rsl_plate_number %}
document.getElementById("{{ sample['submission_rank'] }}_enabled").addEventListener("change", function(){
console.log(typeof({{ sample['submission_rank'] }}) + " " + typeof(this.checked));
backend.enable_sample({{ sample['submission_rank'] }}, this.checked);
});
{% endif %}
{% endfor %}
</script>
<script>
document.getElementById("rsl_plate_number").addEventListener("input", function(){
console.log(this.value);
console.log(typeof(this.value));
backend.set_rsl_plate_number(this.value);
});
</script>
<script>
document.addEventListener('DOMContentLoaded', function() {
backend.activate_export(false);
}, false);
document.getElementById("rsl_plate_num").addEventListener("input", function(){
backend.set_rsl_plate_num(this.value);
});
</script>
{{ super() }}
{% endblock %}