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

Added waiting, future and skipped projects #1

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ Importance is determined by:
2. Due date
3. Order in the list

`@next_action` waterfalls into indented regions. If the top level task that is selected to receive the `@next_action` label has subtasks, the same algorithm is used. The `@next_action` label is only applied to one task.
`@next_action` waterfalls into indented regions. If the top level task that is selected to receive the `@next_action` label has subtasks, the same algorithm is used. The `@next_action` label is only applied to one task. Tasks labeled @waiting or @future are skipped.

Parallel list processing
------
If a list name ends with `=`, the top level of tasks will be treated as parallel `@next_action`s.
The waterfall processing will be applied the same way as sequential lists - every parent task will be treated as sequential. This can be overridden by appending `=` to the name of the parent task.

To skip projects, append a '*' to the end of the name.
104 changes: 75 additions & 29 deletions nextaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,35 @@
import urllib2

API_TOKEN = 'API TOKEN HERE'
NEXT_ACTION_LABEL = u'next_action'
NEXT_ACTION_LABEL = u'nextAction'
WAITING_LABEL = u'waiting'
FUTURE_LABEL = u'future'
SOMEDAY_LABEL = "Someday"

LIST_PREFIX = 'List - '
SEQUENTIAL_POSTFIX = u'--'
PARALLEL_POSTFIX = u'='
# Will remove next_action label within projects with skip_postfix. For tasks set @waiting label to skip next_action label on subtasks
SKIP_POSTFIX = u'*'
TODOIST_VERSION = '5.3'

class TraversalState(object):
"""Simple class to contain the state of the item tree traversal."""
def __init__(self, next_action_label_id):
def __init__(self, next_action_label_id, waiting_label_id, future_label_id):
self.remove_labels = []
self.add_labels = []
self.found_next_action = False
self.next_action_label_id = next_action_label_id
self.waiting_label_id = waiting_label_id
self.future_label_id = future_label_id

def clone(self):
"""Perform a simple clone of this state object.

For parallel traversals it's necessary to produce copies so that every
traversal to a lower node has the same found_next_action status.
"""
t = TraversalState(self.next_action_label_id)
t = TraversalState(self.next_action_label_id, self.waiting_label_id, self.future_label_id)
t.found_next_action = self.found_next_action
return t

Expand Down Expand Up @@ -61,13 +72,20 @@ def __init__(self, initial_data):
self.due_date_utc = datetime.datetime(2100, 1, 1, tzinfo=dateutil.tz.tzutc())

def GetItemMods(self, state):
# recure
if self.IsSequential():
self._SequentialItemMods(state)
elif self.IsParallel():
self._ParallelItemMods(state)
if not state.found_next_action and not self.checked:
# what?
if not state.found_next_action and not self.checked and not state.future_label_id in self.labels:
state.found_next_action = True
if not state.next_action_label_id in self.labels:
# say we are done, but don't set next action label if waiting: if sequential task then skip setting next non-waiting task to next-action if above is waiting

Choose a reason for hiding this comment

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

To me it is closer to GTD that in sequential projects, @waiting is blocking.
if the tasks of my project are:

  • waits for Someone answer
  • forward his answer to SomeoneElse

then I want the waiting to be blocking, isn't it?

Or maybe I do not understand your workflow :)

Copy link
Author

Choose a reason for hiding this comment

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

That's a very interesting point... my waiting tasks aren't always blockers and I've given them the benefit of the doubt here.

As I look at my project list, I'm almost sold on your setup. I have to modify some task lists but good call out.

Thanks, I'm going to create an issue on my repo for this.

if state.waiting_label_id in self.labels:
logging.debug('waiting: item "%s"', self.content)
if state.next_action_label_id in self.labels:
state.remove_labels.append(self)
elif not state.next_action_label_id in self.labels:
state.add_labels.append(self)
elif state.next_action_label_id in self.labels:
state.remove_labels.append(self)
Expand All @@ -87,6 +105,7 @@ def GetLabelRemovalMods(self, state):
def _SequentialItemMods(self, state):
"""
Iterate over every child, walking down the tree.
Iterate in the sortorder Priority > list order
If none of our children are the next action, check if we are.
"""
for item in self.children:
Expand All @@ -104,17 +123,21 @@ def _ParallelItemMods(self, state):
item.GetItemMods(temp_state)
state.merge(temp_state)

def IsWaiting(self):
return self.waiting_label_id in self.labels

# Tasks are be default sequential, hence say its sequential if task name does not end in =
def IsSequential(self):
return not self.content.endswith('=')
#if self.content.endswith('--') or self.content.endswith('='):
# return self.content.endswith('--')
return not self.content.endswith(PARALLEL_POSTFIX)
#if self.content.endswith(SEQUENTIAL_POSTFIX) or self.content.endswith(PARALLEL_POSTFIX):
# return self.content.endswith(SEQUENTIAL_POSTFIX)
#else:
# return self.parent.IsSequential()

def IsParallel(self):
return self.content.endswith('=')
#if self.content.endswith('--') or self.content.endswith('='):
# return self.content.endswith('=')
return self.content.endswith(PARALLEL_POSTFIX)
#if self.content.endswith(SEQUENTIAL_POSTFIX) or self.content.endswith(PARALLEL_POSTFIX):
# return self.content.endswith(PARALLEL_POSTFIX)
#else:
# return self.parent.IsParallel()

Expand Down Expand Up @@ -164,22 +187,17 @@ def subProjects(self):
return self._subProjects

def IsIgnored(self):
return self.name.startswith('Someday') or self.name.startswith('List - ')
return self.name.endswith(SKIP_POSTFIX) or self.name.startswith(LIST_PREFIX) or (self.name == SOMEDAY_LABEL)

def IsSequential(self):
ignored = self.IsIgnored()
endsWithEqual = self.name.endswith('=')
endsWithSequential = self.name.endswith(SEQUENTIAL_POSTFIX)

Choose a reason for hiding this comment

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

Nice :)
I was too lazy to do that :P

Copy link
Author

Choose a reason for hiding this comment

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

thanks! I was going to pull these into a config file... maybe later.

validParent = self.parent == None or not self.parent.IsIgnored()
seq = ((not ignored) and (not endsWithEqual)) and validParent
# if self.name .startsWith('Payer Camille'):
# print startsWithKeyword
# print endsWithEqual
# print parentSequential
# print seq
seq = ((not ignored) and (not endsWithSequential)) and validParent
return seq

def IsParallel(self):
return self.name.endswith('=')
return not (self.name.endswith(SKIP_POSTFIX) or self.IsSequential())

Choose a reason for hiding this comment

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

How can it be sequential and ending with =?

Copy link
Author

Choose a reason for hiding this comment

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

I'm not sure I understand. If's it's sequential, then it isn't parallel and if it's skipped it's not parallel either.

Choose a reason for hiding this comment

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

ah ok, makes sense :)


SortChildren = Item.__dict__['SortChildren']

Expand Down Expand Up @@ -240,12 +258,24 @@ def _SetLabelData(self, label_data):
# Store label data - we need this to set the next_action label.
self._labels_timestamp = label_data['DayOrdersTimestamp']
self._next_action_id = None
self._waiting_id = None
self._future_id = None
for label in label_data['Labels'].values():
if label['name'] == NEXT_ACTION_LABEL:
self._next_action_id = label['id']
logging.info('Found next_action label, id: %s', label['id'])
if label['name'] == WAITING_LABEL:
self._waiting_id = label['id']
logging.info('Found waiting label, id: %s', label['id'])
if label['name'] == FUTURE_LABEL:
self._future_id = label['id']
logging.info('Found future label, id: %s', label['id'])
if self._next_action_id == None:
logging.warning('Failed to find next_action label, need to create it.')
if self._waiting_id == None:
logging.warning('Failed to find waiting label, next_action will be set even on waiting tasks.')
if self._future_id == None:
logging.warning('Failed to find future label, next_action will be set even on future tasks.')

def GetSyncState(self):
project_timestamps = dict()
Expand Down Expand Up @@ -306,7 +336,7 @@ def GetProjectMods(self):
logging.info("Adding next_action label")
return mods
for project in self._projects.itervalues():
state = TraversalState(self._next_action_id)
state = TraversalState(self._next_action_id, self._waiting_id, self._future_id)
project.GetItemMods(state)
if len(state.add_labels) > 0 or len(state.remove_labels) > 0:
logging.info("For project %s, the following mods:", project.name)
Expand All @@ -333,29 +363,41 @@ def GetProjectMods(self):
logging.info("remove next_action from: %s", item.content)
return mods


def urlopen(req):
try:
return urllib2.urlopen(req)
except urllib2.HTTPError, e:
logging.info('HTTPError = ' + str(e.code))
except urllib2.URLError, e:
logging.info('URLError = ' + str(e.reason))
except httplib.HTTPException, e:
logging.info('HTTPException')
except Exception:
import traceback
logging.info('generic exception: ' + traceback.format_exc())
return None

def GetResponse():
values = {'api_token': API_TOKEN, 'resource_types': ['labels']}
data = urllib.urlencode(values)
req = urllib2.Request('https://api.todoist.com/TodoistSync/v' + TODOIST_VERSION + '/get', data)
return urllib2.urlopen(req)
return urlopen(req)

def GetLabels():
req = urllib2.Request('https://todoist.com/API/getLabels?token=' + API_TOKEN)
return urllib2.urlopen(req)
return urlopen(req)

def GetProjects():
req = urllib2.Request('https://todoist.com/API/getProjects?token=' + API_TOKEN)
return urllib2.urlopen(req)
return urlopen(req)

def DoSync(items_to_sync):
values = {'api_token': API_TOKEN,
'items_to_sync': json.dumps(items_to_sync)}
logging.info("posting %s", values)
data = urllib.urlencode(values)
req = urllib2.Request('https://api.todoist.com/TodoistSync/v' + TODOIST_VERSION + '/sync', data)
return urllib2.urlopen(req)
return urlopen(req)

def DoSyncAndGetUpdated(items_to_sync, sync_state):
values = {'api_token': API_TOKEN,
Expand All @@ -365,12 +407,16 @@ def DoSyncAndGetUpdated(items_to_sync, sync_state):
logging.debug("posting %s", values)
data = urllib.urlencode(values)
req = urllib2.Request('https://api.todoist.com/TodoistSync/v' + TODOIST_VERSION + '/syncAndGetUpdated', data)
return urllib2.urlopen(req)
return urlopen(req)

def main():
logging.basicConfig(level=logging.INFO)
logging.basicConfig(level=logging.DEBUG)
response = GetResponse()
json_data = json.loads(response.read())
if response == None:
logging.error("Failed to retrieve Todoist data")
else:
json_data = json.loads(response.read())
logging.debug("Got inital data: %s", json_data)
while True:
response = GetLabels()
json_data['Labels'] = json.loads(response.read())
Expand Down