Skip to content
Snippets Groups Projects
Commit aad780ce authored by Anders H. Rebner's avatar Anders H. Rebner
Browse files

#86 Merge conflict

parents 9b640345 deef8d88
No related branches found
No related tags found
1 merge request!50#86 Modul: Autentiseringstavle
Pipeline #78324 failed
......@@ -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.")
......
......@@ -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()
......
......@@ -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()
......
......@@ -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)
......
soitool/media/freetextmodule.PNG

2.02 KiB

"""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")
......@@ -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,
]
......
......@@ -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)
......
......@@ -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:
......
......@@ -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()
"""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)
......@@ -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"),
)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment