Addition of Equipment and SubmissionType creation.
This commit is contained in:
@@ -18,6 +18,8 @@ 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 SubbmissionTypeAdder
|
||||
|
||||
|
||||
logger = logging.getLogger(f'submissions.{__name__}')
|
||||
logger.info("Hello, I am a logger")
|
||||
@@ -207,11 +209,13 @@ class AddSubForm(QWidget):
|
||||
self.tab1 = QWidget()
|
||||
self.tab2 = QWidget()
|
||||
self.tab3 = QWidget()
|
||||
self.tab4 = QWidget()
|
||||
self.tabs.resize(300,200)
|
||||
# Add tabs
|
||||
self.tabs.addTab(self.tab1,"Submissions")
|
||||
self.tabs.addTab(self.tab2,"Controls")
|
||||
self.tabs.addTab(self.tab3, "Add Kit")
|
||||
self.tabs.addTab(self.tab3, "Add SubmissionType")
|
||||
self.tabs.addTab(self.tab4, "Add Kit")
|
||||
# Create submission adder form
|
||||
self.formwidget = SubmissionFormContainer(self)
|
||||
self.formlayout = QVBoxLayout(self)
|
||||
@@ -238,10 +242,14 @@ class AddSubForm(QWidget):
|
||||
self.tab2.layout.addWidget(self.controls_viewer)
|
||||
self.tab2.setLayout(self.tab2.layout)
|
||||
# create custom widget to add new tabs
|
||||
adder = KitAdder(self)
|
||||
ST_adder = SubbmissionTypeAdder(self)
|
||||
self.tab3.layout = QVBoxLayout(self)
|
||||
self.tab3.layout.addWidget(adder)
|
||||
self.tab3.layout.addWidget(ST_adder)
|
||||
self.tab3.setLayout(self.tab3.layout)
|
||||
kit_adder = KitAdder(self)
|
||||
self.tab4.layout = QVBoxLayout(self)
|
||||
self.tab4.layout.addWidget(kit_adder)
|
||||
self.tab4.setLayout(self.tab4.layout)
|
||||
# add tabs to main widget
|
||||
self.layout.addWidget(self.tabs)
|
||||
self.setLayout(self.layout)
|
||||
89
src/submissions/frontend/widgets/equipment_usage.py
Normal file
89
src/submissions/frontend/widgets/equipment_usage.py
Normal file
@@ -0,0 +1,89 @@
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtWidgets import (QDialog, QComboBox, QCheckBox,
|
||||
QLabel, QWidget, QHBoxLayout,
|
||||
QVBoxLayout, QDialogButtonBox)
|
||||
from backend.db.models import SubmissionType
|
||||
from backend.validators.pydant import PydEquipment, PydEquipmentPool
|
||||
|
||||
class EquipmentUsage(QDialog):
|
||||
|
||||
def __init__(self, parent, submission_type:SubmissionType|str) -> QDialog:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Equipment Checklist")
|
||||
if isinstance(submission_type, str):
|
||||
submission_type = SubmissionType.query(name=submission_type)
|
||||
# self.static_equipment = submission_type.get_equipment()
|
||||
self.opt_equipment = submission_type.get_equipment()
|
||||
self.layout = QVBoxLayout()
|
||||
self.setLayout(self.layout)
|
||||
self.populate_form()
|
||||
|
||||
def populate_form(self):
|
||||
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||
self.buttonBox = QDialogButtonBox(QBtn)
|
||||
self.buttonBox.accepted.connect(self.accept)
|
||||
self.buttonBox.rejected.connect(self.reject)
|
||||
for eq in self.opt_equipment:
|
||||
self.layout.addWidget(eq.toForm(parent=self))
|
||||
self.layout.addWidget(self.buttonBox)
|
||||
|
||||
def parse_form(self):
|
||||
output = []
|
||||
for widget in self.findChildren(QWidget):
|
||||
match widget:
|
||||
case (EquipmentCheckBox()|PoolComboBox()) :
|
||||
output.append(widget.parse_form())
|
||||
case _:
|
||||
pass
|
||||
return [item for item in output if item != None]
|
||||
|
||||
class EquipmentCheckBox(QWidget):
|
||||
|
||||
def __init__(self, parent, equipment:PydEquipment) -> None:
|
||||
super().__init__(parent)
|
||||
self.layout = QHBoxLayout()
|
||||
self.label = QLabel()
|
||||
self.label.setMaximumWidth(125)
|
||||
self.label.setMinimumWidth(125)
|
||||
self.check = QCheckBox()
|
||||
if equipment.static:
|
||||
self.check.setChecked(True)
|
||||
# self.check.setEnabled(False)
|
||||
if equipment.nickname != None:
|
||||
text = f"{equipment.name} ({equipment.nickname})"
|
||||
else:
|
||||
text = equipment.name
|
||||
self.setObjectName(equipment.name)
|
||||
self.label.setText(text)
|
||||
self.layout.addWidget(self.label)
|
||||
self.layout.addWidget(self.check)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def parse_form(self) -> str|None:
|
||||
if self.check.isChecked():
|
||||
return self.objectName()
|
||||
else:
|
||||
return None
|
||||
|
||||
class PoolComboBox(QWidget):
|
||||
|
||||
def __init__(self, parent, pool:PydEquipmentPool) -> None:
|
||||
super().__init__(parent)
|
||||
self.layout = QHBoxLayout()
|
||||
# label = QLabel()
|
||||
# label.setText(pool.name)
|
||||
self.box = QComboBox()
|
||||
self.box.setMaximumWidth(125)
|
||||
self.box.setMinimumWidth(125)
|
||||
self.box.addItems([item.name for item in pool.equipment])
|
||||
self.check = QCheckBox()
|
||||
# self.layout.addWidget(label)
|
||||
self.layout.addWidget(self.box)
|
||||
self.layout.addWidget(self.check)
|
||||
self.setLayout(self.layout)
|
||||
|
||||
def parse_form(self) -> str:
|
||||
if self.check.isChecked():
|
||||
return self.box.currentText()
|
||||
else:
|
||||
return None
|
||||
@@ -82,7 +82,7 @@ class KitAdder(QWidget):
|
||||
print(self.app)
|
||||
# get bottommost row
|
||||
maxrow = self.grid.rowCount()
|
||||
reg_form = ReagentTypeForm()
|
||||
reg_form = ReagentTypeForm(parent=self)
|
||||
reg_form.setObjectName(f"ReagentForm_{maxrow}")
|
||||
# self.grid.addWidget(reg_form, maxrow + 1,0,1,2)
|
||||
self.grid.addWidget(reg_form, maxrow,0,1,4)
|
||||
@@ -139,8 +139,8 @@ class ReagentTypeForm(QWidget):
|
||||
"""
|
||||
custom widget to add information about a new reagenttype
|
||||
"""
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
def __init__(self, parent) -> None:
|
||||
super().__init__(parent)
|
||||
grid = QGridLayout()
|
||||
self.setLayout(grid)
|
||||
grid.addWidget(QLabel("Reagent Type Name"),0,0)
|
||||
|
||||
@@ -53,7 +53,7 @@ class KitSelector(QDialog):
|
||||
super().__init__()
|
||||
self.setWindowTitle(title)
|
||||
self.widget = QComboBox()
|
||||
kits = [item.__str__() for item in KitType.query()]
|
||||
kits = [item.name for item in KitType.query()]
|
||||
self.widget.addItems(kits)
|
||||
self.widget.setEditable(False)
|
||||
# set yes/no buttons
|
||||
|
||||
@@ -15,11 +15,12 @@ from PyQt6.QtWidgets import (
|
||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel
|
||||
from PyQt6.QtGui import QAction, QCursor, QPixmap, QPainter
|
||||
from backend.db.models import BasicSubmission
|
||||
from backend.db.models import BasicSubmission, Equipment, SubmissionEquipmentAssociation
|
||||
from backend.excel import make_report_html, make_report_xlsx
|
||||
from tools import check_if_app, Report, Result, jinja_template_loading, get_first_blank_df_row, row_map
|
||||
from xhtml2pdf import pisa
|
||||
from .pop_ups import QuestionAsker
|
||||
from .equipment_usage import EquipmentUsage
|
||||
from ..visualizations import make_plate_barcode, make_plate_map, make_plate_map_html
|
||||
from .functions import select_save_file, select_open_file
|
||||
from .misc import ReportDatePicker
|
||||
@@ -159,22 +160,44 @@ class SubmissionsSheet(QTableView):
|
||||
# barcodeAction = QAction("Print Barcode", self)
|
||||
commentAction = QAction("Add Comment", self)
|
||||
backupAction = QAction("Backup", self)
|
||||
equipAction = QAction("Add Equipment", self)
|
||||
# hitpickAction = QAction("Hitpicks", self)
|
||||
renameAction.triggered.connect(lambda: self.delete_item(event))
|
||||
detailsAction.triggered.connect(lambda: self.show_details())
|
||||
# barcodeAction.triggered.connect(lambda: self.create_barcode())
|
||||
commentAction.triggered.connect(lambda: self.add_comment())
|
||||
backupAction.triggered.connect(lambda: self.regenerate_submission_form())
|
||||
equipAction.triggered.connect(lambda: self.add_equipment())
|
||||
# hitpickAction.triggered.connect(lambda: self.hit_pick())
|
||||
self.menu.addAction(detailsAction)
|
||||
self.menu.addAction(renameAction)
|
||||
# self.menu.addAction(barcodeAction)
|
||||
self.menu.addAction(commentAction)
|
||||
self.menu.addAction(backupAction)
|
||||
self.menu.addAction(equipAction)
|
||||
# self.menu.addAction(hitpickAction)
|
||||
# add other required actions
|
||||
self.menu.popup(QCursor.pos())
|
||||
|
||||
def add_equipment(self):
|
||||
index = (self.selectionModel().currentIndex())
|
||||
value = index.sibling(index.row(),0).data()
|
||||
self.add_equipment_function(rsl_plate_id=value)
|
||||
|
||||
def add_equipment_function(self, rsl_plate_id):
|
||||
submission = BasicSubmission.query(id=rsl_plate_id)
|
||||
submission_type = submission.submission_type_name
|
||||
dlg = EquipmentUsage(parent=self, submission_type=submission_type)
|
||||
if dlg.exec():
|
||||
equipment = dlg.parse_form()
|
||||
for equip in equipment:
|
||||
e = Equipment.query(name=equip)
|
||||
assoc = SubmissionEquipmentAssociation(submission=submission, equipment=e)
|
||||
# submission.submission_equipment_associations.append(assoc)
|
||||
logger.debug(f"Appending SubmissionEquipmentAssociation: {assoc}")
|
||||
# submission.save()
|
||||
assoc.save()
|
||||
|
||||
def delete_item(self, event):
|
||||
"""
|
||||
Confirms user deletion and sends id to backend for deletion.
|
||||
@@ -193,65 +216,6 @@ class SubmissionsSheet(QTableView):
|
||||
return
|
||||
self.setData()
|
||||
|
||||
# def hit_pick(self):
|
||||
# """
|
||||
# Extract positive samples from submissions with PCR results and export to csv.
|
||||
# NOTE: For this to work for arbitrary samples, positive samples must have 'positive' in their name
|
||||
# """
|
||||
# # Get all selected rows
|
||||
# indices = self.selectionModel().selectedIndexes()
|
||||
# # convert to id numbers
|
||||
# indices = [index.sibling(index.row(), 0).data() for index in indices]
|
||||
# # biomek can handle 4 plates maximum
|
||||
# if len(indices) > 4:
|
||||
# logger.error(f"Error: Had to truncate number of plates to 4.")
|
||||
# indices = indices[:4]
|
||||
# # lookup ids in the database
|
||||
# # subs = [lookup_submissions(ctx=self.ctx, id=id) for id in indices]
|
||||
# subs = [BasicSubmission.query(id=id) for id in indices]
|
||||
# # full list of samples
|
||||
# dicto = []
|
||||
# # list to contain plate images
|
||||
# images = []
|
||||
# for iii, sub in enumerate(subs):
|
||||
# # second check to make sure there aren't too many plates
|
||||
# if iii > 3:
|
||||
# logger.error(f"Error: Had to truncate number of plates to 4.")
|
||||
# continue
|
||||
# plate_dicto = sub.hitpick_plate(plate_number=iii+1)
|
||||
# if plate_dicto == None:
|
||||
# continue
|
||||
# image = make_plate_map(plate_dicto)
|
||||
# images.append(image)
|
||||
# for item in plate_dicto:
|
||||
# if len(dicto) < 94:
|
||||
# dicto.append(item)
|
||||
# else:
|
||||
# logger.error(f"We had to truncate the number of samples to 94.")
|
||||
# logger.debug(f"We found {len(dicto)} to hitpick")
|
||||
# # convert all samples to dataframe
|
||||
# df = make_hitpicks(dicto)
|
||||
# df = df[df.positive != False]
|
||||
# logger.debug(f"Size of the dataframe: {df.shape[0]}")
|
||||
# msg = AlertPop(message=f"We found {df.shape[0]} samples to hitpick", status="INFORMATION")
|
||||
# msg.exec()
|
||||
# if df.size == 0:
|
||||
# return
|
||||
# date = datetime.strftime(datetime.today(), "%Y-%m-%d")
|
||||
# # ask for filename and save as csv.
|
||||
# home_dir = Path(self.ctx.directory_path).joinpath(f"Hitpicks_{date}.csv").resolve().__str__()
|
||||
# fname = Path(QFileDialog.getSaveFileName(self, "Save File", home_dir, filter=".csv")[0])
|
||||
# if fname.__str__() == ".":
|
||||
# logger.debug("Saving csv was cancelled.")
|
||||
# return
|
||||
# df.to_csv(fname.__str__(), index=False)
|
||||
# # show plate maps
|
||||
# for image in images:
|
||||
# try:
|
||||
# image.show()
|
||||
# except Exception as e:
|
||||
# logger.error(f"Could not show image: {e}.")
|
||||
|
||||
def link_extractions(self):
|
||||
self.link_extractions_function()
|
||||
self.app.report.add_result(self.report)
|
||||
|
||||
118
src/submissions/frontend/widgets/submission_type_creator.py
Normal file
118
src/submissions/frontend/widgets/submission_type_creator.py
Normal file
@@ -0,0 +1,118 @@
|
||||
from PyQt6.QtCore import Qt
|
||||
from PyQt6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QScrollArea,
|
||||
QGridLayout, QPushButton, QLabel,
|
||||
QLineEdit, QComboBox, QDoubleSpinBox,
|
||||
QSpinBox, QDateEdit
|
||||
)
|
||||
from sqlalchemy import FLOAT, INTEGER
|
||||
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
||||
from backend.db import SubmissionType, Equipment, SubmissionTypeEquipmentAssociation, BasicSubmission
|
||||
from backend.validators import PydReagentType, PydKit
|
||||
import logging
|
||||
from pprint import pformat
|
||||
from tools import Report
|
||||
from typing import Tuple
|
||||
from .functions import select_open_file
|
||||
|
||||
|
||||
|
||||
logger = logging.getLogger(f"submissions.{__name__}")
|
||||
|
||||
class SubbmissionTypeAdder(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)
|
||||
# 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)
|
||||
# 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)
|
||||
# self.grid.addWidget(QLabel("Used For Submission Type:"),3,0)
|
||||
# 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
|
||||
# convert field name to human readable.
|
||||
# field_name = key
|
||||
# self.grid.addWidget(QLabel(field_name),idx,0)
|
||||
# print(self.columns[key].type)
|
||||
# match self.columns[key].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(key)
|
||||
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):
|
||||
info = self.parse_form()
|
||||
ST = SubmissionType(name=self.st_name.text(), info_map=info)
|
||||
with open(self.template_path, "rb") as f:
|
||||
ST.template_file = f.read()
|
||||
logger.debug(ST.__dict__)
|
||||
|
||||
def parse_form(self):
|
||||
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):
|
||||
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)
|
||||
grid.addWidget(QLabel(key.replace("_", " ").title()),0,0,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):
|
||||
return dict(
|
||||
sheets = self.sheet.text().split(","),
|
||||
row = self.row.value(),
|
||||
column = self.column.value()
|
||||
)
|
||||
@@ -62,12 +62,13 @@ class SubmissionFormContainer(QWidget):
|
||||
self.app.result_reporter()
|
||||
|
||||
def scrape_reagents(self, *args, **kwargs):
|
||||
print(f"\n\n{inspect.stack()[1].function}\n\n")
|
||||
self.scrape_reagents_function(args[0])
|
||||
caller = inspect.stack()[1].function.__repr__().replace("'", "")
|
||||
logger.debug(f"Args: {args}, kwargs: {kwargs}")
|
||||
self.scrape_reagents_function(args[0], caller=caller)
|
||||
self.kit_integrity_completion()
|
||||
self.app.report.add_result(self.report)
|
||||
self.report = Report()
|
||||
match inspect.stack()[1].function:
|
||||
match inspect.stack()[1].function.__repr__():
|
||||
case "import_submission_function":
|
||||
pass
|
||||
case _:
|
||||
@@ -83,7 +84,7 @@ class SubmissionFormContainer(QWidget):
|
||||
self.kit_integrity_completion_function()
|
||||
self.app.report.add_result(self.report)
|
||||
self.report = Report()
|
||||
match inspect.stack()[1].function:
|
||||
match inspect.stack()[1].function.__repr__():
|
||||
case "import_submission_function":
|
||||
pass
|
||||
case _:
|
||||
@@ -161,7 +162,7 @@ class SubmissionFormContainer(QWidget):
|
||||
logger.debug(f"Outgoing report: {self.report.results}")
|
||||
logger.debug(f"All attributes of submission container:\n{pformat(self.__dict__)}")
|
||||
|
||||
def scrape_reagents_function(self, extraction_kit:str):
|
||||
def scrape_reagents_function(self, extraction_kit:str, caller:str|None=None):
|
||||
"""
|
||||
Extracted scrape reagents function that will run when
|
||||
form 'extraction_kit' widget is updated.
|
||||
@@ -173,6 +174,9 @@ class SubmissionFormContainer(QWidget):
|
||||
Returns:
|
||||
Tuple[QMainWindow, dict]: Updated application and result
|
||||
"""
|
||||
self.form.reagents = []
|
||||
logger.debug(f"\n\n{caller}\n\n")
|
||||
# assert caller == "import_submission_function"
|
||||
report = Report()
|
||||
logger.debug(f"Extraction kit: {extraction_kit}")
|
||||
# obj.reagents = []
|
||||
@@ -195,7 +199,15 @@ class SubmissionFormContainer(QWidget):
|
||||
# obj.reagents.append(reagent)
|
||||
# else:
|
||||
# obj.missing_reagents.append(reagent)
|
||||
self.form.reagents = self.prsr.sub['reagents']
|
||||
match caller:
|
||||
case "import_submission_function":
|
||||
self.form.reagents = self.prsr.sub['reagents']
|
||||
case _:
|
||||
already_have = [reagent for reagent in self.prsr.sub['reagents'] if not reagent.missing]
|
||||
names = list(set([item.type for item in already_have]))
|
||||
logger.debug(f"reagents: {already_have}")
|
||||
reagents = [item.to_pydantic() for item in KitType.query(name=extraction_kit).get_reagents(submission_type=self.pyd.submission_type) if item.name not in names]
|
||||
self.form.reagents = already_have + reagents
|
||||
# logger.debug(f"Imported reagents: {obj.reagents}")
|
||||
# logger.debug(f"Missing reagents: {obj.missing_reagents}")
|
||||
self.report.add_result(report)
|
||||
@@ -221,6 +233,7 @@ class SubmissionFormContainer(QWidget):
|
||||
self.ext_kit = kit_widget.currentText()
|
||||
# for reagent in obj.pyd.reagents:
|
||||
for reagent in self.form.reagents:
|
||||
logger.debug(f"Creating widget for {reagent}")
|
||||
add_widget = ReagentFormWidget(parent=self, reagent=reagent, extraction_kit=self.ext_kit)
|
||||
# add_widget.setParent(sub_form_container.form)
|
||||
self.form.layout().addWidget(add_widget)
|
||||
|
||||
Reference in New Issue
Block a user