Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
54fcc75
Wrap 'make test' with 'xvfb-run'
elbenfreund Jul 8, 2017
975623e
Add inline comments
elbenfreund Jun 22, 2017
334753c
Add dedicated helper for fetching recent activities
elbenfreund Jun 22, 2017
4c5b85e
Add 'recent activities' to start-tracking view
elbenfreund Jun 22, 2017
5c17e7a
Sizing for recent activity widget
elbenfreund Jul 1, 2017
d941aab
Adjust existing tests
elbenfreund Jul 8, 2017
8593902
rename 'row_counter' parameter to 'row_index'
elbenfreund Sep 22, 2017
c08bd74
Fix typo
elbenfreund Sep 22, 2017
bec62d8
Change wording within preferences.
elbenfreund Sep 22, 2017
ddafbd1
fix ordering for 'get_recent_activities' helper
elbenfreund Sep 22, 2017
fa93742
Account for empty children when figuring out min_height
elbenfreund Sep 22, 2017
5b16ae9
Recent activities box uses last 24h
elbenfreund Sep 26, 2017
bd39965
Fix docstring
elbenfreund Sep 26, 2017
9ccdc80
Improve 'config' fixture
elbenfreund Sep 26, 2017
09aaac6
Fix 'app' fixture
elbenfreund Sep 26, 2017
95fc765
Fix test if 'HamsterGTK' instantiation
elbenfreund Sep 27, 2017
c0dd639
Rename 'tracking_recent_activities_items' to 'count'
elbenfreund Sep 27, 2017
2177d68
helpers: Refactor 'serialize_activity'
elbenfreund Nov 17, 2017
4b2819b
tracking: Improve categoryless fact serialization
elbenfreund May 4, 2018
90dbcc7
Fix typos
elbenfreund May 4, 2018
fa8057d
tracking: Add issue reference to ToDo comment
elbenfreund May 4, 2018
72af08c
helpers: Remove 'none_category' argument on serialize_activity
elbenfreund May 4, 2018
d685bf2
tests: Fix docker setup
elbenfreund May 4, 2018
c510594
Fix imports
elbenfreund May 6, 2018
1df269a
helpers: Add tests for 'serialize_activity'
elbenfreund May 6, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 28 additions & 5 deletions hamster_gtk/hamster_gtk.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ class SignalHandler(GObject.GObject):
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, ()),
str('daterange-changed'): (GObject.SIGNAL_RUN_LAST, None, (GObject.TYPE_PYOBJECT,)),
Expand Down Expand Up @@ -255,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):
Expand All @@ -279,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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use count or number. items is less clear.

}

def _config_to_configparser(self, config):
Expand Down Expand Up @@ -315,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())
Expand All @@ -329,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

Expand Down Expand Up @@ -385,13 +403,21 @@ 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(),
'fact_min_delta': get_fact_min_delta(),
'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
Expand All @@ -407,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.
Expand Down
28 changes: 28 additions & 0 deletions hamster_gtk/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
from __future__ import absolute_import, unicode_literals

import datetime
import operator
import re

from orderedset import OrderedSet
import six
from six import text_type

Expand Down Expand Up @@ -88,6 +90,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
Expand Down Expand Up @@ -177,6 +181,30 @@ 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."""
# [FIXME]
# This manual sorting within python is of cause less than optimal. We stick
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

of cause → of course

# 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The activities are returned in a convoluted order. It should be reversed prior to removing duplicates.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure I do understand. Could you elaborate please?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be OrderedSet(reversed(recent_activities)), otherwise you will get the activities in the order they were first used, not the last.

facts     1 2 1 3 2 1 3  output   reversed
current   x x   x       → 1 2 3  → 3 2 1
expected          x x x → 3 1 2  → 2 1 3

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the clarification. To be honest, right now FactManager.get_all() does not commit to any ordering at all. I created an issue with hamster-lib to improve this.
For now I will implement a workaround simple sorting call as part of the prelimery get_recent_activities helper which will hopefully address your concerns.



def serialize_activity(activity):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not we have something like this somewhere already?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think earlier hamster-lib branches had something like this. Also the overview has its own "serialize activity" code which is different however:

  • it deals with legacy hamerest representation of activity.category=None. This is something that I would be willing to throw out of the window.
  • It uses - as seperator between activity.name and category. I think this is better looking for the overview. What do you think?

So this helper provides a generic serialization utilizing the standart @ seperator. As far as I can tell current versions of hamster-lib or hamster-gtk do not have such a function yet. But share the feeling that we had this around for a while in various incarnations...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Wait, do we intend to not support category-less facts?
  • I actually prefer the at sign for consistency or habit.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • No we do and want to support category less facts. I am not quite sure that the way legacy hamster did is really where we want to go..
  • Does that mean you would prefer @ in the overview as well? At this stage I do not care about this detail very much as we will have to revisit the general layout at some later stage anyway...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, having @ in overview is what I am accustomed to.

Copy link
Collaborator Author

@elbenfreund elbenfreund Sep 22, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alrighty. Will adjust this then :) Im cool either way tbh.

"""Provide a serialized string version of an 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.
Expand Down
5 changes: 5 additions & 0 deletions hamster_gtk/preferences/preferences_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
(_("Show recent activities for quickly starting tracking."),
HamsterSwitch())),
('tracking_recent_activities_items', (_('How many recent activities?'),
HamsterSpinButton(SimpleAdjustment(0, GObject.G_MAXDOUBLE, 1)))),
]))),
]

