
Eigenschaftsänderungen: hamster
___________________________________________________________________
Name: svn:ignore
   + *.pyc
Makefile.in
hamster-applet
.libs
.deps
Makefile
defs.py



Index: hamster/stats.py
===================================================================
--- hamster/stats.py	(Revision 339)
+++ hamster/stats.py	(Arbeitskopie)
@@ -25,7 +25,7 @@
 import gtk
 import pango
 
-from hamster import dispatcher, storage, SHARED_DATA_DIR, stuff
+from hamster import dispatcher, get_db, SHARED_DATA_DIR, stuff
 from hamster.charting import Chart
 from hamster.add_custom_fact import CustomFactController
 
@@ -139,7 +139,7 @@
         
         week = {"days": [], "totals": []}
         
-        facts = storage.get_facts(self.start_date, self.end_date)
+        facts = get_db().get_facts(self.start_date, self.end_date)
         
         for i in range((self.end_date - self.start_date).days  + 1):
             current_date = self.start_date + dt.timedelta(i)
@@ -365,7 +365,7 @@
             if path > 0:
                 selection.select_path(path)
 
-        storage.remove_fact(model[iter][0])
+        get_db().remove_fact(model[iter][0])
 
     """keyboard events"""
     def on_key_pressed(self, tree, event_key):
@@ -406,7 +406,7 @@
         
     def on_report_button_clicked(self, widget):
         from hamster import reports
-        facts = storage.get_facts(self.start_date, self.end_date)
+        facts = get_db().get_facts(self.start_date, self.end_date)
         reports.simple(facts, self.start_date, self.end_date)
 
     def after_activity_update(self, widget, renames):
Index: hamster/__init__.py
===================================================================
--- hamster/__init__.py	(Revision 339)
+++ hamster/__init__.py	(Arbeitskopie)
@@ -30,7 +30,6 @@
 import locale
 
 # Autotools set the actual data_dir in defs.py
-from db import Storage
 from defs import *
 from dispatcher import Dispatcher
 
@@ -78,16 +77,54 @@
 # Init storage
 
 dispatcher = Dispatcher()
+current_storage_method = None
+available_storage_methods = {}
 storage = None
     
 # Path to images, icons
 ART_DATA_DIR = join(SHARED_DATA_DIR, "art")
 
+def get_db():
+    return storage
 
-def __init_db():
+def _init_db():
     """work arround the problem that we need hamster before setting
        locale info, but that triggers init of DB and thus sets strings
        before they have been localized"""
-    global storage
-    storage = Storage(dispatcher)
-    
+    from hamster.storage_methods import load_storage_method, DEFAULT_METHOD, STORAGE_METHODS
+    from hamster.Configuration import GconfStore
+    global storage, available_storage_methods, current_storage_method
+    for method in STORAGE_METHODS:
+        available_storage_methods[method] = load_storage_method(method).Storage(dispatcher)
+    current_storage_method = GconfStore.get_instance().get_storage_method() or DEFAULT_METHOD
+    storage = available_storage_methods[current_storage_method]
+    while storage.needs_configuration(): # TODO: better way to force valid configuration
+        storage.configuration()
+    storage.activate()
+__init_db = _init_db # values with "__" cannot be imported in classes
+
+def _change_db(method):
+    global storage, current_storage_method
+    if current_storage_method == method:
+        return True
+    if not method in available_storage_methods:
+        return False
+    new_storage = available_storage_methods[method]
+    # TODO: better way to force valid configuration
+    try:
+        i = 0
+        while new_storage.needs_configuration():
+            # TODO: Some way to validate settings, Hamster should not work with invalid settings
+            new_storage.configuration()
+            i += 1
+            if i > 10: # don't try this more than 10 times
+                return False
+    except Exception:
+        # Exception might be used to abort setting this db
+        return False
+    storage.deactivate()
+    storage = new_storage
+    storage.activate()
+    current_storage_method = method
+    return True
+

Eigenschaftsänderungen: hamster/storage_methods
___________________________________________________________________
Name: svn:ignore
   + *.pyc


Index: hamster/storage_methods/__init__.py
===================================================================
--- hamster/storage_methods/__init__.py	(Revision 0)
+++ hamster/storage_methods/__init__.py	(Arbeitskopie)
@@ -17,14 +17,30 @@
 # You should have received a copy of the GNU General Public License
 # along with Project Hamster.  If not, see <http://www.gnu.org/licenses/>.
 
-
 import datetime
 
 class Storage(object):
+    STORAGE_NAME = None
+    STORAGE_ID = None
+	
     def __init__(self, parent):
         self.parent = parent
+
+    def activate(self):
         self.run_fixtures()
 
+    def deactivate(self):
+        pass
+
+    def needs_configuration(self):
+        return False
+
+    def has_configuration(self):
+        return False
+
+    def configuration(self):
+        pass
+
     def run_fixtures(self):
         pass
 
@@ -107,4 +123,15 @@
 
     def get_category_list(self):
         return self.__get_category_list()
-        
+
+# TODO: Autodiscover
+STORAGE_METHODS = [
+	'db',
+	'paymo',
+]
+DEFAULT_METHOD = 'db'
+
+def load_storage_method(name):
+	mod = __import__('hamster.storage_methods', [], [], [name])
+	return getattr(mod, name)
+
Index: hamster/storage_methods/db.py
===================================================================
--- hamster/storage_methods/db.py	(Revision 0)
+++ hamster/storage_methods/db.py	(Arbeitskopie)
@@ -25,11 +25,14 @@
 import os, time
 import datetime
 import hamster
-import hamster.storage
+import hamster.storage_methods
 import datetime as dt
 
-        
-class Storage(hamster.storage.Storage):
+
+class Storage(hamster.storage_methods.Storage):
+    STORAGE_NAME = 'Local database'
+    STORAGE_ID = 'db'
+    
     # we are saving data under $HOME/.gnome2/hamster-applet/hamster.db
     con = None # Connection will be created on demand
 
@@ -378,7 +381,7 @@
 
         res = cur.fetchall()
         cur.close()
-
+        
         return res
 
     def fetchone(self, query, params = None):
