diff --git a/taiga/projects/issues/api.py b/taiga/projects/issues/api.py index dd9a392c..f68aa71f 100644 --- a/taiga/projects/issues/api.py +++ b/taiga/projects/issues/api.py @@ -24,7 +24,7 @@ from rest_framework import status from taiga.base import filters from taiga.base import exceptions as exc -from taiga.base.decorators import detail_route +from taiga.base.decorators import detail_route, list_route from taiga.base.api import ModelCrudViewSet, ModelListViewSet from taiga.base import tags @@ -34,11 +34,12 @@ from taiga.projects.notifications import WatchedResourceMixin from taiga.projects.occ import OCCResourceMixin from taiga.projects.history import HistoryResourceMixin - +from taiga.projects.models import Project from taiga.projects.votes.utils import attach_votescount_to_queryset from taiga.projects.votes import services as votes_service from taiga.projects.votes import serializers as votes_serializers from . import models +from . import services from . import permissions from . import serializers @@ -152,6 +153,28 @@ class IssueViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMixin, if obj.type and obj.type.project != obj.project: raise exc.PermissionDenied(_("You don't have permissions to set this type to this issue.")) + @list_route(methods=["POST"]) + def bulk_create(self, request, **kwargs): + bulk_issues = request.DATA.get('bulkIssues', None) + if bulk_issues is None: + raise exc.BadRequest(_('bulkIssues parameter is mandatory')) + + project_id = request.DATA.get('projectId', None) + if project_id is None: + raise exc.BadRequest(_('projectId parameter is mandatory')) + + project = get_object_or_404(Project, id=project_id) + + self.check_permissions(request, 'bulk_create', project) + + issues = services.create_issues_in_bulk( + bulk_issues, callback=self.post_save, project=project, owner=request.user, + status=project.default_issue_status, severity=project.default_severity, + priority=project.default_priority, type=project.default_issue_type) + + issues_serialized = self.serializer_class(issues, many=True) + return Response(data=issues_serialized.data) + @detail_route(methods=['post']) def upvote(self, request, pk=None): issue = get_object_or_404(models.Issue, pk=pk) diff --git a/taiga/projects/issues/permissions.py b/taiga/projects/issues/permissions.py index b1fd68b1..89582ee7 100644 --- a/taiga/projects/issues/permissions.py +++ b/taiga/projects/issues/permissions.py @@ -30,6 +30,7 @@ class IssuePermission(ResourcePermission): list_perms = AllowAny() upvote_perms = IsAuthenticated() & HasProjectPerm('vote_issues') downvote_perms = IsAuthenticated() & HasProjectPerm('vote_issues') + bulk_create_perms = IsAuthenticated() & (HasProjectPerm('add_issue_to_project') | HasProjectPerm('add_issue')) class HasIssueIdUrlParam(PermissionComponent): diff --git a/taiga/projects/issues/services.py b/taiga/projects/issues/services.py new file mode 100644 index 00000000..752557b5 --- /dev/null +++ b/taiga/projects/issues/services.py @@ -0,0 +1,60 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from taiga.base.utils import db, text + +from . import models + + +def get_issues_from_bulk(bulk_data, **additional_fields): + """Convert `bulk_data` into a list of issues. + + :param bulk_data: List of issues in bulk format. + :param additional_fields: Additional fields when instantiating each issue. + + :return: List of `Issue` instances. + """ + return [models.Issue(subject=line, **additional_fields) + for line in text.split_in_lines(bulk_data)] + + +def create_issues_in_bulk(bulk_data, callback=None, **additional_fields): + """Create issues from `bulk_data`. + + :param bulk_data: List of issues in bulk format. + :param callback: Callback to execute after each issue save. + :param additional_fields: Additional fields when instantiating each issue. + + :return: List of created `Issue` instances. + """ + issues = get_issues_from_bulk(bulk_data, **additional_fields) + db.save_in_bulk(issues, callback) + return issues + + +def update_issues_order_in_bulk(bulk_data): + """Update the order of some issues. + + `bulk_data` should be a list of tuples with the following format: + + [(, ), ...] + """ + issue_ids = [] + new_order_values = [] + for issue_id, new_order_value in bulk_data: + issue_ids.append(issue_id) + new_order_values.append({"order": new_order_value}) + db.update_in_bulk_with_ids(issue_ids, new_order_values, model=models.Issue) diff --git a/tests/factories.py b/tests/factories.py index e0a9ae73..e20ac308 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -376,3 +376,18 @@ def create_userstory(**kwargs): defaults.update(kwargs) return UserStoryFactory(**defaults) + + +def create_project(**kwargs): + "Create a project along with its dependencies" + defaults = {} + defaults.update(kwargs) + + project = ProjectFactory.create(**defaults) + project.default_issue_status = IssueStatusFactory.create(project=project) + project.default_severity = SeverityFactory.create(project=project) + project.default_priority = PriorityFactory.create(project=project) + project.default_issue_type = IssueTypeFactory.create(project=project) + project.save() + + return project diff --git a/tests/integration/test_issues.py b/tests/integration/test_issues.py index 20a3993a..3cc2b64c 100644 --- a/tests/integration/test_issues.py +++ b/tests/integration/test_issues.py @@ -1,12 +1,61 @@ +from unittest import mock + import pytest from django.core.urlresolvers import reverse +from taiga.projects.issues import services, models + from .. import factories as f pytestmark = pytest.mark.django_db +def test_get_issues_from_bulk(): + data = """ +Issue #1 +Issue #2 +""" + issues = services.get_issues_from_bulk(data) + + assert len(issues) == 2 + assert issues[0].subject == "Issue #1" + assert issues[1].subject == "Issue #2" + + +@mock.patch("taiga.projects.issues.services.db") +def test_create_issues_in_bulk(db): + data = """ +Issue #1 +Issue #2 +""" + issues = services.create_issues_in_bulk(data) + + db.save_in_bulk.assert_called_once_with(issues, None) + + +@mock.patch("taiga.projects.issues.services.db") +def test_update_issues_order_in_bulk(db): + data = [(1, 1), (2, 2)] + services.update_issues_order_in_bulk(data) + + db.update_in_bulk_with_ids.assert_called_once_with([1, 2], [{"order": 1}, {"order": 2}], + model=models.Issue) + + +def test_api_create_issues_in_bulk(client): + project = f.create_project() + + url = reverse("issues-bulk-create") + data = {"bulkIssues": "Issue #1\nIssue #2", + "projectId": project.id} + + client.login(project.owner) + response = client.json.post(url, data) + + assert response.status_code == 200, response.data + + def test_api_filter_by_subject(client): f.create_issue() issue = f.create_issue(subject="some random subject")