diff --git a/soitool/database.py b/soitool/database.py index abb77adf0750021cf28a66ce9c6eeaa7c8ebc8c2..65fb99d72462ec0d55ca690c2eab605b799add19 100644 --- a/soitool/database.py +++ b/soitool/database.py @@ -7,7 +7,6 @@ import soitool.coder from soitool.enumerates import CodebookSort from soitool.serialize_export_import_soi import serialize_soi from soitool.compressor import compress -from soitool.dialog_wrappers import exec_warning_dialog # Set name and path to default (future) database DBNAME = "database" @@ -20,9 +19,11 @@ SECONDS_IN_24H = 24 * 60 * 60 # DDL-statements for creating tables SOI = ( "CREATE TABLE SOI(" - "Title VARCHAR PRIMARY KEY NOT NULL CHECK(length(Title) > 0), " + "Title VARCHAR NOT NULL CHECK(length(Title) > 0), " + "Version VARCHAR NOT NULL CHECK(length(Version) > 0), " "SOI TEXT NOT NULL CHECK(length(SOI) > 0), " - "Date DATETIME)" + "Date DATETIME, " + "PRIMARY KEY(Title, Version))" ) CODEBOOK = ( "CREATE TABLE Codebook(" @@ -347,44 +348,35 @@ class Database: self.conn.execute(stmt, (code, word)) self.conn.commit() - def insert_soi(self, soi): - """Serialize, compress and insert SOI into database-table SOI. + def insert_or_update_soi(self, soi): + """Serialize, compress and insert/update SOI in database-table SOI. - If one or more SOI's with the same title exist in db, '(1)', '(2)', ... - is added to the Title-column of the SOI to insert. + SOI's with the same title and version are overwritten. Parameters ---------- soi : soitool.soi.SOI - The SOI-instance to insert into database. + The SOI-instance to insert or update. """ # Serialize and compress SOI compressed_soi = compress(serialize_soi(soi)) - # Count SOI's in database with equal title, - # including titles ending with '(x)' - title = soi.title - length = len(title) - stmt = ( - "SELECT COUNT(*) FROM SOI " - "WHERE substr(Title,0,?)=? " - "AND (length(Title)=? OR length(Title)=?)" - ) - count = self.conn.execute( - stmt, (length + 1, title, length, length + 3) - ).fetchone()[0] - - # Add '(x)' to title if one or more SOI's with equal title exist - if count > 0: - title += "(" + str(count + 1) + ")" - - # Insert SOI into database - stmt = "INSERT INTO SOI (Title, SOI, Date) VALUES(?,?,?)" - try: - self.conn.execute(stmt, (title, compressed_soi, soi.date)) - self.conn.commit() - except sqlite3.IntegrityError: - exec_warning_dialog( - "SOI ble ikke lagret i databasen.", - "Sørg for at SOI'en har en tittel.", + # Check if SOI with the same title and version exists + stmt = "SELECT COUNT(*) FROM SOI " "WHERE Title=? " "AND Version=?" + count = self.conn.execute(stmt, (soi.title, soi.version)).fetchone()[0] + + # If SOI exists, overwrite + if count == 1: + stmt = "UPDATE SOI SET SOI=?, Date=? WHERE Title=? AND Version=?" + self.conn.execute( + stmt, (compressed_soi, soi.date, soi.title, soi.version) ) + # SOI does not exist, insert + else: + stmt = ( + "INSERT INTO SOI (Title, Version, SOI, Date) VALUES(?,?,?,?)" + ) + self.conn.execute( + stmt, (soi.title, soi.version, compressed_soi, soi.date) + ) + self.conn.commit() diff --git a/soitool/main_window.py b/soitool/main_window.py index 6992a245d65fe9674ed7a5c2b67aab4998772b0c..a099da8f2186c4e701403a5506e0cf64c1abe3c4 100644 --- a/soitool/main_window.py +++ b/soitool/main_window.py @@ -22,8 +22,6 @@ from soitool.codebook_widget import CodebookWidget from soitool.codebook_model_view import CodebookTableModel from soitool.database import Database, DBPATH from soitool.help_actions import ShortcutsHelpDialog, BasicUsageHelpDialog -from soitool.soi_db_widget import SOIDbWidget -from soitool.soi_model_view import SOITableModel from soitool.serialize_export_import_soi import ( export_soi, import_soi, @@ -111,7 +109,6 @@ class MainWindow(QMainWindow): open_file_db = QAction("Åpne fra DB", self) open_file_db.setShortcut("Ctrl+d") open_file_db.setStatusTip("Åpne en SOI fra databasen") - open_file_db.triggered.connect(self.show_soi_db) file_menu.addAction(open_file_db) # Preview SOI @@ -310,24 +307,7 @@ class MainWindow(QMainWindow): # If tab contains an SOI if isinstance(tab_widget, SOIWorkspaceWidget): - # Update tab showing SOI's in db if it is open, - # and pause database-lock by codebook-tab if it is open. - soi_db_view = None - codebook_db_view = None - for i in range(self.tabs.count()): - if self.tabs.tabText(i) == "SOI'er i DB": - soi_db_view = self.tabs.widget(i).view - soi_db_view.setModel(None) - elif self.tabs.tabText(i) == "Kodebok": - codebook_db_view = self.tabs.widget(i).view - codebook_db_view.setModel(None) - - self.database.insert_soi(tab_widget.soi) - - if soi_db_view is not None: - soi_db_view.setModel(SOITableModel()) - if codebook_db_view is not None: - codebook_db_view.setModel(CodebookTableModel()) + self.database.insert_or_update_soi(tab_widget.soi) else: exec_info_dialog( "Valgt tab er ingen SOI-tab", @@ -335,23 +315,6 @@ class MainWindow(QMainWindow): "Riktig tab må velges for å lagre en SOI i DB.", ) - def show_soi_db(self): - """Open and select tab containing SOIDbWidget. - - Select tab if it is already open, - create and select tab if not open. - """ - # Loop through tabs to look for existing SOI-db-tab: - for i in range(self.tabs.count()): - if self.tabs.tabText(i) == "SOI'er i DB": - self.tabs.setCurrentIndex(i) - break - # SOI-db-tab does not exist, create, add and select tab - else: - tab = SOIDbWidget(self.database, self.tabs) - self.tabs.addTab(tab, "SOI'er i DB") - self.tabs.setCurrentWidget(tab) - def open_shortcut_help(self): """Open shortcut dialog.""" self.popup_shortcut_help.setWindowTitle("Hurtigtaster") diff --git a/soitool/soi_db_widget.py b/soitool/soi_db_widget.py deleted file mode 100644 index 343c7d4672c7b7db8557cb59c5836960d2dd317b..0000000000000000000000000000000000000000 --- a/soitool/soi_db_widget.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Module containing a widget for viewing and opening SOI's from database.""" -from PySide2.QtWidgets import QWidget, QHBoxLayout -from soitool.soi_model_view import SOITableView - - -class SOIDbWidget(QWidget): - """Widget for viewing and opening SOI's from database.""" - - def __init__(self, database, tab_widget): - super().__init__() - - self.view = SOITableView(database, tab_widget) - self.create_and_set_layout() - - def create_and_set_layout(self): - """Create layout, add widget and set layout.""" - hbox = QHBoxLayout() - hbox.addWidget(self.view) - self.setLayout(hbox) diff --git a/soitool/soi_model_view.py b/soitool/soi_model_view.py deleted file mode 100644 index 3ca3d380fcb36146eada958aa56dcb60ca42a6c0..0000000000000000000000000000000000000000 --- a/soitool/soi_model_view.py +++ /dev/null @@ -1,134 +0,0 @@ -"""GUI-interface towards database-table 'SOI'. - -Contains functionality for showing and opening SOI's in database. -""" -from PySide2.QtWidgets import QTableView -from PySide2.QtSql import QSqlDatabase, QSqlTableModel -from PySide2.QtCore import Qt -from soitool.style import CODEBOOK_HEADER_FONT, CODEBOOK_HEADER_BACKGROUND_CSS -from soitool.serialize_export_import_soi import construct_soi_from_serialized -from soitool.soi_workspace_widget import SOIWorkspaceWidget - -# Name and type of database -CONNAME = "SOIDB" -DBTYPE = "QSQLITE" - - -class SOITableView(QTableView): - """TableView with a model of the 'SOI'-table from database. - - This modified QTableView creates a SOITableModel, which reads data from the - SOI-table. When the user double-clicks or presses the enter-key, a tab - containing SOIWorkspaceWidget, with the SOI from the current row, is opened - and selected. - - Parameters - ---------- - database : soitool.database.Database - Is used to create a QSqlDatabase from the database-file. - tab_widget : QTabWidget - Is used to open a new tab. - - Raises - ------ - RuntimeError - If database does not open. - """ - - def __init__(self, database, tab_widget): - super().__init__() - db = QSqlDatabase.addDatabase(DBTYPE, CONNAME) - db.setDatabaseName(database.db_path) - self.tab_widget = tab_widget - - if not db.open(): - raise RuntimeError("Could not open database.") - - # Enable sorting - self.setSortingEnabled(True) - - # Create and set model - model = SOITableModel() - self.setModel(model) - - # Remove horizontal scrollbar, hide vertical header and 'SOI'-column - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - self.verticalHeader().hide() - self.hideColumn(1) - - # Set horizontal header-text and it's style - self.set_horizontal_header_text() - header = self.horizontalHeader() - header.setFont(CODEBOOK_HEADER_FONT) - header.setStyleSheet(CODEBOOK_HEADER_BACKGROUND_CSS) - - # Resize - self.resizeColumnsToContents() - width = self.columnWidth(0) + self.columnWidth(2) + 2 # +2 offset - self.setFixedWidth(width) - - self.doubleClicked.connect(self.open_soi_tab) - - def set_horizontal_header_text(self): - """Set Norwegian names in horizontal header.""" - self.model().setHeaderData(0, Qt.Horizontal, "Tittel") - self.model().setHeaderData(2, Qt.Horizontal, "Dato") - - def keyPressEvent(self, event): - """Open SOI-tab if enter-key is pressed.""" - if event.key() == Qt.Key_Return: - self.open_soi_tab() - super().keyPressEvent(event) - - def open_soi_tab(self): - """Construct SOI and open SOIWorkspacewidget in new tab.""" - # Get index of the current row and read compressed, serialized SOI - row = self.currentIndex().row() - compressed_soi = self.model().index(row, 1).data() - - # Construct SOI and create SOIWorkspaceWidget - soi = construct_soi_from_serialized(compressed_soi, compressed=True) - tab = SOIWorkspaceWidget(soi) - - # Add and select tab - self.tab_widget.addTab(tab, soi.title) - self.tab_widget.setCurrentWidget(tab) - - def setModel(self, model): - """Set model, resize and hide 'SOI'-column. - - Parameters - ---------- - model : SOITableModel or None - Model containing data to display. - """ - super().setModel(model) - if model is not None: - self.hideColumn(1) - self.resizeColumnsToContents() - width = self.columnWidth(0) + self.columnWidth(2) + 2 # +2 offset - self.setFixedWidth(width) - - -class SOITableModel(QSqlTableModel): - """Uneditable QSqlTableModel of database-table 'SOI'.""" - - def __init__(self): - super().__init__(None, QSqlDatabase.database(CONNAME)) - self.setEditStrategy(QSqlTableModel.OnFieldChange) - self.setTable("SOI") - self.setSort(2, Qt.DescendingOrder) # Sort by date - self.select() - - def flags(self, index): - """Disable editing. - - Parameters - ---------- - index : QModelIndex - Is used to locate data in a model. - """ - flags = super().flags(index) - flags ^= Qt.ItemIsEditable - - return flags diff --git a/test/test_database.py b/test/test_database.py index ab8b773103e98e2534444e966e1e79f7b4b272dd..42c705285dbe516df3471c488d4032263c366fc1 100644 --- a/test/test_database.py +++ b/test/test_database.py @@ -302,7 +302,7 @@ class DatabaseTest(unittest.TestCase): test_date = datetime.now().strftime("%Y-%m-%d") # Create and insert SOI soi = SOI(title=test_title, date=test_date) - self.database.insert_soi(soi) + self.database.insert_or_update_soi(soi) # Assert only one SOI is in table stmt = "SELECT * FROM SOI" @@ -311,16 +311,22 @@ class DatabaseTest(unittest.TestCase): # Assert SOI was written correctly self.assertEqual(queried[0]["Title"], test_title) + self.assertEqual(queried[0]["Version"], soi.version) self.assertEqual(queried[0]["Date"], test_date) self.assertEqual(queried[0]["SOI"], compress(serialize_soi(soi))) - # Insert same SOI again and assert '(2)' is added to title - self.database.insert_soi(soi) - stmt = "SELECT COUNT(*) FROM SOI WHERE Title=?" - count = self.database.conn.execute( - stmt, (test_title + "(2)",) - ).fetchone()[0] - self.assertEqual(count, 1) + # Insert the same SOI (same title & version), assert it is overwritten. + self.database.insert_or_update_soi(soi) + # Assert only one SOI is in table + queried = self.database.conn.execute(stmt).fetchall() + self.assertEqual(len(queried), 1) + + # Insert another version of the same SOI, assert a new row is added. + soi.version = "2" + self.database.insert_or_update_soi(soi) + # Assert two SOI's are in table + queried = self.database.conn.execute(stmt).fetchall() + self.assertEqual(len(queried), 2) def delete_db(self): """Delete generated database-file."""