From 911999244497079617833f9a9faff637616b486d Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Fri, 30 Sep 2022 10:53:57 -0500 Subject: [PATCH 1/8] fix: correct meeting attendance calculations --- ietf/meeting/models.py | 35 ++++++++++++++++++++++++----------- ietf/meeting/tests_models.py | 20 ++++++++++++++++---- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index 417a6ac9b87..925eff763d5 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -244,18 +244,31 @@ def get_attendance(self): number = self.get_number() if number is None or number < 110: return None - Attendance = namedtuple('Attendance', 'onsite online') + Attendance = namedtuple('Attendance', 'onsite online') # TODO: these should probably be 'onsite remote' + + # MeetingRegistration.attended started conflating badge-pickup and session attendance before IETF 114. + # We've separated session attendence off to ietf.meeting.Attended, but need to report attendance at older + # meetings correctly. + + attended_per_meetingregistration = ( + Q(meetingregistration__meeting=self) & ( + Q(meetingregistration__attended=True) | + Q(meetingregistration__checkedin=True) + ) + ) + attended_per_meeting_attended = ( + Q(attended__session__meeting=self) + # Note that we are not filtering to plenary, wg, or rg sessions + # as we do for nomcom eligibility - if picking up a badge (see above) + # is good enough, just attending e.g. a training session is also good enough + ) + attended = Person.objects.filter( + attended_per_meetingregistration | attended_per_meeting_attended + ).distinct() + return Attendance( - onsite=Person.objects.filter( - meetingregistration__meeting=self, - meetingregistration__attended=True, - meetingregistration__reg_type__contains='in_person', - ).distinct().count(), - online=Person.objects.filter( - meetingregistration__meeting=self, - meetingregistration__attended=True, - meetingregistration__reg_type__contains='remote', - ).distinct().count(), + onsite=attended.filter(meetingregistration__reg_type='onsite').count(), + online=attended.filter(meetingregistration__reg_type='remote').count() ) @property diff --git a/ietf/meeting/tests_models.py b/ietf/meeting/tests_models.py index dea31dc044d..ea6587c1e3d 100644 --- a/ietf/meeting/tests_models.py +++ b/ietf/meeting/tests_models.py @@ -3,7 +3,7 @@ """Tests of models in the Meeting application""" import datetime -from ietf.meeting.factories import MeetingFactory, SessionFactory +from ietf.meeting.factories import MeetingFactory, SessionFactory, AttendedFactory from ietf.stats.factories import MeetingRegistrationFactory from ietf.utils.test_utils import TestCase @@ -17,8 +17,8 @@ def test_get_attendance_pre110(self): MeetingRegistrationFactory.create_batch(5, meeting=meeting, reg_type='in_person') self.assertIsNone(meeting.get_attendance()) - def test_get_attendance(self): - """Post-110 meetings do calculate attendance""" + def test_get_attendance_110(self): + """Look at attendance as captured at 110""" meeting = MeetingFactory(type_id='ietf', number='110') # start with attendees that should be ignored @@ -38,7 +38,7 @@ def test_get_attendance(self): self.assertEqual(attendance.onsite, 0) # and the same for onsite attendees - MeetingRegistrationFactory.create_batch(5, meeting=meeting, reg_type='in_person') + MeetingRegistrationFactory.create_batch(5, meeting=meeting, reg_type='onsite') MeetingRegistrationFactory(meeting=meeting, reg_type='in_person', attended=False) attendance = meeting.get_attendance() self.assertIsNotNone(attendance) @@ -52,6 +52,18 @@ def test_get_attendance(self): self.assertEqual(attendance.online, 0) self.assertEqual(attendance.onsite, 5) + def test_get_attendance_113(self): + """Simulate IETF 113 attendance gathering data""" + meeting = MeetingFactory(type_id='ietf', number='113') + MeetingRegistrationFactory(meeting=meeting, reg_type='onsite', attended=True, checkedin=False) + MeetingRegistrationFactory(meeting=meeting, reg_type='onsite', attended=False, checkedin=True) + p1 = MeetingRegistrationFactory(meeting=meeting, reg_type='onsite', attended=False, checkedin=False).person + AttendedFactory(session__meeting=meeting, person=p1) + p2 = MeetingRegistrationFactory(meeting=meeting, reg_type='remote', attended=False, checkedin=False).person + AttendedFactory(session__meeting=meeting, person=p2) + attendance = meeting.get_attendance() + self.assertEqual(attendance.onsite, 3) + self.assertEqual(attendance.online, 1) class SessionTests(TestCase): def test_chat_archive_url_with_jabber(self): From de526f6489467133d756c82d05bc8d667fe5ec52 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Fri, 30 Sep 2022 13:00:35 -0500 Subject: [PATCH 2/8] test: change meetingregistration factory defaults --- ietf/meeting/tests_models.py | 6 +++--- ietf/nomcom/tests.py | 10 +++++----- ietf/stats/factories.py | 3 ++- ietf/stats/utils.py | 7 +++++-- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/ietf/meeting/tests_models.py b/ietf/meeting/tests_models.py index ea6587c1e3d..66f01a95919 100644 --- a/ietf/meeting/tests_models.py +++ b/ietf/meeting/tests_models.py @@ -22,7 +22,7 @@ def test_get_attendance_110(self): meeting = MeetingFactory(type_id='ietf', number='110') # start with attendees that should be ignored - MeetingRegistrationFactory.create_batch(3, meeting=meeting, reg_type='') + MeetingRegistrationFactory.create_batch(3, meeting=meeting, reg_type='', attended=True) MeetingRegistrationFactory(meeting=meeting, reg_type='', attended=False) attendance = meeting.get_attendance() self.assertIsNotNone(attendance) @@ -30,7 +30,7 @@ def test_get_attendance_110(self): self.assertEqual(attendance.onsite, 0) # add online attendees with at least one who registered but did not attend - MeetingRegistrationFactory.create_batch(4, meeting=meeting, reg_type='remote') + MeetingRegistrationFactory.create_batch(4, meeting=meeting, reg_type='remote', attended=True) MeetingRegistrationFactory(meeting=meeting, reg_type='remote', attended=False) attendance = meeting.get_attendance() self.assertIsNotNone(attendance) @@ -38,7 +38,7 @@ def test_get_attendance_110(self): self.assertEqual(attendance.onsite, 0) # and the same for onsite attendees - MeetingRegistrationFactory.create_batch(5, meeting=meeting, reg_type='onsite') + MeetingRegistrationFactory.create_batch(5, meeting=meeting, reg_type='onsite', attended=True) MeetingRegistrationFactory(meeting=meeting, reg_type='in_person', attended=False) attendance = meeting.get_attendance() self.assertIsNotNone(attendance) diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py index fbf06c279f6..0b1b29902bd 100644 --- a/ietf/nomcom/tests.py +++ b/ietf/nomcom/tests.py @@ -2289,7 +2289,7 @@ def setUp(self): for combo in combinations(meetings,combo_len): p = PersonFactory() for m in combo: - MeetingRegistrationFactory(person=p, meeting=m) + MeetingRegistrationFactory(person=p, meeting=m, attended=True) if combo_len<3: self.ineligible_people.append(p) else: @@ -2302,7 +2302,7 @@ def setUp(self): self.other_date = datetime.date(2009,5,1) self.other_people = PersonFactory.create_batch(1) for date in (datetime.date(2009,3,1), datetime.date(2008,11,1), datetime.date(2008,7,1)): - MeetingRegistrationFactory(person=self.other_people[0],meeting__date=date, meeting__type_id='ietf') + MeetingRegistrationFactory(person=self.other_people[0],meeting__date=date, meeting__type_id='ietf', attended=True) def test_is_person_eligible(self): @@ -2347,7 +2347,7 @@ def setUp(self): for combo in combinations(meetings,combo_len): p = PersonFactory() for m in combo: - MeetingRegistrationFactory(person=p, meeting=m) + MeetingRegistrationFactory(person=p, meeting=m, attended=True) if combo_len<3: self.ineligible_people.append(p) else: @@ -2395,7 +2395,7 @@ def test_elig_by_meetings(self): for combo in combinations(prev_five,combo_len): p = PersonFactory() for m in combo: - MeetingRegistrationFactory(person=p, meeting=m) + MeetingRegistrationFactory(person=p, meeting=m, checkedin=True) AttendedFactory(session__meeting=m, session__type_id='plenary',person=p) if combo_len<3: ineligible_people.append(p) @@ -2585,7 +2585,7 @@ def test_volunteer(self): self.assertContains(r, 'NomCom is not accepting volunteers at this time', status_code=200) nomcom.is_accepting_volunteers = True nomcom.save() - MeetingRegistrationFactory(person=person, affiliation='mtg_affiliation') + MeetingRegistrationFactory(person=person, affiliation='mtg_affiliation', checkedin=True) r = self.client.get(url) self.assertContains(r, 'Volunteer for NomCom', status_code=200) self.assertContains(r, 'mtg_affiliation') diff --git a/ietf/stats/factories.py b/ietf/stats/factories.py index 9c790d18101..fdab17aed00 100644 --- a/ietf/stats/factories.py +++ b/ietf/stats/factories.py @@ -14,4 +14,5 @@ class Meta: person = factory.SubFactory(PersonFactory) first_name = factory.LazyAttribute(lambda obj: obj.person.first_name()) last_name = factory.LazyAttribute(lambda obj: obj.person.last_name()) - attended = True \ No newline at end of file + attended = False + checkedin = False diff --git a/ietf/stats/utils.py b/ietf/stats/utils.py index b6ff321e332..ca1163e073e 100644 --- a/ietf/stats/utils.py +++ b/ietf/stats/utils.py @@ -7,6 +7,7 @@ from collections import defaultdict from django.conf import settings +from django.db.models import Q import debug # pyflakes:ignore @@ -320,8 +321,10 @@ def get_meeting_registration_data(meeting): raise RuntimeError("Bad response from registrations API: %s, '%s'" % (response.status_code, response.content)) num_total = MeetingRegistration.objects.filter( meeting_id=meeting.pk, - attended=True, - reg_type__in=['onsite', 'remote']).count() + reg_type__in=['onsite', 'remote'] + ).filter( + Q(attended=True) | Q(checkedin=True) + ).count() if meeting.attendees is None or num_total > meeting.attendees: meeting.attendees = num_total meeting.save() From d1373c3f2c65165ac096f7cb6a124b604c0ccd45 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Fri, 30 Sep 2022 14:23:40 -0500 Subject: [PATCH 3/8] test: Setup stats tests to verify counts honor meeting.Attended But the tests aren't actually looking to see what numbers get generated yet. --- ietf/stats/tests.py | 9 +++++---- ietf/stats/views.py | 11 ++++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/ietf/stats/tests.py b/ietf/stats/tests.py index d34d7b11b44..32a0dfea805 100644 --- a/ietf/stats/tests.py +++ b/ietf/stats/tests.py @@ -21,12 +21,13 @@ from ietf.doc.factories import WgDraftFactory, WgRfcFactory from ietf.doc.models import Document, DocAlias, State, RelatedDocument, NewRevisionDocEvent, DocumentAuthor from ietf.group.factories import RoleFactory -from ietf.meeting.factories import MeetingFactory +from ietf.meeting.factories import MeetingFactory, AttendedFactory from ietf.person.factories import PersonFactory from ietf.person.models import Person, Email from ietf.name.models import FormalLanguageName, DocRelationshipName, CountryName from ietf.review.factories import ReviewRequestFactory, ReviewerSettingsFactory, ReviewAssignmentFactory from ietf.stats.models import MeetingRegistration, CountryAlias +from ietf.stats.factories import MeetingRegistrationFactory from ietf.stats.utils import get_meeting_registration_data @@ -122,11 +123,11 @@ def test_document_stats(self): def test_meeting_stats(self): # create some data for the statistics meeting = MeetingFactory(type_id='ietf', date=datetime.date.today(), number="96") - MeetingRegistration.objects.create(first_name='John', last_name='Smith', country_code='US', email="john.smith@example.us", meeting=meeting, attended=True) + MeetingRegistrationFactory(first_name='John', last_name='Smith', country_code='US', email="john.smith@example.us", meeting=meeting, attended=True) CountryAlias.objects.get_or_create(alias="US", country=CountryName.objects.get(slug="US")) - MeetingRegistration.objects.create(first_name='Jaume', last_name='Guillaume', country_code='FR', email="jaume.guillaume@example.fr", meeting=meeting, attended=True) + p = MeetingRegistrationFactory(first_name='Jaume', last_name='Guillaume', country_code='FR', email="jaume.guillaume@example.fr", meeting=meeting, attended=False).person CountryAlias.objects.get_or_create(alias="FR", country=CountryName.objects.get(slug="FR")) - + AttendedFactory(session__meeting=meeting,person=p) # check redirect url = urlreverse(ietf.stats.views.meeting_stats) diff --git a/ietf/stats/views.py b/ietf/stats/views.py index 593fc1947ee..1667387bce3 100644 --- a/ietf/stats/views.py +++ b/ietf/stats/views.py @@ -817,8 +817,10 @@ def reg_name(r): if meeting and any(stats_type == t[0] for t in possible_stats_types): attendees = MeetingRegistration.objects.filter( meeting=meeting, - attended=True, - reg_type__in=['onsite', 'remote']) + reg_type__in=['onsite', 'remote'] + ).filter( + Q( attended=True) | Q( checkedin=True ) + ) if stats_type == "country": stats_title = "Number of attendees for {} {} per country".format(meeting.type.name, meeting.number) @@ -893,7 +895,10 @@ def reg_name(r): attendees = MeetingRegistration.objects.filter( meeting__type="ietf", attended=True, - reg_type__in=['onsite', 'remote']).select_related('meeting') + reg_type__in=['onsite', 'remote'] + ).filter( + Q( attended=True) | Q( checkedin=True ) + ).select_related('meeting') if stats_type == "overview": stats_title = "Number of attendees per meeting" From 8549cd0ba4b7433a6ec443ad7e592fb5213de221 Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Wed, 5 Oct 2022 15:13:04 -0300 Subject: [PATCH 4/8] test: add test for attendance cross-talk between meetings --- ietf/meeting/tests_models.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/ietf/meeting/tests_models.py b/ietf/meeting/tests_models.py index 66f01a95919..4da330c1d45 100644 --- a/ietf/meeting/tests_models.py +++ b/ietf/meeting/tests_models.py @@ -65,6 +65,28 @@ def test_get_attendance_113(self): self.assertEqual(attendance.onsite, 3) self.assertEqual(attendance.online, 1) + def test_get_attendance_keeps_meetings_distinct(self): + """No cross-talk between attendance for different meetings""" + # numbers are arbitrary here + first_mtg = MeetingFactory(type_id='ietf', number='114') + second_mtg = MeetingFactory(type_id='ietf', number='115') + + # Create a person who attended a remote session for first_mtg and onsite for second_mtg without + # checking in for either. + p = MeetingRegistrationFactory(meeting=second_mtg, reg_type='onsite', attended=False, checkedin=False).person + AttendedFactory(session__meeting=first_mtg, person=p) + MeetingRegistrationFactory(meeting=first_mtg, person=p, reg_type='remote', attended=False, checkedin=False) + AttendedFactory(session__meeting=second_mtg, person=p) + + att = first_mtg.get_attendance() + self.assertEqual(att.onsite, 0) + self.assertEqual(att.online, 1) + + att = second_mtg.get_attendance() + self.assertEqual(att.onsite, 1) + self.assertEqual(att.online, 0) + + class SessionTests(TestCase): def test_chat_archive_url_with_jabber(self): # datatracker 8.8.0 rolled out on 2022-07-15. Before that, chat logs were jabber logs hosted at www.ietf.org. From a73fd559307f8451febbd278b908b3079f14a86e Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Wed, 5 Oct 2022 23:19:35 -0300 Subject: [PATCH 5/8] fix: limit attendance count query to single meeting --- ietf/meeting/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index 925eff763d5..530941f8c2f 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -267,8 +267,8 @@ def get_attendance(self): ).distinct() return Attendance( - onsite=attended.filter(meetingregistration__reg_type='onsite').count(), - online=attended.filter(meetingregistration__reg_type='remote').count() + onsite=attended.filter(meetingregistration__meeting=self, meetingregistration__reg_type='onsite').count(), + online=attended.filter(meetingregistration__meeting=self, meetingregistration__reg_type='remote').count() ) @property From df82bfa31c8325dcf75899d86cae5a80aa42a9cb Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Thu, 6 Oct 2022 11:37:15 -0300 Subject: [PATCH 6/8] refactor: rename attendance.online to .remote --- ietf/meeting/models.py | 4 ++-- ietf/meeting/tests_models.py | 14 +++++++------- ietf/templates/meeting/proceedings/title.html | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index 530941f8c2f..cd095a9c516 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -244,7 +244,7 @@ def get_attendance(self): number = self.get_number() if number is None or number < 110: return None - Attendance = namedtuple('Attendance', 'onsite online') # TODO: these should probably be 'onsite remote' + Attendance = namedtuple('Attendance', 'onsite remote') # MeetingRegistration.attended started conflating badge-pickup and session attendance before IETF 114. # We've separated session attendence off to ietf.meeting.Attended, but need to report attendance at older @@ -268,7 +268,7 @@ def get_attendance(self): return Attendance( onsite=attended.filter(meetingregistration__meeting=self, meetingregistration__reg_type='onsite').count(), - online=attended.filter(meetingregistration__meeting=self, meetingregistration__reg_type='remote').count() + remote=attended.filter(meetingregistration__meeting=self, meetingregistration__reg_type='remote').count() ) @property diff --git a/ietf/meeting/tests_models.py b/ietf/meeting/tests_models.py index 4da330c1d45..71f5b93610d 100644 --- a/ietf/meeting/tests_models.py +++ b/ietf/meeting/tests_models.py @@ -26,7 +26,7 @@ def test_get_attendance_110(self): MeetingRegistrationFactory(meeting=meeting, reg_type='', attended=False) attendance = meeting.get_attendance() self.assertIsNotNone(attendance) - self.assertEqual(attendance.online, 0) + self.assertEqual(attendance.remote, 0) self.assertEqual(attendance.onsite, 0) # add online attendees with at least one who registered but did not attend @@ -34,7 +34,7 @@ def test_get_attendance_110(self): MeetingRegistrationFactory(meeting=meeting, reg_type='remote', attended=False) attendance = meeting.get_attendance() self.assertIsNotNone(attendance) - self.assertEqual(attendance.online, 4) + self.assertEqual(attendance.remote, 4) self.assertEqual(attendance.onsite, 0) # and the same for onsite attendees @@ -42,14 +42,14 @@ def test_get_attendance_110(self): MeetingRegistrationFactory(meeting=meeting, reg_type='in_person', attended=False) attendance = meeting.get_attendance() self.assertIsNotNone(attendance) - self.assertEqual(attendance.online, 4) + self.assertEqual(attendance.remote, 4) self.assertEqual(attendance.onsite, 5) # and once more after removing all the online attendees meeting.meetingregistration_set.filter(reg_type='remote').delete() attendance = meeting.get_attendance() self.assertIsNotNone(attendance) - self.assertEqual(attendance.online, 0) + self.assertEqual(attendance.remote, 0) self.assertEqual(attendance.onsite, 5) def test_get_attendance_113(self): @@ -63,7 +63,7 @@ def test_get_attendance_113(self): AttendedFactory(session__meeting=meeting, person=p2) attendance = meeting.get_attendance() self.assertEqual(attendance.onsite, 3) - self.assertEqual(attendance.online, 1) + self.assertEqual(attendance.remote, 1) def test_get_attendance_keeps_meetings_distinct(self): """No cross-talk between attendance for different meetings""" @@ -80,11 +80,11 @@ def test_get_attendance_keeps_meetings_distinct(self): att = first_mtg.get_attendance() self.assertEqual(att.onsite, 0) - self.assertEqual(att.online, 1) + self.assertEqual(att.remote, 1) att = second_mtg.get_attendance() self.assertEqual(att.onsite, 1) - self.assertEqual(att.online, 0) + self.assertEqual(att.remote, 0) class SessionTests(TestCase): diff --git a/ietf/templates/meeting/proceedings/title.html b/ietf/templates/meeting/proceedings/title.html index 6e7fe8068ab..afeaac8ea41 100644 --- a/ietf/templates/meeting/proceedings/title.html +++ b/ietf/templates/meeting/proceedings/title.html @@ -13,9 +13,9 @@

