Skip to content
Snippets Groups Projects
Commit 71a029e4 authored by Kien Le's avatar Kien Le
Browse files

Fix problems with database editor tables that have no row

parent 50f2f844
No related branches found
No related tags found
1 merge request!289Fix problems with database editor tables that have no row
......@@ -45,15 +45,8 @@ class ChannelDialog(UiDBInfoDialog):
super(ChannelDialog, self).update_data_table_widget_items()
def clear_first_row(self):
"""
Clear content of first row of widgets in self.data_table_widgets in
data_type that has no channels yet.
"""
self.data_table_widget.cellWidget(0, 1).setText('')
self.data_table_widget.cellWidget(0, 2).setText('')
self.data_table_widget.cellWidget(0, 3).setCurrentIndex(-1)
"""Clear the content of the first row of the editor."""
self.data_table_widget.cellWidget(0, 4).setText('1')
self.data_table_widget.cellWidget(0, 5).setText('')
self.data_table_widget.cellWidget(0, 6).setValue(0)
def set_row_widgets(self, row_idx, fk=False):
......
from __future__ import annotations
from typing import Dict, Optional, List, Tuple
from typing import Dict, Optional, List, Tuple, TypeVar
from PySide6 import QtWidgets, QtGui, QtCore
from PySide6.QtCore import Signal
......@@ -85,6 +85,28 @@ def set_widget_color(widget, changed=False, read_only=False):
widget.setPalette(palette)
T = TypeVar('T')
def get_duplicates(items: List[T]) -> Dict[T, List[int]]:
"""
Get duplicate items and their indices in a list.
:param items: the list of items
:return: a dict of duplicates, with the duplicated items being the keys and
the indices they appear in as the values. The returned dict is empty
if no duplicate was found.
"""
# Create a dictionary to store each item and the indices it appears in.
# At the end, we can simply filter this dictionary by value to obtain the
# duplicated items and their indices.
idx_dict: Dict[T, List[int]] = {}
for idx, item in enumerate(items):
idx_dict.setdefault(item, []).append(idx)
return {item[0]: item[1]
for item in idx_dict.items()
if len(item[1]) > 1}
class UiDBInfoDialog(OneWindowAtATimeDialog):
"""
Superclass for info database dialogs under database menu.
......@@ -94,14 +116,14 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
# OneWindowAtATimeDialog.
current_instance: UiDBInfoDialog
def __init__(self, parent, column_headers, col_name, table_name,
def __init__(self, parent, column_headers, primary_column, table_name,
resize_content_columns=[], required_columns={},
need_data_type_choice=False, check_fk=True):
"""
:param parent: QMainWindow/QWidget - the parent widget
:param column_headers: [str,] - headers of the columns
:param col_name: str - key db column in the table (not count dataType
in Channels table)
:param primary_column: str - primary key column of the table in the
database (not count dataType in Channels table)
:param table_name: str - the database table
:param resize_content_columns: [int,] - list of indexes of columns of
which width needs to be resize to content
......@@ -125,7 +147,7 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
self.resize_content_columns.append(self.total_col - 1)
self.required_columns = required_columns
self.need_data_type_choice = need_data_type_choice
self.col_name = col_name
self.primary_column = primary_column
self.table_name = table_name
self.check_fk = check_fk
......@@ -407,6 +429,15 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
"""
pass
def clear_first_row(self):
"""
Clear the content of the first row of the editor. By default, all
editor widgets have their content set to empty, so this method does not
need to be implemented unless a default value is needed for some editor
widgets.
"""
pass
def update_data_table_widget_items(self):
"""
Create widget cell for self.data_table_widget based on
......@@ -424,20 +455,24 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
self.data_table_widget.setRowCount(0)
self.database_rows = self.get_database_rows()
self.changes_array = [
[False] * len(self.database_rows[0])
[False] * (self.total_col - 1)
for _ in range(len(self.database_rows))
]
row_count = len(self.database_rows)
row_count = 1 if row_count == 0 else row_count
self.data_table_widget.setRowCount(row_count)
for i in range(len(self.database_rows)):
fk = self.check_data_foreign_key(self.database_rows[i][0])
self.set_row_widgets(i, fk)
if len(self.database_rows) == 0:
"""
No Row, should leave 1 empty row
"""
if row_count == 0:
# If there is no data in the database, we want to leave an empty
# row in the editor.
# self.add_row() does not work here without some light refactoring
# which might introduce bugs, so we manually set up the first row.
self.data_table_widget.setRowCount(0)
self.add_row()
self.clear_first_row()
else:
self.data_table_widget.setRowCount(row_count)
for i in range(row_count):
fk = self.check_data_foreign_key(self.database_rows[i][0])
self.set_row_widgets(i, fk)
self.update()
......@@ -453,7 +488,7 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
self.data_table_widget.scrollToBottom()
self.data_table_widget.repaint() # to show row's header
self.data_table_widget.cellWidget(row_position, 1).setFocus()
self.changes_array.append([True] * len(self.database_rows[0]))
self.changes_array.append([True] * (self.total_col - 1))
def remove_row(self, remove_row_idx):
"""
......@@ -479,6 +514,8 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
widget_x, widget_y = changed_cell_widget.pos().toTuple()
col_idx = self.data_table_widget.columnAt(widget_x)
row_idx = self.data_table_widget.rowAt(widget_y)
if row_idx == -1:
return
changed = False
if row_idx < len(self.database_rows):
if changed_text != self.database_rows[row_idx][col_idx - 1]:
......@@ -499,8 +536,8 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
"""
if not self.check_fk:
return False
sql = (f"SELECT {self.col_name} FROM channels "
f"WHERE {self.col_name}='{val}'")
sql = (f"SELECT {self.primary_column} FROM channels "
f"WHERE {self.primary_column}='{val}'")
param_rows = execute_db(sql)
if len(param_rows) > 0:
return True
......@@ -592,10 +629,10 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
delete_button_y = row_delete_button.y()
row_idx = self.data_table_widget.rowAt(delete_button_y)
# Because self.changed_rows only track changes to row content and not
# deletions, we want to remove the deleted row's ID from it.
# Because self.changes_array only track changes to row content and not
# deletions, we want to untrack the row that is being deleted.
if row_idx < len(self.database_rows):
self.changes_array[row_idx] = [False] * len(self.database_rows[0])
self.changes_array[row_idx] = [False] * (self.total_col - 1)
else:
# Because rows not in the database are removed from the table when
# deleted, we delete the value that tracks whether they changed
......@@ -623,7 +660,7 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
)
delete_sql = (f"DELETE FROM {self.table_name} "
f"WHERE {self.col_name}='{primary_key}'")
f"WHERE {self.primary_column}='{primary_key}'")
delete_sql += self.delete_sql_supplement
self.queued_row_delete_sqls[row_idx] = delete_sql
......@@ -690,7 +727,6 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
def validate_row(self, row_id: int, row_content: List) -> Tuple[bool, str]:
"""
Check if the given row is valid. Invalid rows are those that:
- contain a duplicate primary key.
- contains an empty primary key.
- has an empty cell in a required column.
......@@ -699,22 +735,9 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
- A message of what the issue is; is the empty string if there is
no issue
"""
primary_keys = {database_row[0] for database_row in self.database_rows}
is_changes_invalid = True
msg = ''
is_row_primary_key_same_as_database = (
row_id < len(self.database_rows) and
row_content[0] == self.database_rows[row_id][0]
)
is_duplicate_primary_key = (
row_content[0] in primary_keys and
not is_row_primary_key_same_as_database
)
if is_duplicate_primary_key:
is_changes_invalid = False
msg = f'Row {row_id}: This row has a duplicate primary key.'
is_empty_primary_key = (row_content[0].strip() == '')
if is_empty_primary_key:
is_changes_invalid = False
......@@ -732,14 +755,36 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
def validate_changes(self) -> Tuple[bool, str]:
"""
Look through the changes the user made and attempt to check whether
they are all valid. Look at self.validate_row() and its overrides for
a list of invalid changes.
they are all valid. Only duplicate primary keys is checked for in this
method. Look at self.validate_row() and its overrides for a list of
other invalid changes.
:return:
- Whether all the changes made in the table are valid
- A message of what the issue is; is the empty string if there is
no issue
"""
# We assume that the first column in the editor is the primary key
# column. This would be a lot less fragile if we use a delegate instead
# of cell widgets, but that would require a complete rewrite of this
# class.
primary_keys = [
self.data_table_widget.cellWidget(row_idx, 1).text().strip()
for row_idx in range(self.data_table_widget.rowCount())
]
duplicated_primary_keys = get_duplicates(primary_keys)
msg = ''
for primary_key, indices in duplicated_primary_keys.items():
# .join() only accepts iterables of strings, so we have to do
# a quick conversion here.
str_indices = [str(i) for i in indices]
msg += (f'Rows {", ".join(str_indices)}: '
f'Primary key "{primary_key}" is duplicated.\n')
# Remove the final new line for better display.
msg = msg.strip('\n')
if msg:
return False, msg
changed_row_ids = [idx
for (idx, is_cell_changed_in_row_array)
in enumerate(self.changes_array)
......
......@@ -171,6 +171,10 @@ class ParamDialog(UiDBInfoDialog):
return [[d[0], d[1], d[2], int(d[3])]
for d in param_rows]
def clear_first_row(self):
"""Clear the content of the first row of the editor."""
self.data_table_widget.cellWidget(0, 4).setValue(0)
def get_row_inputs(self, row_idx):
"""
Get content from a row of widgets.
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment