Ejemplo n.º 1
0
    def create_sprint(self, name, start=None, end=None, duration=20,
                      milestone=None, team=None):
        """Creates a Sprint for the given milestone, if doesn't exists, first
        it creates a Milestone"""
        # If the start day is set to today, the sprint will
        # normalize it to 9:00am of the start day and all the tests
        # will fail, till 9:00am in the morning...
        if start is None:
            # we set hours to 0 so will be normalized to 9am at any
            # time of the day, when running tests.
            start = (now(tz=utc) - timedelta(days=3)).replace(hour=0)
        if milestone is None:
            milestone = self.create_milestone('Milestone for %s' % name)
        # It should automatically load the existing Sprint if already there
        if isinstance(milestone, Milestone):
            milestone = milestone.name
        sprint_controller = SprintController(self.env)
        if start is not None:
            start = shift_to_utc(start)
        if end is not None:
            end = shift_to_utc(end)
        create_sprint_command = SprintController.CreateSprintCommand(self.env, name=name, start=start,
                                                                     end=end, duration=duration, milestone=milestone)
        create_sprint_command.native = True
        sprint = sprint_controller.process_command(create_sprint_command)

        assert sprint is not None
        if team is not None:
            if isinstance(team, basestring):
                team = self.create_team(name=team)
            sprint.team = team
            sprint.save()
        return sprint
Ejemplo n.º 2
0
    def running_to_start_closed_sprints(self, req):
        MAX_ITEMS = 5

        def mark_last_viewed_sprint(sprint_names):
            return [(name, name==last_viewed_sprint_name) for \
                               name in sprint_names]

        last_viewed_sprint_name = SessionScope(req).sprint_name()
        # get sprint data
        get_options = SprintController.GetSprintOptionListCommand(self.env)
        closed, running, to_start = \
            SprintController(self.env).process_command(get_options)
        running_sprints = mark_last_viewed_sprint(running)
        ready_to_start_sprints = mark_last_viewed_sprint(to_start[:MAX_ITEMS])
        closed_sprints = mark_last_viewed_sprint(closed[-MAX_ITEMS:])
        # Show last closed sprints at the top
        closed_sprints.reverse()
        sprint_list = [
            {
                Key.LABEL: _('Running (by Start Date)'),
                Key.OPTIONS: running_sprints
            },
            {
                Key.LABEL: _('To Start (by Start Date)'),
                Key.OPTIONS: ready_to_start_sprints
            },
            {
                Key.LABEL: _('Closed (by End Date)'),
                Key.OPTIONS: closed_sprints
            },
        ]
        return sprint_list
Ejemplo n.º 3
0
 def populate_with_sprint_data(self, sprint_name):
     self.data['sprint_name'] = sprint_name
     tickets = self._get_prefetched_backlog()
     cmd_resources = SprintController.GetResourceStatsCommand(
         self.env, sprint=sprint_name, tickets=tickets)
     #resource_stats = sprint.get_resources_stats(tickets=tickets)
     self.data['resource_stats'] = SprintController(
         self.env).process_command(cmd_resources)
Ejemplo n.º 4
0
 def __init__(self, env, **kwargs):
     template_filename = kwargs.get('template_filename') or 'scrum/backlog/templates/agilo_burndown_chart.html'
     self._define_chart_resources(env, template_filename, kwargs)
     kwargs['scripts'] = ('agilo/js/burndown.js',)
     self.super(env, template_filename, **kwargs)
     self.t_controller = TeamController(self.env)
     self.sp_controller = SprintController(self.env)
     self.b_controller = BacklogController(self.env)
     self.burndown_data = dict()
Ejemplo n.º 5
0
 def _get_sprint(self, req, sprint_name):
     """Retrieve the Sprint for the given name"""
     get_sprint = SprintController.GetSprintCommand(self.env,
                                                    sprint=sprint_name)
     sprint = SprintController(self.env).process_command(get_sprint)
     # we need to convert sprint dates into local timezone
     sprint.start = format_datetime(sprint.start, tzinfo=req.tz)
     sprint.end = format_datetime(sprint.end, tzinfo=req.tz)
     if sprint.team is None:
         msg = _("No Team has been assigned to this Sprint...")
         if not msg in req.chrome['warnings']:
             add_warning(req, msg)
     return sprint
