Prior to merging omni_search.py and sample_search.py

This commit is contained in:
lwark
2024-11-18 09:53:03 -06:00
parent 3d6a42b36f
commit 506aac80c1
13 changed files with 162 additions and 73 deletions

View File

@@ -1,4 +1,5 @@
- [ ] Allow parsing of custom fields to a json 'custom' field in _basicsubmissions - [ ] Find a way to merge omni_search and sample_search
- [x] Allow parsing of custom fields to a json 'custom' field in _basicsubmissions
- [x] Upgrade to generators when returning lists. - [x] Upgrade to generators when returning lists.
- [x] Revamp frontend.widgets.controls_chart to include visualizations? - [x] Revamp frontend.widgets.controls_chart to include visualizations?
- [x] Convert Parsers to using openpyxl. - [x] Convert Parsers to using openpyxl.

View File

@@ -3,6 +3,8 @@ Contains all models for sqlalchemy
""" """
from __future__ import annotations from __future__ import annotations
import sys, logging import sys, logging
import pandas as pd
from sqlalchemy import Column, INTEGER, String, JSON from sqlalchemy import Column, INTEGER, String, JSON
from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query, Session from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query, Session
from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.ext.declarative import declared_attr
@@ -97,6 +99,26 @@ class BaseClass(Base):
singles = list(set(cls.singles + BaseClass.singles)) singles = list(set(cls.singles + BaseClass.singles))
return dict(singles=singles) return dict(singles=singles)
@classmethod
def fuzzy_search(cls, **kwargs):
query: Query = cls.__database_session__.query(cls)
# logger.debug(f"Queried model. Now running searches in {kwargs}")
for k, v in kwargs.items():
# logger.debug(f"Running fuzzy search for attribute: {k} with value {v}")
search = f"%{v}%"
try:
attr = getattr(cls, k)
# NOTE: the secret sauce is in attr.like
query = query.filter(attr.like(search))
except (ArgumentError, AttributeError) as e:
logger.error(f"Attribute {k} unavailable due to:\n\t{e}\nSkipping.")
return query.limit(50).all()
@classmethod
def results_to_df(cls, objects: list, **kwargs):
records = [object.to_sub_dict(**kwargs) for object in objects]
return pd.DataFrame.from_records(records)
@classmethod @classmethod
def query(cls, **kwargs) -> Any | List[Any]: def query(cls, **kwargs) -> Any | List[Any]:
""" """

View File

@@ -7,8 +7,8 @@ from pprint import pformat
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB
from sqlalchemy.orm import relationship, validates, Query from sqlalchemy.orm import relationship, validates, Query
from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.associationproxy import association_proxy
from datetime import date from datetime import date, datetime
from tools import check_authorization, setup_lookup, Report, Result, check_regex_match, yaml_regex_creator from tools import check_authorization, setup_lookup, Report, Result, check_regex_match, yaml_regex_creator, timezone
from typing import List, Literal, Generator, Any from typing import List, Literal, Generator, Any
from pandas import ExcelFile from pandas import ExcelFile
from pathlib import Path from pathlib import Path
@@ -355,7 +355,7 @@ class ReagentRole(BaseClass):
match reagent: match reagent:
case str(): case str():
# logger.debug(f"Lookup ReagentType by reagent str {reagent}") # logger.debug(f"Lookup ReagentType by reagent str {reagent}")
reagent = Reagent.query(lot_number=reagent) reagent = Reagent.query(lot=reagent)
case _: case _:
pass pass
assert reagent.role assert reagent.role
@@ -405,6 +405,8 @@ class Reagent(BaseClass):
Concrete reagent instance Concrete reagent instance
""" """
searchables = ["lot"]
id = Column(INTEGER, primary_key=True) #: primary key id = Column(INTEGER, primary_key=True) #: primary key
role = relationship("ReagentRole", back_populates="instances", role = relationship("ReagentRole", back_populates="instances",
secondary=reagentroles_reagents) #: joined parent reagent type secondary=reagentroles_reagents) #: joined parent reagent type
@@ -430,7 +432,7 @@ class Reagent(BaseClass):
else: else:
return f"<Reagent({self.role.name}-{self.lot})>" return f"<Reagent({self.role.name}-{self.lot})>"
def to_sub_dict(self, extraction_kit: KitType = None, full_data: bool = False) -> dict: def to_sub_dict(self, extraction_kit: KitType = None, full_data: bool = False, **kwargs) -> dict:
""" """
dictionary containing values necessary for gui dictionary containing values necessary for gui
@@ -441,6 +443,7 @@ class Reagent(BaseClass):
Returns: Returns:
dict: representation of the reagent's attributes dict: representation of the reagent's attributes
""" """
if extraction_kit is not None: if extraction_kit is not None:
# NOTE: Get the intersection of this reagent's ReagentType and all ReagentTypes in KitType # NOTE: Get the intersection of this reagent's ReagentType and all ReagentTypes in KitType
try: try:
@@ -449,7 +452,10 @@ class Reagent(BaseClass):
except: except:
reagent_role = self.role[0] reagent_role = self.role[0]
else: else:
reagent_role = self.role[0] try:
reagent_role = self.role[0]
except IndexError:
reagent_role = None
try: try:
rtype = reagent_role.name.replace("_", " ") rtype = reagent_role.name.replace("_", " ")
except AttributeError: except AttributeError:
@@ -475,7 +481,8 @@ class Reagent(BaseClass):
) )
if full_data: if full_data:
output['submissions'] = [sub.rsl_plate_num for sub in self.submissions] output['submissions'] = [sub.rsl_plate_num for sub in self.submissions]
output['excluded'] = ['missing', 'submissions', 'excluded'] output['excluded'] = ['missing', 'submissions', 'excluded', 'editable']
output['editable'] = ['lot', 'expiry']
return output return output
def update_last_used(self, kit: KitType) -> Report: def update_last_used(self, kit: KitType) -> Report:
@@ -508,8 +515,8 @@ class Reagent(BaseClass):
@setup_lookup @setup_lookup
def query(cls, def query(cls,
id: int | None = None, id: int | None = None,
reagent_role: str | ReagentRole | None = None, role: str | ReagentRole | None = None,
lot_number: str | None = None, lot: str | None = None,
name: str | None = None, name: str | None = None,
limit: int = 0 limit: int = 0
) -> Reagent | List[Reagent]: ) -> Reagent | List[Reagent]:
@@ -533,13 +540,13 @@ class Reagent(BaseClass):
limit = 1 limit = 1
case _: case _:
pass pass
match reagent_role: match role:
case str(): case str():
# logger.debug(f"Looking up reagents by reagent type str: {reagent_type}") # logger.debug(f"Looking up reagents by reagent type str: {reagent_type}")
query = query.join(cls.role).filter(ReagentRole.name == reagent_role) query = query.join(cls.role).filter(ReagentRole.name == role)
case ReagentRole(): case ReagentRole():
# logger.debug(f"Looking up reagents by reagent type ReagentType: {reagent_type}") # logger.debug(f"Looking up reagents by reagent type ReagentType: {reagent_type}")
query = query.filter(cls.role.contains(reagent_role)) query = query.filter(cls.role.contains(role))
case _: case _:
pass pass
match name: match name:
@@ -549,16 +556,43 @@ class Reagent(BaseClass):
query = query.filter(cls.name == name) query = query.filter(cls.name == name)
case _: case _:
pass pass
match lot_number: match lot:
case str(): case str():
# logger.debug(f"Looking up reagent by lot number str: {lot_number}") # logger.debug(f"Looking up reagent by lot number str: {lot}")
query = query.filter(cls.lot == lot_number) query = query.filter(cls.lot == lot)
# NOTE: In this case limit number returned. # NOTE: In this case limit number returned.
limit = 1 limit = 1
case _: case _:
pass pass
return cls.execute_query(query=query, limit=limit) return cls.execute_query(query=query, limit=limit)
@check_authorization
def edit_from_search(self, **kwargs):
from frontend.widgets.misc import AddReagentForm
role = ReagentRole.query(kwargs['role'])
if role:
role_name = role.name
else:
role_name = None
dlg = AddReagentForm(reagent_lot=self.lot, reagent_role=role_name, expiry=self.expiry, reagent_name=self.name)
if dlg.exec():
vars = dlg.parse_form()
for key, value in vars.items():
match key:
case "expiry":
if not isinstance(value, date):
field_value = datetime.strptime(value, "%Y-%m-%d").date
field_value.replace(tzinfo=timezone)
else:
field_value = value
case "role":
continue
case _:
field_value = value
logger.debug(f"Setting reagent {key} to {field_value}")
self.__setattr__(key, field_value)
self.save()
class Discount(BaseClass): class Discount(BaseClass):
""" """
@@ -1278,7 +1312,7 @@ class SubmissionReagentAssociation(BaseClass):
case Reagent() | str(): case Reagent() | str():
# logger.debug(f"Lookup SubmissionReagentAssociation by reagent Reagent {reagent}") # logger.debug(f"Lookup SubmissionReagentAssociation by reagent Reagent {reagent}")
if isinstance(reagent, str): if isinstance(reagent, str):
reagent = Reagent.query(lot_number=reagent) reagent = Reagent.query(lot=reagent)
query = query.filter(cls.reagent == reagent) query = query.filter(cls.reagent == reagent)
case _: case _:
pass pass

View File

@@ -844,7 +844,8 @@ class BasicSubmission(BaseClass):
ws.cell(row=item['row'], column=item['column'], value=item['value']) ws.cell(row=item['row'], column=item['column'], value=item['value'])
return input_excel return input_excel
def custom_sample_writer(self, sample:dict) -> dict: @classmethod
def custom_sample_writer(self, sample: dict) -> dict:
return sample return sample
@classmethod @classmethod
@@ -2091,7 +2092,7 @@ class WastewaterArtic(BasicSubmission):
return input_excel return input_excel
@classmethod @classmethod
def custom_sample_writer(self, sample:dict) -> dict: def custom_sample_writer(self, sample: dict) -> dict:
logger.debug("Wastewater Artic custom sample writer") logger.debug("Wastewater Artic custom sample writer")
if sample['source_plate_number'] in [0, "0"]: if sample['source_plate_number'] in [0, "0"]:
sample['source_plate_number'] = "control" sample['source_plate_number'] = "control"

View File

@@ -48,7 +48,7 @@ class PydReagent(BaseModel):
def rescue_type_with_lookup(cls, value, values): def rescue_type_with_lookup(cls, value, values):
if value is None and values.data['lot'] is not None: if value is None and values.data['lot'] is not None:
try: try:
return Reagent.query(lot_number=values.data['lot'].name) return Reagent.query(lot=values.data['lot'].name)
except AttributeError: except AttributeError:
return value return value
return value return value
@@ -127,7 +127,7 @@ class PydReagent(BaseModel):
if self.model_extra is not None: if self.model_extra is not None:
self.__dict__.update(self.model_extra) self.__dict__.update(self.model_extra)
# logger.debug(f"Reagent SQL constructor is looking up type: {self.type}, lot: {self.lot}") # logger.debug(f"Reagent SQL constructor is looking up type: {self.type}, lot: {self.lot}")
reagent = Reagent.query(lot_number=self.lot, name=self.name) reagent = Reagent.query(lot=self.lot, name=self.name)
# logger.debug(f"Result: {reagent}") # logger.debug(f"Result: {reagent}")
if reagent is None: if reagent is None:
reagent = Reagent() reagent = Reagent()

View File

