diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 241dd4ac5fa7095150553e2b6e4fe9228d61c74e..6073eca5d14ce4dec5fb7b1e8f5cba07726c7188 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -42,6 +42,14 @@ job_lint_pydocstyle: script: - pydocstyle --version - pydocstyle --match '.*.py' --convention=numpy soitool test + +job_lint_black: + stage: lint + <<: *docker_runner_tags_definition + image: morkolai/soitool-ci + script: + - black --version + - black -l 79 --check --diff soitool test job_test_gui_ubuntu_vnc: stage: test diff --git a/requirements.txt b/requirements.txt index a3125a740581d76f738d63ef4eb8c80e78785c78..bbcbfcad0b3ddf945af0e2206ca36faa343937fc 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/scripts/CodeQualityCheck.ps1 b/scripts/CodeQualityCheck.ps1 index e2894fa734a9e2054caca683ea971f57eb0119d5..09f685c39a22a9cca2a298ea290272c4319f46fc 100644 --- a/scripts/CodeQualityCheck.ps1 +++ b/scripts/CodeQualityCheck.ps1 @@ -31,4 +31,6 @@ for ($i=0; $i -lt $files.Length; $i++){ bandit $files[$i] Write-Output "`n===PYDOCSTYLE===`n" pydocstyle.exe --convention=numpy $files[$i] + Write-Output "`n===BLACK===`n" + black -l 79 --check --diff $files[$i] } diff --git a/scripts/CodeQualityCheck.sh b/scripts/CodeQualityCheck.sh index 899f9a49d7074d873a2e6ded71a2db4e8efd049c..4cc02886ff10bd9e5b375a6d8bbafa8b4b92fd8f 100644 --- a/scripts/CodeQualityCheck.sh +++ b/scripts/CodeQualityCheck.sh @@ -24,4 +24,6 @@ for file in $files; do bandit $file printf "\n===PYDOCSTYLE===\n" pydocstyle --convention=numpy $file + printf "\n===BLACK===\n" + black -l 79 --check --diff $file done diff --git a/soitool/codebook_row_adder.py b/soitool/codebook_row_adder.py index 39cc45fe93d85ee91aa93011579d2a286321d274..bf6c350d68efdc9b54d8f2c1a6e7cfc8eb45fd94 100644 --- a/soitool/codebook_row_adder.py +++ b/soitool/codebook_row_adder.py @@ -156,16 +156,19 @@ class CodebookRowAdder(QWidget): # Reset feedback-label self.label_feedback.setText("") - # Read input and uppercase first character of word and category + # Read input word_input = self.text_field_word.text() - word_input = word_input[0].upper() + word_input[1:] category_input = self.text_field_category.text() - category_input = category_input[0].upper() + category_input[1:] type_input = self.combo_type.currentText() # If word is not empty if len(word_input) > 0: + # Uppercase first character of word and category (if bot empty) + word_input = word_input[0].upper() + word_input[1:] + if len(category_input) > 0: + category_input = category_input[0].upper() + category_input[1:] + db = Database() try: diff --git a/soitool/codebook_to_pdf.py b/soitool/codebook_to_pdf.py index 1e5ea38669c16b6262e675ff1bed32be58183b53..f7994a06d7640ab7a7555688c329d520d676b198 100644 --- a/soitool/codebook_to_pdf.py +++ b/soitool/codebook_to_pdf.py @@ -5,10 +5,10 @@ from reportlab.pdfgen import canvas from reportlab.platypus import ( Table, Paragraph, - TableStyle, - SimpleDocTemplate, Spacer, PageBreak, + TableStyle, + SimpleDocTemplate, ) from reportlab.lib.styles import ParagraphStyle from reportlab.lib import colors @@ -179,7 +179,7 @@ class CodeAndDecodebookDocTemplate(SimpleDocTemplate): If code- and decodebook use 10 pages each, the total page count will be 20, but this class will draw 'Side 1 av 10' through 'Side 10 av 10' for both. - The first blank page (2 * Pagebreak) added will be marked as a separating + The first blank page (2 * PageBreak) added will be marked as a separating page, and will not contain 'Side x av y'. """ diff --git a/soitool/database.py b/soitool/database.py index ac3e3b8163f5b4677173b44e98695e3b957071b4..7cbca53fae41914765c7206ac2d6317425812578 100644 --- a/soitool/database.py +++ b/soitool/database.py @@ -2,6 +2,7 @@ import os import sqlite3 import json +from datetime import datetime import soitool.coder from soitool.enumerates import CodebookSort @@ -10,6 +11,9 @@ DBNAME = "database" CURDIR = os.path.dirname(__file__) DBPATH = os.path.join(CURDIR, DBNAME) +# Number og seconds in 24h +SECONDS_IN_24H = 24 * 60 * 60 + # DDL-statements for creating tables CODEBOOK = ( "CREATE TABLE CodeBook" @@ -23,6 +27,8 @@ CATEGORYWORDS = ( ) BYHEART = "CREATE TABLE ByHeart(Word VARCHAR PRIMARY KEY)" +LASTUPDATED = "CREATE TABLE LastUpdated(Timestamp DATETIME PRIMARY KEY)" + class Database: """ @@ -30,9 +36,12 @@ class Database: Connects to existing db if found, creates new db if not. If db is created, tables are created and filled. + + Holds a QTimer that requests an update of CodeBook on every timeout. """ def __init__(self): + db_exists = os.path.exists(DBPATH) if db_exists: @@ -53,7 +62,7 @@ class Database: def create_tables(self): """Create tables CodeBook, CategoryWords and ByHeart.""" - stmts = [CODEBOOK, CATEGORYWORDS, BYHEART] + stmts = [CODEBOOK, CATEGORYWORDS, BYHEART, LASTUPDATED] for stmt in stmts: self.conn.execute(stmt) @@ -65,6 +74,7 @@ class Database: self.fill_codebook() self.fill_by_heart() self.fill_category_words() + self.fill_last_updated() self.conn.commit() def fill_codebook(self): @@ -72,9 +82,9 @@ class Database: file_path = os.path.join(CURDIR, "testdata/long_codebook.json") # Load json as dict - f = open(file_path, "r", encoding="utf-8") - entries = json.load(f) - f.close() + file = open(file_path, "r", encoding="utf-8") + entries = json.load(file) + file.close() # Generate codes code_len = soitool.coder.get_code_length_needed(len(entries)) @@ -95,35 +105,45 @@ class Database: def fill_by_heart(self): """Read data from ByHeart.txt and fill DB-table ByHeart.""" file_path = os.path.join(CURDIR, "testdata/ByHeart.txt") - f = open(file_path, "r", encoding="utf-8") + file = open(file_path, "r", encoding="utf-8") # Loop through words on file and insert them into ByHeart-table stmt = "INSERT INTO ByHeart(Word) VALUES(?)" - for expr in f: + for expr in file: self.conn.execute(stmt, (expr.rstrip(),)) - f.close() + file.close() def fill_category_words(self): """Read data from CategoryWords.txt and fill DB-table CategoryWords.""" file_path = os.path.join(CURDIR, "testdata/CategoryWords.txt") - f = open(file_path, "r", encoding="utf-8") + file = open(file_path, "r", encoding="utf-8") # Get number of categories on file - no_of_categories = int(f.readline().rstrip()) + no_of_categories = int(file.readline().rstrip()) # Loop through categories on file for _ in range(no_of_categories): # Get category and number of words in category - line = f.readline().split(", ") + line = file.readline().split(", ") category = line[0] no_of_words = int(line[1].rstrip()) # Loop through words in category and add rows to DB stmt = "INSERT INTO CategoryWords(Word, Category) VALUES(?, ?)" for _ in range(no_of_words): - word = f.readline().rstrip() + word = file.readline().rstrip() self.conn.execute(stmt, (word, category,)) - f.close() + file.close() + + def fill_last_updated(self): + """Fill table with current date and time.""" + stmt = "INSERT INTO LastUpdated(Timestamp) VALUES(?)" + self.conn.execute(stmt, (str(datetime.now()),)) + + def update_last_updated(self): + """Update Timestamp in LastUpdated to current time.""" + stmt = "UPDATE LastUpdated SET Timestamp = ?" + self.conn.execute(stmt, (str(datetime.now()),)) def get_categories(self): """ @@ -208,13 +228,64 @@ class Database: stmt = stmt + "WHERE Word = ?" for i in range(number_of_entries): self.conn.execute(stmt, (codes.pop(), words[i][0])) + # Updates LastUpdated with current time + self.update_last_updated() print("Code in CodeBook updated") + def seconds_to_next_update(self, period): + """ + Return time to next update of Codebook in seconds. + + Parameters + ---------- + period : int + The number of seconds between each update + + Returns + ------- + seconds_to_update : float + Time to next update in seconds + """ + stmt = "SELECT Timestamp FROM LastUpdated" + last_updated = self.conn.execute(stmt).fetchall()[0][0] + # Convert datetime string to datetime object + last_updated = datetime.strptime(last_updated, "%Y-%m-%d %H:%M:%S.%f") + # Calculate the number of seconds until next update + seconds_to_update = ( + period - (datetime.now() - last_updated).total_seconds() + ) + # Since QTimer does not handle negative values + if seconds_to_update <= 0: + return 0 + + return seconds_to_update + + def update_codebook_auto(self, timer): + """ + Update Codebook if needed and update time for timer. + + Parameters + ---------- + timer : QTimer + Timer to set new interval on. + """ + if self.seconds_to_next_update(SECONDS_IN_24H) <= 0: + self.update_codebook() + timer.setInterval(self.seconds_to_next_update(SECONDS_IN_24H) * 1000) + def add_code_to(self, word, mode="ascii"): """ Generate and insert a code for the new word in DB-table CodeBook. + This function is espescially designed for when a single word is added + to the CodeBook table. A unique code is generate and inserted in + Code which for the parameter word from before were NULL. Dependent on + the number of entries in the table various actions are performed. If + the new word makes the number of entries pass 26^x and the length of + the codes does not have he capacity, all the codes are updatet to an + appropriate length. + Parameters ---------- word : string @@ -228,11 +299,18 @@ class Database: stmt = "SELECT COUNT(*) FROM CodeBook" number_of_entries = self.conn.execute(stmt).fetchall()[0][0] stmt = "SELECT Code FROM CodeBook" - # Incase db is approximate empty, min code lenght is 2 - if number_of_entries < 2: - actual_code_len = len(self.conn.execute(stmt).fetchall()[1][0]) + # In special case where table is empty + if number_of_entries <= 0: + raise ValueError("Can't add code to table with no words.") + # In special case table only has a single word + if number_of_entries == 1: + actual_code_len = soitool.coder.get_code_length_needed( + number_of_entries + ) else: - actual_code_len = soitool.coder.get_code_length_needed(0) + # Since the newly added word's code is NULL and at [0][0], + # get [1][0] to get code length from an actual code + actual_code_len = len(self.conn.execute(stmt).fetchall()[1][0]) needed_code_len = soitool.coder.get_code_length_needed( number_of_entries @@ -244,7 +322,7 @@ class Database: # Get all codes and convert to set codes = self.conn.execute(stmt).fetchall() codes = {c[:][0] for c in codes} - # Get new unique code fro param word + # Get new unique code for param word code = soitool.coder.get_code(needed_code_len, mode) while code in codes: code = soitool.coder.get_code(needed_code_len, mode) diff --git a/soitool/inline_editable_soi_view.py b/soitool/inline_editable_soi_view.py index 1734ca3531d101ea5671d735e00d1a28478b1d05..a1aaea031295cc74b1b8a844859c326419186795 100644 --- a/soitool/inline_editable_soi_view.py +++ b/soitool/inline_editable_soi_view.py @@ -1,7 +1,13 @@ """Includes functionality for inline editing of SOI.""" from PySide2.QtCore import Qt, QRectF, QTimer, QPoint, QMarginsF -from PySide2.QtWidgets import QApplication, QScrollArea, QLabel, \ - QGraphicsScene, QGraphicsView, QGraphicsRectItem +from PySide2.QtWidgets import ( + QApplication, + QScrollArea, + QLabel, + QGraphicsScene, + QGraphicsView, + QGraphicsRectItem, +) from PySide2.QtGui import QFont, QPixmap, QBrush, QPalette, QPainter from PySide2.QtPrintSupport import QPrinter @@ -90,9 +96,11 @@ class InlineEditableSOIView(QScrollArea): ok = painter.begin(printer) if not ok: - raise ValueError("Not able to begin QPainter using QPrinter " - "based on argument " - "filename '{}'".format(filename)) + raise ValueError( + "Not able to begin QPainter using QPrinter " + "based on argument " + "filename '{}'".format(filename) + ) # render each page to own PDF page for i, modules in enumerate(self.pages): @@ -100,9 +108,10 @@ class InlineEditableSOIView(QScrollArea): x = 0 y = self.soi.HEIGHT * i + self.soi.PADDING * i - self.scene.render(painter, source=QRectF(x, y, - self.soi.WIDTH, - self.soi.HEIGHT)) + self.scene.render( + painter, + source=QRectF(x, y, self.soi.WIDTH, self.soi.HEIGHT), + ) # if there are more pages, newPage if i + 1 < len(self.pages): @@ -119,8 +128,9 @@ class InlineEditableSOIView(QScrollArea): # adjust page size full_scene_height = y + self.soi.HEIGHT - self.scene.setSceneRect(QRectF(0, 0, self.soi.WIDTH, - full_scene_height)) + self.scene.setSceneRect( + QRectF(0, 0, self.soi.WIDTH, full_scene_height) + ) self.draw_page(x, y) self.draw_header(x + self.soi.PADDING, y + self.soi.PADDING, i + 1) @@ -150,30 +160,37 @@ class InlineEditableSOIView(QScrollArea): page_number.setFont(QFont("Times New Roman", 50)) # source: https://stackoverflow.com/a/8638114/3545896 # CAUTION: does not work if font is set through stylesheet - label_width = \ + 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) # classification classification = QLabel(self.soi.classification) - classification.setStyleSheet("background-color: rgba(0,0,0,0%); " - "color: red") + 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 # 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 + 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) # patch pixmap = QPixmap(self.soi.icon) patch = QLabel() - patch.setPixmap(pixmap.scaled(self.soi.HEADER_HEIGHT, - self.soi.HEADER_HEIGHT)) + patch.setPixmap( + pixmap.scaled(self.soi.HEADER_HEIGHT, self.soi.HEADER_HEIGHT) + ) patch.move(x + self.soi.CONTENT_WIDTH - self.soi.HEADER_HEIGHT, y) self.scene.addWidget(patch) @@ -186,19 +203,32 @@ class InlineEditableSOIView(QScrollArea): y : int """ # color the page white - page_background = QGraphicsRectItem(x, y, self.soi.WIDTH, - self.soi.HEIGHT) + page_background = QGraphicsRectItem( + x, y, self.soi.WIDTH, self.soi.HEIGHT + ) page_background.setBrush(QBrush(Qt.white)) self.scene.addItem(page_background) # draw borders self.scene.addRect(x, y, self.soi.WIDTH, self.soi.HEIGHT) - 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.scene.addRect(x + self.soi.PADDING, y + self.soi.PADDING, - self.soi.CONTENT_WIDTH, self.soi.HEADER_HEIGHT) + 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.scene.addRect( + x + self.soi.PADDING, + y + self.soi.PADDING, + self.soi.CONTENT_WIDTH, + self.soi.HEADER_HEIGHT, + ) def setup_scene(self): """Prepare scene for use. @@ -220,6 +250,7 @@ class InlineEditableSOIView(QScrollArea): Used to demonstrate zooming only, should be removed once the project matures. """ + def do_on_timeout(): self.zoom(1 / 1.00005) @@ -244,8 +275,9 @@ class InlineEditableSOIView(QScrollArea): # Zoom self.view.scale(zoom_factor, zoom_factor) - pos_old = self.view.mapToScene(QPoint(self.soi.WIDTH / 2, - self.soi.HEIGHT / 2)) + pos_old = self.view.mapToScene( + QPoint(self.soi.WIDTH / 2, self.soi.HEIGHT / 2) + ) 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()) diff --git a/soitool/main.py b/soitool/main.py index 8b55ed6c5e7869231b39663df1edfb9672e3d2ea..94daab90f293ea5cacd4b379de9638dc2b27bdfb 100644 --- a/soitool/main.py +++ b/soitool/main.py @@ -44,7 +44,7 @@ class CoolWidget(QtWidgets.QWidget): self, "Change text", "Please type something", - QtWidgets.QLineEdit.Normal + QtWidgets.QLineEdit.Normal, ) if ok: self.qlabel.setText(text) diff --git a/soitool/main_window.py b/soitool/main_window.py index b5a3ddbac81db49e2ab0e7b433ceca1b39abffcf..bf2fb3d706c30a3cbeebb7c25d9c9498d6db86e0 100644 --- a/soitool/main_window.py +++ b/soitool/main_window.py @@ -17,9 +17,10 @@ from PySide2.QtWidgets import ( QAction, ) from PySide2.QtGui import QIcon +from PySide2.QtCore import QTimer +from soitool.soi_workspace_widget import SOIWorkspaceWidget from soitool.codebook import CodeBookTableView from soitool.codebook_row_adder import CodebookRowAdder -from soitool.soi_workspace_widget import SOIWorkspaceWidget from soitool.codebook_to_pdf import generate_codebook_pdf from soitool.database import Database, DBPATH @@ -40,6 +41,19 @@ class MainWindow(QMainWindow): self.setWindowTitle("SOI-tool") self.statusBar() + # Database instance + database = Database() + # Timer for automatic update of codes in CodeBook + self.timer = QTimer() + # Interval i set to msec since last 24h update + self.timer.setInterval( + database.seconds_to_next_update(60 * 60 * 24) * 1000 + ) + self.timer.timeout.connect( + lambda: database.update_codebook_auto(self.timer) + ) + self.timer.start() + # flytt ut til egen funksjon, for setup av menubar menu = self.menuBar() file_menu = menu.addMenu("SOI") @@ -100,7 +114,7 @@ class MainWindow(QMainWindow): # Export full codebook export_codebook_full = QAction("Stor kodebok", self) export_codebook_full.setStatusTip("Eksporter stor kodebok som PDF") - export_codebook_full.triggered.connect(partial(generate_codebook_pdf)) + export_codebook_full.triggered.connect(generate_codebook_pdf) export_codebook.addAction(export_codebook_full) # Export small codebook diff --git a/soitool/module_list.py b/soitool/module_list.py index d8c81252b5a56cb53f6db3f5cf9337c5c2d27270..bf65f8cb421b972260d59a2c0628ca01fa5e1837 100644 --- a/soitool/module_list.py +++ b/soitool/module_list.py @@ -26,11 +26,14 @@ class ModuleList(QListWidget): super().__init__() # full import path below to avoid circular dependency - if not isinstance(parent, - soitool.soi_workspace_widget.SOIWorkspaceWidget): - raise RuntimeError('Only soitool.SOIWorkspaceWidget is ' - 'acceptable type for parent-variable ' - 'in class Module_list.') + if not isinstance( + parent, soitool.soi_workspace_widget.SOIWorkspaceWidget + ): + raise RuntimeError( + "Only soitool.SOIWorkspaceWidget is " + "acceptable type for parent-variable " + "in class Module_list." + ) self.type = module_type self.parent = parent self.original_element_name = None @@ -56,11 +59,14 @@ class ModuleList(QListWidget): """Fill list with elements.""" # Get names of modules/attachments: if ModuleType(self.type) == ModuleType.MAIN_MODULE: - names = [module["meta"]["name"] for - module in self.parent.soi.modules] + names = [ + module["meta"]["name"] for module in self.parent.soi.modules + ] elif ModuleType(self.type) == ModuleType.ATTACHMENT_MODULE: - names = [attachment["meta"]["name"] for - attachment in self.parent.soi.attachments] + names = [ + attachment["meta"]["name"] + for attachment in self.parent.soi.attachments + ] for i, name in enumerate(names): item = QListWidgetItem(name) diff --git a/soitool/modules/module_table.py b/soitool/modules/module_table.py index 29adbce0881c19645afa7c8e0435eefc6748a197..77808f65a055310e6a299db0d16f067bcd3d4aad 100644 --- a/soitool/modules/module_table.py +++ b/soitool/modules/module_table.py @@ -4,7 +4,7 @@ from PySide2 import QtGui, QtCore from soitool.modules.module_base import ModuleBase HEADER_FONT = QtGui.QFont() -HEADER_FONT.setFamily('Arial') +HEADER_FONT.setFamily("Arial") HEADER_FONT.setPointSize(12) HEADER_FONT.setWeight(100) @@ -78,14 +78,20 @@ class TableModule(ModuleBase, QTableWidget, metaclass=Meta): """ if event.key() == QtCore.Qt.Key_Question: self.add_column() - elif (event.modifiers() == QtCore.Qt.ShiftModifier - and event.key() == QtCore.Qt.Key_Underscore): + elif ( + event.modifiers() == QtCore.Qt.ShiftModifier + and event.key() == QtCore.Qt.Key_Underscore + ): self.remove_column() - elif (event.modifiers() == QtCore.Qt.ControlModifier - and event.key() == QtCore.Qt.Key_Plus): + elif ( + event.modifiers() == QtCore.Qt.ControlModifier + and event.key() == QtCore.Qt.Key_Plus + ): self.add_row() - elif (event.modifiers() == QtCore.Qt.ControlModifier - and event.key() == QtCore.Qt.Key_Underscore): + elif ( + event.modifiers() == QtCore.Qt.ControlModifier + and event.key() == QtCore.Qt.Key_Underscore + ): self.remove_row() else: super(TableModule, self).keyPressEvent(event) diff --git a/soitool/pdf_preview_widget.py b/soitool/pdf_preview_widget.py index f241cb20cb087efdffcf52ae9057a1d99f47d143..a868be31aa5ba3a0bd7c86b5c829188a12b8689c 100644 --- a/soitool/pdf_preview_widget.py +++ b/soitool/pdf_preview_widget.py @@ -24,10 +24,12 @@ class PDFPreviewWidget(QWebEngineView): def __init__(self, initial_url): super().__init__() - self.page().settings().setAttribute(QWebEngineSettings.PluginsEnabled, - True) + self.page().settings().setAttribute( + QWebEngineSettings.PluginsEnabled, True + ) # the following setting is the default, but explicitly enabling to be # explicit - self.page().settings().setAttribute(QWebEngineSettings. - PdfViewerEnabled, True) + self.page().settings().setAttribute( + QWebEngineSettings.PdfViewerEnabled, True + ) self.load(QUrl(initial_url)) diff --git a/soitool/setup_settings.py b/soitool/setup_settings.py index 3fef0c0c7d804eeff898b060f91a7e56ea68fa95..1f23448163ae9224e83db8ac1d9ffbecb4fcd7e4 100644 --- a/soitool/setup_settings.py +++ b/soitool/setup_settings.py @@ -4,8 +4,16 @@ This dialog is called when a button in soi_workspace_widget is pressed """ -from PySide2.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, \ - QLabel, QLineEdit, QRadioButton, QPushButton +from PySide2.QtWidgets import ( + QDialog, + QVBoxLayout, + QHBoxLayout, + QFormLayout, + QLabel, + QLineEdit, + QRadioButton, + QPushButton, +) class Setup(QDialog): # pylint: disable = R0902 @@ -34,8 +42,8 @@ class Setup(QDialog): # pylint: disable = R0902 self.layout_setup.addWidget(self.label_header) self.head1 = QLabel("Header1") # Change variablename later self.head2 = QLabel("Header2") # Change variablename later - self.hline1 = QLineEdit() # Change variablename later - self.hline2 = QLineEdit() # Change variablename later + self.hline1 = QLineEdit() # Change variablename later + self.hline2 = QLineEdit() # Change variablename later self.layout_header.addRow(self.head1, self.hline1) self.layout_header.addRow(self.head2, self.hline2) self.layout_setup.addLayout(self.layout_header) @@ -79,3 +87,7 @@ class Setup(QDialog): # pylint: disable = R0902 self.button_cancel.clicked.connect(self.reject) # esc-key (default) self.button_save.clicked.connect(self.accept) # enter-key (default) + + # need two functions, save and cancel + # when cancel, clear lineedits and set radiobuttons to default or SOI-parts + # when save, lineedits writes to labels and find a way to get radiobuttons diff --git a/soitool/soi.py b/soitool/soi.py index 7a4281db04e573965cd4eb7bd8a296fb499fa0c5..4ad77ea0481aa4f83f499bb56eb8acc76bf178fc 100644 --- a/soitool/soi.py +++ b/soitool/soi.py @@ -12,7 +12,7 @@ class ModuleType(Enum): ATTACHMENT_MODULE = 1 -class SOI(): +class SOI: """Datastructure for SOI. Holds all info about an SOI necessary to view and edit it. @@ -72,20 +72,22 @@ class SOI(): # separate data classes (auto-placement stuff in one class, styling in # another, etc). # pylint: disable=r0913 - def __init__(self, - title="Default SOI title", - description="Default SOI description", - version="1", - date=None, - valid_from=None, - valid_to=None, - icon="soitool/media/HVlogo.png", - classification="ugradert", - orientation="landscape", - placement_strategy="auto", - algorithm_bin="BFF", - algorithm_pack="MaxRectsBI", - algorithm_sort="SORT_AREA"): + def __init__( + self, + title="Default SOI title", + description="Default SOI description", + version="1", + date=None, + valid_from=None, + valid_to=None, + icon="soitool/media/HVlogo.png", + classification="ugradert", + orientation="landscape", + placement_strategy="auto", + algorithm_bin="BFF", + algorithm_pack="MaxRectsBI", + algorithm_sort="SORT_AREA", + ): # populate date-related arguments if they are not supplied by the user if date is None: @@ -122,31 +124,16 @@ class SOI(): self.modules = [ { "widget": TableModule(), - "meta": { - "x": 0, - "y": 0, - "page": 1, - "name": 'Tabell1' - } + "meta": {"x": 0, "y": 0, "page": 1, "name": "Tabell1"}, }, { "widget": TableModule(), - "meta": { - "x": 0, - "y": 0, - "page": 1, - "name": 'Tabell2' - } + "meta": {"x": 0, "y": 0, "page": 1, "name": "Tabell2"}, }, { "widget": TableModule(), - "meta": { - "x": 0, - "y": 0, - "page": 2, - "name": 'Tabell3' - } - } + "meta": {"x": 0, "y": 0, "page": 2, "name": "Tabell3"}, + }, ] # NOTE @@ -154,12 +141,7 @@ class SOI(): self.attachments = [ { "widget": TableModule(), - "meta": { - "x": 0, - "y": 0, - "page": 2, - "name": 'Tabell1' - } + "meta": {"x": 0, "y": 0, "page": 2, "name": "Tabell1"}, } ] @@ -177,16 +159,22 @@ 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.CONTENT_HEIGHT + self.PADDING * 2 + self.HEADER_HEIGHT + ) - scene_skip_distance_page_height = \ - distance_to_start_of_next_soi_content_y * \ - (module["meta"]["page"] - 1) + 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_y = module["meta"]["y"] + self.PADDING + self.HEADER_HEIGHT + \ - scene_skip_distance_page_height + new_y = ( + module["meta"]["y"] + + self.PADDING + + self.HEADER_HEIGHT + + scene_skip_distance_page_height + ) module["widget"].set_pos(QPoint(new_x, new_y)) @@ -199,8 +187,9 @@ class SOI(): next to each other from left to right """ x = self.MODULE_PADDING - first_page_modules = [module for module in self.modules - if module["meta"]["page"] == 1] + first_page_modules = [ + module for module in self.modules if module["meta"]["page"] == 1 + ] for module in first_page_modules: module["meta"]["x"] = x @@ -214,8 +203,9 @@ class SOI(): # NOTE the following is simply duplicated.. left like this to KISS # will be replaced by rectpack anyways x = self.MODULE_PADDING - second_page_modules = [module for module in self.modules - if module["meta"]["page"] == 2] + second_page_modules = [ + module for module in self.modules if module["meta"]["page"] == 2 + ] for module in second_page_modules: module["meta"]["x"] = x module["meta"]["y"] = self.MODULE_PADDING diff --git a/soitool/soi_workspace_widget.py b/soitool/soi_workspace_widget.py index 76b5531b35690ce19984de8cdd938edd232b8bb6..c2ea49e1dc3b8edb05fb38cf181269781f2080a6 100644 --- a/soitool/soi_workspace_widget.py +++ b/soitool/soi_workspace_widget.py @@ -3,8 +3,13 @@ Meant for use inside of tabs in our program. """ -from PySide2.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout, QPushButton, \ - QLabel +from PySide2.QtWidgets import ( + QWidget, + QHBoxLayout, + QVBoxLayout, + QPushButton, + QLabel, +) from soitool.soi import SOI, ModuleType from soitool.module_list import ModuleList from soitool.inline_editable_soi_view import InlineEditableSOIView diff --git a/test/test_codebook_to_pdf.py b/test/test_codebook_to_pdf.py index 8a312c9b3961874874d06985d9f6bfefc6cb5a1e..5aa94bee06349f604c97f6065d7c10d0dc8112ab 100644 --- a/test/test_codebook_to_pdf.py +++ b/test/test_codebook_to_pdf.py @@ -19,7 +19,7 @@ class ExportTest(unittest.TestCase): # Assert correct filename for full codebook expected = f"Kodebok_{today}.pdf" - actual = codebook_to_pdf.generate_filename() + actual = codebook_to_pdf.generate_filename(small=False) self.assertEqual(expected, actual) # Assert correct filename for small codebook @@ -31,7 +31,7 @@ class ExportTest(unittest.TestCase): """Test generated PDF-file exist.""" # Test full codebook (default) codebook_to_pdf.generate_codebook_pdf() - file_name = codebook_to_pdf.generate_filename() + file_name = codebook_to_pdf.generate_filename(small=False) file_path_full = os.path.join(SOITOOL_ROOT_PATH, file_name) # Assert file exists self.assertTrue(os.path.exists(file_path_full)) diff --git a/test/test_database.py b/test/test_database.py index 65f7cbbeefa25986503a0d4449cfbff9676860c4..5f020b5e963d622c02d68b6088a9cf1673035257 100644 --- a/test/test_database.py +++ b/test/test_database.py @@ -3,6 +3,8 @@ import os from pathlib import Path import unittest import json +from time import sleep +from datetime import datetime from soitool.database import Database from soitool.coder import get_code_length_needed @@ -105,6 +107,34 @@ class DatabaseTest(unittest.TestCase): self.assertEqual(entry["type"], actual[i][2]) self.assertRegex(actual[i][3], "[A-Z]{" + str(code_len) + "}") + def test_last_updated(self): + """Assert table LastUpdated is filled when db is created.""" + stmt = "SELECT Timestamp FROM LastUpdated" + # [:19] to skip microseconds + last_update = self.database.conn.execute(stmt).fetchall()[0][0][:19] + # Using raw string to not make backslash escape character + self.assertRegex( + last_update, r"(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})" + ) + + def test_update_last_updated(self): + """Assert that the time gets updated when running method.""" + # Get two datetimes that should only differ by sleep_time + stmt = "SELECT Timestamp FROM LastUpdated" + self.database.update_last_updated() + first_time = self.database.conn.execute(stmt).fetchall()[0][0] + sleep_time = 2 + sleep(sleep_time) + self.database.update_last_updated() + second_time = self.database.conn.execute(stmt).fetchall()[0][0] + # Converts from string to datetime object + first_time = datetime.strptime(first_time, "%Y-%m-%d %H:%M:%S.%f") + second_time = datetime.strptime(second_time, "%Y-%m-%d %H:%M:%S.%f") + # Calculates difference in seconds + time_diff = (second_time - first_time).total_seconds() + # Compares difference with sleep_time + self.assertAlmostEqual(time_diff, sleep_time, delta=0.2) + def test_get_categories(self): """Assert function get_categories works as expected.""" file_path = os.path.join(TESTDATA_PATH, "CategoryWords.txt") @@ -175,18 +205,17 @@ class DatabaseTest(unittest.TestCase): def test_update_codebook(self): """Test that the codes get updated.""" - # Get number of entries - stmt = "SELECT COUNT(*) FROM CodeBook ORDER BY Word" - number_of_entries = self.database.conn.execute(stmt).fetchall()[0][0] - # Get old and updated word-code combinations - stmt = "SELECT Word, Code FROM CodeBook" - old = self.database.conn.execute(stmt).fetchall() + # Get entries before and after update + stmt = "SELECT Word, Code FROM CodeBook ORDER BY Word" + old_entries = self.database.conn.execute(stmt).fetchall() self.database.update_codebook() - updated = self.database.conn.execute(stmt).fetchall() - # Collect approximately score of not updated pairs + new_entries = self.database.conn.execute(stmt).fetchall() + # Collect approximately score of not updated pairs since there is a + # chance for a word to get the same code again pairs = 0 + number_of_entries = len(old_entries) for i in range(0, number_of_entries, 2): - if old[i]["Code"] == updated[i]["Code"]: + if old_entries[i]["Code"] == new_entries[i]["Code"]: pairs = pairs + 1 # Test that at least some of the test are new self.assertTrue(pairs < number_of_entries) @@ -195,11 +224,11 @@ class DatabaseTest(unittest.TestCase): """Test code length gets extended when number of entries makes it.""" three_letter_len = 26 ** 2 stmt_count = "SELECT COUNT(*) FROM CodeBook" - numb_of_entries = self.database.conn.execute(stmt_count).fetchall()[0][ + number_of_entries = self.database.conn.execute(stmt_count).fetchall()[ 0 - ] + ][0] # Codes only gets extended when number of entries pass 26**x - if numb_of_entries == (three_letter_len): + if number_of_entries == (three_letter_len): # Get length of current codes stmt_code = "SELECT Code FROM CodeBook" code_len = len( @@ -211,20 +240,42 @@ class DatabaseTest(unittest.TestCase): self.database.conn.execute(stmt, ("676", "676", None)) self.database.update_codebook() # Check that entry got inserted - numb_of_entries = self.database.conn.execute( + number_of_entries = self.database.conn.execute( stmt_count ).fetchall()[0][0] - self.assertEqual(numb_of_entries, three_letter_len + 1) + self.assertEqual(number_of_entries, three_letter_len + 1) # Test that codes got extended length code_len = len( self.database.conn.execute(stmt_code).fetchall()[0][0] ) - self.assertEqual(code_len, get_code_length_needed(numb_of_entries)) + self.assertEqual( + code_len, get_code_length_needed(number_of_entries) + ) else: print("ERROR: Database is not 675 entries long, cant run test") self.assertTrue(False) # pylint: disable=W1503 + def test_seconds_to_next_update(self): + """Compares (24h - time slept) and the function return value.""" + seconds_in_24h = 24 * 60 * 60 + # Insert current time LastUpdated table in db + self.database.update_last_updated() + # Sleeps to make time difference + sleep_time = 2 + sleep(sleep_time) + # Calculates expected return value and gets return value + expected_time = seconds_in_24h - sleep_time + actual_time = self.database.seconds_to_next_update(seconds_in_24h) + # Compares expected and function return value with + self.assertAlmostEqual(expected_time, actual_time, delta=0.2) + + def teset_seconds_to_next_update_complete_period(self): + """Check that seconds to next update can returns 0 and not negative.""" + self.database.update_last_updated() + sleep(2) + self.assertEqual(0, self.database.seconds_to_next_update(1)) + def test_add_code(self): """Test add a single code to CodeBook.""" testdata = ("Testword", "Testcategory") diff --git a/test/test_main.py b/test/test_main.py index 3255793a528443b4115c6ffccb8e029aeec7f9ab..3b116e47582079a8925993f2f5595a87f5b72448 100644 --- a/test/test_main.py +++ b/test/test_main.py @@ -51,8 +51,7 @@ class TestMain(unittest.TestCase): def test_starts_up(self): """Test at widget kan starte opp.""" self.assertEqual( - self.widget.qlabel.text(), - self.test_text1, + self.widget.qlabel.text(), self.test_text1, ) self.assertTrue(self.widget.isVisible()) @@ -65,9 +64,7 @@ class TestMain(unittest.TestCase): # child_line_edit (a child of the active widget) active_widget = app.activeModalWidget() - child_line_edit = active_widget.findChild( - QtWidgets.QLineEdit - ) + child_line_edit = active_widget.findChild(QtWidgets.QLineEdit) QtTest.QTest.keyClicks(child_line_edit, self.test_text2) QtTest.QTest.keyClick(child_line_edit, QtCore.Qt.Key_Enter) @@ -76,8 +73,7 @@ class TestMain(unittest.TestCase): QtTest.QTest.mouseClick(self.widget.button, QtCore.Qt.LeftButton) self.assertEqual( - self.widget.qlabel.text(), - self.test_text2, + self.widget.qlabel.text(), self.test_text2, ) def test_change_text_not_ok(self): @@ -102,10 +98,9 @@ class TestMain(unittest.TestCase): QtTest.QTest.mouseClick(self.widget.button, QtCore.Qt.LeftButton) self.assertNotEqual( - self.widget.qlabel.text(), - self.test_text2, + self.widget.qlabel.text(), self.test_text2, ) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/test/test_main_window.py b/test/test_main_window.py index 5e6290c7ac066f54a4cf05b4192380b956f4393f..894f10cfe69f9b803ad1bb558db9d559f5ed1de8 100644 --- a/test/test_main_window.py +++ b/test/test_main_window.py @@ -31,5 +31,5 @@ class TestMainWindow(unittest.TestCase): self.assertEqual(expected, actual) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main()