diff --git a/soitool/inline_editable_soi_view.py b/soitool/inline_editable_soi_view.py index 93146a79c97d18b49c089b95e3bda7dcb42684ad..2f5903f11cf599e644c0b06372c26b416f736b0f 100644 --- a/soitool/inline_editable_soi_view.py +++ b/soitool/inline_editable_soi_view.py @@ -1,4 +1,5 @@ """Includes functionality for inline editing of SOI.""" +from datetime import datetime from PySide2.QtCore import Qt, QRectF, QTimer, QPoint, QMarginsF from PySide2.QtWidgets import ( QApplication, @@ -12,9 +13,7 @@ from PySide2.QtGui import QFont, QPixmap, QBrush, QPalette, QPainter from PySide2.QtPrintSupport import QPrinter from soitool.soi import ModuleLargerThanBinError from soitool.dialog_wrappers import exec_warning_dialog -from soitool.serialize_export_import_soi import ( - generate_soi_filename, -) +from soitool.serialize_export_import_soi import generate_soi_filename class InlineEditableSOIView(QScrollArea): @@ -180,9 +179,16 @@ class InlineEditableSOIView(QScrollArea): self.update_number_of_pages() self.draw_pages() + # Ignoring Pylint's errors "Too many local variables" and + # "Too many statements" for this function because it's doing GUI layout + # work, which in nature is tedious and repetitive. + # pylint: disable=R0914,R0915 def draw_header(self, x, y, page_number): """Draw header staring at given position. + Source for rotation approach: + * https://stackoverflow.com/a/43389394/3545896 + Parameters ---------- x : int @@ -190,31 +196,101 @@ class InlineEditableSOIView(QScrollArea): """ # title label_title = QLabel(self.soi.title) - label_title.move(x, y) + label_title.move(x, y + self.soi.HEADER_HEIGHT) label_title.setStyleSheet("background-color: rgba(0,0,0,0%)") - label_title.setFont(QFont("Times New Roman", 50)) - self.scene.addWidget(label_title) + label_title.setFont(QFont("Times New Roman", 35)) + proxy = self.scene.addWidget(label_title) + proxy.setRotation(-90) + + # description + label_description = QLabel(self.soi.description) + label_description.move(x + 57, y + self.soi.HEADER_HEIGHT) + label_description.setStyleSheet("background-color: rgba(0,0,0,0%)") + label_description.setFont(QFont("Times New Roman", 15)) + proxy = self.scene.addWidget(label_description) + proxy.setRotation(-90) + + # creation date + creation_date = soi_date_string_to_user_friendly_string(self.soi.date) + label_creation_date = QLabel("Opprettet: {}".format(creation_date)) + label_creation_date.setStyleSheet("background-color: rgba(0,0,0,0%)") + label_creation_date.setFont(QFont("Times New Roman", 15)) + # source: https://stackoverflow.com/a/8638114/3545896 + # CAUTION: does not work if font is set through stylesheet + label_width = ( + label_creation_date.fontMetrics() + .boundingRect(label_creation_date.text()) + .width() + ) + label_creation_date.move(x + 80, y + self.soi.HEADER_HEIGHT) + proxy = self.scene.addWidget(label_creation_date) + proxy.setRotation(-90) + + # patch + pixmap = QPixmap(self.soi.icon) + patch = QLabel() + patch.setPixmap( + pixmap.scaled(self.soi.HEADER_WIDTH - 2, self.soi.HEADER_WIDTH - 2) + ) + patch.move( + x + 1, y + self.soi.HEADER_HEIGHT / 2 + self.soi.HEADER_WIDTH + 10 + ) + proxy = self.scene.addWidget(patch) + proxy.setRotation(-90) + + # copy number + copy_number = QLabel("1 av 20") + copy_number.setStyleSheet("background-color: rgba(0,0,0,0%)") + copy_number.setFont(QFont("Times New Roman", 40)) + # source: https://stackoverflow.com/a/8638114/3545896 + # CAUTION: does not work if font is set through stylesheet + label_width = ( + copy_number.fontMetrics().boundingRect(copy_number.text()).width() + ) + # store for usage when placing copy number title and page number + copy_number_y_pos = y + self.soi.HEADER_HEIGHT / 2 - 100 + copy_number.move(x + 18, copy_number_y_pos + label_width / 2) + proxy = self.scene.addWidget(copy_number) + proxy.setRotation(-90) + + # copy number title + label_copy_number_header = QLabel("Eksemplar") + label_copy_number_header.setStyleSheet( + "background-color: rgba(0,0,0,0%)" + ) + label_copy_number_header.setFont(QFont("Times New Roman", 15)) + # source: https://stackoverflow.com/a/8638114/3545896 + # CAUTION: does not work if font is set through stylesheet + label_width = ( + label_copy_number_header.fontMetrics() + .boundingRect(label_copy_number_header.text()) + .width() + ) + label_copy_number_header.move(x, copy_number_y_pos + label_width / 2) + proxy = self.scene.addWidget(label_copy_number_header) + proxy.setRotation(-90) # page numbering page_number = QLabel( - "{} av {}".format(page_number, self.number_of_pages) + "Side {} av {}".format(page_number, self.number_of_pages) ) page_number.setStyleSheet("background-color: rgba(0,0,0,0%)") - page_number.setFont(QFont("Times New Roman", 50)) + page_number.setFont(QFont("Times New Roman", 15)) # source: https://stackoverflow.com/a/8638114/3545896 # CAUTION: does not work if font is set through stylesheet label_width = ( page_number.fontMetrics().boundingRect(page_number.text()).width() ) - page_number.move(x + (self.soi.CONTENT_WIDTH - label_width) / 2, y) - self.scene.addWidget(page_number) + page_number.move(x + 85, copy_number_y_pos + label_width / 2) + proxy = self.scene.addWidget(page_number) + proxy.setRotation(-90) # classification classification = QLabel(self.soi.classification) classification.setStyleSheet( "background-color: rgba(0,0,0,0%); " "color: red" ) - classification.setFont(QFont("Times New Roman", 50)) + classification.setFont(QFont("Times New Roman", 35)) # source: https://stackoverflow.com/a/8638114/3545896 # CAUTION: does not work if font is set through stylesheet label_width = ( @@ -222,20 +298,43 @@ class InlineEditableSOIView(QScrollArea): .boundingRect(classification.text()) .width() ) - x_pos = ( - x + self.soi.CONTENT_WIDTH - label_width - self.soi.HEADER_HEIGHT - ) - classification.move(x_pos, y) - self.scene.addWidget(classification) + classification.move(x, y + label_width) + proxy = self.scene.addWidget(classification) + proxy.setRotation(-90) - # patch - pixmap = QPixmap(self.soi.icon) - patch = QLabel() - patch.setPixmap( - pixmap.scaled(self.soi.HEADER_HEIGHT, self.soi.HEADER_HEIGHT) + # from date + valid_from = soi_date_string_to_user_friendly_string( + self.soi.valid_from + ) + label_valid_from = QLabel("Gyldig fra: {}".format(valid_from)) + label_valid_from.setStyleSheet("background-color: rgba(0,0,0,0%)") + label_valid_from.setFont(QFont("Times New Roman", 15)) + # source: https://stackoverflow.com/a/8638114/3545896 + # CAUTION: does not work if font is set through stylesheet + label_width = ( + label_valid_from.fontMetrics() + .boundingRect(label_valid_from.text()) + .width() + ) + label_valid_from.move(x + 57, y + label_width) + proxy = self.scene.addWidget(label_valid_from) + proxy.setRotation(-90) + + # to date + valid_to = soi_date_string_to_user_friendly_string(self.soi.valid_to) + label_valid_to = QLabel("Gyldig til: {}".format(valid_to)) + label_valid_to.setStyleSheet("background-color: rgba(0,0,0,0%)") + label_valid_to.setFont(QFont("Times New Roman", 15)) + # source: https://stackoverflow.com/a/8638114/3545896 + # CAUTION: does not work if font is set through stylesheet + label_width = ( + label_valid_to.fontMetrics() + .boundingRect(label_valid_to.text()) + .width() ) - patch.move(x + self.soi.CONTENT_WIDTH - self.soi.HEADER_HEIGHT, y) - self.scene.addWidget(patch) + label_valid_to.move(x + 80, y + label_width) + proxy = self.scene.addWidget(label_valid_to) + proxy.setRotation(-90) def draw_page(self, x, y): """Draw page starting at given position. @@ -257,19 +356,13 @@ class InlineEditableSOIView(QScrollArea): self.scene.addRect( x + self.soi.PADDING, y + self.soi.PADDING, - self.soi.CONTENT_WIDTH, - self.soi.CONTENT_HEIGHT, - ) - self.scene.addRect( - x + self.soi.PADDING, - y + self.soi.PADDING, - self.soi.CONTENT_WIDTH, - self.soi.CONTENT_HEIGHT, + self.soi.WIDTH - self.soi.PADDING * 2, + self.soi.HEIGHT - self.soi.PADDING * 2, ) self.scene.addRect( x + self.soi.PADDING, y + self.soi.PADDING, - self.soi.CONTENT_WIDTH, + self.soi.HEADER_WIDTH, self.soi.HEADER_HEIGHT, ) @@ -324,3 +417,20 @@ class InlineEditableSOIView(QScrollArea): pos_new = self.view.mapToScene(self.soi.WIDTH / 2, self.soi.HEIGHT / 2) delta = pos_new - pos_old self.view.translate(delta.x(), delta.y()) + + +def soi_date_string_to_user_friendly_string(date_string): + """Convert SOI date to user-friendly format. + + Parameters + ---------- + date_string : str + String following convention used in SOI, which is 'YYYY-MM-DD' + + Returns + ------- + str + String following user friendly convention 'DD.MM.YYYY' + """ + parsed_date = datetime.strptime(date_string, "%Y-%m-%d") + return parsed_date.strftime("%d.%m.%Y") diff --git a/soitool/soi.py b/soitool/soi.py index 8ac55dc93b54393b1c28860e9d2244e2af8a3308..058d4e19209d5b2fc052997ce102db198391d7dd 100644 --- a/soitool/soi.py +++ b/soitool/soi.py @@ -141,30 +141,46 @@ class SOI: the class variables. ```text + + SOI.WIDTH + | + v + _________________ + SOI.PADDING - | SOI.CONTENT_WIDTH - | | SOI.PADDING - | | | - v v v - +-------------+ - | | <- SOI.PADDING - | +---------+ | - | | HEADER | | <- SOI.HEADER_HEIGHT - | +---------+ | - | | MODULES | | <- SOI.CONTENT_HEIGHT - | +---------+ | - | | <- SOI.PADDING - +-------------+ - <- SOI.PADDING - +-------------+ - | | <- SOI.PADDING - | +---------+ | - | | HEADER | | <- SOI.HEADER_HEIGHT - | +---------+ | - | | MODULES | | <- SOI.CONTENT_HEIGHT - | +---------+ | - | | <- SOI.PADDING - +-------------+ + | SOI.HEADER_WIDTH + | | SOI.CONTENT_WIDTH + | | | SOI.PADDING + | | | | + v v v v + _ ___ _________ _ + + +-----------------+ + | | | <- SOI.PADDING | <- SOI.HEIGHT + | +---+---------+ | | + | | H | MODULES | | | <- SOI.CONTENT_HEIGHT | + | | E | | | | | + | | A | | | | | + | | D | | | | | + | | E | | | | | + | | R | | | | | + | +---+---------+ | | + | | | <- SOI.PADDING | + +-----------------+ + | <- SOI.PADDING + +-----------------+ + | | | <- SOI.PADDING | <- SOI.HEIGHT + | +---+---------+ | | + | | H | MODULES | | | <- SOI.CONTENT_HEIGHT | + | | E | | | | | + | | A | | | | | + | | D | | | | | + | | E | | | | | + | | R | | | | | + | +---+---------+ | | + | | | <- SOI.PADDING | + +-----------------+ + ``` `MODULES` is where all the modules of the page will show up. Each module @@ -180,10 +196,19 @@ class SOI: * `module["meta"]["y"]` is a value between `0` and `SOI.CONTENT_HEIGHT`. This value is determined by the placement strategy. * `module["widget"].pos().x()` is calculated from `module["meta"]["x"]` - as follows: `module["meta"]["x"] + SOI.PADDING` + as follows: + + ```text + module["meta"]["x"] + SOI.PADDING + SOI.HEADER_WIDTH + ``` + * `module["widget"].pos().y()` is calculated from `module["meta"]["y"]` - as follows: `module["meta"]["y"] + SOI.PADDING + SOI.HEADER_HEIGHT + - (SOI.PADDING + SOI.HEIGHT) * (module["meta"]["page"] - 1)` + as follows: + + ```text + module["meta"]["y"] + SOI.PADDING + + (SOI.PADDING + SOI.HEIGHT) * (module["meta"]["page"] - 1) + ``` The function `SOI.update_module_widget_position` is responsible for updating the widget positions based on the "meta" positions, using the @@ -238,10 +263,11 @@ class SOI: HEIGHT = 1700 WIDTH = HEIGHT * A4_RATIO PADDING = 100 - CONTENT_WIDTH = WIDTH - PADDING * 2 - CONTENT_HEIGHT = HEIGHT - PADDING * 2 - HEADER_HEIGHT = 100 + HEADER_WIDTH = 110 + HEADER_HEIGHT = HEIGHT - PADDING * 2 MODULE_PADDING = 10 + CONTENT_WIDTH = WIDTH - PADDING * 2 - HEADER_WIDTH + CONTENT_HEIGHT = HEIGHT - PADDING * 2 @classmethod def construct_from_compressed_soi_file(cls, filename): @@ -365,20 +391,17 @@ class SOI: itself a dict with fields "x", "y" and "page", and "widget" is a widget based on "ModuleBase" """ - distance_to_start_of_next_soi_content_y = ( - self.CONTENT_HEIGHT + self.PADDING * 2 + self.HEADER_HEIGHT - ) + distance_to_start_of_next_soi_content_y = self.HEIGHT + self.PADDING scene_skip_distance_page_height = ( distance_to_start_of_next_soi_content_y * (module["meta"]["page"] - 1) ) - new_x = module["meta"]["x"] + self.PADDING + new_x = module["meta"]["x"] + self.PADDING + self.HEADER_WIDTH new_y = ( module["meta"]["y"] + self.PADDING - + self.HEADER_HEIGHT + scene_skip_distance_page_height ) @@ -471,9 +494,7 @@ class SOI: # float("inf") to add infinite bins. See https://github.com/secnot/rectpack/blob/master/README.md#api packer.add_bin( - self.CONTENT_WIDTH, - self.CONTENT_HEIGHT - self.HEADER_HEIGHT, - float("inf"), + self.CONTENT_WIDTH, self.CONTENT_HEIGHT, float("inf"), ) packer.pack() diff --git a/test/test_serialize_export_import.py b/test/test_serialize_export_import.py index c1c2afa1e3d7b85e65ae73da6792352935a64b3d..3d69e0ff1ee61e24bd831102f46eca3553fecb34 100644 --- a/test/test_serialize_export_import.py +++ b/test/test_serialize_export_import.py @@ -24,9 +24,9 @@ SOITOOL_ROOT_PATH = Path(__file__).parent.parent TITLE = "testSOI" DESCRIPTION = "This is a description" VERSION = "1" -DATE = "01.01.2020" -VALID_FROM = "01.01.2020" -VALID_TO = "02.01.2020" +DATE = "2020-01-01" +VALID_FROM = "2020-01-01" +VALID_TO = "2020-01-02" ICON = "soitool/media/HVlogo.png" CLASSIFICATION = "UGRADERT" ORIENTATION = "portrait" @@ -74,9 +74,10 @@ class SerializeTest(unittest.TestCase): modules=MODULES, attachments=MODULES, ) - date = datetime.now().strftime("%Y_%m_%d") - file_name_uncompressed = f"SOI_{TITLE}_{date}.json" - file_name_compressed = f"SOI_{TITLE}_{date}.txt" + date = datetime.strptime(DATE, "%Y-%m-%d") + date_formatted_for_file_path = date.strftime("%Y_%m_%d") + file_name_uncompressed = f"SOI_{TITLE}_{date_formatted_for_file_path}.json" + file_name_compressed = f"SOI_{TITLE}_{date_formatted_for_file_path}.txt" self.file_path_uncompressed = os.path.join( SOITOOL_ROOT_PATH, file_name_uncompressed diff --git a/test/test_soi.py b/test/test_soi.py index 659d693f484418bc048d34ddc05aa9587fb8eb28..8a399dd6181baba5bdcebfb2e9e6472f7153a7bb 100644 --- a/test/test_soi.py +++ b/test/test_soi.py @@ -174,7 +174,7 @@ class TestSOI(unittest.TestCase): def test_reorganize_of_too_big_module(self): """Test exception thrown by reorganize in case of too large modules.""" maximum_width = self.soi.CONTENT_WIDTH - maximum_height = self.soi.CONTENT_HEIGHT - self.soi.HEADER_HEIGHT + maximum_height = self.soi.CONTENT_HEIGHT module_too_wide = { "widget": TestModule("red", maximum_width + 1, 100), @@ -230,7 +230,7 @@ class TestSOI(unittest.TestCase): """ # append module of maximum size maximum_width = self.soi.CONTENT_WIDTH - maximum_height = self.soi.CONTENT_HEIGHT - self.soi.HEADER_HEIGHT + maximum_height = self.soi.CONTENT_HEIGHT module_maximum_size = { "widget": TestModule("red", maximum_width, maximum_height), "meta": {"x": 0, "y": 0, "page": 1, "name": "maximum_size"}, @@ -284,13 +284,14 @@ class TestSOI(unittest.TestCase): self.soi.update_module_widget_position(self.soi.modules[0]) # calculate expected widget positions. See SOI class docstring for an - # explanation of the calculation neededfor y - expected_x = page_2_module["meta"]["x"] + self.soi.PADDING + # explanation of the calculation needed for y + expected_x = ( + page_2_module["meta"]["x"] + + self.soi.PADDING + + self.soi.HEADER_WIDTH + ) expected_y = ( - page_2_module["meta"]["y"] - + self.soi.PADDING * 2 - + self.soi.HEIGHT - + self.soi.HEADER_HEIGHT + page_2_module["meta"]["y"] + self.soi.PADDING * 2 + self.soi.HEIGHT ) self.assertEqual(