Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new meeting registration implementation #8408

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 192 additions & 1 deletion ietf/api/tests.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright The IETF Trust 2015-2024, All Rights Reserved
# -*- coding: utf-8 -*-
import base64
import copy
import datetime
import json
import html
Expand Down Expand Up @@ -30,7 +31,7 @@
from ietf.doc.factories import IndividualDraftFactory, WgDraftFactory, WgRfcFactory
from ietf.group.factories import RoleFactory
from ietf.meeting.factories import MeetingFactory, SessionFactory
from ietf.meeting.models import Session
from ietf.meeting.models import Session, Registration
from ietf.nomcom.models import Volunteer
from ietf.nomcom.factories import NomComFactory, nomcom_kwargs_for_year
from ietf.person.factories import PersonFactory, random_faker, EmailFactory, PersonalApiKeyFactory
Expand Down Expand Up @@ -930,6 +931,196 @@ def test_api_new_meeting_registration_nomcom_volunteer(self):
self.assertEqual(volunteer.nomcom, nomcom)
self.assertEqual(volunteer.origin, 'registration')

@override_settings(APP_API_TOKENS={"ietf.api.views.api_new_meeting_registration_v2": ["valid-token"]})
def test_api_new_meeting_registration_v2(self):
meeting = MeetingFactory(type_id='ietf')
person = PersonFactory()
regs = [
{
'affiliation': "Alguma Corporação",
'country_code': 'PT',
'email': person.email().address,
'first_name': person.first_name(),
'last_name': person.last_name(),
'meeting': str(meeting.number),
'reg_type': 'onsite',
'ticket_type': 'week_pass',
'checkedin': False,
'is_nomcom_volunteer': False,
'cancelled': False,
}
]

url = urlreverse('ietf.api.views.api_new_meeting_registration_v2')
#
# Test invalid key
r = self.client.post(url, data=json.dumps(regs), content_type='application/json', headers={"X-Api-Key": "invalid-token"})
self.assertEqual(r.status_code, 403)
#
# Test invalid data
bad_regs = copy.deepcopy(regs)
del(bad_regs[0]['email'])
r = self.client.post(url, data=json.dumps(bad_regs), content_type='application/json', headers={"X-Api-Key": "valid-token"})
self.assertEqual(r.status_code, 400)
#
# Test valid POST
r = self.client.post(url, data=json.dumps(regs), content_type='application/json', headers={"X-Api-Key": "valid-token"})
self.assertContains(r, "Success", status_code=202)
#
# Check record
reg = regs[0]
objects = Registration.objects.filter(email=reg['email'], meeting__number=reg['meeting'])
self.assertEqual(objects.count(), 1)
obj = objects[0]
for key in ['affiliation', 'country_code', 'first_name', 'last_name', 'checkedin']:
self.assertEqual(getattr(obj, key), False if key=='checkedin' else reg.get(key) , "Bad data for field '%s'" % key)
self.assertEqual(obj.tickets.count(), 1)
ticket = obj.tickets.first()
self.assertEqual(ticket.ticket_type.slug, regs[0]['ticket_type'])
self.assertEqual(ticket.attendance_type.slug, regs[0]['reg_type'])
self.assertEqual(obj.person, person)
#
# Test update (switch to remote)
regs = [
{
'affiliation': "Alguma Corporação",
'country_code': 'PT',
'email': person.email().address,
'first_name': person.first_name(),
'last_name': person.last_name(),
'meeting': str(meeting.number),
'reg_type': 'remote',
'ticket_type': 'week_pass',
'checkedin': False,
'is_nomcom_volunteer': False,
'cancelled': False,
}
]
r = self.client.post(url, data=json.dumps(regs), content_type='application/json', headers={"X-Api-Key": "valid-token"})
self.assertContains(r, "Success", status_code=202)
objects = Registration.objects.filter(email=reg['email'], meeting__number=reg['meeting'])
self.assertEqual(objects.count(), 1)
obj = objects[0]
self.assertEqual(obj.tickets.count(), 1)
ticket = obj.tickets.first()
self.assertEqual(ticket.ticket_type.slug, regs[0]['ticket_type'])
self.assertEqual(ticket.attendance_type.slug, regs[0]['reg_type'])
#
# Test multiple
regs = [
{
'affiliation': "Alguma Corporação",
'country_code': 'PT',
'email': person.email().address,
'first_name': person.first_name(),
'last_name': person.last_name(),
'meeting': str(meeting.number),
'reg_type': 'onsite',
'ticket_type': 'one_day',
'checkedin': False,
'is_nomcom_volunteer': False,
'cancelled': False,
},

{
'affiliation': "Alguma Corporação",
'country_code': 'PT',
'email': person.email().address,
'first_name': person.first_name(),
'last_name': person.last_name(),
'meeting': str(meeting.number),
'reg_type': 'remote',
'ticket_type': 'week_pass',
'checkedin': False,
'is_nomcom_volunteer': False,
'cancelled': False,
}
]