Index: hamster/storage_methods/paymo.py
===================================================================
--- hamster/storage_methods/paymo.py	(Revision 0)
+++ hamster/storage_methods/paymo.py	(Revision 0)
@@ -0,0 +1,352 @@
+# - coding: utf-8 -
+
+# Copyright (C) 2008 David Danier <david.danier at team23.de>
+
+# This file is part of Project Hamster.
+
+# Project Hamster 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.
+
+# Project Hamster 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 Project Hamster.  If not, see <http://www.gnu.org/licenses/>.
+
+
+"""use paymo a data service"""
+
+from pymo import client
+from pymo import config # TODO: Save config in hamster
+import os, time
+import datetime
+import hamster
+import hamster.storage_methods
+import datetime as dt
+import gconf
+import pygtk
+import os
+import gtk
+
+class PaymoGconfStore(object):
+
+    __instance = None
+        
+    @staticmethod
+    def get_instance():
+        if not PaymoGconfStore.__instance:
+            PaymoGconfStore.__instance = PaymoGconfStore()
+        return PaymoGconfStore.__instance
+        
+    def __init__(self):
+        """
+        Do not use the constructor directly. Always use L{get_instance}
+        Because otherwise you will have lots of signals running arround
+        """
+        super(PaymoGconfStore, self).__init__()
+        from hamster.Configuration import GconfStore
+        global_config = GconfStore.get_instance()
+        base_path = global_config.get_storage_config_dir('paymo')
+        self.GCONF_DIR = base_path
+        self.GCONF_APY_KEY = self.GCONF_DIR + "/api_key"
+        self.GCONF_HOSTNAME = self.GCONF_DIR + "/hostname"
+        self.GCONF_USERNAME = self.GCONF_DIR + "/username"
+        self.GCONF_PASSWORD = self.GCONF_DIR + "/password"
+        self._client = gconf.client_get_default()
+    
+    def get_api_key(self):
+        return self._client.get_string(self.GCONF_APY_KEY)
+    
+    def get_hostname(self):
+        return self._client.get_string(self.GCONF_HOSTNAME)
+    
+    def get_username(self):
+        return self._client.get_string(self.GCONF_USERNAME)
+    
+    def get_password(self):
+        return self._client.get_string(self.GCONF_PASSWORD)
+    
+    #------------------------    
+    def set_api_key(self, value):
+        self._client.set_string(self.GCONF_APY_KEY, value)
+    
+    def set_hostname(self, value):
+        self._client.set_string(self.GCONF_HOSTNAME, value)
+    
+    def set_username(self, value):
+        self._client.set_string(self.GCONF_USERNAME, value)
+    
+    def set_password(self, value):
+        self._client.set_string(self.GCONF_PASSWORD, value)
+
+class RowDictTuple(object):
+    def __init__(self, **kwargs):
+        self.d = {}
+        self.l = []
+        for x in kwargs:
+            self.d[x] = kwargs[x]
+            self.l.append(kwargs[x])
+    def __getitem__(self, key):
+        if isinstance(key, int):
+            return self.l[key]
+        else:
+            return self.d[key]
+    def __unicode__(self):
+        return unicode(self.d)
+    def __str__(self):
+        return str(self.d)
+
+class PaymoConfiguration(object):
+    def __init__(self):
+        self.aborted = False
+        self.glade = gtk.glade.XML(os.path.join(hamster.SHARED_DATA_DIR, "paymo-configuration.glade"))
+        self.config = PaymoGconfStore.get_instance()
+        self.window = self.get_widget('paymo_configuration_window')
+        
+        self.api_key = self.get_widget('api_key_entry')
+        self.hostname = self.get_widget('hostname_entry')
+        self.username = self.get_widget('username_entry')
+        self.password = self.get_widget('password_entry')
+        
+        self.api_key.set_text(self.config.get_api_key() or '')
+        self.hostname.set_text(self.config.get_hostname() or '')
+        self.username.set_text(self.config.get_username() or '')
+        self.password.set_text(self.config.get_password() or '')
+        
+        self.ok = self.get_widget('ok_button')
+        self.abort = self.get_widget('abort_button')
+        
+        self.ok.connect('clicked', self.configuration_done)
+        self.abort.connect('clicked', self.configuration_aborted)
+    
+    def get_widget(self, name):
+        """ skip one variable (huh) """
+        return self.glade.get_widget(name)
+    
+    def show(self):
+        self.window.show_all()
+
+    def configuration_done(self, button):
+        self.config.set_api_key(self.api_key.get_text())
+        self.config.set_hostname(self.hostname.get_text())
+        self.config.set_username(self.username.get_text())
+        self.config.set_password(self.password.get_text())
+        self.window.destroy()
+    
+    def configuration_aborted(self, button):
+        self.aborted = True
+        self.window.destroy()
+
+class Storage(hamster.storage_methods.Storage):
+    STORAGE_NAME = 'Paymo online service'
+    STORAGE_ID = 'paymo'
+    
+    _client = None # Connection will be created on demand
+    _projects_by_id = {}
+    _tasks_by_id = {}
+    _entries_by_id = {}
+    _last_entry = None
+    _last_entry_is_running = False
+    
+    def activate(self, *args, **kwargs):
+        for p in self.client.projects:
+            self._projects_by_id[p['id']] = p
+            for t in p.tasks:
+                self._tasks_by_id[t['id']] = t
+
+    def deactivate(self, *args, **kwargs):
+        if not self._client is None:
+            self._client.logout()
+            self._client = None
+
+    def needs_configuration(self):
+        try:
+            self.client
+        except:
+            self._client = None
+            return True
+        return False
+
+    def has_configuration(self):
+        return True
+
+    def configuration(self):
+        config = PaymoConfiguration()
+        config.show()
+        destroyed = False
+        def on_destroy(window):
+            gtk.main_quit()
+            if not self._client is None:
+                self._client.logout()
+                self._client = None
+        config.window.connect('destroy', on_destroy)
+        gtk.main()
+        if config.aborted:
+            raise Exception('config aborted')
+
+    def __get_category_list(self):
+        return [RowDictTuple(id=p['id'], name=p['name'], color_code=None, category_order=p['id']) for p in self.client.projects]
+
+    def __change_category(self, id, category_id):
+        pass
+
+    def __insert_category(self, name):
+        return None
+
+    def __update_category(self, id,  name):
+        pass
+    
+    def __move_activity(self, source_id, target_order, insert_after = True):
+        pass
+        
+    def __get_activity_by_name(self, name):
+        for p in self.client.projects:
+            for t in p.tasks:
+                if t['name'] == name:
+                    return t['id']
+        return None
+
+    def __get_fact(self, id):
+        print '__get_fact', id
+        e = self._entries_by_id.get(id, None)
+        end_time = e['end']
+        if not self._last_entry is None and self._last_entry_is_running and e['id'] == self._last_entry['id']:
+            end_time = None
+        return RowDictTuple(id=e['id'], start_time=e['start'], end_time=end_time, name=e['task_name'], activity_id=e['project_id'])
+
+    def __get_last_activity(self):
+        print '__get_last_activity'
+        e = self._last_entry
+        if not e is None:
+            end_time = e['end']
+            if self._last_entry_is_running:
+                end_time = None
+            return RowDictTuple(id=e['id'], start_time=e['start'], end_time=end_time, name=e['task_name'], activity_id=e['project_id'])
+
+    def __touch_fact(self, activity, end_time = None):
+        print '__touch_fact', activity, end_time
+        fact_needs_ending = True
+        if end_time is None:
+            end_time = datetime.datetime.now()
+            fact_needs_ending = False
+        entry_id = activity['id']
+        e = self._entries_by_id.get(entry_id, None)
+        if not e is None:
+            if fact_needs_ending and not self._last_entry is None and e['id'] == self._last_entry['id']:
+                self._last_entry_is_running = False
+            e['end'] = end_time
+            e.update()
+
+    def __add_fact(self, activity_name, start_time = None, end_time = None):
+        print '__add_fact', activity_name, start_time, end_time
+        start_time = start_time or datetime.datetime.now()
+        task = None
+        for p in self.client.projects:
+            for t in p.tasks:
+                if t['name'] == activity_name:
+                    task = t
+        if task is None:
+            return
+        
+        will_be_current = end_time is None
+        print 'will_be_current:', will_be_current
+        if end_time is None:
+            end_time = datetime.datetime.now()
+        
+        e = client.PaymoEntry(self.client, task_id=task['id'], project_id=task['project_id'], start=start_time, end=end_time)
+        e.add()
+        self._entries_by_id[e['id']] = e
+        
+        if will_be_current:
+            if not self._last_entry is None:
+                self._last_entry['end'] = start_time
+                self._last_entry_is_running = False
+                self._last_entry.update()
+            self._last_entry = e
+            self._last_entry_is_running = True
+
+        print '_last_entry:', self._last_entry
+        print '_last_entry_is_running:', self._last_entry_is_running
+
+        return RowDictTuple(id=e['id'], start_time=e['start'], end_time=e['end'], name=e['task_name'], activity_id=e['project_id'])
+
+
+    def __get_facts(self, date, end_date = None):
+        print '__get_facts', date, end_date
+        entries = self.client.get_time_entries(date, end_date)
+        for e in entries:
+            self._entries_by_id[e['id']] = e
+        result = []
+        for e in entries:
+            end_time = e['end']
+            if not self._last_entry is None and self._last_entry_is_running and e['id'] == self._last_entry['id']:
+                end_time = None
+            result.append(RowDictTuple(id=e['id'], start_time=e['start'], end_time=end_time, name=e['task_name'], activity_id=e['project_id'], category=e['project_name'], category_id=e['project_id']))
+        return result
+
+    def __remove_fact(self, fact_id):
+        print '__remove_fact', fact_id
+        e = self._entries_by_id.get(fact_id, None)
+        if not e is None:
+            e.delete()
+            del self._entries_by_id[fact_id]
+
+    def __get_sorted_activities(self):
+        result = []
+        for p in self.client.projects:
+            for t in p.tasks:
+                result.append(RowDictTuple(id=t['id'], name=t['name'], work=None, activity_order=t['id'], deleted=None, category_id=t['project_id'], category_order=t['project_id']))
+        return result
+        
+    def __get_activities(self, category_id = None):
+        result = []
+        for p in self.client.projects:
+            if not category_id is None and p['id'] != category_id:
+                continue
+            for t in p.tasks:
+                result.append(RowDictTuple(id=t['id'], name=t['name'], work=None, activity_order=t['id'], deleted=None, category_id=t['project_id']))
+        return result
+
+    def __remove_activity(self, id):
+        e = self._tasks_by_id.get(id, None)
+        if not e is None:
+            e.complete()
+
+    def __remove_category(self, id):
+        pass
+
+    def __swap_activities(self, id1, id2):
+        pass
+
+    def __insert_activity(self, name, category_id = -1):
+        if category_id == -1:
+            return
+        t = client.PaymoTask(self.client, project_id=category_id, name=name)
+        t.add()
+        self._tasks_by_id[t['id']] = t
+        return t['id']
+
+    def __update_activity(self, id, name, category_id):
+        t = self._tasks_by_id.get(id, None)
+        if not t is None:
+            t['project_id'] = category_id
+            t['name'] = name
+            t.update()
+
+    def get_client(self):
+        if self._client == None:
+            config = PaymoGconfStore.get_instance()
+            try:
+                self._client = client.PaymoClient(config.get_hostname())
+                self._client.login(config.get_username(), config.get_password())
+            except:
+                self._client = None
+                raise
+        return self._client
+
+    client = property(get_client, None)
+
Index: hamster/preferences.py
===================================================================
--- hamster/preferences.py	(Revision 339)
+++ hamster/preferences.py	(Arbeitskopie)
@@ -24,7 +24,7 @@
 import os
 import gtk
 
-from hamster import dispatcher, storage, SHARED_DATA_DIR
+from hamster import dispatcher, get_db, SHARED_DATA_DIR
 from hamster.Configuration import GconfStore
 
 def get_prev(selection, model):
@@ -45,9 +45,11 @@
     def load(self):
         """ Loads activity list from database, ordered by
             activity_order """
+        
+        self.clear()
+        
+        category_list = get_db().get_category_list()
 
-        category_list = storage.get_category_list()
-
         for category in category_list:
             self.append([category['id'],
                          category['name'],
@@ -70,7 +72,7 @@
         if category_id == None:
             return
         
-        activity_list = storage.get_activities(category_id)
+        activity_list = get_db().get_activities(category_id)
 
         for activity in activity_list:
             self.append([activity['id'],
@@ -158,7 +160,25 @@
 
         self.category_tree.connect("drag_data_received",
                                    self.on_category_drop)
-
+        
+        # storage methods
+        from hamster import available_storage_methods
+        self.storage_selection = self.get_widget('storage_selection')
+        self.storage_configuration = self.get_widget('storage_configuration')
+        self._storage_method_index = []
+        for method in available_storage_methods:
+            storage_method = available_storage_methods[method]
+            self.storage_selection.append_text(storage_method.STORAGE_NAME)
+            self._storage_method_index.append(method)
+            if get_db().STORAGE_ID == method:
+                self._last_storage_selection = len(self._storage_method_index) - 1
+                self.storage_selection.set_active(self._last_storage_selection)
+        self.storage_configuration.set_sensitive(get_db().has_configuration())
+        self.storage_selection.connect("changed",
+                                   self.on_storage_selection_changed)
+        self.storage_configuration.connect("clicked",
+                                   self.on_storage_configuration_clicked)
+        
         #select first category
         selection = self.category_tree.get_selection()
         selection.select_path((0,))
@@ -242,10 +262,10 @@
             if (position == gtk.TREE_VIEW_DROP_BEFORE
                 or position == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE):
                 print "insert '%s' before '%s'" % (data, model[iter][3])
-                storage.move_activity(int(data), model[iter][3], insert_after = False)
+                get_db().move_activity(int(data), model[iter][3], insert_after = False)
             else:
                 print "insert '%s' after '%s'" % (data, model[iter][3])
-                storage.move_activity(int(data), model[iter][3], insert_after = True)
+                get_db().move_activity(int(data), model[iter][3], insert_after = True)
         else:
             print "append '%s'" % data
 
@@ -270,7 +290,7 @@
 
             iter = model.get_iter(path)
             
-            storage.change_category(int(data), model[iter][0])
+            get_db().change_category(int(data), model[iter][0])
             
             context.finish(True, True, etime)
         else:
@@ -300,9 +320,9 @@
         id = tree_row[0]
         
         if id == -1:
-            tree_row[0] = storage.insert_activity(name, category_id)
+            tree_row[0] = get_db().insert_activity(name, category_id)
         else:
-            storage.update_activity(id, name, category_id)
+            get_db().update_activity(id, name, category_id)
 
     # callbacks
     def category_edited_cb(self, cell, path, new_text, model):
@@ -311,10 +331,10 @@
         name = model[path][1]
         
         if id == -2:
-            id = storage.insert_category(name)
+            id = get_db().insert_category(name)
             model[path][0] = id
         elif id > -1:  #ignore unsorted category (id = -1)
-            storage.update_category(id, name)
+            get_db().update_category(id, name)
 
 
     def activity_name_edited_cb(self, cell, path, new_text, model):
@@ -431,7 +451,7 @@
         selection = tree.get_selection()
         (model, iter) = selection.get_selected()
         if (event_key.keyval == gtk.keysyms.Delete):
-            storage.remove_activity(model[iter][0])
+            get_db().remove_activity(model[iter][0])
             self._del_selected_row(tree)
 
         elif key == gtk.keysyms.F2 :
@@ -454,7 +474,7 @@
         if  key == gtk.keysyms.Delete:
             id = model[iter][0]
             if id != -1:
-                storage.remove_category(id)
+                get_db().remove_category(id)
                 self._del_selected_row(tree)
         elif key == gtk.keysyms.F2:
             self.categoryCell.set_property("editable", True)
@@ -494,14 +514,14 @@
 
     def on_remove_activity_clicked(self, button):
         removable_id = self._del_selected_row(self.activity_tree)
-        storage.remove_activity(removable_id)
+        get_db().remove_activity(removable_id)
 
     def on_promote_activity_clicked(self, button):
         (model, iter) = self.selection.get_selected()
 
         #previous item
         prev_iter = get_prev(self.selection, model)
-        storage.swap_activities(model[iter][0], model[prev_iter][0])
+        get_db().swap_activities(model[iter][0], model[prev_iter][0])
         model.move_before(iter, prev_iter)
 
         self.activity_changed(self.selection, model)
@@ -510,7 +530,7 @@
         (model, iter) = self.selection.get_selected()
 
         next_iter = model.iter_next(iter)
-        storage.swap_activities(model[iter][0], model[next_iter][0])
+        get_db().swap_activities(model[iter][0], model[next_iter][0])
         self.activity_store.move_after(iter, next_iter)
 
         self.activity_changed(self.selection, model)
@@ -530,3 +550,25 @@
         
     def on_idle_minutes_value_changed(self, slider):
         self.config.set_timeout(slider.get_value())
+    
+    def on_storage_selection_changed(self, selection):
+        selection = selection.get_active()
+        from hamster import _change_db
+        method = self._storage_method_index[selection]
+        if _change_db(method):
+            self.config.set_storage_method(method)
+            self._last_storage_selection = selection
+            self.category_store.load()
+            get_db().dispatch('activity_updated', ())
+            self.storage_configuration.set_sensitive(get_db().has_configuration())
+        else:
+            self.storage_selection.set_active(self._last_storage_selection)
+    
+    def on_storage_configuration_clicked(self, button):
+        if not get_db().has_configuration():
+            return
+        try:
+            get_db().configuration()
+        except:
+            # config aborted
+            pass
Index: hamster/storage.py
===================================================================
--- hamster/storage.py	(Revision 339)
+++ hamster/storage.py	(Arbeitskopie)
@@ -1,110 +0,0 @@
-# - coding: utf-8 -
-
-# Copyright (C) 2007 Patryk Zawadzki <patrys at pld-linux.org>
-
-# This file is part of Project Hamster.
-
-# Project Hamster 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.
-
-# Project Hamster 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 Project Hamster.  If not, see <http://www.gnu.org/licenses/>.
-
-
-import datetime
-
-class Storage(object):
-    def __init__(self, parent):
-        self.parent = parent
-        self.run_fixtures()
-
-    def run_fixtures(self):
-        pass
-
-    def dispatch(self, event, data):
-        self.parent.dispatch(event, data)
-
-    def get_fact(self, id):
-        return self.__get_fact(id)
-
-    def add_fact(self, activity_name, start_time = None,  end_time = None):
-        result = self.__add_fact(activity_name, start_time, end_time)
-        if result:
-            self.dispatch('day_updated', result['start_time'])
-        return result
-
-    def touch_fact(self, fact, end_time = None):
-        end_time = end_time or datetime.datetime.now()
-        result = self.__touch_fact(fact, end_time)
-        self.dispatch('day_updated', fact['start_time'])
-        return result
-
-    def get_facts(self, date, end_date = None):
-        return self.__get_facts(date, end_date)
-
-    def remove_fact(self, fact_id):
-        fact = self.get_fact(fact_id)
-        result = self.__remove_fact(fact_id)
-        self.dispatch('day_updated', fact['start_time'])
-        return result
-
-    def get_sorted_activities(self):
-        return self.__get_sorted_activities()
-        
-    def get_activities(self, category_id = None):
-        return self.__get_activities(category_id = category_id)
-    
-    def get_last_activity(self):
-        return self.__get_last_activity()
-
-    def remove_activity(self, id):
-        result = self.__remove_activity(id)
-        self.dispatch('activity_updated', ())
-        return result
-    
-    def remove_category(self, id):
-        self.__remove_category(id)
-        self.dispatch('activity_updated', ())
-
-    def move_activity(self, source_id, target_order, insert_after = True):
-        self.__move_activity(source_id, target_order, insert_after)
-        self.dispatch('activity_updated', ())
-
-    def change_category(self, id, category_id):
-        self.__change_category(id, category_id)
-        self.dispatch('activity_updated', ())
-
-    def swap_activities(self, id1, id2):
-        res = self.__swap_activities(id1, id2)
-        self.dispatch('activity_updated', ())
-        return res
-
-    def update_activity(self, id, name, category_id):
-        self.__update_activity(id, name, category_id)
-        self.dispatch('activity_updated', ())
-    
-    def insert_activity(self, name, category_id = -1):
-        new_id = self.__insert_activity(name, category_id)
-        self.dispatch('activity_updated', ())
-        return new_id
-
-    def update_category(self, id, name):
-        self.__update_category(id, name)
-        self.dispatch('activity_updated', ())
-
-    def insert_category(self, name):
-        res = self.__insert_category(name)
-        self.dispatch('activity_updated', ())
-        return res
-
-
-    def get_category_list(self):
-        return self.__get_category_list()
-        
Index: hamster/Configuration.py
===================================================================
--- hamster/Configuration.py	(Revision 339)
+++ hamster/Configuration.py	(Arbeitskopie)
@@ -34,6 +34,8 @@
     GCONF_TIMEOUT = GCONF_DIR + "/timeout"
     GCONF_ENABLE_TIMEOUT = GCONF_DIR + "/enable_timeout"
     GCONF_STOP_ON_SHUTDOWN = GCONF_DIR + "/stop_on_shutdown"    
+    GCONF_STORAGE_METHOD = GCONF_DIR + "/storage_method"    
+    GCONF_STORAGE_DIR = GCONF_DIR + "/storage_method_config/%s" # (method)
 
     __instance = None
         
@@ -58,6 +60,7 @@
         self._client.notify_add(self.GCONF_TIMEOUT, lambda x, y, z, a: dispatcher.dispatch("gconf_timeout_changed", z.value.get_int()))
         self._client.notify_add(self.GCONF_ENABLE_TIMEOUT, lambda x, y, z, a: dispatcher.dispatch("gconf_timeout_enabled_changed", z.value.get_bool()))
         self._client.notify_add(self.GCONF_STOP_ON_SHUTDOWN, lambda x, y, z, a: dispatcher.dispatch("gconf_stop_on_shutdown_changed", z.value.get_bool()))
+        self._client.notify_add(self.GCONF_STORAGE_METHOD, lambda x, y, z, a: dispatcher.dispatch("gconf_storage_method", z.value.get_string()))
     
     def get_keybinding(self):
         return self._client.get_string(self.GCONF_KEYBINDING)
@@ -71,6 +74,18 @@
     def get_stop_on_shutdown(self):
         return self._client.get_bool(self.GCONF_STOP_ON_SHUTDOWN)
 
+    def get_storage_method(self):
+        return self._client.get_string(self.GCONF_STORAGE_METHOD)
+    
+    def get_storage_config_dir(self, method):
+        from hamster import available_storage_methods
+        if not method in available_storage_methods:
+            raise Exception('invalid method')
+        storage = available_storage_methods[method]
+        if not storage.has_configuration():
+            raise Exception('no configuration needed')
+        return self.GCONF_STORAGE_DIR % method
+
     #------------------------    
     def set_keybinding(self, binding):
         self._client.set_string(self.GCONF_KEYBINDING, binding)
@@ -83,3 +98,8 @@
         
     def set_stop_on_shutdown(self, enabled):
         self._client.set_bool(self.GCONF_STOP_ON_SHUTDOWN, enabled)
+        
+    def set_storage_method(self, method):
+        self._client.set_string(self.GCONF_STORAGE_METHOD, method)
+
+
Index: hamster/add_custom_fact.py
===================================================================
--- hamster/add_custom_fact.py	(Revision 339)
+++ hamster/add_custom_fact.py	(Arbeitskopie)
@@ -25,7 +25,7 @@
 import gtk
 import gobject
 
-from hamster import dispatcher, storage, SHARED_DATA_DIR
+from hamster import dispatcher, get_db, SHARED_DATA_DIR
 import hamster.eds
 
 import time
@@ -73,7 +73,7 @@
         # handle the case when we get fact_id - that means edit!
         self.fact_id = fact_id
         if fact_id:
-            fact = storage.get_fact(fact_id)
+            fact = get_db().get_fact(fact_id)
             print fact
             self.get_widget('start_date').set_time(time.mktime(fact["start_time"].timetuple()))
             self.get_widget('start_time').set_text("%02d:%02d" % (fact["start_time"].hour, fact["start_time"].minute))
@@ -99,7 +99,7 @@
         self.wTree.signal_autoconnect(self)
         
     def refresh_menu(self):
-        all_activities = storage.get_activities()
+        all_activities = get_db().get_activities()
         self.activities.clear()
         for activity in all_activities:
             self.activities.append([activity['name']])
@@ -109,7 +109,7 @@
         store.clear()
 
         #populate fresh list from DB
-        activities = storage.get_sorted_activities()
+        activities = get_db().get_sorted_activities()
         prev_item = None
 
         today = datetime.date.today()
@@ -179,9 +179,9 @@
         # do some  trickery here - if we were told to update, let's just
         # do insert/delete
         if self.fact_id:
-            storage.remove_fact(self.fact_id)
+            get_db().remove_fact(self.fact_id)
 
-        storage.add_fact(activity, start_time, end_time)
+        get_db().add_fact(activity, start_time, end_time)
 
         if not self.fact_id: #hide panel only on add - on update user will want to see confirmation of changes
             dispatcher.dispatch('panel_visible', False)

Eigenschaftsänderungen: hamster/keybinder
___________________________________________________________________
Name: svn:ignore
   + *.pyc
Makefile.in
.libs
.deps
Makefile


Index: hamster/applet.py
===================================================================
--- hamster/applet.py	(Revision 339)
+++ hamster/applet.py	(Arbeitskopie)
@@ -25,7 +25,7 @@
 import gnomeapplet, gtk
 import gobject
 
-from hamster import dispatcher, storage, SHARED_DATA_DIR
+from hamster import dispatcher, get_db, _change_db, SHARED_DATA_DIR
 import hamster.eds
 from hamster.Configuration import GconfStore
 
@@ -173,14 +173,14 @@
         if self.timeout_enabled and self.timeout and self.last_activity and \
            self.last_activity['end_time'] == None and \
            idle.getIdleSec() / 60.0 >= self.timeout:
-            storage.touch_fact(self.last_activity)
+            get_db().touch_fact(self.last_activity)
             
 
         if self.last_activity and self.last_activity['end_time'] == None:
             # if we have date change - let's finish previous task and start a new one
             if prev_date and prev_date != self.today: 
-                storage.touch_fact(self.last_activity)
-                storage.add_fact(self.last_activity['name'])
+                get_db().touch_fact(self.last_activity)
+                get_db().add_fact(self.last_activity['name'])
             elif self.button.get_active(): # otherwise if we the day view is visible - update day's durations
                 self.load_day()
                     
@@ -219,7 +219,7 @@
     def refresh_menu(self):
         #first populate the autocomplete - contains all entries
         self.activities.clear()
-        all_activities = storage.get_activities()
+        all_activities = get_db().get_activities()
         for activity in all_activities:
             self.activities.append([activity['name']])
 
@@ -229,7 +229,7 @@
         store.clear()
 
         #populate fresh list from DB
-        categorized_activities = storage.get_sorted_activities()
+        categorized_activities = get_db().get_sorted_activities()
 
         for activity in categorized_activities:
             item = store.append([activity['name'], activity['id']])
@@ -251,7 +251,7 @@
 
         (cur, col) = self.treeview.get_cursor()
 
-        storage.remove_fact(model[iter][0])
+        get_db().remove_fact(model[iter][0])
         
         self.treeview.set_cursor(cur)
 
@@ -314,7 +314,7 @@
         if activity_name == "":
             return
         
-        storage.add_fact(activity_name)
+        get_db().add_fact(activity_name)
         dispatcher.dispatch('panel_visible', False)
 
     """listview events"""
@@ -329,7 +329,7 @@
         (model, iter) = selection.get_selected()
         activity_name = model[iter][1]
         if activity_name:
-            storage.add_fact(activity_name)
+            get_db().add_fact(activity_name)
             dispatcher.dispatch('panel_visible', False)        
         
         
@@ -341,7 +341,7 @@
         
     """button events"""
     def on_stop_tracking(self, button):
-        storage.touch_fact(self.last_activity)
+        get_db().touch_fact(self.last_activity)
         self.last_activity = None
         self.update_label()
         dispatcher.dispatch('panel_visible', False)
Index: hamster/db.py
===================================================================
--- hamster/db.py	(Revision 339)
+++ hamster/db.py	(Arbeitskopie)
@@ -1,598 +0,0 @@
-# - coding: utf-8 -
-
-# Copyright (C) 2007, 2008 Toms Bauģis <toms.baugis at gmail.com>
-# Copyright (C) 2007 Patryk Zawadzki <patrys at pld-linux.org>
-
-# This file is part of Project Hamster.
-
-# Project Hamster 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.
-
-# Project Hamster 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 Project Hamster.  If not, see <http://www.gnu.org/licenses/>.
-
-
-"""separate file for database operations"""
-
-from pysqlite2 import dbapi2 as sqlite
-import os, time
-import datetime
-import hamster
-import hamster.storage
-import datetime as dt
-
-        
-class Storage(hamster.storage.Storage):
-    # we are saving data under $HOME/.gnome2/hamster-applet/hamster.db
-    con = None # Connection will be created on demand
-
-    def __get_category_list(self):
-        return self.fetchall("SELECT * FROM categories ORDER BY category_order")
-
-    def __change_category(self, id, category_id):
-        query = "SELECT max(activity_order) + 1 FROM activities WHERE category_id = ?"
-        max_order = self.fetchone(query, (category_id, ))[0] or 1
-        
-        statement = """
-                   UPDATE activities 
-                      SET category_id = ?, activity_order = ?
-                    WHERE id = ?
-        """
-        
-        self.execute(statement, (category_id, max_order, id))
-    
-    def __insert_category(self, name):
-        new_rec = self.fetchone("select max(id) +1, max(category_order) + 1  from categories")
-        id, order = 1, 1
-        if new_rec[0] and new_rec[1]: # handle case when we have no categories at all
-            id, order = new_rec[0], new_rec[1]
-
-        query = """
-                   INSERT INTO categories (id, name, category_order)
-                        VALUES (?, ?, ?)
-        """
-        self.execute(query, (id, name, order))
-        return id
-
-    def __update_category(self, id,  name):
-        if id > -1: # Update, and ignore unsorted, if that was somehow triggered
-            update = """
-                       UPDATE categories
-                           SET name = ?
-                         WHERE id = ?
-            """
-            self.execute(update, (name, id))        
-        
-    def __move_activity(self, source_id, target_order, insert_after = True):
-        statement = "UPDATE activities SET activity_order = activity_order + 1"
-        
-        if insert_after:
-            statement += " WHERE activity_order > ?"
-        else:
-            statement += " WHERE activity_order >= ?"
-
-        self.execute(statement, (target_order, ))
-        
-        statement = "UPDATE activities SET activity_order = ? WHERE id = ?"
-        
-        if insert_after:
-            self.execute(statement, (target_order + 1, source_id))
-        else:
-            self.execute(statement, (target_order, source_id))
-            
-        
-        
-    def __get_activity_by_name(self, name):
-        """get most recent, preferably not deleted activity by it's name"""
-        query = """
-                   SELECT id from activities 
-                    WHERE lower(name) = lower(?)
-                 ORDER BY deleted, id desc
-                    LIMIT 1
-        """
-        
-        res = self.fetchone(query, (name,))
-        
-        if res:
-            return res['id']
-        
-        return None
-
-    def __get_fact(self, id):
-        query = """SELECT a.id AS id,
-                          a.start_time AS start_time,
-                          a.end_time AS end_time,
-                          b.name AS name, b.id as activity_id
-                     FROM facts a
-                LEFT JOIN activities b ON a.activity_id = b.id
-                    WHERE a.id = ? 
-        """
-        return self.fetchone(query, (id,))
-
-    def __get_last_activity(self):
-        query = """
-                   SELECT a.id AS id,
-                          a.start_time AS start_time,
-                          a.end_time AS end_time,
-                          b.name AS name, b.id as activity_id
-                     FROM facts a
-                LEFT JOIN activities b ON a.activity_id = b.id
-                    WHERE date(a.start_time) = ?
-                 ORDER BY a.start_time desc
-                    LIMIT 1
-        """
-        return self.fetchone(query, (dt.date.today(), ))
-
-    def __touch_fact(self, activity, end_time = None):
-        id = activity['id']
-        query = """
-                   UPDATE facts
-                      SET end_time = ?
-                    WHERE id = ?
-        """
-        self.execute(query, (end_time, id))
-
-    def __add_fact(self, activity_name, start_time = None, end_time = None):
-        start_time = start_time or datetime.datetime.now()
-        
-        # try to lookup activity by it's name in db. active ones have priority
-        activity_id = self.__get_activity_by_name(activity_name)
-
-        if not activity_id:
-            activity_id = self.__insert_activity(activity_name)
-
-
-        # now fetch facts for the specified day and check if we have to
-        # split or change span 
-        day_facts = self.__get_facts(start_time.date())
-        for fact in day_facts:
-            # first check if maybe we are overlapping end
-            if fact['end_time'] and fact['start_time'] < start_time < fact['end_time']:
-                #set fact's end time to our start one
-                update = """
-                           UPDATE facts
-                              SET end_time = ?
-                            WHERE id = ?
-                """
-                self.execute(update, (start_time, fact["id"]))
-
-                # now check - maybe we are inside the fact. in that case
-                # we should create another task after our one
-                if end_time and end_time < fact['end_time']:
-                    self.__add_fact(fact['name'], end_time, fact['end_time'])
-
-           
-            else: #end's fine? what about start then?
-                if fact['end_time'] and end_time and fact['start_time'] < end_time < fact['end_time'] \
-                or not fact['end_time'] and end_time and fact['start_time'] < end_time: # case for the entry before the last one
-                    #set fact's start time to our end one
-                    update = """
-                               UPDATE facts
-                                  SET start_time = ?
-                                WHERE id = ?
-                    """
-                    self.execute(update, (end_time, fact["id"]))
-
-        # now check if maybe we are at the last task, and if that's true
-        # look if we maybe have to finish it or delete if it was too short
-        if day_facts:
-            last_fact = day_facts[-1]
-            
-            if last_fact['end_time'] == None and start_time > last_fact['start_time']:
-                delta = (start_time - last_fact['start_time'])
-            
-                if 60 >= delta.seconds >= 0:
-                    self.__remove_fact(last_fact['id'])
-                    start_time = last_fact['start_time']
-
-                    # go further now and check, maybe task before the last one
-                    # is the same we have now. If that happened before less
-                    # than a minute - remove end time!
-                    if len(day_facts) > 2 and day_facts[-2]['name'] == activity_name:
-                        print day_facts[-2]['name'], activity_name, day_facts[-2]['end_time'], start_time
-                        delta = (start_time - day_facts[-2]['end_time'])
-                        if 60 >= delta.seconds >= 0:
-                            update = """
-                                        UPDATE facts
-                                           SET end_time = null
-                                         WHERE id = ?
-                                    """
-                            self.execute(update, (day_facts[-2]["id"],))
-                            return day_facts[-2]
-                else:
-                    #set previous fact end time
-                    update = """
-                               UPDATE facts
-                                  SET end_time = ?
-                                WHERE id = ?
-                    """
-                    self.execute(update, (start_time, fact["id"]))
-
-
-        # finally add the new entry
-        if end_time:
-            insert = """
-                        INSERT INTO facts (activity_id, start_time, end_time)
-                                   VALUES (?, ?, ?)
-            """
-            self.execute(insert, (activity_id, start_time, end_time))
-        else:
-            insert = """
-                        INSERT INTO facts (activity_id, start_time)
-                                   VALUES (?, ?)
-            """
-            self.execute(insert, (activity_id, start_time))
-
-
-        fact_id = self.fetchone("select max(id) as max_id from facts")['max_id']
-        
-        return self.__get_fact(fact_id)
-
-
-    def __get_facts(self, date, end_date = None):
-        query = """
-                   SELECT a.id AS id,
-                          a.start_time AS start_time,
-                          a.end_time AS end_time,
-                          b.name AS name, b.id as activity_id,
-                          coalesce(c.name, ?) as category, c.id as category_id
-                     FROM facts a
-                LEFT JOIN activities b ON a.activity_id = b.id
-                LEFT JOIN categories c on b.category_id = c.id
-                    WHERE date(a.start_time) >= ? and date(a.start_time) <= ?
-                 ORDER BY a.start_time
-        """
-        end_date = end_date or date        
-
-        return self.fetchall(query, (_("Unsorted"), date, end_date))
-
-    def __remove_fact(self, fact_id):
-        query = """
-                   DELETE FROM facts
-                         WHERE id = ?
-        """
-        self.execute(query, (fact_id,))
-
-    def __get_sorted_activities(self):
-        """returns list of acitivities that have categories"""
-        query = """
-                   SELECT a.*, b.category_order
-                     FROM activities a
-                LEFT JOIN categories b on coalesce(b.id, -1) = a.category_id
-                    WHERE a.category_id > -1
-                      AND a.deleted is null
-                 ORDER BY category_order, activity_order
-        """
-        return self.fetchall(query)
-        
-        
-    def __get_activities(self, category_id = None):
-        """returns list of activities, if category is specified, order by name
-           otherwise - by activity_order"""
-        if category_id:
-            query = """
-                       SELECT *
-                         FROM activities
-                        WHERE category_id = ?
-                          AND deleted is null
-            """
-            
-            # unsorted entries we sort by name - others by ID
-            if category_id == -1:
-                query += "ORDER BY lower(name)"
-            else:
-                query += "ORDER BY activity_order"
-                
-            activities = self.fetchall(query, (category_id, ))
-            
-        else:
-            query = """
-                       SELECT *
-                         FROM activities
-                        WHERE deleted is null
-                     ORDER BY lower(name)
-            """
-            activities = self.fetchall(query)
-            
-        return activities
-
-    def __remove_activity(self, id):
-        """ check if we have any facts with this activity and behave accordingly
-            if there are facts - sets activity to deleted = True
-            else, just remove it"""
-        
-        query = "select count(*) as count from facts where activity_id = ?"
-        bound_facts = self.fetchone(query, (id,))['count']
-        
-        if bound_facts > 0:
-            self.execute("UPDATE activities SET deleted = 1 WHERE id = ?", (id,))
-        else:
-            self.execute("delete from activities where id = ?", (id,))
-
-    def __remove_category(self, id):
-        """move all activities to unsorted and remove category"""
-        
-        update = "update activities set category_id = -1 where category_id = ?"
-        self.execute(update, (id, ))
-        
-        self.execute("delete from categories where id = ?", (id, ))
-        
-    
-    def __swap_activities(self, id1, id2):
-        """ swaps nearby activities """
-        # TODO - 2 selects and 2 updates is wrong we could live without selects
-        priority1 = self.fetchone("select activity_order from activities where id = ?", (id1,))[0]
-        priority2 = self.fetchone("select activity_order from activities where id = ?", (id2,))[0]
-        self.execute("update activities set activity_order = ? where id = ?", (priority1, id2) )
-        self.execute("update activities set activity_order = ? where id = ?", (priority2, id1) )
-
-    def __insert_activity(self, name, category_id = -1):
-        new_rec = self.fetchone("select max(id) + 1 , max(activity_order) + 1  from activities")
-        new_id, new_order = 0, 0
-        if new_rec[0] and new_rec[1]: # handle case when we have no activities at all
-            new_id, new_order = new_rec[0], new_rec[1]
-
-        query = """
-                   INSERT INTO activities (id, name, category_id, activity_order)
-                        VALUES (?, ?, ?, ?)
-        """
-        self.execute(query, (new_id, name, category_id, new_order))
-        return new_id
-
-    def __update_activity(self, id, name, category_id):
-        query = """
-                   UPDATE activities
-                       SET name = ?,
-                           category_id = ?
-                     WHERE id = ?
-        """
-        self.execute(query, (name, category_id, id))
-
-    """ Here be dragons (lame connection/cursor wrappers) """
-    def get_connection(self):
-        if self.con == None:
-            self.con = sqlite.connect(hamster.HAMSTER_DB, detect_types=sqlite.PARSE_DECLTYPES|sqlite.PARSE_COLNAMES)
-            self.con.row_factory = sqlite.Row
-
-        return self.con
-
-    connection = property(get_connection, None)
-
-    def fetchall(self, query, params = None):
-        con = self.connection
-        cur = con.cursor()
-
-        print query, params
-
-        if params:
-            cur.execute(query, params)
-        else:
-            cur.execute(query)
-
-        res = cur.fetchall()
-        cur.close()
-
-        return res
-
-    def fetchone(self, query, params = None):
-        res = self.fetchall(query, params)
-        if res:
-            return res[0]
-        else:
-            return None
-
-    def execute(self, statement, params = ()):
-        con = self.connection
-        cur = con.cursor()
-
-        print statement, params
-        res = cur.execute(statement, params)
-
-        con.commit()
-        cur.close()
-
-    def run_fixtures(self):
-        # defaults
-        work_category = {"name": _("Work"),
-                         "entries": [_("Reading news"),
-                                     _("Checking stocks"),
-                                     _("Super secret project X"),
-                                     _("World domination")]}
-        
-        nonwork_category = {"name": _("Day to day"),
-                            "entries": [_("Lunch"),
-                                        _("Watering flowers"),
-                                        _("Doing handstands")]}
-        
-        """upgrade DB to hamster version"""
-        version = self.fetchone("SELECT version FROM version")["version"]
-
-        if version < 2:
-            """moving from fact_date, fact_time to start_time, end_time"""
-    
-            self.execute("""
-                               CREATE TABLE facts_new
-                                            (id integer primary key,
-                                             activity_id integer,
-                                             start_time varchar2(12),
-                                             end_time varchar2(12))
-            """)
-    
-            self.execute("""
-                               INSERT INTO facts_new
-                                           (id, activity_id, start_time)
-                                    SELECT id, activity_id, fact_date || fact_time
-                                      FROM facts
-            """)
-
-            self.execute("DROP TABLE facts")
-            self.execute("ALTER TABLE facts_new RENAME TO facts")
-
-            # run through all facts and set the end time
-            # if previous fact is not on the same date, then it means that it was the
-            # last one in previous, so remove it
-            # this logic saves our last entry from being deleted, which is good
-            facts = self.fetchall("""
-                                        SELECT id, activity_id, start_time,
-                                               substr(start_time,1, 8) start_date
-                                          FROM facts
-                                      ORDER BY start_time
-            """)
-            prev_fact = None
-    
-            for fact in facts:
-                if prev_fact:
-                    if prev_fact['start_date'] == fact['start_date']:
-                        self.execute("UPDATE facts SET end_time = ? where id = ?",
-                                   (fact['start_time'], prev_fact['id']))
-                    else:
-                        #otherwise that's the last entry of the day - remove it
-                        self.execute("DELETE FROM facts WHERE id = ?", (prev_fact["id"],))
-                
-                prev_fact = fact
-
-        #it was kind of silly not to have datetimes in first place
-        if version < 3:
-            self.execute("""
-                               CREATE TABLE facts_new
-                                            (id integer primary key,
-                                             activity_id integer,
-                                             start_time timestamp,
-                                             end_time timestamp)
-            """)
-    
-            self.execute("""
-                               INSERT INTO facts_new
-                                           (id, activity_id, start_time, end_time)
-                                    SELECT id, activity_id,
-                                           substr(start_time,1,4) || "-"
-                                           || substr(start_time, 5, 2) || "-"
-                                           || substr(start_time, 7, 2) || " "
-                                           || substr(start_time, 9, 2) || ":"
-                                           || substr(start_time, 11, 2) || ":00",
-                                           substr(end_time,1,4) || "-"
-                                           || substr(end_time, 5, 2) || "-"
-                                           || substr(end_time, 7, 2) || " "
-                                           || substr(end_time, 9, 2) || ":"
-                                           || substr(end_time, 11, 2) || ":00"
-                                      FROM facts;
-               """)
-
-            self.execute("DROP TABLE facts")
-            self.execute("ALTER TABLE facts_new RENAME TO facts")
-
-
-        #adding categories table to categorize activities
-        if version < 4:
-            #adding the categories table
-            self.execute("""
-                               CREATE TABLE categories
-                                            (id integer primary key,
-                                             name varchar2(500),
-                                             color_code varchar2(50),
-                                             category_order integer)
-            """)
-
-            # adding default categories, and make sure that uncategorized stays on bottom for starters
-            # set order to 2 in case, if we get work in next lines
-            self.execute("""
-                               INSERT INTO categories
-                                           (id, name, category_order)
-                                    VALUES (1, ?, 2);
-               """, (nonwork_category["name"],))
-
-            #check if we have to create work category - consider work everything that has been determined so, and is not deleted
-            work_activities = self.fetchone("""
-                                    SELECT count(*) as work_activities
-                                      FROM activities
-                                     WHERE deleted is null and work=1;
-               """)['work_activities']
-            
-            if work_activities > 0:
-                self.execute("""
-                               INSERT INTO categories
-                                           (id, name, category_order)
-                                    VALUES (2, ?, 1);
-                  """, (work_category["name"],))
-            
-            # now add category field to activities, before starting the move
-            self.execute("""   ALTER TABLE activities
-                                ADD COLUMN category_id integer;
-               """)
-            
-            
-            # starting the move
-            
-            # first remove all deleted activities with no instances in facts
-            self.execute("""
-                               DELETE FROM activities
-                                     WHERE deleted = 1
-                                       AND id not in(select activity_id from facts);
-             """)
-
-            
-            # moving work / non-work to appropriate categories
-            # exploit false/true = 0/1 thing
-            self.execute("""       UPDATE activities
-                                      SET category_id = work + 1
-                                    WHERE deleted is null
-               """)
-            
-            #finally, set category to -1 where there is none            
-            self.execute("""       UPDATE activities
-                                      SET category_id = -1
-                                    WHERE category_id is null
-               """)
-            
-            # drop work column and forget value of deleted
-            # previously deleted records are now unsorted ones
-            # user will be able to mark them as deleted again, in which case
-            # they won't appear in autocomplete, or in categories
-            # ressurection happens, when user enters the exact same name            
-            self.execute("""
-                               CREATE TABLE activities_new (id integer primary key,
-                                                            name varchar2(500),
-                                                            activity_order integer,
-                                                            deleted integer,
-                                                            category_id integer);
-            """)
-    
-            self.execute("""
-                               INSERT INTO activities_new
-                                           (id, name, activity_order, category_id)
-                                    SELECT id, name, activity_order, category_id
-                                      FROM activities;
-               """)
-
-            self.execute("DROP TABLE activities")
-            self.execute("ALTER TABLE activities_new RENAME TO activities")
-            
-
-        #lock down current version
-        self.execute("UPDATE version SET version = 4")
-        
-        
-        """we start with an empty database and then populate with default
-           values. This way defaults can be localized!"""
-        
-        category_count = self.fetchone("select count(*) from categories")[0]
-        
-        if category_count == 0:
-            work_cat_id = self.__insert_category(_(work_category["name"]))
-            for entry in work_category["entries"]:
-                self.__insert_activity(_(entry), work_cat_id)
-        
-            nonwork_cat_id = self.__insert_category(_(nonwork_category["name"]))
-            for entry in nonwork_category["entries"]:
-                self.__insert_activity(_(entry), nonwork_cat_id)
-        
-        
-        
-
Index: hamster/hamster-applet.py
===================================================================
--- hamster/hamster-applet.py	(Revision 339)
+++ hamster/hamster-applet.py	(Arbeitskopie)
@@ -85,10 +85,11 @@
     
     # handle config option to stop tracking on shutdown
     if config.get_stop_on_shutdown():
-        from hamster import storage
-        last_activity = storage.get_last_activity()
+        from hamster import get_db
+        last_activity = get_db().get_last_activity()
         if last_activity['end_time'] == None:
-            storage.touch_fact(last_activity)
+            get_db().touch_fact(last_activity)
+        get_db().deactivate()
         
     gtk.main_quit()
 
Index: hamster/stuff.py
===================================================================
--- hamster/stuff.py	(Revision 339)
+++ hamster/stuff.py	(Arbeitskopie)
@@ -22,7 +22,7 @@
 # cells, columns, trees and other
 
 import gtk
-from hamster import storage
+from hamster import get_db
 from pango import ELLIPSIZE_END
 import datetime as dt
 
@@ -68,7 +68,7 @@
         # Dummy ID to distinct between fact_store, Name, Duration
         self.total_store = gtk.ListStore(int, str, str)
 
-        self.facts = storage.get_facts(date)
+        self.facts = get_db().get_facts(date)
         self.totals = {}
         
         for fact in self.facts:
Index: data/paymo-configuration.glade
===================================================================
--- data/paymo-configuration.glade	(Revision 0)
+++ data/paymo-configuration.glade	(Revision 0)
@@ -0,0 +1,199 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
+<!--Generated with glade3 3.4.0 on Sun Aug  3 12:33:57 2008 -->
+<glade-interface>
+  <widget class="GtkWindow" id="paymo_configuration_window">
+    <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+    <property name="title" translatable="yes">Paymo Configuration</property>
+    <property name="modal">True</property>
+    <child>
+      <widget class="GtkVBox" id="vbox1">
+        <property name="visible">True</property>
+        <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+        <property name="spacing">15</property>
+        <child>
+          <widget class="GtkFrame" id="frame1">
+            <property name="visible">True</property>
+            <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+            <property name="label_xalign">0</property>
+            <property name="shadow_type">GTK_SHADOW_NONE</property>
+            <child>
+              <widget class="GtkAlignment" id="alignment1">
+                <property name="visible">True</property>
+                <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+                <property name="left_padding">12</property>
+                <child>
+                  <widget class="GtkVBox" id="vbox2">
+                    <property name="visible">True</property>
+                    <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+                    <child>
+                      <widget class="GtkHBox" id="hbox1">
+                        <property name="visible">True</property>
+                        <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+                        <property name="spacing">8</property>
+                        <child>
+                          <widget class="GtkLabel" id="label2">
+                            <property name="width_request">18</property>
+                            <property name="visible">True</property>
+                            <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+                            <property name="label" translatable="yes">API-Key</property>
+                          </widget>
+                        </child>
+                        <child>
+                          <widget class="GtkEntry" id="api_key_entry">
+                            <property name="visible">True</property>
+                            <property name="sensitive">False</property>
+                            <property name="can_focus">True</property>
+                            <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+                          </widget>
+                          <packing>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
+                      </widget>
+                    </child>
+                    <child>
+                      <widget class="GtkHBox" id="hbox4">
+                        <property name="visible">True</property>
+                        <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+                        <property name="spacing">8</property>
+                        <child>
+                          <widget class="GtkLabel" id="label3">
+                            <property name="width_request">18</property>
+                            <property name="visible">True</property>
+                            <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+                            <property name="label" translatable="yes">Hostname</property>
+                          </widget>
+                        </child>
+                        <child>
+                          <widget class="GtkEntry" id="hostname_entry">
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+                          </widget>
+                          <packing>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
+                      </widget>
+                      <packing>
+                        <property name="position">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <widget class="GtkHBox" id="hbox5">
+                        <property name="visible">True</property>
+                        <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+                        <property name="spacing">8</property>
+                        <child>
+                          <widget class="GtkLabel" id="label4">
+                            <property name="width_request">18</property>
+                            <property name="visible">True</property>
+                            <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+                            <property name="label" translatable="yes">Username</property>
+                          </widget>
+                        </child>
+                        <child>
+                          <widget class="GtkEntry" id="username_entry">
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+                          </widget>
+                          <packing>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
+                      </widget>
+                      <packing>
+                        <property name="position">2</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <widget class="GtkHBox" id="hbox6">
+                        <property name="visible">True</property>
+                        <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+                        <property name="spacing">8</property>
+                        <child>
+                          <widget class="GtkLabel" id="label5">
+                            <property name="width_request">18</property>
+                            <property name="visible">True</property>
+                            <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+                            <property name="label" translatable="yes">Password</property>
+                          </widget>
+                        </child>
+                        <child>
+                          <widget class="GtkEntry" id="password_entry">
+                            <property name="visible">True</property>
+                            <property name="can_focus">True</property>
+                            <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+                          </widget>
+                          <packing>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
+                      </widget>
+                      <packing>
+                        <property name="position">3</property>
+                      </packing>
+                    </child>
+                  </widget>
+                </child>
+              </widget>
+            </child>
+            <child>
+              <widget class="GtkLabel" id="label1">
+                <property name="visible">True</property>
+                <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+                <property name="label" translatable="yes">&lt;b&gt;Connection&lt;/b&gt;</property>
+                <property name="use_markup">True</property>
+              </widget>
+              <packing>
+                <property name="type">label_item</property>
+              </packing>
+            </child>
+          </widget>
+        </child>
+        <child>
+          <widget class="GtkViewport" id="viewport1">
+            <property name="visible">True</property>
+            <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+            <property name="resize_mode">GTK_RESIZE_QUEUE</property>
+            <child>
+              <widget class="GtkHBox" id="hbox7">
+                <property name="visible">True</property>
+                <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+                <property name="spacing">20</property>
+                <child>
+                  <widget class="GtkButton" id="ok_button">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="receives_default">True</property>
+                    <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+                    <property name="label" translatable="yes">Ok</property>
+                    <property name="response_id">0</property>
+                  </widget>
+                </child>
+                <child>
+                  <widget class="GtkButton" id="abort_button">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="receives_default">True</property>
+                    <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+                    <property name="label" translatable="yes">Abort</property>
+                    <property name="response_id">0</property>
+                  </widget>
+                  <packing>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+              </widget>
+            </child>
+          </widget>
+          <packing>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </widget>
+    </child>
+  </widget>
+</glade-interface>
Index: data/preferences.glade
===================================================================
--- data/preferences.glade	(Revision 339)
+++ data/preferences.glade	(Arbeitskopie)
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
-<!--Generated with glade3 3.4.5 on Fri May 23 01:06:41 2008 -->
+<!--Generated with glade3 3.4.0 on Sun Aug  3 10:43:17 2008 -->
 <glade-interface>
   <widget class="GtkWindow" id="preferences_window">
     <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
@@ -38,52 +38,51 @@
                         <property name="column_spacing">4</property>
                         <property name="row_spacing">4</property>
                         <child>
-                          <widget class="GtkVBox" id="vbox3">
-                            <property name="width_request">150</property>
+                          <widget class="GtkAlignment" id="alignment10">
                             <property name="visible">True</property>
-                            <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
-                            <property name="spacing">4</property>
                             <child>
-                              <widget class="GtkLabel" id="categories_label">
-                                <property name="visible">True</property>
-                                <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
-                                <property name="xalign">0</property>
-                                <property name="label" translatable="yes">_Categories</property>
-                                <property name="use_underline">True</property>
-                              </widget>
-                              <packing>
-                                <property name="expand">False</property>
-                              </packing>
+                              <placeholder/>
                             </child>
+                          </widget>
+                          <packing>
+                            <property name="left_attach">1</property>
+                            <property name="right_attach">2</property>
+                            <property name="top_attach">1</property>
+                            <property name="bottom_attach">2</property>
+                            <property name="y_options">GTK_FILL</property>
+                          </packing>
+                        </child>
+                        <child>
+                          <widget class="GtkAlignment" id="alignment7">
+                            <property name="visible">True</property>
+                            <property name="xalign">1</property>
+                            <property name="xscale">0</property>
                             <child>
-                              <widget class="GtkScrolledWindow" id="scrolledwindow2">
+                              <widget class="GtkButton" id="add_category">
                                 <property name="visible">True</property>
                                 <property name="can_focus">True</property>
+                                <property name="receives_default">True</property>
                                 <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
-                                <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
-                                <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
-                                <property name="shadow_type">GTK_SHADOW_IN</property>
+                                <property name="response_id">0</property>
+                                <signal name="clicked" handler="on_add_category_clicked"/>
                                 <child>
-                                  <widget class="GtkTreeView" id="category_list">
+                                  <widget class="GtkLabel" id="label4">
                                     <property name="visible">True</property>
-                                    <property name="can_focus">True</property>
                                     <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
-                                    <property name="headers_visible">False</property>
-                                    <property name="enable_search">False</property>
-                                    <signal name="button_press_event" handler="on_category_list_button_pressed"/>
-                                    <signal name="drag_motion" handler="on_category_list_drag_motion"/>
-                                    <signal name="key_press_event" handler="on_category_list_key_pressed"/>
-                                    <signal name="button_release_event" handler="on_category_list_button_released"/>
+                                    <property name="xpad">4</property>
+                                    <property name="ypad">2</property>
+                                    <property name="label" translatable="yes">N_ew Category</property>
+                                    <property name="use_underline">True</property>
                                   </widget>
                                 </child>
                               </widget>
-                              <packing>
-                                <property name="position">1</property>
-                              </packing>
                             </child>
                           </widget>
                           <packing>
-                            <property name="x_options"></property>
+                            <property name="top_attach">1</property>
+                            <property name="bottom_attach">2</property>
+                            <property name="x_options">GTK_FILL</property>
+                            <property name="y_options"></property>
                           </packing>
                         </child>
                         <child>
@@ -270,53 +269,54 @@
                           </packing>
                         </child>
                         <child>
-                          <widget class="GtkAlignment" id="alignment7">
+                          <widget class="GtkVBox" id="vbox3">
+                            <property name="width_request">150</property>
                             <property name="visible">True</property>
-                            <property name="xalign">1</property>
-                            <property name="xscale">0</property>
+                            <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+                            <property name="spacing">4</property>
                             <child>
-                              <widget class="GtkButton" id="add_category">
+                              <widget class="GtkLabel" id="categories_label">
                                 <property name="visible">True</property>
+                                <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+                                <property name="xalign">0</property>
+                                <property name="label" translatable="yes">_Categories</property>
+                                <property name="use_underline">True</property>
+                              </widget>
+                              <packing>
+                                <property name="expand">False</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <widget class="GtkScrolledWindow" id="scrolledwindow2">
+                                <property name="visible">True</property>
                                 <property name="can_focus">True</property>
-                                <property name="receives_default">True</property>
                                 <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
-                                <property name="response_id">0</property>
-                                <signal name="clicked" handler="on_add_category_clicked"/>
+                                <property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
+                                <property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
+                                <property name="shadow_type">GTK_SHADOW_IN</property>
                                 <child>
-                                  <widget class="GtkLabel" id="label4">
+                                  <widget class="GtkTreeView" id="category_list">
                                     <property name="visible">True</property>
+                                    <property name="can_focus">True</property>
                                     <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
-                                    <property name="xpad">4</property>
-                                    <property name="ypad">2</property>
-                                    <property name="label" translatable="yes">N_ew Category</property>
-                                    <property name="use_underline">True</property>
+                                    <property name="headers_visible">False</property>
+                                    <property name="enable_search">False</property>
+                                    <signal name="button_press_event" handler="on_category_list_button_pressed"/>
+                                    <signal name="drag_motion" handler="on_category_list_drag_motion"/>
+                                    <signal name="key_press_event" handler="on_category_list_key_pressed"/>
+                                    <signal name="button_release_event" handler="on_category_list_button_released"/>
                                   </widget>
                                 </child>
                               </widget>
+                              <packing>
+                                <property name="position">1</property>
+                              </packing>
                             </child>
                           </widget>
                           <packing>
-                            <property name="top_attach">1</property>
-                            <property name="bottom_attach">2</property>
-                            <property name="x_options">GTK_FILL</property>
-                            <property name="y_options"></property>
+                            <property name="x_options"></property>
                           </packing>
                         </child>
-                        <child>
-                          <widget class="GtkAlignment" id="alignment10">
-                            <property name="visible">True</property>
-                            <child>
-                              <placeholder/>
-                            </child>
-                          </widget>
-                          <packing>
-                            <property name="left_attach">1</property>
-                            <property name="right_attach">2</property>
-                            <property name="top_attach">1</property>
-                            <property name="bottom_attach">2</property>
-                            <property name="y_options">GTK_FILL</property>
-                          </packing>
-                        </child>
                       </widget>
                     </child>
                   </widget>
@@ -472,6 +472,64 @@
                 <property name="position">2</property>
               </packing>
             </child>
+            <child>
+              <widget class="GtkFrame" id="frame5">
+                <property name="visible">True</property>
+                <property name="label_xalign">0</property>
+                <property name="shadow_type">GTK_SHADOW_NONE</property>
+                <child>
+                  <widget class="GtkAlignment" id="alignment11">
+                    <property name="visible">True</property>
+                    <property name="xalign">0</property>
+                    <property name="xscale">0.30000001192092896</property>
+                    <property name="top_padding">8</property>
+                    <property name="left_padding">12</property>
+                    <child>
+                      <widget class="GtkHBox" id="hbox1">
+                        <property name="visible">True</property>
+                        <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+                        <property name="spacing">8</property>
+                        <child>
+                          <widget class="GtkComboBox" id="storage_selection">
+                            <property name="visible">True</property>
+                            <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+                            <property name="items" translatable="yes"></property>
+                          </widget>
+                        </child>
+                        <child>
+                          <widget class="GtkButton" id="storage_configuration">
+                            <property name="visible">True</property>
+                            <property name="sensitive">False</property>
+                            <property name="can_focus">True</property>
+                            <property name="receives_default">True</property>
+                            <property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
+                            <property name="label" translatable="yes">Config</property>
+                            <property name="response_id">0</property>
+                          </widget>
+                          <packing>
+                            <property name="position">1</property>
+                          </packing>
+                        </child>
+                      </widget>
+                    </child>
+                  </widget>
+                </child>
+                <child>
+                  <widget class="GtkLabel" id="label9">
+                    <property name="visible">True</property>
+                    <property name="label" translatable="yes">&lt;b&gt;Data Storage&lt;/b&gt;</property>
+                    <property name="use_markup">True</property>
+                  </widget>
+                  <packing>
+                    <property name="type">label_item</property>
+                  </packing>
+                </child>
+              </widget>
+              <packing>
+                <property name="expand">False</property>
+                <property name="position">3</property>
+              </packing>
+            </child>
           </widget>
         </child>
         <child>
