diff --git a/taiga/base/filters.py b/taiga/base/filters.py index b90740b1..a6090812 100644 --- a/taiga/base/filters.py +++ b/taiga/base/filters.py @@ -587,3 +587,22 @@ class QFilter(FilterBackend): queryset = queryset.extra(where=[where_clause], params=[to_tsquery(q)]) return queryset + + +class RoleFilter(BaseRelatedFieldsFilter): + filter_name = "role_id" + param_name = "role" + + def filter_queryset(self, request, queryset, view): + Membership = apps.get_model('projects', 'Membership') + query = self._get_queryparams(request.QUERY_PARAMS) + if query: + if isinstance(query, dict): + memberships = Membership.objects.filter(**query).values_list("user_id", flat=True) + queryset = queryset.filter(assigned_to__in=memberships) + else: + memberships = Membership.objects.filter(query).values_list("user_id", flat=True) + if memberships: + queryset = queryset.filter(assigned_to__in=memberships) + + return FilterBackend.filter_queryset(self, request, queryset, view) diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py index 11c877fc..af8368a7 100644 --- a/taiga/projects/issues/api.py +++ b/taiga/projects/issues/api.py @@ -50,6 +50,7 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W queryset = models.Issue.objects.all() permission_classes = (permissions.IssuePermission, ) filter_backends = (filters.CanViewIssuesFilterBackend, + filters.RoleFilter, filters.OwnersFilter, filters.AssignedToFilter, filters.StatusesFilter, @@ -188,6 +189,7 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W owners_filter_backends = (f for f in filter_backends if f != filters.OwnersFilter) priorities_filter_backends = (f for f in filter_backends if f != filters.PrioritiesFilter) severities_filter_backends = (f for f in filter_backends if f != filters.SeveritiesFilter) + roles_filter_backends = (f for f in filter_backends if f != filters.RoleFilter) queryset = self.get_queryset() querysets = { @@ -197,7 +199,8 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W "owners": self.filter_queryset(queryset, filter_backends=owners_filter_backends), "priorities": self.filter_queryset(queryset, filter_backends=priorities_filter_backends), "severities": self.filter_queryset(queryset, filter_backends=severities_filter_backends), - "tags": self.filter_queryset(queryset) + "tags": self.filter_queryset(queryset), + "roles": self.filter_queryset(queryset, filter_backends=roles_filter_backends), } return response.Ok(services.get_issues_filters_data(project, querysets)) diff --git a/taiga/projects/issues/services.py b/taiga/projects/issues/services.py index a2646d78..dbcb0d4e 100644 --- a/taiga/projects/issues/services.py +++ b/taiga/projects/issues/services.py @@ -420,6 +420,57 @@ def _get_issues_owners(project, queryset): return sorted(result, key=itemgetter("full_name")) +def _get_issues_roles(project, queryset): + compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) + queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) + where = queryset_where_tuple[0] + where_params = queryset_where_tuple[1] + + extra_sql = """ + WITH "issue_counters" AS ( + SELECT DISTINCT "issues_issue"."status_id" "status_id", + "issues_issue"."id" "issue_id", + "projects_membership"."role_id" "role_id" + FROM "issues_issue" + INNER JOIN "projects_project" + ON ("issues_issue"."project_id" = "projects_project"."id") + LEFT OUTER JOIN "projects_membership" + ON "projects_membership"."user_id" = "issues_issue"."assigned_to_id" + WHERE {where} + ), + "counters" AS ( + SELECT "role_id" as "role_id", + COUNT("role_id") "count" + FROM "issue_counters" + GROUP BY "role_id" + ) + + SELECT "users_role"."id", + "users_role"."name", + "users_role"."order", + COALESCE("counters"."count", 0) + FROM "users_role" + LEFT OUTER JOIN "counters" + ON "counters"."role_id" = "users_role"."id" + WHERE "users_role"."project_id" = %s + ORDER BY "users_role"."order"; + """.format(where=where) + + with closing(connection.cursor()) as cursor: + cursor.execute(extra_sql, where_params + [project.id]) + rows = cursor.fetchall() + + result = [] + for id, name, order, count in rows: + result.append({ + "id": id, + "name": _(name), + "color": None, + "order": order, + "count": count, + }) + return sorted(result, key=itemgetter("order")) + def _get_issues_tags(project, queryset): compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) @@ -478,6 +529,7 @@ def get_issues_filters_data(project, querysets): ("assigned_to", _get_issues_assigned_to(project, querysets["assigned_to"])), ("owners", _get_issues_owners(project, querysets["owners"])), ("tags", _get_issues_tags(project, querysets["tags"])), + ("roles", _get_issues_roles(project, querysets["roles"])), ]) return data diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index a2e62149..7b748e37 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -51,6 +51,7 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa queryset = models.Task.objects.all() permission_classes = (permissions.TaskPermission,) filter_backends = (filters.CanViewTasksFilterBackend, + filters.RoleFilter, filters.OwnersFilter, filters.AssignedToFilter, filters.StatusesFilter, @@ -219,13 +220,15 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa statuses_filter_backends = (f for f in filter_backends if f != filters.StatusesFilter) assigned_to_filter_backends = (f for f in filter_backends if f != filters.AssignedToFilter) owners_filter_backends = (f for f in filter_backends if f != filters.OwnersFilter) + roles_filter_backends = (f for f in filter_backends if f != filters.RoleFilter) queryset = self.get_queryset() querysets = { "statuses": self.filter_queryset(queryset, filter_backends=statuses_filter_backends), "assigned_to": self.filter_queryset(queryset, filter_backends=assigned_to_filter_backends), "owners": self.filter_queryset(queryset, filter_backends=owners_filter_backends), - "tags": self.filter_queryset(queryset) + "tags": self.filter_queryset(queryset), + "roles": self.filter_queryset(queryset, filter_backends=roles_filter_backends), } return response.Ok(services.get_tasks_filters_data(project, querysets)) diff --git a/taiga/projects/tasks/services.py b/taiga/projects/tasks/services.py index 52f1bb55..c0a6272e 100644 --- a/taiga/projects/tasks/services.py +++ b/taiga/projects/tasks/services.py @@ -279,6 +279,58 @@ def _get_tasks_assigned_to(project, queryset): return sorted(result, key=itemgetter("full_name")) +def _get_tasks_roles(project, queryset): + compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) + queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) + where = queryset_where_tuple[0] + where_params = queryset_where_tuple[1] + + extra_sql = """ + WITH "task_counters" AS ( + SELECT DISTINCT "tasks_task"."status_id" "status_id", + "tasks_task"."id" "us_id", + "projects_membership"."role_id" "role_id" + FROM "tasks_task" + INNER JOIN "projects_project" + ON ("tasks_task"."project_id" = "projects_project"."id") + LEFT OUTER JOIN "projects_membership" + ON "projects_membership"."user_id" = "tasks_task"."assigned_to_id" + WHERE {where} + ), + "counters" AS ( + SELECT "role_id" as "role_id", + COUNT("role_id") "count" + FROM "task_counters" + GROUP BY "role_id" + ) + + SELECT "users_role"."id", + "users_role"."name", + "users_role"."order", + COALESCE("counters"."count", 0) + FROM "users_role" + LEFT OUTER JOIN "counters" + ON "counters"."role_id" = "users_role"."id" + WHERE "users_role"."project_id" = %s + ORDER BY "users_role"."order"; + """.format(where=where) + + with closing(connection.cursor()) as cursor: + cursor.execute(extra_sql, where_params + [project.id]) + rows = cursor.fetchall() + + result = [] + for id, name, order, count in rows: + result.append({ + "id": id, + "name": _(name), + "color": None, + "order": order, + "count": count, + }) + return sorted(result, key=itemgetter("order")) + + def _get_tasks_owners(project, queryset): compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) @@ -384,6 +436,7 @@ def get_tasks_filters_data(project, querysets): ("assigned_to", _get_tasks_assigned_to(project, querysets["assigned_to"])), ("owners", _get_tasks_owners(project, querysets["owners"])), ("tags", _get_tasks_tags(project, querysets["tags"])), + ("roles", _get_tasks_roles(project, querysets["roles"])), ]) return data diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index 60256789..dc7cef82 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -62,6 +62,7 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi permission_classes = (permissions.UserStoryPermission,) filter_backends = (base_filters.CanViewUsFilterBackend, filters.EpicFilter, + base_filters.RoleFilter, base_filters.OwnersFilter, base_filters.AssignedToFilter, base_filters.StatusesFilter, @@ -306,6 +307,7 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi assigned_to_filter_backends = (f for f in filter_backends if f != base_filters.AssignedToFilter) owners_filter_backends = (f for f in filter_backends if f != base_filters.OwnersFilter) epics_filter_backends = (f for f in filter_backends if f != filters.EpicFilter) + roles_filter_backends = (f for f in filter_backends if f != base_filters.RoleFilter) queryset = self.get_queryset() querysets = { @@ -313,7 +315,8 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi "assigned_to": self.filter_queryset(queryset, filter_backends=assigned_to_filter_backends), "owners": self.filter_queryset(queryset, filter_backends=owners_filter_backends), "tags": self.filter_queryset(queryset), - "epics": self.filter_queryset(queryset, filter_backends=epics_filter_backends) + "epics": self.filter_queryset(queryset, filter_backends=epics_filter_backends), + "roles": self.filter_queryset(queryset, filter_backends=roles_filter_backends) } return response.Ok(services.get_userstories_filters_data(project, querysets)) diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py index efd66229..7295a208 100644 --- a/taiga/projects/userstories/services.py +++ b/taiga/projects/userstories/services.py @@ -589,6 +589,60 @@ def _get_userstories_epics(project, queryset): return result +def _get_userstories_roles(project, queryset): + compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) + queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) + where = queryset_where_tuple[0] + where_params = queryset_where_tuple[1] + + extra_sql = """ + WITH "us_counters" AS ( + SELECT DISTINCT "userstories_userstory"."status_id" "status_id", + "userstories_userstory"."id" "us_id", + "projects_membership"."role_id" "role_id" + FROM "userstories_userstory" + INNER JOIN "projects_project" + ON ("userstories_userstory"."project_id" = "projects_project"."id") + LEFT OUTER JOIN "epics_relateduserstory" + ON "userstories_userstory"."id" = "epics_relateduserstory"."user_story_id" + LEFT OUTER JOIN "projects_membership" + ON "projects_membership"."user_id" = "userstories_userstory"."assigned_to_id" + WHERE {where} + ), + "counters" AS ( + SELECT "role_id" as "role_id", + COUNT("role_id") "count" + FROM "us_counters" + GROUP BY "role_id" + ) + + SELECT "users_role"."id", + "users_role"."name", + "users_role"."order", + COALESCE("counters"."count", 0) + FROM "users_role" + LEFT OUTER JOIN "counters" + ON "counters"."role_id" = "users_role"."id" + WHERE "users_role"."project_id" = %s + ORDER BY "users_role"."order"; + """.format(where=where) + + with closing(connection.cursor()) as cursor: + cursor.execute(extra_sql, where_params + [project.id]) + rows = cursor.fetchall() + + result = [] + for id, name, order, count in rows: + result.append({ + "id": id, + "name": _(name), + "color": None, + "order": order, + "count": count, + }) + return sorted(result, key=itemgetter("order")) + + def get_userstories_filters_data(project, querysets): """ Given a project and an userstories queryset, return a simple data structure @@ -600,6 +654,7 @@ def get_userstories_filters_data(project, querysets): ("owners", _get_userstories_owners(project, querysets["owners"])), ("tags", _get_userstories_tags(project, querysets["tags"])), ("epics", _get_userstories_epics(project, querysets["epics"])), + ("roles", _get_userstories_roles(project, querysets["roles"])), ]) return data