Improve userstories/filters_data and issues/filter_data

remotes/origin/enhancement/email-actions
David Barragán Merino 2015-06-10 12:21:54 +02:00 committed by Alejandro Alonso
parent b75b88d750
commit 21153ea1aa
16 changed files with 919 additions and 253 deletions

View File

@ -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.

View File

@ -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

View File

@ -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,8 +357,83 @@ 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'):
filter_name = 'tags'
def __init__(self, filter_name=None):
if filter_name:
self.filter_name = filter_name
def _get_tags_queryparams(self, params):
@ -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):

View File

@ -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()

View File

@ -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)

View File

@ -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')

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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')

View File

@ -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

View File

@ -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})

View File

@ -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")

View File

@ -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")