From 480baa3b948818059c0d31c758398ddcd58b1bbb Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 15 Oct 2014 17:29:20 +0200 Subject: [PATCH 01/10] Auto coerce bytes to string on own json module. --- taiga/base/utils/json.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/taiga/base/utils/json.py b/taiga/base/utils/json.py index dcb818a3..bb8dde78 100644 --- a/taiga/base/utils/json.py +++ b/taiga/base/utils/json.py @@ -16,13 +16,15 @@ import json from rest_framework.utils import encoders +from django.utils.encoding import force_text def dumps(data, ensure_ascii=True, encoder_class=encoders.JSONEncoder): return json.dumps(data, cls=encoder_class, indent=None, ensure_ascii=ensure_ascii) - def loads(data): + if isinstance(data, bytes): + data = force_text(data) return json.loads(data) # Some backward compatibility that should From 9b6c58bad941003b482a53773463da54c6626245 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 15 Oct 2014 17:29:58 +0200 Subject: [PATCH 02/10] Normalize some tests file names. --- tests/integration/{test_project_history.py => test_history.py} | 0 .../{test_project_notifications.py => test_notifications.py} | 0 ...oject_references_sequences.py => test_references_sequences.py} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename tests/integration/{test_project_history.py => test_history.py} (100%) rename tests/integration/{test_project_notifications.py => test_notifications.py} (100%) rename tests/integration/{test_project_references_sequences.py => test_references_sequences.py} (100%) diff --git a/tests/integration/test_project_history.py b/tests/integration/test_history.py similarity index 100% rename from tests/integration/test_project_history.py rename to tests/integration/test_history.py diff --git a/tests/integration/test_project_notifications.py b/tests/integration/test_notifications.py similarity index 100% rename from tests/integration/test_project_notifications.py rename to tests/integration/test_notifications.py diff --git a/tests/integration/test_project_references_sequences.py b/tests/integration/test_references_sequences.py similarity index 100% rename from tests/integration/test_project_references_sequences.py rename to tests/integration/test_references_sequences.py From 0f207da0dc77f18d8b67c772fc956736350fb497 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 15 Oct 2014 17:30:22 +0200 Subject: [PATCH 03/10] Put more clear names to user test functions. --- tests/integration/test_change_avatar.py | 54 ------------------------ tests/integration/test_users.py | 56 ++++++++++++++++++------- 2 files changed, 41 insertions(+), 69 deletions(-) delete mode 100644 tests/integration/test_change_avatar.py diff --git a/tests/integration/test_change_avatar.py b/tests/integration/test_change_avatar.py deleted file mode 100644 index a2202f32..00000000 --- a/tests/integration/test_change_avatar.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández -# 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 django.core.urlresolvers import reverse - -from tempfile import NamedTemporaryFile - -import pytest - -from .. import factories as f - -pytestmark = pytest.mark.django_db - -DUMMY_BMP_DATA = b'BM:\x00\x00\x00\x00\x00\x00\x006\x00\x00\x00(\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x18\x00\x00\x00\x00\x00\x04\x00\x00\x00\x13\x0b\x00\x00\x13\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - - -def test_change_avatar(client): - url = reverse('users-change-avatar') - - user = f.UserFactory() - client.login(user) - - with NamedTemporaryFile() as avatar: - # Test no avatar send - post_data = {} - response = client.post(url, post_data) - assert response.status_code == 400 - - # Test invalid file send - post_data = { - 'avatar': avatar - } - response = client.post(url, post_data) - assert response.status_code == 400 - - # Test empty valid avatar send - avatar.write(DUMMY_BMP_DATA) - avatar.seek(0) - response = client.post(url, post_data) - assert response.status_code == 200 diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py index 2beb3762..7c749cdb 100644 --- a/tests/integration/test_users.py +++ b/tests/integration/test_users.py @@ -1,5 +1,6 @@ import pytest import json +from tempfile import NamedTemporaryFile from django.core.urlresolvers import reverse @@ -11,7 +12,7 @@ from taiga.auth.tokens import get_token_for_user pytestmark = pytest.mark.django_db -def test_api_user_normal_user(client): +def test_users_create_through_standard_api(client): user = f.UserFactory.create(is_superuser=True) url = reverse('users-list') @@ -26,7 +27,7 @@ def test_api_user_normal_user(client): assert response.status_code == 405 -def test_api_user_patch_same_email(client): +def test_update_user_with_same_email(client): user = f.UserFactory.create(email="same@email.com") url = reverse('users-detail', kwargs={"pk": user.pk}) data = {"email": "same@email.com"} @@ -38,7 +39,7 @@ def test_api_user_patch_same_email(client): assert response.data['_error_message'] == 'Duplicated email' -def test_api_user_patch_duplicated_email(client): +def test_update_user_with_duplicated_email(client): f.UserFactory.create(email="one@email.com") user = f.UserFactory.create(email="two@email.com") url = reverse('users-detail', kwargs={"pk": user.pk}) @@ -51,7 +52,7 @@ def test_api_user_patch_duplicated_email(client): assert response.data['_error_message'] == 'Duplicated email' -def test_api_user_patch_invalid_email(client): +def test_update_user_with_invalid_email(client): user = f.UserFactory.create(email="my@email.com") url = reverse('users-detail', kwargs={"pk": user.pk}) data = {"email": "my@email"} @@ -63,7 +64,7 @@ def test_api_user_patch_invalid_email(client): assert response.data['_error_message'] == 'Not valid email' -def test_api_user_patch_valid_email(client): +def test_update_user_with_valid_email(client): user = f.UserFactory.create(email="old@email.com") url = reverse('users-detail', kwargs={"pk": user.pk}) data = {"email": "new@email.com"} @@ -77,7 +78,7 @@ def test_api_user_patch_valid_email(client): assert user.new_email == "new@email.com" -def test_api_user_action_change_email_ok(client): +def test_validate_requested_email_change(client): user = f.UserFactory.create(email_token="change_email_token", new_email="new@email.com") url = reverse('users-change-email') data = {"email_token": "change_email_token"} @@ -92,19 +93,17 @@ def test_api_user_action_change_email_ok(client): assert user.email == "new@email.com" -def test_api_user_action_change_email_no_token(client): +def test_validate_requested_email_change_without_token(client): user = f.UserFactory.create(email_token="change_email_token", new_email="new@email.com") url = reverse('users-change-email') data = {} client.login(user) response = client.post(url, json.dumps(data), content_type="application/json") - assert response.status_code == 400 - assert response.data['_error_message'] == 'Invalid, are you sure the token is correct and you didn\'t use it before?' -def test_api_user_action_change_email_invalid_token(client): +def test_validate_requested_email_change_with_invalid_token(client): user = f.UserFactory.create(email_token="change_email_token", new_email="new@email.com") url = reverse('users-change-email') data = {"email_token": "invalid_email_token"} @@ -113,10 +112,9 @@ def test_api_user_action_change_email_invalid_token(client): response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 400 - assert response.data['_error_message'] == 'Invalid, are you sure the token is correct and you didn\'t use it before?' -def test_api_user_delete(client): +def test_delete_self_user(client): user = f.UserFactory.create() url = reverse('users-detail', kwargs={"pk": user.pk}) @@ -128,7 +126,7 @@ def test_api_user_delete(client): assert user.full_name == "Deleted user" -def test_api_user_cancel_valid_token(client): +def test_cancel_self_user_with_valid_token(client): user = f.UserFactory.create() url = reverse('users-cancel') cancel_token = get_token_for_user(user, "cancel_account") @@ -141,7 +139,7 @@ def test_api_user_cancel_valid_token(client): assert user.full_name == "Deleted user" -def test_api_user_cancel_invalid_token(client): +def test_cancel_self_user_with_invalid_token(client): user = f.UserFactory.create() url = reverse('users-cancel') data = {"cancel_token": "invalid_cancel_token"} @@ -149,4 +147,32 @@ def test_api_user_cancel_invalid_token(client): response = client.post(url, json.dumps(data), content_type="application/json") assert response.status_code == 400 - assert response.data['_error_message'] == "Invalid, are you sure the token is correct?" + + +DUMMY_BMP_DATA = b'BM:\x00\x00\x00\x00\x00\x00\x006\x00\x00\x00(\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01\x00\x18\x00\x00\x00\x00\x00\x04\x00\x00\x00\x13\x0b\x00\x00\x13\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + + +def test_change_avatar(client): + url = reverse('users-change-avatar') + + user = f.UserFactory() + client.login(user) + + with NamedTemporaryFile() as avatar: + # Test no avatar send + post_data = {} + response = client.post(url, post_data) + assert response.status_code == 400 + + # Test invalid file send + post_data = { + 'avatar': avatar + } + response = client.post(url, post_data) + assert response.status_code == 400 + + # Test empty valid avatar send + avatar.write(DUMMY_BMP_DATA) + avatar.seek(0) + response = client.post(url, post_data) + assert response.status_code == 200 From 786599239cb69f72f5365f37772bdffb70023a32 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 15 Oct 2014 17:31:14 +0200 Subject: [PATCH 04/10] Minor tests cleaning. --- tests/integration/test_milestones.py | 1 - tests/integration/test_occ.py | 10 +++++----- tests/integration/test_project_us.py | 10 +++++----- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/tests/integration/test_milestones.py b/tests/integration/test_milestones.py index 7af07411..ab5cc962 100644 --- a/tests/integration/test_milestones.py +++ b/tests/integration/test_milestones.py @@ -39,7 +39,6 @@ def test_api_update_milestone(client): points = f.PointsFactory.create(project=project, value=None) us = f.UserStoryFactory.create(project=project, owner=user) - # role_points = f.RolePointsFactory.create(points=points, user_story=us, role=role) url = reverse("milestones-detail", args=[sprint.pk]) diff --git a/tests/integration/test_occ.py b/tests/integration/test_occ.py index 97626290..eb25eef8 100644 --- a/tests/integration/test_occ.py +++ b/tests/integration/test_occ.py @@ -15,11 +15,11 @@ # along with this program. If not, see . import pytest -import json from unittest.mock import patch from django.core.urlresolvers import reverse +from taiga.base.utils import json from taiga.projects.issues.models import Issue from taiga.projects.wiki.models import WikiPage from taiga.projects.userstories.models import UserStory @@ -58,7 +58,7 @@ def test_valid_concurrent_save_for_issue(client): url = reverse("issues-detail", args=(issue.id,)) data = {"version": 10} response = client.patch(url, json.dumps(data), content_type="application/json") - assert json.loads(response.content.decode('utf-8'))['version'] == 11 + assert json.loads(response.content)['version'] == 11 assert response.status_code == 200 issue = Issue.objects.get(id=issue.id) assert issue.version == 11 @@ -85,7 +85,7 @@ def test_valid_concurrent_save_for_wiki_page(client): url = reverse("wiki-detail", args=(wiki_page.id,)) data = {"version": 10} response = client.patch(url, json.dumps(data), content_type="application/json") - assert json.loads(response.content.decode('utf-8'))['version'] == 11 + assert json.loads(response.content)['version'] == 11 assert response.status_code == 200 wiki_page = WikiPage.objects.get(id=wiki_page.id) assert wiki_page.version == 11 @@ -128,7 +128,7 @@ def test_valid_concurrent_save_for_us(client): url = reverse("userstories-detail", args=(userstory.id,)) data = {"version": 10} response = client.patch(url, json.dumps(data), content_type="application/json") - assert json.loads(response.content.decode('utf-8'))['version'] == 11 + assert json.loads(response.content)['version'] == 11 assert response.status_code == 200 userstory = UserStory.objects.get(id=userstory.id) assert userstory.version == 11 @@ -159,7 +159,7 @@ def test_valid_concurrent_save_for_task(client): url = reverse("tasks-detail", args=(task.id,)) data = {"version": 10} response = client.patch(url, json.dumps(data), content_type="application/json") - assert json.loads(response.content.decode('utf-8'))['version'] == 11 + assert json.loads(response.content)['version'] == 11 assert response.status_code == 200 task = Task.objects.get(id=task.id) assert task.version == 11 diff --git a/tests/integration/test_project_us.py b/tests/integration/test_project_us.py index c369daa7..65abf3dc 100644 --- a/tests/integration/test_project_us.py +++ b/tests/integration/test_project_us.py @@ -16,9 +16,9 @@ # along with this program. If not, see . import pytest -import json - from django.core.urlresolvers import reverse + +from taiga.base.utils import json from .. import factories as f @@ -38,12 +38,12 @@ def test_archived_filter(client): data = {} response = client.get(url, data) - assert len(json.loads(response.content.decode('utf-8'))) == 2 + assert len(json.loads(response.content)) == 2 data = {"is_archived": 0} response = client.get(url, data) - assert len(json.loads(response.content.decode('utf-8'))) == 1 + assert len(json.loads(response.content)) == 1 data = {"is_archived": 1} response = client.get(url, data) - assert len(json.loads(response.content.decode('utf-8'))) == 1 + assert len(json.loads(response.content)) == 1 From 21f183556761d5001bf05ff612dc0f5dd47c7e18 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 15 Oct 2014 17:36:19 +0200 Subject: [PATCH 05/10] Fix milestone test name. --- tests/integration/test_milestones.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/integration/test_milestones.py b/tests/integration/test_milestones.py index ab5cc962..e32f23ac 100644 --- a/tests/integration/test_milestones.py +++ b/tests/integration/test_milestones.py @@ -29,8 +29,7 @@ from .. import factories as f pytestmark = pytest.mark.django_db - -def test_api_update_milestone(client): +def test_update_milestone_with_userstories_list(client): user = f.UserFactory.create() project = f.ProjectFactory.create(owner=user) role = f.RoleFactory.create(project=project) From 43fd17aaaa61c7f1cd2872465d4e4c1840867ed7 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 15 Oct 2014 18:05:53 +0200 Subject: [PATCH 06/10] Fix wrong handling moveTo parameter on role destroy. --- taiga/projects/api.py | 30 +++++------- tests/integration/test_roles.py | 82 +++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 18 deletions(-) create mode 100644 tests/integration/test_roles.py diff --git a/taiga/projects/api.py b/taiga/projects/api.py index 1b18e4c3..914c4f77 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -256,20 +256,14 @@ class RolesViewSet(ModelCrudViewSet): filter_backends = (filters.CanViewProjectFilterBackend,) filter_fields = ('project',) - @tx.atomic - def destroy(self, request, *args, **kwargs): - moveTo = self.request.QUERY_PARAMS.get('moveTo', None) - if moveTo is None: - return super().destroy(request, *args, **kwargs) + def pre_delete(self, obj): + move_to = self.request.QUERY_PARAMS.get('moveTo', None) + if move_to: + role_dest = get_object_or_404(self.model, project=obj.project, id=move_to) + qs = models.Membership.objects.filter(project_id=obj.project.pk, role=obj) + qs.update(role=role_dest) - obj = self.get_object_or_none() - - moveItem = get_object_or_404(self.model, project=obj.project, id=moveTo) - - self.check_permissions(request, 'destroy', obj) - - models.Membership.objects.filter(project=obj.project, role=obj).update(role=moveItem) - return super().destroy(request, *args, **kwargs) + super().pre_delete(obj) # User Stories commin ViewSets @@ -317,19 +311,19 @@ class PointsViewSet(ModelCrudViewSet, BulkUpdateOrderMixin): class MoveOnDestroyMixin(object): @tx.atomic def destroy(self, request, *args, **kwargs): - moveTo = self.request.QUERY_PARAMS.get('moveTo', None) - if moveTo is None: + move_to = self.request.QUERY_PARAMS.get('moveTo', None) + if move_to is None: return super().destroy(request, *args, **kwargs) obj = self.get_object_or_none() - moveItem = get_object_or_404(self.model, project=obj.project, id=moveTo) + move_item = get_object_or_404(self.model, project=obj.project, id=move_to) self.check_permissions(request, 'destroy', obj) - kwargs = {self.move_on_destroy_related_field: moveItem} + kwargs = {self.move_on_destroy_related_field: move_item} self.move_on_destroy_related_class.objects.filter(project=obj.project, **{self.move_on_destroy_related_field: obj}).update(**kwargs) if getattr(obj.project, self.move_on_destroy_project_default_field) == obj: - setattr(obj.project, self.move_on_destroy_project_default_field, moveItem) + setattr(obj.project, self.move_on_destroy_project_default_field, move_item) obj.project.save() return super().destroy(request, *args, **kwargs) diff --git a/tests/integration/test_roles.py b/tests/integration/test_roles.py new file mode 100644 index 00000000..e8bd89fa --- /dev/null +++ b/tests/integration/test_roles.py @@ -0,0 +1,82 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# Copyright (C) 2014 Anler Hernández +# 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 +from unittest.mock import patch, Mock + +from django.apps import apps +from django.core.urlresolvers import reverse + +from taiga.base.utils import json + +from taiga.users.models import Role +from taiga.projects.models import Membership +from taiga.projects.models import Project +from taiga.projects.userstories.serializers import UserStorySerializer + +from .. import factories as f + + +pytestmark = pytest.mark.django_db + +def test_destroy_role_and_reassign_members(client): + user1 = f.UserFactory.create() + user2 = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user1) + role1 = f.RoleFactory.create(project=project) + role2 = f.RoleFactory.create(project=project) + member = f.MembershipFactory.create(project=project, user=user1, role=role1) + member = f.MembershipFactory.create(project=project, user=user2, role=role2) + + url = reverse("roles-detail", args=[role2.pk]) + "?moveTo={}".format(role1.pk) + + client.login(user1) + + response = client.delete(url) + assert response.status_code == 204 + + qs = Role.objects.filter(project=project) + assert qs.count() == 1 + + qs = Membership.objects.filter(project=project, role_id=role2.pk) + assert qs.count() == 0 + + qs = Membership.objects.filter(project=project, role_id=role1.pk) + assert qs.count() == 2 + +def test_destroy_role_and_reassign_members_with_deleted_project(client): + """ + Regression test, that fixes some 500 errors on production + """ + + user1 = f.UserFactory.create() + user2 = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user1) + role1 = f.RoleFactory.create(project=project) + role2 = f.RoleFactory.create(project=project) + member = f.MembershipFactory.create(project=project, user=user1, role=role1) + member = f.MembershipFactory.create(project=project, user=user2, role=role2) + + Project.objects.filter(pk=project.id).delete() + + url = reverse("roles-detail", args=[role2.pk]) + "?moveTo={}".format(role1.pk) + client.login(user1) + + response = client.delete(url) + + # FIXME: really should return 403? I think it should be 404 + assert response.status_code == 403, response.content From a3cb48cf8e9fbb01c965f12800032d1c672c2ab9 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 15 Oct 2014 19:49:16 +0200 Subject: [PATCH 07/10] Fix wrong handling role points update. Additionally it moves the logic from serializer to resource. --- taiga/projects/userstories/api.py | 38 +++++++++++-- taiga/projects/userstories/serializers.py | 13 ----- tests/integration/test_userstories.py | 65 +++++++++++++++++++---- 3 files changed, 89 insertions(+), 27 deletions(-) diff --git a/taiga/projects/userstories/api.py b/taiga/projects/userstories/api.py index f4b604ed..f1a87c9a 100644 --- a/taiga/projects/userstories/api.py +++ b/taiga/projects/userstories/api.py @@ -14,9 +14,13 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +from contextlib import suppress + +from django.apps import apps from django.db import transaction from django.utils.translation import ugettext as _ from django.shortcuts import get_object_or_404 +from django.core.exceptions import ObjectDoesNotExist from rest_framework.response import Response from rest_framework import status @@ -62,6 +66,35 @@ class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMi qs = qs.select_related("milestone", "project") return qs + def pre_save(self, obj): + # This is very ugly hack, but having + # restframework is the only way to do it. + # NOTE: code moved as is from serializer + # to api because is not serializer logic. + related_data = getattr(obj, "_related_data", {}) + self._role_points = related_data.pop("role_points", None) + + if not obj.id: + obj.owner = self.request.user + + super().pre_save(obj) + + def post_save(self, obj, created=False): + # Code related to the hack of pre_save method. Rather, + # this is the continuation of it. + + Points = apps.get_model("projects", "Points") + RolePoints = apps.get_model("userstories", "RolePoints") + + if self._role_points: + with suppress(ObjectDoesNotExist): + for role_id, points_id in self._role_points.items(): + role_points = RolePoints.objects.get(role__id=role_id, user_story_id=obj.pk) + role_points.points = Points.objects.get(id=points_id, project_id=obj.project_id) + role_points.save() + + super().post_save(obj, created) + @list_route(methods=["POST"]) def bulk_create(self, request, **kwargs): serializer = serializers.UserStoriesBulkSerializer(data=request.DATA) @@ -145,8 +178,3 @@ class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMi return response - def pre_save(self, obj): - if not obj.id: - obj.owner = self.request.user - - super().pre_save(obj) diff --git a/taiga/projects/userstories/serializers.py b/taiga/projects/userstories/serializers.py index ef2607e2..cc7d1638 100644 --- a/taiga/projects/userstories/serializers.py +++ b/taiga/projects/userstories/serializers.py @@ -52,19 +52,6 @@ class UserStorySerializer(serializers.ModelSerializer): depth = 0 read_only_fields = ('created_date', 'modified_date') - def save_object(self, obj, **kwargs): - role_points = obj._related_data.pop("role_points", None) - super().save_object(obj, **kwargs) - - points_modelcls = apps.get_model("projects", "Points") - - if role_points: - for role_id, points_id in role_points.items(): - role_points = obj.role_points.get(role__id=role_id) - role_points.points = points_modelcls.objects.get(id=points_id, - project=obj.project) - role_points.save() - def get_total_points(self, obj): return obj.get_total_points() diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index 63721eca..61d31535 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.py @@ -1,3 +1,4 @@ +import copy from unittest import mock from django.core.urlresolvers import reverse @@ -11,10 +12,7 @@ pytestmark = pytest.mark.django_db def test_get_userstories_from_bulk(): - data = """ -User Story #1 -User Story #2 -""" + data = "User Story #1\nUser Story #2\n" userstories = services.get_userstories_from_bulk(data) assert len(userstories) == 2 @@ -23,10 +21,7 @@ User Story #2 def test_create_userstories_in_bulk(): - data = """ -User Story #1 -User Story #2 -""" + data = "User Story #1\nUser Story #2\n" with mock.patch("taiga.projects.userstories.services.db") as db: userstories = services.create_userstories_in_bulk(data) @@ -41,7 +36,9 @@ def test_update_userstories_order_in_bulk(): with mock.patch("taiga.projects.userstories.services.db") as db: services.update_userstories_order_in_bulk(data, "backlog_order", project) - db.update_in_bulk_with_ids.assert_called_once_with([1, 2], [{"backlog_order": 1}, {"backlog_order": 2}], + db.update_in_bulk_with_ids.assert_called_once_with([1, 2], + [{"backlog_order": 1}, + {"backlog_order": 2}], model=models.UserStory) @@ -108,3 +105,53 @@ def test_api_update_backlog_order_in_bulk(client): assert response1.status_code == 204, response.data assert response2.status_code == 204, response.data assert response3.status_code == 204, response.data + + +from taiga.projects.userstories.serializers import UserStorySerializer + + +def test_update_userstory_points(client): + user1 = f.UserFactory.create() + user2 = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user1) + + role1 = f.RoleFactory.create(project=project) + role2 = f.RoleFactory.create(project=project) + + member = f.MembershipFactory.create(project=project, user=user1, role=role1) + member = f.MembershipFactory.create(project=project, user=user2, role=role2) + + points1 = f.PointsFactory.create(project=project, value=None) + points2 = f.PointsFactory.create(project=project, value=1) + points3 = f.PointsFactory.create(project=project, value=2) + + us = f.UserStoryFactory.create(project=project, owner=user1) + url = reverse("userstories-detail", args=[us.pk]) + usdata = UserStorySerializer(us).data + + client.login(user1) + + # Api should ignore invalid values + data = {} + data["version"] = usdata["version"] + data["points"] = copy.copy(usdata["points"]) + data["points"].update({'2000':points3.pk}) + + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200, response.data + + # Api should save successful + data = {} + data["version"] = usdata["version"] + data["points"] = copy.copy(usdata["points"]) + data["points"].update({str(role1.pk):points3.pk}) + + response = client.json.patch(url, json.dumps(data)) + assert response.status_code == 200, response.data + + us = models.UserStory.objects.get(pk=us.pk) + rp = list(us.role_points.values_list("role_id", "points_id")) + + assert rp == [(role1.pk, points3.pk), (role2.pk, points1.pk)] + + From 3fddbe2054336364cd9efb0ce91d9dead5d3cf6a Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 15 Oct 2014 20:17:36 +0200 Subject: [PATCH 08/10] Add temporal fix to history attachments changes rendering. --- taiga/projects/history/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/taiga/projects/history/models.py b/taiga/projects/history/models.py index 809cae1c..ab58b312 100644 --- a/taiga/projects/history/models.py +++ b/taiga/projects/history/models.py @@ -171,8 +171,8 @@ class HistoryEntry(models.Model): if changes: change = { - "filename": newattachs[aid]["filename"], - "url": newattachs[aid]["url"], + "filename": newattachs.get(aid, {}).get("filename", ""), + "url": newattachs.get(aid, {}).get("url", ""), "changes": changes } attachments["changed"].append(change) From 5617de891e8a9295ddff43c595e876d995ac5a7d Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 15 Oct 2014 20:25:43 +0200 Subject: [PATCH 09/10] Move some userstories test to right location. --- tests/integration/test_project_us.py | 49 --------------------------- tests/integration/test_userstories.py | 23 +++++++++++++ 2 files changed, 23 insertions(+), 49 deletions(-) delete mode 100644 tests/integration/test_project_us.py diff --git a/tests/integration/test_project_us.py b/tests/integration/test_project_us.py deleted file mode 100644 index 65abf3dc..00000000 --- a/tests/integration/test_project_us.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright (C) 2014 Andrey Antukh -# Copyright (C) 2014 Jesús Espino -# Copyright (C) 2014 David Barragán -# Copyright (C) 2014 Anler Hernández -# 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 -from django.core.urlresolvers import reverse - -from taiga.base.utils import json -from .. import factories as f - - -pytestmark = pytest.mark.django_db - - -def test_archived_filter(client): - user = f.UserFactory.create() - project = f.ProjectFactory.create(owner=user) - f.MembershipFactory.create(project=project, user=user) - f.UserStoryFactory.create(project=project) - f.UserStoryFactory.create(is_archived=True, project=project) - - client.login(user) - - url = reverse("userstories-list") - - data = {} - response = client.get(url, data) - assert len(json.loads(response.content)) == 2 - - data = {"is_archived": 0} - response = client.get(url, data) - assert len(json.loads(response.content)) == 1 - - data = {"is_archived": 1} - response = client.get(url, data) - assert len(json.loads(response.content)) == 1 diff --git a/tests/integration/test_userstories.py b/tests/integration/test_userstories.py index 61d31535..fc1d31e5 100644 --- a/tests/integration/test_userstories.py +++ b/tests/integration/test_userstories.py @@ -155,3 +155,26 @@ def test_update_userstory_points(client): assert rp == [(role1.pk, points3.pk), (role2.pk, points1.pk)] +def test_archived_filter(client): + user = f.UserFactory.create() + project = f.ProjectFactory.create(owner=user) + f.MembershipFactory.create(project=project, user=user) + f.UserStoryFactory.create(project=project) + f.UserStoryFactory.create(is_archived=True, project=project) + + client.login(user) + + url = reverse("userstories-list") + + data = {} + response = client.get(url, data) + assert len(json.loads(response.content)) == 2 + + data = {"is_archived": 0} + response = client.get(url, data) + assert len(json.loads(response.content)) == 1 + + data = {"is_archived": 1} + response = client.get(url, data) + assert len(json.loads(response.content)) == 1 + From 84a7c47210ff400a3376b0ad58ec28a046b783c5 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 15 Oct 2014 20:26:54 +0200 Subject: [PATCH 10/10] Rename project tests. --- tests/integration/test_projects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py index 20762415..f2a6c249 100644 --- a/tests/integration/test_projects.py +++ b/tests/integration/test_projects.py @@ -7,7 +7,7 @@ import pytest pytestmark = pytest.mark.django_db -def test_api_create_project(client): +def test_create_project(client): user = f.create_user() url = reverse("projects-list") data = {"name": "project name", "description": "project description"} @@ -18,7 +18,7 @@ def test_api_create_project(client): assert response.status_code == 201 -def test_api_partially_update_project(client): +def test_partially_update_project(client): project = f.create_project() url = reverse("projects-detail", kwargs={"pk": project.pk}) data = {"name": ""}