From a56439f69ea674bf3ec67fc99c81e7d8e4ef741b Mon Sep 17 00:00:00 2001 From: Ryan Cross Date: Sat, 20 Jul 2024 18:22:19 -0700 Subject: [PATCH 1/5] fix: refactor api_new_meeting_registration. Fixes #7608 - change to use JSON payload - handle multiple records in one request --- ietf/api/views.py | 202 +++++++++++++++++++++++++++------------------- 1 file changed, 119 insertions(+), 83 deletions(-) diff --git a/ietf/api/views.py b/ietf/api/views.py index 6aaed4b6a91..d914a43c353 100644 --- a/ietf/api/views.py +++ b/ietf/api/views.py @@ -141,95 +141,131 @@ def post(self, request): # else: # return HttpResponse(status=405) + +_new_registration_json_validator = jsonschema.Draft202012Validator( + schema={ + "type": "array", + "items": { + "type": "object", + "properties": { + "meeting": {"type": "integer"}, + "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"}, + "checked_in": {"type": "boolean"}, + "is_nomcom_volunteer": {"type": "boolean"}, + "cancelled": {"type": "boolean"}, + }, + "required": ["meeting", "first_name", "last_name", "affiliation", "country_code", "email", "reg_type", "ticket_type", "checked_in", "is_nomcom_volunteer", "cancelled"], + "additionalProperties": "false" + } + } +) + + @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', 'checkedin', 'is_nomcom_volunteer'] - 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 = {'attended': False, } - 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, )) - reg_type = data['reg_type'] - 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, - reg_type=reg_type).delete() - return HttpResponse('OK', status=200, content_type='text/plain') - else: - object, created = MeetingRegistration.objects.get_or_create( - meeting_id=meeting.pk, - email=email, - reg_type=reg_type) + + 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: + # no log level? + log.log(f"api_new_meeting_registration no Person found for {email}") + + # delete existing records + MeetingRegistration.objects.filter(meeting__number=number, email=email).delete() + + for registration in payload: + # handle cancelled + if registration['cancelled']: + # no-op + continue + + # handle regular + MeetingRegistration.objects.create( + meeting_id=meeting.pk, + email=email, + first_name=registration['first_name'], + last_name=registration['last_name'], + affiliation=registration['affiliation'], + country_code=registration['country_code'], + reg_type=registration['reg_type'], + ticket_type=registration['ticket_type'], + checked_in=registration['checked_in'], + is_nomcom_volunteer=registration['is_nomcom_volunteer'], + cancelled=registration['cancelled']) + + # removed account creation email. Registration requires Datatracker account + + # handle nomcom volunteer + if registration['is_nomcom_volunteer'] and person: try: - # Update attributes - for key in set(data.keys())-set(['attended', 'apikey', 'meeting', 'email']): - if key == 'checkedin': - new = bool(data.get(key).lower() == 'true') - else: - new = data.get(key) - setattr(object, key, new) - person = Person.objects.filter(email__address=email) - if person.exists(): - object.person = person.first() - 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__iexact=email).exists() or Email.objects.filter(address=email).exists(): - pass - else: - send_account_creation_email(request, email) - response += ", Email sent" - - # handle nomcom volunteer - if request.POST.get('is_nomcom_volunteer', 'false').lower() == 'true' and object.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=object.person, - defaults={ - "affiliation": data["affiliation"], - "origin": "registration" - } - ) - return HttpResponse(response, status=202, content_type='text/plain') - else: - return HttpResponse(status=405) + 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): From 457270e9b855e44dcccc5558b0fadd9c3007c3b7 Mon Sep 17 00:00:00 2001 From: Ryan Cross Date: Thu, 25 Jul 2024 11:54:55 -0700 Subject: [PATCH 2/5] fix: update existing MeetingRegistration records --- ietf/api/views.py | 48 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/ietf/api/views.py b/ietf/api/views.py index d914a43c353..b38bd792f0d 100644 --- a/ietf/api/views.py +++ b/ietf/api/views.py @@ -39,7 +39,6 @@ from ietf.doc.utils import DraftAliasGenerator, fuzzy_find_documents from ietf.group.utils import GroupAliasGenerator, role_holder_emails 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.nomcom.models import Volunteer, NomCom @@ -173,6 +172,12 @@ def post(self, request): def api_new_meeting_registration(request): '''REST API to notify the datatracker about a new meeting registration''' + def _safe_pop(lst): + if lst: + return lst.pop() + else: + return None + def _http_err(code, text): return HttpResponse(text, status=code, content_type="text/plain") @@ -218,34 +223,43 @@ def _api_response(result): except ValidationError: return _http_err(400, "Invalid email value: '%s'" % (email, )) + # handle cancelled. there will be only one record + if payload[0]['cancelled']: + reg = MeetingRegistration.objects.filter( + meeting__number=number, + email=email, + reg_type=payload[0]['reg_type'], + ticket_type=payload[0]['ticket_type']).first() + if reg: + reg.delete() + return HttpResponse('Success', status=202, content_type='text/plain') + # get person person = Person.objects.filter(email__address=email).first() if not person: # no log level? log.log(f"api_new_meeting_registration no Person found for {email}") - # delete existing records - MeetingRegistration.objects.filter(meeting__number=number, email=email).delete() + # get existing records if any + regs = MeetingRegistration.objects.filter(meeting__number=number, email=email) + pks = [r.pk for r in regs] for registration in payload: - # handle cancelled - if registration['cancelled']: - # no-op - continue - - # handle regular - MeetingRegistration.objects.create( - meeting_id=meeting.pk, - email=email, + new_reg = MeetingRegistration( + meeting=meeting, first_name=registration['first_name'], last_name=registration['last_name'], affiliation=registration['affiliation'], country_code=registration['country_code'], + person=person, + email=email, reg_type=registration['reg_type'], ticket_type=registration['ticket_type'], - checked_in=registration['checked_in'], - is_nomcom_volunteer=registration['is_nomcom_volunteer'], - cancelled=registration['cancelled']) + checkedin=registration['checked_in']) + + # update any existing records if there are any + new_reg.pk = _safe_pop(pks) + new_reg.save() # removed account creation email. Registration requires Datatracker account @@ -265,6 +279,10 @@ def _api_response(result): } ) + # delete any remaining records + if pks: + MeetingRegistration.objects.filter(pk__in=pks).delete() + return HttpResponse('Success', status=202, content_type='text/plain') From ad4d870cadbaa7c635eee723d95f1b41349596a0 Mon Sep 17 00:00:00 2001 From: Ryan Cross Date: Mon, 25 Nov 2024 15:02:54 -0800 Subject: [PATCH 3/5] fix: add tests for new reg api --- ietf/api/tests.py | 157 +++++++++++++++++++++++++++++++++++++++++++++- ietf/api/urls.py | 1 + ietf/api/views.py | 114 ++++++++++++++++++++++++++++++--- 3 files changed, 261 insertions(+), 11 deletions(-) diff --git a/ietf/api/tests.py b/ietf/api/tests.py index fd8eb52cd6b..19654d87d88 100644 --- a/ietf/api/tests.py +++ b/ietf/api/tests.py @@ -1,6 +1,7 @@ # Copyright The IETF Trust 2015-2020, All Rights Reserved # -*- coding: utf-8 -*- import base64 +import copy import datetime import json import html @@ -816,9 +817,112 @@ def test_api_new_meeting_registration(self): missing_fields = [f.strip() for f in fields.split(',')] self.assertEqual(set(missing_fields), set(drop_fields)) + @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] + obj = MeetingRegistration.objects.get(email=reg['email'], meeting__number=reg['meeting']) + for key in ['affiliation', 'country_code', 'first_name', 'last_name', 'reg_type', 'ticket_type', 'checkedin']: + self.assertEqual(getattr(obj, key), False if key=='checkedin' else reg.get(key) , "Bad data for field '%s'" % key) + 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) + obj = MeetingRegistration.objects.get(email=reg['email'], meeting__number=reg['meeting']) + self.assertEqual(obj.reg_type, 'remote') + # + # 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) + q = MeetingRegistration.objects.filter(email=reg['email'], meeting__number=reg['meeting']) + self.assertEqual(q.count(), 2) + self.assertEqual(q.filter(reg_type='onsite').count(), 1) + self.assertEqual(q.filter(reg_type='remote').count(), 1) + + def test_api_new_meeting_registration_nomcom_volunteer(self): '''Test that Volunteer is created if is_nomcom_volunteer=True - is submitted to API + is submitted to API ''' meeting = MeetingFactory(type_id='ietf') reg = { @@ -866,6 +970,57 @@ 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_nomcom_volunteer(self): + '''Test that Volunteer is created if is_nomcom_volunteer=True + is submitted to API + ''' + 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') + + 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.assertEqual(r.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.assertEqual(r.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') diff --git a/ietf/api/urls.py b/ietf/api/urls.py index fb2184a3f0c..57cff2d8a2a 100644 --- a/ietf/api/urls.py +++ b/ietf/api/urls.py @@ -49,6 +49,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'), diff --git a/ietf/api/views.py b/ietf/api/views.py index b38bd792f0d..3855dc80104 100644 --- a/ietf/api/views.py +++ b/ietf/api/views.py @@ -39,6 +39,7 @@ from ietf.doc.utils import DraftAliasGenerator, fuzzy_find_documents from ietf.group.utils import GroupAliasGenerator, role_holder_emails 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.nomcom.models import Volunteer, NomCom @@ -141,13 +142,104 @@ def post(self, request): # return HttpResponse(status=405) +@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', 'checkedin', 'is_nomcom_volunteer'] + 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 = {'attended': False, } + 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, )) + reg_type = data['reg_type'] + 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, + reg_type=reg_type).delete() + return HttpResponse('OK', status=200, content_type='text/plain') + else: + object, created = MeetingRegistration.objects.get_or_create( + meeting_id=meeting.pk, + email=email, + reg_type=reg_type) + try: + # Update attributes + for key in set(data.keys())-set(['attended', 'apikey', 'meeting', 'email']): + if key == 'checkedin': + new = bool(data.get(key).lower() == 'true') + else: + new = data.get(key) + setattr(object, key, new) + person = Person.objects.filter(email__address=email) + if person.exists(): + object.person = person.first() + 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__iexact=email).exists() or Email.objects.filter(address=email).exists(): + pass + else: + send_account_creation_email(request, email) + response += ", Email sent" + + # handle nomcom volunteer + if request.POST.get('is_nomcom_volunteer', 'false').lower() == 'true' and object.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=object.person, + defaults={ + "affiliation": data["affiliation"], + "origin": "registration" + } + ) + return HttpResponse(response, status=202, content_type='text/plain') + else: + return HttpResponse(status=405) + + _new_registration_json_validator = jsonschema.Draft202012Validator( schema={ "type": "array", "items": { "type": "object", "properties": { - "meeting": {"type": "integer"}, + "meeting": {"type": "string"}, "first_name": {"type": "string"}, "last_name": {"type": "string"}, "affiliation": {"type": "string"}, @@ -155,22 +247,21 @@ def post(self, request): "email": {"type": "string"}, "reg_type": {"type": "string"}, "ticket_type": {"type": "string"}, - "checked_in": {"type": "boolean"}, + "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", "checked_in", "is_nomcom_volunteer", "cancelled"], + "required": ["meeting", "first_name", "last_name", "affiliation", "country_code", "email", "reg_type", "ticket_type", "checkedin", "is_nomcom_volunteer", "cancelled"], "additionalProperties": "false" } } ) -@require_api_key -@role_required('Robot') +@requires_api_token @csrf_exempt -def api_new_meeting_registration(request): - '''REST API to notify the datatracker about a new meeting registration''' +def api_new_meeting_registration_v2(request): + '''REST API to notify the datatracker about new or updated meeting registrations''' def _safe_pop(lst): if lst: @@ -237,8 +328,7 @@ def _api_response(result): # get person person = Person.objects.filter(email__address=email).first() if not person: - # no log level? - log.log(f"api_new_meeting_registration no Person found for {email}") + log.log(f"api_new_meeting_registration_v2 no Person found for {email}") # get existing records if any regs = MeetingRegistration.objects.filter(meeting__number=number, email=email) @@ -255,10 +345,14 @@ def _api_response(result): email=email, reg_type=registration['reg_type'], ticket_type=registration['ticket_type'], - checkedin=registration['checked_in']) + checkedin=registration['checkedin']) # update any existing records if there are any new_reg.pk = _safe_pop(pks) + if new_reg.pk: + log.log(f"Updating MeetingRegistration record for meeting:{meeting} email:{email}") + else: + log.log(f"New MeetingRegistration record for meeting:{meeting} email:{email}") new_reg.save() # removed account creation email. Registration requires Datatracker account From 06619ec675ee85a030e97ed9917bc205810fe9e0 Mon Sep 17 00:00:00 2001 From: Ryan Cross Date: Wed, 11 Dec 2024 14:33:05 -0800 Subject: [PATCH 4/5] test: explicitly check query count to improve test output --- ietf/api/tests.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ietf/api/tests.py b/ietf/api/tests.py index a29b94f5d85..562af4233d4 100644 --- a/ietf/api/tests.py +++ b/ietf/api/tests.py @@ -919,7 +919,9 @@ def test_api_new_meeting_registration_v2(self): # # Check record reg = regs[0] - obj = MeetingRegistration.objects.get(email=reg['email'], meeting__number=reg['meeting']) + objects = MeetingRegistration.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', 'reg_type', 'ticket_type', 'checkedin']: self.assertEqual(getattr(obj, key), False if key=='checkedin' else reg.get(key) , "Bad data for field '%s'" % key) self.assertEqual(obj.person, person) @@ -942,7 +944,9 @@ def test_api_new_meeting_registration_v2(self): ] 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) - obj = MeetingRegistration.objects.get(email=reg['email'], meeting__number=reg['meeting']) + objects = MeetingRegistration.objects.filter(email=reg['email'], meeting__number=reg['meeting']) + self.assertEqual(objects.count(), 1) + obj = objects[0] self.assertEqual(obj.reg_type, 'remote') # # Test multiple From 4ae730675d89454bd9a0d7b86257f54a661fa808 Mon Sep 17 00:00:00 2001 From: Ryan Cross Date: Wed, 11 Dec 2024 14:48:08 -0800 Subject: [PATCH 5/5] fix: return error if cancelled reg notification contains more than one record --- ietf/api/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ietf/api/views.py b/ietf/api/views.py index 9d84b888165..c2294ebdc2c 100644 --- a/ietf/api/views.py +++ b/ietf/api/views.py @@ -314,8 +314,10 @@ def _api_response(result): except ValidationError: return _http_err(400, "Invalid email value: '%s'" % (email, )) - # handle cancelled. there will be only one record + # handle cancelled. there should be only one record if payload[0]['cancelled']: + if len(payload) > 1: + return _http_err(400, "Error. Received cancelled registration notification with more than one record. ({})".format(email)) reg = MeetingRegistration.objects.filter( meeting__number=number, email=email,