@@ -13,7 +13,7 @@ from PyQt6.QtGui import QAction
from pathlib import Path from pathlib import Path
from markdown import markdown from markdown import markdown
from __init__ import project_path from __init__ import project_path
from backend import SubmissionType from backend import SubmissionType, Reagent
from tools import check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size from tools import check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size
from .functions import select_save_file, select_open_file from .functions import select_save_file, select_open_file
from datetime import date from datetime import date
@@ -23,8 +23,9 @@ import logging, webbrowser, sys, shutil
from .submission_table import SubmissionsSheet from .submission_table import SubmissionsSheet
from .submission_widget import SubmissionFormContainer from .submission_widget import SubmissionFormContainer
from .controls_chart import ControlsViewer from .controls_chart import ControlsViewer
from .sample_search import SearchBox from .sample_search import SampleSearchBox
from .summary import Summary from .summary import Summary
from .omni_search import SearchBox
logger = logging.getLogger(f'submissions.{__name__}') logger = logging.getLogger(f'submissions.{__name__}')
logger.info("Hello, I am a logger") logger.info("Hello, I am a logger")
@@ -185,7 +186,7 @@ class App(QMainWindow):
""" """
Create a search for samples. Create a search for samples.
""" """
dlg = SearchBox(self) dlg = SampleSearchBox(self)
dlg.exec() dlg.exec()
def backup_database(self): def backup_database(self):
@@ -226,7 +227,7 @@ class App(QMainWindow):
@check_authorization @check_authorization
def edit_reagent(self, *args, **kwargs): def edit_reagent(self, *args, **kwargs):
dlg = EditReagent() dlg = SearchBox(parent=self, object_type=Reagent, extras=['role'])
dlg.exec() dlg.exec()
@check_authorization @check_authorization

View File

@@ -30,8 +30,8 @@ class AddReagentForm(QDialog):
def __init__(self, reagent_lot: str | None = None, reagent_role: str | None = None, expiry: date | None = None, def __init__(self, reagent_lot: str | None = None, reagent_role: str | None = None, expiry: date | None = None,
reagent_name: str | None = None) -> None: reagent_name: str | None = None) -> None:
super().__init__() super().__init__()
if reagent_lot is None: if reagent_name is None:
reagent_lot = reagent_role reagent_name = reagent_role
self.setWindowTitle("Add Reagent") self.setWindowTitle("Add Reagent")
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn) self.buttonBox = QDialogButtonBox(QBtn)

View File

