Merge pull request #599 from taigaio/transfer-project
Adding transfer_token to project modelremotes/origin/logger
commit
79ea56b64a
|
@ -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()
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{% trans project=project.name %}
|
||||||
|
[{{project}}] Project ownership transfer offer accepted!
|
||||||
|
{% endtrans %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{% trans project=project.name %}
|
||||||
|
[{{project}}] Project ownership transfer declined
|
||||||
|
{% endtrans %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{% trans project=project.name %}
|
||||||
|
[{{project}}] Project ownership transfer request
|
||||||
|
{% endtrans %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{% trans project=project.name %}
|
||||||
|
[{{project}}] Project ownership transfer offer
|
||||||
|
{% endtrans %}
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue