From 21153ea1aa186bb44e6c69ef110bf430962ecbff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 10 Jun 2015 12:21:54 +0200 Subject: [PATCH] Improve userstories/filters_data and issues/filter_data --- CHANGELOG.md | 3 +- taiga/base/api/generics.py | 7 +- taiga/base/filters.py | 113 ++++++-- taiga/projects/api.py | 6 - taiga/projects/issues/api.py | 112 ++++---- taiga/projects/issues/permissions.py | 1 + taiga/projects/issues/services.py | 261 ++++++++++++++++++ taiga/projects/permissions.py | 1 - taiga/projects/services/__init__.py | 1 - taiga/projects/services/filters.py | 127 --------- taiga/projects/userstories/api.py | 50 +++- taiga/projects/userstories/permissions.py | 1 + taiga/projects/userstories/services.py | 146 ++++++++++ .../test_projects_resource.py | 19 -- tests/integration/test_issues.py | 192 +++++++++++++ tests/integration/test_userstories.py | 132 ++++++++- 16 files changed, 919 insertions(+), 253 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6049676a..7ae3a7e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,8 +15,9 @@ - Add polish (pl) translation. ### Misc -- API: Mixin fields 'users', 'members' and 'memberships' in ProjectDetailSerializer +- API: Mixin fields 'users', 'members' and 'memberships' in ProjectDetailSerializer. - API: Add stats/system resource with global server stats (total project, total users....) +- API: Improve and fix some errors in issues/filters_data and userstories/filters_data. - Lots of small and not so small bugfixes. diff --git a/taiga/base/api/generics.py b/taiga/base/api/generics.py index feeebf77..de834139 100644 --- a/taiga/base/api/generics.py +++ b/taiga/base/api/generics.py @@ -85,7 +85,7 @@ class GenericAPIView(pagination.PaginationMixin, many=many, partial=partial, context=context) - def filter_queryset(self, queryset): + def filter_queryset(self, queryset, filter_backends=None): """ Given a queryset, filter it with whichever filter backend is in use. @@ -94,7 +94,10 @@ class GenericAPIView(pagination.PaginationMixin, method if you want to apply the configured filtering backend to the default queryset. """ - for backend in self.get_filter_backends(): + #NOTE TAIGA: Added filter_backends to overwrite the default behavior. + + backends = filter_backends or self.get_filter_backends() + for backend in backends: queryset = backend().filter_queryset(self.request, queryset, self) return queryset diff --git a/taiga/base/filters.py b/taiga/base/filters.py index 208be999..cbfdbdef 100644 --- a/taiga/base/filters.py +++ b/taiga/base/filters.py @@ -29,6 +29,11 @@ logger = logging.getLogger(__name__) +##################################################################### +# Base and Mixins +##################################################################### + + class BaseFilterBackend(object): """ A base class from which all filter backend classes should inherit. @@ -95,6 +100,9 @@ class OrderByFilterMixin(QueryParamsFilterMixin): if field_name not in order_by_fields: return queryset + if raw_fieldname in ["owner", "-owner", "assigned_to", "-assigned_to"]: + raw_fieldname = "{}__full_name".format(raw_fieldname) + return super().filter_queryset(request, queryset.order_by(raw_fieldname), view) @@ -105,6 +113,10 @@ class FilterBackend(OrderByFilterMixin): pass +##################################################################### +# Permissions filters +##################################################################### + class PermissionBasedFilterBackend(FilterBackend): permission = None @@ -345,9 +357,84 @@ class IsProjectAdminFromWebhookLogFilterBackend(FilterBackend, BaseIsProjectAdmi return super().filter_queryset(request, queryset, view) +##################################################################### +# Generic Attributes filters +##################################################################### + +class BaseRelatedFieldsFilter(FilterBackend): + def __init__(self, filter_name=None): + if filter_name: + self.filter_name = filter_name + + def _prepare_filter_data(self, query_param_value): + def _transform_value(value): + try: + return int(value) + except: + if value in self._special_values_dict: + return self._special_values_dict[value] + raise exc.BadRequest() + + values = set([x.strip() for x in query_param_value.split(",")]) + values = map(_transform_value, values) + return list(values) + + def _get_queryparams(self, params): + raw_value = params.get(self.filter_name, None) + + if raw_value: + value = self._prepare_filter_data(raw_value) + + if None in value: + qs_in_kwargs = {"{}__in".format(self.filter_name): [v for v in value if v is not None]} + qs_isnull_kwargs = {"{}__isnull".format(self.filter_name): True} + return Q(**qs_in_kwargs) | Q(**qs_isnull_kwargs) + else: + return {"{}__in".format(self.filter_name): value} + + return None + + def filter_queryset(self, request, queryset, view): + query = self._get_queryparams(request.QUERY_PARAMS) + if query: + if isinstance(query, dict): + queryset = queryset.filter(**query) + else: + queryset = queryset.filter(query) + + return super().filter_queryset(request, queryset, view) + + +class OwnersFilter(BaseRelatedFieldsFilter): + filter_name = 'owner' + + +class AssignedToFilter(BaseRelatedFieldsFilter): + filter_name = 'assigned_to' + + +class StatusesFilter(BaseRelatedFieldsFilter): + filter_name = 'status' + + +class IssueTypesFilter(BaseRelatedFieldsFilter): + filter_name = 'type' + + +class PrioritiesFilter(BaseRelatedFieldsFilter): + filter_name = 'priority' + + +class SeveritiesFilter(BaseRelatedFieldsFilter): + filter_name = 'severity' + + class TagsFilter(FilterBackend): - def __init__(self, filter_name='tags'): - self.filter_name = filter_name + filter_name = 'tags' + + def __init__(self, filter_name=None): + if filter_name: + self.filter_name = filter_name def _get_tags_queryparams(self, params): tags = params.get(self.filter_name, None) @@ -364,25 +451,9 @@ class TagsFilter(FilterBackend): return super().filter_queryset(request, queryset, view) -class StatusFilter(FilterBackend): - def __init__(self, filter_name='status'): - self.filter_name = filter_name - - def _get_status_queryparams(self, params): - status = params.get(self.filter_name, None) - if status is not None: - status = set([x.strip() for x in status.split(",")]) - return list(status) - - return None - - def filter_queryset(self, request, queryset, view): - query_status = self._get_status_queryparams(request.QUERY_PARAMS) - if query_status: - queryset = queryset.filter(status__in=query_status) - - return super().filter_queryset(request, queryset, view) - +##################################################################### +# Text search filters +##################################################################### class QFilter(FilterBackend): def filter_queryset(self, request, queryset, view): diff --git a/taiga/projects/api.py b/taiga/projects/api.py index 975dd4ea..94941cb8 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -160,12 +160,6 @@ class ProjectViewSet(HistoryResourceMixin, ModelCrudViewSet): self.check_permissions(request, "issues_stats", project) return response.Ok(services.get_stats_for_project_issues(project)) - @detail_route(methods=["GET"]) - def issue_filters_data(self, request, pk=None): - project = self.get_object() - self.check_permissions(request, "issues_filters_data", project) - return response.Ok(services.get_issues_filters_data(project)) - @detail_route(methods=["GET"]) def tags_colors(self, request, pk=None): project = self.get_object() diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py index 6f59a93a..bb377e8d 100644 --- a/taiga/projects/issues/api.py +++ b/taiga/projects/issues/api.py @@ -42,79 +42,35 @@ from . import permissions from . import serializers -class IssuesFilter(filters.FilterBackend): - filter_fields = ("status", "severity", "priority", "owner", "assigned_to", "tags", "type") - _special_values_dict = { - 'true': True, - 'false': False, - 'null': None, - } - - def _prepare_filters_data(self, request): - def _transform_value(value): - try: - return int(value) - except: - if value in self._special_values_dict.keys(): - return self._special_values_dict[value] - raise exc.BadRequest() - - data = {} - for filtername in self.filter_fields: - if filtername not in request.QUERY_PARAMS: - continue - - raw_value = request.QUERY_PARAMS[filtername] - values = set([x.strip() for x in raw_value.split(",")]) - - if filtername != "tags": - values = map(_transform_value, values) - - data[filtername] = list(values) - return data - - def filter_queryset(self, request, queryset, view): - filterdata = self._prepare_filters_data(request) - - if "tags" in filterdata: - queryset = queryset.filter(tags__contains=filterdata["tags"]) - - for name, value in filter(lambda x: x[0] != "tags", filterdata.items()): - if None in value: - qs_in_kwargs = {"{0}__in".format(name): [v for v in value if v is not None]} - qs_isnull_kwargs = {"{0}__isnull".format(name): True} - queryset = queryset.filter(Q(**qs_in_kwargs) | Q(**qs_isnull_kwargs)) - else: - qs_kwargs = {"{0}__in".format(name): value} - queryset = queryset.filter(**qs_kwargs) - - return queryset - - -class IssuesOrdering(filters.FilterBackend): - def filter_queryset(self, request, queryset, view): - order_by = request.QUERY_PARAMS.get('order_by', None) - - if order_by in ['owner', '-owner', 'assigned_to', '-assigned_to']: - return queryset.order_by( - '{}__full_name'.format(order_by) - ) - return queryset - - class IssueViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet): serializer_class = serializers.IssueNeighborsSerializer list_serializer_class = serializers.IssueSerializer permission_classes = (permissions.IssuePermission, ) - filter_backends = (filters.CanViewIssuesFilterBackend, filters.QFilter, - IssuesFilter, IssuesOrdering,) - retrieve_exclude_filters = (IssuesFilter,) + filter_backends = (filters.CanViewIssuesFilterBackend, + filters.OwnersFilter, + filters.AssignedToFilter, + filters.StatusesFilter, + filters.IssueTypesFilter, + filters.SeveritiesFilter, + filters.PrioritiesFilter, + filters.TagsFilter, + filters.QFilter, + filters.OrderByFilterMixin) + retrieve_exclude_filters = (filters.OwnersFilter, + filters.AssignedToFilter, + filters.StatusesFilter, + filters.IssueTypesFilter, + filters.SeveritiesFilter, + filters.PrioritiesFilter, + filters.TagsFilter,) - filter_fields = ("project", "status__is_closed", "watchers") + filter_fields = ("project", + "status__is_closed", + "watchers") order_by_fields = ("type", - "severity", "status", + "severity", "priority", "created_date", "modified_date", @@ -218,6 +174,32 @@ class IssueViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, issue = get_object_or_404(models.Issue, ref=ref, project_id=project_id) return self.retrieve(request, pk=issue.pk) + @list_route(methods=["GET"]) + def filters_data(self, request, *args, **kwargs): + project_id = request.QUERY_PARAMS.get("project", None) + project = get_object_or_404(Project, id=project_id) + + filter_backends = self.get_filter_backends() + types_filter_backends = (f for f in filter_backends if f != filters.IssueTypesFilter) + 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) + 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) + tags_filter_backends = (f for f in filter_backends if f != filters.TagsFilter) + + queryset = self.get_queryset() + querysets = { + "types": self.filter_queryset(queryset, filter_backends=types_filter_backends), + "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), + "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) + } + return response.Ok(services.get_issues_filters_data(project, querysets)) + @list_route(methods=["GET"]) def csv(self, request): uuid = request.QUERY_PARAMS.get("uuid", None) diff --git a/taiga/projects/issues/permissions.py b/taiga/projects/issues/permissions.py index 0a106092..0eecf121 100644 --- a/taiga/projects/issues/permissions.py +++ b/taiga/projects/issues/permissions.py @@ -28,6 +28,7 @@ class IssuePermission(TaigaResourcePermission): update_perms = HasProjectPerm('modify_issue') destroy_perms = HasProjectPerm('delete_issue') list_perms = AllowAny() + filters_data_perms = AllowAny() csv_perms = AllowAny() upvote_perms = IsAuthenticated() & HasProjectPerm('vote_issues') downvote_perms = IsAuthenticated() & HasProjectPerm('vote_issues') diff --git a/taiga/projects/issues/services.py b/taiga/projects/issues/services.py index d659b527..7d6f3061 100644 --- a/taiga/projects/issues/services.py +++ b/taiga/projects/issues/services.py @@ -16,6 +16,12 @@ import io import csv +from collections import OrderedDict +from operator import itemgetter +from contextlib import closing + +from django.db import connection +from django.utils.translation import ugettext as _ from taiga.base.utils import db, text @@ -101,3 +107,258 @@ def issues_to_csv(project, queryset): writer.writerow(issue_data) return csv_data + + +def _get_issues_statuses(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 = """ + SELECT "projects_issuestatus"."id", + "projects_issuestatus"."name", + "projects_issuestatus"."color", + "projects_issuestatus"."order", + (SELECT count(*) + FROM "issues_issue" + INNER JOIN "projects_project" ON + ("issues_issue"."project_id" = "projects_project"."id") + WHERE {where} AND "issues_issue"."status_id" = "projects_issuestatus"."id") + FROM "projects_issuestatus" + WHERE "projects_issuestatus"."project_id" = %s + ORDER BY "projects_issuestatus"."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, color, order, count in rows: + result.append({ + "id": id, + "name": _(name), + "color": color, + "order": order, + "count": count, + }) + return sorted(result, key=itemgetter("order")) + + +def _get_issues_types(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 = """ + SELECT "projects_issuetype"."id", + "projects_issuetype"."name", + "projects_issuetype"."color", + "projects_issuetype"."order", + (SELECT count(*) + FROM "issues_issue" + INNER JOIN "projects_project" ON + ("issues_issue"."project_id" = "projects_project"."id") + WHERE {where} AND "issues_issue"."type_id" = "projects_issuetype"."id") + FROM "projects_issuetype" + WHERE "projects_issuetype"."project_id" = %s + ORDER BY "projects_issuetype"."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, color, order, count in rows: + result.append({ + "id": id, + "name": _(name), + "color": color, + "order": order, + "count": count, + }) + return sorted(result, key=itemgetter("order")) + + +def _get_issues_priorities(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 = """ + SELECT "projects_priority"."id", + "projects_priority"."name", + "projects_priority"."color", + "projects_priority"."order", + (SELECT count(*) + FROM "issues_issue" + INNER JOIN "projects_project" ON + ("issues_issue"."project_id" = "projects_project"."id") + WHERE {where} AND "issues_issue"."priority_id" = "projects_priority"."id") + FROM "projects_priority" + WHERE "projects_priority"."project_id" = %s + ORDER BY "projects_priority"."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, color, order, count in rows: + result.append({ + "id": id, + "name": _(name), + "color": color, + "order": order, + "count": count, + }) + return sorted(result, key=itemgetter("order")) + + +def _get_issues_severities(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 = """ + SELECT "projects_severity"."id", + "projects_severity"."name", + "projects_severity"."color", + "projects_severity"."order", + (SELECT count(*) + FROM "issues_issue" + INNER JOIN "projects_project" ON + ("issues_issue"."project_id" = "projects_project"."id") + WHERE {where} AND "issues_issue"."severity_id" = "projects_severity"."id") + FROM "projects_severity" + WHERE "projects_severity"."project_id" = %s + ORDER BY "projects_severity"."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, color, order, count in rows: + result.append({ + "id": id, + "name": _(name), + "color": color, + "order": order, + "count": count, + }) + return sorted(result, key=itemgetter("order")) + + +def _get_issues_assigned_to(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 = """ + SELECT NULL, + NULL, + (SELECT count(*) + FROM "issues_issue" + INNER JOIN "projects_project" ON + ("issues_issue"."project_id" = "projects_project"."id" ) + WHERE {where} AND "issues_issue"."assigned_to_id" IS NULL) + UNION SELECT "users_user"."id", + "users_user"."full_name", + (SELECT count(*) + FROM "issues_issue" + INNER JOIN "projects_project" ON + ("issues_issue"."project_id" = "projects_project"."id" ) + WHERE {where} AND "issues_issue"."assigned_to_id" = "projects_membership"."user_id") + FROM "projects_membership" + INNER JOIN "users_user" ON + ("projects_membership"."user_id" = "users_user"."id") + WHERE "projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL; + """.format(where=where) + + with closing(connection.cursor()) as cursor: + cursor.execute(extra_sql, where_params + where_params + [project.id]) + rows = cursor.fetchall() + + result = [] + for id, full_name, count in rows: + result.append({ + "id": id, + "full_name": full_name or "", + "count": count, + }) + return sorted(result, key=itemgetter("full_name")) + + +def _get_issues_owners(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 = """ + SELECT "users_user"."id", + "users_user"."full_name", + (SELECT count(*) + FROM "issues_issue" + INNER JOIN "projects_project" ON + ("issues_issue"."project_id" = "projects_project"."id") + WHERE {where} and "issues_issue"."owner_id" = "projects_membership"."user_id") + FROM "projects_membership" + RIGHT OUTER JOIN "users_user" ON + ("projects_membership"."user_id" = "users_user"."id") + WHERE ("projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL) + OR ("users_user"."is_system" IS TRUE); + """.format(where=where) + + with closing(connection.cursor()) as cursor: + cursor.execute(extra_sql, where_params + [project.id]) + rows = cursor.fetchall() + + result = [] + for id, full_name, count in rows: + if count > 0: + result.append({ + "id": id, + "full_name": full_name, + "count": count, + }) + return sorted(result, key=itemgetter("full_name")) + + +def _get_issues_tags(queryset): + tags = [] + for t_list in queryset.values_list("tags", flat=True): + if t_list is None: + continue + tags += list(t_list) + + tags = [{"name":e, "count":tags.count(e)} for e in set(tags)] + + return sorted(tags, key=itemgetter("name")) + + +def get_issues_filters_data(project, querysets): + """ + Given a project and an issues queryset, return a simple data structure + of all possible filters for the issues in the queryset. + """ + data = OrderedDict([ + ("types", _get_issues_types(project, querysets["types"])), + ("statuses", _get_issues_statuses(project, querysets["statuses"])), + ("priorities", _get_issues_priorities(project, querysets["priorities"])), + ("severities", _get_issues_severities(project, querysets["severities"])), + ("assigned_to", _get_issues_assigned_to(project, querysets["assigned_to"])), + ("owners", _get_issues_owners(project, querysets["owners"])), + ("tags", _get_issues_tags(querysets["tags"])), + ]) + + return data diff --git a/taiga/projects/permissions.py b/taiga/projects/permissions.py index 5483e3d6..943ef379 100644 --- a/taiga/projects/permissions.py +++ b/taiga/projects/permissions.py @@ -60,7 +60,6 @@ class ProjectPermission(TaigaResourcePermission): star_perms = IsAuthenticated() unstar_perms = IsAuthenticated() issues_stats_perms = HasProjectPerm('view_project') - issues_filters_data_perms = HasProjectPerm('view_project') tags_perms = HasProjectPerm('view_project') tags_colors_perms = HasProjectPerm('view_project') fans_perms = HasProjectPerm('view_project') diff --git a/taiga/projects/services/__init__.py b/taiga/projects/services/__init__.py index 12b4276c..8a7284d9 100644 --- a/taiga/projects/services/__init__.py +++ b/taiga/projects/services/__init__.py @@ -27,7 +27,6 @@ from .bulk_update_order import bulk_update_points_order from .bulk_update_order import bulk_update_userstory_status_order from .filters import get_all_tags -from .filters import get_issues_filters_data from .stats import get_stats_for_project_issues from .stats import get_stats_for_project diff --git a/taiga/projects/services/filters.py b/taiga/projects/services/filters.py index e8c2786c..5a2d1ff3 100644 --- a/taiga/projects/services/filters.py +++ b/taiga/projects/services/filters.py @@ -50,113 +50,6 @@ def _get_issues_tags(project): return result -def _get_issues_tags_with_count(project): - extra_sql = ("select unnest(tags) as tagname, count(unnest(tags)) " - "from issues_issue where project_id = %s " - "group by unnest(tags) " - "order by tagname asc") - - with closing(connection.cursor()) as cursor: - cursor.execute(extra_sql, [project.id]) - rows = cursor.fetchall() - - return rows - - -def _get_issues_statuses(project): - extra_sql = ("select status_id, count(status_id) from issues_issue " - "where project_id = %s group by status_id;") - - extra_sql = """ - select id, (select count(*) from issues_issue - where project_id = m.project_id and status_id = m.id) - from projects_issuestatus as m - where project_id = %s order by m.order; - """ - - with closing(connection.cursor()) as cursor: - cursor.execute(extra_sql, [project.id]) - rows = cursor.fetchall() - - return rows - - -def _get_issues_priorities(project): - extra_sql = """ - select id, (select count(*) from issues_issue - where project_id = m.project_id and priority_id = m.id) - from projects_priority as m - where project_id = %s order by m.order; - """ - - with closing(connection.cursor()) as cursor: - cursor.execute(extra_sql, [project.id]) - rows = cursor.fetchall() - - return rows - -def _get_issues_types(project): - extra_sql = """ - select id, (select count(*) from issues_issue - where project_id = m.project_id and type_id = m.id) - from projects_issuetype as m - where project_id = %s order by m.order; - """ - - with closing(connection.cursor()) as cursor: - cursor.execute(extra_sql, [project.id]) - rows = cursor.fetchall() - - return rows - - -def _get_issues_severities(project): - extra_sql = """ - select id, (select count(*) from issues_issue - where project_id = m.project_id and severity_id = m.id) - from projects_severity as m - where project_id = %s order by m.order; - """ - - with closing(connection.cursor()) as cursor: - cursor.execute(extra_sql, [project.id]) - rows = cursor.fetchall() - - return rows - - -def _get_issues_assigned_to(project): - extra_sql = """ - select null, (select count(*) from issues_issue - where project_id = %s and assigned_to_id is null) - UNION select user_id, (select count(*) from issues_issue - where project_id = pm.project_id and assigned_to_id = pm.user_id) - from projects_membership as pm - where project_id = %s and pm.user_id is not null; - """ - - with closing(connection.cursor()) as cursor: - cursor.execute(extra_sql, [project.id, project.id]) - rows = cursor.fetchall() - - return rows - - -def _get_issues_owners(project): - extra_sql = """ - select user_id, (select count(*) from issues_issue - where project_id = pm.project_id and owner_id = pm.user_id) - from projects_membership as pm - where project_id = %s and pm.user_id is not null; - """ - - with closing(connection.cursor()) as cursor: - cursor.execute(extra_sql, [project.id]) - rows = cursor.fetchall() - - return rows - - # Public api def get_all_tags(project): @@ -170,23 +63,3 @@ def get_all_tags(project): result.update(_get_stories_tags(project)) result.update(_get_tasks_tags(project)) return sorted(result) - - -def get_issues_filters_data(project): - """ - Given a project, return a simple data structure - of all possible filters for issues. - """ - - data = { - "types": _get_issues_types(project), - "statuses": _get_issues_statuses(project), - "priorities": _get_issues_priorities(project), - "severities": _get_issues_severities(project), - "assigned_to": _get_issues_assigned_to(project), - "created_by": _get_issues_owners(project), - "owners": _get_issues_owners(project), - "tags": _get_issues_tags_with_count(project), - } - - return data diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index 9820f589..0467ef16 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -49,15 +49,27 @@ class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMi serializer_class = serializers.UserStoryNeighborsSerializer list_serializer_class = serializers.UserStorySerializer permission_classes = (permissions.UserStoryPermission,) - - filter_backends = (filters.StatusFilter, filters.CanViewUsFilterBackend, filters.TagsFilter, - filters.QFilter, filters.OrderByFilterMixin) - - retrieve_exclude_filters = (filters.StatusFilter, filters.TagsFilter,) - filter_fields = ["project", "milestone", "milestone__isnull", - "is_archived", "status__is_archived", "assigned_to", - "status__is_closed", "watchers", "is_closed"] - order_by_fields = ["backlog_order", "sprint_order", "kanban_order"] + filter_backends = (filters.CanViewUsFilterBackend, + filters.OwnersFilter, + filters.AssignedToFilter, + filters.StatusesFilter, + filters.TagsFilter, + filters.QFilter, + filters.OrderByFilterMixin) + retrieve_exclude_filters = (filters.OwnersFilter, + filters.AssignedToFilter, + filters.StatusesFilter, + filters.TagsFilter) + filter_fields = ["project", + "milestone", + "milestone__isnull", + "is_closed", + "status__is_archived", + "status__is_closed", + "watchers"] + order_by_fields = ["backlog_order", + "sprint_order", + "kanban_order"] # Specific filter used for filtering neighbor user stories _neighbor_tags_filter = filters.TagsFilter('neighbor_tags') @@ -138,6 +150,26 @@ class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMi raise exc.PermissionDenied(_("You don't have permissions to set this status " "to this user story.")) + @list_route(methods=["GET"]) + def filters_data(self, request, *args, **kwargs): + project_id = request.QUERY_PARAMS.get("project", None) + project = get_object_or_404(Project, id=project_id) + + filter_backends = self.get_filter_backends() + 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) + tags_filter_backends = (f for f in filter_backends if f != filters.TagsFilter) + + 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) + } + return response.Ok(services.get_userstories_filters_data(project, querysets)) + @list_route(methods=["GET"]) def by_ref(self, request): ref = request.QUERY_PARAMS.get("ref", None) diff --git a/taiga/projects/userstories/permissions.py b/taiga/projects/userstories/permissions.py index 3b836cb6..32c14e01 100644 --- a/taiga/projects/userstories/permissions.py +++ b/taiga/projects/userstories/permissions.py @@ -25,6 +25,7 @@ class UserStoryPermission(TaigaResourcePermission): update_perms = HasProjectPerm('modify_us') destroy_perms = HasProjectPerm('delete_us') list_perms = AllowAny() + filters_data_perms = AllowAny() csv_perms = AllowAny() bulk_create_perms = IsAuthenticated() & (HasProjectPerm('add_us_to_project') | HasProjectPerm('add_us')) bulk_update_order_perms = HasProjectPerm('modify_us') diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py index 13624162..9de3bf53 100644 --- a/taiga/projects/userstories/services.py +++ b/taiga/projects/userstories/services.py @@ -16,8 +16,13 @@ import csv import io +from collections import OrderedDict +from operator import itemgetter +from contextlib import closing +from django.db import connection from django.utils import timezone +from django.utils.translation import ugettext as _ from taiga.base.utils import db, text from taiga.projects.history.services import take_snapshot @@ -170,3 +175,144 @@ def userstories_to_csv(project,queryset): writer.writerow(row) return csv_data + + +def _get_userstories_statuses(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 = """ + SELECT "projects_userstorystatus"."id", + "projects_userstorystatus"."name", + "projects_userstorystatus"."color", + "projects_userstorystatus"."order", + (SELECT count(*) + FROM "userstories_userstory" + INNER JOIN "projects_project" ON + ("userstories_userstory"."project_id" = "projects_project"."id") + WHERE {where} AND "userstories_userstory"."status_id" = "projects_userstorystatus"."id") + FROM "projects_userstorystatus" + WHERE "projects_userstorystatus"."project_id" = %s + ORDER BY "projects_userstorystatus"."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, color, order, count in rows: + result.append({ + "id": id, + "name": _(name), + "color": color, + "order": order, + "count": count, + }) + return sorted(result, key=itemgetter("order")) + + +def _get_userstories_assigned_to(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 = """ + SELECT NULL, + NULL, + (SELECT count(*) + FROM "userstories_userstory" + INNER JOIN "projects_project" ON + ("userstories_userstory"."project_id" = "projects_project"."id" ) + WHERE {where} AND "userstories_userstory"."assigned_to_id" IS NULL) + UNION SELECT "users_user"."id", + "users_user"."full_name", + (SELECT count(*) + FROM "userstories_userstory" + INNER JOIN "projects_project" ON + ("userstories_userstory"."project_id" = "projects_project"."id" ) + WHERE {where} AND "userstories_userstory"."assigned_to_id" = "projects_membership"."user_id") + FROM "projects_membership" + INNER JOIN "users_user" ON + ("projects_membership"."user_id" = "users_user"."id") + WHERE "projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL; + """.format(where=where) + + with closing(connection.cursor()) as cursor: + cursor.execute(extra_sql, where_params + where_params + [project.id]) + rows = cursor.fetchall() + + result = [] + for id, full_name, count in rows: + result.append({ + "id": id, + "full_name": full_name or "", + "count": count, + }) + return sorted(result, key=itemgetter("full_name")) + + +def _get_userstories_owners(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 = """ + SELECT "users_user"."id", + "users_user"."full_name", + (SELECT count(*) + FROM "userstories_userstory" + INNER JOIN "projects_project" ON + ("userstories_userstory"."project_id" = "projects_project"."id") + WHERE {where} AND "userstories_userstory"."owner_id" = "projects_membership"."user_id") + FROM "projects_membership" + RIGHT OUTER JOIN "users_user" ON + ("projects_membership"."user_id" = "users_user"."id") + WHERE (("projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL) + OR ("users_user"."is_system" IS TRUE)); + """.format(where=where) + + with closing(connection.cursor()) as cursor: + cursor.execute(extra_sql, where_params + [project.id]) + rows = cursor.fetchall() + + result = [] + for id, full_name, count in rows: + if count > 0: + result.append({ + "id": id, + "full_name": full_name, + "count": count, + }) + return sorted(result, key=itemgetter("full_name")) + + +def _get_userstories_tags(queryset): + tags = [] + for t_list in queryset.values_list("tags", flat=True): + if t_list is None: + continue + tags += list(t_list) + + tags = [{"name":e, "count":tags.count(e)} for e in set(tags)] + + return sorted(tags, key=itemgetter("name")) + + +def get_userstories_filters_data(project, querysets): + """ + Given a project and an userstories queryset, return a simple data structure + of all possible filters for the userstories in the queryset. + """ + data = OrderedDict([ + ("statuses", _get_userstories_statuses(project, querysets["statuses"])), + ("assigned_to", _get_userstories_assigned_to(project, querysets["assigned_to"])), + ("owners", _get_userstories_owners(project, querysets["owners"])), + ("tags", _get_userstories_tags(querysets["tags"])), + ]) + + return data diff --git a/tests/integration/resources_permissions/test_projects_resource.py b/tests/integration/resources_permissions/test_projects_resource.py index 0c97c952..575e0d62 100644 --- a/tests/integration/resources_permissions/test_projects_resource.py +++ b/tests/integration/resources_permissions/test_projects_resource.py @@ -255,25 +255,6 @@ def test_project_action_issues_stats(client, data): assert results == [404, 404, 200, 200] -def test_project_action_issues_filters_data(client, data): - public_url = reverse('projects-issue-filters-data', kwargs={"pk": data.public_project.pk}) - private1_url = reverse('projects-issue-filters-data', kwargs={"pk": data.private_project1.pk}) - private2_url = reverse('projects-issue-filters-data', kwargs={"pk": data.private_project2.pk}) - - users = [ - None, - data.registered_user, - data.project_member_with_perms, - data.project_owner - ] - results = helper_test_http_method(client, 'get', public_url, None, users) - assert results == [200, 200, 200, 200] - results = helper_test_http_method(client, 'get', private1_url, None, users) - assert results == [200, 200, 200, 200] - results = helper_test_http_method(client, 'get', private2_url, None, users) - assert results == [404, 404, 200, 200] - - def test_project_action_fans(client, data): public_url = reverse('projects-fans', kwargs={"pk": data.public_project.pk}) private1_url = reverse('projects-fans', kwargs={"pk": data.private_project1.pk}) diff --git a/tests/integration/test_issues.py b/tests/integration/test_issues.py index a3aa9db0..84727427 100644 --- a/tests/integration/test_issues.py +++ b/tests/integration/test_issues.py @@ -189,6 +189,198 @@ def test_api_filter_by_text_6(client): assert response.status_code == 200 assert number_of_issues == 1 +def test_api_filters_data(client): + project = f.ProjectFactory.create() + user1 = f.UserFactory.create(is_superuser=True) + f.MembershipFactory.create(user=user1, project=project) + user2 = f.UserFactory.create(is_superuser=True) + f.MembershipFactory.create(user=user2, project=project) + user3 = f.UserFactory.create(is_superuser=True) + f.MembershipFactory.create(user=user3, project=project) + + status0 = f.IssueStatusFactory.create(project=project) + status1 = f.IssueStatusFactory.create(project=project) + status2 = f.IssueStatusFactory.create(project=project) + status3 = f.IssueStatusFactory.create(project=project) + + type1 = f.IssueTypeFactory.create(project=project) + type2 = f.IssueTypeFactory.create(project=project) + + severity0 = f.SeverityFactory.create(project=project) + severity1 = f.SeverityFactory.create(project=project) + severity2 = f.SeverityFactory.create(project=project) + severity3 = f.SeverityFactory.create(project=project) + + priority0 = f.PriorityFactory.create(project=project) + priority1 = f.PriorityFactory.create(project=project) + priority2 = f.PriorityFactory.create(project=project) + priority3 = f.PriorityFactory.create(project=project) + + tag0 = "test1test2test3" + tag1 = "test1" + tag2 = "test2" + tag3 = "test3" + + # ------------------------------------------------------------------------------------------------ + # | Issue | Owner | Assigned To | Status | Type | Priority | Severity | Tags | + # |-------#--------#-------------#---------#-------#-----------#-----------#---------------------| + # | 0 | user2 | None | status3 | type1 | priority2 | severity1 | tag1 | + # | 1 | user1 | None | status3 | type2 | priority2 | severity1 | tag2 | + # | 2 | user3 | None | status1 | type1 | priority3 | severity2 | tag1 tag2 | + # | 3 | user2 | None | status0 | type2 | priority3 | severity1 | tag3 | + # | 4 | user1 | user1 | status0 | type1 | priority2 | severity3 | tag1 tag2 tag3 | + # | 5 | user3 | user1 | status2 | type2 | priority3 | severity2 | tag3 | + # | 6 | user2 | user1 | status3 | type1 | priority2 | severity0 | tag1 tag2 | + # | 7 | user1 | user2 | status0 | type2 | priority1 | severity3 | tag3 | + # | 8 | user3 | user2 | status3 | type1 | priority0 | severity1 | tag1 | + # | 9 | user2 | user3 | status1 | type2 | priority0 | severity2 | tag0 | + # ------------------------------------------------------------------------------------------------ + + issue0 = f.IssueFactory.create(project=project, owner=user2, assigned_to=None, + status=status3, type=type1, priority=priority2, severity=severity1, + tags=[tag1]) + issue1 = f.IssueFactory.create(project=project, owner=user1, assigned_to=None, + status=status3, type=type2, priority=priority2, severity=severity1, + tags=[tag2]) + issue2 = f.IssueFactory.create(project=project, owner=user3, assigned_to=None, + status=status1, type=type1, priority=priority3, severity=severity2, + tags=[tag1, tag2]) + issue3 = f.IssueFactory.create(project=project, owner=user2, assigned_to=None, + status=status0, type=type2, priority=priority3, severity=severity1, + tags=[tag3]) + issue4 = f.IssueFactory.create(project=project, owner=user1, assigned_to=user1, + status=status0, type=type1, priority=priority2, severity=severity3, + tags=[tag1, tag2, tag3]) + issue5 = f.IssueFactory.create(project=project, owner=user3, assigned_to=user1, + status=status2, type=type2, priority=priority3, severity=severity2, + tags=[tag3]) + issue6 = f.IssueFactory.create(project=project, owner=user2, assigned_to=user1, + status=status3, type=type1, priority=priority2, severity=severity0, + tags=[tag1, tag2]) + issue7 = f.IssueFactory.create(project=project, owner=user1, assigned_to=user2, + status=status0, type=type2, priority=priority1, severity=severity3, + tags=[tag3]) + issue8 = f.IssueFactory.create(project=project, owner=user3, assigned_to=user2, + status=status3, type=type1, priority=priority0, severity=severity1, + tags=[tag1]) + issue9 = f.IssueFactory.create(project=project, owner=user2, assigned_to=user3, + status=status1, type=type2, priority=priority0, severity=severity2, + tags=[tag0]) + + url = reverse("issues-filters-data") + "?project={}".format(project.id) + + client.login(user1) + + ## No filter + response = client.get(url) + assert response.status_code == 200 + + assert next(filter(lambda i: i['id'] == user1.id, response.data["owners"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 4 + assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 3 + + assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 4 + assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 1 + + assert next(filter(lambda i: i['id'] == status0.id, response.data["statuses"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == status1.id, response.data["statuses"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == status2.id, response.data["statuses"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == status3.id, response.data["statuses"]))["count"] == 4 + + assert next(filter(lambda i: i['id'] == type1.id, response.data["types"]))["count"] == 5 + assert next(filter(lambda i: i['id'] == type2.id, response.data["types"]))["count"] == 5 + + assert next(filter(lambda i: i['id'] == priority0.id, response.data["priorities"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == priority1.id, response.data["priorities"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == priority2.id, response.data["priorities"]))["count"] == 4 + assert next(filter(lambda i: i['id'] == priority3.id, response.data["priorities"]))["count"] == 3 + + assert next(filter(lambda i: i['id'] == severity0.id, response.data["severities"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == severity1.id, response.data["severities"]))["count"] == 4 + assert next(filter(lambda i: i['id'] == severity2.id, response.data["severities"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == severity3.id, response.data["severities"]))["count"] == 2 + + assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 1 + assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 5 + assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 4 + assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 4 + + ## Filter ((status0 or status3) and type1) + response = client.get(url + "&status={},{}&type={}".format(status3.id, status0.id, type1.id)) + assert response.status_code == 200 + + assert next(filter(lambda i: i['id'] == user1.id, response.data["owners"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 1 + + assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 0 + + assert next(filter(lambda i: i['id'] == status0.id, response.data["statuses"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == status1.id, response.data["statuses"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == status2.id, response.data["statuses"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == status3.id, response.data["statuses"]))["count"] == 3 + + assert next(filter(lambda i: i['id'] == type1.id, response.data["types"]))["count"] == 4 + assert next(filter(lambda i: i['id'] == type2.id, response.data["types"]))["count"] == 3 + + assert next(filter(lambda i: i['id'] == priority0.id, response.data["priorities"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == priority1.id, response.data["priorities"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == priority2.id, response.data["priorities"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == priority3.id, response.data["priorities"]))["count"] == 0 + + assert next(filter(lambda i: i['id'] == severity0.id, response.data["severities"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == severity1.id, response.data["severities"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == severity2.id, response.data["severities"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == severity3.id, response.data["severities"]))["count"] == 1 + + with pytest.raises(StopIteration): + assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 + assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 4 + assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 2 + assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 1 + + ## Filter ((tag1 and tag2) and (user1 or user2)) + response = client.get(url + "&tags={},{}&owner={},{}".format(tag1, tag2, user1.id, user2.id)) + assert response.status_code == 200 + + assert next(filter(lambda i: i['id'] == user1.id, response.data["owners"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 1 + + assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 0 + + assert next(filter(lambda i: i['id'] == status0.id, response.data["statuses"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == status1.id, response.data["statuses"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == status2.id, response.data["statuses"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == status3.id, response.data["statuses"]))["count"] == 1 + + assert next(filter(lambda i: i['id'] == type1.id, response.data["types"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == type2.id, response.data["types"]))["count"] == 0 + + assert next(filter(lambda i: i['id'] == priority0.id, response.data["priorities"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == priority1.id, response.data["priorities"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == priority2.id, response.data["priorities"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == priority3.id, response.data["priorities"]))["count"] == 0 + + assert next(filter(lambda i: i['id'] == severity0.id, response.data["severities"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == severity1.id, response.data["severities"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == severity2.id, response.data["severities"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == severity3.id, response.data["severities"]))["count"] == 1 + + with pytest.raises(StopIteration): + assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 + assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 2 + assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 2 + assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 1 + def test_get_invalid_csv(client): url = reverse("issues-csv") diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index 90b85444..c072b76c 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.py @@ -244,7 +244,6 @@ def test_filter_by_multiple_status(client): url = reverse("userstories-list") url = "{}?status={},{}".format(reverse("userstories-list"), us1.status.id, us2.status.id) - data = {} response = client.get(url, data) assert len(response.data) == 2 @@ -282,6 +281,137 @@ def test_get_total_points(client): assert us_mixed.get_total_points() == 1.0 +def test_api_filters_data(client): + project = f.ProjectFactory.create() + user1 = f.UserFactory.create(is_superuser=True) + f.MembershipFactory.create(user=user1, project=project) + user2 = f.UserFactory.create(is_superuser=True) + f.MembershipFactory.create(user=user2, project=project) + user3 = f.UserFactory.create(is_superuser=True) + f.MembershipFactory.create(user=user3, project=project) + + status0 = f.UserStoryStatusFactory.create(project=project) + status1 = f.UserStoryStatusFactory.create(project=project) + status2 = f.UserStoryStatusFactory.create(project=project) + status3 = f.UserStoryStatusFactory.create(project=project) + + tag0 = "test1test2test3" + tag1 = "test1" + tag2 = "test2" + tag3 = "test3" + + # ------------------------------------------------------ + # | US | Owner | Assigned To | Tags | + # |-------#--------#-------------#---------------------| + # | 0 | user2 | None | tag1 | + # | 1 | user1 | None | tag2 | + # | 2 | user3 | None | tag1 tag2 | + # | 3 | user2 | None | tag3 | + # | 4 | user1 | user1 | tag1 tag2 tag3 | + # | 5 | user3 | user1 | tag3 | + # | 6 | user2 | user1 | tag1 tag2 | + # | 7 | user1 | user2 | tag3 | + # | 8 | user3 | user2 | tag1 | + # | 9 | user2 | user3 | tag0 | + # ------------------------------------------------------ + + user_story0 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None, + status=status3, tags=[tag1]) + user_story1 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=None, + status=status3, tags=[tag2]) + user_story2 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=None, + status=status1, tags=[tag1, tag2]) + user_story3 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None, + status=status0, tags=[tag3]) + user_story4 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user1, + status=status0, tags=[tag1, tag2, tag3]) + user_story5 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user1, + status=status2, tags=[tag3]) + user_story6 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user1, + status=status3, tags=[tag1, tag2]) + user_story7 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user2, + status=status0, tags=[tag3]) + user_story8 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user2, + status=status3, tags=[tag1]) + user_story9 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user3, + status=status1, tags=[tag0]) + + url = reverse("userstories-filters-data") + "?project={}".format(project.id) + + client.login(user1) + + ## No filter + response = client.get(url) + assert response.status_code == 200 + + assert next(filter(lambda i: i['id'] == user1.id, response.data["owners"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 4 + assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 3 + + assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 4 + assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 1 + + assert next(filter(lambda i: i['id'] == status0.id, response.data["statuses"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == status1.id, response.data["statuses"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == status2.id, response.data["statuses"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == status3.id, response.data["statuses"]))["count"] == 4 + + assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 1 + assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 5 + assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 4 + assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 4 + + ## Filter ((status0 or status3) + response = client.get(url + "&status={},{}".format(status3.id, status0.id)) + assert response.status_code == 200 + + assert next(filter(lambda i: i['id'] == user1.id, response.data["owners"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 1 + + assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 0 + + assert next(filter(lambda i: i['id'] == status0.id, response.data["statuses"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == status1.id, response.data["statuses"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == status2.id, response.data["statuses"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == status3.id, response.data["statuses"]))["count"] == 4 + + with pytest.raises(StopIteration): + assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 + assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 4 + assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 3 + assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 3 + + ## Filter ((tag1 and tag2) and (user1 or user2)) + response = client.get(url + "&tags={},{}&owner={},{}".format(tag1, tag2, user1.id, user2.id)) + assert response.status_code == 200 + + assert next(filter(lambda i: i['id'] == user1.id, response.data["owners"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == user2.id, response.data["owners"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == user3.id, response.data["owners"]))["count"] == 1 + + assert next(filter(lambda i: i['id'] == None, response.data["assigned_to"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == user1.id, response.data["assigned_to"]))["count"] == 2 + assert next(filter(lambda i: i['id'] == user2.id, response.data["assigned_to"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == user3.id, response.data["assigned_to"]))["count"] == 0 + + assert next(filter(lambda i: i['id'] == status0.id, response.data["statuses"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == status1.id, response.data["statuses"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == status2.id, response.data["statuses"]))["count"] == 0 + assert next(filter(lambda i: i['id'] == status3.id, response.data["statuses"]))["count"] == 1 + + with pytest.raises(StopIteration): + assert next(filter(lambda i: i['name'] == tag0, response.data["tags"]))["count"] == 0 + assert next(filter(lambda i: i['name'] == tag1, response.data["tags"]))["count"] == 2 + assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 2 + assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 1 + + def test_get_invalid_csv(client): url = reverse("userstories-csv")