diff --git a/requirements.txt b/requirements.txt index 10375718..2810c5f9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,7 +21,7 @@ diff-match-patch==20121119 requests==2.4.1 easy-thumbnails==2.1 -celery==3.1.12 +celery==3.1.17 redis==2.10.3 Unidecode==0.04.16 raven==5.1.1 diff --git a/settings/common.py b/settings/common.py index 81a91568..e27df665 100644 --- a/settings/common.py +++ b/settings/common.py @@ -201,6 +201,7 @@ INSTALLED_APPS = [ "rest_framework", "djmail", "django_jinja", + "django_jinja.contrib._humanize", "easy_thumbnails", "raven.contrib.django.raven_compat", ] @@ -300,7 +301,8 @@ REST_FRAMEWORK = { "DEFAULT_THROTTLE_RATES": { "anon": None, "user": None, - "import-mode": None + "import-mode": None, + "import-dump-mode": "1/minute", }, "FILTER_BACKEND": "taiga.base.filters.FilterBackend", "EXCEPTION_HANDLER": "taiga.base.exceptions.exception_handler", @@ -362,6 +364,9 @@ PROJECT_MODULES_CONFIGURATORS = { BITBUCKET_VALID_ORIGIN_IPS = ["131.103.20.165", "131.103.20.166"] GITLAB_VALID_ORIGIN_IPS = [] +EXPORTS_TTL = 60 * 60 * 24 # 24 hours +CELERY_ENABLED = False + # NOTE: DON'T INSERT MORE SETTINGS AFTER THIS LINE TEST_RUNNER="django.test.runner.DiscoverRunner" diff --git a/settings/testing.py b/settings/testing.py index 2df79576..c3fd878d 100644 --- a/settings/testing.py +++ b/settings/testing.py @@ -28,5 +28,6 @@ INSTALLED_APPS = INSTALLED_APPS + ["tests"] REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"] = { "anon": None, "user": None, - "import-mode": None + "import-mode": None, + "import-dump-mode": None, } diff --git a/taiga/base/management/commands/test_emails.py b/taiga/base/management/commands/test_emails.py index e566c51e..d8e021cd 100644 --- a/taiga/base/management/commands/test_emails.py +++ b/taiga/base/management/commands/test_emails.py @@ -14,7 +14,10 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import datetime + from django.core.management.base import BaseCommand, CommandError +from django.utils import timezone from djmail.template_mail import MagicMailBuilder, InlineCSSTemplateMail @@ -76,6 +79,32 @@ class Command(BaseCommand): email = mbuilder.change_email(test_email, context) email.send() + # Export/Import emails + context = { + "user": User.objects.all().order_by("?").first(), + "error_subject": "Error generating project dump", + "error_message": "Error generating project dump", + } + email = mbuilder.export_import_error(test_email, context) + email.send() + + deletion_date = timezone.now() + datetime.timedelta(seconds=60*60*24) + context = { + "url": "http://dummyurl.com", + "user": User.objects.all().order_by("?").first(), + "project": Project.objects.all().order_by("?").first(), + "deletion_date": deletion_date, + } + email = mbuilder.dump_project(test_email, context) + email.send() + + context = { + "user": User.objects.all().order_by("?").first(), + "project": Project.objects.all().order_by("?").first(), + } + email = mbuilder.load_dump(test_email, context) + email.send() + # Notification emails notification_emails = [ "issues/issue-change", diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py index 8277be63..15fda9ca 100644 --- a/taiga/export_import/api.py +++ b/taiga/export_import/api.py @@ -14,17 +14,24 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import json +import codecs + from rest_framework.exceptions import APIException from rest_framework.response import Response +from rest_framework.decorators import throttle_classes from rest_framework import status from django.utils.decorators import method_decorator +from django.utils.translation import ugettext_lazy as _ from django.db.transaction import atomic from django.db.models import signals +from django.conf import settings from taiga.base.api.mixins import CreateModelMixin from taiga.base.api.viewsets import GenericViewSet -from taiga.base.decorators import detail_route +from taiga.base.decorators import detail_route, list_route +from taiga.base import exceptions as exc from taiga.projects.models import Project, Membership from taiga.projects.issues.models import Issue @@ -32,15 +39,46 @@ from . import mixins from . import serializers from . import service from . import permissions +from . import tasks +from . import dump_service +from . import throttling + +from taiga.base.api.utils import get_object_or_404 class Http400(APIException): status_code = 400 +class ProjectExporterViewSet(mixins.ImportThrottlingPolicyMixin, GenericViewSet): + model = Project + permission_classes = (permissions.ImportExportPermission, ) + + def retrieve(self, request, pk, *args, **kwargs): + throttle = throttling.ImportDumpModeRateThrottle() + + if not throttle.allow_request(request, self): + self.throttled(request, throttle.wait()) + + project = get_object_or_404(self.get_queryset(), pk=pk) + self.check_permissions(request, 'export_project', project) + + if settings.CELERY_ENABLED: + task = tasks.dump_project.delay(request.user, project) + tasks.delete_project_dump.apply_async((project.pk,), countdown=settings.EXPORTS_TTL) + return Response({"export-id": task.id}, status=status.HTTP_202_ACCEPTED) + + return Response( + service.project_to_dict(project), + status=status.HTTP_200_OK, + headers={ + "Content-Disposition": "attachment; filename={}.json".format(project.slug) + } + ) + class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixin, GenericViewSet): model = Project - permission_classes = (permissions.ImportPermission, ) + permission_classes = (permissions.ImportExportPermission, ) @method_decorator(atomic) def create(self, request, *args, **kwargs): @@ -113,6 +151,39 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi headers = self.get_success_headers(response_data) return Response(response_data, status=status.HTTP_201_CREATED, headers=headers) + @list_route(methods=["POST"]) + @method_decorator(atomic) + def load_dump(self, request): + throttle = throttling.ImportDumpModeRateThrottle() + + if not throttle.allow_request(request, self): + self.throttled(request, throttle.wait()) + + self.check_permissions(request, "load_dump", None) + + dump = request.FILES.get('dump', None) + + if not dump: + raise exc.WrongArguments(_("Needed dump file")) + + reader = codecs.getreader("utf-8") + + try: + dump = json.load(reader(dump)) + except Exception: + raise exc.WrongArguments(_("Invalid dump format")) + + if Project.objects.filter(slug=dump['slug']).exists(): + del dump['slug'] + + if settings.CELERY_ENABLED: + task = tasks.load_project_dump.delay(request.user, dump) + return Response({"import-id": task.id}, status=status.HTTP_202_ACCEPTED) + + dump_service.dict_to_project(dump, request.user.email) + return Response(None, status=status.HTTP_204_NO_CONTENT) + + @detail_route(methods=['post']) @method_decorator(atomic) def issue(self, request, *args, **kwargs): diff --git a/taiga/export_import/dump_service.py b/taiga/export_import/dump_service.py index 376f5f28..e4758e97 100644 --- a/taiga/export_import/dump_service.py +++ b/taiga/export_import/dump_service.py @@ -72,6 +72,12 @@ def store_issues(project, data): return issues +def store_tags_colors(project, data): + project.tags_colors = data.get("tags_colors", []) + project.save() + return None + + def dict_to_project(data, owner=None): if owner: data['owner'] = owner @@ -148,3 +154,7 @@ def dict_to_project(data, owner=None): if service.get_errors(clear=False): raise TaigaImportError('error importing issues') + + store_tags_colors(proj, data) + + return proj diff --git a/taiga/export_import/permissions.py b/taiga/export_import/permissions.py index 00e34fa1..2f63d272 100644 --- a/taiga/export_import/permissions.py +++ b/taiga/export_import/permissions.py @@ -19,6 +19,8 @@ from taiga.base.api.permissions import (TaigaResourcePermission, IsProjectOwner, IsAuthenticated) -class ImportPermission(TaigaResourcePermission): +class ImportExportPermission(TaigaResourcePermission): import_project_perms = IsAuthenticated() import_item_perms = IsProjectOwner() + export_project_perms = IsProjectOwner() + load_dump_perms = IsAuthenticated() diff --git a/taiga/export_import/serializers.py b/taiga/export_import/serializers.py index 77773db3..08225694 100644 --- a/taiga/export_import/serializers.py +++ b/taiga/export_import/serializers.py @@ -46,8 +46,10 @@ class AttachedFileField(serializers.WritableField): if not obj: return None + data = base64.b64encode(obj.read()).decode('utf-8') + return OrderedDict([ - ("data", base64.b64encode(obj.read()).decode('utf-8')), + ("data", data), ("name", os.path.basename(obj.name)), ]) @@ -120,7 +122,7 @@ class ProjectRelatedField(serializers.RelatedField): class HistoryUserField(JsonField): def to_native(self, obj): - if obj is None: + if obj is None or obj == {}: return [] try: user = users_models.User.objects.get(pk=obj['pk']) @@ -190,7 +192,7 @@ class HistoryExportSerializer(serializers.ModelSerializer): class Meta: model = history_models.HistoryEntry - exclude = ("id", "comment_html") + exclude = ("id", "comment_html", "key") class HistoryExportSerializerMixin(serializers.ModelSerializer): diff --git a/taiga/export_import/service.py b/taiga/export_import/service.py index 3f1b3145..253f8f9f 100644 --- a/taiga/export_import/service.py +++ b/taiga/export_import/service.py @@ -84,7 +84,7 @@ def store_choice(project, data, field, serializer): def store_choices(project, data, field, serializer): result = [] - for choice_data in data[field]: + for choice_data in data.get(field, []): result.append(store_choice(project, choice_data, field, serializer)) return result @@ -102,7 +102,7 @@ def store_role(project, role): def store_roles(project, data): results = [] - for role in data['roles']: + for role in data.get('roles', []): results.append(store_role(project, role)) return results diff --git a/taiga/export_import/tasks.py b/taiga/export_import/tasks.py new file mode 100644 index 00000000..97b08a8a --- /dev/null +++ b/taiga/export_import/tasks.py @@ -0,0 +1,82 @@ +# 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 . + +import datetime + +from django.core.files.storage import default_storage +from django.core.files.base import ContentFile +from django.utils import timezone +from django.conf import settings + +from djmail.template_mail import MagicMailBuilder + +from taiga.celery import app + +from .service import project_to_dict +from .dump_service import dict_to_project +from .renderers import ExportRenderer + + +@app.task(bind=True) +def dump_project(self, user, project): + mbuilder = MagicMailBuilder() + + path = "exports/{}/{}.json".format(project.pk, self.request.id) + + try: + content = ContentFile(ExportRenderer().render(project_to_dict(project), renderer_context={"indent": 4}).decode('utf-8')) + default_storage.save(path, content) + url = default_storage.url(path) + except Exception: + email = mbuilder.export_import_error( + user.email, + { + "user": user, + "error_subject": "Error generating project dump", + "error_message": "Error generating project dump", + } + ) + email.send() + return + + deletion_date = timezone.now() + datetime.timedelta(seconds=settings.EXPORTS_TTL) + email = mbuilder.dump_project(user.email, {"url": url, "project": project, "user": user, "deletion_date": deletion_date}) + email.send() + +@app.task +def delete_project_dump(project_id, task_id): + default_storage.delete("exports/{}/{}.json".format(project_id, task_id)) + +@app.task +def load_project_dump(user, dump): + mbuilder = MagicMailBuilder() + + try: + project = dict_to_project(dump, user.email) + except Exception: + email = mbuilder.export_import_error( + user.email, + { + "user": user, + "error_subject": "Error loading project dump", + "error_message": "Error loading project dump", + } + ) + email.send() + return + + email = mbuilder.load_dump(user.email, {"user": user, "project": project}) + email.send() diff --git a/taiga/export_import/templates/emails/dump_project-body-html.jinja b/taiga/export_import/templates/emails/dump_project-body-html.jinja new file mode 100644 index 00000000..f1109ccb --- /dev/null +++ b/taiga/export_import/templates/emails/dump_project-body-html.jinja @@ -0,0 +1,28 @@ +{% extends "emails/base.jinja" %} + +{% block body %} + + + + +
+

Project dump generated

+

Hello {{ user.get_full_name() }},

+

Your project dump has been correctly generated.

+

You can download it from here: {{ url }}

+

This file will be deleted on {{ deletion_date|date("r") }}.

+

The Taiga Team

+
+{% endblock %} + +{% block footer %} + Copyright © 2014 Taiga Agile, LLC, All rights reserved. +
+ Contact us: +
+ Support: + support@taiga.io +
+ Our mailing address is: + https://groups.google.com/forum/#!forum/taigaio +{% endblock %} diff --git a/taiga/export_import/templates/emails/dump_project-body-text.jinja b/taiga/export_import/templates/emails/dump_project-body-text.jinja new file mode 100644 index 00000000..4874d4b5 --- /dev/null +++ b/taiga/export_import/templates/emails/dump_project-body-text.jinja @@ -0,0 +1,9 @@ +Hello {{ user.get_full_name() }}, + +Your project dump has been correctly generated. You can download it from here: + +{{ url }} + +This file will be deleted on {{ deletion_date|date("r") }}. + +The Taiga Team diff --git a/taiga/export_import/templates/emails/dump_project-subject.jinja b/taiga/export_import/templates/emails/dump_project-subject.jinja new file mode 100644 index 00000000..cdd31e44 --- /dev/null +++ b/taiga/export_import/templates/emails/dump_project-subject.jinja @@ -0,0 +1 @@ +[Taiga] Your project dump has been generated diff --git a/taiga/export_import/templates/emails/export_import_error-body-html.jinja b/taiga/export_import/templates/emails/export_import_error-body-html.jinja new file mode 100644 index 00000000..c3fb40ae --- /dev/null +++ b/taiga/export_import/templates/emails/export_import_error-body-html.jinja @@ -0,0 +1,14 @@ +{% extends "emails/base.jinja" %} + +{% block body %} + + + + +
+

{{ error_message }}

+

Hello {{ user.get_full_name() }},

+

Please, contact with the support team at support@taiga.io

+

The Taiga Team

+
+{% endblock %} diff --git a/taiga/export_import/templates/emails/export_import_error-body-text.jinja b/taiga/export_import/templates/emails/export_import_error-body-text.jinja new file mode 100644 index 00000000..6ef22223 --- /dev/null +++ b/taiga/export_import/templates/emails/export_import_error-body-text.jinja @@ -0,0 +1,7 @@ +Hello {{ user.get_full_name() }}, + +{{ error_message }} + +Please, contact with the support team at support@taiga.io + +The Taiga Team diff --git a/taiga/export_import/templates/emails/export_import_error-subject.jinja b/taiga/export_import/templates/emails/export_import_error-subject.jinja new file mode 100644 index 00000000..67eaf97f --- /dev/null +++ b/taiga/export_import/templates/emails/export_import_error-subject.jinja @@ -0,0 +1 @@ +[Taiga] {{ error_subject }} diff --git a/taiga/export_import/templates/emails/load_dump-body-html.jinja b/taiga/export_import/templates/emails/load_dump-body-html.jinja new file mode 100644 index 00000000..a253fe00 --- /dev/null +++ b/taiga/export_import/templates/emails/load_dump-body-html.jinja @@ -0,0 +1,29 @@ +{% extends "emails/base.jinja" %} + +{% set final_url = resolve_front_url("project", project.slug) %} + +{% block body %} + + + + +
+

Project dump imported

+

Hello {{ user.get_full_name() }},

+

Your project dump has been correctly imported.

+

You can see the project here: {{ final_url }}

+

The Taiga Team

+
+{% endblock %} + +{% block footer %} + Copyright © 2014 Taiga Agile, LLC, All rights reserved. +
+ Contact us: +
+ Support: + support@taiga.io +
+ Our mailing address is: + https://groups.google.com/forum/#!forum/taigaio +{% endblock %} diff --git a/taiga/export_import/templates/emails/load_dump-body-text.jinja b/taiga/export_import/templates/emails/load_dump-body-text.jinja new file mode 100644 index 00000000..30064644 --- /dev/null +++ b/taiga/export_import/templates/emails/load_dump-body-text.jinja @@ -0,0 +1,7 @@ +Hello {{ user.get_full_name() }}, + +Your project dump has been correctly imported. You can see the project here: + +{{ resolve_front_url('project', project.slug) }} + +The Taiga Team diff --git a/taiga/export_import/templates/emails/load_dump-subject.jinja b/taiga/export_import/templates/emails/load_dump-subject.jinja new file mode 100644 index 00000000..d18f37c4 --- /dev/null +++ b/taiga/export_import/templates/emails/load_dump-subject.jinja @@ -0,0 +1 @@ +[Taiga] Your project dump has been imported diff --git a/taiga/export_import/throttling.py b/taiga/export_import/throttling.py index 3457ee44..77b1bb34 100644 --- a/taiga/export_import/throttling.py +++ b/taiga/export_import/throttling.py @@ -19,3 +19,6 @@ from taiga.base import throttling class ImportModeRateThrottle(throttling.UserRateThrottle): scope = "import-mode" + +class ImportDumpModeRateThrottle(throttling.UserRateThrottle): + scope = "import-dump-mode" diff --git a/taiga/projects/attachments/models.py b/taiga/projects/attachments/models.py index eb444a7d..df466c18 100644 --- a/taiga/projects/attachments/models.py +++ b/taiga/projects/attachments/models.py @@ -80,7 +80,7 @@ class Attachment(models.Model): class Meta: verbose_name = "attachment" verbose_name_plural = "attachments" - ordering = ["project", "created_date"] + ordering = ["project", "created_date", "id"] permissions = ( ("view_attachment", "Can view attachment"), ) diff --git a/taiga/projects/issues/models.py b/taiga/projects/issues/models.py index 0d591b3f..397e03db 100644 --- a/taiga/projects/issues/models.py +++ b/taiga/projects/issues/models.py @@ -69,7 +69,7 @@ class Issue(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models. class Meta: verbose_name = "issue" verbose_name_plural = "issues" - ordering = ["project", "-created_date"] + ordering = ["project", "-id"] permissions = ( ("view_issue", "Can view issue"), ) diff --git a/taiga/projects/tasks/models.py b/taiga/projects/tasks/models.py index 4f652ae8..c35de144 100644 --- a/taiga/projects/tasks/models.py +++ b/taiga/projects/tasks/models.py @@ -70,7 +70,7 @@ class Task(OCCModelMixin, WatchedModelMixin, BlockedMixin, TaggedMixin, models.M class Meta: verbose_name = "task" verbose_name_plural = "tasks" - ordering = ["project", "created_date"] + ordering = ["project", "created_date", "ref"] # unique_together = ("ref", "project") permissions = ( ("view_task", "Can view task"), diff --git a/taiga/projects/userstories/signals.py b/taiga/projects/userstories/signals.py index f728b372..8764b60d 100644 --- a/taiga/projects/userstories/signals.py +++ b/taiga/projects/userstories/signals.py @@ -35,6 +35,7 @@ def cached_prev_us(sender, instance, **kwargs): def update_role_points_when_create_or_edit_us(sender, instance, **kwargs): if instance._importing: return + instance.project.update_role_points(user_stories=[instance]) @@ -52,15 +53,24 @@ def update_milestone_of_tasks_when_edit_us(sender, instance, created, **kwargs): #################################### def try_to_close_or_open_us_and_milestone_when_create_or_edit_us(sender, instance, created, **kwargs): + if instance._importing: + return + _try_to_close_or_open_us_when_create_or_edit_us(instance) _try_to_close_or_open_milestone_when_create_or_edit_us(instance) def try_to_close_milestone_when_delete_us(sender, instance, **kwargs): + if instance._importing: + return + _try_to_close_milestone_when_delete_us(instance) # US def _try_to_close_or_open_us_when_create_or_edit_us(instance): + if instance._importing: + return + from . import services as us_service if us_service.calculate_userstory_is_closed(instance): @@ -71,6 +81,9 @@ def _try_to_close_or_open_us_when_create_or_edit_us(instance): # Milestone def _try_to_close_or_open_milestone_when_create_or_edit_us(instance): + if instance._importing: + return + from taiga.projects.milestones import services as milestone_service if instance.milestone_id: @@ -87,6 +100,9 @@ def _try_to_close_or_open_milestone_when_create_or_edit_us(instance): def _try_to_close_milestone_when_delete_us(instance): + if instance._importing: + return + from taiga.projects.milestones import services as milestone_service with suppress(ObjectDoesNotExist): diff --git a/taiga/routers.py b/taiga/routers.py index 0b6ffba2..98f6a86c 100644 --- a/taiga/routers.py +++ b/taiga/routers.py @@ -45,9 +45,10 @@ router.register(r"search", SearchViewSet, base_name="search") # Importer -from taiga.export_import.api import ProjectImporterViewSet +from taiga.export_import.api import ProjectImporterViewSet, ProjectExporterViewSet router.register(r"importer", ProjectImporterViewSet, base_name="importer") +router.register(r"exporter", ProjectExporterViewSet, base_name="exporter") # Projects & Types diff --git a/tests/integration/test_exporter_api.py b/tests/integration/test_exporter_api.py new file mode 100644 index 00000000..060b1c59 --- /dev/null +++ b/tests/integration/test_exporter_api.py @@ -0,0 +1,84 @@ +# 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 . + +import pytest +import json + +from django.core.urlresolvers import reverse + +from .. import factories as f + + +pytestmark = pytest.mark.django_db + + +def test_invalid_project_export(client): + user = f.UserFactory.create() + client.login(user) + + url = reverse("exporter-detail", args=[1000000]) + + response = client.get(url, content_type="application/json") + assert response.status_code == 404 + + +def test_valid_project_export_with_celery_disabled(client, settings): + settings.CELERY_ENABLED = False + + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + f.MembershipFactory(project=project, user=user, is_owner=True) + client.login(user) + + url = reverse("exporter-detail", args=[project.pk]) + + response = client.get(url, content_type="application/json") + assert response.status_code == 200 + response_data = json.loads(response.content.decode("utf-8")) + assert response_data["slug"] == project.slug + + +def test_valid_project_export_with_celery_enabled(client, settings): + settings.CELERY_ENABLED = True + + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + f.MembershipFactory(project=project, user=user, is_owner=True) + client.login(user) + + url = reverse("exporter-detail", args=[project.pk]) + + response = client.get(url, content_type="application/json") + assert response.status_code == 202 + response_data = json.loads(response.content.decode("utf-8")) + assert "export-id" in response_data + + +def test_valid_project_with_throttling(client, settings): + settings.CELERY_ENABLED = False + settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["import-dump-mode"] = "1/minute" + + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + f.MembershipFactory(project=project, user=user, is_owner=True) + client.login(user) + + url = reverse("exporter-detail", args=[project.pk]) + + response = client.get(url, content_type="application/json") + assert response.status_code == 200 + response = client.get(url, content_type="application/json") + assert response.status_code == 429 diff --git a/tests/integration/test_importer_api.py b/tests/integration/test_importer_api.py index 00480c0d..d458a1a6 100644 --- a/tests/integration/test_importer_api.py +++ b/tests/integration/test_importer_api.py @@ -20,6 +20,7 @@ import base64 import datetime from django.core.urlresolvers import reverse +from django.core.files.base import ContentFile from .. import factories as f @@ -703,3 +704,96 @@ def test_milestone_import_duplicated_milestone(client): assert response.status_code == 400 response_data = json.loads(response.content.decode("utf-8")) assert response_data["milestones"][0]["name"][0] == "Name duplicated for the project" + +def test_invalid_dump_import(client): + user = f.UserFactory.create() + client.login(user) + + url = reverse("importer-load-dump") + + data = ContentFile(b"test") + data.name = "test" + + response = client.post(url, {'dump': data}) + assert response.status_code == 400 + response_data = json.loads(response.content.decode("utf-8")) + assert response_data["_error_message"] == "Invalid dump format" + +def test_valid_dump_import_with_celery_disabled(client, settings): + settings.CELERY_ENABLED = False + + user = f.UserFactory.create() + client.login(user) + + url = reverse("importer-load-dump") + + data = ContentFile(bytes(json.dumps({ + "slug": "valid-project", + "name": "Valid project", + "description": "Valid project desc" + }), "utf-8")) + data.name = "test" + + response = client.post(url, {'dump': data}) + assert response.status_code == 204 + +def test_valid_dump_import_with_celery_enabled(client, settings): + settings.CELERY_ENABLED = True + + user = f.UserFactory.create() + client.login(user) + + url = reverse("importer-load-dump") + + data = ContentFile(bytes(json.dumps({ + "slug": "valid-project", + "name": "Valid project", + "description": "Valid project desc" + }), "utf-8")) + data.name = "test" + + response = client.post(url, {'dump': data}) + assert response.status_code == 202 + response_data = json.loads(response.content.decode("utf-8")) + assert "import-id" in response_data + +def test_dump_import_duplicated_project(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + client.login(user) + + url = reverse("importer-load-dump") + + data = ContentFile(bytes(json.dumps({ + "slug": project.slug, + "name": "Test import", + "description": "Valid project desc" + }), "utf-8")) + data.name = "test" + + response = client.post(url, {'dump': data}) + assert response.status_code == 204 + new_project = Project.objects.all().order_by("-id").first() + assert new_project.name == "Test import" + assert new_project.slug == "{}-test-import".format(user.username) + +def test_dump_import_throttling(client, settings): + settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["import-dump-mode"] = "1/minute" + + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + client.login(user) + + url = reverse("importer-load-dump") + + data = ContentFile(bytes(json.dumps({ + "slug": project.slug, + "name": "Test import", + "description": "Valid project desc" + }), "utf-8")) + data.name = "test" + + response = client.post(url, {'dump': data}) + assert response.status_code == 204 + response = client.post(url, {'dump': data}) + assert response.status_code == 429