From 1bbc3268fd69eba830039b5781fbf62b19dcca65 Mon Sep 17 00:00:00 2001 From: Miguel Gonzalez Date: Wed, 21 Feb 2018 06:59:26 +0100 Subject: [PATCH 1/4] Avoid empty notifications when no effective modifications (#1074) Fix TG-5341 --- taiga/projects/notifications/services.py | 10 ++++++++-- tests/integration/test_notifications.py | 8 ++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/taiga/projects/notifications/services.py b/taiga/projects/notifications/services.py index bc20de54..d0155eac 100644 --- a/taiga/projects/notifications/services.py +++ b/taiga/projects/notifications/services.py @@ -245,14 +245,20 @@ def send_sync_notifications(notification_id): """ notification = HistoryChangeNotification.objects.select_for_update().get(pk=notification_id) - # If the last modification is too recent we ignore it + # If the last modification is too recent we ignore it for the time being now = timezone.now() time_diff = now - notification.updated_datetime if time_diff.seconds < settings.CHANGE_NOTIFICATIONS_MIN_INTERVAL: return history_entries = tuple(notification.history_entries.all().order_by("created_at")) - history_entries = squash_history_entries(history_entries) + history_entries = list(squash_history_entries(history_entries)) + + # If there are no effective modifications we can delete this notification + # without further processing + if notification.history_type == HistoryType.change and not history_entries: + notification.delete() + return obj, _ = get_last_snapshot_for_key(notification.key) obj_class = get_model_from_key(obj.key) diff --git a/tests/integration/test_notifications.py b/tests/integration/test_notifications.py index a552cc4b..e7e9b70c 100644 --- a/tests/integration/test_notifications.py +++ b/tests/integration/test_notifications.py @@ -359,7 +359,7 @@ def test_send_notifications_using_services_method_for_user_stories(settings, mai history_change = f.HistoryEntryFactory.create( project=project, user={"pk": member1.user.id}, - comment="", + comment="test:change", type=HistoryType.change, key="userstories.userstory:{}".format(us.id), is_hidden=False, @@ -454,7 +454,7 @@ def test_send_notifications_using_services_method_for_tasks(settings, mail): history_change = f.HistoryEntryFactory.create( project=project, user={"pk": member1.user.id}, - comment="", + comment="test:change", type=HistoryType.change, key="tasks.task:{}".format(task.id), is_hidden=False, @@ -549,7 +549,7 @@ def test_send_notifications_using_services_method_for_issues(settings, mail): history_change = f.HistoryEntryFactory.create( project=project, user={"pk": member1.user.id}, - comment="", + comment="test:change", type=HistoryType.change, key="issues.issue:{}".format(issue.id), is_hidden=False, @@ -644,7 +644,7 @@ def test_send_notifications_using_services_method_for_wiki_pages(settings, mail) history_change = f.HistoryEntryFactory.create( project=project, user={"pk": member1.user.id}, - comment="", + comment="test:change", type=HistoryType.change, key="wiki.wikipage:{}".format(wiki.id), is_hidden=False, From 6d8c6df657d3a8d7e34e1c7fdfdd0eee291c3c2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 7 Nov 2017 18:53:22 +0100 Subject: [PATCH 2/4] Add role filtering --- taiga/base/filters.py | 19 +++++++++ taiga/projects/issues/api.py | 5 ++- taiga/projects/issues/services.py | 52 ++++++++++++++++++++++++ taiga/projects/tasks/api.py | 5 ++- taiga/projects/tasks/services.py | 53 +++++++++++++++++++++++++ taiga/projects/userstories/api.py | 5 ++- taiga/projects/userstories/services.py | 55 ++++++++++++++++++++++++++ 7 files changed, 191 insertions(+), 3 deletions(-) diff --git a/taiga/base/filters.py b/taiga/base/filters.py index b90740b1..a6090812 100644 --- a/taiga/base/filters.py +++ b/taiga/base/filters.py @@ -587,3 +587,22 @@ class QFilter(FilterBackend): queryset = queryset.extra(where=[where_clause], params=[to_tsquery(q)]) 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) diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py index 11c877fc..af8368a7 100644 --- a/taiga/projects/issues/api.py +++ b/taiga/projects/issues/api.py @@ -50,6 +50,7 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W queryset = models.Issue.objects.all() permission_classes = (permissions.IssuePermission, ) filter_backends = (filters.CanViewIssuesFilterBackend, + filters.RoleFilter, filters.OwnersFilter, filters.AssignedToFilter, 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) 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) + roles_filter_backends = (f for f in filter_backends if f != filters.RoleFilter) queryset = self.get_queryset() querysets = { @@ -197,7 +199,8 @@ class IssueViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, W "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) + "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)) diff --git a/taiga/projects/issues/services.py b/taiga/projects/issues/services.py index a2646d78..dbcb0d4e 100644 --- a/taiga/projects/issues/services.py +++ b/taiga/projects/issues/services.py @@ -420,6 +420,57 @@ def _get_issues_owners(project, queryset): 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): compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) 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"])), ("owners", _get_issues_owners(project, querysets["owners"])), ("tags", _get_issues_tags(project, querysets["tags"])), + ("roles", _get_issues_roles(project, querysets["roles"])), ]) return data diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index a2e62149..7b748e37 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -51,6 +51,7 @@ class TaskViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixin, Wa queryset = models.Task.objects.all() permission_classes = (permissions.TaskPermission,) filter_backends = (filters.CanViewTasksFilterBackend, + filters.RoleFilter, filters.OwnersFilter, filters.AssignedToFilter, 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) 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) + roles_filter_backends = (f for f in filter_backends if f != filters.RoleFilter) 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) + "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)) diff --git a/taiga/projects/tasks/services.py b/taiga/projects/tasks/services.py index 52f1bb55..c0a6272e 100644 --- a/taiga/projects/tasks/services.py +++ b/taiga/projects/tasks/services.py @@ -279,6 +279,58 @@ def _get_tasks_assigned_to(project, queryset): 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): compiler = connection.ops.compiler(queryset.query.compiler)(queryset.query, connection, None) 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"])), ("owners", _get_tasks_owners(project, querysets["owners"])), ("tags", _get_tasks_tags(project, querysets["tags"])), + ("roles", _get_tasks_roles(project, querysets["roles"])), ]) return data diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index 60256789..dc7cef82 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -62,6 +62,7 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi permission_classes = (permissions.UserStoryPermission,) filter_backends = (base_filters.CanViewUsFilterBackend, filters.EpicFilter, + base_filters.RoleFilter, base_filters.OwnersFilter, base_filters.AssignedToFilter, 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) 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) + roles_filter_backends = (f for f in filter_backends if f != base_filters.RoleFilter) queryset = self.get_queryset() querysets = { @@ -313,7 +315,8 @@ class UserStoryViewSet(OCCResourceMixin, VotedResourceMixin, HistoryResourceMixi "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), - "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)) diff --git a/taiga/projects/userstories/services.py b/taiga/projects/userstories/services.py index efd66229..7295a208 100644 --- a/taiga/projects/userstories/services.py +++ b/taiga/projects/userstories/services.py @@ -589,6 +589,60 @@ def _get_userstories_epics(project, queryset): 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): """ 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"])), ("tags", _get_userstories_tags(project, querysets["tags"])), ("epics", _get_userstories_epics(project, querysets["epics"])), + ("roles", _get_userstories_roles(project, querysets["roles"])), ]) return data From 2d54d1b43268ecb85503200ed00d3e99e43519ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Hermida?= Date: Tue, 27 Feb 2018 17:04:16 +0100 Subject: [PATCH 3/4] Fix file upload permissions --- settings/common.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/settings/common.py b/settings/common.py index 53c4c904..7aba66df 100644 --- a/settings/common.py +++ b/settings/common.py @@ -211,9 +211,11 @@ STATICFILES_DIRS = ( # Don't forget to use absolute paths, not relative paths. ) -# Defautl storage +# Default storage DEFAULT_FILE_STORAGE = "taiga.base.storage.FileSystemStorage" +FILE_UPLOAD_PERMISSIONS = 0o644 + SECRET_KEY = "aw3+t2r(8(0kkrhg8)gx6i96v5^kv%6cfep9wxfom0%7dy0m9e" TEMPLATES = [ From 92368c38c44b37d11a683c8bc97b7e0f08882a0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Hermida?= Date: Mon, 5 Mar 2018 22:30:36 +0100 Subject: [PATCH 4/4] Update changelog --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12de9155..3c8f000e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,20 @@ # 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) ### Features