Improving milestone stats API
parent
b142cafc14
commit
a4172012ba
|
@ -113,10 +113,10 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudView
|
||||||
'estimated_finish': milestone.estimated_finish,
|
'estimated_finish': milestone.estimated_finish,
|
||||||
'total_points': total_points,
|
'total_points': total_points,
|
||||||
'completed_points': milestone.closed_points.values(),
|
'completed_points': milestone.closed_points.values(),
|
||||||
'total_userstories': milestone.user_stories.count(),
|
'total_userstories': milestone.get_cached_user_stories().count(),
|
||||||
'completed_userstories': len([us for us in milestone.user_stories.all() if us.is_closed]),
|
'completed_userstories': milestone.get_cached_user_stories().filter(is_closed=True).count(),
|
||||||
'total_tasks': milestone.tasks.all().count(),
|
'total_tasks': milestone.tasks.count(),
|
||||||
'completed_tasks': milestone.tasks.all().filter(status__is_closed=True).count(),
|
'completed_tasks': milestone.tasks.filter(status__is_closed=True).count(),
|
||||||
'iocaine_doses': milestone.tasks.filter(is_iocaine=True).count(),
|
'iocaine_doses': milestone.tasks.filter(is_iocaine=True).count(),
|
||||||
'days': []
|
'days': []
|
||||||
}
|
}
|
||||||
|
@ -125,11 +125,12 @@ class MilestoneViewSet(HistoryResourceMixin, WatchedResourceMixin, ModelCrudView
|
||||||
optimal_points = sumTotalPoints
|
optimal_points = sumTotalPoints
|
||||||
milestone_days = (milestone.estimated_finish - milestone.estimated_start).days
|
milestone_days = (milestone.estimated_finish - milestone.estimated_start).days
|
||||||
optimal_points_per_day = sumTotalPoints / milestone_days if milestone_days else 0
|
optimal_points_per_day = sumTotalPoints / milestone_days if milestone_days else 0
|
||||||
|
|
||||||
while current_date <= milestone.estimated_finish:
|
while current_date <= milestone.estimated_finish:
|
||||||
milestone_stats['days'].append({
|
milestone_stats['days'].append({
|
||||||
'day': current_date,
|
'day': current_date,
|
||||||
'name': current_date.day,
|
'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,
|
'optimal_points': optimal_points,
|
||||||
})
|
})
|
||||||
current_date = current_date + datetime.timedelta(days=1)
|
current_date = current_date + datetime.timedelta(days=1)
|
||||||
|
|
|
@ -14,7 +14,9 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models import Prefetch, Count
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -53,6 +55,8 @@ class Milestone(WatchedModelMixin, models.Model):
|
||||||
order = models.PositiveSmallIntegerField(default=1, null=False, blank=False,
|
order = models.PositiveSmallIntegerField(default=1, null=False, blank=False,
|
||||||
verbose_name=_("order"))
|
verbose_name=_("order"))
|
||||||
_importing = None
|
_importing = None
|
||||||
|
_total_closed_points_by_date = None
|
||||||
|
_cached_user_stories = None
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "milestone"
|
verbose_name = "milestone"
|
||||||
|
@ -82,6 +86,14 @@ class Milestone(WatchedModelMixin, models.Model):
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
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):
|
def _get_user_stories_points(self, user_stories):
|
||||||
role_points = [us.role_points.all() for us in user_stories]
|
role_points = [us.role_points.all() for us in user_stories]
|
||||||
flat_role_points = itertools.chain(*role_points)
|
flat_role_points = itertools.chain(*role_points)
|
||||||
|
@ -91,13 +103,13 @@ class Milestone(WatchedModelMixin, models.Model):
|
||||||
@property
|
@property
|
||||||
def total_points(self):
|
def total_points(self):
|
||||||
return self._get_user_stories_points(
|
return self._get_user_stories_points(
|
||||||
[us for us in self.user_stories.all()]
|
[us for us in self.get_cached_user_stories()]
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def closed_points(self):
|
def closed_points(self):
|
||||||
return self._get_user_stories_points(
|
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):
|
def _get_increment_points(self):
|
||||||
|
@ -148,9 +160,51 @@ class Milestone(WatchedModelMixin, models.Model):
|
||||||
def shared_increment_points(self):
|
def shared_increment_points(self):
|
||||||
return self._get_increment_points()["shared_increment"]
|
return self._get_increment_points()["shared_increment"]
|
||||||
|
|
||||||
def closed_points_by_date(self, date):
|
def total_closed_points_by_date(self, date):
|
||||||
return self._get_user_stories_points([
|
# Milestone instance will keep a cache of the total closed points by date
|
||||||
us for us in self.user_stories.filter(
|
if self._total_closed_points_by_date is None:
|
||||||
finish_date__lt=date + datetime.timedelta(days=1)
|
self._total_closed_points_by_date = {}
|
||||||
).prefetch_related('role_points', 'role_points__points') if us.is_closed
|
|
||||||
])
|
# 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)
|
||||||
|
|
Loading…
Reference in New Issue