Merged in [18975] from rjsparks@nostrum.com:

Add RFC 8989 nomcom eligibility calculations.
 - Legacy-Id: 18980
Note: SVN reference [18975] has been migrated to Git commit fd38a9bf96
This commit is contained in:
Robert Sparks 2021-05-07 19:37:28 +00:00
commit 85bf8cc39c
15 changed files with 616 additions and 70 deletions

View file

@ -365,3 +365,14 @@ class DocumentActionHolderFactory(factory.DjangoModelFactory):
document = factory.SubFactory(WgDraftFactory)
person = factory.SubFactory('ietf.person.factories.PersonFactory')
class DocumentAuthorFactory(factory.DjangoModelFactory):
class Meta:
model = DocumentAuthor
document = factory.SubFactory(DocumentFactory)
person = factory.SubFactory('ietf.person.factories.PersonFactory')
email = factory.LazyAttribute(lambda obj: obj.person.email())
class WgDocumentAuthorFactory(DocumentAuthorFactory):
document = factory.SubFactory(WgDraftFactory)

View file

@ -5,7 +5,8 @@ import factory
from typing import List # pyflakes:ignore
from ietf.group.models import Group, Role, GroupEvent, GroupMilestone
from ietf.group.models import Group, Role, GroupEvent, GroupMilestone, \
GroupHistory, RoleHistory
from ietf.review.factories import ReviewTeamSettingsFactory
class GroupFactory(factory.DjangoModelFactory):
@ -71,3 +72,25 @@ class DatelessGroupMilestoneFactory(BaseGroupMilestoneFactory):
group = factory.SubFactory(GroupFactory, uses_milestone_dates=False)
order = factory.Sequence(lambda n: n)
class GroupHistoryFactory(factory.DjangoModelFactory):
class Meta:
model=GroupHistory
name = factory.LazyAttribute(lambda obj: obj.group.name)
state_id = 'active'
type_id = factory.LazyAttribute(lambda obj: obj.group.type_id)
list_email = factory.LazyAttribute(lambda obj: '%s@ietf.org'% obj.group.acronym)
uses_milestone_dates = True
used_roles = [] # type: List[str]
group = factory.SubFactory(GroupFactory)
acronym = factory.LazyAttribute(lambda obj: obj.group.acronym)
class RoleHistoryFactory(factory.DjangoModelFactory):
class Meta:
model=RoleHistory
group = factory.SubFactory(GroupHistoryFactory)
person = factory.SubFactory('ietf.person.factories.PersonFactory')
email = factory.LazyAttribute(lambda obj: obj.person.email())

View file

@ -18,12 +18,11 @@ from django.utils.safestring import mark_safe
import debug # pyflakes:ignore
from ietf.dbtemplate.models import DBTemplate
from ietf.meeting.models import Session, Meeting, SchedulingEvent, TimeSlot, Constraint, SchedTimeSessAssignment
from ietf.group.models import Group, Role
from ietf.meeting.models import Session, SchedulingEvent, TimeSlot, Constraint, SchedTimeSessAssignment
from ietf.group.models import Group
from ietf.group.utils import can_manage_materials
from ietf.name.models import SessionStatusName, ConstraintName
from ietf.nomcom.utils import DISQUALIFYING_ROLE_QUERY_EXPRESSION
from ietf.person.models import Person, Email
from ietf.person.models import Person
from ietf.secr.proceedings.proc_utils import import_audio_files
def session_time_for_sorting(session, use_meeting_date):
@ -171,25 +170,6 @@ def finalize(meeting):
meeting.save()
return
def attended_ietf_meetings(person):
email_addresses = Email.objects.filter(person=person).values_list('address',flat=True)
return Meeting.objects.filter(
type='ietf',
meetingregistration__email__in=email_addresses,
meetingregistration__attended=True,
)
def attended_in_last_five_ietf_meetings(person, date=datetime.datetime.today()):
previous_five = Meeting.objects.filter(type='ietf',date__lte=date).order_by('-date')[:5]
attended = attended_ietf_meetings(person)
return set(previous_five).intersection(attended)
def is_nomcom_eligible(person, date=datetime.date.today()):
attended = attended_in_last_five_ietf_meetings(person, date)
disqualifying_roles = Role.objects.filter(person=person).filter(DISQUALIFYING_ROLE_QUERY_EXPRESSION)
return len(attended)>=3 and not disqualifying_roles.exists()
def sort_accept_tuple(accept):
tup = []
if accept:

