Add RFC 8989 nomcom eligibility calculations. Commit ready for merge.
- Legacy-Id: 18975
This commit is contained in:
commit
fd38a9bf96
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
]
|
|
@ -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'
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
17
ietf/stats/factories.py
Normal 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
|
|
@ -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)
|
|
@ -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')
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>†</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>
|
||||
|
|
Loading…
Reference in a new issue