Post code clean-up, before attempt to upgrade controls to FigureWidgets

This commit is contained in:
lwark
2024-10-21 08:17:43 -05:00
parent 495d1a5a7f
commit 1f83b61c81
29 changed files with 441 additions and 1089 deletions

View File

@@ -4,6 +4,7 @@ Contains all operations for creating charts, graphs and visual effects.
from PyQt6.QtWidgets import QWidget
import plotly
from plotly.graph_objects import Figure
from plotly.graph_objs import FigureWidget
import pandas as pd
from frontend.widgets.functions import select_save_file
@@ -35,7 +36,6 @@ class CustomFigure(Figure):
output = select_save_file(obj=parent, default_name=group_name, extension="xlsx")
self.df.to_excel(output.absolute().__str__(), engine="openpyxl", index=False)
def to_html(self) -> str:
"""
Creates final html code from plotly

View File

@@ -3,14 +3,13 @@ Functions for constructing irida controls graphs using plotly.
"""
from datetime import date
from pprint import pformat
import plotly
from typing import Generator
import plotly.express as px
import pandas as pd
from PyQt6.QtWidgets import QWidget
from . import CustomFigure
import logging
from tools import get_unique_values_in_df_column, divide_chunks
from frontend.widgets.functions import select_save_file
logger = logging.getLogger(f"submissions.{__name__}")
@@ -67,7 +66,6 @@ class IridaFigure(CustomFigure):
)
bar.update_traces(visible=ii == 0)
self.add_traces(bar.data)
# return generic_figure_markers(modes=modes, ytitle=ytitle)
def generic_figure_markers(self, modes: list = [], ytitle: str | None = None, months: int = 6):
"""
@@ -83,7 +81,7 @@ class IridaFigure(CustomFigure):
"""
if modes:
ytitle = modes[0]
# Creating visibles list for each mode.
# logger.debug("Creating visibles list for each mode.")
self.update_layout(
xaxis_title="Submitted Date (* - Date parsed from fastq file creation date)",
yaxis_title=ytitle,
@@ -100,7 +98,6 @@ class IridaFigure(CustomFigure):
)
]
)
self.update_xaxes(
rangeslider_visible=True,
rangeselector=dict(
@@ -109,7 +106,16 @@ class IridaFigure(CustomFigure):
)
assert isinstance(self, CustomFigure)
def make_plotly_buttons(self, months: int = 6):
def make_plotly_buttons(self, months: int = 6) -> Generator[dict, None, None]:
"""
Creates html buttons to zoom in on date areas
Args:
months (int, optional): Number of months of data given. Defaults to 6.
Yields:
Generator[dict, None, None]: Button details.
"""
rng = [1]
if months > 2:
rng += [iii for iii in range(3, months, 3)]
@@ -121,7 +127,7 @@ class IridaFigure(CustomFigure):
for button in buttons:
yield button
def make_pyqt_buttons(self, modes: list) -> list:
def make_pyqt_buttons(self, modes: list) -> Generator[dict, None, None]:
"""
Creates list of buttons with one for each mode to be used in showing/hiding mode traces.
@@ -130,7 +136,7 @@ class IridaFigure(CustomFigure):
fig_len (int): number of traces in the figure
Returns:
list: list of buttons.
Generator[dict, None, None]: list of buttons.
"""
fig_len = len(self.data)
if len(modes) > 1:

View File

@@ -1,20 +1,21 @@
"""
Functions for constructing irida controls graphs using plotly.
"""
from datetime import date
from pprint import pformat
import plotly
from plotly.graph_objs import FigureWidget, Scatter
from . import CustomFigure
import plotly.express as px
import pandas as pd
from PyQt6.QtWidgets import QWidget
from plotly.graph_objects import Figure
import logging
from tools import get_unique_values_in_df_column, divide_chunks
from frontend.widgets.functions import select_save_file
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):
@@ -23,13 +24,20 @@ class PCRFigure(CustomFigure):
super().__init__(df=df, modes=modes)
logger.debug(f"DF: {self.df}")
self.construct_chart(df=df)
# self.generic_figure_markers(modes=modes, ytitle=ytitle, months=months)
def hello(self):
print("hello")
def construct_chart(self, df: pd.DataFrame):
logger.debug(f"PCR df: {df}")
logger.debug(f"PCR df:\n {df}")
try:
scatter = px.scatter(data_frame=df, x='submitted_date', y="ct", hover_data=["name", "target", "ct", "reagent_lot"], color='target')
express = px.scatter(data_frame=df, x='submitted_date', y="ct",
hover_data=["name", "target", "ct", "reagent_lot"],
color="target")
except ValueError:
scatter = px.scatter()
express = px.scatter()
scatter = FigureWidget([datum for datum in express.data])
self.add_traces(scatter.data)
self.update_traces(marker={'size': 15})

View File