r = self.client.post(url, data=json.dumps(regs), content_type='application/json', headers={"X-Api-Key": "valid-token"})
self.assertContains(r, "Success", status_code=202)
objects = Registration.objects.filter(email=reg['email'], meeting__number=reg['meeting'])
self.assertEqual(objects.count(), 1)
obj = objects[0]
self.assertEqual(obj.tickets.count(), 2)
self.assertEqual(obj.tickets.filter(attendance_type__slug='onsite').count(), 1)
self.assertEqual(obj.tickets.filter(attendance_type__slug='remote').count(), 1)

@override_settings(APP_API_TOKENS={"ietf.api.views.api_new_meeting_registration_v2": ["valid-token"]})
def test_api_new_meeting_registration_v2_cancelled(self):
meeting = MeetingFactory(type_id='ietf')
person = PersonFactory()
regs = [
{
'affiliation': "Acme",
'country_code': 'US',
'email': person.email().address,
'first_name': person.first_name(),
'last_name': person.last_name(),
'meeting': str(meeting.number),
'reg_type': 'onsite',
'ticket_type': 'week_pass',
'checkedin': False,
'is_nomcom_volunteer': False,
'cancelled': False,
}
]
url = urlreverse('ietf.api.views.api_new_meeting_registration_v2')
self.assertEqual(Registration.objects.count(), 0)
r = self.client.post(url, data=json.dumps(regs), content_type='application/json', headers={"X-Api-Key": "valid-token"})
self.assertContains(r, "Success", status_code=202)
self.assertEqual(Registration.objects.count(), 1)
regs[0]['cancelled'] = True
r = self.client.post(url, data=json.dumps(regs), content_type='application/json', headers={"X-Api-Key": "valid-token"})
self.assertContains(r, "Success", status_code=202)
self.assertEqual(Registration.objects.count(), 0)

@override_settings(APP_API_TOKENS={"ietf.api.views.api_new_meeting_registration_v2": ["valid-token"]})
def test_api_new_meeting_registration_v2_nomcom(self):
meeting = MeetingFactory(type_id='ietf')
person = PersonFactory()
regs = [
{
'affiliation': "Acme",
'country_code': 'US',
'email': person.email().address,
'first_name': person.first_name(),
'last_name': person.last_name(),
'meeting': str(meeting.number),
'reg_type': 'onsite',
'ticket_type': 'week_pass',
'checkedin': False,
'is_nomcom_volunteer': False,
'cancelled': False,
}
]

url = urlreverse('ietf.api.views.api_new_meeting_registration_v2')
now = datetime.datetime.now()
if now.month > 10:
year = now.year + 1
else:
year = now.year
# create appropriate group and nomcom objects
nomcom = NomComFactory.create(is_accepting_volunteers=True, **nomcom_kwargs_for_year(year))

# first test is_nomcom_volunteer False
r = self.client.post(url, data=json.dumps(regs), content_type='application/json', headers={"X-Api-Key": "valid-token"})
self.assertContains(r, "Success", status_code=202)
# assert no Volunteers exists
self.assertEqual(Volunteer.objects.count(), 0)

# test is_nomcom_volunteer True
regs[0]['is_nomcom_volunteer'] = True
r = self.client.post(url, data=json.dumps(regs), content_type='application/json', headers={"X-Api-Key": "valid-token"})
self.assertContains(r, "Success", status_code=202)
# assert Volunteer exists
self.assertEqual(Volunteer.objects.count(), 1)
volunteer = Volunteer.objects.last()
self.assertEqual(volunteer.person, person)
self.assertEqual(volunteer.nomcom, nomcom)
self.assertEqual(volunteer.origin, 'registration')

def test_api_version(self):
DumpInfo.objects.create(date=timezone.datetime(2022,8,31,7,10,1,tzinfo=datetime.timezone.utc), host='testapi.example.com',tz='UTC')
url = urlreverse('ietf.api.views.version')
Expand Down
1 change: 1 addition & 0 deletions ietf/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
# Let MeetEcho upload session polls
url(r'^notify/session/polls/?$', meeting_views.api_upload_polls),
# Let the registration system notify us about registrations
url(r'^notify/meeting/registration/v2/?', api_views.api_new_meeting_registration_v2),
url(r'^notify/meeting/registration/?', api_views.api_new_meeting_registration),
# OpenID authentication provider
url(r'^openid/$', TemplateView.as_view(template_name='api/openid-issuer.html'), name='ietf.api.urls.oidc_issuer'),
Expand Down
143 changes: 142 additions & 1 deletion ietf/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
from ietf.ietfauth.utils import role_required
from ietf.ietfauth.views import send_account_creation_email
from ietf.ipr.utils import ingest_response_email as ipr_ingest_response_email
from ietf.meeting.models import Meeting
from ietf.meeting.models import Meeting, Registration
from ietf.nomcom.models import Volunteer, NomCom
from ietf.nomcom.utils import ingest_feedback_email as nomcom_ingest_feedback_email
from ietf.person.models import Person, Email
Expand Down Expand Up @@ -233,6 +233,147 @@ def err(code, text):
return HttpResponse(status=405)


