New table view.

This commit is contained in:
lwark
2025-05-06 13:21:03 -05:00
parent 5508f68bc8
commit 20952f2edd
10 changed files with 382 additions and 17 deletions

View File

@@ -221,10 +221,10 @@ class BaseClass(Base):
Returns: Returns:
Any | List[Any]: Single result if limit = 1 or List if other. Any | List[Any]: Single result if limit = 1 or List if other.
""" """
logger.debug(f"Kwargs: {kwargs}") # logger.debug(f"Kwargs: {kwargs}")
if model is None: if model is None:
model = cls model = cls
logger.debug(f"Model: {model}") # logger.debug(f"Model: {model}")
if query is None: if query is None:
query: Query = cls.__database_session__.query(model) query: Query = cls.__database_session__.query(model)
singles = model.get_default_info('singles') singles = model.get_default_info('singles')
@@ -516,7 +516,7 @@ from .controls import *
from .organizations import * from .organizations import *
from .kits import * from .kits import *
from .submissions import * from .submissions import *
from .audit import * from .audit import AuditLog
# NOTE: Add a creator to the submission for reagent association. Assigned here due to circular import constraints. # NOTE: Add a creator to the submission for reagent association. Assigned here due to circular import constraints.
# https://docs.sqlalchemy.org/en/20/orm/extensions/associationproxy.html#sqlalchemy.ext.associationproxy.association_proxy.params.creator # https://docs.sqlalchemy.org/en/20/orm/extensions/associationproxy.html#sqlalchemy.ext.associationproxy.association_proxy.params.creator

View File

@@ -1289,6 +1289,7 @@ class SubmissionType(BaseClass):
query: Query = cls.__database_session__.query(cls) query: Query = cls.__database_session__.query(cls)
match name: match name:
case str(): case str():
logger.debug(f"querying with {name}")
query = query.filter(cls.name == name) query = query.filter(cls.name == name)
limit = 1 limit = 1
case _: case _:

View File

