kopia lustrzana https://github.com/ctjacobs/pyqso
Make the code pep8 compliant, using the autopep8 tool. Note that we are ignoring the following: E501,F403,E226,E402,W503.
rodzic
1e5a657ca7
commit
c993704e84
215
bin/pyqso
215
bin/pyqso
|
@ -41,93 +41,95 @@ from pyqso.toolbar import *
|
|||
from pyqso.toolbox import *
|
||||
from pyqso.preferences_dialog import *
|
||||
|
||||
|
||||
class PyQSO(Gtk.Window):
|
||||
""" The PyQSO application class. """
|
||||
|
||||
def __init__(self, logbook_path=None):
|
||||
""" Set up the main (root) window, start the event loop, and open a logbook (if the logbook's path is specified by the user in the command line).
|
||||
|
||||
:arg str logbook_path: An optional argument containing the path of the logbook file to open. If no value is provided, this defaults to None and no logbook is opened.
|
||||
"""
|
||||
|
||||
# Call the constructor of the super class (Gtk.Window)
|
||||
Gtk.Window.__init__(self, title="PyQSO")
|
||||
""" The PyQSO application class. """
|
||||
|
||||
# Check that the directory for holding PyQSO configuration files exists. If it doesn't, create it now.
|
||||
try:
|
||||
os.makedirs(os.path.expanduser('~/.config/pyqso'), exist_ok=True)
|
||||
except Exception as e:
|
||||
logging.error("An error occurred whilst creating a directory for PyQSO configuration files. Try creating the directory '~/.config/pyqso' manually.")
|
||||
logging.exception(e)
|
||||
def __init__(self, logbook_path=None):
|
||||
""" Set up the main (root) window, start the event loop, and open a logbook (if the logbook's path is specified by the user in the command line).
|
||||
|
||||
# Get any application-specific preferences from the configuration file
|
||||
config = configparser.ConfigParser()
|
||||
:arg str logbook_path: An optional argument containing the path of the logbook file to open. If no value is provided, this defaults to None and no logbook is opened.
|
||||
"""
|
||||
|
||||
# Check that the configuration file actually exists (and is readable)
|
||||
# otherwise, we will resort to the defaults.
|
||||
have_config = (config.read(os.path.expanduser("~/.config/pyqso/preferences.ini")) != [])
|
||||
# Call the constructor of the super class (Gtk.Window)
|
||||
Gtk.Window.__init__(self, title="PyQSO")
|
||||
|
||||
self.set_size_request(800, 600) # Default to an 800 x 600 resolution.
|
||||
self.set_position(Gtk.WindowPosition.CENTER)
|
||||
possible_icon_paths = [os.path.join(pyqso_path, "icons", "log_64x64.png")]
|
||||
for icon_path in possible_icon_paths:
|
||||
try:
|
||||
self.set_icon_from_file(icon_path)
|
||||
except Exception as error:
|
||||
print(error.message)
|
||||
# Check that the directory for holding PyQSO configuration files exists. If it doesn't, create it now.
|
||||
try:
|
||||
os.makedirs(os.path.expanduser('~/.config/pyqso'), exist_ok=True)
|
||||
except Exception as e:
|
||||
logging.error("An error occurred whilst creating a directory for PyQSO configuration files. Try creating the directory '~/.config/pyqso' manually.")
|
||||
logging.exception(e)
|
||||
|
||||
# Kills the application if the close button is clicked on the main window itself.
|
||||
self.connect("delete-event", Gtk.main_quit)
|
||||
|
||||
vbox_outer = Gtk.VBox()
|
||||
self.add(vbox_outer)
|
||||
# Get any application-specific preferences from the configuration file
|
||||
config = configparser.ConfigParser()
|
||||
|
||||
self.statusbar = Gtk.Statusbar()
|
||||
context_id = self.statusbar.get_context_id("Status")
|
||||
self.statusbar.push(context_id, "No logbook is currently open.")
|
||||
|
||||
# Create a Logbook so we can add/remove/edit logs and records,
|
||||
# once connected to the SQLite database.
|
||||
self.logbook = Logbook(self)
|
||||
self.logbook.set_scrollable(True)
|
||||
# Check that the configuration file actually exists (and is readable)
|
||||
# otherwise, we will resort to the defaults.
|
||||
have_config = (config.read(os.path.expanduser("~/.config/pyqso/preferences.ini")) != [])
|
||||
|
||||
self.toolbox = Toolbox(self)
|
||||
self.set_size_request(800, 600) # Default to an 800 x 600 resolution.
|
||||
self.set_position(Gtk.WindowPosition.CENTER)
|
||||
possible_icon_paths = [os.path.join(pyqso_path, "icons", "log_64x64.png")]
|
||||
for icon_path in possible_icon_paths:
|
||||
try:
|
||||
self.set_icon_from_file(icon_path)
|
||||
except Exception as error:
|
||||
print(error.message)
|
||||
|
||||
# Set up menu and tool bars
|
||||
# These classes depend on the Logbook and Toolbox class,
|
||||
# so pack the logbook and toolbox after the menu and toolbar.
|
||||
self.menu = Menu(self)
|
||||
self.toolbar = Toolbar(self)
|
||||
# Kills the application if the close button is clicked on the main window itself.
|
||||
self.connect("delete-event", Gtk.main_quit)
|
||||
|
||||
vbox_outer.pack_start(self.menu, False, False, 0)
|
||||
vbox_outer.pack_start(self.toolbar, False, False, 0)
|
||||
vbox_outer.pack_start(self.logbook, True, True, 0)
|
||||
vbox_outer.pack_start(self.toolbox, True, True, 0)
|
||||
vbox_outer.pack_start(self.statusbar, False, False, 0)
|
||||
vbox_outer = Gtk.VBox()
|
||||
self.add(vbox_outer)
|
||||
|
||||
self.show_all()
|
||||
self.statusbar = Gtk.Statusbar()
|
||||
context_id = self.statusbar.get_context_id("Status")
|
||||
self.statusbar.push(context_id, "No logbook is currently open.")
|
||||
|
||||
if(have_config):
|
||||
if(config.get("general", "show_toolbox") == "False"):
|
||||
# Create a Logbook so we can add/remove/edit logs and records,
|
||||
# once connected to the SQLite database.
|
||||
self.logbook = Logbook(self)
|
||||
self.logbook.set_scrollable(True)
|
||||
|
||||
self.toolbox = Toolbox(self)
|
||||
|
||||
# Set up menu and tool bars
|
||||
# These classes depend on the Logbook and Toolbox class,
|
||||
# so pack the logbook and toolbox after the menu and toolbar.
|
||||
self.menu = Menu(self)
|
||||
self.toolbar = Toolbar(self)
|
||||
|
||||
vbox_outer.pack_start(self.menu, False, False, 0)
|
||||
vbox_outer.pack_start(self.toolbar, False, False, 0)
|
||||
vbox_outer.pack_start(self.logbook, True, True, 0)
|
||||
vbox_outer.pack_start(self.toolbox, True, True, 0)
|
||||
vbox_outer.pack_start(self.statusbar, False, False, 0)
|
||||
|
||||
self.show_all()
|
||||
|
||||
if(have_config):
|
||||
if(config.get("general", "show_toolbox") == "False"):
|
||||
self.toolbox.toggle_visible_callback()
|
||||
else:
|
||||
# Hide the Toolbox by default
|
||||
self.toolbox.toggle_visible_callback()
|
||||
else:
|
||||
# Hide the Toolbox by default
|
||||
self.toolbox.toggle_visible_callback()
|
||||
|
||||
if(logbook_path is not None):
|
||||
self.logbook.open(widget=None, path=logbook_path)
|
||||
if(logbook_path is not None):
|
||||
self.logbook.open(widget=None, path=logbook_path)
|
||||
|
||||
return
|
||||
return
|
||||
|
||||
def show_about(self, widget):
|
||||
""" Show the About dialog, which includes license information. """
|
||||
about = Gtk.AboutDialog()
|
||||
about.set_modal(True)
|
||||
about.set_transient_for(parent=self)
|
||||
about.set_program_name("PyQSO")
|
||||
about.set_version("0.3")
|
||||
about.set_authors(["Christian T. Jacobs (2E0ICL)"])
|
||||
about.set_license("""This program is free software: you can redistribute it and/or modify
|
||||
def show_about(self, widget):
|
||||
""" Show the About dialog, which includes license information. """
|
||||
about = Gtk.AboutDialog()
|
||||
about.set_modal(True)
|
||||
about.set_transient_for(parent=self)
|
||||
about.set_program_name("PyQSO")
|
||||
about.set_version("0.3")
|
||||
about.set_authors(["Christian T. Jacobs (2E0ICL)"])
|
||||
about.set_license("""This program 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.
|
||||
|
@ -139,45 +141,44 @@ GNU General Public License for more details.
|
|||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.""")
|
||||
about.set_comments("PyQSO: A contact logging tool for amateur radio operators.")
|
||||
possible_icon_paths = [os.path.join(pyqso_path, "icons", "log_64x64.png")]
|
||||
for icon_path in possible_icon_paths:
|
||||
try:
|
||||
about.set_logo(GdkPixbuf.Pixbuf.new_from_file_at_scale(icon_path, 64, 64, False))
|
||||
except Exception as error:
|
||||
print(error.message)
|
||||
about.run()
|
||||
about.destroy()
|
||||
return
|
||||
about.set_comments("PyQSO: A contact logging tool for amateur radio operators.")
|
||||
possible_icon_paths = [os.path.join(pyqso_path, "icons", "log_64x64.png")]
|
||||
for icon_path in possible_icon_paths:
|
||||
try:
|
||||
about.set_logo(GdkPixbuf.Pixbuf.new_from_file_at_scale(icon_path, 64, 64, False))
|
||||
except Exception as error:
|
||||
print(error.message)
|
||||
about.run()
|
||||
about.destroy()
|
||||
return
|
||||
|
||||
def show_preferences(self, widget):
|
||||
""" Show the Preferences dialog. Any changes made by the user after clicking the 'Ok' button are saved in the .cfg file. """
|
||||
preferences = PreferencesDialog(self)
|
||||
response = preferences.run()
|
||||
if(response == Gtk.ResponseType.OK):
|
||||
preferences.commit()
|
||||
preferences.destroy()
|
||||
return
|
||||
def show_preferences(self, widget):
|
||||
""" Show the Preferences dialog. Any changes made by the user after clicking the 'Ok' button are saved in the .cfg file. """
|
||||
preferences = PreferencesDialog(self)
|
||||
response = preferences.run()
|
||||
if(response == Gtk.ResponseType.OK):
|
||||
preferences.commit()
|
||||
preferences.destroy()
|
||||
return
|
||||
|
||||
if(__name__ == "__main__"):
|
||||
# Get any command line arguments
|
||||
parser = argparse.ArgumentParser(prog="pyqso")
|
||||
parser.add_argument("-d", "--debug", action="store_true", default=False, help="Enable debugging. All debugging messages will be written to pyqso.debug.")
|
||||
parser.add_argument("-l", "--logbook", action="store", type=str, metavar="/path/to/my_logbook_file.db", default=None, help="Path to a Logbook file. If this file does not already exist, then it will be created.")
|
||||
args = parser.parse_args()
|
||||
# Get any command line arguments
|
||||
parser = argparse.ArgumentParser(prog="pyqso")
|
||||
parser.add_argument("-d", "--debug", action="store_true", default=False, help="Enable debugging. All debugging messages will be written to pyqso.debug.")
|
||||
parser.add_argument("-l", "--logbook", action="store", type=str, metavar="/path/to/my_logbook_file.db", default=None, help="Path to a Logbook file. If this file does not already exist, then it will be created.")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Output debugging messages to a file
|
||||
if(args.debug):
|
||||
# Get the root logger
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.DEBUG)
|
||||
# Add a file handler
|
||||
handler = logging.FileHandler("pyqso.debug", mode="w")
|
||||
formatter = logging.Formatter(fmt="%(asctime)s %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL) # Exit PyQSO if a SIGINT signal is captured.
|
||||
application = PyQSO(args.logbook) # Populate the main window and show it
|
||||
Gtk.main() # Start up the event loop!
|
||||
# Output debugging messages to a file
|
||||
if(args.debug):
|
||||
# Get the root logger
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.DEBUG)
|
||||
# Add a file handler
|
||||
handler = logging.FileHandler("pyqso.debug", mode="w")
|
||||
formatter = logging.Formatter(fmt="%(asctime)s %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL) # Exit PyQSO if a SIGINT signal is captured.
|
||||
application = PyQSO(args.logbook) # Populate the main window and show it
|
||||
Gtk.main() # Start up the event loop!
|
||||
|
|
1132
pyqso/adif.py
1132
pyqso/adif.py
Plik diff jest za duży
Load Diff
|
@ -20,49 +20,53 @@
|
|||
from gi.repository import Gtk
|
||||
import logging
|
||||
|
||||
|
||||
def error(parent, message):
|
||||
""" Display an error message.
|
||||
|
||||
:arg parent: The Gtk parent window/dialog.
|
||||
:arg str message: The message to display to the user.
|
||||
"""
|
||||
logging.error(message)
|
||||
_handle_gtk_dialog(parent, Gtk.MessageType.ERROR, message, "Error")
|
||||
""" Display an error message.
|
||||
|
||||
:arg parent: The Gtk parent window/dialog.
|
||||
:arg str message: The message to display to the user.
|
||||
"""
|
||||
logging.error(message)
|
||||
_handle_gtk_dialog(parent, Gtk.MessageType.ERROR, message, "Error")
|
||||
|
||||
|
||||
def info(parent, message):
|
||||
""" Display some information.
|
||||
|
||||
:arg parent: The Gtk parent window/dialog.
|
||||
:arg str message: The message to display to the user.
|
||||
"""
|
||||
logging.debug(message)
|
||||
_handle_gtk_dialog(parent, Gtk.MessageType.INFO, message, "Information")
|
||||
|
||||
""" Display some information.
|
||||
|
||||
:arg parent: The Gtk parent window/dialog.
|
||||
:arg str message: The message to display to the user.
|
||||
"""
|
||||
logging.debug(message)
|
||||
_handle_gtk_dialog(parent, Gtk.MessageType.INFO, message, "Information")
|
||||
|
||||
|
||||
def question(parent, message):
|
||||
""" Ask the user a question. The dialog comes with 'Yes' and 'No' response buttons.
|
||||
|
||||
:arg parent: The Gtk parent window/dialog.
|
||||
:arg str message: The message to display to the user.
|
||||
:returns: The 'yes'/'no' response from the user.
|
||||
:rtype: Gtk.ResponseType
|
||||
"""
|
||||
return _handle_gtk_dialog(parent, Gtk.MessageType.QUESTION, message, "Question")
|
||||
""" Ask the user a question. The dialog comes with 'Yes' and 'No' response buttons.
|
||||
|
||||
:arg parent: The Gtk parent window/dialog.
|
||||
:arg str message: The message to display to the user.
|
||||
:returns: The 'yes'/'no' response from the user.
|
||||
:rtype: Gtk.ResponseType
|
||||
"""
|
||||
return _handle_gtk_dialog(parent, Gtk.MessageType.QUESTION, message, "Question")
|
||||
|
||||
|
||||
def _handle_gtk_dialog(parent, msgtype, message, title):
|
||||
"""
|
||||
Instantiate and present a dialog to the user.
|
||||
|
||||
:arg parent: The Gtk parent window/dialog.
|
||||
:arg Gtk.MessageType msgtype: The type of message to present to the user (e.g. a question, or error message).
|
||||
:arg str message: The message to display in the dialog.
|
||||
:arg str title: The title to display at the top of the dialog.
|
||||
:returns: The response from the user, based on which button they pushed.
|
||||
:rtype: Gtk.ResponseType
|
||||
"""
|
||||
bt = Gtk.ButtonsType
|
||||
buttons = bt.YES_NO if msgtype == Gtk.MessageType.QUESTION else bt.OK
|
||||
dialog = Gtk.MessageDialog(parent, Gtk.DialogFlags.DESTROY_WITH_PARENT,
|
||||
msgtype, buttons, message, title=title)
|
||||
response = dialog.run()
|
||||
dialog.destroy()
|
||||
return response
|
||||
"""
|
||||
Instantiate and present a dialog to the user.
|
||||
|
||||
:arg parent: The Gtk parent window/dialog.
|
||||
:arg Gtk.MessageType msgtype: The type of message to present to the user (e.g. a question, or error message).
|
||||
:arg str message: The message to display in the dialog.
|
||||
:arg str title: The title to display at the top of the dialog.
|
||||
:returns: The response from the user, based on which button they pushed.
|
||||
:rtype: Gtk.ResponseType
|
||||
"""
|
||||
bt = Gtk.ButtonsType
|
||||
buttons = bt.YES_NO if msgtype == Gtk.MessageType.QUESTION else bt.OK
|
||||
dialog = Gtk.MessageDialog(parent, Gtk.DialogFlags.DESTROY_WITH_PARENT,
|
||||
msgtype, buttons, message, title=title)
|
||||
response = dialog.run()
|
||||
dialog.destroy()
|
||||
return response
|
||||
|
|
153
pyqso/awards.py
153
pyqso/awards.py
|
@ -20,90 +20,91 @@
|
|||
from gi.repository import Gtk
|
||||
import logging
|
||||
|
||||
|
||||
class Awards(Gtk.VBox):
|
||||
""" A tool for tracking progress towards an award. Currently this only supports the DXCC award.
|
||||
For more information visit http://www.arrl.org/dxcc """
|
||||
|
||||
def __init__(self, parent):
|
||||
""" Set up a table for progress tracking purposes.
|
||||
|
||||
:arg parent: The parent Gtk window.
|
||||
"""
|
||||
#TODO: This only considers the DXCC award for now.
|
||||
logging.debug("New Awards instance created!")
|
||||
|
||||
Gtk.VBox.__init__(self, spacing=2)
|
||||
|
||||
self.parent = parent
|
||||
""" A tool for tracking progress towards an award. Currently this only supports the DXCC award.
|
||||
For more information visit http://www.arrl.org/dxcc """
|
||||
|
||||
self.bands = ["70cm", "2m", "6m", "10m", "12m", "15m", "17m", "20m", "30m", "40m", "80m", "160m"]
|
||||
self.modes = ["Phone", "CW", "Digital", "Mixed"]
|
||||
|
||||
data_types = [str] + [int]*len(self.bands)
|
||||
self.awards = Gtk.ListStore(*data_types)
|
||||
def __init__(self, parent):
|
||||
""" Set up a table for progress tracking purposes.
|
||||
|
||||
# The main table for the awards
|
||||
self.treeview = Gtk.TreeView(self.awards)
|
||||
# A separate, empty column just for the mode names
|
||||
renderer = Gtk.CellRendererText()
|
||||
column = Gtk.TreeViewColumn("Modes", renderer, text=0)
|
||||
column.set_clickable(False)
|
||||
self.treeview.append_column(column)
|
||||
# Now for all the bands...
|
||||
logging.debug("Initialising the columns in the awards table.")
|
||||
for i in range(0, len(self.bands)):
|
||||
renderer = Gtk.CellRendererText()
|
||||
column = Gtk.TreeViewColumn(self.bands[i], renderer, text=i+1)
|
||||
column.set_min_width(40)
|
||||
column.set_clickable(False)
|
||||
self.treeview.append_column(column)
|
||||
:arg parent: The parent Gtk window.
|
||||
"""
|
||||
# TODO: This only considers the DXCC award for now.
|
||||
logging.debug("New Awards instance created!")
|
||||
|
||||
# Add a label to inform the user that this only considers the DXCC award for now.
|
||||
label = Gtk.Label(halign=Gtk.Align.START)
|
||||
label.set_markup("<span size=\"x-large\">%s</span>" % "DXCC Award")
|
||||
self.pack_start(label, False, False, 4)
|
||||
# Show the table in the Awards tab
|
||||
self.add(self.treeview)
|
||||
self.show_all()
|
||||
Gtk.VBox.__init__(self, spacing=2)
|
||||
|
||||
logging.debug("Awards table set up successfully.")
|
||||
self.parent = parent
|
||||
|
||||
self.count()
|
||||
self.bands = ["70cm", "2m", "6m", "10m", "12m", "15m", "17m", "20m", "30m", "40m", "80m", "160m"]
|
||||
self.modes = ["Phone", "CW", "Digital", "Mixed"]
|
||||
|
||||
return
|
||||
data_types = [str] + [int]*len(self.bands)
|
||||
self.awards = Gtk.ListStore(*data_types)
|
||||
|
||||
def count(self):
|
||||
""" Update the table for progress tracking. """
|
||||
|
||||
logging.debug("Counting the band/mode combinations for the awards table...")
|
||||
# Wipe everything and start again
|
||||
self.awards.clear()
|
||||
# For each mode, add a new list for holding the totals, and initialise the values to zero.
|
||||
count = []
|
||||
for i in range(0, len(self.bands)):
|
||||
count.append([0]*len(self.bands))
|
||||
# The main table for the awards
|
||||
self.treeview = Gtk.TreeView(self.awards)
|
||||
# A separate, empty column just for the mode names
|
||||
renderer = Gtk.CellRendererText()
|
||||
column = Gtk.TreeViewColumn("Modes", renderer, text=0)
|
||||
column.set_clickable(False)
|
||||
self.treeview.append_column(column)
|
||||
# Now for all the bands...
|
||||
logging.debug("Initialising the columns in the awards table.")
|
||||
for i in range(0, len(self.bands)):
|
||||
renderer = Gtk.CellRendererText()
|
||||
column = Gtk.TreeViewColumn(self.bands[i], renderer, text=i+1)
|
||||
column.set_min_width(40)
|
||||
column.set_clickable(False)
|
||||
self.treeview.append_column(column)
|
||||
|
||||
for log in self.parent.logbook.logs:
|
||||
records = log.get_all_records()
|
||||
if(records is not None):
|
||||
for r in records:
|
||||
if(r["BAND"] is not None and r["MODE"] is not None):
|
||||
if(r["BAND"].lower() in self.bands and r["MODE"] != ""):
|
||||
band = self.bands.index(r["BAND"].lower())
|
||||
# Phone modes
|
||||
if(r["MODE"].upper() in ["FM", "AM", "SSB", "SSTV"]):
|
||||
count[0][band] += 1
|
||||
elif(r["MODE"].upper() == "CW"):
|
||||
count[1][band] += 1
|
||||
else:
|
||||
#FIXME: This assumes that all the other modes in the ADIF list are digital modes. Is this the case?
|
||||
count[2][band] += 1
|
||||
count[3][band] += 1 # Keep the total of each column in the "Mixed" mode
|
||||
else:
|
||||
logging.error("Could not update the awards table for '%s' because of a database error." % log.name)
|
||||
# Insert the rows containing the totals
|
||||
for i in range(0, len(self.modes)):
|
||||
self.awards.append([self.modes[i]] + count[i])
|
||||
logging.debug("Awards table updated.")
|
||||
return
|
||||
# Add a label to inform the user that this only considers the DXCC award for now.
|
||||
label = Gtk.Label(halign=Gtk.Align.START)
|
||||
label.set_markup("<span size=\"x-large\">%s</span>" % "DXCC Award")
|
||||
self.pack_start(label, False, False, 4)
|
||||
# Show the table in the Awards tab
|
||||
self.add(self.treeview)
|
||||
self.show_all()
|
||||
|
||||
logging.debug("Awards table set up successfully.")
|
||||
|
||||
self.count()
|
||||
|
||||
return
|
||||
|
||||
def count(self):
|
||||
""" Update the table for progress tracking. """
|
||||
|
||||
logging.debug("Counting the band/mode combinations for the awards table...")
|
||||
# Wipe everything and start again
|
||||
self.awards.clear()
|
||||
# For each mode, add a new list for holding the totals, and initialise the values to zero.
|
||||
count = []
|
||||
for i in range(0, len(self.bands)):
|
||||
count.append([0]*len(self.bands))
|
||||
|
||||
for log in self.parent.logbook.logs:
|
||||
records = log.get_all_records()
|
||||
if(records is not None):
|
||||
for r in records:
|
||||
if(r["BAND"] is not None and r["MODE"] is not None):
|
||||
if(r["BAND"].lower() in self.bands and r["MODE"] != ""):
|
||||
band = self.bands.index(r["BAND"].lower())
|
||||
# Phone modes
|
||||
if(r["MODE"].upper() in ["FM", "AM", "SSB", "SSTV"]):
|
||||
count[0][band] += 1
|
||||
elif(r["MODE"].upper() == "CW"):
|
||||
count[1][band] += 1
|
||||
else:
|
||||
# FIXME: This assumes that all the other modes in the ADIF list are digital modes. Is this the case?
|
||||
count[2][band] += 1
|
||||
count[3][band] += 1 # Keep the total of each column in the "Mixed" mode
|
||||
else:
|
||||
logging.error("Could not update the awards table for '%s' because of a database error." % log.name)
|
||||
# Insert the rows containing the totals
|
||||
for i in range(0, len(self.modes)):
|
||||
self.awards.append([self.modes[i]] + count[i])
|
||||
logging.debug("Awards table updated.")
|
||||
return
|
||||
|
|
|
@ -25,388 +25,393 @@ from xml.dom import minidom
|
|||
|
||||
from pyqso.auxiliary_dialogs import *
|
||||
|
||||
|
||||
class CallsignLookupQRZ():
|
||||
""" Use qrz.com to lookup details about a particular callsign. """
|
||||
|
||||
def __init__(self, parent):
|
||||
""" Initialise a new callsign lookup handler.
|
||||
|
||||
:arg parent: The parent Gtk dialog.
|
||||
"""
|
||||
self.parent = parent
|
||||
self.connection = None
|
||||
self.session_key = None
|
||||
return
|
||||
""" Use qrz.com to lookup details about a particular callsign. """
|
||||
|
||||
def connect(self, username, password):
|
||||
""" Initiate a session with the qrz.com server. Hopefully this will provide a session key.
|
||||
|
||||
:arg str username: The username of the qrz.com user account.
|
||||
:arg str password: The password of the qrz.com user account.
|
||||
:returns: True if a successful connection was made to the server, and False otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
logging.debug("Connecting to the qrz.com server...")
|
||||
try:
|
||||
self.connection = http.client.HTTPConnection('xmldata.qrz.com')
|
||||
request = '/xml/current/?username=%s;password=%s;agent=pyqso' % (username, password)
|
||||
self.connection.request('GET', request)
|
||||
response = self.connection.getresponse()
|
||||
except:
|
||||
error(parent=self.parent, message="Could not connect to the qrz.com server. Check connection to the internets?")
|
||||
return False
|
||||
def __init__(self, parent):
|
||||
""" Initialise a new callsign lookup handler.
|
||||
|
||||
xml_data = minidom.parseString(response.read())
|
||||
session_node = xml_data.getElementsByTagName('Session')[0] # There should only be one Session element
|
||||
session_key_node = session_node.getElementsByTagName('Key')
|
||||
if(len(session_key_node) > 0):
|
||||
self.session_key = session_key_node[0].firstChild.nodeValue
|
||||
logging.debug("Successfully connected to the qrz.com server...")
|
||||
connected = True
|
||||
else:
|
||||
connected = False
|
||||
:arg parent: The parent Gtk dialog.
|
||||
"""
|
||||
self.parent = parent
|
||||
self.connection = None
|
||||
self.session_key = None
|
||||
return
|
||||
|
||||
# If there are any errors or warnings, print them out
|
||||
session_error_node = session_node.getElementsByTagName('Error')
|
||||
if(len(session_error_node) > 0):
|
||||
session_error = session_error_node[0].firstChild.nodeValue
|
||||
error(parent=self.parent, message="qrz.com session error: "+session_error)
|
||||
def connect(self, username, password):
|
||||
""" Initiate a session with the qrz.com server. Hopefully this will provide a session key.
|
||||
|
||||
return connected
|
||||
:arg str username: The username of the qrz.com user account.
|
||||
:arg str password: The password of the qrz.com user account.
|
||||
:returns: True if a successful connection was made to the server, and False otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
logging.debug("Connecting to the qrz.com server...")
|
||||
try:
|
||||
self.connection = http.client.HTTPConnection('xmldata.qrz.com')
|
||||
request = '/xml/current/?username=%s;password=%s;agent=pyqso' % (username, password)
|
||||
self.connection.request('GET', request)
|
||||
response = self.connection.getresponse()
|
||||
except:
|
||||
error(parent=self.parent, message="Could not connect to the qrz.com server. Check connection to the internets?")
|
||||
return False
|
||||
|
||||
def lookup(self, full_callsign, ignore_prefix_suffix = True):
|
||||
""" Parse the XML tree that is returned from the qrz.com XML server to obtain the NAME, ADDRESS, STATE, COUNTRY, DXCC, CQZ, ITUZ, and IOTA field data (if present).
|
||||
|
||||
:arg str full_callsign: The callsign to look up (without any prefix/suffix stripping).
|
||||
:arg bool ignore_prefix_suffix: True if callsign prefixes/suffixes should be removed prior to querying the server, False otherwise.
|
||||
:returns: The data in a dictionary called fields_and_data.
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
logging.debug("Looking up callsign. The full callsign (with a prefix and/or suffix) is %s" % full_callsign)
|
||||
|
||||
# Remove any prefix or suffix from the callsign before performing the lookup.
|
||||
if(ignore_prefix_suffix):
|
||||
callsign = strip(full_callsign)
|
||||
else:
|
||||
callsign = full_callsign
|
||||
|
||||
# Commence lookup.
|
||||
fields_and_data = {"NAME":"", "ADDRESS":"", "STATE":"", "COUNTRY":"", "DXCC":"", "CQZ":"", "ITUZ":"", "IOTA":""}
|
||||
if(self.session_key):
|
||||
request = '/xml/current/?s=%s;callsign=%s' % (self.session_key, callsign)
|
||||
self.connection.request('GET', request)
|
||||
response = self.connection.getresponse()
|
||||
xml_data = minidom.parseString(response.read())
|
||||
session_node = xml_data.getElementsByTagName('Session')[0] # There should only be one Session element
|
||||
session_key_node = session_node.getElementsByTagName('Key')
|
||||
if(len(session_key_node) > 0):
|
||||
self.session_key = session_key_node[0].firstChild.nodeValue
|
||||
logging.debug("Successfully connected to the qrz.com server...")
|
||||
connected = True
|
||||
else:
|
||||
connected = False
|
||||
|
||||
xml_data = minidom.parseString(response.read())
|
||||
callsign_node = xml_data.getElementsByTagName('Callsign')
|
||||
if(len(callsign_node) > 0):
|
||||
callsign_node = callsign_node[0] # There should only be a maximum of one Callsign element
|
||||
# If there are any errors or warnings, print them out
|
||||
session_error_node = session_node.getElementsByTagName('Error')
|
||||
if(len(session_error_node) > 0):
|
||||
session_error = session_error_node[0].firstChild.nodeValue
|
||||
error(parent=self.parent, message="qrz.com session error: "+session_error)
|
||||
|
||||
callsign_fname_node = callsign_node.getElementsByTagName('fname')
|
||||
callsign_name_node = callsign_node.getElementsByTagName('name')
|
||||
if(len(callsign_fname_node) > 0):
|
||||
fields_and_data["NAME"] = callsign_fname_node[0].firstChild.nodeValue
|
||||
if(len(callsign_name_node) > 0): # Add the surname, if present
|
||||
fields_and_data["NAME"] = fields_and_data["NAME"] + " " + callsign_name_node[0].firstChild.nodeValue
|
||||
return connected
|
||||
|
||||
callsign_addr1_node = callsign_node.getElementsByTagName('addr1')
|
||||
callsign_addr2_node = callsign_node.getElementsByTagName('addr2')
|
||||
if(len(callsign_addr1_node) > 0):
|
||||
fields_and_data["ADDRESS"] = callsign_addr1_node[0].firstChild.nodeValue
|
||||
if(len(callsign_addr2_node) > 0): # Add the second line of the address, if present
|
||||
fields_and_data["ADDRESS"] = (fields_and_data["ADDRESS"] + ", " if len(callsign_addr1_node) > 0 else "") + callsign_addr2_node[0].firstChild.nodeValue
|
||||
def lookup(self, full_callsign, ignore_prefix_suffix=True):
|
||||
""" Parse the XML tree that is returned from the qrz.com XML server to obtain the NAME, ADDRESS, STATE, COUNTRY, DXCC, CQZ, ITUZ, and IOTA field data (if present).
|
||||
|
||||
callsign_state_node = callsign_node.getElementsByTagName('state')
|
||||
if(len(callsign_state_node) > 0):
|
||||
fields_and_data["STATE"] = callsign_state_node[0].firstChild.nodeValue
|
||||
:arg str full_callsign: The callsign to look up (without any prefix/suffix stripping).
|
||||
:arg bool ignore_prefix_suffix: True if callsign prefixes/suffixes should be removed prior to querying the server, False otherwise.
|
||||
:returns: The data in a dictionary called fields_and_data.
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
callsign_country_node = callsign_node.getElementsByTagName('country')
|
||||
if(len(callsign_country_node) > 0):
|
||||
fields_and_data["COUNTRY"] = callsign_country_node[0].firstChild.nodeValue
|
||||
logging.debug("Looking up callsign. The full callsign (with a prefix and/or suffix) is %s" % full_callsign)
|
||||
|
||||
callsign_ccode_node = callsign_node.getElementsByTagName('ccode')
|
||||
if(len(callsign_ccode_node) > 0):
|
||||
fields_and_data["DXCC"] = callsign_ccode_node[0].firstChild.nodeValue
|
||||
# Remove any prefix or suffix from the callsign before performing the lookup.
|
||||
if(ignore_prefix_suffix):
|
||||
callsign = strip(full_callsign)
|
||||
else:
|
||||
callsign = full_callsign
|
||||
|
||||
callsign_cqzone_node = callsign_node.getElementsByTagName('cqzone')
|
||||
if(len(callsign_cqzone_node) > 0):
|
||||
fields_and_data["CQZ"] = callsign_cqzone_node[0].firstChild.nodeValue
|
||||
# Commence lookup.
|
||||
fields_and_data = {"NAME": "", "ADDRESS": "", "STATE": "", "COUNTRY": "", "DXCC": "", "CQZ": "", "ITUZ": "", "IOTA": ""}
|
||||
if(self.session_key):
|
||||
request = '/xml/current/?s=%s;callsign=%s' % (self.session_key, callsign)
|
||||
self.connection.request('GET', request)
|
||||
response = self.connection.getresponse()
|
||||
|
||||
callsign_ituzone_node = callsign_node.getElementsByTagName('ituzone')
|
||||
if(len(callsign_ituzone_node) > 0):
|
||||
fields_and_data["ITUZ"] = callsign_ituzone_node[0].firstChild.nodeValue
|
||||
xml_data = minidom.parseString(response.read())
|
||||
callsign_node = xml_data.getElementsByTagName('Callsign')
|
||||
if(len(callsign_node) > 0):
|
||||
callsign_node = callsign_node[0] # There should only be a maximum of one Callsign element
|
||||
|
||||
callsign_iota_node = callsign_node.getElementsByTagName('iota')
|
||||
if(len(callsign_iota_node) > 0):
|
||||
fields_and_data["IOTA"] = callsign_iota_node[0].firstChild.nodeValue
|
||||
else:
|
||||
# If there is no Callsign element, then print out the error message in the Session element
|
||||
session_node = xml_data.getElementsByTagName('Session')
|
||||
if(len(session_node) > 0):
|
||||
session_error_node = session_node[0].getElementsByTagName('Error')
|
||||
if(len(session_error_node) > 0):
|
||||
session_error = session_error_node[0].firstChild.nodeValue
|
||||
error(parent=self.parent, message=session_error)
|
||||
# Return empty strings for the field data
|
||||
logging.debug("Callsign lookup complete. Returning data...")
|
||||
return fields_and_data
|
||||
callsign_fname_node = callsign_node.getElementsByTagName('fname')
|
||||
callsign_name_node = callsign_node.getElementsByTagName('name')
|
||||
if(len(callsign_fname_node) > 0):
|
||||
fields_and_data["NAME"] = callsign_fname_node[0].firstChild.nodeValue
|
||||
if(len(callsign_name_node) > 0): # Add the surname, if present
|
||||
fields_and_data["NAME"] = fields_and_data["NAME"] + " " + callsign_name_node[0].firstChild.nodeValue
|
||||
|
||||
callsign_addr1_node = callsign_node.getElementsByTagName('addr1')
|
||||
callsign_addr2_node = callsign_node.getElementsByTagName('addr2')
|
||||
if(len(callsign_addr1_node) > 0):
|
||||
fields_and_data["ADDRESS"] = callsign_addr1_node[0].firstChild.nodeValue
|
||||
if(len(callsign_addr2_node) > 0): # Add the second line of the address, if present
|
||||
fields_and_data["ADDRESS"] = (fields_and_data["ADDRESS"] + ", " if len(callsign_addr1_node) > 0 else "") + callsign_addr2_node[0].firstChild.nodeValue
|
||||
|
||||
callsign_state_node = callsign_node.getElementsByTagName('state')
|
||||
if(len(callsign_state_node) > 0):
|
||||
fields_and_data["STATE"] = callsign_state_node[0].firstChild.nodeValue
|
||||
|
||||
callsign_country_node = callsign_node.getElementsByTagName('country')
|
||||
if(len(callsign_country_node) > 0):
|
||||
fields_and_data["COUNTRY"] = callsign_country_node[0].firstChild.nodeValue
|
||||
|
||||
callsign_ccode_node = callsign_node.getElementsByTagName('ccode')
|
||||
if(len(callsign_ccode_node) > 0):
|
||||
fields_and_data["DXCC"] = callsign_ccode_node[0].firstChild.nodeValue
|
||||
|
||||
callsign_cqzone_node = callsign_node.getElementsByTagName('cqzone')
|
||||
if(len(callsign_cqzone_node) > 0):
|
||||
fields_and_data["CQZ"] = callsign_cqzone_node[0].firstChild.nodeValue
|
||||
|
||||
callsign_ituzone_node = callsign_node.getElementsByTagName('ituzone')
|
||||
if(len(callsign_ituzone_node) > 0):
|
||||
fields_and_data["ITUZ"] = callsign_ituzone_node[0].firstChild.nodeValue
|
||||
|
||||
callsign_iota_node = callsign_node.getElementsByTagName('iota')
|
||||
if(len(callsign_iota_node) > 0):
|
||||
fields_and_data["IOTA"] = callsign_iota_node[0].firstChild.nodeValue
|
||||
else:
|
||||
# If there is no Callsign element, then print out the error message in the Session element
|
||||
session_node = xml_data.getElementsByTagName('Session')
|
||||
if(len(session_node) > 0):
|
||||
session_error_node = session_node[0].getElementsByTagName('Error')
|
||||
if(len(session_error_node) > 0):
|
||||
session_error = session_error_node[0].firstChild.nodeValue
|
||||
error(parent=self.parent, message=session_error)
|
||||
# Return empty strings for the field data
|
||||
logging.debug("Callsign lookup complete. Returning data...")
|
||||
return fields_and_data
|
||||
|
||||
|
||||
class CallsignLookupHamQTH():
|
||||
""" Use hamqth.com to lookup details about a particular callsign. """
|
||||
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
self.connection = None
|
||||
self.session_id = None
|
||||
return
|
||||
""" Use hamqth.com to lookup details about a particular callsign. """
|
||||
|
||||
def connect(self, username, password):
|
||||
""" Initiate a session with the hamqth.com server. Hopefully this will provide a session key.
|
||||
|
||||
:arg str username: The username of the hamqth.com user account.
|
||||
:arg str password: The password of the hamqth.com user account.
|
||||
:returns: True if a successful connection was made to the server, and False otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
def __init__(self, parent):
|
||||
self.parent = parent
|
||||
self.connection = None
|
||||
self.session_id = None
|
||||
return
|
||||
|
||||
logging.debug("Connecting to the hamqth.com server...")
|
||||
try:
|
||||
self.connection = http.client.HTTPConnection('www.hamqth.com')
|
||||
request = '/xml.php?u=%s&p=%s' % (username, password)
|
||||
self.connection.request('GET', request)
|
||||
response = self.connection.getresponse()
|
||||
except:
|
||||
error(parent=self.parent, message="Could not connect to the hamqth.com server. Check connection to the internets?")
|
||||
return False
|
||||
def connect(self, username, password):
|
||||
""" Initiate a session with the hamqth.com server. Hopefully this will provide a session key.
|
||||
|
||||
xml_data = minidom.parseString(response.read())
|
||||
session_node = xml_data.getElementsByTagName('session')[0] # There should only be one Session element
|
||||
session_id_node = session_node.getElementsByTagName('session_id')
|
||||
if(len(session_id_node) > 0):
|
||||
self.session_id = session_id_node[0].firstChild.nodeValue
|
||||
logging.debug("Successfully connected to the hamqth.com server...")
|
||||
connected = True
|
||||
else:
|
||||
connected = False
|
||||
:arg str username: The username of the hamqth.com user account.
|
||||
:arg str password: The password of the hamqth.com user account.
|
||||
:returns: True if a successful connection was made to the server, and False otherwise.
|
||||
:rtype: bool
|
||||
"""
|
||||
|
||||
# If there are any errors or warnings, print them out
|
||||
session_error_node = session_node.getElementsByTagName('error')
|
||||
if(len(session_error_node) > 0):
|
||||
session_error = session_error_node[0].firstChild.nodeValue
|
||||
error(parent=self.parent, message="hamqth.com session error: "+session_error)
|
||||
logging.debug("Connecting to the hamqth.com server...")
|
||||
try:
|
||||
self.connection = http.client.HTTPConnection('www.hamqth.com')
|
||||
request = '/xml.php?u=%s&p=%s' % (username, password)
|
||||
self.connection.request('GET', request)
|
||||
response = self.connection.getresponse()
|
||||
except:
|
||||
error(parent=self.parent, message="Could not connect to the hamqth.com server. Check connection to the internets?")
|
||||
return False
|
||||
|
||||
return connected
|
||||
xml_data = minidom.parseString(response.read())
|
||||
session_node = xml_data.getElementsByTagName('session')[0] # There should only be one Session element
|
||||
session_id_node = session_node.getElementsByTagName('session_id')
|
||||
if(len(session_id_node) > 0):
|
||||
self.session_id = session_id_node[0].firstChild.nodeValue
|
||||
logging.debug("Successfully connected to the hamqth.com server...")
|
||||
connected = True
|
||||
else:
|
||||
connected = False
|
||||
|
||||
def lookup(self, full_callsign, ignore_prefix_suffix = True):
|
||||
""" Parse the XML tree that is returned from the hamqth.com XML server to obtain the NAME, ADDRESS, STATE, COUNTRY, DXCC, CQZ, ITUZ, and IOTA field data (if present),
|
||||
# If there are any errors or warnings, print them out
|
||||
session_error_node = session_node.getElementsByTagName('error')
|
||||
if(len(session_error_node) > 0):
|
||||
session_error = session_error_node[0].firstChild.nodeValue
|
||||
error(parent=self.parent, message="hamqth.com session error: "+session_error)
|
||||
|
||||
:arg str full_callsign: The callsign to look up (without any prefix/suffix stripping).
|
||||
:arg bool ignore_prefix_suffix: True if callsign prefixes/suffixes should be removed prior to querying the server, False otherwise.
|
||||
:returns: The data in a dictionary called fields_and_data.
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
logging.debug("Looking up callsign. The full callsign (with a prefix and/or suffix) is %s" % full_callsign)
|
||||
|
||||
# Remove any prefix or suffix from the callsign before performing the lookup.
|
||||
if(ignore_prefix_suffix):
|
||||
callsign = strip(full_callsign)
|
||||
else:
|
||||
callsign = full_callsign
|
||||
|
||||
# Commence lookup.
|
||||
fields_and_data = {"NAME":"", "ADDRESS":"", "STATE":"", "COUNTRY":"", "DXCC":"", "CQZ":"", "ITUZ":"", "IOTA":""}
|
||||
if(self.session_id):
|
||||
request = '/xml.php?id=%s&callsign=%s&prg=pyqso' % (self.session_id, callsign)
|
||||
self.connection.request('GET', request)
|
||||
response = self.connection.getresponse()
|
||||
return connected
|
||||
|
||||
xml_data = minidom.parseString(response.read())
|
||||
search_node = xml_data.getElementsByTagName('search')
|
||||
if(len(search_node) > 0):
|
||||
search_node = search_node[0] # There should only be a maximum of one Callsign element
|
||||
def lookup(self, full_callsign, ignore_prefix_suffix=True):
|
||||
""" Parse the XML tree that is returned from the hamqth.com XML server to obtain the NAME, ADDRESS, STATE, COUNTRY, DXCC, CQZ, ITUZ, and IOTA field data (if present),
|
||||
|
||||
search_name_node = search_node.getElementsByTagName('nick')
|
||||
if(len(search_name_node) > 0):
|
||||
fields_and_data["NAME"] = search_name_node[0].firstChild.nodeValue
|
||||
:arg str full_callsign: The callsign to look up (without any prefix/suffix stripping).
|
||||
:arg bool ignore_prefix_suffix: True if callsign prefixes/suffixes should be removed prior to querying the server, False otherwise.
|
||||
:returns: The data in a dictionary called fields_and_data.
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
search_addr1_node = search_node.getElementsByTagName('adr_street1')
|
||||
search_addr2_node = search_node.getElementsByTagName('adr_street2')
|
||||
if(len(search_addr1_node) > 0):
|
||||
fields_and_data["ADDRESS"] = search_addr1_node[0].firstChild.nodeValue
|
||||
if(len(search_addr2_node) > 0): # Add the second line of the address, if present
|
||||
fields_and_data["ADDRESS"] = (fields_and_data["ADDRESS"] + ", " if len(search_addr1_node) > 0 else "") + search_addr2_node[0].firstChild.nodeValue
|
||||
logging.debug("Looking up callsign. The full callsign (with a prefix and/or suffix) is %s" % full_callsign)
|
||||
|
||||
search_state_node = search_node.getElementsByTagName('us_state')
|
||||
if(len(search_state_node) > 0):
|
||||
fields_and_data["STATE"] = search_state_node[0].firstChild.nodeValue
|
||||
# Remove any prefix or suffix from the callsign before performing the lookup.
|
||||
if(ignore_prefix_suffix):
|
||||
callsign = strip(full_callsign)
|
||||
else:
|
||||
callsign = full_callsign
|
||||
|
||||
search_country_node = search_node.getElementsByTagName('country')
|
||||
if(len(search_country_node) > 0):
|
||||
fields_and_data["COUNTRY"] = search_country_node[0].firstChild.nodeValue
|
||||
# Commence lookup.
|
||||
fields_and_data = {"NAME": "", "ADDRESS": "", "STATE": "", "COUNTRY": "", "DXCC": "", "CQZ": "", "ITUZ": "", "IOTA": ""}
|
||||
if(self.session_id):
|
||||
request = '/xml.php?id=%s&callsign=%s&prg=pyqso' % (self.session_id, callsign)
|
||||
self.connection.request('GET', request)
|
||||
response = self.connection.getresponse()
|
||||
|
||||
search_cqzone_node = search_node.getElementsByTagName('cq')
|
||||
if(len(search_cqzone_node) > 0):
|
||||
fields_and_data["CQZ"] = search_cqzone_node[0].firstChild.nodeValue
|
||||
xml_data = minidom.parseString(response.read())
|
||||
search_node = xml_data.getElementsByTagName('search')
|
||||
if(len(search_node) > 0):
|
||||
search_node = search_node[0] # There should only be a maximum of one Callsign element
|
||||
|
||||
search_ituzone_node = search_node.getElementsByTagName('itu')
|
||||
if(len(search_ituzone_node) > 0):
|
||||
fields_and_data["ITUZ"] = search_ituzone_node[0].firstChild.nodeValue
|
||||
search_name_node = search_node.getElementsByTagName('nick')
|
||||
if(len(search_name_node) > 0):
|
||||
fields_and_data["NAME"] = search_name_node[0].firstChild.nodeValue
|
||||
|
||||
search_iota_node = search_node.getElementsByTagName('grid')
|
||||
if(len(search_iota_node) > 0):
|
||||
fields_and_data["IOTA"] = search_iota_node[0].firstChild.nodeValue
|
||||
else:
|
||||
# If there is no Callsign element, then print out the error message in the Session element
|
||||
session_node = xml_data.getElementsByTagName('session')
|
||||
if(len(session_node) > 0):
|
||||
session_error_node = session_node[0].getElementsByTagName('error')
|
||||
if(len(session_error_node) > 0):
|
||||
session_error = session_error_node[0].firstChild.nodeValue
|
||||
error(parent=self.parent, message=session_error)
|
||||
# Return empty strings for the field data
|
||||
logging.debug("Callsign lookup complete. Returning data...")
|
||||
return fields_and_data
|
||||
search_addr1_node = search_node.getElementsByTagName('adr_street1')
|
||||
search_addr2_node = search_node.getElementsByTagName('adr_street2')
|
||||
if(len(search_addr1_node) > 0):
|
||||
fields_and_data["ADDRESS"] = search_addr1_node[0].firstChild.nodeValue
|
||||
if(len(search_addr2_node) > 0): # Add the second line of the address, if present
|
||||
fields_and_data["ADDRESS"] = (fields_and_data["ADDRESS"] + ", " if len(search_addr1_node) > 0 else "") + search_addr2_node[0].firstChild.nodeValue
|
||||
|
||||
search_state_node = search_node.getElementsByTagName('us_state')
|
||||
if(len(search_state_node) > 0):
|
||||
fields_and_data["STATE"] = search_state_node[0].firstChild.nodeValue
|
||||
|
||||
search_country_node = search_node.getElementsByTagName('country')
|
||||
if(len(search_country_node) > 0):
|
||||
fields_and_data["COUNTRY"] = search_country_node[0].firstChild.nodeValue
|
||||
|
||||
search_cqzone_node = search_node.getElementsByTagName('cq')
|
||||
if(len(search_cqzone_node) > 0):
|
||||
fields_and_data["CQZ"] = search_cqzone_node[0].firstChild.nodeValue
|
||||
|
||||
search_ituzone_node = search_node.getElementsByTagName('itu')
|
||||
if(len(search_ituzone_node) > 0):
|
||||
fields_and_data["ITUZ"] = search_ituzone_node[0].firstChild.nodeValue
|
||||
|
||||
search_iota_node = search_node.getElementsByTagName('grid')
|
||||
if(len(search_iota_node) > 0):
|
||||
fields_and_data["IOTA"] = search_iota_node[0].firstChild.nodeValue
|
||||
else:
|
||||
# If there is no Callsign element, then print out the error message in the Session element
|
||||
session_node = xml_data.getElementsByTagName('session')
|
||||
if(len(session_node) > 0):
|
||||
session_error_node = session_node[0].getElementsByTagName('error')
|
||||
if(len(session_error_node) > 0):
|
||||
session_error = session_error_node[0].firstChild.nodeValue
|
||||
error(parent=self.parent, message=session_error)
|
||||
# Return empty strings for the field data
|
||||
logging.debug("Callsign lookup complete. Returning data...")
|
||||
return fields_and_data
|
||||
|
||||
|
||||
def strip(full_callsign):
|
||||
""" Remove any prefixes or suffixes from a callsign.
|
||||
|
||||
:arg str full_callsign: The callsign to be considered for prefix/suffix removal.
|
||||
:returns: The callsign with prefixes/suffixes removed.
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
components = full_callsign.split("/") # We assume that prefixes or suffixes come before/after a forward slash character "/".
|
||||
suffixes = ["P", "M", "A", "PM", "MM", "AM", "QRP"]
|
||||
try:
|
||||
if(len(components) == 3):
|
||||
# We have both a prefix and a suffix.
|
||||
callsign = components[1]
|
||||
|
||||
elif(len(components) == 2):
|
||||
if(components[1].upper() in suffixes or components[1].lower() in suffixes):
|
||||
# If the last part of the full_callsign is a valid suffix, then use the part before that.
|
||||
callsign = components[0]
|
||||
logging.debug("Suffix %s found. Callsign to lookup is %s" % (components[1], callsign))
|
||||
else:
|
||||
# We have a prefix, so take the part after the first "/".
|
||||
""" Remove any prefixes or suffixes from a callsign.
|
||||
|
||||
:arg str full_callsign: The callsign to be considered for prefix/suffix removal.
|
||||
:returns: The callsign with prefixes/suffixes removed.
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
components = full_callsign.split("/") # We assume that prefixes or suffixes come before/after a forward slash character "/".
|
||||
suffixes = ["P", "M", "A", "PM", "MM", "AM", "QRP"]
|
||||
try:
|
||||
if(len(components) == 3):
|
||||
# We have both a prefix and a suffix.
|
||||
callsign = components[1]
|
||||
logging.debug("Prefix %s found. Callsign to lookup is %s" % (components[0], callsign))
|
||||
|
||||
elif(len(components) == 1):
|
||||
# We have neither a prefix nor a suffix, so use the full_callsign.
|
||||
callsign = full_callsign
|
||||
logging.debug("No prefix or suffix found. Callsign to lookup is %s" % callsign)
|
||||
|
||||
else:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
callsign = full_callsign
|
||||
return callsign
|
||||
|
||||
|
||||
elif(len(components) == 2):
|
||||
if(components[1].upper() in suffixes or components[1].lower() in suffixes):
|
||||
# If the last part of the full_callsign is a valid suffix, then use the part before that.
|
||||
callsign = components[0]
|
||||
logging.debug("Suffix %s found. Callsign to lookup is %s" % (components[1], callsign))
|
||||
else:
|
||||
# We have a prefix, so take the part after the first "/".
|
||||
callsign = components[1]
|
||||
logging.debug("Prefix %s found. Callsign to lookup is %s" % (components[0], callsign))
|
||||
|
||||
elif(len(components) == 1):
|
||||
# We have neither a prefix nor a suffix, so use the full_callsign.
|
||||
callsign = full_callsign
|
||||
logging.debug("No prefix or suffix found. Callsign to lookup is %s" % callsign)
|
||||
|
||||
else:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
callsign = full_callsign
|
||||
return callsign
|
||||
|
||||
|
||||
class TestCallsignLookup(unittest.TestCase):
|
||||
""" The unit tests for the CallsignLookup class. """
|
||||
|
||||
def setUp(self):
|
||||
""" Set up the objects needed for the unit tests. """
|
||||
self.qrz = CallsignLookupQRZ(parent=None)
|
||||
self.hamqth = CallsignLookupHamQTH(parent=None)
|
||||
""" The unit tests for the CallsignLookup class. """
|
||||
|
||||
def tearDown(self):
|
||||
""" Destroy any unit test resources. """
|
||||
pass
|
||||
def setUp(self):
|
||||
""" Set up the objects needed for the unit tests. """
|
||||
self.qrz = CallsignLookupQRZ(parent=None)
|
||||
self.hamqth = CallsignLookupHamQTH(parent=None)
|
||||
|
||||
def test_strip(self):
|
||||
""" Check that a callsign with a prefix and a suffix is stripped correctly. """
|
||||
callsign = "EA3/MYCALL/MM"
|
||||
assert strip(callsign) == "MYCALL"
|
||||
|
||||
def test_strip_prefix_only(self):
|
||||
""" Check that a callsign with only a prefix is stripped correctly. """
|
||||
callsign = "EA3/MYCALL"
|
||||
assert strip(callsign) == "MYCALL"
|
||||
|
||||
def test_strip_suffix_only(self):
|
||||
""" Check that a callsign with only a suffix is stripped correctly. """
|
||||
callsign = "MYCALL/M"
|
||||
assert strip(callsign) == "MYCALL"
|
||||
def tearDown(self):
|
||||
""" Destroy any unit test resources. """
|
||||
pass
|
||||
|
||||
def test_strip_no_prefix_or_suffix(self):
|
||||
""" Check that a callsign with no prefix or suffix remains unmodified. """
|
||||
callsign = "MYCALL"
|
||||
assert strip(callsign) == "MYCALL"
|
||||
def test_strip(self):
|
||||
""" Check that a callsign with a prefix and a suffix is stripped correctly. """
|
||||
callsign = "EA3/MYCALL/MM"
|
||||
assert strip(callsign) == "MYCALL"
|
||||
|
||||
def test_qrz_connect(self):
|
||||
""" Check the example response from the qrz.com server, and make sure the session key has been correctly extracted. """
|
||||
def test_strip_prefix_only(self):
|
||||
""" Check that a callsign with only a prefix is stripped correctly. """
|
||||
callsign = "EA3/MYCALL"
|
||||
assert strip(callsign) == "MYCALL"
|
||||
|
||||
http.client.HTTPConnection = unittest.mock.Mock(spec=http.client.HTTPConnection)
|
||||
http.client.HTTPResponse = unittest.mock.Mock(spec=http.client.HTTPResponse)
|
||||
connection = http.client.HTTPConnection()
|
||||
response = http.client.HTTPResponse()
|
||||
def test_strip_suffix_only(self):
|
||||
""" Check that a callsign with only a suffix is stripped correctly. """
|
||||
callsign = "MYCALL/M"
|
||||
assert strip(callsign) == "MYCALL"
|
||||
|
||||
response.read.return_value = b'<?xml version="1.0" encoding="utf-8" ?>\n<QRZDatabase version="1.33" xmlns="http://xmldata.qrz.com">\n<Session>\n<Key>3b1fd1d3ba495189984f93ff67bd45b6</Key>\n<Count>61</Count>\n<SubExp>non-subscriber</SubExp>\n<GMTime>Sun Nov 22 21:25:34 2015</GMTime>\n<Remark>cpu: 0.147s</Remark>\n</Session>\n</QRZDatabase>\n'
|
||||
connection.getresponse.return_value = response
|
||||
def test_strip_no_prefix_or_suffix(self):
|
||||
""" Check that a callsign with no prefix or suffix remains unmodified. """
|
||||
callsign = "MYCALL"
|
||||
assert strip(callsign) == "MYCALL"
|
||||
|
||||
result = self.qrz.connect("hello", "world")
|
||||
assert(result)
|
||||
assert(self.qrz.session_key == "3b1fd1d3ba495189984f93ff67bd45b6")
|
||||
def test_qrz_connect(self):
|
||||
""" Check the example response from the qrz.com server, and make sure the session key has been correctly extracted. """
|
||||
|
||||
def test_qrz_lookup(self):
|
||||
""" Check the example callsign lookup response from the qrz.com server, and make sure the callsign information has been correctly extracted. """
|
||||
http.client.HTTPConnection = unittest.mock.Mock(spec=http.client.HTTPConnection)
|
||||
http.client.HTTPResponse = unittest.mock.Mock(spec=http.client.HTTPResponse)
|
||||
connection = http.client.HTTPConnection()
|
||||
response = http.client.HTTPResponse()
|
||||
|
||||
http.client.HTTPConnection = unittest.mock.Mock(spec=http.client.HTTPConnection)
|
||||
http.client.HTTPResponse = unittest.mock.Mock(spec=http.client.HTTPResponse)
|
||||
connection = http.client.HTTPConnection()
|
||||
response = http.client.HTTPResponse()
|
||||
response.read.return_value = b'<?xml version="1.0" encoding="utf-8" ?>\n<QRZDatabase version="1.33" xmlns="http://xmldata.qrz.com">\n<Session>\n<Key>3b1fd1d3ba495189984f93ff67bd45b6</Key>\n<Count>61</Count>\n<SubExp>non-subscriber</SubExp>\n<GMTime>Sun Nov 22 21:25:34 2015</GMTime>\n<Remark>cpu: 0.147s</Remark>\n</Session>\n</QRZDatabase>\n'
|
||||
connection.getresponse.return_value = response
|
||||
|
||||
response.read.return_value = b'<?xml version="1.0" encoding="utf-8" ?>\n<QRZDatabase version="1.33" xmlns="http://xmldata.qrz.com">\n<Callsign>\n<call>MYCALL</call>\n<fname>FIRSTNAME</fname>\n<name>LASTNAME</name>\n<addr2>ADDRESS2</addr2>\n<country>COUNTRY</country>\n</Callsign>\n<Session>\n<Key>3b1fd1d3ba495189984f93ff67bd45b6</Key>\n<Count>61</Count>\n<SubExp>non-subscriber</SubExp>\n<Message>A subscription is required to access the complete record.</Message>\n<GMTime>Sun Nov 22 21:34:46 2015</GMTime>\n<Remark>cpu: 0.026s</Remark>\n</Session>\n</QRZDatabase>\n'
|
||||
connection.getresponse.return_value = response
|
||||
result = self.qrz.connect("hello", "world")
|
||||
assert(result)
|
||||
assert(self.qrz.session_key == "3b1fd1d3ba495189984f93ff67bd45b6")
|
||||
|
||||
self.qrz.connection = connection
|
||||
self.qrz.session_key = "3b1fd1d3ba495189984f93ff67bd45b6"
|
||||
fields_and_data = self.qrz.lookup("MYCALL")
|
||||
assert(fields_and_data["NAME"] == "FIRSTNAME LASTNAME")
|
||||
assert(fields_and_data["ADDRESS"] == "ADDRESS2")
|
||||
assert(fields_and_data["COUNTRY"] == "COUNTRY")
|
||||
def test_qrz_lookup(self):
|
||||
""" Check the example callsign lookup response from the qrz.com server, and make sure the callsign information has been correctly extracted. """
|
||||
|
||||
def test_hamqth_connect(self):
|
||||
""" Check the example response from the hamqth.com server, and make sure the session ID has been correctly extracted. """
|
||||
http.client.HTTPConnection = unittest.mock.Mock(spec=http.client.HTTPConnection)
|
||||
http.client.HTTPResponse = unittest.mock.Mock(spec=http.client.HTTPResponse)
|
||||
connection = http.client.HTTPConnection()
|
||||
response = http.client.HTTPResponse()
|
||||
|
||||
http.client.HTTPConnection = unittest.mock.Mock(spec=http.client.HTTPConnection)
|
||||
http.client.HTTPResponse = unittest.mock.Mock(spec=http.client.HTTPResponse)
|
||||
connection = http.client.HTTPConnection()
|
||||
response = http.client.HTTPResponse()
|
||||
response.read.return_value = b'<?xml version="1.0" encoding="utf-8" ?>\n<QRZDatabase version="1.33" xmlns="http://xmldata.qrz.com">\n<Callsign>\n<call>MYCALL</call>\n<fname>FIRSTNAME</fname>\n<name>LASTNAME</name>\n<addr2>ADDRESS2</addr2>\n<country>COUNTRY</country>\n</Callsign>\n<Session>\n<Key>3b1fd1d3ba495189984f93ff67bd45b6</Key>\n<Count>61</Count>\n<SubExp>non-subscriber</SubExp>\n<Message>A subscription is required to access the complete record.</Message>\n<GMTime>Sun Nov 22 21:34:46 2015</GMTime>\n<Remark>cpu: 0.026s</Remark>\n</Session>\n</QRZDatabase>\n'
|
||||
connection.getresponse.return_value = response
|
||||
|
||||
response.read.return_value = b'<?xml version="1.0"?>\n<HamQTH version="2.6" xmlns="https://www.hamqth.com">\n<session>\n<session_id>09b0ae90050be03c452ad235a1f2915ad684393c</session_id>\n</session>\n</HamQTH>\n'
|
||||
connection.getresponse.return_value = response
|
||||
self.qrz.connection = connection
|
||||
self.qrz.session_key = "3b1fd1d3ba495189984f93ff67bd45b6"
|
||||
fields_and_data = self.qrz.lookup("MYCALL")
|
||||
assert(fields_and_data["NAME"] == "FIRSTNAME LASTNAME")
|
||||
assert(fields_and_data["ADDRESS"] == "ADDRESS2")
|
||||
assert(fields_and_data["COUNTRY"] == "COUNTRY")
|
||||
|
||||
result = self.hamqth.connect("hello", "world")
|
||||
assert(result)
|
||||
assert(self.hamqth.session_id == "09b0ae90050be03c452ad235a1f2915ad684393c")
|
||||
def test_hamqth_connect(self):
|
||||
""" Check the example response from the hamqth.com server, and make sure the session ID has been correctly extracted. """
|
||||
|
||||
def test_hamqth_lookup(self):
|
||||
""" Check the example callsign lookup response from the hamqth.com server, and make sure the callsign information has been correctly extracted. """
|
||||
http.client.HTTPConnection = unittest.mock.Mock(spec=http.client.HTTPConnection)
|
||||
http.client.HTTPResponse = unittest.mock.Mock(spec=http.client.HTTPResponse)
|
||||
connection = http.client.HTTPConnection()
|
||||
response = http.client.HTTPResponse()
|
||||
|
||||
http.client.HTTPConnection = unittest.mock.Mock(spec=http.client.HTTPConnection)
|
||||
http.client.HTTPResponse = unittest.mock.Mock(spec=http.client.HTTPResponse)
|
||||
connection = http.client.HTTPConnection()
|
||||
response = http.client.HTTPResponse()
|
||||
response.read.return_value = b'<?xml version="1.0"?>\n<HamQTH version="2.6" xmlns="https://www.hamqth.com">\n<session>\n<session_id>09b0ae90050be03c452ad235a1f2915ad684393c</session_id>\n</session>\n</HamQTH>\n'
|
||||
connection.getresponse.return_value = response
|
||||
|
||||
response.read.return_value = b'<?xml version="1.0"?>\n<HamQTH version="2.6" xmlns="https://www.hamqth.com">\n<search>\n<callsign>MYCALL</callsign>\n<nick>NAME</nick>\n<country>COUNTRY</country>\n<itu>ITU</itu>\n<cq>CQ</cq>\n<grid>GRID</grid>\n<adr_street1>ADDRESS</adr_street1>\n</search>\n</HamQTH>\n'
|
||||
connection.getresponse.return_value = response
|
||||
result = self.hamqth.connect("hello", "world")
|
||||
assert(result)
|
||||
assert(self.hamqth.session_id == "09b0ae90050be03c452ad235a1f2915ad684393c")
|
||||
|
||||
def test_hamqth_lookup(self):
|
||||
""" Check the example callsign lookup response from the hamqth.com server, and make sure the callsign information has been correctly extracted. """
|
||||
|
||||
http.client.HTTPConnection = unittest.mock.Mock(spec=http.client.HTTPConnection)
|
||||
http.client.HTTPResponse = unittest.mock.Mock(spec=http.client.HTTPResponse)
|
||||
connection = http.client.HTTPConnection()
|
||||
response = http.client.HTTPResponse()
|
||||
|
||||
response.read.return_value = b'<?xml version="1.0"?>\n<HamQTH version="2.6" xmlns="https://www.hamqth.com">\n<search>\n<callsign>MYCALL</callsign>\n<nick>NAME</nick>\n<country>COUNTRY</country>\n<itu>ITU</itu>\n<cq>CQ</cq>\n<grid>GRID</grid>\n<adr_street1>ADDRESS</adr_street1>\n</search>\n</HamQTH>\n'
|
||||
connection.getresponse.return_value = response
|
||||
|
||||
self.hamqth.connection = connection
|
||||
self.hamqth.session_id = "09b0ae90050be03c452ad235a1f2915ad684393c"
|
||||
fields_and_data = self.hamqth.lookup("MYCALL")
|
||||
assert(fields_and_data["NAME"] == "NAME")
|
||||
assert(fields_and_data["ADDRESS"] == "ADDRESS")
|
||||
assert(fields_and_data["COUNTRY"] == "COUNTRY")
|
||||
assert(fields_and_data["CQZ"] == "CQ")
|
||||
assert(fields_and_data["ITUZ"] == "ITU")
|
||||
assert(fields_and_data["IOTA"] == "GRID")
|
||||
|
||||
self.hamqth.connection = connection
|
||||
self.hamqth.session_id = "09b0ae90050be03c452ad235a1f2915ad684393c"
|
||||
fields_and_data = self.hamqth.lookup("MYCALL")
|
||||
assert(fields_and_data["NAME"] == "NAME")
|
||||
assert(fields_and_data["ADDRESS"] == "ADDRESS")
|
||||
assert(fields_and_data["COUNTRY"] == "COUNTRY")
|
||||
assert(fields_and_data["CQZ"] == "CQ")
|
||||
assert(fields_and_data["ITUZ"] == "ITU")
|
||||
assert(fields_and_data["IOTA"] == "GRID")
|
||||
|
||||
if(__name__ == '__main__'):
|
||||
unittest.main()
|
||||
unittest.main()
|
||||
|
|
|
@ -29,322 +29,326 @@ from pyqso.telnet_connection_dialog import *
|
|||
|
||||
BOOKMARKS_FILE = os.path.expanduser('~/.config/pyqso/bookmarks.ini')
|
||||
|
||||
|
||||
class DXCluster(Gtk.VBox):
|
||||
""" A tool for connecting to a DX cluster (specifically Telnet-based DX clusters). """
|
||||
|
||||
def __init__(self, parent):
|
||||
""" Set up the DX cluster's Gtk.VBox, and set up a timer so that PyQSO can retrieve new data from the Telnet server every few seconds.
|
||||
|
||||
:arg parent: The parent Gtk window.
|
||||
"""
|
||||
logging.debug("Setting up the DX cluster...")
|
||||
Gtk.VBox.__init__(self, spacing=2)
|
||||
|
||||
self.connection = None
|
||||
self.parent = parent
|
||||
""" A tool for connecting to a DX cluster (specifically Telnet-based DX clusters). """
|
||||
|
||||
# Set up the menubar
|
||||
self.menubar = Gtk.MenuBar()
|
||||
|
||||
self.items = {}
|
||||
|
||||
###### CONNECTION ######
|
||||
mitem_connection = Gtk.MenuItem(label="Connection")
|
||||
self.menubar.append(mitem_connection)
|
||||
subm_connection = Gtk.Menu()
|
||||
mitem_connection.set_submenu(subm_connection)
|
||||
def __init__(self, parent):
|
||||
""" Set up the DX cluster's Gtk.VBox, and set up a timer so that PyQSO can retrieve new data from the Telnet server every few seconds.
|
||||
|
||||
# Connect
|
||||
mitem_connect = Gtk.ImageMenuItem(label="Connect to Telnet Server")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_CONNECT, Gtk.IconSize.MENU)
|
||||
mitem_connect.set_image(icon)
|
||||
subm_connection.append(mitem_connect)
|
||||
self.items["CONNECT"] = mitem_connect
|
||||
:arg parent: The parent Gtk window.
|
||||
"""
|
||||
logging.debug("Setting up the DX cluster...")
|
||||
Gtk.VBox.__init__(self, spacing=2)
|
||||
|
||||
subm_connect = Gtk.Menu()
|
||||
|
||||
## New
|
||||
mitem_new = Gtk.MenuItem(label="New...")
|
||||
mitem_new.connect("activate", self.new_server)
|
||||
subm_connect.append(mitem_new)
|
||||
self.connection = None
|
||||
self.parent = parent
|
||||
|
||||
## From Bookmark
|
||||
mitem_bookmark = Gtk.MenuItem(label="From Bookmark")
|
||||
self.subm_bookmarks = Gtk.Menu()
|
||||
mitem_bookmark.set_submenu(self.subm_bookmarks)
|
||||
self._populate_bookmarks()
|
||||
subm_connect.append(mitem_bookmark)
|
||||
|
||||
mitem_connect.set_submenu(subm_connect)
|
||||
# Set up the menubar
|
||||
self.menubar = Gtk.MenuBar()
|
||||
|
||||
# Disconnect
|
||||
mitem_disconnect = Gtk.ImageMenuItem(label="Disconnect from Telnet Server")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_DISCONNECT, Gtk.IconSize.MENU)
|
||||
mitem_disconnect.set_image(icon)
|
||||
mitem_disconnect.connect("activate", self.telnet_disconnect)
|
||||
subm_connection.append(mitem_disconnect)
|
||||
self.items["DISCONNECT"] = mitem_disconnect
|
||||
self.items = {}
|
||||
|
||||
self.pack_start(self.menubar, False, False, 0)
|
||||
# CONNECTION ######
|
||||
mitem_connection = Gtk.MenuItem(label="Connection")
|
||||
self.menubar.append(mitem_connection)
|
||||
subm_connection = Gtk.Menu()
|
||||
mitem_connection.set_submenu(subm_connection)
|
||||
|
||||
# A TextView object to display the output from the Telnet server.
|
||||
self.renderer = Gtk.TextView()
|
||||
self.renderer.set_editable(False)
|
||||
self.renderer.set_cursor_visible(False)
|
||||
sw = Gtk.ScrolledWindow()
|
||||
sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
|
||||
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||
sw.add(self.renderer)
|
||||
self.buffer = self.renderer.get_buffer()
|
||||
self.pack_start(sw, True, True, 0)
|
||||
# Connect
|
||||
mitem_connect = Gtk.ImageMenuItem(label="Connect to Telnet Server")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_CONNECT, Gtk.IconSize.MENU)
|
||||
mitem_connect.set_image(icon)
|
||||
subm_connection.append(mitem_connect)
|
||||
self.items["CONNECT"] = mitem_connect
|
||||
|
||||
# Set up the command box.
|
||||
self.commandbox = Gtk.HBox(spacing=2)
|
||||
self.command = Gtk.Entry()
|
||||
self.commandbox.pack_start(self.command, True, True, 0)
|
||||
self.send = Gtk.Button(label="Send Command")
|
||||
self.send.connect("clicked", self.telnet_send_command)
|
||||
self.commandbox.pack_start(self.send, False, False, 0)
|
||||
self.pack_start(self.commandbox, False, False, 0)
|
||||
|
||||
self.set_items_sensitive(True)
|
||||
|
||||
self.show_all()
|
||||
subm_connect = Gtk.Menu()
|
||||
|
||||
logging.debug("DX cluster ready!")
|
||||
# New
|
||||
mitem_new = Gtk.MenuItem(label="New...")
|
||||
mitem_new.connect("activate", self.new_server)
|
||||
subm_connect.append(mitem_new)
|
||||
|
||||
return
|
||||
# From Bookmark
|
||||
mitem_bookmark = Gtk.MenuItem(label="From Bookmark")
|
||||
self.subm_bookmarks = Gtk.Menu()
|
||||
mitem_bookmark.set_submenu(self.subm_bookmarks)
|
||||
self._populate_bookmarks()
|
||||
subm_connect.append(mitem_bookmark)
|
||||
|
||||
mitem_connect.set_submenu(subm_connect)
|
||||
|
||||
# Disconnect
|
||||
mitem_disconnect = Gtk.ImageMenuItem(label="Disconnect from Telnet Server")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_DISCONNECT, Gtk.IconSize.MENU)
|
||||
mitem_disconnect.set_image(icon)
|
||||
mitem_disconnect.connect("activate", self.telnet_disconnect)
|
||||
subm_connection.append(mitem_disconnect)
|
||||
self.items["DISCONNECT"] = mitem_disconnect
|
||||
|
||||
self.pack_start(self.menubar, False, False, 0)
|
||||
|
||||
# A TextView object to display the output from the Telnet server.
|
||||
self.renderer = Gtk.TextView()
|
||||
self.renderer.set_editable(False)
|
||||
self.renderer.set_cursor_visible(False)
|
||||
sw = Gtk.ScrolledWindow()
|
||||
sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
|
||||
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
||||
sw.add(self.renderer)
|
||||
self.buffer = self.renderer.get_buffer()
|
||||
self.pack_start(sw, True, True, 0)
|
||||
|
||||
# Set up the command box.
|
||||
self.commandbox = Gtk.HBox(spacing=2)
|
||||
self.command = Gtk.Entry()
|
||||
self.commandbox.pack_start(self.command, True, True, 0)
|
||||
self.send = Gtk.Button(label="Send Command")
|
||||
self.send.connect("clicked", self.telnet_send_command)
|
||||
self.commandbox.pack_start(self.send, False, False, 0)
|
||||
self.pack_start(self.commandbox, False, False, 0)
|
||||
|
||||
self.set_items_sensitive(True)
|
||||
|
||||
self.show_all()
|
||||
|
||||
logging.debug("DX cluster ready!")
|
||||
|
||||
return
|
||||
|
||||
def new_server(self, widget=None):
|
||||
""" Get Telnet server host and login details specified in the Gtk.Entry boxes in the TelnetConnectionDialog and attempt a connection. """
|
||||
dialog = TelnetConnectionDialog(self.parent)
|
||||
response = dialog.run()
|
||||
|
||||
if(response == Gtk.ResponseType.OK):
|
||||
connection_info = dialog.get_connection_info()
|
||||
host = connection_info["HOST"].get_text()
|
||||
port = connection_info["PORT"].get_text()
|
||||
username = connection_info["USERNAME"].get_text()
|
||||
password = connection_info["PASSWORD"].get_text()
|
||||
|
||||
# Save the server details in a new bookmark, if desired.
|
||||
if(connection_info["BOOKMARK"].get_active()):
|
||||
try:
|
||||
config = configparser.ConfigParser()
|
||||
config.read(BOOKMARKS_FILE)
|
||||
|
||||
# Use the host name as the bookmark's identifier.
|
||||
try:
|
||||
config.add_section(host)
|
||||
except configparser.DuplicateSectionError:
|
||||
# If the hostname already exists, assume the user wants to update the port number, username and/or password.
|
||||
logging.warning("A server with hostname '%s' already exists. Over-writing existing details..." % (host))
|
||||
config.set(host, "host", host)
|
||||
config.set(host, "port", port)
|
||||
config.set(host, "username", username)
|
||||
config.set(host, "password", password)
|
||||
|
||||
# Write the bookmarks to file.
|
||||
if not os.path.exists(os.path.expanduser('~/.config/pyqso')):
|
||||
os.makedirs(os.path.expanduser('~/.config/pyqso'))
|
||||
with open(BOOKMARKS_FILE, 'w') as f:
|
||||
config.write(f)
|
||||
|
||||
self._populate_bookmarks()
|
||||
|
||||
except IOError:
|
||||
# Maybe the bookmarks file could not be written to?
|
||||
logging.error("Bookmark could not be saved. Check bookmarks file permissions? Going ahead with the server connection anyway...")
|
||||
|
||||
dialog.destroy()
|
||||
|
||||
def new_server(self, widget=None):
|
||||
""" Get Telnet server host and login details specified in the Gtk.Entry boxes in the TelnetConnectionDialog and attempt a connection. """
|
||||
dialog = TelnetConnectionDialog(self.parent)
|
||||
response = dialog.run()
|
||||
|
||||
if(response == Gtk.ResponseType.OK):
|
||||
connection_info = dialog.get_connection_info()
|
||||
host = connection_info["HOST"].get_text()
|
||||
port = connection_info["PORT"].get_text()
|
||||
username = connection_info["USERNAME"].get_text()
|
||||
password = connection_info["PASSWORD"].get_text()
|
||||
|
||||
# Save the server details in a new bookmark, if desired.
|
||||
if(connection_info["BOOKMARK"].get_active()):
|
||||
try:
|
||||
config = configparser.ConfigParser()
|
||||
config.read(BOOKMARKS_FILE)
|
||||
|
||||
# Use the host name as the bookmark's identifier.
|
||||
try:
|
||||
config.add_section(host)
|
||||
except configparser.DuplicateSectionError:
|
||||
# If the hostname already exists, assume the user wants to update the port number, username and/or password.
|
||||
logging.warning("A server with hostname '%s' already exists. Over-writing existing details..." % (host))
|
||||
config.set(host, "host", host)
|
||||
config.set(host, "port", port)
|
||||
config.set(host, "username", username)
|
||||
config.set(host, "password", password)
|
||||
# Convert port (currently of type str) into an int.
|
||||
port = int(port)
|
||||
# Attempt a connection with the server.
|
||||
self.telnet_connect(host, port, username, password)
|
||||
except ValueError as e:
|
||||
logging.error("Could not convert the server's port information to an integer.")
|
||||
logging.exception(e)
|
||||
|
||||
# Write the bookmarks to file.
|
||||
if not os.path.exists(os.path.expanduser('~/.config/pyqso')):
|
||||
os.makedirs(os.path.expanduser('~/.config/pyqso'))
|
||||
with open(BOOKMARKS_FILE, 'w') as f:
|
||||
config.write(f)
|
||||
|
||||
self._populate_bookmarks()
|
||||
else:
|
||||
dialog.destroy()
|
||||
return
|
||||
|
||||
except IOError:
|
||||
# Maybe the bookmarks file could not be written to?
|
||||
logging.error("Bookmark could not be saved. Check bookmarks file permissions? Going ahead with the server connection anyway...")
|
||||
|
||||
dialog.destroy()
|
||||
|
||||
try:
|
||||
# Convert port (currently of type str) into an int.
|
||||
port = int(port)
|
||||
# Attempt a connection with the server.
|
||||
def _populate_bookmarks(self):
|
||||
""" Populate the list of bookmarked Telnet servers in the menu. """
|
||||
config = configparser.ConfigParser()
|
||||
have_config = (config.read(BOOKMARKS_FILE) != [])
|
||||
|
||||
if(have_config):
|
||||
try:
|
||||
# Clear the menu of all current bookmarks.
|
||||
for i in self.subm_bookmarks.get_children():
|
||||
self.subm_bookmarks.remove(i)
|
||||
|
||||
# Add all bookmarks in the config file.
|
||||
for bookmark in config.sections():
|
||||
mitem = Gtk.MenuItem(label=bookmark)
|
||||
mitem.connect("activate", self.bookmarked_server, bookmark)
|
||||
self.subm_bookmarks.append(mitem)
|
||||
|
||||
except Exception as e:
|
||||
logging.error("An error occurred whilst populating the DX cluster bookmarks menu.")
|
||||
logging.exception(e)
|
||||
|
||||
self.show_all() # Need to do this to update the bookmarks list in the menu.
|
||||
|
||||
return
|
||||
|
||||
def bookmarked_server(self, widget, name):
|
||||
""" Get Telnet server host and login details from an existing bookmark and attempt a connection.
|
||||
|
||||
:arg str name: The name of the bookmark. This is the same as the server's hostname.
|
||||
"""
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
have_config = (config.read(BOOKMARKS_FILE) != [])
|
||||
try:
|
||||
if(not have_config):
|
||||
raise IOError("The bookmark's details could not be loaded.")
|
||||
|
||||
host = config.get(name, "host")
|
||||
port = int(config.get(name, "port"))
|
||||
username = config.get(name, "username")
|
||||
password = config.get(name, "password")
|
||||
self.telnet_connect(host, port, username, password)
|
||||
except ValueError as e:
|
||||
logging.error("Could not convert the server's port information to an integer.")
|
||||
|
||||
except ValueError as e:
|
||||
# This exception may occur when casting the port (which is a str) to an int.
|
||||
logging.exception(e)
|
||||
except IOError as e:
|
||||
logging.exception(e)
|
||||
except Exception as e:
|
||||
logging.error("Could not connect to Telnet server '%s'" % name)
|
||||
logging.exception(e)
|
||||
|
||||
else:
|
||||
dialog.destroy()
|
||||
return
|
||||
return
|
||||
|
||||
def _populate_bookmarks(self):
|
||||
""" Populate the list of bookmarked Telnet servers in the menu. """
|
||||
config = configparser.ConfigParser()
|
||||
have_config = (config.read(BOOKMARKS_FILE) != [])
|
||||
def telnet_connect(self, host, port=23, username=None, password=None):
|
||||
""" Connect to a user-specified Telnet server.
|
||||
|
||||
if(have_config):
|
||||
try:
|
||||
# Clear the menu of all current bookmarks.
|
||||
for i in self.subm_bookmarks.get_children():
|
||||
self.subm_bookmarks.remove(i)
|
||||
:arg str host: The Telnet server's hostname.
|
||||
:arg int port: The Telnet server's port number. If no port is specified, the default Telnet server port of 23 will be used.
|
||||
:arg str username: The user's username. This is an optional argument.
|
||||
:arg str password: The user's password. This is an optional argument.
|
||||
"""
|
||||
|
||||
# Add all bookmarks in the config file.
|
||||
for bookmark in config.sections():
|
||||
mitem = Gtk.MenuItem(label=bookmark)
|
||||
mitem.connect("activate", self.bookmarked_server, bookmark)
|
||||
self.subm_bookmarks.append(mitem)
|
||||
if(host == "" or host is None):
|
||||
logging.error("No Telnet server specified.")
|
||||
return
|
||||
if(port == "" or port is None):
|
||||
port = 23 # Use the default Telnet port
|
||||
|
||||
except Exception as e:
|
||||
logging.error("An error occurred whilst populating the DX cluster bookmarks menu.")
|
||||
try:
|
||||
self.connection = telnetlib.Telnet(host, port)
|
||||
|
||||
if(username):
|
||||
self.connection.read_until("login: ".encode())
|
||||
self.connection.write((username + "\n").encode())
|
||||
if(password):
|
||||
self.connection.read_until("password: ".encode())
|
||||
self.connection.write((password + "\n").encode())
|
||||
except Exception as e:
|
||||
logging.error("Could not create a connection to the Telnet server")
|
||||
logging.exception(e)
|
||||
self.connection = None
|
||||
return
|
||||
|
||||
self.show_all() # Need to do this to update the bookmarks list in the menu.
|
||||
self.set_items_sensitive(False)
|
||||
|
||||
return
|
||||
self.check_io_event = GObject.timeout_add(1000, self._on_telnet_io)
|
||||
|
||||
def bookmarked_server(self, widget, name):
|
||||
""" Get Telnet server host and login details from an existing bookmark and attempt a connection.
|
||||
|
||||
:arg str name: The name of the bookmark. This is the same as the server's hostname.
|
||||
"""
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
have_config = (config.read(BOOKMARKS_FILE) != [])
|
||||
try:
|
||||
if(not have_config):
|
||||
raise IOError("The bookmark's details could not be loaded.")
|
||||
|
||||
host = config.get(name, "host")
|
||||
port = int(config.get(name, "port"))
|
||||
username = config.get(name, "username")
|
||||
password = config.get(name, "password")
|
||||
self.telnet_connect(host, port, username, password)
|
||||
|
||||
except ValueError as e:
|
||||
# This exception may occur when casting the port (which is a str) to an int.
|
||||
logging.exception(e)
|
||||
except IOError as e:
|
||||
logging.exception(e)
|
||||
except Exception as e:
|
||||
logging.error("Could not connect to Telnet server '%s'" % name)
|
||||
logging.exception(e)
|
||||
return
|
||||
|
||||
return
|
||||
|
||||
def telnet_connect(self, host, port=23, username=None, password=None):
|
||||
""" Connect to a user-specified Telnet server.
|
||||
|
||||
:arg str host: The Telnet server's hostname.
|
||||
:arg int port: The Telnet server's port number. If no port is specified, the default Telnet server port of 23 will be used.
|
||||
:arg str username: The user's username. This is an optional argument.
|
||||
:arg str password: The user's password. This is an optional argument.
|
||||
"""
|
||||
def telnet_disconnect(self, widget=None):
|
||||
""" Disconnect from a Telnet server and remove the I/O timer. """
|
||||
if(self.connection):
|
||||
self.connection.close()
|
||||
self.buffer.set_text("")
|
||||
self.connection = None
|
||||
self.set_items_sensitive(True)
|
||||
|
||||
if(host == "" or host is None):
|
||||
logging.error("No Telnet server specified.")
|
||||
return
|
||||
if(port == "" or port is None):
|
||||
port = 23 # Use the default Telnet port
|
||||
|
||||
try:
|
||||
self.connection = telnetlib.Telnet(host, port)
|
||||
|
||||
if(username):
|
||||
self.connection.read_until("login: ".encode())
|
||||
self.connection.write((username + "\n").encode())
|
||||
if(password):
|
||||
self.connection.read_until("password: ".encode())
|
||||
self.connection.write((password + "\n").encode())
|
||||
except Exception as e:
|
||||
logging.error("Could not create a connection to the Telnet server")
|
||||
logging.exception(e)
|
||||
self.connection = None
|
||||
return
|
||||
|
||||
self.set_items_sensitive(False)
|
||||
|
||||
self.check_io_event = GObject.timeout_add(1000, self._on_telnet_io)
|
||||
|
||||
return
|
||||
|
||||
def telnet_disconnect(self, widget=None):
|
||||
""" Disconnect from a Telnet server and remove the I/O timer. """
|
||||
if(self.connection):
|
||||
self.connection.close()
|
||||
self.buffer.set_text("")
|
||||
self.connection = None
|
||||
self.set_items_sensitive(True)
|
||||
|
||||
# Stop checking for server output once disconnected.
|
||||
try:
|
||||
GObject.source_remove(self.check_io_event)
|
||||
except AttributeError:
|
||||
# This may happen if a connection hasn't yet been established.
|
||||
pass
|
||||
|
||||
return
|
||||
|
||||
def telnet_send_command(self, widget=None):
|
||||
""" Send the user-specified command in the Gtk.Entry box to the Telnet server (if PyQSO is connected to one). """
|
||||
if(self.connection):
|
||||
self.connection.write((self.command.get_text() + "\n").encode())
|
||||
self.command.set_text("")
|
||||
return
|
||||
|
||||
def _on_telnet_io(self):
|
||||
""" Retrieve any new data from the Telnet server and print it out in the Gtk.TextView widget.
|
||||
|
||||
:returns: Always returns True to satisfy the GObject timer.
|
||||
:rtype: bool
|
||||
"""
|
||||
if(self.connection):
|
||||
text = self.connection.read_very_eager().decode()
|
||||
try:
|
||||
text = text.replace("\u0007", "") # Remove the BEL Unicode character from the end of the line
|
||||
except UnicodeDecodeError:
|
||||
# Stop checking for server output once disconnected.
|
||||
try:
|
||||
GObject.source_remove(self.check_io_event)
|
||||
except AttributeError:
|
||||
# This may happen if a connection hasn't yet been established.
|
||||
pass
|
||||
|
||||
# Allow auto-scrolling to the new text entry if the focus is already at
|
||||
# the very end of the Gtk.TextView. Otherwise, don't auto-scroll
|
||||
# in case the user is reading something further up.
|
||||
# Note: This is based on the code from http://forums.gentoo.org/viewtopic-t-445598-view-next.html
|
||||
end_iter = self.buffer.get_end_iter()
|
||||
end_mark = self.buffer.create_mark(None, end_iter)
|
||||
self.renderer.move_mark_onscreen(end_mark)
|
||||
at_end = self.buffer.get_iter_at_mark(end_mark).equal(end_iter)
|
||||
self.buffer.insert(end_iter, text)
|
||||
if(at_end):
|
||||
|
||||
return
|
||||
|
||||
def telnet_send_command(self, widget=None):
|
||||
""" Send the user-specified command in the Gtk.Entry box to the Telnet server (if PyQSO is connected to one). """
|
||||
if(self.connection):
|
||||
self.connection.write((self.command.get_text() + "\n").encode())
|
||||
self.command.set_text("")
|
||||
return
|
||||
|
||||
def _on_telnet_io(self):
|
||||
""" Retrieve any new data from the Telnet server and print it out in the Gtk.TextView widget.
|
||||
|
||||
:returns: Always returns True to satisfy the GObject timer.
|
||||
:rtype: bool
|
||||
"""
|
||||
if(self.connection):
|
||||
text = self.connection.read_very_eager().decode()
|
||||
try:
|
||||
text = text.replace("\u0007", "") # Remove the BEL Unicode character from the end of the line
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
|
||||
# Allow auto-scrolling to the new text entry if the focus is already at
|
||||
# the very end of the Gtk.TextView. Otherwise, don't auto-scroll
|
||||
# in case the user is reading something further up.
|
||||
# Note: This is based on the code from http://forums.gentoo.org/viewtopic-t-445598-view-next.html
|
||||
end_iter = self.buffer.get_end_iter()
|
||||
end_mark = self.buffer.create_mark(None, end_iter)
|
||||
self.renderer.scroll_mark_onscreen(end_mark)
|
||||
self.renderer.move_mark_onscreen(end_mark)
|
||||
at_end = self.buffer.get_iter_at_mark(end_mark).equal(end_iter)
|
||||
self.buffer.insert(end_iter, text)
|
||||
if(at_end):
|
||||
end_mark = self.buffer.create_mark(None, end_iter)
|
||||
self.renderer.scroll_mark_onscreen(end_mark)
|
||||
|
||||
return True
|
||||
return True
|
||||
|
||||
def set_items_sensitive(self, sensitive):
|
||||
""" Enable/disable the relevant buttons for connecting/disconnecting from a DX cluster, so that users cannot click the connect button if PyQSO is already connected.
|
||||
|
||||
:arg bool sensitive: If True, enable the Connect button and disable the Disconnect button. If False, vice versa.
|
||||
"""
|
||||
self.items["CONNECT"].set_sensitive(sensitive)
|
||||
self.items["DISCONNECT"].set_sensitive(not sensitive)
|
||||
self.send.set_sensitive(not sensitive)
|
||||
return
|
||||
|
||||
def set_items_sensitive(self, sensitive):
|
||||
""" Enable/disable the relevant buttons for connecting/disconnecting from a DX cluster, so that users cannot click the connect button if PyQSO is already connected.
|
||||
|
||||
:arg bool sensitive: If True, enable the Connect button and disable the Disconnect button. If False, vice versa.
|
||||
"""
|
||||
self.items["CONNECT"].set_sensitive(sensitive)
|
||||
self.items["DISCONNECT"].set_sensitive(not sensitive)
|
||||
self.send.set_sensitive(not sensitive)
|
||||
return
|
||||
|
||||
class TestDXCluster(unittest.TestCase):
|
||||
""" The unit tests for the DXCluster class. """
|
||||
|
||||
def setUp(self):
|
||||
""" Set up the objects needed for the unit tests. """
|
||||
self.dxcluster = DXCluster(parent=None)
|
||||
""" The unit tests for the DXCluster class. """
|
||||
|
||||
def tearDown(self):
|
||||
""" Destroy any unit test resources. """
|
||||
pass
|
||||
def setUp(self):
|
||||
""" Set up the objects needed for the unit tests. """
|
||||
self.dxcluster = DXCluster(parent=None)
|
||||
|
||||
def test_on_telnet_io(self):
|
||||
""" Check that the response from the Telnet server can be correctly decoded. """
|
||||
def tearDown(self):
|
||||
""" Destroy any unit test resources. """
|
||||
pass
|
||||
|
||||
def test_on_telnet_io(self):
|
||||
""" Check that the response from the Telnet server can be correctly decoded. """
|
||||
|
||||
telnetlib.Telnet = unittest.mock.Mock(spec=telnetlib.Telnet)
|
||||
connection = telnetlib.Telnet("hello", "world")
|
||||
connection.read_very_eager.return_value = b"Test message from the Telnet server."
|
||||
self.dxcluster.connection = connection
|
||||
result = self.dxcluster._on_telnet_io()
|
||||
assert(result)
|
||||
|
||||
telnetlib.Telnet = unittest.mock.Mock(spec=telnetlib.Telnet)
|
||||
connection = telnetlib.Telnet("hello", "world")
|
||||
connection.read_very_eager.return_value = b"Test message from the Telnet server."
|
||||
self.dxcluster.connection = connection
|
||||
result = self.dxcluster._on_telnet_io()
|
||||
assert(result)
|
||||
|
||||
if(__name__ == '__main__'):
|
||||
unittest.main()
|
||||
unittest.main()
|
||||
|
|
|
@ -21,74 +21,75 @@ from gi.repository import Gtk, GObject
|
|||
import logging
|
||||
from datetime import datetime
|
||||
try:
|
||||
import numpy
|
||||
logging.info("Using version %s of numpy." % (numpy.__version__))
|
||||
import matplotlib
|
||||
logging.info("Using version %s of matplotlib." % (matplotlib.__version__))
|
||||
matplotlib.use('Agg')
|
||||
matplotlib.rcParams['font.size'] = 10.0
|
||||
import mpl_toolkits.basemap
|
||||
logging.info("Using version %s of mpl_toolkits.basemap." % (mpl_toolkits.basemap.__version__))
|
||||
from matplotlib.backends.backend_gtk3cairo import FigureCanvasGTK3Cairo as FigureCanvas
|
||||
have_necessary_modules = True
|
||||
import numpy
|
||||
logging.info("Using version %s of numpy." % (numpy.__version__))
|
||||
import matplotlib
|
||||
logging.info("Using version %s of matplotlib." % (matplotlib.__version__))
|
||||
matplotlib.use('Agg')
|
||||
matplotlib.rcParams['font.size'] = 10.0
|
||||
import mpl_toolkits.basemap
|
||||
logging.info("Using version %s of mpl_toolkits.basemap." % (mpl_toolkits.basemap.__version__))
|
||||
from matplotlib.backends.backend_gtk3cairo import FigureCanvasGTK3Cairo as FigureCanvas
|
||||
have_necessary_modules = True
|
||||
except ImportError as e:
|
||||
logging.warning(e)
|
||||
logging.warning("Could not import a non-standard Python module needed by the GreyLine class, or the version of the non-standard module is too old. Check that all the PyQSO dependencies are satisfied.")
|
||||
have_necessary_modules = False
|
||||
logging.warning(e)
|
||||
logging.warning("Could not import a non-standard Python module needed by the GreyLine class, or the version of the non-standard module is too old. Check that all the PyQSO dependencies are satisfied.")
|
||||
have_necessary_modules = False
|
||||
|
||||
|
||||
class GreyLine(Gtk.VBox):
|
||||
""" A tool for visualising the grey line. """
|
||||
|
||||
def __init__(self, parent):
|
||||
""" Set up the drawing canvas and the timer which will re-plot the grey line every 30 minutes.
|
||||
|
||||
:arg parent: The parent Gtk window.
|
||||
"""
|
||||
logging.debug("Setting up the grey line...")
|
||||
Gtk.VBox.__init__(self, spacing=2)
|
||||
self.parent = parent
|
||||
|
||||
if(have_necessary_modules):
|
||||
self.fig = matplotlib.figure.Figure()
|
||||
self.canvas = FigureCanvas(self.fig) # For embedding in the Gtk application
|
||||
self.pack_start(self.canvas, True, True, 0)
|
||||
self.refresh_event = GObject.timeout_add(1800000, self.draw) # Re-draw the grey line automatically after 30 minutes (if the grey line tool is visible).
|
||||
""" A tool for visualising the grey line. """
|
||||
|
||||
self.show_all()
|
||||
def __init__(self, parent):
|
||||
""" Set up the drawing canvas and the timer which will re-plot the grey line every 30 minutes.
|
||||
|
||||
logging.debug("Grey line ready!")
|
||||
:arg parent: The parent Gtk window.
|
||||
"""
|
||||
logging.debug("Setting up the grey line...")
|
||||
Gtk.VBox.__init__(self, spacing=2)
|
||||
self.parent = parent
|
||||
|
||||
return
|
||||
if(have_necessary_modules):
|
||||
self.fig = matplotlib.figure.Figure()
|
||||
self.canvas = FigureCanvas(self.fig) # For embedding in the Gtk application
|
||||
self.pack_start(self.canvas, True, True, 0)
|
||||
self.refresh_event = GObject.timeout_add(1800000, self.draw) # Re-draw the grey line automatically after 30 minutes (if the grey line tool is visible).
|
||||
|
||||
def draw(self):
|
||||
""" Draw the world map and the grey line on top of it.
|
||||
|
||||
:returns: Always returns True to satisfy the GObject timer, unless the necessary GreyLine dependencies are not satisfied (in which case, the method returns False so as to not re-draw the canvas).
|
||||
:rtype: bool
|
||||
"""
|
||||
self.show_all()
|
||||
|
||||
if(have_necessary_modules):
|
||||
if(self.parent.toolbox.tools.get_current_page() != 1 or not self.parent.toolbox.get_visible()):
|
||||
# Don't re-draw if the grey line is not visible.
|
||||
return True # We need to return True in case this is method was called by a timer event.
|
||||
else:
|
||||
logging.debug("Drawing the grey line...")
|
||||
# Re-draw the grey line
|
||||
self.fig.clf()
|
||||
sub = self.fig.add_subplot(111)
|
||||
logging.debug("Grey line ready!")
|
||||
|
||||
# Draw the map of the world. This is based on the example from:
|
||||
# http://matplotlib.org/basemap/users/examples.html
|
||||
m = mpl_toolkits.basemap.Basemap(projection='mill', lon_0=0, ax=sub, resolution='c', fix_aspect=False)
|
||||
m.drawcountries(linewidth=0.5)
|
||||
m.drawcoastlines(linewidth=0.5)
|
||||
m.drawparallels(numpy.arange(-90, 90, 30), labels=[1, 0, 0, 0])
|
||||
m.drawmeridians(numpy.arange(m.lonmin, m.lonmax+30, 60), labels=[0, 0, 0, 1])
|
||||
m.drawmapboundary(fill_color='lightblue')
|
||||
m.fillcontinents(color='darkgreen', lake_color='lightblue')
|
||||
m.nightshade(datetime.utcnow()) # Add in the grey line using UTC time. Note that this requires NetCDF.
|
||||
logging.debug("Grey line drawn.")
|
||||
return True
|
||||
else:
|
||||
return False # Don't try to re-draw the canvas if the necessary modules to do so could not be imported.
|
||||
return
|
||||
|
||||
def draw(self):
|
||||
""" Draw the world map and the grey line on top of it.
|
||||
|
||||
:returns: Always returns True to satisfy the GObject timer, unless the necessary GreyLine dependencies are not satisfied (in which case, the method returns False so as to not re-draw the canvas).
|
||||
:rtype: bool
|
||||
"""
|
||||
|
||||
if(have_necessary_modules):
|
||||
if(self.parent.toolbox.tools.get_current_page() != 1 or not self.parent.toolbox.get_visible()):
|
||||
# Don't re-draw if the grey line is not visible.
|
||||
return True # We need to return True in case this is method was called by a timer event.
|
||||
else:
|
||||
logging.debug("Drawing the grey line...")
|
||||
# Re-draw the grey line
|
||||
self.fig.clf()
|
||||
sub = self.fig.add_subplot(111)
|
||||
|
||||
# Draw the map of the world. This is based on the example from:
|
||||
# http://matplotlib.org/basemap/users/examples.html
|
||||
m = mpl_toolkits.basemap.Basemap(projection='mill', lon_0=0, ax=sub, resolution='c', fix_aspect=False)
|
||||
m.drawcountries(linewidth=0.5)
|
||||
m.drawcoastlines(linewidth=0.5)
|
||||
m.drawparallels(numpy.arange(-90, 90, 30), labels=[1, 0, 0, 0])
|
||||
m.drawmeridians(numpy.arange(m.lonmin, m.lonmax+30, 60), labels=[0, 0, 0, 1])
|
||||
m.drawmapboundary(fill_color='lightblue')
|
||||
m.fillcontinents(color='darkgreen', lake_color='lightblue')
|
||||
m.nightshade(datetime.utcnow()) # Add in the grey line using UTC time. Note that this requires NetCDF.
|
||||
logging.debug("Grey line drawn.")
|
||||
return True
|
||||
else:
|
||||
return False # Don't try to re-draw the canvas if the necessary modules to do so could not be imported.
|
||||
|
|
749
pyqso/log.py
749
pyqso/log.py
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/env python3
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (C) 2013 Christian T. Jacobs.
|
||||
|
||||
|
@ -25,431 +25,434 @@ import unittest
|
|||
from pyqso.adif import AVAILABLE_FIELD_NAMES_ORDERED
|
||||
from pyqso.record_dialog import *
|
||||
|
||||
|
||||
class Log(Gtk.ListStore):
|
||||
""" A single log inside of the whole logbook. A Log object can store multiple Record objects. """
|
||||
|
||||
def __init__(self, connection, name):
|
||||
""" Set up a new Log object.
|
||||
|
||||
:arg connection: An sqlite database connection.
|
||||
:arg str name: The name of the log (i.e. the sqlite table name).
|
||||
"""
|
||||
|
||||
# The ListStore constructor needs to know the data types of the columns.
|
||||
# The index is always an integer. We will assume the fields are strings.
|
||||
data_types = [int] + [str]*len(AVAILABLE_FIELD_NAMES_ORDERED)
|
||||
# Call the constructor of the super class (Gtk.ListStore)
|
||||
Gtk.ListStore.__init__(self, *data_types)
|
||||
""" A single log inside of the whole logbook. A Log object can store multiple Record objects. """
|
||||
|
||||
self.connection = connection
|
||||
self.name = name
|
||||
|
||||
logging.debug("New Log instance created!")
|
||||
return
|
||||
def __init__(self, connection, name):
|
||||
""" Set up a new Log object.
|
||||
|
||||
def populate(self):
|
||||
""" Remove everything in the Gtk.ListStore that is rendered already (via the TreeView), and start afresh. """
|
||||
:arg connection: An sqlite database connection.
|
||||
:arg str name: The name of the log (i.e. the sqlite table name).
|
||||
"""
|
||||
|
||||
logging.debug("Populating '%s'..." % self.name)
|
||||
self.add_missing_db_columns()
|
||||
self.clear()
|
||||
records = self.get_all_records()
|
||||
if(records is not None):
|
||||
for r in records:
|
||||
liststore_entry = [r["id"]]
|
||||
for field_name in AVAILABLE_FIELD_NAMES_ORDERED:
|
||||
# Note: r may contain column names that are not in AVAILABLE_FIELD_NAMES_ORDERED,
|
||||
# so we need to loop over and only select those that are, since the ListStore will
|
||||
# expect a specific number of columns.
|
||||
liststore_entry.append(r[field_name])
|
||||
# The ListStore constructor needs to know the data types of the columns.
|
||||
# The index is always an integer. We will assume the fields are strings.
|
||||
data_types = [int] + [str]*len(AVAILABLE_FIELD_NAMES_ORDERED)
|
||||
# Call the constructor of the super class (Gtk.ListStore)
|
||||
Gtk.ListStore.__init__(self, *data_types)
|
||||
|
||||
self.connection = connection
|
||||
self.name = name
|
||||
|
||||
logging.debug("New Log instance created!")
|
||||
return
|
||||
|
||||
def populate(self):
|
||||
""" Remove everything in the Gtk.ListStore that is rendered already (via the TreeView), and start afresh. """
|
||||
|
||||
logging.debug("Populating '%s'..." % self.name)
|
||||
self.add_missing_db_columns()
|
||||
self.clear()
|
||||
records = self.get_all_records()
|
||||
if(records is not None):
|
||||
for r in records:
|
||||
liststore_entry = [r["id"]]
|
||||
for field_name in AVAILABLE_FIELD_NAMES_ORDERED:
|
||||
# Note: r may contain column names that are not in AVAILABLE_FIELD_NAMES_ORDERED,
|
||||
# so we need to loop over and only select those that are, since the ListStore will
|
||||
# expect a specific number of columns.
|
||||
liststore_entry.append(r[field_name])
|
||||
self.append(liststore_entry)
|
||||
logging.debug("Finished populating '%s'." % self.name)
|
||||
else:
|
||||
logging.error("Could not populate '%s' because of a database error." % self.name)
|
||||
return
|
||||
|
||||
def add_missing_db_columns(self):
|
||||
""" Check whether each field name in AVAILABLE_FIELD_NAMES_ORDERED is in the database table. If not, add it
|
||||
(with all entries being set to an empty string initially).
|
||||
|
||||
:raises sqlite.Error, IndexError: if the existing database column names could not be obtained, or missing column names could not be added.
|
||||
"""
|
||||
logging.debug("Adding any missing database columns...")
|
||||
|
||||
# Get all the column names in the current database table.
|
||||
column_names = []
|
||||
try:
|
||||
with self.connection:
|
||||
c = self.connection.cursor()
|
||||
c.execute("PRAGMA table_info(%s)" % self.name)
|
||||
result = c.fetchall()
|
||||
for t in result:
|
||||
column_names.append(t[1].upper())
|
||||
except (sqlite.Error, IndexError) as e:
|
||||
logging.exception(e)
|
||||
logging.error("Could not obtain the database column names.")
|
||||
return
|
||||
|
||||
for field_name in AVAILABLE_FIELD_NAMES_ORDERED:
|
||||
if(not(field_name in column_names)):
|
||||
try:
|
||||
with self.connection:
|
||||
c.execute("ALTER TABLE %s ADD COLUMN %s TEXT DEFAULT \"\"" % (self.name, field_name.lower()))
|
||||
except sqlite.Error as e:
|
||||
logging.exception(e)
|
||||
logging.error("Could not add the missing database column '%s'." % field_name)
|
||||
pass
|
||||
logging.debug("Finished adding any missing database columns.")
|
||||
return
|
||||
|
||||
def add_record(self, fields_and_data):
|
||||
""" Add a record (or multiple records) to the log.
|
||||
|
||||
:arg fields_and_data: A list of dictionaries (or possibly just a single dictionary), with each dictionary representing a single QSO, to be added to the log.
|
||||
"""
|
||||
logging.debug("Adding record(s) to log...")
|
||||
|
||||
# If a dictionary is given, assume that we only have one record to add.
|
||||
if isinstance(fields_and_data, dict):
|
||||
fields_and_data = [fields_and_data]
|
||||
|
||||
with self.connection:
|
||||
# Get all the column names in the current database table.
|
||||
c = self.connection.cursor()
|
||||
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)
|
||||
last_index = c.fetchone()[0]
|
||||
if last_index is None:
|
||||
# Assume no records are currently present.
|
||||
last_index = 0
|
||||
|
||||
# A list of all the database entries, to be inserted in one go into the database.
|
||||
database_entries = []
|
||||
|
||||
# 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.
|
||||
query = query + ",?"
|
||||
query = query + ")"
|
||||
|
||||
# Gather all the records (making sure that the entries of each record are in the correct order).
|
||||
for r in range(len(fields_and_data)):
|
||||
# What if the database columns are not necessarily in the same order as (or even exist in) AVAILABLE_FIELD_NAMES_ORDERED?
|
||||
# PyQSO handles this here, but needs a separate list (called database_entry) to successfully perform the SQL query.
|
||||
database_entry = []
|
||||
for t in column_names:
|
||||
column_name = str(t[1]) # 't' here is a tuple
|
||||
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.
|
||||
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)
|
||||
logging.debug("Finished populating '%s'." % self.name)
|
||||
else:
|
||||
logging.error("Could not populate '%s' because of a database error." % self.name)
|
||||
return
|
||||
|
||||
def add_missing_db_columns(self):
|
||||
""" Check whether each field name in AVAILABLE_FIELD_NAMES_ORDERED is in the database table. If not, add it
|
||||
(with all entries being set to an empty string initially).
|
||||
|
||||
:raises sqlite.Error, IndexError: if the existing database column names could not be obtained, or missing column names could not be added.
|
||||
"""
|
||||
logging.debug("Adding any missing database columns...")
|
||||
# Execute the query.
|
||||
with self.connection:
|
||||
c.executemany(query, database_entries)
|
||||
|
||||
# Get all the column names in the current database table.
|
||||
column_names = []
|
||||
try:
|
||||
with self.connection:
|
||||
c = self.connection.cursor()
|
||||
c.execute("PRAGMA table_info(%s)" % self.name)
|
||||
result = c.fetchall()
|
||||
for t in result:
|
||||
column_names.append(t[1].upper())
|
||||
except (sqlite.Error, IndexError) as e:
|
||||
logging.exception(e)
|
||||
logging.error("Could not obtain the database column names.")
|
||||
return
|
||||
logging.debug("Successfully added the record(s) to the log.")
|
||||
return
|
||||
|
||||
for field_name in AVAILABLE_FIELD_NAMES_ORDERED:
|
||||
if(not(field_name in column_names)):
|
||||
try:
|
||||
with self.connection:
|
||||
c.execute("ALTER TABLE %s ADD COLUMN %s TEXT DEFAULT \"\"" % (self.name, field_name.lower()))
|
||||
except sqlite.Error as e:
|
||||
logging.exception(e)
|
||||
logging.error("Could not add the missing database column '%s'." % field_name)
|
||||
pass
|
||||
logging.debug("Finished adding any missing database columns.")
|
||||
return
|
||||
def delete_record(self, index, iter=None):
|
||||
""" Delete a specified record from the log. The corresponding record is also deleted from the Gtk.ListStore data structure.
|
||||
|
||||
def add_record(self, fields_and_data):
|
||||
""" Add a record (or multiple records) to the log.
|
||||
|
||||
:arg fields_and_data: A list of dictionaries (or possibly just a single dictionary), with each dictionary representing a single QSO, to be added to the log.
|
||||
"""
|
||||
logging.debug("Adding record(s) to log...")
|
||||
|
||||
# If a dictionary is given, assume that we only have one record to add.
|
||||
if isinstance(fields_and_data, dict):
|
||||
fields_and_data = [fields_and_data]
|
||||
|
||||
with self.connection:
|
||||
# Get all the column names in the current database table.
|
||||
c = self.connection.cursor()
|
||||
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)
|
||||
last_index = c.fetchone()[0]
|
||||
if last_index is None:
|
||||
# Assume no records are currently present.
|
||||
last_index = 0
|
||||
|
||||
# A list of all the database entries, to be inserted in one go into the database.
|
||||
database_entries = []
|
||||
: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.
|
||||
:raises sqlite.Error, IndexError: if the record could not be deleted.
|
||||
"""
|
||||
logging.debug("Deleting record from log...")
|
||||
# Get the selected row in the logbook
|
||||
try:
|
||||
with self.connection:
|
||||
c = self.connection.cursor()
|
||||
query = "DELETE FROM %s" % self.name
|
||||
c.execute(query+" WHERE id=?", [index])
|
||||
if(iter is not None):
|
||||
self.remove(iter)
|
||||
logging.debug("Successfully deleted the record from the log.")
|
||||
except (sqlite.Error, IndexError) as e:
|
||||
logging.exception(e)
|
||||
logging.error("Could not delete the record from the log.")
|
||||
return
|
||||
|
||||
# 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.
|
||||
query = query + ",?"
|
||||
query = query + ")"
|
||||
|
||||
# Gather all the records (making sure that the entries of each record are in the correct order).
|
||||
for r in range(len(fields_and_data)):
|
||||
# What if the database columns are not necessarily in the same order as (or even exist in) AVAILABLE_FIELD_NAMES_ORDERED?
|
||||
# PyQSO handles this here, but needs a separate list (called database_entry) to successfully perform the SQL query.
|
||||
database_entry = []
|
||||
for t in column_names:
|
||||
column_name = str(t[1]) # 't' here is a tuple
|
||||
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.
|
||||
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("")
|
||||
def edit_record(self, index, field_name, data, iter=None, column_index=None):
|
||||
""" Edit a specified record by replacing the current data in a specified field with the data provided.
|
||||
|
||||
# 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:
|
||||
c.executemany(query, database_entries)
|
||||
|
||||
logging.debug("Successfully added the record(s) to the log.")
|
||||
return
|
||||
: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.
|
||||
:raises sqlite.Error, IndexError: if the record could not be edited.
|
||||
"""
|
||||
logging.debug("Editing field '%s' in record %d..." % (field_name, index))
|
||||
try:
|
||||
with self.connection:
|
||||
c = self.connection.cursor()
|
||||
query = "UPDATE %s SET %s" % (self.name, field_name)
|
||||
query = query + "=? WHERE id=?"
|
||||
c.execute(query, [data, index]) # First update the SQL database...
|
||||
if(iter is not None and column_index is not None):
|
||||
self.set(iter, column_index, data) # ...and then the ListStore.
|
||||
logging.debug("Successfully edited field '%s' in record %d in the log." % (field_name, index))
|
||||
except (sqlite.Error, IndexError) as e:
|
||||
logging.exception(e)
|
||||
logging.error("Could not edit field %s in record %d in the log." % (field_name, index))
|
||||
return
|
||||
|
||||
def delete_record(self, index, iter=None):
|
||||
""" 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.
|
||||
:raises sqlite.Error, IndexError: if the record could not be deleted.
|
||||
"""
|
||||
logging.debug("Deleting record from log...")
|
||||
# Get the selected row in the logbook
|
||||
try:
|
||||
with self.connection:
|
||||
c = self.connection.cursor()
|
||||
query = "DELETE FROM %s" % self.name
|
||||
c.execute(query+" WHERE id=?", [index])
|
||||
if(iter is not None):
|
||||
self.remove(iter)
|
||||
logging.debug("Successfully deleted the record from the log.")
|
||||
except (sqlite.Error, IndexError) as e:
|
||||
logging.exception(e)
|
||||
logging.error("Could not delete the record from the log.")
|
||||
return
|
||||
def remove_duplicates(self):
|
||||
""" Remove any duplicate records from the log.
|
||||
|
||||
def edit_record(self, index, field_name, data, iter=None, column_index=None):
|
||||
""" Edit a specified record by replacing the current data in a specified field with the data provided.
|
||||
|
||||
: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.
|
||||
:raises sqlite.Error, IndexError: if the record could not be edited.
|
||||
"""
|
||||
logging.debug("Editing field '%s' in record %d..." % (field_name, index))
|
||||
try:
|
||||
with self.connection:
|
||||
c = self.connection.cursor()
|
||||
query = "UPDATE %s SET %s" % (self.name, field_name)
|
||||
query = query + "=? WHERE id=?"
|
||||
c.execute(query, [data, index]) # First update the SQL database...
|
||||
if(iter is not None and column_index is not None):
|
||||
self.set(iter, column_index, data) # ...and then the ListStore.
|
||||
logging.debug("Successfully edited field '%s' in record %d in the log." % (field_name, index))
|
||||
except (sqlite.Error, IndexError) as e:
|
||||
logging.exception(e)
|
||||
logging.error("Could not edit field %s in record %d in the log." % (field_name, index))
|
||||
return
|
||||
:returns: The total number of duplicates, and the number of duplicates that were successfully removed. Hopefully these will be the same.
|
||||
:rtype: tuple
|
||||
"""
|
||||
duplicates = self.get_duplicates()
|
||||
if(len(duplicates) == 0):
|
||||
return (0, 0) # Nothing to do here.
|
||||
|
||||
def remove_duplicates(self):
|
||||
""" Remove any duplicate records from the log.
|
||||
|
||||
:returns: The total number of duplicates, and the number of duplicates that were successfully removed. Hopefully these will be the same.
|
||||
:rtype: tuple
|
||||
"""
|
||||
duplicates = self.get_duplicates()
|
||||
if(len(duplicates) == 0):
|
||||
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
|
||||
iter = self.iter_next(iter) # Move on to the next row, until iter_next returns None.
|
||||
|
||||
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
|
||||
iter = self.iter_next(iter) # Move on to the next row, until iter_next returns None.
|
||||
assert(removed == len(duplicates))
|
||||
return (len(duplicates), removed)
|
||||
|
||||
assert(removed == len(duplicates))
|
||||
return (len(duplicates), removed)
|
||||
def get_duplicates(self):
|
||||
""" Find the duplicates in the log, based on the CALL, QSO_DATE, TIME_ON, FREQ and MODE fields.
|
||||
|
||||
def get_duplicates(self):
|
||||
""" Find the duplicates in the log, based on the CALL, QSO_DATE, TIME_ON, FREQ and MODE fields.
|
||||
|
||||
:returns: A list of row IDs corresponding to the duplicate records.
|
||||
:rtype: list
|
||||
"""
|
||||
duplicates = []
|
||||
try:
|
||||
with self.connection:
|
||||
c = self.connection.cursor()
|
||||
c.execute(
|
||||
"""SELECT rowid FROM %s WHERE rowid NOT IN
|
||||
:returns: A list of row IDs corresponding to the duplicate records.
|
||||
:rtype: list
|
||||
"""
|
||||
duplicates = []
|
||||
try:
|
||||
with self.connection:
|
||||
c = self.connection.cursor()
|
||||
c.execute(
|
||||
"""SELECT rowid FROM %s WHERE rowid NOT IN
|
||||
(
|
||||
SELECT MIN(rowid) FROM %s GROUP BY call, qso_date, time_on, freq, mode
|
||||
)""" % (self.name, self.name))
|
||||
result = c.fetchall()
|
||||
for rowid in result:
|
||||
duplicates.append(rowid[0]) # Get the integer from inside the tuple.
|
||||
except (sqlite.Error, IndexError) as e:
|
||||
logging.exception(e)
|
||||
return duplicates
|
||||
|
||||
def get_record_by_index(self, index):
|
||||
""" Return a record with a given index in the log.
|
||||
|
||||
:arg int index: The index of the record in the SQL database.
|
||||
:returns: The desired record, represented by a dictionary of field-value pairs.
|
||||
:rtype: dict
|
||||
"""
|
||||
try:
|
||||
with self.connection:
|
||||
c = self.connection.cursor()
|
||||
query = "SELECT * FROM %s WHERE id=?" % self.name
|
||||
c.execute(query, [index])
|
||||
return c.fetchone()
|
||||
except sqlite.Error as e:
|
||||
logging.exception(e)
|
||||
return None
|
||||
result = c.fetchall()
|
||||
for rowid in result:
|
||||
duplicates.append(rowid[0]) # Get the integer from inside the tuple.
|
||||
except (sqlite.Error, IndexError) as e:
|
||||
logging.exception(e)
|
||||
return duplicates
|
||||
|
||||
def get_all_records(self):
|
||||
""" Return a list of all the records in the log.
|
||||
|
||||
:returns: A list of all the records in the log. Each record is represented by a dictionary.
|
||||
:rtype: dict
|
||||
"""
|
||||
try:
|
||||
with self.connection:
|
||||
c = self.connection.cursor()
|
||||
c.execute("SELECT * FROM %s" % self.name)
|
||||
return c.fetchall()
|
||||
except sqlite.Error as e:
|
||||
logging.exception(e)
|
||||
return None
|
||||
def get_record_by_index(self, index):
|
||||
""" Return a record with a given index in the log.
|
||||
|
||||
:arg int index: The index of the record in the SQL database.
|
||||
:returns: The desired record, represented by a dictionary of field-value pairs.
|
||||
:rtype: dict
|
||||
"""
|
||||
try:
|
||||
with self.connection:
|
||||
c = self.connection.cursor()
|
||||
query = "SELECT * FROM %s WHERE id=?" % self.name
|
||||
c.execute(query, [index])
|
||||
return c.fetchone()
|
||||
except sqlite.Error as e:
|
||||
logging.exception(e)
|
||||
return None
|
||||
|
||||
def get_all_records(self):
|
||||
""" Return a list of all the records in the log.
|
||||
|
||||
:returns: A list of all the records in the log. Each record is represented by a dictionary.
|
||||
:rtype: dict
|
||||
"""
|
||||
try:
|
||||
with self.connection:
|
||||
c = self.connection.cursor()
|
||||
c.execute("SELECT * FROM %s" % self.name)
|
||||
return c.fetchall()
|
||||
except sqlite.Error as e:
|
||||
logging.exception(e)
|
||||
return None
|
||||
|
||||
def get_number_of_records(self):
|
||||
""" Return the total number of records in the log.
|
||||
|
||||
:returns: The total number of records in the log.
|
||||
:rtype: int
|
||||
"""
|
||||
try:
|
||||
with self.connection:
|
||||
c = self.connection.cursor()
|
||||
c.execute("SELECT Count(*) FROM %s" % self.name)
|
||||
return c.fetchone()[0]
|
||||
except (sqlite.Error, IndexError) as e:
|
||||
logging.exception(e)
|
||||
return None
|
||||
|
||||
def get_number_of_records(self):
|
||||
""" Return the total number of records in the log.
|
||||
|
||||
:returns: The total number of records in the log.
|
||||
:rtype: int
|
||||
"""
|
||||
try:
|
||||
with self.connection:
|
||||
c = self.connection.cursor()
|
||||
c.execute("SELECT Count(*) FROM %s" % self.name)
|
||||
return c.fetchone()[0]
|
||||
except (sqlite.Error, IndexError) as e:
|
||||
logging.exception(e)
|
||||
return None
|
||||
|
||||
class TestLog(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.connection = sqlite.connect(":memory:")
|
||||
self.connection.row_factory = sqlite.Row
|
||||
def setUp(self):
|
||||
self.connection = sqlite.connect(":memory:")
|
||||
self.connection.row_factory = sqlite.Row
|
||||
|
||||
self.field_names = ["CALL", "QSO_DATE", "TIME_ON", "FREQ", "BAND", "MODE", "RST_SENT", "RST_RCVD"]
|
||||
self.fields_and_data = {"CALL":"TEST123", "QSO_DATE":"20130312", "TIME_ON":"1234", "FREQ":"145.500", "BAND":"2m", "MODE":"FM", "RST_SENT":"59", "RST_RCVD":"59"}
|
||||
self.field_names = ["CALL", "QSO_DATE", "TIME_ON", "FREQ", "BAND", "MODE", "RST_SENT", "RST_RCVD"]
|
||||
self.fields_and_data = {"CALL": "TEST123", "QSO_DATE": "20130312", "TIME_ON": "1234", "FREQ": "145.500", "BAND": "2m", "MODE": "FM", "RST_SENT": "59", "RST_RCVD": "59"}
|
||||
|
||||
c = self.connection.cursor()
|
||||
query = "CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT"
|
||||
for field_name in self.field_names:
|
||||
s = ", %s TEXT" % field_name.lower()
|
||||
query = query + s
|
||||
query = query + ")"
|
||||
c.execute(query)
|
||||
c = self.connection.cursor()
|
||||
query = "CREATE TABLE test (id INTEGER PRIMARY KEY AUTOINCREMENT"
|
||||
for field_name in self.field_names:
|
||||
s = ", %s TEXT" % field_name.lower()
|
||||
query = query + s
|
||||
query = query + ")"
|
||||
c.execute(query)
|
||||
|
||||
self.log = Log(self.connection, "test")
|
||||
self.log = Log(self.connection, "test")
|
||||
|
||||
def tearDown(self):
|
||||
self.connection.close()
|
||||
def tearDown(self):
|
||||
self.connection.close()
|
||||
|
||||
def test_log_add_missing_db_columns(self):
|
||||
def test_log_add_missing_db_columns(self):
|
||||
|
||||
column_names_before = []
|
||||
column_names_after = []
|
||||
column_names_before = []
|
||||
column_names_after = []
|
||||
|
||||
c = self.connection.cursor()
|
||||
c.execute("PRAGMA table_info(test)")
|
||||
result = c.fetchall()
|
||||
for t in result:
|
||||
column_names_before.append(t[1].upper())
|
||||
c = self.connection.cursor()
|
||||
c.execute("PRAGMA table_info(test)")
|
||||
result = c.fetchall()
|
||||
for t in result:
|
||||
column_names_before.append(t[1].upper())
|
||||
|
||||
self.log.add_missing_db_columns()
|
||||
self.log.add_missing_db_columns()
|
||||
|
||||
c.execute("PRAGMA table_info(test)")
|
||||
result = c.fetchall()
|
||||
for t in result:
|
||||
column_names_after.append(t[1].upper())
|
||||
c.execute("PRAGMA table_info(test)")
|
||||
result = c.fetchall()
|
||||
for t in result:
|
||||
column_names_after.append(t[1].upper())
|
||||
|
||||
print("Column names before: ", column_names_before)
|
||||
print("Column names after: ", column_names_after)
|
||||
print("Column names before: ", column_names_before)
|
||||
print("Column names after: ", column_names_after)
|
||||
|
||||
assert(len(column_names_before) == len(self.field_names) + 1) # Added 1 here because of the "ID" column in all database tables.
|
||||
assert(len(column_names_after) == len(AVAILABLE_FIELD_NAMES_ORDERED) + 1)
|
||||
for field_name in AVAILABLE_FIELD_NAMES_ORDERED:
|
||||
assert(field_name in column_names_after)
|
||||
assert(len(column_names_before) == len(self.field_names) + 1) # Added 1 here because of the "ID" column in all database tables.
|
||||
assert(len(column_names_after) == len(AVAILABLE_FIELD_NAMES_ORDERED) + 1)
|
||||
for field_name in AVAILABLE_FIELD_NAMES_ORDERED:
|
||||
assert(field_name in column_names_after)
|
||||
|
||||
def test_log_add_record(self):
|
||||
self.log.add_record(self.fields_and_data)
|
||||
c = self.connection.cursor()
|
||||
c.execute("SELECT * FROM test")
|
||||
records = c.fetchall()
|
||||
|
||||
assert len(records) == 1
|
||||
|
||||
for field_name in self.field_names:
|
||||
print(self.fields_and_data[field_name], records[0][field_name])
|
||||
assert self.fields_and_data[field_name] == records[0][field_name]
|
||||
def test_log_add_record(self):
|
||||
self.log.add_record(self.fields_and_data)
|
||||
c = self.connection.cursor()
|
||||
c.execute("SELECT * FROM test")
|
||||
records = c.fetchall()
|
||||
|
||||
def test_log_delete_record(self):
|
||||
query = "INSERT INTO test VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
c = self.connection.cursor()
|
||||
c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
|
||||
assert len(records) == 1
|
||||
|
||||
c.execute("SELECT * FROM test")
|
||||
records_before = c.fetchall()
|
||||
for field_name in self.field_names:
|
||||
print(self.fields_and_data[field_name], records[0][field_name])
|
||||
assert self.fields_and_data[field_name] == records[0][field_name]
|
||||
|
||||
self.log.delete_record(1)
|
||||
def test_log_delete_record(self):
|
||||
query = "INSERT INTO test VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
c = self.connection.cursor()
|
||||
c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
|
||||
|
||||
c.execute("SELECT * FROM test")
|
||||
records_after = c.fetchall()
|
||||
c.execute("SELECT * FROM test")
|
||||
records_before = c.fetchall()
|
||||
|
||||
assert(len(records_before) == 1)
|
||||
assert(len(records_after) == 0)
|
||||
|
||||
def test_log_edit_record(self):
|
||||
query = "INSERT INTO test VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
c = self.connection.cursor()
|
||||
c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
|
||||
self.log.delete_record(1)
|
||||
|
||||
c.execute("SELECT * FROM test")
|
||||
record_before = c.fetchall()[0]
|
||||
c.execute("SELECT * FROM test")
|
||||
records_after = c.fetchall()
|
||||
|
||||
self.log.edit_record(1, "CALL", "TEST456")
|
||||
self.log.edit_record(1, "FREQ", "145.450")
|
||||
assert(len(records_before) == 1)
|
||||
assert(len(records_after) == 0)
|
||||
|
||||
c.execute("SELECT * FROM test")
|
||||
record_after = c.fetchall()[0]
|
||||
def test_log_edit_record(self):
|
||||
query = "INSERT INTO test VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
c = self.connection.cursor()
|
||||
c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
|
||||
|
||||
assert(record_before["CALL"] == "TEST123")
|
||||
assert(record_after["CALL"] == "TEST456")
|
||||
assert(record_before["FREQ"] == "145.500")
|
||||
assert(record_after["FREQ"] == "145.450")
|
||||
c.execute("SELECT * FROM test")
|
||||
record_before = c.fetchall()[0]
|
||||
|
||||
def test_log_get_record_by_index(self):
|
||||
query = "INSERT INTO test VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
c = self.connection.cursor()
|
||||
c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
|
||||
self.log.edit_record(1, "CALL", "TEST456")
|
||||
self.log.edit_record(1, "FREQ", "145.450")
|
||||
|
||||
record = self.log.get_record_by_index(1)
|
||||
print("Contents of retrieved record: ", record)
|
||||
for field_name in list(record.keys()):
|
||||
if(field_name.upper() == "ID"):
|
||||
continue
|
||||
else:
|
||||
assert(record[field_name.upper()] == self.fields_and_data[field_name.upper()])
|
||||
assert(len(record) == len(self.fields_and_data) + 1)
|
||||
c.execute("SELECT * FROM test")
|
||||
record_after = c.fetchall()[0]
|
||||
|
||||
def test_log_get_all_records(self):
|
||||
query = "INSERT INTO test VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
c = self.connection.cursor()
|
||||
# Add the same record twice
|
||||
c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
|
||||
c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
|
||||
assert(record_before["CALL"] == "TEST123")
|
||||
assert(record_after["CALL"] == "TEST456")
|
||||
assert(record_before["FREQ"] == "145.500")
|
||||
assert(record_after["FREQ"] == "145.450")
|
||||
|
||||
records = self.log.get_all_records()
|
||||
print("Contents of all retrieved records: ", records)
|
||||
assert(len(records) == 2) # There should be 2 records
|
||||
for field_name in self.field_names:
|
||||
assert(records[0][field_name] == self.fields_and_data[field_name])
|
||||
assert(records[1][field_name] == self.fields_and_data[field_name])
|
||||
def test_log_get_record_by_index(self):
|
||||
query = "INSERT INTO test VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
c = self.connection.cursor()
|
||||
c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
|
||||
|
||||
def test_log_get_number_of_records(self):
|
||||
query = "INSERT INTO test VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
c = self.connection.cursor()
|
||||
# Add the same record twice
|
||||
c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
|
||||
c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
|
||||
record = self.log.get_record_by_index(1)
|
||||
print("Contents of retrieved record: ", record)
|
||||
for field_name in list(record.keys()):
|
||||
if(field_name.upper() == "ID"):
|
||||
continue
|
||||
else:
|
||||
assert(record[field_name.upper()] == self.fields_and_data[field_name.upper()])
|
||||
assert(len(record) == len(self.fields_and_data) + 1)
|
||||
|
||||
number_of_records = self.log.get_number_of_records()
|
||||
print("Number of records in the log: ", number_of_records)
|
||||
assert(number_of_records == 2) # There should be 2 records
|
||||
def test_log_get_all_records(self):
|
||||
query = "INSERT INTO test VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
c = self.connection.cursor()
|
||||
# Add the same record twice
|
||||
c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
|
||||
c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
|
||||
|
||||
def test_log_get_duplicates(self):
|
||||
query = "INSERT INTO test VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
c = self.connection.cursor()
|
||||
n = 5 # The total number of records to insert.
|
||||
for i in range(0, n):
|
||||
c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
|
||||
assert len(self.log.get_duplicates()) == n-1 # Expecting n-1 duplicates.
|
||||
records = self.log.get_all_records()
|
||||
print("Contents of all retrieved records: ", records)
|
||||
assert(len(records) == 2) # There should be 2 records
|
||||
for field_name in self.field_names:
|
||||
assert(records[0][field_name] == self.fields_and_data[field_name])
|
||||
assert(records[1][field_name] == self.fields_and_data[field_name])
|
||||
|
||||
def test_log_get_number_of_records(self):
|
||||
query = "INSERT INTO test VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
c = self.connection.cursor()
|
||||
# Add the same record twice
|
||||
c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
|
||||
c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
|
||||
|
||||
number_of_records = self.log.get_number_of_records()
|
||||
print("Number of records in the log: ", number_of_records)
|
||||
assert(number_of_records == 2) # There should be 2 records
|
||||
|
||||
def test_log_get_duplicates(self):
|
||||
query = "INSERT INTO test VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
c = self.connection.cursor()
|
||||
n = 5 # The total number of records to insert.
|
||||
for i in range(0, n):
|
||||
c.execute(query, (self.fields_and_data["CALL"], self.fields_and_data["QSO_DATE"], self.fields_and_data["TIME_ON"], self.fields_and_data["FREQ"], self.fields_and_data["BAND"], self.fields_and_data["MODE"], self.fields_and_data["RST_SENT"], self.fields_and_data["RST_RCVD"]))
|
||||
assert len(self.log.get_duplicates()) == n-1 # Expecting n-1 duplicates.
|
||||
|
||||
if(__name__ == '__main__'):
|
||||
unittest.main()
|
||||
unittest.main()
|
||||
|
|
|
@ -20,47 +20,47 @@
|
|||
from gi.repository import Gtk
|
||||
import logging
|
||||
|
||||
|
||||
class LogNameDialog(Gtk.Dialog):
|
||||
""" A Gtk.Dialog where a user can specify the name of a Log object. """
|
||||
|
||||
def __init__(self, parent, title=None, name=None):
|
||||
""" Create and show the log name dialog to the user.
|
||||
|
||||
:arg parent: The parent Gtk window.
|
||||
:arg title: The title of the dialog. If this is None, it is assumed that a new log is going to be created.
|
||||
:arg name: The existing name of the Log object. Defaults to None if not specified (because the Log does not yet exist).
|
||||
"""
|
||||
|
||||
if(title is None):
|
||||
title = "New Log"
|
||||
else:
|
||||
title = title
|
||||
Gtk.Dialog.__init__(self, title=title, parent=parent, flags=Gtk.DialogFlags.DESTROY_WITH_PARENT, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK))
|
||||
""" A Gtk.Dialog where a user can specify the name of a Log object. """
|
||||
|
||||
hbox_temp = Gtk.HBox(spacing=0)
|
||||
label = Gtk.Label("Log Name:")
|
||||
label.set_alignment(0, 0.5)
|
||||
hbox_temp.pack_start(label, False, False, 6)
|
||||
self.entry = Gtk.Entry()
|
||||
if(name is not None):
|
||||
self.entry.set_text(name)
|
||||
hbox_temp.pack_start(self.entry, False, False, 6)
|
||||
self.vbox.pack_start(hbox_temp, False, False, 6)
|
||||
def __init__(self, parent, title=None, name=None):
|
||||
""" Create and show the log name dialog to the user.
|
||||
|
||||
self.show_all()
|
||||
:arg parent: The parent Gtk window.
|
||||
:arg title: The title of the dialog. If this is None, it is assumed that a new log is going to be created.
|
||||
:arg name: The existing name of the Log object. Defaults to None if not specified (because the Log does not yet exist).
|
||||
"""
|
||||
|
||||
logging.debug("New LogNameDialog instance created!")
|
||||
if(title is None):
|
||||
title = "New Log"
|
||||
else:
|
||||
title = title
|
||||
Gtk.Dialog.__init__(self, title=title, parent=parent, flags=Gtk.DialogFlags.DESTROY_WITH_PARENT, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK))
|
||||
|
||||
return
|
||||
hbox_temp = Gtk.HBox(spacing=0)
|
||||
label = Gtk.Label("Log Name:")
|
||||
label.set_alignment(0, 0.5)
|
||||
hbox_temp.pack_start(label, False, False, 6)
|
||||
self.entry = Gtk.Entry()
|
||||
if(name is not None):
|
||||
self.entry.set_text(name)
|
||||
hbox_temp.pack_start(self.entry, False, False, 6)
|
||||
self.vbox.pack_start(hbox_temp, False, False, 6)
|
||||
|
||||
def get_log_name(self):
|
||||
""" Return the log name specified in the Gtk.Entry box by the user.
|
||||
|
||||
:returns: The log's name.
|
||||
:rtype: str
|
||||
"""
|
||||
self.show_all()
|
||||
|
||||
logging.debug("Retrieving the log name from the LogNameDialog...")
|
||||
return self.entry.get_text()
|
||||
logging.debug("New LogNameDialog instance created!")
|
||||
|
||||
return
|
||||
|
||||
def get_log_name(self):
|
||||
""" Return the log name specified in the Gtk.Entry box by the user.
|
||||
|
||||
:returns: The log's name.
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
logging.debug("Retrieving the log name from the LogNameDialog...")
|
||||
return self.entry.get_text()
|
||||
|
|
2342
pyqso/logbook.py
2342
pyqso/logbook.py
Plik diff jest za duży
Load Diff
472
pyqso/menu.py
472
pyqso/menu.py
|
@ -22,266 +22,264 @@ import logging
|
|||
import configparser
|
||||
import os.path
|
||||
|
||||
|
||||
class Menu(Gtk.MenuBar):
|
||||
""" The PyQSO menu bar along the top of the main window. """
|
||||
|
||||
def __init__(self, parent):
|
||||
""" Set up all menu items and connect to the various functions.
|
||||
|
||||
:arg parent: The parent Gtk window.
|
||||
"""
|
||||
""" The PyQSO menu bar along the top of the main window. """
|
||||
|
||||
logging.debug("New Menu instance created!")
|
||||
|
||||
# First let's call the constructor of the super class (Gtk.MenuBar)
|
||||
Gtk.MenuBar.__init__(self)
|
||||
def __init__(self, parent):
|
||||
""" Set up all menu items and connect to the various functions.
|
||||
|
||||
logging.debug("Setting up the menu bar...")
|
||||
agrp = Gtk.AccelGroup()
|
||||
parent.add_accel_group(agrp)
|
||||
:arg parent: The parent Gtk window.
|
||||
"""
|
||||
|
||||
self.items = {}
|
||||
|
||||
###### LOGBOOK ######
|
||||
mitem_logbook = Gtk.MenuItem("Logbook")
|
||||
self.append(mitem_logbook)
|
||||
subm_logbook = Gtk.Menu()
|
||||
mitem_logbook.set_submenu(subm_logbook)
|
||||
logging.debug("New Menu instance created!")
|
||||
|
||||
# Create logbook
|
||||
mitem_connect = Gtk.ImageMenuItem("Create a New Logbook...")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_NEW, Gtk.IconSize.MENU)
|
||||
mitem_connect.set_image(icon)
|
||||
mitem_connect.connect("activate", parent.logbook.new)
|
||||
subm_logbook.append(mitem_connect)
|
||||
self.items["NEW_LOGBOOK"] = mitem_connect
|
||||
|
||||
# Open logbook
|
||||
mitem_connect = Gtk.ImageMenuItem("Open an Existing Logbook...")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_OPEN, Gtk.IconSize.MENU)
|
||||
mitem_connect.set_image(icon)
|
||||
mitem_connect.connect("activate", parent.logbook.open)
|
||||
key, mod = Gtk.accelerator_parse("<Control>O")
|
||||
mitem_connect.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
|
||||
subm_logbook.append(mitem_connect)
|
||||
self.items["OPEN_LOGBOOK"] = mitem_connect
|
||||
# First let's call the constructor of the super class (Gtk.MenuBar)
|
||||
Gtk.MenuBar.__init__(self)
|
||||
|
||||
# Close logbook
|
||||
mitem_disconnect = Gtk.ImageMenuItem("Close Logbook")
|
||||
mitem_disconnect.connect("activate", parent.logbook.close)
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_CLOSE, Gtk.IconSize.MENU)
|
||||
mitem_disconnect.set_image(icon)
|
||||
key, mod = Gtk.accelerator_parse("<Control>W")
|
||||
mitem_disconnect.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
|
||||
subm_logbook.append(mitem_disconnect)
|
||||
self.items["CLOSE_LOGBOOK"] = mitem_disconnect
|
||||
logging.debug("Setting up the menu bar...")
|
||||
agrp = Gtk.AccelGroup()
|
||||
parent.add_accel_group(agrp)
|
||||
|
||||
subm_logbook.append(Gtk.SeparatorMenuItem())
|
||||
self.items = {}
|
||||
|
||||
# New log
|
||||
mitem_new = Gtk.ImageMenuItem("New Log")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU)
|
||||
mitem_new.set_image(icon)
|
||||
mitem_new.connect("activate", parent.logbook.new_log)
|
||||
key, mod = Gtk.accelerator_parse("<Control>N")
|
||||
mitem_new.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
|
||||
subm_logbook.append(mitem_new)
|
||||
self.items["NEW_LOG"] = mitem_new
|
||||
# LOGBOOK ######
|
||||
mitem_logbook = Gtk.MenuItem("Logbook")
|
||||
self.append(mitem_logbook)
|
||||
subm_logbook = Gtk.Menu()
|
||||
mitem_logbook.set_submenu(subm_logbook)
|
||||
|
||||
# Delete the current log
|
||||
mitem_delete = Gtk.ImageMenuItem("Delete Selected Log")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_DELETE, Gtk.IconSize.MENU)
|
||||
mitem_delete.set_image(icon)
|
||||
mitem_delete.connect("activate", parent.logbook.delete_log)
|
||||
subm_logbook.append(mitem_delete)
|
||||
self.items["DELETE_LOG"] = mitem_delete
|
||||
|
||||
# Rename the current log
|
||||
mitem_rename = Gtk.ImageMenuItem("Rename Selected Log")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_EDIT, Gtk.IconSize.MENU)
|
||||
mitem_rename.set_image(icon)
|
||||
mitem_rename.connect("activate", parent.logbook.rename_log)
|
||||
subm_logbook.append(mitem_rename)
|
||||
self.items["RENAME_LOG"] = mitem_rename
|
||||
# Create logbook
|
||||
mitem_connect = Gtk.ImageMenuItem("Create a New Logbook...")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_NEW, Gtk.IconSize.MENU)
|
||||
mitem_connect.set_image(icon)
|
||||
mitem_connect.connect("activate", parent.logbook.new)
|
||||
subm_logbook.append(mitem_connect)
|
||||
self.items["NEW_LOGBOOK"] = mitem_connect
|
||||
|
||||
subm_logbook.append(Gtk.SeparatorMenuItem())
|
||||
# Open logbook
|
||||
mitem_connect = Gtk.ImageMenuItem("Open an Existing Logbook...")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_OPEN, Gtk.IconSize.MENU)
|
||||
mitem_connect.set_image(icon)
|
||||
mitem_connect.connect("activate", parent.logbook.open)
|
||||
key, mod = Gtk.accelerator_parse("<Control>O")
|
||||
mitem_connect.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
|
||||
subm_logbook.append(mitem_connect)
|
||||
self.items["OPEN_LOGBOOK"] = mitem_connect
|
||||
|
||||
# Import log
|
||||
mitem_import = Gtk.ImageMenuItem("Import Log")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_GO_FORWARD, Gtk.IconSize.MENU)
|
||||
mitem_import.set_image(icon)
|
||||
mitem_import.connect("activate", parent.logbook.import_log)
|
||||
subm_logbook.append(mitem_import)
|
||||
self.items["IMPORT_LOG"] = mitem_import
|
||||
# Close logbook
|
||||
mitem_disconnect = Gtk.ImageMenuItem("Close Logbook")
|
||||
mitem_disconnect.connect("activate", parent.logbook.close)
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_CLOSE, Gtk.IconSize.MENU)
|
||||
mitem_disconnect.set_image(icon)
|
||||
key, mod = Gtk.accelerator_parse("<Control>W")
|
||||
mitem_disconnect.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
|
||||
subm_logbook.append(mitem_disconnect)
|
||||
self.items["CLOSE_LOGBOOK"] = mitem_disconnect
|
||||
|
||||
# Export the current log
|
||||
mitem_export = Gtk.ImageMenuItem("Export Log")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_GO_BACK, Gtk.IconSize.MENU)
|
||||
mitem_export.set_image(icon)
|
||||
mitem_export.connect("activate", parent.logbook.export_log)
|
||||
subm_logbook.append(mitem_export)
|
||||
self.items["EXPORT_LOG"] = mitem_export
|
||||
|
||||
subm_logbook.append(Gtk.SeparatorMenuItem())
|
||||
subm_logbook.append(Gtk.SeparatorMenuItem())
|
||||
|
||||
# Print log
|
||||
mitem_print = Gtk.ImageMenuItem("Print Log")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_PRINT, Gtk.IconSize.MENU)
|
||||
mitem_print.set_image(icon)
|
||||
mitem_print.connect("activate", parent.logbook.print_log)
|
||||
key, mod = Gtk.accelerator_parse("<Control>P")
|
||||
mitem_print.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
|
||||
subm_logbook.append(mitem_print)
|
||||
self.items["PRINT_LOG"] = mitem_print
|
||||
# New log
|
||||
mitem_new = Gtk.ImageMenuItem("New Log")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU)
|
||||
mitem_new.set_image(icon)
|
||||
mitem_new.connect("activate", parent.logbook.new_log)
|
||||
key, mod = Gtk.accelerator_parse("<Control>N")
|
||||
mitem_new.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
|
||||
subm_logbook.append(mitem_new)
|
||||
self.items["NEW_LOG"] = mitem_new
|
||||
|
||||
subm_logbook.append(Gtk.SeparatorMenuItem())
|
||||
# Delete the current log
|
||||
mitem_delete = Gtk.ImageMenuItem("Delete Selected Log")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_DELETE, Gtk.IconSize.MENU)
|
||||
mitem_delete.set_image(icon)
|
||||
mitem_delete.connect("activate", parent.logbook.delete_log)
|
||||
subm_logbook.append(mitem_delete)
|
||||
self.items["DELETE_LOG"] = mitem_delete
|
||||
|
||||
# Quit
|
||||
mitem_quit = Gtk.ImageMenuItem("Quit")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_QUIT, Gtk.IconSize.MENU)
|
||||
mitem_quit.set_image(icon)
|
||||
mitem_quit.connect("activate", Gtk.main_quit)
|
||||
key, mod = Gtk.accelerator_parse("<Control>Q")
|
||||
mitem_quit.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
|
||||
subm_logbook.append(mitem_quit)
|
||||
self.items["QUIT"] = mitem_quit
|
||||
|
||||
|
||||
###### RECORDS ######
|
||||
mitem_records = Gtk.MenuItem("Records")
|
||||
self.append(mitem_records)
|
||||
subm_records = Gtk.Menu()
|
||||
mitem_records.set_submenu(subm_records)
|
||||
|
||||
mitem_addrecord = Gtk.ImageMenuItem("Add Record...")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU)
|
||||
mitem_addrecord.set_image(icon)
|
||||
mitem_addrecord.connect("activate", parent.logbook.add_record_callback)
|
||||
key, mod = Gtk.accelerator_parse("<Control>R")
|
||||
mitem_addrecord.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
|
||||
subm_records.append(mitem_addrecord)
|
||||
self.items["ADD_RECORD"] = mitem_addrecord
|
||||
|
||||
mitem_editrecord = Gtk.ImageMenuItem("Edit Selected Record...")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_EDIT, Gtk.IconSize.MENU)
|
||||
mitem_editrecord.set_image(icon)
|
||||
mitem_editrecord.connect("activate", parent.logbook.edit_record_callback, None, None)
|
||||
key, mod = Gtk.accelerator_parse("<Control>E")
|
||||
mitem_editrecord.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
|
||||
subm_records.append(mitem_editrecord)
|
||||
self.items["EDIT_RECORD"] = mitem_editrecord
|
||||
# Rename the current log
|
||||
mitem_rename = Gtk.ImageMenuItem("Rename Selected Log")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_EDIT, Gtk.IconSize.MENU)
|
||||
mitem_rename.set_image(icon)
|
||||
mitem_rename.connect("activate", parent.logbook.rename_log)
|
||||
subm_logbook.append(mitem_rename)
|
||||
self.items["RENAME_LOG"] = mitem_rename
|
||||
|
||||
mitem_deleterecord = Gtk.ImageMenuItem("Delete Selected Record...")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_DELETE, Gtk.IconSize.MENU)
|
||||
mitem_deleterecord.set_image(icon)
|
||||
mitem_deleterecord.connect("activate", parent.logbook.delete_record_callback)
|
||||
key, mod = Gtk.accelerator_parse("Delete")
|
||||
mitem_deleterecord.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
|
||||
subm_records.append(mitem_deleterecord)
|
||||
self.items["DELETE_RECORD"] = mitem_deleterecord
|
||||
subm_logbook.append(Gtk.SeparatorMenuItem())
|
||||
|
||||
mitem_removeduplicates = Gtk.ImageMenuItem("Remove Duplicate Records")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_FIND_AND_REPLACE, Gtk.IconSize.MENU)
|
||||
mitem_removeduplicates.set_image(icon)
|
||||
mitem_removeduplicates.connect("activate", parent.logbook.remove_duplicates_callback)
|
||||
subm_records.append(mitem_removeduplicates)
|
||||
self.items["REMOVE_DUPLICATES"] = mitem_removeduplicates
|
||||
|
||||
|
||||
###### VIEW ######
|
||||
mitem_view = Gtk.MenuItem("View")
|
||||
self.append(mitem_view)
|
||||
subm_view = Gtk.Menu()
|
||||
mitem_view.set_submenu(subm_view)
|
||||
# Import log
|
||||
mitem_import = Gtk.ImageMenuItem("Import Log")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_GO_FORWARD, Gtk.IconSize.MENU)
|
||||
mitem_import.set_image(icon)
|
||||
mitem_import.connect("activate", parent.logbook.import_log)
|
||||
subm_logbook.append(mitem_import)
|
||||
self.items["IMPORT_LOG"] = mitem_import
|
||||
|
||||
mitem_toolbox = Gtk.CheckMenuItem("Toolbox")
|
||||
config = configparser.ConfigParser()
|
||||
have_config = (config.read(os.path.expanduser('~/.config/pyqso/preferences.ini')) != [])
|
||||
(section, option) = ("general", "show_toolbox")
|
||||
if(have_config and config.has_option(section, option)):
|
||||
mitem_toolbox.set_active(config.get(section, option) == "True")
|
||||
else:
|
||||
mitem_toolbox.set_active(False) # Don't show the toolbox by default
|
||||
mitem_toolbox.connect("activate", parent.toolbox.toggle_visible_callback)
|
||||
subm_view.append(mitem_toolbox)
|
||||
self.items["TOOLBOX"] = mitem_toolbox
|
||||
# Export the current log
|
||||
mitem_export = Gtk.ImageMenuItem("Export Log")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_GO_BACK, Gtk.IconSize.MENU)
|
||||
mitem_export.set_image(icon)
|
||||
mitem_export.connect("activate", parent.logbook.export_log)
|
||||
subm_logbook.append(mitem_export)
|
||||
self.items["EXPORT_LOG"] = mitem_export
|
||||
|
||||
mitem_preferences = Gtk.ImageMenuItem("Preferences...")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_PREFERENCES, Gtk.IconSize.MENU)
|
||||
mitem_preferences.set_image(icon)
|
||||
mitem_preferences.connect("activate", parent.show_preferences)
|
||||
subm_view.append(mitem_preferences)
|
||||
self.items["PREFERENCES"] = mitem_preferences
|
||||
|
||||
subm_logbook.append(Gtk.SeparatorMenuItem())
|
||||
|
||||
###### HELP ######
|
||||
mitem_help = Gtk.MenuItem("Help")
|
||||
self.append(mitem_help)
|
||||
subm_help = Gtk.Menu()
|
||||
mitem_help.set_submenu(subm_help)
|
||||
|
||||
# About
|
||||
mitem_about = Gtk.ImageMenuItem("About PyQSO")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_ABOUT, Gtk.IconSize.MENU)
|
||||
mitem_about.set_image(icon)
|
||||
mitem_about.connect("activate", parent.show_about)
|
||||
subm_help.append(mitem_about)
|
||||
# Print log
|
||||
mitem_print = Gtk.ImageMenuItem("Print Log")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_PRINT, Gtk.IconSize.MENU)
|
||||
mitem_print.set_image(icon)
|
||||
mitem_print.connect("activate", parent.logbook.print_log)
|
||||
key, mod = Gtk.accelerator_parse("<Control>P")
|
||||
mitem_print.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
|
||||
subm_logbook.append(mitem_print)
|
||||
self.items["PRINT_LOG"] = mitem_print
|
||||
|
||||
self.set_logbook_item_sensitive(True)
|
||||
self.set_log_items_sensitive(False)
|
||||
self.set_record_items_sensitive(False)
|
||||
|
||||
logging.debug("Menu bar ready!")
|
||||
subm_logbook.append(Gtk.SeparatorMenuItem())
|
||||
|
||||
return
|
||||
|
||||
def set_logbook_item_sensitive(self, sensitive):
|
||||
""" Enable/disable logbook-related menu items.
|
||||
|
||||
:arg bool sensitive: If True, enable the 'new logbook' and 'open logbook' menu items. If False, disable them.
|
||||
"""
|
||||
logging.debug("Setting the 'Create/Open Logbook' menu item's sensitivity to: %s..." % sensitive)
|
||||
self.items["NEW_LOGBOOK"].set_sensitive(sensitive)
|
||||
self.items["OPEN_LOGBOOK"].set_sensitive(sensitive)
|
||||
self.items["CLOSE_LOGBOOK"].set_sensitive(not sensitive)
|
||||
logging.debug("Set the 'Create/Open Logbook' menu item's sensitivity to: %s." % sensitive)
|
||||
return
|
||||
# Quit
|
||||
mitem_quit = Gtk.ImageMenuItem("Quit")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_QUIT, Gtk.IconSize.MENU)
|
||||
mitem_quit.set_image(icon)
|
||||
mitem_quit.connect("activate", Gtk.main_quit)
|
||||
key, mod = Gtk.accelerator_parse("<Control>Q")
|
||||
mitem_quit.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
|
||||
subm_logbook.append(mitem_quit)
|
||||
self.items["QUIT"] = mitem_quit
|
||||
|
||||
def set_log_items_sensitive(self, sensitive):
|
||||
""" Enable/disable log-related menu items.
|
||||
|
||||
:arg bool sensitive: If True, enable all the log-related menu items. If False, disable them all.
|
||||
"""
|
||||
logging.debug("Setting log-related menu item sensitivity to: %s..." % sensitive)
|
||||
for item_name in ["NEW_LOG", "DELETE_LOG", "RENAME_LOG", "IMPORT_LOG", "EXPORT_LOG", "PRINT_LOG"]:
|
||||
self.items[item_name].set_sensitive(sensitive)
|
||||
logging.debug("Set log-related menu item sensitivity to: %s." % sensitive)
|
||||
return
|
||||
# RECORDS ######
|
||||
mitem_records = Gtk.MenuItem("Records")
|
||||
self.append(mitem_records)
|
||||
subm_records = Gtk.Menu()
|
||||
mitem_records.set_submenu(subm_records)
|
||||
|
||||
def set_record_items_sensitive(self, sensitive):
|
||||
""" Enable/disable record-related menu items.
|
||||
|
||||
:arg bool sensitive: If True, enable all the record-related menu items. If False, disable them all.
|
||||
"""
|
||||
logging.debug("Setting record-related menu item sensitivity to: %s..." % sensitive)
|
||||
for item_name in ["ADD_RECORD", "EDIT_RECORD", "DELETE_RECORD", "REMOVE_DUPLICATES"]:
|
||||
self.items[item_name].set_sensitive(sensitive)
|
||||
logging.debug("Set record-related menu item sensitivity to: %s." % sensitive)
|
||||
return
|
||||
mitem_addrecord = Gtk.ImageMenuItem("Add Record...")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU)
|
||||
mitem_addrecord.set_image(icon)
|
||||
mitem_addrecord.connect("activate", parent.logbook.add_record_callback)
|
||||
key, mod = Gtk.accelerator_parse("<Control>R")
|
||||
mitem_addrecord.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
|
||||
subm_records.append(mitem_addrecord)
|
||||
self.items["ADD_RECORD"] = mitem_addrecord
|
||||
|
||||
mitem_editrecord = Gtk.ImageMenuItem("Edit Selected Record...")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_EDIT, Gtk.IconSize.MENU)
|
||||
mitem_editrecord.set_image(icon)
|
||||
mitem_editrecord.connect("activate", parent.logbook.edit_record_callback, None, None)
|
||||
key, mod = Gtk.accelerator_parse("<Control>E")
|
||||
mitem_editrecord.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
|
||||
subm_records.append(mitem_editrecord)
|
||||
self.items["EDIT_RECORD"] = mitem_editrecord
|
||||
|
||||
mitem_deleterecord = Gtk.ImageMenuItem("Delete Selected Record...")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_DELETE, Gtk.IconSize.MENU)
|
||||
mitem_deleterecord.set_image(icon)
|
||||
mitem_deleterecord.connect("activate", parent.logbook.delete_record_callback)
|
||||
key, mod = Gtk.accelerator_parse("Delete")
|
||||
mitem_deleterecord.add_accelerator("activate", agrp, key, mod, Gtk.AccelFlags.VISIBLE)
|
||||
subm_records.append(mitem_deleterecord)
|
||||
self.items["DELETE_RECORD"] = mitem_deleterecord
|
||||
|
||||
mitem_removeduplicates = Gtk.ImageMenuItem("Remove Duplicate Records")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_FIND_AND_REPLACE, Gtk.IconSize.MENU)
|
||||
mitem_removeduplicates.set_image(icon)
|
||||
mitem_removeduplicates.connect("activate", parent.logbook.remove_duplicates_callback)
|
||||
subm_records.append(mitem_removeduplicates)
|
||||
self.items["REMOVE_DUPLICATES"] = mitem_removeduplicates
|
||||
|
||||
# VIEW ######
|
||||
mitem_view = Gtk.MenuItem("View")
|
||||
self.append(mitem_view)
|
||||
subm_view = Gtk.Menu()
|
||||
mitem_view.set_submenu(subm_view)
|
||||
|
||||
mitem_toolbox = Gtk.CheckMenuItem("Toolbox")
|
||||
config = configparser.ConfigParser()
|
||||
have_config = (config.read(os.path.expanduser('~/.config/pyqso/preferences.ini')) != [])
|
||||
(section, option) = ("general", "show_toolbox")
|
||||
if(have_config and config.has_option(section, option)):
|
||||
mitem_toolbox.set_active(config.get(section, option) == "True")
|
||||
else:
|
||||
mitem_toolbox.set_active(False) # Don't show the toolbox by default
|
||||
mitem_toolbox.connect("activate", parent.toolbox.toggle_visible_callback)
|
||||
subm_view.append(mitem_toolbox)
|
||||
self.items["TOOLBOX"] = mitem_toolbox
|
||||
|
||||
mitem_preferences = Gtk.ImageMenuItem("Preferences...")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_PREFERENCES, Gtk.IconSize.MENU)
|
||||
mitem_preferences.set_image(icon)
|
||||
mitem_preferences.connect("activate", parent.show_preferences)
|
||||
subm_view.append(mitem_preferences)
|
||||
self.items["PREFERENCES"] = mitem_preferences
|
||||
|
||||
# HELP ######
|
||||
mitem_help = Gtk.MenuItem("Help")
|
||||
self.append(mitem_help)
|
||||
subm_help = Gtk.Menu()
|
||||
mitem_help.set_submenu(subm_help)
|
||||
|
||||
# About
|
||||
mitem_about = Gtk.ImageMenuItem("About PyQSO")
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_ABOUT, Gtk.IconSize.MENU)
|
||||
mitem_about.set_image(icon)
|
||||
mitem_about.connect("activate", parent.show_about)
|
||||
subm_help.append(mitem_about)
|
||||
|
||||
self.set_logbook_item_sensitive(True)
|
||||
self.set_log_items_sensitive(False)
|
||||
self.set_record_items_sensitive(False)
|
||||
|
||||
logging.debug("Menu bar ready!")
|
||||
|
||||
return
|
||||
|
||||
def set_logbook_item_sensitive(self, sensitive):
|
||||
""" Enable/disable logbook-related menu items.
|
||||
|
||||
:arg bool sensitive: If True, enable the 'new logbook' and 'open logbook' menu items. If False, disable them.
|
||||
"""
|
||||
logging.debug("Setting the 'Create/Open Logbook' menu item's sensitivity to: %s..." % sensitive)
|
||||
self.items["NEW_LOGBOOK"].set_sensitive(sensitive)
|
||||
self.items["OPEN_LOGBOOK"].set_sensitive(sensitive)
|
||||
self.items["CLOSE_LOGBOOK"].set_sensitive(not sensitive)
|
||||
logging.debug("Set the 'Create/Open Logbook' menu item's sensitivity to: %s." % sensitive)
|
||||
return
|
||||
|
||||
def set_log_items_sensitive(self, sensitive):
|
||||
""" Enable/disable log-related menu items.
|
||||
|
||||
:arg bool sensitive: If True, enable all the log-related menu items. If False, disable them all.
|
||||
"""
|
||||
logging.debug("Setting log-related menu item sensitivity to: %s..." % sensitive)
|
||||
for item_name in ["NEW_LOG", "DELETE_LOG", "RENAME_LOG", "IMPORT_LOG", "EXPORT_LOG", "PRINT_LOG"]:
|
||||
self.items[item_name].set_sensitive(sensitive)
|
||||
logging.debug("Set log-related menu item sensitivity to: %s." % sensitive)
|
||||
return
|
||||
|
||||
def set_record_items_sensitive(self, sensitive):
|
||||
""" Enable/disable record-related menu items.
|
||||
|
||||
:arg bool sensitive: If True, enable all the record-related menu items. If False, disable them all.
|
||||
"""
|
||||
logging.debug("Setting record-related menu item sensitivity to: %s..." % sensitive)
|
||||
for item_name in ["ADD_RECORD", "EDIT_RECORD", "DELETE_RECORD", "REMOVE_DUPLICATES"]:
|
||||
self.items[item_name].set_sensitive(sensitive)
|
||||
logging.debug("Set record-related menu item sensitivity to: %s." % sensitive)
|
||||
return
|
||||
|
|
Plik diff jest za duży
Load Diff
Plik diff jest za duży
Load Diff
|
@ -20,73 +20,74 @@
|
|||
from gi.repository import Gtk
|
||||
import logging
|
||||
|
||||
|
||||
class TelnetConnectionDialog(Gtk.Dialog):
|
||||
""" A simple dialog through which users can specify host and login information for a Telnet server.
|
||||
This can be used to connect to DX clusters. """
|
||||
|
||||
def __init__(self, parent):
|
||||
""" Set up and show the Telnet connection dialog to the user.
|
||||
|
||||
:arg parent: The parent Gtk window/dialog.
|
||||
"""
|
||||
|
||||
logging.debug("Setting up the Telnet connection dialog...")
|
||||
|
||||
Gtk.Dialog.__init__(self, title="New Telnet Connection", parent=parent, flags=Gtk.DialogFlags.DESTROY_WITH_PARENT, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK))
|
||||
""" A simple dialog through which users can specify host and login information for a Telnet server.
|
||||
This can be used to connect to DX clusters. """
|
||||
|
||||
self.sources = {}
|
||||
def __init__(self, parent):
|
||||
""" Set up and show the Telnet connection dialog to the user.
|
||||
|
||||
hbox_temp = Gtk.HBox(spacing=0)
|
||||
label = Gtk.Label("Host: ", halign=Gtk.Align.START)
|
||||
label.set_width_chars(12)
|
||||
label.set_alignment(0, 0.5)
|
||||
hbox_temp.pack_start(label, False, False, 6)
|
||||
self.sources["HOST"] = Gtk.Entry()
|
||||
hbox_temp.pack_start(self.sources["HOST"], True, True, 6)
|
||||
self.vbox.pack_start(hbox_temp, False, False, 6)
|
||||
:arg parent: The parent Gtk window/dialog.
|
||||
"""
|
||||
|
||||
hbox_temp = Gtk.HBox(spacing=0)
|
||||
label = Gtk.Label("Port: ", halign=Gtk.Align.START)
|
||||
label.set_width_chars(12)
|
||||
label.set_alignment(0, 0.5)
|
||||
hbox_temp.pack_start(label, False, False, 6)
|
||||
self.sources["PORT"] = Gtk.Entry()
|
||||
hbox_temp.pack_start(self.sources["PORT"], True, True, 6)
|
||||
self.vbox.pack_start(hbox_temp, False, False, 6)
|
||||
logging.debug("Setting up the Telnet connection dialog...")
|
||||
|
||||
hbox_temp = Gtk.HBox(spacing=0)
|
||||
label = Gtk.Label("Username: ", halign=Gtk.Align.START)
|
||||
label.set_width_chars(12)
|
||||
label.set_alignment(0, 0.5)
|
||||
hbox_temp.pack_start(label, False, False, 6)
|
||||
self.sources["USERNAME"] = Gtk.Entry()
|
||||
hbox_temp.pack_start(self.sources["USERNAME"], True, True, 6)
|
||||
self.vbox.pack_start(hbox_temp, False, False, 6)
|
||||
Gtk.Dialog.__init__(self, title="New Telnet Connection", parent=parent, flags=Gtk.DialogFlags.DESTROY_WITH_PARENT, buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK))
|
||||
|
||||
hbox_temp = Gtk.HBox(spacing=0)
|
||||
label = Gtk.Label("Password: ", halign=Gtk.Align.START)
|
||||
label.set_width_chars(12)
|
||||
label.set_alignment(0, 0.5)
|
||||
hbox_temp.pack_start(label, False, False, 6)
|
||||
self.sources["PASSWORD"] = Gtk.Entry()
|
||||
self.sources["PASSWORD"].set_visibility(False) # Mask the password with the "*" character.
|
||||
hbox_temp.pack_start(self.sources["PASSWORD"], True, True, 6)
|
||||
self.vbox.pack_start(hbox_temp, False, False, 6)
|
||||
|
||||
self.sources["BOOKMARK"] = Gtk.CheckButton("Bookmark server details for next time")
|
||||
self.vbox.pack_start(self.sources["BOOKMARK"], False, False, 6)
|
||||
self.sources = {}
|
||||
|
||||
logging.debug("Telnet connection dialog ready!")
|
||||
hbox_temp = Gtk.HBox(spacing=0)
|
||||
label = Gtk.Label("Host: ", halign=Gtk.Align.START)
|
||||
label.set_width_chars(12)
|
||||
label.set_alignment(0, 0.5)
|
||||
hbox_temp.pack_start(label, False, False, 6)
|
||||
self.sources["HOST"] = Gtk.Entry()
|
||||
hbox_temp.pack_start(self.sources["HOST"], True, True, 6)
|
||||
self.vbox.pack_start(hbox_temp, False, False, 6)
|
||||
|
||||
self.show_all()
|
||||
return
|
||||
hbox_temp = Gtk.HBox(spacing=0)
|
||||
label = Gtk.Label("Port: ", halign=Gtk.Align.START)
|
||||
label.set_width_chars(12)
|
||||
label.set_alignment(0, 0.5)
|
||||
hbox_temp.pack_start(label, False, False, 6)
|
||||
self.sources["PORT"] = Gtk.Entry()
|
||||
hbox_temp.pack_start(self.sources["PORT"], True, True, 6)
|
||||
self.vbox.pack_start(hbox_temp, False, False, 6)
|
||||
|
||||
def get_connection_info(self):
|
||||
""" Return the host and login information stored in the Gtk.Entry boxes.
|
||||
|
||||
:returns: A dictionary of Telnet connection-related information (username, password, port, host).
|
||||
:rtype: dict
|
||||
"""
|
||||
logging.debug("Returning Telnet connection information...")
|
||||
return self.sources
|
||||
hbox_temp = Gtk.HBox(spacing=0)
|
||||
label = Gtk.Label("Username: ", halign=Gtk.Align.START)
|
||||
label.set_width_chars(12)
|
||||
label.set_alignment(0, 0.5)
|
||||
hbox_temp.pack_start(label, False, False, 6)
|
||||
self.sources["USERNAME"] = Gtk.Entry()
|
||||
hbox_temp.pack_start(self.sources["USERNAME"], True, True, 6)
|
||||
self.vbox.pack_start(hbox_temp, False, False, 6)
|
||||
|
||||
hbox_temp = Gtk.HBox(spacing=0)
|
||||
label = Gtk.Label("Password: ", halign=Gtk.Align.START)
|
||||
label.set_width_chars(12)
|
||||
label.set_alignment(0, 0.5)
|
||||
hbox_temp.pack_start(label, False, False, 6)
|
||||
self.sources["PASSWORD"] = Gtk.Entry()
|
||||
self.sources["PASSWORD"].set_visibility(False) # Mask the password with the "*" character.
|
||||
hbox_temp.pack_start(self.sources["PASSWORD"], True, True, 6)
|
||||
self.vbox.pack_start(hbox_temp, False, False, 6)
|
||||
|
||||
self.sources["BOOKMARK"] = Gtk.CheckButton("Bookmark server details for next time")
|
||||
self.vbox.pack_start(self.sources["BOOKMARK"], False, False, 6)
|
||||
|
||||
logging.debug("Telnet connection dialog ready!")
|
||||
|
||||
self.show_all()
|
||||
return
|
||||
|
||||
def get_connection_info(self):
|
||||
""" Return the host and login information stored in the Gtk.Entry boxes.
|
||||
|
||||
:returns: A dictionary of Telnet connection-related information (username, password, port, host).
|
||||
:rtype: dict
|
||||
"""
|
||||
logging.debug("Returning Telnet connection information...")
|
||||
return self.sources
|
||||
|
|
193
pyqso/toolbar.py
193
pyqso/toolbar.py
|
@ -20,121 +20,120 @@
|
|||
from gi.repository import Gtk
|
||||
import logging
|
||||
|
||||
|
||||
class Toolbar(Gtk.HBox):
|
||||
""" The toolbar underneath the menu bar. """
|
||||
|
||||
def __init__(self, parent):
|
||||
""" Set up the various buttons in the toolbar, and connect to their corresponding functions. """
|
||||
""" The toolbar underneath the menu bar. """
|
||||
|
||||
logging.debug("Setting up the toolbar...")
|
||||
|
||||
Gtk.HBox.__init__(self, spacing=2)
|
||||
def __init__(self, parent):
|
||||
""" Set up the various buttons in the toolbar, and connect to their corresponding functions. """
|
||||
|
||||
self.buttons = {}
|
||||
logging.debug("Setting up the toolbar...")
|
||||
|
||||
# Create logbook
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_NEW, Gtk.IconSize.BUTTON)
|
||||
button = Gtk.Button()
|
||||
button.add(icon)
|
||||
button.set_tooltip_text('Create a New Logbook')
|
||||
button.connect("clicked", parent.logbook.new)
|
||||
self.pack_start(button, False, False, 0)
|
||||
self.buttons["NEW_LOGBOOK"] = button
|
||||
|
||||
# Open logbook
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_OPEN, Gtk.IconSize.BUTTON)
|
||||
button = Gtk.Button()
|
||||
button.add(icon)
|
||||
button.set_tooltip_text('Open an Existing Logbook')
|
||||
button.connect("clicked", parent.logbook.open)
|
||||
self.pack_start(button, False, False, 0)
|
||||
self.buttons["OPEN_LOGBOOK"] = button
|
||||
Gtk.HBox.__init__(self, spacing=2)
|
||||
|
||||
# Close logbook
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_CLOSE, Gtk.IconSize.BUTTON)
|
||||
button = Gtk.Button()
|
||||
button.add(icon)
|
||||
button.set_tooltip_text('Close Logbook')
|
||||
button.connect("clicked", parent.logbook.close)
|
||||
self.pack_start(button, False, False, 0)
|
||||
self.buttons["CLOSE_LOGBOOK"] = button
|
||||
self.buttons = {}
|
||||
|
||||
self.pack_start(Gtk.SeparatorToolItem(), False, False, 0)
|
||||
# Create logbook
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_NEW, Gtk.IconSize.BUTTON)
|
||||
button = Gtk.Button()
|
||||
button.add(icon)
|
||||
button.set_tooltip_text('Create a New Logbook')
|
||||
button.connect("clicked", parent.logbook.new)
|
||||
self.pack_start(button, False, False, 0)
|
||||
self.buttons["NEW_LOGBOOK"] = button
|
||||
|
||||
# Add record
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.BUTTON)
|
||||
button = Gtk.Button()
|
||||
button.add(icon)
|
||||
button.set_tooltip_text('Add Record')
|
||||
button.connect("clicked", parent.logbook.add_record_callback)
|
||||
self.pack_start(button, False, False, 0)
|
||||
self.buttons["ADD_RECORD"] = button
|
||||
# Open logbook
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_OPEN, Gtk.IconSize.BUTTON)
|
||||
button = Gtk.Button()
|
||||
button.add(icon)
|
||||
button.set_tooltip_text('Open an Existing Logbook')
|
||||
button.connect("clicked", parent.logbook.open)
|
||||
self.pack_start(button, False, False, 0)
|
||||
self.buttons["OPEN_LOGBOOK"] = button
|
||||
|
||||
# Edit record
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_EDIT, Gtk.IconSize.BUTTON)
|
||||
button = Gtk.Button()
|
||||
button.add(icon)
|
||||
button.set_tooltip_text('Edit Record')
|
||||
button.connect("clicked", parent.logbook.edit_record_callback, None, None)
|
||||
self.pack_start(button, False, False, 0)
|
||||
self.buttons["EDIT_RECORD"] = button
|
||||
# Close logbook
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_CLOSE, Gtk.IconSize.BUTTON)
|
||||
button = Gtk.Button()
|
||||
button.add(icon)
|
||||
button.set_tooltip_text('Close Logbook')
|
||||
button.connect("clicked", parent.logbook.close)
|
||||
self.pack_start(button, False, False, 0)
|
||||
self.buttons["CLOSE_LOGBOOK"] = button
|
||||
|
||||
# Delete record
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_DELETE, Gtk.IconSize.BUTTON)
|
||||
button = Gtk.Button()
|
||||
button.add(icon)
|
||||
button.set_tooltip_text('Delete Record')
|
||||
button.connect("clicked", parent.logbook.delete_record_callback)
|
||||
self.pack_start(button, False, False, 0)
|
||||
self.buttons["DELETE_RECORD"] = button
|
||||
self.pack_start(Gtk.SeparatorToolItem(), False, False, 0)
|
||||
|
||||
self.pack_start(Gtk.SeparatorToolItem(), False, False, 0)
|
||||
# Add record
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.BUTTON)
|
||||
button = Gtk.Button()
|
||||
button.add(icon)
|
||||
button.set_tooltip_text('Add Record')
|
||||
button.connect("clicked", parent.logbook.add_record_callback)
|
||||
self.pack_start(button, False, False, 0)
|
||||
self.buttons["ADD_RECORD"] = button
|
||||
|
||||
# Filter log
|
||||
label = Gtk.Label("Filter by callsign: ")
|
||||
self.pack_start(label, False, False, 0)
|
||||
self.filter_source = Gtk.Entry()
|
||||
self.filter_source.set_width_chars(11)
|
||||
self.filter_source.connect_after("changed", parent.logbook.filter_logs)
|
||||
self.pack_start(self.filter_source, False, False, 0)
|
||||
# Edit record
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_EDIT, Gtk.IconSize.BUTTON)
|
||||
button = Gtk.Button()
|
||||
button.add(icon)
|
||||
button.set_tooltip_text('Edit Record')
|
||||
button.connect("clicked", parent.logbook.edit_record_callback, None, None)
|
||||
self.pack_start(button, False, False, 0)
|
||||
self.buttons["EDIT_RECORD"] = button
|
||||
|
||||
self.set_logbook_button_sensitive(True)
|
||||
self.set_record_buttons_sensitive(False)
|
||||
# Delete record
|
||||
icon = Gtk.Image()
|
||||
icon.set_from_stock(Gtk.STOCK_DELETE, Gtk.IconSize.BUTTON)
|
||||
button = Gtk.Button()
|
||||
button.add(icon)
|
||||
button.set_tooltip_text('Delete Record')
|
||||
button.connect("clicked", parent.logbook.delete_record_callback)
|
||||
self.pack_start(button, False, False, 0)
|
||||
self.buttons["DELETE_RECORD"] = button
|
||||
|
||||
self.filter_source.set_sensitive(False)
|
||||
self.pack_start(Gtk.SeparatorToolItem(), False, False, 0)
|
||||
|
||||
logging.debug("Toolbar ready!")
|
||||
# Filter log
|
||||
label = Gtk.Label("Filter by callsign: ")
|
||||
self.pack_start(label, False, False, 0)
|
||||
self.filter_source = Gtk.Entry()
|
||||
self.filter_source.set_width_chars(11)
|
||||
self.filter_source.connect_after("changed", parent.logbook.filter_logs)
|
||||
self.pack_start(self.filter_source, False, False, 0)
|
||||
|
||||
return
|
||||
self.set_logbook_button_sensitive(True)
|
||||
self.set_record_buttons_sensitive(False)
|
||||
|
||||
def set_logbook_button_sensitive(self, sensitive):
|
||||
""" Enable/disable logbook-related toolbar items.
|
||||
|
||||
:arg bool sensitive: If True, enable the 'new logbook' and 'open logbook' toolbar items. If False, disable them.
|
||||
"""
|
||||
logging.debug("Setting logbook-related toolbar item sensitivity to: %s..." % sensitive)
|
||||
self.buttons["NEW_LOGBOOK"].set_sensitive(sensitive)
|
||||
self.buttons["OPEN_LOGBOOK"].set_sensitive(sensitive)
|
||||
self.buttons["CLOSE_LOGBOOK"].set_sensitive(not sensitive)
|
||||
logging.debug("Set logbook-related toolbar item sensitivity to: %s." % sensitive)
|
||||
return
|
||||
self.filter_source.set_sensitive(False)
|
||||
|
||||
def set_record_buttons_sensitive(self, sensitive):
|
||||
""" Enable/disable record-related toolbar items.
|
||||
|
||||
:arg bool sensitive: If True, enable all the record-related toolbar items. If False, disable them all.
|
||||
"""
|
||||
logging.debug("Setting record-related toolbar item sensitivity to: %s..." % sensitive)
|
||||
for button_name in ["ADD_RECORD", "EDIT_RECORD", "DELETE_RECORD"]:
|
||||
self.buttons[button_name].set_sensitive(sensitive)
|
||||
logging.debug("Set record-related toolbar item sensitivity to: %s." % sensitive)
|
||||
return
|
||||
logging.debug("Toolbar ready!")
|
||||
|
||||
return
|
||||
|
||||
def set_logbook_button_sensitive(self, sensitive):
|
||||
""" Enable/disable logbook-related toolbar items.
|
||||
|
||||
:arg bool sensitive: If True, enable the 'new logbook' and 'open logbook' toolbar items. If False, disable them.
|
||||
"""
|
||||
logging.debug("Setting logbook-related toolbar item sensitivity to: %s..." % sensitive)
|
||||
self.buttons["NEW_LOGBOOK"].set_sensitive(sensitive)
|
||||
self.buttons["OPEN_LOGBOOK"].set_sensitive(sensitive)
|
||||
self.buttons["CLOSE_LOGBOOK"].set_sensitive(not sensitive)
|
||||
logging.debug("Set logbook-related toolbar item sensitivity to: %s." % sensitive)
|
||||
return
|
||||
|
||||
def set_record_buttons_sensitive(self, sensitive):
|
||||
""" Enable/disable record-related toolbar items.
|
||||
|
||||
:arg bool sensitive: If True, enable all the record-related toolbar items. If False, disable them all.
|
||||
"""
|
||||
logging.debug("Setting record-related toolbar item sensitivity to: %s..." % sensitive)
|
||||
for button_name in ["ADD_RECORD", "EDIT_RECORD", "DELETE_RECORD"]:
|
||||
self.buttons[button_name].set_sensitive(sensitive)
|
||||
logging.debug("Set record-related toolbar item sensitivity to: %s." % sensitive)
|
||||
return
|
||||
|
|
|
@ -24,42 +24,43 @@ from pyqso.dx_cluster import *
|
|||
from pyqso.grey_line import *
|
||||
from pyqso.awards import *
|
||||
|
||||
|
||||
class Toolbox(Gtk.Frame):
|
||||
""" Contains a Gtk.Notebook full of amateur radio-related tools. """
|
||||
|
||||
def __init__(self, parent):
|
||||
""" Instantiate and insert the various tools into the toolbox. """
|
||||
""" Contains a Gtk.Notebook full of amateur radio-related tools. """
|
||||
|
||||
logging.debug("Setting up the toolbox...")
|
||||
|
||||
Gtk.Frame.__init__(self)
|
||||
self.set_label("Toolbox")
|
||||
self.parent = parent
|
||||
def __init__(self, parent):
|
||||
""" Instantiate and insert the various tools into the toolbox. """
|
||||
|
||||
self.tools = Gtk.Notebook()
|
||||
logging.debug("Setting up the toolbox...")
|
||||
|
||||
self.dx_cluster = DXCluster(self.parent)
|
||||
self.tools.insert_page(self.dx_cluster, Gtk.Label("DX Cluster"), 0)
|
||||
self.grey_line = GreyLine(self.parent)
|
||||
self.tools.insert_page(self.grey_line, Gtk.Label("Grey Line"), 1)
|
||||
self.awards = Awards(self.parent)
|
||||
self.tools.insert_page(self.awards, Gtk.Label("Awards"), 2)
|
||||
Gtk.Frame.__init__(self)
|
||||
self.set_label("Toolbox")
|
||||
self.parent = parent
|
||||
|
||||
self.add(self.tools)
|
||||
self.tools.connect_after("switch-page", self._on_switch_page)
|
||||
self.tools = Gtk.Notebook()
|
||||
|
||||
logging.debug("Toolbox ready!")
|
||||
self.dx_cluster = DXCluster(self.parent)
|
||||
self.tools.insert_page(self.dx_cluster, Gtk.Label("DX Cluster"), 0)
|
||||
self.grey_line = GreyLine(self.parent)
|
||||
self.tools.insert_page(self.grey_line, Gtk.Label("Grey Line"), 1)
|
||||
self.awards = Awards(self.parent)
|
||||
self.tools.insert_page(self.awards, Gtk.Label("Awards"), 2)
|
||||
|
||||
return
|
||||
self.add(self.tools)
|
||||
self.tools.connect_after("switch-page", self._on_switch_page)
|
||||
|
||||
def toggle_visible_callback(self, widget=None):
|
||||
""" Show/hide the toolbox. """
|
||||
self.set_visible(not self.get_visible())
|
||||
return
|
||||
logging.debug("Toolbox ready!")
|
||||
|
||||
def _on_switch_page(self, widget, label, new_page):
|
||||
""" Re-draw the Grey Line if the user switches to the grey line tab. """
|
||||
if(type(label) == GreyLine):
|
||||
label.draw() # Note that 'label' is actually a GreyLine object.
|
||||
return
|
||||
return
|
||||
|
||||
def toggle_visible_callback(self, widget=None):
|
||||
""" Show/hide the toolbox. """
|
||||
self.set_visible(not self.get_visible())
|
||||
return
|
||||
|
||||
def _on_switch_page(self, widget, label, new_page):
|
||||
""" Re-draw the Grey Line if the user switches to the grey line tab. """
|
||||
if(isinstance(label, GreyLine)):
|
||||
label.draw() # Note that 'label' is actually a GreyLine object.
|
||||
return
|
||||
|
|
19
setup.py
19
setup.py
|
@ -26,16 +26,15 @@ setup(name='PyQSO',
|
|||
author_email='c.jacobs10@imperial.ac.uk',
|
||||
url='https://github.com/ctjacobs/pyqso',
|
||||
packages=['pyqso'],
|
||||
package_dir = {'pyqso': 'pyqso'},
|
||||
package_dir={'pyqso': 'pyqso'},
|
||||
scripts=["bin/pyqso"],
|
||||
data_files=[("icons", ["icons/log_64x64.png"])],
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Intended Audience :: End Users/Desktop',
|
||||
'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
|
||||
'Natural Language :: English',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Topic :: Communications :: Ham Radio',
|
||||
]
|
||||
)
|
||||
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Intended Audience :: End Users/Desktop',
|
||||
'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
|
||||
'Natural Language :: English',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Topic :: Communications :: Ham Radio',
|
||||
]
|
||||
)
|
||||
|
|
Ładowanie…
Reference in New Issue