Merge pull request #123 from taigaio/random-fixes
A collection minor fixes + hotfixesremotes/origin/enhancement/email-actions
commit
154e13cc79
|
@ -16,13 +16,15 @@
|
||||||
|
|
||||||
import json
|
import json
|
||||||
from rest_framework.utils import encoders
|
from rest_framework.utils import encoders
|
||||||
|
from django.utils.encoding import force_text
|
||||||
|
|
||||||
|
|
||||||
def dumps(data, ensure_ascii=True, encoder_class=encoders.JSONEncoder):
|
def dumps(data, ensure_ascii=True, encoder_class=encoders.JSONEncoder):
|
||||||
return json.dumps(data, cls=encoder_class, indent=None, ensure_ascii=ensure_ascii)
|
return json.dumps(data, cls=encoder_class, indent=None, ensure_ascii=ensure_ascii)
|
||||||
|
|
||||||
|
|
||||||
def loads(data):
|
def loads(data):
|
||||||
|
if isinstance(data, bytes):
|
||||||
|
data = force_text(data)
|
||||||
return json.loads(data)
|
return json.loads(data)
|
||||||
|
|
||||||
# Some backward compatibility that should
|
# Some backward compatibility that should
|
||||||
|
|
|
@ -256,20 +256,14 @@ class RolesViewSet(ModelCrudViewSet):
|
||||||
filter_backends = (filters.CanViewProjectFilterBackend,)
|
filter_backends = (filters.CanViewProjectFilterBackend,)
|
||||||
filter_fields = ('project',)
|
filter_fields = ('project',)
|
||||||
|
|
||||||
@tx.atomic
|
def pre_delete(self, obj):
|
||||||
def destroy(self, request, *args, **kwargs):
|
move_to = self.request.QUERY_PARAMS.get('moveTo', None)
|
||||||
moveTo = self.request.QUERY_PARAMS.get('moveTo', None)
|
if move_to:
|
||||||
if moveTo is None:
|
role_dest = get_object_or_404(self.model, project=obj.project, id=move_to)
|
||||||
return super().destroy(request, *args, **kwargs)
|
qs = models.Membership.objects.filter(project_id=obj.project.pk, role=obj)
|
||||||
|
qs.update(role=role_dest)
|
||||||
|
|
||||||
obj = self.get_object_or_none()
|
super().pre_delete(obj)
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
# User Stories commin ViewSets
|
# User Stories commin ViewSets
|
||||||
|
@ -317,19 +311,19 @@ class PointsViewSet(ModelCrudViewSet, BulkUpdateOrderMixin):
|
||||||
class MoveOnDestroyMixin(object):
|
class MoveOnDestroyMixin(object):
|
||||||
@tx.atomic
|
@tx.atomic
|
||||||
def destroy(self, request, *args, **kwargs):
|
def destroy(self, request, *args, **kwargs):
|
||||||
moveTo = self.request.QUERY_PARAMS.get('moveTo', None)
|
move_to = self.request.QUERY_PARAMS.get('moveTo', None)
|
||||||
if moveTo is None:
|
if move_to is None:
|
||||||
return super().destroy(request, *args, **kwargs)
|
return super().destroy(request, *args, **kwargs)
|
||||||
|
|
||||||
obj = self.get_object_or_none()
|
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)
|
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)
|
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:
|
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()
|
obj.project.save()
|
||||||
return super().destroy(request, *args, **kwargs)
|
return super().destroy(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
|
@ -171,8 +171,8 @@ class HistoryEntry(models.Model):
|
||||||
|
|
||||||
if changes:
|
if changes:
|
||||||
change = {
|
change = {
|
||||||
"filename": newattachs[aid]["filename"],
|
"filename": newattachs.get(aid, {}).get("filename", ""),
|
||||||
"url": newattachs[aid]["url"],
|
"url": newattachs.get(aid, {}).get("url", ""),
|
||||||
"changes": changes
|
"changes": changes
|
||||||
}
|
}
|
||||||
attachments["changed"].append(change)
|
attachments["changed"].append(change)
|
||||||
|
|
|
@ -14,9 +14,13 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from contextlib import suppress
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
@ -62,6 +66,35 @@ class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMi
|
||||||
qs = qs.select_related("milestone", "project")
|
qs = qs.select_related("milestone", "project")
|
||||||
return qs
|
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"])
|
@list_route(methods=["POST"])
|
||||||
def bulk_create(self, request, **kwargs):
|
def bulk_create(self, request, **kwargs):
|
||||||
serializer = serializers.UserStoriesBulkSerializer(data=request.DATA)
|
serializer = serializers.UserStoriesBulkSerializer(data=request.DATA)
|
||||||
|
@ -145,8 +178,3 @@ class UserStoryViewSet(OCCResourceMixin, HistoryResourceMixin, WatchedResourceMi
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def pre_save(self, obj):
|
|
||||||
if not obj.id:
|
|
||||||
obj.owner = self.request.user
|
|
||||||
|
|
||||||
super().pre_save(obj)
|
|
||||||
|
|
|
@ -52,19 +52,6 @@ class UserStorySerializer(serializers.ModelSerializer):
|
||||||
depth = 0
|
depth = 0
|
||||||
read_only_fields = ('created_date', 'modified_date')
|
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):
|
def get_total_points(self, obj):
|
||||||
return obj.get_total_points()
|
return obj.get_total_points()
|
||||||
|
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
|
||||||
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
|
||||||
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
|
||||||
# Copyright (C) 2014 Anler Hernández <hello@anler.me>
|
|
||||||
# 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
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
|
|
|
@ -29,8 +29,7 @@ from .. import factories as f
|
||||||
|
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
def test_update_milestone_with_userstories_list(client):
|
||||||
def test_api_update_milestone(client):
|
|
||||||
user = f.UserFactory.create()
|
user = f.UserFactory.create()
|
||||||
project = f.ProjectFactory.create(owner=user)
|
project = f.ProjectFactory.create(owner=user)
|
||||||
role = f.RoleFactory.create(project=project)
|
role = f.RoleFactory.create(project=project)
|
||||||
|
@ -39,7 +38,6 @@ def test_api_update_milestone(client):
|
||||||
|
|
||||||
points = f.PointsFactory.create(project=project, value=None)
|
points = f.PointsFactory.create(project=project, value=None)
|
||||||
us = f.UserStoryFactory.create(project=project, owner=user)
|
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])
|
url = reverse("milestones-detail", args=[sprint.pk])
|
||||||
|
|
||||||
|
|
|
@ -15,11 +15,11 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import json
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
from taiga.base.utils import json
|
||||||
from taiga.projects.issues.models import Issue
|
from taiga.projects.issues.models import Issue
|
||||||
from taiga.projects.wiki.models import WikiPage
|
from taiga.projects.wiki.models import WikiPage
|
||||||
from taiga.projects.userstories.models import UserStory
|
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,))
|
url = reverse("issues-detail", args=(issue.id,))
|
||||||
data = {"version": 10}
|
data = {"version": 10}
|
||||||
response = client.patch(url, json.dumps(data), content_type="application/json")
|
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
|
assert response.status_code == 200
|
||||||
issue = Issue.objects.get(id=issue.id)
|
issue = Issue.objects.get(id=issue.id)
|
||||||
assert issue.version == 11
|
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,))
|
url = reverse("wiki-detail", args=(wiki_page.id,))
|
||||||
data = {"version": 10}
|
data = {"version": 10}
|
||||||
response = client.patch(url, json.dumps(data), content_type="application/json")
|
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
|
assert response.status_code == 200
|
||||||
wiki_page = WikiPage.objects.get(id=wiki_page.id)
|
wiki_page = WikiPage.objects.get(id=wiki_page.id)
|
||||||
assert wiki_page.version == 11
|
assert wiki_page.version == 11
|
||||||
|
@ -128,7 +128,7 @@ def test_valid_concurrent_save_for_us(client):
|
||||||
url = reverse("userstories-detail", args=(userstory.id,))
|
url = reverse("userstories-detail", args=(userstory.id,))
|
||||||
data = {"version": 10}
|
data = {"version": 10}
|
||||||
response = client.patch(url, json.dumps(data), content_type="application/json")
|
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
|
assert response.status_code == 200
|
||||||
userstory = UserStory.objects.get(id=userstory.id)
|
userstory = UserStory.objects.get(id=userstory.id)
|
||||||
assert userstory.version == 11
|
assert userstory.version == 11
|
||||||
|
@ -159,7 +159,7 @@ def test_valid_concurrent_save_for_task(client):
|
||||||
url = reverse("tasks-detail", args=(task.id,))
|
url = reverse("tasks-detail", args=(task.id,))
|
||||||
data = {"version": 10}
|
data = {"version": 10}
|
||||||
response = client.patch(url, json.dumps(data), content_type="application/json")
|
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
|
assert response.status_code == 200
|
||||||
task = Task.objects.get(id=task.id)
|
task = Task.objects.get(id=task.id)
|
||||||
assert task.version == 11
|
assert task.version == 11
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
|
||||||
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
|
||||||
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
|
||||||
# Copyright (C) 2014 Anler Hernández <hello@anler.me>
|
|
||||||
# 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 <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import json
|
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
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.decode('utf-8'))) == 2
|
|
||||||
|
|
||||||
data = {"is_archived": 0}
|
|
||||||
response = client.get(url, data)
|
|
||||||
assert len(json.loads(response.content.decode('utf-8'))) == 1
|
|
||||||
|
|
||||||
data = {"is_archived": 1}
|
|
||||||
response = client.get(url, data)
|
|
||||||
assert len(json.loads(response.content.decode('utf-8'))) == 1
|
|
|
@ -7,7 +7,7 @@ import pytest
|
||||||
pytestmark = pytest.mark.django_db
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
def test_api_create_project(client):
|
def test_create_project(client):
|
||||||
user = f.create_user()
|
user = f.create_user()
|
||||||
url = reverse("projects-list")
|
url = reverse("projects-list")
|
||||||
data = {"name": "project name", "description": "project description"}
|
data = {"name": "project name", "description": "project description"}
|
||||||
|
@ -18,7 +18,7 @@ def test_api_create_project(client):
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
|
||||||
def test_api_partially_update_project(client):
|
def test_partially_update_project(client):
|
||||||
project = f.create_project()
|
project = f.create_project()
|
||||||
url = reverse("projects-detail", kwargs={"pk": project.pk})
|
url = reverse("projects-detail", kwargs={"pk": project.pk})
|
||||||
data = {"name": ""}
|
data = {"name": ""}
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
# Copyright (C) 2014 Andrey Antukh <niwi@niwi.be>
|
||||||
|
# Copyright (C) 2014 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014 David Barragán <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014 Anler Hernández <hello@anler.me>
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
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
|
|
@ -1,5 +1,6 @@
|
||||||
import pytest
|
import pytest
|
||||||
import json
|
import json
|
||||||
|
from tempfile import NamedTemporaryFile
|
||||||
|
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
@ -11,7 +12,7 @@ from taiga.auth.tokens import get_token_for_user
|
||||||
pytestmark = pytest.mark.django_db
|
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)
|
user = f.UserFactory.create(is_superuser=True)
|
||||||
|
|
||||||
url = reverse('users-list')
|
url = reverse('users-list')
|
||||||
|
@ -26,7 +27,7 @@ def test_api_user_normal_user(client):
|
||||||
assert response.status_code == 405
|
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")
|
user = f.UserFactory.create(email="same@email.com")
|
||||||
url = reverse('users-detail', kwargs={"pk": user.pk})
|
url = reverse('users-detail', kwargs={"pk": user.pk})
|
||||||
data = {"email": "same@email.com"}
|
data = {"email": "same@email.com"}
|
||||||
|
@ -38,7 +39,7 @@ def test_api_user_patch_same_email(client):
|
||||||
assert response.data['_error_message'] == 'Duplicated email'
|
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")
|
f.UserFactory.create(email="one@email.com")
|
||||||
user = f.UserFactory.create(email="two@email.com")
|
user = f.UserFactory.create(email="two@email.com")
|
||||||
url = reverse('users-detail', kwargs={"pk": user.pk})
|
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'
|
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")
|
user = f.UserFactory.create(email="my@email.com")
|
||||||
url = reverse('users-detail', kwargs={"pk": user.pk})
|
url = reverse('users-detail', kwargs={"pk": user.pk})
|
||||||
data = {"email": "my@email"}
|
data = {"email": "my@email"}
|
||||||
|
@ -63,7 +64,7 @@ def test_api_user_patch_invalid_email(client):
|
||||||
assert response.data['_error_message'] == 'Not valid email'
|
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")
|
user = f.UserFactory.create(email="old@email.com")
|
||||||
url = reverse('users-detail', kwargs={"pk": user.pk})
|
url = reverse('users-detail', kwargs={"pk": user.pk})
|
||||||
data = {"email": "new@email.com"}
|
data = {"email": "new@email.com"}
|
||||||
|
@ -77,7 +78,7 @@ def test_api_user_patch_valid_email(client):
|
||||||
assert user.new_email == "new@email.com"
|
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")
|
user = f.UserFactory.create(email_token="change_email_token", new_email="new@email.com")
|
||||||
url = reverse('users-change-email')
|
url = reverse('users-change-email')
|
||||||
data = {"email_token": "change_email_token"}
|
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"
|
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")
|
user = f.UserFactory.create(email_token="change_email_token", new_email="new@email.com")
|
||||||
url = reverse('users-change-email')
|
url = reverse('users-change-email')
|
||||||
data = {}
|
data = {}
|
||||||
|
|
||||||
client.login(user)
|
client.login(user)
|
||||||
response = client.post(url, json.dumps(data), content_type="application/json")
|
response = client.post(url, json.dumps(data), content_type="application/json")
|
||||||
|
|
||||||
assert response.status_code == 400
|
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")
|
user = f.UserFactory.create(email_token="change_email_token", new_email="new@email.com")
|
||||||
url = reverse('users-change-email')
|
url = reverse('users-change-email')
|
||||||
data = {"email_token": "invalid_email_token"}
|
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")
|
response = client.post(url, json.dumps(data), content_type="application/json")
|
||||||
|
|
||||||
assert response.status_code == 400
|
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()
|
user = f.UserFactory.create()
|
||||||
url = reverse('users-detail', kwargs={"pk": user.pk})
|
url = reverse('users-detail', kwargs={"pk": user.pk})
|
||||||
|
|
||||||
|
@ -128,7 +126,7 @@ def test_api_user_delete(client):
|
||||||
assert user.full_name == "Deleted user"
|
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()
|
user = f.UserFactory.create()
|
||||||
url = reverse('users-cancel')
|
url = reverse('users-cancel')
|
||||||
cancel_token = get_token_for_user(user, "cancel_account")
|
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"
|
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()
|
user = f.UserFactory.create()
|
||||||
url = reverse('users-cancel')
|
url = reverse('users-cancel')
|
||||||
data = {"cancel_token": "invalid_cancel_token"}
|
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")
|
response = client.post(url, json.dumps(data), content_type="application/json")
|
||||||
|
|
||||||
assert response.status_code == 400
|
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
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import copy
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
@ -11,10 +12,7 @@ pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
|
||||||
def test_get_userstories_from_bulk():
|
def test_get_userstories_from_bulk():
|
||||||
data = """
|
data = "User Story #1\nUser Story #2\n"
|
||||||
User Story #1
|
|
||||||
User Story #2
|
|
||||||
"""
|
|
||||||
userstories = services.get_userstories_from_bulk(data)
|
userstories = services.get_userstories_from_bulk(data)
|
||||||
|
|
||||||
assert len(userstories) == 2
|
assert len(userstories) == 2
|
||||||
|
@ -23,10 +21,7 @@ User Story #2
|
||||||
|
|
||||||
|
|
||||||
def test_create_userstories_in_bulk():
|
def test_create_userstories_in_bulk():
|
||||||
data = """
|
data = "User Story #1\nUser Story #2\n"
|
||||||
User Story #1
|
|
||||||
User Story #2
|
|
||||||
"""
|
|
||||||
|
|
||||||
with mock.patch("taiga.projects.userstories.services.db") as db:
|
with mock.patch("taiga.projects.userstories.services.db") as db:
|
||||||
userstories = services.create_userstories_in_bulk(data)
|
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:
|
with mock.patch("taiga.projects.userstories.services.db") as db:
|
||||||
services.update_userstories_order_in_bulk(data, "backlog_order", project)
|
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)
|
model=models.UserStory)
|
||||||
|
|
||||||
|
|
||||||
|
@ -108,3 +105,76 @@ def test_api_update_backlog_order_in_bulk(client):
|
||||||
assert response1.status_code == 204, response.data
|
assert response1.status_code == 204, response.data
|
||||||
assert response2.status_code == 204, response.data
|
assert response2.status_code == 204, response.data
|
||||||
assert response3.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)]
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue