Merge branch 'master' into stable

remotes/origin/3.4.0rc 3.2.0
Miguel Gonzalez 2018-03-07 07:02:42 +01:00
commit aed3159026
8 changed files with 205 additions and 3 deletions

View File

@ -1,6 +1,20 @@
# Changelog # # Changelog #
## 3.2.0 Betula nana (2018-03-07)
### Features
- Add role filtering in US.
## 3.1.3 (2018-02-28)
### Features
- Increase token entropy.
- Squash field changes on notification emails
- Minor bug fixes.
## 3.1.0 Perovskia Atriplicifolia (2017-03-10) ## 3.1.0 Perovskia Atriplicifolia (2017-03-10)
### Features ### Features

View File

@ -587,3 +587,22 @@ class QFilter(FilterBackend):
queryset = queryset.extra(where=[where_clause], params=[to_tsquery(q)]) queryset = queryset.extra(where=[where_clause], params=[to_tsquery(q)])
return queryset return queryset
class RoleFilter(BaseRelatedFieldsFilter):
filter_name = "role_id"
param_name = "role"
def filter_queryset(self, request, queryset, view):
Membership = apps.get_model('projects', 'Membership')
query = self._get_queryparams(request.QUERY_PARAMS)
if query:
if isinstance(query, dict):
memberships = Membership.objects.filter(**query).values_list("user_id", flat=True)
queryset = queryset.filter(assigned_to__in=memberships)
else:
memberships = Membership.objects.filter(query).values_list("user_id", flat=True)
if memberships:
queryset = queryset.filter(assigned_to__in=memberships)
return FilterBackend.filter_queryset(self, request, queryset, view)

View File

@ -50,6 +50,7 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
queryset = models.Issue.objects.all() queryset = models.Issue.objects.all()
permission_classes = (permissions.IssuePermission, ) permission_classes = (permissions.IssuePermission, )
filter_backends = (filters.CanViewIssuesFilterBackend, filter_backends = (filters.CanViewIssuesFilterBackend,
filters.RoleFilter,
filters.OwnersFilter, filters.OwnersFilter,
filters.AssignedToFilter, filters.AssignedToFilter,
filters.StatusesFilter, filters.StatusesFilter,
@ -188,6 +189,7 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
owners_filter_backends = (f for f in filter_backends if f != filters.OwnersFilter) 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) 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) severities_filter_backends = (f for f in filter_backends if f != filters.SeveritiesFilter)
roles_filter_backends = (f for f in filter_backends if f != filters.RoleFilter)
queryset = self.get_queryset() queryset = self.get_queryset()
querysets = { querysets = {
@ -197,7 +199,8 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W
"owners": self.filter_queryset(queryset, filter_backends=owners_filter_backends), "owners": self.filter_queryset(queryset, filter_backends=owners_filter_backends),
"priorities": self.filter_queryset(queryset, filter_backends=priorities_filter_backends), "priorities": self.filter_queryset(queryset, filter_backends=priorities_filter_backends),
"severities": self.filter_queryset(queryset, filter_backends=severities_filter_backends), "severities": self.filter_queryset(queryset, filter_backends=severities_filter_backends),
"tags": self.filter_queryset(queryset) "tags": self.filter_queryset(queryset),
"roles": self.filter_queryset(queryset, filter_backends=roles_filter_backends),
} }
return response.Ok(services.get_issues_filters_data(project, querysets)) return response.Ok(services.get_issues_filters_data(project, querysets))

View File

