def test_round_trip(self): fact = Fact.parse( "11:00 12:00 activity, with comma@category,, description, with comma #and #tags" ) dbus_fact = to_dbus_fact_json(fact) return_fact = from_dbus_fact_json(dbus_fact) self.assertEqual(return_fact, fact) dbus_fact = to_dbus_fact(fact) return_fact = from_dbus_fact(dbus_fact) self.assertEqual(return_fact, fact) fact = Fact.parse("11:00 activity") dbus_fact = to_dbus_fact_json(fact) return_fact = from_dbus_fact_json(dbus_fact) self.assertEqual(return_fact, fact) dbus_fact = to_dbus_fact(fact) return_fact = from_dbus_fact(dbus_fact) self.assertEqual(return_fact, fact) range, __ = dt.Range.parse("2020-01-19 11:00 - 2020-01-19 12:00") dbus_range = to_dbus_range(range) return_range = from_dbus_range(dbus_range) self.assertEqual(return_range, range)
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_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()) # empty fact fact3 = Fact() self.assertEqual(fact3.serialized(), "")
def AddFact(self, fact_str, start_time, end_time, temporary=False): """Add fact specified by a string. Args: fact_str (str): string to be parsed. start_time (int): Start datetime timestamp. For backward compatibility with the gnome shell extension, 0 is special and means hamster_now(). Otherwise, overrides the parsed value. -1 means None. end_time (int): Start datetime timestamp. If different from 0, overrides the parsed value. -1 means None. Returns: fact id (int), or 0 in case of failure. Note: see datetime.utcfromtimestamp documentation for the precise meaning of timestamps. """ fact = Fact.parse(fact_str) if start_time == -1: fact.start_time = None elif start_time == 0: fact.start_time = stuff.hamster_now() else: fact.start_time = dt.datetime.utcfromtimestamp(start_time) if end_time == -1: fact.end_time = None elif end_time != 0: fact.end_time = dt.datetime.utcfromtimestamp(end_time) return self.add_fact(fact) or 0
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 validate_fields(self): """Check fields information. Update gui status about entry and description validity. Try to merge date, activity and description informations. Return the consolidated fact if successful, or None. """ fact = self.fact now = dt.datetime.now() self.get_widget("button-next-day").set_sensitive(self.date < now.date()) if self.date == now.date(): default_dt = now else: default_dt = dt.datetime.combine(self.date, now.time()) self.draw_preview(fact.start_time or default_dt, fact.end_time or default_dt) try: runtime.storage.check_fact(fact, default_day=self.date) except FactError as error: self.update_status(status="wrong", markup=str(error)) return None roundtrip_fact = Fact.parse(fact.serialized(), default_day=self.date) if roundtrip_fact != fact: self.update_status(status="wrong", markup="Fact could not be parsed back") return None # nothing unusual self.update_status(status="looks good", markup="") return fact
def test_roundtrips(self): for start_time in ( None, dt.time(12, 33), ): for end_time in ( None, dt.time(13, 34), ): for activity in ( "activity", "#123 with two #hash", "activity, with comma", ): for category in ( "", "category", ): for description in ( "", "description", "with #hash", "with, comma", "with @at", ): for tags in ( [], ["single"], ["with space"], ["two", "tags"], ["with @at"], ): start = hamsterday_time_to_datetime( hamster_today(), start_time) if start_time else None end = hamsterday_time_to_datetime( hamster_today(), end_time) if end_time else None if end and not start: # end without start is not parseable continue fact = Fact(start_time=start, end_time=end, activity=activity, category=category, description=description, tags=tags) for range_pos in ("head", "tail"): fact_str = fact.serialized( range_pos=range_pos) parsed = Fact.parse(fact_str, range_pos=range_pos) self.assertEqual(fact, parsed) self.assertEqual(parsed.activity, fact.activity) self.assertEqual(parsed.category, fact.category) self.assertEqual(parsed.description, fact.description) self.assertEqual(parsed.tags, fact.tags)
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 # better fail before opening the transaction self.check_fact(fact) self.start_transaction() result = self.__add_fact(fact, temporary) self.end_transaction() if result: self.facts_changed() return result
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) fact2 = fact1.copy() fact2.range.start = fact1.range.start + dt.timedelta(minutes=1) self.assertNotEqual(fact1, fact2) fact2 = fact1.copy() fact2.range.end = fact1.range.end + dt.timedelta(minutes=1) 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 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 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_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_tags(self): # plain activity name activity = Fact.parse( "#case,, description with #hash,, #and, #some #tägs") self.assertEqual(activity.activity, "#case") self.assertEqual(activity.description, "description with #hash") 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_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 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 start(self, *args): '''Start a new activity.''' if not args: print("Error: please specify activity") return fact = Fact.parse(" ".join(args), range_pos="tail") if fact.start_time is None: fact.start_time = dt.datetime.now() self.storage.check_fact(fact, default_day=dt.hday.today()) self.storage.add_fact(fact)
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 test_commas(self): fact = Fact.parse( "11:00 12:00 activity, with comma@category,, description, with comma" ) self.assertEqual(fact.activity, "activity, with comma") self.assertEqual(fact.category, "category") self.assertEqual(fact.description, "description, with comma") self.assertEqual(fact.tags, []) fact = Fact.parse( "11:00 12:00 activity, with comma@category,, description, with comma, #tag1, #tag2" ) self.assertEqual(fact.activity, "activity, with comma") self.assertEqual(fact.category, "category") self.assertEqual(fact.description, "description, with comma") self.assertEqual(fact.tags, ["tag1", "tag2"]) fact = Fact.parse( "11:00 12:00 activity, with comma@category,, description, with comma and #hash,, #tag1, #tag2" ) self.assertEqual(fact.activity, "activity, with comma") self.assertEqual(fact.category, "category") self.assertEqual(fact.description, "description, with comma and #hash") self.assertEqual(fact.tags, ["tag1", "tag2"])
def on_cmdline_changed(self, widget): if self.master_is_cmdline: fact = Fact.parse(self.cmdline.get_text(), default_day=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 = dt.datetime.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 update_fact(self, fact_id, fact, start_time=None, end_time=None, temporary=False): # better fail before opening the transaction self.check_fact(fact) 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 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 AddFact(self, fact_str, start_time, end_time, temporary): """Add fact specified by a string. If the parsed fact has no start, then now is used. To fully use the hamster fact parser, as on the cmdline, just pass 0 for start_time and end_time. Args: fact_str (str): string to be parsed. start_time (int): Start datetime ovveride timestamp (ignored if 0). -1 means None. end_time (int): datetime ovveride timestamp (ignored if 0). -1 means None. #temporary (boolean): historical mystery, ignored, but needed to keep the method signature stable. Do not forget to pass something (e.g. False)! Returns: fact id (int), 0 means failure. Note: see datetime.utcfromtimestamp documentation for the precise meaning of timestamps. """ fact = Fact.parse(fact_str) # default value if none found if not fact.start_time: fact.start_time = dt.datetime.now() if start_time == -1: fact.start_time = None elif start_time != 0: fact.start_time = dt.datetime.utcfromtimestamp(start_time) if end_time == -1: fact.end_time = None elif end_time != 0: fact.end_time = dt.datetime.utcfromtimestamp(end_time) return self.add_fact(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] """ 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.todays_facts and 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(default_day=self.default_day) 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 test_only_range(self): fact = Fact.parse("-20") assert not fact.activity fact = Fact.parse("-20 -10") assert not fact.activity