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] fields_and_data = [fields_and_data]
with self.connection: with self.connection:
# Get all the column names in the current database table.
c = self.connection.cursor() c = self.connection.cursor()
# Get all the column names in the current database table.
c.execute("PRAGMA table_info(%s)" % self.name) c.execute("PRAGMA table_info(%s)" % self.name)
column_names = c.fetchall() 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] last_index = c.fetchone()[0]
if last_index is None: if last_index is None:
# Assume no records are currently present. # Assume no records are currently present.
@ -130,7 +132,7 @@ class Log(Gtk.ListStore):
# Construct the SQL query. # Construct the SQL query.
query = "INSERT INTO %s VALUES (NULL" % self.name 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 + ",?"
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()))): 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()]) database_entry.append(fields_and_data[r][column_name.upper()])
else: 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_entry.append("")
database_entries.append(database_entry) database_entries.append(database_entry)
# Add the data to the ListStore as well. # Insert records in the database.
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.
with self.connection: with self.connection:
c = self.connection.cursor()
c.executemany(query, database_entries) 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.") logging.debug("Successfully added the record(s) to the log.")
return 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. """ 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 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. :raises sqlite.Error, IndexError: if the record could not be deleted.
""" """
logging.debug("Deleting record from log...") logging.debug("Deleting record from log...")
# Get the selected row in the logbook # Get the selected row in the logbook.
try: try:
with self.connection: with self.connection:
c = self.connection.cursor() 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 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 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 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 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: Should always be given. A default value of None is used for unit testing purposes only. :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. :raises sqlite.Error, IndexError: if the record could not be edited.
""" """
logging.debug("Editing field '%s' in record %d..." % (field_name, index)) 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. 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). 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. iter = self.get_iter_first() # Start with the first row in the log.
path = Gtk.TreePath(0) # 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).
iter = self.get_iter(path) while iter is not None:
while iter is not None: row_index = self.get_value(iter, 0) # Get the index.
row_index = self.get_value(iter, 0) # Get the index. if(row_index in duplicates): # Is this a duplicate row? If so, delete it.
if(row_index in duplicates): # Is this a duplicate row? If so, delete it. self.delete_record(row_index, iter)
self.delete_record(row_index, iter) removed += 1
removed += 1 iter = prev # Go back to the iter before the record that was just removed and continue from there.
break continue
iter = self.iter_next(iter) # Move on to the next row, until iter_next returns None. 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) return (len(duplicates), removed)
def rename(self, new_name): def rename(self, new_name):
@ -265,7 +273,7 @@ class Log(Gtk.ListStore):
def get_duplicates(self): def get_duplicates(self):
""" Find the duplicates in the log, based on the CALL, QSO_DATE, and TIME_ON fields. """ 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 :rtype: list
""" """
duplicates = [] duplicates = []
@ -273,13 +281,14 @@ class Log(Gtk.ListStore):
with self.connection: with self.connection:
c = self.connection.cursor() c = self.connection.cursor()
c.execute( 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)) )""" % (self.name, self.name))
result = c.fetchall() result = c.fetchall()
for rowid in result: for index in result:
duplicates.append(rowid[0]) # Get the integer from inside the tuple. 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: except (sqlite.Error, IndexError) as e:
logging.exception(e) logging.exception(e)
return duplicates return duplicates

Wyświetl plik