@ -420,6 +420,57 @@ def _get_issues_owners(project, queryset):
return sorted(result, key=itemgetter("full_name")) return sorted(result, key=itemgetter("full_name"))
def _get_issues_roles(project, queryset):
compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
where = queryset_where_tuple[0]
where_params = queryset_where_tuple[1]
extra_sql = """
WITH "issue_counters" AS (
SELECT DISTINCT "issues_issue"."status_id" "status_id",
"issues_issue"."id" "issue_id",
"projects_membership"."role_id" "role_id"
FROM "issues_issue"
INNER JOIN "projects_project"
ON ("issues_issue"."project_id" = "projects_project"."id")
LEFT OUTER JOIN "projects_membership"
ON "projects_membership"."user_id" = "issues_issue"."assigned_to_id"
WHERE {where}
),
"counters" AS (
SELECT "role_id" as "role_id",
COUNT("role_id") "count"
FROM "issue_counters"
GROUP BY "role_id"
)
SELECT "users_role"."id",
"users_role"."name",
"users_role"."order",
COALESCE("counters"."count", 0)
FROM "users_role"
LEFT OUTER JOIN "counters"
ON "counters"."role_id" = "users_role"."id"
WHERE "users_role"."project_id" = %s
ORDER BY "users_role"."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, order, count in rows:
result.append({
"id": id,
"name": _(name),
"color": None,
"order": order,
"count": count,
})
return sorted(result, key=itemgetter("order"))
def _get_issues_tags(project, queryset): def _get_issues_tags(project, queryset):
compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
@ -478,6 +529,7 @@ def get_issues_filters_data(project, querysets):
("assigned_to", _get_issues_assigned_to(project, querysets["assigned_to"])), ("assigned_to", _get_issues_assigned_to(project, querysets["assigned_to"])),
("owners", _get_issues_owners(project, querysets["owners"])), ("owners", _get_issues_owners(project, querysets["owners"])),
("tags", _get_issues_tags(project, querysets["tags"])), ("tags", _get_issues_tags(project, querysets["tags"])),
("roles", _get_issues_roles(project, querysets["roles"])),
]) ])
return data return data

View File

@ -51,6 +51,7 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
queryset = models.Task.objects.all() queryset = models.Task.objects.all()
permission_classes = (permissions.TaskPermission,) permission_classes = (permissions.TaskPermission,)
filter_backends = (filters.CanViewTasksFilterBackend, filter_backends = (filters.CanViewTasksFilterBackend,
filters.RoleFilter,
filters.OwnersFilter, filters.OwnersFilter,
filters.AssignedToFilter, filters.AssignedToFilter,
filters.StatusesFilter, filters.StatusesFilter,
@ -219,13 +220,15 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa
statuses_filter_backends = (f for f in filter_backends if f != filters.StatusesFilter) 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) 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) owners_filter_backends = (f for f in filter_backends if f != filters.OwnersFilter)
roles_filter_backends = (f for f in filter_backends if f != filters.RoleFilter)
queryset = self.get_queryset() queryset = self.get_queryset()
querysets = { querysets = {
"statuses": self.filter_queryset(queryset, filter_backends=statuses_filter_backends), "statuses": self.filter_queryset(queryset, filter_backends=statuses_filter_backends),
"assigned_to": self.filter_queryset(queryset, filter_backends=assigned_to_filter_backends), "assigned_to": self.filter_queryset(queryset, filter_backends=assigned_to_filter_backends),
"owners": self.filter_queryset(queryset, filter_backends=owners_filter_backends), "owners": self.filter_queryset(queryset, filter_backends=owners_filter_backends),
"tags": self.filter_queryset(queryset) "tags": self.filter_queryset(queryset),
"roles": self.filter_queryset(queryset, filter_backends=roles_filter_backends),
} }
return response.Ok(services.get_tasks_filters_data(project, querysets)) return response.Ok(services.get_tasks_filters_data(project, querysets))

View File

@ -279,6 +279,58 @@ def _get_tasks_assigned_to(project, queryset):
return sorted(result, key=itemgetter("full_name")) return sorted(result, key=itemgetter("full_name"))
def _get_tasks_roles(project, queryset):
compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
where = queryset_where_tuple[0]
where_params = queryset_where_tuple[1]
extra_sql = """
WITH "task_counters" AS (
SELECT DISTINCT "tasks_task"."status_id" "status_id",
"tasks_task"."id" "us_id",
"projects_membership"."role_id" "role_id"
FROM "tasks_task"
INNER JOIN "projects_project"
ON ("tasks_task"."project_id" = "projects_project"."id")
LEFT OUTER JOIN "projects_membership"
ON "projects_membership"."user_id" = "tasks_task"."assigned_to_id"
WHERE {where}
),
"counters" AS (
SELECT "role_id" as "role_id",
COUNT("role_id") "count"
FROM "task_counters"
GROUP BY "role_id"
)
SELECT "users_role"."id",
"users_role"."name",
"users_role"."order",
COALESCE("counters"."count", 0)
FROM "users_role"
LEFT OUTER JOIN "counters"
ON "counters"."role_id" = "users_role"."id"
WHERE "users_role"."project_id" = %s
ORDER BY "users_role"."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, order, count in rows:
result.append({
"id": id,
"name": _(name),
"color": None,
"order": order,
"count": count,
})
return sorted(result, key=itemgetter("order"))
def _get_tasks_owners(project, queryset): def _get_tasks_owners(project, queryset):
compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
queryset_where_tuple = queryset.query.where.as_sql(compiler, connection) queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
@ -384,6 +436,7 @@ def get_tasks_filters_data(project, querysets):
("assigned_to", _get_tasks_assigned_to(project, querysets["assigned_to"])), ("assigned_to", _get_tasks_assigned_to(project, querysets["assigned_to"])),
("owners", _get_tasks_owners(project, querysets["owners"])), ("owners", _get_tasks_owners(project, querysets["owners"])),
("tags", _get_tasks_tags(project, querysets["tags"])), ("tags", _get_tasks_tags(project, querysets["tags"])),
("roles", _get_tasks_roles(project, querysets["roles"])),
]) ])
return data return data

View File

@ -62,6 +62,7 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
permission_classes = (permissions.UserStoryPermission,) permission_classes = (permissions.UserStoryPermission,)
filter_backends = (base_filters.CanViewUsFilterBackend, filter_backends = (base_filters.CanViewUsFilterBackend,
filters.EpicFilter, filters.EpicFilter,
base_filters.RoleFilter,
base_filters.OwnersFilter, base_filters.OwnersFilter,
base_filters.AssignedToFilter, base_filters.AssignedToFilter,
base_filters.StatusesFilter, base_filters.StatusesFilter,
@ -306,6 +307,7 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
assigned_to_filter_backends = (f for f in filter_backends if f != base_filters.AssignedToFilter) assigned_to_filter_backends = (f for f in filter_backends if f != base_filters.AssignedToFilter)
owners_filter_backends = (f for f in filter_backends if f != base_filters.OwnersFilter) owners_filter_backends = (f for f in filter_backends if f != base_filters.OwnersFilter)
epics_filter_backends = (f for f in filter_backends if f != filters.EpicFilter) epics_filter_backends = (f for f in filter_backends if f != filters.EpicFilter)
roles_filter_backends = (f for f in filter_backends if f != base_filters.RoleFilter)
queryset = self.get_queryset() queryset = self.get_queryset()
querysets = { querysets = {
@ -313,7 +315,8 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi
"assigned_to": self.filter_queryset(queryset, filter_backends=assigned_to_filter_backends), "assigned_to": self.filter_queryset(queryset, filter_backends=assigned_to_filter_backends),
"owners": self.filter_queryset(queryset, filter_backends=owners_filter_backends), "owners": self.filter_queryset(queryset, filter_backends=owners_filter_backends),
"tags": self.filter_queryset(queryset), "tags": self.filter_queryset(queryset),
"epics": self.filter_queryset(queryset, filter_backends=epics_filter_backends) "epics": self.filter_queryset(queryset, filter_backends=epics_filter_backends),
"roles": self.filter_queryset(queryset, filter_backends=roles_filter_backends)
} }
return response.Ok(services.get_userstories_filters_data(project, querysets)) return response.Ok(services.get_userstories_filters_data(project, querysets))

View File

@ -589,6 +589,60 @@ def _get_userstories_epics(project, queryset):
return result return result
def _get_userstories_roles(project, queryset):
compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None)
queryset_where_tuple = queryset.query.where.as_sql(compiler, connection)
where = queryset_where_tuple[0]
where_params = queryset_where_tuple[1]
extra_sql = """
WITH "us_counters" AS (
SELECT DISTINCT "userstories_userstory"."status_id" "status_id",
"userstories_userstory"."id" "us_id",
"projects_membership"."role_id" "role_id"
FROM "userstories_userstory"
INNER JOIN "projects_project"
ON ("userstories_userstory"."project_id" = "projects_project"."id")
LEFT OUTER JOIN "epics_relateduserstory"
ON "userstories_userstory"."id" = "epics_relateduserstory"."user_story_id"
LEFT OUTER JOIN "projects_membership"
ON "projects_membership"."user_id" = "userstories_userstory"."assigned_to_id"
WHERE {where}
),
"counters" AS (
SELECT "role_id" as "role_id",
COUNT("role_id") "count"
FROM "us_counters"
GROUP BY "role_id"
)
SELECT "users_role"."id",
"users_role"."name",
"users_role"."order",
COALESCE("counters"."count", 0)
FROM "users_role"
LEFT OUTER JOIN "counters"
ON "counters"."role_id" = "users_role"."id"
WHERE "users_role"."project_id" = %s
ORDER BY "users_role"."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, order, count in rows:
result.append({
"id": id,
"name": _(name),
"color": None,
"order": order,
"count": count,
})
return sorted(result, key=itemgetter("order"))
def get_userstories_filters_data(project, querysets): def get_userstories_filters_data(project, querysets):
""" """
Given a project and an userstories queryset, return a simple data structure Given a project and an userstories queryset, return a simple data structure
@ -600,6 +654,7 @@ def get_userstories_filters_data(project, querysets):
("owners", _get_userstories_owners(project, querysets["owners"])), ("owners", _get_userstories_owners(project, querysets["owners"])),
("tags", _get_userstories_tags(project, querysets["tags"])), ("tags", _get_userstories_tags(project, querysets["tags"])),
("epics", _get_userstories_epics(project, querysets["epics"])), ("epics", _get_userstories_epics(project, querysets["epics"])),
("roles", _get_userstories_roles(project, querysets["roles"])),
]) ])
return data return data