diff --git a/soitool/codebook_row_adder.py b/soitool/codebook_row_adder.py index 2372001b36cba575590b9c77fb94c61771d51479..091bf6f7e31901764ae8bbfb52738fdd65ba0014 100644 --- a/soitool/codebook_row_adder.py +++ b/soitool/codebook_row_adder.py @@ -10,7 +10,6 @@ from PySide2.QtWidgets import ( QWidget, ) from PySide2.QtCore import Qt -from soitool.database import Database from soitool.codebook_model_view import CodeBookTableModel, CodeBookTableView @@ -29,6 +28,8 @@ class CodebookRowAdder(QWidget): database. Therefore, this reference is needed to be able to insert new rows, but it is also used to refresh the view when the row is inserted. + database : soitool.database.Database + Database to use. Raises ------ @@ -37,7 +38,7 @@ class CodebookRowAdder(QWidget): None (default) or CodeBookTableView. """ - def __init__(self, codebook_view=None): + def __init__(self, database, codebook_view=None): super().__init__() # Raise error if argument is invalid @@ -49,6 +50,7 @@ class CodebookRowAdder(QWidget): + "'{}'".format(codebook_view) ) + self.database = database self.codebook_view = codebook_view self.create_widgets() @@ -169,8 +171,6 @@ class CodebookRowAdder(QWidget): if len(category_input) > 0: category_input = category_input[0].upper() + category_input[1:] - db = Database() - try: # If a view is used, remove its model temporarily if self.codebook_view is not None: @@ -181,13 +181,13 @@ class CodebookRowAdder(QWidget): "INSERT INTO CodeBook(Word, Category, Type)" + "VALUES(?, ?, ?)" ) - db.conn.execute( + self.database.conn.execute( stmt, (word_input, category_input, type_input,) ) # Add unique code to row and commit changes - db.add_code_to(word_input) - db.conn.commit() + self.database.add_code_to(word_input) + self.database.conn.commit() # Give feedback and reset input self.label_feedback.setText("Ord/Uttrykk lagt til.") diff --git a/soitool/codebook_widget.py b/soitool/codebook_widget.py index 9f418daad940e50385f76b342dd6f3f021c13647..53c45acc884ee1237e87ddafcad31fbfb6864e72 100644 --- a/soitool/codebook_widget.py +++ b/soitool/codebook_widget.py @@ -7,12 +7,12 @@ from soitool.codebook_row_adder import CodebookRowAdder class CodebookWidget(QWidget): """Widget for viewing and editing codebook.""" - def __init__(self): + def __init__(self, database): super().__init__() # Create widgets - self.view = CodeBookTableView() - self.row_adder = CodebookRowAdder() + self.view = CodeBookTableView(database) + self.row_adder = CodebookRowAdder(database, self.view) self.create_and_set_layouts() diff --git a/soitool/inline_editable_soi_view.py b/soitool/inline_editable_soi_view.py index e56d498a095c42df4ccf139e0225b010747fdb97..621d06e8608a6e7ed93ec38e2d0f876e9bba3602 100644 --- a/soitool/inline_editable_soi_view.py +++ b/soitool/inline_editable_soi_view.py @@ -78,6 +78,7 @@ class InlineEditableSOIView(QScrollArea): # add listeners to react properly to SOI changes self.soi.add_reorganization_listener(self.update_pages) self.soi.add_new_module_listener(self.ensure_proxies) + self.soi.add_update_property_listener(self.update_pages) # self.launch_auto_zoom() diff --git a/soitool/main_window.py b/soitool/main_window.py index ad0c15311f257f49e65c40b9179bbe05ff02132d..4cd761f9badc0f5a0e76a6449a85dbfc044cc163 100644 --- a/soitool/main_window.py +++ b/soitool/main_window.py @@ -197,7 +197,7 @@ class MainWindow(QMainWindow): break # Codebook-tab does not exist, create, add and select tab else: - tab = CodebookWidget() + tab = CodebookWidget(self.database) self.tabs.addTab(tab, "Kodebok") self.tabs.setCurrentWidget(tab) diff --git a/soitool/media/freetextmodule.PNG b/soitool/media/freetextmodule.PNG new file mode 100644 index 0000000000000000000000000000000000000000..55cfe4d305ae89e278c726093e8042001f61dd69 Binary files /dev/null and b/soitool/media/freetextmodule.PNG differ diff --git a/soitool/modules/module_freetext.py b/soitool/modules/module_freetext.py new file mode 100644 index 0000000000000000000000000000000000000000..4c3ab4fecc34754f61558675ee35550c90f79888 --- /dev/null +++ b/soitool/modules/module_freetext.py @@ -0,0 +1,199 @@ +"""SOI module for arbitrary text. + +Sources: +* https://stackoverflow.com/questions/47710329/how-to-adjust-qtextedit-to-fit-its-contents +* https://stackoverflow.com/questions/3050537/resizing-qts-qtextedit-to-match-text-height-maximumviewportsize +""" +from PySide2.QtWidgets import ( + QWidget, + QVBoxLayout, + QLineEdit, + QTextEdit, + QSizePolicy, +) +from PySide2.QtCore import QSize, Qt +from PySide2.QtGui import QIcon, QFontMetricsF +from soitool.modules.module_base import ModuleBase, HEADLINE_FONT + + +class TextEditWithSizeOfContent(QTextEdit): + """Custom QTextEdit subclass that grows to fit it's text.""" + + def __init__(self, *arg, **kwargs): + super(TextEditWithSizeOfContent, self).__init__(*arg, **kwargs) + + self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setLineWrapMode(QTextEdit.NoWrap) + + def sizeHint(self): + """Give size hint using width of text.""" + size = QSize(self.document().size().toSize()) + # NOTE that the following is not respected for dimensions below 100,100 + size.setWidth(max(100, size.width())) + size.setHeight(max(100, size.height())) + return size + + def resizeEvent(self, event): + """Update geometry before handling the resizeEvent. + + See sources in module docstring. + + Parameters + ---------- + event : QResizeEvent + event sent by Qt + """ + self.updateGeometry() + super(TextEditWithSizeOfContent, self).resizeEvent(event) + + +class LineEditWithSizeOfContent(QLineEdit): + """Custom QLineEdit subclass that grows to fit it's text.""" + + def __init__(self, *arg, **kwargs): + super(LineEditWithSizeOfContent, self).__init__(*arg, **kwargs) + self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) + + def sizeHint(self): + """Give size hint using width of text.""" + size_hint_parent = super(LineEditWithSizeOfContent, self).sizeHint() + content_width = QFontMetricsF(self.fontMetrics()).horizontalAdvance( + self.text() + ) + # Hardcoded addition because font metrics is slightly off... + # Likely because of the font weight + content_width += 10 + return QSize(content_width, size_hint_parent.height()) + + def resizeEvent(self, event): + """Update geometry before handling the resizeEvent. + + See sources in module docstring. + + Parameters + ---------- + event : QResizeEvent + event sent by Qt + """ + self.updateGeometry() + super(LineEditWithSizeOfContent, self).resizeEvent(event) + + +class Meta(type(ModuleBase), type(QWidget)): + """Used as a metaclass to enable multiple inheritance.""" + + +class FreeTextModule(ModuleBase, QWidget, metaclass=Meta): + """Module for arbitrary text. + + ## Note about widget size + + This widget might be too small to be shown in a window by itself. If you + need to show only this widget, you should show it by wrapping it in a + QWidget. + + Parameters + ---------- + size + Not used. + data : list + First index should be header, second index should be body + """ + + # Ignoring Pylint's "Unused argument 'size'" error because it is + # intentionally left unused + # pylint: disable=W0613 + def __init__(self, size=None, data=None): + self.type = "FreeTextModule" + QWidget.__init__(self) + ModuleBase.__init__(self) + + self.line_edit_header = LineEditWithSizeOfContent() + self.line_edit_header.setFont(HEADLINE_FONT) + self.text_edit_body = TextEditWithSizeOfContent() + + # When the contents of these widgets change we need to manually trigger + # adjust of size, even on self. Without adjust of size on self the + # widget will leave behind gray color when it is shrunk in the + # QGraphicsScene + self.line_edit_header.textChanged.connect( + lambda: (self.line_edit_header.adjustSize(), self.adjustSize()) + ) + self.text_edit_body.textChanged.connect( + lambda: (self.text_edit_body.adjustSize(), self.adjustSize()) + ) + + self.layout = QVBoxLayout() + self.layout.setAlignment(Qt.AlignLeft | Qt.AlignTop) + self.layout.setSpacing(0) + self.layout.setMargin(0) + self.layout.addWidget(self.line_edit_header) + self.layout.addWidget(self.text_edit_body) + self.setLayout(self.layout) + + if data is not None: + header, body = data[0], data[1] + self.line_edit_header.setText(header) + self.text_edit_body.setText(body) + + def resizeEvent(self, event): + """Update geometry before handling the resizeEvent. + + See sources in module docstring. + + Parameters + ---------- + event : QResizeEvent + event sent by Qt + """ + self.updateGeometry() + super(FreeTextModule, self).resizeEvent(event) + + def get_size(self): + """Get size of widget. + + Returns + ------- + Tuple + (width, height) + """ + size = self.sizeHint() + return (size.width(), size.height()) + + def set_pos(self, pos): + """Set position of widget. + + Parameters + ---------- + pos : QPoint + Position (x, y). + """ + self.move(pos) + + def render_onto_pdf(self): + """Render onto pdf.""" + + def get_data(self): + """Return list containing module data. + + Returns + ------- + list of module content + first index contains header, second index contains body + """ + return [ + self.line_edit_header.text(), + self.text_edit_body.toPlainText(), + ] + + @staticmethod + def get_user_friendly_name(): + """Get user-friendly name of module.""" + return "Fritekst" + + @staticmethod + def get_icon(): + """Get icon of module.""" + return QIcon("soitool/media/freetextmodule.png") diff --git a/soitool/new_module_dialog.py b/soitool/new_module_dialog.py index 2fd3501b3b27c606dac34482280c70d95d975138..acc3746ca017a7d8869694a71d58ee7788843338 100644 --- a/soitool/new_module_dialog.py +++ b/soitool/new_module_dialog.py @@ -16,6 +16,7 @@ from soitool.modules.module_table import TableModule from soitool.modules.module_authentication_board import ( AuthenticationBoardModule, ) +from soitool.modules.module_freetext import FreeTextModule # Constant holding all modules the user can choose from. This is intended as a @@ -24,6 +25,7 @@ from soitool.modules.module_authentication_board import ( MODULE_CHOICES = [ TableModule, AuthenticationBoardModule, + FreeTextModule, ] diff --git a/soitool/serialize_export_import_soi.py b/soitool/serialize_export_import_soi.py index f61b8f6b3dfce534446eccfd4520982499eca08e..d7856c1e2f6bd48e840f3486f093918864075abd 100644 --- a/soitool/serialize_export_import_soi.py +++ b/soitool/serialize_export_import_soi.py @@ -8,6 +8,7 @@ from soitool.modules.module_table import TableModule from soitool.modules.module_authentication_board import ( AuthenticationBoardModule, ) +from soitool.modules.module_freetext import FreeTextModule # Valid schema for serialized SOI SERIALIZED_SOI_SCHEMA = Schema( @@ -230,6 +231,12 @@ def import_soi(file_path): "meta": module["meta"], } ) + elif module_type == "FreeTextModule": + size = module["size"] + data = module["data"] + modules.append( + {"widget": FreeTextModule(size, data), "meta": module["meta"]} + ) else: raise TypeError( "Module-type '{}' is not recognized.".format(module_type) diff --git a/soitool/setup_settings.py b/soitool/setup_settings.py index 9e19b468cea539fc23027de3799ee1a50fa6b646..1b8b1ff0d65194e0026f1b6c24de963095dbce34 100644 --- a/soitool/setup_settings.py +++ b/soitool/setup_settings.py @@ -16,6 +16,11 @@ from PySide2.QtWidgets import ( QComboBox, QGroupBox, ) +from soitool.soi import ( + STRING_TO_ALGORITHM_RECTPACK_BIN, + STRING_TO_ALGORITHM_RECTPACK_PACK, + STRING_TO_ALGORITHM_INITIAL_SORT, +) class Setup(QDialog): # pylint: disable = R0902 @@ -120,28 +125,25 @@ class Setup(QDialog): # pylint: disable = R0902 # placement algorithm self.layout_setup.addWidget(self.label_algorithm) - self.rbutton_bin1 = QRadioButton("BFF") - self.rbutton_bin2 = QRadioButton("BBF") - self.rbutton_pack1 = QRadioButton("MaxRectsBI") - self.rbutton_pack2 = QRadioButton("SkylinBl") - self.rbutton_pack3 = QRadioButton("GuillotineBssfSas") - self.rbutton_sort1 = QRadioButton("none") - self.rbutton_sort2 = QRadioButton("area") - self.rbutton_sort3 = QRadioButton("width") - self.rbutton_sort4 = QRadioButton("height") + self.rbuttons_option_bin = [ + QRadioButton(name) for name in STRING_TO_ALGORITHM_RECTPACK_BIN + ] + self.rbuttons_option_pack = [ + QRadioButton(name) for name in STRING_TO_ALGORITHM_RECTPACK_PACK + ] + self.rbuttons_option_sort = [ + QRadioButton(name) for name in STRING_TO_ALGORITHM_INITIAL_SORT + ] self.layout_alg_button_bin.addWidget(QLabel("Bin")) - self.layout_alg_button_bin.addWidget(self.rbutton_bin1) - self.layout_alg_button_bin.addWidget(self.rbutton_bin2) + for rbutton in self.rbuttons_option_bin: + self.layout_alg_button_bin.addWidget(rbutton) self.layout_alg_button_pack.addWidget(QLabel("Pack")) - self.layout_alg_button_pack.addWidget(self.rbutton_pack1) - self.layout_alg_button_pack.addWidget(self.rbutton_pack2) - self.layout_alg_button_pack.addWidget(self.rbutton_pack3) + for rbutton in self.rbuttons_option_pack: + self.layout_alg_button_pack.addWidget(rbutton) self.layout_alg_button_sort.addWidget(QLabel("Sort")) - self.layout_alg_button_sort.addWidget(self.rbutton_sort1) - self.layout_alg_button_sort.addWidget(self.rbutton_sort2) - self.layout_alg_button_sort.addWidget(self.rbutton_sort3) - self.layout_alg_button_sort.addWidget(self.rbutton_sort4) + for rbutton in self.rbuttons_option_sort: + self.layout_alg_button_sort.addWidget(rbutton) self.group_algorithm_bin.setLayout(self.layout_alg_button_bin) self.group_algorithm_pack.setLayout(self.layout_alg_button_pack) @@ -169,43 +171,48 @@ class Setup(QDialog): # pylint: disable = R0902 def save(self): """Save and update the SOI with the given changes.""" - self.soi.title = self.edit_title.text() - self.soi.description = self.edit_description.text() - self.soi.version = self.edit_version.text() - self.soi.date = self.edit_date.text() - self.soi.valid_from = self.edit_valid_from.text() - self.soi.valid_to = self.edit_valid_to.text() - self.soi.classification = ( - self.edit_classification.currentText().lower() - ) - - # find which radiobutton that is checked + # Dict will contain all changes to make + property_changes = {} + property_changes["title"] = self.edit_title.text() + property_changes["description"] = self.edit_description.text() + property_changes["version"] = self.edit_version.text() + property_changes["date"] = self.edit_date.text() + property_changes["valid_from"] = self.edit_valid_from.text() + property_changes["valid_to"] = self.edit_valid_to.text() + property_changes[ + "classification" + ] = self.edit_classification.currentText() + + # Find which radiobutton that is checked if self.rbutton_landscape.isChecked(): - self.soi.orientation = "landscape" + property_changes["orientation"] = "landscape" else: - self.soi.orientation = "portrait" + property_changes["orientation"] = "portrait" if self.rbutton_auto.isChecked(): - self.soi.placement_strategy = "auto" + property_changes["placement_strategy"] = "auto" else: - self.soi.placement_strategy = "manual" + property_changes["placement_strategy"] = "manual" - # loop through groupbox to find checked radiobuttons + # Loop through groupbox to find checked radiobuttons for i in self.group_algorithm_bin.findChildren(QRadioButton): if i.isChecked(): - self.soi.algorithm_bin = i.text() + property_changes["algorithm_bin"] = i.text() break for i in self.group_algorithm_pack.findChildren(QRadioButton): if i.isChecked(): - self.soi.algorithm_pack = i.text() + property_changes["algorithm_pack"] = i.text() break for i in self.group_algorithm_sort.findChildren(QRadioButton): if i.isChecked(): - self.soi.algorithm_sort = i.text() + property_changes["algorithm_sort"] = i.text() break + # Pass changes as unpacked variable list + self.soi.update_properties(**property_changes) + self.accept() def get_from_soi(self): @@ -216,11 +223,9 @@ class Setup(QDialog): # pylint: disable = R0902 self.edit_date.setText(self.soi.date) self.edit_valid_from.setText(self.soi.valid_from) self.edit_valid_to.setText(self.soi.valid_to) - self.edit_classification.setCurrentText( - self.soi.classification.upper() - ) + self.edit_classification.setCurrentText(self.soi.classification) - # check radiobuttons according to current SOI settings + # Check radiobuttons according to current SOI settings if self.soi.orientation == "landscape": self.rbutton_landscape.setChecked(True) else: diff --git a/soitool/soi.py b/soitool/soi.py index d9aa63fb664b0cfd97bd3b8d73664b6726cd4fad..8ac55dc93b54393b1c28860e9d2244e2af8a3308 100644 --- a/soitool/soi.py +++ b/soitool/soi.py @@ -189,6 +189,12 @@ class SOI: updating the widget positions based on the "meta" positions, using the formulas above. + ## Note about properties + + To ensure other users of SOI are properly notified, all property updates + should happen through member functions. `update_properties` for general + properties, and `add_module` for modules and attachments. + Parameters ---------- title : string @@ -308,21 +314,22 @@ class SOI: self.modules = modules self.attachments = attachments - # the following properties are relevant when self.placement_strategy + # The following properties are relevant when self.placement_strategy # is "auto". They are used by rectpack self.algorithm_bin = algorithm_bin self.algorithm_pack = algorithm_pack self.algorithm_sort = algorithm_sort - # prepare listener lists: list of functions to call after certain + # Prepare listener lists: list of functions to call after certain # events happen self.reorganization_listeners = [] self.new_module_listeners = [] + self.update_property_listeners = [] self.reorganize() def add_reorganization_listener(self, function): - """Add function to list of functions to be called after reorganization. + """Add to list of functions to be called after reorganization. This is useful for users of this class to handle changes to the SOI. As an example a class displaying an SOI can be updated after changes. @@ -330,13 +337,21 @@ class SOI: self.reorganization_listeners.append(function) def add_new_module_listener(self, function): - """Add function to list of functions to be called after added module. + """Add to list of functions to be called after added module. This is useful for users of this class to handle changes to the SOI. As an example a class displaying an SOI can be updated after changes. """ self.new_module_listeners.append(function) + def add_update_property_listener(self, function): + """Add to list of functions to be called after properties change. + + This is useful for users of this class to handle changes to the SOI. + As an example a class displaying an SOI can be updated after changes. + """ + self.update_property_listeners.append(function) + def update_module_widget_position(self, module): """Update position of module widget based on meta position. @@ -502,7 +517,7 @@ class SOI: name : str Name of new module widget_implementation : subclass of ModuleBase - The widget implementation of the module. + An instance of the widget implementation of the module. is_attachment : bool True if the module should be added as an attachment. False otherwise @@ -525,3 +540,80 @@ class SOI: # call listener functions for listener in self.new_module_listeners: listener() + + # Ignoring pylint "Too many branches" error as this function is a special + # case where the if-elif-elif-...-else makes sense + def update_properties(self, **kwargs): # pylint: disable=R0912 + """Update properties given as input, and call listeners. + + This function exists solely because there are listeners on the + properties. A "cleaner" way to achieve the same goal would be to + create a "setter" for each property and call the listeners there, but + for bulk changes this would get unwieldy. Another way to achieve the + goal of calling listeners after changes to properties would be to + create a separate function that the user is expected to call after + changing the properties directly, but this would put unnecessary + responsibility on the user. + + All properties except "modules" and "attachements" can be updated + using this function. + + If a change is made that affects placement of modules this function + will call `reorganize` + + Parameters + ---------- + **kwargs : key, value pairs of properties to update + Accepts both a normal dict, and passing arguments as normal: + `update_properties({'title': 'my title'})` and + update_properties(title='my title')`. Accepted keys are properties + of the SOI class, except 'modules' and 'attachements'. Explanation + of **kwargs: + https://stackoverflow.com/a/1769475/3545896 + """ + # For every key, value pair passed in kwargs, update corresponding + # property + for key, value in kwargs.items(): + if key == "title": + self.title = value + elif key == "description": + self.description = value + elif key == "version": + self.version = value + elif key == "date": + self.date = value + elif key == "valid_from": + self.valid_from = value + elif key == "valid_to": + self.valid_to = value + elif key == "icon": + self.icon = value + elif key == "classification": + self.classification = value + elif key == "orientation": + self.orientation = value + elif key == "placement_strategy": + self.placement_strategy = value + elif key == "algorithm_bin": + self.algorithm_bin = value + elif key == "algorithm_pack": + self.algorithm_pack = value + elif key == "algorithm_sort": + self.algorithm_sort = value + else: + raise ValueError( + f"Unknown property name {key} passed with value {value}" + ) + + for listener in self.update_property_listeners: + listener() + + # If any properties relating to module placement were touched, + # reorganize + if ( + "placement_strategy" in kwargs + or "algorithm_bin" in kwargs + or "algorithm_pack" in kwargs + or "algorithm_sort" in kwargs + ): + self.reorganize() diff --git a/test/test_module_freetext.py b/test/test_module_freetext.py new file mode 100644 index 0000000000000000000000000000000000000000..00016ffbed346e63737219e377d269abdfc1fefd --- /dev/null +++ b/test/test_module_freetext.py @@ -0,0 +1,93 @@ +"""Test FreeTextModule.""" + +import unittest +from PySide2 import QtGui +from PySide2.QtWidgets import QApplication, QWidget, QVBoxLayout +from PySide2.QtCore import Qt +from PySide2.QtTest import QTest +from soitool.modules.module_freetext import FreeTextModule +from soitool.soi import SOI + +if isinstance(QtGui.qApp, type(None)): + app = QApplication([]) +else: + app = QtGui.qApp + + +class TestFreeTextModuleWithSetUp(unittest.TestCase): + """TestCase for FreeText Module.""" + + def setUp(self): + """Prepare widget with module to test as child.""" + # NOTE have to put module inside wrapping widget because module by + # itself is too small + self.widget = QWidget() + self.layout = QVBoxLayout() + self.freetext_module = FreeTextModule() + self.layout.addWidget(self.freetext_module) + self.widget.setLayout(self.layout) + self.widget.show() + + def test_type_header_and_body(self): + """Type into header and body and assert expected content.""" + test_header = "This is a header" + test_body = "This is a body" + + QTest.keyClicks(self.freetext_module.line_edit_header, test_header) + QTest.keyClicks(self.freetext_module.text_edit_body, test_body) + + self.assertEqual( + test_header, self.freetext_module.line_edit_header.text() + ) + self.assertEqual( + test_body, self.freetext_module.text_edit_body.toPlainText() + ) + + def test_module_dynamic_size_body(self): + """Type content into body to make it grow, and assert growth.""" + before_width, before_height = self.freetext_module.get_size() + + # Make enough newlines into the body to force the module to grow + # vertically + for _ in range(30): + QTest.keyClick(self.freetext_module.text_edit_body, Qt.Key_Enter) + + # Insert enough keys in the body to force the module to grow + # horizontally + for _ in range(100): + QTest.keyClick(self.freetext_module.text_edit_body, Qt.Key_A) + + after_width, after_height = self.freetext_module.get_size() + + # Make sure the module grew + self.assertTrue(before_width < after_width) + self.assertTrue(before_height < after_height) + + def test_add_to_soi_smoke_test(self): + """Test that can add to SOI successfully.""" + soi = SOI() + test_name = "Test name" + soi.add_module(test_name, self.freetext_module, False) + self.assertTrue(soi.module_name_taken(test_name)) + + +class TestFreeTextModuleWithoutSetUp(unittest.TestCase): + """TestCase for FreeTextModule without setUp.""" + + def test_create_with_data(self): + """Test creation from data.""" + test_header = "This is the header" + test_body = "This is the body" + test_data = [test_header, test_body] + freetext_module = FreeTextModule(data=test_data) + freetext_module.show() + + # Assert expected data is in real widget + self.assertEqual(freetext_module.line_edit_header.text(), test_header) + self.assertEqual( + freetext_module.text_edit_body.toPlainText(), test_body + ) + + # Assert get back our data with get_data + fetched_data = freetext_module.get_data() + self.assertEqual(test_data, fetched_data) diff --git a/test/test_soi.py b/test/test_soi.py index 9e1a4d2ea85ea4668ea246c2189308b442704663..659d693f484418bc048d34ddc05aa9587fb8eb28 100644 --- a/test/test_soi.py +++ b/test/test_soi.py @@ -361,3 +361,28 @@ class TestSOI(unittest.TestCase): ) except ModuleNameTaken: self.fail("Exception should not be raised when name is unique") + + def test_update_properties(self): + """Test updating properties and corresponding listener.""" + test_title = "a test title" + + listener_called = False + + def listener_update_properties(): + # nonlocal is required to access variable in nesting function's + # scope + nonlocal listener_called + listener_called = True + + self.soi.add_update_property_listener(listener_update_properties) + self.soi.update_properties(title=test_title) + self.assertTrue(listener_called) + + self.assertEqual(self.soi.title, test_title) + + def test_update_properties_invalid_key(self): + """Test update_properties properly throws exception if invalid key.""" + self.assertRaises( + ValueError, + lambda: self.soi.update_properties(invalid_key="garbage"), + )