{% if attendance is not None %}
{% if attendance.onsite > 0 %} - {{ attendance.onsite }} onsite participant{{ attendance.onsite|pluralize }}{% if attendance.online > 0 %},{% endif %} + {{ attendance.onsite }} onsite participant{{ attendance.onsite|pluralize }}{% if attendance.remote > 0 %},{% endif %} {% endif %} - {% if attendance.online > 0 %}{{ attendance.online }} online participant{{ attendance.online|pluralize }}{% endif %} + {% if attendance.remote > 0 %}{{ attendance.remote }} online participant{{ attendance.remote|pluralize }}{% endif %}
{% endif %} From 190796a7f5655529245f2ee75d20df15f5748a8e Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 12 Oct 2022 16:12:09 -0500 Subject: [PATCH 7/8] fix: only count a given person as onsite or remote, but never both --- ietf/meeting/models.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index cd095a9c516..b863f5254d4 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -266,9 +266,13 @@ def get_attendance(self): attended_per_meetingregistration | attended_per_meeting_attended ).distinct() + onsite=set(attended.filter(meetingregistration__meeting=self, meetingregistration__reg_type='onsite')) + remote=set(attended.filter(meetingregistration__meeting=self, meetingregistration__reg_type='remote')) + remote.difference_update(onsite) + return Attendance( - onsite=attended.filter(meetingregistration__meeting=self, meetingregistration__reg_type='onsite').count(), - remote=attended.filter(meetingregistration__meeting=self, meetingregistration__reg_type='remote').count() + onsite=len(onsite), + remote=len(remote) ) @property From 368bdc08ffba9adf4d9d40cc210f7296c6e71d22 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Wed, 12 Oct 2022 16:24:10 -0500 Subject: [PATCH 8/8] test: align tests with cleanup --- ietf/nomcom/tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py index 2bd295842a6..27f55329b61 100644 --- a/ietf/nomcom/tests.py +++ b/ietf/nomcom/tests.py @@ -2395,7 +2395,7 @@ def test_elig_by_meetings(self): for combo in combinations(prev_five,combo_len): p = PersonFactory() for m in combo: - MeetingRegistrationFactory(person=p, meeting=m, checkedin=True) + MeetingRegistrationFactory(person=p, meeting=m, attended=True) # not checkedin because this forces looking at older meetings AttendedFactory(session__meeting=m, session__type_id='plenary',person=p) if combo_len<3: ineligible_people.append(p) @@ -2710,7 +2710,7 @@ def test_decorate_volunteers_with_qualifications(self): ('106', datetime.date(2019, 11, 16)), ]] for m in meetings: - MeetingRegistrationFactory(meeting=m,person=meeting_person) + MeetingRegistrationFactory(meeting=m, person=meeting_person, attended=True) AttendedFactory(session__meeting=m, session__type_id='plenary', person=meeting_person) nomcom.volunteer_set.create(person=meeting_person)