Skip to content

Instantly share code, notes, and snippets.

@rfletchr
Created February 24, 2023 11:17
Show Gist options
  • Save rfletchr/a178f6545ce59c267fdef2c19cc0ebba to your computer and use it in GitHub Desktop.
Save rfletchr/a178f6545ce59c267fdef2c19cc0ebba to your computer and use it in GitHub Desktop.
Simple Validation Framework
import enum
import time
import textwrap
import qtawesome
from PySide2 import QtCore, QtWidgets, QtGui
__ICONS = {}
def get_icons():
global __ICONS
if not __ICONS:
__ICONS = {
ValidationStatus.pending: qtawesome.icon("fa5s.question-circle", color="grey"),
ValidationStatus.working: qtawesome.icon("fa5s.play-circle", color="black"),
ValidationStatus.success: qtawesome.icon("fa5s.check-circle", color="green"),
ValidationStatus.error: qtawesome.icon("fa5s.exclamation-circle", color="red"),
ValidationStatus.failed: qtawesome.icon("fa5s.times-circle", color="red")
}
return __ICONS
class ValidationStatus(enum.Enum):
pending = 1
working = 2
success = 3
error = 4
failed = 5
class BaseItem(QtGui.QStandardItem):
progressRole = QtCore.Qt.UserRole + 1
statusRole = QtCore.Qt.UserRole + 2
def __init__(self, label):
super().__init__(label)
self.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable)
self._icons = get_icons()
def setProgress(self, progress):
self.setData(progress, self.progressRole)
def setStatus(self, status: ValidationStatus):
self.setIcon(self._icons[status])
self.setData(status, self.statusRole)
def execute(self, data):
raise NotImplementedError
def data(self, role=QtCore.Qt.DisplayRole):
status = super().data(self.statusRole)
if role == QtCore.Qt.BackgroundRole:
if status == ValidationStatus.pending:
return super().data(role)
elif status == ValidationStatus.working:
return QtGui.QBrush(QtGui.QColor(255, 255, 0, 10))
elif status == ValidationStatus.success:
return QtGui.QBrush(QtGui.QColor(0, 255, 0, 10))
elif status == ValidationStatus.error:
return QtGui.QBrush(QtGui.QColor(255, 0, 0, 10))
elif status == ValidationStatus.failed:
return QtGui.QBrush(QtGui.QColor(255, 0, 0, 10))
elif role == QtCore.Qt.DecorationRole:
if status == ValidationStatus.pending:
return self._icons[ValidationStatus.pending]
elif status == ValidationStatus.working:
return self._icons[ValidationStatus.working]
elif status == ValidationStatus.success:
return self._icons[ValidationStatus.success]
elif status == ValidationStatus.error:
return self._icons[ValidationStatus.error]
elif status == ValidationStatus.failed:
return self._icons[ValidationStatus.failed]
return super().data(role)
class CategoryItem(BaseItem):
def execute(self, data):
self.setProgress(0)
self.setStatus(ValidationStatus.working)
results = []
for row in range(self.rowCount()):
item = self.child(row)
results.append(item.execute(data))
self.setProgress((row + 1) / self.rowCount() * 100)
if all(results):
self.setStatus(ValidationStatus.success)
return True
else:
self.setStatus(ValidationStatus.failed)
return False
class ValidationItemBase(BaseItem):
label = "Unset"
description = "Unset"
category = "Unset"
def __init__(self):
super().__init__(self.label)
self.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable)
def setProgress(self, progress):
self.setData(progress, self.progressRole)
def setStatus(self, status: ValidationStatus):
self.setIcon(self._icons[status])
self.setData(status, self.statusRole)
def execute(self, data):
self.setStatus(ValidationStatus.working)
try:
result = self.validate(data)
if result:
self.setStatus(ValidationStatus.success)
return True
else:
self.setStatus(ValidationStatus.failed)
return False
except Exception as e:
self.setStatus(ValidationStatus.error)
raise e
def validate(self, data):
raise NotImplementedError
class ValidationModel(QtGui.QStandardItemModel):
progress = QtCore.Signal(str, int, int)
def __init__(self, parent=None):
super().__init__(parent)
self._root_nodes = {}
self._categories = {}
self._icons = get_icons()
def addValidationItem(self, item: ValidationItemBase):
if item.category not in self._root_nodes:
ns_item = self._root_nodes[item.category] = CategoryItem(item.category)
self.invisibleRootItem().appendRow(ns_item)
self._categories.setdefault(item.category, []).append(item)
self._root_nodes[item.category].appendRow(item)
def execute(self, data):
result = []
for index, category in enumerate(self._root_nodes.values()):
self.progress.emit(f"Validating: {category.text()}", index, len(self._root_nodes))
result.append(category.execute(data))
if all(result):
self.progress.emit("Success 😊", 1, 1)
return True
else:
self.progress.emit("Failed 😢", 1, 1)
return False
def clear(self) -> None:
self._root_nodes = {}
self._categories = {}
super().clear()
class ValidationController(QtCore.QObject):
def __init__(self, parent=None):
super().__init__(parent)
self._model = ValidationModel()
self._view = ValidationView()
self._view.setModel(self._model)
self._model.progress.connect(self._view.setProgress)
self._view.itemClicked.connect(self.onItemClicked)
self._view.validateClicked.connect(self.execute)
def populate_model(self):
raise NotImplementedError
def collect_data(self):
raise NotImplementedError
def execute(self):
QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor)
self._model.clear()
self.populate_model()
QtWidgets.QApplication.processEvents()
try:
data = self.collect_data()
self._model.execute(data)
except Exception as e:
QtWidgets.QMessageBox.critical(self._view, "Error", str(e))
finally:
QtWidgets.QApplication.restoreOverrideCursor()
def onItemClicked(self, index):
item = self._model.itemFromIndex(index)
if isinstance(item, ValidationItemBase):
self._view.setDescription(item.description)
else:
self._view.setDescription("")
def show(self, parent=None):
# center the widget in the screen
self._view.setParent(parent)
self._view.show()
class ValidationDelegate(QtWidgets.QStyledItemDelegate):
def paint(self, painter, option, index):
# select the background color based on the status, respecting the selection state
if option.state & QtWidgets.QStyle.State_Selected:
brush = option.palette.highlight()
else:
brush = index.data(QtCore.Qt.BackgroundRole) or option.palette.base()
painter.setPen(QtCore.Qt.NoPen)
painter.setBrush(brush)
painter.drawRect(option.rect)
painter.setPen(option.palette.text().color())
# render the icon on the left hand side
icon = index.data(QtCore.Qt.DecorationRole)
# create rect with width and height of 0.9 * the height of the rect
icon_rect = QtCore.QRect(option.rect)
icon_rect.setWidth(icon_rect.height() * 0.6)
icon_rect.setHeight(icon_rect.height() * 0.6)
# move the rect to the right of the option rect
icon_rect.moveCenter(option.rect.center())
icon_rect.moveLeft(option.rect.left())
progress_rect = QtCore.QRect(icon_rect)
progress_rect.moveRight(option.rect.right() - 2)
if index.data(ValidationItemBase.statusRole) == ValidationStatus.working:
progress = index.data(ValidationItemBase.progressRole)
progress_bar = QtWidgets.QStyleOptionProgressBar()
progress_bar.rect = progress_rect
progress_bar.minimum = 0
progress_bar.maximum = 100 if progress is not None else 0
progress_bar.progress = progress if progress is not None else 0
progress_bar.textVisible = False
QtWidgets.QApplication.style().drawControl(QtWidgets.QStyle.CE_ProgressBar, progress_bar, painter)
if icon is not None:
icon.paint(painter, icon_rect)
# create a left aligned rect for the text with the width of the rect minus the width of the icon rect
# with a margin of 5 pixels
text_rect = QtCore.QRect(option.rect)
text_rect.setLeft(icon_rect.right() + 5)
text_rect.setRight(progress_rect.left() - 2)
# render the text left aligned
painter.drawText(text_rect, QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter, index.data(QtCore.Qt.DisplayRole))
def sizeHint(self, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> QtCore.QSize:
base = super().sizeHint(option, index)
base.setHeight(32)
return base
class ValidationTree(QtWidgets.QTreeView):
def __init__(self, parent=None):
super().__init__(parent)
self.setAlternatingRowColors(True)
self.setItemDelegate(ValidationDelegate(self))
self.header().hide()
def setModel(self, model: ValidationModel):
super().setModel(model)
model.dataChanged.connect(self.onDataChanged)
def onDataChanged(self, *args, **kwargs):
QtWidgets.QApplication.processEvents()
self.expandAll()
class ValidationView(QtWidgets.QWidget):
itemClicked = QtCore.Signal(QtCore.QModelIndex)
validateClicked = QtCore.Signal()
acceptClicked = QtCore.Signal()
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setStyleSheet("QWidget { font-family: 'Noto', 'Noto Emoji'; font-size: 20px }")
self._validation_tree = ValidationTree()
self._description_view = QtWidgets.QTextEdit()
self._description_view.setReadOnly(True)
self._progress_bar = QtWidgets.QProgressBar()
self._validate_button = QtWidgets.QPushButton("Validate")
self._accept_button = QtWidgets.QPushButton("Accept")
inner_layout = QtWidgets.QHBoxLayout()
inner_layout.addWidget(self._validation_tree, 3)
inner_layout.addWidget(self._description_view, 7)
button_layout = QtWidgets.QHBoxLayout()
button_layout.addStretch(100)
button_layout.addWidget(self._validate_button)
button_layout.addWidget(self._accept_button)
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.addLayout(inner_layout)
main_layout.addWidget(self._progress_bar)
main_layout.addLayout(button_layout)
self._validate_button.clicked.connect(self.onValidateClicked)
self._accept_button.clicked.connect(self.onAcceptClicked)
self._validation_tree.clicked.connect(self.itemClicked)
def onValidateClicked(self):
self.validateClicked.emit()
def onAcceptClicked(self):
self.acceptClicked.emit()
def setDescription(self, html: str):
self._description_view.setHtml(html)
def setModel(self, model: ValidationModel):
self._validation_tree.setModel(model)
def setProgress(self, message, progress, total):
self._progress_bar.setFormat(message)
self._progress_bar.setMaximum(total)
self._progress_bar.setValue(progress)
def sizeHint(self) -> QtCore.QSize:
# calculate the size of the hint based on the width and height of the screen
screen = QtWidgets.QApplication.primaryScreen()
screen_size = screen.size()
return QtCore.QSize(screen_size.width() * 0.5, screen_size.height() * 0.9)
class ValidateSomething(ValidationItemBase):
label = "Validate Something"
category = "Burps"
def validate(self, data):
for i in range(100):
time.sleep(0.01)
self.setProgress(i)
return True
class ValidateSomethingElse(ValidationItemBase):
label = "Validate Something Else"
category = "Farts"
description = textwrap.dedent("""
<h1>Validate something else</h2>
""")
def validate(self, data):
for i in range(100):
time.sleep(0.01)
self.setProgress(i)
return False
class SomethingValidator(ValidationController):
def populate_model(self):
self._model.addValidationItem(ValidateSomething())
self._model.addValidationItem(ValidateSomething())
self._model.addValidationItem(ValidateSomethingElse())
def collect_data(self):
return None
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
validator = SomethingValidator()
validator.show()
validator.execute()
sys.exit(app.exec_())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment