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. - Add polish (pl) translation.
### Misc ### 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: 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. - Lots of small and not so small bugfixes.

View File

@ -85,7 +85,7 @@ class GenericAPIView(pagination.PaginationMixin,
many=many, partial=partial, context=context) 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. 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 method if you want to apply the configured filtering backend to the
default queryset. 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) queryset = backend().filter_queryset(self.request, queryset, self)
return queryset return queryset

View File

@ -29,6 +29,11 @@ logger = logging.getLogger(__name__)
#####################################################################
# Base and Mixins
#####################################################################
class BaseFilterBackend(object): class BaseFilterBackend(object):
""" """
A base class from which all filter backend classes should inherit. 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: if field_name not in order_by_fields:
return queryset 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) return super().filter_queryset(request, queryset.order_by(raw_fieldname), view)
@ -105,6 +113,10 @@ class FilterBackend(OrderByFilterMixin):
pass pass
#####################################################################
# Permissions filters
#####################################################################
class PermissionBasedFilterBackend(FilterBackend): class PermissionBasedFilterBackend(FilterBackend):
permission = None permission = None
@ -345,9 +357,84 @@ class IsProjectAdminFromWebhookLogFilterBackend(FilterBackend, BaseIsProjectAdmi
return super().filter_queryset(request, queryset, view) 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): class TagsFilter(FilterBackend):
def __init__(self, filter_name='tags'): filter_name = 'tags'
self.filter_name = filter_name
def __init__(self, filter_name=None):
if filter_name:
self.filter_name = filter_name
def _get_tags_queryparams(self, params): def _get_tags_queryparams(self, params):
tags = params.get(self.filter_name, None) tags = params.get(self.filter_name, None)
@ -364,25 +451,9 @@ class TagsFilter(FilterBackend):
return super().filter_queryset(request, queryset, view) return super().filter_queryset(request, queryset, view)
class StatusFilter(FilterBackend): #####################################################################
def __init__(self, filter_name='status'): # Text search filters
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)
class QFilter(FilterBackend): class QFilter(FilterBackend):
def filter_queryset(self, request, queryset, view): def filter_queryset(self, request, queryset, view):

View File

@ -160,12 +160,6 @@ class ProjectViewSet(HistoryResourceMixin, ModelCrudViewSet):
self.check_permissions(request, "issues_stats", project) self.check_permissions(request, "issues_stats", project)
return response.Ok(services.get_stats_for_project_issues(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"]) @detail_route(methods=["GET"])
def tags_colors(self, request, pk=None): def tags_colors(self, request, pk=None):
project = self.get_object() project = self.get_object()

View File

@ -42,79 +42,35 @@ from . import permissions
from . import serializers 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): class IssueViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, ModelCrudViewSet):
serializer_class = serializers.IssueNeighborsSerializer serializer_class = serializers.IssueNeighborsSerializer
list_serializer_class = serializers.IssueSerializer list_serializer_class = serializers.IssueSerializer
permission_classes = (permissions.IssuePermission, ) permission_classes = (permissions.IssuePermission, )
filter_backends = (filters.CanViewIssuesFilterBackend, filters.QFilter, filter_backends = (filters.CanViewIssuesFilterBackend,
IssuesFilter, IssuesOrdering,) filters.OwnersFilter,
retrieve_exclude_filters = (IssuesFilter,) 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", order_by_fields = ("type",
"severity",
"status", "status",
"severity",
"priority", "priority",
"created_date", "created_date",
"modified_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) issue = get_object_or_404(models.Issue, ref=ref, project_id=project_id)
return self.retrieve(request, pk=issue.pk) 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"]) @list_route(methods=["GET"])
def csv(self, request): def csv(self, request):
uuid = request.QUERY_PARAMS.get("uuid", None) uuid = request.QUERY_PARAMS.get("uuid", None)

View File

@ -28,6 +28,7 @@ class IssuePermission(TaigaResourcePermission):
update_perms = HasProjectPerm('modify_issue') update_perms = HasProjectPerm('modify_issue')
destroy_perms = HasProjectPerm('delete_issue') destroy_perms = HasProjectPerm('delete_issue')
list_perms = AllowAny() list_perms = AllowAny()
filters_data_perms = AllowAny()
csv_perms = AllowAny() csv_perms = AllowAny()
upvote_perms = IsAuthenticated() & HasProjectPerm('vote_issues') upvote_perms = IsAuthenticated() & HasProjectPerm('vote_issues')
downvote_perms = IsAuthenticated() & HasProjectPerm('vote_issues') downvote_perms = IsAuthenticated() & HasProjectPerm('vote_issues')

View File

@ -16,6 +16,12 @@
import io import io
import csv 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 from taiga.base.utils import db, text
@ -101,3 +107,258 @@ def issues_to_csv(project, queryset):
writer.writerow(issue_data) writer.writerow(issue_data)
return csv_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() star_perms = IsAuthenticated()
unstar_perms = IsAuthenticated() unstar_perms = IsAuthenticated()
issues_stats_perms = HasProjectPerm('view_project') issues_stats_perms = HasProjectPerm('view_project')
issues_filters_data_perms = HasProjectPerm('view_project')
tags_perms = HasProjectPerm('view_project') tags_perms = HasProjectPerm('view_project')
tags_colors_perms = HasProjectPerm('view_project') tags_colors_perms = HasProjectPerm('view_project')
fans_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 .bulk_update_order import bulk_update_userstory_status_order
from .filters import get_all_tags 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_issues
from .stats import get_stats_for_project from .stats import get_stats_for_project

View File

@ -50,113 +50,6 @@ def _get_issues_tags(project):
return result 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 # Public api
def get_all_tags(project): def get_all_tags(project):
@ -170,23 +63,3 @@ def get_all_tags(project):
result.update(_get_stories_tags(project)) result.update(_get_stories_tags(project))
result.update(_get_tasks_tags(project)) result.update(_get_tasks_tags(project))
return sorted(result) 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 serializer_class = serializers.UserStoryNeighborsSerializer
list_serializer_class = serializers.UserStorySerializer list_serializer_class = serializers.UserStorySerializer
permission_classes = (permissions.UserStoryPermission,) permission_classes = (permissions.UserStoryPermission,)
filter_backends = (filters.CanViewUsFilterBackend,
filter_backends = (filters.StatusFilter, filters.CanViewUsFilterBackend, filters.TagsFilter, filters.OwnersFilter,
filters.QFilter, filters.OrderByFilterMixin) filters.AssignedToFilter,
filters.StatusesFilter,
retrieve_exclude_filters = (filters.StatusFilter, filters.TagsFilter,) filters.TagsFilter,
filter_fields = ["project", "milestone", "milestone__isnull", filters.QFilter,
"is_archived", "status__is_archived", "assigned_to", filters.OrderByFilterMixin)
"status__is_closed", "watchers", "is_closed"] retrieve_exclude_filters = (filters.OwnersFilter,
order_by_fields = ["backlog_order", "sprint_order", "kanban_order"] 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 # Specific filter used for filtering neighbor user stories
_neighbor_tags_filter = filters.TagsFilter('neighbor_tags') _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 " raise exc.PermissionDenied(_("You don't have permissions to set this status "
"to this user story.")) "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"]) @list_route(methods=["GET"])
def by_ref(self, request): def by_ref(self, request):
ref = request.QUERY_PARAMS.get("ref", None) ref = request.QUERY_PARAMS.get("ref", None)

View File

@ -25,6 +25,7 @@ class UserStoryPermission(TaigaResourcePermission):
update_perms = HasProjectPerm('modify_us') update_perms = HasProjectPerm('modify_us')
destroy_perms = HasProjectPerm('delete_us') destroy_perms = HasProjectPerm('delete_us')
list_perms = AllowAny() list_perms = AllowAny()
filters_data_perms = AllowAny()
csv_perms = AllowAny() csv_perms = AllowAny()
bulk_create_perms = IsAuthenticated() & (HasProjectPerm('add_us_to_project') | HasProjectPerm('add_us')) bulk_create_perms = IsAuthenticated() & (HasProjectPerm('add_us_to_project') | HasProjectPerm('add_us'))
bulk_update_order_perms = HasProjectPerm('modify_us') bulk_update_order_perms = HasProjectPerm('modify_us')

View File

@ -16,8 +16,13 @@
import csv import csv
import io 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 import timezone
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
@ -170,3 +175,144 @@ def userstories_to_csv(project,queryset):
writer.writerow(row) writer.writerow(row)
return csv_data 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] 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): def test_project_action_fans(client, data):
public_url = reverse('projects-fans', kwargs={"pk": data.public_project.pk}) public_url = reverse('projects-fans', kwargs={"pk": data.public_project.pk})
private1_url = reverse('projects-fans', kwargs={"pk": data.private_project1.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 response.status_code == 200
assert number_of_issues == 1 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): def test_get_invalid_csv(client):
url = reverse("issues-csv") url = reverse("issues-csv")

View File

@ -244,7 +244,6 @@ def test_filter_by_multiple_status(client):
url = reverse("userstories-list") url = reverse("userstories-list")
url = "{}?status={},{}".format(reverse("userstories-list"), us1.status.id, us2.status.id) url = "{}?status={},{}".format(reverse("userstories-list"), us1.status.id, us2.status.id)
data = {} data = {}
response = client.get(url, data) response = client.get(url, data)
assert len(response.data) == 2 assert len(response.data) == 2
@ -282,6 +281,137 @@ def test_get_total_points(client):
assert us_mixed.get_total_points() == 1.0 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): def test_get_invalid_csv(client):
url = reverse("userstories-csv") url = reverse("userstories-csv")