Exemplo n.º 1
0
    def save_to_tw(self):
        # This method persumes all the dependencies have been created at the
        # point it was called, hence move set the dependencies for the underlying
        # task. Remove dependencies for all other tasks within the viewport.

        # This happens so that the dependencies are rebuilt from the tree after
        # each save, since tasks may have been moved within the tree and dependencies
        # added / removed in this implicit manner.
        port = self.cache.get_viewport_by_task(self.task)
        if port is not None:
            self.task['depends'] -= set(port.viewport_tasks)

        self.task['depends'] |= set(s.task for s in self.add_dependencies)

        # Push the values to the Task only if the Vimwiki representation
        # somehow differs
        if self.task.modified or not self.uuid:
            self.task.save()

            # If task was first time saved now, add it to the cache and remove
            # the temporary reference
            if self.__unsaved_task is not None:
                self.uuid = ShortUUID(self.__unsaved_task['uuid'], self.tw)
                self.cache.task[self.uuid] = self.__unsaved_task
                self.__unsaved_task = None

            # If we saved the task, we need to update. Hooks may have changed data.
            self.update_from_task()
Exemplo n.º 2
0
    def task(self, task):
        # Make sure we're updating by a correct task
        if task['uuid'] != self.uuid:
            raise ValueError("Task '%s' with '%s' cannot be updated by "
                             "task with uuid '%s'." %
                             (self['description'], self.uuid, task['uuid']))

        self.uuid = ShortUUID(task['uuid'], self.tw)
Exemplo n.º 3
0
 def __init__(self, cache, uuid, tw):
     """
     Constructs a Vimwiki task from line at given position at the buffer
     """
     self.cache = cache
     self.tw = tw
     self.vim_data = dict(indent='', completed_mark=' ', line_number=None)
     self._buffer_data = None
     self.__unsaved_task = None
     self.uuid = ShortUUID(uuid, self.tw) if uuid is not None else None
Exemplo n.º 4
0
    def update_from_task(self):
        if not self.task.saved:
            return

        self.uuid = ShortUUID(self.task['uuid'], self.tw)
        self['completed_mark'] = self.get_completed_mark()