@@ -54,7 +54,7 @@ class ClientSubmission(BaseClass, LogMixin):
_submission_category = Column( _submission_category = Column(
String(64)) #: ["Research", "Diagnostic", "Surveillance", "Validation"], else defaults to submission_type_name String(64)) #: ["Research", "Diagnostic", "Surveillance", "Validation"], else defaults to submission_type_name
sample_count = Column(INTEGER) #: Number of samples in the submission sample_count = Column(INTEGER) #: Number of samples in the submission
comment = Column(JSON)
runs = relationship("BasicSubmission", back_populates="client_submission") #: many-to-one relationship runs = relationship("BasicSubmission", back_populates="client_submission") #: many-to-one relationship
contact = relationship("Contact", back_populates="submissions") #: client org contact = relationship("Contact", back_populates="submissions") #: client org
@@ -92,6 +92,192 @@ class ClientSubmission(BaseClass, LogMixin):
except AttributeError: except AttributeError:
self._submission_category = "NA" self._submission_category = "NA"
@classmethod
def recruit_parser(cls):
pass
@classmethod
@setup_lookup
def query(cls,
submissiontype: str | SubmissionType | None = None,
submission_type_name: str | None = None,
id: int | str | None = None,
submitter_plate_num: str | None = None,
start_date: date | datetime | str | int | None = None,
end_date: date | datetime | str | int | None = None,
chronologic: bool = False,
limit: int = 0,
page: int = 1,
page_size: None | int = 250,
**kwargs
) -> BasicSubmission | List[BasicSubmission]:
"""
Lookup submissions based on a number of parameters. Overrides parent.
Args:
submission_type (str | models.SubmissionType | None, optional): Submission type of interest. Defaults to None.
id (int | str | None, optional): Submission id in the database (limits results to 1). Defaults to None.
rsl_plate_num (str | None, optional): Submission name in the database (limits results to 1). Defaults to None.
start_date (date | str | int | None, optional): Beginning date to search by. Defaults to None.
end_date (date | str | int | None, optional): Ending date to search by. Defaults to None.
reagent (models.Reagent | str | None, optional): A reagent used in the submission. Defaults to None.
chronologic (bool, optional): Return results in chronologic order. Defaults to False.
limit (int, optional): Maximum number of results to return. Defaults to 0.
Returns:
models.BasicSubmission | List[models.BasicSubmission]: Submission(s) of interest
"""
# from ... import SubmissionReagentAssociation
# NOTE: if you go back to using 'model' change the appropriate cls to model in the query filters
query: Query = cls.__database_session__.query(cls)
if start_date is not None and end_date is None:
logger.warning(f"Start date with no end date, using today.")
end_date = date.today()
if end_date is not None and start_date is None:
# NOTE: this query returns a tuple of (object, datetime), need to get only datetime.
start_date = cls.__database_session__.query(cls, func.min(cls.submitted_date)).first()[1]
logger.warning(f"End date with no start date, using first submission date: {start_date}")
if start_date is not None:
start_date = cls.rectify_query_date(start_date)
end_date = cls.rectify_query_date(end_date, eod=True)
logger.debug(f"Start date: {start_date}, end date: {end_date}")
query = query.filter(cls.submitted_date.between(start_date, end_date))
# NOTE: by rsl number (returns only a single value)
match submitter_plate_num:
case str():
query = query.filter(cls.submitter_plate_num == submitter_plate_num)
limit = 1
case _:
pass
match submission_type_name:
case str():
query = query.filter(cls.submission_type_name == submission_type_name)
case _:
pass
# NOTE: by id (returns only a single value)
match id:
case int():
query = query.filter(cls.id == id)
limit = 1
case str():
query = query.filter(cls.id == int(id))
limit = 1
case _:
pass
# query = query.order_by(cls.submitted_date.desc())
# NOTE: Split query results into pages of size {page_size}
if page_size > 0:
query = query.limit(page_size)
page = page - 1
if page is not None:
query = query.offset(page * page_size)
return cls.execute_query(query=query, model=cls, limit=limit, **kwargs)
@classmethod
def submissions_to_df(cls, submission_type: str | None = None, limit: int = 0,
chronologic: bool = True, page: int = 1, page_size: int = 250) -> pd.DataFrame:
"""
Convert all submissions to dataframe
Args:
page_size (int, optional): Number of items to include in query result. Defaults to 250.
page (int, optional): Limits the number of submissions to a page size. Defaults to 1.
chronologic (bool, optional): Sort submissions in chronologic order. Defaults to True.
submission_type (str | None, optional): Filter by SubmissionType. Defaults to None.
limit (int, optional): Maximum number of results to return. Defaults to 0.
Returns:
pd.DataFrame: Pandas Dataframe of all relevant submissions
"""
# NOTE: use lookup function to create list of dicts
subs = [item.to_dict() for item in
cls.query(submissiontype=submission_type, limit=limit, chronologic=chronologic, page=page,
page_size=page_size)]
df = pd.DataFrame.from_records(subs)
# NOTE: Exclude sub information
exclude = ['controls', 'extraction_info', 'pcr_info', 'comment', 'comments', 'samples', 'reagents',
'equipment', 'gel_info', 'gel_image', 'dna_core_submission_number', 'gel_controls',
'source_plates', 'pcr_technician', 'ext_technician', 'artic_technician', 'cost_centre',
'signed_by', 'artic_date', 'gel_barcode', 'gel_date', 'ngs_date', 'contact_phone', 'contact',
'tips', 'gel_image_path', 'custom']
# NOTE: dataframe equals dataframe of all columns not in exclude
df = df.loc[:, ~df.columns.isin(exclude)]
if chronologic:
try:
df.sort_values(by="id", axis=0, inplace=True, ascending=False)
except KeyError:
logger.error("No column named 'id'")
# NOTE: Human friendly column labels
df.columns = [item.replace("_", " ").title() for item in df.columns]
return df
def to_dict(self, full_data: bool = False, backup: bool = False, report: bool = False) -> dict:
"""
Constructs dictionary used in submissions summary
Args:
expand (bool, optional): indicates if generators to be expanded. Defaults to False.
report (bool, optional): indicates if to be used for a report. Defaults to False.
full_data (bool, optional): indicates if sample dicts to be constructed. Defaults to False.
backup (bool, optional): passed to adjust_to_dict_samples. Defaults to False.
Returns:
dict: dictionary used in submissions summary and details
"""
# NOTE: get lab from nested organization object
try:
sub_lab = self.submitting_lab.name
except AttributeError:
sub_lab = None
try:
sub_lab = sub_lab.replace("_", " ").title()
except AttributeError:
pass
# NOTE: get extraction kit name from nested kit object
output = {
"id": self.id,
"submission_type": self.submission_type_name,
"submitter_plate_number": self.submitter_plate_num,
"submitted_date": self.submitted_date.strftime("%Y-%m-%d"),
"submitting_lab": sub_lab,
"sample_count": self.sample_count,
}
if report:
return output
if full_data:
# dicto, _ = self.extraction_kit.construct_xl_map_for_use(self.submission_type)
# samples = self.generate_associations(name="submission_sample_associations")
samples = None
runs = [item.to_dict() for item in self.runs]
# custom = self.custom
else:
samples = None
custom = None
runs = None
try:
comments = self.comment
except Exception as e:
logger.error(f"Error setting comment: {self.comment}, {e}")
comments = None
try:
contact = self.contact.name
except AttributeError as e:
try:
contact = f"Defaulted to: {self.submitting_lab.contacts[0].name}"
except (AttributeError, IndexError):
contact = "NA"
try:
contact_phone = self.contact.phone
except AttributeError:
contact_phone = "NA"
output["submission_category"] = self.submission_category
output["samples"] = samples
output["comment"] = comments
output["contact"] = contact
output["contact_phone"] = contact_phone
# output["custom"] = custom
output["runs"] = runs
return output
class BasicSubmission(BaseClass, LogMixin): class BasicSubmission(BaseClass, LogMixin):
""" """

View File

@@ -546,6 +546,7 @@ class EquipmentParser(object):
logger.error(f"Unable to add {eq} to list.") logger.error(f"Unable to add {eq} to list.")
continue continue
class TipParser(object): class TipParser(object):
""" """
Object to pull data for tips in excel sheet Object to pull data for tips in excel sheet
@@ -678,3 +679,19 @@ class ConcentrationParser(object):
self.submission_obj = submission self.submission_obj = submission
rsl_plate_num = self.submission_obj.rsl_plate_num rsl_plate_num = self.submission_obj.rsl_plate_num
self.samples = self.submission_obj.parse_concentration(xl=self.xl, rsl_plate_num=rsl_plate_num) self.samples = self.submission_obj.parse_concentration(xl=self.xl, rsl_plate_num=rsl_plate_num)
# NOTE: Generified parsers below
class InfoParserV2(object):
"""
Object for retrieving submitter info from sample list sheet
"""
default_range = dict(
start_row=2,
end_row=18,
start_column=7,
end_column=8,
sheet="Sample List"
)

View File

@@ -205,4 +205,4 @@ class RSLNamer(object):
from .pydant import PydSubmission, PydKitType, PydContact, PydOrganization, PydSample, PydReagent, PydReagentRole, \ from .pydant import PydSubmission, PydKitType, PydContact, PydOrganization, PydSample, PydReagent, PydReagentRole, \
PydEquipment, PydEquipmentRole, PydTips, PydPCRControl, PydIridaControl, PydProcess, PydElastic PydEquipment, PydEquipmentRole, PydTips, PydPCRControl, PydIridaControl, PydProcess, PydElastic, PydClientSubmission

View File

@@ -1328,3 +1328,35 @@ class PydElastic(BaseModel, extra="allow", arbitrary_types_allowed=True):
field_value = getattr(self, field) field_value = getattr(self, field)
self.instance.__setattr__(field, field_value) self.instance.__setattr__(field, field_value)
return self.instance return self.instance
# NOTE: Generified objects below:
class PydClientSubmission(BaseModel, extra="allow"):
filepath: Path
submission_type: dict | None
submitter_plate_num: dict | None = Field(default=dict(value=None, missing=True), validate_default=True)
submitted_date: dict | None
submitted_date: dict | None = Field(default=dict(value=date.today(), missing=True), validate_default=True)
submitting_lab: dict | None
sample_count: dict | None
kittype: dict | None
submission_category: dict | None = Field(default=dict(value=None, missing=True), validate_default=True)
comment: dict | None = Field(default=dict(value="", missing=True), validate_default=True)
cost_centre: dict | None = Field(default=dict(value=None, missing=True), validate_default=True)
contact: dict | None = Field(default=dict(value=None, missing=True), validate_default=True)
def to_form(self, parent: QWidget, disable: list | None = None):
"""
Converts this instance into a frontend.widgets.submission_widget.SubmissionFormWidget
Args:
disable (list, optional): a list of widgets to be disabled in the form. Defaults to None.
parent (QWidget): parent widget of the constructed object
Returns:
SubmissionFormWidget: Submission form widget
"""
from frontend.widgets.submission_widget import ClientSubmissionFormWidget
return ClientSubmissionFormWidget(parent=parent, submission=self, disable=disable)

View File

@@ -22,7 +22,7 @@ from .date_type_picker import DateTypePicker
from .functions import select_save_file from .functions import select_save_file
from .pop_ups import HTMLPop from .pop_ups import HTMLPop
from .misc import Pagifier from .misc import Pagifier
from .submission_table import SubmissionsSheet from .submission_table import SubmissionsSheet, SubmissionsTree, ClientRunModel
from .submission_widget import SubmissionFormContainer from .submission_widget import SubmissionFormContainer
from .controls_chart import ControlsViewer from .controls_chart import ControlsViewer
from .summary import Summary from .summary import Summary
@@ -253,7 +253,8 @@ class AddSubForm(QWidget):
self.sheetwidget = QWidget(self) self.sheetwidget = QWidget(self)
self.sheetlayout = QVBoxLayout(self) self.sheetlayout = QVBoxLayout(self)
self.sheetwidget.setLayout(self.sheetlayout) self.sheetwidget.setLayout(self.sheetlayout)
self.sub_wid = SubmissionsSheet(parent=parent) # self.sub_wid = SubmissionsSheet(parent=parent)
self.sub_wid = SubmissionsTree(parent=parent, model=ClientRunModel(self))
self.pager = Pagifier(page_max=self.sub_wid.total_count / page_size) self.pager = Pagifier(page_max=self.sub_wid.total_count / page_size)
self.sheetlayout.addWidget(self.sub_wid) self.sheetlayout.addWidget(self.sub_wid)
self.sheetlayout.addWidget(self.pager) self.sheetlayout.addWidget(self.pager)

View File

@@ -34,7 +34,11 @@ class SampleChecker(QDialog):
template_path = Path(template.environment.loader.__getattribute__("searchpath")[0]) template_path = Path(template.environment.loader.__getattribute__("searchpath")[0])
with open(template_path.joinpath("css", "styles.css"), "r") as f: with open(template_path.joinpath("css", "styles.css"), "r") as f:
css = f.read() css = f.read()
html = template.render(samples=self.formatted_list, css=css) try:
samples = self.formatted_list
except AttributeError:
samples = []
html = template.render(samples=samples, css=css)
self.webview.setHtml(html) self.webview.setHtml(html)
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn) self.buttonBox = QDialogButtonBox(QBtn)

