From 1101c5a719d8ca239126a66d7d08bff75ea10aa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 2 Dec 2014 11:44:01 +0100 Subject: [PATCH 1/3] Add bulk update order in user story and taskboard for tasks --- taiga/projects/tasks/api.py | 25 +++++++++++++++++++ taiga/projects/tasks/permissions.py | 1 + taiga/projects/tasks/serializers.py | 19 +++++++++++---- taiga/projects/tasks/services.py | 32 +++++++++++++++++++++++++ taiga/projects/tasks/validators.py | 14 +++++++++++ taiga/projects/userstories/api.py | 37 +++++++---------------------- tests/integration/test_tasks.py | 24 +++++++++++++++++++ 7 files changed, 119 insertions(+), 33 deletions(-) create mode 100644 taiga/projects/tasks/validators.py diff --git a/taiga/projects/tasks/api.py b/taiga/projects/tasks/api.py index d3727b78..ed7cd208 100644 --- a/taiga/projects/tasks/api.py +++ b/taiga/projects/tasks/api.py @@ -15,6 +15,7 @@ # along with this program. If not, see . from django.utils.translation import ugettext_lazy as _ +from django.shortcuts import get_object_or_404 from taiga.base import filters, response from taiga.base import exceptions as exc @@ -79,3 +80,27 @@ class TaskViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, return response.Ok(tasks_serialized.data) return response.BadRequest(serializer.errors) + + def _bulk_update_order(self, order_field, request, **kwargs): + serializer = serializers.UpdateTasksOrderBulkSerializer(data=request.DATA) + if not serializer.is_valid(): + return response.BadRequest(serializer.errors) + + data = serializer.data + project = get_object_or_404(Project, pk=data["project_id"]) + + self.check_permissions(request, "bulk_update_order", project) + services.update_tasks_order_in_bulk(data["bulk_tasks"], + project=project, + field=order_field) + services.snapshot_tasks_in_bulk(data["bulk_tasks"], request.user) + + return response.NoContent() + + @list_route(methods=["POST"]) + def bulk_update_taskboard_order(self, request, **kwargs): + return self._bulk_update_order("taskboard_order", request, **kwargs) + + @list_route(methods=["POST"]) + def bulk_update_us_order(self, request, **kwargs): + return self._bulk_update_order("us_order", request, **kwargs) diff --git a/taiga/projects/tasks/permissions.py b/taiga/projects/tasks/permissions.py index 46cde396..1983d7a6 100644 --- a/taiga/projects/tasks/permissions.py +++ b/taiga/projects/tasks/permissions.py @@ -27,3 +27,4 @@ class TaskPermission(TaigaResourcePermission): destroy_perms = HasProjectPerm('delete_task') list_perms = AllowAny() bulk_create_perms = HasProjectPerm('add_task') + bulk_update_order_perms = HasProjectPerm('modify_task') diff --git a/taiga/projects/tasks/serializers.py b/taiga/projects/tasks/serializers.py index 2dcf6097..fe71186b 100644 --- a/taiga/projects/tasks/serializers.py +++ b/taiga/projects/tasks/serializers.py @@ -18,9 +18,9 @@ from rest_framework import serializers from taiga.base.serializers import Serializer, PickleField, NeighborsSerializerMixin, PgArrayField from taiga.mdrender.service import render as mdrender -from taiga.projects.validators import ProjectExistsValidator, TaskStatusExistsValidator +from taiga.projects.validators import ProjectExistsValidator from taiga.projects.milestones.validators import SprintExistsValidator -from taiga.projects.userstories.validators import UserStoryExistsValidator +from taiga.projects.tasks.validators import TaskExistsValidator from taiga.projects.notifications.validators import WatchersValidator from . import models @@ -70,10 +70,21 @@ class NeighborTaskSerializer(serializers.ModelSerializer): depth = 0 -class TasksBulkSerializer(ProjectExistsValidator, SprintExistsValidator, TaskStatusExistsValidator, - UserStoryExistsValidator, Serializer): +class TasksBulkSerializer(ProjectExistsValidator, SprintExistsValidator, + TaskExistsValidator, Serializer): project_id = serializers.IntegerField() sprint_id = serializers.IntegerField() status_id = serializers.IntegerField(required=False) us_id = serializers.IntegerField(required=False) bulk_tasks = serializers.CharField() + +## Order bulk serializers + +class _TaskOrderBulkSerializer(TaskExistsValidator, Serializer): + task_id = serializers.IntegerField() + order = serializers.IntegerField() + + +class UpdateTasksOrderBulkSerializer(ProjectExistsValidator, Serializer): + project_id = serializers.IntegerField() + bulk_tasks = _TaskOrderBulkSerializer(many=True) diff --git a/taiga/projects/tasks/services.py b/taiga/projects/tasks/services.py index 6e9e571c..379d1321 100644 --- a/taiga/projects/tasks/services.py +++ b/taiga/projects/tasks/services.py @@ -15,6 +15,8 @@ # along with this program. If not, see . from taiga.base.utils import db, text +from taiga.projects.history.services import take_snapshot +from taiga.events import events from . import models @@ -43,3 +45,33 @@ def create_tasks_in_bulk(bulk_data, callback=None, precall=None, **additional_fi tasks = get_tasks_from_bulk(bulk_data, **additional_fields) db.save_in_bulk(tasks, callback, precall) return tasks + + +def update_tasks_order_in_bulk(bulk_data:list, field:str, project:object): + """ + Update the order of some tasks. + `bulk_data` should be a list of tuples with the following format: + + [(, {: , ...}), ...] + """ + task_ids = [] + new_order_values = [] + for task_data in bulk_data: + task_ids.append(task_data["task_id"]) + new_order_values.append({field: task_data["order"]}) + + events.emit_event_for_ids(ids=task_ids, + content_type="tasks.task", + projectid=project.pk) + + db.update_in_bulk_with_ids(task_ids, new_order_values, model=models.Task) + + +def snapshot_tasks_in_bulk(bulk_data, user): + task_ids = [] + for task_data in bulk_data: + try: + task = models.Task.objects.get(pk=task_data['task_id']) + take_snapshot(task, user=user) + except models.UserStory.DoesNotExist: + pass diff --git a/taiga/projects/tasks/validators.py b/taiga/projects/tasks/validators.py new file mode 100644 index 00000000..c7f1293b --- /dev/null +++ b/taiga/projects/tasks/validators.py @@ -0,0 +1,14 @@ +from django.utils.translation import ugettext_lazy as _ + +from rest_framework import serializers + +from . import models + + +class TaskExistsValidator: + def validate_task_id(self, attrs, source): + value = attrs[source] + if not models.Task.objects.filter(pk=value).exists(): + msg = _("There's no task with that id") + raise serializers.ValidationError(msg) + return attrs diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index f95b0007..166ddd26 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -110,8 +110,7 @@ class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMi return response.Ok(user_stories_serialized.data) return response.BadRequest(serializer.errors) - @list_route(methods=["POST"]) - def bulk_update_backlog_order(self, request, **kwargs): + def _bulk_update_order(self, order_field, request, **kwargs): serializer = serializers.UpdateUserStoriesOrderBulkSerializer(data=request.DATA) if not serializer.is_valid(): return response.BadRequest(serializer.errors) @@ -122,42 +121,22 @@ class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMi self.check_permissions(request, "bulk_update_order", project) services.update_userstories_order_in_bulk(data["bulk_stories"], project=project, - field="backlog_order") + field=order_field) services.snapshot_userstories_in_bulk(data["bulk_stories"], request.user) return response.NoContent() + @list_route(methods=["POST"]) + def bulk_update_backlog_order(self, request, **kwargs): + return self._bulk_update_order("backlog_order", request, **kwargs) + @list_route(methods=["POST"]) def bulk_update_sprint_order(self, request, **kwargs): - serializer = serializers.UpdateUserStoriesOrderBulkSerializer(data=request.DATA) - if not serializer.is_valid(): - return response.BadRequest(serializer.errors) - - data = serializer.data - project = get_object_or_404(Project, pk=data["project_id"]) - - self.check_permissions(request, "bulk_update_order", project) - services.update_userstories_order_in_bulk(data["bulk_stories"], - project=project, - field="sprint_order") - services.snapshot_userstories_in_bulk(data["bulk_stories"], request.user) - return response.NoContent() + return self._bulk_update_order("sprint_order", request, **kwargs) @list_route(methods=["POST"]) def bulk_update_kanban_order(self, request, **kwargs): - serializer = serializers.UpdateUserStoriesOrderBulkSerializer(data=request.DATA) - if not serializer.is_valid(): - return response.BadRequest(serializer.errors) - - data = serializer.data - project = get_object_or_404(Project, pk=data["project_id"]) - - self.check_permissions(request, "bulk_update_order", project) - services.update_userstories_order_in_bulk(data["bulk_stories"], - project=project, - field="kanban_order") - services.snapshot_userstories_in_bulk(data["bulk_stories"], request.user) - return response.NoContent() + return self._bulk_update_order("kanban_order", request, **kwargs) @transaction.atomic def create(self, *args, **kwargs): diff --git a/tests/integration/test_tasks.py b/tests/integration/test_tasks.py index d18bd1a8..924e8e99 100644 --- a/tests/integration/test_tasks.py +++ b/tests/integration/test_tasks.py @@ -87,3 +87,27 @@ def test_api_create_invalid_task(client): client.login(us.owner) response = client.json.post(url, json.dumps(data)) assert response.status_code == 400 + + +def test_api_update_order_in_bulk(client): + project = f.create_project() + f.MembershipFactory.create(project=project, user=project.owner, is_owner=True) + task1 = f.create_task(project=project) + task2 = f.create_task(project=project) + + url1 = reverse("tasks-bulk-update-taskboard-order") + url2 = reverse("tasks-bulk-update-us-order") + + data = { + "project_id": project.id, + "bulk_tasks": [{"task_id": task1.id, "order": 1}, + {"task_id": task2.id, "order": 2}] + } + + client.login(project.owner) + + response1 = client.json.post(url1, json.dumps(data)) + response2 = client.json.post(url2, json.dumps(data)) + + assert response1.status_code == 204, response1.data + assert response2.status_code == 204, response2.data From d9f48b07538304b9cbc49aed654d7a7345602d0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Tue, 2 Dec 2014 18:09:27 +0100 Subject: [PATCH 2/3] No notify hidden (ordering) changes --- taiga/projects/notifications/services.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/taiga/projects/notifications/services.py b/taiga/projects/notifications/services.py index ecd3a2e8..a278cee2 100644 --- a/taiga/projects/notifications/services.py +++ b/taiga/projects/notifications/services.py @@ -207,6 +207,9 @@ def _make_template_mail(name:str): @transaction.atomic def send_notifications(obj, *, history): + if history.is_hidden: + return None + key = make_key_from_model_object(obj) owner = User.objects.get(pk=history.user["pk"]) notification, created = (HistoryChangeNotification.objects.select_for_update() From dde7bf12b92fb7c967b00cd025d12c87a4a8f969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Tue, 2 Dec 2014 19:07:48 +0100 Subject: [PATCH 3/3] Don't show order changes in emails --- .../templates/emails/includes/fields_diff-html.jinja | 6 +++++- .../templates/emails/includes/fields_diff-text.jinja | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/taiga/projects/history/templates/emails/includes/fields_diff-html.jinja b/taiga/projects/history/templates/emails/includes/fields_diff-html.jinja index 150269b7..12090cd6 100644 --- a/taiga/projects/history/templates/emails/includes/fields_diff-html.jinja +++ b/taiga/projects/history/templates/emails/includes/fields_diff-html.jinja @@ -2,7 +2,11 @@ "description", "description_html", "content", - "content_html" + "content_html", + "backlog_order", + "kanban_order", + "taskboard_order", + "us_order" ] %}
diff --git a/taiga/projects/history/templates/emails/includes/fields_diff-text.jinja b/taiga/projects/history/templates/emails/includes/fields_diff-text.jinja index 71e6dbbe..88d80fbc 100644 --- a/taiga/projects/history/templates/emails/includes/fields_diff-text.jinja +++ b/taiga/projects/history/templates/emails/includes/fields_diff-text.jinja @@ -2,7 +2,11 @@ "description_diff", "description_html", "content_diff", - "content_html" + "content_html", + "backlog_order", + "kanban_order", + "taskboard_order", + "us_order" ] %} {% for field_name, values in changed_fields.items() %} {% if field_name not in excluded_fields %}