Skip to content

Commit ec5d159

Browse files
committed
Added a new API endpoint to be used by the registration system, to trigger account creation.
- Legacy-Id: 17941
1 parent 2416a46 commit ec5d159

File tree

6 files changed

+135
-18
lines changed

6 files changed

+135
-18
lines changed

ietf/api/tests.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from ietf.meeting.test_data import make_meeting_test_data
2424
from ietf.person.factories import PersonFactory
2525
from ietf.person.models import PersonalApiKey
26+
from ietf.utils.mail import outbox, get_payload_text
2627
from ietf.utils.test_utils import TestCase, login_testing_unauthorized
2728

2829
OMITTED_APPS = (
@@ -31,7 +32,7 @@
3132
'ietf.ipr',
3233
)
3334

34-
class CustomApiTestCase(TestCase):
35+
class CustomApiTests(TestCase):
3536

3637
# Using mock to patch the import functions in ietf.meeting.views, where
3738
# api_import_recordings() are using them:
@@ -197,6 +198,48 @@ def test_api_v2_person_access_meetecho(self):
197198
self.assertEqual(data['name'], person.name)
198199
self.assertEqual(data['email'], person.email().address)
199200

201+
def test_api_new_meeting_registration(self):
202+
meeting = MeetingFactory(type_id='ietf')
203+
reg = {
204+
'apikey': 'invalid',
205+
'affiliation': "Alguma Corporação",
206+
'country_code': 'PT',
207+
'email': 'foo@example.pt',
208+
'first_name': 'Foo',
209+
'last_name': 'Bar',
210+
'meeting': meeting.number,
211+
'reg_type': 'hackathon',
212+
'ticket_type': 'regular',
213+
}
214+
url = urlreverse('ietf.api.views.api_new_meeting_registration')
215+
r = self.client.post(url, reg)
216+
self.assertContains(r, 'Invalid apikey', status_code=400)
217+
person = PersonFactory(user__is_staff=True)
218+
# Make sure 'person' has an acceptable role
219+
RoleFactory(name_id='robot', person=person, email=person.email(), group__acronym='secretariat')
220+
key = PersonalApiKey.objects.create(person=person, endpoint=url)
221+
reg['apikey'] = key.hash()
222+
#
223+
# Test valid POST
224+
r = self.client.post(url, reg)
225+
self.assertContains(r, "Accepted, New registration, Email sent", status_code=202)
226+
#
227+
# Check outgoing mail
228+
self.assertEqual(len(outbox), 1)
229+
body = get_payload_text(outbox[-1])
230+
self.assertIn(reg['email'], outbox[-1]['To'] )
231+
self.assertIn(reg['email'], body)
232+
self.assertIn('account creation request', body)
233+
#
234+
# Test incomplete POST
235+
drop_fields = ['affiliation', 'first_name', 'reg_type']
236+
for field in drop_fields:
237+
del reg[field]
238+
r = self.client.post(url, reg)
239+
self.assertContains(r, 'Missing parameters:', status_code=400)
240+
err, fields = r.content.decode().split(':', 1)
241+
missing_fields = [ f.strip() for f in fields.split(',') ]
242+
self.assertEqual(set(missing_fields), set(drop_fields))
200243

201244
class TastypieApiTestCase(ResourceTestCaseMixin, TestCase):
202245
def __init__(self, *args, **kwargs):

ietf/api/urls.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,30 @@
1212
api.autodiscover()
1313

1414
urlpatterns = [
15-
# Top endpoint for Tastypie's REST API (this isn't standard Tastypie):
15+
# General API help page
1616
url(r'^$', api_views.api_help),
17+
# Top endpoint for Tastypie's REST API (this isn't standard Tastypie):
1718
url(r'^v1/?$', api_views.top_level),
18-
# Custom API endpoints
19-
url(r'^notify/meeting/import_recordings/(?P<number>[a-z0-9-]+)/?$', meeting_views.api_import_recordings),
20-
url(r'^meeting/session/video/url$', meeting_views.api_set_session_video_url),
21-
url(r'^submit/?$', submit_views.api_submit),
22-
url(r'^iesg/position', views_ballot.api_set_position),
23-
# GPRD: export of personal information for the logged-in person
24-
url(r'^export/personal-information/$', api_views.PersonalInformationExportView.as_view()),
2519
# For mailarchive use, requires secretariat role
2620
url(r'^v2/person/person', api_views.ApiV2PersonExportView.as_view()),
27-
# For meetecho access
28-
url(r'^person/access/meetecho', api_views.person_access_meetecho),
21+
#
22+
# --- Custom API endpoints, sorted alphabetically ---
23+
# GPRD: export of personal information for the logged-in person
24+
url(r'^export/personal-information/$', api_views.PersonalInformationExportView.as_view()),
25+
# Let IESG members set positions programmatically
26+
url(r'^iesg/position', views_ballot.api_set_position),
27+
# Let Meetecho set session video URLs
28+
url(r'^meeting/session/video/url$', meeting_views.api_set_session_video_url),
29+
# Let Meetecho trigger recording imports
30+
url(r'^notify/meeting/import_recordings/(?P<number>[a-z0-9-]+)/?$', meeting_views.api_import_recordings),
31+
# Let the registration system notify us about registrations
32+
url(r'^notify/meeting/registration', api_views.api_new_meeting_registration),
2933
# OpenID authentication provider
3034
url(r'^openid/', include('oidc_provider.urls', namespace='oidc_provider')),
35+
# For meetecho access
36+
url(r'^person/access/meetecho', api_views.person_access_meetecho),
37+
# Draft submission API
38+
url(r'^submit/?$', submit_views.api_submit),
3139
]
3240