View File

@@ -2,11 +2,13 @@
Contains widgets specific to the submission summary and submission details. Contains widgets specific to the submission summary and submission details.
""" """
import logging import logging
import sys
from pprint import pformat from pprint import pformat
from PyQt6.QtWidgets import QTableView, QMenu from PyQt6.QtWidgets import QTableView, QMenu, QTreeView, QStyledItemDelegate, QStyle, QStyleOptionViewItem, \
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel QHeaderView, QAbstractItemView
from PyQt6.QtGui import QAction, QCursor from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel, pyqtSlot, QModelIndex
from backend.db.models import BasicSubmission from PyQt6.QtGui import QAction, QCursor, QStandardItemModel, QStandardItem, QIcon, QColor
from backend.db.models import BasicSubmission, ClientSubmission
from tools import Report, Result, report_result from tools import Report, Result, report_result
from .functions import select_open_file from .functions import select_open_file
@@ -84,6 +86,7 @@ class SubmissionsSheet(QTableView):
""" """
sets data in model sets data in model
""" """
# self.data = ClientSubmission.submissions_to_df(page=page, page_size=page_size)
self.data = BasicSubmission.submissions_to_df(page=page, page_size=page_size) self.data = BasicSubmission.submissions_to_df(page=page, page_size=page_size)
try: try:
self.data['Id'] = self.data['Id'].apply(str) self.data['Id'] = self.data['Id'].apply(str)
@@ -222,3 +225,106 @@ class SubmissionsSheet(QTableView):
sub.save() sub.save()
report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information')) report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information'))
return report return report
class RunDelegate(QStyledItemDelegate):
def __init__(self, parent=None):
super(RunDelegate, self).__init__(parent)
self._plus_icon = QIcon("plus.png")
self._minus_icon = QIcon("minus.png")
def initStyleOption(self, option, index):
super(RunDelegate, self).initStyleOption(option, index)
if not index.parent().isValid():
is_open = bool(option.state & QStyle.StateFlag.State_Open)
option.features |= QStyleOptionViewItem.ViewItemFeature.HasDecoration
option.icon = self._minus_icon if is_open else self._plus_icon
class SubmissionsTree(QTreeView):
"""
https://stackoverflow.com/questions/54385437/how-can-i-make-a-table-that-can-collapse-its-rows-into-categories-in-qt
"""
def __init__(self, model, parent=None):
super(SubmissionsTree, self).__init__(parent)
self.total_count = 1
self.setIndentation(0)
self.setExpandsOnDoubleClick(False)
self.clicked.connect(self.on_clicked)
delegate = RunDelegate(self)
self.setItemDelegateForColumn(0, delegate)
self.model = model
self.setModel(self.model)
# self.header().setSectionResizeMode(0, QHeaderView.sectionResizeMode(self,0).ResizeToContents)
self.setSelectionBehavior(QAbstractItemView.selectionBehavior(self).SelectRows)
# self.setStyleSheet("background-color: #0D1225;")
self.set_data()
@pyqtSlot(QModelIndex)
def on_clicked(self, index):
if not index.parent().isValid() and index.column() == 0:
self.setExpanded(index, not self.isExpanded(index))
def set_data(self, page: int = 1, page_size: int = 250) -> None:
"""
sets data in model
"""
# self.data = ClientSubmission.submissions_to_df(page=page, page_size=page_size)
self.data = [item.to_dict(full_data=True) for item in ClientSubmission.query(chronologic=True, page=page, page_size=page_size)]
logger.debug(pformat(self.data))
# sys.exit()
for submission in self.data:
group_item = self.model.add_group(submission['submitter_plate_number'])
for run in submission['runs']:
self.model.append_element_to_group(group_item=group_item, texts=run['plate_number'])
def link_extractions(self):
pass
def link_pcr(self):
pass
class ClientRunModel(QStandardItemModel):
def __init__(self, parent=None):
super(ClientRunModel, self).__init__(parent)
self.setColumnCount(8)
self.setHorizontalHeaderLabels(["id", "Name", "Library", "Release Date", "Genre(s)", "Last Played", "Time Played", ""])
for i in range(self.columnCount()):
it = self.horizontalHeaderItem(i)
# it.setForeground(QColor("#F2F2F2"))
def add_group(self, group_name):
item_root = QStandardItem()
item_root.setEditable(False)
item = QStandardItem(group_name)
item.setEditable(False)
ii = self.invisibleRootItem()
i = ii.rowCount()
for j, it in enumerate((item_root, item)):
ii.setChild(i, j, it)
ii.setEditable(False)
for j in range(self.columnCount()):
it = ii.child(i, j)
if it is None:
it = QStandardItem()
ii.setChild(i, j, it)
# it.setBackground(QColor("#002842"))
# it.setForeground(QColor("#F2F2F2"))
return item_root
def append_element_to_group(self, group_item, texts):
j = group_item.rowCount()
item_icon = QStandardItem()
item_icon.setEditable(False)
item_icon.setIcon(QIcon("game.png"))
# item_icon.setBackground(QColor("#0D1225"))
group_item.setChild(j, 0, item_icon)
for i, text in enumerate(texts):
item = QStandardItem(text)
item.setEditable(False)
# item.setBackground(QColor("#0D1225"))
# item.setForeground(QColor("#F2F2F2"))
group_item.setChild(j, i+1, item)

