Make the code pep8 compliant, using the autopep8 tool. Note that we are ignoring the following: E501,F403,E226,E402,W503.

pull/51/head
Christian T. Jacobs 2016-01-27 16:23:09 +00:00
rodzic 1e5a657ca7
commit c993704e84
17 zmienionych plików z 4497 dodań i 4464 usunięć

215
bin/pyqso
Wyświetl plik

@ -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!

Plik diff jest za duży Load Diff

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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()

Wyświetl plik

@ -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()

Wyświetl plik

@ -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.

Wyświetl plik

@ -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()

Wyświetl plik

@ -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()

Plik diff jest za duży Load Diff

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -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',
]
)