diff --git a/taiga/projects/milestones/api.py b/taiga/projects/milestones/api.py index a61806cc..aca1ab24 100644 --- a/taiga/projects/milestones/api.py +++ b/taiga/projects/milestones/api.py @@ -113,10 +113,10 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudView 'estimated_finish': milestone.estimated_finish, 'total_points': total_points, 'completed_points': milestone.closed_points.values(), - 'total_userstories': milestone.user_stories.count(), - 'completed_userstories': len([us for us in milestone.user_stories.all() if us.is_closed]), - 'total_tasks': milestone.tasks.all().count(), - 'completed_tasks': milestone.tasks.all().filter(status__is_closed=True).count(), + 'total_userstories': milestone.get_cached_user_stories().count(), + 'completed_userstories': milestone.get_cached_user_stories().filter(is_closed=True).count(), + 'total_tasks': milestone.tasks.count(), + 'completed_tasks': milestone.tasks.filter(status__is_closed=True).count(), 'iocaine_doses': milestone.tasks.filter(is_iocaine=True).count(), 'days': [] } @@ -125,11 +125,12 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudView optimal_points = sumTotalPoints milestone_days = (milestone.estimated_finish - milestone.estimated_start).days optimal_points_per_day = sumTotalPoints / milestone_days if milestone_days else 0 + while current_date <= milestone.estimated_finish: milestone_stats['days'].append({ 'day': current_date, 'name': current_date.day, - 'open_points': sumTotalPoints - sum(milestone.closed_points_by_date(current_date).values()), + 'open_points': sumTotalPoints - milestone.total_closed_points_by_date(current_date), 'optimal_points': optimal_points, }) current_date = current_date + datetime.timedelta(days=1) diff --git a/taiga/projects/milestones/models.py b/taiga/projects/milestones/models.py index 83fe8cfc..30936e9a 100644 --- a/taiga/projects/milestones/models.py +++ b/taiga/projects/milestones/models.py @@ -14,7 +14,9 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from django.apps import apps from django.db import models +from django.db.models import Prefetch, Count from django.conf import settings from django.utils.translation import ugettext_lazy as _ from django.utils import timezone @@ -53,6 +55,8 @@ class Milestone(WatchedModelMixin, models.Model): order = models.PositiveSmallIntegerField(default=1, null=False, blank=False, verbose_name=_("order")) _importing = None + _total_closed_points_by_date = None + _cached_user_stories = None class Meta: verbose_name = "milestone" @@ -82,6 +86,14 @@ class Milestone(WatchedModelMixin, models.Model): super().save(*args, **kwargs) + def get_cached_user_stories(self): + if self._cached_user_stories is None: + self._cached_user_stories = self.user_stories.\ + prefetch_related("role_points", "role_points__points").\ + annotate(num_tasks=Count("tasks")) + + return self._cached_user_stories + def _get_user_stories_points(self, user_stories): role_points = [us.role_points.all() for us in user_stories] flat_role_points = itertools.chain(*role_points) @@ -91,13 +103,13 @@ class Milestone(WatchedModelMixin, models.Model): @property def total_points(self): return self._get_user_stories_points( - [us for us in self.user_stories.all()] + [us for us in self.get_cached_user_stories()] ) @property def closed_points(self): return self._get_user_stories_points( - [us for us in self.user_stories.all() if us.is_closed] + [us for us in self.get_cached_user_stories() if us.is_closed] ) def _get_increment_points(self): @@ -148,9 +160,51 @@ class Milestone(WatchedModelMixin, models.Model): def shared_increment_points(self): return self._get_increment_points()["shared_increment"] - def closed_points_by_date(self, date): - return self._get_user_stories_points([ - us for us in self.user_stories.filter( - finish_date__lt=date + datetime.timedelta(days=1) - ).prefetch_related('role_points', 'role_points__points') if us.is_closed - ]) + def total_closed_points_by_date(self, date): + # Milestone instance will keep a cache of the total closed points by date + if self._total_closed_points_by_date is None: + self._total_closed_points_by_date = {} + + # We need to keep the milestone user stories indexed by id in a dict + user_stories = {} + for us in self.get_cached_user_stories(): + us._total_us_points = sum(self._get_user_stories_points([us]).values()) + user_stories[us.id] = us + + tasks = self.tasks.\ + select_related("user_story").\ + exclude(finished_date__isnull=True).\ + exclude(user_story__isnull=True) + + # For each finished task we try to know the proporional part of points + # it represetnts from the user story and add it to the closed points + # for that date + # This calulation is the total user story points divided by its number of tasks + for task in tasks: + user_story = user_stories[task.user_story.id] + total_us_points = user_story._total_us_points + us_tasks_counter = user_story.num_tasks + + # If the task was finished before starting the sprint it needs + # to be included + finished_date = task.finished_date.date() + if finished_date < self.estimated_start: + finished_date = self.estimated_start + + points_by_date = self._total_closed_points_by_date.get(finished_date, 0) + points_by_date += total_us_points / us_tasks_counter + self._total_closed_points_by_date[finished_date] = points_by_date + + # At this point self._total_closed_points_by_date keeps a dict where the + # finished date of the task is the key and the value is the increment of points + # We are transforming this dict of increments in an acumulation one including + # all the dates from the sprint + + acumulated_date_points = 0 + current_date = self.estimated_start + while current_date <= self.estimated_finish: + acumulated_date_points += self._total_closed_points_by_date.get(current_date, 0) + self._total_closed_points_by_date[current_date] = acumulated_date_points + current_date = current_date + datetime.timedelta(days=1) + + return self._total_closed_points_by_date.get(date, 0)