[gnome-applets] invest applet: introduces index value expansion and tree organization of quotes - indices like the N



commit bc1169f981551fa96417a03fea08ea2b8099907a
Author: Enrico Minack <enrico-minack gmx de>
Date:   Mon Jul 11 21:35:30 2011 +0200

    invest applet: introduces index value expansion and tree organization of quotes
    - indices like the NASDAQ can automatically be expanded to its contained stocks
    - stocks can be arranged into groups and sub-groups, allowing tree organization

 invest-applet/data/prefs-dialog.ui  |   59 ++++++-
 invest-applet/invest/__init__.py    |   34 +++-
 invest-applet/invest/applet.py      |    9 +
 invest-applet/invest/preferences.py |  184 ++++++++++++++----
 invest-applet/invest/quotes.py      |  361 ++++++++++++++++++++++++++---------
 invest-applet/invest/widgets.py     |   35 +++-
 6 files changed, 534 insertions(+), 148 deletions(-)
---
diff --git a/invest-applet/data/prefs-dialog.ui b/invest-applet/data/prefs-dialog.ui
index fed2e2a..ae3c5c7 100644
--- a/invest-applet/data/prefs-dialog.ui
+++ b/invest-applet/data/prefs-dialog.ui
@@ -128,6 +128,7 @@
                                 <property name="can_focus">True</property>
                                 <property name="reorderable">True</property>
                                 <property name="rules_hint">True</property>
+                                <property name="enable_tree_lines">True</property>
                                 <child internal-child="selection">
                                   <object class="GtkTreeSelection" id="treeview-selection1"/>
                                 </child>
@@ -147,7 +148,7 @@
                             <property name="spacing">6</property>
                             <property name="homogeneous">True</property>
                             <child>
-                              <object class="GtkButton" id="add">
+                              <object class="GtkButton" id="addstock">
                                 <property name="label">gtk-add</property>
                                 <property name="visible">True</property>
                                 <property name="can_focus">True</property>
@@ -162,6 +163,20 @@
                               </packing>
                             </child>
                             <child>
+                              <object class="GtkButton" id="addgroup">
+                                <property name="label" translatable="yes" context=" " comments="Instead of adding a single stock to the list of stocks, the 'Add Group' button adds a group (kind of a sub folder) to which numerous stocks can be added. A group here refers to a group of stocks.">Add Group</property>
+                                <property name="visible">True</property>
+                                <property name="can_focus">True</property>
+                                <property name="receives_default">True</property>
+                                <property name="use_action_appearance">False</property>
+                              </object>
+                              <packing>
+                                <property name="expand">True</property>
+                                <property name="fill">True</property>
+                                <property name="position">1</property>
+                              </packing>
+                            </child>
+                            <child>
                               <object class="GtkButton" id="remove">
                                 <property name="label">gtk-remove</property>
                                 <property name="visible">True</property>
@@ -173,7 +188,7 @@
                               <packing>
                                 <property name="expand">True</property>
                                 <property name="fill">True</property>
-                                <property name="position">1</property>
+                                <property name="position">2</property>
                               </packing>
                             </child>
                           </object>
@@ -184,6 +199,43 @@
                           </packing>
                         </child>
                         <child>
+                          <object class="GtkCheckButton" id="indexexpansion">
+                            <property name="label" translatable="yes" comments="An index value (for instance the NASDAQ Composite) is based on a number of stocks. This option allows to also show the quotes of the stocks an index is based on. ">Show stocks of index values</property>
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="receives_default">False</property>
+                            <property name="has_tooltip">True</property>
+                            <property name="tooltip_markup" translatable="yes">An index value, for instance the &lt;i&gt;NASDAQ Composite&lt;/i&gt; (^IXIC), is based on a number of stocks. This option allows to also show the quotes of the &lt;i&gt;&lt;b&gt;stocks&lt;/b&gt;&lt;/i&gt; an index is based on.</property>
+                            <property name="use_action_appearance">False</property>
+                            <property name="xalign">0</property>
+                            <property name="active">True</property>
+                            <property name="draw_indicator">True</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">2</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <object class="GtkCheckButton" id="hidecharts">
+                            <property name="label" translatable="yes">Hide charts in quotes list</property>
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="receives_default">False</property>
+                            <property name="tooltip_text" translatable="yes">A small chart image is shown next to each quote. The retrieval of each chart image causes network traffic. Hiding charts reduces the network bandwidth demand significantly.</property>
+                            <property name="use_action_appearance">False</property>
+                            <property name="relief">none</property>
+                            <property name="xalign">0</property>
+                            <property name="draw_indicator">True</property>
+                          </object>
+                          <packing>
+                            <property name="expand">False</property>
+                            <property name="fill">True</property>
+                            <property name="position">3</property>
+                          </packing>
+                        </child>
+                        <child>
                           <object class="GtkLabel" id="default_info">
                             <property name="visible">True</property>
                             <property name="can_focus">False</property>
@@ -194,7 +246,7 @@
                           <packing>
                             <property name="expand">False</property>
                             <property name="fill">False</property>
-                            <property name="position">2</property>
+                            <property name="position">4</property>
                           </packing>
                         </child>
                       </object>
@@ -233,6 +285,7 @@
                       <object class="GtkEntry" id="currency">
                         <property name="visible">True</property>
                         <property name="can_focus">True</property>
+                        <property name="tooltip_text" translatable="yes">Type the target currency to which all stock quotes will be converted to.</property>
                       </object>
                     </child>
                   </object>