@@ -1,17 +1,14 @@
'''
"""
Contains all custom generated PyQT6 derivative widgets.
'''
"""
# from .app import App
from .functions import *
from .misc import *
from .pop_ups import *
from .submission_table import *
from .submission_widget import *
from .controls_chart import *
from .kit_creator import *
from .submission_details import *
from .equipment_usage import *
from .gel_checker import *
from .submission_type_creator import *
from .app import App

View File

@@ -9,7 +9,6 @@ from PyQt6.QtWidgets import (
)
from PyQt6.QtGui import QAction
from pathlib import Path
from markdown import markdown
from __init__ import project_path
from tools import check_if_app, Settings, Report, jinja_template_loading, check_authorization, page_size
@@ -21,8 +20,6 @@ import logging, webbrowser, sys, shutil
from .submission_table import SubmissionsSheet
from .submission_widget import SubmissionFormContainer
from .controls_chart import ControlsViewer
from .kit_creator import KitAdder
from .submission_type_creator import SubmissionTypeAdder, SubmissionType
from .sample_search import SearchBox
from .summary import Summary
@@ -72,7 +69,6 @@ class App(QMainWindow):
fileMenu = menuBar.addMenu("&File")
# NOTE: Creating menus using a title
methodsMenu = menuBar.addMenu("&Methods")
# reportMenu = menuBar.addMenu("&Reports")
maintenanceMenu = menuBar.addMenu("&Monthly")
helpMenu = menuBar.addMenu("&Help")
helpMenu.addAction(self.helpAction)
@@ -83,7 +79,6 @@ class App(QMainWindow):
fileMenu.addAction(self.yamlImportAction)
methodsMenu.addAction(self.searchLog)
methodsMenu.addAction(self.searchSample)
# reportMenu.addAction(self.generateReportAction)
maintenanceMenu.addAction(self.joinExtractionAction)
maintenanceMenu.addAction(self.joinPCRAction)
@@ -105,7 +100,6 @@ class App(QMainWindow):
# logger.debug(f"Creating actions...")
self.importAction = QAction("&Import Submission", self)
self.addReagentAction = QAction("Add Reagent", self)
# self.generateReportAction = QAction("Make Report", self)
self.addKitAction = QAction("Import Kit", self)
self.addOrgAction = QAction("Import Org", self)
self.joinExtractionAction = QAction("Link Extraction Logs")
@@ -125,7 +119,6 @@ class App(QMainWindow):
# logger.debug(f"Connecting actions...")
self.importAction.triggered.connect(self.table_widget.formwidget.importSubmission)
self.addReagentAction.triggered.connect(self.table_widget.formwidget.add_reagent)
# self.generateReportAction.triggered.connect(self.table_widget.sub_wid.generate_report)
self.joinExtractionAction.triggered.connect(self.table_widget.sub_wid.link_extractions)
self.joinPCRAction.triggered.connect(self.table_widget.sub_wid.link_pcr)
self.helpAction.triggered.connect(self.showAbout)
@@ -233,6 +226,7 @@ class App(QMainWindow):
ap.exec()
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":
@@ -262,7 +256,6 @@ class AddSubForm(QWidget):
self.tabs.addTab(self.tab2, "Irida Controls")
self.tabs.addTab(self.tab3, "PCR Controls")
self.tabs.addTab(self.tab4, "Cost Report")
# self.tabs.addTab(self.tab4, "Add Kit")
# NOTE: Create submission adder form
self.formwidget = SubmissionFormContainer(self)
self.formlayout = QVBoxLayout(self)
@@ -294,16 +287,10 @@ class AddSubForm(QWidget):
self.pcr_viewer = ControlsViewer(self, archetype="PCR Control")
self.tab3.layout.addWidget(self.pcr_viewer)
self.tab3.setLayout(self.tab3.layout)
# NOTE: create custom widget to add new tabs
# ST_adder = SubmissionTypeAdder(self)
summary_report = Summary(self)
self.tab4.layout = QVBoxLayout(self)
self.tab4.layout.addWidget(summary_report)
self.tab4.setLayout(self.tab4.layout)
# kit_adder = KitAdder(self)
# self.tab4.layout = QVBoxLayout(self)
# self.tab4.layout.addWidget(kit_adder)
# self.tab4.setLayout(self.tab4.layout)
# NOTE: add tabs to main widget
self.layout.addWidget(self.tabs)
self.setLayout(self.layout)

View File

@@ -1,23 +1,17 @@
"""
Handles display of control charts
"""
import re
import sys
from datetime import timedelta, date
from datetime import date
from pprint import pformat
from typing import Tuple
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QComboBox, QHBoxLayout,
QDateEdit, QLabel, QSizePolicy, QPushButton, QGridLayout
QWidget, QComboBox, QPushButton, QGridLayout
)
from PyQt6.QtCore import QSignalBlocker
from backend.db import ControlType, IridaControl
from PyQt6.QtCore import QDate, QSize
import logging
from pandas import DataFrame
from tools import Report, Result, get_unique_values_in_df_column, Settings, report_result
from frontend.visualizations import IridaFigure, PCRFigure, CustomFigure
from tools import Report, report_result
from frontend.visualizations import CustomFigure
from .misc import StartEndDatePicker
logger = logging.getLogger(f"submissions.{__name__}")
@@ -70,19 +64,12 @@ class ControlsViewer(QWidget):
self.save_button.pressed.connect(self.save_chart_function)
self.export_button.pressed.connect(self.save_data_function)
def save_chart_function(self):
self.fig.save_figure(parent=self)
def save_data_function(self):
self.fig.save_data(parent=self)
# def controls_getter(self):
# """
# Lookup controls from database and send to chartmaker
# """
# self.controls_getter_function()
@report_result
def controls_getter_function(self, *args, **kwargs):
"""
@@ -128,7 +115,18 @@ class ControlsViewer(QWidget):
self.chart_maker_function()
return report
def diff_month(self, d1: date, d2: date):
@classmethod
def diff_month(self, d1: date, d2: date) -> float:
"""
Gets the number of months difference between two different dates
Args:
d1 (date): Start date.
d2 (date): End date.
Returns:
float: Number of months difference
"""
return abs((d1.year - d2.year) * 12 + d1.month - d2.month)
@report_result
@@ -169,164 +167,3 @@ class ControlsViewer(QWidget):
# logger.debug("Figure updated... I hope.")
return report
# def convert_data_list_to_df(self, input_df: list[dict]) -> DataFrame:
# """
# Convert list of control records to dataframe
#
# Args:
# ctx (dict): settings passed from gui
# input_df (list[dict]): list of dictionaries containing records
# mode_sub_type (str | None, optional): sub_type of submission type. Defaults to None.
#
# Returns:
# DataFrame: dataframe of controls
# """
#
# df = DataFrame.from_records(input_df)
# safe = ['name', 'submitted_date', 'genus', 'target']
# for column in df.columns:
# if column not in safe:
# if self.mode_sub_type is not None and column != self.mode_sub_type:
# continue
# else:
# safe.append(column)
# if "percent" in column:
# # count_col = [item for item in df.columns if "count" in item][0]
# try:
# count_col = next(item for item in df.columns if "count" in item)
# except StopIteration:
# continue
# # NOTE: The actual percentage from kraken was off due to exclusion of NaN, recalculating.
# df[column] = 100 * df[count_col] / df.groupby('name')[count_col].transform('sum')
# df = df[[c for c in df.columns if c in safe]]
# # NOTE: move date of sample submitted on same date as previous ahead one.
# df = self.displace_date(df=df)
# # NOTE: ad hoc method to make data labels more accurate.
# df = self.df_column_renamer(df=df)
# return df
#
# def df_column_renamer(self, df: DataFrame) -> DataFrame:
# """
# Ad hoc function I created to clarify some fields
#
# Args:
# df (DataFrame): input dataframe
#
# Returns:
# DataFrame: dataframe with 'clarified' column names
# """
# df = df[df.columns.drop(list(df.filter(regex='_hashes')))]
# return df.rename(columns={
# "contains_ratio": "contains_shared_hashes_ratio",
# "matches_ratio": "matches_shared_hashes_ratio",
# "kraken_count": "kraken2_read_count_(top_50)",
# "kraken_percent": "kraken2_read_percent_(top_50)"
# })
#
# def displace_date(self, df: DataFrame) -> DataFrame:
# """
# This function serves to split samples that were submitted on the same date by incrementing dates.
# It will shift the date forward by one day if it is the same day as an existing date in a list.
#
# Args:
# df (DataFrame): input dataframe composed of control records
#
# Returns:
# DataFrame: output dataframe with dates incremented.
# """
# # logger.debug(f"Unique items: {df['name'].unique()}")
# # NOTE: get submitted dates for each control
# dict_list = [dict(name=item, date=df[df.name == item].iloc[0]['submitted_date']) for item in
# sorted(df['name'].unique())]
# previous_dates = set()
# # for _, item in enumerate(dict_list):
# for item in dict_list:
# df, previous_dates = self.check_date(df=df, item=item, previous_dates=previous_dates)
# return df
#
# def check_date(self, df: DataFrame, item: dict, previous_dates: set) -> Tuple[DataFrame, list]:
# """
# Checks if an items date is already present in df and adjusts df accordingly
#
# Args:
# df (DataFrame): input dataframe
# item (dict): control for checking
# previous_dates (list): list of dates found in previous controls
#
# Returns:
# Tuple[DataFrame, list]: Output dataframe and appended list of previous dates
# """
# try:
# check = item['date'] in previous_dates
# except IndexError:
# check = False
# previous_dates.add(item['date'])
# if check:
# # logger.debug(f"We found one! Increment date!\n\t{item['date']} to {item['date'] + timedelta(days=1)}")
# # NOTE: get df locations where name == item name
# mask = df['name'] == item['name']
# # NOTE: increment date in dataframe
# df.loc[mask, 'submitted_date'] = df.loc[mask, 'submitted_date'].apply(lambda x: x + timedelta(days=1))
# item['date'] += timedelta(days=1)
# passed = False
# else:
# passed = True
# # logger.debug(f"\n\tCurrent date: {item['date']}\n\tPrevious dates:{previous_dates}")
# # logger.debug(f"DF: {type(df)}, previous_dates: {type(previous_dates)}")
# # NOTE: if run didn't lead to changed date, return values
# if passed:
# # logger.debug(f"Date check passed, returning.")
# return df, previous_dates
# # NOTE: if date was changed, rerun with new date
# else:
# logger.warning(f"Date check failed, running recursion")
# df, previous_dates = self.check_date(df, item, previous_dates)
# return df, previous_dates
#
# def prep_df(self, ctx: Settings, df: DataFrame) -> Tuple[DataFrame, list]:
# """
# Constructs figures based on parsed pandas dataframe.
#
# Args:
# ctx (Settings): settings passed down from gui
# df (pd.DataFrame): input dataframe
# ytitle (str | None, optional): title for the y-axis. Defaults to None.
#
# Returns:
# Figure: Plotly figure
# """
# # NOTE: converts starred genera to normal and splits off list of starred
# if df.empty:
# return None
# df['genus'] = df['genus'].replace({'\*': ''}, regex=True).replace({"NaN": "Unknown"})
# df['genera'] = [item[-1] if item and item[-1] == "*" else "" for item in df['genus'].to_list()]
# # NOTE: remove original runs, using reruns if applicable
# df = self.drop_reruns_from_df(ctx=ctx, df=df)
# # NOTE: sort by and exclude from
# sorts = ['submitted_date', "target", "genus"]
# exclude = ['name', 'genera']
# modes = [item for item in df.columns if item not in sorts and item not in exclude]
# # NOTE: Set descending for any columns that have "{mode}" in the header.
# ascending = [False if item == "target" else True for item in sorts]
# df = df.sort_values(by=sorts, ascending=ascending)
# # logger.debug(df[df.isna().any(axis=1)])
# # NOTE: actual chart construction is done by
# return df, modes
#
# def drop_reruns_from_df(self, ctx: Settings, df: DataFrame) -> DataFrame:
# """
# Removes semi-duplicates from dataframe after finding sequencing repeats.
#
# Args:
# settings (dict): settings passed from gui
# df (DataFrame): initial dataframe
#
# Returns:
# DataFrame: dataframe with originals removed in favour of repeats.
# """
# if 'rerun_regex' in ctx:
# sample_names = get_unique_values_in_df_column(df, column_name="name")
# rerun_regex = re.compile(fr"{ctx.rerun_regex}")
# exclude = [re.sub(rerun_regex, "", sample) for sample in sample_names if rerun_regex.search(sample)]
# df = df[df.name not in exclude]
# return df

View File

@@ -8,7 +8,7 @@ from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox,
from backend.db.models import Equipment, BasicSubmission, Process
from backend.validators.pydant import PydEquipment, PydEquipmentRole, PydTips
import logging
from typing import List, Generator
from typing import Generator
logger = logging.getLogger(f"submissions.{__name__}")
@@ -50,7 +50,7 @@ class EquipmentUsage(QDialog):
Pull info from all RoleComboBox widgets
Returns:
List[PydEquipment]: All equipment pulled from widgets
Generator[PydEquipment, None, None]: All equipment pulled from widgets
"""
for widget in self.findChildren(QWidget):
match widget:

View File

@@ -4,7 +4,6 @@ functions used by all windows in the application's frontend
from pathlib import Path
import logging
from PyQt6.QtWidgets import QMainWindow, QFileDialog
from tools import Result
logger = logging.getLogger(f"submissions.{__name__}")

View File

@@ -1,232 +0,0 @@
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QScrollArea,
QGridLayout, QPushButton, QLabel,
QLineEdit, QComboBox, QDoubleSpinBox,
QSpinBox, QDateEdit
)
from sqlalchemy import FLOAT, INTEGER
from backend.db import SubmissionTypeKitTypeAssociation, SubmissionType, ReagentRole
from backend.validators import PydReagentRole, PydKit
import logging
from pprint import pformat
from tools import Report
from typing import Tuple
logger = logging.getLogger(f"submissions.{__name__}")
class KitAdder(QWidget):
"""
dialog to get information to add kit
"""
def __init__(self, parent) -> None:
super().__init__(parent)
self.report = Report()
self.app = parent.parent
main_box = QVBoxLayout(self)
scroll = QScrollArea(self)
main_box.addWidget(scroll)
scroll.setWidgetResizable(True)
scrollContent = QWidget(scroll)
self.grid = QGridLayout()
scrollContent.setLayout(self.grid)
# NOTE: insert submit button at top
self.submit_btn = QPushButton("Submit")
self.grid.addWidget(self.submit_btn,0,0,1,1)
self.grid.addWidget(QLabel("Kit Name:"),2,0)
# NOTE: widget to get kit name
kit_name = QLineEdit()
kit_name.setObjectName("kit_name")
self.grid.addWidget(kit_name,2,1)
self.grid.addWidget(QLabel("Used For Submission Type:"),3,0)
# NOTE: widget to get uses of kit
used_for = QComboBox()
used_for.setObjectName("used_for")
# NOTE: Insert all existing sample types
used_for.addItems([item.name for item in SubmissionType.query()])
used_for.setEditable(True)
self.grid.addWidget(used_for,3,1)
# NOTE: Get all fields in SubmissionTypeKitTypeAssociation
self.columns = [item for item in SubmissionTypeKitTypeAssociation.__table__.columns if len(item.foreign_keys) == 0]
for iii, column in enumerate(self.columns):
idx = iii + 4
# NOTE: convert field name to human readable.
field_name = column.name.replace("_", " ").title()
self.grid.addWidget(QLabel(field_name),idx,0)
match column.type:
case FLOAT():
add_widget = QDoubleSpinBox()
add_widget.setMinimum(0)
add_widget.setMaximum(9999)
case INTEGER():
add_widget = QSpinBox()
add_widget.setMinimum(0)
add_widget.setMaximum(9999)
case _:
add_widget = QLineEdit()
add_widget.setObjectName(column.name)
self.grid.addWidget(add_widget, idx,1)
self.add_RT_btn = QPushButton("Add Reagent Type")
self.grid.addWidget(self.add_RT_btn)
self.add_RT_btn.clicked.connect(self.add_RT)
self.submit_btn.clicked.connect(self.submit)
scroll.setWidget(scrollContent)
self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
"qt_scrollarea_vcontainer", "submit_btn"
]
def add_RT(self) -> None:
"""
insert new reagent type row
"""
# NOTE: get bottommost row
maxrow = self.grid.rowCount()
reg_form = ReagentRoleForm(parent=self)
reg_form.setObjectName(f"ReagentForm_{maxrow}")
self.grid.addWidget(reg_form, maxrow,0,1,4)
def submit(self) -> None:
"""
send kit to database
"""
report = Report()
# NOTE: get form info
info, reagents = self.parse_form()
info = {k:v for k,v in info.items() if k in [column.name for column in self.columns] + ['kit_name', 'used_for']}
# logger.debug(f"kit info: {pformat(info)}")
# logger.debug(f"kit reagents: {pformat(reagents)}")
info['reagent_roles'] = reagents
# logger.debug(pformat(info))
# NOTE: send to kit constructor
kit = PydKit(name=info['kit_name'])
for reagent in info['reagent_roles']:
uses = {
info['used_for']:
{'sheet':reagent['sheet'],
'name':reagent['name'],
'lot':reagent['lot'],
'expiry':reagent['expiry']
}}
kit.reagent_roles.append(PydReagentRole(name=reagent['rtname'], eol_ext=reagent['eol'], uses=uses))
# logger.debug(f"Output pyd object: {kit.__dict__}")
sqlobj, result = kit.toSQL(self.ctx)
report.add_result(result=result)
sqlobj.save()
self.__init__(self.parent())
def parse_form(self) -> Tuple[dict, list]:
"""
Pulls reagent and general info from form
Returns:
Tuple[dict, list]: dict=info, list=reagents
"""
# logger.debug(f"Hello from {self.__class__} parser!")
info = {}
reagents = []
widgets = [widget for widget in self.findChildren(QWidget) if widget.objectName() not in self.ignore and not isinstance(widget.parent(), ReagentRoleForm)]
for widget in widgets:
# logger.debug(f"Parsed widget: {widget.objectName()} of type {type(widget)} with parent {widget.parent()}")
match widget:
case ReagentRoleForm():
reagents.append(widget.parse_form())
case QLineEdit():
info[widget.objectName()] = widget.text()
case QComboBox():
info[widget.objectName()] = widget.currentText()
case QDateEdit():
info[widget.objectName()] = widget.date().toPyDate()
return info, reagents
class ReagentRoleForm(QWidget):
"""
custom widget to add information about a new reagenttype
"""
def __init__(self, parent) -> None:
super().__init__(parent)
grid = QGridLayout()
self.setLayout(grid)
grid.addWidget(QLabel("Reagent Type Name"),0,0)
# Widget to get reagent info
self.reagent_getter = QComboBox()
self.reagent_getter.setObjectName("rtname")
# lookup all reagent type names from db
lookup = ReagentRole.query()
# logger.debug(f"Looked up ReagentType names: {lookup}")
self.reagent_getter.addItems([item.name for item in lookup])
self.reagent_getter.setEditable(True)
grid.addWidget(self.reagent_getter,0,1)
grid.addWidget(QLabel("Extension of Life (months):"),0,2)
# NOTE: widget to get extension of life
self.eol = QSpinBox()
self.eol.setObjectName('eol')
self.eol.setMinimum(0)
grid.addWidget(self.eol, 0,3)
grid.addWidget(QLabel("Excel Location Sheet Name:"),1,0)
self.location_sheet_name = QLineEdit()
self.location_sheet_name.setObjectName("sheet")
self.location_sheet_name.setText("e.g. 'Reagent Info'")
grid.addWidget(self.location_sheet_name, 1,1)
for iii, item in enumerate(["Name", "Lot", "Expiry"]):
idx = iii + 2
grid.addWidget(QLabel(f"{item} Row:"), idx, 0)
row = QSpinBox()
row.setFixedWidth(50)
row.setObjectName(f'{item.lower()}_row')
row.setMinimum(0)
grid.addWidget(row, idx, 1)
grid.addWidget(QLabel(f"{item} Column:"), idx, 2)
col = QSpinBox()
col.setFixedWidth(50)
col.setObjectName(f'{item.lower()}_column')
col.setMinimum(0)
grid.addWidget(col, idx, 3)
self.setFixedHeight(175)
max_row = grid.rowCount()
self.r_button = QPushButton("Remove")
self.r_button.clicked.connect(self.remove)
grid.addWidget(self.r_button,max_row,0,1,1)
self.ignore = [None, "", "qt_spinbox_lineedit", "qt_scrollarea_viewport", "qt_scrollarea_hcontainer",
"qt_scrollarea_vcontainer", "submit_btn", "eol", "sheet", "rtname"
]
def remove(self):
"""
Destroys this row of reagenttype from the form
"""
self.setParent(None)
self.destroy()
def parse_form(self) -> dict:
"""
Pulls ReagentType info from the form.
Returns:
dict: _description_
"""
# logger.debug(f"Hello from {self.__class__} parser!")
info = {}
info['eol'] = self.eol.value()
info['sheet'] = self.location_sheet_name.text()
info['rtname'] = self.reagent_getter.currentText()
widgets = [widget for widget in self.findChildren(QWidget) if widget.objectName() not in self.ignore]
for widget in widgets:
# logger.debug(f"Parsed widget: {widget.objectName()} of type {type(widget)} with parent {widget.parent()}")
match widget:
case QLineEdit():
info[widget.objectName()] = widget.text()
case QComboBox():
info[widget.objectName()] = widget.currentText()
case QDateEdit():
info[widget.objectName()] = widget.date().toPyDate()
case QSpinBox() | QDoubleSpinBox():
if "_" in widget.objectName():
key, sub_key = widget.objectName().split("_")
if key not in info.keys():
info[key] = {}
# logger.debug(f"Adding key {key}, {sub_key} and value {widget.value()} to {info}")
info[key][sub_key] = widget.value()
return info

View File

@@ -3,7 +3,6 @@ Contains miscellaneous widgets for frontend functions
'''
import math
from datetime import date
from PyQt6.QtGui import QPageLayout, QPageSize, QStandardItem, QIcon
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import (
@@ -51,7 +50,6 @@ class AddReagentForm(QDialog):
self.exp_input.setObjectName('expiry')
# NOTE: if expiry is not passed in from gui, use today
if expiry is None:
# self.exp_input.setDate(QDate.currentDate())
self.exp_input.setDate(QDate(1970, 1, 1))
else:
try:
@@ -244,4 +242,4 @@ class Pagifier(QWidget):
self.update_current_page()
def update_current_page(self):
self.current_page.setText(f"{self.page_anchor} of {self.page_max}")
self.current_page.setText(f"{self.page_anchor} of {self.page_max}")

View File

@@ -9,7 +9,7 @@ from PyQt6.QtWebEngineWidgets import QWebEngineView
from tools import jinja_template_loading
import logging
from backend.db import models
from typing import Literal
from typing import Any, Literal
logger = logging.getLogger(f"submissions.{__name__}")
@@ -54,9 +54,8 @@ class AlertPop(QMessageBox):
class HTMLPop(QDialog):
def __init__(self, html: str, owner: str | None = None, title: str = "python"):
def __init__(self, html: str, title: str = "python"):
super().__init__()
self.webview = QWebEngineView(parent=self)
self.layout = QVBoxLayout()
self.setWindowTitle(title)

View File

@@ -74,7 +74,6 @@ class SearchBox(QDialog):
# logger.debug(f"Running update_data with sample type: {self.type}")
fields = self.parse_form()
# logger.debug(f"Got fields: {fields}")
# sample_list_creator = self.type.fuzzy_search(sample_type=self.type, **fields)
sample_list_creator = self.type.fuzzy_search(**fields)
data = self.type.samples_to_df(sample_list=sample_list_creator)
# logger.debug(f"Data: {data}")

View File

@@ -1,13 +1,11 @@
"""
Webview to show submission and sample details.
"""
from PyQt6.QtGui import QColor, QPageSize, QPageLayout
from PyQt6.QtPrintSupport import QPrinter
from PyQt6.QtWidgets import (QDialog, QPushButton, QVBoxLayout,
QDialogButtonBox, QTextEdit, QGridLayout)
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWebChannel import QWebChannel
from PyQt6.QtCore import Qt, pyqtSlot, QMarginsF, QSize
from PyQt6.QtCore import Qt, pyqtSlot
from jinja2 import TemplateNotFound
from backend.db.models import BasicSubmission, BasicSample, Reagent, KitType
from tools import is_power_user, jinja_template_loading
@@ -41,7 +39,6 @@ class SubmissionDetails(QDialog):
self.webview.setMaximumWidth(900)
self.webview.loadFinished.connect(self.activate_export)
self.layout = QGridLayout()
# self.setFixedSize(900, 500)
# NOTE: button to export a pdf version
self.btn = QPushButton("Export PDF")
self.btn.setFixedWidth(775)
@@ -69,7 +66,6 @@ class SubmissionDetails(QDialog):
def back_function(self):
self.webview.back()
# @pyqtSlot(bool)
def activate_export(self):
title = self.webview.title()
self.setWindowTitle(title)
@@ -144,7 +140,6 @@ class SubmissionDetails(QDialog):
self.base_dict = submission.to_dict(full_data=True)
# logger.debug(f"Submission details data:\n{pformat({k:v for k,v in self.base_dict.items() if k == 'reagents'})}")
# NOTE: don't want id
self.base_dict = submission.finalize_details(self.base_dict)
# logger.debug(f"Creating barcode.")
# logger.debug(f"Making platemap...")
self.base_dict['platemap'] = submission.make_plate_map(sample_list=submission.hitpick_plate())

View File

@@ -10,8 +10,6 @@ from backend.db.models import BasicSubmission
from tools import Report, Result, report_result
from .functions import select_open_file
# from .misc import ReportDatePicker
logger = logging.getLogger(f"submissions.{__name__}")
@@ -91,7 +89,7 @@ class SubmissionsSheet(QTableView):
"""
sets data in model
"""
self.data = BasicSubmission.submissions_to_df(page=page)
self.data = BasicSubmission.submissions_to_df(page=page, page_size=page_size)
try:
self.data['Id'] = self.data['Id'].apply(str)
self.data['Id'] = self.data['Id'].str.zfill(4)
@@ -101,7 +99,7 @@ class SubmissionsSheet(QTableView):
proxyModel.setSourceModel(pandasModel(self.data))
self.setModel(proxyModel)
def contextMenuEvent(self, event):
def contextMenuEvent(self):
"""
Creates actions for right click menu events.
@@ -157,7 +155,7 @@ class SubmissionsSheet(QTableView):
report = Report()
fname = select_open_file(self, file_extension="csv")
with open(fname.__str__(), 'r') as f:
# split csv on commas
# NOTE: split csv on commas
runs = [col.strip().split(",") for col in f.readlines()]
count = 0
for run in runs:

