Skip to content

Commit 698f031

Browse files
authored
feat: separate MeetingRegistration rows for each registration type. updates the registration API (ietf-tools#3641)
* Registration API Update - change MeetingRegistration.reg_type field to hold only one type - allow multiple MeetingRegistration records per person/meeting (one for each reg_type) * Fix scope claims * Add meeting 114 to MeetingRegistration migration * fix: update stats views for MeetingRegistration model use changes * refactor: remove unused imports
1 parent df27d0f commit 698f031

7 files changed

Lines changed: 137 additions & 137 deletions

File tree

ietf/api/tests.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -279,23 +279,23 @@ def test_api_v2_person_export_view(self):
279279
def test_api_new_meeting_registration(self):
280280
meeting = MeetingFactory(type_id='ietf')
281281
reg = {
282-
'apikey': 'invalid',
283-
'affiliation': "Alguma Corporação",
284-
'country_code': 'PT',
285-
'email': 'foo@example.pt',
286-
'first_name': 'Foo',
287-
'last_name': 'Bar',
288-
'meeting': meeting.number,
289-
'reg_type': 'hackathon',
290-
'ticket_type': '',
291-
}
282+
'apikey': 'invalid',
283+
'affiliation': "Alguma Corporação",
284+
'country_code': 'PT',
285+
'email': 'foo@example.pt',
286+
'first_name': 'Foo',
287+
'last_name': 'Bar',
288+
'meeting': meeting.number,
289+
'reg_type': 'hackathon',
290+
'ticket_type': '',
291+
}
292292
url = urlreverse('ietf.api.views.api_new_meeting_registration')
293293
r = self.client.post(url, reg)
294294
self.assertContains(r, 'Invalid apikey', status_code=403)
295295
oidcp = PersonFactory(user__is_staff=True)
296296
# Make sure 'oidcp' has an acceptable role
297297
RoleFactory(name_id='robot', person=oidcp, email=oidcp.email(), group__acronym='secretariat')
298-
key = PersonalApiKey.objects.create(person=oidcp, endpoint=url)
298+
key = PersonalApiKey.objects.create(person=oidcp, endpoint=url)
299299
reg['apikey'] = key.hash()
300300
#
301301
# Test valid POST
@@ -313,7 +313,7 @@ def test_api_new_meeting_registration(self):
313313
#
314314
# Check record
315315
obj = MeetingRegistration.objects.get(email=reg['email'], meeting__number=reg['meeting'])
316-
for key in [ 'affiliation', 'country_code', 'first_name', 'last_name', 'person', 'reg_type', 'ticket_type', ]:
316+
for key in ['affiliation', 'country_code', 'first_name', 'last_name', 'person', 'reg_type', 'ticket_type']:
317317
self.assertEqual(getattr(obj, key), reg.get(key), "Bad data for field '%s'" % key)
318318
#
319319
# Test with existing user
@@ -328,15 +328,15 @@ def test_api_new_meeting_registration(self):
328328
# There should be no new outgoing mail
329329
self.assertEqual(len(outbox), old_len + 1)
330330
#
331-
# Test combination of reg types
331+
# Test multiple reg types
332332
reg['reg_type'] = 'remote'
333333
reg['ticket_type'] = 'full_week_pass'
334334
r = self.client.post(url, reg)
335-
self.assertContains(r, "Accepted, Updated registration", status_code=202)
336-
obj = MeetingRegistration.objects.get(email=reg['email'], meeting__number=reg['meeting'])
337-
self.assertIn('hackathon', set(obj.reg_type.split()))
338-
self.assertIn('remote', set(obj.reg_type.split()))
339-
self.assertIn('full_week_pass', set(obj.ticket_type.split()))
335+
self.assertContains(r, "Accepted, New registration", status_code=202)
336+
objs = MeetingRegistration.objects.filter(email=reg['email'], meeting__number=reg['meeting'])
337+
self.assertEqual(len(objs), 2)
338+
self.assertEqual(objs.filter(reg_type='hackathon').count(), 1)
339+
self.assertEqual(objs.filter(reg_type='remote', ticket_type='full_week_pass').count(), 1)
340340
self.assertEqual(len(outbox), old_len + 1)
341341
#
342342
# Test incomplete POST
@@ -346,7 +346,7 @@ def test_api_new_meeting_registration(self):
346346
r = self.client.post(url, reg)
347347
self.assertContains(r, 'Missing parameters:', status_code=400)
348348
err, fields = r.content.decode().split(':', 1)
349-
missing_fields = [ f.strip() for f in fields.split(',') ]
349+
missing_fields = [f.strip() for f in fields.split(',')]
350350
self.assertEqual(set(missing_fields), set(drop_fields))
351351

