Mid clean-up.

This commit is contained in:
lwark
2025-01-10 13:49:24 -06:00
parent d93da3c90c
commit 5cded949ed
12 changed files with 178 additions and 90 deletions

View File

@@ -1,4 +1,6 @@
- [ ] Find a way to merge AddEdit with ReagentAdder
- [ ] Stop displacing date on Irida controls and just do what Turnaround time does.
- [ ] Get Manager window working for KitType, maybe SubmissionType
- [x] Find a way to merge AddEdit with ReagentAdder
- [x] 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.
@@ -13,7 +15,7 @@
- [x] Fix Artic RSLNamer
- [x] Put "Not applicable" reagents in to_dict() method.
- Currently in to_pydantic().
- [x] Critical: Convert Json lits to dicts so I can have them update properly without using crashy Sqlalchemy-json
- [x] Critical: Convert Json list to dicts so I can have them update properly without using crashy Sqlalchemy-json
- Was actually not necessary.
- [x] Fix Parsed/Missing mix ups.
- [x] Have sample parser check for controls and add to reagents?

View File

@@ -4,6 +4,7 @@ Contains all models for sqlalchemy
from __future__ import annotations
import sys, logging
from pandas import DataFrame
from pydantic import BaseModel
from sqlalchemy import Column, INTEGER, String, JSON
from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query, Session
from sqlalchemy.ext.declarative import declared_attr
@@ -17,6 +18,7 @@ from tools import report_result
if 'pytest' in sys.modules:
sys.path.append(Path(__file__).parents[4].absolute().joinpath("tests").__str__())
# NOTE: For inheriting in LogMixin
Base: DeclarativeMeta = declarative_base()
logger = logging.getLogger(f"submissions.{__name__}")
@@ -31,6 +33,7 @@ class LogMixin(Base):
if len(name) > 64:
name = name.replace("<", "").replace(">", "")
if len(name) > 64:
# NOTE: As if re'agent'
name = name.replace("agent", "")
if len(name) > 64:
name = f"...{name[-61:]}"
@@ -116,7 +119,7 @@ class BaseClass(Base):
return dict(singles=singles)
@classmethod
def find_regular_subclass(cls, name: str | None = None) -> Any:
def find_regular_subclass(cls, name: str = "") -> Any:
"""
Args:
@@ -126,8 +129,9 @@ class BaseClass(Base):
Any: Subclass of this object
"""
if not name:
return cls
# if not name:
# logger.warning("You need to include a name of what you're looking for.")
# return cls
if " " in name:
search = name.title().replace(" ", "")
else:
@@ -171,7 +175,7 @@ class BaseClass(Base):
try:
records = [obj.to_sub_dict(**kwargs) for obj in objects]
except AttributeError:
records = [obj.to_dict() for obj in objects]
records = [obj.to_omnigui_dict() for obj in objects]
return DataFrame.from_records(records)
@classmethod
@@ -190,7 +194,7 @@ class BaseClass(Base):
Execute sqlalchemy query with relevant defaults.
Args:
model (Any, optional): model to be queried. Defaults to None
model (Any, optional): model to be queried, allows for plugging in. Defaults to None
query (Query, optional): input query object. Defaults to None
limit (int): Maximum number of results. (0 = all). Defaults to 0
@@ -237,13 +241,28 @@ class BaseClass(Base):
report.add_result(Result(msg=e, status="Critical"))
return report
def to_dict(self):
def to_omnigui_dict(self) -> dict:
"""
For getting any object in an omni-thing friendly output.
Returns:
dict: Dictionary of object minus _sa_instance_state with id at the front.
"""
dicto = {k: v for k, v in self.__dict__.items() if k not in ["_sa_instance_state"]}
try:
dicto = {'id': dicto.pop('id'), **dicto}
except KeyError:
pass
return dicto
@classmethod
def get_pydantic_model(cls):
def get_pydantic_model(cls) -> BaseModel:
"""
Gets the pydantic model corresponding to this object.
Returns:
Pydantic model with name "Pyd{cls.__name__}"
"""
from backend.validators import pydant
try:
model = getattr(pydant, f"Pyd{cls.__name__}")
@@ -252,7 +271,13 @@ class BaseClass(Base):
return model
@classproperty
def add_edit_tooltips(self):
def add_edit_tooltips(self) -> dict:
"""
Gets tooltips for Omni-add-edit
Returns:
dict: custom dictionary for this class.
"""
return dict()
@@ -270,7 +295,7 @@ class ConfigItem(BaseClass):
@classmethod
def get_config_items(cls, *args) -> ConfigItem | List[ConfigItem]:
"""
Get desired config items from database
Get desired config items, or all from database
Returns:
ConfigItem|List[ConfigItem]: Config item(s)
@@ -283,6 +308,7 @@ class ConfigItem(BaseClass):
case 1:
config_items = query.filter(cls.key == args[0]).first()
case _:
# NOTE: All items whose key field is in args.
config_items = query.filter(cls.key.in_(args)).all()
return config_items

View File

@@ -24,7 +24,7 @@ class AuditLog(Base):
changes = Column(JSON)
def __repr__(self):
return f"<{self.user} @ {self.time}>"
return f"<{self.object}: {self.user} @ {self.time}>"
@classmethod
def query(cls, start_date: date | str | int | None = None, end_date: date | str | int | None = None) -> List["AuditLog"]:

View File

@@ -255,6 +255,7 @@ class Control(BaseClass):
f"Could not get polymorph {polymorphic_identity} of {cls} due to {e}, falling back to BasicSubmission")
case _:
pass
# NOTE: if attrs passed in and this cls doesn't have all attributes in attr
if attrs and any([not hasattr(cls, attr) for attr in attrs.keys()]):
# NOTE: looks for first model that has all included kwargs
try:
@@ -272,6 +273,9 @@ class Control(BaseClass):
Args:
parent (QWidget): chart holding widget to add buttons to.
Returns:
None: Child methods will return things.
"""
return None
@@ -284,7 +288,7 @@ class Control(BaseClass):
chart_settings (dict): settings passed down from chart widget
ctx (Settings): settings passed down from gui
"""
return None
return Report(), None
def delete(self):
self.__database_session__.delete(self)
@@ -315,8 +319,14 @@ class PCRControl(Control):
Returns:
dict: Output dict of name, ct, subtype, target, reagent_lot and submitted_date
"""
return dict(name=self.name, ct=self.ct, subtype=self.subtype, target=self.target, reagent_lot=self.reagent_lot,
submitted_date=self.submitted_date.date())
return dict(
name=self.name,
ct=self.ct,
subtype=self.subtype,
target=self.target,
reagent_lot=self.reagent_lot,
submitted_date=self.submitted_date.date()
)
@classmethod
@report_result
@@ -403,9 +413,9 @@ class IridaControl(Control):
kraken = self.kraken
except TypeError:
kraken = {}
kraken_cnt_total = sum([kraken[item]['kraken_count'] for item in kraken])
kraken_cnt_total = sum([item['kraken_count'] for item in kraken.values()])
new_kraken = [dict(name=item, kraken_count=kraken[item]['kraken_count'],
kraken_percent="{0:.0%}".format(kraken[item]['kraken_count'] / kraken_cnt_total),
kraken_percent=f"{kraken[item]['kraken_count'] / kraken_cnt_total:0.2%}",
target=item in self.controltype.targets)
for item in kraken]
new_kraken = sorted(new_kraken, key=itemgetter('kraken_count'), reverse=True)
@@ -479,6 +489,7 @@ class IridaControl(Control):
@classmethod
def make_parent_buttons(cls, parent: QWidget) -> None:
"""
Creates buttons for controlling
Args:
parent (QWidget): chart holding widget to add buttons to.
@@ -486,6 +497,7 @@ class IridaControl(Control):
"""
super().make_parent_buttons(parent=parent)
rows = parent.layout.rowCount() - 2
# NOTE: check box for consolidating off-target items
checker = QCheckBox(parent)
checker.setChecked(True)
checker.setObjectName("irida_check")
@@ -703,6 +715,12 @@ class IridaControl(Control):
df = df[df.name not in exclude]
return df
def to_pydantic(self):
def to_pydantic(self) -> "PydIridaControl":
"""
Constructs a pydantic version of this object.
Returns:
PydIridaControl: This object as a pydantic model.
"""
from backend.validators import PydIridaControl
return PydIridaControl(**self.__dict__)

View File

@@ -129,8 +129,10 @@ class KitType(BaseClass):
"""
return f"<KitType({self.name})>"
def get_reagents(self, required: bool = False, submission_type: str | SubmissionType | None = None) -> Generator[
ReagentRole, None, None]:
def get_reagents(self,
required: bool = False,
submission_type: str | SubmissionType | None = None
) -> Generator[ReagentRole, None, None]:
"""
Return ReagentTypes linked to kit through KitTypeReagentTypeAssociation.
@@ -240,23 +242,23 @@ class KitType(BaseClass):
dict: Dictionary containing relevant info for SubmissionType construction
"""
base_dict = dict(name=self.name, reagent_roles=[], equipment_roles=[])
for k, v in self.construct_xl_map_for_use(submission_type=submission_type):
for key, value in self.construct_xl_map_for_use(submission_type=submission_type):
try:
assoc = next(item for item in self.kit_reagentrole_associations if item.reagent_role.name == k)
assoc = next(item for item in self.kit_reagentrole_associations if item.reagent_role.name == key)
except StopIteration as e:
continue
for kk, vv in assoc.to_export_dict().items():
v[kk] = vv
base_dict['reagent_roles'].append(v)
for k, v in submission_type.construct_field_map("equipment"):
value[kk] = vv
base_dict['reagent_roles'].append(value)
for key, value in submission_type.construct_field_map("equipment"):
try:
assoc = next(item for item in submission_type.submissiontype_equipmentrole_associations if
item.equipment_role.name == k)
item.equipment_role.name == key)
except StopIteration:
continue
for kk, vv in assoc.to_export_dict(extraction_kit=self).items():
v[kk] = vv
base_dict['equipment_roles'].append(v)
value[kk] = vv
base_dict['equipment_roles'].append(value)
return base_dict
@classmethod
@@ -402,6 +404,7 @@ class ReagentRole(BaseClass):
case _:
pass
assert reagent.role
# NOTE: Get all roles common to the reagent and the kit.
result = set(kit_type.reagent_roles).intersection(reagent.role)
return next((item for item in result), None)
match name:
@@ -500,7 +503,7 @@ class Reagent(BaseClass, LogMixin):
except (TypeError, AttributeError) as e:
place_holder = date.today()
logger.error(f"We got a type error setting {self.lot} expiry: {e}. setting to today for testing")
# NOTE: The notation for not having an expiry is 1970.1.1
# NOTE: The notation for not having an expiry is 1970.01.01
if self.expiry.year == 1970:
place_holder = "NA"
else:
@@ -555,7 +558,7 @@ class Reagent(BaseClass, LogMixin):
instance = PydReagent(**kwargs)
new = True
instance, _ = instance.toSQL()
logger.debug(f"Instance: {instance}")
logger.info(f"Instance from query or create: {instance}")
return instance, new
@classmethod
@@ -609,33 +612,70 @@ class Reagent(BaseClass, LogMixin):
pass
return cls.execute_query(query=query, limit=limit)
def set_attribute(self, key, value):
match key:
case "lot":
value = value.upper()
case "role":
match value:
case ReagentRole():
role = value
case str():
role = ReagentRole.query(name=value, limit=1)
case _:
return
if role and role not in self.role:
self.role.append(role)
return
case "comment":
return
case "expiry":
if isinstance(value, str):
value = date(year=1970, month=1, day=1)
# NOTE: if min time is used, any reagent set to expire today (Bac postive control, eg) will have expired at midnight and therefore be flagged.
# NOTE: Make expiry at date given, plus maximum time = end of day
value = datetime.combine(value, datetime.max.time())
value = value.replace(tzinfo=timezone)
case _:
pass
logger.debug(f"Role to be set to: {value}")
try:
self.__setattr__(key, value)
except AttributeError as e:
logger.error(f"Could not set {key} due to {e}")
@check_authorization
def edit_from_search(self, obj, **kwargs):
from frontend.widgets.misc import AddReagentForm
from frontend.widgets.omni_add_edit import AddEdit
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)
# dlg = AddReagentForm(reagent_lot=self.lot, reagent_role=role_name, expiry=self.expiry, reagent_name=self.name)
dlg = AddEdit(parent=None, instance=self)
if dlg.exec():
vars = dlg.parse_form()
for key, value in vars.items():
match key:
case "expiry":
if isinstance(value, str):
field_value = datetime.strptime(value, "%Y-%m-%d")
elif isinstance(value, date):
field_value = datetime.combine(value, datetime.max.time())
else:
field_value = value
field_value.replace(tzinfo=timezone)
case "role":
continue
case _:
field_value = value
self.__setattr__(key, field_value)
pyd = dlg.parse_form()
for field in pyd.model_fields:
self.set_attribute(field, pyd.__getattribute__(field))
# for key, value in vars.items():
# match key:
# case "expiry":
# if isinstance(value, str):
# field_value = datetime.strptime(value, "%Y-%m-%d")
# elif isinstance(value, date):
# field_value = datetime.combine(value, datetime.max.time())
# else:
# field_value = value
# field_value.replace(tzinfo=timezone)
# case "role":
# continue
# case _:
# field_value = value
# self.__setattr__(key, field_value)
self.save()
# print(self.__dict__)
@classproperty
def add_edit_tooltips(self):
@@ -767,7 +807,7 @@ class SubmissionType(BaseClass):
Grabs the default excel template file.
Returns:
bytes: The excel sheet.
bytes: The Excel sheet.
"""
submission_type = cls.query(name="Bacterial Culture")
return submission_type.template_file
@@ -787,13 +827,13 @@ class SubmissionType(BaseClass):
def set_template_file(self, filepath: Path | str):
"""
Sets the binary store to an excel file.
Sets the binary store to an Excel file.
Args:
filepath (Path | str): Path to the template file.
Raises:
ValueError: Raised if file is not excel file.
ValueError: Raised if file is not Excel file.
"""
if isinstance(filepath, str):
filepath = Path(filepath)