View File

@@ -1,124 +0,0 @@
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QScrollArea,
QGridLayout, QPushButton, QLabel,
QLineEdit, QSpinBox, QCheckBox
)
from sqlalchemy.orm.attributes import InstrumentedAttribute
from backend.db import SubmissionType, BasicSubmission
import logging
from tools import Report
from .functions import select_open_file
logger = logging.getLogger(f"submissions.{__name__}")
class SubmissionTypeAdder(QWidget):
def __init__(self, parent) -> None:
super().__init__(parent)
self.report = Report()
self.app = parent.parent()
self.template_path = ""
main_box = QVBoxLayout(self)
scroll = QScrollArea(self)
main_box.addWidget(scroll)
scroll.setWidgetResizable(True)
scrollContent = QWidget(scroll)
self.grid = QGridLayout()
scrollContent.setLayout(self.grid)
# NOTE: insert submit button at top
self.submit_btn = QPushButton("Submit")
self.grid.addWidget(self.submit_btn,0,0,1,1)
self.grid.addWidget(QLabel("Submission Type Name:"),2,0)
# NOTE: widget to get kit name
self.st_name = QLineEdit()
self.st_name.setObjectName("submission_type_name")
self.grid.addWidget(self.st_name,2,1,1,2)
self.grid.addWidget(QLabel("Template File"),3,0)
template_selector = QPushButton("Select")
self.grid.addWidget(template_selector,3,1)
self.template_label = QLabel("None")
self.grid.addWidget(self.template_label,3,2)
# NOTE: widget to get uses of kit
exclude = ['id', 'submitting_lab_id', 'extraction_kit_id', 'reagents_id', 'extraction_info', 'pcr_info', 'run_cost']
self.columns = {key:value for key, value in BasicSubmission.__dict__.items() if isinstance(value, InstrumentedAttribute)}
self.columns = {key:value for key, value in self.columns.items() if hasattr(value, "type") and key not in exclude}
for iii, key in enumerate(self.columns):
idx = iii + 4
self.grid.addWidget(InfoWidget(parent=self, key=key), idx,0,1,3)
scroll.setWidget(scrollContent)
self.submit_btn.clicked.connect(self.submit)
template_selector.clicked.connect(self.get_template_path)
def submit(self):
"""
Create SubmissionType and send to db
"""
info = self.parse_form()
ST = SubmissionType(name=self.st_name.text(), info_map=info)
try:
with open(self.template_path, "rb") as f:
ST.template_file = f.read()
except FileNotFoundError:
logger.error(f"Could not find template file: {self.template_path}")
ST.save()
def parse_form(self) -> dict:
"""
Pulls info from form
Returns:
dict: information from form
"""
widgets = [widget for widget in self.findChildren(QWidget) if isinstance(widget, InfoWidget)]
return {widget.objectName():widget.parse_form() for widget in widgets}
def get_template_path(self):
"""
Sets path for loading a submission form template
"""
self.template_path = select_open_file(obj=self, file_extension="xlsx")
self.template_label.setText(self.template_path.__str__())
class InfoWidget(QWidget):
def __init__(self, parent: QWidget, key) -> None:
super().__init__(parent)
grid = QGridLayout()
self.setLayout(grid)
self.active = QCheckBox()
self.active.setChecked(True)
grid.addWidget(self.active, 0,0,1,1)
grid.addWidget(QLabel(key.replace("_", " ").title()),0,1,1,4)
self.setObjectName(key)
grid.addWidget(QLabel("Sheet Names (comma seperated):"),1,0)
self.sheet = QLineEdit()
self.sheet.setObjectName("sheets")
grid.addWidget(self.sheet, 1,1,1,3)
grid.addWidget(QLabel("Row:"),2,0,alignment=Qt.AlignmentFlag.AlignRight)
self.row = QSpinBox()
self.row.setObjectName("row")
grid.addWidget(self.row,2,1)
grid.addWidget(QLabel("Column:"),2,2,alignment=Qt.AlignmentFlag.AlignRight)
self.column = QSpinBox()
self.column.setObjectName("column")
grid.addWidget(self.column,2,3)
def parse_form(self) -> dict|None:
"""
Pulls info from the Info form.
Returns:
dict: sheets, row, column
"""
if self.active.isChecked():
return dict(
sheets = self.sheet.text().split(","),
row = self.row.value(),
column = self.column.value()
)
else:
return None