View file

@ -0,0 +1,18 @@
# Generated by Django 2.2.20 on 2021-04-22 14:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('nomcom', '0009_auto_20201109_0439'),
]
operations = [
migrations.AddField(
model_name='nomcom',
name='first_call_for_volunteers',
field=models.DateField(blank=True, null=True, verbose_name='Date of the first call for volunteers'),
),
]

View file

@ -59,6 +59,7 @@ class NomCom(models.Model):
help_text='Display pictures of each nominee (if available) on the feedback pages')
show_accepted_nominees = models.BooleanField(verbose_name='Show accepted nominees', default=True,
help_text='Show accepted nominees on the public nomination page')
first_call_for_volunteers = models.DateField(verbose_name='Date of the first call for volunteers', blank=True, null=True)
class Meta:
verbose_name_plural = 'NomComs'

View file

@ -9,6 +9,7 @@ import shutil
from pyquery import PyQuery
from urllib.parse import urlparse
from itertools import combinations
from django.db import IntegrityError
from django.db.models import Max
@ -22,7 +23,9 @@ import debug # pyflakes:ignore
from ietf.dbtemplate.factories import DBTemplateFactory
from ietf.dbtemplate.models import DBTemplate
from ietf.group.models import Group
from ietf.doc.factories import DocEventFactory, WgDocumentAuthorFactory
from ietf.group.factories import GroupFactory, GroupHistoryFactory, RoleFactory, RoleHistoryFactory
from ietf.group.models import Group, Role
from ietf.meeting.factories import MeetingFactory
from ietf.message.models import Message
from ietf.nomcom.test_data import nomcom_test_data, generate_cert, check_comments, \
@ -35,10 +38,13 @@ from ietf.nomcom.management.commands.send_reminders import Command, is_time_to_s
from ietf.nomcom.factories import NomComFactory, FeedbackFactory, TopicFactory, \
nomcom_kwargs_for_year, provide_private_key_to_test_client, \
key
from ietf.nomcom.utils import get_nomcom_by_year, make_nomineeposition, get_hash_nominee_position
from ietf.nomcom.utils import get_nomcom_by_year, make_nomineeposition, \
get_hash_nominee_position, is_eligible, list_eligible, \
get_eligibility_date
from ietf.person.factories import PersonFactory, EmailFactory
from ietf.person.models import Email, Person
from ietf.stats.models import MeetingRegistration
from ietf.stats.factories import MeetingRegistrationFactory
from ietf.utils.mail import outbox, empty_outbox, get_payload_text
from ietf.utils.test_utils import login_testing_unauthorized, TestCase, unicontent
@ -2117,3 +2123,302 @@ class TopicTests(TestCase):
self.assertEqual(r.status_code,200)
self.assertEqual(topic.feedback_set.count(),1)
self.client.logout()
class EligibilityUnitTests(TestCase):
def test_get_eligibility_date(self):
# No Nomcoms exist:
self.assertEqual(get_eligibility_date(), datetime.date(datetime.date.today().year,5,1))
# a provided date trumps anything in the database
self.assertEqual(get_eligibility_date(date=datetime.date(2001,2,3)), datetime.date(2001,2,3))
n = NomComFactory(group__acronym='nomcom2015',populate_personnel=False)
self.assertEqual(get_eligibility_date(date=datetime.date(2001,2,3)), datetime.date(2001,2,3))
self.assertEqual(get_eligibility_date(nomcom=n, date=datetime.date(2001,2,3)), datetime.date(2001,2,3))
# Now there's a nomcom in the database
self.assertEqual(get_eligibility_date(nomcom=n), datetime.date(2015,5,1))
n.first_call_for_volunteers = datetime.date(2015,5,17)
n.save()
self.assertEqual(get_eligibility_date(nomcom=n), datetime.date(2015,5,17))
# No nomcoms in the database with seated members
self.assertEqual(get_eligibility_date(), datetime.date(datetime.date.today().year,5,1))
RoleFactory(group=n.group,name_id='member')
self.assertEqual(get_eligibility_date(),datetime.date(2016,5,1))
NomComFactory(group__acronym='nomcom2016', populate_personnel=False, first_call_for_volunteers=datetime.date(2016,5,4))
self.assertEqual(get_eligibility_date(),datetime.date(2016,5,4))
this_year = datetime.date.today().year
NomComFactory(group__acronym=f'nomcom{this_year}', first_call_for_volunteers=datetime.date(this_year,5,6))
self.assertEqual(get_eligibility_date(),datetime.date(this_year,5,6))
class rfc8713EligibilityTests(TestCase):
def setUp(self):
self.nomcom = NomComFactory(group__acronym='nomcom2019', populate_personnel=False, first_call_for_volunteers=datetime.date(2019,5,1))
meetings = [ MeetingFactory(date=date,type_id='ietf') for date in (
datetime.date(2019,3,1),
datetime.date(2018,11,1),
datetime.date(2018,7,1),
datetime.date(2018,3,1),
datetime.date(2017,11,1),
)]
self.eligible_people = list()
self.ineligible_people = list()
for combo_len in range(0,6):
for combo in combinations(meetings,combo_len):
p = PersonFactory()
for m in combo:
MeetingRegistrationFactory(person=p, meeting=m)
if combo_len<3:
self.ineligible_people.append(p)
else:
self.eligible_people.append(p)
# No-one is eligible for the other_nomcom
self.other_nomcom = NomComFactory(group__acronym='nomcom2018',first_call_for_volunteers=datetime.date(2018,5,1))
# Someone is eligible at this other_date
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')
def test_is_person_eligible(self):
for person in self.eligible_people:
self.assertTrue(is_eligible(person,self.nomcom))
self.assertTrue(is_eligible(person))
self.assertFalse(is_eligible(person,nomcom=self.other_nomcom))
self.assertFalse(is_eligible(person,date=self.other_date))
for person in self.ineligible_people:
self.assertFalse(is_eligible(person,self.nomcom))
for person in self.other_people:
self.assertTrue(is_eligible(person,date=self.other_date))
def test_list_eligible(self):
self.assertEqual(set(list_eligible()), set(self.eligible_people))
self.assertEqual(set(list_eligible(self.nomcom)), set(self.eligible_people))
self.assertEqual(set(list_eligible(self.other_nomcom)),set(self.other_people))
self.assertEqual(set(list_eligible(date=self.other_date)),set(self.other_people))
class rfc8788EligibilityTests(TestCase):
def setUp(self):
self.nomcom = NomComFactory(group__acronym='nomcom2020', populate_personnel=False, first_call_for_volunteers=datetime.date(2020,5,1))
meetings = [MeetingFactory(number=number, date=date, type_id='ietf') for number,date in [
('106', datetime.date(2019, 11, 16)),
('105', datetime.date(2019, 7, 20)),
('104', datetime.date(2019, 3, 23)),
('103', datetime.date(2018, 11, 3)),
('102', datetime.date(2018, 7, 14)),
]]
self.eligible_people = list()
self.ineligible_people = list()
for combo_len in range(0,6):
for combo in combinations(meetings,combo_len):
p = PersonFactory()
for m in combo:
MeetingRegistrationFactory(person=p, meeting=m)
if combo_len<3:
self.ineligible_people.append(p)
else:
self.eligible_people.append(p)
def test_is_person_eligible(self):
for person in self.eligible_people:
self.assertTrue(is_eligible(person,self.nomcom))
for person in self.ineligible_people:
self.assertFalse(is_eligible(person,self.nomcom))
def test_list_eligible(self):
self.assertEqual(set(list_eligible(self.nomcom)), set(self.eligible_people))
class rfc8989EligibilityTests(TestCase):
def setUp(self):
self.nomcom = NomComFactory(group__acronym='nomcom2021', populate_personnel=False, first_call_for_volunteers=datetime.date(2021,5,15))
# make_immutable_test_data makes things this test does not want
Role.objects.filter(name_id__in=('chair','secr')).delete()
def test_elig_by_meetings(self):
meetings = [MeetingFactory(number=number, date=date, type_id='ietf') for number,date in [
('110', datetime.date(2021, 3, 6)),
('109', datetime.date(2020, 11, 14)),
('108', datetime.date(2020, 7, 25)),
('107', datetime.date(2020, 3, 21)),
('106', datetime.date(2019, 11, 16)),
]]
eligible_people = list()
ineligible_people = list()
for combo_len in range(0,6):
for combo in combinations(meetings,combo_len):
p = PersonFactory()
for m in combo:
MeetingRegistrationFactory(person=p, meeting=m)
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))
def test_elig_by_office_active_groups(self):
chair = RoleFactory(name_id='chair').person
secr = RoleFactory(name_id='secr').person
nobody=PersonFactory()
self.assertTrue(is_eligible(person=chair,nomcom=self.nomcom))
self.assertTrue(is_eligible(person=secr,nomcom=self.nomcom))
self.assertFalse(is_eligible(person=nobody,nomcom=self.nomcom))
self.assertEqual(set([chair,secr]), set(list_eligible(nomcom=self.nomcom)))
def test_elig_by_office_edge(self):
elig_date=get_eligibility_date(self.nomcom)
day_after = elig_date + datetime.timedelta(days=1)
two_days_after = elig_date + datetime.timedelta(days=2)
group = GroupFactory(time=two_days_after)
GroupHistoryFactory(group=group,time=day_after)
after_chair = RoleFactory(name_id='chair',group=group).person
self.assertFalse(is_eligible(person=after_chair,nomcom=self.nomcom))
def test_elig_by_office_closed_groups(self):
elig_date=get_eligibility_date(self.nomcom)
day_before = elig_date-datetime.timedelta(days=1)
year_before = datetime.date(elig_date.year-1,elig_date.month,elig_date.day)
three_years_before = datetime.date(elig_date.year-3,elig_date.month,elig_date.day)
just_after_three_years_before = three_years_before + datetime.timedelta(days=1)
just_before_three_years_before = three_years_before - datetime.timedelta(days=1)
eligible = list()
ineligible = list()
p1 = RoleHistoryFactory(
name_id='chair',
group__time=day_before,
group__group__state_id='conclude',
).person
eligible.append(p1)
p2 = RoleHistoryFactory(
name_id='secr',
group__time=year_before,
group__group__state_id='conclude',
).person
eligible.append(p2)
p3 = RoleHistoryFactory(
name_id='secr',
group__time=just_after_three_years_before,
group__group__state_id='conclude',
).person
eligible.append(p3)
p4 = RoleHistoryFactory(
name_id='chair',
group__time=three_years_before,
group__group__state_id='conclude',
).person
eligible.append(p4)
p5 = RoleHistoryFactory(
name_id='chair',
group__time=just_before_three_years_before,
group__group__state_id='conclude',
).person
ineligible.append(p5)
for person in eligible:
self.assertTrue(is_eligible(person,self.nomcom))
for person in ineligible:
self.assertFalse(is_eligible(person,self.nomcom))
self.assertEqual(set(list_eligible(nomcom=self.nomcom)),set(eligible))
def test_elig_by_author(self):
elig_date = get_eligibility_date(self.nomcom)
last_date = elig_date
first_date = datetime.date(last_date.year-5,last_date.month,last_date.day)
day_after_last_date = last_date+datetime.timedelta(days=1)
day_before_first_date = first_date-datetime.timedelta(days=1)
middle_date = datetime.date(last_date.year-3,last_date.month,last_date.day)
eligible = set()
ineligible = set()
p = PersonFactory()
ineligible.add(p)
p = PersonFactory()
da = WgDocumentAuthorFactory(person=p)
DocEventFactory(type='published_rfc',doc=da.document,time=middle_date)
ineligible.add(p)
p = PersonFactory()
da = WgDocumentAuthorFactory(person=p)
DocEventFactory(type='iesg_approved',doc=da.document,time=last_date)
da = WgDocumentAuthorFactory(person=p)
DocEventFactory(type='published_rfc',doc=da.document,time=first_date)
eligible.add(p)
p = PersonFactory()
da = WgDocumentAuthorFactory(person=p)
DocEventFactory(type='iesg_approved',doc=da.document,time=middle_date)
da = WgDocumentAuthorFactory(person=p)
DocEventFactory(type='published_rfc',doc=da.document,time=day_before_first_date)
ineligible.add(p)
p = PersonFactory()
da = WgDocumentAuthorFactory(person=p)
DocEventFactory(type='iesg_approved',doc=da.document,time=day_after_last_date)
da = WgDocumentAuthorFactory(person=p)
DocEventFactory(type='published_rfc',doc=da.document,time=middle_date)
ineligible.add(p)
for person in eligible:
self.assertTrue(is_eligible(person,self.nomcom))
for person in ineligible:
self.assertFalse(is_eligible(person,self.nomcom))
self.assertEqual(set(list_eligible(nomcom=self.nomcom)),set(eligible))

View file

@ -13,7 +13,7 @@ from email.header import decode_header
from email.iterators import typed_subpart_iterator
from email.utils import parseaddr
from django.db.models import Q
from django.db.models import Q, Count
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.exceptions import ObjectDoesNotExist
@ -22,8 +22,11 @@ from django.template.loader import render_to_string
from django.shortcuts import get_object_or_404
from ietf.dbtemplate.models import DBTemplate
from ietf.doc.models import DocEvent
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.utils.pipe import pipe
from ietf.utils.mail import send_mail_text, send_mail, get_payload_text
from ietf.utils.log import log
@ -477,4 +480,113 @@ def create_feedback_email(nomcom, msg):
class EncryptedException(Exception):
pass
def remove_disqualified(queryset):
disqualified_roles = Role.objects.filter(DISQUALIFYING_ROLE_QUERY_EXPRESSION)
return queryset.exclude(role__in=disqualified_roles)
def is_eligible(person, nomcom=None, date=None):
return list_eligible(nomcom=nomcom, date=date, base_qs=Person.objects.filter(pk=person.pk)).exists()
def list_eligible(nomcom=None, date=None, base_qs=None):
if not base_qs:
base_qs = Person.objects.all()
eligibility_date = get_eligibility_date(nomcom, date)
if eligibility_date.year in range(2008,2020):
return list_eligible_8713(date=eligibility_date, base_qs=base_qs)
elif eligibility_date.year == 2020:
return list_eligible_8788(date=eligibility_date, base_qs=base_qs)
elif eligibility_date.year == 2021:
return list_eligible_8989(date=eligibility_date, base_qs=base_qs)
else:
return Person.objects.none()
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))
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))
def list_eligible_8989(date, base_qs=None):
if not base_qs:
base_qs = Person.objects.all()
previous_five = previous_five_meetings(date)
three_of_five_qs = three_of_five_eligible(previous_five=previous_five, queryset=base_qs)
three_years_ago = datetime.date(date.year-3,date.month,date.day)
officer_qs = base_qs.filter(
# is currently an officer
Q(role__name_id__in=('chair','secr'),
role__group__state_id='active',
role__group__type_id='wg',
role__group__time__lte=date,
)
# 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,
rolehistory__group__time__lte=date,
rolehistory__name_id__in=('chair','secr'),
rolehistory__group__state_id='active',
rolehistory__group__type_id='wg',
)
).distinct()
five_years_ago = datetime.date(date.year-5,date.month,date.day)
rfc_pks = set(DocEvent.objects.filter(type='published_rfc',time__gte=five_years_ago,time__lte=date).values_list('doc__pk',flat=True))
iesgappr_pks = set(DocEvent.objects.filter(type='iesg_approved',time__gte=five_years_ago,time__lte=date).values_list('doc__pk',flat=True))
qualifying_pks = rfc_pks.union(iesgappr_pks.difference(rfc_pks))
author_qs = base_qs.filter(
documentauthor__document__pk__in=qualifying_pks
).annotate(
document_author_count = Count('documentauthor')
).filter(document_author_count__gte=2)
# Would be nice to use queryset union here, but the annotations make that difficult
return remove_disqualified(Person.objects.filter(pk__in=
set(three_of_five_qs.values_list('pk',flat=True)).union(
set(officer_qs.values_list('pk',flat=True))).union(
set(author_qs.values_list('pk',flat=True)))
))
def get_eligibility_date(nomcom=None, date=None):
if date:
return date
elif nomcom:
if nomcom.first_call_for_volunteers:
return nomcom.first_call_for_volunteers
else:
return datetime.date(int(nomcom.group.acronym[6:]),5,1)
else:
last_seated=Role.objects.filter(group__type_id='nomcom',name_id='member').order_by('-group__acronym').first()
if last_seated:
last_nomcom_year = int(last_seated.group.acronym[6:])
if last_nomcom_year == datetime.date.today().year:
next_nomcom_year = last_nomcom_year
else:
next_nomcom_year = int(last_seated.group.acronym[6:])+1
next_nomcom_group = Group.objects.filter(acronym=f'nomcom{next_nomcom_year}').first()
if next_nomcom_group and next_nomcom_group.nomcom_set.first().first_call_for_volunteers:
return next_nomcom_group.nomcom_set.first().first_call_for_volunteers
else:
return datetime.date(next_nomcom_year,5,1)
else:
return datetime.date(datetime.date.today().year,5,1)
def previous_five_meetings(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):
""" 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.
"""
if not queryset:
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)

