X-Git-Url: https://git.madduck.net/etc/taskwarrior.git/blobdiff_plain/72a0095b972ed123f89667af80e112f8135222c2..98a9ea60e0c22e140650ff5a89dfd544d9c05cb0:/tasklib/task.py diff --git a/tasklib/task.py b/tasklib/task.py index c72a9b3..170fc77 100644 --- a/tasklib/task.py +++ b/tasklib/task.py @@ -71,7 +71,7 @@ class TaskAnnotation(TaskResource): class Task(TaskResource): - read_only_fields = ['id', 'entry', 'urgency'] + read_only_fields = ['id', 'entry', 'urgency', 'uuid'] class DoesNotExist(Exception): pass @@ -88,9 +88,20 @@ class Task(TaskResource): """ pass - def __init__(self, warrior, data={}): + class NotSaved(Exception): + """ + Raised when the operation cannot be performed on the task, because + it has not been saved to TaskWarrior yet. + """ + pass + + def __init__(self, warrior, data={}, **kwargs): self.warrior = warrior - self._load_data(data) + + # We keep data for backwards compatibility + kwargs.update(data) + + self._load_data(kwargs) self._modified_fields = set() def __unicode__(self): @@ -112,6 +123,10 @@ class Task(TaskResource): def pending(self): return self['status'] == six.text_type('pending') + @property + def saved(self): + return self['uuid'] is not None or self['id'] is not None + def serialize_due(self, date): return date.strftime(DATE_FORMAT) @@ -132,13 +147,16 @@ class Task(TaskResource): return ','.join(tags) if tags else '' def delete(self): + if not self.saved: + raise self.NotSaved("Task needs to be saved before it can be deleted") + # Refresh the status, and raise exception if the task is deleted self.refresh(only_fields=['status']) if self.deleted: raise self.DeletedTask("Task was already deleted") - self.warrior.execute_command([self['id'], 'delete'], config_override={ + self.warrior.execute_command([self['uuid'], 'delete'], config_override={ 'confirmation': 'no', }) @@ -147,6 +165,9 @@ class Task(TaskResource): def done(self): + if not self.saved: + raise self.NotSaved("Task needs to be saved before it can be completed") + # Refresh, and raise exception if task is already completed/deleted self.refresh(only_fields=['status']) @@ -155,37 +176,75 @@ class Task(TaskResource): elif self.deleted: raise self.DeletedTask("Deleted task cannot be completed") - self.warrior.execute_command([self['id'], 'done']) + self.warrior.execute_command([self['uuid'], 'done']) # Refresh the status again, so that we have updated info stored self.refresh(only_fields=['status']) def save(self): - args = [self['id'], 'modify'] if self['id'] else ['add'] + args = [self['uuid'], 'modify'] if self.saved else ['add'] args.extend(self._get_modified_fields_as_args()) - self.warrior.execute_command(args) + output = self.warrior.execute_command(args) + + # Parse out the new ID, if the task is being added for the first time + if not self.saved: + id_lines = [l for l in output if l.startswith('Created task ')] + + # Complain loudly if it seems that more tasks were created + # Should not happen + if len(id_lines) != 1 or len(id_lines[0].split(' ')) != 3: + raise TaskWarriorException("Unexpected output when creating " + "task: %s" % '\n'.join(id_lines)) + + # Circumvent the ID storage, since ID is considered read-only + self._data['id'] = int(id_lines[0].split(' ')[2].rstrip('.')) + self._modified_fields.clear() + self.refresh() def add_annotation(self, annotation): - args = [self['id'], 'annotate', annotation] + if not self.saved: + raise self.NotSaved("Task needs to be saved to add annotation") + + args = [self['uuid'], 'annotate', annotation] self.warrior.execute_command(args) self.refresh(only_fields=['annotations']) def remove_annotation(self, annotation): + if not self.saved: + raise self.NotSaved("Task needs to be saved to add annotation") + if isinstance(annotation, TaskAnnotation): annotation = annotation['description'] - args = [self['id'], 'denotate', annotation] + args = [self['uuid'], 'denotate', annotation] self.warrior.execute_command(args) self.refresh(only_fields=['annotations']) def _get_modified_fields_as_args(self): args = [] - for field in self._modified_fields: - args.append('{}:{}'.format(field, self._data[field])) + + # If we're modifying saved task, simply pass on all modified fields + if self.saved: + for field in self._modified_fields: + args.append('{0}:{1}'.format(field, self._data[field])) + # For new tasks, pass all fields that make sense + else: + for field in self._data.keys(): + if field in self.read_only_fields: + continue + args.append('{0}:{1}'.format(field, self._data[field])) + return args def refresh(self, only_fields=[]): - args = [self['uuid'], 'export'] + # Raise error when trying to refresh a task that has not been saved + if not self.saved: + raise self.NotSaved("Task needs to be saved to be refreshed") + + # We need to use ID as backup for uuid here for the refreshes + # of newly saved tasks. Any other place in the code is fine + # with using UUID only. + args = [self['uuid'] or self['id'], 'export'] new_data = json.loads(self.warrior.execute_command(args)[0]) if only_fields: to_update = dict(