[hamster-applet] refactored parse_activity_input into Fact class. WARNING: possible breakages (did unit tests on pars



commit b220140b7b0fb041be53b8ddcba7cdabe47b5767
Author: Toms Bauģis <toms baugis gmail com>
Date:   Sat Aug 21 21:39:18 2010 +0100

    refactored parse_activity_input into Fact class. WARNING: possible breakages (did unit tests on parsing though but there are many bits to check)

 src/hamster-cli                      |    4 +-
 src/hamster-time-tracker             |   66 +++++++------
 src/hamster/applet.py                |   65 +++++++------
 src/hamster/client.py                |   76 +++++++---------
 src/hamster/db.py                    |   78 +++++-----------
 src/hamster/edit_activity.py         |   37 ++-----
 src/hamster/overview_activities.py   |   24 ++---
 src/hamster/storage.py               |   17 +--
 src/hamster/utils/stuff.py           |  168 ++++++++++++++++++++--------------
 src/hamster/utils/trophies.py        |   30 +++----
 src/hamster/widgets/activityentry.py |   31 +++---
 tests/stuff_test.py                  |   76 ++++++++-------
 12 files changed, 321 insertions(+), 351 deletions(-)
---
diff --git a/src/hamster-cli b/src/hamster-cli
index 3fc2c9d..394cb10 100755
--- a/src/hamster-cli
+++ b/src/hamster-cli
@@ -45,7 +45,9 @@ class HamsterClient(object):
 
     def start_tracking(self, activity, start_time = None, end_time = None):
         '''Start a new activity.'''
-        self.storage.add_fact(activity, start_time = start_time, end_time = end_time)
+        self.storage.add_fact(stuff.Fact(activity,
+                                         start_time = start_time,
+                                         end_time = end_time))
 
 
     def stop_tracking(self):
diff --git a/src/hamster-time-tracker b/src/hamster-time-tracker
index 946eaf4..cd396a4 100755
--- a/src/hamster-time-tracker
+++ b/src/hamster-time-tracker
@@ -259,12 +259,12 @@ class ProjectHamster(object):
 
     def on_today_row_activated(self, tree, path, column):
         fact = tree.get_selected_fact()
-
-        if fact:
-            runtime.storage.add_fact(fact["name"],
-                                     ", ".join(fact["tags"]),
-                                     category_name = fact["category"],
-                                     description = fact["description"])
+        fact = stuff.Fact(fact["name"],
+                    tags = ", ".join(fact["tags"]),
+                    category = fact["category"],
+                    description = fact["description"])
+        if fact.activity:
+            runtime.storage.add_fact(fact)
 
 
     """button events"""
@@ -328,24 +328,24 @@ class ProjectHamster(object):
             # first try to look up activity by desktop name
             mapping = conf.get("workspace_mapping")
 
-            parsed_activity = None
+            fact = None
             if new < len(mapping):
-                parsed_activity = stuff.parse_activity_input(mapping[new])
+                fact = stuff.Fact(mapping[new])
 
-            if parsed_activity:
-                category_id = None
-                if parsed_activity.category_name:
-                    category_id = runtime.storage.get_category_id(parsed_activity.category_name)
+                if fact.activity:
+                    category_id = None
+                    if fact.category:
+                        category_id = runtime.storage.get_category_id(fact.category)
 
-                activity = runtime.storage.get_activity_by_name(parsed_activity.activity_name,
-                                                                category_id,
-                                                                ressurect = False)
-                if activity:
-                    # we need dict below
-                    activity = dict(name = activity['name'],
-                                    category = activity['category'],
-                                    description = parsed_activity.description,
-                                    tags = parsed_activity.tags)
+                    activity = runtime.storage.get_activity_by_name(fact.activity,
+                                                                    category_id,
+                                                                    ressurect = False)
+                    if activity:
+                        # we need dict below
+                        activity = dict(name = activity['name'],
+                                        category = activity['category'],
+                                        description = fact.description,
+                                        tags = fact.tags)
 
 
         if not activity and "memory" in self.workspace_tracking:
@@ -366,10 +366,11 @@ class ProjectHamster(object):
             return
 
         # ok, switch
-        runtime.storage.add_fact(activity['name'],
-                                 ", ".join(activity['tags']),
-                                 category_name = activity['category'],
-                                 description = activity['description'])
+        fact = stuff.Fact(activity['name'],
+                          tags = ", ".join(activity['tags']),
+                          category = activity['category'],
+                          description = activity['description']);
+        runtime.storage.add_fact(fact)
 
         if self.notification:
             self.notification.update(_("Changed activity"),
@@ -403,11 +404,15 @@ class ProjectHamster(object):
         self.get_widget("switch_activity").set_sensitive(widget.get_text() != "")
 
     def on_switch_activity_clicked(self, widget):
-        if not self.new_name.get_text():
-            return False
+        activity, temporary = self.new_name.get_value()
+
+        fact = stuff.Fact(activity,
+                          tags = self.new_tags.get_text().decode("utf8", "replace"),
+                          temporary = temporary)
+        if not fact.activity:
+            return
 
-        runtime.storage.add_fact(self.new_name.get_text().decode("utf8", "replace"),
-                                 self.new_tags.get_text().decode("utf8", "replace"))
+        runtime.storage.add_fact(fact)
         self.new_name.set_text("")
         self.new_tags.set_text("")
 
@@ -515,7 +520,8 @@ if __name__ == "__main__":
     gtk.gdk.threads_init()
     gtk.window_set_default_icon_name("hamster-applet")
 
-    from hamster.utils import stuff, widgets, idle, trophies
+    from hamster import widgets, idle
+    from hamster.utils import stuff, trophies
 
     try:
         import wnck
diff --git a/src/hamster/applet.py b/src/hamster/applet.py
index d747202..ecc51f2 100755
--- a/src/hamster/applet.py
+++ b/src/hamster/applet.py
@@ -508,12 +508,13 @@ class HamsterApplet(object):
 
     def on_today_row_activated(self, tree, path, column):
         fact = tree.get_selected_fact()
+        fact = stuff.Fact(fact["name"],
+                    tags = ", ".join(fact["tags"]),
+                    category = fact["category"],
+                    description = fact["description"])
 
-        if fact:
-            runtime.storage.add_fact(fact["name"],
-                                     ", ".join(fact["tags"]),
-                                     category_name = fact["category"],
-                                     description = fact["description"])
+        if fact.activity:
+            runtime.storage.add_fact(fact)
             self.__show_toggle(False)
 
 
@@ -587,30 +588,29 @@ class HamsterApplet(object):
         # on switch, update our mapping between spaces and activities
         self.workspace_activities[prev] = self.last_activity
 
-
         activity = None
         if "name" in self.workspace_tracking:
             # first try to look up activity by desktop name
             mapping = conf.get("workspace_mapping")
 
-            parsed_activity = None
+            fact = None
             if new < len(mapping):
-                parsed_activity = stuff.parse_activity_input(mapping[new])
+                fact = stuff.Fact(mapping[new])
 
-            if parsed_activity:
-                category_id = None
-                if parsed_activity.category_name:
-                    category_id = runtime.storage.get_category_id(parsed_activity.category_name)
+                if fact.activity:
+                    category_id = None
+                    if fact.category:
+                        category_id = runtime.storage.get_category_id(fact.category)
 
-                activity = runtime.storage.get_activity_by_name(parsed_activity.activity_name,
-                                                                category_id,
-                                                                resurrect = False)
-                if activity:
-                    # we need dict below
-                    activity = dict(name = activity['name'],
-                                    category = activity['category'],
-                                    description = parsed_activity.description,
-                                    tags = parsed_activity.tags)
+                    activity = runtime.storage.get_activity_by_name(fact.activity,
+                                                                    category_id,
+                                                                    ressurect = False)
+                    if activity:
+                        # we need dict below
+                        activity = dict(name = activity['name'],
+                                        category = activity['category'],
+                                        description = fact.description,
+                                        tags = fact.tags)
 
 
         if not activity and "memory" in self.workspace_tracking:
@@ -631,10 +631,11 @@ class HamsterApplet(object):
             return
 
         # ok, switch
-        runtime.storage.add_fact(activity['name'],
-                                 ", ".join(activity['tags']),
-                                 category_name = activity['category'],
-                                 description = activity['description'])
+        fact = stuff.Fact(activity['name'],
+                          tags = ", ".join(activity['tags']),
+                          category = activity['category'],
+                          description = activity['description']);
+        runtime.storage.add_fact(fact)
 
         if self.notification:
             self.notification.update(_("Changed activity"),
@@ -668,13 +669,15 @@ class HamsterApplet(object):
         self.get_widget("switch_activity").set_sensitive(widget.get_text() != "")
 
     def on_switch_activity_clicked(self, widget):
-        activity_name, temporary = self.new_name.get_value()
-        if not activity_name:
-            return False
+        activity, temporary = self.new_name.get_value()
+
+        fact = stuff.Fact(activity,
+                          tags = self.new_tags.get_text().decode("utf8", "replace"),
+                          temporary = temporary)
+        if not fact.activity:
+            return
 
-        runtime.storage.add_fact(self.new_name.get_text().decode("utf8", "replace"),
-                                 self.new_tags.get_text().decode("utf8", "replace"),
-                                 temporary = temporary)
+        runtime.storage.add_fact(fact)
         self.new_name.set_text("")
         self.new_tags.set_text("")
         self.__show_toggle(False)
diff --git a/src/hamster/client.py b/src/hamster/client.py
index 6fad885..007240b 100644
--- a/src/hamster/client.py
+++ b/src/hamster/client.py
@@ -23,7 +23,8 @@ import datetime as dt
 from calendar import timegm
 import dbus, dbus.mainloop.glib
 import gobject
-from utils.trophies import checker
+from utils import stuff, trophies
+
 
 
 def from_dbus_fact(fact):
@@ -162,39 +163,37 @@ class Storage(gobject.GObject):
         """returns fact by it's ID"""
         return from_dbus_fact(self.conn.GetFact(id))
 
-    def add_fact(self, activity_name, tags = '', start_time = None, end_time = None,
-                                      category_name = None, description = None,
-                                      temporary = False):
-        """Add fact. activity name can use `[-]start_time[-end_time] activity category, description #tag1 #tag2`
+    def add_fact(self, fact):
+        """Add fact. activity name can use the
+        `[-]start_time[-end_time] activity category, description #tag1 #tag2`
         syntax, or params can be stated explicitly.
-        Params, except for the start and end times will take precedence over
-        derived values.
+        Params will take precedence over the derived values.
         start_time defaults to current moment.
         """
+        if not fact.activity:
+            return None
+
+        fact.start_time = fact.start_time or dt.datetime.now()
 
+        serialized = fact.serialized_name()
 
-        start_timestamp = start_time or 0
+        start_timestamp = fact.start_time or 0
         if start_timestamp:
-            start_timestamp = timegm(start_time.timetuple())
+            start_timestamp = timegm(fact.start_time.timetuple())
 
-        end_timestamp = end_time or 0
+        end_timestamp = fact.end_time or 0
         if end_timestamp:
-            end_timestamp = timegm(end_time.timetuple())
-
-        if isinstance(tags, list): #make sure we send what storage expects
-            tags = ", ".join(tags)
-        tags = tags or ''
-
-        category_name = category_name or ''
-        description = description or ''
+            end_timestamp = timegm(fact.end_time.timetuple())
 
-        new_id = self.conn.AddFact(activity_name, tags, start_timestamp, end_timestamp, category_name, description, temporary)
+        new_id = self.conn.AddFact(serialized,
+                                   start_timestamp,
+                                   end_timestamp,
+                                   fact.temporary)
 
         # TODO - the parsing should happen just once and preferably here
         # we should feed (serialized_activity, start_time, end_time) into AddFact and others
         if new_id:
-            checker.check_fact_based(activity_name, tags, start_time, end_time, category_name, description)
-
+            trophies.checker.check_fact_based(fact)
         return new_id
 
     def stop_tracking(self, end_time = None):
@@ -207,36 +206,27 @@ class Storage(gobject.GObject):
         "delete fact from database"
         self.conn.RemoveFact(fact_id)
 
-    def update_fact(self, fact_id, activity_name, tags = None,
-                    start_time = None, end_time = None,
-                    category_name = None, description = None, temporary = False):
+    def update_fact(self, fact_id, fact):
         """Update fact values. See add_fact for rules.
         Update is performed via remove/insert, so the
         fact_id after update should not be used anymore. Instead use the ID
         from the fact dict that is returned by this function"""
 
-        category_name = category_name or '' # so to override None
-        description = description or '' # so to override None
-
-        start_time = start_time or 0
-        if start_time:
-            start_time = timegm(start_time.timetuple())
-
-        end_time = end_time or 0
-        if end_time:
-            end_time = timegm(end_time.timetuple())
+        start_time = fact.start_time or 0
+        if fact.start_time:
+            start_time = timegm(fact.start_time.timetuple())
 
-        if tags and isinstance(tags, list):
-            tags = ", ".join(tags)
-        tags = tags or ''
+        end_time = fact.end_time or 0
+        if fact.end_time:
+            end_time = timegm(fact.end_time.timetuple())
 
-        new_id =  self.conn.UpdateFact(fact_id, activity_name, tags,
-                                       start_time, end_time,
-                                       category_name, description, temporary)
+        new_id =  self.conn.UpdateFact(fact_id,
+                                       fact.serialized_name(),
+                                       start_time,
+                                       end_time,
+                                       fact.temporary)
 
-        checker.check_update_based(fact_id, new_id, activity_name, tags,
-                                   start_time, end_time,
-                                   category_name, description)
+        trophies.checker.check_update_based(fact_id, new_id, fact)
         return new_id
 
 
diff --git a/src/hamster/db.py b/src/hamster/db.py
index 1925d7f..3afbb74 100644
--- a/src/hamster/db.py
+++ b/src/hamster/db.py
@@ -35,13 +35,13 @@ except ImportError:
 import os, time
 import datetime
 import storage
-from utils import stuff
 from shutil import copy as copyfile
+import itertools
 import datetime as dt
 import gio
 from xdg.BaseDirectory import xdg_data_home
 
-import itertools
+from utils import stuff, trophies
 
 class Storage(storage.Storage):
     con = None # Connection will be created on demand
@@ -74,11 +74,7 @@ class Storage(storage.Storage):
                 self.dispatch_overwrite()
 
                 # plan "b" â?? synchronize the time tracker's database from external source while the tracker is running
-                try:
-                    import trophies
-                    trophies.unlock("plan_b")
-                except:
-                    pass
+                trophies.unlock("plan_b")
 
 
         self.__database_file = gio.File(self.db_path)
@@ -493,36 +489,20 @@ class Storage(storage.Storage):
                              (start_time, fact["id"]))
 
 
-    def __add_fact(self, activity_name, tags, start_time = None,
-                   end_time = None, category_name = None,
-                   description = None, temporary = False):
+    def __add_fact(self, activity_name, start_time, end_time = None, temporary = False):
+        fact = stuff.Fact(activity_name,
+                          start_time = start_time,
+                          end_time = end_time,
+                          temporary = temporary)
 
-        activity = stuff.parse_activity_input(activity_name)
-
-        # make sure that we do have an activity name after parsing
-        if not activity.activity_name:
+        if not fact.activity or start_time is None:  # sanity check
             return 0
 
-        # explicitly stated takes precedence
-        activity.description = description or activity.description
-
-        tags = [tag.strip() for tag in tags.split(",") if tag.strip()]  # split by comma
-        tags = tags or activity.tags # explicitly stated tags take priority
-
-        tags = [dict(zip(('id', 'name', 'autocomplete'), row)) for row in self.GetTagIds(tags)] #this will create any missing tags too
-
-
-        if category_name:
-            activity.category_name = category_name
-        if description:
-            activity.description = description #override
 
-        start_time = activity.start_time or start_time or datetime.datetime.now()
+        # get tags from database - this will create any missing tags too
+        tags = [dict(zip(('id', 'name', 'autocomplete'), row))
+                                           for row in self.GetTagIds(fact.tags)]
 
-        start_time = start_time.replace(microsecond = 0)
-        end_time = activity.end_time or end_time
-        if end_time:
-            end_time = end_time.replace(microsecond = 0)
 
         now = datetime.datetime.now()
         # if in future - roll back to past
@@ -537,29 +517,25 @@ class Storage(storage.Storage):
                 end_time -= dt.timedelta(days = 1)
 
 
-        if not start_time or not activity.activity_name:  # sanity check
-            return None
-
         # now check if maybe there is also a category
         category_id = None
-        if activity.category_name:
-            category_id = self.__get_category_id(activity.category_name)
+        if fact.category:
+            category_id = self.__get_category_id(fact.category)
             if not category_id:
-                category_id = self.__add_category(activity.category_name)
+                category_id = self.__add_category(fact.category)
 
         # try to find activity, resurrect if not temporary
-        activity_id = self.__get_activity_by_name(activity.activity_name,
+        activity_id = self.__get_activity_by_name(fact.activity,
                                                   category_id,
                                                   resurrect = not temporary)
         if not activity_id:
-            activity_id = self.__add_activity(activity.activity_name,
+            activity_id = self.__add_activity(fact.activity,
                                               category_id, temporary)
         else:
             activity_id = activity_id['id']
 
         # if we are working on +/- current day - check the last_activity
         if (dt.datetime.now() - start_time <= dt.timedelta(days=1)):
-
             # pull in previous facts
             facts = self.__get_todays_facts()
 
@@ -570,8 +546,8 @@ class Storage(storage.Storage):
             if previous and previous['start_time'] < start_time:
                 # check if maybe that is the same one, in that case no need to restart
                 if previous["activity_id"] == activity_id \
-                   and previous["tags"] == sorted([tag["name"] for tag in tags]) \
-                   and (previous["description"] or "") == (description or ""):
+                   and set(previous["tags"]) == set([tag["name"] for tag in tags]) \
+                   and (previous["description"] or "") == (fact.description or ""):
                     return None
 
                 # otherwise, if no description is added
@@ -618,7 +594,7 @@ class Storage(storage.Storage):
                     INSERT INTO facts (activity_id, start_time, end_time, description)
                                VALUES (?, ?, ?, ?)
         """
-        self.execute(insert, (activity_id, start_time, end_time, activity.description))
+        self.execute(insert, (activity_id, start_time, end_time, fact.description))
 
         fact_id = self.__last_insert_rowid()
 
@@ -792,11 +768,7 @@ class Storage(storage.Storage):
 
         # Finished! - deleted an activity with more than 50 facts on it
         if bound_facts >= 50:
-            try:
-                import trophies
-                trophies.unlock("finished")
-            except:
-                pass
+            trophies.unlock("finished")
 
     def __remove_category(self, id):
         """move all activities to unsorted and remove category"""
@@ -1200,12 +1172,8 @@ class Storage(storage.Storage):
             self.execute("UPDATE version SET version = %d" % current_version)
             print "updated database from version %d to %d" % (version, current_version)
 
-            try:
-                # oldtimer â?? database version structure had been performed on startup (thus we know that he has been on at least 2 versions)
-                import trophies
-                trophies.unlock("oldtimer")
-            except:
-                pass
+            # oldtimer â?? database version structure had been performed on startup (thus we know that he has been on at least 2 versions)
+            trophies.unlock("oldtimer")
 
 
         """we start with an empty database and then populate with default
diff --git a/src/hamster/edit_activity.py b/src/hamster/edit_activity.py
index d80fb7b..e23919f 100644
--- a/src/hamster/edit_activity.py
+++ b/src/hamster/edit_activity.py
@@ -190,40 +190,25 @@ class CustomFactController:
 
     def on_save_button_clicked(self, button):
         activity_name, temporary = self.new_name.get_value()
-        parsed = stuff.parse_activity_input(activity_name)
-
-        if not parsed.activity_name:
-            return False
-
-        # explicit takes precedence
-        description = self.figure_description()
-
-        tags = self.new_tags.get_text().decode('utf8')
-
-        start_time = self._get_datetime("start")
 
         if self.get_widget("in_progress").get_active():
             end_time = None
         else:
             end_time = self._get_datetime("end")
 
+        fact = stuff.Fact(activity_name,
+                          description = self.figure_description(),
+                          tags = self.new_tags.get_text().decode('utf8'),
+                          start_time = self._get_datetime("start"),
+                          end_time = end_time,
+                          temporary = temporary)
+        if not fact.activity:
+            return False
+
         if self.fact_id:
-            runtime.storage.update_fact(self.fact_id,
-                                        activity_name,
-                                        tags,
-                                        start_time,
-                                        end_time,
-                                        parsed.category_name,
-                                        description,
-                                        temporary = temporary)
+            runtime.storage.update_fact(self.fact_id, fact)
         else:
-            runtime.storage.add_fact(activity_name,
-                                     tags,
-                                     start_time,
-                                     end_time,
-                                     parsed.category_name,
-                                     description,
-                                     temporary = temporary)
+            runtime.storage.add_fact(fact)
 
         self.close_window()
 
diff --git a/src/hamster/overview_activities.py b/src/hamster/overview_activities.py
index 2d0dffe..70b09b0 100644
--- a/src/hamster/overview_activities.py
+++ b/src/hamster/overview_activities.py
@@ -165,24 +165,18 @@ class OverviewBox(gtk.VBox):
         else:
             selected_date = fact["date"]
 
-        fact = stuff.parse_activity_input(text.decode("utf-8"))
+        fact = stuff.Fact(text.decode("utf-8"))
 
-        if not all((fact.activity_name, fact.start_time, fact.end_time)):
+        if not all((fact.activity, fact.start_time, fact.end_time)):
             return
 
-        start_time = fact.start_time.replace(year = selected_date.year,
-                                             month = selected_date.month,
-                                             day = selected_date.day)
-        end_time = fact.end_time.replace(year = selected_date.year,
-                                         month = selected_date.month,
-                                         day = selected_date.day)
-
-        new_id = runtime.storage.add_fact(fact.activity_name,
-                                          ", ".join(fact.tags),
-                                          start_time,
-                                          end_time,
-                                          fact.category_name,
-                                          fact.description)
+        fact.start_time = fact.start_time.replace(year = selected_date.year,
+                                                  month = selected_date.month,
+                                                  day = selected_date.day)
+        fact.end_time = fact.end_time.replace(year = selected_date.year,
+                                              month = selected_date.month,
+                                              day = selected_date.day)
+        new_id = runtime.storage.add_fact(fact)
 
         # You can do that?! - copy/pasted an activity
         trophies.unlock("can_do_that")
diff --git a/src/hamster/storage.py b/src/hamster/storage.py
index 91baf6f..b7e7159 100644
--- a/src/hamster/storage.py
+++ b/src/hamster/storage.py
@@ -104,10 +104,8 @@ class Storage(dbus.service.Object):
         self.ToggleCalled()
 
     # facts
-    @dbus.service.method("org.gnome.Hamster", in_signature='ssiissb', out_signature='i')
-    def AddFact(self, activity_name, tags, start_time, end_time,
-                category_name = None, description = None, temporary = False):
-
+    @dbus.service.method("org.gnome.Hamster", in_signature='siib', out_signature='i')
+    def AddFact(self, fact, start_time, end_time, temporary = False):
         start_time = start_time or None
         if start_time:
             start_time = dt.datetime.utcfromtimestamp(start_time)
@@ -117,7 +115,7 @@ class Storage(dbus.service.Object):
             end_time = dt.datetime.utcfromtimestamp(end_time)
 
         self.start_transaction()
-        result = self.__add_fact(activity_name, tags, start_time, end_time, category_name, description, temporary)
+        result = self.__add_fact(fact, start_time, end_time, temporary)
         self.end_transaction()
 
         if result:
@@ -135,10 +133,8 @@ class Storage(dbus.service.Object):
         return to_dbus_fact(fact)
 
 
-    @dbus.service.method("org.gnome.Hamster", in_signature='issiissb', out_signature='i')
-    def UpdateFact(self, fact_id, activity_name, tags,
-                   start_time, end_time,
-                   category_name = None, description = None, temporary = False):
+    @dbus.service.method("org.gnome.Hamster", in_signature='isiib', out_signature='i')
+    def UpdateFact(self, fact_id, fact, start_time, end_time, temporary = False):
         if start_time:
             start_time = dt.datetime.utcfromtimestamp(start_time)
         else:
@@ -151,8 +147,7 @@ class Storage(dbus.service.Object):
 
         self.start_transaction()
         self.__remove_fact(fact_id)
-        result = self.__add_fact(activity_name, tags, start_time, end_time,
-                                 category_name, description, temporary)
+        result = self.__add_fact(fact, start_time, end_time, temporary)
 
         self.end_transaction()
 
diff --git a/src/hamster/utils/stuff.py b/src/hamster/utils/stuff.py
index bcff4d1..8f5cf4c 100644
--- a/src/hamster/utils/stuff.py
+++ b/src/hamster/utils/stuff.py
@@ -246,75 +246,103 @@ def figure_time(str_time):
     return dt.datetime.now().replace(hour = hours, minute = minutes,
                                      second = 0, microsecond = 0)
 
-def parse_activity_input(text):
-    """Currently pretty braindead function that tries to parse arbitrary input
-    into a activity"""
-    class InputParseResult(object):
-        def __init__(self):
-            self.activity_name = None
-            self.category_name = None
-            self.start_time = None
-            self.end_time = None
-            self.description = None
-            self.tags = []
-            self.ponies = False
-
-
-    res = InputParseResult()
-
-    input_parts = text.strip().split(" ")
-    if len(input_parts) > 1 and re.match('^-?\d', input_parts[0]): #look for time only if there is more
-        potential_time = text.split(" ")[0]
-        potential_end_time = None
-        if len(potential_time) > 1 and  potential_time.startswith("-"):
-            #if starts with minus, treat as minus delta minutes
-            res.start_time = dt.datetime.now() + dt.timedelta(minutes =
-                                                                int(potential_time))
-
-        else:
-            if potential_time.find("-") > 0:
-                potential_time, potential_end_time = potential_time.split("-", 2)
-                res.end_time = figure_time(potential_end_time)
-
-            res.start_time = figure_time(potential_time)
-
-        #remove parts that worked
-        if res.start_time and potential_end_time and not res.end_time:
-            res.start_time = None #scramble
-        elif res.start_time:
-            text = text[text.find(" ")+1:]
-
-    #see if we have description of activity somewhere here (delimited by comma)
-    if text.find(",") > 0:
-        text, res.description = text.split(",", 1)
-        res.description = res.description.strip()
-
-        # more of a hidden feature for copy and paste purposes
-        # tags are marked with hash sign and parsed only at the end of description
-        words = res.description.split(" ")
-        tags = []
-        for word in reversed(words):
-            if word.startswith("#"):
-                tags.append(word.strip("#,"))  #avoid commas
-            else:
-                break
-
-        if tags:
-            res.tags = tags
-            res.description = " ".join(res.description.split(" ")[:-len(tags)])
-
 
+class Fact(object):
+    def __init__(self, activity, category = "", description = "", tags = "",
+                 start_time = None, end_time = None, id = None,
+                 temporary = False):
+        """the category, description and tags can be either passed in explicitly
+        or by using the "activity category, description #tag #tag" syntax.
+        explicitly stated values will take precedence over derived ones"""
+        self.original_activity = activity # unparsed version, mainly for trophies right now
+        self.activity = None
+        self.category = None
+        self.description = None
+        self.tags = None
+        self.start_time = None
+        self.end_time = None
+        self.temporary = temporary
+        self.id = id
+        self.ponies = False
+
+        # parse activity
+        input_parts = activity.strip().split(" ")
+        if len(input_parts) > 1 and re.match('^-?\d', input_parts[0]): #look for time only if there is more
+            potential_time = activity.split(" ")[0]
+            potential_end_time = None
+            if len(potential_time) > 1 and  potential_time.startswith("-"):
+                #if starts with minus, treat as minus delta minutes
+                self.start_time = dt.datetime.now() + dt.timedelta(minutes = int(potential_time))
 
-    if text.find("@") > 0:
-        text, res.category_name = text.split("@", 1)
-        res.category_name = res.category_name.strip()
-
-    #only thing left now is the activity name itself
-    res.activity_name = text.strip()
-
-    #this is most essential
-    if any([b in text for b in ("bbq", "barbeque", "barbecue")]) and "omg" in text:
-        res.ponies = True
-        res.description = "[ponies = 1], [rainbows = 0]"
-
-    return res
+            else:
+                if potential_time.find("-") > 0:
+                    potential_time, potential_end_time = potential_time.split("-", 2)
+                    self.end_time = figure_time(potential_end_time)
+
+                self.start_time = figure_time(potential_time)
+
+            #remove parts that worked
+            if self.start_time and potential_end_time and not self.end_time:
+                self.start_time = None #scramble
+            elif self.start_time:
+                activity = activity[activity.find(" ")+1:]
+
+        #see if we have description of activity somewhere here (delimited by comma)
+        if activity.find(",") > 0:
+            activity, self.description = activity.split(",", 1)
+            self.description = self.description.strip()
+
+            # tags are marked with hash sign and parsed only at the end of description
+            # stop looking for tags if we stumble upon a non tag
+            words = self.description.split(" ")
+            parsed_tags = []
+            for word in reversed(words):
+                if word.startswith("#"):
+                    parsed_tags.append(word.strip("#,"))  #avoid commas
+                else:
+                    break
+
+            if parsed_tags:
+                self.tags = parsed_tags
+                self.description = " ".join(self.description.split(" ")[:-len(self.tags)])
+
+        if activity.find("@") > 0:
+            activity, self.category = activity.split("@", 1)
+            self.category = self.category.strip()
+
+        #this is most essential
+        if any([b in activity for b in ("bbq", "barbeque", "barbecue")]) and "omg" in activity:
+            self.ponies = True
+            self.description = "[ponies = 1], [rainbows = 0]"
+
+        #only thing left now is the activity name itself
+        self.activity = activity.strip()
+
+        if tags and isinstance(tags, list):
+            tags = " ".join(tags)
+        tags = [tag.strip() for tag in tags.split(",") if tag.strip()]
+
+        # override implicit with explicit
+        self.category = category.replace("#", "").replace(",", "") or self.category or None
+        self.description = description.replace("#", "") or self.description or None
+        self.tags =  tags or self.tags or None
+        self.start_time = start_time or self.start_time or None
+        self.end_time = end_time or self.end_time or None
+
+
+    def serialized_name(self):
+        res = self.activity
+
+        if self.category:
+            res += "@%s" % self.category
+
+        if self.description or self.tags:
+            res += ",%s %s" % (self.description or "",
+                               " ".join(["#%s" % tag for tag in self.tags]))
+        return res
+
+    def __str__(self):
+        time = self.start_time.strftime("%d %m %Y %H:%M")
+        if self.end_time:
+            time = "%s - %s" % (time, self.end_time.strftime("%H:%M"))
+        return "%s %s" % (time, self.serialized_name())
diff --git a/src/hamster/utils/trophies.py b/src/hamster/utils/trophies.py
index 70666a4..f4fd9cc 100644
--- a/src/hamster/utils/trophies.py
+++ b/src/hamster/utils/trophies.py
@@ -80,7 +80,7 @@ class Checker(object):
         self.flags = {}
 
 
-    def check_update_based(self, prev_id, new_id, activity_name, tags, start_time, end_time, category_name, description):
+    def check_update_based(self, prev_id, new_id, fact):
         if not storage: return
 
         if not self.flags.get('last_update_id') or prev_id != self.flags['last_update_id']:
@@ -95,7 +95,7 @@ class Checker(object):
             unlock("all_wrong")
 
 
-    def check_fact_based(self, activity_name, tags, start_time, end_time, category_name, description):
+    def check_fact_based(self, fact):
         """quite possibly these all could be called from the service as
            there is bigger certainty as to what did actually happen"""
 
@@ -103,24 +103,18 @@ class Checker(object):
         if not storage: return
 
         # explicit over implicit
-        fact = stuff.parse_activity_input(activity_name)
-        if not fact.activity_name:  # TODO - parse_activity could return None for these cases
+        if not fact.activity:  # TODO - parse_activity could return None for these cases
             return
 
         # full plate - use all elements of syntax parsing
-        if all((fact.category_name, fact.description, fact.tags, fact.start_time, fact.end_time)):
+        derived_fact = stuff.Fact(fact.original_activity)
+        if all((derived_fact.category, derived_fact.description,
+                derived_fact.tags, derived_fact.start_time, derived_fact.end_time)):
             unlock("full_plate")
 
 
-        fact.tags = [tag.strip() for tag in tags.split(",") if tag.strip()] or fact.tags
-        fact.category_name = category_name or fact.category_name
-        fact.description = description or fact.description
-        fact.start_time = start_time or fact.start_time or dt.datetime.now()
-        fact.end_time = end_time or fact.end_time
-
-
         # Jumper - hidden - made 10 switches within an hour (radical)
-        if not fact.end_time: #end time normally denotes switch
+        if not fact.end_time: # end time normally denotes switch
             last_ten = self.flags.setdefault('last_ten_ongoing', [])
             last_ten.append(fact)
             last_ten = last_ten[-10:]
@@ -135,7 +129,7 @@ class Checker(object):
                 unlock("good_memory")
 
         # layering - entered 4 activities in a row in one of previous days, each one overlapping the previous one
-        #            avoiding today as in that case the layering
+        # avoiding today as in that case the layering might be automotical
         last_four = self.flags.setdefault('last_four', [])
         last_four.append(fact)
         last_four = last_four[-4:]
@@ -158,18 +152,18 @@ class Checker(object):
 
 
         # alpha bravo charlie â?? used delta times to enter at least 50 activities
-        if fact.start_time and activity_name.startswith("-"):
+        if fact.start_time and fact.original_activity.startswith("-"):
             counter = increment("hamster-applet", "alpha_bravo_charlie")
             if counter == 50:
                 unlock("alpha_bravo_charlie")
 
 
         # cryptic - hidden - used word shorter than 4 letters for the activity name
-        if len(fact.activity_name) < 4:
+        if len(fact.activity) < 4:
             unlock("cryptic")
 
         # madness â?? hidden â?? entered an activity in all caps
-        if fact.activity_name == fact.activity_name.upper():
+        if fact.activity == fact.activity.upper():
             unlock("madness")
 
         # verbose - hidden - description longer than 5 words
@@ -189,7 +183,7 @@ class Checker(object):
         #        patrys complains about who's gonna garbage collect. should think
         #        about this
         if not storage.check_achievement("hamster-applet", "ultra_focused"):
-            activity_count = increment("hamster-applet", "focused_%s %s" % (fact.activity_name, fact.category_name or ""))
+            activity_count = increment("hamster-applet", "focused_%s %s" % (fact.activity, fact.category or ""))
             # focused â?? 100 facts with single activity
             if activity_count == 100:
                 unlock("hamster-applet", "focused")
diff --git a/src/hamster/widgets/activityentry.py b/src/hamster/widgets/activityentry.py
index 0ad67e9..fa0a6ac 100644
--- a/src/hamster/widgets/activityentry.py
+++ b/src/hamster/widgets/activityentry.py
@@ -133,15 +133,15 @@ class ActivityEntry(gtk.Entry):
         if not self._parent_click_watcher:
             self._parent_click_watcher = self.get_toplevel().connect("button-press-event", self._on_focus_out_event)
 
-        activity = stuff.parse_activity_input(self.filter)
+        fact = stuff.Fact(self.filter)
         time = ''
-        if activity.start_time:
-            time = activity.start_time.strftime("%H:%M")
-            if activity.end_time:
-                time += "-%s" % activity.end_time.strftime("%H:%M")
+        if fact.start_time:
+            time = fact.start_time.strftime("%H:%M")
+            if fact.end_time:
+                time += "-%s" % fact.end_time.strftime("%H:%M")
 
-        self.time_icon_column.set_visible(activity.start_time != None and self.filter.find("@") == -1)
-        self.time_column.set_visible(activity.start_time != None and self.filter.find("@") == -1)
+        self.time_icon_column.set_visible(fact.start_time is not None and self.filter.find("@") == -1)
+        self.time_column.set_visible(fact.start_time is not None and self.filter.find("@") == -1)
 
 
         self.category_column.set_visible(self.filter.find("@") == -1)
@@ -179,7 +179,6 @@ class ActivityEntry(gtk.Entry):
 
     def complete_inline(self):
         model = self.tree.get_model()
-        activity = stuff.parse_activity_input(self.filter)
         subject = self.get_text()
 
         if not subject or model.iter_n_children(None) == 0:
@@ -218,21 +217,21 @@ class ActivityEntry(gtk.Entry):
             return #same thing, no need to repopulate
 
         self.filter = self.get_text().decode('utf8', 'replace')[:cursor]
-        input_activity = stuff.parse_activity_input(self.filter)
+        fact = stuff.Fact(self.filter)
 
         # do not cache as ordering and available options change over time
-        self.activities = runtime.storage.get_activities(input_activity.activity_name)
-        self.external_activities = self.external.get_activities(input_activity.activity_name)
+        self.activities = runtime.storage.get_activities(fact.activity)
+        self.external_activities = self.external.get_activities(fact.activity)
         self.activities.extend(self.external_activities)
 
         self.categories = self.categories or runtime.storage.get_categories()
 
 
         time = ''
-        if input_activity.start_time:
-            time = input_activity.start_time.strftime("%H:%M")
-            if input_activity.end_time:
-                time += "-%s" % input_activity.end_time.strftime("%H:%M")
+        if fact.start_time:
+            time = fact.start_time.strftime("%H:%M")
+            if fact.end_time:
+                time += "-%s" % fact.end_time.strftime("%H:%M")
 
 
         store = self.tree.get_model()
@@ -248,7 +247,7 @@ class ActivityEntry(gtk.Entry):
                     fillable = (self.filter[:self.filter.find("@") + 1] + category['name'])
                     store.append([fillable, category['name'], fillable, time])
         else:
-            key = input_activity.activity_name.decode('utf8', 'replace').lower()
+            key = fact.activity.decode('utf8', 'replace').lower()
             for activity in self.activities:
                 fillable = activity['name'].lower()
                 if activity['category']:
diff --git a/tests/stuff_test.py b/tests/stuff_test.py
index 6d602fa..47fc43f 100644
--- a/tests/stuff_test.py
+++ b/tests/stuff_test.py
@@ -3,74 +3,80 @@ import sys, os.path
 sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), "..")))
 
 import unittest
-from hamster import stuff
+from hamster.utils import stuff
 
 class TestActivityInputParsing(unittest.TestCase):
     def test_plain_name(self):
         # plain activity name
-        activity = stuff.parse_activity_input("just a simple case")
-        self.assertEquals(activity.activity_name, "just a simple case")
-        assert activity.category_name is None and activity.start_time is None \
-               and activity.end_time is None and activity.category_name is None\
-               and activity.description is None
+        activity = stuff.Fact("just a simple case")
+        self.assertEquals(activity.activity, "just a simple case")
+
+        assert activity.category is None
+        assert activity.start_time is None
+        assert activity.end_time is None
+        assert activity.category is None
+        assert activity.description is None
 
     def test_with_start_time(self):
         # with time
-        activity = stuff.parse_activity_input("12:35 with start time")
-        self.assertEquals(activity.activity_name, "with start time")
+        activity = stuff.Fact("12:35 with start time")
+        self.assertEquals(activity.activity, "with start time")
         self.assertEquals(activity.start_time.strftime("%H:%M"), "12:35")
 
         #rest must be empty
-        assert activity.category_name is None \
-               and activity.end_time is None and activity.category_name is None\
-               and activity.description is None
+        assert activity.category is None
+        assert activity.end_time is None
+        assert activity.description is None
 
     def test_with_start_and_end_time(self):
         # with time
-        activity = stuff.parse_activity_input("12:35-14:25 with start-end time")
-        self.assertEquals(activity.activity_name, "with start-end time")
+        activity = stuff.Fact("12:35-14:25 with start-end time")
+        self.assertEquals(activity.activity, "with start-end time")
         self.assertEquals(activity.start_time.strftime("%H:%M"), "12:35")
         self.assertEquals(activity.end_time.strftime("%H:%M"), "14:25")
 
         #rest must be empty
-        assert activity.category_name is None \
-               and activity.category_name is None\
-               and activity.description is None
+        assert activity.category is None
+        assert activity.description is None
 
     def test_category(self):
         # plain activity name
-        activity = stuff.parse_activity_input("just a simple case hamster")
-        self.assertEquals(activity.activity_name, "just a simple case")
-        self.assertEquals(activity.category_name, "hamster")
-        assert activity.start_time is None \
-               and activity.end_time is None \
-               and activity.description is None
+        activity = stuff.Fact("just a simple case hamster")
+        self.assertEquals(activity.activity, "just a simple case")
+        self.assertEquals(activity.category, "hamster")
+        assert activity.start_time is None
+        assert activity.end_time is None
+        assert activity.description is None
 
     def test_description(self):
         # plain activity name
-        activity = stuff.parse_activity_input("case, with added description")
-        self.assertEquals(activity.activity_name, "case")
+        activity = stuff.Fact("case, with added description")
+        self.assertEquals(activity.activity, "case")
         self.assertEquals(activity.description, "with added description")
-        assert activity.category_name is None and activity.start_time is None \
-               and activity.end_time is None and activity.category_name is None
+        assert activity.category is None
+        assert activity.start_time is None
+        assert activity.end_time is None
+        assert activity.category is None
 
     def test_tags(self):
         # plain activity name
-        activity = stuff.parse_activity_input("case, with added #de description #and, #some #tags")
-        self.assertEquals(activity.activity_name, "case")
+        activity = stuff.Fact("case, with added #de description #and, #some #tags")
+        self.assertEquals(activity.activity, "case")
         self.assertEquals(activity.description, "with added #de description")
-        self.assertEquals(activity.tags, ["tags", "some", "and"]) #the list is parsed from the other end
-        assert activity.category_name is None and activity.start_time is None \
-               and activity.end_time is None
+        self.assertEquals(set(activity.tags), set(["and", "some", "tags"]))
+        assert activity.category is None
+        assert activity.start_time is None
+        assert activity.end_time is None
 
     def test_full(self):
         # plain activity name
-        activity = stuff.parse_activity_input("1225-1325 case cat, description")
+        activity = stuff.Fact("1225-1325 case cat, description #ta non-tag #tag #bag")
         self.assertEquals(activity.start_time.strftime("%H:%M"), "12:25")
         self.assertEquals(activity.end_time.strftime("%H:%M"), "13:25")
-        self.assertEquals(activity.activity_name, "case")
-        self.assertEquals(activity.category_name, "cat")
-        self.assertEquals(activity.description, "description")
+        self.assertEquals(activity.activity, "case")
+        self.assertEquals(activity.category, "cat")
+        self.assertEquals(activity.description, "description #ta non-tag")
+        self.assertEquals(set(activity.tags), set(["bag", "tag"]))
 
 if __name__ == '__main__':
     unittest.main()



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