Expand Down
148 changes: 148 additions & 0 deletions hamster_gtk/tracking/screens.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -186,8 +187,16 @@ 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
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):
"""
Start a new *ongoing fact*.
Expand Down Expand Up @@ -230,6 +239,29 @@ 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)
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is really ugly and crashes when there are no items:

$ env XDG_CONFIG_HOME=$HOME/.dev_config XDG_DATA_HOME=$HOME/.dev_local hamster-gtk
Hamster-GTK started.
Traceback (most recent call last):
  File "/nix/store/fzrxvdjas3icqb2n5akahvix758jnfvj-python3.6-hamster-gtk/lib/python3.6/site-packages/hamster_gtk/hamster_gtk.py", line 213, in _activate
    self.window = MainWindow(app)
  File "/nix/store/fzrxvdjas3icqb2n5akahvix758jnfvj-python3.6-hamster-gtk/lib/python3.6/site-packages/hamster_gtk/hamster_gtk.py", line 107, in __init__
    self.add(TrackingScreen(self.app))
  File "/nix/store/fzrxvdjas3icqb2n5akahvix758jnfvj-python3.6-hamster-gtk/lib/python3.6/site-packages/hamster_gtk/tracking/screens.py", line 49, in __init__
    self.start_tracking_view = StartTrackingBox(self._app)
  File "/nix/store/fzrxvdjas3icqb2n5akahvix758jnfvj-python3.6-hamster-gtk/lib/python3.6/site-packages/hamster_gtk/tracking/screens.py", line 195, in __init__
    self.recent_activities_widget = self._get_recent_activities_widget()
  File "/nix/store/fzrxvdjas3icqb2n5akahvix758jnfvj-python3.6-hamster-gtk/lib/python3.6/site-packages/hamster_gtk/tracking/screens.py", line 255, in _get_recent_activities_widget
    child = grid.get_children()[1]
IndexError: list index out of range
Traceback (most recent call last):
  File "/nix/store/fzrxvdjas3icqb2n5akahvix758jnfvj-python3.6-hamster-gtk/lib/python3.6/site-packages/hamster_gtk/hamster_gtk.py", line 218, in _activate
    app.add_window(self.window)
TypeError: Argument 1 does not allow None as a value
Hamster-GTK shut down.

Copy link
Collaborator Author

@elbenfreund elbenfreund Sep 22, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that the way we derive our min_height hint is far from elegant (but fixed value layouts are far worse even). I am absolutely open to better approaches! Empty children are definitely a nice catch and have been addressed in the latest version.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a todo comment and go with this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created #224 as I would like to avoid todo comments.

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)
return scrolled_window

# Callbacks
def _on_start_tracking_button(self, button):
"""Callback for the 'start tracking' button."""
Expand All @@ -238,3 +270,119 @@ 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()

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 because 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."""

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_index, 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_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(1)
activities = helpers.get_recent_activities(self._controller, start, today)

row_index = 0
for activity in activities:
add_row_widgets(row_index, activity)
row_index += 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()
12 changes: 9 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -104,10 +108,12 @@ 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,
'autocomplete_split_activity': False,
'tracking_show_recent_activities': True,
'tracking_recent_activities_items': 6,
}
return config
Loading