Skip to content

Commit 6162eb7

Browse files
authored
Organizations: allow to add owners by email (#8651)
Similar when adding a team member. We don't create an invitation if the email doesn't exist.
1 parent 000f5e8 commit 6162eb7

File tree

2 files changed

+72
-29
lines changed

2 files changed

+72
-29
lines changed

readthedocs/organizations/forms.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,22 +131,28 @@ class Meta:
131131
model = OrganizationOwner
132132
fields = ['owner']
133133

134-
owner = forms.CharField()
134+
owner = forms.CharField(label=_('Email address or username'))
135135

136136
def __init__(self, *args, **kwargs):
137137
self.organization = kwargs.pop('organization', None)
138138
super().__init__(*args, **kwargs)
139139

140140
def clean_owner(self):
141-
"""Lookup owner by username, detect collisions with existing owners."""
141+
"""Lookup owner by username or email, detect collisions with existing owners."""
142142
username = self.cleaned_data['owner']
143-
owner = User.objects.filter(username=username).first()
143+
owner = (
144+
User.objects.filter(
145+
Q(username=username) |
146+
Q(emailaddress__verified=True, emailaddress__email=username)
147+
)
148+
.first()
149+
)
144150
if owner is None:
145151
raise forms.ValidationError(
146152
_('User %(username)s does not exist'),
147153
params={'username': username},
148154
)
149-
if self.organization.owners.filter(username=username).exists():
155+
if self.organization.owners.filter(pk=owner.pk).exists():
150156
raise forms.ValidationError(
151157
_('User %(username)s is already an owner'),
152158
params={'username': username},

readthedocs/organizations/tests/test_views.py

Lines changed: 62 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
1-
import django_dynamic_fixture as fixture
21
from allauth.account.views import SignupView
32
from django.contrib.auth.models import User
43
from django.core.cache import cache
54
from django.test import TestCase
65
from django.test.utils import override_settings
76
from django.urls import reverse
7+
from django_dynamic_fixture import get
88

99
from readthedocs.organizations.models import (
1010
Organization,
1111
Team,
1212
TeamInvite,
1313
TeamMember,
1414
)
15-
from readthedocs.organizations.views import private as private_views
1615
from readthedocs.organizations.views import public as public_views
1716
from readthedocs.projects.models import Project
1817
from readthedocs.rtd_tests.base import RequestFactoryTestMixin
@@ -24,24 +23,23 @@ class OrganizationViewTests(RequestFactoryTestMixin, TestCase):
2423
"""Organization views tests."""
2524

2625
def setUp(self):
27-
self.owner = fixture.get(User)
28-
self.project = fixture.get(Project)
29-
self.organization = fixture.get(
26+
self.owner = get(User, username='owner', email='[email protected]')
27+
self.owner.emailaddress_set.create(email=self.owner.email, verified=True)
28+
self.project = get(Project)
29+
self.organization = get(
3030
Organization,
3131
owners=[self.owner],
3232
projects=[self.project],
3333
)
34-
self.team = fixture.get(Team, organization=self.organization)
34+
self.team = get(Team, organization=self.organization)
35+
self.client.force_login(self.owner)
3536

3637
def test_delete(self):
3738
"""Delete organization on post."""
38-
req = self.request(
39-
'post',
40-
'/organizations/{}/delete/'.format(self.organization.slug),
41-
user=self.owner,
39+
resp = self.client.post(
40+
reverse('organization_delete', args=[self.organization.slug])
4241
)
43-
view = private_views.DeleteOrganization.as_view()
44-
resp = view(req, slug=self.organization.slug)
42+
self.assertEqual(resp.status_code, 302)
4543

4644
self.assertFalse(Organization.objects
4745
.filter(pk=self.organization.pk)
@@ -53,30 +51,69 @@ def test_delete(self):
5351
.filter(pk=self.project.pk)
5452
.exists())
5553

54+
def test_add_owner(self):
55+
url = reverse('organization_owner_add', args=[self.organization])
56+
user = get(User, username='test-user', email='[email protected]')
57+
user.emailaddress_set.create(email=user.email, verified=False)
58+
59+
user_b = get(User, username='test-user-b', email='[email protected]')
60+
user_b.emailaddress_set.create(email=user_b.email, verified=True)
61+
62+
# Adding an already owner.
63+
for username in [self.owner.username, self.owner.email]:
64+
resp = self.client.post(url, data={'owner': username})
65+
form = resp.context_data['form']
66+
self.assertFalse(form.is_valid())
67+
self.assertIn('is already an owner', form.errors['owner'][0])
68+
69+
# Unknown user.
70+
resp = self.client.post(url, data={'owner': 'non-existent'})
71+
form = resp.context_data['form']
72+
self.assertFalse(form.is_valid())
73+
self.assertIn('does not exist', form.errors['owner'][0])
74+
75+
# From an unverified email.
76+
resp = self.client.post(url, data={'owner': user.email})
77+
form = resp.context_data['form']
78+
self.assertFalse(form.is_valid())
79+
self.assertIn('does not exist', form.errors['owner'][0])
80+
81+
# Using a username.
82+
self.assertFalse(user in self.organization.owners.all())
83+
resp = self.client.post(url, data={'owner': user.username})
84+
self.assertEqual(resp.status_code, 302)
85+
self.assertTrue(user in self.organization.owners.all())
86+
87+
# Using an email.
88+
self.assertFalse(user_b in self.organization.owners.all())
89+
resp = self.client.post(url, data={'owner': user_b.email})
90+
self.assertEqual(resp.status_code, 302)
91+
self.assertTrue(user_b in self.organization.owners.all())
92+
5693

5794
@override_settings(RTD_ALLOW_ORGANIZATIONS=True)
5895
class OrganizationInviteViewTests(RequestFactoryTestMixin, TestCase):
5996

6097
"""Tests for invite handling in views."""
6198

6299
def setUp(self):
63-
self.owner = fixture.get(User)
64-
self.organization = fixture.get(
100+
self.owner = get(User)
101+
self.organization = get(
65102
Organization,
66103
owners=[self.owner],
67104
)
68-
self.team = fixture.get(Team, organization=self.organization)
105+
self.team = get(Team, organization=self.organization)
69106

70107
def tearDown(self):
71108
cache.clear()
72109

73110
def test_redemption_by_authed_user(self):
74-
user = fixture.get(User)
75-
invite = fixture.get(
111+
user = get(User)
112+
invite = get(
76113
TeamInvite, email=user.email, team=self.team,
77114
organization=self.organization,
78115
)
79-
team_member = fixture.get(
116+
team_member = get(
80117
TeamMember,
81118
invite=invite,
82119
member=None,
@@ -102,11 +139,11 @@ def test_redemption_by_unauthed_user(self):
102139
103140
with self.assertRaises(User.DoesNotExist):
104141
User.objects.get(email=email)
105-
invite = fixture.get(
142+
invite = get(
106143
TeamInvite, email=email, team=self.team,
107144
organization=self.organization,
108145
)
109-
team_member = fixture.get(
146+
team_member = get(
110147
TeamMember,
111148
invite=invite,
112149
member=None,
@@ -163,18 +200,18 @@ def test_redemption_by_unauthed_user(self):
163200
)
164201

165202
def test_redemption_by_dulpicate_user(self):
166-
user = fixture.get(User)
167-
invite = fixture.get(
203+
user = get(User)
204+
invite = get(
168205
TeamInvite, email=user.email, team=self.team,
169206
organization=self.organization,
170207
)
171-
team_member_a = fixture.get(
208+
team_member_a = get(
172209
TeamMember,
173210
invite=None,
174211
member=user,
175212
team=self.team,
176213
)
177-
team_member_b = fixture.get(
214+
team_member_b = get(
178215
TeamMember,
179216
invite=invite,
180217
member=None,
@@ -204,7 +241,7 @@ def tearDown(self):
204241

205242
def test_organization_signup(self):
206243
self.assertEqual(Organization.objects.count(), 0)
207-
user = fixture.get(User)
244+
user = get(User)
208245
self.client.force_login(user)
209246
data = {
210247
'name': 'Testing Organization',

0 commit comments

Comments
 (0)