Merge pull request #599 from taigaio/transfer-project

Adding transfer_token to project model
remotes/origin/logger
David Barragán Merino 2016-02-05 09:43:11 +01:00
commit 79ea56b64a
25 changed files with 955 additions and 7 deletions

View File

@ -187,3 +187,38 @@ class Command(BaseCommand):
cls = type("InlineCSSTemplateMail", (InlineCSSTemplateMail,), {"name": notification_email[1]})
email = cls()
email.send(test_email, context)
# Transfer Emails
context = {
"project": Project.objects.all().order_by("?").first(),
"requester": User.objects.all().order_by("?").first(),
}
email = mail_builder.transfer_request(test_email, context)
email.send()
context = {
"project": Project.objects.all().order_by("?").first(),
"receiver": User.objects.all().order_by("?").first(),
"token": "test-token",
"reason": "Test reason"
}
email = mail_builder.transfer_start(test_email, context)
email.send()
context = {
"project": Project.objects.all().order_by("?").first(),
"old_owner": User.objects.all().order_by("?").first(),
"new_owner": User.objects.all().order_by("?").first(),
"reason": "Test reason"
}
email = mail_builder.transfer_accept(test_email, context)
email.send()
context = {
"project": Project.objects.all().order_by("?").first(),
"rejecter": User.objects.all().order_by("?").first(),
"reason": "Test reason"
}
email = mail_builder.transfer_reject(test_email, context)
email.send()

View File

@ -219,7 +219,7 @@ class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixi
try:
dump = json.load(reader(dump))
is_private = dump["is_private"]
is_private = dump.get("is_private", False)
except Exception:
raise exc.WrongArguments(_("Invalid dump format"))

View File

@ -94,7 +94,7 @@ def dict_to_project(data, owner=None):
members = len(data.get("memberships", []))
(enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
owner,
project=Project(is_private=data["is_private"], id=None),
project=Project(is_private=data.get("is_private", False), id=None),
members=members
)
if not enough_slots:

View File

@ -46,6 +46,8 @@ urls = {
"team": "/project/{0}/team/", # project.slug
"project-transfer": "/project/{0}/transfer/{1}", # project.slug, project.transfer_token
"project-admin": "/project/{0}/admin/project-profile/details", # project.slug
}

View File

@ -38,10 +38,6 @@ def _get_object_project(obj):
def is_project_owner(user, obj):
"""
The owner attribute of a project is just an historical reference
"""
if user.is_superuser:
return True

View File

@ -328,6 +328,74 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
self.check_permissions(request, "tags_colors", project)
return response.Ok(dict(project.tags_colors))
@detail_route(methods=["POST"])
def transfer_request(self, request, pk=None):
project = self.get_object()
self.check_permissions(request, "transfer_request", project)
services.request_project_transfer(project, request.user)
return response.Ok()
@detail_route(methods=['post'])
def transfer_start(self, request, pk=None):
project = self.get_object()
self.check_permissions(request, "transfer_start", project)
user_id = request.DATA.get('user', None)
if user_id is None:
raise exc.WrongArguments(_("Invalid user id"))
user_model = apps.get_model("users", "User")
try:
user = user_model.objects.get(id=user_id)
except user_model.DoesNotExist:
return response.BadRequest(_("The user doesn't exist"))
# Check the user is an admin membership from the project
try:
project.memberships.get(is_owner=True, user=user)
except apps.get_model("projects", "Membership").DoesNotExist:
return response.BadRequest(_("The user must be an admin member of the project"))
reason = request.DATA.get('reason', None)
transfer_token = services.start_project_transfer(project, user, reason)
return response.Ok()
@detail_route(methods=["POST"])
def transfer_accept(self, request, pk=None):
token = request.DATA.get('token', None)
if token is None:
raise exc.WrongArguments(_("Invalid token"))
project = self.get_object()
self.check_permissions(request, "transfer_accept", project)
members = project.memberships.count()
(enough_slots, not_enough_slots_error) = users_service.has_available_slot_for_project(
request.user,
project=project,
members=members
)
if not enough_slots:
raise exc.BadRequest(not_enough_slots_error)
reason = request.DATA.get('reason', None)
services.accept_project_transfer(project, request.user, token, reason)
return response.Ok()
@detail_route(methods=["POST"])
def transfer_reject(self, request, pk=None):
token = request.DATA.get('token', None)
if token is None:
raise exc.WrongArguments(_("Invalid token"))
project = self.get_object()
self.check_permissions(request, "transfer_reject", project)
reason = request.DATA.get('reason', None)
services.reject_project_transfer(project, request.user, token, reason)
return response.Ok()
def _set_base_permissions(self, obj):
update_permissions = False
if not obj.id:

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('projects', '0035_project_blocked_code'),
]
operations = [
migrations.AddField(
model_name='project',
name='transfer_token',
field=models.CharField(max_length=255, default=None, blank=True, null=True, verbose_name='project transfer token'),
),
]

