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]}) cls = type("InlineCSSTemplateMail", (InlineCSSTemplateMail,), {"name": notification_email[1]})
email = cls() email = cls()
email.send(test_email, context) 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: try:
dump = json.load(reader(dump)) dump = json.load(reader(dump))
is_private = dump["is_private"] is_private = dump.get("is_private", False)
except Exception: except Exception:
raise exc.WrongArguments(_("Invalid dump format")) raise exc.WrongArguments(_("Invalid dump format"))

View File

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

View File

@ -46,6 +46,8 @@ urls = {
"team": "/project/{0}/team/", # project.slug "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 "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): def is_project_owner(user, obj):
"""
The owner attribute of a project is just an historical reference
"""
if user.is_superuser: if user.is_superuser:
return True return True

View File

@ -328,6 +328,74 @@ class ProjectViewSet(LikedResourceMixin, HistoryResourceMixin,
self.check_permissions(request, "tags_colors", project) self.check_permissions(request, "tags_colors", project)
return response.Ok(dict(project.tags_colors)) 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): def _set_base_permissions(self, obj):
update_permissions = False update_permissions = False
if not obj.id: 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, tags_colors = TextArrayField(dimension=2, default=[], null=False, blank=True,
verbose_name=_("tags colors")) verbose_name=_("tags colors"))
transfer_token = models.CharField(max_length=255, null=True, blank=True, default=None,
verbose_name=_("project transfer token"))
#Totals: #Totals:
totals_updated_datetime = models.DateTimeField(null=False, blank=False, auto_now_add=True, totals_updated_datetime = models.DateTimeField(null=False, blank=False, auto_now_add=True,
verbose_name=_("updated date time"), db_index=True) verbose_name=_("updated date time"), db_index=True)

View File

@ -42,6 +42,15 @@ class CanLeaveProject(PermissionComponent):
except Membership.DoesNotExist: except Membership.DoesNotExist:
return False 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): class ProjectPermission(TaigaResourcePermission):
retrieve_perms = HasProjectPerm('view_project') retrieve_perms = HasProjectPerm('view_project')
@ -68,6 +77,10 @@ class ProjectPermission(TaigaResourcePermission):
unwatch_perms = IsAuthenticated() & HasProjectPerm('view_project') unwatch_perms = IsAuthenticated() & HasProjectPerm('view_project')
create_template_perms = IsSuperUser() create_template_perms = IsSuperUser()
leave_perms = CanLeaveProject() leave_perms = CanLeaveProject()
transfer_request_perms = IsProjectOwner()
transfer_start_perms = IsMainOwner()
transfer_reject_perms = IsProjectOwner()
transfer_accept_perms = IsProjectOwner()
class ProjectFansPermission(TaigaResourcePermission): class ProjectFansPermission(TaigaResourcePermission):

View File

@ -328,7 +328,8 @@ class ProjectSerializer(FanResourceSerializerMixin, WatchedResourceModelSerializ
model = models.Project model = models.Project
read_only_fields = ("created_date", "modified_date", "owner", "slug", "blocked_code") read_only_fields = ("created_date", "modified_date", "owner", "slug", "blocked_code")
exclude = ("logo", "last_us_ref", "last_task_ref", "last_issue_ref", 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): def get_my_permissions(self, obj):
if "request" in self.context: 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 .stats import get_member_stats_for_project
from .tags_colors import update_project_tags_colors_handler 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.core.urlresolvers import reverse
from django.conf import settings from django.conf import settings
from django.core.files import File from django.core.files import File
from django.core import mail
from django.core import signing
from taiga.base.utils import json from taiga.base.utils import json
from taiga.projects.services import stats as stats_services from taiga.projects.services import stats as stats_services
@ -19,6 +21,17 @@ import pytest
pytestmark = pytest.mark.django_db 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): def test_get_project_by_slug(client):
project = f.create_project() 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[0]["id"] == project3.id
assert response.data[1]["id"] == project2.id assert response.data[1]["id"] == project2.id
assert response.data[2]["id"] == project1.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