-
-
Notifications
You must be signed in to change notification settings - Fork 185
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(organizations): transfer projects to organization owner when the…
… recipient belongs to a MMO TASK-1338 (#5335) ### 📣 Summary Added logic to transfer project ownership to the organization when the recipient belongs to a Multi-Member Organization (MMO). ### 📖 Description This PR introduces functionality to automatically transfer project ownership to the organization when the recipient of the transfer is a member of a Multi-Member Organization (MMO).
- Loading branch information
1 parent
b8d91c4
commit c55a349
Showing
9 changed files
with
219 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
49 changes: 49 additions & 0 deletions
49
kobo/apps/project_ownership/templates/emails/new_invite_org.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
{% load i18n %} | ||
{% load strings %} | ||
{% trans "Projects:" as projects_label %} | ||
|
||
<p>{% trans "Dear" %} {{ username }},</p> | ||
|
||
{% if transfers|length == 1 %} | ||
<p>{% blocktrans with asset_uid=transfers.0.asset_uid asset_name=transfers.0.asset_name %}{{ sender_username }} ({{ sender_email }}) has requested to transfer ownership of the project <a href="{{ base_url }}/#/forms/{{ asset_uid }}/landing">{{ asset_name }}</a> to you.{% endblocktrans %}</p> | ||
|
||
<p>{% trans "Because you are part of a team, this project will be owned by the team, but you will retain the ability to manage project permissions. If you do not want the team to own this project, then don’t click on the link below." %}</p> | ||
|
||
<p>{% blocktrans %} | ||
If you accept the transfer: | ||
<ul> | ||
<li>All submissions, data storage, and transcription/translation usage for this project will count toward the team’s plan limits</li> | ||
<li>If you leave the team in the future, your account will still retain manage project permissions for this project</li> | ||
</ul> | ||
{% endblocktrans %} | ||
</p> | ||
{% else %} | ||
<p>{% blocktrans %}{{ sender_username }} ({{ sender_email }}) has requested to transfer ownership of the following projects to you:{% endblocktrans %} | ||
<ul> | ||
{% for transfer in transfers %} | ||
<li><a href="{{ base_url }}/#/forms/{{ transfer.asset_uid }}/landing">{{ transfer.asset_name }}</a></li> | ||
{% endfor %} | ||
</ul> | ||
</p> | ||
|
||
<p>{% trans "Because you are part of a team, this project will be owned by the team, but you will retain the ability to manage project permissions. If you do not want the team to own this project, then don’t click on the link below." %}</p> | ||
|
||
<p>{% blocktrans %} | ||
If you accept the transfer: | ||
<ul> | ||
<li>All submissions, data storage, and transcription/translation usage for these projects will count toward the team’s plan limits</li> | ||
<li>If you leave the team in the future, your account will still retain manage project permissions for these projects</li> | ||
</ul> | ||
{% endblocktrans %} | ||
</p> | ||
{% endif %} | ||
|
||
<p>{% trans "If you are unsure, please contact the current owner." %}</p> | ||
|
||
<p>{% blocktrans %}This transfer request will expire in {{ invite_expiry }} days.{% endblocktrans %}</p> | ||
|
||
<p>{% blocktrans %}To respond to this transfer request, please use the following link: {{ base_url }}/#/projects/home?invite={{ invite_uid }}{% endblocktrans %}</p> | ||
|
||
<p> | ||
- KoboToolbox | ||
</p> |
39 changes: 39 additions & 0 deletions
39
kobo/apps/project_ownership/templates/emails/new_invite_org.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
{% load i18n %} | ||
{% load strings %} | ||
{% trans "Projects:" as projects_label %} | ||
|
||
{% trans "Dear" %} {{ username }}, | ||
{% if transfers|length == 1 %} | ||
{% blocktrans with asset_uid=transfers.0.asset_uid asset_name=transfers.0.asset_name %}{{ sender_username }} ({{ sender_email }}) has requested to transfer ownership of the project {{ asset_name }} ({{ base_url }}/#/forms/{{ asset_uid }}/landing) to you.{% endblocktrans %} | ||
|
||
{% trans "Because you are part of a team, this project will be owned by the team, but you will retain the ability to manage project permissions. If you do not want the team to own this project, then don’t click on the link below." %} | ||
|
||
{% blocktrans %} | ||
If you accept the transfer: | ||
- All submissions, data storage, and transcription/translation usage for this project will count toward the team’s plan limits | ||
- If you leave the team in the future, your account will still retain manage project permissions for this project | ||
{% endblocktrans %} | ||
|
||
{% else %} | ||
{% blocktrans %}{{ sender_username }} ({{ sender_email }}) has requested to transfer ownership of the following projects to you:{% endblocktrans %} | ||
{% for transfer in transfers %} | ||
* {{ transfer.asset_name }} - [{{ base_url }}/#/forms/{{ asset_uid }}/landing] | ||
{% endfor %} | ||
|
||
{% trans "Because you are part of a team, these projects will be owned by the team, but you will retain the ability to manage project permissions. If you do not want the team to own these projects, then don’t click on the link below." %} | ||
|
||
{% blocktrans %} | ||
If you accept the transfer: | ||
- All submissions, data storage, and transcription/translation usage for these projects will count toward the team’s plan limits | ||
- If you leave the team in the future, your account will still retain manage project permissions for these projects | ||
{% endblocktrans %} | ||
|
||
{% endif %} | ||
|
||
{% trans "If you are unsure, please contact the current owner." %} | ||
|
||
{% blocktrans %}This transfer request will expire in {{ invite_expiry }} days.{% endblocktrans %} | ||
|
||
{% blocktrans %}To respond to this transfer request, please use the following link: {{ base_url }}/#/projects/home?invite={{ invite_uid }}{% endblocktrans %} | ||
|
||
- KoboToolbox |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,8 @@ | ||
from constance.test import override_config | ||
from django.contrib.auth import get_user_model | ||
from django.core import mail | ||
from rest_framework.reverse import reverse | ||
|
||
from kobo.apps.kobo_auth.shortcuts import User | ||
from kpi.models import Asset | ||
from kpi.tests.kpi_test_case import KpiTestCase | ||
from kpi.urls.router_api_v2 import URL_NAMESPACE as ROUTER_URL_NAMESPACE | ||
|
@@ -16,7 +16,6 @@ class ProjectOwnershipMailTestCase(KpiTestCase): | |
|
||
def setUp(self) -> None: | ||
super().setUp() | ||
User = get_user_model() # noqa | ||
self.someuser = User.objects.get(username='someuser') | ||
self.anotheruser = User.objects.get(username='anotheruser') | ||
|
||
|
@@ -36,6 +35,7 @@ def test_recipient_receives_invite(self): | |
invite_uid = Invite.objects.first().uid | ||
self.assertEqual(mail.outbox[0].to[0], self.anotheruser.email) | ||
self.assertIn(invite_uid, mail.outbox[0].body) | ||
self.assertNotIn('Because you are part of a team', mail.outbox[0].body) | ||
|
||
def test_sender_receives_new_owner_acceptance(self): | ||
invite = Invite.objects.create(sender=self.someuser, recipient=self.anotheruser) | ||
|
@@ -90,3 +90,27 @@ def test_admins_receive_failure_report(self): | |
mail.outbox[0].subject, | ||
'KoboToolbox Notifications: Project ownership transfer failure', | ||
) | ||
|
||
def test_recipient_as_org_member_receives_invite(self): | ||
alice = User.objects.create_user( | ||
username='alice', password='alice', email='[email protected]' | ||
) | ||
# Add alice to anotheruser's organization | ||
organization = self.anotheruser.organization | ||
organization.mmo_override = True | ||
organization.save() | ||
organization.add_user(alice) | ||
|
||
self.client.login(username='someuser', password='someuser') | ||
payload = { | ||
'recipient': self.absolute_reverse( | ||
self._get_endpoint('user-kpi-detail'), args=[alice.username] | ||
), | ||
'assets': [self.asset.uid], | ||
} | ||
self.client.post(self.invite_url, data=payload, format='json') | ||
|
||
invite_uid = Invite.objects.first().uid | ||
self.assertEqual(mail.outbox[0].to[0], alice.email) | ||
self.assertIn(invite_uid, mail.outbox[0].body) | ||
self.assertIn('Because you are part of a team', mail.outbox[0].body) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
from constance.test import override_config | ||
from django.test import TestCase | ||
|
||
from kobo.apps.kobo_auth.shortcuts import User | ||
from kpi.constants import PERM_MANAGE_ASSET | ||
from kpi.models import Asset | ||
from kpi.tests.utils.transaction import immediate_on_commit | ||
from ..utils import create_invite | ||
|
||
|
||
@override_config(PROJECT_OWNERSHIP_AUTO_ACCEPT_INVITES=True) | ||
class ProjectOwnershipPermissionTestCase(TestCase): | ||
""" | ||
The purpose of this test suite is solely to verify permission assignment. | ||
To achieve this, PROJECT_OWNERSHIP_AUTO_ACCEPT_INVITES is set to True, allowing | ||
the email invitation system to be bypassed and eliminating the need to process it | ||
during testing. | ||
""" | ||
|
||
fixtures = ['test_data'] | ||
|
||
def setUp(self): | ||
|
||
self.someuser = User.objects.get(username='someuser') | ||
self.anotheruser = User.objects.get(username='anotheruser') | ||
self.thirduser = User.objects.create_user( | ||
username='thirduser', | ||
password='thirduser', | ||
email='[email protected]', | ||
) | ||
self.asset = Asset.objects.get(pk=1) | ||
|
||
def test_recipient_as_regular_user_is_owner(self): | ||
assert self.asset.owner == self.someuser | ||
with immediate_on_commit(): | ||
create_invite( | ||
sender=self.someuser, | ||
recipient=self.anotheruser, | ||
assets=[self.asset], | ||
invite_class_name='Invite', | ||
) | ||
|
||
self.asset.refresh_from_db() | ||
# New owner should anotheruser | ||
assert self.asset.owner == self.anotheruser | ||
# The previous owner should have received "manage_asset" permission | ||
assert self.asset.has_perm(self.someuser, PERM_MANAGE_ASSET) | ||
|
||
def test_recipient_as_org_member_is_owner(self): | ||
# Make anotheruser's organization a MMO… | ||
organization = self.anotheruser.organization | ||
organization.mmo_override = True | ||
organization.save() | ||
# … and add thirduser to it | ||
organization.add_user(self.thirduser) | ||
assert self.asset.owner == self.someuser | ||
# send the invite to thirduser | ||
with immediate_on_commit(): | ||
create_invite( | ||
sender=self.someuser, | ||
recipient=self.thirduser, | ||
assets=[self.asset], | ||
invite_class_name='Invite', | ||
) | ||
|
||
self.asset.refresh_from_db() | ||
# anotheruser should be the owner now (because they are the owner of | ||
# thirduser's organization | ||
assert self.asset.owner == self.anotheruser | ||
|
||
# The previous owner should have received "manage_asset" permission | ||
assert self.asset.has_perm(self.someuser, PERM_MANAGE_ASSET) | ||
# The invite recipient should have received "manage_asset" permission too | ||
assert self.asset.has_perm(self.thirduser, PERM_MANAGE_ASSET) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters