Beispiel #1
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)
Beispiel #2
0
    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
Beispiel #3
0
def fact_dict(fact_data, with_date):
    fact = {}
    if with_date:
        fmt = '%Y-%m-%d %H:%M'
    else:
        fmt = '%H:%M'

    fact['start'] = fact_data.start_time.strftime(fmt)
    if fact_data.end_time:
        fact['end'] = fact_data.end_time.strftime(fmt)
    else:
        end_date = stuff.hamster_now()
        fact['end'] = ''

    fact['duration'] = stuff.format_duration(fact_data.delta)

    fact['activity'] = fact_data.activity
    fact['category'] = fact_data.category
    if fact_data.tags:
        fact['tags'] = ' '.join('#%s' % tag for tag in fact_data.tags)
    else:
        fact['tags'] = ''

    fact['description'] = fact_data.description

    return fact
Beispiel #4
0
    def load_suggestions(self):
        self.todays_facts = self.storage.get_todays_facts()

        # list of facts of last month
        now = stuff.hamster_now()
        last_month = self.storage.get_facts(now - dt.timedelta(days=30), now)

        # naive recency and frequency rank
        # score is as simple as you get 30-days_ago points for each occurence
        suggestions = defaultdict(int)
        for fact in last_month:
            days = 30 - (now - dt.datetime.combine(
                fact.date, dt.time())).total_seconds() / 60 / 60 / 24
            label = fact.activity
            if fact.category:
                label += "@%s" % fact.category

            suggestions[label] += days

            if fact.tags:
                label += " #%s" % (" #".join(fact.tags))
                suggestions[label] += days

        for rec in self.storage.get_activities():
            label = rec["name"]
            if rec["category"]:
                label += "@%s" % rec["category"]
            suggestions[label] += 0

        # list of (label, score), higher scores first
        self.suggestions = sorted(suggestions.items(),
                                  key=lambda x: x[1],
                                  reverse=True)
Beispiel #5
0
    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 = hamster_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
Beispiel #6
0
    def __init__(self, start_time=None):
        graphics.Scene.__init__(self)
        self.set_can_focus(False)  # no interaction

        self.day_start = conf.day_start

        start_time = start_time or stuff.hamster_now()

        self.view_time = start_time or dt.datetime.combine(
            start_time.date(), self.day_start)

        self.scope_hours = 24

        self.fact_bars = []
        self.categories = []

        self.connect("on-enter-frame", self.on_enter_frame)

        self.plot_area = graphics.Sprite(y=15)

        self.chosen_selection = Selection()
        self.plot_area.add_child(self.chosen_selection)

        self.drag_start = None
        self.current_x = None

        self.date_label = graphics.Label(color=self._style.get_color(
            gtk.StateFlags.NORMAL),
                                         x=5,
                                         y=16)

        self.add_child(self.plot_area, self.date_label)
Beispiel #7
0
 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
Beispiel #8
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 #9
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 #10
0
    def add_fact(self, fact, temporary_activity=False):
        """Add fact (Fact)."""
        assert fact.activity, "missing activity"

        if not fact.start_time:
            logger.info("Adding fact without any start_time is deprecated")
            fact.start_time = hamster_now()

        dbus_fact = to_dbus_fact(fact)
        new_id = self.conn.AddFactVerbatim(dbus_fact)

        return new_id
Beispiel #11
0
 def __touch_fact(self, fact, end_time=None):
     end_time = end_time or hamster_now()
     # tasks under one minute do not count
     if end_time - fact.start_time < dt.timedelta(minutes=1):
         self.__remove_fact(fact.id)
     else:
         query = """
                    UPDATE facts
                       SET end_time = ?
                     WHERE id = ?
         """
         self.execute(query, (end_time, fact.id))
Beispiel #12
0
    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 = hamster_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)

        if fact.start_time is None:
            self.update_status(status="wrong", markup="Missing start time")
            return None

        if not fact.activity:
            self.update_status(status="wrong", markup="Missing activity")
            return None

        if (fact.delta < dt.timedelta(0)) and fact.end_time:
            fact.end_time += dt.timedelta(days=1)
            markup = dedent("""\
                            <b>Working late ?</b>
                            Duration would be negative.
                            This happens when the activity crosses the
                            hamster day start time ({:%H:%M} from tracking settings).

                            Changing the end time date to the next day.
                            Pressing the button would save
                            an actvity going from
                            {}
                            to
                            {}
                            (in civil local time)
                            """.format(conf.day_start, fact.start_time,
                                       fact.end_time))
            self.update_status(status="warning", markup=markup)
            return fact

        # nothing unusual
        self.update_status(status="looks good", markup="")
        return fact
Beispiel #13
0
 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()
Beispiel #14
0
    def add_fact(self, fact, start_time, end_time, temporary=False):
        """Add fact.

        fact: either a Fact instance or
              a string that can be parsed through Fact.parse.
        """
        if isinstance(fact, Fact):
            fact_str = fact.serialized_name()
        else:
            fact_str = fact
        start_time = start_time or hamster_now()

        self.start_transaction()
        result = self.__add_fact(fact_str, start_time, end_time, temporary)
        self.end_transaction()

        if result:
            self.facts_changed()
        return result
Beispiel #15
0
def figure_time(str_time):
    if not str_time or not str_time.strip():
        return None

    # strip everything non-numeric and consider hours to be first number
    # and minutes - second number
    numbers = re.split("\D", str_time)
    numbers = [x for x in numbers if x!=""]

    hours, minutes = None, None

    if len(numbers) == 1 and len(numbers[0]) == 4:
        hours, minutes = int(numbers[0][:2]), int(numbers[0][2:])
    else:
        if len(numbers) >= 1:
            hours = int(numbers[0])
        if len(numbers) >= 2:
            minutes = int(numbers[1])

    if (hours is None or minutes is None) or hours > 24 or minutes > 60:
        return None #no can do

    return hamster_now()
Beispiel #16
0
    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)
Beispiel #17
0
    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
Beispiel #18
0
def parse_fact(text, phase=None, res=None, date=None):
    """tries to extract fact fields from the string
        the optional arguments in the syntax makes us actually try parsing
        values and fallback to next phase
        start -> [end] -> activity[@category] -> tags

        Returns dict for the fact and achieved phase

        TODO - While we are now bit cooler and going recursively, this code
        still looks rather awfully spaghetterian. What is the real solution?

        Tentative syntax:
        [date] start_time[-end_time] activity[@category][, description]{[,] { })#tag}
        According to the legacy tests, # were allowed in the description
    """
    now = hamster_now()

    # determine what we can look for
    phases = [
        "date",  # hamster day
        "start_time",
        "end_time",
        "tags",
        "activity",
        "category",
    ]

    phase = phase or phases[0]
    phases = phases[phases.index(phase):]
    if res is None:
        res = {}

    text = text.strip()
    if not text:
        return res

    fragment = re.split("[\s|#]", text, 1)[0].strip()

    # remove a fragment assumed to be at the beginning of text
    remove_fragment = lambda text, fragment: text[len(fragment):]

    if "date" in phases:
        # if there is any date given, it must be at the front
        try:
            date = dt.datetime.strptime(fragment, DATE_FMT).date()
            remaining_text = remove_fragment(text, fragment)
        except ValueError:
            date = datetime_to_hamsterday(now)
            remaining_text = text
        return parse_fact(remaining_text, "start_time", res, date)

    if "start_time" in phases or "end_time" in phases:

        # -delta ?
        delta_re = re.compile("^-[0-9]{1,3}$")
        if delta_re.match(fragment):
            # TODO untested
            # delta_re was probably thought to be used
            # alone or together with a start_time
            # but using "now" prevents the latter
            res[phase] = now + dt.timedelta(minutes=int(fragment))
            remaining_text = remove_fragment(text, fragment)
            return parse_fact(remaining_text, phases[phases.index(phase)+1], res, date)

        # only starting time ?
        m = re.search(time_re, fragment)
        if m:
            time = extract_time(m)
            res[phase] = hamsterday_time_to_datetime(date, time)
            remaining_text = remove_fragment(text, fragment)
            return parse_fact(remaining_text, phases[phases.index(phase)+1], res, date)

        # start-end ?
        start, __, end = fragment.partition("-")
        m_start = re.search(time_re, start)
        m_end = re.search(time_re, end)
        if m_start and m_end:
            start_time = extract_time(m_start)
            end_time = extract_time(m_end)
            res["start_time"] = hamsterday_time_to_datetime(date, start_time)
            res["end_time"] = hamsterday_time_to_datetime(date, end_time)
            remaining_text = remove_fragment(text, fragment)
            return parse_fact(remaining_text, "tags", res, date)

    if "tags" in phases:
        # Need to start from the end, because
        # the description can hold some '#' characters
        tags = []
        remaining_text = text
        while True:
            m = re.search(tag_re, remaining_text)
            if not m:
                break
            tag = m.group(1)
            tags.append(tag)
            # strip the matched string (including #)
            remaining_text = remaining_text[:m.start()]
        # put tags back in input order
        res["tags"] = list(reversed(tags))
        return parse_fact(remaining_text, "activity", res, date)

    if "activity" in phases:
        activity = re.split("[@|#|,]", text, 1)[0]
        if looks_like_time(activity):
            # want meaningful activities
            return res

        res["activity"] = activity
        remaining_text = remove_fragment(text, activity)
        return parse_fact(remaining_text, "category", res, date)

    if "category" in phases:
        category, _, description = text.partition(",")
        res["category"] = category.lstrip("@").strip() or None
        res["description"] = description.strip() or None
        return res

    return {}
Beispiel #19
0
 def delta(self):
     """Duration (datetime.timedelta)."""
     end_time = self.end_time if self.end_time else hamster_now()
     return end_time - self.start_time
Beispiel #20
0
    def on_enter_frame(self, scene, context):
        g = graphics.Graphics(context)

        self.plot_area.height = self.height - 30

        vertical = min(self.plot_area.height / 5, 7)
        minute_pixel = (self.scope_hours * 60.0 - 15) / self.width

        g.set_line_style(width=1)
        g.translate(0.5, 0.5)

        colors = {
            "normal":
            self._style.get_color(gtk.StateFlags.NORMAL),
            "normal_bg":
            self._style.get_background_color(gtk.StateFlags.NORMAL),
            "selected":
            self._style.get_color(gtk.StateFlags.SELECTED),
            "selected_bg":
            self._style.get_background_color(gtk.StateFlags.SELECTED),
        }

        bottom = self.plot_area.y + self.plot_area.height

        for bar in self.fact_bars:
            bar.y = vertical * bar.category + 5
            bar.height = vertical

            bar_start_time = bar.fact.start_time - self.view_time
            minutes = bar_start_time.seconds / 60 + bar_start_time.days * self.scope_hours * 60

            bar.x = round(minutes / minute_pixel) + 0.5
            bar.width = round((bar.fact.delta).seconds / 60 / minute_pixel)

        if self.chosen_selection.start_time and self.chosen_selection.width is None:
            # we have time but no pixels
            minutes = round(
                (self.chosen_selection.start_time - self.view_time).seconds /
                60 / minute_pixel) + 0.5
            self.chosen_selection.x = minutes
            if self.chosen_selection.end_time:
                self.chosen_selection.width = round(
                    (self.chosen_selection.end_time -
                     self.chosen_selection.start_time).seconds / 60 /
                    minute_pixel)
            else:
                self.chosen_selection.width = 0
            self.chosen_selection.height = self.chosen_selection.parent.height

            # use the oportunity to set proper colors too
            self.chosen_selection.fill = colors['selected_bg']
            self.chosen_selection.duration_label.color = colors['selected']

        #time scale
        g.set_color("#000")

        background = colors["normal_bg"]
        text = colors["normal"]

        tick_color = g.colors.contrast(background, 80)

        layout = g.create_layout(size=10)
        for i in range(self.scope_hours * 60):
            time = (self.view_time + dt.timedelta(minutes=i))

            g.set_color(tick_color)
            if time.minute == 0:
                g.move_to(round(i / minute_pixel), bottom - 15)
                g.line_to(round(i / minute_pixel), bottom)
                g.stroke()
            elif time.minute % 15 == 0:
                g.move_to(round(i / minute_pixel), bottom - 5)
                g.line_to(round(i / minute_pixel), bottom)
                g.stroke()

            if time.minute == 0 and time.hour % 4 == 0:
                if time.hour == 0:
                    g.move_to(round(i / minute_pixel), self.plot_area.y)
                    g.line_to(round(i / minute_pixel), bottom)
                    label_minutes = time.strftime("%b %d")
                else:
                    label_minutes = time.strftime(
                        "%H<small><sup>%M</sup></small>")

                g.set_color(text)
                layout.set_markup(label_minutes)

                g.move_to(round(i / minute_pixel) + 2, 0)
                pangocairo.show_layout(context, layout)

        #current time
        if self.view_time < stuff.hamster_now(
        ) < self.view_time + dt.timedelta(hours=self.scope_hours):
            minutes = round((stuff.hamster_now() - self.view_time).seconds /
                            60 / minute_pixel)
            g.rectangle(minutes, 0, self.width, self.height)
            g.fill(colors['normal_bg'], 0.7)

            g.move_to(minutes, self.plot_area.y)
            g.line_to(minutes, bottom)
            g.stroke("#f00", 0.4)
Beispiel #21
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 #22
0
    def __get_facts(self, date, end_date=None, search_terms=""):
        split_time = conf.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, seconds=-1)

        logger.info("searching for facts from {} to {}".format(
            datetime_from, datetime_to))

        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 \
                 (hamster_now() - fact["start_time"]) <= dt.timedelta(hours=12):
                fact_end_time = hamster_now()
            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["start_time"] < datetime_from - dt.timedelta(days=30):
                # ignoring old on-going facts
                continue

            fact["date"] = fact_date
            fact["delta"] = fact_end_time - fact["start_time"]
            res.append(fact)

        return res
Beispiel #23
0
 def stop_tracking(self, end_time=None):
     """Stop tracking current activity. end_time can be passed in if the
     activity should have other end time than the current moment"""
     end_time = timegm((end_time or hamster_now()).timetuple())
     return self.conn.StopTracking(end_time)
Beispiel #24
0
    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"]))
Beispiel #25
0
    def __add_fact(self, fact, temporary=False):
        """Add fact to database.

        Args:
            fact (Fact)
        Returns:
            int, the new fact id in the database (> 0) on success,
                 0 if nothing needed to be done
                 (e.g. if the same fact was already on-going),
                 note: a sanity check on the given fact is performed first,
                       that would raise an AssertionError.
                       Other errors would also be handled through exceptions.
        """
        logger.info("adding fact {}".format(fact))

        start_time = fact.start_time
        end_time = fact.end_time

        # 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 = 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 is 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 "")):
                    logger.info("same fact, already on-going, nothing to do")
                    return 0

                # 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
Beispiel #26
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