Ejemplo n.º 1
0
 def test_comparison(self):
     fact1 = Fact("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 = dt.datetime.now()
     self.assertNotEqual(fact1, fact2)
     fact2 = fact1.copy()
     fact2.end_time = dt.datetime.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)
Ejemplo n.º 2
0
class CustomFactController(gobject.GObject):
    __gsignals__ = {
        "on-close": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
    }

    def __init__(self, parent=None, fact_id=None, base_fact=None):
        gobject.GObject.__init__(self)

        self._gui = load_ui_file("edit_activity.ui")
        self.window = self.get_widget('custom_fact_window')
        self.window.set_size_request(600, 200)
        self.parent = parent
        # None if creating a new fact, instead of editing one
        self.fact_id = fact_id

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

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

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

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

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

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

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

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

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

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

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

        if fact_id:
            # editing
            self.fact = runtime.storage.get_fact(fact_id)
            self.date = self.fact.date
            self.window.set_title(_("Update activity"))
        else:
            self.window.set_title(_("Add activity"))
            self.date = hamster_today()
            self.get_widget("delete_button").set_sensitive(False)
            if base_fact:
                # start a clone now.
                self.fact = base_fact.copy(start_time=hamster_now(),
                                           end_time=None)
            else:
                self.fact = Fact(start_time=hamster_now())

        original_fact = self.fact

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

        self.cmdline.original_fact = original_fact

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

        self._gui.connect_signals(self)
        self.validate_fields()
        self.window.show_all()

    def on_prev_day_clicked(self, button):
        self.increment_date(-1)

    def on_next_day_clicked(self, button):
        self.increment_date(+1)

    def draw_preview(self, start_time, end_time=None):
        day_facts = runtime.storage.get_facts(self.date)
        self.dayline.plot(self.date, day_facts, start_time, end_time)

    def get_widget(self, name):
        """ skip one variable (huh) """
        return self._gui.get_object(name)

    def increment_date(self, days):
        delta = dt.timedelta(days=days)
        self.date += delta
        if self.fact.start_time:
            self.fact.start_time += delta
        if self.fact.end_time:
            self.fact.end_time += delta
        self.update_fields()

    def show(self):
        self.window.show()

    def figure_description(self):
        buf = self.description_buffer
        description = buf.get_text(buf.get_start_iter(), buf.get_end_iter(), 0)
        return description.strip()

    def on_activity_changed(self, widget):
        if not self.master_is_cmdline:
            self.fact.activity = self.activity_entry.get_text()
            self.validate_fields()
            self.update_cmdline()

    def on_category_changed(self, widget):
        if not self.master_is_cmdline:
            self.fact.category = self.category_entry.get_text()
            self.validate_fields()
            self.update_cmdline()

    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()

    def on_cmdline_focus_in_event(self, widget, event):
        self.master_is_cmdline = True

    def on_cmdline_focus_out_event(self, widget, event):
        self.master_is_cmdline = False

    def on_description_changed(self, text):
        if not self.master_is_cmdline:
            self.fact.description = self.figure_description()
            self.validate_fields()
            self.update_cmdline()

    def on_end_date_changed(self, widget):
        if not self.master_is_cmdline:
            if self.fact.end_time:
                time = self.fact.end_time.time()
                self.fact.end_time = dt.datetime.combine(
                    self.end_date.date, time)
                self.validate_fields()
                self.update_cmdline()
            elif self.end_date.date:
                # No end time means on-going, hence date would be meaningless.
                # And a default end date may be provided when end time is set,
                # so there should never be a date without time.
                self.end_date.date = None

    def on_end_date_expander_activated(self, widget):
        # state has not changed yet, toggle also start_date calendar visibility
        previous_state = self.end_date.expander.get_expanded()
        self.start_date.expander.set_expanded(not previous_state)

    def on_end_time_changed(self, widget):
        if not self.master_is_cmdline:
            # self.end_time.start_time() was given a datetime,
            # so self.end_time.time is a datetime too.
            end = self.end_time.time
            self.fact.end_time = end
            self.end_date.date = end.date() if end else None
            self.validate_fields()
            self.update_cmdline()

    def on_start_date_changed(self, widget):
        if not self.master_is_cmdline:
            if self.fact.start_time:
                previous_date = self.fact.start_time.date()
                new_date = self.start_date.date
                delta = new_date - previous_date
                self.fact.start_time += delta
                if self.fact.end_time:
                    # preserve fact duration
                    self.fact.end_time += delta
                    self.end_date.date = self.fact.end_time
            self.date = self.fact.date or hamster_today()
            self.validate_fields()
            self.update_cmdline()

    def on_start_date_expander_activated(self, widget):
        # state has not changed yet, toggle also end_date calendar visibility
        previous_state = self.start_date.expander.get_expanded()
        self.end_date.expander.set_expanded(not previous_state)

    def on_start_time_changed(self, widget):
        if not self.master_is_cmdline:
            # note: resist the temptation to preserve duration here;
            # for instance, end time might be at the beginning of next fact.
            new_time = self.start_time.time
            if new_time:
                if self.fact.start_time:
                    new_start_time = dt.datetime.combine(
                        self.fact.start_time.date(), new_time)
                else:
                    # date not specified; result must fall in current hamster_day
                    new_start_time = hamsterday_time_to_datetime(
                        hamster_today(), new_time)
            else:
                new_start_time = None
            self.fact.start_time = new_start_time
            # let start_date extract date or handle None
            self.start_date.date = new_start_time
            self.validate_fields()
            self.update_cmdline()

    def on_tags_changed(self, widget):
        if not self.master_is_cmdline:
            self.fact.tags = self.tags_entry.get_tags()
            self.update_cmdline()

    def update_cmdline(self, select=None):
        """Update the cmdline entry content."""
        self.cmdline_fact = self.fact.copy(description=None)
        label = self.cmdline_fact.serialized(prepend_date=False)
        with self.cmdline.handler_block(self.cmdline.checker):
            self.cmdline.set_text(label)
            if select:
                time_str = self.cmdline_fact.serialized_time(
                    prepend_date=False)
                self.cmdline.select_region(0, len(time_str))

    def update_fields(self):
        """Update gui fields content."""
        self.start_time.time = self.fact.start_time
        self.end_time.time = self.fact.end_time
        self.end_time.set_start_time(self.fact.start_time)
        self.start_date.date = self.fact.start_time
        self.end_date.date = self.fact.end_time
        self.activity_entry.set_text(self.fact.activity)
        self.category_entry.set_text(self.fact.category)
        self.description_buffer.set_text(self.fact.description)
        self.tags_entry.set_tags(self.fact.tags)
        self.validate_fields()

    def update_status(self, status, markup):
        """Set save button sensitivity and tooltip."""
        self.save_button.set_tooltip_markup(markup)
        if status == "looks good":
            self.save_button.set_label("gtk-save")
            self.save_button.set_sensitive(True)
        elif status == "warning":
            self.save_button.set_label("gtk-dialog-warning")
            self.save_button.set_sensitive(True)
        elif status == "wrong":
            self.save_button.set_label("gtk-save")
            self.save_button.set_sensitive(False)
        else:
            raise ValueError("unknown status: '{}'".format(status))

    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

    def on_delete_clicked(self, button):
        runtime.storage.remove_fact(self.fact_id)
        self.close_window()

    def on_cancel_clicked(self, button):
        self.close_window()

    def on_close(self, widget, event):
        self.close_window()

    def on_save_button_clicked(self, button):
        if self.fact_id:
            runtime.storage.update_fact(self.fact_id, self.fact)
        else:
            runtime.storage.add_fact(self.fact)
        self.close_window()

    def on_window_key_pressed(self, tree, event_key):
        popups = (self.cmdline.popup.get_property("visible")
                  or self.start_time.popup.get_property("visible")
                  or self.end_time.popup.get_property("visible")
                  or self.tags_entry.popup.get_property("visible"))

        if (event_key.keyval == gdk.KEY_Escape or \
           (event_key.keyval == gdk.KEY_w and event_key.state & gdk.ModifierType.CONTROL_MASK)):
            if popups:
                return False

            self.close_window()

        elif event_key.keyval in (gdk.KEY_Return, gdk.KEY_KP_Enter):
            if popups:
                return False
            if self.description_box.has_focus():
                return False
            if self.validate_fields():
                self.on_save_button_clicked(None)

    def close_window(self):
        if not self.parent:
            gtk.main_quit()
        else:
            self.window.destroy()
            self.window = None
            self._gui = None
            self.emit("on-close")