Ejemplo n.º 6
0
 def _get_sprint(self, req, sprint_name):
     """Retrieve the Sprint for the given name"""
     get_sprint = SprintController.GetSprintCommand(self.env,
                                                    sprint=sprint_name)
     sprint = SprintController(self.env).process_command(get_sprint)
     # we need to convert sprint dates into local timezone
     sprint.start = format_datetime(sprint.start, tzinfo=req.tz)
     sprint.end = format_datetime(sprint.end, tzinfo=req.tz)
     if sprint.team is None:
         msg = _("No Team has been assigned to this Sprint...")
         if not msg in req.chrome['warnings']:
             add_warning(req, msg)
     return sprint
Ejemplo n.º 7
0
    def create_sprint(self,
                      name,
                      start=None,
                      end=None,
                      duration=20,
                      milestone=None,
                      team=None):
        """Creates a Sprint for the given milestone, if doesn't exists, first
        it creates a Milestone"""
        # If the start day is set to today, the sprint will
        # normalize it to 9:00am of the start day and all the tests
        # will fail, till 9:00am in the morning...
        if start is None:
            # we set hours to 0 so will be normalized to 9am at any
            # time of the day, when running tests.
            start = (now(tz=utc) - timedelta(days=3)).replace(hour=0)
        if milestone is None:
            milestone = self.create_milestone('Milestone for %s' % name)
        # It should automatically load the existing Sprint if already there
        if isinstance(milestone, Milestone):
            milestone = milestone.name
        sprint_controller = SprintController(self.env)
        if start is not None:
            start = shift_to_utc(start)
        if end is not None:
            end = shift_to_utc(end)
        create_sprint_command = SprintController.CreateSprintCommand(
            self.env,
            name=name,
            start=start,
            end=end,
            duration=duration,
            milestone=milestone)
        create_sprint_command.native = True
        sprint = sprint_controller.process_command(create_sprint_command)

        assert sprint is not None
        if team is not None:
            if isinstance(team, basestring):
                team = self.create_team(name=team)
            sprint.team = team
            sprint.save()
        return sprint
Ejemplo n.º 8
0
    def populate_with_sprint_data(self, sprint_name):
        """Populate the chart with sprint statistics"""
        cmd_stats = SprintController.GetTicketsStatisticsCommand(
            self.env, sprint=sprint_name)
        tickets_stats = SprintController(self.env).process_command(cmd_stats)

        planned, closed, total, labels = [], [], [], []

        aliases = AgiloConfig(self.env).ALIASES
        for i, t_type in enumerate(tickets_stats):
            nr_planned, nr_in_progress, nr_closed = tickets_stats[t_type]
            planned.append((i, nr_planned))
            closed.append((i, nr_closed))
            nr_total = nr_planned + nr_in_progress + nr_closed
            total.append((i, nr_total))
            alias = aliases.get(t_type, t_type)
            labels.append((i, alias))

        self.data.update(
            dict(sprint_name=sprint_name,
                 labels=labels,
                 planned=planned,
                 closed=closed,
                 total=total))
Ejemplo n.º 9
0
 def _get_remaining_time_series(self, sprint_name):
     cmd_rem_times = SprintController.GetActualBurndownCommand(self.env,
         sprint=sprint_name,  filter_by_component=self.data.get('filter_by'),
         remaining_field=self.data.get('remaining_field'))
     return self.sp_controller.process_command(cmd_rem_times)
Ejemplo n.º 10
0
class BurndownWidget(ScrumFlotChartWidget):
    """Burndown chart widget which generates HTML and JS code so that Flot
    can generate a burndown chart for the sprint including actual data, ideal 
    burndown and moving average."""
    
    default_width =  750
    default_height = 350
    
    GOOD_COLOR = '#94d31a'
    WARNING_COLOR = '#e0e63d'
    BAD_COLOR = '#f35e5e'
    
    def __init__(self, env, **kwargs):
        template_filename = kwargs.get('template_filename') or 'scrum/backlog/templates/agilo_burndown_chart.html'
        self._define_chart_resources(env, template_filename, kwargs)
        kwargs['scripts'] = ('agilo/js/burndown.js',)
        self.super(env, template_filename, **kwargs)
        self.t_controller = TeamController(self.env)
        self.sp_controller = SprintController(self.env)
        self.b_controller = BacklogController(self.env)
        self.burndown_data = dict()
    
    def json_data(self):
        return self.burndown_data
    
    def populate_with_sprint_data(self, sprint_name):
        # REFACT: migrate to member variable?
        sprint = self._load_sprint(sprint_name)
        if sprint is None:
            return
        
        self.data.update(dict(
            # REFACT: consider to remove these, we don't really need them cached
            sprint_start=sprint.start,
            sprint_end=sprint.end,
        ))
    
    # REFACT: We should remove the utc_ variants from the data dict so that 
    # there is no confusion which item to use. However we need to check that his
    # does not breaks caching... (or remove the caching altogether)
    def prepare_rendering(self, req):
        self.super()
        self._populate_with_sprint_and_viewer_timezone(self.data['sprint_name'], req.tz)
        self._convert_utc_times_to_local_timezone(req.tz)
        self._add_jsonized_plot_data()
    
    def _populate_with_sprint_and_viewer_timezone(self, sprint_name, viewer_timezone):
        sprint = self._load_sprint(sprint_name, native=True)
        if sprint is None:
            return
        
        days_to_remove = self._days_to_remove_from_burndown(sprint, viewer_timezone)
        container = ValueObject(
            remaining_times = self._get_remaining_time_series(sprint_name),
            capacity_data = self._get_capacity(sprint, viewer_timezone, self._is_filtered_backlog(), self._is_point_burndown()),
            ticks = self._calculate_ticks(sprint, viewer_timezone, days_to_remove),
            weekend_data = self._get_weekend_starts(sprint.start, sprint.end, viewer_timezone),
            today_data = self._get_today_data(sprint.start, sprint.end, viewer_timezone),
        )
        self._compact_values_by_removing_days(container, days_to_remove)

        days_without_capacity_to_hide = ValuesPerTimeCompactor.final_shift(days_to_remove)
        container.trend_data = self._trend_line(container.remaining_times, sprint.end - days_without_capacity_to_hide)

        # REFACT: put container directly into data and then unsmart the values in the utc shifting method
        utc_remaining_times = self._smart_to_tuple_series(container.remaining_times)
        utc_capacity_data = self._smart_to_tuple_series(container.capacity_data)
        # need that in non utc form too to compact
        first_remaining_time = self._first_remaining_time(container.remaining_times)
        utc_ideal_data = self._calculate_ideal_burndown(utc_capacity_data, first_remaining_time, sprint)
        utc_trend_data = self._smart_to_tuple_series(container.trend_data)
        utc_ticks = self._smart_to_tuple_series(container.ticks)
        utc_weekend_data = self._smart_to_tuple_series(container.weekend_data)
        utc_today_data = self._smart_to_tuple_series(container.today_data)
        
        self.data.update(dict(
            utc_remaining_times=utc_remaining_times,
            utc_ideal_data=utc_ideal_data,
            utc_capacity_data=utc_capacity_data,
            utc_trend_data=utc_trend_data,
            utc_ticks=utc_ticks,
            utc_weekend_data=utc_weekend_data,
            utc_today_data=utc_today_data,
        ))
        self.burndown_data.update(dict(
            today_color=self._today_color(sprint, container.remaining_times, utc_ideal_data),
        ))
    
    def _days_to_remove_from_burndown(self, sprint, viewer_timezone):
        if not AgiloConfig(self.env).burndown_should_show_working_days_only:
            return []
        if sprint.team is None:
            return []
        days_to_remove = sprint.team.capacity(viewer_timezone).days_without_capacity_in_interval(sprint.start, sprint.end)
        
        return days_to_remove
    
    def _add_jsonized_plot_data(self):
        from agilo.utils.compat import json
        self.data['jsonized_burndown_values'] = json.dumps(self.data_as_json())
    
    def _datetime_to_js_milliseconds(self, datetime_date, tz):
        """Convert a datetime into milliseconds which are directly usable by 
        flot"""
        # Flot always uses UTC. In order to display the data with the correct 
        # days, we have to move the data to UTC
        # FIXME: we no longer use flott in date mode, so this is not neccessary anymore
        # Perhaps better do the serialization in the json interface
        # Probably we still need this, since js cannot read times with timezones
        
        # "Normally you want the timestamps to be displayed according to a
        # certain time zone, usually the time zone in which the data has been
        # produced. However, Flot always displays timestamps according to UTC.
        # It has to as the only alternative with core Javascript is to interpret
        # the timestamps according to the time zone that the visitor is in,
        # which means that the ticks will shift unpredictably with the time zone
        # and daylight savings of each visitor.
        fake_utc_datetime = datetime_date.astimezone(tz).replace(tzinfo=utc)
        seconds_since_epoch = to_timestamp(fake_utc_datetime)
        milliseconds_since_epoch = seconds_since_epoch * 1000
        return milliseconds_since_epoch
    
    def _convert_to_utc_timeseries(self, start, end, input_data, now=None):
        """Takes a list of input_data and converts it into a list of tuples 
        (datetime in UTC, data). It returns a tuple every 24h from the sprint
        start datetime."""
        utc_data = []
        stop = False
        current_date = start
        for data in input_data:
            if now is not None and current_date > now:
                current_date = now
                # FIXME: (AT) This is an hack to make sure the last value is 
                # always the last one, we need to send the request timezone 
                # through to get the sprint days shifted to the current timezone.
                data = input_data[-1]
                # AT: We need to break after the append, we are already at now
                # the last data, if available would be the same exact value as
                # now. This happens in tests because the sprint is created in
                # the same microsecond as the now will be calculated adding two
                # values at last
                stop = True
            elif current_date > end:
                current_date = end
            # AT: the dates should be already UTC, but you never know
            day_data = (current_date.astimezone(utc), data)
            utc_data.append(day_data)
            if stop:
                break
            current_date += timedelta(days=1)
        return utc_data
    
    def _today_color(self, sprint, actual_data, ideal_data):
        today_color = self.GOOD_COLOR
        if not sprint.is_currently_running:
            return today_color
        # ideal_data can be empty if no actual data exists (e.g. sprint not start yet)
        if len(ideal_data) == 0 or len(actual_data) == 0:
            return today_color
        
        current_actual_burndown = actual_data[-1]
        current_remaining_time = current_actual_burndown.remaining_time
        ideal_remaining_time = self._calculate_ideal_burndown_at_datetime(ideal_data, current_actual_burndown.when)
        
        if (ideal_remaining_time == 0) and (current_remaining_time == 0):
            return today_color
        elif (ideal_remaining_time == 0) \
            or (current_remaining_time / ideal_remaining_time > 1.3):
            return self.BAD_COLOR
        elif current_remaining_time / ideal_remaining_time > 1.1:
            return self.WARNING_COLOR
        return today_color
    
    def _calculate_ticks(self, sprint, viewer_timezone, days_to_remove):
        generator = TickGenerator.for_sprint(sprint, viewer_timezone, days_to_remove)
        return generator.generate_tick_labels()
    
    def _get_capacity_data(self, sprint, viewer_timezone):
        """
        Returns the capacity data for this sprint in the form of a list.
        Capacity per day is calculated as the whole team capacity that day,
        removed a proportional amount for the contingent set by the team in 
        this sprint.
        """
        if sprint.team is None:
            return []
        
        return sprint.team.capacity(viewer_timezone).summed_hourly_capacities_in_sprint(sprint)
    
    def _get_capacity(self, sprint, viewer_timezone, is_filtered_burndown, is_point_burndown=False):
        if is_filtered_burndown or is_point_burndown:
            return []
        return self._get_capacity_data(sprint, viewer_timezone)
    
    def _calculate_ideal_burndown(self, utc_capacity_data, first_remaining_time, sprint):
        has_remaining_time = (first_remaining_time is not None)
        has_capacity_data = (len(utc_capacity_data) > 0 and utc_capacity_data[0][1] != 0)
        if has_remaining_time and has_capacity_data:
            return calculate_ideal_burndown(utc_capacity_data, first_remaining_time, sprint)
        elif has_remaining_time:
            # Just let flot draw a straight line...
            return [(first_remaining_time.when, first_remaining_time.remaining_time), (sprint.end, 0)]
        else:
            return []
    
    def _calculate_ideal_burndown_at_datetime(self, ideal_data, a_datetime):
        before, after = _entries_from_timeseries_nearest_to(ideal_data, a_datetime)
        if before is None or after is None:
            return 0
        
        return Line.from_two_tupples(before, after).y_from_x(a_datetime)
    
    def _get_remaining_time_series(self, sprint_name):
        cmd_rem_times = SprintController.GetActualBurndownCommand(self.env,
            sprint=sprint_name,  filter_by_component=self.data.get('filter_by'),
            remaining_field=self.data.get('remaining_field'))
        return self.sp_controller.process_command(cmd_rem_times)
    
    def _first_remaining_time(self, actual_data):
        if len(actual_data) > 0:
            return actual_data[0]
        return None
    
    def _trend_line(self, actual_data, end):
        return BurndownTrendLineGenerator().calculate(actual_data, end)
    
    def _smart_to_tuple_series(self, a_series):
        return [(item.when, item.value) for item in a_series]
    
    def _is_filtered_backlog(self):
        return self.data.get('filter_by') is not None

    def _is_point_burndown(self):
        return self.data.get('remaining_field') == BurndownDataConstants.REMAINING_POINTS

    def _convert_to_flot_timeseries(self, data, tz):
        flot_data = []
        for point_in_time, value in data:
            flot_milliseconds = self._datetime_to_js_milliseconds(point_in_time, tz)
            if isinstance(value, datetime):
                value = self._datetime_to_js_milliseconds(value, tz)
            flot_data.append((flot_milliseconds, value))
        return flot_data
    
    def _get_weekend_starts(self, start, end, tz):
        weekend_data = []
        # The start is stored in UTC but the weekend should be drawn
        # localized
        day = start.astimezone(tz)
        while day < end:
            if day.isoweekday() in (6, 7):
                weekend_start = midnight_with_utc_shift(day)
                weekend_data.append(DayMarker(weekend_start))
            day += timedelta(days=1)
        return weekend_data
    
    def _get_today_data(self, start, end, tz):
        if not (start <= now(tz) <= end):
            return []
        
        # Now is already calculated in the given timezone so we have to get
        # the midnight in that timezone, shifted to UTC time
        # TODO: this is being shifted later, does midnight suffice?
        today_midnight = midnight_with_utc_shift(now(tz))
        return [DayMarker(today_midnight)]
    
    def _convert_utc_times_to_local_timezone(self, tz):
        for key in self.data.keys():
            if key.startswith('utc_'):
                new_key = key[len('utc_'):]
                flot_data = self._convert_to_flot_timeseries(self.data[key], tz)
                self.burndown_data[new_key] = flot_data
    
    def _compact_values_by_removing_days(self, values, days_to_remove):
        for key, value in values.items():
            compactor = ValuesPerTimeCompactor(value, days_to_remove)
            values[key] = compactor.compact_values()