Fixed the QSO index used in the Gtk.ListStore. Just before a QSO is added with add_record it was assumed that it's index would be max(rowid)+1, which is not always the case. This led to inconsistencies between the Gtk.ListStore and the database. Indices used in the Gtk.ListStore are now obtained directly from the database after insertion. Addresses issue #56.

pull/61/head
Christian T. Jacobs 2017-06-27 20:10:20 +01:00
rodzic f3bacf9dc7
commit 2d42acde9c
2 zmienionych plików z 102 dodań i 74 usunięć

Wyświetl plik

@ -114,12 +114,14 @@ class Log(Gtk.ListStore):
fields_and_data = [fields_and_data]
with self.connection:
# Get all the column names in the current database table.
c = self.connection.cursor()
# Get all the column names in the current database table.
c.execute("PRAGMA table_info(%s)" % self.name)
column_names = c.fetchall()
# Get the index of the last inserted record in the database.
c.execute('SELECT max(id) FROM %s' % self.name)
# Get the index/rowid of the last inserted record in the database.
c.execute("SELECT max(id) FROM %s" % self.name)
last_index = c.fetchone()[0]
if last_index is None:
# Assume no records are currently present.
@ -130,7 +132,7 @@ class Log(Gtk.ListStore):
# Construct the SQL query.
query = "INSERT INTO %s VALUES (NULL" % self.name
for i in range(len(column_names)-1): # -1 here because we don't want to count the database's 'id' field.
for i in range(len(column_names)-1): # -1 here because we don't want to count the database's 'id' column, since this is autoincremented.
query = query + ",?"
query = query + ")"
@ -144,28 +146,34 @@ class Log(Gtk.ListStore):
if((column_name.upper() in AVAILABLE_FIELD_NAMES_ORDERED) and (column_name.upper() in list(fields_and_data[r].keys()))):
database_entry.append(fields_and_data[r][column_name.upper()])
else:
if(column_name != "id"): # Ignore the row index field. This is a special case since it's not in AVAILABLE_FIELD_NAMES_ORDERED.
if(column_name != "id"): # Ignore the index/rowid field. This is a special case since it's not in AVAILABLE_FIELD_NAMES_ORDERED.
database_entry.append("")
database_entries.append(database_entry)
# Add the data to the ListStore as well.
liststore_entry = []
field_names = AVAILABLE_FIELD_NAMES_ORDERED
for i in range(0, len(field_names)):
if(field_names[i] in list(fields_and_data[r].keys())):
liststore_entry.append(fields_and_data[r][field_names[i]])
else:
liststore_entry.append("")
# Add the record's index.
index = last_index + (r+1) # +1 here because r begins at zero, and we don't want to count the already-present record with index last_index.
liststore_entry.insert(0, index)
self.append(liststore_entry)
# Execute the query.
# Insert records in the database.
with self.connection:
c = self.connection.cursor()
c.executemany(query, database_entries)
# Get the indices/rowids of the newly-inserted records.
query = "SELECT id FROM %s WHERE id > %s ORDER BY id ASC" % (self.name, last_index)
c.execute(query)
inserted = c.fetchall()
# Check that the number of records we wanted to insert is the same as the number of records successfully inserted.
assert(len(inserted) == len(database_entries))
# Add the records to the ListStore as well.
for r in range(len(fields_and_data)):
liststore_entry = [inserted[r]["id"]] # Add the record's index.
field_names = AVAILABLE_FIELD_NAMES_ORDERED
for i in range(0, len(field_names)):
if(field_names[i] in list(fields_and_data[r].keys())):
liststore_entry.append(fields_and_data[r][field_names[i]])
else:
liststore_entry.append("")
self.append(liststore_entry)
logging.debug("Successfully added the record(s) to the log.")
return
@ -173,11 +181,11 @@ class Log(Gtk.ListStore):
""" Delete a specified record from the log. The corresponding record is also deleted from the Gtk.ListStore data structure.
:arg int index: The index of the record in the SQL database.
:arg iter: iter should always be given. It is given a default value of None for unit testing purposes only.
:arg iter: The iterator pointing to the record to be deleted in the Gtk.ListStore. If the default value of None is used, only the database entry is deleted and the corresponding Gtk.ListStore is left alone.
:raises sqlite.Error, IndexError: if the record could not be deleted.
"""
logging.debug("Deleting record from log...")
# Get the selected row in the logbook
# Get the selected row in the logbook.
try:
with self.connection:
c = self.connection.cursor()
@ -197,8 +205,8 @@ class Log(Gtk.ListStore):
:arg int index: The index of the record in the SQL database.
:arg str field_name: The name of the field whose data should be modified.
:arg str data: The data that should replace the current data in the field.
:arg iter: Should always be given. A default value of None is used for unit testing purposes only.
:arg column_index: Should always be given. A default value of None is used for unit testing purposes only.
:arg iter: The iterator pointing to the record to be edited in the Gtk.ListStore. If the default value of None is used, only the database entry is edited and the corresponding Gtk.ListStore is left alone.
:arg column_index: The index of the column in the Gtk.ListStore to be edited. If the default value of None is used, only the database entry is edited and the corresponding Gtk.ListStore is left alone.
:raises sqlite.Error, IndexError: if the record could not be edited.
"""
logging.debug("Editing field '%s' in record %d..." % (field_name, index))
@ -227,18 +235,18 @@ class Log(Gtk.ListStore):
return (0, 0) # Nothing to do here.
removed = 0 # Count the number of records that are removed. Hopefully this will be the same as len(duplicates).
while removed != len(duplicates): # Unfortunately, in certain cases, extra passes may be necessary to ensure that all duplicates are removed.
path = Gtk.TreePath(0) # Start with the first row in the log.
iter = self.get_iter(path)
while iter is not None:
row_index = self.get_value(iter, 0) # Get the index.
if(row_index in duplicates): # Is this a duplicate row? If so, delete it.
self.delete_record(row_index, iter)
removed += 1
break
iter = self.iter_next(iter) # Move on to the next row, until iter_next returns None.
iter = self.get_iter_first() # Start with the first row in the log.
prev = iter # Keep track of the previous iter (initially this will be the same as the first row in the log).
while iter is not None:
row_index = self.get_value(iter, 0) # Get the index.
if(row_index in duplicates): # Is this a duplicate row? If so, delete it.
self.delete_record(row_index, iter)
removed += 1
iter = prev # Go back to the iter before the record that was just removed and continue from there.
continue
prev = iter
iter = self.iter_next(iter) # Move on to the next row, until iter_next returns None.
assert(removed == len(duplicates))
return (len(duplicates), removed)
def rename(self, new_name):
@ -265,7 +273,7 @@ class Log(Gtk.ListStore):
def get_duplicates(self):
""" Find the duplicates in the log, based on the CALL, QSO_DATE, and TIME_ON fields.
:returns: A list of row IDs corresponding to the duplicate records.
:returns: A list of indices/ids corresponding to the duplicate records.
:rtype: list
"""
duplicates = []
@ -273,13 +281,14 @@ class Log(Gtk.ListStore):
with self.connection:
c = self.connection.cursor()
c.execute(
"""SELECT rowid FROM %s WHERE rowid NOT IN
"""SELECT id FROM %s WHERE id NOT IN
(
SELECT MIN(rowid) FROM %s GROUP BY call, qso_date, time_on
SELECT MIN(id) FROM %s GROUP BY call, qso_date, time_on
)""" % (self.name, self.name))
result = c.fetchall()
for rowid in result:
duplicates.append(rowid[0]) # Get the integer from inside the tuple.
for index in result:
duplicates.append(index[0]) # Get the integer from inside the tuple.
duplicates.sort() # These indices should monotonically increasing, but let's sort the list just in case.
except (sqlite.Error, IndexError) as e:
logging.exception(e)
return duplicates

