Prior to merging omni_search.py and sample_search.py
This commit is contained in:
3
TODO.md
3
TODO.md
@@ -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.
|
||||||
|
|||||||
@@ -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]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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'] %}
|
||||||
<b>{{ key | replace("_", " ") | title }}: </b>{{ value }}<br>
|
<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 }}");
|
||||||
|
|||||||
Reference in New Issue
Block a user