diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py index fbf06c279..0732d394b 100644 --- a/ietf/nomcom/tests.py +++ b/ietf/nomcom/tests.py @@ -2569,6 +2569,59 @@ class rfc8989EligibilityTests(TestCase): 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): diff --git a/ietf/nomcom/utils.py b/ietf/nomcom/utils.py index d5a69627d..2bf63e194 100644 --- a/ietf/nomcom/utils.py +++ b/ietf/nomcom/utils.py @@ -10,6 +10,7 @@ import os 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 @@ -28,7 +29,7 @@ from ietf.doc.models import DocEvent, NewRevisionDocEvent 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 @@ -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() @@ -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: @@ -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, @@ -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) @@ -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() diff --git a/ietf/stats/factories.py b/ietf/stats/factories.py index 9c790d181..6e160dd1b 100644 --- a/ietf/stats/factories.py +++ b/ietf/stats/factories.py @@ -12,6 +12,7 @@ class MeetingRegistrationFactory(factory.django.DjangoModelFactory): 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 \ No newline at end of file + attended = True