From 54fcc7566075c50b0c9246b9eac7c8217137d65f Mon Sep 17 00:00:00 2001 From: elbenfreund Date: Sat, 8 Jul 2017 14:05:18 +0200 Subject: [PATCH 01/25] Wrap 'make test' with 'xvfb-run' Closes: 206 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 0ac7e7c0..31216dd2 100644 --- a/Makefile +++ b/Makefile @@ -72,7 +72,7 @@ lint: flake8 hamster-dbus tests test: - py.test $(TEST_ARGS) tests/ + xvfb-run py.test $(TEST_ARGS) tests/ test-all: tox From 975623e679ddff216f295b5d7b26aa8539a66565 Mon Sep 17 00:00:00 2001 From: elbenfreund Date: Thu, 22 Jun 2017 14:11:41 +0200 Subject: [PATCH 02/25] Add inline comments --- hamster_gtk/hamster_gtk.py | 4 ++++ hamster_gtk/helpers.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/hamster_gtk/hamster_gtk.py b/hamster_gtk/hamster_gtk.py index 07f98a96..2c3d07bb 100755 --- a/hamster_gtk/hamster_gtk.py +++ b/hamster_gtk/hamster_gtk.py @@ -116,6 +116,10 @@ class SignalHandler(GObject.GObject): Once signals have been 'registered' here you can ``emit`` or ``connect`` to them via its class instances. """ + # [TODO] + # Explain semantics of each signal + # [TODO] + # Add signals for all changed hamster-lib objects? __gsignals__ = { str('facts-changed'): (GObject.SIGNAL_RUN_LAST, None, ()), diff --git a/hamster_gtk/helpers.py b/hamster_gtk/helpers.py index 9eadf9b8..36c941fe 100644 --- a/hamster_gtk/helpers.py +++ b/hamster_gtk/helpers.py @@ -88,6 +88,8 @@ def clear_children(widget): It seems GTK really does not have this build in. Iterating over all seems a bit blunt, but seems to be the way to do this. """ + # [TODO] + # Replace with Gtk.Container.foreach()? for child in widget.get_children(): child.destroy() return widget From 334753c86569ba63036333537d2f7e0a2da377db Mon Sep 17 00:00:00 2001 From: elbenfreund Date: Thu, 22 Jun 2017 14:12:33 +0200 Subject: [PATCH 03/25] Add dedicated helper for fetching recent activities --- hamster_gtk/helpers.py | 9 +++++++++ hamster_gtk/misc/widgets/raw_fact_entry.py | 2 ++ 2 files changed, 11 insertions(+) diff --git a/hamster_gtk/helpers.py b/hamster_gtk/helpers.py index 36c941fe..ed1701b0 100644 --- a/hamster_gtk/helpers.py +++ b/hamster_gtk/helpers.py @@ -24,6 +24,7 @@ import datetime import re +from orderedset import OrderedSet import six from six import text_type @@ -179,6 +180,14 @@ def decompose_raw_fact_string(text, raw=False): return result +# [TODO] +# Oncec LIB-251 has been fixed this should no longer be needed. +def get_recent_activities(controller, start, end): + """Return a list of all activities logged in facts within the given timeframe.""" + recent_activities = [fact.activity for fact in controller.facts.get_all(start=start, end=end)] + return OrderedSet(recent_activities) + + def get_delta_string(delta): """ Return a human readable representation of ``datetime.timedelta`` instance. diff --git a/hamster_gtk/misc/widgets/raw_fact_entry.py b/hamster_gtk/misc/widgets/raw_fact_entry.py index b6ec801a..37a1adf4 100644 --- a/hamster_gtk/misc/widgets/raw_fact_entry.py +++ b/hamster_gtk/misc/widgets/raw_fact_entry.py @@ -258,9 +258,11 @@ def _populate_stores(self, evt): text = activity.name self._activities_with_categories_model.append([text]) + activities_store = Gtk.ListStore(GObject.TYPE_STRING) for activity in activities: self._activities_model.append([activity]) + categories_store = Gtk.ListStore(GObject.TYPE_STRING) for category in categories: self._categories_model.append([category]) From 4c5b85eb663b3671171cb574543bf4035d3a0306 Mon Sep 17 00:00:00 2001 From: elbenfreund Date: Thu, 22 Jun 2017 14:14:12 +0200 Subject: [PATCH 04/25] Add 'recent activities' to start-tracking view This commits extends the 'start tracking' view with a list of recent activities that can be used to euther copy the data to the entry field, ready to be extended and then started manually by the user or to start/continue tracking this activity right away. Ref: #136 --- hamster_gtk/helpers.py | 8 +++ hamster_gtk/tracking/screens.py | 120 ++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) diff --git a/hamster_gtk/helpers.py b/hamster_gtk/helpers.py index ed1701b0..3e7a483b 100644 --- a/hamster_gtk/helpers.py +++ b/hamster_gtk/helpers.py @@ -188,6 +188,14 @@ def get_recent_activities(controller, start, end): return OrderedSet(recent_activities) +def serialize_activity(activity): + if activity.category: + result = '{a.name}@{a.category.name}'.format(a=activity) + else: + result = activity.name + return text_type(result) + + def get_delta_string(delta): """ Return a human readable representation of ``datetime.timedelta`` instance. diff --git a/hamster_gtk/tracking/screens.py b/hamster_gtk/tracking/screens.py index 40b3d584..10c138d2 100644 --- a/hamster_gtk/tracking/screens.py +++ b/hamster_gtk/tracking/screens.py @@ -188,6 +188,9 @@ def __init__(self, app, *args, **kwargs): start_button.connect('clicked', self._on_start_tracking_button) self.pack_start(start_button, False, False, 0) + # Recent activities + self.pack_start(self._get_recent_activities_widget(), True, True, 0) + def _start_ongoing_fact(self): """ Start a new *ongoing fact*. @@ -230,6 +233,16 @@ def reset(self): """Clear all data entry fields.""" self.raw_fact_entry.props.text = '' + def set_raw_fact(self, raw_fact): + """Set the text in the raw fact entry.""" + self.raw_fact_entry.props.text = raw_fact + + def _get_recent_activities_widget(self): + scrolled_window = Gtk.ScrolledWindow() + scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + scrolled_window.add(RecentActivitiesGrid(self, self._controller)) + return scrolled_window + # Callbacks def _on_start_tracking_button(self, button): """Callback for the 'start tracking' button.""" @@ -238,3 +251,110 @@ def _on_start_tracking_button(self, button): def _on_raw_fact_entry_activate(self, evt): """Callback for when ``enter`` is pressed within the entry.""" self._start_ongoing_fact() + + +class RecentActivitiesGrid(Gtk.Grid): + """A widget that lists recent activities and allows for quick continued tracking.""" + + def __init__(self, start_tracking_widget, controller, *args, **kwargs): + """ + Initiate widget. + + Args: + start_tracking_widget (StartTrackingBox): Is needed in order to set the raw fact. + controller: Is needed in order to query for recent activities. + """ + + super(Gtk.Grid, self).__init__(*args, **kwargs) + self._start_tracking_widget = start_tracking_widget + self._controller = controller + + self._controller.signal_handler.connect('facts-changed', self.refresh) + self._populate() + + + def refresh(self, sender=None): + """Clear the current content and re-populate and re-draw the widget.""" + helpers.clear_children(self) + self._populate() + self.show_all() + + + def _populate(self): + """Fill the widget with rows per activity.""" + + def add_row_widgets(row_counter, activity): + """" + Add a set of widgets to a specific row based on the activity passed. + + Args: + row_counter (int): Which row to add to. + activity (hamster_lib.Activity): The activity that is represented by this row. + """ + def get_label(activity): + """Label representing the activity/category combination.""" + label = Gtk.Label(helpers.serialize_activity(activity)) + label.set_halign(Gtk.Align.START) + return label + + def get_copy_button(activity): + """ + A button that will copy the activity/category string to the raw fact entry. + + The main use case for this is a user that want to add a description or tag before + actually starting the tracking. + """ + button = Gtk.Button('Copy') + activity = helpers.serialize_activity(activity) + button.connect('clicked', self._on_copy_button, activity) + return button + + def get_start_button(activity): + """A button that will start a new ongoing fact based on that activity.""" + button = Gtk.Button('Start') + activity = helpers.serialize_activity(activity) + button.connect('clicked', self._on_start_button, activity) + return button + + self.attach(get_label(activity), 0, row_counter, 1, 1) + self.attach(get_copy_button(activity), 1, row_counter, 1, 1) + self.attach(get_start_button(activity), 2, row_counter, 1, 1) + + today = datetime.date.today() + start = today - datetime.timedelta(90) + activities = reversed(helpers.get_recent_activities(self._controller, start, today)) + + row_counter = 0 + for activity in activities: + add_row_widgets(row_counter, activity) + row_counter += 1 + + + def _on_copy_button(self, button, activity): + """" + Set the activity/category text in the 'start tracking entry'. + + Args: + button (Gtk.Button): The button that was clicked. + activity (text_type): Activity text to be copied as raw fact. + + Note: + Besides copying the text we also assign focus and place the cursor + at the end of the pasted text as to facilitate fast entry of + additional text. + """ + self._start_tracking_widget.set_raw_fact(activity) + self._start_tracking_widget.raw_fact_entry.grab_focus_without_selecting() + self._start_tracking_widget.raw_fact_entry.set_position(len(activity)) + + + def _on_start_button(self, button, activity): + """" + Start a new ongoing fact based on this activity/category. + + Args: + button (Gtk.Button): The button that was clicked. + activity (text_type): Activity text to be copied as raw fact. + """ + self._start_tracking_widget.set_raw_fact(activity) + self._start_tracking_widget._start_ongoing_fact() From 5c17e7ac4da4e362893ace7a365beb410d503e82 Mon Sep 17 00:00:00 2001 From: elbenfreund Date: Sat, 1 Jul 2017 18:42:44 +0200 Subject: [PATCH 05/25] Sizing for recent activity widget --- hamster_gtk/hamster_gtk.py | 22 +++++++++ hamster_gtk/helpers.py | 1 + hamster_gtk/misc/widgets/raw_fact_entry.py | 2 - hamster_gtk/preferences/preferences_dialog.py | 5 ++ hamster_gtk/tracking/screens.py | 48 ++++++++++++++----- tests/tracking/test_screens.py | 2 +- 6 files changed, 66 insertions(+), 14 deletions(-) diff --git a/hamster_gtk/hamster_gtk.py b/hamster_gtk/hamster_gtk.py index 2c3d07bb..f2529a59 100755 --- a/hamster_gtk/hamster_gtk.py +++ b/hamster_gtk/hamster_gtk.py @@ -116,6 +116,7 @@ class SignalHandler(GObject.GObject): Once signals have been 'registered' here you can ``emit`` or ``connect`` to them via its class instances. """ + # [TODO] # Explain semantics of each signal # [TODO] @@ -259,6 +260,7 @@ def _reload_config(self): """Reload configuration from designated store.""" config = self._get_config_from_file() self._config = config + self.config = config return config def _config_changed(self, sender): @@ -283,6 +285,8 @@ def _get_default_config(self): # Frontend 'autocomplete_activities_range': 30, 'autocomplete_split_activity': False, + 'tracking_show_recent_activities': True, + 'tracking_recent_activities_items': 6, } def _config_to_configparser(self, config): @@ -319,6 +323,12 @@ def get_autocomplete_activities_range(): def get_autocomplete_split_activity(): return text_type(config['autocomplete_split_activity']) + def get_tracking_show_recent_activities(): + return text_type(config['tracking_show_recent_activities']) + + def get_tracking_recent_activities_items(): + return text_type(config['tracking_recent_activities_items']) + cp_instance = SafeConfigParser() cp_instance.add_section('Backend') cp_instance.set('Backend', 'store', get_store()) @@ -333,6 +343,10 @@ def get_autocomplete_split_activity(): get_autocomplete_activities_range()) cp_instance.set('Frontend', 'autocomplete_split_activity', get_autocomplete_split_activity()) + cp_instance.set('Frontend', 'tracking_show_recent_activities', + get_tracking_show_recent_activities()) + cp_instance.set('Frontend', 'tracking_recent_activities_items', + get_tracking_recent_activities_items()) return cp_instance @@ -389,6 +403,12 @@ def get_autocomplete_activities_range(): def get_autocomplete_split_activity(): return cp_instance.getboolean('Frontend', 'autocomplete_split_activity') + def get_tracking_show_recent_activities(): + return cp_instance.getboolean('Frontend', 'tracking_show_recent_activities') + + def get_tracking_recent_activities_items(): + return int(cp_instance.get('Frontend', 'tracking_recent_activities_items')) + result = { 'store': get_store(), 'day_start': get_day_start(), @@ -396,6 +416,8 @@ def get_autocomplete_split_activity(): 'tmpfile_path': get_tmpfile_path(), 'autocomplete_activities_range': get_autocomplete_activities_range(), 'autocomplete_split_activity': get_autocomplete_split_activity(), + 'tracking_show_recent_activities': get_tracking_recent_activities_items(), + 'tracking_recent_activities_items': get_tracking_recent_activities_items(), } result.update(get_db_config()) return result diff --git a/hamster_gtk/helpers.py b/hamster_gtk/helpers.py index 3e7a483b..c0c73b5b 100644 --- a/hamster_gtk/helpers.py +++ b/hamster_gtk/helpers.py @@ -189,6 +189,7 @@ def get_recent_activities(controller, start, end): def serialize_activity(activity): + """Provide a serialized string version of an activity.""" if activity.category: result = '{a.name}@{a.category.name}'.format(a=activity) else: diff --git a/hamster_gtk/misc/widgets/raw_fact_entry.py b/hamster_gtk/misc/widgets/raw_fact_entry.py index 37a1adf4..b6ec801a 100644 --- a/hamster_gtk/misc/widgets/raw_fact_entry.py +++ b/hamster_gtk/misc/widgets/raw_fact_entry.py @@ -258,11 +258,9 @@ def _populate_stores(self, evt): text = activity.name self._activities_with_categories_model.append([text]) - activities_store = Gtk.ListStore(GObject.TYPE_STRING) for activity in activities: self._activities_model.append([activity]) - categories_store = Gtk.ListStore(GObject.TYPE_STRING) for category in categories: self._categories_model.append([category]) diff --git a/hamster_gtk/preferences/preferences_dialog.py b/hamster_gtk/preferences/preferences_dialog.py index 07a862bc..c7e70f9f 100644 --- a/hamster_gtk/preferences/preferences_dialog.py +++ b/hamster_gtk/preferences/preferences_dialog.py @@ -80,6 +80,11 @@ def __init__(self, parent, app, initial, *args, **kwargs): ('autocomplete_split_activity', (_("Autocomplete activities and categories separately"), HamsterSwitch())), + ('tracking_show_recent_activities', + (_("Allow tracking based on recent activities."), + HamsterSwitch())), + ('tracking_recent_activities_items', (_('How many recent activities?'), + HamsterSpinButton(SimpleAdjustment(0, GObject.G_MAXDOUBLE, 1)))), ]))), ] diff --git a/hamster_gtk/tracking/screens.py b/hamster_gtk/tracking/screens.py index 10c138d2..36ab84f7 100644 --- a/hamster_gtk/tracking/screens.py +++ b/hamster_gtk/tracking/screens.py @@ -169,6 +169,7 @@ def __init__(self, app, *args, **kwargs): spacing=10, *args, **kwargs) self._app = app self.set_homogeneous(False) + self._app.controller.signal_handler.connect('config-changed', self._on_config_changed) # [FIXME] # Refactor to call separate 'get_widget' methods instead. @@ -186,10 +187,15 @@ def __init__(self, app, *args, **kwargs): # Buttons start_button = Gtk.Button(label=_("Start Tracking")) start_button.connect('clicked', self._on_start_tracking_button) + self.start_button = start_button self.pack_start(start_button, False, False, 0) # Recent activities - self.pack_start(self._get_recent_activities_widget(), True, True, 0) + if self._app.config['tracking_show_recent_activities']: + self.recent_activities_widget = self._get_recent_activities_widget() + self.pack_start(self.recent_activities_widget, True, True, 0) + else: + self.recent_activities_widget = None def _start_ongoing_fact(self): """ @@ -240,7 +246,18 @@ def set_raw_fact(self, raw_fact): def _get_recent_activities_widget(self): scrolled_window = Gtk.ScrolledWindow() scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) - scrolled_window.add(RecentActivitiesGrid(self, self._controller)) + grid = RecentActivitiesGrid(self, self._app.controller) + # We need to 'show' the grid early in order to make sure space is + # allocated to its children so they actually have a height that we can + # use. + grid.show_all() + # We fetch an arbitrary Button as height-reference + child = grid.get_children()[1] + height = child.get_preferred_height()[1] + min_height = self._app.config['tracking_recent_activities_items'] * height + + scrolled_window.set_min_content_height(min_height) + scrolled_window.add(grid) return scrolled_window # Callbacks @@ -252,6 +269,21 @@ def _on_raw_fact_entry_activate(self, evt): """Callback for when ``enter`` is pressed within the entry.""" self._start_ongoing_fact() + def _on_config_changed(self, sender): + """Callback triggered when 'config-changed' event fired.""" + if self._app.config['tracking_show_recent_activities']: + # We re-create it even if one existed before necause its parameters + # (e.g. size) may have changed. + if self.recent_activities_widget: + self.recent_activities_widget.destroy() + self.recent_activities_widget = self._get_recent_activities_widget() + self.pack_start(self.recent_activities_widget, True, True, 0) + else: + if self.recent_activities_widget: + self.recent_activities_widget.destroy() + self.recent_activities_widget = None + self.show_all() + class RecentActivitiesGrid(Gtk.Grid): """A widget that lists recent activities and allows for quick continued tracking.""" @@ -264,7 +296,6 @@ def __init__(self, start_tracking_widget, controller, *args, **kwargs): start_tracking_widget (StartTrackingBox): Is needed in order to set the raw fact. controller: Is needed in order to query for recent activities. """ - super(Gtk.Grid, self).__init__(*args, **kwargs) self._start_tracking_widget = start_tracking_widget self._controller = controller @@ -272,19 +303,16 @@ def __init__(self, start_tracking_widget, controller, *args, **kwargs): self._controller.signal_handler.connect('facts-changed', self.refresh) self._populate() - def refresh(self, sender=None): """Clear the current content and re-populate and re-draw the widget.""" helpers.clear_children(self) self._populate() self.show_all() - def _populate(self): """Fill the widget with rows per activity.""" - def add_row_widgets(row_counter, activity): - """" + """ Add a set of widgets to a specific row based on the activity passed. Args: @@ -329,9 +357,8 @@ def get_start_button(activity): add_row_widgets(row_counter, activity) row_counter += 1 - def _on_copy_button(self, button, activity): - """" + """ Set the activity/category text in the 'start tracking entry'. Args: @@ -347,9 +374,8 @@ def _on_copy_button(self, button, activity): self._start_tracking_widget.raw_fact_entry.grab_focus_without_selecting() self._start_tracking_widget.raw_fact_entry.set_position(len(activity)) - def _on_start_button(self, button, activity): - """" + """ Start a new ongoing fact based on this activity/category. Args: diff --git a/tests/tracking/test_screens.py b/tests/tracking/test_screens.py index 0e20ebf5..4ac5499c 100644 --- a/tests/tracking/test_screens.py +++ b/tests/tracking/test_screens.py @@ -46,7 +46,7 @@ def test_init(self, app): """Make sure instances matches expectation.""" result = screens.StartTrackingBox(app) assert isinstance(result, screens.StartTrackingBox) - assert len(result.get_children()) == 3 + assert len(result.get_children()) == 4 def test__on_start_tracking_button(self, start_tracking_box, fact, mocker): """Make sure a new 'ongoing fact' is created.""" From d941aabeedef62df4a2224d5ed449e8d1e1986d3 Mon Sep 17 00:00:00 2001 From: elbenfreund Date: Sat, 8 Jul 2017 14:03:31 +0200 Subject: [PATCH 06/25] Adjust existing tests --- tests/conftest.py | 2 ++ tests/preferences/conftest.py | 17 ++++++++++++++++- tests/preferences/test_preferences_dialog.py | 4 +++- tests/test_hamster-gtk.py | 2 +- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index b4eb30d2..0d7cb9c9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -109,5 +109,7 @@ def config(request, tmpdir): 'db_path': ':memory:', 'autocomplete_activities_range': 30, 'autocomplete_split_activity': False, + 'tracking_show_recent_activities': True, + 'tracking_recent_activities_items': 6, } return config diff --git a/tests/preferences/conftest.py b/tests/preferences/conftest.py index 533ce09a..94df22cc 100644 --- a/tests/preferences/conftest.py +++ b/tests/preferences/conftest.py @@ -80,11 +80,24 @@ def autocomplete_split_activity_parametrized(request): return request.param +@pytest.fixture(params=(True, False)) +def tracking_show_recent_activities_parametrized(request): + """Return a parametrized tracking_show_recent_activities_parametrized value.""" + return request.param + + +@pytest.fixture(params=(0, 1, 5, 15)) +def tracking_recent_activities_items_parametrized(request): + """Return a parametrized tracking_recent_activities_items_parametrized value.""" + return request.param + + @pytest.fixture def config_parametrized(request, store_parametrized, day_start_parametrized, fact_min_delta_parametrized, tmpfile_path_parametrized, db_engine_parametrized, db_path_parametrized, autocomplete_activities_range_parametrized, - autocomplete_split_activity_parametrized): + autocomplete_split_activity_parametrized, tracking_show_recent_activities_parametrized, + tracking_recent_activities_items_parametrized): """Return a config fixture with heavily parametrized config values.""" return { 'store': store_parametrized, @@ -95,6 +108,8 @@ def config_parametrized(request, store_parametrized, day_start_parametrized, 'db_path': db_path_parametrized, 'autocomplete_activities_range': autocomplete_activities_range_parametrized, 'autocomplete_split_activity': autocomplete_split_activity_parametrized, + 'tracking_show_recent_activities': tracking_show_recent_activities_parametrized, + 'tracking_recent_activities_items': tracking_recent_activities_items_parametrized, } diff --git a/tests/preferences/test_preferences_dialog.py b/tests/preferences/test_preferences_dialog.py index 7582c380..b920d356 100644 --- a/tests/preferences/test_preferences_dialog.py +++ b/tests/preferences/test_preferences_dialog.py @@ -19,8 +19,9 @@ def test_init(self, dummy_window, app, config, empty_initial): grids = result.get_content_area().get_children()[0].get_children() # This assumes 2 children per config entry (label and widget). grid_entry_counts = [len(g.get_children()) / 2 for g in grids] - assert sum(grid_entry_counts) == 8 + assert sum(grid_entry_counts) == 10 + @pytest.mark.slowtest def test_get_config(self, preferences_dialog, config_parametrized): """ Make sure retrieval of field values works as expected. @@ -32,6 +33,7 @@ def test_get_config(self, preferences_dialog, config_parametrized): result = preferences_dialog.get_config() assert result == config_parametrized + @pytest.mark.slowtest def test_set_config(self, preferences_dialog, config_parametrized): """Make sure setting the field values works as expected.""" preferences_dialog._set_config(config_parametrized) diff --git a/tests/test_hamster-gtk.py b/tests/test_hamster-gtk.py index 1df583a5..8f7f7c4c 100755 --- a/tests/test_hamster-gtk.py +++ b/tests/test_hamster-gtk.py @@ -29,7 +29,7 @@ def test__reload_config(self, app, config, mocker): def test__get_default_config(self, app, appdirs): """Make sure the defaults use appdirs for relevant paths.""" result = app._get_default_config() - assert len(result) == 8 + assert len(result) == 10 assert os.path.dirname(result['tmpfile_path']) == appdirs.user_data_dir assert os.path.dirname(result['db_path']) == appdirs.user_data_dir From 859390288ecdb5843af32a6e9fec0593ad68baa9 Mon Sep 17 00:00:00 2001 From: elbenfreund Date: Fri, 22 Sep 2017 14:35:22 +0200 Subject: [PATCH 07/25] rename 'row_counter' parameter to 'row_index' --- hamster_gtk/tracking/screens.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/hamster_gtk/tracking/screens.py b/hamster_gtk/tracking/screens.py index 36ab84f7..75a3a511 100644 --- a/hamster_gtk/tracking/screens.py +++ b/hamster_gtk/tracking/screens.py @@ -311,7 +311,7 @@ def refresh(self, sender=None): def _populate(self): """Fill the widget with rows per activity.""" - def add_row_widgets(row_counter, activity): + def add_row_widgets(row_index, activity): """ Add a set of widgets to a specific row based on the activity passed. @@ -344,18 +344,18 @@ def get_start_button(activity): button.connect('clicked', self._on_start_button, activity) return button - self.attach(get_label(activity), 0, row_counter, 1, 1) - self.attach(get_copy_button(activity), 1, row_counter, 1, 1) - self.attach(get_start_button(activity), 2, row_counter, 1, 1) + self.attach(get_label(activity), 0, row_index, 1, 1) + self.attach(get_copy_button(activity), 1, row_index, 1, 1) + self.attach(get_start_button(activity), 2, row_index, 1, 1) today = datetime.date.today() start = today - datetime.timedelta(90) activities = reversed(helpers.get_recent_activities(self._controller, start, today)) - row_counter = 0 + row_index = 0 for activity in activities: - add_row_widgets(row_counter, activity) - row_counter += 1 + add_row_widgets(row_index, activity) + row_index += 1 def _on_copy_button(self, button, activity): """ From c08bd74be897c9f6075bc2e9792d49157418c708 Mon Sep 17 00:00:00 2001 From: elbenfreund Date: Fri, 22 Sep 2017 14:41:11 +0200 Subject: [PATCH 08/25] Fix typo --- hamster_gtk/tracking/screens.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hamster_gtk/tracking/screens.py b/hamster_gtk/tracking/screens.py index 75a3a511..bcd3b942 100644 --- a/hamster_gtk/tracking/screens.py +++ b/hamster_gtk/tracking/screens.py @@ -272,7 +272,7 @@ def _on_raw_fact_entry_activate(self, evt): def _on_config_changed(self, sender): """Callback triggered when 'config-changed' event fired.""" if self._app.config['tracking_show_recent_activities']: - # We re-create it even if one existed before necause its parameters + # We re-create it even if one existed before because its parameters # (e.g. size) may have changed. if self.recent_activities_widget: self.recent_activities_widget.destroy() From bec62d82fba762fd706754eb3aa5f4aa76305f2c Mon Sep 17 00:00:00 2001 From: elbenfreund Date: Fri, 22 Sep 2017 15:00:47 +0200 Subject: [PATCH 09/25] Change wording within preferences. --- hamster_gtk/preferences/preferences_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hamster_gtk/preferences/preferences_dialog.py b/hamster_gtk/preferences/preferences_dialog.py index c7e70f9f..cfd6e7fa 100644 --- a/hamster_gtk/preferences/preferences_dialog.py +++ b/hamster_gtk/preferences/preferences_dialog.py @@ -81,7 +81,7 @@ def __init__(self, parent, app, initial, *args, **kwargs): (_("Autocomplete activities and categories separately"), HamsterSwitch())), ('tracking_show_recent_activities', - (_("Allow tracking based on recent activities."), + (_("Show recent activities for quickly starting tracking."), HamsterSwitch())), ('tracking_recent_activities_items', (_('How many recent activities?'), HamsterSpinButton(SimpleAdjustment(0, GObject.G_MAXDOUBLE, 1)))), From ddafbd1aac6f7058be114fa40e38a3560b994b3e Mon Sep 17 00:00:00 2001 From: elbenfreund Date: Fri, 22 Sep 2017 17:43:45 +0200 Subject: [PATCH 10/25] fix ordering for 'get_recent_activities' helper --- hamster_gtk/helpers.py | 10 +++++++++- hamster_gtk/tracking/screens.py | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/hamster_gtk/helpers.py b/hamster_gtk/helpers.py index c0c73b5b..046a239a 100644 --- a/hamster_gtk/helpers.py +++ b/hamster_gtk/helpers.py @@ -22,6 +22,7 @@ from __future__ import absolute_import, unicode_literals import datetime +import operator import re from orderedset import OrderedSet @@ -184,7 +185,14 @@ def decompose_raw_fact_string(text, raw=False): # Oncec LIB-251 has been fixed this should no longer be needed. def get_recent_activities(controller, start, end): """Return a list of all activities logged in facts within the given timeframe.""" - recent_activities = [fact.activity for fact in controller.facts.get_all(start=start, end=end)] + # [FIXME] + # This manual sorting within python is of cause less than optimal. We stick + # with it for now as this is just a preliminary workaround helper anyway and + # effective sorting will need to be implemented by the storage backend in + # ``hamster-lib``. + facts = sorted(controller.facts.get_all(start=start, end=end), + key=operator.attrgetter('start'), reverse=True) + recent_activities = [fact.activity for fact in facts] return OrderedSet(recent_activities) diff --git a/hamster_gtk/tracking/screens.py b/hamster_gtk/tracking/screens.py index bcd3b942..c929080f 100644 --- a/hamster_gtk/tracking/screens.py +++ b/hamster_gtk/tracking/screens.py @@ -350,7 +350,7 @@ def get_start_button(activity): today = datetime.date.today() start = today - datetime.timedelta(90) - activities = reversed(helpers.get_recent_activities(self._controller, start, today)) + activities = helpers.get_recent_activities(self._controller, start, today) row_index = 0 for activity in activities: From fa937423f229d616c3a19ce66ee7fad232ea4994 Mon Sep 17 00:00:00 2001 From: elbenfreund Date: Fri, 22 Sep 2017 18:04:29 +0200 Subject: [PATCH 11/25] Account for empty children when figuring out min_height --- hamster_gtk/tracking/screens.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/hamster_gtk/tracking/screens.py b/hamster_gtk/tracking/screens.py index c929080f..c5c1b643 100644 --- a/hamster_gtk/tracking/screens.py +++ b/hamster_gtk/tracking/screens.py @@ -252,9 +252,11 @@ def _get_recent_activities_widget(self): # use. grid.show_all() # We fetch an arbitrary Button as height-reference - child = grid.get_children()[1] - height = child.get_preferred_height()[1] - min_height = self._app.config['tracking_recent_activities_items'] * height + min_height = 0 + children = grid.get_children() + if children: + height = children[1].get_preferred_height()[1] + min_height = self._app.config['tracking_recent_activities_items'] * height scrolled_window.set_min_content_height(min_height) scrolled_window.add(grid) From 5b16ae9d26d376d7f3eae729f121f07f92d4fe78 Mon Sep 17 00:00:00 2001 From: elbenfreund Date: Tue, 26 Sep 2017 16:04:09 +0200 Subject: [PATCH 12/25] Recent activities box uses last 24h --- hamster_gtk/tracking/screens.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hamster_gtk/tracking/screens.py b/hamster_gtk/tracking/screens.py index c5c1b643..28d0b146 100644 --- a/hamster_gtk/tracking/screens.py +++ b/hamster_gtk/tracking/screens.py @@ -351,7 +351,7 @@ def get_start_button(activity): self.attach(get_start_button(activity), 2, row_index, 1, 1) today = datetime.date.today() - start = today - datetime.timedelta(90) + start = today - datetime.timedelta(1) activities = helpers.get_recent_activities(self._controller, start, today) row_index = 0 From bd39965fa16ca4392f933f9197c6f4dd3fb44959 Mon Sep 17 00:00:00 2001 From: elbenfreund Date: Tue, 26 Sep 2017 16:05:45 +0200 Subject: [PATCH 13/25] Fix docstring --- hamster_gtk/hamster_gtk.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/hamster_gtk/hamster_gtk.py b/hamster_gtk/hamster_gtk.py index f2529a59..abd81323 100755 --- a/hamster_gtk/hamster_gtk.py +++ b/hamster_gtk/hamster_gtk.py @@ -433,12 +433,9 @@ def _write_config_to_file(self, configparser_instance): def _get_config_from_file(self): """ - Return a config dictionary from acp_instanceg file. + Return a config dictionary from app_instance file. - If there is none create a default config file. This methods main job is - to convert strings from the loaded ConfigParser File to appropiate - instances suitable for our config dictionary. The actual data retrival - is provided by a hamster-lib helper function. + If there is none create a default config file. Returns: dict: Dictionary of config key/values. From 9ccdc8029d59deca71ae0b9efc16d58a42488bdf Mon Sep 17 00:00:00 2001 From: elbenfreund Date: Tue, 26 Sep 2017 17:49:07 +0200 Subject: [PATCH 14/25] Improve 'config' fixture Until now, we returned a ``PathLocal`` instance as value for the ``tmpfile_path`` key. This has been changed to a proper string representations instead. --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0d7cb9c9..88179212 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -104,7 +104,7 @@ def config(request, tmpdir): 'store': 'sqlalchemy', 'day_start': datetime.time(5, 30, 0), 'fact_min_delta': 1, - 'tmpfile_path': tmpdir.join('tmpfile.hamster'), + 'tmpfile_path': str(tmpdir.join('tmpfile.hamster')), 'db_engine': 'sqlite', 'db_path': ':memory:', 'autocomplete_activities_range': 30, From 09aaac6dc49f05cfe43794355ff0c1c18070362e Mon Sep 17 00:00:00 2001 From: elbenfreund Date: Tue, 26 Sep 2017 17:51:26 +0200 Subject: [PATCH 15/25] Fix 'app' fixture Our ``app`` fixture (which is used extensively) used to instantiate the default ``HamsterGTK`` class. This meant that the default config loading was triggered and as a consequence the systems live config being used. This is obviously an issue. Instead we monkey patch the class now to skip the config loading machinery and use/return the ``config`` fixture. --- tests/conftest.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 88179212..aee94e07 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -35,13 +35,17 @@ def appdirs(request): # Instances @pytest.fixture -def app(request): +def app(request, config): """ Return an ``Application`` fixture. Please note: the app has just been started but not activated. """ - app = hamster_gtk.HamsterGTK() + def monkeypatched_reload_config(self): + return config + HamsterGTK = hamster_gtk.HamsterGTK + HamsterGTK._reload_config = monkeypatched_reload_config + app = HamsterGTK() app._startup(app) return app From 95fc765f871ef707467fc75b79ab6186a39f990d Mon Sep 17 00:00:00 2001 From: elbenfreund Date: Wed, 27 Sep 2017 11:55:37 +0200 Subject: [PATCH 16/25] Fix test if 'HamsterGTK' instantiation Our previous test simply instantiated the class, hence tiggering the config loading/creation machinery. This will directly interfere with the actual user config! To avoid this we provide a new test that monkeypatches the class accordingly. As a consequence the config loading needs to be tested seperatly now though. --- tests/test_hamster-gtk.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/test_hamster-gtk.py b/tests/test_hamster-gtk.py index 8f7f7c4c..129fad99 100755 --- a/tests/test_hamster-gtk.py +++ b/tests/test_hamster-gtk.py @@ -14,9 +14,19 @@ class TestHamsterGTK(object): """Unittests for the main app class.""" - def test_instantiation(self): - """Make sure class instatiation works as intended.""" - app = hamster_gtk.HamsterGTK() + def test_instantiation(self, config): + """ + Make sure class instatiation works as intended. + + We actually test against a monkeypatched class in order to avoid the + config loading machinery as this would access the user data on fs. + The relevant skiped methods need to be tested separately. + """ + def monkeypatched_reload_config(self): + return config + HamsterGTK = hamster_gtk.HamsterGTK + HamsterGTK._reload_config = monkeypatched_reload_config + app = HamsterGTK() assert app def test__reload_config(self, app, config, mocker): From c0dd639b381d50299d3711d2a45befebd59c0ea7 Mon Sep 17 00:00:00 2001 From: elbenfreund Date: Wed, 27 Sep 2017 14:38:29 +0200 Subject: [PATCH 17/25] Rename 'tracking_recent_activities_items' to 'count' --- hamster_gtk/hamster_gtk.py | 18 +++++++++--------- hamster_gtk/preferences/preferences_dialog.py | 2 +- hamster_gtk/tracking/screens.py | 2 +- tests/conftest.py | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/hamster_gtk/hamster_gtk.py b/hamster_gtk/hamster_gtk.py index abd81323..8950aa87 100755 --- a/hamster_gtk/hamster_gtk.py +++ b/hamster_gtk/hamster_gtk.py @@ -286,7 +286,7 @@ def _get_default_config(self): 'autocomplete_activities_range': 30, 'autocomplete_split_activity': False, 'tracking_show_recent_activities': True, - 'tracking_recent_activities_items': 6, + 'tracking_recent_activities_count': 6, } def _config_to_configparser(self, config): @@ -326,8 +326,8 @@ def get_autocomplete_split_activity(): def get_tracking_show_recent_activities(): return text_type(config['tracking_show_recent_activities']) - def get_tracking_recent_activities_items(): - return text_type(config['tracking_recent_activities_items']) + def get_tracking_recent_activities_count(): + return text_type(config['tracking_recent_activities_count']) cp_instance = SafeConfigParser() cp_instance.add_section('Backend') @@ -345,8 +345,8 @@ def get_tracking_recent_activities_items(): get_autocomplete_split_activity()) cp_instance.set('Frontend', 'tracking_show_recent_activities', get_tracking_show_recent_activities()) - cp_instance.set('Frontend', 'tracking_recent_activities_items', - get_tracking_recent_activities_items()) + cp_instance.set('Frontend', 'tracking_recent_activities_count', + get_tracking_recent_activities_count()) return cp_instance @@ -406,8 +406,8 @@ def get_autocomplete_split_activity(): def get_tracking_show_recent_activities(): return cp_instance.getboolean('Frontend', 'tracking_show_recent_activities') - def get_tracking_recent_activities_items(): - return int(cp_instance.get('Frontend', 'tracking_recent_activities_items')) + def get_tracking_recent_activities_count(): + return int(cp_instance.get('Frontend', 'tracking_recent_activities_count')) result = { 'store': get_store(), @@ -416,8 +416,8 @@ def get_tracking_recent_activities_items(): 'tmpfile_path': get_tmpfile_path(), 'autocomplete_activities_range': get_autocomplete_activities_range(), 'autocomplete_split_activity': get_autocomplete_split_activity(), - 'tracking_show_recent_activities': get_tracking_recent_activities_items(), - 'tracking_recent_activities_items': get_tracking_recent_activities_items(), + 'tracking_show_recent_activities': get_tracking_show_recent_activities(), + 'tracking_recent_activities_count': get_tracking_recent_activities_count(), } result.update(get_db_config()) return result diff --git a/hamster_gtk/preferences/preferences_dialog.py b/hamster_gtk/preferences/preferences_dialog.py index cfd6e7fa..abf6d901 100644 --- a/hamster_gtk/preferences/preferences_dialog.py +++ b/hamster_gtk/preferences/preferences_dialog.py @@ -83,7 +83,7 @@ def __init__(self, parent, app, initial, *args, **kwargs): ('tracking_show_recent_activities', (_("Show recent activities for quickly starting tracking."), HamsterSwitch())), - ('tracking_recent_activities_items', (_('How many recent activities?'), + ('tracking_recent_activities_count', (_('How many recent activities?'), HamsterSpinButton(SimpleAdjustment(0, GObject.G_MAXDOUBLE, 1)))), ]))), ] diff --git a/hamster_gtk/tracking/screens.py b/hamster_gtk/tracking/screens.py index 28d0b146..690d97c0 100644 --- a/hamster_gtk/tracking/screens.py +++ b/hamster_gtk/tracking/screens.py @@ -256,7 +256,7 @@ def _get_recent_activities_widget(self): children = grid.get_children() if children: height = children[1].get_preferred_height()[1] - min_height = self._app.config['tracking_recent_activities_items'] * height + min_height = self._app.config['tracking_recent_activities_count'] * height scrolled_window.set_min_content_height(min_height) scrolled_window.add(grid) diff --git a/tests/conftest.py b/tests/conftest.py index aee94e07..742d936a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -114,6 +114,6 @@ def config(request, tmpdir): 'autocomplete_activities_range': 30, 'autocomplete_split_activity': False, 'tracking_show_recent_activities': True, - 'tracking_recent_activities_items': 6, + 'tracking_recent_activities_count': 6, } return config From 2177d680e1bbff1c2c1946527067733dfe01cec4 Mon Sep 17 00:00:00 2001 From: elbenfreund Date: Fri, 17 Nov 2017 12:33:08 +0100 Subject: [PATCH 18/25] helpers: Refactor 'serialize_activity' Ref.: #136 --- hamster_gtk/helpers.py | 33 ++++++++++++++++++++--- hamster_gtk/overview/widgets/fact_grid.py | 13 ++------- tests/conftest.py | 2 +- tests/preferences/conftest.py | 6 ++--- tests/test_hamster-gtk.py | 2 +- 5 files changed, 36 insertions(+), 20 deletions(-) diff --git a/hamster_gtk/helpers.py b/hamster_gtk/helpers.py index 046a239a..794b063a 100644 --- a/hamster_gtk/helpers.py +++ b/hamster_gtk/helpers.py @@ -22,6 +22,7 @@ from __future__ import absolute_import, unicode_literals import datetime +from gettext import gettext as _ import operator import re @@ -182,7 +183,7 @@ def decompose_raw_fact_string(text, raw=False): # [TODO] -# Oncec LIB-251 has been fixed this should no longer be needed. +# Once LIB-251 has been fixed this should no longer be needed. def get_recent_activities(controller, start, end): """Return a list of all activities logged in facts within the given timeframe.""" # [FIXME] @@ -196,10 +197,34 @@ def get_recent_activities(controller, start, end): return OrderedSet(recent_activities) -def serialize_activity(activity): - """Provide a serialized string version of an activity.""" +def serialize_activity(activity, separator='@', none_category='not categorized'): + """ + Provide a serialized string version of an activity. + + Args: + activity (Activity): ``Activity`` instance to serialize. + separator (str, optional): ``string`` used to separate ``activity.name`` and + ``category.name``. The separator will be omitted if ``none_categoty=''`` and + ``activity.category=None``. Defaults to ``@``. + none_category (str, optional): ``string`` to represent the 'lack of a cactegory' for an + activity instance. Defaults to ``not categorized``. + + Returns: + str: A string representation of the passed activity. + """ + if not separator: + raise ValueError(_("No valid separator has been provided.")) + if not none_category and none_category is not '': + raise ValueError(_("No valid text for 'none_category' has been provided.")) + if activity.category: - result = '{a.name}@{a.category.name}'.format(a=activity) + category_text = activity.category.name + else: + category_text = none_category + + if category_text: + result = '{activity_text}{separator}{category_text}'.format(activity_text=activity.name, + category_text=category_text, separator=separator) else: result = activity.name return text_type(result) diff --git a/hamster_gtk/overview/widgets/fact_grid.py b/hamster_gtk/overview/widgets/fact_grid.py index 8eff81cd..6c3cdade 100644 --- a/hamster_gtk/overview/widgets/fact_grid.py +++ b/hamster_gtk/overview/widgets/fact_grid.py @@ -201,18 +201,9 @@ def __init__(self, fact): def _get_activity_widget(self, fact): """Return widget to render the activity, including its related category.""" - # [FIXME] - # Once 'preferences/config' is live, we can change this. - # Most likly we do not actually need to jump through extra hoops as - # legacy hamster did but just use a i18n'ed string and be done. - if not fact.category: - category = 'not categorised' - else: - category = str(fact.category) activity_label = Gtk.Label() - activity_label.set_markup("{activity} - {category}".format( - activity=GObject.markup_escape_text(fact.activity.name), - category=GObject.markup_escape_text(category))) + label_text = helpers.serialize_activity(fact.activity) + activity_label.set_markup(GObject.markup_escape_text(label_text)) activity_label.props.halign = Gtk.Align.START return activity_label diff --git a/tests/conftest.py b/tests/conftest.py index 742d936a..3318de9f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -43,7 +43,7 @@ def app(request, config): """ def monkeypatched_reload_config(self): return config - HamsterGTK = hamster_gtk.HamsterGTK + HamsterGTK = hamster_gtk.HamsterGTK # NOQA HamsterGTK._reload_config = monkeypatched_reload_config app = HamsterGTK() app._startup(app) diff --git a/tests/preferences/conftest.py b/tests/preferences/conftest.py index 94df22cc..dda65adc 100644 --- a/tests/preferences/conftest.py +++ b/tests/preferences/conftest.py @@ -87,7 +87,7 @@ def tracking_show_recent_activities_parametrized(request): @pytest.fixture(params=(0, 1, 5, 15)) -def tracking_recent_activities_items_parametrized(request): +def tracking_recent_activities_count_parametrized(request): """Return a parametrized tracking_recent_activities_items_parametrized value.""" return request.param @@ -97,7 +97,7 @@ def config_parametrized(request, store_parametrized, day_start_parametrized, fact_min_delta_parametrized, tmpfile_path_parametrized, db_engine_parametrized, db_path_parametrized, autocomplete_activities_range_parametrized, autocomplete_split_activity_parametrized, tracking_show_recent_activities_parametrized, - tracking_recent_activities_items_parametrized): + tracking_recent_activities_count_parametrized): """Return a config fixture with heavily parametrized config values.""" return { 'store': store_parametrized, @@ -109,7 +109,7 @@ def config_parametrized(request, store_parametrized, day_start_parametrized, 'autocomplete_activities_range': autocomplete_activities_range_parametrized, 'autocomplete_split_activity': autocomplete_split_activity_parametrized, 'tracking_show_recent_activities': tracking_show_recent_activities_parametrized, - 'tracking_recent_activities_items': tracking_recent_activities_items_parametrized, + 'tracking_recent_activities_count': tracking_recent_activities_count_parametrized, } diff --git a/tests/test_hamster-gtk.py b/tests/test_hamster-gtk.py index 129fad99..4ac13eea 100755 --- a/tests/test_hamster-gtk.py +++ b/tests/test_hamster-gtk.py @@ -24,7 +24,7 @@ def test_instantiation(self, config): """ def monkeypatched_reload_config(self): return config - HamsterGTK = hamster_gtk.HamsterGTK + HamsterGTK = hamster_gtk.HamsterGTK # NOQA HamsterGTK._reload_config = monkeypatched_reload_config app = HamsterGTK() assert app From 4b2819beee75ffb818931d8572bff35cd1bc3efb Mon Sep 17 00:00:00 2001 From: Eric Goller Date: Fri, 4 May 2018 15:41:25 +0200 Subject: [PATCH 19/25] tracking: Improve categoryless fact serialization When serializing facts for the 'continue tracking' functionality, we need to make sure that the 'uncategorized' placeholder is omitted, otherwise a category of that name would be created! Ref.: #136 --- hamster_gtk/tracking/screens.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hamster_gtk/tracking/screens.py b/hamster_gtk/tracking/screens.py index 690d97c0..1688e1fe 100644 --- a/hamster_gtk/tracking/screens.py +++ b/hamster_gtk/tracking/screens.py @@ -323,7 +323,7 @@ def add_row_widgets(row_index, activity): """ def get_label(activity): """Label representing the activity/category combination.""" - label = Gtk.Label(helpers.serialize_activity(activity)) + label = Gtk.Label(helpers.serialize_activity(activity, none_category='')) label.set_halign(Gtk.Align.START) return label @@ -335,14 +335,14 @@ def get_copy_button(activity): actually starting the tracking. """ button = Gtk.Button('Copy') - activity = helpers.serialize_activity(activity) + activity = helpers.serialize_activity(activity, none_category='') button.connect('clicked', self._on_copy_button, activity) return button def get_start_button(activity): """A button that will start a new ongoing fact based on that activity.""" button = Gtk.Button('Start') - activity = helpers.serialize_activity(activity) + activity = helpers.serialize_activity(activity, none_category='') button.connect('clicked', self._on_start_button, activity) return button From 90dbcc7c3c65134681acb13d474ae4fb4bfe1cd8 Mon Sep 17 00:00:00 2001 From: Eric Goller Date: Fri, 4 May 2018 15:53:19 +0200 Subject: [PATCH 20/25] Fix typos --- hamster_gtk/helpers.py | 6 +++--- tests/test_hamster-gtk.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/hamster_gtk/helpers.py b/hamster_gtk/helpers.py index 794b063a..0c4dfac4 100644 --- a/hamster_gtk/helpers.py +++ b/hamster_gtk/helpers.py @@ -187,7 +187,7 @@ def decompose_raw_fact_string(text, raw=False): def get_recent_activities(controller, start, end): """Return a list of all activities logged in facts within the given timeframe.""" # [FIXME] - # This manual sorting within python is of cause less than optimal. We stick + # This manual sorting within python is of course less than optimal. We stick # with it for now as this is just a preliminary workaround helper anyway and # effective sorting will need to be implemented by the storage backend in # ``hamster-lib``. @@ -204,9 +204,9 @@ def serialize_activity(activity, separator='@', none_category='not categorized') Args: activity (Activity): ``Activity`` instance to serialize. separator (str, optional): ``string`` used to separate ``activity.name`` and - ``category.name``. The separator will be omitted if ``none_categoty=''`` and + ``category.name``. The separator will be omitted if ``none_category=''`` and ``activity.category=None``. Defaults to ``@``. - none_category (str, optional): ``string`` to represent the 'lack of a cactegory' for an + none_category (str, optional): ``string`` to represent the 'lack of a category' for an activity instance. Defaults to ``not categorized``. Returns: diff --git a/tests/test_hamster-gtk.py b/tests/test_hamster-gtk.py index 4ac13eea..1e7b8f5a 100755 --- a/tests/test_hamster-gtk.py +++ b/tests/test_hamster-gtk.py @@ -16,7 +16,7 @@ class TestHamsterGTK(object): def test_instantiation(self, config): """ - Make sure class instatiation works as intended. + Make sure class instantiation works as intended. We actually test against a monkeypatched class in order to avoid the config loading machinery as this would access the user data on fs. From fa8057dbdd85fb8cc054260fa00ce2cdb47d4822 Mon Sep 17 00:00:00 2001 From: Eric Goller Date: Fri, 4 May 2018 16:08:23 +0200 Subject: [PATCH 21/25] tracking: Add issue reference to ToDo comment Ref,: #136 --- hamster_gtk/tracking/screens.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hamster_gtk/tracking/screens.py b/hamster_gtk/tracking/screens.py index 1688e1fe..5af97788 100644 --- a/hamster_gtk/tracking/screens.py +++ b/hamster_gtk/tracking/screens.py @@ -251,7 +251,7 @@ def _get_recent_activities_widget(self): # allocated to its children so they actually have a height that we can # use. grid.show_all() - # We fetch an arbitrary Button as height-reference + # We fetch an arbitrary Button as height-reference [#224] min_height = 0 children = grid.get_children() if children: From 72af08c01d0a4f51f027f02c737a172ee2d460fb Mon Sep 17 00:00:00 2001 From: Eric Goller Date: Fri, 4 May 2018 16:31:16 +0200 Subject: [PATCH 22/25] helpers: Remove 'none_category' argument on serialize_activity --- hamster_gtk/helpers.py | 12 ++++-------- hamster_gtk/tracking/screens.py | 6 +++--- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/hamster_gtk/helpers.py b/hamster_gtk/helpers.py index 0c4dfac4..4f0e4140 100644 --- a/hamster_gtk/helpers.py +++ b/hamster_gtk/helpers.py @@ -197,30 +197,26 @@ def get_recent_activities(controller, start, end): return OrderedSet(recent_activities) -def serialize_activity(activity, separator='@', none_category='not categorized'): +def serialize_activity(activity, separator='@'): """ Provide a serialized string version of an activity. Args: activity (Activity): ``Activity`` instance to serialize. separator (str, optional): ``string`` used to separate ``activity.name`` and - ``category.name``. The separator will be omitted if ``none_category=''`` and + ``category.name``. The separator will be omitted if ``activity.category=None``. Defaults to ``@``. - none_category (str, optional): ``string`` to represent the 'lack of a category' for an - activity instance. Defaults to ``not categorized``. Returns: str: A string representation of the passed activity. """ if not separator: raise ValueError(_("No valid separator has been provided.")) - if not none_category and none_category is not '': - raise ValueError(_("No valid text for 'none_category' has been provided.")) + + category_text = None if activity.category: category_text = activity.category.name - else: - category_text = none_category if category_text: result = '{activity_text}{separator}{category_text}'.format(activity_text=activity.name, diff --git a/hamster_gtk/tracking/screens.py b/hamster_gtk/tracking/screens.py index 5af97788..6c3232a4 100644 --- a/hamster_gtk/tracking/screens.py +++ b/hamster_gtk/tracking/screens.py @@ -323,7 +323,7 @@ def add_row_widgets(row_index, activity): """ def get_label(activity): """Label representing the activity/category combination.""" - label = Gtk.Label(helpers.serialize_activity(activity, none_category='')) + label = Gtk.Label(helpers.serialize_activity(activity)) label.set_halign(Gtk.Align.START) return label @@ -335,14 +335,14 @@ def get_copy_button(activity): actually starting the tracking. """ button = Gtk.Button('Copy') - activity = helpers.serialize_activity(activity, none_category='') + activity = helpers.serialize_activity(activity) button.connect('clicked', self._on_copy_button, activity) return button def get_start_button(activity): """A button that will start a new ongoing fact based on that activity.""" button = Gtk.Button('Start') - activity = helpers.serialize_activity(activity, none_category='') + activity = helpers.serialize_activity(activity) button.connect('clicked', self._on_start_button, activity) return button From d685bf2749846077e1a4ba3b3df101aecf8fac75 Mon Sep 17 00:00:00 2001 From: Eric Goller Date: Fri, 4 May 2018 16:44:23 +0200 Subject: [PATCH 23/25] tests: Fix docker setup --- ci/run_docker.sh | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ci/run_docker.sh b/ci/run_docker.sh index c39641f2..fb455830 100755 --- a/ci/run_docker.sh +++ b/ci/run_docker.sh @@ -11,13 +11,11 @@ xvfb=$! export DISPLAY=:99 -pip install --upgrade pip pip install -r requirements/test.pip - -python setup.py install +pip install . make resources - make test-all + # See: https://docs.codecov.io/docs/testing-with-docker for details bash <(curl -s https://codecov.io/bash) test $err = 0 From c5105941ae4a1f4dccf016dda527c99a3cba830c Mon Sep 17 00:00:00 2001 From: Eric Goller Date: Sun, 6 May 2018 14:03:30 +0200 Subject: [PATCH 24/25] Fix imports --- hamster_gtk/helpers.py | 4 ++-- hamster_gtk/preferences/preferences_dialog.py | 3 +-- hamster_gtk/preferences/widgets/__init__.py | 2 +- tests/misc/widgets/test_raw_fact_completion.py | 3 +-- tests/test_helpers.py | 5 +++-- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/hamster_gtk/helpers.py b/hamster_gtk/helpers.py index 4f0e4140..b62425f2 100644 --- a/hamster_gtk/helpers.py +++ b/hamster_gtk/helpers.py @@ -22,12 +22,12 @@ from __future__ import absolute_import, unicode_literals import datetime -from gettext import gettext as _ import operator import re +from gettext import gettext as _ -from orderedset import OrderedSet import six +from orderedset import OrderedSet from six import text_type diff --git a/hamster_gtk/preferences/preferences_dialog.py b/hamster_gtk/preferences/preferences_dialog.py index abf6d901..21583f43 100644 --- a/hamster_gtk/preferences/preferences_dialog.py +++ b/hamster_gtk/preferences/preferences_dialog.py @@ -30,9 +30,8 @@ from hamster_gtk.misc.widgets import LabelledWidgetsGrid from hamster_gtk.preferences.widgets import (ComboFileChooser, - HamsterSwitch, HamsterComboBoxText, - HamsterSpinButton, + HamsterSpinButton, HamsterSwitch, SimpleAdjustment, TimeEntry) diff --git a/hamster_gtk/preferences/widgets/__init__.py b/hamster_gtk/preferences/widgets/__init__.py index d81ddd82..fa0a28a2 100644 --- a/hamster_gtk/preferences/widgets/__init__.py +++ b/hamster_gtk/preferences/widgets/__init__.py @@ -19,7 +19,7 @@ from .combo_file_chooser import ComboFileChooser # NOQA from .config_widget import ConfigWidget # NOQA -from .hamster_switch import HamsterSwitch # NOQA from .hamster_combo_box_text import HamsterComboBoxText # NOQA from .hamster_spin_button import HamsterSpinButton, SimpleAdjustment # NOQA +from .hamster_switch import HamsterSwitch # NOQA from .time_entry import TimeEntry # NOQA diff --git a/tests/misc/widgets/test_raw_fact_completion.py b/tests/misc/widgets/test_raw_fact_completion.py index 977fc82d..4b24388b 100644 --- a/tests/misc/widgets/test_raw_fact_completion.py +++ b/tests/misc/widgets/test_raw_fact_completion.py @@ -20,9 +20,8 @@ from __future__ import absolute_import, unicode_literals -from orderedset import OrderedSet - from gi.repository import Gtk +from orderedset import OrderedSet from hamster_gtk.misc.widgets.raw_fact_entry import RawFactCompletion diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 20da0a31..470af154 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- -import datetime +from __future__ import unicode_literals -from gi.repository import Gtk +import datetime import pytest +from gi.repository import Gtk import hamster_gtk.helpers as helpers From 1df269ac23798c13d874374a30b4995a07256dbe Mon Sep 17 00:00:00 2001 From: Eric Goller Date: Sun, 6 May 2018 14:04:05 +0200 Subject: [PATCH 25/25] helpers: Add tests for 'serialize_activity' --- tests/test_helpers.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 470af154..49b6cfef 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -146,3 +146,26 @@ def test__get_delta_string(minutes, expectation): delta = datetime.timedelta(minutes=minutes) result = helpers.get_delta_string(delta) assert result == expectation + + +class TestSerializeActivity(object): + """Unit tests for `serialize_activity` helper function.""" + + #@pytest.mark.parametrize('seperator', ( + def test_with_category(self, activity): + """Make sure that the serialized activity matches expectations.""" + result = helpers.serialize_activity(activity) + assert result == '{s.name}@{s.category.name}'.format(s=activity) + + @pytest.mark.parametrize('activity__category', (None,)) + def test_without_category(self, activity): + """Make sure that the serialized activity matches expectations.""" + result = helpers.serialize_activity(activity) + assert result == '{s.name}'.format(s=activity) + + @pytest.mark.parametrize('separator', (';', '/', '%')) + def test_seperators(self, activity, separator): + """Make sure that the serialized activity matches expectations.""" + result = helpers.serialize_activity(activity, separator) + assert result == '{s.name}{seperator}{s.category.name}'.format(s=activity, + seperator=separator)