def test_dt_patterns(self): p = specific_dt_pattern(1) s = "12:03" m = re.fullmatch(p, s, re.VERBOSE) time = _extract_datetime(m, d="date1", h="hour1", m="minute1", r="relative1", default_day=hamster_today()) self.assertEqual(time.strftime("%H:%M"), "12:03") s = "2019-12-01 12:36" m = re.fullmatch(p, s, re.VERBOSE) time = _extract_datetime(m, d="date1", h="hour1", m="minute1", r="relative1") self.assertEqual(time.strftime("%Y-%m-%d %H:%M"), "2019-12-01 12:36") s = "-25" m = re.fullmatch(p, s, re.VERBOSE) timedelta = _extract_datetime(m, d="date1", h="hour1", m="minute1", r="relative1", default_day=hamster_today()) self.assertEqual(timedelta, dt.timedelta(minutes=-25)) s = "2019-12-05" m = re.search(p, s, re.VERBOSE) self.assertEqual(m, None)
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 __init__(self): gtk.HeaderBar.__init__(self) self.set_show_close_button(True) box = gtk.Box(False) self.time_back = gtk.Button.new_from_icon_name("go-previous-symbolic", gtk.IconSize.MENU) self.time_forth = gtk.Button.new_from_icon_name( "go-next-symbolic", gtk.IconSize.MENU) box.add(self.time_back) box.add(self.time_forth) gtk.StyleContext.add_class(box.get_style_context(), "linked") self.pack_start(box) self.range_pick = RangePick(stuff.hamster_today()) self.pack_start(self.range_pick) self.system_button = gtk.MenuButton() self.system_button.set_image( gtk.Image.new_from_icon_name("open-menu-symbolic", gtk.IconSize.MENU)) self.system_button.set_tooltip_markup(_("Menu")) self.pack_end(self.system_button) self.search_button = gtk.ToggleButton() self.search_button.set_image( gtk.Image.new_from_icon_name("edit-find-symbolic", gtk.IconSize.MENU)) self.search_button.set_tooltip_markup(_("Filter activities")) self.pack_end(self.search_button) self.stop_button = gtk.Button() self.stop_button.set_image( gtk.Image.new_from_icon_name("process-stop-symbolic", gtk.IconSize.MENU)) self.stop_button.set_tooltip_markup(_("Stop tracking (Ctrl-SPACE)")) self.pack_end(self.stop_button) self.add_activity_button = gtk.Button() self.add_activity_button.set_image( gtk.Image.new_from_icon_name("list-add-symbolic", gtk.IconSize.MENU)) self.add_activity_button.set_tooltip_markup(_("Add activity (Ctrl-+)")) self.pack_end(self.add_activity_button) self.system_menu = gtk.Menu() self.system_button.set_popup(self.system_menu) self.menu_export = gtk.MenuItem(label=_("Export...")) self.system_menu.append(self.menu_export) self.menu_prefs = gtk.MenuItem(label=_("Tracking Settings")) self.system_menu.append(self.menu_prefs) self.menu_help = gtk.MenuItem(label=_("Help")) self.system_menu.append(self.menu_help) self.system_menu.show_all() self.time_back.connect("clicked", self.on_time_back_click) self.time_forth.connect("clicked", self.on_time_forth_click) self.connect("button-press-event", self.on_button_press)
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.activity = widgets.ActivityEntry() self.activity.connect("changed", self.on_activity_changed) self.get_widget("activity_box").add(self.activity) self.day_start = conf.day_start 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.description_buffer.connect("changed", self.on_description_changed) self.save_button = self.get_widget("save_button") self.activity.grab_focus() if fact_id: # editing fact = runtime.storage.get_fact(fact_id) self.date = fact.date original_fact = fact self.window.set_title(_("Update activity")) else: self.date = hamster_today() self.get_widget("delete_button").set_sensitive(False) if base_fact: # start a clone now. original_fact = base_fact.copy(start_time=hamster_now(), end_time=None) else: original_fact = None if original_fact: stripped_fact = original_fact.copy() stripped_fact.description = None label = stripped_fact.serialized(prepend_date=False) with self.activity.handler_block(self.activity.checker): self.activity.set_text(label) time_len = len(label) - len(stripped_fact.serialized_name()) self.activity.select_region(0, time_len - 1) self.description_buffer.set_text(original_fact.description) self.activity.original_fact = original_fact self._gui.connect_signals(self) self.validate_fields() self.window.show_all()
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.activity = widgets.ActivityEntry() self.activity.connect("changed", self.on_activity_changed) self.get_widget("activity_box").add(self.activity) self.day_start = conf.day_start self.dayline = widgets.DayLine() self._gui.get_object("day_preview").add(self.dayline) self.activity.grab_focus() if fact_id: # editing fact = runtime.storage.get_fact(fact_id) self.date = fact.date original_fact = fact self.get_widget("save_button").set_label("gtk-save") self.window.set_title(_("Update activity")) else: self.date = hamster_today() self.get_widget("delete_button").set_sensitive(False) if base_fact: # cloning original_fact = base_fact.copy() # start running now. # Do not try to pass end_time=None to copy(), above; # it would be discarded. original_fact.start_time = dt.datetime.now() original_fact.end_time = None else: original_fact = None if original_fact: label = original_fact.serialized(prepend_date=False) with self.activity.handler_block(self.activity.checker): self.activity.set_text(label) time_len = len(label) - len(original_fact.serialized_name()) self.activity.select_region(time_len, -1) buf = gtk.TextBuffer() buf.set_text(original_fact.description or "") self.get_widget('description').set_buffer(buf) self.activity.original_fact = original_fact self._gui.connect_signals(self) self.validate_fields() self.window.show_all()
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 = stuff.hamster_now() self.storage.check_fact(fact, default_day=stuff.hamster_today()) self.storage.add_fact(fact)
def on_start_date_changed(self, widget): if not self.master_is_cmdline: if self.fact.start_time: previous_date = self.fact.start_time.date() new_date = self.start_date.date delta = new_date - previous_date self.fact.start_time += delta if self.fact.end_time: # preserve fact duration self.fact.end_time += delta self.end_date.date = self.fact.end_time self.date = self.fact.date or hamster_today() self.validate_fields() self.update_cmdline()
def on_start_time_changed(self, widget): if not self.master_is_cmdline: # note: resist the temptation to preserve duration here; # for instance, end time might be at the beginning of next fact. new_time = self.start_time.time if new_time: if self.fact.start_time: new_start_time = dt.datetime.combine( self.fact.start_time.date(), new_time) else: # date not specified; result must fall in current hamster_day new_start_time = hamsterday_time_to_datetime( hamster_today(), new_time) else: new_start_time = None self.fact.start_time = new_start_time # let start_date extract date or handle None self.start_date.date = new_start_time self.validate_fields() self.update_cmdline()
def set_facts(self, facts): # FactTree adds attributes to its facts. isolate these side effects # copy the id too; most of the checks are based on id here. self.facts = [fact.copy(id=fact.id) for fact in facts] del facts # make sure facts is not used by inadvertance below. self.y = 0 self.hover_fact = None if self.vadjustment: self.vadjustment.set_value(0) if self.facts: start = self.facts[0].date end = self.facts[-1].date else: start = end = stuff.hamster_today() by_date = defaultdict(list) for fact in self.facts: by_date[fact.date].append(fact) days = [] for i in range((end-start).days + 1): current_date = start + dt.timedelta(days=i) days.append((current_date, by_date[current_date])) self.days = days self.set_row_heights() if (self.current_fact and self.current_fact.id in (fact.id for fact in self.facts) ): self.on_scroll() else: # will also trigger an on_scroll self.unset_current_fact()
def __get_todays_facts(self): return self.__get_facts(hamster_today())
def __get_facts(self, date, end_date=None, search_terms=""): try: from hamster.lib.configuration import conf day_start = conf.get("day_start_minutes") day_start_h, day_start_m = divmod(day_start) except: # default day start to 5am day_start_h = 5 day_start_m = 0 day_start = dt.time(day_start_h, day_start_m) split_time = day_start datetime_from = dt.datetime.combine(date, split_time) end_date = end_date or date datetime_to = dt.datetime.combine(end_date, split_time) + dt.timedelta(days=1) query = """ SELECT a.id AS id, a.start_time AS start_time, a.end_time AS end_time, a.description as description, b.name AS name, b.id as activity_id, coalesce(c.name, ?) as category, e.name as tag FROM facts a LEFT JOIN activities b ON a.activity_id = b.id LEFT JOIN categories c ON b.category_id = c.id LEFT JOIN fact_tags d ON d.fact_id = a.id LEFT JOIN tags e ON e.id = d.tag_id WHERE (a.end_time >= ? OR a.end_time IS NULL) AND a.start_time <= ? """ if search_terms: # check if we need changes to the index self.__check_index(datetime_from, datetime_to) # flip the query around when it starts with "not " reverse_search_terms = search_terms.lower().startswith("not ") if reverse_search_terms: search_terms = search_terms[4:] search_terms = search_terms.replace('\\', '\\\\').replace( '%', '\\%').replace('_', '\\_').replace("'", "''") query += """ AND a.id %s IN (SELECT id FROM fact_index WHERE fact_index MATCH '%s')""" % ( 'NOT' if reverse_search_terms else '', search_terms) query += " ORDER BY a.start_time, e.name" facts = self.fetchall( query, (self._unsorted_localized, datetime_from, datetime_to)) #first let's put all tags in an array facts = self.__group_tags(facts) res = [] for fact in facts: # heuristics to assign tasks to proper days # if fact has no end time, set the last minute of the day, # or current time if fact has happened in last 12 hours if fact["end_time"]: fact_end_time = fact["end_time"] elif (hamster_today() == fact["start_time"].date()) or \ (dt.datetime.now() - fact["start_time"]) <= dt.timedelta(hours=12): fact_end_time = dt.datetime.now().replace(microsecond=0) else: fact_end_time = fact["start_time"] fact_start_date = fact["start_time"].date() \ - dt.timedelta(1 if fact["start_time"].time() < split_time else 0) fact_end_date = fact_end_time.date() \ - dt.timedelta(1 if fact_end_time.time() < split_time else 0) fact_date_span = fact_end_date - fact_start_date # check if the task spans across two dates if fact_date_span.days == 1: datetime_split = dt.datetime.combine(fact_end_date, split_time) start_date_duration = datetime_split - fact["start_time"] end_date_duration = fact_end_time - datetime_split if start_date_duration > end_date_duration: # most of the task was done during the previous day fact_date = fact_start_date else: fact_date = fact_end_date else: # either doesn't span or more than 24 hrs tracked # (in which case we give up) fact_date = fact_start_date if fact_date < date or fact_date > end_date: # due to spanning we've jumped outside of given period continue fact["date"] = fact_date fact["delta"] = fact_end_time - fact["start_time"] res.append(fact) return res
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()
def parse_datetime_range(text, position="exact", separator="\s+", default_day=None, ref="now"): """Parse a start-end range from text. position (str): "exact" to match exactly the full text "head" to search only at the beginning of text, and "tail" to search only at the end. separator (str): regexp pattern (e.g. '\s+') meant to separate the datetime from the rest. Discarded for "exact" position. default_day (dt.date): If start is given without any date (e.g. just hh:mm), put the corresponding datetime in default_day. Defaults to hamster_today. Note: the default end day is always the start day, so "2019-11-27 23:50 - 00:20" lasts 30 minutes. ref (dt.datetime): reference for relative times (e.g. -15: quarter hour before ref). For testing purposes only (note: this will be removed later on, and replaced with hamster_now mocking in pytest). For users, it should be "now". Return: (start, end, rest) """ if ref == "now": ref = hamster_now() if default_day is None: default_day = hamster_today() assert position in ("exact", "head", "tail"), "position unknown: '{}'".format(position) if position == "exact": p = "^{}$".format(range_pattern) elif position == "head": # require at least a space after, to avoid matching 10.00@cat # .*? so rest is as little as possible p = "^{}{}(?P<rest>.*?)$".format(range_pattern, separator) elif position == "tail": # require at least a space after, to avoid matching #10.00 # .*? so rest is as little as possible p = "^(?P<rest>.*?){}{}$".format(separator, range_pattern) # no need to compile, recent patterns are cached by re m = re.search(p, text, flags=re.VERBOSE) if not m: return None, None, text elif position == "exact": rest = "" else: rest = m.group("rest") if m.group('firstday'): # only day given for start firstday = parse_date(m.group('firstday')) start = hamsterday_start(firstday) else: firstday = None start = _extract_datetime(m, d="date1", h="hour1", m="minute1", r="relative1", default_day=default_day) if isinstance(start, dt.timedelta): # relative to ref, actually delta1 = start start = ref + delta1 if m.group('lastday'): lastday = parse_date(m.group('lastday')) end = hamsterday_end(lastday) elif firstday: end = hamsterday_end(firstday) else: end = _extract_datetime(m, d="date2", h="hour2", m="minute2", r="relative2", default_day=datetime_to_hamsterday(start)) if isinstance(end, dt.timedelta): # relative to start, actually delta2 = end if delta2 > dt.timedelta(0): # wip: currently not reachable (would need [-\+]\d{1,3} in the parser). end = start + delta2 elif ref and delta2 < dt.timedelta(0): end = ref + delta2 else: end = None return start, end, rest