@@ -2,14 +2,13 @@
Search box that performs fuzzy search for samples Search box that performs fuzzy search for samples
''' '''
from pprint import pformat from pprint import pformat
from typing import Tuple from typing import Tuple, Any, List
from pandas import DataFrame from pandas import DataFrame
from PyQt6.QtCore import QSortFilterProxyModel from PyQt6.QtCore import QSortFilterProxyModel
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QLabel, QVBoxLayout, QDialog, QLabel, QVBoxLayout, QDialog,
QComboBox, QTableView, QWidget, QLineEdit, QGridLayout QTableView, QWidget, QLineEdit, QGridLayout, QComboBox
) )
from backend.db.models import BasicSample
from .submission_table import pandasModel from .submission_table import pandasModel
import logging import logging
@@ -18,21 +17,22 @@ logger = logging.getLogger(f"submissions.{__name__}")
class SearchBox(QDialog): class SearchBox(QDialog):
def __init__(self, parent): def __init__(self, parent, object_type: Any, extras: List[str], **kwargs):
super().__init__(parent) super().__init__(parent)
self.layout = QGridLayout(self) self.object_type = object_type
self.sample_type = QComboBox(self) # options = ["Any"] + [cls.__name__ for cls in self.object_type.__subclasses__()]
self.sample_type.setObjectName("sample_type") # self.sub_class = QComboBox(self)
self.sample_type.currentTextChanged.connect(self.update_widgets) # self.sub_class.setObjectName("sub_class")
options = ["Any"] + [cls.__mapper_args__['polymorphic_identity'] for cls in BasicSample.__subclasses__()] # self.sub_class.currentTextChanged.connect(self.update_widgets)
self.sample_type.addItems(options) # self.sub_class.addItems(options)
self.sample_type.setEditable(False) # self.sub_class.setEditable(False)
self.setMinimumSize(600, 600) self.setMinimumSize(600, 600)
self.sample_type.setMinimumWidth(self.minimumWidth()) # self.sub_class.setMinimumWidth(self.minimumWidth())
self.layout.addWidget(self.sample_type, 0, 0) # self.layout.addWidget(self.sub_class, 0, 0)
self.results = SearchResults() self.results = SearchResults(parent=self, object_type=self.object_type, extras=extras, **kwargs)
self.layout.addWidget(self.results, 5, 0) self.layout.addWidget(self.results, 5, 0)
self.setLayout(self.layout) self.setLayout(self.layout)
self.setWindowTitle(f"Search {self.object_type.__name__}")
self.update_widgets() self.update_widgets()
self.update_data() self.update_data()
@@ -40,21 +40,10 @@ class SearchBox(QDialog):
""" """
Changes form inputs based on sample type Changes form inputs based on sample type
""" """
deletes = [item for item in self.findChildren(FieldSearch)] for iii, searchable in enumerate(self.object_type.searchables):
# logger.debug(deletes) self.widget = FieldSearch(parent=self, label=searchable, field_name=searchable)
for item in deletes: self.layout.addWidget(self.widget, 1, 0)
item.setParent(None) self.widget.search_widget.textChanged.connect(self.update_data)
if self.sample_type.currentText() == "Any":
self.type = BasicSample
else:
self.type = BasicSample.find_polymorphic_subclass(self.sample_type.currentText())
# logger.debug(f"Sample type: {self.type}")
searchables = self.type.get_searchables()
start_row = 1
for iii, item in enumerate(searchables):
widget = FieldSearch(parent=self, label=item['label'], field_name=item['field'])
self.layout.addWidget(widget, start_row + iii, 0)
widget.search_widget.textChanged.connect(self.update_data)
self.update_data() self.update_data()
def parse_form(self) -> dict: def parse_form(self) -> dict:
@@ -74,9 +63,8 @@ class SearchBox(QDialog):
# logger.debug(f"Running update_data with sample type: {self.type}") # logger.debug(f"Running update_data with sample type: {self.type}")
fields = self.parse_form() fields = self.parse_form()
# logger.debug(f"Got fields: {fields}") # logger.debug(f"Got fields: {fields}")
sample_list_creator = self.type.fuzzy_search(**fields) sample_list_creator = self.object_type.fuzzy_search(**fields)
data = self.type.samples_to_df(sample_list=sample_list_creator) data = self.object_type.results_to_df(objects=sample_list_creator)
# logger.debug(f"Data: {data}")
self.results.setData(df=data) self.results.setData(df=data)
@@ -108,21 +96,43 @@ class FieldSearch(QWidget):
class SearchResults(QTableView): class SearchResults(QTableView):
def __init__(self): def __init__(self, parent: SearchBox, object_type: Any, extras: List[str], **kwargs):
super().__init__() super().__init__()
self.doubleClicked.connect( self.context = kwargs
lambda x: BasicSample.query(submitter_id=x.sibling(x.row(), 0).data()).show_details(self)) self.parent = parent
self.object_type = object_type
self.extras = extras + self.object_type.searchables
def setData(self, df: DataFrame) -> None: def setData(self, df: DataFrame) -> None:
""" """
sets data in model sets data in model
""" """
self.data = df self.data = df
print(self.data)
try:
self.columns_of_interest = [dict(name=item, column=self.data.columns.get_loc(item)) for item in self.extras]
except KeyError:
self.columns_of_interest = []
try: try:
self.data['id'] = self.data['id'].apply(str) self.data['id'] = self.data['id'].apply(str)
self.data['id'] = self.data['id'].str.zfill(3) self.data['id'] = self.data['id'].str.zfill(3)
except (TypeError, KeyError): except (TypeError, KeyError) as e:
logger.error("Couldn't format id string.") logger.error(f"Couldn't format id string: {e}")
proxy_model = QSortFilterProxyModel() proxy_model = QSortFilterProxyModel()
proxy_model.setSourceModel(pandasModel(self.data)) proxy_model.setSourceModel(pandasModel(self.data))
self.setModel(proxy_model) self.setModel(proxy_model)
self.doubleClicked.connect(self.parse_row)
def parse_row(self, x):
context = {item['name']: x.sibling(x.row(), item['column']).data() for item in self.columns_of_interest}
try:
object = self.object_type.query(**{self.object_type.search: context[self.object_type.search]})
except KeyError:
object = None
try:
object.edit_from_search(**context)
except AttributeError:
pass
self.doubleClicked.disconnect()
self.parent.update_data()

