Merge pull request #1 from RolandWH/testing

Implement input validation with optional fields support
This commit is contained in:
Roland W-H 2025-02-17 10:08:28 +00:00 committed by GitHub
commit 6bfeaa6e0d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 331 additions and 131 deletions

View File

@ -6,20 +6,20 @@
<rect>
<x>0</x>
<y>0</y>
<width>480</width>
<height>305</height>
<width>498</width>
<height>303</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>480</width>
<height>305</height>
<width>498</width>
<height>303</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>480</width>
<height>305</height>
<width>498</width>
<height>303</height>
</size>
</property>
<property name="windowTitle">
@ -28,10 +28,10 @@
<widget class="QWidget" name="gridLayoutWidget">
<property name="geometry">
<rect>
<x>10</x>
<y>10</y>
<x>20</x>
<y>20</y>
<width>461</width>
<height>251</height>
<height>243</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
@ -53,99 +53,166 @@
<property name="verticalSpacing">
<number>5</number>
</property>
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="plat_name_lab">
<property name="text">
<string>Platform name</string>
<item row="0" column="3">
<widget class="QCheckBox" name="plat_name_check">
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="6" column="3">
<widget class="QCheckBox" name="share_deal_fee_check">
<property name="enabled">
<bool>false</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="5" column="2">
<widget class="FastEditQDoubleSpinBox" name="share_plat_max_fee_box">
<property name="enabled">
<bool>false</bool>
</property>
<property name="buttonSymbols">
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
</property>
<property name="correctionMode">
<enum>QAbstractSpinBox::CorrectionMode::CorrectToNearestValue</enum>
</property>
<property name="prefix">
<string>£</string>
</property>
<property name="maximum">
<double>999.000000000000000</double>
</property>
</widget>
</item>
<item row="2" column="3">
<widget class="QCheckBox" name="fund_deal_fee_check">
<property name="enabled">
<bool>false</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="6" column="2">
<widget class="FastEditQDoubleSpinBox" name="share_deal_fee_box">
<property name="buttonSymbols">
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
</property>
<property name="correctionMode">
<enum>QAbstractSpinBox::CorrectionMode::CorrectToNearestValue</enum>
</property>
<property name="prefix">
<string>£</string>
</property>
<property name="maximum">
<double>999.000000000000000</double>
</property>
</widget>
</item>
<item row="8" column="0" colspan="2">
<widget class="QLabel" name="share_deal_reduce_amount_lab">
<property name="text">
<string>Share dealing discount amount</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLineEdit" name="plat_name_box"/>
</item>
<item row="3" column="0" colspan="2">
<widget class="QLabel" name="share_plat_fee_lab">
<property name="text">
<string>Share platform fee*</string>
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<widget class="QLabel" name="share_deal_reduce_trades_lab">
<property name="text">
<string>Share dealing discount # of trades</string>
</property>
</widget>
</item>
<item row="9" column="3">
<widget class="QCheckBox" name="share_deal_reduce_amount_check"/>
</item>
<item row="5" column="0" colspan="2">
<widget class="QLabel" name="share_plat_max_fee_lab">
<property name="text">
<string>Share platform monthly fee cap</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="FastEditQDoubleSpinBox" name="fund_deal_fee_box">
<property name="buttonSymbols">
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
</property>
<property name="correctionMode">
<enum>QAbstractSpinBox::CorrectionMode::CorrectToNearestValue</enum>
</property>
<property name="prefix">
<string>£</string>
</property>
<property name="maximum">
<double>999.000000000000000</double>
</property>
</widget>
</item>
<item row="6" column="0" colspan="2">
<widget class="QLabel" name="share_deal_fee_lab">
<property name="text">
<string>Share dealing fee*</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<item row="8" column="2">
<widget class="FastEditQSpinBox" name="share_deal_reduce_trades_box">
<property name="enabled">
<bool>false</bool>
</property>
<property name="buttonSymbols">
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
</property>
<property name="correctionMode">
<enum>QAbstractSpinBox::CorrectionMode::CorrectToNearestValue</enum>
</property>
<property name="maximum">
<number>999</number>
</property>
</widget>
</item>
<item row="8" column="3">
<widget class="QCheckBox" name="share_deal_reduce_trades_check"/>
</item>
<item row="4" column="3">
<widget class="QCheckBox" name="share_plat_fee_check">
<property name="enabled">
<bool>false</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QLabel" name="fund_deal_fee_lab">
<property name="text">
<string>Fund dealing fee*</string>
</property>
</widget>
</item>
<item row="4" column="0" colspan="2">
<widget class="QLabel" name="share_plat_max_fee_lab">
<item row="5" column="3">
<widget class="QCheckBox" name="share_plat_max_fee_check"/>
</item>
<item row="0" column="2">
<widget class="QLineEdit" name="plat_name_box"/>
</item>
<item row="9" column="0" colspan="2">
<widget class="QLabel" name="share_deal_reduce_amount_lab">
<property name="text">
<string>Share platform fee cap/mth</string>
<string>Share dealing discount amount</string>
</property>
</widget>
</item>
<item row="8" column="2">
<widget class="QDoubleSpinBox" name="share_deal_reduce_amount_box">
<property name="buttonSymbols">
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
</property>
<property name="correctionMode">
<enum>QAbstractSpinBox::CorrectionMode::CorrectToNearestValue</enum>
</property>
<property name="prefix">
<string>£</string>
</property>
</widget>
</item>
<item row="5" column="2">
<widget class="QDoubleSpinBox" name="share_deal_fee_box">
<property name="buttonSymbols">
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
</property>
<property name="correctionMode">
<enum>QAbstractSpinBox::CorrectionMode::CorrectToNearestValue</enum>
</property>
<property name="prefix">
<string>£</string>
<item row="4" column="0" colspan="2">
<widget class="QLabel" name="share_plat_fee_lab">
<property name="text">
<string>Share platform fee*</string>
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QDoubleSpinBox" name="share_plat_max_fee_box">
<property name="buttonSymbols">
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
</property>
<property name="correctionMode">
<enum>QAbstractSpinBox::CorrectionMode::CorrectToNearestValue</enum>
</property>
<property name="prefix">
<string>£</string>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QDoubleSpinBox" name="share_plat_fee_box">
<widget class="FastEditQDoubleSpinBox" name="share_plat_fee_box">
<property name="buttonSymbols">
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
</property>
@ -155,10 +222,23 @@
<property name="suffix">
<string>%</string>
</property>
<property name="maximum">
<double>99.000000000000000</double>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QDoubleSpinBox" name="fund_deal_fee_box">
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="plat_name_lab">
<property name="text">
<string>Platform name</string>
</property>
</widget>
</item>
<item row="9" column="2">
<widget class="FastEditQDoubleSpinBox" name="share_deal_reduce_amount_box">
<property name="enabled">
<bool>false</bool>
</property>
<property name="buttonSymbols">
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
</property>
@ -168,15 +248,8 @@
<property name="prefix">
<string>£</string>
</property>
</widget>
</item>
<item row="7" column="2">
<widget class="QSpinBox" name="share_deal_reduce_trades_box">
<property name="buttonSymbols">
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
</property>
<property name="correctionMode">
<enum>QAbstractSpinBox::CorrectionMode::CorrectToNearestValue</enum>
<property name="maximum">
<double>999.000000000000000</double>
</property>
</widget>
</item>
@ -188,7 +261,7 @@
</property>
<property name="geometry">
<rect>
<x>330</x>
<x>350</x>
<y>270</y>
<width>141</width>
<height>24</height>
@ -198,11 +271,11 @@
<string>Save</string>
</property>
</widget>
<widget class="QLabel" name="label">
<widget class="QLabel" name="req_field_lab">
<property name="geometry">
<rect>
<x>20</x>
<y>270</y>
<x>10</x>
<y>280</y>
<width>191</width>
<height>21</height>
</rect>
@ -211,7 +284,35 @@
<string>* Indicates required field</string>
</property>
</widget>
<widget class="QLabel" name="active_lab">
<property name="geometry">
<rect>
<x>437</x>
<y>10</y>
<width>61</width>
<height>16</height>
</rect>
</property>
<property name="text">
<string>Active?</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</widget>
<customwidgets>
<customwidget>
<class>FastEditQDoubleSpinBox</class>
<extends>QDoubleSpinBox</extends>
<header>widgets/fastedit_spinbox</header>
</customwidget>
<customwidget>
<class>FastEditQSpinBox</class>
<extends>QSpinBox</extends>
<header>widgets/fastedit_spinbox</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>plat_name_box</tabstop>
<tabstop>fund_deal_fee_box</tabstop>
@ -221,6 +322,13 @@
<tabstop>share_deal_reduce_trades_box</tabstop>
<tabstop>share_deal_reduce_amount_box</tabstop>
<tabstop>save_but</tabstop>
<tabstop>plat_name_check</tabstop>
<tabstop>fund_deal_fee_check</tabstop>
<tabstop>share_plat_fee_check</tabstop>
<tabstop>share_plat_max_fee_check</tabstop>
<tabstop>share_deal_fee_check</tabstop>
<tabstop>share_deal_reduce_trades_check</tabstop>
<tabstop>share_deal_reduce_amount_check</tabstop>
</tabstops>
<resources/>
<connections>

View File

@ -14,6 +14,7 @@ class SIPPCompare(QMainWindow):
# Initialise class variables
# Inputs
self.optional_boxes = []
self.fund_plat_fee = 0.0
self.plat_name = ""
self.fund_deal_fee = 0.0
@ -39,7 +40,7 @@ class SIPPCompare(QMainWindow):
self.actionEdit_Platforms.triggered.connect(self.show_platform_edit)
# Update percentage mix label when slider moved
self.mix_slider.valueChanged.connect(self.update_slider_lab)
#self.value_input.valueChanged.connect(self.check_valid)
self.value_input.valueChanged.connect(self.check_valid)
self.share_trades_combo.currentTextChanged.connect(self.check_valid)
self.fund_trades_combo.currentTextChanged.connect(self.check_valid)
@ -55,24 +56,42 @@ class SIPPCompare(QMainWindow):
def check_valid(self):
if self.share_trades_combo.currentText() != "" \
and self.fund_trades_combo.currentText() != "":
and self.fund_trades_combo.currentText() != "" \
and self.value_input.value() != 0:
self.calc_but.setEnabled(True)
else:
self.calc_but.setEnabled(False)
# Get variables from platform editor input fields
def init_variables(self):
self.plat_name = self.platform_win.get_plat_name()
self.fund_plat_fee = self.platform_win.get_fund_plat_fee()
self.fund_deal_fee = self.platform_win.get_fund_deal_fee()
self.share_plat_fee = self.platform_win.get_share_plat_fee()
self.share_plat_max_fee = self.platform_win.get_share_plat_max_fee()
self.share_deal_fee = self.platform_win.get_share_deal_fee()
self.share_deal_reduce_trades = self.platform_win.get_share_deal_reduce_trades()
self.share_deal_reduce_amount = self.platform_win.get_share_deal_reduce_amount()
self.optional_boxes = self.platform_win.get_optional_boxes()
self.fund_plat_fee = self.platform_win.get_fund_plat_fee()
self.fund_deal_fee = self.platform_win.get_fund_deal_fee()
self.share_plat_fee = self.platform_win.get_share_plat_fee()
self.share_deal_fee = self.platform_win.get_share_deal_fee()
# TODO: This is HORRIBLE - find better way of doing it! (maybe enums?)
if self.optional_boxes[0]:
self.plat_name = self.platform_win.get_plat_name()
else:
self.plat_name = None
if self.optional_boxes[1]:
self.share_plat_max_fee = self.platform_win.get_share_plat_max_fee()
else:
self.share_plat_max_fee = None
if self.optional_boxes[2]:
self.share_deal_reduce_trades = self.platform_win.get_share_deal_reduce_trades()
else:
self.share_deal_reduce_trades = None
if self.optional_boxes[3]:
self.share_deal_reduce_amount = self.platform_win.get_share_deal_reduce_amount()
else:
self.share_deal_reduce_amount = None
# Calculate fees
# TODO: Error checking on combo boxes
def calculate_fees(self):
self.init_variables()
# Set to zero each time to avoid persistence
@ -98,13 +117,15 @@ class SIPPCompare(QMainWindow):
break
shares_value = (1 - (slider_val / 100)) * value_num
if (self.share_plat_fee * shares_value / 12) > self.share_plat_max_fee:
if self.share_plat_max_fee is not None and \
(self.share_plat_fee * shares_value / 12) > self.share_plat_max_fee:
self.share_plat_fees = self.share_plat_max_fee * 12
else:
self.share_plat_fees = self.share_plat_fee * shares_value
share_trades_num = int(self.share_trades_combo.currentText())
if (share_trades_num / 12) >= self.share_deal_reduce_trades:
if self.share_deal_reduce_trades is not None and \
(share_trades_num / 12) >= self.share_deal_reduce_trades:
self.share_deal_fees = self.share_deal_reduce_amount * share_trades_num
else:
self.share_deal_fees = self.share_deal_fee * share_trades_num

View File

@ -23,7 +23,12 @@ class OutputWindow(QWidget):
cur_time = datetime.datetime.now()
if not os.path.exists("output"):
os.makedirs("output")
filename_str = f"output/{self.platform_name}-{cur_time.year}.{cur_time.month}.{cur_time.day}.txt"
filename_str = f"output/"
if self.platform_name is not None:
filename_str += f"{self.platform_name}"
else:
filename_str += "Unnamed"
filename_str += f"-{cur_time.year}.{cur_time.month}.{cur_time.day}.txt"
output_file = open(filename_str, "wt", encoding = "utf-8")
output_file.write(self.results_str)
@ -31,7 +36,10 @@ class OutputWindow(QWidget):
def display_output(self, fund_plat_fees: float, fund_deal_fees: float,
share_plat_fees: float, share_deal_fees: float, plat_name: str):
self.platform_name = plat_name
self.results_str = f"Fees breakdown (Platform \"{self.platform_name}\"):"
if self.platform_name is not None:
self.results_str = f"Fees breakdown (Platform \"{self.platform_name}\"):"
else:
self.results_str = f"Fees breakdown:"
self.results_str += "\n\nPlatform fees:"
# :.2f is used in order to display 2 decimal places (currency form)

View File

@ -1,4 +1,4 @@
from PyQt6.QtCore import QRegularExpression, QEvent, QObject, QTimer
from PyQt6.QtCore import QRegularExpression
from PyQt6.QtGui import QRegularExpressionValidator
from PyQt6.QtWidgets import QWidget
from PyQt6 import uic
@ -13,6 +13,9 @@ class PlatformEdit(QWidget):
uic.loadUi("gui/platform_edit.ui", self)
# Initialise class variables
# Create main window object, passing this instance as param
self.main_win = main_window.SIPPCompare(self)
# TODO: Make fund_plat_fee user-defined
self.fund_plat_fee = [
[0, 250000, 1000000, 2000000],
@ -30,23 +33,49 @@ class PlatformEdit(QWidget):
if autofill:
self.save_but.setEnabled(True)
# Create main window object, passing this instance as param
self.main_win = main_window.SIPPCompare(self)
self.required_fields = [
self.fund_deal_fee_box,
self.share_plat_fee_box,
self.share_deal_fee_box
]
self.optional_fields = [
self.plat_name_box,
self.share_plat_max_fee_box,
self.share_deal_reduce_trades_box,
self.share_deal_reduce_amount_box
]
self.optional_check_boxes = [
self.plat_name_check,
self.share_plat_max_fee_check,
self.share_deal_reduce_trades_check,
self.share_deal_reduce_amount_check
]
self.check_boxes_ticked = [
True,
False,
False,
False
]
# Handle events
for field in self.required_fields:
field.valueChanged.connect(self.check_valid)
for field in self.optional_fields:
field_type = field.staticMetaObject.className()
if field_type == "QLineEdit":
field.textChanged.connect(self.check_valid)
elif field_type == "FastEditQDoubleSpinBox" or field_type == "FastEditQSpinBox":
field.valueChanged.connect(self.check_valid)
for check_box in self.optional_check_boxes:
check_box.checkStateChanged.connect(self.check_valid)
# NOTE: Signal defined in UI file to close window when save button clicked
self.save_but.clicked.connect(self.init_variables)
self.fund_deal_fee_box.valueChanged.connect(self.check_valid)
self.share_plat_fee_box.valueChanged.connect(self.check_valid)
self.share_deal_fee_box.valueChanged.connect(self.check_valid)
# Install event filter on input boxes in order to select all text on focus
self.fund_deal_fee_box.installEventFilter(self)
self.share_plat_fee_box.installEventFilter(self)
self.share_plat_max_fee_box.installEventFilter(self)
self.share_deal_fee_box.installEventFilter(self)
self.share_deal_reduce_trades_box.installEventFilter(self)
self.share_deal_reduce_amount_box.installEventFilter(self)
# Set validators
# Regex accepts any characters that match [a-Z], [0-9] or _
@ -77,33 +106,54 @@ class PlatformEdit(QWidget):
# Once user input is received show main window
self.main_win.show()
# When focus is given to an input box, select all text in it (easier to edit)
def eventFilter(self, obj: QObject, event: QEvent):
if event.type() == QEvent.Type.FocusIn:
# Alternative condition for % suffix - currently unused
#if obj.value() == 0 or obj == self.share_plat_fee_box:
QTimer.singleShot(0, obj.selectAll)
return False
# Check if all required fields have valid (non-zero) input
# TODO: Find a better way of doing this if possible
# This method does multiple things in order to validate the user's inputs:
# 1) Check all required fields have a non-zero value
# 2) If an optional checkbox is toggled: toggle editing of the corresponding field
# 3) Check all optional fields the user has picked have a non-zero value
# 4) If the above two conditions are met (1 & 3), make the 'Save' button clickable
# 5) Keep a record of which optional fields the user has chosen to fill in
# It's called when an optional check box emits a checkStateChanged() signal
# It's also called when any field emits a textChanged() or valueChanged() signal
def check_valid(self):
values = [self.fund_deal_fee_box.value(),
self.share_plat_fee_box.value(),
self.share_deal_fee_box.value()
]
valid = True
for value in values:
if value == 0:
# Check all required fields have a non-zero value
for field in self.required_fields:
if field.value() == 0:
valid = False
for i in range(len(self.optional_check_boxes)):
# Find the coordinates of the input box corresponding to the checkbox
# It will be on the same row, in the column to the left (-1)
check_box_idx = self.gridLayout.indexOf(self.optional_check_boxes[i])
check_box_pos = self.gridLayout.getItemPosition(check_box_idx)
input_box_pos = list(check_box_pos)[:2]
input_box_pos[1] -= 1
# Return copy of input field widget from its coordinates
input_box_item = self.gridLayout.itemAtPosition(input_box_pos[0], input_box_pos[1]).widget()
if self.optional_check_boxes[i].isChecked():
input_box_item.setEnabled(True)
self.check_boxes_ticked[i] = True
input_box_type = input_box_item.staticMetaObject.className()
if input_box_type == "QLineEdit":
if input_box_item.text() == "":
valid = False
elif input_box_type == "FastEditQDoubleSpinBox" or input_box_type == "FastEditQSpinBox":
if input_box_item.value() == 0:
valid = False
else:
input_box_item.setEnabled(False)
self.check_boxes_ticked[i] = False
if valid:
self.save_but.setEnabled(True)
else:
self.save_but.setEnabled(False)
# Getter functions (is this necessary? maybe directly reading class vars would be best...)
def get_optional_boxes(self):
return self.check_boxes_ticked
def get_plat_name(self):
return self.plat_name

View File

@ -0,0 +1,13 @@
from PyQt6.QtCore import QTimer
from PyQt6.QtWidgets import QSpinBox, QDoubleSpinBox
class FastEditQDoubleSpinBox(QDoubleSpinBox):
def focusInEvent(self, e):
QTimer.singleShot(0, self.selectAll)
super(FastEditQDoubleSpinBox, self).focusInEvent(e)
class FastEditQSpinBox(QSpinBox):
def focusInEvent(self, e):
QTimer.singleShot(0, self.selectAll)
super(FastEditQSpinBox, self).focusInEvent(e)