Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for local / personnal aliases #29

Merged
merged 13 commits into from
Jul 30, 2014
5 changes: 5 additions & 0 deletions doc/tksrc.sample
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ auto_add = auto
# empty
# auto_fill_days = 0,1,2,3,4


# Defines a list of local aliases that you will be able to use in your timesheets
# but that will never be pushed to Zebra
local_aliases = _lunch, _coffee_break

# This section contains your project name <-> id mapping. The format is:
# project_name = project_id/activity_id
[wrmap]
Expand Down
14 changes: 10 additions & 4 deletions taxi/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,21 +350,27 @@ def run(self):
non_workday_entries = t.get_non_current_workday_entries()

if non_workday_entries:
dates = [d[0] for d in non_workday_entries]
self.view.non_working_dates_commit_error(dates)
self.view.non_working_dates_commit_error(
non_workday_entries.keys()
)

return

self.view.pushing_entries()
r = remote.ZebraRemote(self.settings.get('site'),
self.settings.get('username'),
self.settings.get('password'))
entries_to_push = t.get_entries(self.options.get('date', None),
exclude_ignored=True)
entries_to_push = t.get_entries(self.options.get('date', None), exclude_ignored=True, exclude_local=True)
(pushed_entries, failed_entries) = r.send_entries(entries_to_push,
self._entry_pushed)

local_entries = t.get_local_entries(self.options.get('date', None))
local_entries_list = []
for (date, entries) in local_entries.iteritems():
local_entries_list.extend(entries)

t.fix_entries_start_time()
t.comment_entries(local_entries_list)
t.comment_entries(pushed_entries)
t.save()

Expand Down
67 changes: 44 additions & 23 deletions taxi/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
from collections import defaultdict
import datetime
import re

Expand All @@ -10,6 +11,7 @@
from taxi.parser import DateLine, EntryLine, TextLine, ParseError
from taxi.utils import date as date_utils


class Entry:
def __init__(self, date, project_name, hours, description, id=None):
self.project_name = project_name
Expand All @@ -18,13 +20,16 @@ def __init__(self, date, project_name, hours, description, id=None):
self.date = date
self.pushed = False
self.ignored = False
self.local = False
self.project_id = None
self.activity_id = None
self.id = id

def __unicode__(self):
if self.is_ignored():
project_name = u'%s (ignored)' % (self.project_name)
elif self.is_local():
project_name = u'%s (local)' % (self.project_name)
else:
project_name = u'%s (%s/%s)' % (self.project_name, self.project_id, self.activity_id)

Expand All @@ -33,6 +38,13 @@ def __unicode__(self):
def is_ignored(self):
return self.ignored or self.get_duration() == 0

def is_local(self):
"""
Return true if the entry is local, ie. we don't have to push it to the
remote
"""
return self.local

def get_duration(self):
if isinstance(self.duration, tuple):
if None in self.duration:
Expand All @@ -50,6 +62,7 @@ def get_duration(self):

return self.duration


class Project:
STATUS_NOT_STARTED = 0
STATUS_ACTIVE = 1
Expand Down Expand Up @@ -221,14 +234,17 @@ def _update_entries(self):
if entry.project_name in self.mappings:
entry.project_id = self.mappings[entry.project_name][0]
entry.activity_id = self.mappings[entry.project_name][1]

if entry.project_id is None or entry.activity_id is None:
entry.local = True
else:
if not entry.is_ignored():
raise UndefinedAliasError(line.get_alias())

self.entries[current_date].append(entry)

def get_entries(self, date=None, exclude_ignored=False):
entries_dict = {}
def get_filtered_entries(self, date=None, filter_callback=None):
entries_dict = defaultdict(list)

# Date can either be a single date (only 1 day) or a tuple for a
# date range
Expand All @@ -237,31 +253,33 @@ def get_entries(self, date=None, exclude_ignored=False):

for (entrydate, entries) in self.entries.iteritems():
if date is None or (entrydate >= date[0] and entrydate <= date[1]):
if entrydate not in entries_dict:
entries_dict[entrydate] = []

if not exclude_ignored:
entries_dict[entrydate].extend(entries)
if filter_callback is None:
d_list = entries
else:
d_list = [entry for entry in entries if not entry.is_ignored()]
entries_dict[entrydate].extend(d_list)
d_list = [
entry for entry in entries
if (filter_callback is not None
and filter_callback(entry))
]

entries_dict[entrydate].extend(d_list)

return entries_dict

def get_ignored_entries(self, date=None):
entries_dict = self.get_entries(date, False)
ignored_entries = {}
def get_entries(self, date=None, exclude_ignored=False,
exclude_local=False):
def entry_filter(entry):
return (not (exclude_ignored and entry.is_ignored())
and not (exclude_local and entry.is_local()))

for (date, entries) in entries_dict.iteritems():
ignored_entries_list = []
for entry in entries:
if entry.is_ignored():
ignored_entries_list.append(entry)
return self.get_filtered_entries(date, entry_filter)

if ignored_entries_list:
ignored_entries[date] = ignored_entries_list
def get_local_entries(self, date=None):
return self.get_filtered_entries(date, lambda entry: entry.is_local())

return ignored_entries
def get_ignored_entries(self, date=None):
return self.get_filtered_entries(date,
lambda entry: entry.is_ignored())

def _get_latest_entry_for_date(self, date):
date_line = None
Expand Down Expand Up @@ -416,15 +434,18 @@ def to_lines(self):
return lines