Wyświetl plik

@ -79,7 +79,7 @@ class Logbook:
path = None
dialog.destroy()
if(path is None): # If the Cancel button has been clicked, path will still be None
if(path is None): # If the Cancel button has been clicked, path will still be None.
logging.debug("No file path specified.")
return
else:
@ -110,13 +110,13 @@ class Logbook:
path = dialog.get_filename()
dialog.destroy()
if(path is None): # If the Cancel button has been clicked, path will still be None
if(path is None): # If the Cancel button has been clicked, path will still be None.
logging.debug("No file path specified.")
return False
connected = self.db_connect(path)
if(connected):
# If the connection setup was successful, then open all the logs in the database
# If the connection setup was successful, then open all the logs in the database.
self.path = path
@ -198,7 +198,7 @@ class Logbook:
"""
logging.debug("Attempting to connect to the logbook database...")
# Try setting up the SQL database connection
# Try setting up the SQL database connection.
try:
self.db_disconnect() # Destroy any existing connections first.
self.connection = sqlite.connect(path)
@ -259,6 +259,7 @@ class Logbook:
try:
with self.connection:
c = self.connection.cursor()
# NOTE: "id" is simply an alias for the "rowid" column here.
query = "CREATE TABLE %s (id INTEGER PRIMARY KEY AUTOINCREMENT" % log_name
for field_name in AVAILABLE_FIELD_NAMES_ORDERED:
s = ", %s TEXT" % field_name.lower()
@ -277,7 +278,8 @@ class Logbook:
ln.dialog.destroy()
l = Log(self.connection, log_name) # Empty log
# Instantiate and populate a new Log object.
l = Log(self.connection, log_name)
l.populate()
self.logs.append(l)
@ -296,12 +298,12 @@ class Logbook:
return
if(page is None):
page_index = self.notebook.get_current_page() # Gets the index of the selected tab in the logbook
page_index = self.notebook.get_current_page() # Get the index of the selected tab in the logbook.
if(page_index == 0): # If we are on the Summary page...
logging.debug("No log currently selected!")
return
else:
page = self.notebook.get_nth_page(page_index) # Gets the Gtk.VBox of the selected tab in the logbook
page = self.notebook.get_nth_page(page_index) # Get the Gtk.VBox of the selected tab in the logbook.
log_index = self.get_log_index(name=page.get_name())
log = self.logs[log_index]
@ -310,7 +312,7 @@ class Logbook:
# This may not be the same as what get_current_page() returns.
page_index = self.notebook.page_num(page)
if(page_index == 0 or page_index == self.notebook.get_n_pages()-1): # Only the "New Log" tab is present (i.e. no actual logs in the logbook)
if(page_index == 0 or page_index == self.notebook.get_n_pages()-1): # Only the "New Log" tab is present (i.e. no actual logs in the logbook).
logging.debug("No logs to delete!")
return
@ -326,12 +328,12 @@ class Logbook:
return
self.logs.pop(log_index)
# Remove the log from the renderers too
# Remove the log from the renderers too.
self.treeview.pop(log_index)
self.treeselection.pop(log_index)
self.sorter.pop(log_index)
self.filter.pop(log_index)
# And finally remove the tab in the Logbook
# And finally remove the tab in the Logbook.
self.notebook.remove_page(page_index)
self.summary.update()
@ -370,7 +372,7 @@ class Logbook:
:arg int index: The index of the Log (in the list of Logs) to render.
"""
self.filter.append(self.logs[index].filter_new(root=None))
# Set the callsign column as the column we want to filter by
# Set the callsign column as the column we want to filter by.
self.filter[index].set_visible_func(self.filter_by_callsign, data=None)
self.sorter.append(Gtk.TreeModelSort(model=self.filter[index]))
self.sorter[index].set_sort_column_id(0, Gtk.SortType.ASCENDING)
@ -380,7 +382,7 @@ class Logbook:
self.treeview[index].connect("row-activated", self.edit_record_callback)
self.treeselection.append(self.treeview[index].get_selection())
self.treeselection[index].set_mode(Gtk.SelectionMode.SINGLE)
# Allow the Log to be scrolled up/down
# Allow the Log to be scrolled up/down.
sw = Gtk.ScrolledWindow()
sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
@ -395,7 +397,7 @@ class Logbook:
hbox.pack_start(label, False, False, 0)
hbox.show_all()
self.notebook.insert_page(vbox, hbox, index+1) # Append the new log as a new tab
self.notebook.insert_page(vbox, hbox, index+1) # Append the new log as a new tab.
# The first column of the logbook will always be the unique record index.
# Let's append this separately to the field names.
@ -561,7 +563,7 @@ class Logbook:
if(response == Gtk.ResponseType.OK):
log_name = ln.name
if(self.log_name_exists(log_name)):
# Import into existing log
# Import into existing log.
exists = True
l = self.logs[self.get_log_index(name=log_name)]
response = question(parent=ln.dialog, message="Are you sure you want to import into an existing log?")
@ -573,7 +575,7 @@ class Logbook:
ln.dialog.destroy()
return
else:
# Create a new log with the name the user supplies
# Create a new log with the name the user supplies.
exists = False
try:
with self.connection:
@ -597,7 +599,6 @@ class Logbook:
ln.dialog.destroy()
adif = ADIF()
logging.debug("Importing records from the ADIF file with path: %s" % path)
records = adif.read(path)
l.add_record(records)
l.populate()
@ -605,14 +606,18 @@ class Logbook:
if(not exists):
self.logs.append(l)
self.render_log(self.log_count-1)
# Update statistics, etc.
self.summary.update()
self.application.toolbox.awards.count(self)
info(parent=self.application.window, message="Imported %d QSOs into log '%s'." % (len(records), l.name))
return
def export_log_adif(self, widget=None):
""" Export the log (that is currently selected) to an ADIF file. """
page_index = self.notebook.get_current_page() # Gets the index of the selected tab in the logbook
page_index = self.notebook.get_current_page() # Get the index of the selected tab in the logbook.
if(page_index == 0): # If we are on the Summary page...
logging.debug("No log currently selected!")
return
@ -651,14 +656,18 @@ class Logbook:
adif = ADIF()
records = log.records
if(records is not None):
adif.write(records, path)
try:
adif.write(records, path)
info(parent=self.application.window, message="Exported %d QSOs to %s in ADIF format." % (len(records), path))
except:
error(parent=self.application.window, message="Could not export the records.")
else:
error(self.application.window, "Could not retrieve the records from the SQL database. No records have been exported.")
error(parent=self.application.window, message="Could not retrieve the records from the SQL database. No records have been exported.")
return
def export_log_cabrillo(self, widget=None):
""" Export the log (that is currently selected) to a Cabrillo file. """
page_index = self.notebook.get_current_page() # Gets the index of the selected tab in the logbook
page_index = self.notebook.get_current_page() # Get the index of the selected tab in the logbook.
if(page_index == 0): # If we are on the Summary page...
logging.debug("No log currently selected!")
return
@ -708,15 +717,19 @@ class Logbook:
cabrillo = Cabrillo()
records = log.records
if(records is not None):
cabrillo.write(records, path, contest=contest, mycall=mycall)
try:
cabrillo.write(records, path, contest=contest, mycall=mycall)
info(parent=self.application.window, message="Exported %d QSOs to %s in Cabrillo format." % (len(records), path))
except:
error(parent=self.application.window, message="Could not export the records.")
else:
error(self.application.window, "Could not retrieve the records from the SQL database. No records have been exported.")
error(parent=self.application.window, message="Could not retrieve the records from the SQL database. No records have been exported.")
return
def print_log(self, widget=None):
""" Print all the records in the log (that is currently selected).
Note that only a few important fields are printed because of the restricted width of the page. """
page_index = self.notebook.get_current_page() # Gets the index of the selected tab in the logbook
page_index = self.notebook.get_current_page() # Get the index of the selected tab in the logbook.
if(page_index == 0): # If we are on the Summary page...
logging.debug("No log currently selected!")
return
@ -732,7 +745,7 @@ class Logbook:
def add_record_callback(self, widget):
""" A callback function used to add a particular record/QSO. """
# Get the log index
# Get the log index.
try:
log_index = self.get_log_index()
if(log_index is None):
@ -800,7 +813,7 @@ class Logbook:
def delete_record_callback(self, widget):
""" A callback function used to delete a particular record/QSO. """
# Get the log index
# Get the log index.
try:
log_index = self.get_log_index()
if(log_index is None):
@ -830,13 +843,13 @@ class Logbook:
self.application.toolbox.awards.count(self)
return
def edit_record_callback(self, widget, path, view_column):
def edit_record_callback(self, widget, path=None, view_column=None):
""" A callback function used to edit a particular record/QSO.
Note that the widget, path and view_column arguments are not used,
but need to be passed in since they associated with the row-activated signal
but need to be passed in since they are associated with the row-activated signal
which is generated when the user double-clicks on a record. """
# Get the log index
# Get the log index.
try:
log_index = self.get_log_index()
if(log_index is None):
@ -846,7 +859,7 @@ class Logbook:
return
log = self.logs[log_index]
(sort_model, path) = self.treeselection[log_index].get_selected_rows() # Get the selected row in the log
(sort_model, path) = self.treeselection[log_index].get_selected_rows() # Get the selected row in the log.
try:
sort_iter = sort_model.get_iter(path[0])
filter_iter = self.sorter[log_index].convert_iter_to_child_iter(sort_iter)
@ -900,14 +913,20 @@ class Logbook:
def remove_duplicates_callback(self, widget=None):
""" Remove duplicate records in a log.
Detecting duplicate records is done based on the CALL, QSO_DATE, TIME_ON, FREQ, and MODE fields. """
Detecting duplicate records is done based on the CALL, QSO_DATE, and TIME_ON fields. """
logging.debug("Removing duplicate records...")
log_index = self.get_log_index()
log = self.logs[log_index]
(number_of_duplicates, number_of_duplicates_removed) = log.remove_duplicates()
info(self.application.window, "Found %d duplicate(s). Successfully removed %d duplicate(s)." % (number_of_duplicates, number_of_duplicates_removed))
info(parent=self.application.window, message="Found %d duplicate(s). Successfully removed %d duplicate(s)." % (number_of_duplicates, number_of_duplicates_removed))
if(number_of_duplicates_removed > 0):
# Update statistics.
self.summary.update()
self.application.toolbox.awards.count(self)
return
@property
@ -956,8 +975,8 @@ class Logbook:
:rtype: int
"""
if(name is None):
# If no page name is supplied, then just use the currently selected page
page_index = self.notebook.get_current_page() # Gets the index of the selected tab in the logbook
# If no page name is supplied, then just use the currently selected page.
page_index = self.notebook.get_current_page() # Get the index of the selected tab in the logbook.
if(page_index == 0 or page_index == self.notebook.get_n_pages()-1):
# We either have the Summary page, or the "+" (add log) blank/dummy page.
logging.debug("No log currently selected!")
@ -979,7 +998,7 @@ class Logbook:
:returns: A list containing all the logs in the logbook, or None if the retrieval was unsuccessful.
:rtype: list
"""
logs = [] # A fresh stack of Log objects
logs = []
try:
with self.connection:
c = self.connection.cursor()