View File

@ -235,6 +235,9 @@ class Project(ProjectDefaults, TaggedMixin, models.Model):
tags_colors = TextArrayField(dimension=2, default=[], null=False, blank=True,
verbose_name=_("tags colors"))
transfer_token = models.CharField(max_length=255, null=True, blank=True, default=None,
verbose_name=_("project transfer token"))
#Totals:
totals_updated_datetime = models.DateTimeField(null=False, blank=False, auto_now_add=True,
verbose_name=_("updated date time"), db_index=True)

View File

@ -42,6 +42,15 @@ class CanLeaveProject(PermissionComponent):
except Membership.DoesNotExist:
return False
class IsMainOwner(PermissionComponent):
def check_permissions(self, request, view, obj=None):
if not obj or not request.user.is_authenticated():
return False
if obj.owner is None:
return False
return obj.owner == request.user
class ProjectPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project')
@ -68,6 +77,10 @@ class ProjectPermission(TaigaResourcePermission):
unwatch_perms = IsAuthenticated() & HasProjectPerm('view_project')
create_template_perms = IsSuperUser()
leave_perms = CanLeaveProject()
transfer_request_perms = IsProjectOwner()
transfer_start_perms = IsMainOwner()
transfer_reject_perms = IsProjectOwner()
transfer_accept_perms = IsProjectOwner()
class ProjectFansPermission(TaigaResourcePermission):

View File

@ -328,7 +328,8 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
model = models.Project
read_only_fields = ("created_date", "modified_date", "owner", "slug", "blocked_code")
exclude = ("logo", "last_us_ref", "last_task_ref", "last_issue_ref",
"issues_csv_uuid", "tasks_csv_uuid", "userstories_csv_uuid")
"issues_csv_uuid", "tasks_csv_uuid", "userstories_csv_uuid",
"transfer_token")
def get_my_permissions(self, obj):
if "request" in self.context:

View File

@ -46,3 +46,7 @@ from .stats import get_stats_for_project
from .stats import get_member_stats_for_project
from .tags_colors import update_project_tags_colors_handler
from .modules_config import get_modules_config
from .transfer import request_project_transfer, start_project_transfer
from .transfer import accept_project_transfer, reject_project_transfer

View File

