Esempio n. 1
0
 def test_range(self):
     t1 = dt.datetime(2020, 1, 15, 13, 30)
     t2 = dt.datetime(2020, 1, 15, 15, 30)
     range = dt.Range(t1, t2)
     fact = Fact(range=range)
     self.assertEqual(fact.range.start, t1)
     self.assertEqual(fact.range.end, t2)
     fact = Fact(start=t1, end=t2)
     self.assertEqual(fact.range.start, t1)
     self.assertEqual(fact.range.end, t2)
     # backward compatibility (before v3.0)
     fact = Fact(start_time=t1, end_time=t2)
     self.assertEqual(fact.range.start, t1)
     self.assertEqual(fact.range.end, t2)
Esempio n. 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)
Esempio n. 3
0
 def _dbfact_to_libfact(self, db_fact):
     """Convert a db fact (coming from __group_facts) to Fact."""
     return Fact(activity=db_fact["name"],
                 category=db_fact["category"],
                 description=db_fact["description"],
                 tags=db_fact["tags"],
                 start_time=db_fact["start_time"],
                 end_time=db_fact["end_time"],
                 id=db_fact["id"],
                 activity_id=db_fact["activity_id"])
Esempio n. 4
0
def from_dbus_fact(dbus_fact):
    """unpack the struct into a proper dict"""
    return Fact(activity=dbus_fact[4],
                start_time=dt.datetime.utcfromtimestamp(dbus_fact[1]),
                end_time=dt.datetime.utcfromtimestamp(dbus_fact[2])
                if dbus_fact[2] else None,
                description=dbus_fact[3],
                activity_id=dbus_fact[5],
                category=dbus_fact[6],
                tags=dbus_fact[7],
                id=dbus_fact[0])
Esempio n. 5
0
def from_dbus_fact_json(dbus_fact):
    """Convert D-Bus JSON to Fact."""
    d = loads(dbus_fact)
    range_d = d['range']
    # should use pdt.datetime.fromisoformat,
    # but that appears only in python3.7, nevermind
    start_s = range_d['start']
    end_s = range_d['end']
    range = dt.Range(start=dt.datetime.parse(start_s) if start_s else None,
                     end=dt.datetime.parse(end_s) if end_s else None)
    d['range'] = range
    return Fact(**d)
Esempio n. 6
0
 def test_spaces(self):
     # cf. issue #114
     fact = Fact.parse("11:00 12:00 BPC-261 - Task title@Project#code")
     self.assertEqual(fact.activity, "BPC-261 - Task title")
     self.assertEqual(fact.category, "Project")
     self.assertEqual(fact.description, "")
     self.assertEqual(fact.tags, ["code"])
     # space between category and tag
     fact2 = Fact.parse("11:00 12:00 BPC-261 - Task title@Project #code")
     self.assertEqual(fact.serialized(), fact2.serialized())
     # empty fact
     fact3 = Fact()
     self.assertEqual(fact3.serialized(), "")
Esempio n. 7
0
def from_dbus_fact(dbus_fact):
    """Unpack the struct into a proper dict.

    Legacy: to besuperceded by from_dbus_fact_json at some point.
    """
    return Fact(activity=dbus_fact[4],
                start_time=dt.datetime.utcfromtimestamp(dbus_fact[1]),
                end_time=dt.datetime.utcfromtimestamp(dbus_fact[2])
                if dbus_fact[2] else None,
                description=dbus_fact[3],
                activity_id=dbus_fact[5],
                category=dbus_fact[6],
                tags=dbus_fact[7],
                id=dbus_fact[0])
Esempio n. 8
0
    def check_fact(cls, fact, default_day=None):
        """Check Fact validity for inclusion in the storage.

        Raise FactError(message) on failure.
        """
        if fact.start_time is None:
            raise FactError("Missing start time")

        if fact.end_time and (fact.delta < dt.timedelta(0)):
            fixed_fact = Fact(start_time=fact.start_time,
                              end_time=fact.end_time + dt.timedelta(days=1))
            suggested_range_str = fixed_fact.range.format(default_day=default_day)
            # work around cyclic imports
            from hamster.lib.configuration import conf
            raise FactError(dedent(
                """\
                Duration would be negative.
                Working late ?
                This happens when the activity crosses the
                hamster day start time ({:%H:%M} from tracking settings).

                Suggestion: move the end to the next day; the range would become:
                {}
                (in civil local time)
                """.format(conf.day_start, suggested_range_str)
                ))

        if not fact.activity:
            raise FactError("Missing activity")

        if ',' in fact.category:
            raise FactError(dedent(
                """\
                Forbidden comma in category: '{}'
                Note: The description separator changed
                      from single comma to double comma ',,' (cf. PR #482).
                """.format(fact.category)
                ))
Esempio n. 9
0
    def __init__(self, action, fact_id=None):
        Controller.__init__(self)

        self._date = None  # for the date property

        self._gui = load_ui_file("edit_activity.ui")
        self.window = self.get_widget('custom_fact_window')
        self.window.set_size_request(600, 200)

        self.action = action
        self.fact_id = fact_id

        self.category_entry = widgets.CategoryEntry(
            widget=self.get_widget('category'))
        self.activity_entry = widgets.ActivityEntry(
            widget=self.get_widget('activity'),
            category_widget=self.category_entry)

        self.cmdline = widgets.CmdLineEntry(
            parent=self.get_widget("cmdline box"))
        self.cmdline.connect("focus_in_event", self.on_cmdline_focus_in_event)
        self.cmdline.connect("focus_out_event",
                             self.on_cmdline_focus_out_event)

        self.dayline = widgets.DayLine()
        self._gui.get_object("day_preview").add(self.dayline)

        self.description_box = self.get_widget('description')
        self.description_buffer = self.description_box.get_buffer()

        self.end_date = widgets.Calendar(
            widget=self.get_widget("end date"),
            expander=self.get_widget("end date expander"))

        self.end_time = widgets.TimeInput(
            parent=self.get_widget("end time box"))

        self.start_date = widgets.Calendar(
            widget=self.get_widget("start date"),
            expander=self.get_widget("start date expander"))

        self.start_time = widgets.TimeInput(
            parent=self.get_widget("start time box"))

        self.tags_entry = widgets.TagsEntry()
        self.get_widget("tags box").add(self.tags_entry)

        self.save_button = self.get_widget("save_button")

        # this will set self.master_is_cmdline
        self.cmdline.grab_focus()

        title = _("Update activity") if action == "edit" else _("Add activity")
        self.window.set_title(title)
        self.get_widget("delete_button").set_sensitive(action == "edit")
        if action == "edit":
            self.fact = runtime.storage.get_fact(fact_id)
        elif action == "clone":
            base_fact = runtime.storage.get_fact(fact_id)
            self.fact = base_fact.copy(start_time=dt.datetime.now(),
                                       end_time=None)
        else:
            self.fact = Fact(start_time=dt.datetime.now())

        original_fact = self.fact
        # TODO: should use hday, not date.
        self.date = self.fact.date

        self.update_fields()
        self.update_cmdline(select=True)

        self.cmdline.original_fact = original_fact

        # This signal should be emitted only after a manual modification,
        # not at init time when cmdline might not always be fully parsable.
        self.cmdline.connect("changed", self.on_cmdline_changed)
        self.description_buffer.connect("changed", self.on_description_changed)
        self.start_time.connect("changed", self.on_start_time_changed)
        self.start_date.connect("day-selected", self.on_start_date_changed)
        self.start_date.expander.connect("activate",
                                         self.on_start_date_expander_activated)
        self.end_time.connect("changed", self.on_end_time_changed)
        self.end_date.connect("day-selected", self.on_end_date_changed)
        self.end_date.expander.connect("activate",
                                       self.on_end_date_expander_activated)
        self.activity_entry.connect("changed", self.on_activity_changed)
        self.category_entry.connect("changed", self.on_category_changed)
        self.tags_entry.connect("changed", self.on_tags_changed)

        self._gui.connect_signals(self)
        self.validate_fields()
        self.window.show_all()
Esempio n. 10
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"]))