diff --git a/.gitignore b/.gitignore index 673ab0bc892bc30d1c485353c1b600f7d565bbd8..5d90984aa8cd0607c9d1192fd1166eeaea1f013a 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 5a84d5fca96015cf2da4e731486558fcf8aa7698..323ae98c48055f4811de6a0d7e2fa0851e564bc7 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 a2d4cef93229699b529dcd7ffc7fd3489c8ac96d..87cd70992600d8f20df4cbe127f3eb2701116a7f 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 0000000000000000000000000000000000000000..41d0491a7380498455a0edc12cb81e8dc489ba63 --- /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 f66a307f4107c0a139de8734ace6d5795a124a5a..0000000000000000000000000000000000000000 --- 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)