View File

@@ -375,6 +375,9 @@ class BasicSubmission(BaseClass, LogMixin):
output["contact_phone"] = contact_phone
output["custom"] = custom
output["controls"] = controls
try:
output["completed_date"] = self.completed_date.strftime("%Y-%m-%d")
except AttributeError:
output["completed_date"] = self.completed_date
return output

View File

@@ -3,6 +3,7 @@ contains writer objects for pushing values to submission sheet templates.
"""
import logging
from copy import copy
from datetime import datetime
from operator import itemgetter
from pprint import pformat
from typing import List, Generator, Tuple

View File

@@ -48,7 +48,7 @@ class PydReagent(BaseModel):
def rescue_type_with_lookup(cls, value, values):
if value is None and values.data['lot'] is not None:
try:
return Reagent.query(lot=values.data['lot'].name)
return Reagent.query(lot=values.data['lot']).name
except AttributeError:
return value
return value
@@ -133,28 +133,8 @@ class PydReagent(BaseModel):
for key, value in self.__dict__.items():
if isinstance(value, dict):
value = value['value']
# NOTE: set fields based on keys in dictionary
match key:
case "lot":
reagent.lot = value.upper()
case "role":
reagent_role = ReagentRole.query(name=value)
if reagent_role is not None:
reagent.role.append(reagent_role)
case "comment":
continue
case "expiry":
if isinstance(value, str):
value = date(year=1970, month=1, day=1)
# NOTE: if min time is used, any reagent set to expire today (Bac postive control, eg) will have expired at midnight and therefore be flagged.
# NOTE: Make expiry at date given, plus now time + 1 hour
value = datetime.combine(value, datetime.max.time())
reagent.expiry = value.replace(tzinfo=timezone)
case _:
try:
reagent.__setattr__(key, value)
except AttributeError:
logger.error(f"Couldn't set {key} to {value}")
# NOTE: reagent method sets fields based on keys in dictionary
reagent.set_attribute(key, value)
if submission is not None and reagent not in submission.reagents:
assoc = SubmissionReagentAssociation(reagent=reagent, submission=submission)
assoc.comments = self.comment
@@ -830,7 +810,7 @@ class PydSubmission(BaseModel, extra='allow'):
case item if item in instance.timestamps():
logger.warning(f"Incoming timestamp key: {item}, with value: {value}")
if isinstance(value, date):
value = datetime.combine(value, datetime.max.time())
value = datetime.combine(value, datetime.now().time())
value = value.replace(tzinfo=timezone)
elif isinstance(value, str):
value: datetime = datetime.strptime(value, "%Y-%m-%d")

View File

@@ -12,7 +12,7 @@ from PyQt6.QtGui import QAction
from pathlib import Path
from markdown import markdown
from __init__ import project_path
from backend import SubmissionType, Reagent, BasicSample, Organization
from backend import SubmissionType, Reagent, BasicSample, Organization, KitType
from tools import check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size, is_power_user
from .functions import select_save_file, select_open_file
# from datetime import date
@@ -84,6 +84,7 @@ class App(QMainWindow):
maintenanceMenu.addAction(self.joinPCRAction)
editMenu.addAction(self.editReagentAction)
editMenu.addAction(self.manageOrgsAction)
# editMenu.addAction(self.manageKitsAction)
if not is_power_user():
editMenu.setEnabled(False)
@@ -111,6 +112,7 @@ class App(QMainWindow):
self.yamlImportAction = QAction("Import Type Template", self)
self.editReagentAction = QAction("Edit Reagent", self)
self.manageOrgsAction = QAction("Manage Clients", self)
self.manageKitsAction = QAction("Manage Kits", self)
def _connectActions(self):
"""
@@ -129,6 +131,7 @@ class App(QMainWindow):
self.table_widget.pager.current_page.textChanged.connect(self.update_data)
self.editReagentAction.triggered.connect(self.edit_reagent)
self.manageOrgsAction.triggered.connect(self.manage_orgs)
self.manageKitsAction.triggered.connect(self.manage_kits)
def showAbout(self):
"""
@@ -219,6 +222,11 @@ class App(QMainWindow):
new_org = dlg.parse_form()
# logger.debug(new_org.__dict__)
def manage_kits(self):
dlg = ManagerWindow(parent=self, object_type=KitType, extras=[])
if dlg.exec():
print(dlg.parse_form())
class AddSubForm(QWidget):
def __init__(self, parent: QWidget):

View File

@@ -11,7 +11,7 @@ import logging
from sqlalchemy.orm.relationships import _RelationshipDeclared
from tools import Report, Result
from tools import Report, Result, report_result
logger = logging.getLogger(f"submissions.{__name__}")
@@ -23,7 +23,7 @@ class AddEdit(QDialog):
self.instance = instance
self.object_type = instance.__class__
self.layout = QGridLayout(self)
logger.debug(f"Manager: {manager}")
# logger.debug(f"Manager: {manager}")
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
@@ -36,7 +36,7 @@ class AddEdit(QDialog):
fields = {'name': fields.pop('name'), **fields}
except KeyError:
pass
logger.debug(pformat(fields, indent=4))
# logger.debug(pformat(fields, indent=4))
height_counter = 0
for key, field in fields.items():
try:
@@ -47,7 +47,7 @@ class AddEdit(QDialog):
logger.debug(f"{key} property: {type(field['class_attr'].property)}")
# widget = EditProperty(self, key=key, column_type=field.property.expression.type,
# value=getattr(self.instance, key))
logger.debug(f"Column type: {field}, Value: {value}")
# logger.debug(f"Column type: {field}, Value: {value}")
widget = EditProperty(self, key=key, column_type=field, value=value)
except AttributeError as e:
logger.error(f"Problem setting widget {key}: {e}")
@@ -60,6 +60,7 @@ class AddEdit(QDialog):
self.setMinimumSize(600, 50 * height_counter)
self.setLayout(self.layout)
@report_result
def parse_form(self) -> Tuple[BaseModel, Report]:
report = Report()
parsed = {result[0].strip(":"): result[1] for result in [item.parse_form() for item in self.findChildren(EditProperty)] if result[0]}

View File

@@ -188,6 +188,8 @@ class EditRelationship(QWidget):
dlg = AddEdit(self, instance=instance, manager=self.parent().object_type.__name__.lower())
if dlg.exec():
new_instance = dlg.parse_form()
new_instance, result = new_instance.toSQL()
logger.debug(f"New instance: {new_instance}")
addition = getattr(self.parent().instance, self.objectName())
if isinstance(addition, InstrumentedList):
addition.append(new_instance)
@@ -211,7 +213,7 @@ class EditRelationship(QWidget):
sets data in model
"""
# logger.debug(self.data)
self.data = DataFrame.from_records([item.to_dict() for item in self.data])
self.data = DataFrame.from_records([item.to_omnigui_dict() for item in self.data])
try:
self.columns_of_interest = [dict(name=item, column=self.data.columns.get_loc(item)) for item in self.extras]
except (KeyError, AttributeError):

View File

@@ -68,7 +68,11 @@ class SearchBox(QDialog):
self.object_type = self.original_type
else:
self.object_type = self.original_type.find_regular_subclass(self.sub_class.currentText())
for iii, searchable in enumerate(self.object_type.searchables):
try:
search_fields = self.object_type.searchables
except AttributeError:
search_fields = []
for iii, searchable in enumerate(search_fields):
widget = FieldSearch(parent=self, label=searchable, field_name=searchable)
widget.setObjectName(searchable)
self.layout.addWidget(widget, 1 + iii, 0)
@@ -142,7 +146,10 @@ class SearchResults(QTableView):
self.context = kwargs
self.parent = parent
self.object_type = object_type
try:
self.extras = extras + self.object_type.searchables
except AttributeError:
self.extras = extras
def setData(self, df: DataFrame) -> None:
"""