def get_non_current_workday_entries(self, limit_date=None):
non_workday_entries = []
entries = self.get_entries(limit_date, exclude_ignored=True)
non_workday_entries = defaultdict(list)
entries = self.get_entries(
limit_date,
lambda e: not e.is_ignored() and not e.is_local()
)
today = datetime.date.today()
yesterday = date_utils.get_previous_working_day(today)

for (date, date_entries) in entries.iteritems():
if date not in (today, yesterday) or date.strftime('%w') in [6, 0]:
if date_entries:
non_workday_entries.append((date, date_entries))
non_workday_entries[date].extend(date_entries)

return non_workday_entries

Expand Down
12 changes: 11 additions & 1 deletion taxi/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def get_auto_fill_days(self):

return [int(e.strip()) for e in auto_fill_days.split(',')]

def get_aliases(self, include_shared=True):
def get_aliases(self, include_shared=True, include_local=True):
aliases = {}
config_aliases = self.config.items('wrmap')
shared_config_aliases = (self.config.items('shared_wrmap')
Expand All @@ -68,6 +68,16 @@ def get_aliases(self, include_shared=True):
(alias, mapping)
)

if include_local:
try:
local_aliases = self.config.get('default', 'local_aliases')
except ConfigParser.NoOptionError:
local_aliases = ''

if local_aliases:
for alias in local_aliases.split(','):
aliases[alias.strip()] = (None, None)

return aliases

def get_reversed_aliases(self, include_shared=True):
Expand Down
8 changes: 7 additions & 1 deletion taxi/ui/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ def alias_added(self, alias, mapping):
def _show_mapping(self, mapping, project, alias_first=True):
(alias, t) = mapping

# Handle local aliases
if t == (None, None):
self.msg(u"%s -> local alias" % alias)
return

mapping_name = '%s/%s' % t

if not project:
Expand Down Expand Up @@ -202,7 +207,8 @@ def show_status(self, entries_dict):
date_utils.unicode_strftime(date, '%A %d %B').capitalize())
for entry in entries:
self.msg(unicode(entry))
subtotal_hours += entry.get_duration() or 0
if not entry.is_local():
subtotal_hours += entry.get_duration() or 0

self.msg(u'%-29s %5.2f\n' % ('', subtotal_hours))
total_hours += subtotal_hours
Expand Down
9 changes: 8 additions & 1 deletion tests/commands/test_alias.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class AliasCommandTestCase(CommandTestCase):
def setUp(self):
super(AliasCommandTestCase, self).setUp()

self.config = self.default_config
self.config = self.default_config.copy()
self.config['wrmap'] = {
'alias_1': '123/456',
'alias_2': '123/457',
Expand Down Expand Up @@ -71,3 +71,10 @@ def test_alias_add(self):
config_lines = f.readlines()

self.assertIn('bar = 123/458\n', config_lines)

def test_local_alias(self):
self.config = self.default_config.copy()
self.config['default']['local_aliases'] = 'pingpong'

stdout = self.run_alias_command([], self.config)
self.assertIn('pingpong -> local alias', stdout)
13 changes: 12 additions & 1 deletion tests/commands/test_commit.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
@freeze_time('2014-01-20')
class CommitCommandTestCase(CommandTestCase):
def test_commit(self):
config = self.default_config
config = self.default_config.copy()
config['wrmap']['fail'] = '456/789'

self.write_entries("""20/01/2014
Expand All @@ -22,3 +22,14 @@ def test_commit(self):
lines = entries.readlines()

self.assertEqual(lines[4], 'fail 09:15-11:45 Make printer work\n')

def test_local_alias(self):
config = self.default_config.copy()
config['default']['local_aliases'] = '_pingpong'

self.write_entries("""20/01/2014
_pingpong 0800-0900 Play ping-pong
""")

stdout = self.run_command('commit', options=self.default_options)
self.assertIn("Total pushed 0.00", stdout)
39 changes: 39 additions & 0 deletions tests/commands/test_status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from freezegun import freeze_time

from . import CommandTestCase


@freeze_time('2014-01-20')
class StatusCommandTestCase(CommandTestCase):
def test_local_alias(self):
config = self.default_config.copy()
config['default']['local_aliases'] = '_pingpong'

self.write_entries("""20/01/2014
_pingpong 0800-0900 Play ping-pong
""")
stdout = self.run_command('status', options=self.default_options)

self.assertIn(
"_pingpong (local) 1.00 Play ping-pong",
stdout
)

def test_multiple_local_aliases(self):
config = self.default_config.copy()
config['default']['local_aliases'] = '_pingpong, _coffee'

self.write_entries("""20/01/2014
_pingpong 0800-0900 Play ping-pong
_coffee 0900-1000 Drink some coffee
""")

stdout = self.run_command('status', options=self.default_options)
self.assertIn(
"_pingpong (local) 1.00 Play ping-pong",
stdout
)
self.assertIn(
"_coffee (local) 1.00 Drink some coffee",
stdout
)
2 changes: 1 addition & 1 deletion tests/timesheet/test_timesheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def test_complete_timesheet(self):
"foo 1400-? ?"])

ignored_entries = t.get_ignored_entries()
self.assertEquals(len(ignored_entries), 1)
self.assertEquals(len(ignored_entries), 3)
self.assertEquals(len(ignored_entries[datetime.date(2012, 10, 12)]), 3)

t.continue_entry(datetime.date(2012, 10, 12), datetime.time(15, 12))
Expand Down