Beispiel #1
0
 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)
Beispiel #2
0
 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)
Beispiel #3
0
    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)
Beispiel #4
0
    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()
Beispiel #5
0
    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()
Beispiel #6
0
    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)
Beispiel #7
0
 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()
Beispiel #8
0
 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()
Beispiel #9
0
    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()
Beispiel #10
0
 def __get_todays_facts(self):
     return self.__get_facts(hamster_today())
Beispiel #11
0
    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
Beispiel #12
0
    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()
Beispiel #13
0
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