Exemplo n.º 5
0
class VimwikiTask(object):
    # Lists all data keys that are reflected in Vim representation
    buffer_keys = ('indent', 'description', 'uuid', 'completed_mark',
                   'line_number', 'priority', 'due')

    def __init__(self, cache, uuid, tw):
        """
        Constructs a Vimwiki task from line at given position at the buffer
        """
        self.cache = cache
        self.tw = tw
        self.vim_data = dict(indent='', completed_mark=' ', line_number=None)
        self._buffer_data = None
        self.__unsaved_task = None
        self.uuid = ShortUUID(uuid, self.tw) if uuid is not None else None

    def __getitem__(self, key):
        if key in self.vim_data.keys():
            return self.vim_data[key]
        else:
            return self.task[key]

    def __setitem__(self, key, value):
        if key in self.vim_data.keys():
            self.vim_data[key] = value
        else:
            self.task[key] = value

    @classmethod
    def find_closest(cls, cache):
        current_line = util.get_current_line_number()

        # Search lines in order: first all above, than all below
        line_numbers = itertools.chain(
            reversed(range(0, current_line + 1)),
            range(current_line + 1, len(cache.buffer)))

        for i in line_numbers:
            vwtask = cls.from_line(cache, i)
            if vwtask:
                return vwtask

    @classmethod
    def parse_line(cls, cache, number):
        return re.search(regexp.GENERIC_TASK, cache.buffer[number])

    @classmethod
    def from_line(cls, cache, number):
        """
        Creates a Vimwiki object from given line in the buffer.
          - If line does not contain a Vimwiki task, returns None.
        """
        # Protected access is ok here
        # pylint: disable=W0212

        match = cache.line[(cls, number)]

        if not match:
            return None

        tw = cache.warriors[match.group('source') or 'default']
        self = cls(cache, match.group('uuid'), tw)

        # Save vim-only related data
        self.vim_data.update({
            'indent': match.group('space'),
            'completed_mark': match.group('completed'),
            'line_number': number,
        })

        # Save task related data into Task object directly
        # Use data from the buffer to update the task only if we
        # explicitly stated that buffer has authority or if the task
        # being loaded is not saved in TW
        if cache.buffer_has_authority or not self.task.saved:
            if six.PY2:
                self.task['description'] = match.group('text').decode('utf-8')
            else:
                self.task['description'] = match.group('text')

            self.task['priority'] = convert_priority_to_tw_format(
                len(match.group('priority')
                    or []))  # This is either 0,1,2 or 3

            # Also make sure changes in the progress field are reflected
            if self['completed_mark'] is 'X':
                self.task['status'] = 'completed'
                self.task['start'] = None
                self.task['end'] = self.task['end'] or datetime.now()
            elif self['completed_mark'] is 'S':
                self.task['status'] = 'pending'
                self.task['start'] = self.task['start'] or datetime.now()
                self.task['end'] = None
            elif self['completed_mark'] == 'D':
                self.task['status'] = 'deleted'
                self.task['start'] = None
                self.task['end'] = self.task['end'] or datetime.now()
            elif self['completed_mark'] == ' ':
                self.task['status'] = "pending"
                self.task['start'] = None
                self.task['end'] = None
                self.task['wait'] = None

            # To get local time aware timestamp, we need to convert to
            # from local datetime to UTC time, since that is what
            # tasklib (correctly) uses
            due = match.group('due')
            if due:
                # With strptime, we get a native datetime object
                parsed_due = None

                try:
                    parsed_due = datetime.strptime(due, regexp.DATETIME_FORMAT)
                except ValueError:
                    try:
                        parsed_due = datetime.strptime(due, regexp.DATE_FORMAT)
                    except ValueError:
                        vim.command('echom "Taskwiki: Invalid timestamp '
                                    'on line %s, ignored."' %
                                    self['line_number'])

                # We need to interpret it as timezone aware object in user's
                # timezone, This properly handles DST and timezone offset.
                if parsed_due:
                    self.task['due'] = parsed_due

            # After all line-data parsing, save the data in the buffer
            self._buffer_data = {key: self[key] for key in self.buffer_keys}

        # We need to track depedency set in a extra attribute, since
        # this may be a new task, and hence it need not to be saved yet.
        # We circumvent this problem by iteration order in the TaskCache
        self.add_dependencies = set()

        self.parent = self.find_parent_task()

        # Make parent task dependant on this task
        if self.parent:
            self.parent.add_dependencies |= set([self])

        # For new tasks, apply defaults from above viewport
        if not self.uuid:
            self.apply_defaults()

            # If -- is in description, assume it's separator for metadata
            # * [ ] this is new task -- project:home
            # should turn into
            # * [ ] this is new task
            # with project:home applied

            if '--' in self['description']:
                first_part, second_part = self['description'].split('--', 1)

                new_description = first_part.strip()
                modstring = second_part.strip()

                # Convert the modstring
                modifications = util.tw_modstring_to_kwargs(modstring)
                for key in modifications.keys():
                    self[key] = modifications[key]

                # Apply the new description
                self['description'] = new_description

        return self

    @classmethod
    def from_task(cls, cache, task):
        self = cls(cache, task['uuid'], task.backend)
        self.update_from_task()

        return self

    @property
    def task(self):
        # New task object accessed second or later time
        if self.__unsaved_task is not None:
            return self.__unsaved_task

        # Return the corresponding task if alrady set
        # Else try to load it or create a new one
        if self.uuid:
            try:
                return self.cache.task[self.uuid]
            except Task.DoesNotExist:
                # Task with stale uuid, recreate
                self.__unsaved_task = Task(self.tw)
                # If task cannot be loaded, we need to remove the UUID
                vim.command(
                    'echom "UUID \'{0}\' not found, Task on line {1} will be '
                    're-created in TaskWarrior."'.format(
                        self.uuid, self['line_number'] + 1))
                self.uuid = None
        else:
            # New task object accessed first time
            self.__unsaved_task = Task(self.tw)

        return self.__unsaved_task

    @task.setter
    def task(self, task):
        # Make sure we're updating by a correct task
        if task['uuid'] != self.uuid:
            raise ValueError("Task '%s' with '%s' cannot be updated by "
                             "task with uuid '%s'." %
                             (self['description'], self.uuid, task['uuid']))

        self.uuid = ShortUUID(task['uuid'], self.tw)

    @property
    def priority_from_tw_format(self):
        return convert_priority_from_tw_format(self.task['priority'])

    @property
    def priority_to_tw_format(self):
        return convert_priority_to_tw_format(self['priority'])

    def save_to_tw(self):
        # This method persumes all the dependencies have been created at the
        # point it was called, hence move set the dependencies for the underlying
        # task. Remove dependencies for all other tasks within the viewport.

        # This happens so that the dependencies are rebuilt from the tree after
        # each save, since tasks may have been moved within the tree and dependencies
        # added / removed in this implicit manner.
        port = self.cache.get_viewport_by_task(self.task)
        if port is not None:
            self.task['depends'] -= set(port.viewport_tasks)

        self.task['depends'] |= set(s.task for s in self.add_dependencies)

        # Push the values to the Task only if the Vimwiki representation
        # somehow differs
        if self.task.modified or not self.uuid:
            self.task.save()

            # If task was first time saved now, add it to the cache and remove
            # the temporary reference
            if self.__unsaved_task is not None:
                self.uuid = ShortUUID(self.__unsaved_task['uuid'], self.tw)
                self.cache.task[self.uuid] = self.__unsaved_task
                self.__unsaved_task = None

            # If we saved the task, we need to update. Hooks may have changed data.
            self.update_from_task()

    def get_completed_mark(self):
        mark = self['completed_mark']

        if self.task.completed:
            mark = 'X'
        elif mark == 'X':
            mark = ' '

        if self.task.active:
            mark = 'S'
        elif mark == 'S':
            mark = ' '

        if self.task.deleted:
            mark = 'D'
        elif mark == 'D':
            mark = ' '

        if self.task.recurring:
            mark = 'R'
        elif mark == 'R':
            mark = ' '

        if self.task.waiting:
            mark = 'W'
        elif mark == 'W':
            mark = ' '

        return mark

    def update_from_task(self):
        if not self.task.saved:
            return

        self.uuid = ShortUUID(self.task['uuid'], self.tw)
        self['completed_mark'] = self.get_completed_mark()

    def update_in_buffer(self):
        # Look if any of the data that show up in Vim has changed
        buffer_data = {key: self[key] for key in self.buffer_keys}
        if self._buffer_data != buffer_data:
            # If so, update the line in vim and saved buffer data
            self.cache.buffer[self['line_number']] = str(self)
            self._buffer_data = buffer_data

    def __str__(self):
        due_str = ' ' + (self['due'].strftime(regexp.DATETIME_FORMAT)
                         if self['due'].time() else self['due'].strftime(
                             regexp.DATE_FORMAT)) if self['due'] else ''

        return ''.join([
            self['indent'],
            '* [',
            self['completed_mark'],
            '] ',
            (self['description'].encode('utf-8') if six.PY2 else
             self['description']) if self['description'] else 'TEXT MISSING?',
            ' ' +
            '!' * self.priority_from_tw_format if self['priority'] else '',
            due_str,
            '  #' +
            self.uuid.vim_representation(self.cache) if self.uuid else '',
        ])

    def find_parent_task(self):
        # If this task is not indented, we have nothing to do here
        if not self['indent']:
            return None

        for i in reversed(range(0, self['line_number'])):
            # Stop looking if there is less indentation
            if not self.cache.buffer[i].startswith(self['indent']):
                # The from_line constructor returns None if line doesn't match a task
                line = self.cache.line[(VimwikiTask, i)]
                if line:
                    return self.cache.vwtask[i]
                else:
                    return None

    def apply_defaults(self):
        from taskwiki import viewport

        for i in reversed(range(0, self['line_number'])):
            port = viewport.ViewPort.from_line(i, self.cache)
            if port:
                # The task should have the same source as the viewport has
                self.tw = port.tw
                self.task.backend = port.tw

                # Any defaults specified should be inherited
                if port.defaults:
                    for key in port.defaults.keys():
                        self[key] = port.defaults[key]

                # If port was detected, break the search
                break

            # If line matches any header that is not a viewport,
            # break the search too
            line = self.cache.buffer[i]
            if re.match(regexp.GENERIC_HEADER, line):
                break