Second round of code cleanup.

This commit is contained in:
lwark
2024-10-30 07:34:39 -05:00
parent 1f83b61c81
commit ba1b3e5cf3
19 changed files with 176 additions and 110 deletions

View File

@@ -1,3 +1,7 @@
## 202410.05
- Code clean up.
## 202410.03
- Added code for cataloging of PCR controls.

Binary file not shown.

View File

@@ -28,6 +28,8 @@ class BaseClass(Base):
__table_args__ = {'extend_existing': True} #: Will only add new columns
singles = ['id']
@classmethod
@declared_attr
def __tablename__(cls) -> str:
@@ -92,17 +94,21 @@ class BaseClass(Base):
Returns:
dict | list | str: Output of key:value dict or single (list, str) desired variable
"""
dicto = dict(singles=['id'])
output = {}
for k, v in dicto.items():
if len(args) > 0 and k not in args:
# logger.debug(f"{k} not selected as being of interest.")
continue
else:
output[k] = v
if len(args) == 1:
return output[args[0]]
return output
# if issubclass(cls, BaseClass) and cls.__name__ != "BaseClass":
singles = list(set(cls.singles + BaseClass.singles))
# else:
# singles = cls.singles
# output = dict(singles=singles)
# output = {}
# for k, v in dicto.items():
# if len(args) > 0 and k not in args:
# # logger.debug(f"{k} not selected as being of interest.")
# continue
# else:
# output[k] = v
# if len(args) == 1:
# return output[args[0]]
return dict(singles=singles)
@classmethod
def query(cls, **kwargs) -> Any | List[Any]:
@@ -190,10 +196,15 @@ class ConfigItem(BaseClass):
Returns:
ConfigItem|List[ConfigItem]: Config item(s)
"""
config_items = cls.__database_session__.query(cls).all()
config_items = [item for item in config_items if item.key in args]
if len(args) == 1:
config_items = config_items[0]
query = cls.__database_session__.query(cls)
# config_items = [item for item in config_items if item.key in args]
match len(args):
case 0:
config_items = query.all()
case 1:
config_items = query.filter(cls.key == args[0]).first()
case _:
config_items = query.filter(cls.key.in_(args)).all()
return config_items

View File

@@ -131,10 +131,8 @@ class Control(BaseClass):
__mapper_args__ = {
"polymorphic_identity": "Basic Control",
"polymorphic_on": case(
(controltype_name == "PCR Control", "PCR Control"),
(controltype_name == "Irida Control", "Irida Control"),
else_="Basic Control"
),
"with_polymorphic": "*",
@@ -147,15 +145,15 @@ class Control(BaseClass):
def find_polymorphic_subclass(cls, polymorphic_identity: str | ControlType | None = None,
attrs: dict | None = None) -> Control:
"""
Find subclass based on polymorphic identity or relevant attributes.
Find subclass based on polymorphic identity or relevant attributes.
Args:
polymorphic_identity (str | None, optional): String representing polymorphic identity. Defaults to None.
attrs (str | SubmissionType | None, optional): Attributes of the relevant class. Defaults to None.
Args:
polymorphic_identity (str | None, optional): String representing polymorphic identity. Defaults to None.
attrs (str | SubmissionType | None, optional): Attributes of the relevant class. Defaults to None.
Returns:
Control: Subclass of interest.
"""
Returns:
Control: Subclass of interest.
"""
if isinstance(polymorphic_identity, dict):
# logger.debug(f"Controlling for dict value")
polymorphic_identity = polymorphic_identity['value']
@@ -189,14 +187,11 @@ class Control(BaseClass):
Args:
parent (QWidget): chart holding widget to add buttons to.
Returns:
"""
pass
return None
@classmethod
def make_chart(cls, parent, chart_settings: dict, ctx):
def make_chart(cls, parent, chart_settings: dict, ctx) -> Tuple[Report, "CustomFigure" | None]:
"""
Dummy operation to be overridden by child classes.
@@ -307,6 +302,7 @@ class PCRControl(Control):
return cls.execute_query(query=query, limit=limit)
@classmethod
@report_result
def make_chart(cls, parent, chart_settings: dict, ctx: Settings) -> Tuple[Report, "PCRFigure"]:
"""
Creates a PCRFigure. Overrides parent

View File

@@ -4,6 +4,7 @@ All kit and reagent related models
from __future__ import annotations
import datetime
import json
import sys
from pprint import pformat
import yaml
from sqlalchemy import Column, String, TIMESTAMP, JSON, INTEGER, ForeignKey, Interval, Table, FLOAT, BLOB
@@ -693,6 +694,9 @@ class SubmissionType(BaseClass):
Returns:
List[str]: List of sheet names
"""
# print(f"Getting template file from {self.__database_session__.get_bind()}")
if "pytest" in sys.modules:
return ExcelFile("C:\\Users\lwark\Documents\python\submissions\mytests\\test_assets\RSL-AR-20240513-1.xlsx").sheet_names
return ExcelFile(BytesIO(self.template_file), engine="openpyxl").sheet_names
def set_template_file(self, filepath: Path | str):

View File

@@ -6,7 +6,7 @@ import sys
import types
from copy import deepcopy
from getpass import getuser
import logging, uuid, tempfile, re, yaml, base64
import logging, uuid, tempfile, re, base64
from zipfile import ZipFile
from tempfile import TemporaryDirectory, TemporaryFile
from operator import itemgetter
@@ -167,28 +167,24 @@ class BasicSubmission(BaseClass):
"""
# NOTE: Create defaults for all submission_types
parent_defs = super().get_default_info()
# NOTE: Singles tells the query which fields to set limit to 1
dicto = super().get_default_info()
recover = ['filepath', 'samples', 'csv', 'comment', 'equipment']
dicto = dict(
dicto.update(dict(
details_ignore=['excluded', 'reagents', 'samples',
'extraction_info', 'comment', 'barcode',
'platemap', 'export_map', 'equipment', 'tips', 'custom'],
# NOTE: Fields not placed in ui form
form_ignore=['reagents', 'ctx', 'id', 'cost', 'extraction_info', 'signed_by', 'comment', 'namer',
'submission_object', "tips", 'contact_phone', 'custom'] + recover,
'submission_object', "tips", 'contact_phone', 'custom', 'cost_centre'] + recover,
# NOTE: Fields not placed in ui form to be moved to pydantic
form_recover=recover
)
# NOTE: Singles tells the query which fields to set limit to 1
dicto['singles'] = parent_defs['singles']
))
# NOTE: Grab mode_sub_type specific info.
output = {}
for k, v in dicto.items():
if len(args) > 0 and k not in args:
# logger.debug(f"Don't want {k}")
continue
else:
output[k] = v
if args:
output = {k: v for k, v in dicto.items() if k in args}
else:
output = {k: v for k, v in dicto.items()}
if isinstance(submission_type, SubmissionType):
st = submission_type
else:
@@ -198,7 +194,7 @@ class BasicSubmission(BaseClass):
else:
output['submission_type'] = st.name
for k, v in st.defaults.items():
if len(args) > 0 and k not in args:
if args and k not in args:
# logger.debug(f"Don't want {k}")
continue
else:
@@ -272,6 +268,7 @@ class BasicSubmission(BaseClass):
field = self.__getattribute__(name)
except AttributeError:
return None
# assert isinstance(field, list)
for item in field:
if extra:
yield item.to_sub_dict(extra)
@@ -1137,9 +1134,9 @@ class BasicSubmission(BaseClass):
limit = 1
case _:
pass
if chronologic:
logger.debug("Attempting sort by date descending")
query = query.order_by(cls.submitted_date.desc())
# if chronologic:
# logger.debug("Attempting sort by date descending")
query = query.order_by(cls.submitted_date.desc())
if page_size is not None:
query = query.limit(page_size)
page = page - 1
@@ -2980,7 +2977,6 @@ class WastewaterArticAssociation(SubmissionSampleAssociation):
Returns:
dict: Updated dictionary with row, column and well updated
"""
sample = super().to_sub_dict()
sample['ct'] = self.ct
sample['source_plate'] = self.source_plate

View File

@@ -2,6 +2,7 @@
contains parser objects for pulling values from client generated submission sheets.
'''
import json
import sys
from copy import copy
from getpass import getuser
from pprint import pformat
@@ -95,6 +96,7 @@ class SheetParser(object):
parser = ReagentParser(xl=self.xl, submission_type=self.submission_type,
extraction_kit=extraction_kit)
self.sub['reagents'] = parser.parse_reagents()
logger.debug(f"Reagents out of parser: {pformat(self.sub['reagents'])}")
def parse_samples(self):
"""
@@ -273,7 +275,8 @@ class ReagentParser(object):
# logger.debug(f"Reagent Parser map: {self.map}")
self.xl = xl
def fetch_kit_info_map(self, submission_type: str) -> dict:
@report_result
def fetch_kit_info_map(self, submission_type: str) -> Tuple[Report, dict]:
"""
Gets location of kit reagents from database
@@ -283,7 +286,7 @@ class ReagentParser(object):
Returns:
dict: locations of reagent info for the kit.
"""
report = Report()
if isinstance(submission_type, dict):
submission_type = submission_type['value']
reagent_map = {k: v for k, v in self.kit_object.construct_xl_map_for_use(submission_type)}
@@ -291,7 +294,12 @@ class ReagentParser(object):
del reagent_map['info']
except KeyError:
pass
return reagent_map
# logger.debug(f"Reagent map: {pformat(reagent_map)}")
if not reagent_map.keys():
report.add_result(Result(owner=__name__, code=0, msg=f"No kit map found for {self.kit_object.name}.\n\n"
f"Are you sure you used the right kit?",
status="Critical"))
return report, reagent_map
def parse_reagents(self) -> Generator[dict, None, None]:
"""
@@ -401,6 +409,7 @@ class SampleParser(object):
"""
invalids = [0, "0", "EMPTY"]
smap = self.sample_info_map['plate_map']
print(smap)
ws = self.xl[smap['sheet']]
plate_map_samples = []
for ii, row in enumerate(range(smap['start_row'], smap['end_row'] + 1), start=1):
@@ -469,8 +478,10 @@ class SampleParser(object):
yield new
else:
merge_on_id = self.sample_info_map['lookup_table']['merge_on_id']
plate_map_samples = sorted(copy(self.plate_map_samples), key=lambda d: d['id'])
lookup_samples = sorted(copy(self.lookup_samples), key=lambda d: d[merge_on_id])
# plate_map_samples = sorted(copy(self.plate_map_samples), key=lambda d: d['id'])
# lookup_samples = sorted(copy(self.lookup_samples), key=lambda d: d[merge_on_id])
plate_map_samples = sorted(copy(self.plate_map_samples), key=itemgetter('id'))
lookup_samples = sorted(copy(self.lookup_samples), key=itemgetter(merge_on_id))
for ii, psample in enumerate(plate_map_samples):
# NOTE: See if we can do this the easy way and just use the same list index.
try:
@@ -483,6 +494,8 @@ class SampleParser(object):
lookup_samples[ii] = {}
else:
logger.warning(f"Match for {psample['id']} not direct, running search.")
searchables = [(jj, sample) for jj, sample in enumerate(lookup_samples)
if merge_on_id in sample.keys()]
# for jj, lsample in enumerate(lookup_samples):
# try:
# check = lsample[merge_on_id] == psample['id']
@@ -494,14 +507,18 @@ class SampleParser(object):
# break
# else:
# new = psample
jj, new = next(((jj, lsample) for jj, lsample in enumerate(lookup_samples) if lsample[merge_on_id] == psample['id']), (-1, psample))
jj, new = next(((jj, lsample | psample) for jj, lsample in searchables
if lsample[merge_on_id] == psample['id']), (-1, psample))
logger.debug(f"Assigning from index {jj} - {new}")
if jj >= 0:
lookup_samples[jj] = {}
if not check_key_or_attr(key='submitter_id', interest=new, check_none=True):
new['submitter_id'] = psample['id']
new = self.sub_object.parse_samples(new)
del new['id']
try:
del new['id']
except KeyError:
pass
yield new
@@ -586,7 +603,7 @@ class EquipmentParser(object):
nickname=eq.nickname)
except AttributeError:
logger.error(f"Unable to add {eq} to list.")
class TipParser(object):
"""
@@ -649,7 +666,7 @@ class TipParser(object):
yield dict(name=eq.name, role=k, lot=lot)
except AttributeError:
logger.error(f"Unable to add {eq} to PydTips list.")
class PCRParser(object):
"""Object to pull data from Design and Analysis PCR export file."""
@@ -705,4 +722,3 @@ class PCRParser(object):
pcr['imported_by'] = getuser()
# logger.debug(f"PCR: {pformat(pcr)}")
return pcr

View File

@@ -3,6 +3,7 @@ contains writer objects for pushing values to submission sheet templates.
"""
import logging
from copy import copy
from operator import itemgetter
from pprint import pformat
from typing import List, Generator, Tuple
from openpyxl import load_workbook, Workbook
@@ -272,7 +273,8 @@ class SampleWriter(object):
self.sample_map = submission_type.construct_sample_map()['lookup_table']
# NOTE: exclude any samples without a submission rank.
samples = [item for item in self.reconcile_map(sample_list) if item['submission_rank'] > 0]
self.samples = sorted(samples, key=lambda k: k['submission_rank'])
# self.samples = sorted(samples, key=lambda k: k['submission_rank'])
self.samples = sorted(samples, key=itemgetter('submission_rank'))
def reconcile_map(self, sample_list: list) -> Generator[dict, None, None]:
"""

View File

@@ -11,7 +11,7 @@ from dateutil.parser import ParserError
from typing import List, Tuple, Literal
from . import RSLNamer
from pathlib import Path
from tools import check_not_nan, convert_nans_to_nones, Report, Result
from tools import check_not_nan, convert_nans_to_nones, Report, Result, timezone
from backend.db.models import *
from sqlalchemy.exc import StatementError, IntegrityError
from PyQt6.QtWidgets import QWidget
@@ -148,7 +148,9 @@ class PydReagent(BaseModel):
case "expiry":
if isinstance(value, str):
value = date(year=1970, month=1, day=1)
reagent.expiry = value
value = datetime.combine(value, datetime.min.time())
logger.debug(f"Expiry date coming into sql: {value} with type {type(value)}")
reagent.expiry = value.replace(tzinfo=timezone)
case _:
try:
reagent.__setattr__(key, value)
@@ -187,7 +189,11 @@ class PydSample(BaseModel, extra='allow'):
for k, v in data.model_extra.items():
if k in model.timestamps():
if isinstance(v, str):
# try:
v = datetime.strptime(v, "%Y-%m-%d")
# except ValueError:
# logger.warning(f"Attribute {k} value {v} for sample {data.submitter_id} could not be coerced into date. Setting to None.")
# v = None
data.__setattr__(k, v)
# logger.debug(f"Data coming out of validation: {pformat(data)}")
return data
@@ -678,6 +684,7 @@ class PydSubmission(BaseModel, extra='allow'):
return value
def __init__(self, run_custom: bool = False, **data):
logger.debug(f"{__name__} input data: {data}")
super().__init__(**data)
# NOTE: this could also be done with default_factory
self.submission_object = BasicSubmission.find_polymorphic_subclass(
@@ -833,6 +840,18 @@ class PydSubmission(BaseModel, extra='allow'):
continue
if association is not None and association not in instance.submission_tips_associations:
instance.submission_tips_associations.append(association)
case item if item in instance.timestamps():
logger.warning(f"Incoming timestamp key: {item}, with value: {value}")
# value = value.replace(tzinfo=timezone)
if isinstance(value, date):
value = datetime.combine(value, datetime.min.time())
value = value.replace(tzinfo=timezone)
elif isinstance(value, str):
value: datetime = datetime.strptime(value, "%Y-%m-%d")
value = value.replace(tzinfo=timezone)
else:
value = value
instance.set_attribute(key=key, value=value)
case item if item in instance.jsons():
# logger.debug(f"{item} is a json.")
try:
@@ -941,7 +960,7 @@ class PydSubmission(BaseModel, extra='allow'):
# NOTE: Exclude any reagenttype found in this pyd not expected in kit.
expected_check = [item.role for item in ext_kit_rtypes]
output_reagents = [rt for rt in self.reagents if rt.role in expected_check]
# logger.debug(f"Already have these reagent types: {output_reagents}")
logger.debug(f"Already have these reagent types: {output_reagents}")
missing_check = [item.role for item in output_reagents]
missing_reagents = [rt for rt in ext_kit_rtypes if rt.role not in missing_check]
missing_reagents += [rt for rt in output_reagents if rt.missing]

View File

@@ -2,12 +2,13 @@
Contains all operations for creating charts, graphs and visual effects.
'''
from PyQt6.QtWidgets import QWidget
import plotly
import plotly, logging
from plotly.graph_objects import Figure
from plotly.graph_objs import FigureWidget
import pandas as pd
from frontend.widgets.functions import select_save_file
logger = logging.getLogger(f"submissions.{__name__}")
class CustomFigure(Figure):
@@ -40,16 +41,12 @@ class CustomFigure(Figure):
"""
Creates final html code from plotly
Args:
figure (Figure): input figure
Returns:
str: html string
"""
html = '<html><body>'
if self is not None:
html += plotly.offline.plot(self, output_type='div',
include_plotlyjs='cdn') #, image = 'png', auto_open=True, image_filename='plot_image')
html += plotly.offline.plot(self, output_type='div', include_plotlyjs='cdn')
else:
html += "<h1>No data was retrieved for the given parameters.</h1>"
html += '</body></html>'

View File

@@ -2,9 +2,6 @@
Functions for constructing irida controls graphs using plotly.
"""
from pprint import pformat
from plotly.graph_objs import FigureWidget, Scatter
from . import CustomFigure
import plotly.express as px
import pandas as pd
@@ -13,30 +10,23 @@ import logging
logger = logging.getLogger(f"submissions.{__name__}")
# NOTE: For click events try (haven't got working yet) ipywidgets >=7.0.0 required for figurewidgets:
# https://plotly.com/python/click-events/
class PCRFigure(CustomFigure):
def __init__(self, df: pd.DataFrame, modes: list, ytitle: str | None = None, parent: QWidget | None = None,
months: int = 6):
super().__init__(df=df, modes=modes)
logger.debug(f"DF: {self.df}")
# logger.debug(f"DF: {self.df}")
self.construct_chart(df=df)
def hello(self):
print("hello")
def construct_chart(self, df: pd.DataFrame):
logger.debug(f"PCR df:\n {df}")
# logger.debug(f"PCR df:\n {df}")
try:
express = px.scatter(data_frame=df, x='submitted_date', y="ct",
scatter = px.scatter(data_frame=df, x='submitted_date', y="ct",
hover_data=["name", "target", "ct", "reagent_lot"],
color="target")
except ValueError:
express = px.scatter()
scatter = FigureWidget([datum for datum in express.data])
scatter = px.scatter()
self.add_traces(scatter.data)
self.update_traces(marker={'size': 15})

View File

@@ -2,6 +2,7 @@
Constructs main application.
"""
from pprint import pformat
from PyQt6.QtCore import qInstallMessageHandler
from PyQt6.QtWidgets import (
QTabWidget, QWidget, QVBoxLayout,
QHBoxLayout, QScrollArea, QMainWindow,
@@ -11,6 +12,7 @@ from PyQt6.QtGui import QAction
from pathlib import Path
from markdown import markdown
from __init__ import project_path
from backend import SubmissionType
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 datetime import date
@@ -32,12 +34,13 @@ class App(QMainWindow):
def __init__(self, ctx: Settings = None):
# logger.debug(f"Initializing main window...")
super().__init__()
qInstallMessageHandler(lambda x, y, z: None)
self.ctx = ctx
self.last_dir = ctx.directory_path
self.report = Report()
# NOTE: indicate version and connected database in title bar
try:
self.title = f"Submissions App (v{ctx.package.__version__}) - {ctx.database_session.get_bind().url}"
self.title = f"Submissions App (v{ctx.package.__version__}) - {ctx.database_path}/{ctx.database_name}"
except (AttributeError, KeyError):
self.title = f"Submissions App"
# NOTE: set initial app position and size

View File

@@ -155,7 +155,7 @@ class ControlsViewer(QWidget):
chart_settings = dict(sub_type=self.con_sub_type, start_date=self.start_date, end_date=self.end_date,
mode=self.mode,
sub_mode=self.mode_sub_type, parent=self, months=months)
_, self.fig = self.archetype.get_instance_class().make_chart(chart_settings=chart_settings, parent=self, ctx=self.app.ctx)
self.fig = self.archetype.get_instance_class().make_chart(chart_settings=chart_settings, parent=self, ctx=self.app.ctx)
if issubclass(self.fig.__class__, CustomFigure):
self.save_button.setEnabled(True)
# logger.debug(f"Updating figure...")

View File

@@ -1,8 +1,9 @@
'''
Creates forms that the user can enter equipment info into.
'''
import time
from pprint import pformat
from PyQt6.QtCore import Qt
from PyQt6.QtCore import Qt, QSignalBlocker
from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox,
QLabel, QWidget, QVBoxLayout, QDialogButtonBox, QGridLayout)
from backend.db.models import Equipment, BasicSubmission, Process
@@ -127,13 +128,14 @@ class RoleComboBox(QWidget):
# logger.debug(f"Updating equipment: {equip}")
equip2 = next((item for item in self.role.equipment if item.name == equip), self.role.equipment[0])
# logger.debug(f"Using: {equip2}")
self.process.clear()
with QSignalBlocker(self.process) as blocker:
self.process.clear()
self.process.addItems([item for item in equip2.processes if item in self.role.processes])
def update_tips(self):
"""
Changes what tips are available when process is changed
"""
"""
process = self.process.currentText().strip()
# logger.debug(f"Checking process: {process} for equipment {self.role.name}")
process = Process.query(name=process)

View File

@@ -1,6 +1,7 @@
"""
Gel box for artic quality control
"""
from operator import itemgetter
from PyQt6.QtWidgets import (QWidget, QDialog, QGridLayout,
QLabel, QLineEdit, QDialogButtonBox,
QTextEdit, QComboBox
@@ -65,7 +66,8 @@ class GelBox(QDialog):
layout.addWidget(self.imv, 0, 1, 20, 20)
# NOTE: setting this widget as central widget of the main window
try:
control_info = sorted(self.submission.gel_controls, key=lambda d: d['location'])
# control_info = sorted(self.submission.gel_controls, key=lambda d: d['location'])
control_info = sorted(self.submission.gel_controls, key=itemgetter('location'))
except KeyError:
control_info = None
self.form = ControlsForm(parent=self, control_info=control_info)

View File

@@ -99,19 +99,21 @@ class SubmissionsSheet(QTableView):
proxyModel.setSourceModel(pandasModel(self.data))
self.setModel(proxyModel)
def contextMenuEvent(self):
def contextMenuEvent(self, event):
"""
Creates actions for right click menu events.
Args:
event (_type_): the item of interest
"""
# logger.debug(event().__dict__)
# logger.debug(event.__dict__)
id = self.selectionModel().currentIndex()
id = id.sibling(id.row(), 0).data()
submission = BasicSubmission.query(id=id)
# logger.debug(f"Event submission: {submission}")
self.menu = QMenu(self)
self.con_actions = submission.custom_context_events()
# logger.debug(f"Menu options: {self.con_actions}")
for k in self.con_actions.keys():
# logger.debug(f"Adding {k}")
action = QAction(k, self)

View File

@@ -1,6 +1,8 @@
'''
Contains all submission related frontend functions
'''
import sys
from PyQt6.QtWidgets import (
QWidget, QPushButton, QVBoxLayout,
QComboBox, QDateEdit, QLineEdit, QLabel
@@ -190,7 +192,8 @@ class SubmissionFormWidget(QWidget):
self.app = parent.app
self.pyd = submission
self.missing_info = []
st = SubmissionType.query(name=self.pyd.submission_type['value']).get_submission_class()
self.submission_type = SubmissionType.query(name=self.pyd.submission_type['value'])
st = self.submission_type.get_submission_class()
defaults = st.get_default_info("form_recover", "form_ignore", submission_type=self.pyd.submission_type['value'])
self.recover = defaults['form_recover']
self.ignore = defaults['form_ignore']
@@ -215,7 +218,7 @@ class SubmissionFormWidget(QWidget):
value = self.pyd.model_extra[k]
except KeyError:
value = dict(value=None, missing=True)
add_widget = self.create_widget(key=k, value=value, submission_type=self.pyd.submission_type['value'],
add_widget = self.create_widget(key=k, value=value, submission_type=self.submission_type,
sub_obj=st, disable=check)
if add_widget is not None:
self.layout.addWidget(add_widget)
@@ -224,7 +227,7 @@ class SubmissionFormWidget(QWidget):
self.setStyleSheet(main_form_style)
self.scrape_reagents(self.pyd.extraction_kit)
def create_widget(self, key: str, value: dict | PydReagent, submission_type: str | None = None,
def create_widget(self, key: str, value: dict | PydReagent, submission_type: str | SubmissionType| None = None,
extraction_kit: str | None = None, sub_obj: BasicSubmission | None = None,
disable: bool = False) -> "self.InfoItem":
"""
@@ -240,6 +243,8 @@ class SubmissionFormWidget(QWidget):
self.InfoItem: Form widget to hold name:value
"""
# logger.debug(f"Key: {key}, Disable: {disable}")
if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type)
if key not in self.ignore:
match value:
case PydReagent():
@@ -272,7 +277,7 @@ class SubmissionFormWidget(QWidget):
"""
extraction_kit = args[0]
report = Report()
# logger.debug(f"Extraction kit: {extraction_kit}")
logger.debug(f"Extraction kit: {extraction_kit}")
# NOTE: Remove previous reagent widgets
try:
old_reagents = self.find_widgets()
@@ -284,7 +289,7 @@ class SubmissionFormWidget(QWidget):
if isinstance(reagent, self.ReagentFormWidget) or isinstance(reagent, QPushButton):
reagent.setParent(None)
reagents, integrity_report = self.pyd.check_kit_integrity(extraction_kit=extraction_kit)
# logger.debug(f"Missing reagents: {obj.missing_reagents}")
logger.debug(f"Got reagents: {pformat(reagents)}")
for reagent in reagents:
add_widget = self.ReagentFormWidget(parent=self, reagent=reagent, extraction_kit=self.pyd.extraction_kit)
self.layout.addWidget(add_widget)
@@ -454,9 +459,11 @@ class SubmissionFormWidget(QWidget):
class InfoItem(QWidget):
def __init__(self, parent: QWidget, key: str, value: dict, submission_type: str | None = None,
def __init__(self, parent: QWidget, key: str, value: dict, submission_type: str | SubmissionType | None = None,
sub_obj: BasicSubmission | None = None) -> None:
super().__init__(parent)
if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type)
layout = QVBoxLayout()
self.label = self.ParsedQLabel(key=key, value=value)
self.input: QWidget = self.set_widget(parent=parent, key=key, value=value, submission_type=submission_type,
@@ -497,7 +504,7 @@ class SubmissionFormWidget(QWidget):
return None, None
return self.input.objectName(), dict(value=value, missing=self.missing)
def set_widget(self, parent: QWidget, key: str, value: dict, submission_type: str | None = None,
def set_widget(self, parent: QWidget, key: str, value: dict, submission_type: str | SubmissionType | None = None,
sub_obj: BasicSubmission | None = None) -> QWidget:
"""
Creates form widget
@@ -511,8 +518,10 @@ class SubmissionFormWidget(QWidget):
Returns:
QWidget: Form object
"""
if isinstance(submission_type, str):
submission_type = SubmissionType.query(name=submission_type)
if sub_obj is None:
sub_obj = SubmissionType.query(name=submission_type).get_submission_class()
sub_obj = submission_type.get_submission_class()
try:
value = value['value']
except (TypeError, KeyError):
@@ -544,7 +553,8 @@ class SubmissionFormWidget(QWidget):
add_widget = MyQComboBox(scrollWidget=parent)
# NOTE: lookup existing kits by 'submission_type' decided on by sheetparser
# logger.debug(f"Looking up kits used for {submission_type}")
uses = [item.name for item in KitType.query(used_for=submission_type)]
# uses = [item.name for item in KitType.query(used_for=submission_type)]
uses = [item.name for item in submission_type.kit_types]
obj.uses = uses
# logger.debug(f"Kits received for {submission_type}: {uses}")
if check_not_nan(value):
@@ -668,7 +678,7 @@ class SubmissionFormWidget(QWidget):
dlg = QuestionAsker(title=f"Add {lot}?",
message=f"Couldn't find reagent type {self.reagent.role}: {lot} in the database.\n\nWould you like to add it?")
if dlg.exec():
wanted_reagent, _ = self.parent().parent().add_reagent(reagent_lot=lot,
wanted_reagent = self.parent().parent().add_reagent(reagent_lot=lot,
reagent_role=self.reagent.role,
expiry=self.reagent.expiry,
name=self.reagent.name)

View File

@@ -42,7 +42,6 @@ class Summary(QWidget):
self.setLayout(self.layout)
self.get_report()
def get_report(self):
orgs = [self.org_select.itemText(i) for i in range(self.org_select.count()) if self.org_select.itemChecked(i)]
if self.datepicker.start_date.date() > self.datepicker.end_date.date():

View File

@@ -17,13 +17,15 @@ from sqlalchemy import create_engine, text, MetaData
from pydantic import field_validator, BaseModel, Field
from pydantic_settings import BaseSettings, SettingsConfigDict
from typing import Any, Tuple, Literal, List
print(inspect.stack()[1])
# print(inspect.stack()[1])
from __init__ import project_path
from configparser import ConfigParser
from tkinter import Tk # NOTE: This is for choosing database path before app is created.
from tkinter.filedialog import askdirectory
from sqlalchemy.exc import IntegrityError as sqlalcIntegrityError
from pytz import timezone as tz
timezone = tz("America/Winnipeg")
logger = logging.getLogger(f"submissions.{__name__}")
@@ -386,16 +388,22 @@ class Settings(BaseSettings, extra="allow"):
case "sqlite":
value = f"/{values.data['database_path']}"
db_name = f"{values.data['database_name']}.db"
template = jinja_template_loading().from_string(
"{{ values['database_schema'] }}://{{ value }}/{{ db_name }}")
case "mssql+pyodbc":
value = values.data['database_path']
db_name = values.data['database_name']
template = jinja_template_loading().from_string(
"{{ values['database_schema'] }}://{{ value }}/{{ db_name }}?driver=ODBC+Driver+18+for+SQL+Server&TrustServerCertificate=yes&Trusted_Connection=yes"
)
case _:
# print(pprint.pprint(values.data))
tmp = jinja_template_loading().from_string(
"{% if values['database_user'] %}{{ values['database_user'] }}{% if values['database_password'] %}:{{ values['database_password'] }}{% endif %}{% endif %}@{{ values['database_path'] }}")
value = tmp.render(values=values.data)
db_name = values.data['database_name']
template = jinja_template_loading().from_string(
"{{ values['database_schema'] }}://{{ value }}/{{ db_name }}")
database_path = template.render(values=values.data, value=value, db_name=db_name)
# print(f"Using {database_path} for database path")
print(f"Using {database_path} for database path")
engine = create_engine(database_path)
session = Session(engine)
return session
@@ -939,8 +947,7 @@ def report_result(func):
"""
def wrapper(*args, **kwargs):
# logger.debug(f"Arguments: {args}")
# logger.debug(f"Keyword arguments: {kwargs}")
logger.debug(f"Report result being called by {func.__name__}")
output = func(*args, **kwargs)
match output:
case Report():
@@ -966,6 +973,12 @@ def report_result(func):
except Exception as e:
logger.error(f"Problem reporting due to {e}")
logger.error(result.msg)
# logger.debug(f"Returning: {output}")
return output
if output:
true_output = tuple(item for item in output if not isinstance(item, Report))
if len(true_output) == 1:
true_output = true_output[0]
else:
true_output = None
# logger.debug(f"Returning true output: {true_output}")
return true_output
return wrapper