@ -0,0 +1,107 @@
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
# 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 import signing
from django.utils.translation import ugettext as _
import datetime
from taiga.base.mails import mail_builder
from taiga.base import exceptions as exc
def request_project_transfer(project, user):
template = mail_builder.transfer_request
email = template(project.owner, {"project": project, "requester": user})
email.send()
def start_project_transfer(project, user, reason):
"""Generates the transfer token for a project transfer and notify to the destination user
:param project: Project trying to transfer
:param user: Destination user
:param reason: Reason to transfer the project
"""
signer = signing.TimestampSigner()
token = signer.sign(user.id)
project.transfer_token = token
project.save()
template = mail_builder.transfer_start
context = {
"project": project,
"receiver": user,
"token": token,
"reason": reason
}
email = template(project.owner, context)
email.send()
def _validate_token(token, project_token, user_id):
signer = signing.TimestampSigner()
if project_token != token:
raise exc.WrongArguments(_("Token is invalid"))
try:
value = signer.unsign(token, max_age=datetime.timedelta(days=7))
except signing.SignatureExpired:
raise exc.WrongArguments(_("Token has expired"))
except signing.BadSignature:
raise exc.WrongArguments(_("Token is invalid"))
if str(value) != str(user_id):
raise exc.WrongArguments(_("Token is invalid"))
def reject_project_transfer(project, user, token, reason):
_validate_token(token, project.transfer_token, user.id)
project.transfer_token = None
project.save()
template = mail_builder.transfer_reject
context = {
"project": project,
"rejecter": user,
"reason": reason
}
email = template(project.owner, context)
email.send()
def accept_project_transfer(project, user, token, reason):
_validate_token(token, project.transfer_token, user.id)
old_owner = project.owner
project.transfer_token = None
project.owner = user
project.save()
template = mail_builder.transfer_accept
context = {
"project": project,
"old_owner": old_owner,
"new_owner": user,
"reason": reason
}
email = template(old_owner, context)
email.send()

View File

@ -0,0 +1,19 @@
{% extends "emails/hero-body-html.jinja" %}
{% block body %}
{% trans old_owner_name=old_owner.get_full_name(), new_owner_name=new_owner.get_full_name(), project_name=project.name %}
<p>Hi {{old_owner_name}},</p>
<p>{{ new_owner_name}} has accepted your offer and will become the new project owner for "{{project_name}}".</p>
{% endtrans %}
{% if reason %}
{% trans %}<p>This is the reason/comment:</p>{% endtrans %}
<p>{{reason}}</p>
{% endif %}
{% trans %}
<p>From now on, your new status for this project will be "admin".</p>
{% endtrans %}
<p><small>{% trans %}The Taiga Team{% endtrans %}</small></p>
{% endblock %}

View File

@ -0,0 +1,17 @@
{% trans old_owner_name=old_owner.get_full_name(), new_owner_name=new_owner.get_full_name(), project_name=project.name %}
Hi {{old_owner_name}},
{{ new_owner_name}} has accepted your offer and will become the new project owner for "{{project_name}}".
{% endtrans %}
{% if reason %}{% trans %}This is the reason/comment:{% endtrans %}
{{reason}}
{% endif %}
{% trans %}
From now on, your new status for this project will be "admin".
{% endtrans %}
---
{% trans %}
The Taiga Team
{% endtrans %}

View File

@ -0,0 +1,3 @@
{% trans project=project.name %}
[{{project}}] Project ownership transfer offer accepted!
{% endtrans %}

View File

@ -0,0 +1,27 @@
{% extends "emails/hero-body-html.jinja" %}
{% block body %}
{% trans owner_name=project.owner.get_full_name(), rejecter_name=rejecter.get_full_name(), project_name=project.name %}
<p>Hi {{owner_name}},</p>
<p>{{ rejecter_name}} has declined your offer and will not become the new project owner for "{{project_name}}".</p>
{% endtrans %}
{% if reason %}
{% trans %}
<p>This is the reason/comment:</p>
{% endtrans %}
<p>{{ reason }}</p>
{% endif %}
{% trans %}
<p>If you want, you can still try to transfer the project ownership to a different person.</p>
{% endtrans %}
<a class="button" href="{{ resolve_front_url("project-admin", project.slug) }}"
title="{% trans %}Request transfer to a different person{% endtrans %}">
{% trans %}Request transfer to a different person{% endtrans %}
</a>
<p><small>{% trans %}The Taiga Team{% endtrans %}</small></p>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% trans owner_name=project.owner.get_full_name(), rejecter_name=rejecter.get_full_name(), project_name=project.name %}
Hi {{owner_name}},
{{ rejecter_name}} has declined your offer and will not become the new project owner for "{{project_name}}".
{% endtrans %}
{% if reason %}{% trans %}This is the reason/comment:{% endtrans %}
{{ reason }}
{% endif %}
{% trans %}
If you want, you can still try to transfer the project ownership to a different person.
{% endtrans %}
{% trans %}Request transfer to a different person:{% endtrans %}
{{ resolve_front_url("project-admin", project.slug) }}
---
{% trans %}
The Taiga Team
{% endtrans %}

