From 144d0bb415b916a3b38b0b1ef1bc6084b87a92c0 Mon Sep 17 00:00:00 2001 From: "Anders H. Rebner" <anderhre@stud.ntnu.no> Date: Fri, 20 Mar 2020 16:46:18 +0100 Subject: [PATCH] #38 #39 Opprydding og dokumentering --- .gitignore | 4 + soitool/main_window.py | 151 +++++++-------------- soitool/modules/module_table.py | 50 +++---- soitool/serialize_export_import_soi.py | 174 +++++++++++++++++++++++++ soitool/soi_export.py | 50 ------- 5 files changed, 255 insertions(+), 174 deletions(-) create mode 100644 soitool/serialize_export_import_soi.py delete mode 100644 soitool/soi_export.py diff --git a/.gitignore b/.gitignore index 673ab0b..5d90984 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,7 @@ __pycache__/ # Generated codebook PDF-files Kodebok_*.pdf + +# Generated SOI-files +SOI_*_*_*_*.txt +SOI_*_*_*_*.json \ No newline at end of file diff --git a/soitool/main_window.py b/soitool/main_window.py index 5a84d5f..323ae98 100644 --- a/soitool/main_window.py +++ b/soitool/main_window.py @@ -5,8 +5,6 @@ Built up by widgets implemented in other modules. """ import sys import os -import json -from datetime import datetime from enum import Enum from functools import partial from PySide2.QtWidgets import ( @@ -22,15 +20,15 @@ from PySide2.QtWidgets import ( from PySide2.QtGui import QIcon from PySide2.QtCore import QTimer from soitool.soi_workspace_widget import SOIWorkspaceWidget -from soitool.soi import SOI from soitool.codebook import CodeBookTableView from soitool.codebook_row_adder import CodebookRowAdder from soitool.codebook_to_pdf import generate_codebook_pdf -from soitool.soi_export import serialize_soi -from soitool.compressor import compress, decompress from soitool.dialog_wrappers import exec_info_dialog -from soitool.database import Database, DBPATH -from soitool.modules.module_table import TableModule +from soitool.database import Database +from soitool.serialize_export_import_soi import ( + export_soi, + import_soi, +) class ModuleType(Enum): @@ -78,6 +76,8 @@ class MainWindow(QMainWindow): filepath = os.path.join(dirname, filename) self.setWindowIcon(QIcon(filepath)) + # pylint: disable=R0914, R0915 + # Ignoring "Too manu local variables" and "Too many statements" def setup_menubar(self): """Set up menubar with submenus and actions.""" menu = self.menuBar() @@ -86,7 +86,7 @@ class MainWindow(QMainWindow): help_menu = menu.addMenu("Hjelp") # New SOI - new_soi = QAction("Ny SOI", self) + new_soi = QAction("Ny", self) new_soi.setShortcut("Ctrl+n") new_soi.setStatusTip("Opprett en ny SOI") file_menu.addAction(new_soi) @@ -117,12 +117,24 @@ class MainWindow(QMainWindow): save_soi.setStatusTip("Lagre SOI i databasen") file_menu.addAction(save_soi) - # Export - export = QAction("Eksporter", self) - export.setShortcut("Ctrl+e") - export.setStatusTip("Eksporter SOI til annet filformat") - export.triggered.connect(partial(self.export_soi, compressed=True)) - file_menu.addAction(export) + # Export SOI + export_serialized_soi = file_menu.addMenu("Eksporter") + # Compressed SOI + export_compressed = QAction("Komprimert", self) + export_compressed.setShortcut("Ctrl+e") + export_compressed.setStatusTip("Eksporter komprimert SOI") + export_compressed.triggered.connect( + partial(self.try_export_soi, compressed=True) + ) + # Uncompressed SOI + export_uncompressed = QAction("Ukomprimert", self) + export_uncompressed.setStatusTip("Eksporter ukomprimert SOI") + export_uncompressed.triggered.connect( + partial(self.try_export_soi, compressed=False) + ) + export_serialized_soi.addAction(export_uncompressed) + export_serialized_soi.addAction(export_compressed) + file_menu.addMenu(export_serialized_soi) # View/edit Codebook codebook = QAction("Se/rediger kodebok", self) @@ -192,29 +204,22 @@ class MainWindow(QMainWindow): self.tabs.addTab(tab, "Kodebok") self.tabs.setCurrentWidget(tab) - def export_soi(self, compressed=True): - """Export SOI if current tab is SOIWorkspaceWidget. + def try_export_soi(self, compressed=True): + """Export the SOI in the current tab. + + Feedback is given through a dialog if the current tab does not show a + SOI (tab is not a SOIWorkspaceWidget). - Give feedback to user if tab is not SOIWorkspaceWidget. + Parameters + ---------- + compressed : bool, optional + Serialized SOI is compressed if True (default) """ tab_widget = self.tabs.currentWidget() + # If tab contains a SOI if isinstance(tab_widget, SOIWorkspaceWidget): - serialized = serialize_soi(tab_widget.soi) - - title = tab_widget.soi.title - date = datetime.now().strftime("%Y_%m_%d") - file_name = f"SOI_{title}_{date}" - - if compressed: - serialized = compress(serialized) - file = open(file_name + ".txt", "w") - else: - file = open(file_name + ".json", "w") - - file.write(str(serialized)) - file.close() - + export_soi(tab_widget.soi, compressed) else: exec_info_dialog( "Valgt tab er ingen SOI-tab", @@ -223,89 +228,31 @@ class MainWindow(QMainWindow): ) def import_soi(self): - """Import uncompressed or compressed soi. + """Import serialized SOI. - Launches a QFileDialog with .txt and .json as accepted file formats. - Reads content and decompresses if necessary, .txt-files are compressed. - Opens and selects new tab with the imported SOI. + Launches a QFileDialog with .txt and .json as accepted file extensions. + A SOIWorkspaceWidget containing the SOI-object is created and opened + in a new tab, which is selected. """ - + # Get file-name from dialog file_name = QFileDialog().getOpenFileName( self, - "Open SOI", + "Ã…pne SOI", os.getcwd(), - "Text/JSON-Files (SOI_*.txt SOI_*.json)", + "Text/JSON-filer (SOI_*.txt SOI_*.json)", )[0] if len(file_name) > 0: + soi = import_soi(file_name) - with open(file_name, "r") as file: - if file_name[-4::] == ".txt": - serialized = eval(decompress(file.read())) - else: - serialized = eval(file.read()) - - modules = [] - for module in serialized["modules"]: - module_type = module["type"] - - if module_type == "TableModule": - size = module["data"]["size"] - content = module["data"]["content"] - meta = module["meta"] - modules.append( - {"widget": TableModule(size, content), "meta": meta} - ) - else: - raise ValueError - - # attachments - attachments = [] - for attachment in serialized["attachments"]: - module_type = attachment["type"] - - if module_type == "TableModule": - size = module["data"]["size"] - content = module["data"]["content"] - meta = module["meta"] - attachments.append( - {"widget": TableModule(size, content), "meta": meta} - ) - else: - raise ValueError - # Ã…PNE SOI I TAB - soi = SOI( - serialized["title"], - serialized["description"], - "1", # version - None, # date - serialized["valid"]["from"], - serialized["valid"]["to"], - serialized["icon"], - serialized["classification"], - serialized["orientation"], - serialized["placement_strategy"], - serialized["algorithm_bin"], - serialized["algorithm_pack"], - serialized["algorithm_sort"], - modules, - attachments, - ) - - # Create SOIWorkspaceWidget - tab = SOIWorkspaceWidget(soi) - self.tabs.addTab(tab, soi.title) - self.tabs.setCurrentWidget(tab) - - # SOI(modules=.., attachments=...) + # Create and select tab + tab = SOIWorkspaceWidget(soi) + self.tabs.addTab(tab, soi.title) + self.tabs.setCurrentWidget(tab) if __name__ == "__main__": - # Create and set up database if it does not exist - if not os.path.exists(DBPATH): - Database() - app = QApplication(sys.argv) WINDOW = MainWindow() WINDOW.showMaximized() diff --git a/soitool/modules/module_table.py b/soitool/modules/module_table.py index a2d4cef..87cd709 100644 --- a/soitool/modules/module_table.py +++ b/soitool/modules/module_table.py @@ -8,8 +8,8 @@ HEADER_FONT.setFamily("Arial") HEADER_FONT.setPointSize(12) HEADER_FONT.setWeight(100) -START_COLUMNS = 2 START_ROWS = 2 +START_COLUMNS = 2 class Meta(type(ModuleBase), type(QTableWidget)): @@ -25,17 +25,12 @@ class TableModule(ModuleBase, QTableWidget, metaclass=Meta): The widget does not use more room than needed, and resizes dynamically. Columnheaders are styled with light grey background and bold text. Has shortcuts for adding and removing rows and columns. - """ - def set_pos(self, pos): - """Set position of widget. - - Parameters - ---------- - pos : QPoint - Position (x, y). - """ - self.move(pos) + By default, the widget initializes as an empty START_ROWS * START_COLUMNS + table. If parameters are given, the table initializes accordingly: + 'size' is a dict: {"width": int, "height": int}, + 'content' is a 2D list where content[x][y] represents row x, column y. + """ def __init__(self, size=None, content=None): self.type = "TableModule" @@ -47,37 +42,38 @@ class TableModule(ModuleBase, QTableWidget, metaclass=Meta): self.horizontalHeader().hide() self.verticalHeader().hide() + # If parameters are None, start as empty table. if size is None and content is None: - print("size og content is None") # Set number of columns and rows self.setColumnCount(START_COLUMNS) self.setRowCount(START_ROWS) - # Resize width and height of columns and rows, and set size of window + + # Resize width and height of columns and rows, & set size of window self.resize() self.setFixedWidth(START_COLUMNS * self.columnWidth(0) + 2) self.setFixedHeight(START_ROWS * self.rowHeight(0) + 5) - # Set headeritems + + # Set header-items for i in range(self.columnCount()): self.set_header_item(i, "") - else: - # print("size og content er noe") - # print("columncount: ", len(content[0])) - # print("rowcount: ", len(content)) self.setColumnCount(len(content[0])) self.setRowCount(len(content)) - # Set headeritems + # Set header-items for i in range(self.columnCount()): self.set_header_item(i, content[0][i]) + # Set cell-items for i in range(1, self.rowCount()): for j in range(self.columnCount()): item = QTableWidgetItem(content[i][j]) self.setItem(i, j, item) - self.resize() - # self.setFixedWidth(size["width"]) - # self.setFixedHeight(size["height"]) + + self.resizeColumnsToContents() + self.resizeRowsToContents() + self.setFixedWidth(size["width"]) + self.setFixedHeight(size["height"]) # Remove scrollbars self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) @@ -200,6 +196,16 @@ class TableModule(ModuleBase, QTableWidget, metaclass=Meta): return width, 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.""" diff --git a/soitool/serialize_export_import_soi.py b/soitool/serialize_export_import_soi.py new file mode 100644 index 0000000..41d0491 --- /dev/null +++ b/soitool/serialize_export_import_soi.py @@ -0,0 +1,174 @@ +"""Includes functionality for serializing, exporting and importing SOI.""" +import json +from datetime import datetime +from ast import literal_eval +from soitool.soi import SOI +from soitool.compressor import compress, decompress +from soitool.modules.module_table import TableModule + + +def serialize_soi(soi): + """Serialize SOI to JSON-string. + + Parameters + ---------- + soi : soitool.soi.SOI + SOI to serialize. + + Returns + ------- + String + JSON-string containing all SOI-information. + + Raises + ------ + ValueError + Raises error if parameter 'soi' is not a SOI. + """ + # If parameter 'soi' is not a SOI + if not isinstance(soi, SOI): + raise ValueError( + "Invalid value for parameter 'soi': " + "'{}'.".format(soi) + ) + + # Create dict with relevant module-information + modules = [] + for module in soi.modules: + modules.append( + { + "type": module["widget"].type, + "data": module["widget"].get_as_dict(), + "meta": module["meta"], + } + ) + + # Create dict with relevant attachment-information + attachments = [] + for attachment in soi.attachments: + attachments.append( + { + "type": attachment["widget"].type, + "data": attachment["widget"].get_as_dict(), + "meta": attachment["meta"], + } + ) + + # Create dict with all relevant SOI-information + serialized = { + "title": soi.title, + "description": soi.description, + "date": soi.date, + "valid": {"from": soi.valid_from, "to": soi.valid_to}, + "icon": soi.icon, + "classification": soi.classification, + "orientation": soi.orientation, + "placement_strategy": soi.placement_strategy, + "algorithm_bin": soi.algorithm_bin, + "algorithm_pack": soi.algorithm_pack, + "algorithm_sort": soi.algorithm_sort, + "modules": modules, + "attachments": attachments, + } + + return json.dumps(serialized) + + +def export_soi(soi, compressed=True): + """Export SOI. + + 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 + SOI to export + compressed : bool, optional + Serialized SOI is compressed if True (default). + """ + # Serialize SOI + serialized = serialize_soi(soi) + + # Generate filename + title = soi.title + date = datetime.now().strftime("%Y_%m_%d") + file_name = f"SOI_{title}_{date}" + + if compressed: + serialized = compress(serialized) + file = open(file_name + ".txt", "w") + else: + file = open(file_name + ".json", "w") + + file.write(str(serialized)) + file.close() + + +def import_soi(file_name): + """Import compressed or uncompressed serialized SOI. + + Reads content of file and decompresses it for .txt-files. + Creates an SOI-object based on the file content. + """ + with open(file_name, "r") as file: + if file_name[-4::] == ".txt": + serialized = literal_eval(decompress(file.read())) + else: + serialized = literal_eval(file.read()) + + # Create dict for modules with modified format + modules = [] + for module in serialized["modules"]: + module_type = module["type"] + + if module_type == "TableModule": + size = module["data"]["size"] + content = module["data"]["content"] + meta = module["meta"] + modules.append( + {"widget": TableModule(size, content), "meta": meta} + ) + else: + raise ValueError( + "Module-type '{}' is not recognized.".format(module_type) + ) + + # Create dict for attachments with modified format + attachments = [] + for attachment in serialized["attachments"]: + module_type = attachment["type"] + + if module_type == "TableModule": + size = attachment["data"]["size"] + content = attachment["data"]["content"] + meta = attachment["meta"] + attachments.append( + {"widget": TableModule(size, content), "meta": meta} + ) + else: + raise ValueError( + "Module-type '{}' is not recognized.".format(module_type) + ) + # Create SOI + soi = SOI( + serialized["title"], + serialized["description"], + "1", # version + None, # date + serialized["valid"]["from"], + serialized["valid"]["to"], + serialized["icon"], + serialized["classification"], + serialized["orientation"], + serialized["placement_strategy"], + serialized["algorithm_bin"], + serialized["algorithm_pack"], + serialized["algorithm_sort"], + modules, + attachments, + ) + + return soi diff --git a/soitool/soi_export.py b/soitool/soi_export.py deleted file mode 100644 index f66a307..0000000 --- a/soitool/soi_export.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Includes functionality for serializing SOI as a Dictionary.""" -import json -from soitool.soi import SOI - - -def serialize_soi(soi): - """Export SOI to JSON(compressed?).""" - print("Exporting") - if not isinstance(soi, SOI): - raise ValueError( - "Invalid value for parameter 'soi': " + "'{}'".format(soi) - ) - - modules = [] - for module in soi.modules: - modules.append( - { - "type": module["widget"].type, - "data": module["widget"].get_as_dict(), - "meta": module["meta"], - } - ) - - attachments = [] - for attachment in soi.attachments: - attachments.append( - { - "type": attachment["widget"].type, - "data": attachment["widget"].get_as_dict(), - "meta": attachment["meta"], - } - ) - - serialized = { - "title": soi.title, - "description": soi.description, - "date": soi.date, - "valid": {"from": soi.valid_from, "to": soi.valid_to}, - "icon": soi.icon, - "classification": soi.classification, - "orientation": soi.orientation, - "placement_strategy": soi.placement_strategy, - "algorithm_bin": soi.algorithm_bin, - "algorithm_pack": soi.algorithm_pack, - "algorithm_sort": soi.algorithm_sort, - "modules": modules, - "attachments": attachments, - } - - return json.dumps(serialized) -- GitLab