352352
def test_api_version(self):
@@ -422,4 +422,4 @@ def test_all_model_resources_exist(self):
422422
if not model._meta.model_name in list(app_resources.keys()):
423423
#print("There doesn't seem to be any resource for model %s.models.%s"%(app.__name__,model.__name__,))
424424
self.assertIn(model._meta.model_name, list(app_resources.keys()),
425-
"There doesn't seem to be any API resource for model %s.models.%s"%(app.__name__,model.__name__,))
425+
"There doesn't seem to be any API resource for model %s.models.%s"%(app.__name__,model.__name__,))

ietf/api/views.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -162,30 +162,28 @@ def err(code, text):
162162
meeting = Meeting.objects.get(number=number)
163163
except Meeting.DoesNotExist:
164164
return err(400, "Invalid meeting value: '%s'" % (number, ))
165+
reg_type = data['reg_type']
165166
email = data['email']
166167
try:
167168
validate_email(email)
168169
except ValidationError:
169170
return err(400, "Invalid email value: '%s'" % (email, ))
170171
if request.POST.get('cancelled', 'false') == 'true':
171-
MeetingRegistration.objects.filter(meeting_id=meeting.pk, email=email).delete()
172+
MeetingRegistration.objects.filter(
173+
meeting_id=meeting.pk,
174+
email=email,
175+
reg_type=reg_type).delete()
172176
return HttpResponse('OK', status=200, content_type='text/plain')
173177
else:
174-
object, created = MeetingRegistration.objects.get_or_create(meeting_id=meeting.pk, email=email)
178+
object, created = MeetingRegistration.objects.get_or_create(
179+
meeting_id=meeting.pk,
180+
email=email,
181+
reg_type=reg_type)
175182
try:
176-
# Set attributes not already in the object
177-
for key in set(data.keys())-set(['attended', 'apikey', 'meeting', 'email',]):
183+
# Update attributes
184+
for key in set(data.keys())-set(['attended', 'apikey', 'meeting', 'email']):
178185
new = data.get(key)
179-
cur = getattr(object, key, None)
180-
if key in ['reg_type', 'ticket_type', ] and new:
181-
# Special handling for multiple reg types
182-
if cur:
183-
if not new in cur:
184-
setattr(object, key, cur+' '+new)
185-
else:
186-
setattr(object, key, new)
187-
else:
188-
setattr(object, key, new)
186+
setattr(object, key, new)
189187
person = Person.objects.filter(email__address=email)
190188
if person.exists():
191189
object.person = person.first()