View File

@ -0,0 +1,3 @@
{% trans project=project.name %}
[{{project}}] Project ownership transfer declined
{% endtrans %}

View File

@ -0,0 +1,17 @@
{% extends "emails/hero-body-html.jinja" %}
{% block body %}
{% trans owner_name=project.owner.get_full_name(), requester_name=requester.get_full_name(), project_name=project.name %}
<p>Hi {{owner_name}},</p>
<p>{{ requester_name }} has requested to become the project owner for "{{project_name}}".</p>
{% endtrans %}
{% trans %}
<p>Please, click on "Continue" if you would like to start the project transfer from the administration panel.</p>
{% endtrans %}
<a class="button" href="{{ resolve_front_url("project-admin", project.slug) }}"
title="{% trans %}Continue{% endtrans %}">{% trans %}Continue{% endtrans %}</a>
<p><small>{% trans %}The Taiga Team{% endtrans %}</small></p>
{% endblock %}

View File

@ -0,0 +1,15 @@
{% trans owner_name=project.owner.get_full_name(), requester_name=requester.get_full_name(), project_name=project.name %}
Hi {{owner_name}},
{{ requester_name }} has requested to become the project owner for "{{project_name}}".
{% endtrans %}
{% trans %}
Please, go to your project settings if you would like to start the project transfer from the administration panel.
{% endtrans %}
{{ _("Go to your project settings:") }} {{ resolve_front_url("project-admin", project.slug) }}
---
{% trans %}
The Taiga Team
{% endtrans %}

View File

@ -0,0 +1,3 @@
{% trans project=project.name %}
[{{project}}] Project ownership transfer request
{% endtrans %}

View File

@ -0,0 +1,25 @@
{% extends "emails/hero-body-html.jinja" %}
{% block body %}
{% trans owner_name=project.owner.get_full_name(), receiver_name=receiver.get_full_name(), project_name=project.name %}
<p>Hi {{receiver_name}},</p>
<p>{{ owner_name }}, the current project owner at "{{project_name}}" would like you to become the new project owner.</p>
{% endtrans %}
{% if reason %}
{% trans %}
<p>This is the reason/comment:</p>
{% endtrans %}
<p>{{ reason }}</p>
{% endif %}
{% trans %}
<p>Please, click on "Continue" to either accept or reject this proposal.</p>
{% endtrans %}
<a class="button" href="{{ resolve_front_url("project-transfer", project.slug, project.transfer_token) }}"
title="{{ _("Continue") }}">{{ _("Continue") }}</a>
<p><small>{{ _("The Taiga Team") }}</small></p>
{% endblock %}

View File

@ -0,0 +1,19 @@
{% trans owner_name=project.owner.get_full_name(), receiver_name=receiver.get_full_name(), project_name=project.name %}
Hi {{receiver_name}},
{{ owner_name }}, the current project owner at "{{project_name}}" would like you to become the new project owner.
{% endtrans %}
{% if reason %}{% trans %}This is the reason/comment:{% endtrans %}
{{ reason }}
{% endif %}
{% trans %}
Please, go to the following link to either accept or reject this proposal.</p>
{% endtrans %}
{{ _("Accept or reject the project transfer:") }} {{ resolve_front_url("project-transfer", project.slug, project.transfer_token) }}
---
{% trans %}
The Taiga Team
{% endtrans %}

View File

@ -0,0 +1,3 @@
{% trans project=project.name %}
[{{project}}] Project ownership transfer offer
{% endtrans %}

View File

@ -1,6 +1,8 @@
from django.core.urlresolvers import reverse
from django.conf import settings
from django.core.files import File
from django.core import mail
from django.core import signing
from taiga.base.utils import json
from taiga.projects.services import stats as stats_services
@ -19,6 +21,17 @@ import pytest
pytestmark = pytest.mark.django_db
class ExpiredSigner(signing.TimestampSigner):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.salt = "django.core.signing.TimestampSigner"
def timestamp(self):
from django.utils import baseconv
import time
time_in_the_far_past = int(time.time()) - 24*60*60*1000
return baseconv.base62.encode(time_in_the_far_past)
def test_get_project_by_slug(client):
project = f.create_project()
@ -698,3 +711,519 @@ def test_project_list_with_search_query_order_by_ranking(client):
assert response.data[0]["id"] == project3.id
assert response.data[1]["id"] == project2.id
assert response.data[2]["id"] == project1.id
def test_transfer_request_from_not_anonimous(client):
user = f.UserFactory.create()
project = f.create_project(anon_permissions=["view_project"])
url = reverse("projects-transfer-request", args=(project.id,))
mail.outbox = []
response = client.json.post(url)
assert response.status_code == 401
assert len(mail.outbox) == 0
def test_transfer_request_from_not_project_member(client):
user = f.UserFactory.create()
project = f.create_project(public_permissions=["view_project"])
url = reverse("projects-transfer-request", args=(project.id,))
mail.outbox = []
client.login(user)
response = client.json.post(url)
assert response.status_code == 403
assert len(mail.outbox) == 0
def test_transfer_request_from_not_admin_member(client):
user = f.UserFactory.create()
project = f.create_project()
role = f.RoleFactory(project=project, permissions=["view_project"])
f.MembershipFactory(user=user, project=project, role=role, is_owner=False)
url = reverse("projects-transfer-request", args=(project.id,))
mail.outbox = []
client.login(user)
response = client.json.post(url)
assert response.status_code == 403
assert len(mail.outbox) == 0
def test_transfer_request_from_admin_member(client):
user = f.UserFactory.create()
project = f.create_project()
role = f.RoleFactory(project=project, permissions=["view_project"])
f.MembershipFactory(user=user, project=project, role=role, is_owner=True)
url = reverse("projects-transfer-request", args=(project.id,))
mail.outbox = []
client.login(user)
response = client.json.post(url)
assert response.status_code == 200
assert len(mail.outbox) == 1
def test_project_transfer_start_to_not_a_membership(client):
user_from = f.UserFactory.create()
project = f.create_project(owner=user_from)
f.MembershipFactory(user=user_from, project=project, is_owner=True)
client.login(user_from)
url = reverse("projects-transfer-start", kwargs={"pk": project.pk})
data = {
"user": 666,
}
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
assert "The user doesn't exist" in response.data
def test_project_transfer_start_to_not_a_membership_admin(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create()
project = f.create_project(owner=user_from)
f.MembershipFactory(user=user_from, project=project, is_owner=True)
f.MembershipFactory(user=user_to, project=project)
client.login(user_from)
url = reverse("projects-transfer-start", kwargs={"pk": project.pk})
data = {
"user": user_to.id,
}
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
assert "The user must be" in response.data
def test_project_transfer_start_to_a_valid_user(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create()
project = f.create_project(owner=user_from)
f.MembershipFactory(user=user_from, project=project, is_owner=True)
f.MembershipFactory(user=user_to, project=project, is_owner=True)
client.login(user_from)
url = reverse("projects-transfer-start", kwargs={"pk": project.pk})
data = {
"user": user_to.id,
}
mail.outbox = []
assert project.transfer_token is None
response = client.json.post(url, json.dumps(data))
assert response.status_code == 200
project = Project.objects.get(id=project.id)
assert project.transfer_token is not None
assert len(mail.outbox) == 1
def test_project_transfer_reject_from_admin_member_without_token(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create()
signer = signing.TimestampSigner()
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token)
f.MembershipFactory(user=user_from, project=project, is_owner=True)
f.MembershipFactory(user=user_to, project=project, is_owner=True)
client.login(user_to)
url = reverse("projects-transfer-reject", kwargs={"pk": project.pk})
data = {}
mail.outbox = []
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
assert len(mail.outbox) == 0
def test_project_transfer_reject_from_not_admin_member(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create()
signer = signing.TimestampSigner()
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token, public_permissions=["view_project"])
f.MembershipFactory(user=user_from, project=project, is_owner=True)
f.MembershipFactory(user=user_to, project=project, is_owner=False)
client.login(user_to)
url = reverse("projects-transfer-reject", kwargs={"pk": project.pk})
data = {
"token": token,
}
mail.outbox = []
response = client.json.post(url, json.dumps(data))
assert response.status_code == 403
assert len(mail.outbox) == 0
def test_project_transfer_reject_from_admin_member_with_invalid_token(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create()
project = f.create_project(owner=user_from, transfer_token="invalid-token")
f.MembershipFactory(user=user_from, project=project, is_owner=True)
f.MembershipFactory(user=user_to, project=project, is_owner=True)
client.login(user_to)
url = reverse("projects-transfer-reject", kwargs={"pk": project.pk})
data = {
"token": "invalid-token",
}
mail.outbox = []
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
assert "Token is invalid" == response.data["_error_message"]
assert len(mail.outbox) == 0
def test_project_transfer_reject_from_admin_member_with_other_user_token(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create()
other_user = f.UserFactory.create()
signer = signing.TimestampSigner()
token = signer.sign(other_user.id)
project = f.create_project(owner=user_from, transfer_token=token)
f.MembershipFactory(user=user_from, project=project, is_owner=True)
f.MembershipFactory(user=user_to, project=project, is_owner=True)
client.login(user_to)
url = reverse("projects-transfer-reject", kwargs={"pk": project.pk})
data = {
"token": token,
}
mail.outbox = []
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
assert "Token is invalid" == response.data["_error_message"]
assert len(mail.outbox) == 0
def test_project_transfer_reject_from_admin_member_with_expired_token(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create()
signer = ExpiredSigner()
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token)
f.MembershipFactory(user=user_from, project=project, is_owner=True)
f.MembershipFactory(user=user_to, project=project, is_owner=True)
client.login(user_to)
url = reverse("projects-transfer-reject", kwargs={"pk": project.pk})
data = {
"token": token,
}
mail.outbox = []
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
assert "Token has expired" == response.data["_error_message"]
assert len(mail.outbox) == 0
def test_project_transfer_reject_from_admin_member_with_valid_token(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create()
signer = signing.TimestampSigner()
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token)
f.MembershipFactory(user=user_from, project=project, is_owner=True)
f.MembershipFactory(user=user_to, project=project, is_owner=True)
client.login(user_to)
url = reverse("projects-transfer-reject", kwargs={"pk": project.pk})
data = {
"token": token,
}
mail.outbox = []
response = client.json.post(url, json.dumps(data))
assert response.status_code == 200
assert len(mail.outbox) == 1
assert mail.outbox[0].to == [user_from.email]
def test_project_transfer_accept_from_admin_member_without_token(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create()
signer = signing.TimestampSigner()
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token)
f.MembershipFactory(user=user_from, project=project, is_owner=True)
f.MembershipFactory(user=user_to, project=project, is_owner=True)
client.login(user_to)
url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
data = {}
mail.outbox = []
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
assert len(mail.outbox) == 0
def test_project_transfer_accept_from_not_admin_member(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create()
signer = signing.TimestampSigner()
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token, public_permissions=["view_project"])
f.MembershipFactory(user=user_from, project=project, is_owner=True)
f.MembershipFactory(user=user_to, project=project, is_owner=False)
client.login(user_to)
url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
data = {
"token": token,
}
mail.outbox = []
response = client.json.post(url, json.dumps(data))
assert response.status_code == 403
assert len(mail.outbox) == 0
def test_project_transfer_accept_from_admin_member_with_invalid_token(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create()
project = f.create_project(owner=user_from, transfer_token="invalid-token")
f.MembershipFactory(user=user_from, project=project, is_owner=True)
f.MembershipFactory(user=user_to, project=project, is_owner=True)
client.login(user_to)
url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
data = {
"token": "invalid-token",
}
mail.outbox = []
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
assert "Token is invalid" == response.data["_error_message"]
assert len(mail.outbox) == 0
def test_project_transfer_accept_from_admin_member_with_other_user_token(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create()
other_user = f.UserFactory.create()
signer = signing.TimestampSigner()
token = signer.sign(other_user.id)
project = f.create_project(owner=user_from, transfer_token=token)
f.MembershipFactory(user=user_from, project=project, is_owner=True)
f.MembershipFactory(user=user_to, project=project, is_owner=True)
client.login(user_to)
url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
data = {
"token": token,
}
mail.outbox = []
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
assert "Token is invalid" == response.data["_error_message"]
assert len(mail.outbox) == 0
def test_project_transfer_accept_from_admin_member_with_expired_token(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create()
signer = ExpiredSigner()
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token)
f.MembershipFactory(user=user_from, project=project, is_owner=True)
f.MembershipFactory(user=user_to, project=project, is_owner=True)
client.login(user_to)
url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
data = {
"token": token,
}
mail.outbox = []
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
assert "Token has expired" == response.data["_error_message"]
assert len(mail.outbox) == 0
def test_project_transfer_accept_from_admin_member_with_valid_token_without_enough_slots(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create(max_private_projects=0)
signer = signing.TimestampSigner()
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token, is_private=True)
f.MembershipFactory(user=user_from, project=project, is_owner=True)
f.MembershipFactory(user=user_to, project=project, is_owner=True)
client.login(user_to)
url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
data = {
"token": token,
}
mail.outbox = []
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
assert len(mail.outbox) == 0
project = Project.objects.get(pk=project.pk)
assert project.owner.id == user_from.id
assert project.transfer_token is not None
def test_project_transfer_accept_from_admin_member_with_valid_token_without_enough_memberships_public_project_slots(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create(max_members_public_projects=5)
signer = signing.TimestampSigner()
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token, is_private=False)
f.MembershipFactory(user=user_from, project=project, is_owner=True)
f.MembershipFactory(user=user_to, project=project, is_owner=True)
f.MembershipFactory(project=project)
f.MembershipFactory(project=project)
f.MembershipFactory(project=project)
f.MembershipFactory(project=project)
f.MembershipFactory(project=project)
client.login(user_to)
url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
data = {
"token": token,
}
mail.outbox = []
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
assert len(mail.outbox) == 0
project = Project.objects.get(pk=project.pk)
assert project.owner.id == user_from.id
assert project.transfer_token is not None
def test_project_transfer_accept_from_admin_member_with_valid_token_without_enough_memberships_private_project_slots(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create(max_members_private_projects=5)
signer = signing.TimestampSigner()
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token, is_private=True)
f.MembershipFactory(user=user_from, project=project, is_owner=True)
f.MembershipFactory(user=user_to, project=project, is_owner=True)
f.MembershipFactory(project=project)
f.MembershipFactory(project=project)
f.MembershipFactory(project=project)
f.MembershipFactory(project=project)
f.MembershipFactory(project=project)
client.login(user_to)
url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
data = {
"token": token,
}
mail.outbox = []
response = client.json.post(url, json.dumps(data))
assert response.status_code == 400
assert len(mail.outbox) == 0
project = Project.objects.get(pk=project.pk)
assert project.owner.id == user_from.id
assert project.transfer_token is not None
def test_project_transfer_accept_from_admin_member_with_valid_token_with_enough_slots(client):
user_from = f.UserFactory.create()
user_to = f.UserFactory.create(max_private_projects=1)
signer = signing.TimestampSigner()
token = signer.sign(user_to.id)
project = f.create_project(owner=user_from, transfer_token=token, is_private=True)
f.MembershipFactory(user=user_from, project=project, is_owner=True)
f.MembershipFactory(user=user_to, project=project, is_owner=True)
client.login(user_to)
url = reverse("projects-transfer-accept", kwargs={"pk": project.pk})
data = {
"token": token,
}
mail.outbox = []
response = client.json.post(url, json.dumps(data))
assert response.status_code == 200
assert len(mail.outbox) == 1
assert mail.outbox[0].to == [user_from.email]
project = Project.objects.get(pk=project.pk)
assert project.owner.id == user_to.id
assert project.transfer_token is None