def test_explicit(self): '''testing whether explicit assignments work as defined''' activity = Fact(start_time="12:25", end_time="13:25", activity="case", category="cat", description="description non-tag#ta", tags="tag, bag") self.assertEquals(activity.start_time, "12:25") self.assertEquals(activity.end_time, "13:25") self.assertEquals(activity.activity, "case") self.assertEquals(activity.category, "cat") self.assertEquals(activity.description, "description non-tag#ta") self.assertEquals(set(activity.tags), set(["bag", "tag"]))
def test_iter_explicit(self): '''testing iter with explicit assingments''' activity = Fact(start_time="12:34", end_time="15:12", activity="test activity", category="test category", description="test description", tags="testtags") activity2 = Fact(start_time="12:34", end_time="15:12", activity="test activity", category="test category", description="test description", tags="testtags") self.assertEquals([elem for elem in activity], [elem for elem in activity2]) activity3 = Fact(start_time="12:14", end_time="15:12", activity="test activity", category="test category", description="test description", tags="testtags") self.assertNotEqual([elem for elem in activity], [elem for elem in activity3])
def on_workspace_changed(self, screen, previous_workspace): if not previous_workspace: # wnck has a slight hiccup on init and after that calls # workspace changed event with blank previous state that should be # ignored return if not self.workspace_tracking: return # default to not doing anything current_workspace = screen.get_active_workspace() # rely on workspace numbers as names change prev = previous_workspace.get_number() new = current_workspace.get_number() # 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") fact = None if new < len(mapping): fact = Fact(mapping[new]) 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(fact.activity, category_id, resurrect = 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: # now see if maybe we have any memory of the new workspace # (as in - user was here and tracking Y) # if the new workspace is in our dict, switch to the specified activity if new in self.workspace_activities and self.workspace_activities[new]: activity = self.workspace_activities[new] if not activity: return # check if maybe there is no need to switch, as field match: if self.last_activity and \ self.last_activity.name.lower() == activity.name.lower() and \ (self.last_activity.category or "").lower() == (activity.category or "").lower() and \ ", ".join(self.last_activity.tags).lower() == ", ".join(activity.tags).lower(): return # ok, switch fact = Fact(activity.name, tags = ", ".join(activity.tags), category = activity.category, description = activity.description); runtime.storage.add_fact(fact)
def __add_fact(self, serialized_fact, start_time, end_time=None, temporary=False): fact = Fact(serialized_fact, start_time=start_time, end_time=end_time) start_time = start_time or fact.start_time end_time = end_time or fact.end_time if not fact.activity or start_time is None: # sanity check return 0 # get tags from database - this will create any missing tags too tags = [(tag['id'], tag['name'], tag['autocomplete']) for tag in self.get_tag_ids(fact.tags)] # now check if maybe there is also a category category_id = None if fact.category: category_id = self.__get_category_id(fact.category) if not category_id: category_id = self.__add_category(fact.category) if trophies: trophies.unlock("no_hands") # try to find activity, resurrect if not temporary activity_id = self.__get_activity_by_name(fact.activity, category_id, resurrect=not temporary) if not activity_id: 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.timedelta(days=-1) <= dt.datetime.now() - start_time <= dt.timedelta(days=1)): # pull in previous facts facts = self.__get_todays_facts() previous = None if facts and facts[-1]["end_time"] == None: previous = facts[-1] 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 set(previous["tags"]) == set([tag[1] for tag in tags]) \ and (previous["description"] or "") == (fact.description or ""): return None # if no description is added # see if maybe previous was too short to qualify as an activity if not previous["description"] \ and 60 >= (start_time - previous['start_time']).seconds >= 0: self.__remove_fact(previous['id']) # now that we removed the previous one, see if maybe the one # before that is actually same as the one we want to start # (glueing) if len(facts) > 1 and 60 >= ( start_time - facts[-2]['end_time']).seconds >= 0: before = facts[-2] if before["activity_id"] == activity_id \ and set(before["tags"]) == set([tag[1] for tag in tags]): # resume and return update = """ UPDATE facts SET end_time = null WHERE id = ? """ self.execute(update, (before["id"], )) return before["id"] else: # otherwise stop update = """ UPDATE facts SET end_time = ? WHERE id = ? """ self.execute(update, (start_time, previous["id"])) # done with the current activity, now we can solve overlaps if not end_time: end_time = self.__squeeze_in(start_time) else: self.__solve_overlaps(start_time, end_time) # finally add the new entry insert = """ INSERT INTO facts (activity_id, start_time, end_time, description) VALUES (?, ?, ?, ?) """ self.execute(insert, (activity_id, start_time, end_time, fact.description)) fact_id = self.__last_insert_rowid() #now link tags insert = ["insert into fact_tags(fact_id, tag_id) values(?, ?)" ] * len(tags) params = [(fact_id, tag[0]) for tag in tags] self.execute(insert, params) self.__remove_index([fact_id]) return fact_id
def __solve_overlaps(self, start_time, end_time): """finds facts that happen in given interval and shifts them to make room for new fact """ if end_time is None or start_time is None: return # possible combinations and the OR clauses that catch them # (the side of the number marks if it catches the end or start time) # |----------------- NEW -----------------| # |--- old --- 1| |2 --- old --- 1| |2 --- old ---| # |3 ----------------------- big old ------------------------ 3| query = """ SELECT a.*, b.name, c.name as category FROM facts a LEFT JOIN activities b on b.id = a.activity_id LEFT JOIN categories c on b.category_id = c.id WHERE (end_time > ? and end_time < ?) OR (start_time > ? and start_time < ?) OR (start_time < ? and end_time > ?) ORDER BY start_time """ conflicts = self.fetchall( query, (start_time, end_time, start_time, end_time, start_time, end_time)) for fact in conflicts: # won't eliminate as it is better to have overlapping entries than loosing data if start_time < fact["start_time"] and end_time > fact["end_time"]: continue # split - truncate until beginning of new entry and create new activity for end if fact["start_time"] < start_time < fact["end_time"] and \ fact["start_time"] < end_time < fact["end_time"]: logging.info("splitting %s" % fact["name"]) # truncate until beginning of the new entry self.execute( """UPDATE facts SET end_time = ? WHERE id = ?""", (start_time, fact["id"])) fact_name = fact["name"] # create new fact for the end new_fact = Fact(fact["name"], category=fact["category"], description=fact["description"]) new_fact_id = self.__add_fact(new_fact.serialized_name(), end_time, fact["end_time"]) # copy tags tag_update = """INSERT INTO fact_tags(fact_id, tag_id) SELECT ?, tag_id FROM fact_tags WHERE fact_id = ?""" self.execute(tag_update, (new_fact_id, fact["id"])) #clone tags if trophies: trophies.unlock("split") # overlap start elif start_time < fact["start_time"] < end_time: logging.info("Overlapping start of %s" % fact["name"]) self.execute("UPDATE facts SET start_time=? WHERE id=?", (end_time, fact["id"])) # overlap end elif start_time < fact["end_time"] < end_time: logging.info("Overlapping end of %s" % fact["name"]) self.execute("UPDATE facts SET end_time=? WHERE id=?", (start_time, fact["id"]))
def update_suggestions(self, text=""): """ * from previous activity | set time | minutes ago | start now * to ongoing | set time * activity * [@category] * #tags, #tags, #tags * we will leave description for later all our magic is space separated, strictly, start-end can be just dash phases: [start_time] | [-end_time] | activity | [@category] | [#tag] """ res = [] fact = Fact(text) now = dt.datetime.now() # figure out what we are looking for # time -> activity[@category] -> tags -> description # presence of an attribute means that we are not looking for the previous one # we still might be looking for the current one though looking_for = "start_time" fields = ["start_time", "end_time", "activity", "category", "tags", "description", "done"] for field in reversed(fields): if getattr(fact, field, None): looking_for = field if text[-1] == " ": looking_for = fields[fields.index(field)+1] break fragments = [f for f in re.split("[\s|#]", text)] current_fragment = fragments[-1] if fragments else "" search = extract_search(text) matches = [] for match, score in self.suggestions: if search in match: if match.startswith(search): score += 10**8 # boost beginnings matches.append((match, score)) # need to limit these guys, sorry matches = sorted(matches, key=lambda x: x[1], reverse=True)[:7] for match, score in matches: label = (fact.start_time or now).strftime("%H:%M") if fact.end_time: label += fact.end_time.strftime("-%H:%M") markup_label = label + " " + (stuff.escape_pango(match).replace(search, "<b>%s</b>" % search) if search else match) label += " " + match res.append(DataRow(markup_label, match, label)) # list of tuples (description, variant) variants = [] if self.original_fact: # editing an existing fact variant_fact = None if self.original_fact.end_time is None: description = "stop now" variant_fact = self.original_fact.copy() variant_fact.end_time = now elif self.original_fact == self.todays_facts[-1]: # that one is too dangerous, except for the last entry description = "keep up" # Do not use Fact(..., end_time=None): it would be a no-op variant_fact = self.original_fact.copy() variant_fact.end_time = None if variant_fact: variant = variant_fact.serialized(prepend_date=False) variants.append((description, variant)) else: # brand new fact description = "start now" variant = now.strftime("%H:%M ") variants.append((description, variant)) prev_fact = self.todays_facts[-1] if self.todays_facts else None if prev_fact and prev_fact.end_time: since = stuff.format_duration(now - prev_fact.end_time) description = "from previous activity, %s ago" % since variant = prev_fact.end_time.strftime("%H:%M ") variants.append((description, variant)) description = "start activity -n minutes ago (1 or 3 digits allowed)" variant = "-" variants.append((description, variant)) text = text.strip() if text: description = "clear" variant = "" variants.append((description, variant)) for (description, variant) in variants: res.append(DataRow(variant, description=description)) self.complete_tree.set_rows(res)
def on_switch_activity_clicked(self, widget): activity, temporary = self.new_name.get_value() # Redmine integration - if activity is connected with Redmine, it must use new data structures to save additional data fact = None if conf.get("redmine_integration_enabled"): redmine_issue_subject = self.get_widget( "issue_combo").get_active_text() if redmine_issue_subject == None or redmine_issue_subject == "None": arbitrary_issue_id = self.get_widget( "arbitrary_issue_id_entry").get_text() if arbitrary_issue_id == "" or arbitrary_issue_id == None: fact = Fact(activity, tags=self.new_tags.get_text().decode( "utf8", "replace")) else: redcon = redmine.RedmineConnector( conf.get("redmine_url"), conf.get("redmine_api_key")) try: redcon.get_arbitrary_issue_data(arbitrary_issue_id) arbitrary_issue_id = int(arbitrary_issue_id) redmine_time_activity_name = self.get_widget( "time_activity_combo").get_active_text() if redmine_time_activity_name == None: dialog = gtk.Dialog( "Failed to start tracking", self.window, gtk.DIALOG_MODAL, (gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) label = gtk.Label( "Redmine activity cannot be empty!") dialog.vbox.pack_start(label) label.show() dialog.run() dialog.destroy() return redmine_activity_id = redcon.get_redmine_activity_id( redmine_time_activity_name) fact = RedmineFact( activity, arbitrary_issue_id, redmine_activity_id, tags=self.new_tags.get_text().decode( "utf8", "replace")) except redmine.RedmineConnectionException: dialog = gtk.Dialog( "Failed to start tracking", self.window, gtk.DIALOG_MODAL, (gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) label = gtk.Label("Invalid arbitrary issue number!") dialog.vbox.pack_start(label) label.show() dialog.run() dialog.destroy() return else: redmine_time_activity_name = self.get_widget( "time_activity_combo").get_active_text() if redmine_time_activity_name == None: dialog = gtk.Dialog("Failed to start tracking", self.window, gtk.DIALOG_MODAL, (gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)) label = gtk.Label("Redmine activity cannot be empty!") dialog.vbox.pack_start(label) label.show() dialog.run() dialog.destroy() return redcon = redmine.RedmineConnector(conf.get("redmine_url"), conf.get("redmine_api_key")) redmine_issue_id = redcon.get_redmine_issue_id( redmine_issue_subject) redmine_activity_id = redcon.get_redmine_activity_id( redmine_time_activity_name) fact = RedmineFact(activity, redmine_issue_id, redmine_activity_id, tags=self.new_tags.get_text().decode( "utf8", "replace")) else: fact = Fact(activity, tags=self.new_tags.get_text().decode( "utf8", "replace")) if not fact.activity: return runtime.storage.add_fact(fact, temporary) self.new_name.set_text("") self.new_tags.set_text("")
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""" # checks fact based trophies if not storage: return # explicit over implicit if not fact.activity: # TODO - parse_activity could return None for these cases return # full plate - use all elements of syntax parsing derived_fact = 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") # Jumper - hidden - made 10 switches within an hour (radical) 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:] if len(last_ten) == 10 and ( last_ten[-1].start_time - last_ten[0].start_time) <= dt.timedelta(hours=1): unlock("jumper") # good memory - entered three past activities during a day if fact.end_time and fact.start_time.date() == dt.date.today(): good_memory = increment("past_activities", dt.date.today().strftime("%Y%m%d")) if good_memory == 3: 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 might be automotical last_four = self.flags.setdefault('last_four', []) last_four.append(fact) last_four = last_four[-4:] if len(last_four) == 4: layered = True for prev, next in zip(last_four, last_four[1:]): if next.start_time.date() == dt.date.today() or \ next.start_time < prev.start_time or \ (prev.end_time and prev.end_time < next.start_time): layered = False if layered: unlock("layered") # wait a minute! - Switch to a new activity within 60 seconds if len(last_four) >= 2: prev, next = last_four[-2:] if prev.end_time is None and next.end_time is None and ( next.start_time - prev.start_time) < dt.timedelta(minutes=1): unlock("wait_a_minute") # alpha bravo charlie – used delta times to enter at least 50 activities if fact.start_time and fact.original_activity.startswith("-"): counter = increment("hamster-time-tracker", "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) < 4: unlock("cryptic") # madness – hidden – entered an activity in all caps if fact.activity == fact.activity.upper(): unlock("madness") # verbose - hidden - description longer than 5 words if fact.description and len([ word for word in fact.description.split(" ") if len(word.strip()) > 2 ]) >= 5: unlock("verbose") # overkill - used 8 or more tags on a single activity if len(fact.tags) >= 8: unlock("overkill") # ponies - hidden - discovered the ponies if fact.ponies: unlock("ponies") # TODO - after the trophies have been unlocked there is not much point in going on # patrys complains about who's gonna garbage collect. should think # about this if not check("ultra_focused"): activity_count = increment( "hamster-time-tracker", "focused_%s@%s" % (fact.activity, fact.category or "")) # focused – 100 facts with single activity if activity_count == 100: unlock("focused") # ultra focused – 500 facts with single activity if activity_count == 500: unlock("ultra_focused") # elite - hidden - start an activity at 13:37 if dt.datetime.now().hour == 13 and dt.datetime.now().minute == 37: unlock("elite")
def __init__(self, parent=None, fact_id=None, base_fact=None): gobject.GObject.__init__(self) self._gui = load_ui_file("edit_activity.ui") self.window = self.get_widget('custom_fact_window') self.window.set_size_request(600, 200) self.parent = parent # None if creating a new fact, instead of editing one self.fact_id = fact_id self.category_entry = widgets.CategoryEntry( widget=self.get_widget('category')) self.activity_entry = widgets.ActivityEntry( widget=self.get_widget('activity'), category_widget=self.category_entry) self.cmdline = widgets.CmdLineEntry() self.get_widget("command line box").add(self.cmdline) self.cmdline.connect("focus_in_event", self.on_cmdline_focus_in_event) self.cmdline.connect("focus_out_event", self.on_cmdline_focus_out_event) self.dayline = widgets.DayLine() self._gui.get_object("day_preview").add(self.dayline) self.description_box = self.get_widget('description') self.description_buffer = self.description_box.get_buffer() self.end_date = widgets.Calendar( widget=self.get_widget("end date"), expander=self.get_widget("end date expander")) self.end_time = widgets.TimeInput() self.get_widget("end time box").add(self.end_time) self.start_date = widgets.Calendar( widget=self.get_widget("start date"), expander=self.get_widget("start date expander")) self.start_time = widgets.TimeInput() self.get_widget("start time box").add(self.start_time) self.tags_entry = widgets.TagsEntry() self.get_widget("tags box").add(self.tags_entry) self.save_button = self.get_widget("save_button") # this will set self.master_is_cmdline self.cmdline.grab_focus() if fact_id: # editing self.fact = runtime.storage.get_fact(fact_id) self.date = self.fact.date self.window.set_title(_("Update activity")) else: self.window.set_title(_("Add activity")) self.date = hamster_today() self.get_widget("delete_button").set_sensitive(False) if base_fact: # start a clone now. self.fact = base_fact.copy(start_time=hamster_now(), end_time=None) else: self.fact = Fact(start_time=hamster_now()) original_fact = self.fact self.update_fields() self.update_cmdline(select=True) self.cmdline.original_fact = original_fact # This signal should be emitted only after a manual modification, # not at init time when cmdline might not always be fully parsable. self.cmdline.connect("changed", self.on_cmdline_changed) self.description_buffer.connect("changed", self.on_description_changed) self.start_time.connect("changed", self.on_start_time_changed) self.start_date.connect("day-selected", self.on_start_date_changed) self.start_date.expander.connect("activate", self.on_start_date_expander_activated) self.end_time.connect("changed", self.on_end_time_changed) self.end_date.connect("day-selected", self.on_end_date_changed) self.end_date.expander.connect("activate", self.on_end_date_expander_activated) self.activity_entry.connect("changed", self.on_activity_changed) self.category_entry.connect("changed", self.on_category_changed) self.tags_entry.connect("changed", self.on_tags_changed) self._gui.connect_signals(self) self.validate_fields() self.window.show_all()
return self.fact.activity def get_text(self, fact): text = "" if fact.description: text += ": %s" % (fact.description) if fact.tags: text += " ("+", ".join(fact.tags)+")" return text d = date(2005, 7, 14) t = ti(12, 30) facts = list([\ Fact(activity = "#123: 1", category = "category", description = "description", id=1), \ Fact(activity = "#123: 2", category = "category", description = "description", id=2), \ Fact(activity = "#222: 3", category = "category", description = "description", id=6), \ Fact(activity = "#123: 4", category = "category", description = "description", id=3), \ Fact(activity = "#222: 5", category = "category", description = "description", id=7), \ Fact(activity = "#123: 6", category = "category", description = "description", id=4), \ Fact(activity = "#123: 7", category = "category", description = "description", id=5), \ ]) rows = [ExportRow(fact) for fact in facts] tickets = {} for ticket, group in groupby(rows, lambda export_row: export_row.id): print ticket, list(group) rows.sort(key = lambda row: row.id) for ticket, group in groupby(rows, lambda export_row: export_row.id): print ticket, list(group)
def on_last_activity_activated(self, widget, name): fact = Fact(name) if not fact.activity: return runtime.storage.add_fact(fact)
def localized_fact(self): """makes sure fact is in our date""" fact = Fact(self.activity.get_text()) fact.date = self.date return fact
def update_suggestions(self, text=""): """ * from previous activity | set time | minutes ago | start now * to ongoing | set time * activity * [@category] * #tags, #tags, #tags * we will leave description for later all our magic is space separated, strictly, start-end can be just dash phases: [start_time] | [-end_time] | activity | [@category] | [#tag] """ now = dt.datetime.now() text = text.lstrip() time_re = re.compile("^([0-1]?[0-9]|[2][0-3]):([0-5][0-9])$") time_range_re = re.compile("^([0-1]?[0-9]|[2][0-3]):([0-5][0-9])-([0-1]?[0-9]|[2][0-3]):([0-5][0-9])$") delta_re = re.compile("^-[0-9]{1,3}$") # when the time is filled, we need to make sure that the chunks parse correctly delta_fragment_re = re.compile("^-[0-9]{0,3}$") templates = { "start_time": "", "start_delta": ("start activity -n minutes ago", "-"), } # need to set the start_time template before prev_fact = self.todays_facts[-1] if self.todays_facts else None if prev_fact and prev_fact.end_time: templates["start_time"] = ("from previous activity %s ago" % stuff.format_duration(now - prev_fact.end_time), prev_fact.end_time.strftime("%H:%M ")) variants = [] fact = Fact(text) # figure out what we are looking for # time -> activity[@category] -> tags -> description # presence of each next attribute means that we are not looking for the previous one # we still might be looking for the current one though looking_for = "start_time" fields = ["start_time", "end_time", "activity", "category", "tags", "description", "done"] for field in reversed(fields): if getattr(fact, field, None): looking_for = field if text[-1] == " ": looking_for = fields[fields.index(field)+1] break fragments = [f for f in re.split("[\s|#]", text)] current_fragment = fragments[-1] if fragments else "" if not text.strip(): variants = [templates[name] for name in ("start_time", "start_delta") if templates[name]] elif looking_for == "start_time" and text == "-": if len(current_fragment) > 1: # avoid blank "-" templates["start_delta"] = ("%s minutes ago" % (-int(current_fragment)), current_fragment) variants.append(templates["start_delta"]) res = [] for (description, variant) in variants: res.append(DataRow(variant, description=description)) # regular activity if (looking_for in ("start_time", "end_time") and not looks_like_time(text.split(" ")[-1])) or \ looking_for in ("activity", "category"): search = extract_search(text) matches = [] for match, score in self.suggestions: if search in match: if match.startswith(search): score += 10**8 # boost beginnings matches.append((match, score)) matches = sorted(matches, key=lambda x: x[1], reverse=True)[:7] # need to limit these guys, sorry for match, score in matches: label = (fact.start_time or now).strftime("%H:%M") if fact.end_time: label += fact.end_time.strftime("-%H:%M") markup_label = label + " " + (stuff.escape_pango(match).replace(search, "<b>%s</b>" % search) if search else match) label += " " + match res.append(DataRow(markup_label, match, label)) if not res: # in case of nothing to show, add preview so that the user doesn't # think they are lost label = (fact.start_time or now).strftime("%H:%M") if fact.end_time: label += fact.end_time.strftime("-%H:%M") if fact.activity: label += " " + fact.activity if fact.category: label += "@" + fact.category if fact.tags: label += " #" + " #".join(fact.tags) res.append(DataRow(stuff.escape_pango(label), description="Start tracking")) self.complete_tree.set_rows(res)