Improve userstories/filters_data and issues/filter_data
parent
b75b88d750
commit
21153ea1aa
|
@ -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.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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,8 +357,83 @@ 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'
|
||||||
|
|
||||||
|
def __init__(self, filter_name=None):
|
||||||
|
if filter_name:
|
||||||
self.filter_name = filter_name
|
self.filter_name = filter_name
|
||||||
|
|
||||||
def _get_tags_queryparams(self, params):
|
def _get_tags_queryparams(self, params):
|
||||||
|
@ -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):
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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})
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue