-
Notifications
You must be signed in to change notification settings - Fork 4
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
base: master
Are you sure you want to change the base?
Changes from all commits
0368ba9
41ba0cf
afe9a49
832481f
5515403
668d594
df30181
ed1d8ea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
||
|
@@ -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 | ||
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) | ||
|
@@ -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: | ||
|
@@ -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() | ||
|
||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice :) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How can it be sequential and ending with There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah ok, makes sense :) |
||
|
||
SortChildren = Item.__dict__['SortChildren'] | ||
|
||
|
@@ -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() | ||
|
@@ -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) | ||
|
@@ -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, | ||
|
@@ -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()) | ||
|
There was a problem hiding this comment.
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:
then I want the waiting to be blocking, isn't it?
Or maybe I do not understand your workflow :)
There was a problem hiding this comment.
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.