@ -79,7 +79,7 @@ class Logbook:
path = None path = None
dialog.destroy() 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.") logging.debug("No file path specified.")
return return
else: else:
@ -110,13 +110,13 @@ class Logbook:
path = dialog.get_filename() path = dialog.get_filename()
dialog.destroy() 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.") logging.debug("No file path specified.")
return False return False
connected = self.db_connect(path) connected = self.db_connect(path)
if(connected): 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 self.path = path
@ -198,7 +198,7 @@ class Logbook:
""" """
logging.debug("Attempting to connect to the logbook database...") logging.debug("Attempting to connect to the logbook database...")
# Try setting up the SQL database connection # Try setting up the SQL database connection.
try: try:
self.db_disconnect() # Destroy any existing connections first. self.db_disconnect() # Destroy any existing connections first.
self.connection = sqlite.connect(path) self.connection = sqlite.connect(path)
@ -259,6 +259,7 @@ class Logbook:
try: try:
with self.connection: with self.connection:
c = self.connection.cursor() 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 query = "CREATE TABLE %s (id INTEGER PRIMARY KEY AUTOINCREMENT" % log_name
for field_name in AVAILABLE_FIELD_NAMES_ORDERED: for field_name in AVAILABLE_FIELD_NAMES_ORDERED:
s = ", %s TEXT" % field_name.lower() s = ", %s TEXT" % field_name.lower()
@ -277,7 +278,8 @@ class Logbook:
ln.dialog.destroy() 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() l.populate()
self.logs.append(l) self.logs.append(l)
@ -296,12 +298,12 @@ class Logbook:
return return
if(page is None): 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... if(page_index == 0): # If we are on the Summary page...
logging.debug("No log currently selected!") logging.debug("No log currently selected!")
return return
else: 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_index = self.get_log_index(name=page.get_name())
log = self.logs[log_index] log = self.logs[log_index]
@ -310,7 +312,7 @@ class Logbook:
# This may not be the same as what get_current_page() returns. # This may not be the same as what get_current_page() returns.
page_index = self.notebook.page_num(page) 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!") logging.debug("No logs to delete!")
return return
@ -326,12 +328,12 @@ class Logbook:
return return
self.logs.pop(log_index) 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.treeview.pop(log_index)
self.treeselection.pop(log_index) self.treeselection.pop(log_index)
self.sorter.pop(log_index) self.sorter.pop(log_index)
self.filter.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.notebook.remove_page(page_index)
self.summary.update() self.summary.update()
@ -370,7 +372,7 @@ class Logbook:
:arg int index: The index of the Log (in the list of Logs) to render. :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)) 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.filter[index].set_visible_func(self.filter_by_callsign, data=None)
self.sorter.append(Gtk.TreeModelSort(model=self.filter[index])) self.sorter.append(Gtk.TreeModelSort(model=self.filter[index]))
self.sorter[index].set_sort_column_id(0, Gtk.SortType.ASCENDING) 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.treeview[index].connect("row-activated", self.edit_record_callback)
self.treeselection.append(self.treeview[index].get_selection()) self.treeselection.append(self.treeview[index].get_selection())
self.treeselection[index].set_mode(Gtk.SelectionMode.SINGLE) 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 = Gtk.ScrolledWindow()
sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN) sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
@ -395,7 +397,7 @@ class Logbook:
hbox.pack_start(label, False, False, 0) hbox.pack_start(label, False, False, 0)
hbox.show_all() 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. # The first column of the logbook will always be the unique record index.
# Let's append this separately to the field names. # Let's append this separately to the field names.
@ -561,7 +563,7 @@ class Logbook:
if(response == Gtk.ResponseType.OK): if(response == Gtk.ResponseType.OK):
log_name = ln.name log_name = ln.name
if(self.log_name_exists(log_name)): if(self.log_name_exists(log_name)):
# Import into existing log # Import into existing log.
exists = True exists = True
l = self.logs[self.get_log_index(name=log_name)] 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?") 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() ln.dialog.destroy()
return return
else: else:
# Create a new log with the name the user supplies # Create a new log with the name the user supplies.
exists = False exists = False
try: try:
with self.connection: with self.connection:
@ -597,7 +599,6 @@ class Logbook:
ln.dialog.destroy() ln.dialog.destroy()
adif = ADIF() adif = ADIF()
logging.debug("Importing records from the ADIF file with path: %s" % path)
records = adif.read(path) records = adif.read(path)
l.add_record(records) l.add_record(records)
l.populate() l.populate()
@ -605,14 +606,18 @@ class Logbook:
if(not exists): if(not exists):
self.logs.append(l) self.logs.append(l)
self.render_log(self.log_count-1) self.render_log(self.log_count-1)
# Update statistics, etc.
self.summary.update() self.summary.update()
self.application.toolbox.awards.count(self) self.application.toolbox.awards.count(self)
info(parent=self.application.window, message="Imported %d QSOs into log '%s'." % (len(records), l.name))
return return
def export_log_adif(self, widget=None): def export_log_adif(self, widget=None):
""" Export the log (that is currently selected) to an ADIF file. """ """ 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... if(page_index == 0): # If we are on the Summary page...
logging.debug("No log currently selected!") logging.debug("No log currently selected!")
return return
@ -651,14 +656,18 @@ class Logbook:
adif = ADIF() adif = ADIF()
records = log.records records = log.records
if(records is not None): 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: 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 return
def export_log_cabrillo(self, widget=None): def export_log_cabrillo(self, widget=None):
""" Export the log (that is currently selected) to a Cabrillo file. """ """ 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... if(page_index == 0): # If we are on the Summary page...
logging.debug("No log currently selected!") logging.debug("No log currently selected!")
return return
@ -708,15 +717,19 @@ class Logbook:
cabrillo = Cabrillo() cabrillo = Cabrillo()
records = log.records records = log.records
if(records is not None): 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: 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 return
def print_log(self, widget=None): def print_log(self, widget=None):
""" Print all the records in the log (that is currently selected). """ 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. """ 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... if(page_index == 0): # If we are on the Summary page...
logging.debug("No log currently selected!") logging.debug("No log currently selected!")
return return
@ -732,7 +745,7 @@ class Logbook:
def add_record_callback(self, widget): def add_record_callback(self, widget):
""" A callback function used to add a particular record/QSO. """ """ A callback function used to add a particular record/QSO. """
# Get the log index # Get the log index.
try: try:
log_index = self.get_log_index() log_index = self.get_log_index()
if(log_index is None): if(log_index is None):
@ -800,7 +813,7 @@ class Logbook:
def delete_record_callback(self, widget): def delete_record_callback(self, widget):
""" A callback function used to delete a particular record/QSO. """ """ A callback function used to delete a particular record/QSO. """
# Get the log index # Get the log index.
try: try:
log_index = self.get_log_index() log_index = self.get_log_index()
if(log_index is None): if(log_index is None):
@ -830,13 +843,13 @@ class Logbook:
self.application.toolbox.awards.count(self) self.application.toolbox.awards.count(self)
return 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. """ A callback function used to edit a particular record/QSO.
Note that the widget, path and view_column arguments are not used, 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. """ which is generated when the user double-clicks on a record. """
# Get the log index # Get the log index.
try: try:
log_index = self.get_log_index() log_index = self.get_log_index()
if(log_index is None): if(log_index is None):
@ -846,7 +859,7 @@ class Logbook:
return return
log = self.logs[log_index] 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: try:
sort_iter = sort_model.get_iter(path[0]) sort_iter = sort_model.get_iter(path[0])
filter_iter = self.sorter[log_index].convert_iter_to_child_iter(sort_iter) 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): def remove_duplicates_callback(self, widget=None):
""" Remove duplicate records in a log. """ 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...") logging.debug("Removing duplicate records...")
log_index = self.get_log_index() log_index = self.get_log_index()
log = self.logs[log_index] log = self.logs[log_index]
(number_of_duplicates, number_of_duplicates_removed) = log.remove_duplicates() (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 return
@property @property
@ -956,8 +975,8 @@ class Logbook:
:rtype: int :rtype: int
""" """
if(name is None): if(name is None):
# If no page name is supplied, then just use the currently selected page # 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 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): 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. # We either have the Summary page, or the "+" (add log) blank/dummy page.
logging.debug("No log currently selected!") 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. :returns: A list containing all the logs in the logbook, or None if the retrieval was unsuccessful.
:rtype: list :rtype: list
""" """
logs = [] # A fresh stack of Log objects logs = []
try: try:
with self.connection: with self.connection:
c = self.connection.cursor() c = self.connection.cursor()