Filter userstories by epics
parent
5cda117c1b
commit
ff7d241a37
|
@ -22,7 +22,7 @@ from django.db import transaction
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
|
||||||
from taiga.base import filters
|
from taiga.base import filters as base_filters
|
||||||
from taiga.base import exceptions as exc
|
from taiga.base import exceptions as exc
|
||||||
from taiga.base import response
|
from taiga.base import response
|
||||||
from taiga.base import status
|
from taiga.base import status
|
||||||
|
@ -45,6 +45,7 @@ from taiga.projects.votes.mixins.viewsets import VotedResourceMixin
|
||||||
from taiga.projects.votes.mixins.viewsets import VotersViewSetMixin
|
from taiga.projects.votes.mixins.viewsets import VotersViewSetMixin
|
||||||
from taiga.projects.userstories.utils import attach_extra_info
|
from taiga.projects.userstories.utils import attach_extra_info
|
||||||
|
|
||||||
|
from . import filters
|
||||||
from . import models
|
from . import models
|
||||||
from . import permissions
|
from . import permissions
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
@ -57,17 +58,18 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
|
||||||
validator_class = validators.UserStoryValidator
|
validator_class = validators.UserStoryValidator
|
||||||
queryset = models.UserStory.objects.all()
|
queryset = models.UserStory.objects.all()
|
||||||
permission_classes = (permissions.UserStoryPermission,)
|
permission_classes = (permissions.UserStoryPermission,)
|
||||||
filter_backends = (filters.CanViewUsFilterBackend,
|
filter_backends = (base_filters.CanViewUsFilterBackend,
|
||||||
filters.OwnersFilter,
|
filters.EpicsFilter,
|
||||||
filters.AssignedToFilter,
|
base_filters.OwnersFilter,
|
||||||
filters.StatusesFilter,
|
base_filters.AssignedToFilter,
|
||||||
filters.TagsFilter,
|
base_filters.StatusesFilter,
|
||||||
filters.WatchersFilter,
|
base_filters.TagsFilter,
|
||||||
filters.QFilter,
|
base_filters.WatchersFilter,
|
||||||
filters.CreatedDateFilter,
|
base_filters.QFilter,
|
||||||
filters.ModifiedDateFilter,
|
base_filters.CreatedDateFilter,
|
||||||
filters.FinishDateFilter,
|
base_filters.ModifiedDateFilter,
|
||||||
filters.OrderByFilterMixin)
|
base_filters.FinishDateFilter,
|
||||||
|
base_filters.OrderByFilterMixin)
|
||||||
filter_fields = ["project",
|
filter_fields = ["project",
|
||||||
"project__slug",
|
"project__slug",
|
||||||
"milestone",
|
"milestone",
|
||||||
|
@ -270,16 +272,18 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
|
||||||
project = get_object_or_404(Project, id=project_id)
|
project = get_object_or_404(Project, id=project_id)
|
||||||
|
|
||||||
filter_backends = self.get_filter_backends()
|
filter_backends = self.get_filter_backends()
|
||||||
statuses_filter_backends = (f for f in filter_backends if f != filters.StatusesFilter)
|
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 != filters.AssignedToFilter)
|
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 != filters.OwnersFilter)
|
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)
|
||||||
|
|
||||||
queryset = self.get_queryset()
|
queryset = self.get_queryset()
|
||||||
querysets = {
|
querysets = {
|
||||||
"statuses": self.filter_queryset(queryset, filter_backends=statuses_filter_backends),
|
"statuses": self.filter_queryset(queryset, filter_backends=statuses_filter_backends),
|
||||||
"assigned_to": self.filter_queryset(queryset, filter_backends=assigned_to_filter_backends),
|
"assigned_to": self.filter_queryset(queryset, filter_backends=assigned_to_filter_backends),
|
||||||
"owners": self.filter_queryset(queryset, filter_backends=owners_filter_backends),
|
"owners": self.filter_queryset(queryset, filter_backends=owners_filter_backends),
|
||||||
"tags": self.filter_queryset(queryset)
|
"tags": self.filter_queryset(queryset),
|
||||||
|
"epics": self.filter_queryset(queryset, filter_backends=epics_filter_backends)
|
||||||
}
|
}
|
||||||
return response.Ok(services.get_userstories_filters_data(project, querysets))
|
return response.Ok(services.get_userstories_filters_data(project, querysets))
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as
|
||||||
|
# published by the Free Software Foundation, either version 3 of the
|
||||||
|
# License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
from taiga.base import filters
|
||||||
|
|
||||||
|
|
||||||
|
class EpicsFilter(filters.BaseRelatedFieldsFilter):
|
||||||
|
filter_name = 'epics'
|
|
@ -244,7 +244,7 @@ def userstories_to_csv(project, queryset):
|
||||||
"tasks": ",".join([str(task.ref) for task in us.tasks.all()]),
|
"tasks": ",".join([str(task.ref) for task in us.tasks.all()]),
|
||||||
"tags": ",".join(us.tags or []),
|
"tags": ",".join(us.tags or []),
|
||||||
"watchers": us.watchers,
|
"watchers": us.watchers,
|
||||||
"voters": us.total_voters,
|
"voters": us.total_voters
|
||||||
}
|
}
|
||||||
|
|
||||||
us_role_points_by_role_id = {us_rp.role.id: us_rp.points.value for us_rp in us.role_points.all()}
|
us_role_points_by_role_id = {us_rp.role.id: us_rp.points.value for us_rp in us.role_points.all()}
|
||||||
|
@ -456,6 +456,74 @@ def _get_userstories_tags(project, queryset):
|
||||||
return sorted(result, key=itemgetter("name"))
|
return sorted(result, key=itemgetter("name"))
|
||||||
|
|
||||||
|
|
||||||
|
def _get_userstories_epics(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 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")
|
||||||
|
WHERE {where}
|
||||||
|
GROUP BY "epics_relateduserstory"."epic_id"
|
||||||
|
)
|
||||||
|
SELECT "epics_epic"."id" AS "id",
|
||||||
|
"epics_epic"."ref" AS "ref",
|
||||||
|
"epics_epic"."subject" AS "subject",
|
||||||
|
"epics_epic"."epics_order" AS "order",
|
||||||
|
COALESCE("counters"."counter", 0) AS "counter"
|
||||||
|
FROM "epics_epic"
|
||||||
|
LEFT OUTER JOIN "counters"
|
||||||
|
ON ("counters"."epic_id" = "epics_epic"."id")
|
||||||
|
WHERE "epics_epic"."project_id" = %s
|
||||||
|
|
||||||
|
-- User stories with no epics (return results only if there are userstories)
|
||||||
|
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")
|
||||||
|
WHERE {where} AND "epics_relateduserstory"."epic_id" IS NULL
|
||||||
|
GROUP BY "epics_relateduserstory"."epic_id"
|
||||||
|
""".format(where=where)
|
||||||
|
|
||||||
|
with closing(connection.cursor()) as cursor:
|
||||||
|
cursor.execute(extra_sql, where_params + [project.id] + where_params)
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for id, ref, subject, order, count in rows:
|
||||||
|
result.append({
|
||||||
|
"id": id,
|
||||||
|
"ref": ref,
|
||||||
|
"subject": subject,
|
||||||
|
"order": order,
|
||||||
|
"count": count,
|
||||||
|
})
|
||||||
|
|
||||||
|
result = sorted(result, key=itemgetter("order"))
|
||||||
|
|
||||||
|
# Add row when there is no user stories with no epics
|
||||||
|
if result[0]["id"] is not None:
|
||||||
|
result.insert(0, {
|
||||||
|
"id": None,
|
||||||
|
"ref": None,
|
||||||
|
"subject": None,
|
||||||
|
"order": 0,
|
||||||
|
"count": 0,
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def get_userstories_filters_data(project, querysets):
|
def get_userstories_filters_data(project, querysets):
|
||||||
"""
|
"""
|
||||||
Given a project and an userstories queryset, return a simple data structure
|
Given a project and an userstories queryset, return a simple data structure
|
||||||
|
@ -466,6 +534,7 @@ def get_userstories_filters_data(project, querysets):
|
||||||
("assigned_to", _get_userstories_assigned_to(project, querysets["assigned_to"])),
|
("assigned_to", _get_userstories_assigned_to(project, querysets["assigned_to"])),
|
||||||
("owners", _get_userstories_owners(project, querysets["owners"])),
|
("owners", _get_userstories_owners(project, querysets["owners"])),
|
||||||
("tags", _get_userstories_tags(project, querysets["tags"])),
|
("tags", _get_userstories_tags(project, querysets["tags"])),
|
||||||
|
("epics", _get_userstories_epics(project, querysets["epics"])),
|
||||||
])
|
])
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
|
@ -249,11 +249,20 @@ class EpicFactory(Factory):
|
||||||
ref = factory.Sequence(lambda n: n)
|
ref = factory.Sequence(lambda n: n)
|
||||||
project = factory.SubFactory("tests.factories.ProjectFactory")
|
project = factory.SubFactory("tests.factories.ProjectFactory")
|
||||||
owner = factory.SubFactory("tests.factories.UserFactory")
|
owner = factory.SubFactory("tests.factories.UserFactory")
|
||||||
subject = factory.Sequence(lambda n: "User Story {}".format(n))
|
subject = factory.Sequence(lambda n: "Epic {}".format(n))
|
||||||
description = factory.Sequence(lambda n: "User Story {} description".format(n))
|
description = factory.Sequence(lambda n: "Epic {} description".format(n))
|
||||||
status = factory.SubFactory("tests.factories.EpicStatusFactory")
|
status = factory.SubFactory("tests.factories.EpicStatusFactory")
|
||||||
|
|
||||||
|
|
||||||
|
class RelatedUserStory(Factory):
|
||||||
|
class Meta:
|
||||||
|
model = "epics.RelatedUserStory"
|
||||||
|
strategy = factory.CREATE_STRATEGY
|
||||||
|
|
||||||
|
epic = factory.SubFactory("tests.factories.EpicFactory")
|
||||||
|
user_story = factory.SubFactory("tests.factories.UserStoryFactory")
|
||||||
|
|
||||||
|
|
||||||
class MilestoneFactory(Factory):
|
class MilestoneFactory(Factory):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = "milestones.Milestone"
|
model = "milestones.Milestone"
|
||||||
|
|
|
@ -625,45 +625,55 @@ def test_api_filters_data(client):
|
||||||
status2 = f.UserStoryStatusFactory.create(project=project)
|
status2 = f.UserStoryStatusFactory.create(project=project)
|
||||||
status3 = f.UserStoryStatusFactory.create(project=project)
|
status3 = f.UserStoryStatusFactory.create(project=project)
|
||||||
|
|
||||||
|
epic0 = f.EpicFactory.create(project=project)
|
||||||
|
epic1 = f.EpicFactory.create(project=project)
|
||||||
|
epic2 = f.EpicFactory.create(project=project)
|
||||||
|
|
||||||
tag0 = "test1test2test3"
|
tag0 = "test1test2test3"
|
||||||
tag1 = "test1"
|
tag1 = "test1"
|
||||||
tag2 = "test2"
|
tag2 = "test2"
|
||||||
tag3 = "test3"
|
tag3 = "test3"
|
||||||
|
|
||||||
# ------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# | US | Owner | Assigned To | Tags |
|
# | US | Status | Owner | Assigned To | Tags | Epic |
|
||||||
# |-------#--------#-------------#---------------------|
|
# |-------#---------#--------#-------------#---------------------#--------------
|
||||||
# | 0 | user2 | None | tag1 |
|
# | 0 | status3 | user2 | None | tag1 | epic0 |
|
||||||
# | 1 | user1 | None | tag2 |
|
# | 1 | status3 | user1 | None | tag2 | None |
|
||||||
# | 2 | user3 | None | tag1 tag2 |
|
# | 2 | status1 | user3 | None | tag1 tag2 | epic1 |
|
||||||
# | 3 | user2 | None | tag3 |
|
# | 3 | status0 | user2 | None | tag3 | None |
|
||||||
# | 4 | user1 | user1 | tag1 tag2 tag3 |
|
# | 4 | status0 | user1 | user1 | tag1 tag2 tag3 | epic0 |
|
||||||
# | 5 | user3 | user1 | tag3 |
|
# | 5 | status2 | user3 | user1 | tag3 | None |
|
||||||
# | 6 | user2 | user1 | tag1 tag2 |
|
# | 6 | status3 | user2 | user1 | tag1 tag2 | epic0 epic2 |
|
||||||
# | 7 | user1 | user2 | tag3 |
|
# | 7 | status0 | user1 | user2 | tag3 | None |
|
||||||
# | 8 | user3 | user2 | tag1 |
|
# | 8 | status3 | user3 | user2 | tag1 | epic2 |
|
||||||
# | 9 | user2 | user3 | tag0 |
|
# | 9 | status1 | user2 | user3 | tag0 | none |
|
||||||
# ------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None,
|
us0 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None,
|
||||||
status=status3, tags=[tag1])
|
status=status3, tags=[tag1])
|
||||||
f.UserStoryFactory.create(project=project, owner=user1, assigned_to=None,
|
f.RelatedUserStory.create(user_story=us0, epic=epic0)
|
||||||
|
us1 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=None,
|
||||||
status=status3, tags=[tag2])
|
status=status3, tags=[tag2])
|
||||||
f.UserStoryFactory.create(project=project, owner=user3, assigned_to=None,
|
us2 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=None,
|
||||||
status=status1, tags=[tag1, tag2])
|
status=status1, tags=[tag1, tag2])
|
||||||
f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None,
|
f.RelatedUserStory.create(user_story=us2, epic=epic1)
|
||||||
|
us3 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=None,
|
||||||
status=status0, tags=[tag3])
|
status=status0, tags=[tag3])
|
||||||
f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user1,
|
us4 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user1,
|
||||||
status=status0, tags=[tag1, tag2, tag3])
|
status=status0, tags=[tag1, tag2, tag3])
|
||||||
f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user1,
|
f.RelatedUserStory.create(user_story=us4, epic=epic0)
|
||||||
|
us5 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user1,
|
||||||
status=status2, tags=[tag3])
|
status=status2, tags=[tag3])
|
||||||
f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user1,
|
us6 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user1,
|
||||||
status=status3, tags=[tag1, tag2])
|
status=status3, tags=[tag1, tag2])
|
||||||
f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user2,
|
f.RelatedUserStory.create(user_story=us6, epic=epic0)
|
||||||
|
f.RelatedUserStory.create(user_story=us6, epic=epic2)
|
||||||
|
us7 = f.UserStoryFactory.create(project=project, owner=user1, assigned_to=user2,
|
||||||
status=status0, tags=[tag3])
|
status=status0, tags=[tag3])
|
||||||
f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user2,
|
us8 = f.UserStoryFactory.create(project=project, owner=user3, assigned_to=user2,
|
||||||
status=status3, tags=[tag1])
|
status=status3, tags=[tag1])
|
||||||
f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user3,
|
f.RelatedUserStory.create(user_story=us8, epic=epic2)
|
||||||
|
us9 = f.UserStoryFactory.create(project=project, owner=user2, assigned_to=user3,
|
||||||
status=status1, tags=[tag0])
|
status=status1, tags=[tag0])
|
||||||
|
|
||||||
url = reverse("userstories-filters-data") + "?project={}".format(project.id)
|
url = reverse("userstories-filters-data") + "?project={}".format(project.id)
|
||||||
|
@ -693,6 +703,11 @@ def test_api_filters_data(client):
|
||||||
assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 4
|
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
|
assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 4
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
# Filter ((status0 or status3)
|
# Filter ((status0 or status3)
|
||||||
response = client.get(url + "&status={},{}".format(status3.id, status0.id))
|
response = client.get(url + "&status={},{}".format(status3.id, status0.id))
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
@ -716,6 +731,11 @@ def test_api_filters_data(client):
|
||||||
assert next(filter(lambda i: i['name'] == tag2, response.data["tags"]))["count"] == 3
|
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
|
assert next(filter(lambda i: i['name'] == tag3, response.data["tags"]))["count"] == 3
|
||||||
|
|
||||||
|
assert next(filter(lambda i: i['id'] is None, response.data["epics"]))["count"] == 3
|
||||||
|
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"] == 0
|
||||||
|
assert next(filter(lambda i: i['id'] == epic2.id, response.data["epics"]))["count"] == 2
|
||||||
|
|
||||||
# Filter ((tag1 and tag2) and (user1 or user2))
|
# Filter ((tag1 and tag2) and (user1 or user2))
|
||||||
response = client.get(url + "&tags={},{}&owner={},{}".format(tag1, tag2, user1.id, user2.id))
|
response = client.get(url + "&tags={},{}&owner={},{}".format(tag1, tag2, user1.id, user2.id))
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
@ -739,6 +759,11 @@ def test_api_filters_data(client):
|
||||||
assert next(filter(lambda i: i['name'] == tag2, 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
|
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"] == 0
|
||||||
|
assert next(filter(lambda i: i['id'] == epic0.id, response.data["epics"]))["count"] == 2
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
def test_get_invalid_csv(client):
|
def test_get_invalid_csv(client):
|
||||||
url = reverse("userstories-csv")
|
url = reverse("userstories-csv")
|
||||||
|
|
Loading…
Reference in New Issue