Skip to content

Commit 97d2180

Browse files
authored
feat: include checkins at the registration desk in nomcom eligibility calculations (ietf-tools#4519)
* chore: rename utility functions to reflect rfc guiding them * feat: include new checkedin flag in nomcom calculations * fix: reflect history a bit more accurately. * fix: address review comment on readability * fix: finish what c44000d started
1 parent cb9e576 commit 97d2180

3 files changed

Lines changed: 92 additions & 15 deletions

File tree

ietf/nomcom/tests.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2569,6 +2569,59 @@ def test_elig_by_author(self):
25692569
self.assertEqual(set(list_eligible(nomcom=nomcom)),set(eligible))
25702570
Person.objects.filter(pk__in=[p.pk for p in eligible.union(ineligible)]).delete()
25712571

2572+
class rfc8989bisEligibilityTests(TestCase):
2573+
2574+
def setUp(self):
2575+
super().setUp()
2576+
self.nomcom = NomComFactory(group__acronym='nomcom2023', populate_personnel=False, first_call_for_volunteers=datetime.date(2023,5,15))
2577+
self.meetings = [
2578+
MeetingFactory(number=number, date=date, type_id='ietf') for number,date in [
2579+
('115', datetime.date(2022, 11, 5)),
2580+
('114', datetime.date(2022, 7, 23)),
2581+
('113', datetime.date(2022, 3, 19)),
2582+
('112', datetime.date(2021, 11, 8)),
2583+
('111', datetime.date(2021, 7, 26)),
2584+
]
2585+
]
2586+
# make_immutable_test_data makes things this test does not want
2587+
Role.objects.filter(name_id__in=('chair','secr')).delete()
2588+
2589+
def test_registration_is_not_enough(self):
2590+
p = PersonFactory()
2591+
for meeting in self.meetings:
2592+
MeetingRegistrationFactory(person=p, meeting=meeting, checkedin=False)
2593+
self.assertFalse(is_eligible(p, self.nomcom))
2594+
2595+
def test_elig_by_meetings(self):
2596+
eligible_people = list()
2597+
ineligible_people = list()
2598+
attendance_methods = ('checkedin', 'session', 'both')
2599+
for combo_len in range(0,6): # Someone might register for 0 to 5 previous meetings
2600+
for combo in combinations(self.meetings, combo_len):
2601+
# Cover cases where someone
2602+
# - checked in, but attended no sessions
2603+
# - checked in _and_ attended sessions
2604+
# - didn't check_in but attended sessions
2605+
# (Intentionally not covering the permutations of those cases)
2606+
for method in attendance_methods:
2607+
p = PersonFactory()
2608+
for meeting in combo:
2609+
MeetingRegistrationFactory(person=p, meeting=meeting, reg_type='onsite', checkedin=(method in ('checkedin', 'both')))
2610+
if method in ('session', 'both'):
2611+
AttendedFactory(session__meeting=meeting, session__type_id='plenary',person=p)
2612+
if combo_len<3:
2613+
ineligible_people.append(p)
2614+
else:
2615+
eligible_people.append(p)
2616+
2617+
self.assertEqual(set(eligible_people),set(list_eligible(self.nomcom)))
2618+
2619+
for person in eligible_people:
2620+
self.assertTrue(is_eligible(person,self.nomcom))
2621+
2622+
for person in ineligible_people:
2623+
self.assertFalse(is_eligible(person,self.nomcom))
2624+
25722625
class VolunteerTests(TestCase):
25732626

25742627
def test_volunteer(self):

ietf/nomcom/utils.py

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import re
1111
import tempfile
1212

13+
from collections import defaultdict
1314
from email import message_from_string, message_from_bytes
1415
from email.header import decode_header
1516
from email.iterators import typed_subpart_iterator
@@ -28,7 +29,7 @@
2829
from ietf.group.models import Group, Role
2930
from ietf.person.models import Email, Person
3031
from ietf.mailtrigger.utils import gather_address_lists
31-
from ietf.meeting.models import Meeting
32+
from ietf.meeting.models import Meeting, Attended
3233
from ietf.utils.pipe import pipe
3334
from ietf.utils.mail import send_mail_text, send_mail, get_payload_text
3435
from ietf.utils.log import log
@@ -510,6 +511,8 @@ def list_eligible(nomcom=None, date=None, base_qs=None):
510511
return list_eligible_8788(date=eligibility_date, base_qs=base_qs)
511512
elif eligibility_date.year in (2021,2022):
512513
return list_eligible_8989(date=eligibility_date, base_qs=base_qs)
514+
elif eligibility_date.year > 2022:
515+
return list_eligible_8989bis(date=eligibility_date, base_qs=base_qs)
513516
else:
514517
return Person.objects.none()
515518

@@ -536,20 +539,26 @@ def list_eligible_8713(date, base_qs=None):
536539
if not base_qs:
537540
base_qs = Person.objects.all()
538541
previous_five = previous_five_meetings(date)
539-
return remove_disqualified(three_of_five_eligible(previous_five=previous_five, queryset=base_qs))
542+
return remove_disqualified(three_of_five_eligible_8713(previous_five=previous_five, queryset=base_qs))
540543

541544
def list_eligible_8788(date, base_qs=None):
542545
if not base_qs:
543546
base_qs = Person.objects.all()
544547
previous_five = Meeting.objects.filter(number__in=['102','103','104','105','106'])
545-
return remove_disqualified(three_of_five_eligible(previous_five=previous_five, queryset=base_qs))
548+
return remove_disqualified(three_of_five_eligible_8713(previous_five=previous_five, queryset=base_qs))
546549

547550
def get_8989_eligibility_querysets(date, base_qs):
551+
return get_threerule_eligibility_querysets(date, base_qs, three_of_five_callable=three_of_five_eligible_8713)
552+
553+
def get_8989bis_eligibility_querysets(date, base_qs):
554+
return get_threerule_eligibility_querysets(date, base_qs, three_of_five_callable=three_of_five_eligible_8989bis)
555+
556+
def get_threerule_eligibility_querysets(date, base_qs, three_of_five_callable):
548557
if not base_qs:
549558
base_qs = Person.objects.all()
550559

551560
previous_five = previous_five_meetings(date)
552-
three_of_five_qs = new_three_of_five_eligible(previous_five=previous_five, queryset=base_qs)
561+
three_of_five_qs = three_of_five_callable(previous_five=previous_five, queryset=base_qs)
553562

554563
# If date is Feb 29, neither 3 nor 5 years ago has a Feb 29. Use Feb 28 instead.
555564
if date.month == 2 and date.day == 29:
@@ -564,7 +573,7 @@ def get_8989_eligibility_querysets(date, base_qs):
564573
Q(role__name_id__in=('chair','secr'),
565574
role__group__state_id='active',
566575
role__group__type_id='wg',
567-
role__group__time__lte=date,
576+
role__group__time__lte=date, ## TODO - inspect - lots of things affect group__time...
568577
)
569578
# was an officer since the given date (I think this is wrong - it looks at when roles _start_, not when roles end)
570579
| Q(rolehistory__group__time__gte=three_years_ago,
@@ -589,7 +598,15 @@ def list_eligible_8989(date, base_qs=None):
589598
if not base_qs:
590599
base_qs = Person.objects.all()
591600
three_of_five_qs, officer_qs, author_qs = get_8989_eligibility_querysets(date, base_qs)
592-
# Would be nice to use queryset union here, but the annotations in the three existing querysets make that difficult
601+
three_of_five_pks = three_of_five_qs.values_list('pk',flat=True)
602+
officer_pks = officer_qs.values_list('pk',flat=True)
603+
author_pks = author_qs.values_list('pk',flat=True)
604+
return remove_disqualified(Person.objects.filter(pk__in=set(three_of_five_pks).union(set(officer_pks)).union(set(author_pks))))
605+
606+
def list_eligible_8989bis(date, base_qs=None):
607+
if not base_qs:
608+
base_qs = Person.objects.all()
609+
three_of_five_qs, officer_qs, author_qs = get_8989bis_eligibility_querysets(date, base_qs)
593610
three_of_five_pks = three_of_five_qs.values_list('pk',flat=True)
594611
officer_pks = officer_qs.values_list('pk',flat=True)
595612
author_pks = author_qs.values_list('pk',flat=True)
@@ -624,28 +641,34 @@ def previous_five_meetings(date = None):
624641
date = datetime.date.today()
625642
return Meeting.objects.filter(type='ietf',date__lte=date).order_by('-date')[:5]
626643

627-
def three_of_five_eligible(previous_five, queryset=None):
644+
def three_of_five_eligible_8713(previous_five, queryset=None):
628645
""" Return a list of Person records who attended at least
629646
3 of the 5 type_id='ietf' meetings before the given
630647
date. Does not disqualify anyone based on held roles.
648+
This variant bases the calculation on MeetingRegistration.attended
631649
"""
632650
if queryset is None:
633651
queryset = Person.objects.all()
634652
return queryset.filter(meetingregistration__meeting__in=list(previous_five),meetingregistration__attended=True).annotate(mtg_count=Count('meetingregistration')).filter(mtg_count__gte=3)
635653

636-
def new_three_of_five_eligible(previous_five, queryset=None):
637-
""" Return a list of Person records who attended at least
654+
def three_of_five_eligible_8989bis(previous_five, queryset=None):
655+
""" Return a list of Person records who attended at least
638656
3 of the 5 type_id='ietf' meetings before the given
639657
date. Does not disqualify anyone based on held roles.
640-
This 'new' variant bases the calculation on the Meeting.Session model rather than Stats.MeetingRegistration
658+
This variant bases the calculation on Meeting.Session and MeetingRegistration.checked_in
641659
Leadership will have to create a new RFC specifying eligibility (RFC8989 is timing out) before it can be used.
642660
"""
643661
if queryset is None:
644662
queryset = Person.objects.all()
645-
return queryset.filter(
646-
Q(attended__session__meeting__in=list(previous_five)),
647-
Q(attended__session__type='plenary')|Q(attended__session__group__type__in=['wg','rg'])
648-
).annotate(mtg_count=Count('attended__session__meeting',distinct=True)).filter(mtg_count__gte=3)
663+
664+
counts = defaultdict(lambda: 0)
665+
for meeting in previous_five:
666+
checked_in = meeting.meetingregistration_set.filter(reg_type='onsite', checkedin=True).values_list('person', flat=True)
667+
sessions = meeting.session_set.filter(Q(type='plenary') | Q(group__type__in=['wg', 'rg']))
668+
attended = Attended.objects.filter(session__in=sessions).values_list('person', flat=True)
669+
for id in set(checked_in) | set(attended):
670+
counts[id] += 1
671+
return queryset.filter(pk__in=[id for id, count in counts.items() if count >= 3])
649672

650673
def suggest_affiliation(person):
651674
recent_meeting = person.meetingregistration_set.order_by('-meeting__date').first()

ietf/stats/factories.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class Meta:
1212

1313
meeting = factory.SubFactory(MeetingFactory)
1414
person = factory.SubFactory(PersonFactory)
15+
reg_type = 'onsite'
1516
first_name = factory.LazyAttribute(lambda obj: obj.person.first_name())
1617
last_name = factory.LazyAttribute(lambda obj: obj.person.last_name())
17-
attended = True
18+
attended = True

0 commit comments

Comments
 (0)