def test_spaces(self): # cf. issue #114 fact = Fact.parse("11:00 12:00 BPC-261 - Task title@Project#code") self.assertEqual(fact.activity, "BPC-261 - Task title") self.assertEqual(fact.category, "Project") self.assertEqual(fact.description, "") self.assertEqual(fact.tags, ["code"]) # space between category and tag fact2 = Fact.parse("11:00 12:00 BPC-261 - Task title@Project #code") self.assertEqual(fact.serialized(), fact2.serialized())
def test_decimal_in_activity(self): # cf. issue #270 fact = Fact.parse("12:25-13:25 10.0@ABC, Two Words #tag #bäg") self.assertEqual(fact.activity, "10.0") self.assertEqual(fact.category, "ABC") self.assertEqual(fact.description, "Two Words") # should not pick up a time here fact = Fact.parse("10.00@ABC, Two Words #tag #bäg") self.assertEqual(fact.activity, "10.00") self.assertEqual(fact.category, "ABC") self.assertEqual(fact.description, "Two Words")
def test_comparison(self): fact1 = Fact.parse("12:25-13:25 case@cat, description #tag #bäg") fact2 = fact1.copy() self.assertEqual(fact1, fact2) fact2 = fact1.copy() fact2.activity = "abcd" self.assertNotEqual(fact1, fact2) fact2 = fact1.copy() fact2.category = "abcd" self.assertNotEqual(fact1, fact2) fact2 = fact1.copy() fact2.description = "abcd" self.assertNotEqual(fact1, fact2) import datetime as dt fact2 = fact1.copy() fact2.start_time = hamster_now() self.assertNotEqual(fact1, fact2) fact2 = fact1.copy() fact2.end_time = hamster_now() self.assertNotEqual(fact1, fact2) # wrong order fact2 = fact1.copy() fact2.tags = ["bäg", "tag"] self.assertNotEqual(fact1, fact2) # correct order fact2 = fact1.copy() fact2.tags = ["tag", "bäg"] self.assertEqual(fact1, fact2)
def add_fact(self, fact, start_time=None, end_time=None, temporary=False): """Add fact. fact: either a Fact instance or a string that can be parsed through Fact.parse. note: start_time and end_time are used only when fact is a string, for backward compatibility. Passing fact as a string is deprecated and will be removed in a future version. Parsing should be done in the caller. """ if isinstance(fact, str): logger.info("Passing fact as a string is deprecated") fact = Fact.parse(fact) fact.start_time = start_time fact.end_time = end_time self.start_transaction() result = self.__add_fact(fact, temporary) self.end_transaction() if result: self.facts_changed() return result
def test_category(self): # plain activity name activity = Fact.parse("just a simple case@hämster") self.assertEqual(activity.activity, "just a simple case") self.assertEqual(activity.category, "hämster") assert activity.start_time is None assert activity.end_time is None assert not activity.description
def localized_fact(self): """Make sure fact has the correct start_time.""" fact = Fact.parse(self.activity.get_text()) if fact.start_time: fact.date = self.date else: fact.start_time = hamster_now() return fact
def test_plain_name(self): # plain activity name activity = Fact.parse("just a simple case with ütf-8") self.assertEqual(activity.activity, "just a simple case with ütf-8") assert activity.start_time is None assert activity.end_time is None assert not activity.category assert not activity.description
def extract_search(text): fact = Fact.parse(text) search = fact.activity if fact.category: search += "@%s" % fact.category if fact.tags: search += " #%s" % (" #".join(fact.tags)) return search
def test_full(self): # plain activity name activity = Fact.parse("1225-1325 case@cat, description #ta non-tag #tag #bäg") self.assertEqual(activity.start_time.strftime("%H:%M"), "12:25") self.assertEqual(activity.end_time.strftime("%H:%M"), "13:25") self.assertEqual(activity.activity, "case") self.assertEqual(activity.category, "cat") self.assertEqual(activity.description, "description #ta non-tag") self.assertEqual(set(activity.tags), set(["bäg", "tag"]))
def test_tags(self): # plain activity name activity = Fact.parse("case, with added #de description #and, #some #tägs") self.assertEqual(activity.activity, "case") self.assertEqual(activity.description, "with added #de description") self.assertEqual(set(activity.tags), set(["and", "some", "tägs"])) assert not activity.category assert activity.start_time is None assert activity.end_time is None
def test_description(self): # plain activity name activity = Fact.parse("case, with added descriptiön") self.assertEqual(activity.activity, "case") self.assertEqual(activity.description, "with added descriptiön") assert not activity.category assert activity.start_time is None assert activity.end_time is None assert not activity.category
def test_with_start_and_end_time(self): # with time activity = Fact.parse("12:35-14:25 with start-end time") self.assertEqual(activity.activity, "with start-end time") self.assertEqual(activity.start_time.strftime("%H:%M"), "12:35") self.assertEqual(activity.end_time.strftime("%H:%M"), "14:25") #rest must be empty assert not activity.category assert not activity.description
def complete_first(self): text = self.get_text() fact = Fact.parse(text) search = extract_search(text) if not self.complete_tree.rows or not fact.activity: return text, None label = self.complete_tree.rows[0].data if label.startswith(search): return text, label[len(search):] return text, None
def on_cmdline_changed(self, widget): if self.master_is_cmdline: fact = Fact.parse(self.cmdline.get_text(), date=self.date) previous_cmdline_fact = self.cmdline_fact # copy the entered fact before any modification self.cmdline_fact = fact.copy() if fact.start_time is None: fact.start_time = hamster_now() if fact.description == previous_cmdline_fact.description: # no change to description here, keep the main one fact.description = self.fact.description self.fact = fact self.update_fields()
def test_copy(self): fact1 = Fact.parse("12:25-13:25 case@cat, description #tag #bäg") fact2 = fact1.copy() self.assertEqual(fact1.start_time, fact2.start_time) self.assertEqual(fact1.end_time, fact2.end_time) self.assertEqual(fact1.activity, fact2.activity) self.assertEqual(fact1.category, fact2.category) self.assertEqual(fact1.description, fact2.description) self.assertEqual(fact1.tags, fact2.tags) fact3 = fact1.copy(activity="changed") self.assertEqual(fact3.activity, "changed") fact3 = fact1.copy(category="changed") self.assertEqual(fact3.category, "changed") fact3 = fact1.copy(description="changed") self.assertEqual(fact3.description, "changed") fact3 = fact1.copy(tags=["changed"]) self.assertEqual(fact3.tags, ["changed"])
def update_fact(self, fact_id, fact, start_time=None, end_time=None, temporary=False): self.start_transaction() self.__remove_fact(fact_id) # to be removed once update facts use Fact directly. if isinstance(fact, str): fact = Fact.parse(fact) fact = fact.copy(start_time=start_time, end_time=end_time) result = self.__add_fact(fact, temporary) if not result: logger.warning("failed to update fact {} ({})".format( fact_id, fact)) self.end_transaction() if result: self.facts_changed() return result
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.parse(text) now = stuff.hamster_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_fact.description = None 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 __add_fact(self, serialized_fact, start_time, end_time=None, temporary=False): fact = Fact.parse(serialized_fact) fact.start_time = start_time fact.end_time = end_time logger.info("adding fact {}".format(fact)) 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) # 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) <= hamster_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]) logger.info("fact successfully added, with id #{}".format(fact_id)) return fact_id