View File

@@ -16,7 +16,7 @@ import logging
logger = logging.getLogger(f"submissions.{__name__}") logger = logging.getLogger(f"submissions.{__name__}")
class SearchBox(QDialog): class SampleSearchBox(QDialog):
def __init__(self, parent): def __init__(self, parent):
super().__init__(parent) super().__init__(parent)

View File

@@ -106,10 +106,10 @@ class SubmissionDetails(QDialog):
@pyqtSlot(str, str) @pyqtSlot(str, str)
def reagent_details(self, reagent: str | Reagent, kit: str | KitType): def reagent_details(self, reagent: str | Reagent, kit: str | KitType):
if isinstance(reagent, str): if isinstance(reagent, str):
reagent = Reagent.query(lot_number=reagent) reagent = Reagent.query(lot=reagent)
if isinstance(kit, str): if isinstance(kit, str):
kit = KitType.query(name=kit) self.kit = KitType.query(name=kit)
base_dict = reagent.to_sub_dict(extraction_kit=kit, full_data=True) base_dict = reagent.to_sub_dict(extraction_kit=self.kit, full_data=True)
env = jinja_template_loading() env = jinja_template_loading()
temp_name = "reagent_details.html" temp_name = "reagent_details.html"
# logger.debug(f"Returning template: {temp_name}") # logger.debug(f"Returning template: {temp_name}")
@@ -121,10 +121,22 @@ class SubmissionDetails(QDialog):
template_path = Path(self.template.environment.loader.__getattribute__("searchpath")[0]) template_path = Path(self.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()
html = template.render(reagent=base_dict, css=css) html = template.render(reagent=base_dict, permission=is_power_user(), css=css)
self.webview.setHtml(html) self.webview.setHtml(html)
self.setWindowTitle(f"Reagent Details - {reagent.name} - {reagent.lot}") self.setWindowTitle(f"Reagent Details - {reagent.name} - {reagent.lot}")
@pyqtSlot(str, str, str)
def update_reagent(self, old_lot: str, new_lot: str, expiry: str):
expiry = datetime.strptime(expiry, "%Y-%m-%d")
reagent = Reagent.query(lot=old_lot)
if reagent:
reagent.lot = new_lot
reagent.expiry = expiry
reagent.save()
self.reagent_details(reagent=reagent, kit=self.kit)
else:
logger.error(f"Reagent with lot {old_lot} not found.")
@pyqtSlot(str) @pyqtSlot(str)
def submission_details(self, submission: str | BasicSubmission): def submission_details(self, submission: str | BasicSubmission):
""" """
@@ -150,7 +162,7 @@ class SubmissionDetails(QDialog):
css = f.read() css = f.read()
# logger.debug(f"Submission_details: {pformat(self.base_dict)}") # logger.debug(f"Submission_details: {pformat(self.base_dict)}")
# logger.debug(f"User is power user: {is_power_user()}") # logger.debug(f"User is power user: {is_power_user()}")
self.html = self.template.render(sub=self.base_dict, signing_permission=is_power_user(), css=css) self.html = self.template.render(sub=self.base_dict, permission=is_power_user(), css=css)
self.webview.setHtml(self.html) self.webview.setHtml(self.html)
@pyqtSlot(str) @pyqtSlot(str)

View File

@@ -675,7 +675,7 @@ class SubmissionFormWidget(QWidget):
report = Report() report = Report()
lot = self.lot.currentText() lot = self.lot.currentText()
# logger.debug(f"Using this lot for the reagent {self.reagent}: {lot}") # logger.debug(f"Using this lot for the reagent {self.reagent}: {lot}")
wanted_reagent = Reagent.query(lot_number=lot, reagent_role=self.reagent.role) wanted_reagent = Reagent.query(lot=lot, role=self.reagent.role)
# NOTE: if reagent doesn't exist in database, offer to add it (uses App.add_reagent) # NOTE: if reagent doesn't exist in database, offer to add it (uses App.add_reagent)
if wanted_reagent is None: if wanted_reagent is None:
dlg = QuestionAsker(title=f"Add {lot}?", dlg = QuestionAsker(title=f"Add {lot}?",
@@ -745,7 +745,7 @@ class SubmissionFormWidget(QWidget):
relevant_reagents.insert(0, str(reagent.lot)) relevant_reagents.insert(0, str(reagent.lot))
else: else:
try: try:
looked_up_reg = Reagent.query(lot_number=looked_up_rt.last_used) looked_up_reg = Reagent.query(lot=looked_up_rt.last_used)
except AttributeError: except AttributeError:
looked_up_reg = None looked_up_reg = None
if isinstance(looked_up_reg, list): if isinstance(looked_up_reg, list):

View File

@@ -71,7 +71,7 @@
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block signing_button %} {% block signing_button %}
{% if signing_permission %} {% if permission %}
<button type="button" id="sign_btn">Sign Off</button> <button type="button" id="sign_btn">Sign Off</button>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@@ -10,8 +10,11 @@
<h2><u>Reagent Details for {{ reagent['name'] }} - {{ reagent['lot'] }}</u></h2> <h2><u>Reagent Details for {{ reagent['name'] }} - {{ reagent['lot'] }}</u></h2>
{{ super() }} {{ super() }}
<p>{% for key, value in reagent.items() if key not in reagent['excluded'] %} <p>{% for key, value in reagent.items() if key not in reagent['excluded'] %}
&nbsp;&nbsp;&nbsp;&nbsp;<b>{{ key | replace("_", " ") | title }}: </b>{{ value }}<br> &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>
{% endfor %}</p> {% endfor %}</p>
{% if permission %}
<button type="button" id="save_btn">Save</button>
{% endif %}
{% if reagent['submissions'] %}<h2>Submissions:</h2> {% if reagent['submissions'] %}<h2>Submissions:</h2>
{% for submission in reagent['submissions'] %} {% for submission in reagent['submissions'] %}
<p><b><a class="data-link" id="{{ submission }}">{{ submission }}:</a></b> {{ reagent['role'] }}</p> <p><b><a class="data-link" id="{{ submission }}">{{ submission }}:</a></b> {{ reagent['role'] }}</p>
@@ -22,6 +25,11 @@
<script> <script>
{% block script %} {% block script %}
{{ super() }} {{ super() }}
document.getElementById("save_btn").addEventListener("click", function(){
var new_lot = document.getElementById('lot').value
var new_exp = document.getElementById('expiry').value
backend.update_reagent("{{ reagent['lot'] }}", new_lot, new_exp);
});
{% for submission in reagent['submissions'] %} {% for submission in reagent['submissions'] %}
document.getElementById("{{ submission }}").addEventListener("click", function(){ document.getElementById("{{ submission }}").addEventListener("click", function(){
backend.submission_details("{{ submission }}"); backend.submission_details("{{ submission }}");