Skip to content

Commit 92d914b

Browse files
Fix form permissions not updated when project is transferred (#2775)
* fix forms not transferred when project is transferred * refactor code * enhance test * enhance test * fix failing tests * transfer project to individual users using management command users are only disallowed to transfer project to individual users when using the RESTFUL API * revert file changes revert changes made to onadata/apps/logger/tests/test_transfer_project_command.py * add test * update comment * update test * update test * address failing tests
1 parent 197fef7 commit 92d914b

File tree

5 files changed

+154
-81
lines changed

5 files changed

+154
-81
lines changed

onadata/apps/api/tests/viewsets/test_project_viewset.py

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1296,11 +1296,16 @@ def test_project_transfer_upgrades_permissions(self):
12961296
self.assertEqual(self.project.created_by, alice_profile.user)
12971297
alice_project = self.project
12981298

1299-
# create org owned by bob then make alice admin
1299+
# Publish a form to Alice's project
1300+
self._publish_xls_form_to_project()
1301+
alice_xform = self.xform
1302+
1303+
# Create organization owned by Bob
13001304
self._login_user_and_profile({"username": bob.username, "email": bob.email})
13011305
self._org_create()
13021306
self.assertEqual(self.organization.created_by, bob)
1303-
org_url = f"http://testserver/api/v1/users/{self.organization.user.username}"
1307+
1308+
# Add Alice as admin to Bob's organization
13041309
view = OrganizationProfileViewSet.as_view({"post": "members"})
13051310
data = {"username": alice_profile.user.username, "role": OwnerRole.name}
13061311
request = self.factory.post(
@@ -1312,6 +1317,20 @@ def test_project_transfer_upgrades_permissions(self):
13121317
owners_team = get_or_create_organization_owners_team(self.organization)
13131318
self.assertIn(alice_profile.user, owners_team.user_set.all())
13141319

1320+
# Add Jane to Bob's organization with dataentry role
1321+
jane_data = {"username": "jane", "email": "[email protected]"}
1322+
jane_profile = self._create_user_profile(jane_data)
1323+
data = {"username": jane_profile.user.username, "role": DataEntryRole.name}
1324+
request = self.factory.post("/", data=data, **self.extra)
1325+
request = self.factory.post(
1326+
"/", data=json.dumps(data), content_type="application/json", **self.extra
1327+
)
1328+
response = view(request, user=self.organization.user.username)
1329+
self.assertEqual(response.status_code, 201)
1330+
self.assertTrue(
1331+
DataEntryRole.user_has_role(jane_profile.user, self.organization)
1332+
)
1333+
13151334
# Share project to bob as editor
13161335
data = {"username": bob.username, "role": EditorRole.name}
13171336
view = ProjectViewSet.as_view({"post": "share"})
@@ -1322,28 +1341,24 @@ def test_project_transfer_upgrades_permissions(self):
13221341
self.assertEqual(response.status_code, 204)
13231342

13241343
# Transfer project to Bobs Organization
1344+
org_url = f"http://testserver/api/v1/users/{self.organization.user.username}"
13251345
data = {"owner": org_url, "name": alice_project.name}
13261346
view = ProjectViewSet.as_view({"patch": "partial_update"})
13271347
request = self.factory.patch("/", data=data, **auth_credentials)
13281348
response = view(request, pk=alice_project.pk)
13291349
self.assertEqual(response.status_code, 200)
13301350

1331-
# Ensure all Admins have admin privileges to the project
1332-
# once transferred
1333-
view = ProjectViewSet.as_view({"get": "retrieve"})
1334-
request = self.factory.get("/", **self.extra)
1335-
response = view(request, pk=alice_project.pk)
1336-
self.assertEqual(response.status_code, 200)
1337-
project_users = response.data["users"]
1338-
1339-
org_owners = get_or_create_organization_owners_team(
1340-
self.organization
1341-
).user_set.all()
1342-
1343-
for user in project_users:
1344-
owner = org_owners.filter(username=user["user"]).first()
1345-
if owner:
1346-
self.assertEqual(user["role"], OwnerRole.name)
1351+
# Admins have owner privileges to the transferred project
1352+
# and forms
1353+
self.assertTrue(OwnerRole.user_has_role(bob, alice_project))
1354+
self.assertTrue(OwnerRole.user_has_role(bob, alice_xform))
1355+
self.assertTrue(OwnerRole.user_has_role(alice_profile.user, alice_project))
1356+
self.assertTrue(OwnerRole.user_has_role(alice_profile.user, alice_xform))
1357+
1358+
# Non-admins have readonly privileges to the transferred project
1359+
# and forms
1360+
self.assertTrue(ReadOnlyRole.user_has_role(jane_profile.user, alice_project))
1361+
self.assertTrue(ReadOnlyRole.user_has_role(jane_profile.user, alice_xform))
13471362

13481363
# pylint: disable=invalid-name
13491364
@override_settings(ALLOW_PUBLIC_DATASETS=False)

onadata/apps/logger/management/commands/transferproject.py

Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55
from django.core.management.base import BaseCommand
66
from django.db import transaction
77

8-
from onadata.apps.logger.models import DataView, MergedXForm, Project, XForm
8+
from onadata.apps.api.models import Team
9+
from onadata.apps.api.models.organization_profile import get_organization_members_team
10+
from onadata.apps.logger.models import MergedXForm, Project, XForm
911
from onadata.apps.logger.models.project import (
1012
set_object_permissions as set_project_permissions,
1113
)
14+
from onadata.libs.permissions import ReadOnlyRole
1215
from onadata.libs.utils.project_utils import set_project_perms_to_xform
1316

1417

@@ -64,55 +67,72 @@ def get_user(self, username):
6467
self.errors.append(f"User {username} does not exist")
6568
return user
6669

67-
def update_xform_with_new_user(self, project, user):
68-
"""
69-
Update XForm user update the DataViews and also set the permissions
70-
for the xForm and the project.
71-
"""
70+
def _transfer_xform(self, project, to_user):
71+
"""Transfer XForm to the new owner."""
7272
xforms = XForm.objects.filter(
7373
project=project, deleted_at__isnull=True, downloadable=True
7474
)
7575
for form in xforms:
76-
form.user = user
77-
form.created_by = user
76+
form.user = to_user
77+
form.created_by = to_user
7878
form.save()
79-
self.update_data_views(form)
8079
set_project_perms_to_xform(form, project)
8180

82-
@staticmethod
83-
def update_data_views(form):
84-
"""Update DataView project for the XForm given."""
85-
dataviews = DataView.objects.filter(
86-
xform=form, project=form.project, deleted_at__isnull=True
87-
)
88-
for data_view in dataviews:
89-
data_view.project = form.project
90-
data_view.save()
91-
92-
@staticmethod
93-
def update_merged_xform(project, user):
94-
"""Update ownership of MergedXforms."""
81+
def _transfer_merged_xform(self, project, to_user):
82+
"""Transfer MergedXForm to the new owner."""
9583
merged_xforms = MergedXForm.objects.filter(
9684
project=project, deleted_at__isnull=True
9785
)
9886
for form in merged_xforms:
99-
form.user = user
100-
form.created_by = user
87+
form.user = to_user
88+
form.created_by = to_user
10189
form.save()
10290
set_project_perms_to_xform(form, project)
10391

92+
def transfer_project(self, project, to_user):
93+
"""Transfer Project to the new owner."""
94+
project.organization = to_user
95+
project.created_by = to_user
96+
project.save()
97+
98+
set_project_permissions(Project, project, created=True)
99+
100+
if hasattr(to_user, "profile") and hasattr(
101+
to_user.profile, "organizationprofile"
102+
):
103+
# Give readonly permission to members
104+
organization = to_user.profile.organizationprofile
105+
owners_team = Team.objects.get(
106+
name=f"{to_user.username}#{Team.OWNER_TEAM_NAME}", organization=to_user
107+
)
108+
members_team = get_organization_members_team(organization)
109+
110+
ReadOnlyRole.add(members_team, project)
111+
112+
# Owners are also members so we exclude them
113+
members = members_team.user_set.exclude(
114+
username__in=[user.username for user in owners_team.user_set.all()]
115+
)
116+
117+
for member in members:
118+
ReadOnlyRole.add(member, project)
119+
120+
self._transfer_xform(project, to_user)
121+
self._transfer_merged_xform(project, to_user)
122+
104123
@transaction.atomic()
105124
def handle(self, *args, **options):
106125
"""Transfer projects from one user to another."""
107126
from_user = self.get_user(options["current_owner"])
108127
to_user = self.get_user(options["new_owner"])
109-
project_id = options.get("project_id")
110-
transfer_all_projects = options.get("all_projects")
111128

112129
if self.errors:
113130
self.stdout.write("".join(self.errors))
114131
return
115132

133+
project_id = options.get("project_id")
134+
transfer_all_projects = options.get("all_projects")
135+
116136
# No need to validate project ownership as they filtered
117137
# against current_owner
118138
projects = []
@@ -124,12 +144,6 @@ def handle(self, *args, **options):
124144
projects = Project.objects.filter(id=project_id, organization=from_user)
125145

126146
for project in projects:
127-
project.organization = to_user
128-
project.created_by = to_user
129-
project.save()
130-
131-
set_project_permissions(Project, project, created=True)
132-
self.update_xform_with_new_user(project, to_user)
133-
self.update_merged_xform(project, to_user)
147+
self.transfer_project(project, to_user)
134148

135149
self.stdout.write("Projects transferred successfully")

onadata/apps/logger/tests/test_transfer_project_command.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
"""Tests for project transfer command"""
2+
23
import os
34
import sys
45

56
from django.contrib.auth import get_user_model
67
from django.core.management import call_command
8+
79
from six import StringIO
810

11+
from onadata.apps.api.models import Team
912
from onadata.apps.logger.models import Project, XForm
1013
from onadata.apps.main.tests.test_base import TestBase
14+
from onadata.libs.permissions import OwnerRole, ReadOnlyRole
1115

1216

1317
class TestMoveProjectToAnewOwner(TestBase): # pylint: disable=C0111
@@ -141,6 +145,58 @@ def test_xforms_are_transferred_as_well(self): # pylint: disable=C0103
141145
self.assertEqual(0, bobs_forms.count())
142146
self.assertEqual(1, new_owner_forms.count())
143147

148+
def test_transfer_to_org(self):
149+
"""Transferring to an organization works"""
150+
# Create users
151+
bob = self._create_user("bob", "test_pass")
152+
alice = self._create_user("alice", "test_pass")
153+
jane = self._create_user("jane", "test_pass")
154+
# Create Project owned by Bob and publish form
155+
project = Project.objects.create(
156+
name="Test_project",
157+
organization=bob,
158+
created_by=bob,
159+
)
160+
self.project = project
161+
self._publish_transportation_form()
162+
# Create organization owned by Alice
163+
alice_org = self._create_organization("alice_inc", "Alice Inc", alice)
164+
owners_team, _ = Team.objects.get_or_create(
165+
name=f"{alice_org.user.username}#Owners",
166+
organization=alice_org.user,
167+
)
168+
members_team, _ = Team.objects.get_or_create(
169+
name=f"{alice_org.user.username}#members",
170+
organization=alice_org.user,
171+
)
172+
# Add Bob as admin to Alice's organization
173+
bob.groups.add(owners_team)
174+
bob.groups.add(members_team)
175+
# Add Jane as member to Alice's organization
176+
jane.groups.add(members_team)
177+
# Transfer Bob's project to Alice's organization
178+
call_command(
179+
"transferproject",
180+
current_owner=bob.username,
181+
new_owner=alice_org.user.username,
182+
project_id=project.id,
183+
)
184+
self.xform.refresh_from_db()
185+
self.project.refresh_from_db()
186+
self.assertEqual(self.project.organization, alice_org.user)
187+
self.assertEqual(self.project.organization, alice_org.user)
188+
self.assertEqual(self.xform.user, alice_org.user)
189+
self.assertEqual(self.xform.created_by, alice_org.user)
190+
# Admins have owner privileges
191+
self.assertTrue(OwnerRole.user_has_role(bob, project))
192+
self.assertTrue(OwnerRole.user_has_role(bob, self.xform))
193+
self.assertTrue(OwnerRole.user_has_role(alice, project))
194+
self.assertTrue(OwnerRole.user_has_role(alice, self.xform))
195+
# Non-admins have readonly privileges
196+
self.assertFalse(OwnerRole.user_has_role(jane, project))
197+
self.assertTrue(ReadOnlyRole.user_has_role(jane, project))
198+
self.assertTrue(ReadOnlyRole.user_has_role(jane, self.xform))
199+
144200

145201
class TestUserValidation(TestBase):
146202
"""Created this class to specifically test for the user validation.

onadata/apps/main/tests/test_base.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"""
33
TestBase - a TestCase base class.
44
"""
5+
56
from __future__ import unicode_literals
67

78
import base64
@@ -13,8 +14,6 @@
1314
from io import StringIO
1415
from tempfile import NamedTemporaryFile
1516

16-
from pyxform.builder import create_survey_element_from_dict
17-
1817
from django.conf import settings
1918
from django.contrib.auth import authenticate, get_user_model
2019
from django.core.files.uploadedfile import InMemoryUploadedFile
@@ -24,10 +23,12 @@
2423

2524
from django_digest.test import Client as DigestClient
2625
from django_digest.test import DigestAuth
26+
from pyxform.builder import create_survey_element_from_dict
2727
from rest_framework.test import APIRequestFactory
2828
from six.moves.urllib.error import URLError
2929
from six.moves.urllib.request import urlopen
3030

31+
from onadata.apps.api.models import OrganizationProfile
3132
from onadata.apps.api.viewsets.xform_viewset import XFormViewSet
3233
from onadata.apps.logger.models import Instance, MergedXForm, XForm, XFormVersion
3334
from onadata.apps.logger.views import submission
@@ -86,6 +87,13 @@ def _create_user(self, username, password, create_profile=False):
8687

8788
return user
8889

90+
def _create_organization(self, username, name, created_by):
91+
user = self._create_user(username, "", False)
92+
organization, _ = OrganizationProfile.objects.get_or_create(
93+
user=user, defaults={"name": name, "creator": created_by}
94+
)
95+
return organization
96+
8997
# pylint: disable=no-self-use
9098
def _login(self, username, password):
9199
client = Client()

onadata/libs/serializers/project_serializer.py

Lines changed: 8 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,18 @@
66
from django.conf import settings
77
from django.contrib.auth import get_user_model
88
from django.core.cache import cache
9+
from django.core.management import call_command
910
from django.db.utils import IntegrityError
1011
from django.utils.translation import gettext as _
1112

1213
from rest_framework import serializers
1314
from six import itervalues
1415

15-
from onadata.apps.api.models.organization_profile import (
16-
OrganizationProfile,
17-
get_or_create_organization_owners_team,
18-
get_organization_members_team,
19-
)
16+
from onadata.apps.api.models.organization_profile import OrganizationProfile
2017
from onadata.apps.logger.models.project import Project
2118
from onadata.apps.logger.models.xform import XForm
2219
from onadata.apps.main.models.user_profile import UserProfile
23-
from onadata.libs.permissions import (
24-
ManagerRole,
25-
OwnerRole,
26-
ReadOnlyRole,
27-
get_role,
28-
is_organization,
29-
)
20+
from onadata.libs.permissions import ManagerRole, OwnerRole, get_role, is_organization
3021
from onadata.libs.serializers.fields.json_field import JsonField
3122
from onadata.libs.serializers.tag_list_serializer import TagListSerializer
3223
from onadata.libs.utils.analytics import TrackObjectEvent
@@ -542,23 +533,12 @@ def update(self, instance, validated_data):
542533
set_owners_permission(owner, instance)
543534

544535
if is_organization(owner.profile):
545-
owners_team = get_or_create_organization_owners_team(owner.profile)
546-
members_team = get_organization_members_team(owner.profile)
547-
OwnerRole.add(owners_team, instance)
548-
ReadOnlyRole.add(members_team, instance)
549-
owners = owners_team.user_set.all()
550-
# Owners are also members
551-
members = members_team.user_set.exclude(
552-
username__in=[user.username for user in owners]
536+
call_command(
537+
"transferproject",
538+
current_owner=instance.organization,
539+
new_owner=owner,
540+
project_id=instance.pk,
553541
)
554-
# Exclude new owner if in members
555-
members = members.exclude(username=owner.username)
556-
557-
# Add permissions to all users in Owners and Members team
558-
for owner in owners:
559-
OwnerRole.add(owner, instance)
560-
for member in members:
561-
ReadOnlyRole.add(member, instance)
562542

563543
# clear cache
564544
safe_delete(f"{PROJ_PERM_CACHE}{instance.pk}")

0 commit comments

Comments
 (0)