diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index df1b495a..6ee1d72c 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -59,7 +59,6 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi queryset = models.UserStory.objects.all() permission_classes = (permissions.UserStoryPermission,) filter_backends = (base_filters.CanViewUsFilterBackend, - filters.EpicsFilter, filters.EpicFilter, base_filters.OwnersFilter, base_filters.AssignedToFilter, @@ -104,7 +103,13 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi include_attachments = "include_attachments" in self.request.QUERY_PARAMS include_tasks = "include_tasks" in self.request.QUERY_PARAMS - epic_id = self.request.QUERY_PARAMS.get("epic", None) + epic_id = self.request.QUERY_PARAMS.get("epic", None) + # We can be filtering by more than one epic so epic_id can consist + # of different ids separete by comma. In that situation we will use + # only the first + if epic_id is not None: + epic_id = epic_id.split(",")[0] + qs = attach_extra_info(qs, user=self.request.user, include_attachments=include_attachments, include_tasks=include_tasks, @@ -278,7 +283,7 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi statuses_filter_backends = (f for f in filter_backends if f != base_filters.StatusesFilter) 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.EpicsFilter) + epics_filter_backends = (f for f in filter_backends if f != filters.EpicFilter) queryset = self.get_queryset() querysets = { diff --git a/taiga/projects/userstories/filters.py b/taiga/projects/userstories/filters.py index f061667e..ec19b6a7 100644 --- a/taiga/projects/userstories/filters.py +++ b/taiga/projects/userstories/filters.py @@ -23,7 +23,3 @@ from taiga.base import filters class EpicFilter(filters.BaseRelatedFieldsFilter): filter_name = "epics" param_name = "epic" - - -class EpicsFilter(filters.BaseRelatedFieldsFilter): - filter_name = "epics" diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py index 0d781429..7cb0be4f 100644 --- a/taiga/projects/userstories/services.py +++ b/taiga/projects/userstories/services.py @@ -273,18 +273,31 @@ def _get_userstories_statuses(project, queryset): 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"; + WITH "us_counters" AS ( + SELECT DISTINCT "userstories_userstory"."status_id" "status_id", + "userstories_userstory"."id" "us_id" + FROM "userstories_userstory" + LEFT JOIN "epics_relateduserstory" + ON "userstories_userstory"."id" = "epics_relateduserstory"."user_story_id" + WHERE {where} + ), + "counters" AS ( + SELECT "status_id", + COUNT("status_id") "count" + FROM "us_counters" + GROUP BY "status_id" + ) + + SELECT "projects_userstorystatus"."id", + "projects_userstorystatus"."name", + "projects_userstorystatus"."color", + "projects_userstorystatus"."order", + COALESCE("counters"."count", 0) + FROM "projects_userstorystatus" + LEFT JOIN "counters" + ON "counters"."status_id" = "projects_userstorystatus"."id" + WHERE "projects_userstorystatus"."project_id" = %s + ORDER BY "projects_userstorystatus"."order"; """.format(where=where) with closing(connection.cursor()) as cursor: @@ -310,31 +323,47 @@ def _get_userstories_assigned_to(project, queryset): where_params = queryset_where_tuple[1] extra_sql = """ - WITH counters AS ( - SELECT assigned_to_id, count(assigned_to_id) 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 NOT NULL - GROUP BY assigned_to_id - ) + WITH "us_counters" AS ( + SELECT DISTINCT "userstories_userstory"."assigned_to_id" "assigned_to_id", + "userstories_userstory"."id" "us_id" + FROM "userstories_userstory" + LEFT JOIN "epics_relateduserstory" + ON "userstories_userstory"."id" = "epics_relateduserstory"."user_story_id" + WHERE {where} + ), - SELECT "projects_membership"."user_id" user_id, - "users_user"."full_name", - "users_user"."username", - COALESCE("counters".count, 0) count - FROM projects_membership - LEFT OUTER JOIN counters ON ("projects_membership"."user_id" = "counters"."assigned_to_id") - INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id") + "counters" AS ( + SELECT "assigned_to_id", + COUNT("assigned_to_id") + FROM "us_counters" + GROUP BY "assigned_to_id" + ) + + SELECT "projects_membership"."user_id" "user_id", + "users_user"."full_name" "full_name", + "users_user"."username" "username", + COALESCE("counters".count, 0) "count" + FROM "projects_membership" + LEFT OUTER JOIN "counters" + ON ("projects_membership"."user_id" = "counters"."assigned_to_id") + 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 -- unassigned userstories UNION - SELECT NULL user_id, NULL, NULL, count(coalesce(assigned_to_id, -1)) count + SELECT NULL "user_id", + NULL "full_name", + NULL "username", + count(coalesce("assigned_to_id", -1)) "count" FROM "userstories_userstory" - INNER JOIN "projects_project" ON ("userstories_userstory"."project_id" = "projects_project"."id") + INNER JOIN "projects_project" + ON ("userstories_userstory"."project_id" = "projects_project"."id") + LEFT JOIN "epics_relateduserstory" + ON ("userstories_userstory"."id" = "epics_relateduserstory"."user_story_id") WHERE {where} AND "userstories_userstory"."assigned_to_id" IS NULL - GROUP BY assigned_to_id + GROUP BY "assigned_to_id" """.format(where=where) with closing(connection.cursor()) as cursor: @@ -371,33 +400,43 @@ def _get_userstories_owners(project, queryset): where_params = queryset_where_tuple[1] extra_sql = """ - WITH counters AS ( - SELECT "userstories_userstory"."owner_id" owner_id, - count(coalesce("userstories_userstory"."owner_id", -1)) count - FROM "userstories_userstory" - INNER JOIN "projects_project" ON ("userstories_userstory"."project_id" = "projects_project"."id") - WHERE {where} - GROUP BY "userstories_userstory"."owner_id" - ) + WITH "us_counters" AS( + SELECT DISTINCT "userstories_userstory"."owner_id" "owner_id", + "userstories_userstory"."id" "us_id" + FROM "userstories_userstory" + LEFT OUTER JOIN "epics_relateduserstory" + ON ("userstories_userstory"."id" = "epics_relateduserstory"."user_story_id") + WHERE {where} + ), - SELECT "projects_membership"."user_id" id, + "counters" AS ( + SELECT "owner_id", + COUNT("owner_id") + FROM "us_counters" + GROUP BY "owner_id" + ) + + SELECT "projects_membership"."user_id" "user_id", "users_user"."full_name", "users_user"."username", - COALESCE("counters".count, 0) count - FROM projects_membership - LEFT OUTER JOIN counters ON ("projects_membership"."user_id" = "counters"."owner_id") - INNER JOIN "users_user" ON ("projects_membership"."user_id" = "users_user"."id") + COALESCE("counters".count, 0) "count" + FROM "projects_membership" + LEFT OUTER JOIN "counters" + ON ("projects_membership"."user_id" = "counters"."owner_id") + 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 -- System users - UNION + UNION - SELECT "users_user"."id" user_id, - "users_user"."full_name" full_name, - "users_user"."username" username, - COALESCE("counters".count, 0) count - FROM users_user - LEFT OUTER JOIN counters ON ("users_user"."id" = "counters"."owner_id") + SELECT "users_user"."id" "user_id", + "users_user"."full_name" "full_name", + "users_user"."username" "username", + COALESCE("counters"."count", 0) "count" + FROM "users_user" + LEFT OUTER JOIN "counters" + ON ("users_user"."id" = "counters"."owner_id") WHERE ("users_user"."is_system" IS TRUE) """.format(where=where) @@ -423,24 +462,31 @@ def _get_userstories_tags(project, queryset): where_params = queryset_where_tuple[1] extra_sql = """ - WITH userstories_tags AS ( - SELECT tag, - COUNT(tag) counter FROM ( - SELECT UNNEST(userstories_userstory.tags) tag - FROM userstories_userstory - INNER JOIN projects_project - ON (userstories_userstory.project_id = projects_project.id) - WHERE {where}) tags - GROUP BY tag), - project_tags AS ( - SELECT reduce_dim(tags_colors) tag_color - FROM projects_project - WHERE id=%s) + WITH "userstories_tags" AS ( + SELECT "tag", + COUNT("tag") "counter" + FROM ( + SELECT DISTINCT "userstories_userstory"."id" "us_id", + UNNEST("userstories_userstory"."tags") "tag" + FROM "userstories_userstory" + INNER JOIN "projects_project" + ON ("userstories_userstory"."project_id" = "projects_project"."id") + LEFT JOIN "epics_relateduserstory" + ON ("userstories_userstory"."id" = "epics_relateduserstory"."user_story_id") + WHERE {where} + ) "tags" + GROUP BY "tag"), - SELECT tag_color[1] tag, COALESCE(userstories_tags.counter, 0) counter - FROM project_tags - LEFT JOIN userstories_tags ON project_tags.tag_color[1] = userstories_tags.tag - ORDER BY tag + "project_tags" AS ( + SELECT reduce_dim("tags_colors") "tag_color" + FROM "projects_project" + WHERE "id"=%s) + + SELECT "tag_color"[1] "tag", COALESCE("userstories_tags"."counter", 0) "counter" + FROM "project_tags" + LEFT JOIN "userstories_tags" + ON "project_tags"."tag_color"[1] = "userstories_tags"."tag" + ORDER BY "tag" """.format(where=where) with closing(connection.cursor()) as cursor: @@ -461,33 +507,35 @@ def _get_userstories_epics(project, queryset): queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) where = queryset_where_tuple[0] where_params = queryset_where_tuple[1] - extra_sql = """ - WITH counters AS ( - SELECT "epics_relateduserstory"."epic_id" AS "epic_id", - count("epics_relateduserstory"."id") AS "counter" - FROM "epics_relateduserstory" - INNER JOIN "userstories_userstory" - ON ("userstories_userstory"."id" = "epics_relateduserstory"."user_story_id") - INNER JOIN "projects_project" - ON ("userstories_userstory"."project_id" = "projects_project"."id") - WHERE {where} - GROUP BY "epics_relateduserstory"."epic_id" - ) + WITH "counters" AS ( + SELECT "epics_relateduserstory"."epic_id" AS "epic_id", + count("epics_relateduserstory"."id") AS "counter" + FROM "epics_relateduserstory" + INNER JOIN "userstories_userstory" + ON ("userstories_userstory"."id" = "epics_relateduserstory"."user_story_id") + INNER JOIN "projects_project" + ON ("userstories_userstory"."project_id" = "projects_project"."id") + WHERE {where} + GROUP BY "epics_relateduserstory"."epic_id" + ) + -- User stories with no epics (return results only if there are userstories) - SELECT NULL AS "id", - NULL AS "ref", - NULL AS "subject", - 0 AS "order", - count(COALESCE("epics_relateduserstory"."epic_id", -1)) AS "counter" - FROM "userstories_userstory" - LEFT OUTER JOIN "epics_relateduserstory" - ON ("epics_relateduserstory"."user_story_id" = "userstories_userstory"."id") - INNER JOIN "projects_project" - ON ("userstories_userstory"."project_id" = "projects_project"."id") - WHERE {where} AND "epics_relateduserstory"."epic_id" IS NULL - GROUP BY "epics_relateduserstory"."epic_id" - UNION + SELECT NULL AS "id", + NULL AS "ref", + NULL AS "subject", + 0 AS "order", + count(COALESCE("epics_relateduserstory"."epic_id", -1)) AS "counter" + FROM "userstories_userstory" + LEFT OUTER JOIN "epics_relateduserstory" + ON ("epics_relateduserstory"."user_story_id" = "userstories_userstory"."id") + INNER JOIN "projects_project" + ON ("userstories_userstory"."project_id" = "projects_project"."id") + WHERE {where} AND "epics_relateduserstory"."epic_id" IS NULL + GROUP BY "epics_relateduserstory"."epic_id" + + UNION + SELECT "epics_epic"."id" AS "id", "epics_epic"."ref" AS "ref", "epics_epic"."subject" AS "subject", @@ -498,9 +546,9 @@ def _get_userstories_epics(project, queryset): ON ("counters"."epic_id" = "epics_epic"."id") WHERE "epics_epic"."project_id" = %s """.format(where=where) - + with closing(connection.cursor()) as cursor: - cursor.execute(extra_sql, where_params + [project.id] + where_params) + cursor.execute(extra_sql, where_params + where_params + [project.id]) rows = cursor.fetchall() result = [] diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index 57a2f520..5d7bec2a 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.py @@ -684,7 +684,7 @@ def test_api_filters_data(client): # | 6 | status3 | user2 | user1 | tag1 tag2 | epic0 epic2 | # | 7 | status0 | user1 | user2 | tag3 | None | # | 8 | status3 | user3 | user2 | tag1 | epic2 | - # | 9 | status1 | user2 | user3 | tag0 | none | + # | 9 | status1 | user2 | user3 | tag0 | None | # ------------------------------------------------------------------------------ us0 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None, @@ -802,6 +802,34 @@ def test_api_filters_data(client): assert next(filter(lambda i: i['id'] == epic1.id, response.data["epics"]))["count"] == 0 assert next(filter(lambda i: i['id'] == epic2.id, response.data["epics"]))["count"] == 1 + # Filter (epic0 epic2) + response = client.get(url + "&epic={},{}".format(epic0.id, epic2.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'] is 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"] == 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"] == 3 + + 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 + + assert next(filter(lambda i: i['id'] is None, response.data["epics"]))["count"] == 5 + assert next(filter(lambda i: i['id'] == epic0.id, response.data["epics"]))["count"] == 3 + assert next(filter(lambda i: i['id'] == epic1.id, response.data["epics"]))["count"] == 1 + assert next(filter(lambda i: i['id'] == epic2.id, response.data["epics"]))["count"] == 2 + def test_get_invalid_csv(client): url = reverse("userstories-csv")