diff --git a/invest-applet/invest/__init__.py b/invest-applet/invest/__init__.py
index 014d42d..986284d 100644
--- a/invest-applet/invest/__init__.py
+++ b/invest-applet/invest/__init__.py
@@ -122,22 +122,38 @@ def update_to_exchange_stock_format(stocks):
 
 	return stocks
 
+# converts the given stocks from the dict format into a list
+def update_to_list_stock_format(stocks):
+	new = []
+
+	for ticker, stock in stocks.items():
+		stock['ticker'] = ticker
+		new.append(stock)
+
+	return new
+
 STOCKS_FILE = join(USER_INVEST_DIR, "stocks.pickle")
 
 try:
 	STOCKS = cPickle.load(file(STOCKS_FILE))
 
-	# if the stocks file is in the stocks format without labels,
-	# then we need to convert it into the new labeled format
-	if labelless_stock_format(STOCKS):
-		STOCKS = update_to_labeled_stock_format(STOCKS);
+	# if the stocks file contains a list, the subsequent tests are obsolete
+	if type(STOCKS) != list:
+		# if the stocks file is in the stocks format without labels,
+		# then we need to convert it into the new labeled format
+		if labelless_stock_format(STOCKS):
+			STOCKS = update_to_labeled_stock_format(STOCKS)
+
+		# if the stocks file does not contain exchange rates, add them
+		if exchangeless_stock_format(STOCKS):
+			STOCKS = update_to_exchange_stock_format(STOCKS)
+
+		# here, stocks is a most up-to-date dict, but we need it to be a list
+		STOCKS = update_to_list_stock_format(STOCKS)
 
-	# if the stocks file does not contain exchange rates, add them
-	if exchangeless_stock_format(STOCKS):
-		STOCKS = update_to_exchange_stock_format(STOCKS);
 except Exception, msg:
 	error("Could not load the stocks from %s: %s" % (STOCKS_FILE, msg) )
-	STOCKS = {}
+	STOCKS = []
 
 #STOCKS = {
 #	"AAPL": {
@@ -164,7 +180,9 @@ except Exception, msg:
 	error("Could not load the configuration from %s: %s" % (CONFIG_FILE, msg) )
 	CONFIG = {}       # default configuration
 
+CURRENCIES_FILE = join(USER_INVEST_DIR, "currencies.csv")
 QUOTES_FILE = join(USER_INVEST_DIR, "quotes.csv")
+INDEX_QUOTES_FILE_TEMPLATE = "quotes.#.csv"
 
 
 # set default proxy config
diff --git a/invest-applet/invest/applet.py b/invest-applet/invest/applet.py
index a4adff2..ac013c2 100644
--- a/invest-applet/invest/applet.py
+++ b/invest-applet/invest/applet.py
@@ -99,6 +99,9 @@ class InvestmentsListWindow(Gtk.Window):
 	def __init__(self, applet, list):
 		Gtk.Window.__init__(self, type=Gtk.WindowType.TOPLEVEL)
 		self.list = list
+		self.list.connect('row-collapsed', self.on_collapsed)
+		self.list.connect('row-expanded', self.on_expanded)
+
 		self.set_type_hint(Gdk.WindowTypeHint.DOCK)
 		self.stick()
 		self.set_border_width(WidgetBorderWidth)
@@ -124,6 +127,12 @@ class InvestmentsListWindow(Gtk.Window):
 			self.hide()
 			self.hidden = True
 
