2015-10-12 00:23:37 +00:00
#!/usr/bin/env python3
2013-03-22 22:16:31 +00:00
2018-02-24 13:24:39 +00:00
# Copyright (C) 2012-2018 Christian Thomas Jacobs.
2013-03-22 22:16:31 +00:00
# This file is part of PyQSO.
# PyQSO is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyQSO is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with PyQSO. If not, see <http://www.gnu.org/licenses/>.
2017-04-11 21:47:16 +00:00
from gi . repository import Gtk
2013-03-22 22:16:31 +00:00
import logging
2013-04-14 14:18:46 +00:00
import sqlite3 as sqlite
2018-02-04 23:15:57 +00:00
import json
2017-04-14 18:48:09 +00:00
from os . path import expanduser
2017-02-07 14:17:14 +00:00
try :
import configparser
except ImportError :
import ConfigParser as configparser
2016-01-27 16:23:09 +00:00
2015-10-12 00:23:37 +00:00
from pyqso . adif import *
2017-04-05 21:42:43 +00:00
from pyqso . cabrillo import *
2015-10-12 00:23:37 +00:00
from pyqso . log import *
from pyqso . auxiliary_dialogs import *
2017-02-24 00:20:43 +00:00
from pyqso . log_name_dialog import LogNameDialog
2017-03-01 10:16:03 +00:00
from pyqso . record_dialog import RecordDialog
2017-04-05 21:54:26 +00:00
from pyqso . cabrillo_export_dialog import CabrilloExportDialog
2017-04-14 20:33:16 +00:00
from pyqso . summary import Summary
2017-04-14 22:14:27 +00:00
from pyqso . blank import Blank
2017-04-14 18:48:09 +00:00
from pyqso . printer import Printer
from pyqso . compare import compare_date_and_time , compare_default
2016-01-27 16:23:09 +00:00
2017-03-02 09:56:45 +00:00
2017-02-21 19:36:25 +00:00
class Logbook :
2016-01-27 16:23:09 +00:00
""" A Logbook object can store multiple Log objects. """
2017-03-31 09:06:11 +00:00
def __init__ ( self , application ) :
2016-01-27 16:23:09 +00:00
""" Create a new Logbook object and initialise the list of Logs.
2017-03-31 09:06:11 +00:00
: arg application : The PyQSO application containing the main Gtk window , etc .
2016-01-27 16:23:09 +00:00
"""
2017-03-31 09:06:11 +00:00
self . application = application
self . builder = self . application . builder
2017-02-21 19:36:25 +00:00
self . notebook = self . builder . get_object ( " logbook " )
2016-01-27 16:23:09 +00:00
self . connection = None
self . logs = [ ]
2017-06-30 11:30:20 +00:00
2016-01-27 16:23:09 +00:00
return
def new ( self , widget = None ) :
2017-06-24 14:08:20 +00:00
""" Create a new logbook, and open it.
: returns : True if the new logbook is successfully opened , and False otherwise .
: rtype : bool
"""
2016-01-27 16:23:09 +00:00
# Get the new file's path from a dialog.
dialog = Gtk . FileChooserDialog ( " Create a New SQLite Database File " ,
2017-03-31 09:06:11 +00:00
self . application . window ,
2016-01-27 16:23:09 +00:00
Gtk . FileChooserAction . SAVE ,
2017-02-24 00:20:43 +00:00
( Gtk . STOCK_CANCEL , Gtk . ResponseType . CANCEL ,
2017-02-24 00:27:03 +00:00
Gtk . STOCK_SAVE , Gtk . ResponseType . OK ) )
2016-01-27 16:23:09 +00:00
dialog . set_do_overwrite_confirmation ( True )
response = dialog . run ( )
if ( response == Gtk . ResponseType . OK ) :
2015-02-07 21:22:16 +00:00
path = dialog . get_filename ( )
2016-01-27 16:23:09 +00:00
else :
path = None
2017-02-23 22:40:47 +00:00
dialog . destroy ( )
2016-01-27 16:23:09 +00:00
2017-06-27 19:10:20 +00:00
if ( path is None ) : # If the Cancel button has been clicked, path will still be None.
2015-02-07 21:22:16 +00:00
logging . debug ( " No file path specified. " )
return
2016-01-27 16:23:09 +00:00
else :
# Clear the contents of the file, in case the file exists already.
open ( path , ' w ' ) . close ( )
# Open the new logbook, ready for use.
2017-06-24 14:08:20 +00:00
opened = self . open ( path = path )
return opened
2016-01-27 16:23:09 +00:00
2017-06-24 19:56:04 +00:00
def open ( self , widget = None , path = None ) :
2016-01-27 16:23:09 +00:00
""" Open a logbook, and render all the logs within it.
: arg str path : An optional argument containing the database file location , if already known . If this is None , a file selection dialog will appear .
2017-06-24 14:08:20 +00:00
: returns : True if the logbook is successfully opened , and False otherwise .
: rtype : bool
2016-01-27 16:23:09 +00:00
"""
if ( path is None ) :
# If no path has been provided, get one from a "File Open" dialog.
dialog = Gtk . FileChooserDialog ( " Open SQLite Database File " ,
2017-03-31 09:06:11 +00:00
self . application . window ,
2016-01-27 16:23:09 +00:00
Gtk . FileChooserAction . OPEN ,
2017-02-24 00:20:43 +00:00
( Gtk . STOCK_CANCEL , Gtk . ResponseType . CANCEL ,
2017-02-24 00:27:03 +00:00
Gtk . STOCK_OPEN , Gtk . ResponseType . OK ) )
2013-09-14 18:27:45 +00:00
2016-01-27 16:23:09 +00:00
response = dialog . run ( )
if ( response == Gtk . ResponseType . OK ) :
path = dialog . get_filename ( )
2017-02-23 22:40:47 +00:00
dialog . destroy ( )
2016-01-27 16:23:09 +00:00
2017-06-27 19:10:20 +00:00
if ( path is None ) : # If the Cancel button has been clicked, path will still be None.
2016-01-27 16:23:09 +00:00
logging . debug ( " No file path specified. " )
2017-06-24 14:08:20 +00:00
return False
2016-01-27 16:23:09 +00:00
connected = self . db_connect ( path )
if ( connected ) :
2017-06-27 19:10:20 +00:00
# If the connection setup was successful, then open all the logs in the database.
2016-01-27 16:23:09 +00:00
self . path = path
2017-04-20 21:50:08 +00:00
logging . debug ( " Retrieving all the logs in the logbook... " )
2017-07-03 21:35:28 +00:00
try :
self . logs = self . get_logs ( )
except ( sqlite . Error , IndexError ) as e :
logging . exception ( e )
2017-04-20 21:50:08 +00:00
error ( parent = self . application . window , message = " Could not open logbook. Something went wrong when trying to retrieve the logs. Perhaps the logbook file is encrypted, corrupted, or in the wrong format? " )
2017-06-24 14:08:20 +00:00
return False
2017-07-03 21:35:28 +00:00
logging . debug ( " All logs retrieved successfully. " )
2016-01-27 16:23:09 +00:00
2017-06-24 19:56:04 +00:00
logging . debug ( " Rendering logs... " )
# For rendering the logs. One treeview and one treeselection per Log.
self . treeview = [ ]
self . treeselection = [ ]
self . sorter = [ ]
self . filter = [ ]
self . summary = Summary ( self . application )
self . blank = Blank ( self . application )
2016-01-27 16:23:09 +00:00
2017-06-24 19:56:04 +00:00
# FIXME: This is an unfortunate work-around. If the area around the "+/New Log" button
# is clicked, PyQSO will change to an empty page. This signal is used to stop this from happening.
self . notebook . connect ( " switch-page " , self . on_switch_page )
2016-01-27 16:23:09 +00:00
2017-06-24 19:56:04 +00:00
for i in range ( len ( self . logs ) ) :
self . render_log ( i )
logging . debug ( " All logs rendered successfully. " )
2016-01-27 16:23:09 +00:00
2017-06-24 19:56:04 +00:00
self . summary . update ( )
self . application . toolbox . awards . count ( self )
2016-01-27 16:23:09 +00:00
2017-06-24 19:56:04 +00:00
context_id = self . application . statusbar . get_context_id ( " Status " )
self . application . statusbar . push ( context_id , " Logbook: %s " % self . path )
self . application . toolbar . set_logbook_button_sensitive ( False )
self . application . menu . set_logbook_item_sensitive ( False )
self . application . menu . set_log_items_sensitive ( True )
self . application . toolbar . filter_source . set_sensitive ( True )
2016-01-27 16:23:09 +00:00
2017-06-24 19:56:04 +00:00
self . notebook . show_all ( )
2016-01-27 16:23:09 +00:00
else :
logging . debug ( " Not connected to a logbook. No logs were opened. " )
2017-06-24 14:08:20 +00:00
return False
2016-01-27 16:23:09 +00:00
2017-06-24 14:08:20 +00:00
return True
2016-01-27 16:23:09 +00:00
def close ( self , widget = None ) :
2017-06-24 14:08:20 +00:00
""" Close the logbook that is currently open.
: returns : True if the logbook is successfully closed , and False otherwise .
: rtype : bool
"""
2016-01-27 16:23:09 +00:00
disconnected = self . db_disconnect ( )
if ( disconnected ) :
logging . debug ( " Closing all logs in the logbook... " )
2017-02-21 19:36:25 +00:00
while ( self . notebook . get_n_pages ( ) > 0 ) :
2016-01-27 16:23:09 +00:00
# Once a page is removed, the other pages get re-numbered,
# so a 'for' loop isn't the best option here.
2017-02-21 19:36:25 +00:00
self . notebook . remove_page ( 0 )
2016-01-27 16:23:09 +00:00
logging . debug ( " All logs now closed. " )
2017-03-31 09:06:11 +00:00
context_id = self . application . statusbar . get_context_id ( " Status " )
self . application . statusbar . push ( context_id , " No logbook is currently open. " )
self . application . toolbar . set_logbook_button_sensitive ( True )
self . application . menu . set_logbook_item_sensitive ( True )
self . application . menu . set_log_items_sensitive ( False )
self . application . toolbar . filter_source . set_sensitive ( False )
2016-01-27 16:23:09 +00:00
else :
logging . debug ( " Unable to disconnect from the database. No logs were closed. " )
2017-06-24 14:08:20 +00:00
return False
return True
2016-01-27 16:23:09 +00:00
def db_connect ( self , path ) :
""" Create an SQL database connection to the Logbook ' s data source.
: arg str path : The path of the database file .
"""
logging . debug ( " Attempting to connect to the logbook database... " )
2017-06-27 19:10:20 +00:00
# Try setting up the SQL database connection.
2016-01-27 16:23:09 +00:00
try :
self . db_disconnect ( ) # Destroy any existing connections first.
self . connection = sqlite . connect ( path )
self . connection . row_factory = sqlite . Row
except sqlite . Error as e :
2017-07-03 12:27:12 +00:00
# Cannot connect to the database.
2013-04-15 01:37:02 +00:00
logging . exception ( e )
2017-07-03 12:27:12 +00:00
error ( parent = self . application . window , message = " Cannot connect to the database. Check file permissions? " )
2013-09-14 18:27:45 +00:00
return False
2016-01-27 16:23:09 +00:00
logging . debug ( " Database connection created successfully! " )
return True
def db_disconnect ( self ) :
""" Destroy the connection to the Logbook ' s data source.
: returns : True if the connection was successfully destroyed , and False otherwise .
: rtype : bool
"""
logging . debug ( " Cleaning up any existing database connections... " )
if ( self . connection ) :
2013-04-21 23:15:07 +00:00
try :
2016-01-27 16:23:09 +00:00
self . connection . close ( )
2013-04-21 23:15:07 +00:00
except sqlite . Error as e :
2016-01-27 16:23:09 +00:00
logging . exception ( e )
return False
else :
logging . debug ( " Already disconnected. Nothing to do here. " )
return True
2017-04-14 20:45:59 +00:00
def on_switch_page ( self , widget , label , new_page ) :
2016-01-27 16:23:09 +00:00
""" Handle a tab/page change, and enable/disable the relevant Record-related buttons. """
2017-02-21 19:36:25 +00:00
if ( new_page == self . notebook . get_n_pages ( ) - 1 ) : # The last (right-most) tab is the "New Log" tab.
self . notebook . stop_emission ( " switch-page " )
2016-01-27 16:23:09 +00:00
# Disable the record buttons if a log page is not selected.
if ( new_page == 0 ) :
2017-03-31 09:06:11 +00:00
self . application . toolbar . set_record_buttons_sensitive ( False )
self . application . menu . set_record_items_sensitive ( False )
2016-01-27 16:23:09 +00:00
else :
2017-03-31 09:06:11 +00:00
self . application . toolbar . set_record_buttons_sensitive ( True )
self . application . menu . set_record_items_sensitive ( True )
2016-01-27 16:23:09 +00:00
return
2018-01-18 20:52:44 +00:00
def on_button_release_event ( self , treeview , event ) :
""" Show a popup menu when the user right-clicks a record in the logbook. """
if ( event . button == 3 ) :
self . application . popup . menu . popup ( None , None , None , None , event . button , event . time )
self . application . popup . menu . show_all ( )
return True
2016-01-27 16:23:09 +00:00
def new_log ( self , widget = None ) :
""" Create a new log in the logbook. """
if ( self . connection is None ) :
2013-09-14 20:33:01 +00:00
return
2016-01-27 16:23:09 +00:00
exists = True
2017-03-31 09:21:43 +00:00
ln = LogNameDialog ( self . application )
2016-01-27 16:23:09 +00:00
while ( exists ) :
2017-02-23 22:40:47 +00:00
response = ln . dialog . run ( )
2016-01-27 16:23:09 +00:00
if ( response == Gtk . ResponseType . OK ) :
2017-02-23 22:40:47 +00:00
log_name = ln . name
2016-01-27 16:23:09 +00:00
try :
with self . connection :
c = self . connection . cursor ( )
2017-06-27 19:10:20 +00:00
# NOTE: "id" is simply an alias for the "rowid" column here.
2016-01-27 16:23:09 +00:00
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 ( )
query = query + s
query = query + " ) "
c . execute ( query )
exists = False
except sqlite . Error as e :
logging . exception ( e )
# Data is not valid - inform the user.
2017-03-02 10:14:21 +00:00
error ( parent = ln . dialog , message = " Database error. Try another log name. " )
2016-01-27 16:23:09 +00:00
exists = True
else :
2017-02-23 22:40:47 +00:00
ln . dialog . destroy ( )
2016-01-27 16:23:09 +00:00
return
2017-02-23 22:40:47 +00:00
ln . dialog . destroy ( )
2016-01-27 16:23:09 +00:00
2017-06-27 19:10:20 +00:00
# Instantiate and populate a new Log object.
l = Log ( self . connection , log_name )
2016-01-27 16:23:09 +00:00
l . populate ( )
self . logs . append ( l )
2017-04-14 20:45:59 +00:00
self . render_log ( self . log_count - 1 )
self . summary . update ( )
2013-04-21 23:15:07 +00:00
2017-04-11 22:06:15 +00:00
self . notebook . set_current_page ( self . log_count )
2016-01-27 16:23:09 +00:00
return
def delete_log ( self , widget , page = None ) :
""" Delete the log that is currently selected in the logbook.
: arg Gtk . Widget page : An optional argument corresponding to the currently - selected page / tab .
"""
if ( self . connection is None ) :
return
if ( page is None ) :
2017-06-27 19:10:20 +00:00
page_index = self . notebook . get_current_page ( ) # Get the index of the selected tab in the logbook.
2016-01-27 16:23:09 +00:00
if ( page_index == 0 ) : # If we are on the Summary page...
logging . debug ( " No log currently selected! " )
return
else :
2017-06-27 19:10:20 +00:00
page = self . notebook . get_nth_page ( page_index ) # Get the Gtk.VBox of the selected tab in the logbook.
2016-01-27 16:23:09 +00:00
2017-04-14 20:45:59 +00:00
log_index = self . get_log_index ( name = page . get_name ( ) )
2016-01-27 16:23:09 +00:00
log = self . logs [ log_index ]
# We also need the page's index in order to remove it using remove_page below.
2017-04-05 21:42:43 +00:00
# This may not be the same as what get_current_page() returns.
2017-02-21 19:36:25 +00:00
page_index = self . notebook . page_num ( page )
2016-01-27 16:23:09 +00:00
2017-06-27 19:10:20 +00:00
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).
2016-01-27 16:23:09 +00:00
logging . debug ( " No logs to delete! " )
return
2017-03-31 09:06:11 +00:00
response = question ( parent = self . application . window , message = " Are you sure you want to delete log %s ? " % log . name )
2016-01-27 16:23:09 +00:00
if ( response == Gtk . ResponseType . YES ) :
try :
with self . connection :
c = self . connection . cursor ( )
c . execute ( " DROP TABLE %s " % log . name )
except sqlite . Error as e :
logging . exception ( e )
2017-03-31 09:06:11 +00:00
error ( parent = self . application . window , message = " Database error. Could not delete the log. " )
2016-01-27 16:23:09 +00:00
return
self . logs . pop ( log_index )
2017-06-27 19:10:20 +00:00
# Remove the log from the renderers too.
2016-01-27 16:23:09 +00:00
self . treeview . pop ( log_index )
self . treeselection . pop ( log_index )
self . sorter . pop ( log_index )
self . filter . pop ( log_index )
2017-06-27 19:10:20 +00:00
# And finally remove the tab in the Logbook.
2017-02-21 19:36:25 +00:00
self . notebook . remove_page ( page_index )
2016-01-27 16:23:09 +00:00
2017-04-14 20:45:59 +00:00
self . summary . update ( )
2017-03-31 09:06:11 +00:00
self . application . toolbox . awards . count ( self )
2016-01-27 16:23:09 +00:00
return
def filter_logs ( self , widget = None ) :
""" Re-filter all the logs when the user-defined expression is changed. """
for i in range ( 0 , len ( self . filter ) ) :
self . filter [ i ] . refilter ( )
return
2017-04-14 20:45:59 +00:00
def filter_by_callsign ( self , model , iter , data ) :
2016-01-27 16:23:09 +00:00
""" Filter all the logs in the logbook by the callsign field, based on a user-defined expression.
: arg Gtk . TreeModel model : The model used to filter the log data .
: arg Gtk . TreeIter iter : A pointer to a particular row in the model .
: arg data : The user - defined expression to filter by .
: returns : True if a record matches the expression , or if there is nothing to filter . Otherwise , returns False .
: rtype : bool
"""
value = model . get_value ( iter , 1 )
2017-03-31 09:06:11 +00:00
callsign = self . application . toolbar . filter_source . get_text ( )
2016-01-27 16:23:09 +00:00
if ( callsign is None or callsign == " " ) :
# If there is nothing to filter with, then show all the records!
return True
else :
# This should be case insensitive.
# Also, we could use value[:][0:len(callsign))] if we wanted to match from the very start of each callsign.
return callsign . upper ( ) in value or callsign . lower ( ) in value
2017-04-14 20:45:59 +00:00
def render_log ( self , index ) :
2016-01-27 16:23:09 +00:00
""" Render a Log in the Gtk.Notebook.
: 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 ) )
2017-06-27 19:10:20 +00:00
# Set the callsign column as the column we want to filter by.
2017-04-14 20:45:59 +00:00
self . filter [ index ] . set_visible_func ( self . filter_by_callsign , data = None )
2016-01-27 16:23:09 +00:00
self . sorter . append ( Gtk . TreeModelSort ( model = self . filter [ index ] ) )
self . sorter [ index ] . set_sort_column_id ( 0 , Gtk . SortType . ASCENDING )
2017-06-24 19:56:04 +00:00
self . treeview . append ( Gtk . TreeView ( model = self . sorter [ index ] ) )
2016-01-27 16:23:09 +00:00
self . treeview [ index ] . set_grid_lines ( Gtk . TreeViewGridLines . BOTH )
self . treeview [ index ] . connect ( " row-activated " , self . edit_record_callback )
2018-01-18 20:52:44 +00:00
self . treeview [ index ] . connect ( " button-release-event " , self . on_button_release_event )
2016-01-27 16:23:09 +00:00
self . treeselection . append ( self . treeview [ index ] . get_selection ( ) )
self . treeselection [ index ] . set_mode ( Gtk . SelectionMode . SINGLE )
2018-01-18 20:52:44 +00:00
2017-06-27 19:10:20 +00:00
# Allow the Log to be scrolled up/down.
2016-01-27 16:23:09 +00:00
sw = Gtk . ScrolledWindow ( )
sw . set_shadow_type ( Gtk . ShadowType . ETCHED_IN )
sw . set_policy ( Gtk . PolicyType . AUTOMATIC , Gtk . PolicyType . AUTOMATIC )
sw . add ( self . treeview [ index ] )
vbox = Gtk . VBox ( )
vbox . set_name ( self . logs [ index ] . name ) # Set a name for the tab itself so we can match it up with the associated Log object later.
vbox . pack_start ( sw , True , True , 0 )
# Add a close button to the tab
2017-06-24 19:56:04 +00:00
hbox = Gtk . HBox ( homogeneous = False , spacing = 0 )
label = Gtk . Label ( label = self . logs [ index ] . name )
2016-01-27 16:23:09 +00:00
hbox . pack_start ( label , False , False , 0 )
hbox . show_all ( )
2017-06-27 19:10:20 +00:00
self . notebook . insert_page ( vbox , hbox , index + 1 ) # Append the new log as a new tab.
2016-01-27 16:23:09 +00:00
# The first column of the logbook will always be the unique record index.
# Let's append this separately to the field names.
renderer = Gtk . CellRendererText ( )
column = Gtk . TreeViewColumn ( " Index " , renderer , text = 0 )
column . set_resizable ( True )
column . set_min_width ( 50 )
column . set_clickable ( True )
column . set_sort_order ( Gtk . SortType . ASCENDING )
column . set_sort_indicator ( True )
column . connect ( " clicked " , self . sort_log , 0 )
self . treeview [ index ] . append_column ( column )
# Set up column names for each selected field
field_names = AVAILABLE_FIELD_NAMES_ORDERED
for i in range ( 0 , len ( field_names ) ) :
renderer = Gtk . CellRendererText ( )
2017-07-13 16:21:26 +00:00
# Keep each row to a single line.
renderer . set_property ( " single-paragraph-mode " , True )
2016-01-27 16:23:09 +00:00
column = Gtk . TreeViewColumn ( AVAILABLE_FIELD_NAMES_FRIENDLY [ field_names [ i ] ] , renderer , text = i + 1 )
column . set_resizable ( True )
column . set_min_width ( 50 )
column . set_clickable ( True )
# Special cases
if ( field_names [ i ] == " NOTES " ) :
2017-07-13 16:21:26 +00:00
# Give the 'Notes' column some extra space, since this is likely to contain some long sentences ...
2016-01-27 16:23:09 +00:00
column . set_min_width ( 300 )
2017-07-13 16:21:26 +00:00
# ... but not too much extra space ...
column . set_max_width ( 600 )
# ... and don't let the column automatically re-size itself.
2016-01-27 16:23:09 +00:00
column . set_sizing ( Gtk . TreeViewColumnSizing . FIXED )
column . connect ( " clicked " , self . sort_log , i + 1 )
config = configparser . ConfigParser ( )
have_config = ( config . read ( expanduser ( ' ~/.config/pyqso/preferences.ini ' ) ) != [ ] )
( section , option ) = ( " view " , AVAILABLE_FIELD_NAMES_ORDERED [ i ] . lower ( ) )
if ( have_config and config . has_option ( section , option ) ) :
2017-07-09 10:42:42 +00:00
column . set_visible ( config . getboolean ( section , option ) )
2016-01-27 16:23:09 +00:00
self . treeview [ index ] . append_column ( column )
2017-02-21 19:36:25 +00:00
self . notebook . show_all ( )
2016-01-27 16:23:09 +00:00
return
def sort_log ( self , widget , column_index ) :
""" Sort the log (that is currently selected) with respect to a given field.
: arg int column_index : The index of the column to sort by .
"""
2017-04-14 20:45:59 +00:00
log_index = self . get_log_index ( )
2016-01-27 16:23:09 +00:00
column = self . treeview [ log_index ] . get_column ( column_index )
if ( AVAILABLE_FIELD_NAMES_ORDERED [ column_index - 1 ] == " QSO_DATE " ) :
# If the field being sorted is the QSO_DATE, then also sort by the TIME_ON field so we get the
# correct chronological order.
# Note: This assumes that the TIME_ON field is always immediately to the right of the QSO_DATE field.
2017-04-14 18:48:09 +00:00
self . sorter [ log_index ] . set_sort_func ( column_index , compare_date_and_time , user_data = [ column_index , column_index + 1 ] )
2016-01-27 16:23:09 +00:00
else :
2017-04-14 18:48:09 +00:00
self . sorter [ log_index ] . set_sort_func ( column_index , compare_default , user_data = column_index )
2016-01-27 16:23:09 +00:00
# If we are operating on the currently-sorted column...
if ( self . sorter [ log_index ] . get_sort_column_id ( ) [ 0 ] == column_index ) :
order = column . get_sort_order ( )
# ...then check if we need to reverse the order of searching.
if ( order == Gtk . SortType . ASCENDING ) :
self . sorter [ log_index ] . set_sort_column_id ( column_index , Gtk . SortType . DESCENDING )
column . set_sort_order ( Gtk . SortType . DESCENDING )
else :
self . sorter [ log_index ] . set_sort_column_id ( column_index , Gtk . SortType . ASCENDING )
column . set_sort_order ( Gtk . SortType . ASCENDING )
else :
# Otherwise, change to the new sorted column. Default to ASCENDING order.
2013-04-30 02:17:40 +00:00
self . sorter [ log_index ] . set_sort_column_id ( column_index , Gtk . SortType . ASCENDING )
column . set_sort_order ( Gtk . SortType . ASCENDING )
2016-01-27 16:23:09 +00:00
# Show an arrow pointing in the direction of the sorting.
# (First we need to remove the arrow from the previously-sorted column.
# Since we don't know which one that was, just remove the arrow from all columns
# and start again. This only loops over a few dozen columns at most, so
# hopefully it won't take too much time.)
for i in range ( 0 , len ( AVAILABLE_FIELD_NAMES_ORDERED ) ) :
column = self . treeview [ log_index ] . get_column ( i )
column . set_sort_indicator ( False )
column = self . treeview [ log_index ] . get_column ( column_index )
column . set_sort_indicator ( True )
return
def rename_log ( self , widget = None ) :
""" Rename the log that is currently selected. """
if ( self . connection is None ) :
return
2017-02-21 19:36:25 +00:00
page_index = self . notebook . get_current_page ( )
2016-01-27 16:23:09 +00:00
if ( page_index == 0 ) : # If we are on the Summary page...
logging . debug ( " No log currently selected! " )
return
2017-06-24 15:37:42 +00:00
page = self . notebook . get_nth_page ( page_index ) # Get the Gtk.VBox of the selected tab in the logbook.
2016-01-27 16:23:09 +00:00
old_log_name = page . get_name ( )
2017-04-14 20:45:59 +00:00
log_index = self . get_log_index ( name = old_log_name )
2016-01-27 16:23:09 +00:00
2017-04-14 23:55:52 +00:00
success = False
2017-03-31 09:21:43 +00:00
ln = LogNameDialog ( self . application , title = " Rename Log " , name = old_log_name )
2017-04-14 23:55:52 +00:00
while ( not success ) :
2017-02-23 23:38:29 +00:00
response = ln . dialog . run ( )
2016-01-27 16:23:09 +00:00
if ( response == Gtk . ResponseType . OK ) :
2017-02-23 23:38:29 +00:00
new_log_name = ln . name
2017-04-14 23:55:52 +00:00
success = self . logs [ log_index ] . rename ( new_log_name )
if ( success ) :
ln . dialog . destroy ( )
else :
# Unsuccessful rename attempt. Inform the user.
2017-03-02 10:14:21 +00:00
error ( parent = ln . dialog , message = " Database error. Try another log name. " )
2016-01-27 16:23:09 +00:00
else :
2017-02-23 23:38:29 +00:00
ln . dialog . destroy ( )
2016-01-27 16:23:09 +00:00
return
2017-04-14 23:59:21 +00:00
# Remember to change the page's name ...
2017-04-15 00:37:52 +00:00
page . set_name ( new_log_name )
2016-01-27 16:23:09 +00:00
2017-04-14 23:59:21 +00:00
# ... and update the tab's label.
2017-06-24 19:56:04 +00:00
hbox = Gtk . HBox ( homogeneous = False , spacing = 0 )
label = Gtk . Label ( label = new_log_name )
2016-01-27 16:23:09 +00:00
hbox . pack_start ( label , False , False , 0 )
hbox . show_all ( )
2017-02-21 19:36:25 +00:00
self . notebook . set_tab_label ( page , hbox )
2016-01-27 16:23:09 +00:00
# The number of logs will obviously stay the same, but
# we want to update the logbook's modification date.
2017-04-14 20:45:59 +00:00
self . summary . update ( )
2016-01-27 16:23:09 +00:00
return
def import_log ( self , widget = None ) :
""" Import a log from an ADIF file. """
2017-07-02 23:58:44 +00:00
# Get the path to the ADIF file.
2016-01-27 16:23:09 +00:00
dialog = Gtk . FileChooserDialog ( " Import ADIF Log File " ,
2017-03-31 09:06:11 +00:00
self . application . window ,
2016-01-27 16:23:09 +00:00
Gtk . FileChooserAction . OPEN ,
( Gtk . STOCK_CANCEL , Gtk . ResponseType . CANCEL ,
Gtk . STOCK_OPEN , Gtk . ResponseType . OK ) )
filter = Gtk . FileFilter ( )
filter . set_name ( " All ADIF files (*.adi, *.ADI) " )
filter . add_pattern ( " *.adi " )
filter . add_pattern ( " *.ADI " )
dialog . add_filter ( filter )
filter = Gtk . FileFilter ( )
filter . set_name ( " All files " )
filter . add_pattern ( " * " )
dialog . add_filter ( filter )
response = dialog . run ( )
if ( response == Gtk . ResponseType . OK ) :
path = dialog . get_filename ( )
else :
path = None
2017-02-23 23:38:29 +00:00
dialog . destroy ( )
2016-01-27 16:23:09 +00:00
if ( path is None ) :
logging . debug ( " No file path specified. " )
return
2017-07-03 12:27:12 +00:00
# Read the records.
adif = ADIF ( )
try :
2017-07-02 23:58:44 +00:00
records = adif . read ( path )
2017-07-03 12:27:12 +00:00
except IOError as e :
error ( parent = self . application . window , message = " Could not import the log. I/O error %d : %s " % ( e . errno , e . strerror ) )
return
except Exception as e :
error ( parent = self . application . window , message = " Could not import the log. " )
logging . exception ( e )
return
2016-01-27 16:23:09 +00:00
2017-07-02 23:58:44 +00:00
# Get the new log's name (or the name of the existing log the user wants to import into).
2017-03-31 09:21:43 +00:00
ln = LogNameDialog ( self . application , title = " Import Log " )
2016-01-27 16:23:09 +00:00
while ( True ) :
2017-02-23 23:38:29 +00:00
response = ln . dialog . run ( )
2016-01-27 16:23:09 +00:00
if ( response == Gtk . ResponseType . OK ) :
2017-02-23 23:38:29 +00:00
log_name = ln . name
2017-07-04 16:02:59 +00:00
# Check if the log name exists.
try :
exists = self . log_name_exists ( log_name )
except ( sqlite . Error , IndexError ) as e :
# Could not determine if the log name exists. It's safer to stop here than to try to add a new log.
logging . exception ( e )
error ( parent = ln . dialog , message = " Database error. Could not check if the log name exists. " )
ln . dialog . destroy ( )
return
if ( exists ) :
2017-06-27 19:10:20 +00:00
# Import into existing log.
2017-04-14 20:45:59 +00:00
l = self . logs [ self . get_log_index ( name = log_name ) ]
2017-03-02 10:14:21 +00:00
response = question ( parent = ln . dialog , message = " Are you sure you want to import into an existing log? " )
2016-01-27 16:23:09 +00:00
if ( response == Gtk . ResponseType . YES ) :
break
else :
2017-06-27 19:10:20 +00:00
# Create a new log with the name the user supplies.
2016-01-27 16:23:09 +00:00
try :
with self . connection :
c = self . connection . cursor ( )
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 ( )
query = query + s
query = query + " ) "
c . execute ( query )
l = Log ( self . connection , log_name )
break
except sqlite . Error as e :
logging . exception ( e )
# Data is not valid - inform the user.
2017-03-02 10:14:21 +00:00
error ( parent = ln . dialog , message = " Database error. Try another log name. " )
2016-01-27 16:23:09 +00:00
else :
2017-02-23 23:38:29 +00:00
ln . dialog . destroy ( )
2016-01-27 16:23:09 +00:00
return
2017-02-23 23:38:29 +00:00
ln . dialog . destroy ( )
2016-01-27 16:23:09 +00:00
2017-07-02 23:58:44 +00:00
# Update new or existing Log object.
l . add_record ( records )
l . populate ( )
2017-06-27 19:10:20 +00:00
2017-07-02 23:58:44 +00:00
if ( not exists ) :
self . logs . append ( l )
self . render_log ( self . log_count - 1 )
2017-07-02 23:43:38 +00:00
2017-07-02 23:58:44 +00:00
# Update statistics, etc.
self . summary . update ( )
self . application . toolbox . awards . count ( self )
2016-01-27 16:23:09 +00:00
2017-07-02 23:58:44 +00:00
info ( parent = self . application . window , message = " Imported %d QSOs into log ' %s ' . " % ( len ( records ) , l . name ) )
2017-06-27 19:10:20 +00:00
2016-01-27 16:23:09 +00:00
return
2017-04-05 21:42:43 +00:00
def export_log_adif ( self , widget = None ) :
2016-01-27 16:23:09 +00:00
""" Export the log (that is currently selected) to an ADIF file. """
2018-01-18 20:52:44 +00:00
# Get the index of the selected tab in the logbook.
try :
log_index = self . get_log_index ( )
if ( log_index is None ) :
raise ValueError ( " The log index could not be determined. Perhaps the Summary page is selected? " )
except ValueError as e :
error ( parent = self . application . window , message = e )
2013-05-10 13:05:33 +00:00
return
2016-01-27 16:23:09 +00:00
log = self . logs [ log_index ]
2017-04-05 21:42:43 +00:00
dialog = Gtk . FileChooserDialog ( " Export Log as ADIF " ,
2017-03-31 09:06:11 +00:00
self . application . window ,
2016-01-27 16:23:09 +00:00
Gtk . FileChooserAction . SAVE ,
( Gtk . STOCK_CANCEL , Gtk . ResponseType . CANCEL ,
Gtk . STOCK_SAVE , Gtk . ResponseType . OK ) )
dialog . set_do_overwrite_confirmation ( True )
filter = Gtk . FileFilter ( )
filter . set_name ( " All ADIF files (*.adi, *.ADI) " )
filter . add_pattern ( " *.adi " )
filter . add_pattern ( " *.ADI " )
dialog . add_filter ( filter )
filter = Gtk . FileFilter ( )
filter . set_name ( " All files " )
filter . add_pattern ( " * " )
dialog . add_filter ( filter )
response = dialog . run ( )
if ( response == Gtk . ResponseType . OK ) :
path = dialog . get_filename ( )
else :
path = None
2017-02-23 23:38:29 +00:00
dialog . destroy ( )
2016-01-27 16:23:09 +00:00
if ( path is None ) :
logging . debug ( " No file path specified. " )
else :
2017-07-03 12:27:12 +00:00
# Retrieve the log's records from the database.
try :
records = log . records
except sqlite . Error as e :
logging . exception ( e )
2017-07-03 21:35:28 +00:00
error ( parent = self . application . window , message = " Could not retrieve the records from the SQL database. No records have been exported. " )
2017-07-03 12:27:12 +00:00
return
# Write the records.
adif = ADIF ( )
try :
adif . write ( records , path )
info ( parent = self . application . window , message = " Exported %d QSOs to %s in ADIF format. " % ( len ( records ) , path ) )
except IOError as e :
error ( parent = self . application . window , message = " Could not export the records. I/O error %d : %s " % ( e . errno , e . strerror ) )
except Exception as e : # All other exceptions.
logging . exception ( e )
2017-07-03 21:35:28 +00:00
error ( parent = self . application . window , message = " Could not export the records. " )
2017-07-03 12:27:12 +00:00
2016-01-27 16:23:09 +00:00
return
2017-04-05 21:42:43 +00:00
def export_log_cabrillo ( self , widget = None ) :
""" Export the log (that is currently selected) to a Cabrillo file. """
2018-01-18 20:52:44 +00:00
# Get the index of the selected tab in the logbook.
try :
log_index = self . get_log_index ( )
if ( log_index is None ) :
raise ValueError ( " The log index could not be determined. Perhaps the Summary page is selected? " )
except ValueError as e :
error ( parent = self . application . window , message = e )
2017-04-05 21:42:43 +00:00
return
log = self . logs [ log_index ]
dialog = Gtk . FileChooserDialog ( " Export Log as Cabrillo " ,
self . application . window ,
Gtk . FileChooserAction . SAVE ,
( Gtk . STOCK_CANCEL , Gtk . ResponseType . CANCEL ,
Gtk . STOCK_SAVE , Gtk . ResponseType . OK ) )
dialog . set_do_overwrite_confirmation ( True )
filter = Gtk . FileFilter ( )
filter . set_name ( " All Cabrillo files (*.log, *.LOG) " )
filter . add_pattern ( " *.log " )
filter . add_pattern ( " *.LOG " )
dialog . add_filter ( filter )
filter = Gtk . FileFilter ( )
filter . set_name ( " All files " )
filter . add_pattern ( " * " )
dialog . add_filter ( filter )
response = dialog . run ( )
if ( response == Gtk . ResponseType . OK ) :
path = dialog . get_filename ( )
else :
path = None
dialog . destroy ( )
if ( path is None ) :
logging . debug ( " No file path specified. " )
else :
# Get Cabrillo-specific fields, such as the callsign used during a contest and the contest's name.
ced = CabrilloExportDialog ( self . application )
response = ced . dialog . run ( )
if ( response == Gtk . ResponseType . OK ) :
contest = ced . contest
mycall = ced . mycall
else :
ced . dialog . destroy ( )
return
ced . dialog . destroy ( )
2017-07-03 12:27:12 +00:00
# Retrieve the log's records from the database.
try :
records = log . records
except sqlite . Error as e :
logging . exception ( e )
2017-07-03 21:35:28 +00:00
error ( parent = self . application . window , message = " Could not retrieve the records from the SQL database. No records have been exported. " )
2017-07-03 12:27:12 +00:00
return
# Write the records.
cabrillo = Cabrillo ( )
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 IOError as e :
error ( parent = self . application . window , message = " Could not export the records. I/O error %d : %s " % ( e . errno , e . strerror ) )
except Exception as e : # All other exceptions.
logging . exception ( e )
2017-07-03 21:35:28 +00:00
error ( parent = self . application . window , message = " Could not export the records. " )
2017-07-03 12:27:12 +00:00
2017-04-05 21:42:43 +00:00
return
2016-01-27 16:23:09 +00:00
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 . """
2017-07-03 12:27:12 +00:00
2018-01-18 20:52:44 +00:00
# Get the index of the selected tab in the logbook.
try :
log_index = self . get_log_index ( )
if ( log_index is None ) :
raise ValueError ( " The log index could not be determined. Perhaps the Summary page is selected? " )
except ValueError as e :
error ( parent = self . application . window , message = e )
2016-01-27 16:23:09 +00:00
return
log = self . logs [ log_index ]
2017-07-03 12:27:12 +00:00
# Retrieve the records.
try :
records = log . records
except sqlite . Error as e :
logging . exception ( e )
2017-07-03 21:35:28 +00:00
error ( parent = self . application . window , message = " Could not retrieve the records from the SQL database. No records have been printed. " )
2017-07-03 12:27:12 +00:00
return
# Print the records.
printer = Printer ( self . application )
printer . print_records ( records , title = " Log: %s " % log . name )
2016-01-27 16:23:09 +00:00
return
def add_record_callback ( self , widget ) :
""" A callback function used to add a particular record/QSO. """
2018-01-18 20:52:44 +00:00
# Get the index of the selected tab in the logbook.
2016-01-27 16:23:09 +00:00
try :
2017-04-14 20:45:59 +00:00
log_index = self . get_log_index ( )
2016-01-27 16:23:09 +00:00
if ( log_index is None ) :
2017-06-27 21:41:00 +00:00
raise ValueError ( " The log index could not be determined. Perhaps the Summary page is selected? " )
2016-01-27 16:23:09 +00:00
except ValueError as e :
2017-07-03 20:54:52 +00:00
error ( parent = self . application . window , message = e )
2016-01-27 16:23:09 +00:00
return
log = self . logs [ log_index ]
# Keep the dialog open after adding a record?
config = configparser . ConfigParser ( )
have_config = ( config . read ( expanduser ( ' ~/.config/pyqso/preferences.ini ' ) ) != [ ] )
( section , option ) = ( " general " , " keep_open " )
if ( have_config and config . has_option ( section , option ) ) :
2017-07-09 10:42:42 +00:00
keep_open = config . getboolean ( " general " , " keep_open " )
2016-01-27 16:23:09 +00:00
else :
keep_open = False
2017-07-03 20:33:03 +00:00
2016-01-27 16:23:09 +00:00
adif = ADIF ( )
exit = False
while not exit :
2017-03-31 09:06:11 +00:00
rd = RecordDialog ( application = self . application , log = log , index = None )
2016-01-27 16:23:09 +00:00
all_valid = False # Are all the field entries valid?
# Shall we exit the while loop (and therefore close the Add Record dialog)?
if keep_open :
exit = False
else :
exit = True
while not all_valid :
# This while loop gives the user infinite attempts at giving valid data.
# The add/edit record window will stay open until the user gives valid data,
# or until the Cancel button is clicked.
all_valid = True
2017-03-01 10:16:03 +00:00
response = rd . dialog . run ( )
2016-01-27 16:23:09 +00:00
if ( response == Gtk . ResponseType . OK ) :
fields_and_data = { }
field_names = AVAILABLE_FIELD_NAMES_ORDERED
for i in range ( 0 , len ( field_names ) ) :
# Validate user input.
2017-03-01 10:16:03 +00:00
fields_and_data [ field_names [ i ] ] = rd . get_data ( field_names [ i ] )
2016-01-27 16:23:09 +00:00
if ( not ( adif . is_valid ( field_names [ i ] , fields_and_data [ field_names [ i ] ] , AVAILABLE_FIELD_NAMES_TYPES [ field_names [ i ] ] ) ) ) :
# Data is not valid - inform the user.
2017-03-02 10:14:21 +00:00
error ( parent = rd . dialog , message = " The data in field \" %s \" is not valid! " % field_names [ i ] )
2016-01-27 16:23:09 +00:00
all_valid = False
break # Don't check the other data until the user has fixed the current one.
if ( all_valid ) :
# All data has been validated, so we can go ahead and add the new record.
2017-07-03 20:49:45 +00:00
try :
log . add_record ( fields_and_data )
except ( sqlite . Error , IndexError ) as e :
logging . exception ( e )
2017-07-03 21:35:28 +00:00
error ( parent = self . application . window , message = " Could not add the record to the log. " )
2017-07-03 20:49:45 +00:00
# Scroll to the new record's row in the treeview (but don't select it).
try :
record_count = log . record_count
treepath = Gtk . TreePath ( record_count - 1 )
self . treeview [ log_index ] . scroll_to_cell ( treepath )
except ( sqlite . Error , IndexError ) as e :
logging . exception ( e )
# Update summary, etc.
2017-04-14 20:45:59 +00:00
self . summary . update ( )
2017-03-31 09:06:11 +00:00
self . application . toolbox . awards . count ( self )
2017-07-03 20:50:13 +00:00
2016-01-27 16:23:09 +00:00
else :
exit = True
break
2017-03-01 10:16:03 +00:00
rd . dialog . destroy ( )
2016-01-27 16:23:09 +00:00
return
def delete_record_callback ( self , widget ) :
""" A callback function used to delete a particular record/QSO. """
2017-06-27 19:10:20 +00:00
# Get the log index.
2016-01-27 16:23:09 +00:00
try :
2017-04-14 20:45:59 +00:00
log_index = self . get_log_index ( )
2016-01-27 16:23:09 +00:00
if ( log_index is None ) :
2017-06-27 21:41:00 +00:00
raise ValueError ( " The log index could not be determined. Perhaps the Summary page is selected? " )
2016-01-27 16:23:09 +00:00
except ValueError as e :
2017-07-03 20:33:03 +00:00
error ( parent = self . application . window , message = e )
2016-01-27 16:23:09 +00:00
return
log = self . logs [ log_index ]
( 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 )
# ...and the ListStore model (i.e. the log) is a child of the filter model.
child_iter = self . filter [ log_index ] . convert_iter_to_child_iter ( filter_iter )
row_index = log . get_value ( child_iter , 0 )
except IndexError :
logging . debug ( " Trying to delete a record, but there are no records in the log! " )
return
2017-03-31 09:06:11 +00:00
response = question ( parent = self . application . window , message = " Are you sure you want to delete record %d ? " % row_index )
2016-01-27 16:23:09 +00:00
if ( response == Gtk . ResponseType . YES ) :
# Deletes the record with index 'row_index' from the Records list.
# 'iter' is needed to remove the record from the ListStore itself.
2017-07-03 20:33:03 +00:00
try :
log . delete_record ( row_index , iter = child_iter )
except ( sqlite . Error , IndexError ) as e :
logging . exception ( e )
2017-07-03 21:35:28 +00:00
error ( parent = self . application . window , message = " Could not delete the record from the log. " )
2017-07-03 20:49:45 +00:00
# Update summary, etc.
self . summary . update ( )
self . application . toolbox . awards . count ( self )
2016-01-27 16:23:09 +00:00
return
2017-06-27 19:10:20 +00:00
def edit_record_callback ( self , widget , path = None , view_column = None ) :
2016-01-27 16:23:09 +00:00
""" A callback function used to edit a particular record/QSO.
Note that the widget , path and view_column arguments are not used ,
2017-06-27 19:10:20 +00:00
but need to be passed in since they are associated with the row - activated signal
2016-01-27 16:23:09 +00:00
which is generated when the user double - clicks on a record . """
2017-06-27 19:10:20 +00:00
# Get the log index.
2016-01-27 16:23:09 +00:00
try :
2017-04-14 20:45:59 +00:00
log_index = self . get_log_index ( )
2016-01-27 16:23:09 +00:00
if ( log_index is None ) :
2017-06-27 21:41:00 +00:00
raise ValueError ( " The log index could not be determined. Perhaps the Summary page is selected? " )
2016-01-27 16:23:09 +00:00
except ValueError as e :
2017-07-03 20:54:52 +00:00
error ( parent = self . application . window , message = e )
2016-01-27 16:23:09 +00:00
return
log = self . logs [ log_index ]
2017-06-27 19:10:20 +00:00
( sort_model , path ) = self . treeselection [ log_index ] . get_selected_rows ( ) # Get the selected row in the log.
2016-01-27 16:23:09 +00:00
try :
sort_iter = sort_model . get_iter ( path [ 0 ] )
filter_iter = self . sorter [ log_index ] . convert_iter_to_child_iter ( sort_iter )
# ...and the ListStore model (i.e. the log) is a child of the filter model.
child_iter = self . filter [ log_index ] . convert_iter_to_child_iter ( filter_iter )
row_index = log . get_value ( child_iter , 0 )
except IndexError :
logging . debug ( " Could not find the selected row ' s index! " )
2013-05-16 20:47:17 +00:00
return
2016-01-27 16:23:09 +00:00
2017-04-01 14:43:55 +00:00
rd = RecordDialog ( application = self . application , log = self . logs [ log_index ] , index = row_index )
2016-01-27 16:23:09 +00:00
all_valid = False # Are all the field entries valid?
adif = ADIF ( )
while ( not all_valid ) :
2016-01-24 21:29:35 +00:00
# This while loop gives the user infinite attempts at giving valid data.
# The add/edit record window will stay open until the user gives valid data,
# or until the Cancel button is clicked.
all_valid = True
2017-03-01 10:16:03 +00:00
response = rd . dialog . run ( )
2016-01-24 21:29:35 +00:00
if ( response == Gtk . ResponseType . OK ) :
2016-01-27 16:23:09 +00:00
fields_and_data = { }
field_names = AVAILABLE_FIELD_NAMES_ORDERED
for i in range ( 0 , len ( field_names ) ) :
# Validate user input.
2017-03-01 10:16:03 +00:00
fields_and_data [ field_names [ i ] ] = rd . get_data ( field_names [ i ] )
2016-01-27 16:23:09 +00:00
if ( not ( adif . is_valid ( field_names [ i ] , fields_and_data [ field_names [ i ] ] , AVAILABLE_FIELD_NAMES_TYPES [ field_names [ i ] ] ) ) ) :
# Data is not valid - inform the user.
2017-03-02 10:14:21 +00:00
error ( parent = rd . dialog , message = " The data in field \" %s \" is not valid! " % field_names [ i ] )
2016-01-27 16:23:09 +00:00
all_valid = False
break # Don't check the other fields until the user has fixed the current field's data.
if ( all_valid ) :
2017-07-03 20:33:03 +00:00
try :
# Get the record in its current state from the database.
record = log . get_record_by_index ( row_index )
# Iterate over all fields and check whether the data has actually changed. Database updates can be expensive.
2016-01-27 16:23:09 +00:00
for i in range ( 0 , len ( field_names ) ) :
if ( record [ field_names [ i ] . lower ( ) ] != fields_and_data [ field_names [ i ] ] ) :
# Update the record in the database and then in the ListStore.
# We add 1 onto the column_index here because we don't want to consider the index column.
log . edit_record ( row_index , field_names [ i ] , fields_and_data [ field_names [ i ] ] , iter = child_iter , column_index = i + 1 )
2017-07-03 20:33:03 +00:00
except ( sqlite . Error , IndexError ) as e :
logging . exception ( e )
2017-07-03 21:35:28 +00:00
error ( parent = rd . dialog , message = " Could not edit record %d . " % row_index )
2016-01-27 16:23:09 +00:00
2017-07-03 20:49:45 +00:00
# Update summary, etc.
self . summary . update ( )
self . application . toolbox . awards . count ( self )
2017-03-01 10:16:03 +00:00
rd . dialog . destroy ( )
2016-01-27 16:23:09 +00:00
return
def remove_duplicates_callback ( self , widget = None ) :
2017-06-27 22:13:09 +00:00
""" A callback function used to remove duplicate records in a log.
2017-06-27 19:10:20 +00:00
Detecting duplicate records is done based on the CALL , QSO_DATE , and TIME_ON fields . """
2016-01-27 16:23:09 +00:00
logging . debug ( " Removing duplicate records... " )
2017-06-27 21:41:00 +00:00
# Get the log index.
try :
log_index = self . get_log_index ( )
if ( log_index is None ) :
raise ValueError ( " The log index could not be determined. Perhaps the Summary page is selected? " )
except ValueError as e :
2017-07-03 20:54:52 +00:00
error ( parent = self . application . window , message = e )
2017-06-27 21:41:00 +00:00
return
2016-01-27 16:23:09 +00:00
log = self . logs [ log_index ]
( number_of_duplicates , number_of_duplicates_removed ) = log . remove_duplicates ( )
2017-06-27 19:10:20 +00:00
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 )
2016-01-27 16:23:09 +00:00
return
2017-06-27 20:13:59 +00:00
def record_count_callback ( self , widget = None ) :
2017-06-27 22:13:09 +00:00
""" A callback function used to show the record count for the selected log. """
2017-07-03 20:33:03 +00:00
2017-06-27 21:41:00 +00:00
# Get the log index.
try :
log_index = self . get_log_index ( )
if ( log_index is None ) :
raise ValueError ( " The log index could not be determined. Perhaps the Summary page is selected? " )
except ValueError as e :
2017-07-03 20:54:52 +00:00
error ( parent = self . application . window , message = e )
2017-06-27 20:13:59 +00:00
return
2017-07-03 20:33:03 +00:00
# Get the number of records.
2017-06-27 20:13:59 +00:00
log = self . logs [ log_index ]
2017-07-03 20:33:03 +00:00
try :
record_count = log . record_count
2017-06-27 20:13:59 +00:00
info ( parent = self . application . window , message = " Log ' %s ' contains %d records. " % ( log . name , record_count ) )
2017-07-03 20:33:03 +00:00
except sqlite . Error as e :
logging . exception ( e )
2017-07-03 21:35:28 +00:00
error ( parent = self . application . window , message = " Could not get the record count for ' %s ' because of a database error. " % log . name )
2017-07-03 20:33:03 +00:00
2017-06-27 20:13:59 +00:00
return
2018-01-18 20:52:44 +00:00
def pinpoint_callback ( self , widget = None , path = None ) :
2018-02-24 13:24:39 +00:00
""" A callback function used to pinpoint the callsign on the world map. """
2018-01-18 20:52:44 +00:00
try :
log_index = self . get_log_index ( )
row_index = self . get_record_index ( )
if ( log_index is None or row_index is None ) :
raise ValueError ( " Could not determine the log and/or record index. " )
r = self . logs [ log_index ] . get_record_by_index ( row_index )
except ValueError as e :
logging . error ( e )
return
2018-02-24 13:24:39 +00:00
self . application . toolbox . world_map . pinpoint ( r )
2018-01-18 20:52:44 +00:00
return
2018-02-04 23:15:57 +00:00
def copy_callback ( self , widget = None , path = None ) :
""" A callback function used to copy selected logs. """
try :
log_index = self . get_log_index ( )
row_index = self . get_record_index ( )
if ( log_index is None or row_index is None ) :
raise ValueError ( " Could not determine the log and/or record index. " )
r = self . logs [ log_index ] . get_record_by_index ( row_index )
except ValueError as e :
logging . error ( e )
return
d = { }
for key in r . keys ( ) :
d [ key . upper ( ) ] = r [ key ]
j = json . dumps ( d )
self . application . clipboard . set_text ( j , len ( j ) )
return
def clipboard_text_received ( self , clipboard , text , log ) :
r = json . loads ( text )
log . add_record ( r )
return
def paste_callback ( self , widget = None , path = None ) :
""" A callback function used to paste selected logs. """
try :
log_index = self . get_log_index ( )
if ( log_index is None ) :
raise ValueError ( " Could not determine the log index. " )
l = self . logs [ log_index ]
except ValueError as e :
logging . error ( e )
return
self . application . clipboard . request_text ( self . clipboard_text_received , l )
return
2017-04-11 22:06:15 +00:00
@property
def log_count ( self ) :
2016-01-27 16:23:09 +00:00
""" Return the total number of logs in the logbook.
: returns : The total number of logs in the logbook .
: rtype : int
"""
return len ( self . logs )
2017-04-11 22:06:15 +00:00
@property
def record_count ( self ) :
2016-01-27 16:23:09 +00:00
""" Return the total number of QSOs/records in the whole logbook.
: returns : The total number of QSOs / records in the whole logbook .
: rtype : int
"""
2017-04-11 22:40:06 +00:00
return sum ( [ log . record_count for log in self . logs ] )
2016-01-27 16:23:09 +00:00
def log_name_exists ( self , table_name ) :
""" Determine whether a Log object with a given name exists in the SQL database.
: arg str table_name : The name of the log ( i . e . the name of the table in the SQL database ) .
2017-07-04 16:02:59 +00:00
: returns : True if the log name already exists in the logbook ; False if it does not already exist .
: rtype : bool
: raises sqlite . Error : If a database error occurs .
2016-01-27 16:23:09 +00:00
"""
2017-07-04 16:02:59 +00:00
with self . connection :
c = self . connection . cursor ( )
c . execute ( " SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE name=?) " , [ table_name ] )
exists = c . fetchone ( )
if ( exists [ 0 ] == 1 ) :
return True
else :
return False
2016-01-27 16:23:09 +00:00
2017-04-14 20:45:59 +00:00
def get_log_index ( self , name = None ) :
2016-01-27 16:23:09 +00:00
""" Given the name of a log, return its index in the list of Log objects.
: arg str name : The name of the log . If None , use the name of the currently - selected log .
2018-01-18 20:52:44 +00:00
: returns : The index of the named log in the list of Log objects . Returns None if the log cannot be found .
2016-01-27 16:23:09 +00:00
: rtype : int
"""
if ( name is None ) :
2017-06-27 19:10:20 +00:00
# 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.
2017-02-21 19:36:25 +00:00
if ( page_index == 0 or page_index == self . notebook . get_n_pages ( ) - 1 ) :
2017-04-14 22:14:27 +00:00
# We either have the Summary page, or the "+" (add log) blank/dummy page.
2016-01-27 16:23:09 +00:00
logging . debug ( " No log currently selected! " )
return None
2017-02-21 19:36:25 +00:00
name = self . notebook . get_nth_page ( page_index ) . get_name ( )
2016-01-27 16:23:09 +00:00
# If a page of the logbook (and therefore a Log object) gets deleted,
# then the page_index may not correspond to the index of the log in the self.logs list.
# Therefore, we have to search for the tab with the same name as the log.
2017-04-23 19:04:38 +00:00
log_index = None
2016-01-27 16:23:09 +00:00
for i in range ( 0 , len ( self . logs ) ) :
if ( self . logs [ i ] . name == name ) :
log_index = i
break
return log_index
2017-04-20 21:50:08 +00:00
2018-01-18 20:52:44 +00:00
def get_record_index ( self ) :
""" Return the index of the currently selected record.
: returns : The index of the currently selected record in the currently selected log . Returns None if the record or log cannot be found .
: rtype : int
"""
# Get the index of the selected log.
try :
log_index = self . get_log_index ( )
if ( log_index is None ) :
raise ValueError ( " The log index could not be determined. Perhaps the Summary page is selected? " )
except ValueError as e :
logging . error ( e )
return None
log = self . logs [ log_index ]
# Get the selected row in the log.
( sort_model , path ) = self . treeselection [ log_index ] . get_selected_rows ( )
try :
sort_iter = sort_model . get_iter ( path [ 0 ] )
filter_iter = self . sorter [ log_index ] . convert_iter_to_child_iter ( sort_iter )
# ...and the ListStore model (i.e. the log) is a child of the filter model.
child_iter = self . filter [ log_index ] . convert_iter_to_child_iter ( filter_iter )
row_index = log . get_value ( child_iter , 0 )
except IndexError :
logging . error ( " Could not find the selected row ' s index! " )
return None
return row_index
2017-04-20 21:50:08 +00:00
def get_logs ( self ) :
""" Retrieve all the logs in the logbook file, and create Log objects that represent them.
2017-07-04 16:02:59 +00:00
: returns : A list containing all the logs in the logbook .
2017-04-20 21:50:08 +00:00
: rtype : list
2017-07-03 21:35:28 +00:00
: raises sqlite . Error : If the log names could not be determined from the sqlite_master table in the database .
2017-04-20 21:50:08 +00:00
"""
2017-06-27 19:10:20 +00:00
logs = [ ]
2017-07-03 21:35:28 +00:00
with self . connection :
c = self . connection . cursor ( )
c . execute ( " SELECT name FROM sqlite_master WHERE type= ' table ' AND name NOT GLOB ' sqlite_* ' " )
for name in c :
l = Log ( self . connection , name [ 0 ] )
l . populate ( )
logs . append ( l )
2017-04-20 21:50:08 +00:00
return logs