View file

@ -24,7 +24,6 @@ from ietf.dbtemplate.views import group_template_edit, group_template_show
from ietf.name.models import NomineePositionStateName, FeedbackTypeName
from ietf.group.models import Group, GroupEvent, Role
from ietf.message.models import Message
from ietf.meeting.models import Meeting
from ietf.nomcom.decorators import nomcom_private_key_required
from ietf.nomcom.forms import (NominateForm, NominateNewPersonForm, FeedbackForm, QuestionnaireForm,
@ -36,12 +35,11 @@ from ietf.nomcom.forms import (NominateForm, NominateNewPersonForm, FeedbackForm
from ietf.nomcom.models import (Position, NomineePosition, Nominee, Feedback, NomCom, ReminderDates,
FeedbackLastSeen, Topic, TopicFeedbackLastSeen, )
from ietf.nomcom.utils import (get_nomcom_by_year, store_nomcom_private_key,
get_hash_nominee_position, send_reminder_to_nominees,
HOME_TEMPLATE, NOMINEE_ACCEPT_REMINDER_TEMPLATE,NOMINEE_QUESTIONNAIRE_REMINDER_TEMPLATE,
DISQUALIFYING_ROLE_QUERY_EXPRESSION)
get_hash_nominee_position, send_reminder_to_nominees, list_eligible,
HOME_TEMPLATE, NOMINEE_ACCEPT_REMINDER_TEMPLATE,NOMINEE_QUESTIONNAIRE_REMINDER_TEMPLATE, )
from ietf.ietfauth.utils import role_required
from ietf.person.models import Person
from ietf.utils import log
from ietf.utils.response import permission_denied
import debug # pyflakes:ignore
@ -1275,31 +1273,7 @@ def extract_email_lists(request, year):
def eligible(request, year):
nomcom = get_nomcom_by_year(year)
# This should probably be refined. If the nomcom year is this year, then
# today's date makes sense; for previous nomcoms, we should probably get
# the date of the announcement of the Call for Volunteers, instead
date = datetime.date.today()
previous_five = ( Meeting.objects.filter(type='ietf',date__lte=date)
.exclude(city='').exclude(city='Virtual')
.order_by('-date')[:5] )
log.assertion("len(previous_five) == 5")
attendees = {}
potentials = set()
for m in previous_five:
registration_emails = m.meetingregistration_set.filter(attended=True).values_list('email',flat=True)
attendees[m] = Person.objects.filter(email__address__in=registration_emails).distinct()
# See RFC8713 section 4.15
disqualified_roles = Role.objects.filter(DISQUALIFYING_ROLE_QUERY_EXPRESSION)
potentials.update(attendees[m].exclude(role__in=disqualified_roles))
eligible_persons = []
for p in potentials:
count = 0
for m in previous_five:
if p in attendees[m]:
count += 1
if count >= 3:
eligible_persons.append(p)
eligible_persons = list(list_eligible(nomcom=nomcom))
eligible_persons.sort(key=lambda p: p.last_name() )
return render(request, 'nomcom/eligible.html',

View file

@ -6,14 +6,14 @@ from django import template
import debug # pyflakes:ignore
from ietf.meeting.utils import is_nomcom_eligible as util_is_nomcom_eligible
from ietf.nomcom.utils import is_eligible
from ietf.person.models import Alias
register = template.Library()
@register.filter
def is_nomcom_eligible(person, date=datetime.date.today()):
return util_is_nomcom_eligible(person,date)
return is_eligible(person=person,date=date)
@register.filter
def person_by_name(name):

17
ietf/stats/factories.py Normal file
View file

@ -0,0 +1,17 @@
# Copyright The IETF Trust 2021, All Rights Reserved
import factory
from ietf.stats.models import MeetingRegistration
from ietf.meeting.factories import MeetingFactory
from ietf.person.factories import PersonFactory
class MeetingRegistrationFactory(factory.DjangoModelFactory):
class Meta:
model = MeetingRegistration
meeting = factory.SubFactory(MeetingFactory)
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

View file

@ -0,0 +1,37 @@
# Copyright The IETF Trust 2021, All Rights Reserved
import debug # pyflakes:ignore
from django.core.management.base import BaseCommand
from ietf.stats.utils import find_meetingregistration_person_issues
class Command(BaseCommand):
help = "Find possible Person/Email objects to repair based on MeetingRegistration objects"
def add_arguments(self, parser):
parser.add_argument('--meeting',action='append')
def handle(self, *args, **options):
meetings = options['meeting'] or None
summary = find_meetingregistration_person_issues(meetings)
print(f'{summary.ok_records} records are OK')
for msg in summary.could_be_fixed:
print(msg)
for msg in summary.maybe_address:
print(msg)
for msg in summary.different_person:
print(msg)
for msg in summary.no_person:
print(msg)
for msg in summary.maybe_person:
print(msg)
for msg in summary.no_email:
print(msg)

View file

@ -0,0 +1,18 @@
# Copyright The IETF Trust 2021, All Rights Reserved
import debug # pyflakes:ignore
from django.core.management.base import BaseCommand
from ietf.stats.utils import repair_meetingregistration_person
class Command(BaseCommand):
help = "Repair MeetingRegistration objects that have no person but an email matching a person"
def add_arguments(self, parser):
parser.add_argument('--meeting',action='append')
def handle(self, *args, **options):
meetings = options['meeting'] or None
repaired = repair_meetingregistration_person(meetings)
print(f'Repaired {repaired} MeetingRegistration objects')

View file

@ -344,3 +344,57 @@ def get_meeting_registration_data(meeting):
meeting.attendees = num_total
meeting.save()
return num_created, num_processed, num_total
def repair_meetingregistration_person(meetings=None):
repaired_records = 0
qs = MeetingRegistration.objects.all()
if meetings:
qs = qs.filter(meeting__number__in=meetings)
for mr in qs:
if mr.email and not mr.person:
email_person = Person.objects.filter(email__address=mr.email).first()
if email_person:
mr.person = email_person
mr.save()
repaired_records += 1
return repaired_records
class MeetingRegistrationIssuesSummary(object):
pass
def find_meetingregistration_person_issues(meetings=None):
summary = MeetingRegistrationIssuesSummary()
summary.could_be_fixed = set()
summary.maybe_address = set()
summary.different_person = set()
summary.no_person = set()
summary.maybe_person = set()
summary.no_email = set()
summary.ok_records = 0
qs = MeetingRegistration.objects.all()
if meetings:
qs = qs.filter(meeting__number__in=meetings)
for mr in qs:
if mr.person and mr.email and mr.email in mr.person.email_set.values_list('address',flat=True):
summary.ok_records += 1
elif mr.email:
email_person = Person.objects.filter(email__address=mr.email).first()
if mr.person:
if not email_person:
summary.maybe_address.add(f'{mr.email} is not present in any Email object. The MeetingRegistration object implies this is an address for {mr.person} ({mr.person.pk})')
elif email_person != mr.person:
summary.different_person.add(f'{mr} ({mr.pk}) has person {mr.person} ({mr.person.pk}) but an email {mr.email} attached to a different person {email_person} ({email_person.pk}).')
elif email_person:
summary.could_be_fixed.add(f'{mr} ({mr.pk}) has no person, but email {mr.email} matches {email_person} ({email_person.pk})')
else:
maybe_person_qs = Person.objects.filter(name__icontains=mr.last_name).filter(name__icontains=mr.first_name)
if maybe_person_qs.exists():
summary.maybe_person.add(f'{mr} ({mr.pk}) has email address {mr.email} which cannot be associated with any Person. Consider these possible people {[(p,p.pk) for p in maybe_person_qs]}')
else:
summary.no_person.add(f'{mr} ({mr.pk}) has email address {mr.email} which cannot be associated with any Person')
else:
summary.no_email.add(f'{mr} ({mr.pk}) provides no email address')
return summary

View file

@ -14,9 +14,6 @@
{% origin %}
<h2>Eligible People for {{ nomcom.group }}</h2>
<p class="alert alert-info">
This calculation is experimental and is likely wrong. Check carefully against the secretariat eligibility tools if it matters. This page lists people who would be nomcom eligible if the selection were made <em>today</em>. Thus if today is not between the spring and summer IETF meetings, the list won't reflect eligibility at the time actual selections will be made.
</p>
<table class="table table-condensed table-striped tablesorter">
<thead>
<th>Last Name</th>

View file

@ -69,17 +69,16 @@
<div class="col-sm-1 form-control-static">{{person|is_nomcom_eligible|yesno:'Yes,No,No'}}</div>
<div class="col-sm-9">
<p class="alert alert-info form-control-static">
This calculation is EXPERIMENTAL.<br/>
If you believe it is incorrect, make sure you've added all the
If you believe this calculation is incorrect, make sure you've added all the
email addresses you've registered for IETF meetings with to the
list below.<br/>
If you've done so and the calculation is still incorrect, please
send a note to
<a href="mailto:{{settings.SECRETARIAT_INFO_EMAIL}}">{{settings.SECRETARIAT_INFO_EMAIL}}</a>.<br/>
See <a href="{% url 'ietf.doc.views_doc.document_main' name='rfc7437'%}">RFC 7437</a>
for eligibility requirements.
<a href="mailto:{{settings.SECRETARIAT_SUPPORT_EMAIL}}">{{settings.SECRETARIAT_SUPPORT_EMAIL}}</a>.<br/>
See <a href="{% url 'ietf.doc.views_doc.document_main' name='rfc8713'%}">RFC 8713</a>
for eligibility requirements.
For the 2021 nomcom, see also <a href="{% url 'ietf.doc.views_doc.document_main' name='rfc8989' %}">RFC 8989</a>.
</p>
</div>
@ -181,7 +180,7 @@
dagger symbol (<strong>&dagger;</strong>) next to it, or listed on your
<a href="{% url 'ietf.community.views.view_list' user.username %}">notification subscription page</a>. Most of this
information can be edited or removed on these pages. There are some exceptions, such
as photos, which currently require an email to <a href="mailto:{{settings.SECRETARIAT_INFO_EMAIL}}">the Secretariat</a>
as photos, which currently require an email to <a href="mailto:{{settings.SECRETARIAT_SUPPORT_EMAIL}}">the Secretariat</a>
if you wish to update or remove the information.
</p>