diff --git a/.gitignore b/.gitignore index 2d721184ba24d7d5e8af9206e9699fe0418a59a4..673ab0bc892bc30d1c485353c1b600f7d565bbd8 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ __pycache__/ # Compiled bytecode of Python source files *.pyc + +# Generated codebook PDF-files +Kodebok_*.pdf diff --git a/scripts/.pylintrc b/scripts/.pylintrc index 787b3013b41d48b769d30826589e6cf341697902..cfd3fa7abc0a0fd319049a5cffaff8331b20cce6 100644 --- a/scripts/.pylintrc +++ b/scripts/.pylintrc @@ -68,6 +68,7 @@ confidence= # page: https://github.com/psf/black/issues/48, and this issue on pylints # GitHub page referenced by the first issue: # https://github.com/PyCQA/pylint/issues/289 + disable=print-statement, parameter-unpacking, unpacking-in-except, @@ -423,8 +424,7 @@ good-names=app, Run, f, _, - x, - y + db # Include a hint for the correct naming format with invalid-name. include-naming-hint=no diff --git a/soitool/codebook.py b/soitool/codebook.py new file mode 100644 index 0000000000000000000000000000000000000000..b1a5f3cc0f9cabfac71cc59bc8f62abf120d29c5 --- /dev/null +++ b/soitool/codebook.py @@ -0,0 +1,152 @@ +"""Codebook. + +Contains functionality for viewing, editing and inserting new rows +in the database-table 'CodeBook'. +""" +from PySide2.QtWidgets import QTableView +from PySide2.QtSql import QSqlDatabase, QSqlTableModel +from PySide2.QtCore import Qt +from soitool.database import DBPATH # , Database +from soitool.style import CODEBOOK_HEADER_FONT, CODEBOOK_HEADER_BACKGROUND_CSS + +# Name and type of database +CONNAME = "SOIDB" +DBTYPE = "QSQLITE" + + +class CodeBookTableView(QTableView): + """TableView with a model of the 'codebook'-table from database. + + This modified QTableView creates a CodeBookTableModel, + which reads the codebook-table. + User can add, edit and insert entries to the database through this View. + + Raises RuntimeError if database does not open. + """ + + def __init__(self): + super().__init__() + db = QSqlDatabase.addDatabase(DBTYPE, CONNAME) + db.setDatabaseName(DBPATH) + + if not db.open(): + raise RuntimeError("Could not open database.") + + # Enable sorting and sort by column 'Word' + self.setSortingEnabled(True) + self.verticalHeader().hide() + + # Create and set model: + model = CodeBookTableModel() + self.setModel(model) + + self.setEditTriggers(self.DoubleClicked) + + self.set_horizontal_header_text() + + # Set style of horizontal header + header = self.horizontalHeader() + header.setFont(CODEBOOK_HEADER_FONT) + header.setStyleSheet(CODEBOOK_HEADER_BACKGROUND_CSS) + + self.setFixedWidth(600) + + self.resizeColumnsToContents() + + def set_horizontal_header_text(self): + """Set Norwegian names in horizontal header.""" + self.model().setHeaderData(0, Qt.Horizontal, "Ord/Uttrykk") + self.model().setHeaderData(1, Qt.Horizontal, "Kategori") + self.model().setHeaderData(2, Qt.Horizontal, "Type") + self.model().setHeaderData(3, Qt.Horizontal, "Kode") + + def close_db_connection(self): + """Close database-connection.""" + self.setModel(None) + QSqlDatabase.removeDatabase(CONNAME) + + def dataChanged(self, top_left, top_right, roles): + """Resize column-width to contents when data changes. + + All parameters are sent to super. + """ + super().dataChanged(top_left, top_right, roles) + + self.resizeColumnsToContents() + + def setModel(self, model): + """Resize column-width to contents when a model is set. + + Parameter is sent to super. + """ + super().setModel(model) + if model is not None: + self.set_horizontal_header_text() + self.resizeColumnsToContents() + + +class CodeBookTableModel(QSqlTableModel): + """Editable QSqlTableModel of database-table 'CodeBook'. + + This modified QSqlTableModel writes to database + when a value is changed by the view. + + The last row is used by the view to insert a new database record, + therefore, only column 'word' (Primary key) is editable in this row. + All columns except 'Code' are editable. + + This class initializes by connecting to database and selecting table. + """ + + def __init__(self): + super().__init__(None, QSqlDatabase.database(CONNAME)) + self.setEditStrategy(QSqlTableModel.OnFieldChange) + self.setTable("CodeBook") + self.setSort(0, Qt.AscendingOrder) + self.select() + + def flags(self, index): + """Add or remove correct flags for items. + + Set items in columns Word, Category & Type editable. + Disable items in column Code, these are not editable. + + Parameters + ---------- + index : QModelIndex + Is used to locate data in a data model. + + Returns + ------- + ItemFlag + Describes the properties of an item. + """ + flags = super().flags(index) + + # Disable items in column 'Code' + if index.column() == 3: + flags ^= Qt.ItemIsEnabled + # Enable editing on items in columns 'Word', 'Category' and 'Type' + else: + flags |= Qt.ItemIsEditable + + return flags + + def setData(self, index, value, role): + """Validate correct values in column 'Type'. + + Allowed values for column 'Type' are 'Liten' and 'Stor'. + This function ensures value is correctly cased. + Returns False if incorrect Type value, returns super otherwise. + """ + # If a change is made to column 'Type' + if index.column() == 2: + # If correct string, replace with correctly cased str + if value.lower() == "liten": + value = "Liten" + elif value.lower() == "stor": + value = "Stor" + else: + return False + + return super().setData(index, value, role) diff --git a/soitool/codebook_row_adder.py b/soitool/codebook_row_adder.py new file mode 100644 index 0000000000000000000000000000000000000000..39cc45fe93d85ee91aa93011579d2a286321d274 --- /dev/null +++ b/soitool/codebook_row_adder.py @@ -0,0 +1,202 @@ +"""Includes functionality for inserting rows into database-table CodeBook.""" +from sqlite3 import IntegrityError +from PySide2.QtWidgets import ( + QHBoxLayout, + QVBoxLayout, + QLabel, + QLineEdit, + QComboBox, + QPushButton, + QWidget, +) +from PySide2.QtCore import Qt +from soitool.database import Database +from soitool.codebook import CodeBookTableView, CodeBookTableModel + + +class CodebookRowAdder(QWidget): + """Widget for adding rows to database-table CodeBook. + + This class can be used alone, or alongside a soitool.CodeBookTableView. + If used with a CodeBookTableView, this class will tell the view to + update/refresh when new rows are inserted. + + Parameters + ---------- + codebook_view : soitool.CodeBookTableView or None (default) + Reference to view if it is used alongside this class, None otherwise. + The view's model (CodeBookTableModel) has an exclusive lock on the + database. Therefore, this reference is needed to be able to + insert new rows, but it is also used to refresh the view when + the row is inserted. + + Raises + ------ + ValueError + If argument codebook_view is not + None (default) or CodeBookTableView. + """ + + def __init__(self, codebook_view=None): + super().__init__() + + # Raise error if argument is invalid + if codebook_view is not None and not isinstance( + codebook_view, CodeBookTableView + ): + raise ValueError( + "Invalid value for argument 'codebook_view': " + + "'{}'".format(codebook_view) + ) + + self.codebook_view = codebook_view + + self.create_widgets() + self.set_widget_widths() + self.create_and_set_layout() + self.set_size() + + def create_widgets(self): + """Create labels, input-widgets and button needed to insert rows.""" + # Create labels + self.label_word = QLabel("Ord/Uttrykk") + self.label_category = QLabel("Kategori") + self.label_type = QLabel("Type") + self.label_feedback = QLabel("") + + # Create input-widgets + self.text_field_category = QLineEdit() + self.text_field_word = QLineEdit() + self.combo_type = QComboBox() + self.combo_type.addItem("Stor") + self.combo_type.addItem("Liten") + + # Create button + self.button = QPushButton("Legg til", self) + self.button.setFixedHeight(22) + self.button.clicked.connect(self.insert_row) + + def set_widget_widths(self): + """Set width of widgets.""" + # Match width of widgets to columnwidths of the view if it is used + if self.codebook_view is not None: + self.text_field_word.setFixedWidth( + self.codebook_view.columnWidth(0) - 12 + ) + self.text_field_category.setFixedWidth( + self.codebook_view.columnWidth(1) - 10 + ) + self.combo_type.setFixedWidth( + self.codebook_view.columnWidth(2) - 5 + ) + self.button.setFixedWidth(self.codebook_view.columnWidth(3) - 5) + # Set standard witdh of widgets if a view is not used + else: + self.text_field_word.setFixedWidth(140) + self.text_field_category.setFixedWidth(120) + self.combo_type.setFixedWidth(80) + self.button.setFixedWidth(80) + + def create_and_set_layout(self): + """Create layouts, add them to main layout and set main layout.""" + # Create layout for word + vbox_word = QVBoxLayout() + vbox_word.addWidget(self.label_word) + vbox_word.addWidget(self.text_field_word) + vbox_word.setAlignment(self.label_word, Qt.AlignHCenter) + + # Create layout for category + vbox_category = QVBoxLayout() + vbox_category.addWidget(self.label_category) + vbox_category.addWidget(self.text_field_category) + vbox_category.setAlignment(self.label_category, Qt.AlignHCenter) + + # Create layout for type + vbox_type = QVBoxLayout() + vbox_type.addWidget(self.label_type, Qt.AlignHCenter) + vbox_type.addWidget(self.combo_type) + vbox_type.setAlignment(self.label_type, Qt.AlignHCenter) + + # Create layout for button + vbox_button = QVBoxLayout() + vbox_button.addSpacing(25) + vbox_button.addWidget(self.button) + + # Create layout to add layouts next to eachother + hbox = QHBoxLayout() + hbox.addLayout(vbox_word) + hbox.addLayout(vbox_category) + hbox.addLayout(vbox_type) + hbox.addLayout(vbox_button) + + # Create layout to place feedback at the bottom + vbox = QVBoxLayout() + vbox.addLayout(hbox) + vbox.addWidget(self.label_feedback) + + self.setLayout(vbox) + + def set_size(self): + """Set standard size or size based on the view's size.""" + # Set own width equal to the sum of the view's columnwidths + if self.codebook_view is not None: + codebook_view_width = 0 + for i in range(self.codebook_view.model().columnCount()): + codebook_view_width += self.codebook_view.columnWidth(i) + self.setFixedSize(codebook_view_width, 80) + # Set standard size + else: + self.setFixedSize(465, 80) + + def insert_row(self): + """Read input and insert row to database. + + Give feedback if word (Primary key) is empty or already exists. + """ + # Reset feedback-label + self.label_feedback.setText("") + + # Read input and uppercase first character of word and category + 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: + + db = Database() + + try: + # If a view is used, remove its model temporarily + if self.codebook_view is not None: + self.codebook_view.setModel(None) + + # Try to add row to database + stmt = ( + "INSERT INTO CodeBook(Word, Category, Type)" + + "VALUES(?, ?, ?)" + ) + db.conn.execute( + stmt, (word_input, category_input, type_input,) + ) + + # Add unique code to row and commit changes + db.add_code_to(word_input) + db.conn.commit() + + # Give feedback and reset input + self.label_feedback.setText("Ord/Uttrykk lagt til.") + self.text_field_word.setText("") + self.text_field_category.setText("") + self.combo_type.setCurrentIndex(0) + + except IntegrityError: + self.label_feedback.setText("Ord/uttrykk eksisterer allerede.") + else: + self.label_feedback.setText("Ord/Uttrykk er ikke fylt inn.") + + # If a view is used, create and set a new, updated model + if self.codebook_view is not None: + self.codebook_view.setModel(CodeBookTableModel()) diff --git a/soitool/codebook_to_pdf.py b/soitool/codebook_to_pdf.py new file mode 100644 index 0000000000000000000000000000000000000000..4001e9037bd090723ad31a26c811a5192431faaf --- /dev/null +++ b/soitool/codebook_to_pdf.py @@ -0,0 +1,181 @@ +"""Contains functionality for generating codebook as PDF.""" + +from datetime import datetime +from enum import Enum +from reportlab.pdfgen import canvas +from reportlab.platypus import Table, Paragraph, TableStyle, \ + SimpleDocTemplate, Spacer +from reportlab.lib.styles import ParagraphStyle +from reportlab.lib import colors +from reportlab.lib.pagesizes import A4, portrait +from reportlab.lib.units import cm +from soitool.database import Database + +A4_WIDTH = A4[0] +TITLE_FULL = '<u>Kodebok</u>' +TITLE_SMALL = "<u>Liten Kodebok</u>" +HEADERS = ["Ord/Uttrykk", "Kategori", "Type", "Kode"] +HEADER_BG_COLOR = colors.HexColor("#a6a6a6") + +TITLE_STYLE = ParagraphStyle( + name='Title', + fontName='Helvetica', + fontSize=20, + alignment=1, + underlineWidth=1.5 +) + +PAGE_NUMBER_FONT = "Helvetica" + +TABLE_STYLE = TableStyle([ + ('FONTSIZE', (0, 0), (3, 0), 16), # Header-font + ('BOTTOMPADDING', (0, 0), (3, 0), 10), # Header-padding bottom + ("BACKGROUND", (0, 0), (3, 0), HEADER_BG_COLOR), # Header background-color + ("ALIGN", (0, 0), (3, 0), "CENTER"), # Header-text centered + ('GRID', (0, 0), (-1, -1), 1, colors.black), # Border around cells +]) + + +class CodebookSize(Enum): + """Enumerate with possible codebook-sizes.""" + + FULL = 0 + SMALL = 1 + + +def generate_codebook_pdf(codebook_size=CodebookSize.FULL, + page_size=A4, orientation=portrait): + """Generate PDF with data from database-table 'CodeBook'. + + Parameters + ---------- + codebook_size : int, optional + 0 or 1 (enumerate value from 'CodebookSize') + Data is from full codebook if 0 (default), from small codebook if 1. + page_size : reportlab.lib.pagesizes, optional + Size of each page, by default A4 + orientation : reportlab.lib.pagesizes portrait or landscape + Paper orientation, by default portrait + """ + # Set title/headline + if codebook_size == CodebookSize.FULL: + title = Paragraph(TITLE_FULL, TITLE_STYLE) + else: + title = Paragraph(TITLE_SMALL, TITLE_STYLE) + + # Get data from database + data = get_codebook_data(codebook_size) + + # Create Table with data and predefined style + table = Table(data, repeatRows=1) + table.setStyle(TABLE_STYLE) + + # Add title, vertical spacer and table to element-list + elements = [] + elements.append(title) + elements.append(Spacer(1, 1 * cm)) + elements.append(table) + + # Generate filename + file_name = generate_filename(codebook_size) + + # Create document, add elements and save as PDF + doc = SimpleDocTemplate(file_name, pagesize=orientation(page_size), + topMargin=30) + doc.build(elements, canvasmaker=PageNumCanvas) + + +def generate_filename(codebook_size): + """Generate filename with current date for PDF. + + Parameters + ---------- + codebook_size : int + 0 or 1 (enumerate value from 'CodebookSize') + Filename will contain 'Kodebok' if 0, 'Kodebok liten' if 1. + + Returns + ------- + String + Filename for PDF. + 'Kodebok_YYYY_mm_dd.pdf' or 'Kodebok_liten_YYYY_mm_dd.pdf' + """ + # Get date on format YYYY_mm_dd + today = datetime.now().strftime("%Y_%m_%d") + + if codebook_size == CodebookSize.FULL: + return f"Kodebok_{today}.pdf" + + return f"Kodebok_liten_{today}.pdf" + + +def get_codebook_data(codebook_size=CodebookSize.FULL): + """Read codebook-data from database and change datatype to 2D list. + + Parameters + ---------- + codebook_size : int + 0 or 1 (enumerate value from 'CodebookSize') + Retrieves full codebook data if 0, small codebook data if 1. + + Returns + ------- + 2D List of strings + Data from codebook. + """ + db = Database() + + # Get data from CodeBook-table + if codebook_size == CodebookSize.FULL: + db_data = db.get_codebook(small=False) + else: + db_data = db.get_codebook(small=True) + + data = [] + # Add column-headers + data.append([HEADERS[0], HEADERS[1], HEADERS[2], HEADERS[3]]) + + # Add row data + for row in db_data: + data.append([row["word"], row["category"], row["type"], row["code"]]) + + return data + + +# pylint: disable=W0223 +# Disabling pylint-warning 'Method is abstract in class but is not overridden'. +# The methods are 'inkAnnotation' and 'inkAnnotation0', and they are supposed +# to add PDF-annotations. PDF-annotations enable PDF-editing such as forms, +# text highlighting etc, and are not needed in the generated codebook-PDF. +class PageNumCanvas(canvas.Canvas): + """Subclassed Canvas for adding 'page x of y' at bottom of all pages. + + Modified version of: + http://www.blog.pythonlibrary.org/2013/08/12/reportlab-how-to-add-page-numbers/ + """ + + def __init__(self, *args, **kwargs): + canvas.Canvas.__init__(self, *args, **kwargs) + self.pages = [] + + def showPage(self): + """On a page break, add page data.""" + self.pages.append(dict(self.__dict__)) + self._startPage() + + def save(self): + """Add the page number to pages (page x of y) before saving.""" + page_count = len(self.pages) + + for page in self.pages: + self.__dict__.update(page) + self.draw_page_number(page_count) + canvas.Canvas.showPage(self) + + canvas.Canvas.save(self) + + def draw_page_number(self, page_count): + """Add the page number.""" + page = "Side %s av %s" % (self._pageNumber, page_count) + self.setFont(PAGE_NUMBER_FONT, 10) + self.drawString(A4_WIDTH / 2 - 20, 25, page) diff --git a/soitool/database.py b/soitool/database.py index dcd2bf54bebaf60b8ef2194bef7e8cbc27fb4cef..6e998717b7fce6540b5acc5536f9b2e844156cca 100644 --- a/soitool/database.py +++ b/soitool/database.py @@ -10,18 +10,17 @@ CURDIR = os.path.dirname(__file__) DBPATH = os.path.join(CURDIR, DBNAME) # DDL-statements for creating tables -CODEBOOK = ( - "CREATE TABLE CodeBook" - "(Word VARCHAR PRIMARY KEY, Category VARCHAR," - "Code VARCHAR UNIQUE, Type int DEFAULT 0)" -) -CATEGORYWORDS = ( - "CREATE TABLE CategoryWords" "(Word VARCHAR PRIMARY KEY, Category VARCHAR)" -) +CODEBOOK = "CREATE TABLE CodeBook" \ + "(Word VARCHAR PRIMARY KEY NOT NULL CHECK(length(Word) > 0), " \ + "Category VARCHAR, " \ + "Type VARCHAR DEFAULT 'Stor' CHECK(Type='Stor' OR Type='Liten'), " \ + "Code VARCHAR UNIQUE)" +CATEGORYWORDS = "CREATE TABLE CategoryWords" \ + "(Word VARCHAR PRIMARY KEY, Category VARCHAR)" BYHEART = "CREATE TABLE ByHeart(Word VARCHAR PRIMARY KEY)" -class Database: +class Database(): """ Holds database-connection and related functions. @@ -40,6 +39,7 @@ class Database: else: print("Creating new DB.") self.conn = sqlite3.connect(DBPATH) + self.create_tables() print("DB created.") self.fill_tables() @@ -66,10 +66,11 @@ class Database: def fill_codebook(self): """Read data from long_codebook.json and fill DB-table CodeBook.""" file_path = os.path.join(CURDIR, "testdata/long_codebook.json") + # Load json as dict - with open(file_path, "r") as file: - entries = json.load(file) - file.close() + f = open(file_path, "r", encoding="utf-8") + entries = json.load(f) + f.close() # Generate codes code_len = soitool.coder.get_code_length_needed(len(entries)) diff --git a/soitool/main_window.py b/soitool/main_window.py index 275b1e903d801526c709fda2e34788a977dd6fdb..11e6510bd19d0da697811af4abd3c2298fca9c8b 100644 --- a/soitool/main_window.py +++ b/soitool/main_window.py @@ -6,9 +6,15 @@ Built up by widgets implemented in other modules. import sys import os from enum import Enum -from PySide2.QtWidgets import QMainWindow, QApplication, QTabWidget, QAction +from functools import partial +from PySide2.QtWidgets import QTabWidget, QWidget, QMainWindow, \ + QApplication, QHBoxLayout, QVBoxLayout, QAction from PySide2.QtGui import QIcon +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, CodebookSize +from soitool.database import Database, DBPATH class ModuleType(Enum): @@ -30,9 +36,8 @@ class MainWindow(QMainWindow): # flytt ut til egen funksjon, for setup av menubar menu = self.menuBar() file_menu = menu.addMenu("SOI") - codebook = menu.addMenu("Kodebok") + codebook_menu = menu.addMenu("Kodebok") help_menu = menu.addMenu("Hjelp") - # Hadde egentlig help her, men "klagegutten" klagde på det # New SOI new_soi = QAction("Ny SOI", self) @@ -71,15 +76,33 @@ class MainWindow(QMainWindow): export.setStatusTip("Eksporter SOI til annet filformat") file_menu.addAction(export) - # Small codebook - small_codebook = QAction("Liten kodebok", self) - small_codebook.setStatusTip("Vis liten kodebok") - codebook.addAction(small_codebook) - - # Big codebook - big_codebook = QAction("Stor kodebok", self) - big_codebook.setStatusTip("Vis stor kodebok") - codebook.addAction(big_codebook) + # View/edit Codebook + codebook = QAction("Se/rediger kodebok", self) + codebook.setStatusTip("Se/rediger kodebok") + codebook.triggered.connect(self.open_codebook_tab) + codebook_menu.addAction(codebook) + + # Regenerate codebook-codes: + regenerate_codes = QAction("Nye koder i db", self) + regenerate_codes.setStatusTip("Nye koder lages tilfeldig") + codebook_menu.addAction(regenerate_codes) + + # Export codebook as PDF + export_codebook = codebook_menu.addMenu("Eksporter") + + # 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, + CodebookSize.FULL)) + export_codebook.addAction(export_codebook_full) + + # Export small codebook + export_codebook_small = QAction("Liten kodebok", self) + export_codebook_small.setStatusTip("Eksporter liten kodebok som PDF") + export_codebook_small.triggered.connect(partial(generate_codebook_pdf, + CodebookSize.SMALL)) + export_codebook.addAction(export_codebook_small) # Hot keys hotkeys = QAction("Hurtigtaster", self) @@ -91,10 +114,11 @@ class MainWindow(QMainWindow): easy_use.setStatusTip("Vis enkel bruk av programvaren") help_menu.addAction(easy_use) - # Legger til MainWidget som en tab, kanskje flytt ut til egen funksjon + # Add SOIWorkspaceWidget-tab self.tabs = QTabWidget() - tab = SOIWorkspaceWidget() + self.tabs.setTabsClosable(True) + tab = SOIWorkspaceWidget() self.tabs.addTab(tab, "MainTab") self.setCentralWidget(self.tabs) @@ -104,8 +128,43 @@ class MainWindow(QMainWindow): filepath = os.path.join(dirname, filename) self.setWindowIcon(QIcon(filepath)) + def open_codebook_tab(self): + """Open tab containing CodeBookTableView and CodebookRowAdder. + + Select codebook-tab if it is already open, + create and select codebook-tab if not open. + """ + # Loop through tabs to look for existing codebook-tab: + for i in range(self.tabs.count()): + if self.tabs.tabText(i) == "Kodebok": + self.tabs.setCurrentIndex(i) + break + # Codebook-tab does not exist, create, add and select tab + else: + # Create widgets + tab = QWidget() + view = CodeBookTableView() + row_adder = CodebookRowAdder(view) + + # Add widgets to layouts + vbox = QVBoxLayout() + vbox.addWidget(row_adder) + vbox.addWidget(view) + hbox = QHBoxLayout() + hbox.addLayout(vbox) + + # Set layout, add tab and select tab + tab.setLayout(hbox) + self.tabs.addTab(tab, 'Kodebok') + 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/style.py b/soitool/style.py new file mode 100644 index 0000000000000000000000000000000000000000..61195a2c2fb1253a52be992f3ad6b0fcfc1c6475 --- /dev/null +++ b/soitool/style.py @@ -0,0 +1,12 @@ +"""Fonts and CSS used.""" + +from PySide2.QtGui import QFont + +# Font and background-color for horizontal header in codebook-view. +CODEBOOK_HEADER_FONT = QFont() +CODEBOOK_HEADER_FONT.setFamily("Arial") +CODEBOOK_HEADER_FONT.setPointSize(14) +CODEBOOK_HEADER_FONT.setWeight(50) +CODEBOOK_HEADER_BACKGROUND_CSS = ( + "QHeaderView::section " "{background-color:rgb(240, 240, 240)}" +) diff --git a/soitool/testdata/codebook.json b/soitool/testdata/codebook.json index c325ceeac786b13135828845d89adab4b8fefb38..053026acbd3c92ceca7f68f30dfcd0d2d7a1ac32 100644 --- a/soitool/testdata/codebook.json +++ b/soitool/testdata/codebook.json @@ -2,221 +2,221 @@ { "word": "Vann (l)", "category": "Etterforsyninger", - "type": 0 + "type": "Stor" }, { "word": "Meldingsblankett (blokker)", "category": "Etterforsyninger", - "type": 0 + "type": "Stor" }, { "word": "PACE-batteri/BA-3090 (stk)", "category": "Etterforsyninger", - "type": 0 + "type": "Stor" }, { "word": "Rødsprit (l)", "category": "Etterforsyninger", - "type": 0 + "type": "Stor" }, { "word": "Proviant (DOS)", "category": "Etterforsyninger", - "type": 0 + "type": "Stor" }, { "word": "Kryss", "category": "Landemerker", - "type": 0 + "type": "Stor" }, { "word": "Sti", "category": "Landemerker", - "type": 0 + "type": "Stor" }, { "word": "Veg", "category": "Landemerker", - "type": 0 + "type": "Stor" }, { "word": "Høyde", "category": "Landemerker", - "type": 0 + "type": "Stor" }, { "word": "Rute", "category": "Landemerker", - "type": 0 + "type": "Stor" }, { "word": "Elv", "category": "Landemerker", - "type": 0 + "type": "Stor" }, { "word": "Dal", "category": "Landemerker", - "type": 0 + "type": "Stor" }, { "word": "Bro", "category": "Landemerker", - "type": 0 + "type": "Stor" }, { "word": "Lysrakett", "category": "Våpenteknisk", - "type": 0 + "type": "Stor" }, { "word": "40 mm", "category": "Våpenteknisk", - "type": 0 + "type": "Stor" }, { "word": "Håndgranat", "category": "Våpenteknisk", - "type": 0 + "type": "Stor" }, { "word": "P-80", "category": "Våpenteknisk", - "type": 0 + "type": "Stor" }, { "word": "Bombe", "category": "Våpenteknisk", - "type": 0 + "type": "Stor" }, { "word": "Ammo", "category": "Våpenteknisk", - "type": 0 + "type": "Stor" }, { "word": "Signalpistol", "category": "Våpenteknisk", - "type": 0 + "type": "Stor" }, { "word": "ERYX", "category": "Våpenteknisk", - "type": 0 + "type": "Stor" }, { "word": "MP-5", "category": "Våpenteknisk", - "type": 1 + "type": "Liten" }, { "word": "HK-416", "category": "Våpenteknisk", - "type": 0 + "type": "Stor" }, { "word": "Går av nett", "category": "Uttrykk/tiltak/oppdrag", - "type": 0 + "type": "Stor" }, { "word": "Er i rute", "category": "Uttrykk/tiltak/oppdrag", - "type": 0 + "type": "Stor" }, { "word": "Stans, -e, -et", "category": "Uttrykk/tiltak/oppdrag", - "type": 0 + "type": "Stor" }, { "word": "Leopard 2", "category": "Kjøretøy", - "type": 0 + "type": "Stor" }, { "word": "BV-206", "category": "Kjøretøy", - "type": 0 + "type": "Stor" }, { "word": "CV 90", "category": "Kjøretøy", - "type": 0 + "type": "Stor" }, { "word": "M109", "category": "Kjøretøy", - "type": 0 + "type": "Stor" }, { "word": "M04", "category": "Bekledning", - "type": 0 + "type": "Stor" }, { "word": "Regntøy", "category": "Bekledning", - "type": 0 + "type": "Stor" }, { "word": "Vernemaske", "category": "Personlig utrustning", - "type": 0 + "type": "Stor" }, { "word": "Vest plate", "category": "Personnlig utrustning", - "type": 0 + "type": "Stor" }, { "word": "AG3", "category": "Våpenteknisk", - "type": 0 + "type": "Stor" }, { "word": "Rekylfri kanon", "category": "Våpenteknisk", - "type": 0 + "type": "Stor" }, { "word": "MB Multi", "category": "Kjøretøy", - "type": 0 + "type": "Stor" }, { "word": "MG-3", "category": "Våpenteknisk", - "type": 0 + "type": "Stor" }, { "word": "M2 Browning", "category": "Våpenteknisk", - "type": 0 + "type": "Stor" }, { "word": "M77", "category": "Personlig utrustning", - "type": 0 + "type": "Stor" }, { "word": "Netting under over", "category": "Personlig utrustning", - "type": 0 + "type": "Stor" }, { "word": "Underbukse ull kort lang", "category": "Personlig utrustning", - "type": 0 + "type": "Stor" }, { "word": "Kompass", "category": "Personlig utrustning", - "type": 0 + "type": "Stor" }, { "word": "Ferdigstilling", "category": "Straff", - "type": 0 + "type": "Stor" } ] \ No newline at end of file diff --git a/soitool/testdata/long_codebook.json b/soitool/testdata/long_codebook.json index d2832208dfb770e9f951d1fe4d6168e4ead19cc4..48b0a3abfc2e81e9fad00347574964a04da2592e 100644 --- a/soitool/testdata/long_codebook.json +++ b/soitool/testdata/long_codebook.json @@ -2,3381 +2,3381 @@ { "word": "Vann (l)", "category": "Etterforsyninger", - "type": 0 + "type": "Stor" }, { "word": "Meldingsblankett (blokker)", "category": "Etterforsyninger", - "type": 0 + "type": "Stor" }, { "word": "PACE-batteri/BA-3090 (stk)", "category": "Etterforsyninger", - "type": 0 + "type": "Stor" }, { "word": "R\u00f8dsprit (l)", "category": "Etterforsyninger", - "type": 0 + "type": "Stor" }, { "word": "Proviant (DOS)", "category": "Etterforsyninger", - "type": 0 + "type": "Stor" }, { "word": "Kryss", "category": "Landemerker", - "type": 0 + "type": "Stor" }, { "word": "Sti", "category": "Landemerker", - "type": 0 + "type": "Stor" }, { "word": "Veg", "category": "Landemerker", - "type": 0 + "type": "Stor" }, { "word": "H\u00f8yde", "category": "Landemerker", - "type": 0 + "type": "Stor" }, { "word": "Rute", "category": "Landemerker", - "type": 0 + "type": "Stor" }, { "word": "Elv", "category": "Landemerker", - "type": 0 + "type": "Stor" }, { "word": "Dal", "category": "Landemerker", - "type": 0 + "type": "Stor" }, { "word": "Bro", "category": "Landemerker", - "type": 0 + "type": "Stor" }, { "word": "Lysrakett", "category": "V\u00e5penteknisk", - "type": 0 + "type": "Stor" }, { "word": "40 mm", "category": "V\u00e5penteknisk", - "type": 0 + "type": "Stor" }, { "word": "H\u00e5ndgranat", "category": "V\u00e5penteknisk", - "type": 0 + "type": "Stor" }, { "word": "P-80", "category": "V\u00e5penteknisk", - "type": 0 + "type": "Stor" }, { "word": "Bombe", "category": "V\u00e5penteknisk", - "type": 0 + "type": "Stor" }, { "word": "Ammo", "category": "V\u00e5penteknisk", - "type": 0 + "type": "Stor" }, { "word": "Signalpistol", "category": "V\u00e5penteknisk", - "type": 0 + "type": "Stor" }, { "word": "ERYX", "category": "V\u00e5penteknisk", - "type": 0 + "type": "Stor" }, { "word": "MP-5", "category": "V\u00e5penteknisk", - "type": 1 + "type": "Liten" }, { "word": "HK-416", "category": "V\u00e5penteknisk", - "type": 0 + "type": "Stor" }, { "word": "G\u00e5r av nett", "category": "Uttrykk/tiltak/oppdrag", - "type": 0 + "type": "Stor" }, { "word": "Er i rute", "category": "Uttrykk/tiltak/oppdrag", - "type": 0 + "type": "Stor" }, { "word": "Stans, -e, -et", "category": "Uttrykk/tiltak/oppdrag", - "type": 0 + "type": "Stor" }, { "word": "Leopard 2", "category": "Kj\u00f8ret\u00f8y", - "type": 0 + "type": "Stor" }, { "word": "BV-206", "category": "Kj\u00f8ret\u00f8y", - "type": 0 + "type": "Stor" }, { "word": "CV 90", "category": "Kj\u00f8ret\u00f8y", - "type": 0 + "type": "Stor" }, { "word": "M109", "category": "Kj\u00f8ret\u00f8y", - "type": 0 + "type": "Stor" }, { "word": "M04", "category": "Bekledning", - "type": 0 + "type": "Stor" }, { "word": "Regnt\u00f8y", "category": "Bekledning", - "type": 0 + "type": "Stor" }, { "word": "Vernemaske", "category": "Personlig utrustning", - "type": 0 + "type": "Stor" }, { "word": "Vest plate", "category": "Personnlig utrustning", - "type": 0 + "type": "Stor" }, { "word": "AG3", "category": "V\u00e5penteknisk", - "type": 0 + "type": "Stor" }, { "word": "Rekylfri kanon", "category": "V\u00e5penteknisk", - "type": 0 + "type": "Stor" }, { "word": "MB Multi", "category": "Kj\u00f8ret\u00f8y", - "type": 0 + "type": "Stor" }, { "word": "MG-3", "category": "V\u00e5penteknisk", - "type": 0 + "type": "Stor" }, { "word": "M2 Browning", "category": "V\u00e5penteknisk", - "type": 0 + "type": "Stor" }, { "word": "M77", "category": "Personlig utrustning", - "type": 0 + "type": "Stor" }, { "word": "Netting under over", "category": "Personlig utrustning", - "type": 0 + "type": "Stor" }, { "word": "Underbukse ull kort lang", "category": "Personlig utrustning", - "type": 0 + "type": "Stor" }, { "word": "Kompass", "category": "Personlig utrustning", - "type": 0 + "type": "Stor" }, { "word": "Ferdigstilling", "category": "Straff", - "type": 0 + "type": "Stor" }, { "word": "ventriculus", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "stentors", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "retroact", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "best-best", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "pumplike", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "unparanoid", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "inflammably", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "debonairty", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "4what", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Liverpudlians", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "camarasaurus", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "clairvoyance", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "whirl", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "belligerencies", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "paratroop", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "clothe", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "conqueresses", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "tussocks", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "outvoting", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "taivers", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "capsulitis", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "sagakomi", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Milhaud", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "coldcocked", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "cross", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "oestrogenic", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "squooshing", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "mormons", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "catholicness", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "interglandular", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "reimplanting", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "counter-scuffle", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "todos", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Territorian", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "tigresses", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "gymnastic", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "downlighter", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "capitalise", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "transform", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "twelveth", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "replated", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "dyeing", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "boarding-car", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "dendrogram", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Filene", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Sarah", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "wisheth", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "brown-black", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "regius", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "aerogrammes", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "fallrate", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "swamped", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "condones", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "mislaid", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "boton", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "legative", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "teratons", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "listens", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "dsRNA", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "frisette", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "abducing", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "hennaed", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "wuthered", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "manhole", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "latticing", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "spooming", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "sesquipedalians", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "interstitial", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "alumna", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "fortification", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "unendeared", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "immobilism", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "unruly", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "inchmeal", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "defogs", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "moonwalk", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "collimating", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "immundicity", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "bleeding", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "promotional", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "icosaedron", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "irrefragable", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "catholic", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "cragsmen", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "distrainor", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "tholins", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "canescence", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "restauranteers", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "pseudochrysalis", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "brasslike", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "falcidia", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "redye", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "dungheap", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "land-side", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "shearing", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "notchy", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "tompion", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "preformations", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "disagreeable", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "duchess", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "megafaunas", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "lichenologist", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "compliable", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "devoutness", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Washington", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "carpathian", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "wibbled", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "viewing", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "demeclocycline", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "trophoneurotic", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "fashionably", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "tenish", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "magician", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "all-clear", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "bullet", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "triformity", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "foyers", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "laddess", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "tunic", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Woronovsky", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "insulinomas", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "risible", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "singlehandedly", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "gamogenetic", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "poppa", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "wounded", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "maximin", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "unrattled", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "rumkin", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "dacre", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "unhypnotized", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "undespairing", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "three-hooped", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "rigid", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "refrozen", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "wargames", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "cacoplastic", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "low-lived", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "wholely", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "water-loving", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "lachrymatories", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "slantwise", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "abnormalness", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "screech-owls", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "resistless", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "discount-broker", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "zoisite", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "robusta", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "betol", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "benedictions", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "oolong", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "ad-libbing", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "flautino", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "unceasingly", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "domesticize", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "solidifying", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "muzzle-loading", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "fruitfulness", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "amphistomous", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "unrolled", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "lettering", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "unpoison", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "vice-captain", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "wisard", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "simple-hearted", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "covered", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "glove-hook", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Mittal-led", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "odious", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "bioprosthesis", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "hinting", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "renverse", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "mnemonics", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "rabblement", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "orientated", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "weaning", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "nondreamer", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "groin", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "eulogist", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "parqueterie", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "slappers", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "bonier", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "vasoactive", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "dopeheads", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "disspiriting", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "unforthcoming", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "nonforeigners", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "upsend", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "unidentifiable", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "shaft-horse", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "singsongs", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "serrated", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "parfleches", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "arrondissement", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "elements", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "guard", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Estonia", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "reliefless", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "inconstancy", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "whippet", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "soothful", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Westgate", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "ushered", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "unmotivating", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "crusts", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "uncontestedly", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Pan-European", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "bo's'ns", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "remnants", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "prolog", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "discover", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "vogueing", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "propos", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "highborn", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "alang", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "unrighteous", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "oleophobic", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "defoliating", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "deindividualize", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "births", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "tumefacient", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "terror-struck", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "tim-whiskey", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "creetur", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "transceiver", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "varnishing-day", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Bloom", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "sacraria", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "winterise", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "news-editor", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "bookplates", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "laminates", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "rhodonite", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "barhopper", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "oncet", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "deauthorizing", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "revisional", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Megara", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "purposing", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "fough", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "throned", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "debriefed", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "imagelike", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "tankard-bearer", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "widow-maker", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "autoradiographs", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "infraconscious", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "undulator", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "inquisitiveness", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "chipmaker", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "comast", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "anthracene", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "weaned", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Hogtown", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "urochrome", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "katurai", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "antisodomy", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Lomond", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "illiberality", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "fluorescently", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "signate", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "treacheries", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Eakins", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "haloing", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "reencoding", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Accadians", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "superfluids", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "activists", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Boers", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "videotex", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "ultrasonication", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "untangled", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "unwhipt", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "jeofails", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "hatted", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "lock-bolt", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "pretertiary", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "defines", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "coal-drift", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "moralists", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "countersay", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "squale", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "tormentress", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "precipitant", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "electrifiable", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "mouther", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "second-", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "renavigate", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "moirae", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "liquor", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "grandparent", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "clock-watcher", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "simon", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "setiferous", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "megabanks", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "editorially", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "unbranched", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "bedwarmer", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "opsonize", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Hebbian", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "erythemas", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "gaulin", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "mesnality", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "window-stool", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "tongue-lash", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "blameshifts", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Handan", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "gasconading", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "upstyle", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "chuman", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "electivity", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "brewing", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "banneton", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "triumviri", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "grassy", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Campinas", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "impulsivity", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "name-calling", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "privatism", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "injures", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "defrayed", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "unperfectly", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "ateliers", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "verification", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "nonalloy", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "awapuhi", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "educatory", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "heli-skis", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "dijudication", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "markers", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "semitransparent", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "unzoned", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "hamburger", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "adultescents", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "schist", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "cornflakes", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "laidback", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "hackers", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "jeer-fall", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "scintillates", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "decadally", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "monetarists", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Yuwaalaraay", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "cater", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "fly-specked", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "hallowed", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "flitch", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "wine-coloured", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "bipeds", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "vinylidene", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "jumpups", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "leptiform", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "shaken", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "beneficiaries", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "townee", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "queercore", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "tagmemics", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "dyery", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "brochan", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "rapids", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "enveigle", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "unpartial", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "unplained", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "wassat", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "letterpress", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "karting", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Ajaxified", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "perennity", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "buckle", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "unbox", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "pouter", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "concerningly", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "ChickBomb", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "aerators", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "ancyroid", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "overlooked", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "hotrod", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "interfere", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "miscue", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Judases", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "episome", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "rescored", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "brownies", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "schuyt", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "siphuncular", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "well-beseeming", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "nonribosomal", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "tined", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "precipitious", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "audibles", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "posterity", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "ultraquick", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "casualize", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "antihelix", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "rewards", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "emperours", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "unblemishable", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "lives", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "nut-oil", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "sorrily", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "crimson-purple", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Disraeli", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "rumormongers", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "under-timed", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "overgrowth", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Italianism", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "lesbian", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "brodiaea", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Angel", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "stonewash", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "nagware", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Cooper", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "trephination", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "nazis", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Quaker", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "railest", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "inglut", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "red-dog", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "retrogressively", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "leguminos\u00e6", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "chronologies", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "wassailing", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "avulse", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "unhealth", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "tea-things", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "meaninglessly", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "soap-ashes", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "weizenbock", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "manipulative", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Allerton", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "skelp", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "transfat", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "unchallengeable", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Undershirt", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "burdensome", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "over-eggs", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "distant", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "munter", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "latin", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "basepath", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "osteoplastic", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "accoladed", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "mealtime", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "gugglets", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Alhambra", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "muranese", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "rectified", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "bloke", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "underwriting", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "haemolysins", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "latitudinarian", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "troop", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "admissability", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "virino", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "disport", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "bluebloods", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "paraboloidal", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "hyperphrygian", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "falters", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "ranchhands", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "senegin", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "noninvestor", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "trombones", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "foresees", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "froggies", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "thermoacoustics", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "uncredible", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "seisins", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "legibleness", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "perimeters", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "nonmineral", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "guppy", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "buckytubes", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "osteochondroma", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "confessor", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "aping", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Polypodium", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "love-child", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "shelters", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Dothan", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "courtship", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "airlifting", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "featherless", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "ladies", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "lilting", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "distending", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "flirtiest", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "sanct", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "ciconiidae", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "signal-light", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "plucks", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "angas", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "common-sensical", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "prawns", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "whole-souled", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "unkenned", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Wilson", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "amino", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "therians", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Hereward", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "discord", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "trophies", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "sights", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "displeasingly", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "insupportably", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "technically", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "riddle", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "paphia", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "pantometer", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "ground-plane", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "maxim", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "dissipating", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "amusingly", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "postern", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "nongraduate", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "dog-legged", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Eretria", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "come-off", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "litigators", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "coal-mining", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "cladistics", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "putchuk", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "toffs", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "susurrus", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "vision", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "photographed", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "hydantoin", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "mailboxes", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "perimetrical", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "middle-income", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "licking", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "traducing", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "shootout", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "engrossment", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Johannes", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "yesternight", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "epuration", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "indisputable", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "albuminate", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "aureobasidium", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Allan", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "amphitheatres", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "protract", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "understaffing", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "ultrasonographs", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "upshots", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "akathisia", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "discordantly", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "feebleness", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "antisymmetry", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "brochet", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "right-minded", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "diffluent", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "immute", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Schiller", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "detur", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "warders", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Cape Cod", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "greys", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "cross-stitched", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "unratifiable", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "inconsiderate", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Blackbeard", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "neurotoxicities", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "vitrifying", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "condemnatorily", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "printability", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "whomever", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "imitations", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "formell", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "gregorian", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "inalienability", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "halophytic", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "alibility", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "dromia", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "giantship", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "tendable", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "cryptarithm", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "orangutangs", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "linkage", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Pius VII", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "aiguillettes", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "ultrastable", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "interjoin", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "meltiest", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "multilocus", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "origamis", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "caddishness", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "paper-pulp", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "cedule", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "driblet", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "ankle-boot", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "unrepublican", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "remonetize", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "cribl\u00e9", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "intracranial", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Willard", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "immunodeficient", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "martagon", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "permanency", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "podcasted", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "futurism", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "crane-post", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "N'Djamena", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "recapitalises", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "intermixes", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "sanctification", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "fossilization", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Virgin Islands", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "sememes", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "intracellularly", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "reveals", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "emphasizers", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "cateress", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "coniferophyta", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Clarence House", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "imide", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "inconscience", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "tenthredo", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "tinmen", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "unattractive", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "time-drain", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "chowder-headed", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "spaetzles", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "illuminations", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Warren", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "Heath", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "doorknob", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "bureaucratize", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "nonagesimal", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "gelees", "category": "Testkategori", - "type": 0 + "type": "Stor" }, { "word": "hurde", "category": "Testkategori", - "type": 0 + "type": "Stor" } ] \ No newline at end of file diff --git a/test/test_codebook_to_pdf.py b/test/test_codebook_to_pdf.py new file mode 100644 index 0000000000000000000000000000000000000000..5dacda11af3b8849bce250a2ace30840a1535304 --- /dev/null +++ b/test/test_codebook_to_pdf.py @@ -0,0 +1,60 @@ +"""Test exporting codebook to PDF.""" +import unittest +import os +from functools import partial +from pathlib import Path +from datetime import datetime +from soitool import codebook_to_pdf + +SOITOOL_ROOT_PATH = Path(__file__).parent.parent + + +class ExportTest(unittest.TestCase): + """Test codebook_to_pdf.py.""" + + def test_generate_filename(self): + """Test generated filename is as expected.""" + # Get current date in format YYYY_mm_dd + today = datetime.now().strftime("%Y_%m_%d") + + # Assert correct filename for full codebook + expected = f"Kodebok_{today}.pdf" + actual = codebook_to_pdf.generate_filename(codebook_to_pdf. + CodebookSize.FULL) + self.assertEqual(expected, actual) + + # Assert correct filename for small codebook + expected = f"Kodebok_liten_{today}.pdf" + actual = codebook_to_pdf.generate_filename(codebook_to_pdf. + CodebookSize.SMALL) + self.assertEqual(expected, actual) + + def test_generate_codebook_pdf(self): + """Test generated PDF-file exist.""" + # Test full codebook (default) + codebook_to_pdf.generate_codebook_pdf() + file_name = codebook_to_pdf.generate_filename(codebook_to_pdf. + CodebookSize.FULL) + file_path_full = os.path.join(SOITOOL_ROOT_PATH, file_name) + # Assert file exists + self.assertTrue(os.path.exists(file_path_full)) + + # Test small codebook + codebook_to_pdf.generate_codebook_pdf(codebook_to_pdf. + CodebookSize.SMALL) + file_name = codebook_to_pdf.generate_filename(codebook_to_pdf. + CodebookSize.SMALL) + file_path_small = os.path.join(SOITOOL_ROOT_PATH, file_name) + # Assert file exists + self.assertTrue(os.path.exists(file_path_small)) + + self.addCleanup(partial(delete_generated_files, + file_path_full, file_path_small)) + + +def delete_generated_files(file_path1, file_path2): + """Delete generated PDF-files.""" + if os.path.exists(file_path1): + os.remove(file_path1) + if os.path.exists(file_path2): + os.remove(file_path2) diff --git a/test/test_database.py b/test/test_database.py index 842dc9a48136412028ed5fccbf4dadb25e422cd7..6609df817bbc514db03a1f34b3c74e14fce04a5d 100644 --- a/test/test_database.py +++ b/test/test_database.py @@ -84,9 +84,9 @@ class DatabaseTest(unittest.TestCase): """Assert function get_codebook works as expected.""" # Get test-data from json file_path = os.path.join(TESTDATA_PATH, "long_codebook.json") - with open(file_path, "r") as file: - expected = json.load(file) - file.close() + f = open(file_path, "r", encoding="utf-8") + expected = json.load(f) + f.close() # Get data from db stmt = "SELECT * FROM CodeBook" @@ -102,8 +102,8 @@ class DatabaseTest(unittest.TestCase): for i, entry in enumerate(expected): self.assertEqual(entry["word"], actual[i][0]) self.assertEqual(entry["category"], actual[i][1]) - self.assertRegex(actual[i][2], "[A-Z]{" + str(code_len) + "}") - self.assertEqual(entry["type"], actual[i][3]) + self.assertRegex(actual[i][3], "[A-Z]{" + str(code_len) + "}") + self.assertEqual(entry["type"], actual[i][2]) def test_get_categories(self): """Assert function get_categories works as expected.""" @@ -130,9 +130,10 @@ class DatabaseTest(unittest.TestCase): """Assert function get_codebook returns full codebook.""" # Load full codebook file_path = os.path.join(TESTDATA_PATH, "long_codebook.json") - with open(file_path, "r") as file: - expected = json.load(file) - file.close() + f = open(file_path, "r", encoding="utf-8") + expected = json.load(f) + f.close() + # Get full codebook from db actual = self.database.get_codebook() # Compare lenth @@ -150,9 +151,10 @@ class DatabaseTest(unittest.TestCase): """Assert function get_codebook only return the small codebook.""" # Load full codebook file_path = os.path.join(TESTDATA_PATH, "long_codebook.json") - with open(file_path, "r") as file: - data = json.load(file) - file.close() + f = open(file_path, "r", encoding="utf-8") + data = json.load(f) + f.close() + # Fill expected with only small codebook entries expected = [] for entry in data: