<gh_stars>1-10
import pprint
import datetime
import re
import logging

from copy import copy

from itertools import zip_longest

from schedules_tools import SchedulesToolsException

from .utils import sort_tasks, slugify

log = logging.getLogger(__name__)
re_flags_separator = re.compile(r'[, ]+')

ATTR_PREFIX_FLAG = 'Flags'
ATTR_PREFIX_LINK = 'Link'
ATTR_PREFIX_NOTE = 'Note'

TASK_SLUG_NUMERATION_LIMIT = 9999
TASK_SLUG_CHAR_LIMIT = 255
TASK_SLUG_TRUNCATION_LIMIT = TASK_SLUG_CHAR_LIMIT - (1 + len(str(TASK_SLUG_NUMERATION_LIMIT)))


class Task(object):
    index = ''
    slug = ''
    name = ''
    note = ''
    priority = 500
    dStart = datetime.datetime.max
    dFinish = datetime.datetime.min
    duration = None
    milestone = False
    p_complete = 0.0
    flags = []
    level = 1
    link = ''

    tasks = []

    resource = None

    _date_format = '%Y-%m-%dT%H:%M:%S'
    _rx = None
    _schedule = None

    def __init__(self, schedule=None, level=1):
        self.tasks = []
        self.flags = []
        self._schedule = schedule
        self.p_complete = 0.0
        self.priority = 500
        self.milestone = False
        self.level = level
        self._subtree_hash_cache = {}

    def __str__(self):
        return f'{self.slug} {self.name} MS:{self.milestone}  ({self.dStart} - {self.dFinish})  ' \
               f'F{self.flags}  [{len(self.tasks)}]'

    def parse_extended_attr(self, value, key=None):
        """
        According to given value it will guess if it is flag, link or note
        definition and fill proper task attribute.

        Args:
            value: string to parse, or set attribute specified by 'key' arg
            key: (optional) set value to task attribute according to this key
        """
        if not value:
            return

        if key:
            val = value
        elif hasattr(value, 'split'):
            # test value for flag / note / link
            pieces = value.split(':', 1)
            if len(pieces) != 2:
                # it's not a string in format 'Flag: qe, dev' - don't process
                return

            key, val = pieces
            key = key.strip().lower()
            val = val.strip()

        else:
            return

        if key.lower() == ATTR_PREFIX_FLAG.lower():
            val = str(val).lower()
            flags = re_flags_separator.split(val)
            for flag in flags:
                flag = str(flag.strip())
                if flag:
                    self.flags.append(flag)
            self._schedule.used_flags |= set(self.flags)

        elif key.lower() == ATTR_PREFIX_LINK.lower():
            self.link = str(val)

        elif key.lower() == ATTR_PREFIX_NOTE.lower():
            # in case of multiple notes - concatenate
            self.note = ' '.join([self.note, val]).lstrip()

        else:
            log.info(f'Extended attr "{key}" wasn\'t recognized.')

    @staticmethod
    def _workaround_it_phase_names(eTask):
        """Corrects phase names"""
        name_map = {'Concept': 'Concept Phase',
                    'Planning': 'Planning Phase',
                    'Develop': 'Development Phase',
                    'Test': 'Testing Phase',
                    'Launch': 'Launch Phase',
                    'Maintenance': 'Maintenance Phase',
                    }

        eName_list = eTask.xpath('Name')
        if eName_list:
            name = eName_list[0].text
            if name:
                name = name.strip()

            level = int(eTask.xpath('OutlineLevel')[0].text)

            if level in [1, 2] and name in name_map.keys():
                # check that there is no full named task else in the schedule
                root = eTask.getroottree()
                eFullnamed_task_list = root.xpath('//Task[Name = "%s"]' % name_map[name])

                if not eFullnamed_task_list:  # if not - map the name
                    return name_map[name]
            return name
        return ''

    @property
    def desc_tasks_count(self):
        count = len(self.tasks)
        for task in self.tasks:
            count += task.desc_tasks_count
        return count

    def get_type(self, check_same_start_finish=False):
        if self.tasks:
            return 'Container'
        elif self.milestone and (not check_same_start_finish or self.dStart == self.dFinish):
            return 'Milestone'
        else:
            return 'Task'

    def dump_as_dict(self, recursive=True):
        attrs = copy(vars(self))
        # avoid infinite looping schedule > task > schedule ...
        exclude = ['_schedule', '_subtree_hash_cache']

        if recursive:
            attrs['tasks'] = []
            for task in self.tasks:
                attrs['tasks'].append(task.dump_as_dict())
        else:
            exclude.append('tasks')

        [attrs.pop(item) for item in exclude if item in attrs]

        return attrs

    @classmethod
    def load_from_dict(cls, input_dict, schedule):
        task = cls(schedule)

        for key, val in input_dict.items():
            task.__setattr__(key, val)

        task.tasks = []
        for new_task in input_dict.get('tasks', []):
            task.tasks.append(Task.load_from_dict(new_task, schedule))

        return task

    def get_subtree_hash(self, fields):
        if not isinstance(fields, tuple):
            fields = tuple(fields)

        if fields not in self._subtree_hash_cache:
            if self.tasks:
                hashes = []
                for child_task in self.tasks:
                    task_hash_values = []
                    for attr_name in fields:
                        attr_value = getattr(child_task, attr_name)

                        if isinstance(attr_value, list):
                            attr_value = sorted(attr_value)

                        task_hash_values.append(str(attr_value))

                    hashes.append(
                        '%s%s' % (
                            ''.join(task_hash_values),
                            child_task.get_subtree_hash(fields)
                        )
                    )

                subtree_hash = ''.join(hashes)

            else:
                subtree_hash = ''

            self._subtree_hash_cache[fields] = subtree_hash

        return self._subtree_hash_cache[fields]

    def get_slug_key(self, prefix=None):
        """
        Generates slug key only for the task itself.
        Doesn't include parent tasks slugs i.e. slug key is not a full slug path to the task.
        """
        slug_key = slugify(self.name)

        if prefix is not None:
            return '.'.join((prefix, slug_key))
        return slug_key


class Schedule(object):
    slug = ''
    name = ''  # Product 1.2
    tasks = []
    dStart = None
    dFinish = None
    changelog = {}
    mtime = None

    resources = {}
    assignments = []
    used_flags = None
    ext_attr = {}
    flags_attr_id = None
    id_reg = set()

    _eTask_list = []
    _task_index = 1
    _taskname_flat_registry = None

    errors_import = []

    def __init__(self):
        self.tasks = []
        self.used_flags = set()
        self.changelog = {}
        self.ext_attr = {}
        self.unique_id_re = re.compile(r'[\W_]+')
        self.errors_import = []
        self.mtime = None
        self.tasks_slugs = None

    def make_flat(self):
        '''Convert tasks structure to flat list'''
        def add_tasks_to_list(tasks, tasks_list):
            for task in tasks:
                subtasks = copy(task.tasks)
                task.tasks = []
                tasks_list.append(task)
                add_tasks_to_list(subtasks, tasks_list)

        flat_tasks = []
        add_tasks_to_list(self.tasks, flat_tasks)
        self.tasks = flat_tasks

    def filter_milestones(self):
        '''Keep only tasks that are milestones or contain children that are'''
        def filter_tasks(tasks):
            tasks_list = []
            for task in tasks:
                filtered_subtasks = filter_tasks(task.tasks)

                logmsg_fmt = 'REMOVE: {} {}'

                if task.milestone or filtered_subtasks:
                    task.tasks = filtered_subtasks
                    tasks_list.append(task)
                    logmsg_fmt = 'ADD: {} {}'

                log.info(logmsg_fmt.format(task.name, task.milestone))

            return tasks_list

        self.tasks = filter_tasks(self.tasks)

    def filter_flags(self, show=None, hide=None):
        '''
        Keep only tasks that match flag conditions or contain children tha match

        Args:
            show: list of flags to show
            hide: list of flags to hide
        '''

        def filter_tasks(tasks):
            tasks_list = []
            for task in tasks:
                # see if there are any matching children
                filtered_subtasks = filter_tasks(task.tasks)

                # Add to result if not hidden and (has subtasks or fits show)
                logmsg_fmt = 'REMOVE: {} {}'
                if (not (set(task.flags) & set(hide))
                    and (filtered_subtasks
                         or (set(task.flags) & set(show))
                         or not show)):
                    task.tasks = filtered_subtasks
                    tasks_list.append(task)

                    logmsg_fmt = 'ADD: {} {}'

                    self.used_flags.update(task.flags)

                log.info(logmsg_fmt.format(task.name, task.flags))

            return tasks_list

        if show is None:
            show = []
        if hide is None:
            hide = []

        if show or hide:
            self.used_flags = set()

            log.info('FLAG filter: SHOW {}   HIDE {}'.format(show, hide))

            self.tasks = filter_tasks(self.tasks)

    def sort_tasks(self, field):
        self.tasks = sort_tasks(self.tasks, field)

    def check_top_task(self):
        """ Add single root task if multiple """

        def _raise_index(task):
            task.index = int(task.index) + 1
            for t in task.tasks:
                _raise_index(t)

        if len(self.tasks) <= 1:
            return

        # need one top task
        top_task = Task(self)
        top_task.index = 1
        # top_task.name = '%s %s' % (self.name, self.version)
        # removed version as per BZ#1396303 - see how that works
        top_task.name = self.name
        top_task.slug = self.slug

        for task in self.tasks:
            if top_task.dStart:
                top_task.dStart = min(top_task.dStart,
                                      task.dStart)
            else:
                top_task.dStart = task.dStart

            if top_task.dFinish:
                top_task.dFinish = max(top_task.dFinish,
                                       task.dFinish)
            else:
                top_task.dFinish = task.dFinish

            _raise_index(task)
            top_task.tasks.append(task)

        self.tasks = [top_task]

    def generate_slugs(self):
        def gen_slugs_recursive(tasks, slug_prefix=None):
            for task in tasks:
                task_slug = task.get_slug_key(slug_prefix)

                if len(task_slug) > TASK_SLUG_TRUNCATION_LIMIT:
                    task_slug = task_slug[:TASK_SLUG_TRUNCATION_LIMIT]

                if task_slug in self.tasks_slugs:
                    for i in range(2, TASK_SLUG_NUMERATION_LIMIT + 1):
                        task_slug_numerated = '%s_%d' % (task_slug, i)
                        if task_slug_numerated not in self.tasks_slugs:
                            final_task_slug = task_slug_numerated
                            break
                        elif i == TASK_SLUG_NUMERATION_LIMIT:
                            raise SchedulesToolsException('Task slug generation error: '
                                                          'reached numeration limit')
                else:
                    final_task_slug = task_slug

                task.slug = final_task_slug
                self.tasks_slugs.add(task.slug)
                gen_slugs_recursive(task.tasks, task.slug)

        self.tasks_slugs = set()
        gen_slugs_recursive(self.tasks)

    def print_tasks(self, tasks=None, level=0):
        if tasks is None:
            tasks = self.tasks
        for task in tasks:
            print(level * '  ', str(task))
            if task.tasks:
                self.print_tasks(task.tasks, level + 1)

    @staticmethod
    def _diff_task_attrs(left, right, attrs, whole_days=False):
        diff = {}

        for a in attrs:
            left_val = left.__getattribute__(a)
            right_val = right.__getattribute__(a)
            if left_val != right_val:
                try:
                    if whole_days and \
                                    left_val.day == right_val.day and \
                                    left_val.month == right_val.month and \
                                    left_val.year == right_val.year:
                        # When we compare just whole days, we don't mind
                        # to have diff in time
                        continue
                except AttributeError:
                    pass

                diff[a] = (str(left_val), str(right_val))
        return diff

    def _diff_tasks(self, left, right, attrs=None, whole_days=False):
        default_attrs = ['name', 'dStart', 'dFinish', 'link', 'note']
        if not attrs:
            attrs = default_attrs
        ret = ''
        missing_str = ('{}: Some phase upwards is missing to compare with '
                       'this one on the {} side.\n')

        for left, right in zip_longest(left, right):
            if not left:
                ret += missing_str.format(right.name, 'LEFT')
                break
            if not right:
                ret += missing_str.format(left.name, 'RIGHT')
                break

            d = self._diff_task_attrs(left, right, attrs, whole_days)
            if len(d):
                name_warning = ''
                if 'name' in d.keys():
                    name_warning = (' <--- Name of the phase doesn\'t match. '
                                    'Missing phase?')
                # width=1 force wrap tuple to newline
                ret += '{}:{}\n{}\n'.format(
                    left.name,
                    name_warning,
                    pprint.pformat(d, width=1)
                )

            ret += self._diff_tasks(left.tasks, right.tasks, attrs, whole_days)
        return ret

    def _diff_schedule_attrs(self, schedule, whole_days=False):
        attrs = ['name', 'changelog', 'dStart', 'dFinish', 'assignments', 'used_flags']
        ret = ''
        diff = dict()

        for attr in attrs:
            self_s = self.__getattribute__(attr)
            schedule_s = schedule.__getattribute__(attr)

            if self_s != schedule_s:
                try:
                    if whole_days and \
                                    self_s.day == schedule_s.day and \
                                    self_s.month == schedule_s.month and \
                                    self_s.year == schedule_s.year:
                        # When we compare just whole days, we don't mind
                        # to have diff in time
                        continue
                except AttributeError:
                    pass
                diff[attr] = (str(self_s), str(schedule_s))

        if len(diff):
            # width=1 force wrap tuple to newline
            ret += 'Schedule attrs:\n'
            ret += pprint.pformat(diff, width=1)
        return ret

    def _diff_schedule_version(self, schedule):
        attrs = self._version.keys()
        ret = ''
        diff = dict()

        for attr in attrs:
            self_s = self._version[attr]
            schedule_s = self._version[attr]

            if self_s != schedule_s:
                diff[attr] = (self_s, schedule_s)

        if len(diff):
            # width=1 force wrap tuple to newline
            ret += 'Schedule version:\n'
            ret += pprint.pformat(diff, width=1)

        return ret

    def diff(self, schedule, attrs=None, whole_days=False):
        """
        Really simple diff to match two schedules.

        @param schedule Instance to another schedule to figure out differences
        @param attrs Task attributes to check
        @param whole_days Flag to ignore time differences of dates
        """
        ret = ''
        diff_tasks = self._diff_tasks(self.tasks, schedule.tasks, attrs,
                                      whole_days)
        if diff_tasks:
            ret += 'Tasks attrs:\n'
            ret += diff_tasks
        ret += self._diff_schedule_attrs(schedule, whole_days=whole_days)

        return ret.strip()

    def slugify_str(self, orig_str):
        return self.unique_id_re.sub('_', orig_str.lower())

    def dump_as_dict(self):
        schedule = copy(vars(self))

        schedule['tasks'] = []
        for task in self.tasks:
            schedule['tasks'].append(task.dump_as_dict())

        # set() is not serializable into json
        schedule['used_flags'] = sorted(list(self.used_flags))
        return schedule

    @classmethod
    def load_from_dict(cls, input_dict):
        # This will preserve reference to original class attributes
        schedule = cls()
        for key, val in input_dict.items():
            schedule.__setattr__(key, val)

        schedule.tasks = []
        for task in input_dict['tasks']:
            schedule.tasks.append(Task.load_from_dict(task, schedule))

        schedule.used_flags = set(input_dict['used_flags'])

        return schedule

    def _build_tasks_flat_registry(self, tasks_param):
        if self._taskname_flat_registry is None:
            self._taskname_flat_registry = set()

        for task in tasks_param:
            self._taskname_flat_registry.add(task.name)
            self._build_tasks_flat_registry(task.tasks)

    def check_for_taskname(self, tasks):
        """
        Method consume list of task names to check their existence specified
        as dict
        Args:
            tasks: dict specification of task names to check. Example:
            {'exactTaskname': False, 'matchBeginningTaskname': True}

        Returns:
            Set of task names, that haven't been found in schedule.

        """
        if not isinstance(tasks, dict):
            raise Exception(
                "'tasks' argument has to be dict, for example: {'task1': False,"
                " 'matchBeginningTaskname': True}")

        if not self._taskname_flat_registry:
            self._build_tasks_flat_registry(self.tasks)

        missing_tasks = set()
        for task_name, start_with_flag in tasks.items():
            if start_with_flag:
                re_task_str = re.escape(task_name)
                re_task = re.compile(re_task_str + '.*')
                found = False

                for task in self._taskname_flat_registry:
                    if re_task.match(task):
                        found = True
                        break
                if not found:
                    missing_tasks.add(task_name)
            elif task_name not in self._taskname_flat_registry:
                missing_tasks.add(task_name)

        return missing_tasks

import pprint
import datetime
import re
import logging

from copy import copy

from itertools import zip_longest

from schedules_tools import SchedulesToolsException

from .utils import sort_tasks, slugify

log = logging.getLogger(__name__)
re_flags_separator = re.compile(r'[, ]+')

ATTR_PREFIX_FLAG = 'Flags'
ATTR_PREFIX_LINK = 'Link'
ATTR_PREFIX_NOTE = 'Note'

TASK_SLUG_NUMERATION_LIMIT = 9999
TASK_SLUG_CHAR_LIMIT = 255
TASK_SLUG_TRUNCATION_LIMIT = TASK_SLUG_CHAR_LIMIT - (1 + len(str(TASK_SLUG_NUMERATION_LIMIT)))


class Task(object):
    index = ''
    slug = ''
    name = ''
    note = ''
    priority = 500
    dStart = datetime.datetime.max
    dFinish = datetime.datetime.min
    duration = None
    milestone = False
    p_complete = 0.0
    flags = []
    level = 1
    link = ''

    tasks = []

    resource = None

    _date_format = '%Y-%m-%dT%H:%M:%S'
    _rx = None
    _schedule = None

    def __init__(self, schedule=None, level=1):
        self.tasks = []
        self.flags = []
        self._schedule = schedule
        self.p_complete = 0.0
        self.priority = 500
        self.milestone = False
        self.level = level
        self._subtree_hash_cache = {}

    def __str__(self):
        return f'{self.slug} {self.name} MS:{self.milestone}  ({self.dStart} - {self.dFinish})  ' \
               f'F{self.flags}  [{len(self.tasks)}]'

    def parse_extended_attr(self, value, key=None):
        """
        According to given value it will guess if it is flag, link or note
        definition and fill proper task attribute.

        Args:
            value: string to parse, or set attribute specified by 'key' arg
            key: (optional) set value to task attribute according to this key
        """
        if not value:
            return

        if key:
            val = value
        elif hasattr(value, 'split'):
            # test value for flag / note / link
            pieces = value.split(':', 1)
            if len(pieces) != 2:
                # it's not a string in format 'Flag: qe, dev' - don't process
                return

            key, val = pieces
            key = key.strip().lower()
            val = val.strip()

        else:
            return

        if key.lower() == ATTR_PREFIX_FLAG.lower():
            val = str(val).lower()
            flags = re_flags_separator.split(val)
            for flag in flags:
                flag = str(flag.strip())
                if flag:
                    self.flags.append(flag)
            self._schedule.used_flags |= set(self.flags)

        elif key.lower() == ATTR_PREFIX_LINK.lower():
            self.link = str(val)

        elif key.lower() == ATTR_PREFIX_NOTE.lower():
            # in case of multiple notes - concatenate
            self.note = ' '.join([self.note, val]).lstrip()

        else:
            log.info(f'Extended attr "{key}" wasn\'t recognized.')

    @staticmethod
    def _workaround_it_phase_names(eTask):
        """Corrects phase names"""
        name_map = {'Concept': 'Concept Phase',
                    'Planning': 'Planning Phase',
                    'Develop': 'Development Phase',
                    'Test': 'Testing Phase',
                    'Launch': 'Launch Phase',
                    'Maintenance': 'Maintenance Phase',
                    }

        eName_list = eTask.xpath('Name')
        if eName_list:
            name = eName_list[0].text
            if name:
                name = name.strip()

            level = int(eTask.xpath('OutlineLevel')[0].text)

            if level in [1, 2] and name in name_map.keys():
                # check that there is no full named task else in the schedule
                root = eTask.getroottree()
                eFullnamed_task_list = root.xpath('//Task[Name = "%s"]' % name_map[name])

                if not eFullnamed_task_list:  # if not - map the name
                    return name_map[name]
            return name
        return ''

    @property
    def desc_tasks_count(self):
        count = len(self.tasks)
        for task in self.tasks:
            count += task.desc_tasks_count
        return count

    def get_type(self, check_same_start_finish=False):
        if self.tasks:
            return 'Container'
        elif self.milestone and (not check_same_start_finish or self.dStart == self.dFinish):
            return 'Milestone'
        else:
            return 'Task'

    def dump_as_dict(self, recursive=True):
        attrs = copy(vars(self))
        # avoid infinite looping schedule > task > schedule ...
        exclude = ['_schedule', '_subtree_hash_cache']

        if recursive:
            attrs['tasks'] = []
            for task in self.tasks:
                attrs['tasks'].append(task.dump_as_dict())
        else:
            exclude.append('tasks')

        [attrs.pop(item) for item in exclude if item in attrs]

        return attrs

    @classmethod
    def load_from_dict(cls, input_dict, schedule):
        task = cls(schedule)

        for key, val in input_dict.items():
            task.__setattr__(key, val)

        task.tasks = []
        for new_task in input_dict.get('tasks', []):
            task.tasks.append(Task.load_from_dict(new_task, schedule))

        return task

    def get_subtree_hash(self, fields):
        if not isinstance(fields, tuple):
            fields = tuple(fields)

        if fields not in self._subtree_hash_cache:
            if self.tasks:
                hashes = []
                for child_task in self.tasks:
                    task_hash_values = []
                    for attr_name in fields:
                        attr_value = getattr(child_task, attr_name)

                        if isinstance(attr_value, list):
                            attr_value = sorted(attr_value)

                        task_hash_values.append(str(attr_value))

                    hashes.append(
                        '%s%s' % (
                            ''.join(task_hash_values),
                            child_task.get_subtree_hash(fields)
                        )
                    )

                subtree_hash = ''.join(hashes)

            else:
                subtree_hash = ''

            self._subtree_hash_cache[fields] = subtree_hash

        return self._subtree_hash_cache[fields]

    def get_slug_key(self, prefix=None):
        """
        Generates slug key only for the task itself.
        Doesn't include parent tasks slugs i.e. slug key is not a full slug path to the task.
        """
        slug_key = slugify(self.name)

        if prefix is not None:
            return '.'.join((prefix, slug_key))
        return slug_key


class Schedule(object):
    slug = ''
    name = ''  # Product 1.2
    tasks = []
    dStart = None
    dFinish = None
    changelog = {}
    mtime = None

    resources = {}
    assignments = []
    used_flags = None
    ext_attr = {}
    flags_attr_id = None
    id_reg = set()

    _eTask_list = []
    _task_index = 1
    _taskname_flat_registry = None

    errors_import = []

    def __init__(self):
        self.tasks = []
        self.used_flags = set()
        self.changelog = {}
        self.ext_attr = {}
        self.unique_id_re = re.compile(r'[\W_]+')
        self.errors_import = []
        self.mtime = None
        self.tasks_slugs = None

    def make_flat(self):
        '''Convert tasks structure to flat list'''
        def add_tasks_to_list(tasks, tasks_list):
            for task in tasks:
                subtasks = copy(task.tasks)
                task.tasks = []
                tasks_list.append(task)
                add_tasks_to_list(subtasks, tasks_list)

        flat_tasks = []
        add_tasks_to_list(self.tasks, flat_tasks)
        self.tasks = flat_tasks

    def filter_milestones(self):
        '''Keep only tasks that are milestones or contain children that are'''
        def filter_tasks(tasks):
            tasks_list = []
            for task in tasks:
                filtered_subtasks = filter_tasks(task.tasks)

                logmsg_fmt = 'REMOVE: {} {}'

                if task.milestone or filtered_subtasks:
                    task.tasks = filtered_subtasks
                    tasks_list.append(task)
                    logmsg_fmt = 'ADD: {} {}'

                log.info(logmsg_fmt.format(task.name, task.milestone))

            return tasks_list

        self.tasks = filter_tasks(self.tasks)

    def filter_flags(self, show=None, hide=None):
        '''
        Keep only tasks that match flag conditions or contain children tha match

        Args:
            show: list of flags to show
            hide: list of flags to hide
        '''

        def filter_tasks(tasks):
            tasks_list = []
            for task in tasks:
                # see if there are any matching children
                filtered_subtasks = filter_tasks(task.tasks)

                # Add to result if not hidden and (has subtasks or fits show)
                logmsg_fmt = 'REMOVE: {} {}'
                if (not (set(task.flags) & set(hide))
                    and (filtered_subtasks
                         or (set(task.flags) & set(show))
                         or not show)):
                    task.tasks = filtered_subtasks
                    tasks_list.append(task)

                    logmsg_fmt = 'ADD: {} {}'

                    self.used_flags.update(task.flags)

                log.info(logmsg_fmt.format(task.name, task.flags))

            return tasks_list

        if show is None:
            show = []
        if hide is None:
            hide = []

        if show or hide:
            self.used_flags = set()

            log.info('FLAG filter: SHOW {}   HIDE {}'.format(show, hide))

            self.tasks = filter_tasks(self.tasks)

    def sort_tasks(self, field):
        self.tasks = sort_tasks(self.tasks, field)

    def check_top_task(self):
        """ Add single root task if multiple """

        def _raise_index(task):
            task.index = int(task.index) + 1
            for t in task.tasks:
                _raise_index(t)

        if len(self.tasks) <= 1:
            return

        # need one top task
        top_task = Task(self)
        top_task.index = 1
        # top_task.name = '%s %s' % (self.name, self.version)
        # removed version as per BZ#1396303 - see how that works
        top_task.name = self.name
        top_task.slug = self.slug

        for task in self.tasks:
            if top_task.dStart:
                top_task.dStart = min(top_task.dStart,
                                      task.dStart)
            else:
                top_task.dStart = task.dStart

            if top_task.dFinish:
                top_task.dFinish = max(top_task.dFinish,
                                       task.dFinish)
            else:
                top_task.dFinish = task.dFinish

            _raise_index(task)
            top_task.tasks.append(task)

        self.tasks = [top_task]

    def generate_slugs(self):
        def gen_slugs_recursive(tasks, slug_prefix=None):
            for task in tasks:
                task_slug = task.get_slug_key(slug_prefix)

                if len(task_slug) > TASK_SLUG_TRUNCATION_LIMIT:
                    task_slug = task_slug[:TASK_SLUG_TRUNCATION_LIMIT]

                if task_slug in self.tasks_slugs:
                    for i in range(2, TASK_SLUG_NUMERATION_LIMIT + 1):
                        task_slug_numerated = '%s_%d' % (task_slug, i)
                        if task_slug_numerated not in self.tasks_slugs:
                            final_task_slug = task_slug_numerated
                            break
                        elif i == TASK_SLUG_NUMERATION_LIMIT:
                            raise SchedulesToolsException('Task slug generation error: '
                                                          'reached numeration limit')
                else:
                    final_task_slug = task_slug

                task.slug = final_task_slug
                self.tasks_slugs.add(task.slug)
                gen_slugs_recursive(task.tasks, task.slug)

        self.tasks_slugs = set()
        gen_slugs_recursive(self.tasks)

    def print_tasks(self, tasks=None, level=0):
        if tasks is None:
            tasks = self.tasks
        for task in tasks:
            print(level * '  ', str(task))
            if task.tasks:
                self.print_tasks(task.tasks, level + 1)

    @staticmethod
    def _diff_task_attrs(left, right, attrs, whole_days=False):
        diff = {}

        for a in attrs:
            left_val = left.__getattribute__(a)
            right_val = right.__getattribute__(a)
            if left_val != right_val:
                try:
                    if whole_days and \
                                    left_val.day == right_val.day and \
                                    left_val.month == right_val.month and \
                                    left_val.year == right_val.year:
                        # When we compare just whole days, we don't mind
                        # to have diff in time
                        continue
                except AttributeError:
                    pass

                diff[a] = (str(left_val), str(right_val))
        return diff

    def _diff_tasks(self, left, right, attrs=None, whole_days=False):
        default_attrs = ['name', 'dStart', 'dFinish', 'link', 'note']
        if not attrs:
            attrs = default_attrs
        ret = ''
        missing_str = ('{}: Some phase upwards is missing to compare with '
                       'this one on the {} side.\n')

        for left, right in zip_longest(left, right):
            if not left:
                ret += missing_str.format(right.name, 'LEFT')
                break
            if not right:
                ret += missing_str.format(left.name, 'RIGHT')
                break

            d = self._diff_task_attrs(left, right, attrs, whole_days)
            if len(d):
                name_warning = ''
                if 'name' in d.keys():
                    name_warning = (' <--- Name of the phase doesn\'t match. '
                                    'Missing phase?')
                # width=1 force wrap tuple to newline
                ret += '{}:{}\n{}\n'.format(
                    left.name,
                    name_warning,
                    pprint.pformat(d, width=1)
                )

            ret += self._diff_tasks(left.tasks, right.tasks, attrs, whole_days)
        return ret

    def _diff_schedule_attrs(self, schedule, whole_days=False):
        attrs = ['name', 'changelog', 'dStart', 'dFinish', 'assignments', 'used_flags']
        ret = ''
        diff = dict()

        for attr in attrs:
            self_s = self.__getattribute__(attr)
            schedule_s = schedule.__getattribute__(attr)

            if self_s != schedule_s:
                try:
                    if whole_days and \
                                    self_s.day == schedule_s.day and \
                                    self_s.month == schedule_s.month and \
                                    self_s.year == schedule_s.year:
                        # When we compare just whole days, we don't mind
                        # to have diff in time
                        continue
                except AttributeError:
                    pass
                diff[attr] = (str(self_s), str(schedule_s))

        if len(diff):
            # width=1 force wrap tuple to newline
            ret += 'Schedule attrs:\n'
            ret += pprint.pformat(diff, width=1)
        return ret

    def _diff_schedule_version(self, schedule):
        attrs = self._version.keys()
        ret = ''
        diff = dict()

        for attr in attrs:
            self_s = self._version[attr]
            schedule_s = self._version[attr]

            if self_s != schedule_s:
                diff[attr] = (self_s, schedule_s)

        if len(diff):
            # width=1 force wrap tuple to newline
            ret += 'Schedule version:\n'
            ret += pprint.pformat(diff, width=1)

        return ret

    def diff(self, schedule, attrs=None, whole_days=False):
        """
        Really simple diff to match two schedules.

        @param schedule Instance to another schedule to figure out differences
        @param attrs Task attributes to check
        @param whole_days Flag to ignore time differences of dates
        """
        ret = ''
        diff_tasks = self._diff_tasks(self.tasks, schedule.tasks, attrs,
                                      whole_days)
        if diff_tasks:
            ret += 'Tasks attrs:\n'
            ret += diff_tasks
        ret += self._diff_schedule_attrs(schedule, whole_days=whole_days)

        return ret.strip()

    def slugify_str(self, orig_str):
        return self.unique_id_re.sub('_', orig_str.lower())

    def dump_as_dict(self):
        schedule = copy(vars(self))

        schedule['tasks'] = []
        for task in self.tasks:
            schedule['tasks'].append(task.dump_as_dict())

        # set() is not serializable into json
        schedule['used_flags'] = sorted(list(self.used_flags))
        return schedule

    @classmethod
    def load_from_dict(cls, input_dict):
        # This will preserve reference to original class attributes
        schedule = cls()
        for key, val in input_dict.items():
            schedule.__setattr__(key, val)

        schedule.tasks = []
        for task in input_dict['tasks']:
            schedule.tasks.append(Task.load_from_dict(task, schedule))

        schedule.used_flags = set(input_dict['used_flags'])

        return schedule

    def _build_tasks_flat_registry(self, tasks_param):
        if self._taskname_flat_registry is None:
            self._taskname_flat_registry = set()

        for task in tasks_param:
            self._taskname_flat_registry.add(task.name)
            self._build_tasks_flat_registry(task.tasks)

    def check_for_taskname(self, tasks):
        """
        Method consume list of task names to check their existence specified
        as dict
        Args:
            tasks: dict specification of task names to check. Example:
            {'exactTaskname': False, 'matchBeginningTaskname': True}

        Returns:
            Set of task names, that haven't been found in schedule.

        """
        if not isinstance(tasks, dict):
            raise Exception(
                "'tasks' argument has to be dict, for example: {'task1': False,"
                " 'matchBeginningTaskname': True}")

        if not self._taskname_flat_registry:
            self._build_tasks_flat_registry(self.tasks)

        missing_tasks = set()
        for task_name, start_with_flag in tasks.items():
            if start_with_flag:
                re_task_str = re.escape(task_name)
                re_task = re.compile(re_task_str + '.*')
                found = False

                for task in self._taskname_flat_registry:
                    if re_task.match(task):
                        found = True
                        break
                if not found:
                    missing_tasks.add(task_name)
            elif task_name not in self._taskname_flat_registry:
                missing_tasks.add(task_name)

        return missing_tasks
According to given value it will guess if it is flag, link or note         definition and fill proper task attribute.          Args:             value: string to parse, or set attribute specified by 'key' arg             key: (optional) set value to task attribute according to this key # test value for flag / note / link # it's not a string in format 'Flag: qe, dev' - don't process # in case of multiple notes - concatenate Corrects phase names # check that there is no full named task else in the schedule # if not - map the name # avoid infinite looping schedule > task > schedule ... Generates slug key only for the task itself.         Doesn't include parent tasks slugs i.e. slug key is not a full slug path to the task. # Product 1.2 Convert tasks structure to flat list Keep only tasks that are milestones or contain children that are Keep only tasks that match flag conditions or contain children tha match          Args:             show: list of flags to show             hide: list of flags to hide # see if there are any matching children # Add to result if not hidden and (has subtasks or fits show) Add single root task if multiple # need one top task # top_task.name = '%s %s' % (self.name, self.version) # removed version as per BZ#1396303 - see how that works # When we compare just whole days, we don't mind # to have diff in time # width=1 force wrap tuple to newline # When we compare just whole days, we don't mind # to have diff in time # width=1 force wrap tuple to newline # width=1 force wrap tuple to newline Really simple diff to match two schedules.          @param schedule Instance to another schedule to figure out differences         @param attrs Task attributes to check         @param whole_days Flag to ignore time differences of dates # set() is not serializable into json # This will preserve reference to original class attributes Method consume list of task names to check their existence specified         as dict         Args:             tasks: dict specification of task names to check. Example:             {'exactTaskname': False, 'matchBeginningTaskname': True}          Returns:             Set of task names, that haven't been found in schedule.