Improving the stats API performance

remotes/origin/logger
Alejandro Alonso 2015-11-11 15:35:38 +01:00 committed by David Barragán Merino
parent 674b83c1a0
commit ad47f8f0c9
3 changed files with 234 additions and 183 deletions

View File

@ -271,31 +271,6 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
rp_query = rp_query.exclude(role__id__in=roles.values_list("id", flat=True))
rp_query.delete()
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)
flat_role_dicts = map(lambda x: {x.role_id: x.points.value if x.points.value else 0},
flat_role_points)
return dict_sum(*flat_role_dicts)
def _get_points_increment(self, client_requirement, team_requirement):
last_milestones = self.milestones.order_by('-estimated_finish')
last_milestone = last_milestones[0] if last_milestones else None
if last_milestone:
user_stories = self.user_stories.filter(
created_date__gte=last_milestone.estimated_finish,
client_requirement=client_requirement,
team_requirement=team_requirement
)
else:
user_stories = self.user_stories.filter(
client_requirement=client_requirement,
team_requirement=team_requirement
)
user_stories = user_stories.prefetch_related('role_points', 'role_points__points')
return self._get_user_stories_points(user_stories)
@property
def project(self):
return self
@ -304,45 +279,6 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
def project(self):
return self
@property
def future_team_increment(self):
team_increment = self._get_points_increment(False, True)
shared_increment = {key: value / 2 for key, value in self.future_shared_increment.items()}
return dict_sum(team_increment, shared_increment)
@property
def future_client_increment(self):
client_increment = self._get_points_increment(True, False)
shared_increment = {key: value / 2 for key, value in self.future_shared_increment.items()}
return dict_sum(client_increment, shared_increment)
@property
def future_shared_increment(self):
return self._get_points_increment(True, True)
@property
def closed_points(self):
return self.calculated_points["closed"]
@property
def defined_points(self):
return self.calculated_points["defined"]
@property
def assigned_points(self):
return self.calculated_points["assigned"]
@property
def calculated_points(self):
user_stories = self.user_stories.all().prefetch_related('role_points', 'role_points__points')
closed_user_stories = user_stories.filter(is_closed=True)
assigned_user_stories = user_stories.filter(milestone__isnull=False)
return {
"defined": self._get_user_stories_points(user_stories),
"closed": self._get_user_stories_points(closed_user_stories),
"assigned": self._get_user_stories_points(assigned_user_stories),
}
def _get_q_watchers(self):
return Q(notify_policies__project_id=self.id) & ~Q(notify_policies__notify_level=NotifyLevel.none)

View File

@ -19,94 +19,11 @@ from django.db.models import Q, Count
from django.apps import apps
import datetime
import copy
import collections
from taiga.projects.history.models import HistoryEntry
from taiga.projects.userstories.models import RolePoints
def _get_total_story_points(project):
return (project.total_story_points if project.total_story_points not in [None, 0] else
sum(project.calculated_points["defined"].values()))
def _get_total_milestones(project):
return (project.total_milestones if project.total_milestones not in [None, 0] else
project.milestones.count())
def _get_milestones_stats_for_backlog(project):
"""
Get collection of stats for each millestone of project.
Data returned by this function are used on backlog.
"""
current_evolution = 0
current_team_increment = 0
current_client_increment = 0
optimal_points_per_sprint = 0
total_story_points = _get_total_story_points(project)
total_milestones = _get_total_milestones(project)
if total_story_points and total_milestones:
optimal_points_per_sprint = total_story_points / total_milestones
future_team_increment = sum(project.future_team_increment.values())
future_client_increment = sum(project.future_client_increment.values())
milestones = project.milestones.order_by('estimated_start').\
prefetch_related("user_stories",
"user_stories__role_points",
"user_stories__role_points__points")
milestones = list(milestones)
milestones_count = len(milestones)
optimal_points = 0
team_increment = 0
client_increment = 0
for current_milestone in range(0, max(milestones_count, total_milestones)):
optimal_points = (total_story_points -
(optimal_points_per_sprint * current_milestone))
evolution = (total_story_points - current_evolution
if current_evolution is not None else None)
if current_milestone < milestones_count:
ml = milestones[current_milestone]
milestone_name = ml.name
team_increment = current_team_increment
client_increment = current_client_increment
current_evolution += sum(ml.closed_points.values())
current_team_increment += sum(ml.team_increment_points.values())
current_client_increment += sum(ml.client_increment_points.values())
else:
milestone_name = _("Future sprint")
team_increment = current_team_increment + future_team_increment,
client_increment = current_client_increment + future_client_increment,
current_evolution = None
yield {
'name': milestone_name,
'optimal': optimal_points,
'evolution': evolution,
'team-increment': team_increment,
'client-increment': client_increment,
}
optimal_points -= optimal_points_per_sprint
evolution = (total_story_points - current_evolution
if current_evolution is not None and total_story_points else None)
yield {
'name': _('Project End'),
'optimal': optimal_points,
'evolution': evolution,
'team-increment': team_increment,
'client-increment': client_increment,
}
def _count_status_object(status_obj, counting_storage):
if status_obj.id in counting_storage:
counting_storage[status_obj.id]['count'] += 1
@ -218,35 +135,217 @@ def get_stats_for_project_issues(project):
return project_issues_stats
def _get_milestones_stats_for_backlog(project, milestones):
"""
Calculates the stats associated to the milestones parameter.
- project is a Project model instance
We assume this object have also the following numeric attributes:
- _defined_points
- _future_team_increment
- _future_client_increment
- milestones is a sorted dict of Milestone model instances sorted by estimated_start.
We assume this objects have also the following numeric attributes:
- _closed_points
- _team_increment_points
- _client_increment_points
The returned result is a list of dicts where each entry contains the following keys:
- name
- optimal
- evolution
- team
- client
"""
current_evolution = 0
current_team_increment = 0
current_client_increment = 0
optimal_points_per_sprint = 0
optimal_points = 0
team_increment = 0
client_increment = 0
total_story_points = project.total_story_points\
if project.total_story_points not in [None, 0] else project._defined_points
total_milestones = project.total_milestones\
if project.total_milestones not in [None, 0] else len(milestones)
if total_story_points and total_milestones:
optimal_points_per_sprint = total_story_points / total_milestones
milestones_count = len(milestones)
milestones_stats = []
for current_milestone_pos in range(0, max(milestones_count, total_milestones)):
optimal_points = (total_story_points -
(optimal_points_per_sprint * current_milestone_pos))
evolution = (total_story_points - current_evolution
if current_evolution is not None else None)
if current_milestone_pos < milestones_count:
current_milestone = list(milestones.values())[current_milestone_pos]
milestone_name = current_milestone.name
team_increment = current_team_increment
client_increment = current_client_increment
current_evolution += current_milestone._closed_points
current_team_increment += current_milestone._team_increment_points
current_client_increment += current_milestone._client_increment_points
else:
milestone_name = _("Future sprint")
team_increment = current_team_increment + project._future_team_increment,
client_increment = current_client_increment + project._future_client_increment,
current_evolution = None
milestones_stats.append({
'name': milestone_name,
'optimal': optimal_points,
'evolution': evolution,
'team-increment': team_increment,
'client-increment': client_increment,
})
optimal_points -= optimal_points_per_sprint
evolution = (total_story_points - current_evolution
if current_evolution is not None and total_story_points else None)
milestones_stats.append({
'name': _('Project End'),
'optimal': optimal_points,
'evolution': evolution,
'team-increment': team_increment,
'client-increment': client_increment,
})
return milestones_stats
def get_stats_for_project(project):
project = apps.get_model("projects", "Project").objects.\
prefetch_related("milestones",
"user_stories").\
get(id=project.id)
# Let's fetch all the estimations related to a project with all the necesary
# related data
role_points = RolePoints.objects.filter(
user_story__project = project,
).prefetch_related(
"user_story",
"user_story__assigned_to",
"user_story__milestone",
"user_story__status",
"role",
"points")
points = project.calculated_points
closed_points = sum(points["closed"].values())
closed_points_from_closed_milestones = sum(RolePoints.objects.filter(
Q(user_story__project=project) & Q(user_story__milestone__closed=True)
).exclude(points__value__isnull=True).values_list("points__value", flat=True))
# Data inicialization
project._closed_points = 0
project._closed_points_per_role = {}
project._closed_points_from_closed_milestones = 0
project._defined_points = 0
project._defined_points_per_role = {}
project._assigned_points = 0
project._assigned_points_per_role = {}
project._future_team_increment = 0
project._future_client_increment = 0
closed_milestones = project.milestones.filter(closed=True).count()
# The key will be the milestone id and it will be ordered by estimated_start
milestones = collections.OrderedDict()
for milestone in project.milestones.order_by("estimated_start"):
milestone._closed_points = 0
milestone._team_increment_points = 0
milestone._client_increment_points = 0
milestones[milestone.id] = milestone
def _find_milestone_for_userstory(user_story):
for m in milestones.values():
if m.estimated_finish > user_story.created_date.date() and\
m.estimated_start <= user_story.created_date.date():
return m
return None
def _update_team_increment(milestone, value):
if milestone:
milestones[milestone.id]._team_increment_points += value
else:
project._future_team_increment += value
def _update_client_increment(milestone, value):
if milestone:
milestones[milestone.id]._client_increment_points += value
else:
project._future_client_increment += value
# Iterate over all the project estimations and update our stats
for role_point in role_points:
role_id = role_point.role.id
points_value = role_point.points.value
milestone = role_point.user_story.milestone
is_team_requirement = role_point.user_story.team_requirement
is_client_requirement = role_point.user_story.client_requirement
us_milestone = _find_milestone_for_userstory(role_point.user_story)
# None estimations doesn't affect to project stats
if points_value is None:
continue
# Total defined points
project._defined_points += points_value
# Defined points per role
project._defined_points_for_role = project._defined_points_per_role.get(role_id, 0)
project._defined_points_for_role += points_value
project._defined_points_per_role[role_id] = project._defined_points_for_role
# Closed points
if role_point.user_story.is_closed:
project._closed_points += points_value
closed_points_for_role = project._closed_points_per_role.get(role_id, 0)
closed_points_for_role += points_value
project._closed_points_per_role[role_id] = closed_points_for_role
if milestone is not None:
milestones[milestone.id]._closed_points += points_value
if milestone is not None and milestone.closed:
project._closed_points_from_closed_milestones += points_value
# Assigned to milestone points
if role_point.user_story.milestone is not None:
project._assigned_points += points_value
assigned_points_for_role = project._assigned_points_per_role.get(role_id, 0)
assigned_points_for_role += points_value
project._assigned_points_per_role[role_id] = assigned_points_for_role
# Extra requirements
if is_team_requirement and is_client_requirement:
_update_team_increment(us_milestone, points_value/2)
_update_client_increment(us_milestone, points_value/2)
if is_team_requirement and not is_client_requirement:
_update_team_increment(us_milestone, points_value)
if not is_team_requirement and is_client_requirement:
_update_client_increment(us_milestone, points_value)
# Speed calculations
speed = 0
closed_milestones = len([m for m in milestones.values() if m.closed])
if closed_milestones != 0:
speed = closed_points_from_closed_milestones / closed_milestones
speed = project._closed_points_from_closed_milestones / closed_milestones
milestones_stats = _get_milestones_stats_for_backlog(project, milestones)
project_stats = {
'name': project.name,
'total_milestones': project.total_milestones,
'total_points': project.total_story_points,
'closed_points': closed_points,
'closed_points_per_role': points["closed"],
'defined_points': sum(points["defined"].values()),
'defined_points_per_role': points["defined"],
'assigned_points': sum(points["assigned"].values()),
'assigned_points_per_role': points["assigned"],
'milestones': _get_milestones_stats_for_backlog(project),
'closed_points': project._closed_points,
'closed_points_per_role': project._closed_points_per_role,
'defined_points': project._defined_points,
'defined_points_per_role': project._defined_points_per_role,
'assigned_points': project._assigned_points,
'assigned_points_per_role': project._assigned_points_per_role,
'milestones': milestones_stats,
'speed': speed,
}
return project_stats

