From 80527355d15af4a2e0895e7ff00faf3ede909212 Mon Sep 17 00:00:00 2001 From: lwark Date: Fri, 6 Dec 2024 12:02:39 -0600 Subject: [PATCH] Moments before disaster. --- src/config.yml | 3 +- src/submissions/backend/db/__init__.py | 8 +- src/submissions/backend/db/models/kits.py | 77 ++----------------- .../backend/db/models/submissions.py | 10 +-- src/submissions/backend/excel/parser.py | 36 +++------ src/submissions/backend/excel/reports.py | 15 +++- src/submissions/backend/excel/writer.py | 16 +--- src/submissions/backend/validators/pydant.py | 21 +---- .../frontend/visualizations/irida_charts.py | 1 - .../visualizations/turnaround_chart.py | 5 +- src/submissions/frontend/widgets/app.py | 30 +++----- .../frontend/widgets/controls_chart.py | 20 ++--- .../frontend/widgets/equipment_usage.py | 1 - .../frontend/widgets/gel_checker.py | 3 - src/submissions/frontend/widgets/info_tab.py | 42 +++++++--- src/submissions/frontend/widgets/misc.py | 51 ------------ .../frontend/widgets/omni_search.py | 13 +++- .../frontend/widgets/submission_details.py | 4 +- .../frontend/widgets/submission_table.py | 1 + .../frontend/widgets/submission_widget.py | 31 +++----- src/submissions/frontend/widgets/summary.py | 50 ++++-------- .../frontend/widgets/turnaround.py | 33 ++++---- src/submissions/tools/__init__.py | 11 ++- 23 files changed, 157 insertions(+), 325 deletions(-) diff --git a/src/config.yml b/src/config.yml index f34198d..5155c71 100644 --- a/src/config.yml +++ b/src/config.yml @@ -3,4 +3,5 @@ database_path: null database_schema: null database_user: null database_password: null -database_name: null \ No newline at end of file +database_name: null +logging_enabled: false \ No newline at end of file diff --git a/src/submissions/backend/db/__init__.py b/src/submissions/backend/db/__init__.py index 8a73e24..502da09 100644 --- a/src/submissions/backend/db/__init__.py +++ b/src/submissions/backend/db/__init__.py @@ -1,10 +1,8 @@ """ All database related operations. """ -import sqlalchemy.orm from sqlalchemy import event, inspect from sqlalchemy.engine import Engine - from tools import ctx @@ -48,7 +46,11 @@ def update_log(mapper, connection, target): hist = attr.load_history() if not hist.has_changes(): continue + if attr.key == "custom": + continue added = [str(item) for item in hist.added] + if attr.key in ['submission_sample_associations', 'submission_reagent_associations']: + added = ['Numbers truncated for space purposes.'] deleted = [str(item) for item in hist.deleted] change = dict(field=attr.key, added=added, deleted=deleted) # logger.debug(f"Adding: {pformat(change)}") @@ -69,6 +71,6 @@ def update_log(mapper, connection, target): else: logger.info(f"No changes detected, not updating logs.") -# if ctx.database_schema == "sqlite": +# if ctx.logging_enabled: event.listen(LogMixin, 'after_update', update_log, propagate=True) event.listen(LogMixin, 'after_insert', update_log, propagate=True) diff --git a/src/submissions/backend/db/models/kits.py b/src/submissions/backend/db/models/kits.py index d0daf54..2f18dd3 100644 --- a/src/submissions/backend/db/models/kits.py +++ b/src/submissions/backend/db/models/kits.py @@ -1414,25 +1414,6 @@ class Equipment(BaseClass): if extraction_kit and extraction_kit not in process.kit_types: continue yield process - # processes = (process for process in self.processes if submission_type in process.submission_types) - # match extraction_kit: - # case str(): - # # logger.debug(f"Filtering processes by extraction_kit str {extraction_kit}") - # processes = (process for process in processes if - # extraction_kit in [kit.name for kit in process.kit_types]) - # case KitType(): - # # logger.debug(f"Filtering processes by extraction_kit KitType {extraction_kit}") - # processes = (process for process in processes if extraction_kit in process.kit_types) - # case _: - # pass - # # NOTE: Convert to strings - # # processes = [process.name for process in processes] - # # assert all([isinstance(process, str) for process in processes]) - # # if len(processes) == 0: - # # processes = [''] - # # return processes - # for process in processes: - # yield process.name @classmethod @setup_lookup @@ -1650,25 +1631,6 @@ class EquipmentRole(BaseClass): if extraction_kit and extraction_kit not in process.kit_types: continue yield process.name - # if submission_type is not None: - # # logger.debug("Getting all processes for this EquipmentRole") - # processes = [process for process in self.processes if submission_type in process.submission_types] - # else: - # processes = self.processes - # match extraction_kit: - # case str(): - # # logger.debug(f"Filtering processes by extraction_kit str {extraction_kit}") - # processes = [item for item in processes if extraction_kit in [kit.name for kit in item.kit_types]] - # case KitType(): - # # logger.debug(f"Filtering processes by extraction_kit KitType {extraction_kit}") - # processes = [item for item in processes if extraction_kit in [kit for kit in item.kit_types]] - # case _: - # pass - # output = [item.name for item in processes] - # if len(output) == 0: - # return [''] - # else: - # return output def to_export_dict(self, submission_type: SubmissionType, kit_type: KitType): """ @@ -1730,9 +1692,8 @@ class SubmissionEquipmentAssociation(BaseClass): @classmethod @setup_lookup - def query(cls, equipment_id: int, submission_id: int, role: str | None = None, limit: int = 0, **kwargs) -> Any | \ - List[ - Any]: + def query(cls, equipment_id: int, submission_id: int, role: str | None = None, limit: int = 0, **kwargs) \ + -> Any | List[Any]: query: Query = cls.__database_session__.query(cls) query = query.filter(cls.equipment_id == equipment_id) query = query.filter(cls.submission_id == submission_id) @@ -1777,44 +1738,22 @@ class SubmissionTypeEquipmentRoleAssociation(BaseClass): raise ValueError(f'Invalid required value {value}. Must be 0 or 1.') return value - def get_all_processes(self, extraction_kit: KitType | str | None = None) -> List[Process]: - """ - Get all processes associated with this SubmissionTypeEquipmentRole - - Args: - extraction_kit (KitType | str | None, optional): KitType of interest. Defaults to None. - - Returns: - List[Process]: All associated processes - """ - processes = [equipment.get_processes(self.submission_type) for equipment in self.equipment_role.instances] - # NOTE: flatten list - processes = [item for items in processes for item in items if item is not None] - match extraction_kit: - case str(): - # logger.debug(f"Filtering Processes by extraction_kit str {extraction_kit}") - processes = [item for item in processes if extraction_kit in [kit.name for kit in item.kit_type]] - case KitType(): - # logger.debug(f"Filtering Processes by extraction_kit KitType {extraction_kit}") - processes = [item for item in processes if extraction_kit in [kit for kit in item.kit_type]] - case _: - pass - return processes - @check_authorization def save(self): super().save() - def to_export_dict(self, extraction_kit: KitType) -> dict: + def to_export_dict(self, extraction_kit: KitType | str) -> dict: """ Creates dictionary for exporting to yml used in new SubmissionType Construction Args: - kit_type (KitType): KitType of interest. + extraction_kit (KitType | str): KitType of interest. Returns: dict: Dictionary containing relevant info for SubmissionType construction """ + if isinstance(extraction_kit, str): + extraction_kit = KitType.query(name=extraction_kit) base_dict = {k: v for k, v in self.equipment_role.to_export_dict(submission_type=self.submission_type, kit_type=extraction_kit).items()} base_dict['static'] = self.static @@ -2013,8 +1952,8 @@ class SubmissionTipsAssociation(BaseClass): @classmethod @setup_lookup - def query(cls, tip_id: int, role: str, submission_id: int | None = None, limit: int = 0, **kwargs) -> Any | List[ - Any]: + def query(cls, tip_id: int, role: str, submission_id: int | None = None, limit: int = 0, **kwargs) \ + -> Any | List[Any]: query: Query = cls.__database_session__.query(cls) query = query.filter(cls.tip_id == tip_id) if submission_id is not None: diff --git a/src/submissions/backend/db/models/submissions.py b/src/submissions/backend/db/models/submissions.py index e18121b..6bbbe31 100644 --- a/src/submissions/backend/db/models/submissions.py +++ b/src/submissions/backend/db/models/submissions.py @@ -2,13 +2,10 @@ Models for the main submission and sample types. """ from __future__ import annotations -# import sys -# import types -# import zipfile from copy import deepcopy from getpass import getuser import logging, uuid, tempfile, re, base64, numpy as np, pandas as pd, types, sys -from zipfile import ZipFile +from zipfile import ZipFile, BadZipfile from tempfile import TemporaryDirectory, TemporaryFile from operator import itemgetter from pprint import pformat @@ -20,7 +17,6 @@ from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.exc import OperationalError as AlcOperationalError, IntegrityError as AlcIntegrityError, StatementError, \ ArgumentError from sqlite3 import OperationalError as SQLOperationalError, IntegrityError as SQLIntegrityError -# import pandas as pd from openpyxl import Workbook from openpyxl.drawing.image import Image as OpenpyxlImage from tools import row_map, setup_lookup, jinja_template_loading, rreplace, row_keys, check_key_or_attr, Result, Report, \ @@ -1276,7 +1272,7 @@ class BasicSubmission(BaseClass, LogMixin): if msg.exec(): try: self.backup(fname=fname, full_backup=True) - except zipfile.BadZipfile: + except BadZipfile: logger.error("Couldn't open zipfile for writing.") self.__database_session__.delete(self) try: @@ -2261,7 +2257,7 @@ class WastewaterArtic(BasicSubmission): # Sample Classes -class BasicSample(BaseClass): +class BasicSample(BaseClass, LogMixin): """ Base of basic sample which polymorphs into BCSample and WWSample """ diff --git a/src/submissions/backend/excel/parser.py b/src/submissions/backend/excel/parser.py index 1971400..42a5999 100644 --- a/src/submissions/backend/excel/parser.py +++ b/src/submissions/backend/excel/parser.py @@ -1,8 +1,7 @@ ''' contains parser objects for pulling values from client generated submission sheets. ''' -import json -import sys +import logging from copy import copy from getpass import getuser from pprint import pformat @@ -11,7 +10,6 @@ from openpyxl import load_workbook, Workbook from pathlib import Path from backend.db.models import * from backend.validators import PydSubmission, RSLNamer -import logging, re from collections import OrderedDict from tools import check_not_nan, is_missing, check_key_or_attr @@ -195,7 +193,7 @@ class InfoParser(object): ws = self.xl[sheet] relevant = [] for k, v in self.map.items(): - # NOTE: If the value is hardcoded put it in the dictionary directly. + # NOTE: If the value is hardcoded put it in the dictionary directly. Ex. Artic kit if k == "custom": continue if isinstance(v, str): @@ -230,7 +228,7 @@ class InfoParser(object): case "submitted_date": value, missing = is_missing(value) logger.debug(f"Parsed submitted date: {value}") - # NOTE: is field a JSON? + # NOTE: is field a JSON? Includes: Extraction info, PCR info, comment, custom case thing if thing in self.sub_object.jsons(): value, missing = is_missing(value) if missing: continue @@ -300,11 +298,11 @@ class ReagentParser(object): del reagent_map['info'] except KeyError: pass - logger.debug(f"Reagent map: {pformat(reagent_map)}") + # logger.debug(f"Reagent map: {pformat(reagent_map)}") # NOTE: If reagent map is empty, maybe the wrong kit was given, check if there's only one kit for that submission type and use it if so. if not reagent_map: temp_kit_object = self.submission_type_obj.get_default_kit() - logger.debug(f"Temp kit: {temp_kit_object}") + # logger.debug(f"Temp kit: {temp_kit_object}") if temp_kit_object: self.kit_object = temp_kit_object # reagent_map = {k: v for k, v in self.kit_object.construct_xl_map_for_use(submission_type)} @@ -333,7 +331,7 @@ class ReagentParser(object): for sheet in self.xl.sheetnames: ws = self.xl[sheet] relevant = {k.strip(): v for k, v in self.map.items() if sheet in self.map[k]['sheet']} - logger.debug(f"relevant map for {sheet}: {pformat(relevant)}") + # logger.debug(f"relevant map for {sheet}: {pformat(relevant)}") if relevant == {}: continue for item in relevant: @@ -499,8 +497,7 @@ 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]) + logger.info(f"Merging sample info using {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): @@ -517,20 +514,9 @@ class SampleParser(object): 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'] - # except KeyError: - # check = False - # if check: - # new = lsample | psample - # lookup_samples[jj] = {} - # break - # else: - # new = 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}") + # 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): @@ -554,7 +540,7 @@ class EquipmentParser(object): xl (Workbook): Openpyxl workbook from submitted excel file. submission_type (str | SubmissionType): Type of submission expected (Wastewater, Bacterial Culture, etc.) """ - logger.debug("\n\nHello from EquipmentParser!\n\n") + logger.info("\n\nHello from EquipmentParser!\n\n") if isinstance(submission_type, str): submission_type = SubmissionType.query(name=submission_type) self.submission_type = submission_type @@ -568,7 +554,6 @@ class EquipmentParser(object): Returns: List[dict]: List of locations """ - # return {k: v for k, v in self.submission_type.construct_equipment_map()} return {k: v for k, v in self.submission_type.construct_field_map("equipment")} def get_asset_number(self, input: str) -> str: @@ -638,7 +623,7 @@ class TipParser(object): xl (Workbook): Openpyxl workbook from submitted excel file. submission_type (str | SubmissionType): Type of submission expected (Wastewater, Bacterial Culture, etc.) """ - logger.debug("\n\nHello from TipParser!\n\n") + logger.info("\n\nHello from TipParser!\n\n") if isinstance(submission_type, str): submission_type = SubmissionType.query(name=submission_type) self.submission_type = submission_type @@ -652,7 +637,6 @@ class TipParser(object): Returns: List[dict]: List of locations """ - # return {k: v for k, v in self.submission_type.construct_tips_map()} return {k: v for k, v in self.submission_type.construct_field_map("tip")} def parse_tips(self) -> List[dict]: diff --git a/src/submissions/backend/excel/reports.py b/src/submissions/backend/excel/reports.py index 44d88b4..6e04219 100644 --- a/src/submissions/backend/excel/reports.py +++ b/src/submissions/backend/excel/reports.py @@ -2,7 +2,6 @@ Contains functions for generating summary reports ''' from pprint import pformat - from pandas import DataFrame, ExcelWriter import logging from pathlib import Path @@ -72,7 +71,6 @@ class ReportMaker(object): for row in df.iterrows(): # logger.debug(f"Row {ii}: {row}") lab = row[0][0] - # logger.debug(type(row)) # logger.debug(f"Old lab: {old_lab}, Current lab: {lab}") # logger.debug(f"Name: {row[0][1]}") data = [item for item in row[1]] @@ -151,7 +149,16 @@ class TurnaroundMaker(object): self.df = DataFrame.from_records(records) @classmethod - def build_record(cls, sub): + def build_record(cls, sub: BasicSubmission) -> dict: + """ + Build a turnaround dictionary from a submission + + Args: + sub (BasicSubmission): The submission to be processed. + + Returns: + + """ days, tat_ok = sub.get_turnaround_time() return dict(name=str(sub.rsl_plate_num), days=days, submitted_date=sub.submitted_date, completed_date=sub.completed_date, acceptable=tat_ok) @@ -170,4 +177,4 @@ class TurnaroundMaker(object): self.writer = ExcelWriter(filename.with_suffix(".xlsx"), engine='openpyxl') self.df.to_excel(self.writer, sheet_name="Turnaround") # logger.debug(f"Writing report to: {filename}") - self.writer.close() \ No newline at end of file + self.writer.close() diff --git a/src/submissions/backend/excel/writer.py b/src/submissions/backend/excel/writer.py index a49eb0a..c13bf55 100644 --- a/src/submissions/backend/excel/writer.py +++ b/src/submissions/backend/excel/writer.py @@ -41,21 +41,11 @@ class SheetWriter(object): self.sub[k] = v['value'] else: self.sub[k] = v - # logger.debug(f"\n\nWriting to {submission.filepath.__str__()}\n\n") - # if self.filepath.stem.startswith("tmp"): - # template = self.submission_type.template_file - # workbook = load_workbook(BytesIO(template)) - # else: - # try: - # workbook = load_workbook(self.filepath) - # except Exception as e: - # logger.error(f"Couldn't open workbook due to {e}") template = self.submission_type.template_file if not template: logger.error(f"No template file found, falling back to Bacterial Culture") template = SubmissionType.retrieve_template_file() workbook = load_workbook(BytesIO(template)) - # self.workbook = workbook self.xl = workbook self.write_info() self.write_reagents() @@ -152,11 +142,9 @@ class InfoWriter(object): try: dicto['locations'] = info_map[k] except KeyError: - # continue pass dicto['value'] = v if len(dicto) > 0: - # output[k] = dicto yield k, dicto def write_info(self) -> Workbook: @@ -279,7 +267,6 @@ 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=itemgetter('submission_rank')) def reconcile_map(self, sample_list: list) -> Generator[dict, None, None]: @@ -368,7 +355,8 @@ class EquipmentWriter(object): logger.error(f"No {equipment['role']} in {pformat(equipment_map)}") # logger.debug(f"{equipment['role']} map: {mp_info}") placeholder = copy(equipment) - if mp_info == {}: + # if mp_info == {}: + if not mp_info: for jj, (k, v) in enumerate(equipment.items(), start=1): dicto = dict(value=v, row=ii, column=jj) placeholder[k] = dicto diff --git a/src/submissions/backend/validators/pydant.py b/src/submissions/backend/validators/pydant.py index 82f3f24..7ffd234 100644 --- a/src/submissions/backend/validators/pydant.py +++ b/src/submissions/backend/validators/pydant.py @@ -2,8 +2,7 @@ Contains pydantic models and accompanying validators ''' from __future__ import annotations -import sys -import uuid, re, logging, csv +import uuid, re, logging, csv, sys from pydantic import BaseModel, field_validator, Field, model_validator from datetime import date, datetime, timedelta from dateutil.parser import parse @@ -165,13 +164,7 @@ class PydReagent(BaseModel): report.add_result(Result(owner=__name__, code=0, msg="New reagent created.", status="Information")) else: if submission is not None and reagent not in submission.reagents: - # assoc = SubmissionReagentAssociation(reagent=reagent, submission=submission) - # assoc.comments = self.comment submission.update_reagentassoc(reagent=reagent, role=self.role) - # else: - # assoc = None - # add end-of-life extension from reagent type to expiry date - # NOTE: this will now be done only in the reporting phase to account for potential changes in end-of-life extensions return reagent, report @@ -191,11 +184,7 @@ 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 @@ -379,7 +368,6 @@ class PydEquipment(BaseModel, extra='ignore'): role=self.role, limit=1) except TypeError as e: logger.error(f"Couldn't get association due to {e}, returning...") - # return equipment, None assoc = None if assoc is None: assoc = SubmissionEquipmentAssociation(submission=submission, equipment=equipment) @@ -830,11 +818,6 @@ class PydSubmission(BaseModel, extra='allow'): logger.debug(f"Checking reagent {reagent.lot}") reagent, _ = reagent.toSQL(submission=instance) # logger.debug(f"Association: {assoc}") - # if assoc is not None: # and assoc not in instance.submission_reagent_associations: - # if assoc not in instance.submission_reagent_associations: - # instance.submission_reagent_associations.append(assoc) - # else: - # logger.warning(f"Reagent association {assoc} is already present in {instance.submission_reagent_associations}") case "samples": for sample in self.samples: sample, associations, _ = sample.toSQL(submission=instance) @@ -871,7 +854,6 @@ class PydSubmission(BaseModel, extra='allow'): logger.warning(f"Tips association {association} is already present in {instance}") 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) @@ -903,7 +885,6 @@ class PydSubmission(BaseModel, extra='allow'): if check: try: instance.set_attribute(key=key, value=value) - # instance.update({key:value}) except AttributeError as e: logger.error(f"Could not set attribute: {key} to {value} due to: \n\n {e}") continue diff --git a/src/submissions/frontend/visualizations/irida_charts.py b/src/submissions/frontend/visualizations/irida_charts.py index eb8cd9d..42e2172 100644 --- a/src/submissions/frontend/visualizations/irida_charts.py +++ b/src/submissions/frontend/visualizations/irida_charts.py @@ -40,7 +40,6 @@ class IridaFigure(CustomFigure): Returns: Figure: output stacked bar chart. """ - # fig = Figure() for ii, mode in enumerate(modes): if "count" in mode: df[mode] = pd.to_numeric(df[mode], errors='coerce') diff --git a/src/submissions/frontend/visualizations/turnaround_chart.py b/src/submissions/frontend/visualizations/turnaround_chart.py index e1b8d21..32f170f 100644 --- a/src/submissions/frontend/visualizations/turnaround_chart.py +++ b/src/submissions/frontend/visualizations/turnaround_chart.py @@ -1,8 +1,10 @@ +""" +Construct turnaround time charts +""" from pprint import pformat from . import CustomFigure import plotly.express as px import pandas as pd -import numpy as np from PyQt6.QtWidgets import QWidget import logging @@ -25,7 +27,6 @@ class TurnaroundChart(CustomFigure): self.construct_chart() if threshold: self.add_hline(y=threshold) - # self.update_xaxes() self.update_layout(showlegend=False) def construct_chart(self, df: pd.DataFrame | None = None): diff --git a/src/submissions/frontend/widgets/app.py b/src/submissions/frontend/widgets/app.py index b0efa6b..5167855 100644 --- a/src/submissions/frontend/widgets/app.py +++ b/src/submissions/frontend/widgets/app.py @@ -1,7 +1,6 @@ """ Constructs main application. """ -import os from pprint import pformat from PyQt6.QtCore import qInstallMessageHandler from PyQt6.QtWidgets import ( @@ -18,12 +17,11 @@ from tools import check_if_app, Settings, Report, jinja_template_loading, check_ from .functions import select_save_file, select_open_file from datetime import date from .pop_ups import HTMLPop, AlertPop -from .misc import LogParser, Pagifier +from .misc import Pagifier import logging, webbrowser, sys, shutil from .submission_table import SubmissionsSheet from .submission_widget import SubmissionFormContainer from .controls_chart import ControlsViewer -# from .sample_search import SampleSearchBox from .summary import Summary from .turnaround import TurnaroundTime from .omni_search import SearchBox @@ -84,7 +82,7 @@ class App(QMainWindow): fileMenu.addAction(self.importAction) fileMenu.addAction(self.yamlExportAction) fileMenu.addAction(self.yamlImportAction) - methodsMenu.addAction(self.searchLog) + # methodsMenu.addAction(self.searchLog) methodsMenu.addAction(self.searchSample) maintenanceMenu.addAction(self.joinExtractionAction) maintenanceMenu.addAction(self.joinPCRAction) @@ -98,8 +96,8 @@ class App(QMainWindow): toolbar = QToolBar("My main toolbar") self.addToolBar(toolbar) toolbar.addAction(self.addReagentAction) - toolbar.addAction(self.addKitAction) - toolbar.addAction(self.addOrgAction) + # toolbar.addAction(self.addKitAction) + # toolbar.addAction(self.addOrgAction) def _createActions(self): """ @@ -108,13 +106,13 @@ class App(QMainWindow): # logger.debug(f"Creating actions...") self.importAction = QAction("&Import Submission", self) self.addReagentAction = QAction("Add Reagent", self) - self.addKitAction = QAction("Import Kit", self) - self.addOrgAction = QAction("Import Org", self) + # self.addKitAction = QAction("Import Kit", self) + # self.addOrgAction = QAction("Import Org", self) self.joinExtractionAction = QAction("Link Extraction Logs") self.joinPCRAction = QAction("Link PCR Logs") self.helpAction = QAction("&About", self) self.docsAction = QAction("&Docs", self) - self.searchLog = QAction("Search Log", self) + # self.searchLog = QAction("Search Log", self) self.searchSample = QAction("Search Sample", self) self.githubAction = QAction("Github", self) self.yamlExportAction = QAction("Export Type Example", self) @@ -132,14 +130,13 @@ class App(QMainWindow): self.joinPCRAction.triggered.connect(self.table_widget.sub_wid.link_pcr) self.helpAction.triggered.connect(self.showAbout) self.docsAction.triggered.connect(self.openDocs) - self.searchLog.triggered.connect(self.runSearch) + # self.searchLog.triggered.connect(self.runSearch) self.searchSample.triggered.connect(self.runSampleSearch) self.githubAction.triggered.connect(self.openGithub) self.yamlExportAction.triggered.connect(self.export_ST_yaml) self.yamlImportAction.triggered.connect(self.import_ST_yaml) self.table_widget.pager.current_page.textChanged.connect(self.update_data) self.editReagentAction.triggered.connect(self.edit_reagent) - self.destroyed.connect(self.final_commit) def showAbout(self): """ @@ -180,15 +177,14 @@ class App(QMainWindow): instr = HTMLPop(html=html, title="Instructions") instr.exec() - def runSearch(self): - dlg = LogParser(self) - dlg.exec() + # def runSearch(self): + # dlg = LogParser(self) + # dlg.exec() def runSampleSearch(self): """ Create a search for samples. """ - # dlg = SampleSearchBox(self) dlg = SearchBox(self, object_type=BasicSample, extras=[]) dlg.exec() @@ -244,7 +240,6 @@ class App(QMainWindow): st = SubmissionType.import_from_json(filepath=fname) if st: # NOTE: Do not delete the print statement below. - # print(pformat(st.to_export_dict())) choice = input("Save the above submission type? [y/N]: ") if choice.lower() == "y": pass @@ -254,9 +249,6 @@ class App(QMainWindow): def update_data(self): self.table_widget.sub_wid.setData(page=self.table_widget.pager.page_anchor, page_size=page_size) - def final_commit(self): - logger.debug("Running final commit") - self.ctx.database_session.commit() class AddSubForm(QWidget): diff --git a/src/submissions/frontend/widgets/controls_chart.py b/src/submissions/frontend/widgets/controls_chart.py index d5483a9..cd88b6e 100644 --- a/src/submissions/frontend/widgets/controls_chart.py +++ b/src/submissions/frontend/widgets/controls_chart.py @@ -10,9 +10,10 @@ from PyQt6.QtWidgets import ( from PyQt6.QtCore import QSignalBlocker from backend.db import ControlType, IridaControl import logging -from tools import Report, report_result +from tools import Report, report_result, Result from frontend.visualizations import CustomFigure from .misc import StartEndDatePicker +from .info_tab import InfoPane logger = logging.getLogger(f"submissions.{__name__}") @@ -37,11 +38,11 @@ class ControlsViewer(QWidget): # NOTE: fetch types of controls con_sub_types = [item for item in self.archetype.targets.keys()] self.control_sub_typer.addItems(con_sub_types) - # NOTE: create custom widget to get types of analysis + # NOTE: create custom widget to get types of analysis -- disabled by PCR control self.mode_typer = QComboBox() mode_types = IridaControl.get_modes() self.mode_typer.addItems(mode_types) - # NOTE: create custom widget to get subtypes of analysis + # NOTE: create custom widget to get subtypes of analysis -- disabled by PCR control self.mode_sub_typer = QComboBox() self.mode_sub_typer.setEnabled(False) # NOTE: add widgets to tab2 layout @@ -83,15 +84,15 @@ class ControlsViewer(QWidget): pass # NOTE: correct start date being more recent than end date and rerun if self.datepicker.start_date.date() > self.datepicker.end_date.date(): - logger.warning("Start date after end date is not allowed!") threemonthsago = self.datepicker.end_date.date().addDays(-60) - # NOTE: block signal that will rerun controls getter and set start date - # Without triggering this function again + msg = f"Start date after end date is not allowed! Setting to {threemonthsago.toString()}." + logger.warning(msg) + # NOTE: block signal that will rerun controls getter and set start date Without triggering this function again with QSignalBlocker(self.datepicker.start_date) as blocker: self.datepicker.start_date.setDate(threemonthsago) - self.controls_getter() - self.report.add_result(report) - return + self.controls_getter_function() + report.add_result(Result(owner=self.__str__(), msg=msg, status="Warning")) + return report # NOTE: convert to python useable date objects self.start_date = self.datepicker.start_date.date().toPyDate() self.end_date = self.datepicker.end_date.date().toPyDate() @@ -167,4 +168,3 @@ class ControlsViewer(QWidget): self.webengineview.update() # logger.debug("Figure updated... I hope.") return report - diff --git a/src/submissions/frontend/widgets/equipment_usage.py b/src/submissions/frontend/widgets/equipment_usage.py index a96156f..adb0fa7 100644 --- a/src/submissions/frontend/widgets/equipment_usage.py +++ b/src/submissions/frontend/widgets/equipment_usage.py @@ -1,7 +1,6 @@ ''' Creates forms that the user can enter equipment info into. ''' -import time from pprint import pformat from PyQt6.QtCore import Qt, QSignalBlocker from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox, diff --git a/src/submissions/frontend/widgets/gel_checker.py b/src/submissions/frontend/widgets/gel_checker.py index ee8a1f8..7dc3a7c 100644 --- a/src/submissions/frontend/widgets/gel_checker.py +++ b/src/submissions/frontend/widgets/gel_checker.py @@ -66,7 +66,6 @@ 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=itemgetter('location')) except KeyError: control_info = None @@ -79,7 +78,6 @@ class GelBox(QDialog): layout.addWidget(self.buttonBox, 23, 1, 1, 1) self.setLayout(layout) - def parse_form(self) -> Tuple[str, str | Path, list]: """ Get relevant values from self/form @@ -124,7 +122,6 @@ class ControlsForm(QWidget): self.layout.addWidget(label, iii, 1, 1, 1) for iii in range(3): for jjj in range(3): - # widge = QLineEdit() widge = QComboBox() widge.addItems(['Neg', 'Pos']) widge.setCurrentIndex(0) diff --git a/src/submissions/frontend/widgets/info_tab.py b/src/submissions/frontend/widgets/info_tab.py index 679e170..ff582cf 100644 --- a/src/submissions/frontend/widgets/info_tab.py +++ b/src/submissions/frontend/widgets/info_tab.py @@ -1,6 +1,11 @@ +""" +A pane to show info e.g. cost reports and turnaround times. +TODO: Can I merge this with the controls chart pane? +""" +from PyQt6.QtCore import QSignalBlocker from PyQt6.QtWebEngineWidgets import QWebEngineView -from PyQt6.QtWidgets import QWidget, QGridLayout, QPushButton -from tools import Report +from PyQt6.QtWidgets import QWidget, QGridLayout +from tools import Report, report_result, Result from .misc import StartEndDatePicker, save_pdf from .functions import select_save_file import logging @@ -17,22 +22,29 @@ class InfoPane(QWidget): self.report = Report() self.datepicker = StartEndDatePicker(default_start=-31) self.webview = QWebEngineView() - self.datepicker.start_date.dateChanged.connect(self.date_changed) - self.datepicker.end_date.dateChanged.connect(self.date_changed) + self.datepicker.start_date.dateChanged.connect(self.update_data) + self.datepicker.end_date.dateChanged.connect(self.update_data) self.layout = QGridLayout(self) self.layout.addWidget(self.datepicker, 0, 0, 1, 2) - self.save_excel_button = QPushButton("Save Excel", parent=self) - self.save_excel_button.pressed.connect(self.save_excel) - self.save_pdf_button = QPushButton("Save PDF", parent=self) - self.save_pdf_button.pressed.connect(self.save_pdf) - self.layout.addWidget(self.save_excel_button, 0, 2, 1, 1) - self.layout.addWidget(self.save_pdf_button, 0, 3, 1, 1) self.layout.addWidget(self.webview, 2, 0, 1, 4) self.setLayout(self.layout) - def date_changed(self): + @report_result + def update_data(self, *args, **kwargs): + report = Report() self.start_date = self.datepicker.start_date.date().toPyDate() self.end_date = self.datepicker.end_date.date().toPyDate() + if self.datepicker.start_date.date() > self.datepicker.end_date.date(): + lastmonth = self.datepicker.end_date.date().addDays(-31) + msg = f"Start date after end date is not allowed! Setting to {lastmonth.toString()}." + logger.warning(msg) + # NOTE: block signal that will rerun controls getter and set start date + # Without triggering this function again + with QSignalBlocker(self.datepicker.start_date) as blocker: + self.datepicker.start_date.setDate(lastmonth) + self.update_data() + report.add_result(Result(owner=self.__str__(), msg=msg, status="Warning")) + return report def save_excel(self): fname = select_save_file(self, default_name=f"Report {self.start_date.strftime('%Y%m%d')} - {self.end_date.strftime('%Y%m%d')}", extension="xlsx") @@ -42,4 +54,10 @@ class InfoPane(QWidget): fname = select_save_file(obj=self, default_name=f"Report {self.start_date.strftime('%Y%m%d')} - {self.end_date.strftime('%Y%m%d')}", extension="pdf") - save_pdf(obj=self.webview, filename=fname) \ No newline at end of file + save_pdf(obj=self.webview, filename=fname) + + def save_png(self): + fname = select_save_file(obj=self, + default_name=f"Plotly {self.start_date.strftime('%Y%m%d')} - {self.end_date.strftime('%Y%m%d')}", + extension="png") + self.fig.write_image(fname.absolute().__str__(), engine="kaleido") \ No newline at end of file diff --git a/src/submissions/frontend/widgets/misc.py b/src/submissions/frontend/widgets/misc.py index 177594e..938928e 100644 --- a/src/submissions/frontend/widgets/misc.py +++ b/src/submissions/frontend/widgets/misc.py @@ -14,8 +14,6 @@ from PyQt6.QtCore import Qt, QDate, QSize, QMarginsF from tools import jinja_template_loading from backend.db.models import * import logging -from .pop_ups import AlertPop -from .functions import select_open_file logger = logging.getLogger(f"submissions.{__name__}") @@ -114,55 +112,6 @@ class AddReagentForm(QDialog): self.name_input.addItems(list(set([item.name for item in lookup]))) -class LogParser(QDialog): - - def __init__(self, parent): - super().__init__(parent) - self.app = self.parent() - self.filebutton = QPushButton(self) - self.filebutton.setText("Import File") - self.phrase_looker = QComboBox(self) - self.phrase_looker.setEditable(True) - self.btn = QPushButton(self) - self.btn.setText("Search") - self.layout = QFormLayout(self) - self.layout.addRow(self.tr("&File:"), self.filebutton) - self.layout.addRow(self.tr("&Search Term:"), self.phrase_looker) - self.layout.addRow(self.btn) - self.filebutton.clicked.connect(self.filelookup) - self.btn.clicked.connect(self.runsearch) - self.setMinimumWidth(400) - - def filelookup(self): - """ - Select file to search - """ - self.fname = select_open_file(self, "tabular") - - def runsearch(self): - """ - Gets total/percent occurences of string in tabular file. - """ - count: int = 0 - total: int = 0 - # logger.debug(f"Current search term: {self.phrase_looker.currentText()}") - try: - with open(self.fname, "r") as f: - for chunk in readInChunks(fileObj=f): - total += len(chunk) - for line in chunk: - if self.phrase_looker.currentText().lower() in line.lower(): - count += 1 - percent = (count / total) * 100 - msg = f"I found {count} instances of the search phrase out of {total} = {percent:.2f}%." - status = "Information" - except AttributeError: - msg = f"No file was selected." - status = "Error" - dlg = AlertPop(message=msg, status=status) - dlg.exec() - - class StartEndDatePicker(QWidget): """ custom widget to pick start and end dates for controls graphs diff --git a/src/submissions/frontend/widgets/omni_search.py b/src/submissions/frontend/widgets/omni_search.py index 82da54d..2774f04 100644 --- a/src/submissions/frontend/widgets/omni_search.py +++ b/src/submissions/frontend/widgets/omni_search.py @@ -16,6 +16,9 @@ logger = logging.getLogger(f"submissions.{__name__}") class SearchBox(QDialog): + """ + The full search widget. + """ def __init__(self, parent, object_type: Any, extras: List[str], **kwargs): super().__init__(parent) @@ -36,7 +39,7 @@ class SearchBox(QDialog): else: self.sub_class = None self.results = SearchResults(parent=self, object_type=self.object_type, extras=self.extras, **kwargs) - logger.debug(f"results: {self.results}") + # logger.debug(f"results: {self.results}") self.layout.addWidget(self.results, 5, 0) self.setLayout(self.layout) self.setWindowTitle(f"Search {self.object_type.__name__}") @@ -51,6 +54,7 @@ class SearchBox(QDialog): # logger.debug(deletes) for item in deletes: item.setParent(None) + # NOTE: Handle any subclasses if not self.sub_class: self.update_data() else: @@ -89,6 +93,9 @@ class SearchBox(QDialog): class FieldSearch(QWidget): + """ + Search bar. + """ def __init__(self, parent, label, field_name): super().__init__(parent) @@ -115,6 +122,9 @@ class FieldSearch(QWidget): class SearchResults(QTableView): + """ + Results table. + """ def __init__(self, parent: SearchBox, object_type: Any, extras: List[str], **kwargs): super().__init__() @@ -146,7 +156,6 @@ class SearchResults(QTableView): context = {item['name']: x.sibling(x.row(), item['column']).data() for item in self.columns_of_interest} logger.debug(f"Context: {context}") try: - # object = self.object_type.query(**{self.object_type.searchables: context[self.object_type.searchables]}) object = self.object_type.query(**context) except KeyError: object = None diff --git a/src/submissions/frontend/widgets/submission_details.py b/src/submissions/frontend/widgets/submission_details.py index 8693363..60bda33 100644 --- a/src/submissions/frontend/widgets/submission_details.py +++ b/src/submissions/frontend/widgets/submission_details.py @@ -42,7 +42,7 @@ class SubmissionDetails(QDialog): # NOTE: button to export a pdf version self.btn = QPushButton("Export PDF") self.btn.setFixedWidth(775) - self.btn.clicked.connect(self.export) + self.btn.clicked.connect(self.save_pdf) self.back = QPushButton("Back") self.back.setFixedWidth(100) # self.back.clicked.connect(self.back_function) @@ -181,7 +181,7 @@ class SubmissionDetails(QDialog): submission.save() self.submission_details(submission=self.rsl_plate_num) - def export(self): + def save_pdf(self): """ Renders submission to html, then creates and saves .pdf file to user selected file. """ diff --git a/src/submissions/frontend/widgets/submission_table.py b/src/submissions/frontend/widgets/submission_table.py index efc7bce..dfa9da5 100644 --- a/src/submissions/frontend/widgets/submission_table.py +++ b/src/submissions/frontend/widgets/submission_table.py @@ -83,6 +83,7 @@ class SubmissionsSheet(QTableView): self.resizeRowsToContents() self.setSortingEnabled(True) self.doubleClicked.connect(lambda x: BasicSubmission.query(id=x.sibling(x.row(), 0).data()).show_details(self)) + # NOTE: Have to run native query here because mine just returns results? self.total_count = BasicSubmission.__database_session__.query(BasicSubmission).count() def setData(self, page: int = 1, page_size: int = 250) -> None: diff --git a/src/submissions/frontend/widgets/submission_widget.py b/src/submissions/frontend/widgets/submission_widget.py index 7d2561c..a3f2564 100644 --- a/src/submissions/frontend/widgets/submission_widget.py +++ b/src/submissions/frontend/widgets/submission_widget.py @@ -1,15 +1,13 @@ ''' Contains all submission related frontend functions ''' -import sys - from PyQt6.QtWidgets import ( QWidget, QPushButton, QVBoxLayout, QComboBox, QDateEdit, QLineEdit, QLabel ) from PyQt6.QtCore import pyqtSignal, Qt from . import select_open_file, select_save_file -import logging, difflib +import logging from pathlib import Path from tools import Report, Result, check_not_nan, main_form_style, report_result from backend.excel.parser import SheetParser @@ -187,6 +185,8 @@ class SubmissionFormContainer(QWidget): class SubmissionFormWidget(QWidget): + update_reagent_fields = ['extraction_kit'] + def __init__(self, parent: QWidget, submission: PydSubmission, disable: list | None = None) -> None: super().__init__(parent) # logger.debug(f"Disable: {disable}") @@ -203,7 +203,7 @@ class SubmissionFormWidget(QWidget): # logger.debug(f"Attempting to extend ignore list with {self.pyd.submission_type['value']}") self.layout = QVBoxLayout() for k in list(self.pyd.model_fields.keys()) + list(self.pyd.model_extra.keys()): - logger.debug(f"Creating widget: {k}") + # logger.debug(f"Creating widget: {k}") if k in self.ignore: logger.warning(f"{k} in form_ignore {self.ignore}, not creating widget") continue @@ -225,10 +225,10 @@ class SubmissionFormWidget(QWidget): sub_obj=st, disable=check) if add_widget is not None: self.layout.addWidget(add_widget) - if k == "extraction_kit": + # if k == "extraction_kit": + if k in self.__class__.update_reagent_fields: add_widget.input.currentTextChanged.connect(self.scrape_reagents) self.setStyleSheet(main_form_style) - # self.scrape_reagents(self.pyd.extraction_kit) self.scrape_reagents(self.extraction_kit) def create_widget(self, key: str, value: dict | PydReagent, submission_type: str | SubmissionType | None = None, @@ -293,9 +293,8 @@ 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=self.extraction_kit) - logger.debug(f"Got reagents: {pformat(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) add_widget = self.ReagentFormWidget(parent=self, reagent=reagent, extraction_kit=self.extraction_kit) self.layout.addWidget(add_widget) report.add_result(integrity_report) @@ -334,10 +333,6 @@ class SubmissionFormWidget(QWidget): query = [widget for widget in query if widget.objectName() == object_name] return query - # def update_pyd(self): - # results = self.parse_form() - # logger.debug(pformat(results)) - @report_result def submit_new_sample_function(self, *args) -> Report: """ @@ -538,19 +533,14 @@ class SubmissionFormWidget(QWidget): except (TypeError, KeyError): pass obj = parent.parent().parent() - logger.debug(f"Object: {obj}") - logger.debug(f"Parent: {parent.parent()}") + # logger.debug(f"Object: {obj}") + # logger.debug(f"Parent: {parent.parent()}") # logger.debug(f"Creating widget for: {key}") match key: case 'submitting_lab': add_widget = MyQComboBox(scrollWidget=parent) # NOTE: lookup organizations suitable for submitting_lab (ctx: self.InfoItem.SubmissionFormWidget.SubmissionFormContainer.AddSubForm ) labs = [item.name for item in Organization.query()] - # NOTE: try to set closest match to top of list - # try: - # labs = difflib.get_close_matches(value, labs, len(labs), 0) - # except (TypeError, ValueError): - # pass if isinstance(value, dict): value = value['value'] if isinstance(value, Organization): @@ -559,7 +549,7 @@ class SubmissionFormWidget(QWidget): looked_up_lab = Organization.query(name=value, limit=1) except AttributeError: looked_up_lab = None - logger.debug(f"\n\nLooked up lab: {looked_up_lab}") + # logger.debug(f"\n\nLooked up lab: {looked_up_lab}") if looked_up_lab: try: labs.remove(str(looked_up_lab.name)) @@ -579,7 +569,6 @@ 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 submission_type.kit_types] obj.uses = uses # logger.debug(f"Kits received for {submission_type}: {uses}") diff --git a/src/submissions/frontend/widgets/summary.py b/src/submissions/frontend/widgets/summary.py index ad33f88..a8d8f66 100644 --- a/src/submissions/frontend/widgets/summary.py +++ b/src/submissions/frontend/widgets/summary.py @@ -1,12 +1,11 @@ -from PyQt6.QtCore import QSignalBlocker -from PyQt6.QtWebEngineWidgets import QWebEngineView +""" +Pane to hold information e.g. cost summary. +""" from .info_tab import InfoPane -from PyQt6.QtWidgets import QWidget, QGridLayout, QPushButton, QLabel +from PyQt6.QtWidgets import QWidget, QLabel, QPushButton from backend.db import Organization from backend.excel import ReportMaker -from tools import Report -from .misc import StartEndDatePicker, save_pdf, CheckableComboBox -from .functions import select_save_file +from .misc import CheckableComboBox import logging logger = logging.getLogger(f"submissions.{__name__}") @@ -16,32 +15,27 @@ class Summary(InfoPane): def __init__(self, parent: QWidget) -> None: super().__init__(parent) + self.save_excel_button = QPushButton("Save Excel", parent=self) + self.save_excel_button.pressed.connect(self.save_excel) + self.save_pdf_button = QPushButton("Save PDF", parent=self) + self.save_pdf_button.pressed.connect(self.save_pdf) + self.layout.addWidget(self.save_excel_button, 0, 2, 1, 1) + self.layout.addWidget(self.save_pdf_button, 0, 3, 1, 1) self.org_select = CheckableComboBox() self.org_select.setEditable(False) self.org_select.addItem("Select", header=True) for org in [org.name for org in Organization.query()]: self.org_select.addItem(org) - self.org_select.model().itemChanged.connect(self.date_changed) + self.org_select.model().itemChanged.connect(self.update_data) self.layout.addWidget(QLabel("Client"), 1, 0, 1, 1) self.layout.addWidget(self.org_select, 1, 1, 1, 3) - self.date_changed() + self.update_data() - def date_changed(self): + + def update_data(self): + super().update_data() 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(): - logger.warning("Start date after end date is not allowed!") - lastmonth = self.datepicker.end_date.date().addDays(-31) - # NOTE: block signal that will rerun controls getter and set start date - # Without triggering this function again - with QSignalBlocker(self.datepicker.start_date) as blocker: - self.datepicker.start_date.setDate(lastmonth) - self.date_changed() - return - # NOTE: convert to python useable date objects - # self.start_date = self.datepicker.start_date.date().toPyDate() - # self.end_date = self.datepicker.end_date.date().toPyDate() - super().date_changed() - logger.debug(f"Getting report from {self.start_date} to {self.end_date} using {orgs}") + # logger.debug(f"Getting report from {self.start_date} to {self.end_date} using {orgs}") self.report_obj = ReportMaker(start_date=self.start_date, end_date=self.end_date, organizations=orgs) self.webview.setHtml(self.report_obj.html) if self.report_obj.subs: @@ -50,13 +44,3 @@ class Summary(InfoPane): else: self.save_pdf_button.setEnabled(False) self.save_excel_button.setEnabled(False) - - # def save_excel(self): - # fname = select_save_file(self, default_name=f"Report {self.start_date.strftime('%Y%m%d')} - {self.end_date.strftime('%Y%m%d')}", extension="xlsx") - # self.report_obj.write_report(fname, obj=self) - # - # def save_pdf(self): - # fname = select_save_file(obj=self, - # default_name=f"Report {self.start_date.strftime('%Y%m%d')} - {self.end_date.strftime('%Y%m%d')}", - # extension="pdf") - # save_pdf(obj=self.webview, filename=fname) diff --git a/src/submissions/frontend/widgets/turnaround.py b/src/submissions/frontend/widgets/turnaround.py index cd59628..46032d4 100644 --- a/src/submissions/frontend/widgets/turnaround.py +++ b/src/submissions/frontend/widgets/turnaround.py @@ -15,28 +15,25 @@ class TurnaroundTime(InfoPane): def __init__(self, parent: QWidget): super().__init__(parent) - self.chart = None + self.save_button = QPushButton("Save Chart", parent=self) + self.save_button.pressed.connect(self.save_png) + self.layout.addWidget(self.save_button, 0, 2, 1, 1) + self.export_button = QPushButton("Save Data", parent=self) + self.export_button.pressed.connect(self.save_excel) + self.layout.addWidget(self.export_button, 0, 3, 1, 1) + self.fig = None self.report_object = None self.submission_typer = QComboBox(self) - subs = ["Any"] + [item.name for item in SubmissionType.query()] + subs = ["All"] + [item.name for item in SubmissionType.query()] self.submission_typer.addItems(subs) self.layout.addWidget(self.submission_typer, 1, 1, 1, 3) - self.submission_typer.currentTextChanged.connect(self.date_changed) - self.date_changed() + self.submission_typer.currentTextChanged.connect(self.update_data) + self.update_data() - def date_changed(self): - if self.datepicker.start_date.date() > self.datepicker.end_date.date(): - logger.warning("Start date after end date is not allowed!") - lastmonth = self.datepicker.end_date.date().addDays(-31) - # NOTE: block signal that will rerun controls getter and set start date - # Without triggering this function again - with QSignalBlocker(self.datepicker.start_date) as blocker: - self.datepicker.start_date.setDate(lastmonth) - self.date_changed() - return - super().date_changed() + def update_data(self): + super().update_data() chart_settings = dict(start_date=self.start_date, end_date=self.end_date) - if self.submission_typer.currentText() == "Any": + if self.submission_typer.currentText() == "All": submission_type = None subtype_obj = None else: @@ -47,5 +44,5 @@ class TurnaroundTime(InfoPane): threshold = subtype_obj.defaults['turnaround_time'] + 0.5 else: threshold = None - self.chart = TurnaroundChart(df=self.report_obj.df, settings=chart_settings, modes=[], threshold=threshold) - self.webview.setHtml(self.chart.to_html()) + self.fig = TurnaroundChart(df=self.report_obj.df, settings=chart_settings, modes=[], threshold=threshold) + self.webview.setHtml(self.fig.to_html()) diff --git a/src/submissions/tools/__init__.py b/src/submissions/tools/__init__.py index 4e52d0d..ce990e2 100644 --- a/src/submissions/tools/__init__.py +++ b/src/submissions/tools/__init__.py @@ -3,13 +3,11 @@ Contains miscellaenous functions used by both frontend and backend. ''' from __future__ import annotations -import json -import pprint from datetime import date, datetime, timedelta from json import JSONDecodeError +from pprint import pprint import numpy as np -import logging, re, yaml, sys, os, stat, platform, getpass, inspect -import pandas as pd +import logging, re, yaml, sys, os, stat, platform, getpass, inspect, json, pandas as pd from dateutil.easter import easter from jinja2 import Environment, FileSystemLoader from logging import handlers @@ -20,7 +18,6 @@ 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]) from __init__ import project_path from configparser import ConfigParser from tkinter import Tk # NOTE: This is for choosing database path before app is created. @@ -261,6 +258,7 @@ class Settings(BaseSettings, extra="allow"): submission_types: dict | None = None database_session: Session | None = None package: Any | None = None + logging_enabled: bool = Field(default=False) model_config = SettingsConfigDict(env_file_encoding='utf-8') @@ -422,6 +420,7 @@ class Settings(BaseSettings, extra="allow"): super().__init__(*args, **kwargs) self.set_from_db() + pprint(f"User settings:\n{self.__dict__}") def set_from_db(self): if 'pytest' in sys.modules: @@ -819,7 +818,7 @@ class Result(BaseModel, arbitrary_types_allowed=True): self.owner = inspect.stack()[1].function def report(self): - from frontend.widgets.misc import AlertPop + from frontend.widgets.pop_ups import AlertPop return AlertPop(message=self.msg, status=self.status, owner=self.owner)