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

View File

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

View File

@@ -43,9 +43,10 @@ class DefaultParser(object):
""" """
self.proceduretype = proceduretype self.proceduretype = proceduretype
try: try:
self._pyd_object = getattr(pydant, f"Pyd{self.__class__.__name__.replace('Parser', '')}") self._pyd_object = getattr(pydant, f"Pyd{self.__class__.__name__.replace('Parser', '').replace('Info', '')}")
except AttributeError: except AttributeError as e:
self._pyd_object = pydant.PydResults 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) self.workbook = load_workbook(self.filepath, data_only=True)
if not range_dict: if not range_dict:
self.range_dict = self.__class__.default_range_dict self.range_dict = self.__class__.default_range_dict
@@ -118,7 +119,7 @@ class DefaultTABLEParser(DefaultParser):
if isinstance(key, str): if isinstance(key, str):
key = key.lower().replace(" ", "_") key = key.lower().replace(" ", "_")
key = re.sub(r"_(\(.*\)|#)", "", key) key = re.sub(r"_(\(.*\)|#)", "", key)
logger.debug(f"Row {ii} values: {key}: {value}") # logger.debug(f"Row {ii} values: {key}: {value}")
output[key] = value output[key] = value
yield output yield output
@@ -126,4 +127,4 @@ class DefaultTABLEParser(DefaultParser):
return [self._pyd_object(**output) for output in self.parsed_info] return [self._pyd_object(**output) for output in self.parsed_info]
from .clientsubmission_parser import * 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 Object for retrieving submitter info from "sample list" sheet
""" """
pyd_name = "PydClientSubmission"
default_range_dict = [dict( default_range_dict = [dict(
start_row=2, start_row=2,
end_row=18, end_row=18,
@@ -110,6 +112,8 @@ class ClientSubmissionSampleParser(DefaultTABLEParser, SubmissionTyperMixin):
Object for retrieving submitter samples from "sample list" sheet Object for retrieving submitter samples from "sample list" sheet
""" """
pyd_name = "PydSample"
default_range_dict = [dict( default_range_dict = [dict(
header_row=19, header_row=19,
end_row=115, end_row=115,
@@ -126,7 +130,7 @@ class ClientSubmissionSampleParser(DefaultTABLEParser, SubmissionTyperMixin):
def parsed_info(self) -> Generator[dict, None, None]: def parsed_info(self) -> Generator[dict, None, None]:
output = super().parsed_info output = super().parsed_info
for ii, sample in enumerate(output): 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]: if isinstance(sample["row"], str) and sample["row"].lower() in ascii_lowercase[0:8]:
try: try:
sample["row"] = row_keys[sample["row"]] sample["row"] = row_keys[sample["row"]]

View File

@@ -290,6 +290,16 @@ class PydSample(PydBaseClass):
value = row_keys[value] value = row_keys[value]
return 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): class PydTips(BaseModel):
name: str name: str

View File

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

View File

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

View File

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

View File

@@ -1,54 +1,60 @@
{% extends "details.html" %} {% extends "details.html" %}
<head> <html>
{% block head %} <head>
{{ super() }} {% block head %}
<title>Sample Checker</title> {{ super() }}
{% endblock %} <title>Sample Checker</title>
</head> {% endblock %}
<body> </head>
{% block body %} <body>
<h2><u>Sample Checker</u></h2> {% block body %}
<br> <h2><u>Sample Checker</u></h2>
{% if rsl_plate_num %} <br>
<label for="rsl_plate_number">RSL Plate Number:</label><br> {% if rsl_plate_number %}
<input type="text" id="rsl_plate_number" name="sample_id" value="{{ rsl_plate_number }}" size="40"> <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>
<br>
<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_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_number %}disabled{% endif %}>
<br/>
{% endfor %}
</form>
{% endblock %}
</body>
{% endif %} {% block script %}
<br>
<p>Take a moment to verify sample names.</p>
<br>
<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 %}
{{ '%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">-->
<br/>
{% endfor %}
</form>
{% endblock %}
</body>
{% block script %} <script>
{{ super() }} {% for sample in samples %}
<script> document.getElementById("{{ sample['submission_rank'] }}_id").addEventListener("input", function(){
{% for sample in samples %} backend.text_changed("{{ sample['submission_rank'] }}", this.name, this.value);
document.getElementById("{{ sample['submission_rank'] }}_id").addEventListener("input", function(){ });
backend.text_changed("{{ sample['submission_rank'] }}", this.name, this.value); {% if rsl_plate_number %}
}); document.getElementById("{{ sample['submission_rank'] }}_enabled").addEventListener("change", function(){
{% if rsl_plate_num %} console.log(typeof({{ sample['submission_rank'] }}) + " " + typeof(this.checked));
document.getElementById("{{ sample['submission_rank'] }}_enabled").addEventListener("input", function(){ backend.enable_sample({{ sample['submission_rank'] }}, this.checked);
backend.enable_sample("{{ sample['submission_rank'] }}", this.checked); });
}); {% endif %}
{% endif %} {% endfor %}
{% endfor %} </script>
document.addEventListener('DOMContentLoaded', function() { <script>
backend.activate_export(false); document.getElementById("rsl_plate_number").addEventListener("input", function(){
}, false); console.log(this.value);
document.getElementById("rsl_plate_num").addEventListener("input", function(){ console.log(typeof(this.value));
backend.set_rsl_plate_num(this.value); backend.set_rsl_plate_number(this.value);
}); });
</script> </script>
{% endblock %} <script>
document.addEventListener('DOMContentLoaded', function() {
backend.activate_export(false);
}, false);
</script>
{{ super() }}
{% endblock %}