View File

@@ -1,8 +1,6 @@
'''
Contains all submission related frontend functions
'''
import sys
from PyQt6.QtWidgets import (
QWidget, QPushButton, QVBoxLayout,
QComboBox, QDateEdit, QLineEdit, QLabel
@@ -11,7 +9,7 @@ from PyQt6.QtCore import pyqtSignal, Qt
from . import select_open_file, select_save_file
import logging, difflib
from pathlib import Path
from tools import Report, Result, check_not_nan, main_form_style, report_result, check_regex_match
from tools import Report, Result, check_not_nan, main_form_style, report_result
from backend.excel.parser import SheetParser
from backend.validators import PydSubmission, PydReagent
from backend.db import (
@@ -28,6 +26,9 @@ logger = logging.getLogger(f"submissions.{__name__}")
class MyQComboBox(QComboBox):
"""
Custom combobox that disables wheel events until focussed on.
"""
def __init__(self, scrollWidget=None, *args, **kwargs):
super(MyQComboBox, self).__init__(*args, **kwargs)
self.scrollWidget = scrollWidget
@@ -42,6 +43,9 @@ class MyQComboBox(QComboBox):
class MyQDateEdit(QDateEdit):
"""
Custom date editor that disables wheel events until focussed on.
"""
def __init__(self, scrollWidget=None, *args, **kwargs):
super(MyQDateEdit, self).__init__(*args, **kwargs)
self.scrollWidget = scrollWidget
@@ -340,8 +344,6 @@ class SubmissionFormWidget(QWidget):
_, result = self.pyd.check_kit_integrity()
report.add_result(result)
if len(result.results) > 0:
# self.app.report.add_result(report)
# self.app.report_result()
return
# logger.debug(f"PYD before transformation into SQL:\n\n{self.pyd}\n\n")
base_submission, result = self.pyd.to_sql()
@@ -370,14 +372,10 @@ class SubmissionFormWidget(QWidget):
else:
self.app.ctx.database_session.rollback()
report.add_result(Result(msg="Overwrite cancelled", status="Information"))
# self.app.report.add_result(report)
# self.app.report_result()
return report
# NOTE: code 2: No RSL plate number given
case 2:
report.add_result(result)
# self.app.report.add_result(report)
# self.app.report_result()
return report
case _:
pass
@@ -451,7 +449,6 @@ class SubmissionFormWidget(QWidget):
info[item] = value
for k, v in info.items():
self.pyd.set_attribute(key=k, value=v)
# NOTE: return submission
report.add_result(report)
return report
@@ -527,18 +524,18 @@ class SubmissionFormWidget(QWidget):
match key:
case 'submitting_lab':
add_widget = MyQComboBox(scrollWidget=parent)
# lookup organizations suitable for submitting_lab (ctx: self.InfoItem.SubmissionFormWidget.SubmissionFormContainer.AddSubForm )
# NOTE: lookup organizations suitable for submitting_lab (ctx: self.InfoItem.SubmissionFormWidget.SubmissionFormContainer.AddSubForm )
labs = [item.name for item in Organization.query()]
# try to set closest match to top of list
# 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
# set combobox values to lookedup values
# NOTE: set combobox values to lookedup values
add_widget.addItems(labs)
add_widget.setToolTip("Select submitting lab.")
case 'extraction_kit':
# if extraction kit not available, all other values fail
# NOTE: if extraction kit not available, all other values fail
if not check_not_nan(value):
msg = AlertPop(message="Make sure to check your extraction kit in the excel sheet!",
status="warning")
@@ -573,8 +570,6 @@ class SubmissionFormWidget(QWidget):
add_widget.addItems(cats)
add_widget.setToolTip("Enter submission category or select from list.")
case _:
# if key in sub_obj.get_default_info("form_ignore", submission_type=submission_type):
# return None
if key in sub_obj.timestamps():
add_widget = MyQDateEdit(calendarPopup=True, scrollWidget=parent)
# NOTE: sets submitted date based on date found in excel sheet
@@ -593,7 +588,6 @@ class SubmissionFormWidget(QWidget):
if add_widget is not None:
add_widget.setObjectName(key)
add_widget.setParent(parent)
# add_widget.setStyleSheet(main_form_style)
return add_widget
def update_missing(self):
@@ -649,7 +643,6 @@ class SubmissionFormWidget(QWidget):
self.label = self.ReagentParsedLabel(reagent=reagent)
layout.addWidget(self.label)
self.lot = self.ReagentLot(scrollWidget=parent, reagent=reagent, extraction_kit=extraction_kit)
# self.lot.setStyleSheet(main_form_style)
layout.addWidget(self.lot)
# NOTE: Remove spacing between reagents
layout.setContentsMargins(0, 0, 0, 0)
@@ -738,8 +731,6 @@ class SubmissionFormWidget(QWidget):
if check_not_nan(reagent.lot):
relevant_reagents.insert(0, str(reagent.lot))
else:
# looked_up_rt = KitTypeReagentRoleAssociation.query(reagent_role=reagent.role,
# kit_type=extraction_kit)
try:
looked_up_reg = Reagent.query(lot_number=looked_up_rt.last_used)
except AttributeError:
@@ -768,22 +759,4 @@ class SubmissionFormWidget(QWidget):
self.setObjectName(f"lot_{reagent.role}")
self.addItems(relevant_reagents)
self.setToolTip(f"Enter lot number for the reagent used for {reagent.role}")
# self.setStyleSheet(main_form_style)
# def relevant_reagents(self, assoc: KitTypeReagentRoleAssociation):
# # logger.debug(f"Attempting lookup of reagents by type: {reagent.type}")
# lookup = Reagent.query(reagent_role=assoc.reagent_role)
# try:
# regex = assoc.uses['exclude_regex']
# except KeyError:
# regex = "^$"
# relevant_reagents = [item for item in lookup if
# not check_regex_match(pattern=regex, check=str(item.lot))]
# for rel_reagent in relevant_reagents:
# # # NOTE: extract strings from any sets.
# # if isinstance(rel_reagent, set):
# # for thing in rel_reagent:
# # yield thing
# # elif isinstance(rel_reagent, str):
# # yield rel_reagent
# yield rel_reagent

View File

@@ -1,6 +1,6 @@
from PyQt6.QtCore import QSignalBlocker
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWidgets import QWidget, QGridLayout, QPushButton, QComboBox, QLabel
from PyQt6.QtWidgets import QWidget, QGridLayout, QPushButton, QLabel
from backend.db import Organization
from backend.excel import ReportMaker
from tools import Report
@@ -34,7 +34,6 @@ class Summary(QWidget):
for org in [org.name for org in Organization.query()]:
self.org_select.addItem(org)
self.org_select.model().itemChanged.connect(self.get_report)
# self.org_select.itemChecked.connect(self.get_report)
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)