Skip to content

Commit ce4adfe

Browse files
authored
Merge pull request #7438 from readthedocs/move-organizations-forms
Organizations: move forms
2 parents 52cd040 + ca7b579 commit ce4adfe

File tree

4 files changed

+485
-0
lines changed

4 files changed

+485
-0
lines changed

readthedocs/organizations/forms.py

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
"""Organization forms."""
2+
3+
from django import forms
4+
from django.contrib.auth.models import User
5+
from django.core.exceptions import NON_FIELD_ERRORS, ValidationError
6+
from django.core.validators import EmailValidator
7+
from django.db.models import Q
8+
from django.utils.translation import ugettext_lazy as _
9+
10+
from readthedocs.core.utils import slugify
11+
from readthedocs.core.utils.extend import SettingsOverrideObject
12+
from readthedocs.organizations.constants import ADMIN_ACCESS, READ_ONLY_ACCESS
13+
from readthedocs.organizations.models import (
14+
Organization,
15+
OrganizationOwner,
16+
Team,
17+
TeamInvite,
18+
TeamMember,
19+
)
20+
21+
22+
class OrganizationForm(forms.ModelForm):
23+
24+
"""
25+
Base organization form.
26+
27+
:param user: User instance, responsible for ownership of Organization
28+
:type user: django.contrib.auth.models.User
29+
"""
30+
31+
class Meta:
32+
model = Organization
33+
fields = ['name', 'email', 'description', 'url']
34+
labels = {
35+
'name': _('Organization Name'),
36+
'email': _('Billing Email'),
37+
}
38+
39+
# Don't use a URLField as a widget, the validation is too strict on FF
40+
url = forms.URLField(
41+
widget=forms.TextInput(attrs={'placeholder': 'http://'}),
42+
label=_('Site URL'),
43+
required=False,
44+
)
45+
46+
def __init__(self, *args, **kwargs):
47+
try:
48+
self.user = kwargs.pop('user')
49+
except KeyError:
50+
raise TypeError(
51+
'OrganizationForm expects a `user` keyword argument',
52+
)
53+
super().__init__(*args, **kwargs)
54+
55+
def clean_name(self):
56+
"""Raise exception on duplicate organization."""
57+
name = self.cleaned_data['name']
58+
if self.instance and self.instance.name and name == self.instance.name:
59+
return name
60+
if Organization.objects.filter(slug=slugify(name)).exists():
61+
raise forms.ValidationError(
62+
_('Organization %(name)s already exists'),
63+
params={'name': name},
64+
)
65+
return name
66+
67+
68+
class OrganizationSignupFormBase(OrganizationForm):
69+
70+
"""
71+
Simple organization creation form.
72+
73+
This trims down the number of inputs required to create a new organization.
74+
This is used on the initial organization signup, to keep signup terse.
75+
76+
:param user: User instance, responsible for ownership of Organization
77+
:type user: django.contrib.auth.models.User
78+
"""
79+
80+
class Meta:
81+
model = Organization
82+
fields = ['name', 'email']
83+
labels = {
84+
'name': _('Organization Name'),
85+
'email': _('Billing Email'),
86+
}
87+
88+
url = None
89+
90+
@staticmethod
91+
def _create_default_teams(organization):
92+
organization.teams.create(name='Admins', access=ADMIN_ACCESS)
93+
organization.teams.create(name='Read Only', access=READ_ONLY_ACCESS)
94+
95+
def save(self, commit=True):
96+
org = super().save(commit)
97+
98+
# If not commiting, we can't save M2M fields
99+
if not commit:
100+
return org
101+
102+
# Add default teams
103+
OrganizationOwner.objects.create(
104+
owner=self.user,
105+
organization=org,
106+
)
107+
self._create_default_teams(org)
108+
return org
109+
110+
111+
class OrganizationSignupForm(SettingsOverrideObject):
112+
113+
_default_class = OrganizationSignupFormBase
114+
115+
116+
class OrganizationOwnerForm(forms.ModelForm):
117+
118+
"""Form to manage owners of the organization."""
119+
120+
class Meta:
121+
model = OrganizationOwner
122+
fields = ['owner']
123+
124+
owner = forms.CharField()
125+
126+
def __init__(self, *args, **kwargs):
127+
self.organization = kwargs.pop('organization', None)
128+
super().__init__(*args, **kwargs)
129+
130+
def clean_owner(self):
131+
"""Lookup owner by username, detect collisions with existing owners."""
132+
username = self.cleaned_data['owner']
133+
owner = User.objects.filter(username=username).first()
134+
if owner is None:
135+
raise forms.ValidationError(
136+
_('User %(username)s does not exist'),
137+
params={'username': username},
138+
)
139+
if self.organization.owners.filter(username=username).exists():
140+
raise forms.ValidationError(
141+
_('User %(username)s is already an owner'),
142+
params={'username': username},
143+
)
144+
return owner
145+
146+
147+
class OrganizationTeamBasicForm(forms.ModelForm):
148+
149+
"""Form to manage teams."""
150+
151+
class Meta:
152+
model = Team
153+
fields = ['name', 'access', 'organization']
154+
error_messages = {
155+
NON_FIELD_ERRORS: {
156+
'unique_together': _('Team already exists'),
157+
},
158+
}
159+
160+
organization = forms.CharField(widget=forms.HiddenInput(), required=False)
161+
162+
def __init__(self, *args, **kwargs):
163+
self.organization = kwargs.pop('organization', None)
164+
super().__init__(*args, **kwargs)
165+
166+
def clean_organization(self):
167+
"""Hard code organization return on form."""
168+
return self.organization
169+
170+
171+
class OrganizationTeamProjectForm(forms.ModelForm):
172+
173+
"""Form to manage access of teams to projects."""
174+
175+
class Meta:
176+
model = Team
177+
fields = ['projects']
178+
179+
def __init__(self, *args, **kwargs):
180+
self.organization = kwargs.pop('organization', None)
181+
super().__init__(*args, **kwargs)
182+
self.fields['projects'] = forms.ModelMultipleChoiceField(
183+
queryset=self.organization.projects,
184+
widget=forms.CheckboxSelectMultiple,
185+
)
186+
187+
188+
class OrganizationTeamMemberForm(forms.ModelForm):
189+
190+
"""Form to manage all members of the organization."""
191+
192+
class Meta:
193+
model = TeamMember
194+
fields = []
195+
196+
member = forms.CharField(label=_('Email address or username'))
197+
198+
def __init__(self, *args, **kwargs):
199+
self.team = kwargs.pop('team', None)
200+
super().__init__(*args, **kwargs)
201+
202+
def clean_member(self):
203+
"""
204+
Get member or invite from field.
205+
206+
If input is email, try to look up a user using that email address, if
207+
that doesn't return a user, then consider this a fresh invite. If the
208+
input is not an email, treat it as a user name and lookup a user. Throw
209+
a validation error if the username doesn't not exist.
210+
211+
Return a User instance, or a TeamInvite instance, depending on the above
212+
conditions.
213+
"""
214+
lookup = self.cleaned_data['member']
215+
216+
# Look up user emails first, see if a verified user can be added
217+
try:
218+
validator = EmailValidator(code='lookup not an email')
219+
validator(lookup)
220+
221+
member = (
222+
User.objects.filter(
223+
emailaddress__verified=True,
224+
emailaddress__email=lookup,
225+
is_active=True,
226+
).first()
227+
)
228+
if member is not None:
229+
return self.validate_member_user(member)
230+
231+
invite = TeamInvite(
232+
organization=self.team.organization,
233+
team=self.team,
234+
email=lookup,
235+
)
236+
237+
return self.validate_member_invite(invite)
238+
except ValidationError as error:
239+
if error.code != 'lookup not an email':
240+
raise
241+
242+
# Not an email, attempt username lookup
243+
try:
244+
member = User.objects.get(username=lookup, is_active=True)
245+
return self.validate_member_user(member)
246+
except User.DoesNotExist:
247+
raise forms.ValidationError('User not found')
248+
249+
def validate_member_user(self, member):
250+
"""Verify duplicate team member doesn't already exists."""
251+
if TeamMember.objects.filter(team=self.team, member=member).exists():
252+
raise forms.ValidationError(_('User is already a team member'),)
253+
return member
254+
255+
def validate_member_invite(self, invite):
256+
"""
257+
Verify team member and team invite don't already exist.
258+
259+
Query searches for duplicate :py:cls:`TeamMember` instances, and also
260+
for existing :py:cls:`TeamInvite` instances, sharing the team and email
261+
address of the given ``invite``
262+
263+
:param invite: :py:cls:`TeamInvite` instance
264+
"""
265+
queryset = TeamMember.objects.filter(
266+
Q(
267+
team=self.team,
268+
invite__team=self.team,
269+
invite__email=invite.email,
270+
),
271+
)
272+
if queryset.exists():
273+
raise forms.ValidationError(
274+
_('An invitation was already sent to this email'),
275+
)
276+
return invite
277+
278+
def clean(self):
279+
"""
280+
Treat an invite email as an invite in the member field.
281+
282+
This will drop an invite object as the invite field if the member field
283+
returned a TeamInvite instance
284+
"""
285+
data = super().clean()
286+
287+
if not self.is_valid():
288+
return data
289+
290+
self.instance.team = self.team
291+
member = data['member']
292+
293+
if isinstance(member, User):
294+
self.instance.invite = None
295+
self.instance.member = member
296+
elif isinstance(member, TeamInvite):
297+
member.save()
298+
self.instance.member = None
299+
self.instance.invite = member
300+
301+
self._validate_unique = True
302+
return data

readthedocs/organizations/tests/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)