Пример #1
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)
Пример #2
0
    def test_round_trip(self):
        fact = Fact.parse(
            "11:00 12:00 activity, with comma@category,, description, with comma #and #tags"
        )
        dbus_fact = to_dbus_fact_json(fact)
        return_fact = from_dbus_fact_json(dbus_fact)
        self.assertEqual(return_fact, fact)

        dbus_fact = to_dbus_fact(fact)
        return_fact = from_dbus_fact(dbus_fact)
        self.assertEqual(return_fact, fact)

        fact = Fact.parse("11:00 activity")
        dbus_fact = to_dbus_fact_json(fact)
        return_fact = from_dbus_fact_json(dbus_fact)
        self.assertEqual(return_fact, fact)

        dbus_fact = to_dbus_fact(fact)
        return_fact = from_dbus_fact(dbus_fact)
        self.assertEqual(return_fact, fact)

        range, __ = dt.Range.parse("2020-01-19 11:00 - 2020-01-19 12:00")
        dbus_range = to_dbus_range(range)
        return_range = from_dbus_range(dbus_range)
        self.assertEqual(return_range, range)
Пример #3
0
 def test_decimal_in_activity(self):
     # cf. issue #270
     fact = Fact.parse("12:25-13:25 10.0@ABC,, Two Words #tag #bäg")
     self.assertEqual(fact.activity, "10.0")
     self.assertEqual(fact.category, "ABC")
     self.assertEqual(fact.description, "Two Words")
     # should not pick up a time here
     fact = Fact.parse("10.00@ABC,, Two Words #tag #bäg")
     self.assertEqual(fact.activity, "10.00")
     self.assertEqual(fact.category, "ABC")
     self.assertEqual(fact.description, "Two Words")
Пример #4
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(), "")
Пример #5
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)
Пример #6
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)
     fact2 = fact1.copy()
     fact2.range.start = fact1.range.start + dt.timedelta(minutes=1)
     self.assertNotEqual(fact1, fact2)
     fact2 = fact1.copy()
     fact2.range.end = fact1.range.end + dt.timedelta(minutes=1)
     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)
Пример #7
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
Пример #8
0
    def add_fact(self, fact, start_time=None, end_time=None, temporary=False):
        """Add fact.

        fact: either a Fact instance or
              a string that can be parsed through Fact.parse.

        note: start_time and end_time are used only when fact is a string,
              for backward compatibility.
              Passing fact as a string is deprecated
              and will be removed in a future version.
              Parsing should be done in the caller.
        """
        if isinstance(fact, str):
            logger.info("Passing fact as a string is deprecated")
            fact = Fact.parse(fact)
            fact.start_time = start_time
            fact.end_time = end_time

        # better fail before opening the transaction
        self.check_fact(fact)
        self.start_transaction()
        result = self.__add_fact(fact, temporary)
        self.end_transaction()

        if result:
            self.facts_changed()
        return result
Пример #9
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 = dt.datetime.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
Пример #10
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)
Пример #11
0
def extract_search(text):
    fact = Fact.parse(text)
    search = fact.activity
    if fact.category:
        search += "@%s" % fact.category
    if fact.tags:
        search += " #%s" % (" #".join(fact.tags))
    return search
Пример #12
0
 def test_plain_name(self):
     # plain activity name
     activity = Fact.parse("just a simple case with ütf-8")
     self.assertEqual(activity.activity, "just a simple case with ütf-8")
     assert activity.start_time is None
     assert activity.end_time is None
     assert not activity.category
     assert not activity.description
Пример #13
0
 def test_category(self):
     # plain activity name
     activity = Fact.parse("just a simple case@hämster")
     self.assertEqual(activity.activity, "just a simple case")
     self.assertEqual(activity.category, "hämster")
     assert activity.start_time is None
     assert activity.end_time is None
     assert not activity.description
Пример #14
0
 def test_description(self):
     # plain activity name
     activity = Fact.parse("case,, with added descriptiön")
     self.assertEqual(activity.activity, "case")
     self.assertEqual(activity.description, "with added descriptiön")
     assert not activity.category
     assert activity.start_time is None
     assert activity.end_time is None
     assert not activity.category
Пример #15
0
 def test_tags(self):
     # plain activity name
     activity = Fact.parse(
         "#case,, description with #hash,, #and, #some #tägs")
     self.assertEqual(activity.activity, "#case")
     self.assertEqual(activity.description, "description with #hash")
     self.assertEqual(set(activity.tags), set(["and", "some", "tägs"]))
     assert not activity.category
     assert activity.start_time is None
     assert activity.end_time is None
Пример #16
0
    def test_with_start_and_end_time(self):
        # with time
        activity = Fact.parse("12:35-14:25 with start-end time")
        self.assertEqual(activity.activity, "with start-end time")
        self.assertEqual(activity.start_time.strftime("%H:%M"), "12:35")
        self.assertEqual(activity.end_time.strftime("%H:%M"), "14:25")

        #rest must be empty
        assert not activity.category
        assert not activity.description
Пример #17
0
 def test_full(self):
     # plain activity name
     activity = Fact.parse(
         "1225-1325 case@cat,, description #ta non-tag,, #tag #bäg")
     self.assertEqual(activity.start_time.strftime("%H:%M"), "12:25")
     self.assertEqual(activity.end_time.strftime("%H:%M"), "13:25")
     self.assertEqual(activity.activity, "case")
     self.assertEqual(activity.category, "cat")
     self.assertEqual(activity.description, "description #ta non-tag")
     self.assertEqual(set(activity.tags), set(["bäg", "tag"]))
Пример #18
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"])
Пример #19
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])
Пример #20
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 = dt.datetime.now()
        self.storage.check_fact(fact, default_day=dt.hday.today())
        self.storage.add_fact(fact)
Пример #21
0
    def complete_first(self):
        text = self.get_text()
        fact = Fact.parse(text)
        search = extract_search(text)
        if not self.complete_tree.rows or not fact.activity:
            return text, None

        label = self.complete_tree.rows[0].data
        if label.startswith(search):
            return text, label[len(search):]

        return text, None
Пример #22
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)
Пример #23
0
 def test_commas(self):
     fact = Fact.parse(
         "11:00 12:00 activity, with comma@category,, description, with comma"
     )
     self.assertEqual(fact.activity, "activity, with comma")
     self.assertEqual(fact.category, "category")
     self.assertEqual(fact.description, "description, with comma")
     self.assertEqual(fact.tags, [])
     fact = Fact.parse(
         "11:00 12:00 activity, with comma@category,, description, with comma, #tag1, #tag2"
     )
     self.assertEqual(fact.activity, "activity, with comma")
     self.assertEqual(fact.category, "category")
     self.assertEqual(fact.description, "description, with comma")
     self.assertEqual(fact.tags, ["tag1", "tag2"])
     fact = Fact.parse(
         "11:00 12:00 activity, with comma@category,, description, with comma and #hash,, #tag1, #tag2"
     )
     self.assertEqual(fact.activity, "activity, with comma")
     self.assertEqual(fact.category, "category")
     self.assertEqual(fact.description, "description, with comma and #hash")
     self.assertEqual(fact.tags, ["tag1", "tag2"])
Пример #24
0
 def on_cmdline_changed(self, widget):
     if self.master_is_cmdline:
         fact = Fact.parse(self.cmdline.get_text(), default_day=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 = dt.datetime.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()
Пример #25
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])
Пример #26
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.serialized_range(
                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)))
Пример #27
0
 def update_fact(self, fact_id, fact, start_time=None, end_time=None, temporary=False):
     # better fail before opening the transaction
     self.check_fact(fact)
     self.start_transaction()
     self.__remove_fact(fact_id)
     # to be removed once update facts use Fact directly.
     if isinstance(fact, str):
         fact = Fact.parse(fact)
         fact = fact.copy(start_time=start_time, end_time=end_time)
     result = self.__add_fact(fact, temporary)
     if not result:
         logger.warning("failed to update fact {} ({})".format(fact_id, fact))
     self.end_transaction()
     if result:
         self.facts_changed()
     return result
Пример #28
0
 def test_copy(self):
     fact1 = Fact.parse("12:25-13:25 case@cat,, description #tag #bäg")
     fact2 = fact1.copy()
     self.assertEqual(fact1.start_time, fact2.start_time)
     self.assertEqual(fact1.end_time, fact2.end_time)
     self.assertEqual(fact1.activity, fact2.activity)
     self.assertEqual(fact1.category, fact2.category)
     self.assertEqual(fact1.description, fact2.description)
     self.assertEqual(fact1.tags, fact2.tags)
     fact3 = fact1.copy(activity="changed")
     self.assertEqual(fact3.activity, "changed")
     fact3 = fact1.copy(category="changed")
     self.assertEqual(fact3.category, "changed")
     fact3 = fact1.copy(description="changed")
     self.assertEqual(fact3.description, "changed")
     fact3 = fact1.copy(tags=["changed"])
     self.assertEqual(fact3.tags, ["changed"])
Пример #29
0
    def AddFact(self, fact_str, start_time, end_time, temporary):
        """Add fact specified by a string.

        If the parsed fact has no start, then now is used.
        To fully use the hamster fact parser, as on the cmdline,
        just pass 0 for start_time and end_time.

        Args:
            fact_str (str): string to be parsed.
            start_time (int): Start datetime ovveride timestamp (ignored if 0).
                              -1 means None.
            end_time (int): datetime ovveride timestamp (ignored if 0).
                            -1 means None.
            #temporary (boolean): historical mystery, ignored, but needed to
                                 keep the method signature stable.
                                 Do not forget to pass something (e.g. False)!
        Returns:
            fact id (int), 0 means failure.

        Note: see datetime.utcfromtimestamp documentation
              for the precise meaning of timestamps.
        """
        fact = Fact.parse(fact_str)

        # default value if none found
        if not fact.start_time:
            fact.start_time = dt.datetime.now()

        if start_time == -1:
            fact.start_time = None
        elif start_time != 0:
            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)
Пример #30
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()