Add filter_data to tasks API endpoint

remotes/origin/issue/4795/notification_even_they_are_disabled
David Barragán Merino 2016-06-22 22:24:52 +02:00
parent 4904e1859f
commit 6b6f6e80be
3 changed files with 422 additions and 44 deletions

View File

@ -44,8 +44,18 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet): TaggedResourceMixin, BlockedByProjectMixin, ModelCrudViewSet):
queryset = models.Task.objects.all() queryset = models.Task.objects.all()
permission_classes = (permissions.TaskPermission,) permission_classes = (permissions.TaskPermission,)
filter_backends = (filters.CanViewTasksFilterBackend, filters.WatchersFilter) filter_backends = (filters.CanViewTasksFilterBackend,
retrieve_exclude_filters = (filters.WatchersFilter,) filters.OwnersFilter,
filters.AssignedToFilter,
filters.StatusesFilter,
filters.TagsFilter,
filters.WatchersFilter,
filters.QFilter)
retrieve_exclude_filters = (filters.OwnersFilter,
filters.AssignedToFilter,
filters.StatusesFilter,
filters.TagsFilter,
filters.WatchersFilter)
filter_fields = ["user_story", filter_fields = ["user_story",
"milestone", "milestone",
"project", "project",
@ -62,6 +72,44 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
return serializers.TaskSerializer return serializers.TaskSerializer
def get_queryset(self):
qs = super().get_queryset()
qs = self.attach_votes_attrs_to_queryset(qs)
qs = qs.select_related("milestone",
"project",
"status",
"owner",
"assigned_to")
qs = self.attach_watchers_attrs_to_queryset(qs)
if "include_attachments" in self.request.QUERY_PARAMS:
qs = attach_basic_attachments(qs)
qs = qs.extra(select={"include_attachments": "True"})
return qs
def pre_conditions_on_save(self, obj):
super().pre_conditions_on_save(obj)
if obj.milestone and obj.milestone.project != obj.project:
raise exc.WrongArguments(_("You don't have permissions to set this sprint to this task."))
if obj.user_story and obj.user_story.project != obj.project:
raise exc.WrongArguments(_("You don't have permissions to set this user story to this task."))
if obj.status and obj.status.project != obj.project:
raise exc.WrongArguments(_("You don't have permissions to set this status to this task."))
if obj.milestone and obj.user_story and obj.milestone != obj.user_story.milestone:
raise exc.WrongArguments(_("You don't have permissions to set this sprint to this task."))
def pre_save(self, obj):
if obj.user_story:
obj.milestone = obj.user_story.milestone
if not obj.id:
obj.owner = self.request.user
super().pre_save(obj)
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
self.object = self.get_object_or_none() self.object = self.get_object_or_none()
project_id = request.DATA.get('project', None) project_id = request.DATA.get('project', None)
@ -93,44 +141,24 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
return super().update(request, *args, **kwargs) return super().update(request, *args, **kwargs)
def get_queryset(self): @list_route(methods=["GET"])
qs = super().get_queryset() def filters_data(self, request, *args, **kwargs):
qs = self.attach_votes_attrs_to_queryset(qs) project_id = request.QUERY_PARAMS.get("project", None)
qs = qs.select_related( project = get_object_or_404(Project, id=project_id)
"milestone",
"owner",
"assigned_to",
"status",
"project")
qs = self.attach_watchers_attrs_to_queryset(qs) filter_backends = self.get_filter_backends()
if "include_attachments" in self.request.QUERY_PARAMS: statuses_filter_backends = (f for f in filter_backends if f != filters.StatusesFilter)
qs = attach_basic_attachments(qs) assigned_to_filter_backends = (f for f in filter_backends if f != filters.AssignedToFilter)
qs = qs.extra(select={"include_attachments": "True"}) owners_filter_backends = (f for f in filter_backends if f != filters.OwnersFilter)
return qs queryset = self.get_queryset()
querysets = {
def pre_save(self, obj): "statuses": self.filter_queryset(queryset, filter_backends=statuses_filter_backends),
if obj.user_story: "assigned_to": self.filter_queryset(queryset, filter_backends=assigned_to_filter_backends),
obj.milestone = obj.user_story.milestone "owners": self.filter_queryset(queryset, filter_backends=owners_filter_backends),
if not obj.id: "tags": self.filter_queryset(queryset)
obj.owner = self.request.user }
super().pre_save(obj) return response.Ok(services.get_tasks_filters_data(project, querysets))
def pre_conditions_on_save(self, obj):
super().pre_conditions_on_save(obj)
if obj.milestone and obj.milestone.project != obj.project:
raise exc.WrongArguments(_("You don't have permissions to set this sprint to this task."))
if obj.user_story and obj.user_story.project != obj.project:
raise exc.WrongArguments(_("You don't have permissions to set this user story to this task."))
if obj.status and obj.status.project != obj.project:
raise exc.WrongArguments(_("You don't have permissions to set this status to this task."))
if obj.milestone and obj.user_story and obj.milestone != obj.user_story.milestone:
raise exc.WrongArguments(_("You don't have permissions to set this sprint to this task."))
@list_route(methods=["GET"]) @list_route(methods=["GET"])
def by_ref(self, request): def by_ref(self, request):

View File

@ -16,14 +16,19 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import io
import csv import csv
import io
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 from taiga.base.utils import db, text
from taiga.projects.history.services import take_snapshot from taiga.projects.history.services import take_snapshot
from taiga.projects.tasks.apps import ( from taiga.projects.tasks.apps import connect_tasks_signals
connect_tasks_signals, from taiga.projects.tasks.apps import disconnect_tasks_signals
disconnect_tasks_signals)
from taiga.events import events from taiga.events import events
from taiga.projects.votes.utils import attach_total_voters_to_queryset from taiga.projects.votes.utils import attach_total_voters_to_queryset
from taiga.projects.notifications.utils import attach_watchers_to_queryset from taiga.projects.notifications.utils import attach_watchers_to_queryset
@ -31,6 +36,10 @@ from taiga.projects.notifications.utils import attach_watchers_to_queryset
from . import models from . import models
#####################################################
# Bulk actions
#####################################################
def get_tasks_from_bulk(bulk_data, **additional_fields): def get_tasks_from_bulk(bulk_data, **additional_fields):
"""Convert `bulk_data` into a list of tasks. """Convert `bulk_data` into a list of tasks.
@ -85,7 +94,6 @@ def update_tasks_order_in_bulk(bulk_data:list, field:str, project:object):
def snapshot_tasks_in_bulk(bulk_data, user): def snapshot_tasks_in_bulk(bulk_data, user):
task_ids = []
for task_data in bulk_data: for task_data in bulk_data:
try: try:
task = models.Task.objects.get(pk=task_data['task_id']) task = models.Task.objects.get(pk=task_data['task_id'])
@ -94,6 +102,10 @@ def snapshot_tasks_in_bulk(bulk_data, user):
pass pass
#####################################################
# CSV
#####################################################
def tasks_to_csv(project, queryset): def tasks_to_csv(project, queryset):
csv_data = io.StringIO() csv_data = io.StringIO()
fieldnames = ["ref", "subject", "description", "user_story", "sprint", "sprint_estimated_start", fieldnames = ["ref", "subject", "description", "user_story", "sprint", "sprint_estimated_start",
@ -153,3 +165,212 @@ def tasks_to_csv(project, queryset):
writer.writerow(task_data) writer.writerow(task_data)
return csv_data return csv_data
#####################################################
# Api filter data
#####################################################
def _get_tasks_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_taskstatus"."id",
"projects_taskstatus"."name",
"projects_taskstatus"."color",
"projects_taskstatus"."order",
(SELECT count(*)
FROM "tasks_task"
INNER JOIN "projects_project" ON
("tasks_task"."project_id" = "projects_project"."id")
WHERE {where} AND "tasks_task"."status_id" = "projects_taskstatus"."id")
FROM "projects_taskstatus"
WHERE "projects_taskstatus"."project_id" = %s
ORDER BY "projects_taskstatus"."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_tasks_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 = """
WITH counters AS (
SELECT assigned_to_id, count(assigned_to_id) count
FROM "tasks_task"
INNER JOIN "projects_project" ON ("tasks_task"."project_id" = "projects_project"."id")
WHERE {where} AND "tasks_task"."assigned_to_id" IS NOT NULL
GROUP BY assigned_to_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"."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 tasks
UNION
SELECT NULL user_id, NULL, NULL, count(coalesce(assigned_to_id, -1)) count
FROM "tasks_task"
INNER JOIN "projects_project" ON ("tasks_task"."project_id" = "projects_project"."id")
WHERE {where} AND "tasks_task"."assigned_to_id" IS NULL
GROUP BY assigned_to_id
""".format(where=where)
with closing(connection.cursor()) as cursor:
cursor.execute(extra_sql, where_params + [project.id] + where_params)
rows = cursor.fetchall()
result = []
none_valued_added = False
for id, full_name, username, count in rows:
result.append({
"id": id,
"full_name": full_name or username or "",
"count": count,
})
if id is None:
none_valued_added = True
# If there was no task with null assigned_to we manually add it
if not none_valued_added:
result.append({
"id": None,
"full_name": "",
"count": 0,
})
return sorted(result, key=itemgetter("full_name"))
def _get_tasks_owners(project, queryset):
compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
where = queryset_where_tuple[0]
where_params = queryset_where_tuple[1]
extra_sql = """
WITH counters AS (
SELECT "tasks_task"."owner_id" owner_id,
count(coalesce("tasks_task"."owner_id", -1)) count
FROM "tasks_task"
INNER JOIN "projects_project" ON ("tasks_task"."project_id" = "projects_project"."id")
WHERE {where}
GROUP BY "tasks_task"."owner_id"
)
SELECT "projects_membership"."user_id" 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")
WHERE "projects_membership"."project_id" = %s AND "projects_membership"."user_id" IS NOT NULL
-- System users
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")
WHERE ("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, username, count in rows:
if count > 0:
result.append({
"id": id,
"full_name": full_name or username or "",
"count": count,
})
return sorted(result, key=itemgetter("full_name"))
def _get_tasks_tags(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 tasks_tags AS (
SELECT tag,
COUNT(tag) counter FROM (
SELECT UNNEST(tasks_task.tags) tag
FROM tasks_task
INNER JOIN projects_project
ON (tasks_task.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)
SELECT tag_color[1] tag, COALESCE(tasks_tags.counter, 0) counter
FROM project_tags
LEFT JOIN tasks_tags ON project_tags.tag_color[1] = tasks_tags.tag
ORDER BY tag
""".format(where=where)
with closing(connection.cursor()) as cursor:
cursor.execute(extra_sql, where_params + [project.id])
rows = cursor.fetchall()
result = []
for name, count in rows:
result.append({
"name": name,
"count": count,
})
return sorted(result, key=itemgetter("name"))
def get_tasks_filters_data(project, querysets):
"""
Given a project and an tasks queryset, return a simple data structure
of all possible filters for the tasks in the queryset.
"""
data = OrderedDict([
("statuses", _get_tasks_statuses(project, querysets["statuses"])),
("assigned_to", _get_tasks_assigned_to(project, querysets["assigned_to"])),
("owners", _get_tasks_owners(project, querysets["owners"])),
("tags", _get_tasks_tags(project, querysets["tags"])),
])
return data

View File

@ -206,3 +206,132 @@ def test_get_tasks_including_attachments(client):
response = client.get(url) response = client.get(url)
assert response.status_code == 200 assert response.status_code == 200
assert len(response.data[0].get("attachments")) == 1 assert len(response.data[0].get("attachments")) == 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.TaskStatusFactory.create(project=project)
status1 = f.TaskStatusFactory.create(project=project)
status2 = f.TaskStatusFactory.create(project=project)
status3 = f.TaskStatusFactory.create(project=project)
tag0 = "test1test2test3"
tag1 = "test1"
tag2 = "test2"
tag3 = "test3"
# ------------------------------------------------------
# | Task | 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 |
# ------------------------------------------------------
task0 = f.TaskFactory.create(project=project, owner=user2, assigned_to=None,
status=status3, tags=[tag1])
task1 = f.TaskFactory.create(project=project, owner=user1, assigned_to=None,
status=status3, tags=[tag2])
task2 = f.TaskFactory.create(project=project, owner=user3, assigned_to=None,
status=status1, tags=[tag1, tag2])
task3 = f.TaskFactory.create(project=project, owner=user2, assigned_to=None,
status=status0, tags=[tag3])
task4 = f.TaskFactory.create(project=project, owner=user1, assigned_to=user1,
status=status0, tags=[tag1, tag2, tag3])
task5 = f.TaskFactory.create(project=project, owner=user3, assigned_to=user1,
status=status2, tags=[tag3])
task6 = f.TaskFactory.create(project=project, owner=user2, assigned_to=user1,
status=status3, tags=[tag1, tag2])
task7 = f.TaskFactory.create(project=project, owner=user1, assigned_to=user2,
status=status0, tags=[tag3])
task8 = f.TaskFactory.create(project=project, owner=user3, assigned_to=user2,
status=status3, tags=[tag1])
task9 = f.TaskFactory.create(project=project, owner=user2, assigned_to=user3,
status=status1, tags=[tag0])
url = reverse("tasks-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
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
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