diff --git a/taiga/projects/api.py b/taiga/projects/api.py index f291a994..19ba5ffd 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -80,6 +80,12 @@ class ProjectViewSet(ModelCrudViewSet): self.check_permissions(request, 'stats', project) return Response(services.get_stats_for_project(project)) + @detail_route(methods=['get']) + def member_stats(self, request, pk=None): + project = self.get_object() + self.check_permissions(request, 'member_stats', project) + return Response(services.get_member_stats_for_project(project)) + @detail_route(methods=['get']) def issues_stats(self, request, pk=None): project = self.get_object() diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py index 4443f596..288b2aea 100644 --- a/taiga/projects/permissions.py +++ b/taiga/projects/permissions.py @@ -28,6 +28,7 @@ class ProjectPermission(TaigaResourcePermission): modules_perms = IsProjectOwner() list_perms = AllowAny() stats_perms = AllowAny() + member_stats_perms = HasProjectPerm('view_project') star_perms = IsAuthenticated() unstar_perms = IsAuthenticated() issues_stats_perms = AllowAny() diff --git a/taiga/projects/services/__init__.py b/taiga/projects/services/__init__.py index 35341f8d..06e47b54 100644 --- a/taiga/projects/services/__init__.py +++ b/taiga/projects/services/__init__.py @@ -30,6 +30,7 @@ from .filters import get_issues_filters_data from .stats import get_stats_for_project_issues from .stats import get_stats_for_project +from .stats import get_member_stats_for_project from .members import create_members_in_bulk from .members import get_members_from_bulk diff --git a/taiga/projects/services/stats.py b/taiga/projects/services/stats.py index 9782879f..a770f95b 100644 --- a/taiga/projects/services/stats.py +++ b/taiga/projects/services/stats.py @@ -18,6 +18,8 @@ from django.db.models import Q, Count import datetime import copy +from taiga.projects.history.models import HistoryEntry + def _get_milestones_stats_for_backlog(project): """ @@ -212,3 +214,77 @@ def get_stats_for_project(project): 'speed': speed, } return project_stats + + +def _get_closed_bugs_per_member_stats(project): + # Closed bugs per user + closed_bugs = project.issues.filter(status__is_closed=True)\ + .values('assigned_to')\ + .annotate(count=Count('assigned_to'))\ + .order_by() + closed_bugs = { p["assigned_to"]: p["count"] for p in closed_bugs} + return closed_bugs + + +def _get_iocaine_tasks_per_member_stats(project): + # Iocaine tasks assigned per user + iocaine_tasks = project.tasks.filter(is_iocaine=True)\ + .values('assigned_to')\ + .annotate(count=Count('assigned_to'))\ + .order_by() + iocaine_tasks = { t["assigned_to"]: t["count"] for t in iocaine_tasks} + return iocaine_tasks + + +def _get_wiki_changes_per_member_stats(project): + # Wiki changes + wiki_changes = {} + wiki_page_keys = ["wiki.wikipage:%s"%id for id in project.wiki_pages.values_list("id", flat=True)] + history_entries = HistoryEntry.objects.filter(key__in=wiki_page_keys).values('user') + for entry in history_entries: + editions = wiki_changes.get(entry["user"]["pk"], 0) + wiki_changes[entry["user"]["pk"]] = editions + 1 + + return wiki_changes + + +def _get_created_bugs_per_member_stats(project): + # Created_bugs + created_bugs = project.issues\ + .values('owner')\ + .annotate(count=Count('owner'))\ + .order_by() + created_bugs = { p["owner"]: p["count"] for p in created_bugs } + return created_bugs + + +def _get_closed_tasks_per_member_stats(project): + # Closed tasks + closed_tasks = project.tasks.filter(status__is_closed=True)\ + .values('assigned_to')\ + .annotate(count=Count('assigned_to'))\ + .order_by() + closed_tasks = {p["assigned_to"]: p["count"] for p in closed_tasks} + return closed_tasks + +def get_member_stats_for_project(project): + base_counters = {id: 0 for id in project.members.values_list("id", flat=True)} + closed_bugs = base_counters.copy() + closed_bugs.update(_get_closed_bugs_per_member_stats(project)) + iocaine_tasks = base_counters.copy() + iocaine_tasks.update(_get_iocaine_tasks_per_member_stats(project)) + wiki_changes = base_counters.copy() + wiki_changes.update(_get_wiki_changes_per_member_stats(project)) + created_bugs = base_counters.copy() + created_bugs.update(_get_created_bugs_per_member_stats(project)) + closed_tasks = base_counters.copy() + closed_tasks.update(_get_closed_tasks_per_member_stats(project)) + + member_stats = { + "closed_bugs": closed_bugs, + "iocaine_tasks": iocaine_tasks, + "wiki_changes": wiki_changes, + "created_bugs": created_bugs, + "closed_tasks": closed_tasks, + } + return member_stats diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py index 7178362c..4667b124 100644 --- a/tests/integration/test_projects.py +++ b/tests/integration/test_projects.py @@ -1,5 +1,7 @@ from django.core.urlresolvers import reverse from taiga.base.utils import json +from taiga.projects.services import stats as stats_services +from taiga.projects.history.services import take_snapshot from .. import factories as f @@ -84,6 +86,7 @@ def test_issue_status_slug_generation(client): assert response.status_code == 200 assert response.data["slug"] == "new-status" + def test_points_name_duplicated(client): point_1 = f.PointsFactory() point_2 = f.PointsFactory(project=point_1.project) @@ -96,9 +99,64 @@ def test_points_name_duplicated(client): assert response.status_code == 400 assert response.data["name"][0] == "Name duplicated for the project" + def test_update_points_when_not_null_values_for_points(client): points = f.PointsFactory(name="?", value="6") role = f.RoleFactory(project=points.project, computable=True) assert points.project.points.filter(value__isnull=True).count() == 0 points.project.update_role_points() assert points.project.points.filter(value__isnull=True).count() == 1 + + +def test_get_closed_bugs_per_member_stats(): + project = f.ProjectFactory() + membership_1 = f.MembershipFactory(project=project) + membership_2 = f.MembershipFactory(project=project) + issue_closed_status = f.IssueStatusFactory(is_closed=True, project=project) + issue_open_status = f.IssueStatusFactory(is_closed=False, project=project) + issue_closed = f.IssueFactory(project=project, + status=issue_closed_status, + owner=membership_1.user, + assigned_to=membership_1.user) + issue_open = f.IssueFactory(project=project, + status=issue_open_status, + owner=membership_2.user, + assigned_to=membership_2.user) + task_closed_status = f.TaskStatusFactory(is_closed=True, project=project) + task_open_status = f.TaskStatusFactory(is_closed=False, project=project) + task_closed = f.TaskFactory(project=project, + status=task_closed_status, + owner=membership_1.user, + assigned_to=membership_1.user) + task_open = f.TaskFactory(project=project, + status=task_open_status, + owner=membership_2.user, + assigned_to=membership_2.user) + task_iocaine = f.TaskFactory(project=project, + status=task_open_status, + owner=membership_2.user, + assigned_to=membership_2.user, + is_iocaine=True) + + wiki_page = f.WikiPageFactory.create(project=project, owner=membership_1.user) + take_snapshot(wiki_page, user=membership_1.user) + wiki_page.content="Frontend, future" + wiki_page.save() + take_snapshot(wiki_page, user=membership_1.user) + + stats = stats_services.get_member_stats_for_project(project) + + assert stats["closed_bugs"][membership_1.user.id] == 1 + assert stats["closed_bugs"][membership_2.user.id] == 0 + + assert stats["iocaine_tasks"][membership_1.user.id] == 0 + assert stats["iocaine_tasks"][membership_2.user.id] == 1 + + assert stats["wiki_changes"][membership_1.user.id] == 2 + assert stats["wiki_changes"][membership_2.user.id] == 0 + + assert stats["created_bugs"][membership_1.user.id] == 1 + assert stats["created_bugs"][membership_2.user.id] == 1 + + assert stats["closed_tasks"][membership_1.user.id] == 1 + assert stats["closed_tasks"][membership_2.user.id] == 0