ietf/ietfauth/utils.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -291,15 +291,13 @@ def scope_registration(self):
291291
ticket_types = set([])
292292
reg_types = set([])
293293
for reg in regs:
294-
for t in reg.ticket_type.split():
295-
ticket_types.add(t)
296-
for r in reg.reg_type.split():
297-
reg_types.add(r)
294+
ticket_types.add(reg.ticket_type)
295+
reg_types.add(reg.reg_type)
298296
info = {
299297
'meeting': meeting.number,
300298
# full_week, one_day, student:
301299
'ticket_type': ' '.join(ticket_types),
302-
# in_person, onliine, hackathon:
300+
# onsite, remote, hackathon_onsite, hackathon_remote:
303301
'reg_type': ' '.join(reg_types),
304302
'affiliation': ([ reg.affiliation for reg in regs if reg.affiliation ] or [''])[0],
305303
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Generated by Django 2.2.26 on 2022-01-19 16:36
2+
3+
from django.db import migrations
4+
5+
6+
def forward(apps, schema_editor):
7+
'''Split records that have 2 reg_types into two separate records'''
8+
MeetingRegistration = apps.get_model('stats', 'MeetingRegistration')
9+
meetings = [108, 109, 110, 111, 112, 113, 114]
10+
for reg in MeetingRegistration.objects.filter(meeting__number__in=meetings):
11+
reg_types = reg.reg_type.split()
12+
if len(reg_types) == 2:
13+
reg.reg_type = reg_types[0]
14+
reg.save()
15+
# create copy
16+
reg.pk = None
17+
reg.reg_type = reg_types[1]
18+
reg.save()
19+
20+
21+
def reverse(apps, schema_editor):
22+
pass
23+
24+
25+
class Migration(migrations.Migration):
26+
27+
dependencies = [
28+
('stats', '0003_meetingregistration_attended'),
29+
]
30+
31+
operations = [
32+
migrations.RunPython(forward, reverse)
33+
]

ietf/stats/tests.py

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import debug # pyflakes:ignore
1414

1515
from django.urls import reverse as urlreverse
16-
from django.contrib.auth.models import User
1716

1817
from ietf.utils.test_utils import login_testing_unauthorized, TestCase
1918
import ietf.stats.views
@@ -231,33 +230,38 @@ def test_review_stats(self):
231230
@patch('requests.get')
232231
def test_get_meeting_registration_data(self, mock_get):
233232
'''Test function to get reg data. Confirm leading/trailing spaces stripped'''
234-
response = Response()
235-
response.status_code = 200
236-
response._content = b'[{"LastName":"Smith ","FirstName":" John","Company":"ABC","Country":"US","Email":"john.doe@example.us"}]'
237-
mock_get.return_value = response
238-
meeting = MeetingFactory(type_id='ietf', date=datetime.date(2016,7,14), number="96")
233+
person = PersonFactory()
234+
data = {
235+
'LastName': person.last_name() + ' ',
236+
'FirstName': person.first_name(),
237+
'Company': 'ABC',
238+
'Country': 'US',
239+
'Email': person.email().address,
240+
'RegType': 'onsite'
241+
}
242+
data2 = data.copy()
243+
data2['RegType'] = 'hackathon'
244+
response_a = Response()
245+
response_a.status_code = 200
246+
response_a._content = json.dumps([data, data2]).encode('utf8')
247+
# second response one less record, it's been deleted
248+
response_b = Response()
249+
response_b.status_code = 200
250+
response_b._content = json.dumps([data]).encode('utf8')
251+
# mock_get.return_value = response
252+
mock_get.side_effect = [response_a, response_b]
253+
meeting = MeetingFactory(type_id='ietf', date=datetime.date(2016, 7, 14), number="96")
239254
get_meeting_registration_data(meeting)
240-
query = MeetingRegistration.objects.filter(first_name='John',last_name='Smith',country_code='US')
241-
self.assertTrue(query.count(), 1)
242-
self.assertTrue(isinstance(query[0].person,Person))
243-
244-
@patch('requests.get')
245-
def test_get_meeting_registration_data_user_exists(self, mock_get):
246-
response = Response()
247-
response.status_code = 200
248-
response._content = b'[{"LastName":"Smith","FirstName":"John","Company":"ABC","Country":"US","Email":"john.doe@example.us"}]'
249-
email = "john.doe@example.us"
250-
user = User.objects.create(username=email)
251-
user.save()
252-
253-
mock_get.return_value = response
254-
meeting = MeetingFactory(type_id='ietf', date=datetime.date(2016,7,14), number="96")
255+
query = MeetingRegistration.objects.filter(
256+
first_name=person.first_name(),
257+
last_name=person.last_name(),
258+
country_code='US')
259+
self.assertEqual(query.count(), 2)
260+
self.assertEqual(query.filter(reg_type='onsite').count(), 1)
261+
self.assertEqual(query.filter(reg_type='hackathon').count(), 1)
262+
# call a second time to test delete
255263
get_meeting_registration_data(meeting)
256-
query = MeetingRegistration.objects.filter(first_name='John',last_name='Smith',country_code='US')
257-
emails = Email.objects.filter(address=email)
258-
self.assertTrue(query.count(), 1)
259-
self.assertTrue(isinstance(query[0].person, Person))
260-
self.assertTrue(len(emails)>=1)
261-
self.assertEqual(query[0].person, emails[0].person)
262-
263-
264+
query = MeetingRegistration.objects.filter(meeting=meeting, email=person.email())
265+
self.assertEqual(query.count(), 1)
266+
self.assertEqual(query.filter(reg_type='onsite').count(), 1)
267+
self.assertEqual(query.filter(reg_type='hackathon').count(), 0)

ietf/stats/utils.py

Lines changed: 28 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,17 @@
77
from collections import defaultdict
88

99
from django.conf import settings
10-
from django.contrib.auth.models import User
1110

1211
import debug # pyflakes:ignore
1312

1413
from ietf.stats.models import AffiliationAlias, AffiliationIgnoredEnding, CountryAlias, MeetingRegistration
1514
from ietf.name.models import CountryName
16-
from ietf.person.models import Person, Email, Alias
17-
from ietf.person.name import unidecode_name
15+
from ietf.person.models import Person, Email
1816
from ietf.utils.log import log
1917

18+
import logging
19+
logger = logging.getLogger('django')
20+
2021

2122
def compile_affiliation_ending_stripping_regexp():
2223
parts = []
@@ -250,7 +251,7 @@ def get_meeting_registration_data(meeting):
250251
raise RuntimeError("Could not decode response from registrations API: '%s...'" % (response.content[:64], ))
251252

252253
records = MeetingRegistration.objects.filter(meeting_id=meeting.pk).select_related('person')
253-
meeting_registrations = {r.email:r for r in records}
254+
meeting_registrations = {(r.email, r.reg_type):r for r in records}
254255
for registration in decoded:
255256
person = None
256257
# capture the stripped registration values for later use
@@ -259,11 +260,15 @@ def get_meeting_registration_data(meeting):
259260
affiliation = registration['Company'].strip()
260261
country_code = registration['Country'].strip()
261262
address = registration['Email'].strip()
262-
if address in meeting_registrations:
263-
object = meeting_registrations[address]
263+
reg_type = registration['RegType'].strip()
264+
if (address, reg_type) in meeting_registrations:
265+
object = meeting_registrations.pop((address, reg_type))
264266
created = False
265267
else:
266-
object = MeetingRegistration.objects.create(meeting_id=meeting.pk, email=address)
268+
object = MeetingRegistration.objects.create(
269+
meeting_id=meeting.pk,
270+
email=address,
271+
reg_type=reg_type)
267272
created = True
268273

269274
if (object.first_name != first_name[:200] or
@@ -286,75 +291,31 @@ def get_meeting_registration_data(meeting):
286291
person = emails.first().person
287292
# Create a new Person object
288293
else:
289-
try:
290-
# Normalize all-caps or all-lower entries. Don't touch
291-
# others, there might be names properly spelled with
292-
# internal uppercase letters.
293-
if ( ( first_name == first_name.upper() or first_name == first_name.lower() )
294-
and ( last_name == last_name.upper() or last_name == last_name.lower() ) ):
295-
first_name = first_name.capitalize()
296-
last_name = last_name.capitalize()
297-
regname = "%s %s" % (first_name, last_name)
298-
# if there are any unicode characters decode the string to ascii
299-
ascii_name = unidecode_name(regname)
300-
301-
# Create a new user object if it does not exist already
302-
# if the user already exists do not try to create a new one
303-
users = User.objects.filter(username=address)
304-
if users.exists():
305-
user = users.first()
306-
else:
307-
# Create a new user.
308-
user = User.objects.create(
309-
first_name=first_name[:30],
310-
last_name=last_name[:30],
311-
username=address,
312-
email=address,
313-
)
314-
315-
try:
316-
person = user.person
317-
except Person.DoesNotExist:
318-
aliases = Alias.objects.filter(name=regname)
319-
if aliases.exists():
320-
person = aliases.first().person
321-
else:
322-
# Create the new Person object.
323-
person = Person.objects.create(
324-
name=regname,
325-
ascii=ascii_name,
326-
user=user,
327-
)
328-
329-
# Create an associated Email address for this Person
330-
try:
331-
email = Email.objects.get(person=person, address=address[:64])
332-
except Email.DoesNotExist:
333-
email = Email.objects.create(person=person, address=address[:64], origin='registration: ietf-%s'%meeting.number)
334-
335-
# If this is the only email address, set primary to true.
336-
# If the person already existed (found through Alias) and
337-
# had email addresses, we don't do this.
338-
if Email.objects.filter(person=person).count() == 1:
339-
email.primary = True
340-
email.save()
341-
except:
342-
debug.show('first_name')
343-
debug.show('last_name')
344-
debug.show('regname')
345-
debug.show('user')
346-
debug.show('aliases')
347-
raise
294+
logger.error("No Person record for registration. email={}".format(address))
348295
# update the person object to an actual value
349296
object.person = person
350297
object.save()
351298

352299
if created:
353300
num_created += 1
354301
num_processed += 1
302+
303+
# handle deleted registrations, if count is reasonable
304+
# any registrations left in meeting_registrations no longer exist in reg
305+
# so must have been deleted
306+
if 0 < len(meeting_registrations) < 5:
307+
for r in meeting_registrations:
308+
try:
309+
MeetingRegistration.objects.get(meeting=meeting,email=r[0],reg_type=r[1]).delete()
310+
logger.info('Removing deleted registration. email={}, reg_type={}'.format(r[0], r[1]))
311+
except MeetingRegistration.DoesNotExist:
312+
pass
355313
else:
356314
raise RuntimeError("Bad response from registrations API: %s, '%s'" % (response.status_code, response.content))
357-
num_total = MeetingRegistration.objects.filter(meeting_id=meeting.pk).count()
315+
num_total = MeetingRegistration.objects.filter(
316+
meeting_id=meeting.pk,
317+
attended=True,
318+
reg_type__in=['onsite', 'remote']).count()
358319
if meeting.attendees is None or num_total > meeting.attendees:
359320
meeting.attendees = num_total
360321
meeting.save()

0 commit comments

Comments
 (0)