From ec5d159b4f3c8bbb95ebac4ec0f0a52ad74102ee Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz Date: Mon, 8 Jun 2020 19:51:10 +0000 Subject: [PATCH] Added a new API endpoint to be used by the registration system, to trigger account creation. - Legacy-Id: 17941 --- ietf/api/tests.py | 45 +++++++++++++++++++++++++++- ietf/api/urls.py | 28 ++++++++++------- ietf/api/views.py | 68 ++++++++++++++++++++++++++++++++++++++++-- ietf/ietfauth/utils.py | 2 +- ietf/person/models.py | 1 + ietf/stats/utils.py | 9 +++--- 6 files changed, 135 insertions(+), 18 deletions(-) diff --git a/ietf/api/tests.py b/ietf/api/tests.py index 6066f6300..bb3a02fff 100644 --- a/ietf/api/tests.py +++ b/ietf/api/tests.py @@ -23,6 +23,7 @@ from ietf.meeting.factories import MeetingFactory, SessionFactory from ietf.meeting.test_data import make_meeting_test_data from ietf.person.factories import PersonFactory from ietf.person.models import PersonalApiKey +from ietf.utils.mail import outbox, get_payload_text from ietf.utils.test_utils import TestCase, login_testing_unauthorized OMITTED_APPS = ( @@ -31,7 +32,7 @@ OMITTED_APPS = ( 'ietf.ipr', ) -class CustomApiTestCase(TestCase): +class CustomApiTests(TestCase): # Using mock to patch the import functions in ietf.meeting.views, where # api_import_recordings() are using them: @@ -197,6 +198,48 @@ class CustomApiTestCase(TestCase): self.assertEqual(data['name'], person.name) self.assertEqual(data['email'], person.email().address) + def test_api_new_meeting_registration(self): + meeting = MeetingFactory(type_id='ietf') + reg = { + 'apikey': 'invalid', + 'affiliation': "Alguma Corporação", + 'country_code': 'PT', + 'email': 'foo@example.pt', + 'first_name': 'Foo', + 'last_name': 'Bar', + 'meeting': meeting.number, + 'reg_type': 'hackathon', + 'ticket_type': 'regular', + } + url = urlreverse('ietf.api.views.api_new_meeting_registration') + r = self.client.post(url, reg) + self.assertContains(r, 'Invalid apikey', status_code=400) + person = PersonFactory(user__is_staff=True) + # Make sure 'person' has an acceptable role + RoleFactory(name_id='robot', person=person, email=person.email(), group__acronym='secretariat') + key = PersonalApiKey.objects.create(person=person, endpoint=url) + reg['apikey'] = key.hash() + # + # Test valid POST + r = self.client.post(url, reg) + self.assertContains(r, "Accepted, New registration, Email sent", status_code=202) + # + # Check outgoing mail + self.assertEqual(len(outbox), 1) + body = get_payload_text(outbox[-1]) + self.assertIn(reg['email'], outbox[-1]['To'] ) + self.assertIn(reg['email'], body) + self.assertIn('account creation request', body) + # + # Test incomplete POST + drop_fields = ['affiliation', 'first_name', 'reg_type'] + for field in drop_fields: + del reg[field] + r = self.client.post(url, reg) + self.assertContains(r, 'Missing parameters:', status_code=400) + err, fields = r.content.decode().split(':', 1) + missing_fields = [ f.strip() for f in fields.split(',') ] + self.assertEqual(set(missing_fields), set(drop_fields)) class TastypieApiTestCase(ResourceTestCaseMixin, TestCase): def __init__(self, *args, **kwargs): diff --git a/ietf/api/urls.py b/ietf/api/urls.py index 367438dfc..62c30bc03 100644 --- a/ietf/api/urls.py +++ b/ietf/api/urls.py @@ -12,22 +12,30 @@ from ietf.utils.urls import url api.autodiscover() urlpatterns = [ - # Top endpoint for Tastypie's REST API (this isn't standard Tastypie): + # General API help page url(r'^$', api_views.api_help), + # Top endpoint for Tastypie's REST API (this isn't standard Tastypie): url(r'^v1/?$', api_views.top_level), - # Custom API endpoints - url(r'^notify/meeting/import_recordings/(?P[a-z0-9-]+)/?$', meeting_views.api_import_recordings), - url(r'^meeting/session/video/url$', meeting_views.api_set_session_video_url), - url(r'^submit/?$', submit_views.api_submit), - url(r'^iesg/position', views_ballot.api_set_position), - # GPRD: export of personal information for the logged-in person - url(r'^export/personal-information/$', api_views.PersonalInformationExportView.as_view()), # For mailarchive use, requires secretariat role url(r'^v2/person/person', api_views.ApiV2PersonExportView.as_view()), - # For meetecho access - url(r'^person/access/meetecho', api_views.person_access_meetecho), + # + # --- Custom API endpoints, sorted alphabetically --- + # GPRD: export of personal information for the logged-in person + url(r'^export/personal-information/$', api_views.PersonalInformationExportView.as_view()), + # Let IESG members set positions programmatically + url(r'^iesg/position', views_ballot.api_set_position), + # Let Meetecho set session video URLs + url(r'^meeting/session/video/url$', meeting_views.api_set_session_video_url), + # Let Meetecho trigger recording imports + url(r'^notify/meeting/import_recordings/(?P[a-z0-9-]+)/?$', meeting_views.api_import_recordings), + # Let the registration system notify us about registrations + url(r'^notify/meeting/registration', api_views.api_new_meeting_registration), # OpenID authentication provider url(r'^openid/', include('oidc_provider.urls', namespace='oidc_provider')), + # For meetecho access + url(r'^person/access/meetecho', api_views.person_access_meetecho), + # Draft submission API + url(r'^submit/?$', submit_views.api_submit), ] # Additional (standard) Tastypie endpoints diff --git a/ietf/api/views.py b/ietf/api/views.py index 2bed0db20..17cd24f00 100644 --- a/ietf/api/views.py +++ b/ietf/api/views.py @@ -8,6 +8,9 @@ from jwcrypto.jwk import JWK from django.conf import settings from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User +from django.core.exceptions import ValidationError +from django.core.validators import validate_email from django.http import HttpResponse from django.shortcuts import render, get_object_or_404 from django.urls import reverse @@ -23,11 +26,14 @@ from tastypie.serializers import Serializer import debug # pyflakes:ignore -from ietf.person.models import Person +from ietf.person.models import Person, Email from ietf.api import _api_list from ietf.api.serializer import JsonExportMixin -from ietf.utils.decorators import require_api_key +from ietf.ietfauth.views import send_account_creation_email from ietf.ietfauth.utils import role_required +from ietf.meeting.models import Meeting +from ietf.stats.models import MeetingRegistration +from ietf.utils.decorators import require_api_key def top_level(request): @@ -108,3 +114,61 @@ def person_access_meetecho(request): 'secr': list(person.role_set.filter(name='secr', group__state__in=['active', 'bof', 'proposed']).values_list('group__acronym', flat=True)), } }), content_type='application/json') + +@require_api_key +@role_required('Robot') +@csrf_exempt +def api_new_meeting_registration(request): + '''REST API to notify the datatracker about a new meeting registration''' + def err(code, text): + return HttpResponse(text, status=code, content_type='text/plain') + required_fields = [ 'meeting', 'first_name', 'last_name', 'affiliation', 'country_code', 'email', 'reg_type', 'ticket_type', ] + fields = required_fields + [] + if request.method == 'POST': + # parameters: + # apikey: + # meeting + # name + # email + # reg_type (In Person, Remote, Hackathon Only) + # ticket_type (full_week, one_day, student) + # + data = {} + missing_fields = [] + for item in fields: + value = request.POST.get(item, None) + if value is None and item in required_fields: + missing_fields.append(item) + data[item] = value + if missing_fields: + return err(400, "Missing parameters: %s" % ', '.join(missing_fields)) + number = data['meeting'] + try: + meeting = Meeting.objects.get(number=number) + except Meeting.DoesNotExist: + return err(400, "Invalid meeting value: '%s'" % (number, )) + email = data['email'] + try: + validate_email(email) + except ValidationError: + return err(400, "Invalid email value: '%s'" % (email, )) + if request.POST.get('cancelled', 'false') == 'true': + MeetingRegistration.objects.filter(meeting_id=meeting.pk, email=email).delete() + return HttpResponse('OK', status=200, content_type='text/plain') + else: + object, created = MeetingRegistration.objects.get_or_create(meeting_id=meeting.pk, email=email) + try: + MeetingRegistration.objects.filter(id=object.id).update(**data) + object.save() + except ValueError as e: + return err(400, "Unexpected POST data: %s" % e) + response = "Accepted, New registration" if created else "Accepted, Updated registration" + if User.objects.filter(username=email).exists() or Email.objects.filter(address=email).exists(): + pass + else: + send_account_creation_email(request, email) + response += ", Email sent" + return HttpResponse(response, status=202, content_type='text/plain') + else: + return HttpResponse(status=405) + \ No newline at end of file diff --git a/ietf/ietfauth/utils.py b/ietf/ietfauth/utils.py index d7916013d..1af4ff592 100644 --- a/ietf/ietfauth/utils.py +++ b/ietf/ietfauth/utils.py @@ -85,7 +85,7 @@ def has_role(user, role_names, *args, **kwargs): "Reviewer": Q(person=person, name="reviewer", group__state="active"), "Review Team Secretary": Q(person=person, name="secr", group__reviewteamsettings__isnull=False,group__state="active", ), "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")), - + "Robot": Q(person=person, name="robot", group__acronym="secretariat"), } filter_expr = Q() diff --git a/ietf/person/models.py b/ietf/person/models.py index d48206972..d3315ad5a 100644 --- a/ietf/person/models.py +++ b/ietf/person/models.py @@ -337,6 +337,7 @@ PERSON_API_KEY_VALUES = [ ("/api/v2/person/person", "/api/v2/person/person", "Secretariat"), ("/api/meeting/session/video/url", "/api/meeting/session/video/url", "Recording Manager"), ("/api/person/access/meetecho", "/api/person/access/meetecho", None), + ("/api/notify/meeting/registration", "/api/notify/meeting/registration", "Robot"), ] PERSON_API_KEY_ENDPOINTS = [ (v, n) for (v, n, r) in PERSON_API_KEY_VALUES ] diff --git a/ietf/stats/utils.py b/ietf/stats/utils.py index 93c12a45f..727dc8a78 100644 --- a/ietf/stats/utils.py +++ b/ietf/stats/utils.py @@ -249,12 +249,13 @@ def get_meeting_registration_data(meeting): address = registration['Email'].strip() object, created = MeetingRegistration.objects.get_or_create( meeting_id=meeting.pk, - first_name=first_name[:200], - last_name=last_name[:200], - affiliation=affiliation, - country_code=country_code, email=address, ) + object.first_name=first_name[:200] + object.last_name=last_name[:200] + object.affiliation=affiliation + object.country_code=country_code + object.save() # Add a Person object to MeetingRegistration object # if valid email is available