3341
# Additional (standard) Tastypie endpoints

ietf/api/views.py

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
from django.conf import settings
1010
from django.contrib.auth.decorators import login_required
11+
from django.contrib.auth.models import User
12+
from django.core.exceptions import ValidationError
13+
from django.core.validators import validate_email
1114
from django.http import HttpResponse
1215
from django.shortcuts import render, get_object_or_404
1316
from django.urls import reverse
@@ -23,11 +26,14 @@
2326

2427
import debug # pyflakes:ignore
2528

26-
from ietf.person.models import Person
29+
from ietf.person.models import Person, Email
2730
from ietf.api import _api_list
2831
from ietf.api.serializer import JsonExportMixin
29-
from ietf.utils.decorators import require_api_key
32+
from ietf.ietfauth.views import send_account_creation_email
3033
from ietf.ietfauth.utils import role_required
34+
from ietf.meeting.models import Meeting
35+
from ietf.stats.models import MeetingRegistration
36+
from ietf.utils.decorators import require_api_key
3137

3238

3339
def top_level(request):
@@ -108,3 +114,61 @@ def person_access_meetecho(request):
108114
'secr': list(person.role_set.filter(name='secr', group__state__in=['active', 'bof', 'proposed']).values_list('group__acronym', flat=True)),
109115
}
110116
}), content_type='application/json')
117+
118+
@require_api_key
119+
@role_required('Robot')
120+
@csrf_exempt
121+
def api_new_meeting_registration(request):
122+
'''REST API to notify the datatracker about a new meeting registration'''
123+
def err(code, text):
124+
return HttpResponse(text, status=code, content_type='text/plain')
125+
required_fields = [ 'meeting', 'first_name', 'last_name', 'affiliation', 'country_code', 'email', 'reg_type', 'ticket_type', ]
126+
fields = required_fields + []
127+
if request.method == 'POST':
128+
# parameters:
129+
# apikey:
130+
# meeting
131+
# name
132+
# email
133+
# reg_type (In Person, Remote, Hackathon Only)
134+
# ticket_type (full_week, one_day, student)
135+
#
136+
data = {}
137+
missing_fields = []
138+
for item in fields:
139+
value = request.POST.get(item, None)
140+
if value is None and item in required_fields:
141+
missing_fields.append(item)
142+
data[item] = value
143+
if missing_fields:
144+
return err(400, "Missing parameters: %s" % ', '.join(missing_fields))
145+
number = data['meeting']
146+
try:
147+
meeting = Meeting.objects.get(number=number)
148+
except Meeting.DoesNotExist:
149+
return err(400, "Invalid meeting value: '%s'" % (number, ))
150+
email = data['email']
151+
try:
152+
validate_email(email)
153+
except ValidationError:
154+
return err(400, "Invalid email value: '%s'" % (email, ))
155+
if request.POST.get('cancelled', 'false') == 'true':
156+
MeetingRegistration.objects.filter(meeting_id=meeting.pk, email=email).delete()
157+
return HttpResponse('OK', status=200, content_type='text/plain')
158+
else:
159+
object, created = MeetingRegistration.objects.get_or_create(meeting_id=meeting.pk, email=email)
160+
try:
161+
MeetingRegistration.objects.filter(id=object.id).update(**data)
162+
object.save()
163+
except ValueError as e:
164+
return err(400, "Unexpected POST data: %s" % e)
165+
response = "Accepted, New registration" if created else "Accepted, Updated registration"
166+
if User.objects.filter(username=email).exists() or Email.objects.filter(address=email).exists():
167+
pass
168+
else:
169+
send_account_creation_email(request, email)
170+
response += ", Email sent"
171+
return HttpResponse(response, status=202, content_type='text/plain')
172+
else:
173+
return HttpResponse(status=405)
174+

ietf/ietfauth/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def has_role(user, role_names, *args, **kwargs):
8585
"Reviewer": Q(person=person, name="reviewer", group__state="active"),
8686
"Review Team Secretary": Q(person=person, name="secr", group__reviewteamsettings__isnull=False,group__state="active", ),
8787
"IRSG Member": (Q(person=person, name="member", group__acronym="irsg") | Q(person=person, name="chair", group__acronym="irtf") | Q(person=person, name="atlarge", group__acronym="irsg")),
88-
88+
"Robot": Q(person=person, name="robot", group__acronym="secretariat"),
8989
}
9090

9191
filter_expr = Q()

ietf/person/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ def salt():
337337
("/api/v2/person/person", "/api/v2/person/person", "Secretariat"),
338338
("/api/meeting/session/video/url", "/api/meeting/session/video/url", "Recording Manager"),
339339
("/api/person/access/meetecho", "/api/person/access/meetecho", None),
340+
("/api/notify/meeting/registration", "/api/notify/meeting/registration", "Robot"),
340341
]
341342
PERSON_API_KEY_ENDPOINTS = [ (v, n) for (v, n, r) in PERSON_API_KEY_VALUES ]
342343

ietf/stats/utils.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -249,12 +249,13 @@ def get_meeting_registration_data(meeting):
249249
address = registration['Email'].strip()
250250
object, created = MeetingRegistration.objects.get_or_create(
251251
meeting_id=meeting.pk,
252-
first_name=first_name[:200],
253-
last_name=last_name[:200],
254-
affiliation=affiliation,
255-
country_code=country_code,
256252
email=address,
257253
)
254+
object.first_name=first_name[:200]
255+
object.last_name=last_name[:200]
256+
object.affiliation=affiliation
257+
object.country_code=country_code
258+
object.save()
258259

259260
# Add a Person object to MeetingRegistration object
260261
# if valid email is available

0 commit comments

Comments
 (0)