View File

@ -86,36 +86,46 @@ def data():
def test_project_defined_points(client, data):
assert data.project.defined_points == {data.role1.pk: 63}
project_stats = get_stats_for_project(data.project)
assert project_stats["defined_points_per_role"] == {data.role1.pk: 63}
data.role_points1.role = data.role2
data.role_points1.save()
assert data.project.defined_points == {data.role1.pk: 62, data.role2.pk: 1}
project_stats = get_stats_for_project(data.project)
assert project_stats["defined_points_per_role"] == {data.role1.pk: 62, data.role2.pk: 1}
def test_project_closed_points(client, data):
assert data.project.closed_points == {}
project_stats = get_stats_for_project(data.project)
assert project_stats["closed_points_per_role"] == {}
data.role_points1.role = data.role2
data.role_points1.save()
assert data.project.closed_points == {}
project_stats = get_stats_for_project(data.project)
assert project_stats["closed_points_per_role"] == {}
data.user_story1.is_closed = True
data.user_story1.save()
assert data.project.closed_points == {data.role2.pk: 1}
project_stats = get_stats_for_project(data.project)
assert project_stats["closed_points_per_role"] == {data.role2.pk: 1}
data.user_story2.is_closed = True
data.user_story2.save()
assert data.project.closed_points == {data.role1.pk: 2, data.role2.pk: 1}
project_stats = get_stats_for_project(data.project)
assert project_stats["closed_points_per_role"] == {data.role1.pk: 2, data.role2.pk: 1}
data.user_story3.is_closed = True
data.user_story3.save()
assert data.project.closed_points == {data.role1.pk: 6, data.role2.pk: 1}
project_stats = get_stats_for_project(data.project)
assert project_stats["closed_points_per_role"] == {data.role1.pk: 6, data.role2.pk: 1}
data.user_story4.is_closed = True
data.user_story4.save()
assert data.project.closed_points == {data.role1.pk: 14, data.role2.pk: 1}
project_stats = get_stats_for_project(data.project)
assert project_stats["closed_points_per_role"] == {data.role1.pk: 14, data.role2.pk: 1}
data.user_story5.is_closed = True
data.user_story5.save()
assert data.project.closed_points == {data.role1.pk: 30, data.role2.pk: 1}
project_stats = get_stats_for_project(data.project)
assert project_stats["closed_points_per_role"] == {data.role1.pk: 30, data.role2.pk: 1}
data.user_story6.is_closed = True
data.user_story6.save()
assert data.project.closed_points == {data.role1.pk: 62, data.role2.pk: 1}
project_stats = get_stats_for_project(data.project)
assert project_stats["closed_points_per_role"] == {data.role1.pk: 62, data.role2.pk: 1}
project_stats = get_stats_for_project(data.project)
assert project_stats["closed_points"] == 63
@ -123,19 +133,25 @@ def test_project_closed_points(client, data):
def test_project_assigned_points(client, data):
assert data.project.assigned_points == {data.role1.pk: 48}
project_stats = get_stats_for_project(data.project)
assert project_stats["assigned_points_per_role"] == {data.role1.pk: 48}
data.role_points1.role = data.role2
data.role_points1.save()
assert data.project.assigned_points == {data.role1.pk: 48}
project_stats = get_stats_for_project(data.project)
assert project_stats["assigned_points_per_role"] == {data.role1.pk: 48}
data.user_story1.milestone = data.milestone
data.user_story1.save()
assert data.project.assigned_points == {data.role1.pk: 48, data.role2.pk: 1}
project_stats = get_stats_for_project(data.project)
assert project_stats["assigned_points_per_role"] == {data.role1.pk: 48, data.role2.pk: 1}
data.user_story2.milestone = data.milestone
data.user_story2.save()
assert data.project.assigned_points == {data.role1.pk: 50, data.role2.pk: 1}
project_stats = get_stats_for_project(data.project)
assert project_stats["assigned_points_per_role"] == {data.role1.pk: 50, data.role2.pk: 1}
data.user_story3.milestone = data.milestone
data.user_story3.save()
assert data.project.assigned_points == {data.role1.pk: 54, data.role2.pk: 1}
project_stats = get_stats_for_project(data.project)
assert project_stats["assigned_points_per_role"] == {data.role1.pk: 54, data.role2.pk: 1}
data.user_story4.milestone = data.milestone
data.user_story4.save()
assert data.project.assigned_points == {data.role1.pk: 62, data.role2.pk: 1}
project_stats = get_stats_for_project(data.project)
assert project_stats["assigned_points_per_role"] == {data.role1.pk: 62, data.role2.pk: 1}