2013-03-22 22:16:31 +00:00
|
|
|
#!/usr/bin/env python
|
|
|
|
# File: logbook.py
|
|
|
|
|
|
|
|
# Copyright (C) 2012 Christian Jacobs.
|
|
|
|
|
|
|
|
# This file is part of PyQSO.
|
|
|
|
|
|
|
|
# PyQSO is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# PyQSO is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with PyQSO. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
2013-07-10 23:22:27 +00:00
|
|
|
from gi.repository import Gtk, GObject, Pango, PangoCairo
|
2013-03-22 22:16:31 +00:00
|
|
|
import logging
|
2013-04-14 14:18:46 +00:00
|
|
|
import sqlite3 as sqlite
|
2013-07-01 22:23:43 +00:00
|
|
|
from os.path import basename, getctime, getmtime, expanduser
|
2013-04-21 23:15:07 +00:00
|
|
|
import datetime
|
2013-07-01 22:23:43 +00:00
|
|
|
import ConfigParser
|
2013-03-22 22:16:31 +00:00
|
|
|
|
|
|
|
from adif import *
|
2013-03-27 20:29:43 +00:00
|
|
|
from log import *
|
2013-05-10 13:05:33 +00:00
|
|
|
from log_name_dialog import *
|
2013-08-03 19:11:00 +00:00
|
|
|
from auxiliary_dialogs import *
|
2013-03-22 22:16:31 +00:00
|
|
|
|
2013-03-27 20:29:43 +00:00
|
|
|
class Logbook(Gtk.Notebook):
|
2013-09-04 20:12:04 +00:00
|
|
|
""" A Logbook object can store multiple Log objects. """
|
2013-03-22 22:16:31 +00:00
|
|
|
|
2013-08-03 19:11:00 +00:00
|
|
|
def __init__(self, parent):
|
2013-03-27 20:29:43 +00:00
|
|
|
|
|
|
|
Gtk.Notebook.__init__(self)
|
2013-04-16 22:53:24 +00:00
|
|
|
|
2013-08-03 19:11:00 +00:00
|
|
|
self.parent = parent
|
2013-04-21 14:52:06 +00:00
|
|
|
self.connection = None
|
2013-04-21 23:15:07 +00:00
|
|
|
self.summary = {}
|
2013-08-15 20:23:10 +00:00
|
|
|
self.logs = []
|
2013-04-21 14:52:06 +00:00
|
|
|
logging.debug("New Logbook instance created!")
|
2013-09-14 18:27:45 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
def open(self, widget=None, path=None):
|
|
|
|
""" Open a logbook, and render all the logs within it.
|
|
|
|
An optional 'path' argument can be specified if the database file location is known.
|
|
|
|
Otherwise, a file selection dialog will appear. """
|
|
|
|
|
|
|
|
connected = self.db_connect(path)
|
|
|
|
if(connected):
|
|
|
|
# If the connection setup was successful, then open all the logs in the database
|
|
|
|
|
|
|
|
logging.debug("Trying to retrieve all the logs in the logbook...")
|
|
|
|
self.logs = [] # A fresh stack of Log objects
|
|
|
|
try:
|
2013-09-30 15:56:11 +00:00
|
|
|
with self.connection:
|
|
|
|
c = self.connection.cursor()
|
|
|
|
c.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
|
|
|
names = c.fetchall()
|
|
|
|
for name in names:
|
|
|
|
if(name[0][0:7] == "sqlite_"):
|
|
|
|
continue # Skip SQLite internal tables
|
|
|
|
l = Log(self.connection, name[0])
|
2013-10-01 20:23:37 +00:00
|
|
|
l.populate()
|
|
|
|
self.logs.append(l)
|
2013-09-30 15:56:11 +00:00
|
|
|
except (sqlite.Error, IndexError) as e:
|
|
|
|
logging.exception(e)
|
2013-09-14 18:27:45 +00:00
|
|
|
logging.exception("Oops! Something went wrong when trying to retrieve the logs from the logbook.")
|
|
|
|
return
|
|
|
|
|
|
|
|
logging.debug("All logs retrieved successfully. Now attempting to render them all in the Gtk.Notebook...")
|
|
|
|
# For rendering the logs. One treeview and one treeselection per Log.
|
|
|
|
self.treeview = []
|
|
|
|
self.treeselection = []
|
|
|
|
self.sorter = []
|
|
|
|
self.filter = []
|
|
|
|
self._create_summary_page()
|
|
|
|
self._create_dummy_page()
|
|
|
|
|
|
|
|
# FIXME: This is an unfortunate work-around. If the area around the "+/New Log" button
|
|
|
|
# is clicked, PyQSO will change to an empty page. This signal is used to stop this from happening.
|
|
|
|
self.connect("switch-page", self._on_switch_page)
|
|
|
|
|
|
|
|
for i in range(len(self.logs)):
|
|
|
|
self._render_log(i)
|
2013-09-14 19:15:12 +00:00
|
|
|
logging.debug("All logs rendered successfully.")
|
|
|
|
|
|
|
|
self.update_summary()
|
2013-09-14 18:27:45 +00:00
|
|
|
self.parent.toolbox.awards.count()
|
|
|
|
|
|
|
|
context_id = self.parent.statusbar.get_context_id("Status")
|
|
|
|
self.parent.statusbar.push(context_id, "Logbook: %s" % self.path)
|
|
|
|
self.parent.toolbar.set_logbook_button_sensitive(False)
|
|
|
|
self.parent.menu.set_logbook_item_sensitive(False)
|
|
|
|
self.parent.menu.set_log_items_sensitive(True)
|
|
|
|
self.parent.toolbar.filter_source.set_sensitive(True)
|
|
|
|
|
|
|
|
self.show_all()
|
2013-09-14 19:15:12 +00:00
|
|
|
|
2013-09-14 18:27:45 +00:00
|
|
|
else:
|
2013-09-14 19:31:12 +00:00
|
|
|
logging.debug("Not connected to a logbook. No logs were opened.")
|
2013-09-14 18:27:45 +00:00
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
def close(self, widget=None):
|
2013-09-14 19:15:12 +00:00
|
|
|
""" Close the logbook that is currently open. """
|
2013-09-14 18:27:45 +00:00
|
|
|
|
|
|
|
disconnected = self.db_disconnect()
|
|
|
|
if(disconnected):
|
|
|
|
logging.debug("Closing all logs in the logbook...")
|
|
|
|
while(self.get_n_pages() > 0):
|
|
|
|
# Once a page is removed, the other pages get re-numbered,
|
|
|
|
# so a 'for' loop isn't the best option here.
|
|
|
|
self.remove_page(0)
|
|
|
|
logging.debug("All logs now closed.")
|
2013-09-15 03:43:03 +00:00
|
|
|
|
|
|
|
context_id = self.parent.statusbar.get_context_id("Status")
|
|
|
|
self.parent.statusbar.push(context_id, "No logbook is currently open.")
|
|
|
|
self.parent.toolbar.set_logbook_button_sensitive(True)
|
|
|
|
self.parent.menu.set_logbook_item_sensitive(True)
|
|
|
|
self.parent.menu.set_log_items_sensitive(False)
|
|
|
|
self.parent.toolbar.filter_source.set_sensitive(False)
|
2013-09-14 18:27:45 +00:00
|
|
|
else:
|
|
|
|
logging.debug("Unable to disconnect from the database. No logs were closed.")
|
|
|
|
return
|
2013-04-21 14:52:06 +00:00
|
|
|
|
2013-09-14 18:27:45 +00:00
|
|
|
def db_connect(self, path=None):
|
|
|
|
""" Create an SQL database connection to the Logbook's data source """
|
|
|
|
|
|
|
|
logging.debug("Attempting to connect to the logbook database...")
|
2013-04-21 14:52:06 +00:00
|
|
|
if(path is None):
|
|
|
|
# If no path has been provided, get one from a "File Open" dialog.
|
|
|
|
dialog = Gtk.FileChooserDialog("Open SQLite Database File",
|
|
|
|
None,
|
|
|
|
Gtk.FileChooserAction.OPEN,
|
|
|
|
(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
|
|
|
|
Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
|
|
|
|
|
|
|
|
response = dialog.run()
|
|
|
|
if(response == Gtk.ResponseType.OK):
|
|
|
|
path = dialog.get_filename()
|
|
|
|
dialog.destroy()
|
|
|
|
|
2013-09-14 18:27:45 +00:00
|
|
|
if(path is None): # If the Cancel button has been clicked, path will still be None
|
2013-04-21 14:52:06 +00:00
|
|
|
logging.debug("No file path specified.")
|
2013-09-14 18:27:45 +00:00
|
|
|
return False
|
2013-04-21 14:52:06 +00:00
|
|
|
else:
|
|
|
|
self.path = path
|
|
|
|
else:
|
|
|
|
# Use existing user input
|
|
|
|
self.path = path
|
|
|
|
|
|
|
|
# Try setting up the SQL database connection
|
2013-04-21 16:47:09 +00:00
|
|
|
try:
|
2013-09-14 18:27:45 +00:00
|
|
|
self.db_disconnect() # Destroy any existing connections first.
|
2013-04-21 14:52:06 +00:00
|
|
|
self.connection = sqlite.connect(self.path)
|
2013-04-22 13:37:21 +00:00
|
|
|
self.connection.row_factory = sqlite.Row
|
2013-04-21 14:52:06 +00:00
|
|
|
except sqlite.Error as e:
|
2013-07-02 14:13:25 +00:00
|
|
|
# PyQSO can't connect to the database.
|
2013-04-21 14:52:06 +00:00
|
|
|
logging.exception(e)
|
2013-08-03 19:11:00 +00:00
|
|
|
error(parent=self.parent, message="PyQSO cannot connect to the database. Check file permissions?")
|
2013-09-14 18:27:45 +00:00
|
|
|
return False
|
2013-04-21 14:52:06 +00:00
|
|
|
|
2013-09-14 18:27:45 +00:00
|
|
|
logging.debug("Database connection created successfully!")
|
|
|
|
return True
|
2013-04-15 01:37:02 +00:00
|
|
|
|
2013-09-14 18:27:45 +00:00
|
|
|
def db_disconnect(self):
|
2013-09-14 19:15:12 +00:00
|
|
|
""" Destroy the connection to the Logbook's data source. """
|
|
|
|
logging.debug("Cleaning up any existing database connections...")
|
2013-04-15 01:37:02 +00:00
|
|
|
if(self.connection):
|
|
|
|
try:
|
|
|
|
self.connection.close()
|
|
|
|
except sqlite.Error as e:
|
|
|
|
logging.exception(e)
|
2013-09-14 18:27:45 +00:00
|
|
|
return False
|
2013-04-15 01:37:02 +00:00
|
|
|
else:
|
2013-09-14 18:27:45 +00:00
|
|
|
logging.debug("Already disconnected. Nothing to do here.")
|
|
|
|
return True
|
2013-03-30 17:56:22 +00:00
|
|
|
|
2013-09-14 18:27:45 +00:00
|
|
|
def _create_dummy_page(self):
|
|
|
|
""" Create a blank page in the Gtk.Notebook for the "+" (New Log) tab. """
|
2014-05-18 02:17:09 +00:00
|
|
|
blank_treeview = Gtk.TreeView()
|
2013-04-16 22:53:24 +00:00
|
|
|
# Allow the Log to be scrolled up/down
|
|
|
|
sw = Gtk.ScrolledWindow()
|
|
|
|
sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
|
|
|
|
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
|
|
|
sw.add(blank_treeview)
|
|
|
|
vbox = Gtk.VBox()
|
|
|
|
vbox.pack_start(sw, True, True, 0)
|
|
|
|
|
|
|
|
# Add a "+" button to the tab
|
|
|
|
hbox = Gtk.HBox(False, 0)
|
|
|
|
icon = Gtk.Image.new_from_stock(Gtk.STOCK_ADD, Gtk.IconSize.MENU)
|
|
|
|
button = Gtk.Button()
|
|
|
|
button.set_relief(Gtk.ReliefStyle.NONE)
|
|
|
|
button.set_focus_on_click(False)
|
|
|
|
button.connect("clicked", self.new_log)
|
|
|
|
button.add(icon)
|
2013-04-21 16:47:09 +00:00
|
|
|
button.set_tooltip_text('New Log')
|
2013-04-16 22:53:24 +00:00
|
|
|
hbox.pack_start(button, False, False, 0)
|
|
|
|
hbox.show_all()
|
|
|
|
vbox.show_all()
|
|
|
|
|
2013-04-16 23:51:28 +00:00
|
|
|
self.insert_page(vbox, hbox, 1)
|
|
|
|
self.show_all()
|
|
|
|
self.set_current_page(0)
|
|
|
|
return
|
|
|
|
|
|
|
|
def _create_summary_page(self):
|
2013-09-14 18:27:45 +00:00
|
|
|
""" Create a summary page containing the number of logs in the logbook, and the logbook's modification date. """
|
2013-04-16 23:51:28 +00:00
|
|
|
vbox = Gtk.VBox()
|
2013-04-21 23:15:07 +00:00
|
|
|
|
2013-05-19 22:02:24 +00:00
|
|
|
# Database name in large font at the top of the summary page
|
2013-04-21 23:15:07 +00:00
|
|
|
hbox = Gtk.HBox()
|
2013-04-21 16:47:09 +00:00
|
|
|
label = Gtk.Label(halign=Gtk.Align.START)
|
2013-04-21 23:15:07 +00:00
|
|
|
label.set_markup("<span size=\"x-large\">%s</span>" % basename(self.path))
|
|
|
|
hbox.pack_start(label, False, False, 6)
|
|
|
|
vbox.pack_start(hbox, False, False, 2)
|
|
|
|
|
|
|
|
hbox = Gtk.HBox()
|
|
|
|
label = Gtk.Label("Number of logs: ", halign=Gtk.Align.START)
|
|
|
|
hbox.pack_start(label, False, False, 6)
|
|
|
|
self.summary["NUMBER_OF_LOGS"] = Gtk.Label("0")
|
|
|
|
hbox.pack_start(self.summary["NUMBER_OF_LOGS"], False, False, 2)
|
|
|
|
vbox.pack_start(hbox, False, False, 2)
|
|
|
|
|
|
|
|
hbox = Gtk.HBox()
|
|
|
|
label = Gtk.Label("Date modified: ", halign=Gtk.Align.START)
|
|
|
|
hbox.pack_start(label, False, False, 6)
|
|
|
|
self.summary["DATE_MODIFIED"] = Gtk.Label("0")
|
|
|
|
hbox.pack_start(self.summary["DATE_MODIFIED"], False, False, 2)
|
|
|
|
vbox.pack_start(hbox, False, False, 2)
|
2013-04-16 23:51:28 +00:00
|
|
|
|
|
|
|
hbox = Gtk.HBox(False, 0)
|
2013-04-21 14:52:06 +00:00
|
|
|
label = Gtk.Label("Summary ")
|
|
|
|
icon = Gtk.Image.new_from_stock(Gtk.STOCK_INDEX, Gtk.IconSize.MENU)
|
2013-04-16 23:51:28 +00:00
|
|
|
hbox.pack_start(label, False, False, 0)
|
2013-04-21 14:52:06 +00:00
|
|
|
hbox.pack_start(icon, False, False, 0)
|
2013-04-16 23:51:28 +00:00
|
|
|
hbox.show_all()
|
|
|
|
|
|
|
|
self.insert_page(vbox, hbox, 0) # Append the new log as a new tab
|
|
|
|
self.show_all()
|
2013-04-16 22:53:24 +00:00
|
|
|
|
|
|
|
return
|
|
|
|
|
2013-09-14 19:15:12 +00:00
|
|
|
def update_summary(self):
|
2013-09-14 18:27:45 +00:00
|
|
|
""" Update the information presented on the summary page. """
|
2013-04-21 23:15:07 +00:00
|
|
|
self.summary["NUMBER_OF_LOGS"].set_label(str(self.get_number_of_logs()))
|
2013-09-30 17:16:06 +00:00
|
|
|
try:
|
|
|
|
t = datetime.fromtimestamp(getmtime(self.path)).strftime("%d %B %Y @ %H:%M")
|
|
|
|
self.summary["DATE_MODIFIED"].set_label(str(t))
|
|
|
|
except (IOError, OSError) as e:
|
|
|
|
logging.exception(e)
|
2013-04-21 23:15:07 +00:00
|
|
|
return
|
|
|
|
|
2013-04-16 22:53:24 +00:00
|
|
|
def _on_switch_page(self, widget, label, new_page):
|
|
|
|
if(new_page == self.get_n_pages()-1): # The last (right-most) tab is the "New Log" tab.
|
|
|
|
self.stop_emission("switch-page")
|
2013-05-09 10:29:55 +00:00
|
|
|
|
|
|
|
# Disable the record buttons if a log page is not selected.
|
2013-04-26 09:10:05 +00:00
|
|
|
if(new_page == 0):
|
2013-08-03 19:11:00 +00:00
|
|
|
self.parent.toolbar.set_record_buttons_sensitive(False)
|
|
|
|
self.parent.menu.set_record_items_sensitive(False)
|
2013-04-26 09:10:05 +00:00
|
|
|
else:
|
2013-08-03 19:11:00 +00:00
|
|
|
self.parent.toolbar.set_record_buttons_sensitive(True)
|
|
|
|
self.parent.menu.set_record_items_sensitive(True)
|
2013-04-16 22:53:24 +00:00
|
|
|
return
|
|
|
|
|
2013-04-14 14:18:46 +00:00
|
|
|
def new_log(self, widget=None):
|
2013-09-14 18:27:45 +00:00
|
|
|
""" Create a new log in the logbook. """
|
2013-04-21 16:47:09 +00:00
|
|
|
if(self.connection is None):
|
|
|
|
return
|
2013-04-16 22:53:24 +00:00
|
|
|
exists = True
|
2013-08-03 19:11:00 +00:00
|
|
|
dialog = LogNameDialog(self.parent)
|
2013-04-16 22:53:24 +00:00
|
|
|
while(exists):
|
|
|
|
response = dialog.run()
|
|
|
|
if(response == Gtk.ResponseType.OK):
|
|
|
|
log_name = dialog.get_log_name()
|
2013-04-21 23:15:07 +00:00
|
|
|
try:
|
2013-09-30 15:56:11 +00:00
|
|
|
with self.connection:
|
|
|
|
c = self.connection.cursor()
|
|
|
|
query = "CREATE TABLE %s (id INTEGER PRIMARY KEY AUTOINCREMENT" % log_name
|
|
|
|
for field_name in AVAILABLE_FIELD_NAMES_ORDERED:
|
|
|
|
s = ", %s TEXT" % field_name.lower()
|
|
|
|
query = query + s
|
|
|
|
query = query + ")"
|
|
|
|
c.execute(query)
|
|
|
|
exists = False
|
2013-04-21 23:15:07 +00:00
|
|
|
except sqlite.Error as e:
|
|
|
|
logging.exception(e)
|
2013-04-16 22:53:24 +00:00
|
|
|
# Data is not valid - inform the user.
|
2013-08-03 19:11:00 +00:00
|
|
|
error(parent=self.parent, message="Database error. Try another log name.")
|
2013-04-21 23:15:07 +00:00
|
|
|
exists = True
|
2013-04-16 22:53:24 +00:00
|
|
|
else:
|
|
|
|
dialog.destroy()
|
|
|
|
return
|
2013-04-15 01:49:16 +00:00
|
|
|
|
2013-04-16 22:53:24 +00:00
|
|
|
dialog.destroy()
|
|
|
|
|
2013-04-21 23:15:07 +00:00
|
|
|
l = Log(self.connection, log_name) # Empty log
|
|
|
|
l.populate()
|
|
|
|
|
|
|
|
self.logs.append(l)
|
2013-09-14 18:27:45 +00:00
|
|
|
self._render_log(self.get_number_of_logs()-1)
|
2013-09-14 19:15:12 +00:00
|
|
|
self.update_summary()
|
2013-04-21 23:15:07 +00:00
|
|
|
|
2013-04-16 23:51:28 +00:00
|
|
|
self.set_current_page(self.get_number_of_logs())
|
2013-03-30 17:56:22 +00:00
|
|
|
return
|
|
|
|
|
2013-05-09 10:29:55 +00:00
|
|
|
def delete_log(self, widget, page=None):
|
2013-09-14 19:15:12 +00:00
|
|
|
""" Delete the log that is currently selected in the logbook. """
|
2013-04-21 16:47:09 +00:00
|
|
|
if(self.connection is None):
|
|
|
|
return
|
2013-05-09 10:29:55 +00:00
|
|
|
|
|
|
|
if(page is None):
|
2013-07-02 02:20:51 +00:00
|
|
|
page_index = self.get_current_page() # Gets the index of the selected tab in the logbook
|
|
|
|
if(page_index == 0): # If we are on the Summary page...
|
|
|
|
logging.debug("No log currently selected!")
|
|
|
|
return
|
|
|
|
else:
|
|
|
|
page = self.get_nth_page(page_index) # Gets the Gtk.VBox of the selected tab in the logbook
|
|
|
|
|
2013-09-14 18:27:45 +00:00
|
|
|
log_index = self._get_log_index(name=page.get_name())
|
2013-05-09 10:29:55 +00:00
|
|
|
log = self.logs[log_index]
|
|
|
|
|
|
|
|
# We also need the page's index in order to remove it using remove_page below.
|
|
|
|
# This may not be the same as what self.get_current_page() returns.
|
|
|
|
page_index = self.page_num(page)
|
|
|
|
|
2013-04-16 23:51:28 +00:00
|
|
|
if(page_index == 0 or page_index == self.get_n_pages()-1): # Only the "New Log" tab is present (i.e. no actual logs in the logbook)
|
2013-04-15 01:37:02 +00:00
|
|
|
logging.debug("No logs to delete!")
|
2013-03-27 20:29:43 +00:00
|
|
|
return
|
2013-03-30 17:56:22 +00:00
|
|
|
|
2013-08-03 19:11:00 +00:00
|
|
|
response = question(parent=self.parent, message="Are you sure you want to delete log %s?" % log.name)
|
2013-04-15 01:37:02 +00:00
|
|
|
if(response == Gtk.ResponseType.YES):
|
2013-09-14 20:33:01 +00:00
|
|
|
try:
|
2013-09-30 15:56:11 +00:00
|
|
|
with self.connection:
|
|
|
|
c = self.connection.cursor()
|
|
|
|
c.execute("DROP TABLE %s" % log.name)
|
2013-09-14 20:33:01 +00:00
|
|
|
except sqlite.Error as e:
|
|
|
|
logging.exception(e)
|
|
|
|
error(parent=self.parent, message="Database error. Could not delete the log.")
|
|
|
|
return
|
2013-04-21 23:15:07 +00:00
|
|
|
|
2013-04-16 23:51:28 +00:00
|
|
|
self.logs.pop(log_index)
|
2013-04-15 01:37:02 +00:00
|
|
|
# Remove the log from the renderers too
|
2013-04-16 23:51:28 +00:00
|
|
|
self.treeview.pop(log_index)
|
|
|
|
self.treeselection.pop(log_index)
|
2013-04-30 02:17:40 +00:00
|
|
|
self.sorter.pop(log_index)
|
2013-05-19 16:05:17 +00:00
|
|
|
self.filter.pop(log_index)
|
2013-04-15 01:37:02 +00:00
|
|
|
# And finally remove the tab in the Logbook
|
2013-04-16 23:51:28 +00:00
|
|
|
self.remove_page(page_index)
|
2013-04-14 14:18:46 +00:00
|
|
|
|
2013-09-14 19:15:12 +00:00
|
|
|
self.update_summary()
|
2013-08-15 20:23:10 +00:00
|
|
|
self.parent.toolbox.awards.count()
|
2013-03-27 20:29:43 +00:00
|
|
|
return
|
|
|
|
|
2013-09-14 19:15:12 +00:00
|
|
|
def filter_logs(self, widget):
|
|
|
|
""" Re-filter all the logs when the user-defined expression is changed. """
|
2013-05-19 15:51:47 +00:00
|
|
|
for i in range(0, len(self.filter)):
|
|
|
|
self.filter[i].refilter()
|
|
|
|
return
|
|
|
|
|
2013-09-14 18:27:45 +00:00
|
|
|
def _filter_by_callsign(self, model, iter, data):
|
2013-09-14 19:15:12 +00:00
|
|
|
""" Filter all the logs in the logbook by the callsign field, based on a user-defined expression. """
|
2013-05-19 15:51:47 +00:00
|
|
|
value = model.get_value(iter, 1)
|
2013-08-03 19:11:00 +00:00
|
|
|
callsign = self.parent.toolbar.filter_source.get_text()
|
2013-05-19 15:51:47 +00:00
|
|
|
|
|
|
|
if(callsign is None or callsign == ""):
|
|
|
|
# If there is nothing to filter with, then show all the records!
|
|
|
|
return True
|
|
|
|
else:
|
2013-09-14 19:15:12 +00:00
|
|
|
# This should be case insensitive.
|
|
|
|
# Also, we could use value[:][0:len(callsign))] if we wanted to match from the very start of each callsign.
|
2013-07-28 15:50:52 +00:00
|
|
|
return callsign.upper() in value or callsign.lower() in value
|
2013-05-19 15:51:47 +00:00
|
|
|
|
2013-09-14 18:27:45 +00:00
|
|
|
def _render_log(self, index):
|
2013-09-14 19:15:12 +00:00
|
|
|
""" Render the Log (identified by 'index') in the Gtk.Notebook. """
|
2013-05-19 15:51:47 +00:00
|
|
|
self.filter.append(self.logs[index].filter_new(root=None))
|
|
|
|
# Set the callsign column as the column we want to filter by
|
2013-09-14 18:27:45 +00:00
|
|
|
self.filter[index].set_visible_func(self._filter_by_callsign, data=None)
|
2013-05-19 15:51:47 +00:00
|
|
|
self.sorter.append(Gtk.TreeModelSort(model=self.filter[index]))
|
2013-04-30 02:17:40 +00:00
|
|
|
self.sorter[index].set_sort_column_id(0, Gtk.SortType.ASCENDING)
|
2013-05-19 15:51:47 +00:00
|
|
|
|
2013-04-30 02:17:40 +00:00
|
|
|
self.treeview.append(Gtk.TreeView(self.sorter[index]))
|
2013-04-16 22:53:24 +00:00
|
|
|
self.treeview[index].set_grid_lines(Gtk.TreeViewGridLines.BOTH)
|
2013-04-16 23:51:28 +00:00
|
|
|
self.treeview[index].connect("row-activated", self.edit_record_callback)
|
2013-04-16 22:53:24 +00:00
|
|
|
self.treeselection.append(self.treeview[index].get_selection())
|
|
|
|
self.treeselection[index].set_mode(Gtk.SelectionMode.SINGLE)
|
2013-03-27 20:29:43 +00:00
|
|
|
# Allow the Log to be scrolled up/down
|
|
|
|
sw = Gtk.ScrolledWindow()
|
|
|
|
sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN)
|
|
|
|
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
|
2013-04-16 22:53:24 +00:00
|
|
|
sw.add(self.treeview[index])
|
2013-03-27 20:29:43 +00:00
|
|
|
vbox = Gtk.VBox()
|
2013-05-09 10:29:55 +00:00
|
|
|
vbox.set_name(self.logs[index].name) # Set a name for the tab itself so we can match it up with the associated Log object later.
|
2013-03-27 20:29:43 +00:00
|
|
|
vbox.pack_start(sw, True, True, 0)
|
2013-04-16 22:53:24 +00:00
|
|
|
|
|
|
|
# Add a close button to the tab
|
|
|
|
hbox = Gtk.HBox(False, 0)
|
|
|
|
label = Gtk.Label(self.logs[index].name)
|
|
|
|
hbox.pack_start(label, False, False, 0)
|
|
|
|
hbox.show_all()
|
|
|
|
|
2013-04-16 23:51:28 +00:00
|
|
|
self.insert_page(vbox, hbox, index+1) # Append the new log as a new tab
|
2013-03-27 20:29:43 +00:00
|
|
|
|
|
|
|
# The first column of the logbook will always be the unique record index.
|
|
|
|
# Let's append this separately to the field names.
|
|
|
|
renderer = Gtk.CellRendererText()
|
|
|
|
column = Gtk.TreeViewColumn("Index", renderer, text=0)
|
|
|
|
column.set_resizable(True)
|
|
|
|
column.set_min_width(50)
|
2013-04-30 02:17:40 +00:00
|
|
|
column.set_clickable(True)
|
|
|
|
column.set_sort_order(Gtk.SortType.ASCENDING)
|
|
|
|
column.set_sort_indicator(True)
|
|
|
|
column.connect("clicked", self.sort_log, 0)
|
2013-04-16 22:53:24 +00:00
|
|
|
self.treeview[index].append_column(column)
|
2013-03-27 20:29:43 +00:00
|
|
|
|
|
|
|
# Set up column names for each selected field
|
2013-06-23 16:31:27 +00:00
|
|
|
field_names = AVAILABLE_FIELD_NAMES_ORDERED
|
2013-03-27 20:29:43 +00:00
|
|
|
for i in range(0, len(field_names)):
|
|
|
|
renderer = Gtk.CellRendererText()
|
2013-06-23 16:31:27 +00:00
|
|
|
column = Gtk.TreeViewColumn(AVAILABLE_FIELD_NAMES_FRIENDLY[field_names[i]], renderer, text=i+1)
|
2013-03-27 20:29:43 +00:00
|
|
|
column.set_resizable(True)
|
|
|
|
column.set_min_width(50)
|
2013-04-30 02:17:40 +00:00
|
|
|
column.set_clickable(True)
|
2014-03-01 18:06:50 +00:00
|
|
|
|
|
|
|
# Special cases
|
2014-02-05 00:53:16 +00:00
|
|
|
if(field_names[i] == "NOTES"):
|
|
|
|
# Give the 'Notes' column some extra space, since this is likely to contain some long sentences...
|
|
|
|
column.set_min_width(300)
|
|
|
|
# ... but don't let it automatically re-size itself.
|
|
|
|
column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
|
|
|
|
|
2013-04-30 02:17:40 +00:00
|
|
|
column.connect("clicked", self.sort_log, i+1)
|
2013-07-01 22:23:43 +00:00
|
|
|
|
|
|
|
config = ConfigParser.ConfigParser()
|
|
|
|
have_config = (config.read(expanduser('~/.pyqso.ini')) != [])
|
2014-03-31 09:44:46 +00:00
|
|
|
(section, option) = ("view", AVAILABLE_FIELD_NAMES_ORDERED[i].lower())
|
|
|
|
if(have_config and config.has_option(section, option)):
|
|
|
|
column.set_visible(config.get(section, option) == "True")
|
2013-04-16 22:53:24 +00:00
|
|
|
self.treeview[index].append_column(column)
|
2013-03-27 20:29:43 +00:00
|
|
|
|
|
|
|
self.show_all()
|
2013-04-16 23:51:28 +00:00
|
|
|
return
|
2013-03-27 20:29:43 +00:00
|
|
|
|
2014-03-01 18:06:50 +00:00
|
|
|
def _compare_date_and_time(self, model, row1, row2, user_data):
|
|
|
|
""" Compares two rows in a Gtk.ListStore, and sorts by both date and time. """
|
|
|
|
date1 = model.get_value(row1, user_data[0])
|
|
|
|
date2 = model.get_value(row2, user_data[0])
|
|
|
|
time1 = model.get_value(row1, user_data[1])
|
|
|
|
time2 = model.get_value(row2, user_data[1])
|
|
|
|
if(date1 < date2):
|
|
|
|
return 1
|
|
|
|
elif(date1 == date2):
|
|
|
|
# If the dates are the same, then let's also sort by time.
|
|
|
|
if(time1 > time2):
|
|
|
|
return -1
|
|
|
|
elif(time1 == time2):
|
|
|
|
return 0
|
|
|
|
else:
|
|
|
|
return 1
|
|
|
|
else:
|
|
|
|
return -1
|
|
|
|
|
|
|
|
def _compare_default(self, model, row1, row2, user_data):
|
|
|
|
""" The default sorting function for all Gtk.ListStore objects. """
|
|
|
|
value1 = model.get_value(row1, user_data)
|
|
|
|
value2 = model.get_value(row2, user_data)
|
|
|
|
if(value1 < value2):
|
|
|
|
return 1
|
|
|
|
elif(value1 == value2):
|
|
|
|
return 0
|
|
|
|
else:
|
|
|
|
return -1
|
|
|
|
|
2013-04-30 02:17:40 +00:00
|
|
|
def sort_log(self, widget, column_index):
|
2013-09-14 19:15:12 +00:00
|
|
|
""" Sort the log (that is currently selected) based on the column identified by column_index. """
|
2013-09-14 18:27:45 +00:00
|
|
|
log_index = self._get_log_index()
|
2013-04-30 02:17:40 +00:00
|
|
|
column = self.treeview[log_index].get_column(column_index)
|
|
|
|
|
2014-03-01 18:06:50 +00:00
|
|
|
if(AVAILABLE_FIELD_NAMES_ORDERED[column_index-1] == "QSO_DATE"):
|
|
|
|
# If the field being sorted is the QSO_DATE, then also sort by the TIME_ON field so we get the
|
|
|
|
# correct chronological order.
|
|
|
|
# Note: This assumes that the TIME_ON field is always immediately to the right of the QSO_DATE field.
|
|
|
|
self.sorter[log_index].set_sort_func(column_index, self._compare_date_and_time, user_data=[column_index, column_index+1])
|
|
|
|
else:
|
|
|
|
self.sorter[log_index].set_sort_func(column_index, self._compare_default, user_data=column_index)
|
|
|
|
|
2013-04-30 02:21:42 +00:00
|
|
|
# If we are operating on the currently-sorted column...
|
2013-04-30 02:17:40 +00:00
|
|
|
if(self.sorter[log_index].get_sort_column_id()[0] == column_index):
|
|
|
|
order = column.get_sort_order()
|
2013-04-30 02:21:42 +00:00
|
|
|
# ...then check if we need to reverse the order of searching.
|
2013-04-30 02:17:40 +00:00
|
|
|
if(order == Gtk.SortType.ASCENDING):
|
|
|
|
self.sorter[log_index].set_sort_column_id(column_index, Gtk.SortType.DESCENDING)
|
|
|
|
column.set_sort_order(Gtk.SortType.DESCENDING)
|
|
|
|
else:
|
|
|
|
self.sorter[log_index].set_sort_column_id(column_index, Gtk.SortType.ASCENDING)
|
|
|
|
column.set_sort_order(Gtk.SortType.ASCENDING)
|
|
|
|
else:
|
|
|
|
# Otherwise, change to the new sorted column. Default to ASCENDING order.
|
|
|
|
self.sorter[log_index].set_sort_column_id(column_index, Gtk.SortType.ASCENDING)
|
|
|
|
column.set_sort_order(Gtk.SortType.ASCENDING)
|
|
|
|
|
|
|
|
# Show an arrow pointing in the direction of the sorting.
|
2013-04-30 02:21:42 +00:00
|
|
|
# (First we need to remove the arrow from the previously-sorted column.
|
|
|
|
# Since we don't know which one that was, just remove the arrow from all columns
|
|
|
|
# and start again. This only loops over a few dozen columns at most, so
|
|
|
|
# hopefully it won't take too much time.)
|
2013-06-23 16:31:27 +00:00
|
|
|
for i in range(0, len(AVAILABLE_FIELD_NAMES_ORDERED)):
|
2013-04-30 02:17:40 +00:00
|
|
|
column = self.treeview[log_index].get_column(i)
|
|
|
|
column.set_sort_indicator(False)
|
|
|
|
column = self.treeview[log_index].get_column(column_index)
|
|
|
|
column.set_sort_indicator(True)
|
|
|
|
return
|
2013-05-10 13:05:33 +00:00
|
|
|
|
|
|
|
def rename_log(self, widget=None):
|
2013-09-14 19:15:12 +00:00
|
|
|
""" Rename the log that is currently selected. """
|
2013-05-10 13:05:33 +00:00
|
|
|
if(self.connection is None):
|
|
|
|
return
|
|
|
|
page_index = self.get_current_page()
|
2013-06-23 22:07:46 +00:00
|
|
|
if(page_index == 0): # If we are on the Summary page...
|
|
|
|
logging.debug("No log currently selected!")
|
|
|
|
return
|
2013-05-10 13:05:33 +00:00
|
|
|
page = self.get_nth_page(page_index) # Gets the Gtk.VBox of the selected tab in the logbook
|
|
|
|
old_log_name = page.get_name()
|
|
|
|
|
2013-09-14 18:27:45 +00:00
|
|
|
log_index = self._get_log_index(name=old_log_name)
|
2013-05-10 13:05:33 +00:00
|
|
|
|
|
|
|
exists = True
|
2013-08-03 19:11:00 +00:00
|
|
|
dialog = LogNameDialog(self.parent, title="Rename Log", name=old_log_name)
|
2013-05-10 13:05:33 +00:00
|
|
|
while(exists):
|
|
|
|
response = dialog.run()
|
|
|
|
if(response == Gtk.ResponseType.OK):
|
|
|
|
new_log_name = dialog.get_log_name()
|
|
|
|
try:
|
2013-09-30 15:56:11 +00:00
|
|
|
with self.connection:
|
|
|
|
c = self.connection.cursor()
|
|
|
|
query = "ALTER TABLE %s RENAME TO %s" % (old_log_name, new_log_name)
|
|
|
|
c.execute(query)
|
|
|
|
exists = False
|
2013-05-10 13:05:33 +00:00
|
|
|
except sqlite.Error as e:
|
|
|
|
logging.exception(e)
|
|
|
|
# Data is not valid - inform the user.
|
2013-08-03 19:11:00 +00:00
|
|
|
error(parent=self.parent, message="Database error. Try another log name.")
|
2013-05-10 13:05:33 +00:00
|
|
|
exists = True
|
|
|
|
else:
|
|
|
|
dialog.destroy()
|
|
|
|
return
|
|
|
|
|
|
|
|
dialog.destroy()
|
|
|
|
|
|
|
|
# Remember to change the Log object's name...
|
|
|
|
self.logs[log_index].name = new_log_name
|
|
|
|
|
|
|
|
# ...and the page's name
|
|
|
|
page.set_name(self.logs[log_index].name)
|
|
|
|
|
|
|
|
# ...and update the tab's label
|
|
|
|
hbox = Gtk.HBox(False, 0)
|
|
|
|
label = Gtk.Label(new_log_name)
|
|
|
|
hbox.pack_start(label, False, False, 0)
|
|
|
|
hbox.show_all()
|
|
|
|
self.set_tab_label(page, hbox)
|
|
|
|
|
|
|
|
# The number of logs will obviously stay the same, but
|
|
|
|
# we want to update the logbook's modification date.
|
2013-09-14 19:15:12 +00:00
|
|
|
self.update_summary()
|
2013-05-10 13:05:33 +00:00
|
|
|
|
|
|
|
return
|
2013-04-14 14:18:46 +00:00
|
|
|
|
2013-04-26 22:53:23 +00:00
|
|
|
def import_log(self, widget=None):
|
2013-09-14 19:15:12 +00:00
|
|
|
""" Import a log from an ADIF file. """
|
2013-04-14 14:18:46 +00:00
|
|
|
dialog = Gtk.FileChooserDialog("Import ADIF Log File",
|
|
|
|
None,
|
|
|
|
Gtk.FileChooserAction.OPEN,
|
|
|
|
(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
|
|
|
|
Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
|
|
|
|
filter = Gtk.FileFilter()
|
2014-03-01 18:39:23 +00:00
|
|
|
filter.set_name("All ADIF files (*.adi)")
|
2013-04-14 14:18:46 +00:00
|
|
|
filter.add_pattern("*.adi")
|
|
|
|
dialog.add_filter(filter)
|
2014-03-01 18:39:23 +00:00
|
|
|
|
|
|
|
filter = Gtk.FileFilter()
|
|
|
|
filter.set_name("All files")
|
|
|
|
filter.add_pattern("*")
|
|
|
|
dialog.add_filter(filter)
|
2013-04-14 14:18:46 +00:00
|
|
|
|
|
|
|
response = dialog.run()
|
|
|
|
if(response == Gtk.ResponseType.OK):
|
|
|
|
path = dialog.get_filename()
|
|
|
|
else:
|
|
|
|
path = None
|
|
|
|
dialog.destroy()
|
|
|
|
|
|
|
|
if(path is None):
|
|
|
|
logging.debug("No file path specified.")
|
|
|
|
return
|
|
|
|
|
2013-08-03 19:11:00 +00:00
|
|
|
dialog = LogNameDialog(self.parent, title="Import Log")
|
2013-07-02 01:47:21 +00:00
|
|
|
while(True):
|
2013-05-16 20:47:17 +00:00
|
|
|
response = dialog.run()
|
|
|
|
if(response == Gtk.ResponseType.OK):
|
2013-07-02 01:47:21 +00:00
|
|
|
log_name = dialog.get_log_name()
|
|
|
|
if(self.log_name_exists(log_name)):
|
|
|
|
# Import into existing log
|
|
|
|
exists = True
|
2013-09-14 18:27:45 +00:00
|
|
|
l = self.logs[self._get_log_index(name=log_name)]
|
2013-08-03 19:11:00 +00:00
|
|
|
response = question(parent=self.parent, message="Are you sure you want to import into an existing log?")
|
2013-07-02 01:47:21 +00:00
|
|
|
if(response == Gtk.ResponseType.YES):
|
|
|
|
break
|
2013-09-14 20:33:01 +00:00
|
|
|
elif(self.log_name_exists(log_name) is None):
|
|
|
|
# Could not determine if the log name exists. It's safer to stop here than to try to add a new log.
|
2013-09-14 21:12:10 +00:00
|
|
|
error(parent=self.parent, message="Database error. Could not check if the log name exists.")
|
2013-09-14 20:33:01 +00:00
|
|
|
dialog.destroy()
|
|
|
|
return
|
2013-07-02 01:47:21 +00:00
|
|
|
else:
|
|
|
|
# Create a new log with the name the user supplies
|
|
|
|
exists = False
|
|
|
|
try:
|
2013-09-30 15:56:11 +00:00
|
|
|
with self.connection:
|
|
|
|
c = self.connection.cursor()
|
|
|
|
query = "CREATE TABLE %s (id INTEGER PRIMARY KEY AUTOINCREMENT" % log_name
|
|
|
|
for field_name in AVAILABLE_FIELD_NAMES_ORDERED:
|
|
|
|
s = ", %s TEXT" % field_name.lower()
|
|
|
|
query = query + s
|
|
|
|
query = query + ")"
|
|
|
|
c.execute(query)
|
|
|
|
l = Log(self.connection, log_name)
|
|
|
|
break
|
2013-07-02 01:47:21 +00:00
|
|
|
except sqlite.Error as e:
|
|
|
|
logging.exception(e)
|
|
|
|
# Data is not valid - inform the user.
|
2013-08-03 19:11:00 +00:00
|
|
|
error(parent=self.parent, message="Database error. Try another log name.")
|
2013-05-16 20:47:17 +00:00
|
|
|
else:
|
|
|
|
dialog.destroy()
|
|
|
|
return
|
2013-04-14 14:18:46 +00:00
|
|
|
|
2013-05-16 20:47:17 +00:00
|
|
|
dialog.destroy()
|
|
|
|
|
2013-04-14 14:18:46 +00:00
|
|
|
adif = ADIF()
|
2013-07-02 01:47:21 +00:00
|
|
|
records = adif.read(path)
|
2013-09-14 20:33:01 +00:00
|
|
|
logging.debug("Importing records from the ADIF file with path: %s" % path)
|
2013-04-26 22:53:23 +00:00
|
|
|
for record in records:
|
|
|
|
l.add_record(record)
|
|
|
|
l.populate()
|
|
|
|
|
2013-07-02 01:47:21 +00:00
|
|
|
if(not exists):
|
|
|
|
self.logs.append(l)
|
2013-09-14 18:27:45 +00:00
|
|
|
self._render_log(self.get_number_of_logs()-1)
|
2013-09-14 19:15:12 +00:00
|
|
|
self.update_summary()
|
2013-08-15 20:23:10 +00:00
|
|
|
self.parent.toolbox.awards.count()
|
2013-04-14 14:18:46 +00:00
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
def export_log(self, widget=None):
|
2013-09-14 19:15:12 +00:00
|
|
|
""" Export the log (that is currently selected) to an ADIF file. """
|
2013-06-30 13:38:08 +00:00
|
|
|
page_index = self.get_current_page() # Gets the index of the selected tab in the logbook
|
|
|
|
if(page_index == 0): # If we are on the Summary page...
|
|
|
|
logging.debug("No log currently selected!")
|
|
|
|
return
|
2013-04-14 14:18:46 +00:00
|
|
|
|
2013-09-14 18:27:45 +00:00
|
|
|
log_index = self._get_log_index()
|
2013-04-26 22:53:23 +00:00
|
|
|
log = self.logs[log_index]
|
2013-04-14 14:18:46 +00:00
|
|
|
|
|
|
|
dialog = Gtk.FileChooserDialog("Export Log to File",
|
|
|
|
None,
|
|
|
|
Gtk.FileChooserAction.SAVE,
|
|
|
|
(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
|
|
|
|
Gtk.STOCK_SAVE, Gtk.ResponseType.OK))
|
2013-06-16 01:09:53 +00:00
|
|
|
dialog.set_do_overwrite_confirmation(True)
|
2014-03-01 18:39:23 +00:00
|
|
|
|
|
|
|
filter = Gtk.FileFilter()
|
|
|
|
filter.set_name("All ADIF files (*.adi)")
|
|
|
|
filter.add_pattern("*.adi")
|
|
|
|
dialog.add_filter(filter)
|
|
|
|
|
|
|
|
filter = Gtk.FileFilter()
|
|
|
|
filter.set_name("All files")
|
|
|
|
filter.add_pattern("*")
|
|
|
|
dialog.add_filter(filter)
|
|
|
|
|
2013-04-14 14:18:46 +00:00
|
|
|
response = dialog.run()
|
|
|
|
if(response == Gtk.ResponseType.OK):
|
|
|
|
path = dialog.get_filename()
|
|
|
|
else:
|
|
|
|
path = None
|
|
|
|
dialog.destroy()
|
|
|
|
|
|
|
|
if(path is None):
|
|
|
|
logging.debug("No file path specified.")
|
2013-06-07 20:56:36 +00:00
|
|
|
else:
|
|
|
|
adif = ADIF()
|
2013-09-15 01:49:12 +00:00
|
|
|
records = log.get_all_records()
|
|
|
|
if(records is not None):
|
|
|
|
adif.write(records, path)
|
|
|
|
else:
|
|
|
|
error(self.parent, "Could not retrieve the records from the SQL database. No records have been exported.")
|
2013-04-14 14:18:46 +00:00
|
|
|
return
|
|
|
|
|
2013-07-10 23:22:27 +00:00
|
|
|
def print_log(self, widget=None):
|
2013-09-14 19:31:12 +00:00
|
|
|
""" Print all the records in the log (that is currently selected).
|
|
|
|
Note that only a few important fields are printed because of the restricted width of the page. """
|
2013-07-10 23:22:27 +00:00
|
|
|
page_index = self.get_current_page() # Gets the index of the selected tab in the logbook
|
|
|
|
if(page_index == 0): # If we are on the Summary page...
|
|
|
|
logging.debug("No log currently selected!")
|
|
|
|
return
|
2013-09-14 18:27:45 +00:00
|
|
|
log_index = self._get_log_index()
|
2013-07-10 23:22:27 +00:00
|
|
|
log = self.logs[log_index]
|
|
|
|
|
2013-07-13 15:43:27 +00:00
|
|
|
self.text_to_print = "Callsign\t---\tDate\t---\tTime\t---\tFrequency\t---\tMode\n"
|
2013-07-10 23:22:27 +00:00
|
|
|
records = log.get_all_records()
|
2013-09-15 01:49:12 +00:00
|
|
|
if(records is not None):
|
|
|
|
for r in records:
|
|
|
|
self.text_to_print += str(r["CALL"]) + "\t---\t" + str(r["QSO_DATE"]) + "\t---\t" + str(r["TIME_ON"]) + "\t---\t" + str(r["FREQ"]) + "\t---\t" + str(r["MODE"]) + "\n"
|
|
|
|
|
|
|
|
action = Gtk.PrintOperationAction.PRINT_DIALOG
|
|
|
|
operation = Gtk.PrintOperation()
|
|
|
|
operation.set_default_page_setup(Gtk.PageSetup())
|
|
|
|
operation.set_unit(Gtk.Unit.MM)
|
|
|
|
|
|
|
|
operation.connect("begin_print", self._begin_print)
|
|
|
|
operation.connect("draw_page", self._draw_page)
|
|
|
|
result = operation.run(action, parent=self.parent)
|
|
|
|
else:
|
|
|
|
error(self.parent, "Could not retrieve the records from the SQL database. No records have been printed.")
|
2013-07-10 23:22:27 +00:00
|
|
|
return
|
|
|
|
|
2013-09-14 18:27:45 +00:00
|
|
|
def _begin_print(self, operation, context):
|
2013-07-10 23:22:27 +00:00
|
|
|
width = context.get_width()
|
|
|
|
height = context.get_height()
|
|
|
|
layout = context.create_pango_layout()
|
|
|
|
layout.set_font_description(Pango.FontDescription("normal 10"))
|
|
|
|
layout.set_width(int(width*Pango.SCALE))
|
|
|
|
layout.set_text(self.text_to_print, -1)
|
|
|
|
|
|
|
|
number_of_pages = 0
|
|
|
|
page_height = 0
|
|
|
|
for line in range(0, layout.get_line_count()):
|
|
|
|
layout_line = layout.get_line(line)
|
|
|
|
ink_rectangle, logical_rectangle = layout_line.get_extents()
|
|
|
|
x_bearing, y_bearing, logical_rectangle_width, logical_rectangle_height = logical_rectangle.x, logical_rectangle.y, logical_rectangle.width, logical_rectangle.height
|
|
|
|
self.line_height = logical_rectangle.height/1024.0 + 3
|
|
|
|
page_height += self.line_height
|
|
|
|
if(page_height + self.line_height > height):
|
|
|
|
number_of_pages += 1
|
|
|
|
page_height = self.line_height
|
|
|
|
operation.set_n_pages(number_of_pages + 1)
|
|
|
|
self.text_to_print = self.text_to_print.split("\n")
|
2013-07-13 19:23:53 +00:00
|
|
|
return
|
2013-07-10 23:22:27 +00:00
|
|
|
|
2013-09-14 18:27:45 +00:00
|
|
|
def _draw_page(self, operation, context, page_number):
|
2013-07-10 23:22:27 +00:00
|
|
|
cr = context.get_cairo_context()
|
|
|
|
cr.set_source_rgb(0, 0, 0)
|
|
|
|
layout = context.create_pango_layout()
|
|
|
|
|
|
|
|
current_line_number = 0
|
|
|
|
for line in self.text_to_print:
|
|
|
|
layout.set_text(line, -1)
|
|
|
|
cr.move_to(5, current_line_number*self.line_height)
|
|
|
|
PangoCairo.update_layout(cr, layout)
|
|
|
|
PangoCairo.show_layout(cr, layout)
|
|
|
|
current_line_number = current_line_number + 1
|
|
|
|
if(current_line_number*self.line_height > context.get_height()):
|
|
|
|
for j in range(0, current_line_number):
|
|
|
|
self.text_to_print.pop(0) # Remove what has been printed already before draw_page is called again
|
|
|
|
break
|
2013-07-13 19:23:53 +00:00
|
|
|
return
|
2013-07-10 23:22:27 +00:00
|
|
|
|
2013-04-16 22:53:24 +00:00
|
|
|
def add_record_callback(self, widget):
|
2013-09-14 18:27:45 +00:00
|
|
|
log_index = self._get_log_index()
|
2013-04-16 23:51:28 +00:00
|
|
|
log = self.logs[log_index]
|
2013-05-10 13:05:33 +00:00
|
|
|
|
2013-08-03 19:11:00 +00:00
|
|
|
dialog = RecordDialog(parent=self.parent, log=log, index=None)
|
2013-03-25 19:43:10 +00:00
|
|
|
all_valid = False # Are all the field entries valid?
|
2013-03-24 19:18:36 +00:00
|
|
|
|
2013-04-27 15:56:02 +00:00
|
|
|
adif = ADIF()
|
2013-03-25 19:43:10 +00:00
|
|
|
while(not all_valid):
|
|
|
|
# This while loop gives the user infinite attempts at giving valid data.
|
|
|
|
# The add/edit record window will stay open until the user gives valid data,
|
|
|
|
# or until the Cancel button is clicked.
|
|
|
|
all_valid = True
|
2013-09-14 19:31:12 +00:00
|
|
|
response = dialog.run()
|
2013-03-25 19:43:10 +00:00
|
|
|
if(response == Gtk.ResponseType.OK):
|
|
|
|
fields_and_data = {}
|
2013-06-23 16:31:27 +00:00
|
|
|
field_names = AVAILABLE_FIELD_NAMES_ORDERED
|
2013-03-25 19:43:10 +00:00
|
|
|
for i in range(0, len(field_names)):
|
|
|
|
#TODO: Validate user input!
|
|
|
|
fields_and_data[field_names[i]] = dialog.get_data(field_names[i])
|
2013-06-23 16:31:27 +00:00
|
|
|
if(not(adif.is_valid(field_names[i], fields_and_data[field_names[i]], AVAILABLE_FIELD_NAMES_TYPES[field_names[i]]))):
|
2013-03-25 19:43:10 +00:00
|
|
|
# Data is not valid - inform the user.
|
2013-08-03 19:11:00 +00:00
|
|
|
error(parent=self.parent, message="The data in field \"%s\" is not valid!" % field_names[i])
|
2013-03-25 19:43:10 +00:00
|
|
|
all_valid = False
|
|
|
|
break # Don't check the other data until the user has fixed the current one.
|
2013-03-24 19:18:36 +00:00
|
|
|
|
2013-03-25 19:43:10 +00:00
|
|
|
if(all_valid):
|
|
|
|
# All data has been validated, so we can go ahead and add the new record.
|
2013-03-27 20:29:43 +00:00
|
|
|
log.add_record(fields_and_data)
|
2013-09-14 19:15:12 +00:00
|
|
|
self.update_summary()
|
2013-08-15 20:23:10 +00:00
|
|
|
self.parent.toolbox.awards.count()
|
2013-03-25 19:43:10 +00:00
|
|
|
# Select the new Record's row in the treeview.
|
2013-09-15 01:49:12 +00:00
|
|
|
number_of_records = log.get_number_of_records()
|
|
|
|
if(number_of_records is not None):
|
|
|
|
self.treeselection[log_index].select_path(number_of_records)
|
2013-03-25 19:43:10 +00:00
|
|
|
|
|
|
|
dialog.destroy()
|
2013-03-22 22:16:31 +00:00
|
|
|
return
|
|
|
|
|
2013-04-16 22:53:24 +00:00
|
|
|
def delete_record_callback(self, widget):
|
2013-09-14 18:27:45 +00:00
|
|
|
log_index = self._get_log_index()
|
2013-08-03 18:00:39 +00:00
|
|
|
log = self.logs[log_index]
|
2013-05-19 17:55:04 +00:00
|
|
|
(sort_model, path) = self.treeselection[log_index].get_selected_rows() # Get the selected row in the log
|
2013-03-25 19:43:10 +00:00
|
|
|
try:
|
2013-08-03 18:00:39 +00:00
|
|
|
sort_iter = sort_model.get_iter(path[0])
|
|
|
|
# Remember that the filter model is a child of the sort model...
|
2013-05-19 17:55:04 +00:00
|
|
|
filter_model = sort_model.get_model()
|
2013-08-03 18:00:39 +00:00
|
|
|
filter_iter = self.sorter[log_index].convert_iter_to_child_iter(sort_iter)
|
|
|
|
# ...and the ListStore model (i.e. the log) is a child of the filter model.
|
2013-05-19 17:55:04 +00:00
|
|
|
child_iter = self.filter[log_index].convert_iter_to_child_iter(filter_iter)
|
2013-08-03 18:00:39 +00:00
|
|
|
row_index = log.get_value(child_iter,0)
|
2013-03-25 19:43:10 +00:00
|
|
|
except IndexError:
|
2013-03-27 22:23:54 +00:00
|
|
|
logging.debug("Trying to delete a record, but there are no records in the log!")
|
2013-03-25 19:43:10 +00:00
|
|
|
return
|
|
|
|
|
2013-08-03 19:11:00 +00:00
|
|
|
response = question(parent=self.parent, message = "Are you sure you want to delete record %d?" % row_index)
|
2013-03-25 19:43:10 +00:00
|
|
|
if(response == Gtk.ResponseType.YES):
|
2013-05-19 17:55:04 +00:00
|
|
|
# Deletes the record with index 'row_index' from the Records list.
|
2013-03-25 19:43:10 +00:00
|
|
|
# 'iter' is needed to remove the record from the ListStore itself.
|
2013-09-16 02:17:37 +00:00
|
|
|
log.delete_record(row_index, iter=child_iter)
|
2013-09-14 19:15:12 +00:00
|
|
|
self.update_summary()
|
2013-08-15 20:23:10 +00:00
|
|
|
self.parent.toolbox.awards.count()
|
2013-03-25 19:43:10 +00:00
|
|
|
return
|
|
|
|
|
2013-04-16 22:53:24 +00:00
|
|
|
def edit_record_callback(self, widget, path, view_column):
|
2013-03-25 19:43:10 +00:00
|
|
|
# Note: the path and view_column arguments need to be passed in
|
|
|
|
# since they associated with the row-activated signal.
|
|
|
|
|
2013-09-14 18:27:45 +00:00
|
|
|
log_index = self._get_log_index()
|
2013-04-16 23:51:28 +00:00
|
|
|
log = self.logs[log_index]
|
2013-03-31 01:10:02 +00:00
|
|
|
|
2013-05-19 17:55:04 +00:00
|
|
|
(sort_model, path) = self.treeselection[log_index].get_selected_rows() # Get the selected row in the log
|
2013-03-25 19:43:10 +00:00
|
|
|
try:
|
2013-08-03 18:00:39 +00:00
|
|
|
sort_iter = sort_model.get_iter(path[0])
|
|
|
|
# Remember that the filter model is a child of the sort model...
|
2013-05-19 17:55:04 +00:00
|
|
|
filter_model = sort_model.get_model()
|
2013-08-03 18:00:39 +00:00
|
|
|
filter_iter = self.sorter[log_index].convert_iter_to_child_iter(sort_iter)
|
|
|
|
# ...and the ListStore model (i.e. the log) is a child of the filter model.
|
2013-05-19 17:55:04 +00:00
|
|
|
child_iter = self.filter[log_index].convert_iter_to_child_iter(filter_iter)
|
2013-08-03 18:00:39 +00:00
|
|
|
row_index = log.get_value(child_iter,0)
|
2013-03-25 19:43:10 +00:00
|
|
|
except IndexError:
|
|
|
|
logging.debug("Could not find the selected row's index!")
|
|
|
|
return
|
|
|
|
|
2013-08-03 19:11:00 +00:00
|
|
|
dialog = RecordDialog(parent=self.parent, log=self.logs[log_index], index=row_index)
|
2013-03-25 19:43:10 +00:00
|
|
|
all_valid = False # Are all the field entries valid?
|
|
|
|
|
2013-04-27 15:56:02 +00:00
|
|
|
adif = ADIF()
|
2013-03-25 19:43:10 +00:00
|
|
|
while(not all_valid):
|
|
|
|
# This while loop gives the user infinite attempts at giving valid data.
|
|
|
|
# The add/edit record window will stay open until the user gives valid data,
|
|
|
|
# or until the Cancel button is clicked.
|
|
|
|
all_valid = True
|
2013-09-14 19:31:12 +00:00
|
|
|
response = dialog.run()
|
2013-03-25 19:43:10 +00:00
|
|
|
if(response == Gtk.ResponseType.OK):
|
|
|
|
fields_and_data = {}
|
2013-06-23 16:31:27 +00:00
|
|
|
field_names = AVAILABLE_FIELD_NAMES_ORDERED
|
2013-03-25 19:43:10 +00:00
|
|
|
for i in range(0, len(field_names)):
|
|
|
|
#TODO: Validate user input!
|
|
|
|
fields_and_data[field_names[i]] = dialog.get_data(field_names[i])
|
2013-06-23 16:31:27 +00:00
|
|
|
if(not(adif.is_valid(field_names[i], fields_and_data[field_names[i]], AVAILABLE_FIELD_NAMES_TYPES[field_names[i]]))):
|
2013-03-25 19:43:10 +00:00
|
|
|
# Data is not valid - inform the user.
|
2013-08-03 19:11:00 +00:00
|
|
|
error(parent=self.parent, message="The data in field \"%s\" is not valid!" % field_names[i])
|
2013-03-25 19:43:10 +00:00
|
|
|
all_valid = False
|
2013-08-15 21:33:59 +00:00
|
|
|
break # Don't check the other fields until the user has fixed the current field's data.
|
2013-03-25 19:43:10 +00:00
|
|
|
|
|
|
|
if(all_valid):
|
2013-08-15 21:33:59 +00:00
|
|
|
# All data has been validated, so we can go ahead and update the record.
|
2013-09-15 01:49:12 +00:00
|
|
|
record = log.get_record_by_index(row_index)
|
|
|
|
if(record is None):
|
|
|
|
message = "Could not retrieve record with row_index %d from the SQL database. The record has not been edited." % row_index
|
|
|
|
logging.error(message)
|
|
|
|
error(parent=self.parent, message=message)
|
|
|
|
else:
|
|
|
|
for i in range(0, len(field_names)):
|
|
|
|
# Check whether the data has actually changed. Database updates can be expensive.
|
|
|
|
if(record[field_names[i].lower()] != fields_and_data[field_names[i]]):
|
2013-09-16 02:17:37 +00:00
|
|
|
# Update the record in the database and then in the ListStore.
|
|
|
|
# We add 1 onto the column_index here because we don't want to consider the index column.
|
|
|
|
log.edit_record(row_index, field_names[i], fields_and_data[field_names[i]], iter=child_iter, column_index=i+1)
|
2013-09-15 01:49:12 +00:00
|
|
|
self.update_summary()
|
|
|
|
self.parent.toolbox.awards.count()
|
2013-03-25 19:43:10 +00:00
|
|
|
|
|
|
|
dialog.destroy()
|
2013-03-22 22:16:31 +00:00
|
|
|
return
|
2013-03-23 22:41:06 +00:00
|
|
|
|
2013-08-15 21:33:59 +00:00
|
|
|
def remove_duplicates_callback(self, widget=None):
|
2013-09-14 18:27:45 +00:00
|
|
|
""" Remove duplicate records in a log.
|
|
|
|
Detecting duplicate records is done based on the CALL, QSO_DATE, TIME_ON, FREQ, and MODE fields. """
|
2013-08-15 21:33:59 +00:00
|
|
|
logging.debug("Removing duplicate records...")
|
|
|
|
|
2013-09-14 18:27:45 +00:00
|
|
|
log_index = self._get_log_index()
|
2013-08-15 21:33:59 +00:00
|
|
|
log = self.logs[log_index]
|
|
|
|
|
2013-09-16 02:17:37 +00:00
|
|
|
(number_of_duplicates, number_of_duplicates_removed) = log.remove_duplicates()
|
|
|
|
info(self.parent, "Found %d duplicate(s). Successfully removed %d duplicate(s)." % (number_of_duplicates, number_of_duplicates_removed))
|
2013-08-15 21:33:59 +00:00
|
|
|
return
|
|
|
|
|
2013-03-27 20:29:43 +00:00
|
|
|
def get_number_of_logs(self):
|
2013-09-14 19:15:12 +00:00
|
|
|
""" Return the total number of logs in the logbook. """
|
2013-03-27 20:29:43 +00:00
|
|
|
return len(self.logs)
|
2013-03-24 00:03:16 +00:00
|
|
|
|
2013-04-16 22:53:24 +00:00
|
|
|
def log_name_exists(self, table_name):
|
2013-09-14 21:12:10 +00:00
|
|
|
""" Return True if the log name already exists in the logbook, and False if it does not already exist. Return None if there is a database error. """
|
2013-09-14 20:33:01 +00:00
|
|
|
try:
|
2013-09-30 15:56:11 +00:00
|
|
|
with self.connection:
|
|
|
|
c = self.connection.cursor()
|
|
|
|
c.execute("SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE name=?)", [table_name])
|
|
|
|
exists = c.fetchone()
|
2013-04-21 23:15:07 +00:00
|
|
|
if(exists[0] == 1):
|
2013-04-16 22:53:24 +00:00
|
|
|
return True
|
2013-09-14 20:33:01 +00:00
|
|
|
else:
|
|
|
|
return False
|
2013-09-30 15:56:11 +00:00
|
|
|
except (sqlite.Error, IndexError) as e:
|
2013-09-14 21:12:10 +00:00
|
|
|
logging.exception(e) # Database error. PyQSO could not check if the log name exists.
|
2013-09-14 20:33:01 +00:00
|
|
|
return None
|
2013-06-08 15:36:27 +00:00
|
|
|
|
2013-09-14 18:27:45 +00:00
|
|
|
def _get_log_index(self, name=None):
|
2013-09-14 19:15:12 +00:00
|
|
|
""" Given the name of a log, return its index in the self.log list. """
|
2013-06-08 15:36:27 +00:00
|
|
|
if(name is None):
|
|
|
|
# If no page name is supplied, then just use the currently selected page
|
|
|
|
page_index = self.get_current_page() # Gets the index of the selected tab in the logbook
|
|
|
|
if(page_index == 0 or page_index == self.get_n_pages()-1):
|
|
|
|
# We either have the Summary page, or the "+" (add log) dummy page.
|
|
|
|
logging.debug("No log currently selected!")
|
|
|
|
return None
|
|
|
|
name = self.get_nth_page(page_index).get_name()
|
|
|
|
# If a page of the logbook (and therefore a Log object) gets deleted,
|
|
|
|
# then the page_index may not correspond to the index of the log in the self.logs list.
|
|
|
|
# Therefore, we have to search for the tab with the same name as the log.
|
2013-05-10 13:05:33 +00:00
|
|
|
for i in range(0, len(self.logs)):
|
|
|
|
if(self.logs[i].name == name):
|
|
|
|
log_index = i
|
|
|
|
break
|
|
|
|
return log_index
|
2013-04-16 22:53:24 +00:00
|
|
|
|