_new_registration_json_validator = jsonschema.Draft202012Validator(
schema={
"type": "array",
"items": {
"type": "object",
"properties": {
"meeting": {"type": "string"},
"first_name": {"type": "string"},
"last_name": {"type": "string"},
"affiliation": {"type": "string"},
"country_code": {"type": "string"},
"email": {"type": "string"},
"reg_type": {"type": "string"},
"ticket_type": {"type": "string"},
"checkedin": {"type": "boolean"},
"is_nomcom_volunteer": {"type": "boolean"},
"cancelled": {"type": "boolean"},
},
"required": ["meeting", "first_name", "last_name", "affiliation", "country_code", "email", "reg_type", "ticket_type", "checkedin", "is_nomcom_volunteer", "cancelled"],
"additionalProperties": "false"
}
}
)


@requires_api_token
@csrf_exempt
def api_new_meeting_registration_v2(request):
'''REST API to notify the datatracker about a new meeting registration'''
def _http_err(code, text):
return HttpResponse(text, status=code, content_type="text/plain")

def _api_response(result):
return JsonResponse(data={"result": result})

if request.method != "POST":
return _http_err(405, "Method not allowed")

if request.content_type != "application/json":
return _http_err(415, "Content-Type must be application/json")

# Validate
try:
payload = json.loads(request.body)
_new_registration_json_validator.validate(payload)
except json.decoder.JSONDecodeError as err:
return _http_err(400, f"JSON parse error at line {err.lineno} col {err.colno}: {err.msg}")
except jsonschema.exceptions.ValidationError as err:
return _http_err(400, f"JSON schema error at {err.json_path}: {err.message}")
except Exception:
return _http_err(400, "Invalid request format")

# Validate consistency
# - if receive multiple records they should be for same meeting, same person (email)
if len(payload) > 1:
if len(set([r['meeting'] for r in payload])) != 1:
return _http_err(400, "Different meeting values")
if len(set([r['email'] for r in payload])) != 1:
return _http_err(400, "Different email values")

# Validate meeting
number = payload[0]['meeting']
try:
meeting = Meeting.objects.get(number=number)
except Meeting.DoesNotExist:
return _http_err(400, "Invalid meeting value: '%s'" % (number, ))

# Validate email
email = payload[0]['email']
try:
validate_email(email)
except ValidationError:
return _http_err(400, "Invalid email value: '%s'" % (email, ))

# get person
person = Person.objects.filter(email__address=email).first()
if not person:
log.log(f"api_new_meeting_registration_v2 no Person found for {email}")

registration = payload[0]
# handle cancelled
if registration['cancelled']:
if len(payload) > 1:
return _http_err(400, "Error. Received cancelled registration notification with more than one record. ({})".format(email))
try:
obj = Registration.objects.get(meeting=meeting, email=email)
except Registration.DoesNotExist:
return _http_err(400, "Error. Received cancelled registration notification for non-existing registration. ({})".format(email))
if obj.tickets.count() == 1:
obj.delete()
else:
obj.tickets.filter(
attendance_type__slug=registration.reg_type,
ticket_type__slug=registration.ticket_type).delete()
return HttpResponse('Success', status=202, content_type='text/plain')

# create or update MeetingRegistration
update_fields = ['first_name', 'last_name', 'affiliation', 'country_code', 'checkedin', 'is_nomcom_volunteer']
try:
reg = Registration.objects.get(meeting=meeting, email=email)
for key, value in registration.items():
if key in update_fields:
setattr(reg, key, value)
reg.save()
except Registration.DoesNotExist:
reg = Registration.objects.create(
meeting_id=meeting.pk,
person=person,
email=email,
first_name=registration['first_name'],
last_name=registration['last_name'],
affiliation=registration['affiliation'],
country_code=registration['country_code'],
checkedin=registration['checkedin'])

# handle registration tickets
reg.tickets.all().delete()
for registration in payload:
reg.tickets.create(
attendance_type_id=registration['reg_type'],
ticket_type_id=registration['ticket_type'],
)
# handle nomcom volunteer
if registration['is_nomcom_volunteer'] and person:
try:
nomcom = NomCom.objects.get(is_accepting_volunteers=True)
except (NomCom.DoesNotExist, NomCom.MultipleObjectsReturned):
nomcom = None
if nomcom:
Volunteer.objects.get_or_create(
nomcom=nomcom,
person=person,
defaults={
"affiliation": registration["affiliation"],
"origin": "registration"
}
)

return HttpResponse('Success', status=202, content_type='text/plain')


def version(request):
dumpdate = None
dumpinfo = DumpInfo.objects.order_by('-date').first()
Expand Down
Loading