Skip to content

Commit

Permalink
feat: include checkins at the registration desk in nomcom eligibility…
Browse files Browse the repository at this point in the history
… calculations (#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
  • Loading branch information
rjsparks authored Oct 7, 2022
1 parent cb9e576 commit 97d2180
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 15 deletions.
53 changes: 53 additions & 0 deletions ietf/nomcom/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2569,6 +2569,59 @@ def test_elig_by_author(self):
self.assertEqual(set(list_eligible(nomcom=nomcom)),set(eligible))
Person.objects.filter(pk__in=[p.pk for p in eligible.union(ineligible)]).delete()

class rfc8989bisEligibilityTests(TestCase):

def setUp(self):
super().setUp()
self.nomcom = NomComFactory(group__acronym='nomcom2023', populate_personnel=False, first_call_for_volunteers=datetime.date(2023,5,15))
self.meetings = [
MeetingFactory(number=number, date=date, type_id='ietf') for number,date in [
('115', datetime.date(2022, 11, 5)),
('114', datetime.date(2022, 7, 23)),
('113', datetime.date(2022, 3, 19)),
('112', datetime.date(2021, 11, 8)),
('111', datetime.date(2021, 7, 26)),
]
]
# make_immutable_test_data makes things this test does not want
Role.objects.filter(name_id__in=('chair','secr')).delete()

def test_registration_is_not_enough(self):
p = PersonFactory()
for meeting in self.meetings:
MeetingRegistrationFactory(person=p, meeting=meeting, checkedin=False)
self.assertFalse(is_eligible(p, self.nomcom))

def test_elig_by_meetings(self):
eligible_people = list()
ineligible_people = list()
attendance_methods = ('checkedin', 'session', 'both')
for combo_len in range(0,6): # Someone might register for 0 to 5 previous meetings
for combo in combinations(self.meetings, combo_len):
# Cover cases where someone
# - checked in, but attended no sessions
# - checked in _and_ attended sessions
# - didn't check_in but attended sessions
# (Intentionally not covering the permutations of those cases)
for method in attendance_methods:
p = PersonFactory()
for meeting in combo:
MeetingRegistrationFactory(person=p, meeting=meeting, reg_type='onsite', checkedin=(method in ('checkedin', 'both')))
if method in ('session', 'both'):
AttendedFactory(session__meeting=meeting, session__type_id='plenary',person=p)
if combo_len<3:
ineligible_people.append(p)
else:
eligible_people.append(p)

self.assertEqual(set(eligible_people),set(list_eligible(self.nomcom)))

for person in eligible_people:
self.assertTrue(is_eligible(person,self.nomcom))

for person in ineligible_people:
self.assertFalse(is_eligible(person,self.nomcom))

class VolunteerTests(TestCase):

def test_volunteer(self):
Expand Down
51 changes: 37 additions & 14 deletions ietf/nomcom/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import re
import tempfile

from collections import defaultdict
from email import message_from_string, message_from_bytes
from email.header import decode_header
from email.iterators import typed_subpart_iterator
Expand All @@ -28,7 +29,7 @@
from ietf.group.models import Group, Role
from ietf.person.models import Email, Person
from ietf.mailtrigger.utils import gather_address_lists
from ietf.meeting.models import Meeting
from ietf.meeting.models import Meeting, Attended
from ietf.utils.pipe import pipe
from ietf.utils.mail import send_mail_text, send_mail, get_payload_text
from ietf.utils.log import log
Expand Down Expand Up @@ -510,6 +511,8 @@ def list_eligible(nomcom=None, date=None, base_qs=None):
return list_eligible_8788(date=eligibility_date, base_qs=base_qs)
elif eligibility_date.year in (2021,2022):
return list_eligible_8989(date=eligibility_date, base_qs=base_qs)
elif eligibility_date.year > 2022:
return list_eligible_8989bis(date=eligibility_date, base_qs=base_qs)
else:
return Person.objects.none()

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

def list_eligible_8788(date, base_qs=None):
if not base_qs:
base_qs = Person.objects.all()
previous_five = Meeting.objects.filter(number__in=['102','103','104','105','106'])
return remove_disqualified(three_of_five_eligible(previous_five=previous_five, queryset=base_qs))
return remove_disqualified(three_of_five_eligible_8713(previous_five=previous_five, queryset=base_qs))

def get_8989_eligibility_querysets(date, base_qs):
return get_threerule_eligibility_querysets(date, base_qs, three_of_five_callable=three_of_five_eligible_8713)

def get_8989bis_eligibility_querysets(date, base_qs):
return get_threerule_eligibility_querysets(date, base_qs, three_of_five_callable=three_of_five_eligible_8989bis)

def get_threerule_eligibility_querysets(date, base_qs, three_of_five_callable):
if not base_qs:
base_qs = Person.objects.all()

previous_five = previous_five_meetings(date)
three_of_five_qs = new_three_of_five_eligible(previous_five=previous_five, queryset=base_qs)
three_of_five_qs = three_of_five_callable(previous_five=previous_five, queryset=base_qs)

# If date is Feb 29, neither 3 nor 5 years ago has a Feb 29. Use Feb 28 instead.
if date.month == 2 and date.day == 29:
Expand All @@ -564,7 +573,7 @@ def get_8989_eligibility_querysets(date, base_qs):
Q(role__name_id__in=('chair','secr'),
role__group__state_id='active',
role__group__type_id='wg',
role__group__time__lte=date,
role__group__time__lte=date, ## TODO - inspect - lots of things affect group__time...
)
# was an officer since the given date (I think this is wrong - it looks at when roles _start_, not when roles end)
| Q(rolehistory__group__time__gte=three_years_ago,
Expand All @@ -589,7 +598,15 @@ def list_eligible_8989(date, base_qs=None):
if not base_qs:
base_qs = Person.objects.all()
three_of_five_qs, officer_qs, author_qs = get_8989_eligibility_querysets(date, base_qs)
# Would be nice to use queryset union here, but the annotations in the three existing querysets make that difficult
three_of_five_pks = three_of_five_qs.values_list('pk',flat=True)
officer_pks = officer_qs.values_list('pk',flat=True)
author_pks = author_qs.values_list('pk',flat=True)
return remove_disqualified(Person.objects.filter(pk__in=set(three_of_five_pks).union(set(officer_pks)).union(set(author_pks))))

def list_eligible_8989bis(date, base_qs=None):
if not base_qs:
base_qs = Person.objects.all()
three_of_five_qs, officer_qs, author_qs = get_8989bis_eligibility_querysets(date, base_qs)
three_of_five_pks = three_of_five_qs.values_list('pk',flat=True)
officer_pks = officer_qs.values_list('pk',flat=True)
author_pks = author_qs.values_list('pk',flat=True)
Expand Down Expand Up @@ -624,28 +641,34 @@ def previous_five_meetings(date = None):
date = datetime.date.today()
return Meeting.objects.filter(type='ietf',date__lte=date).order_by('-date')[:5]

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

def new_three_of_five_eligible(previous_five, queryset=None):
""" Return a list of Person records who attended at least
def three_of_five_eligible_8989bis(previous_five, queryset=None):
""" Return a list of Person records who attended at least
3 of the 5 type_id='ietf' meetings before the given
date. Does not disqualify anyone based on held roles.
This 'new' variant bases the calculation on the Meeting.Session model rather than Stats.MeetingRegistration
This variant bases the calculation on Meeting.Session and MeetingRegistration.checked_in
Leadership will have to create a new RFC specifying eligibility (RFC8989 is timing out) before it can be used.
"""
if queryset is None:
queryset = Person.objects.all()
return queryset.filter(
Q(attended__session__meeting__in=list(previous_five)),
Q(attended__session__type='plenary')|Q(attended__session__group__type__in=['wg','rg'])
).annotate(mtg_count=Count('attended__session__meeting',distinct=True)).filter(mtg_count__gte=3)

counts = defaultdict(lambda: 0)
for meeting in previous_five:
checked_in = meeting.meetingregistration_set.filter(reg_type='onsite', checkedin=True).values_list('person', flat=True)
sessions = meeting.session_set.filter(Q(type='plenary') | Q(group__type__in=['wg', 'rg']))
attended = Attended.objects.filter(session__in=sessions).values_list('person', flat=True)
for id in set(checked_in) | set(attended):
counts[id] += 1
return queryset.filter(pk__in=[id for id, count in counts.items() if count >= 3])

def suggest_affiliation(person):
recent_meeting = person.meetingregistration_set.order_by('-meeting__date').first()
Expand Down
3 changes: 2 additions & 1 deletion ietf/stats/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class Meta:

meeting = factory.SubFactory(MeetingFactory)
person = factory.SubFactory(PersonFactory)
reg_type = 'onsite'
first_name = factory.LazyAttribute(lambda obj: obj.person.first_name())
last_name = factory.LazyAttribute(lambda obj: obj.person.last_name())
attended = True
attended = True

0 comments on commit 97d2180

Please sign in to comment.