View File

@@ -10,7 +10,7 @@ from .functions import select_open_file, select_save_file
import logging import logging
from pathlib import Path from pathlib import Path
from tools import Report, Result, check_not_nan, main_form_style, report_result, get_application_from_parent from tools import Report, Result, check_not_nan, main_form_style, report_result, get_application_from_parent
from backend.excel.parser import SheetParser from backend.excel.parsers import SheetParser, InfoParserV2
from backend.validators import PydSubmission, PydReagent from backend.validators import PydSubmission, PydReagent
from backend.db import ( from backend.db import (
Organization, SubmissionType, Reagent, Organization, SubmissionType, Reagent,
@@ -121,13 +121,14 @@ class SubmissionFormContainer(QWidget):
return report return report
# NOTE: create sheetparser using excel sheet and context from gui # NOTE: create sheetparser using excel sheet and context from gui
try: try:
self.prsr = SheetParser(filepath=fname) # self.prsr = SheetParser(filepath=fname)
self.parser = InfoParserV2(filepath=fname)
except PermissionError: except PermissionError:
logger.error(f"Couldn't get permission to access file: {fname}") logger.error(f"Couldn't get permission to access file: {fname}")
return return
except AttributeError: except AttributeError:
self.prsr = SheetParser(filepath=fname) self.parser = InfoParserV2(filepath=fname)
self.pyd = self.prsr.to_pydantic() self.pyd = self.parser.to_pydantic()
# logger.debug(f"Samples: {pformat(self.pyd.samples)}") # logger.debug(f"Samples: {pformat(self.pyd.samples)}")
checker = SampleChecker(self, "Sample Checker", self.pyd) checker = SampleChecker(self, "Sample Checker", self.pyd)
if checker.exec(): if checker.exec():
@@ -177,11 +178,13 @@ class SubmissionFormWidget(QWidget):
self.missing_info = [] self.missing_info = []
self.submission_type = SubmissionType.query(name=self.pyd.submission_type['value']) self.submission_type = SubmissionType.query(name=self.pyd.submission_type['value'])
basic_submission_class = self.submission_type.submission_class basic_submission_class = self.submission_type.submission_class
logger.debug(f"Basic submission class: {basic_submission_class}")
defaults = basic_submission_class.get_default_info("form_recover", "form_ignore", submission_type=self.pyd.submission_type['value']) defaults = basic_submission_class.get_default_info("form_recover", "form_ignore", submission_type=self.pyd.submission_type['value'])
self.recover = defaults['form_recover'] self.recover = defaults['form_recover']
self.ignore = defaults['form_ignore'] self.ignore = defaults['form_ignore']
self.layout = QVBoxLayout() self.layout = QVBoxLayout()
for k in list(self.pyd.model_fields.keys()) + list(self.pyd.model_extra.keys()): for k in list(self.pyd.model_fields.keys()) + list(self.pyd.model_extra.keys()):
logger.debug(f"Pydantic field: {k}")
if k in self.ignore: if k in self.ignore:
logger.warning(f"{k} in form_ignore {self.ignore}, not creating widget") logger.warning(f"{k} in form_ignore {self.ignore}, not creating widget")
continue continue
@@ -197,6 +200,7 @@ class SubmissionFormWidget(QWidget):
value = self.pyd.model_extra[k] value = self.pyd.model_extra[k]
except KeyError: except KeyError:
value = dict(value=None, missing=True) value = dict(value=None, missing=True)
logger.debug(f"Pydantic value: {value}")
add_widget = self.create_widget(key=k, value=value, submission_type=self.submission_type, add_widget = self.create_widget(key=k, value=value, submission_type=self.submission_type,
sub_obj=basic_submission_class, disable=check) sub_obj=basic_submission_class, disable=check)
if add_widget is not None: if add_widget is not None:
@@ -208,7 +212,8 @@ class SubmissionFormWidget(QWidget):
self.layout.addWidget(self.disabler) self.layout.addWidget(self.disabler)
self.disabler.checkbox.checkStateChanged.connect(self.disable_reagents) self.disabler.checkbox.checkStateChanged.connect(self.disable_reagents)
self.setStyleSheet(main_form_style) self.setStyleSheet(main_form_style)
self.scrape_reagents(self.extraction_kit) # self.scrape_reagents(self.extraction_kit)
self.setLayout(self.layout)
def disable_reagents(self): def disable_reagents(self):
""" """
@@ -774,3 +779,16 @@ class SubmissionFormWidget(QWidget):
layout.addWidget(self.label) layout.addWidget(self.label)
layout.addWidget(self.checkbox) layout.addWidget(self.checkbox)
self.setLayout(layout) self.setLayout(layout)
class ClientSubmissionFormWidget(SubmissionFormWidget):
def __init__(self, parent: QWidget, submission: PydSubmission, disable: list | None = None) -> None:
super().__init__(parent, submission=submission, disable=disable)
save_btn = QPushButton("Save")
start_run_btn = QPushButton("Save && Start Run")
self.layout.addWidget(save_btn)
self.layout.addWidget(start_run_btn)