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

Merge branch 'soi-header-matche-design' into 'master'

#104 & #49: PDF som menyvalg & nytt header design

See merge request !54
parents e4fecc8c db704d08
No related branches found
No related tags found
1 merge request!54#104 & #49: PDF som menyvalg & nytt header design
Pipeline #80804 passed
......@@ -48,4 +48,5 @@ Kodebok_*.pdf
# Generated SOI-files
SOI_*_*_*_*.txt
SOI_*_*_*_*.json
\ No newline at end of file
SOI_*_*_*_*.json
SOI_*_*_*_*.pdf
"""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,6 +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
class InlineEditableSOIView(QScrollArea):
......@@ -82,7 +84,7 @@ class InlineEditableSOIView(QScrollArea):
# self.launch_auto_zoom()
def produce_pdf(self, filename):
def produce_pdf(self, filename=None):
"""Produce PDF using QGraphicsScene.
Renders the QGraphicsScene-representation of the SOI as a PDF. This
......@@ -98,13 +100,18 @@ class InlineEditableSOIView(QScrollArea):
filename : str
Name of file to store PDF in. If file exists it will be
overwritten. Note that the filename should contain the extension
'.pdf' to be properly handled by operating systems.
'.pdf' to be properly handled by operating systems. If no filename
is supplied one will be generated following the same convention as
for exported JSON and TXT files
Raises
------
ValueError
If filename is invalid.
"""
if filename is None:
filename = generate_soi_filename(self.soi) + ".pdf"
printer = QPrinter(QPrinter.HighResolution)
printer.setOutputFormat(QPrinter.PdfFormat)
printer.setOutputFileName(filename)
......@@ -177,9 +184,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
......@@ -187,52 +201,145 @@ 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 N")
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))
# source: https://stackoverflow.com/a/8638114/3545896
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))
# source: https://stackoverflow.com/a/8638114/3545896
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 = (
classification.fontMetrics()
.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
)
patch.move(x + self.soi.CONTENT_WIDTH - self.soi.HEADER_HEIGHT, y)
self.scene.addWidget(patch)
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()
)
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.
......@@ -254,19 +361,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,
)
......@@ -321,3 +422,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")
......@@ -36,6 +36,14 @@ class ModuleType(Enum):
ATTACHMENT_MODULE = 1
class ExportMedium(Enum):
"""Enumerate with mediums possible for export."""
COMPRESSED = 0
UNCOMPRESSED = 1
PDF = 2
class MainWindow(QMainWindow):
"""MainWindow, shell of the entire application.
......@@ -133,16 +141,23 @@ class MainWindow(QMainWindow):
export_compressed.setShortcut("Ctrl+e")
export_compressed.setStatusTip("Eksporter komprimert SOI")
export_compressed.triggered.connect(
lambda: self.try_export_soi(compressed=True)
lambda: self.try_export_soi(medium=ExportMedium.COMPRESSED)
)
# Uncompressed SOI
export_uncompressed = QAction("Ukomprimert", self)
export_uncompressed.setStatusTip("Eksporter ukomprimert SOI")
export_uncompressed.triggered.connect(
lambda: self.try_export_soi(compressed=False)
lambda: self.try_export_soi(medium=ExportMedium.UNCOMPRESSED)
)
# SOI PDF
export_pdf = QAction("PDF", self)
export_pdf.setStatusTip("Eksporter til PDF klar for utskrift")
export_pdf.triggered.connect(
lambda: self.try_export_soi(medium=ExportMedium.PDF)
)
export_serialized_soi.addAction(export_compressed)
export_serialized_soi.addAction(export_uncompressed)
export_serialized_soi.addAction(export_pdf)
file_menu.addMenu(export_serialized_soi)
# View/edit Codebook
......@@ -228,7 +243,7 @@ class MainWindow(QMainWindow):
self.tabs.removeTab(index)
def try_export_soi(self, compressed=True):
def try_export_soi(self, medium=ExportMedium.COMPRESSED):
"""Export the SOI in the current tab.
Feedback is given through a dialog if the current tab does not contain
......@@ -236,14 +251,27 @@ class MainWindow(QMainWindow):
Parameters
----------
compressed : bool, optional
Serialized SOI is compressed if True (default)
medium : ExportMedium
Which medium to export SOI to. Must be one of the enums in
ExportMedium.
Raises
------
ValueError
If export medium is unknown.
"""
tab_widget = self.tabs.currentWidget()
# If tab contains an SOI
if isinstance(tab_widget, SOIWorkspaceWidget):
export_soi(tab_widget.soi, compressed)
if medium == ExportMedium.COMPRESSED:
export_soi(tab_widget.soi, True)
elif medium == ExportMedium.UNCOMPRESSED:
export_soi(tab_widget.soi, False)
elif medium == ExportMedium.PDF:
tab_widget.view.produce_pdf()
else:
raise ValueError(f"Unknown medium for export '{medium}'")
else:
exec_info_dialog(
"Valgt tab er ingen SOI-tab",
......
......@@ -151,9 +151,6 @@ def export_soi(soi, compressed=True):
A .txt-file is created to contain compressed SOI.
A .json-file is created to contain uncompressed SOI.
The generated file-name will be on the format: "SOI_title_YYYY_mm_dd",
where title is the SOI-title.
Parameters
----------
soi: soitool.soi.SOI
......@@ -164,10 +161,7 @@ def export_soi(soi, compressed=True):
# Serialize SOI
serialized = serialize_soi(soi)
# Generate filename
title = soi.title
date = datetime.now().strftime("%Y_%m_%d")
file_name = f"SOI_{title}_{date}"
file_name = generate_soi_filename(soi)
if compressed:
serialized = compress(serialized)
......@@ -319,3 +313,22 @@ def construct_soi_from_serialized(serialized, compressed=False):
)
return soi
def generate_soi_filename(soi):
"""Generate filename for SOI without extension.
Parameters
----------
soi : SOI
SOI to generate filename for.
Returns
-------
str
Filename for the SOI of the format 'SOI_title_YYYY_mm_dd'.
"""
title = soi.title
parsed_date = datetime.strptime(soi.date, "%Y-%m-%d")
date_string = parsed_date.strftime("%Y_%m_%d")
return f"SOI_{title}_{date_string}"
......@@ -149,30 +149,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
......@@ -188,10 +204,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
......@@ -246,10 +271,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):
......@@ -389,20 +415,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
)
......@@ -509,9 +532,7 @@ class SOI:
# 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()
......
......@@ -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,14 @@ 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
......
......@@ -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(
......
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