From ba1b3e5cf3a5e83216ce5880f1f3765b6fefb253 Mon Sep 17 00:00:00 2001 From: lwark Date: Wed, 30 Oct 2024 07:34:39 -0500 Subject: [PATCH] Second round of code cleanup. --- CHANGELOG.md | 4 ++ requirements.txt | Bin 5812 -> 5812 bytes src/submissions/backend/db/models/__init__.py | 41 +++++++++++------- src/submissions/backend/db/models/controls.py | 24 +++++----- src/submissions/backend/db/models/kits.py | 4 ++ .../backend/db/models/submissions.py | 34 +++++++-------- src/submissions/backend/excel/parser.py | 36 ++++++++++----- src/submissions/backend/excel/writer.py | 4 +- src/submissions/backend/validators/pydant.py | 25 +++++++++-- .../frontend/visualizations/__init__.py | 11 ++--- .../frontend/visualizations/pcr_charts.py | 18 ++------ src/submissions/frontend/widgets/app.py | 5 ++- .../frontend/widgets/controls_chart.py | 2 +- .../frontend/widgets/equipment_usage.py | 8 ++-- .../frontend/widgets/gel_checker.py | 4 +- .../frontend/widgets/submission_table.py | 6 ++- .../frontend/widgets/submission_widget.py | 30 ++++++++----- src/submissions/frontend/widgets/summary.py | 1 - src/submissions/tools/__init__.py | 29 +++++++++---- 19 files changed, 176 insertions(+), 110 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e4144c..1a51282 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 202410.05 + +- Code clean up. + ## 202410.03 - Added code for cataloging of PCR controls. diff --git a/requirements.txt b/requirements.txt index 5426079caa60900d561b7ce8a357375ae0036b94..3aa00eaefa878aa1007b604c10ac8d0750cb92b7 100644 GIT binary patch delta 14 Vcmdm@yG3`y6k$fQ%~OTtnE@)&1kL~e delta 14 Vcmdm@yG3`y6k$e#%~OTtnE@)U1jqmY diff --git a/src/submissions/backend/db/models/__init__.py b/src/submissions/backend/db/models/__init__.py index 647d490..f110a9a 100644 --- a/src/submissions/backend/db/models/__init__.py +++ b/src/submissions/backend/db/models/__init__.py @@ -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 diff --git a/src/submissions/backend/db/models/controls.py b/src/submissions/backend/db/models/controls.py index 3ee7a94..dc5ab91 100644 --- a/src/submissions/backend/db/models/controls.py +++ b/src/submissions/backend/db/models/controls.py @@ -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 diff --git a/src/submissions/backend/db/models/kits.py b/src/submissions/backend/db/models/kits.py index 70c9105..9191513 100644 --- a/src/submissions/backend/db/models/kits.py +++ b/src/submissions/backend/db/models/kits.py @@ -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): diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index 410aa35..239a58b 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -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 diff --git a/src/submissions/backend/excel/parser.py b/src/submissions/backend/excel/parser.py index 9a685fb..6b6fd76 100644 --- a/src/submissions/backend/excel/parser.py +++ b/src/submissions/backend/excel/parser.py @@ -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 - diff --git a/src/submissions/backend/excel/writer.py b/src/submissions/backend/excel/writer.py index eeaff60..52d5505 100644 --- a/src/submissions/backend/excel/writer.py +++ b/src/submissions/backend/excel/writer.py @@ -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]: """ diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py index 3ac719d..4c7f339 100644 --- a/src/submissions/backend/validators/pydant.py +++ b/src/submissions/backend/validators/pydant.py @@ -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] diff --git a/src/submissions/frontend/visualizations/__init__.py b/src/submissions/frontend/visualizations/__init__.py index 0aca882..c4af2e9 100644 --- a/src/submissions/frontend/visualizations/__init__.py +++ b/src/submissions/frontend/visualizations/__init__.py @@ -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 = '' 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 += "

No data was retrieved for the given parameters.

" html += '' diff --git a/src/submissions/frontend/visualizations/pcr_charts.py b/src/submissions/frontend/visualizations/pcr_charts.py index 32435b1..58041f8 100644 --- a/src/submissions/frontend/visualizations/pcr_charts.py +++ b/src/submissions/frontend/visualizations/pcr_charts.py @@ -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}) diff --git a/src/submissions/frontend/widgets/app.py b/src/submissions/frontend/widgets/app.py index 9adaa85..d057583 100644 --- a/src/submissions/frontend/widgets/app.py +++ b/src/submissions/frontend/widgets/app.py @@ -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 diff --git a/src/submissions/frontend/widgets/controls_chart.py b/src/submissions/frontend/widgets/controls_chart.py index 1db1358..962b302 100644 --- a/src/submissions/frontend/widgets/controls_chart.py +++ b/src/submissions/frontend/widgets/controls_chart.py @@ -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...") diff --git a/src/submissions/frontend/widgets/equipment_usage.py b/src/submissions/frontend/widgets/equipment_usage.py index a17d99b..a96156f 100644 --- a/src/submissions/frontend/widgets/equipment_usage.py +++ b/src/submissions/frontend/widgets/equipment_usage.py @@ -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) diff --git a/src/submissions/frontend/widgets/gel_checker.py b/src/submissions/frontend/widgets/gel_checker.py index bf31278..ee8a1f8 100644 --- a/src/submissions/frontend/widgets/gel_checker.py +++ b/src/submissions/frontend/widgets/gel_checker.py @@ -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) diff --git a/src/submissions/frontend/widgets/submission_table.py b/src/submissions/frontend/widgets/submission_table.py index 3dc58fc..efc7bce 100644 --- a/src/submissions/frontend/widgets/submission_table.py +++ b/src/submissions/frontend/widgets/submission_table.py @@ -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) diff --git a/src/submissions/frontend/widgets/submission_widget.py b/src/submissions/frontend/widgets/submission_widget.py index 950497e..4b324ed 100644 --- a/src/submissions/frontend/widgets/submission_widget.py +++ b/src/submissions/frontend/widgets/submission_widget.py @@ -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) diff --git a/src/submissions/frontend/widgets/summary.py b/src/submissions/frontend/widgets/summary.py index 5a08348..0bd360f 100644 --- a/src/submissions/frontend/widgets/summary.py +++ b/src/submissions/frontend/widgets/summary.py @@ -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(): diff --git a/src/submissions/tools/__init__.py b/src/submissions/tools/__init__.py index 226d42b..2eba114 100644 --- a/src/submissions/tools/__init__.py +++ b/src/submissions/tools/__init__.py @@ -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