def test_range(self): t1 = dt.datetime(2020, 1, 15, 13, 30) t2 = dt.datetime(2020, 1, 15, 15, 30) range = dt.Range(t1, t2) fact = Fact(range=range) self.assertEqual(fact.range.start, t1) self.assertEqual(fact.range.end, t2) fact = Fact(start=t1, end=t2) self.assertEqual(fact.range.start, t1) self.assertEqual(fact.range.end, t2) # backward compatibility (before v3.0) fact = Fact(start_time=t1, end_time=t2) self.assertEqual(fact.range.start, t1) self.assertEqual(fact.range.end, t2)
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 _dbfact_to_libfact(self, db_fact): """Convert a db fact (coming from __group_facts) to Fact.""" return Fact(activity=db_fact["name"], category=db_fact["category"], description=db_fact["description"], tags=db_fact["tags"], start_time=db_fact["start_time"], end_time=db_fact["end_time"], id=db_fact["id"], activity_id=db_fact["activity_id"])
def from_dbus_fact(dbus_fact): """unpack the struct into a proper dict""" return Fact(activity=dbus_fact[4], start_time=dt.datetime.utcfromtimestamp(dbus_fact[1]), end_time=dt.datetime.utcfromtimestamp(dbus_fact[2]) if dbus_fact[2] else None, description=dbus_fact[3], activity_id=dbus_fact[5], category=dbus_fact[6], tags=dbus_fact[7], id=dbus_fact[0])
def from_dbus_fact_json(dbus_fact): """Convert D-Bus JSON to Fact.""" d = loads(dbus_fact) range_d = d['range'] # should use pdt.datetime.fromisoformat, # but that appears only in python3.7, nevermind start_s = range_d['start'] end_s = range_d['end'] range = dt.Range(start=dt.datetime.parse(start_s) if start_s else None, end=dt.datetime.parse(end_s) if end_s else None) d['range'] = range return Fact(**d)
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 from_dbus_fact(dbus_fact): """Unpack the struct into a proper dict. Legacy: to besuperceded by from_dbus_fact_json at some point. """ return Fact(activity=dbus_fact[4], start_time=dt.datetime.utcfromtimestamp(dbus_fact[1]), end_time=dt.datetime.utcfromtimestamp(dbus_fact[2]) if dbus_fact[2] else None, description=dbus_fact[3], activity_id=dbus_fact[5], category=dbus_fact[6], tags=dbus_fact[7], id=dbus_fact[0])
def check_fact(cls, fact, default_day=None): """Check Fact validity for inclusion in the storage. Raise FactError(message) on failure. """ if fact.start_time is None: raise FactError("Missing start time") if fact.end_time and (fact.delta < dt.timedelta(0)): fixed_fact = Fact(start_time=fact.start_time, end_time=fact.end_time + dt.timedelta(days=1)) suggested_range_str = fixed_fact.range.format(default_day=default_day) # work around cyclic imports from hamster.lib.configuration import conf raise FactError(dedent( """\ Duration would be negative. Working late ? This happens when the activity crosses the hamster day start time ({:%H:%M} from tracking settings). Suggestion: move the end to the next day; the range would become: {} (in civil local time) """.format(conf.day_start, suggested_range_str) )) if not fact.activity: raise FactError("Missing activity") if ',' in fact.category: raise FactError(dedent( """\ Forbidden comma in category: '{}' Note: The description separator changed from single comma to double comma ',,' (cf. PR #482). """.format(fact.category) ))
def __init__(self, action, fact_id=None): Controller.__init__(self) self._date = None # for the date property self._gui = load_ui_file("edit_activity.ui") self.window = self.get_widget('custom_fact_window') self.window.set_size_request(600, 200) self.action = action 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( parent=self.get_widget("cmdline box")) 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( parent=self.get_widget("end time box")) self.start_date = widgets.Calendar( widget=self.get_widget("start date"), expander=self.get_widget("start date expander")) self.start_time = widgets.TimeInput( parent=self.get_widget("start time box")) 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() title = _("Update activity") if action == "edit" else _("Add activity") self.window.set_title(title) self.get_widget("delete_button").set_sensitive(action == "edit") if action == "edit": self.fact = runtime.storage.get_fact(fact_id) elif action == "clone": base_fact = runtime.storage.get_fact(fact_id) self.fact = base_fact.copy(start_time=dt.datetime.now(), end_time=None) else: self.fact = Fact(start_time=dt.datetime.now()) original_fact = self.fact # TODO: should use hday, not date. self.date = self.fact.date 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()
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: # fact is a sqlite.Row, indexable by column name fact_end_time = fact["end_time"] or hamster_now() # 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: logger.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(activity=fact["name"], category=fact["category"], description=fact["description"], start_time=end_time, end_time=fact_end_time) storage.Storage.check_fact(new_fact) new_fact_id = self.__add_fact(new_fact) # 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 # overlap start elif start_time < fact["start_time"] < end_time: logger.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: logger.info("Overlapping end of %s" % fact["name"]) self.execute("UPDATE facts SET end_time=? WHERE id=?", (start_time, fact["id"]))