+	def on_collapsed(self, treeview, iter, path):
+		self.update_position()
+
+	def on_expanded(self, treeview, iter, path):
+		self.update_position()
+
 	def update_position (self):
 		"""
 		Calculates the position and moves the window to it.
diff --git a/invest-applet/invest/preferences.py b/invest-applet/invest/preferences.py
index 0aadc63..caff3c4 100644
--- a/invest-applet/invest/preferences.py
+++ b/invest-applet/invest/preferences.py
@@ -16,9 +16,22 @@ class PrefsDialog:
 		self.currency = self.ui.get_object("currency")
 		self.currency_code = None
 		self.currencies = currencies.Currencies.currencies
+		self.indexexpansion = self.ui.get_object("indexexpansion")
+		if invest.CONFIG.has_key('indexexpansion'):
+			self.indexexpansion.set_active(invest.CONFIG['indexexpansion'])
+		else:
+			self.indexexpansion.set_active(False)
+
+		self.hidecharts = self.ui.get_object("hidecharts")
+		if invest.CONFIG.has_key('hidecharts'):
+			self.hidecharts.set_active(invest.CONFIG['hidecharts'])
+		else:
+			self.hidecharts.set_active(False)
 
-		self.ui.get_object("add").connect('clicked', self.on_add_stock)
-		self.ui.get_object("add").connect('activate', self.on_add_stock)
+		self.ui.get_object("addstock").connect('clicked', self.on_add, False)
+		self.ui.get_object("addstock").connect('activate', self.on_add, False)
+		self.ui.get_object("addgroup").connect('clicked', self.on_add, True)
+		self.ui.get_object("addgroup").connect('activate', self.on_add, True)
 		self.ui.get_object("remove").connect('clicked', self.on_remove_stock)
 		self.ui.get_object("remove").connect('activate', self.on_remove_stock)
 		self.ui.get_object("help").connect('clicked', self.on_help)
@@ -29,7 +42,7 @@ class PrefsDialog:
 
 		self.typs = (str, str, float, float, float, float)
 		self.names = (_("Symbol"), _("Label"), _("Amount"), _("Price"), _("Commission"), _("Currency Rate"))
-		store = Gtk.ListStore(*self.typs)
+		store = Gtk.TreeStore(*self.typs)
 		store.set_sort_column_id(0, Gtk.SortType.ASCENDING)
 		self.treeview.set_model(store)
 		self.model = store
@@ -56,23 +69,22 @@ class PrefsDialog:
 		if self.currency_code != None:
 			self.add_exchange_column()
 
-		stock_items = invest.STOCKS.items ()
-		stock_items.sort ()
-		for key, data in stock_items:
-			label = data["label"]
-			purchases = data["purchases"]
-			for purchase in purchases:
-				if purchase.has_key("exchange"):
-					exchange =  purchase["exchange"]
-				else:
-					exchange = 0.0
-				store.append([key, label, float(purchase["amount"]), float(purchase["bought"]), float(purchase["comission"]), float(exchange)])
+		self.add_to_store(store, None, invest.STOCKS)
 
 		self.sync_ui()
 
+	def is_group(self, iter):
+		return self.model[iter][1] == None
+
+	def is_stock(self, iter):
+		return not self.is_group(iter)
+
 	def on_cell_edited(self, cell, path, new_text, col, typ):
+		if col != 0 and self.is_group(self.model.get_iter(path)):
+			return
+
 		try:
-			if col == 0:    # stock symbols must be uppercase
+			if col == 0 and self.is_stock(self.model.get_iter(path)):    # stock symbols must be uppercase
 				new_text = str.upper(new_text)
 			if col < 2:
 				self.model[path][col] = new_text
@@ -88,16 +100,24 @@ class PrefsDialog:
 
 	def get_cell_data(self, column, cell, model, iter, data):
 		typ, col = data
+		if self.is_group(iter):
+			if col == 0:
+				val = model[iter][col]
+				cell.set_property('markup', "<b>%s</b>" % typ(val))
+			else:
+				cell.set_property('text', "")
+			return
+
+		val = model[iter][col]
 		if typ == int:
-			cell.set_property('text', "%d" % typ(model[iter][col]))
+			cell.set_property('text', "%d" % typ(val))
 		elif typ == float:
 			# provide float numbers with at least 2 fractional digits
-			val = model[iter][col]
 			digits = self.fraction_digits(val)
 			fmt = "%%.%df" % max(digits, 2)
 			cell.set_property('text', self.format(fmt, val))
 		else:
-			cell.set_property('text', typ(model[iter][col]))
+			cell.set_property('text', typ(val))
 
 	# determine the number of non zero digits in the fraction of the value
 	def fraction_digits(self, value):
@@ -114,10 +134,8 @@ class PrefsDialog:
 		cell_description.connect("edited", self.on_cell_edited, column, typ)
 		column_description = Gtk.TreeViewColumn (name, cell_description)
 		if typ == str:
-			column_description.add_attribute(cell_description, "text", column)
 			column_description.set_sort_column_id(column)
-		if typ == float:
-			column_description.set_cell_data_func(cell_description, self.get_cell_data, (float, column))
+		column_description.set_cell_data_func(cell_description, self.get_cell_data, (typ, column))
 		view.append_column(column_description)
 
 	def add_exchange_column(self):
@@ -138,49 +156,133 @@ class PrefsDialog:
 			pass
 		self.dialog.destroy()
 
-		invest.STOCKS = {}
-
-		def save_symbol(model, path, iter, data):
-			#if int(model[iter][1]) == 0 or float(model[iter][2]) < 0.0001:
-			#	return
-
-			if not model[iter][0] in invest.STOCKS:
-				invest.STOCKS[model[iter][0]] = { 'label': model[iter][1], 'purchases': [] }
-				
-			invest.STOCKS[model[iter][0]]["purchases"].append({
-				"amount": float(model[iter][2]),
-				"bought": float(model[iter][3]),
-				"comission": float(model[iter][4]),
-				"exchange": float(model[iter][5])
-			})
-		self.model.foreach(save_symbol, None)
+
+		# transform the stocks treestore into the STOCKS list
+		invest.STOCKS = self.to_list(self.model.get_iter_first())
+
+		# store the STOCKS into the pickles file
 		try:
 			cPickle.dump(invest.STOCKS, file(invest.STOCKS_FILE, 'w'))
 			invest.debug('Stocks written to file')
 		except Exception, msg:
 			invest.error('Could not save stocks file: %s' % msg)
 
+		# store the CONFIG (currency, index expansion) into the config file
 		invest.CONFIG = {}
 		if self.currency_code != None and len(self.currency_code) == 3:
 			invest.CONFIG['currency'] = self.currency_code
+		invest.CONFIG['indexexpansion'] = self.indexexpansion.get_active()
+		invest.CONFIG['hidecharts'] = self.hidecharts.get_active()
 		try:
 			cPickle.dump(invest.CONFIG, file(invest.CONFIG_FILE, 'w'))
 			invest.debug('Configuration written to file')
 		except Exception, msg:
 			invest.debug('Could not save configuration file: %s' % msg)
 
+
+	def to_list(self, iter):
+		stocks = {}
+		groups = []
+
+		while iter != None:
+			if self.is_stock(iter):
+				ticker = self.model[iter][0]
+				if not ticker in stocks:
+					stocks[ticker] = {
+								'ticker': ticker,
+								'label': self.model[iter][1],
+								'purchases': []
+							 }
+				stocks[ticker]["purchases"].append({
+					"amount": float(self.model[iter][2]),
+					"bought": float(self.model[iter][3]),
+					"comission": float(self.model[iter][4]),
+					"exchange": float(self.model[iter][5])
+				})
+
+			else:
+				name = self.model[iter][0]
+				list = self.to_list(self.model.iter_children(iter))
+				groups.append({ 'name': name, 'list': list })
+
+			iter = self.model.iter_next(iter)
+
+		groups.extend(stocks.values())
+		return groups
+
+	def add_to_store(self, store, parent, stocks):
+		for stock in stocks:
+			if not stock.has_key('ticker'):
+				name = stock['name']
+				list = stock['list']
+				row = store.append(parent, [name, None, None, None, None, None])
+				self.add_to_store(store, row, list)
+			else:
+				ticker = stock['ticker']
+				label = stock["label"]
+				purchases = stock["purchases"]
+				for purchase in purchases:
+					if purchase.has_key("exchange"):
+						exchange =  purchase["exchange"]
+					else:
+						exchange = 0.0
+					store.append(parent, [ticker, label, float(purchase["amount"]), float(purchase["bought"]), float(purchase["comission"]), float(exchange)])
+
 	def sync_ui(self):
 		pass
 
-	def on_add_stock(self, w):
-		iter = self.model.append(["GOOG", "Google Inc.", 0.0, 0.0, 0.0, 0.0])
-		path = self.model.get_path(iter)
+	def on_add(self, w, group = False):
+		# get the selected row
+		(model, parent) = self.treeview.get_selection().get_selected()
+
+		# if the selected row is a stock, move to its parent
+		while parent != None and self.is_stock(parent):
+			parent = model.iter_parent(parent)
+
+		# add a new row as child of the selected row
+		if group:
+			iter = self.model.append(parent, [_("Stock Group"), None, None, None, None, None])
+			# select the group and add a stock
+			path = self.model.get_path(iter)
+			self.treeview.set_cursor(path, None, False)
+			self.on_add(w, False)
+			# make sure the new group is elapsed
+			self.treeview.expand_row(path, False)
+			# select the new group for editing
+		else:
+			iter = self.model.append(parent, ["YHOO", "Yahoo! Inc.", 0.0, 0.0, 0.0, 0.0])
+			# make sure the new stock's parent is elapsed
+			if parent != None:
+				path = self.model.get_path(parent)
+				self.treeview.expand_row(path, False)
+			# select the new row for editing
+			path = self.model.get_path(iter)
 		self.treeview.set_cursor(path, self.treeview.get_column(0), True)
 
+
 	def on_remove_stock(self, w):
 		model, paths = self.treeview.get_selection().get_selected_rows()
 		for path in paths:
-			model.remove(model.get_iter(path))
+			iter = model.get_iter(path)
+			if model.iter_n_children(iter) > 0:
+				# translators: Asks the user to confirm deletion of a group of stocks
+				dialog = Gtk.Dialog(_("Delete entire stock group?"),
+						None,
+						Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT,
+						(Gtk.STOCK_CANCEL, Gtk.ResponseType.REJECT,
+						 Gtk.STOCK_OK, Gtk.ResponseType.ACCEPT))
+				# translators: stocks can be grouped together into a "stock group".
+				# The user wants to delete a group, but the group still contains stocks.
+				# By deleting the group, also all stocks will be removed from configuration
+				label = Gtk.Label(_("This removes all stocks contained in this stock group!\nDo you really want to remove this stock group?"))
+				dialog.get_content_area().pack_start(label, True, True, 10)
+				label.show()
+				response = dialog.run()
+				dialog.destroy()
+				if response == Gtk.ResponseType.REJECT:
+					continue
+
+			model.remove(iter)
 
 	def on_help(self, w):
 		invest.help.show_help_section("invest-applet-usage")
diff --git a/invest-applet/invest/quotes.py b/invest-applet/invest/quotes.py
index 38eed6a..767b6ea 100644
--- a/invest-applet/invest/quotes.py
+++ b/invest-applet/invest/quotes.py
@@ -6,10 +6,13 @@ import locale
 from urllib import urlopen
 import datetime
 from threading import Thread
-
+from os import listdir, unlink
+import re
 import invest, invest.about, invest.chart
 import currencies
 
+# TODO: start currency retrieval after _all_ index expansion completed !!!
+
 CHUNK_SIZE = 512*1024 # 512 kB
 AUTOREFRESH_TIMEOUT = 15*60*1000 # 15 minutes
 
@@ -51,11 +54,10 @@ class QuotesRetriever(Thread, _IdleObject):
 		self.retrieved = False
 		self.data = []
 		self.currencies = []
-		invest.debug("QuotesRetriever created");
 
 	def run(self):
-		invest.debug("QuotesRetriever started");
 		quotes_url = QUOTES_URL % {"s": self.tickers}
+		invest.debug("QuotesRetriever started: %s" % quotes_url);
 		try:
 			quotes_file = urlopen(quotes_url, proxies = invest.PROXY)
 			self.data = quotes_file.read ()
@@ -67,26 +69,28 @@ class QuotesRetriever(Thread, _IdleObject):
 		self.emit("completed")
 
 
-class QuoteUpdater(Gtk.ListStore):
+class QuoteUpdater(Gtk.TreeStore):
 	updated = False
 	last_updated = None
 	quotes_valid = False
 	timeout_id = None
 	SYMBOL, LABEL, CURRENCY, TICKER_ONLY, BALANCE, BALANCE_PCT, VALUE, VARIATION_PCT, PB = range(9)
 	def __init__ (self, change_icon_callback, set_tooltip_callback):
-		Gtk.ListStore.__init__ (self, GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING, bool, float, float, float, float, GdkPixbuf.Pixbuf)
+		Gtk.TreeStore.__init__ (self, GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING, bool, float, float, float, float, GdkPixbuf.Pixbuf)
 		self.set_update_interval(AUTOREFRESH_TIMEOUT)
 		self.change_icon_callback = change_icon_callback
 		self.set_tooltip_callback = set_tooltip_callback
 		self.set_sort_column_id(1, Gtk.SortType.ASCENDING)
-		self.load()	# read the last cached quotes file
-		self.refresh()	# download a new quotes file, this may fail if disconnected
+		self.load_quotes()		# read the last cached quotes file
+		self.load_all_index_quotes()	# expand indices if requested
+		self.load_currencies()		# convert currencies from cached file
+		self.refresh()			# download a new quotes file, this may fail if disconnected
 
 		# tell the network manager to notify me when network status changes
 		invest.nm.set_statechange_callback(self.nm_state_changed)
 
 	# loads the cached csv file and its last-modification-time as self.last_updated
-	def load(self):
+	def load_quotes(self):
 		invest.debug("Loading quotes");
 		try:
 			f = open(invest.QUOTES_FILE, 'r')
@@ -101,7 +105,7 @@ class QuoteUpdater(Gtk.ListStore):
 			invest.error("Could not load the cached quotes file %s: %s" % (invest.QUOTES_FILE, msg) )
 
 	# stores the csv content on disk so it can be used on next start up
-	def save(self, data):
+	def save_quotes(self, data):
 		invest.debug("Storing quotes")
 		try:
 			f = open(invest.QUOTES_FILE, 'w')
@@ -110,6 +114,95 @@ class QuoteUpdater(Gtk.ListStore):
 		except Exception, msg:
 			invest.error("Could not save the retrieved quotes file to %s: %s" % (invest.QUOTES_FILE, msg) )
 
+
+
+	def expand_indices(self):
+		if not ( invest.CONFIG.has_key('indexexpansion') and invest.CONFIG['indexexpansion'] ):
+			# retrieve currencies immediately
+			self.retrieve_currencies()
+			return
+
+		# trigger retrieval for each index
+		for index in self.get_indices(invest.STOCKS):
+			quotes_retriever = QuotesRetriever("@%s" % index)
+			quotes_retriever.connect("completed", self.on_index_quote_retriever_completed, index)
+			quotes_retriever.start()
+
+	def on_index_quote_retriever_completed(self, retriever, index):
+		if retriever.retrieved == False:
+			invest.error("Failed to retrieve quotes for index %s!" % index)
+		else:
+			self.save_index_quotes(index, retriever.data)
+			self.load_index_quotes(index)
+			self.retrieve_currencies()
+
+	def save_index_quotes(self, index, data):
+		# store the index quotes
+		invest.debug("Storing quotes of index %s" % index)
+		try:
+			filename = invest.INDEX_QUOTES_FILE_TEMPLATE.replace('#', index)
+			filename = join(invest.USER_INVEST_DIR, filename)
+			f = open(filename, 'w')
+			f.write(data)
+			f.close()
+		except Exception, msg:
+			invest.error("Could not save the retrieved index quotes file of %s to %s: %s" % (index, filename, msg) )
+			return
+
+	def load_index_quotes(self, index):
+		# load the file
+		try:
+			filename = invest.INDEX_QUOTES_FILE_TEMPLATE.replace('#', index)
+			filename = join(invest.USER_INVEST_DIR, filename)
+			f = open(filename, 'r')
+			data = f.readlines()
+			f.close()
+		except Exception, msg:
+			invest.error("Could not load index quotes file %s of index %s: %s" % (filename, index, msg) )
+			return
+
+		# expand the index
+		self.expand_index(index, data)
+
+	def load_all_index_quotes(self):
+		if not ( invest.CONFIG.has_key('indexexpansion') and invest.CONFIG['indexexpansion'] ):
+			return
+
+		# load all existing index quotes files
+		files = listdir(invest.USER_INVEST_DIR)
+		for file in files:
+			# is this a index quote file?
+			m = re.match(invest.INDEX_QUOTES_FILE_TEMPLATE.replace('#', '([^.]+)'), file)
+			if m:
+				index = m.group(1)
+				filename = join(invest.USER_INVEST_DIR, file)
+
+				# load the file
+				f = open(filename, 'r')
+				data = f.readlines()
+				f.close()
+
+				# expand respective indices
+				if not self.expand_index(index, data):
+					# delete index file because the index is not used anymore
+					unlink(filename)
+
+	def expand_index(self, index, data):
+		invest.debug("Expanding index %s" % index)
+
+		quotes = self.parse_yahoo_csv(csv.reader(data))
+
+		nodes = self.find_stock(index)
+		for node in nodes:
+			for ticker in quotes.keys():
+				quote = self.get_quote(quotes, ticker)
+				row = self.insert(self.get_iter(node), 0, [ticker, quote["label"], quote["currency"], True, 0.0, 0.0, float(quote["trade"]), float(quote["variation_pct"]), None])
+				self.retrieve_image(ticker, row)
+
+		# indicate if index was found
+		return len(nodes) > 0
+
+
 	def set_update_interval(self, interval):
 		if self.timeout_id != None:
 			invest.debug("Canceling refresh timer")
@@ -139,16 +232,43 @@ class QuoteUpdater(Gtk.ListStore):
 			invest.debug("No stocks configured")
 			return True
 
-		tickers = '+'.join(invest.STOCKS.keys())
-		invest.debug("creating QuotesRetriever")
+		tickers = '+'.join( self.get_tickers(invest.STOCKS) )
 		quotes_retriever = QuotesRetriever(tickers)
 		quotes_retriever.connect("completed", self.on_retriever_completed)
-		invest.debug("starting QuotesRetriever")
 		quotes_retriever.start()
-		invest.debug("started QuotesRetriever")
 
 		return True
 
+	def get_tickers(self, stocks):
+		tickers = []
+		for stock in stocks:
+			if stock.has_key('ticker'):
+				ticker = stock['ticker']
+				tickers.append(ticker)
+			else:
+				tickers.extend(self.get_tickers(stock['list']))
+		return tickers
+
+	def get_indices(self, stocks):
+		indices = []
+		for stock in stocks:
+			if stock.has_key('ticker'):
+				ticker = stock['ticker']
+				if ticker.startswith('^'):
+					indices.append(ticker)
+			else:
+				indices.extend(self.get_indices(stock['list']))
+		return indices
+
+	def find_stock(self, symbol):
+		list = []
+		self.foreach(self.find_stock_cb, (symbol, list))
+		return list
+
+	def find_stock_cb(self, model, path, iter, data):
+		(symbol, list) = data
+		if model[path][0] == symbol:
+			list.append(str(path))
 
 	# locale-aware formatting of the percent float (decimal point, thousand grouping point) with 2 decimal digits
 	def format_percent(self, value):
@@ -160,22 +280,46 @@ class QuoteUpdater(Gtk.ListStore):
 
 	def on_retriever_completed(self, retriever):
 		if retriever.retrieved == False:
-			invest.debug("QuotesRetriever failed");
+			invest.debug("QuotesRetriever failed for tickers '%s'" % retriever.tickers);
 			self.update_tooltip(_('Invest could not connect to Yahoo! Finance'))
 		else:
 			invest.debug("QuotesRetriever completed");
 			# cache the retrieved csv file
-			self.save(retriever.data)
+			self.save_quotes(retriever.data)
 			# load the cache and parse it
-			self.load()
+			self.load_quotes()
+			# expand index values if requested
+			self.expand_indices()
+
 
 	def on_currency_retriever_completed(self, retriever):
 		if retriever.retrieved == False:
 			invest.error("Failed to retrieve currency rates!")
 		else:
-			self.convert_currencies(self.parse_yahoo_csv(csv.reader(retriever.data)))
+			self.save_currencies(retriever.data)
+			self.load_currencies()
 		self.update_tooltip()
 
+	def save_currencies(self, data):
+		invest.debug("Storing currencies to %s" % invest.CURRENCIES_FILE)
+		try:
+			f = open(invest.CURRENCIES_FILE, 'w')
+			f.write(data)
+			f.close()
+		except Exception, msg:
+			invest.error("Could not save the retrieved currencies to %s: %s" % (invest.CURRENCIES_FILE, msg) )
+
+	def load_currencies(self):
+		invest.debug("Loading currencies from %s" % invest.CURRENCIES_FILE)
+		try:
+			f = open(invest.CURRENCIES_FILE, 'r')
+			data = f.readlines()
+			f.close()
+
+			self.convert_currencies(self.parse_yahoo_csv(csv.reader(data)))
+		except Exception, msg:
+			invest.error("Could not load the currencies from %s: %s" % (invest.CURRENCIES_FILE, msg) )
+
 	def update_tooltip(self, msg = None):
 		tooltip = []
 		if self.quotes_count > 0:
@@ -203,6 +347,10 @@ class QuoteUpdater(Gtk.ListStore):
 			if len(fields) == 0:
 				continue
 
+			if len(fields) != len(QUOTES_CSV_FIELDS):
+				invest.debug("CSV line has unexpected number of fields, expected %d, has %d: %s" % (len(QUOTES_CSV_FIELDS), len(fields), fields))
+				continue
+
 			result[fields[0]] = {}
 			for i, field in enumerate(QUOTES_CSV_FIELDS):
 				if type(field) == tuple:
@@ -264,73 +412,16 @@ class QuoteUpdater(Gtk.ListStore):
 			quote_items = quotes.items ()
 			quote_items.sort ()
 
-			quotes_change = 0
+			self.quotes_change = 0
 			self.quotes_count = 0
 			self.statistics = {}
 
-			for ticker, val in quote_items:
-				pb = None
-
-				# ignore unknown stocks
-				if ticker not in invest.STOCKS.keys():
-					invest.debug("Observed unknown stock: %s" % ticker)
-					continue
-
-				# get the label of this stock for later reuse
-				label = invest.STOCKS[ticker]["label"]
-				if len(label) == 0:
-					if len(val["label"]) != 0:
-						label = val["label"]
-					else:
-						label = ticker
-
-				# make sure the currency field is upper case
-				val["currency"] = val["currency"].upper();
-
-				# the currency of currency conversion rates like EURUSD=X is wrong in csv
-				# this can be fixed easily by reusing the latter currency in the symbol
-				if len(ticker) == 8 and ticker.endswith("=X"):
-					val["currency"] = ticker[3:6]
-
-				# indices should not have a currency, though yahoo says so
-				if ticker.startswith("^"):
-					val["currency"] = ""
-
-				# sometimes, funny currencies are returned (special characters), only consider known currencies
-				if len(val["currency"]) > 0 and val["currency"] not in currencies.Currencies.currencies:
-					invest.debug("Currency '%s' is not known, dropping" % val["currency"])
-					val["currency"] = ""
-
-				# if this is a currency not yet seen and different from the target currency, memorize it
-				if val["currency"] not in self.currencies and len(val["currency"]) > 0:
-					self.currencies.append(val["currency"])
-
-				# Check whether the symbol is a simple quote, or a portfolio value
-				is_simple_quote = True
-				for purchase in invest.STOCKS[ticker]["purchases"]:
-					if purchase["amount"] != 0:
-						is_simple_quote = False
-						break
-
-				if is_simple_quote:
-					row = self.insert(0, [ticker, label, val["currency"], True, 0.0, 0.0, val["trade"], val["variation_pct"], pb])
-				else:
-					(balance, change) = self.balance(invest.STOCKS[ticker]["purchases"], val["trade"])
-					row = self.insert(0, [ticker, label, val["currency"], False, balance, change, val["trade"], val["variation_pct"], pb])
-					self.add_balance_change(balance, change, val["currency"])
-
-				url = 'http://ichart.yahoo.com/h?s=%s' % ticker
-
-				image_retriever = invest.chart.ImageRetriever(url)
-				image_retriever.connect("completed", self.set_pb_callback, row)
-				image_retriever.start()
-
-				quotes_change += val['variation_pct']
-				self.quotes_count += 1
+			# iterate over the STOCKS tree and build up this treestore
+			self.add_quotes(quotes, invest.STOCKS, None)
 
 			# we can only compute an avg quote change if there are quotes
 			if self.quotes_count > 0:
-				self.avg_quotes_change = quotes_change/float(self.quotes_count)
+				self.avg_quotes_change = self.quotes_change/float(self.quotes_count)
 
 				# change icon
 				quotes_change_sign = 0
@@ -340,7 +431,6 @@ class QuoteUpdater(Gtk.ListStore):
 			else:
 				self.avg_quotes_change = 0
 
-				
 			# mark quotes to finally be valid
 			self.quotes_valid = True
 
@@ -349,6 +439,8 @@ class QuoteUpdater(Gtk.ListStore):
 			invest.debug(quotes)
 			self.quotes_valid = False
 
+
+	def retrieve_currencies(self):
 		# start retrieving currency conversion rates
 		if invest.CONFIG.has_key("currency"):
 			target_currency = invest.CONFIG["currency"]
@@ -369,6 +461,90 @@ class QuoteUpdater(Gtk.ListStore):
 				quotes_retriever.connect("completed", self.on_currency_retriever_completed)
 				quotes_retriever.start()
 
+
+	def add_quotes(self, quotes, stocks, parent):
+		for stock in stocks:
+			if not stock.has_key('ticker'):
+				name = stock['name']
+				list = stock['list']
+				# here, the stock group name is used as the label,
+				# so in quotes, the key == None indicates a group
+				# in preferences, the label == None indicates this
+				try:
+					row = self.insert(parent, 0, [None, name, None, True, None, None, None, None, None])
+				except Exception, msg:
+					invest.debug("Failed to insert group %s: %s" % (name, msg))
+				self.add_quotes(quotes, list, row)
+				# Todo: update the summary statistics of row
+			else:
+				ticker = stock['ticker'];
+				if not quotes.has_key(ticker):
+					invest.debug("no quote for %s retrieved" % ticker)
+					continue
+
+				# get the quote
+				quote = self.get_quote(quotes, ticker)
+
+				# get the label of this stock for later reuse
+				label = stock["label"]
+				if len(label) == 0:
+					if len(quote["label"]) != 0:
+						label = quote["label"]
+					else:
+						label = ticker
+
+				# Check whether the symbol is a simple quote, or a portfolio value
+				try:
+					if self.is_simple_quote(stock):
+						row = self.insert(parent, 0, [ticker, label, quote["currency"], True, 0.0, 0.0, float(quote["trade"]), float(quote["variation_pct"]), None])
+					else:
+						(balance, change) = self.balance(stock["purchases"], quote["trade"])
+						row = self.insert(parent, 0, [ticker, label, quote["currency"], False, float(balance), float(change), float(quote["trade"]), float(quote["variation_pct"]), None])
+						self.add_balance_change(balance, change, quote["currency"])
+				except Exception, msg:
+					invest.debug("Failed to insert stock %s: %s" % (stock, msg))
+
+				self.quotes_change += quote['variation_pct']
+				self.quotes_count += 1
+
+				self.retrieve_image(ticker, row)
+
+	def retrieve_image(self, ticker, row):
+		if invest.CONFIG.has_key('hidecharts') and invest.CONFIG['hidecharts']:
+			return
+
+		url = 'http://ichart.yahoo.com/h?s=%s' % ticker
+		image_retriever = invest.chart.ImageRetriever(url)
+		image_retriever.connect("completed", self.set_pb_callback, row)
+		image_retriever.start()
+
+	def get_quote(self, quotes, ticker):
+		# the data for this quote
+		quote = quotes[ticker];
+
+		# make sure the currency field is upper case
+		quote["currency"] = quote["currency"].upper();
+
+		# the currency of currency conversion rates like EURUSD=X is wrong in csv
+		# this can be fixed easily by reusing the latter currency in the symbol
+		if len(ticker) == 8 and ticker.endswith("=X"):
+			quote["currency"] = ticker[3:6]
+
+		# indices should not have a currency, though yahoo says so
+		if ticker.startswith("^"):
+			quote["currency"] = ""
+
+		# sometimes, funny currencies are returned (special characters), only consider known currencies
+		if len(quote["currency"]) > 0 and quote["currency"] not in currencies.Currencies.currencies:
+			invest.debug("Currency '%s' is not known, dropping" % quote["currency"])
+			quote["currency"] = ""
+
+		# if this is a currency not yet seen and different from the target currency, memorize it
+		if quote["currency"] not in self.currencies and len(quote["currency"]) > 0:
+			self.currencies.append(quote["currency"])
+
+		return quote
+
 	def convert_currencies(self, quotes):
 		# if there is no target currency, this method should never have been called
 		if not invest.CONFIG.has_key("currency"):
@@ -389,6 +565,9 @@ class QuoteUpdater(Gtk.ListStore):
 		iter = self.get_iter_first()
 		while iter != None:
 			currency = self.get_value(iter, self.CURRENCY)
+			if currency == None:
+				iter = self.iter_next(iter)
+				continue
 			symbol = self.get_value(iter, self.SYMBOL)
 			# ignore stocks that are currency conversions
 			# and only convert stocks that are not in the target currency
@@ -432,12 +611,18 @@ class QuoteUpdater(Gtk.ListStore):
 		self.set_value(row, self.PB, retriever.image.get_pixbuf())
 
 	# check if we have only simple quotes
-	def simple_quotes_only(self):
-		res = True
-		for entry, data in invest.STOCKS.iteritems():
-			purchases = data["purchases"]
-			for purchase in purchases:
-				if purchase["amount"] != 0:
-					res = False
-					break
-		return res
+	def simple_quotes_only(self, stocks):
+		for stock in stocks:
+			if stock.has_key('purchases'):
+				if not self.is_simple_quote(stock):
+					return False
+			else:
+				if not self.simple_quotes_only(stock['list']):
+					return False
+		return True
+
+	def is_simple_quote(self, stock):
+		for purchase in stock["purchases"]:
+			if purchase["amount"] != 0:
+				return False
+		return True
diff --git a/invest-applet/invest/widgets.py b/invest-applet/invest/widgets.py
index 0442e0f..3177b36 100644
--- a/invest-applet/invest/widgets.py
+++ b/invest-applet/invest/widgets.py
@@ -39,10 +39,11 @@ class InvestWidget(Gtk.TreeView):
 	def __init__(self, quotes_updater):
 		Gtk.TreeView.__init__(self)
 		self.set_property("rules-hint", True)
-		self.set_reorderable(True)
-		self.set_hover_selection(True)
+#		self.set_property("enable-grid-lines", True)
+#		self.set_property("reorderable", True)
+		self.set_property("hover-selection", True)
 
-		simple_quotes_only = quotes_updater.simple_quotes_only()
+		simple_quotes_only = quotes_updater.simple_quotes_only(invest.STOCKS)
 
 		# model: SYMBOL, LABEL, TICKER_ONLY, BALANCE, BALANCE_PCT, VALUE, VARIATION_PCT, PB
 		# Translators: these words all refer to a stock. Last is short
@@ -65,6 +66,8 @@ class InvestWidget(Gtk.TreeView):
 				column.set_cell_data_func(cell, col_cellgetdata_functions[i])
 				self.append_column(column)
 			elif i == 3:
+				if invest.CONFIG.has_key('hidecharts') and invest.CONFIG['hidecharts']:
+					continue
 				cell_pb = Gtk.CellRendererPixbuf()
 				column = Gtk.TreeViewColumn (col_name, cell_pb, pixbuf=quotes_updater.PB)
 				self.append_column(column)
@@ -81,9 +84,6 @@ class InvestWidget(Gtk.TreeView):
 					column.set_cell_data_func(cell, col_cellgetdata_functions[i])
 					self.append_column(column)
 
-		if simple_quotes_only == True:
-			self.set_property('headers-visible', False)
-
 		self.connect('row-activated', self.on_row_activated)
 		self.set_model(quotes_updater)
 
@@ -102,10 +102,19 @@ class InvestWidget(Gtk.TreeView):
 
 
 	def _getcelldata_label(self, column, cell, model, iter, userdata):
-		cell.set_property('text', model[iter][model.LABEL])
+		label = model[iter][model.LABEL]
+		if self.is_stock(iter):
+			cell.set_property('text', label)
+		else:
+			cell.set_property('markup', "<b>%s</b>" % label)
 
 	def _getcelldata_value(self, column, cell, model, iter, userdata):
-		cell.set_property('text', self.format_currency(model[iter][model.VALUE], model[iter][model.CURRENCY]))
+		value = model[iter][model.VALUE];
+		currency = model[iter][model.CURRENCY];
+		if value == None or currency == None:
+			cell.set_property('text', "")
+		else:
+			cell.set_property('text', self.format_currency(value, currency))
 
 	def is_selected(self, model, iter):
 		m, it = self.get_selection().get_selected()
@@ -121,6 +130,10 @@ class InvestWidget(Gtk.TreeView):
 		return palette[intensity]
 
 	def _getcelldata_variation(self, column, cell, model, iter, userdata):
+		if self.is_group(iter):
+			cell.set_property('text', '')
+			return
+
 		color = self.get_color(model, iter, model.VARIATION_PCT)
 		change_pct = self.format_percent(model[iter][model.VARIATION_PCT])
 		cell.set_property('markup',
@@ -156,6 +169,12 @@ class InvestWidget(Gtk.TreeView):
 
 		invest.chart.show_chart([ticker])
 
+	def is_group(self, iter):
+		return self.get_model()[iter][0] == None
+
+	def is_stock(self, iter):
+		return not self.is_group(iter)
+
 #class InvestTicker(Gtk.Label):
 #	def __init__(self):
 #		Gtk.Label.__init__(self, _("Waiting..."))



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]