Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ietf/doc/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import logging
import os

import django.db
import rfc2html

from io import BufferedReader
Expand Down
157 changes: 130 additions & 27 deletions ietf/nomcom/tests.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# Copyright The IETF Trust 2012-2023, All Rights Reserved
# -*- coding: utf-8 -*-
# Copyright The IETF Trust 2012-2025, All Rights Reserved


import datetime
Expand Down Expand Up @@ -27,8 +26,14 @@
from ietf.api.views import EmailIngestionError
from ietf.dbtemplate.factories import DBTemplateFactory
from ietf.dbtemplate.models import DBTemplate
from ietf.doc.factories import DocEventFactory, WgDocumentAuthorFactory, \
NewRevisionDocEventFactory, DocumentAuthorFactory, RfcAuthorFactory
from ietf.doc.factories import (
DocEventFactory,
WgDocumentAuthorFactory,
NewRevisionDocEventFactory,
DocumentAuthorFactory,
RfcAuthorFactory,
WgDraftFactory, WgRfcFactory,
)
from ietf.group.factories import GroupFactory, GroupHistoryFactory, RoleFactory, RoleHistoryFactory
from ietf.group.models import Group, Role
from ietf.meeting.factories import MeetingFactory, AttendedFactory, RegistrationFactory
Expand All @@ -45,10 +50,20 @@
nomcom_kwargs_for_year, provide_private_key_to_test_client, \
key
from ietf.nomcom.tasks import send_nomcom_reminders_task
from ietf.nomcom.utils import get_nomcom_by_year, make_nomineeposition, \
get_hash_nominee_position, is_eligible, list_eligible, \
get_eligibility_date, suggest_affiliation, ingest_feedback_email, \
decorate_volunteers_with_qualifications, send_reminders, _is_time_to_send_reminder
from ietf.nomcom.utils import (
get_nomcom_by_year,
make_nomineeposition,
get_hash_nominee_position,
is_eligible,
list_eligible,
get_eligibility_date,
suggest_affiliation,
ingest_feedback_email,
decorate_volunteers_with_qualifications,
send_reminders,
_is_time_to_send_reminder,
get_qualified_author_queryset,
)
from ietf.person.factories import PersonFactory, EmailFactory
from ietf.person.models import Email, Person
from ietf.utils.mail import outbox, empty_outbox, get_payload_text
Expand Down Expand Up @@ -2440,6 +2455,86 @@ def test_get_eligibility_date(self):
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))

def test_get_qualified_author_queryset(self):
"""get_qualified_author_queryset implements the eligiblity rules correctly

This is not an exhaustive test of corner cases. Overlaps considerably with
rfc8989EligibilityTests.test_elig_by_author().
"""
people = PersonFactory.create_batch(2)
extra_person = PersonFactory()
base_qs = Person.objects.filter(pk__in=[person.pk for person in people])
now = datetime.datetime.now(tz=datetime.UTC)
one_year = datetime.timedelta(days=365)

# Authors with no qualifying drafts
self.assertCountEqual(
get_qualified_author_queryset(base_qs, now - 5 * one_year, now), []
)

# Authors with one qualifying draft
approved_draft = WgDraftFactory(authors=people, states=[("draft", "active")])
DocEventFactory(
type="iesg_approved",
doc=approved_draft,
time=now - 4 * one_year,
)
self.assertCountEqual(
get_qualified_author_queryset(base_qs, now - 5 * one_year, now), []
)

# Create a draft that was published into an RFC. Give it an extra author who
# should not be eligible.
published_draft = WgDraftFactory(authors=people, states=[("draft", "rfc")])
DocEventFactory(
type="iesg_approved",
doc=published_draft,
time=now - 5.5 * one_year, # < 6 years ago
)
rfc = WgRfcFactory(
authors=people + [extra_person],
group=published_draft.group,
)
DocEventFactory(
type="published_rfc",
doc=rfc,
time=now - 0.5 * one_year, # < 1 year ago
)
# Period 6 years ago to 1 year ago - authors are eligible due to the
# iesg-approved draft in this window
self.assertCountEqual(
get_qualified_author_queryset(base_qs, now - 6 * one_year, now - one_year),
people,
)

# Period 5 years ago to now - authors are eligible due to the RFC publication
self.assertCountEqual(
get_qualified_author_queryset(base_qs, now - 5 * one_year, now),
people,
)

# Use the extra_person to check that a single doc can't count both as an
# RFC _and_ an approved draft. Use an eligibility interval that includes both
# the approval and the RFC publication
self.assertCountEqual(
get_qualified_author_queryset(base_qs, now - 6 * one_year, now),
people, # does not include extra_person!
)

# Now add an RfcAuthor for only one of the two authors to the RFC. This should
# remove the other author from the eligibility list because the DocumentAuthor
# records are no longer used.
RfcAuthorFactory(
document=rfc,
person=people[0],
titlepage_name="P. Zero",
email=people[0].email_set.first(),
)
self.assertCountEqual(
get_qualified_author_queryset(base_qs, now - 5 * one_year, now),
[people[0]],
)


class rfc8713EligibilityTests(TestCase):

Expand Down Expand Up @@ -2724,33 +2819,41 @@ def test_elig_by_author(self):
ineligible = set()

p = PersonFactory()
ineligible.add(p)

ineligible.add(p) # no RFCs or iesg-approved drafts
p = PersonFactory()
da = WgDocumentAuthorFactory(person=p)
DocEventFactory(type='published_rfc',doc=da.document,time=middle_date)
ineligible.add(p)
doc = WgRfcFactory(authors=[p])
DocEventFactory(type='published_rfc', doc=doc, time=middle_date)
ineligible.add(p) # only one RFC

p = PersonFactory()
da = WgDocumentAuthorFactory(person=p)
da = WgDocumentAuthorFactory(
person=p,
document__states=[("draft", "active"), ("draft-rfceditor", "ref")],
)
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)
doc = WgRfcFactory(authors=[p])
DocEventFactory(type='published_rfc', doc=doc, time=first_date)
eligible.add(p) # one RFC and one iesg-approved draft

p = PersonFactory()
da = WgDocumentAuthorFactory(person=p)
da = WgDocumentAuthorFactory(
person=p,
document__states=[("draft", "active"), ("draft-rfceditor", "ref")],
)
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)
doc = WgRfcFactory(authors=[p])
DocEventFactory(type='published_rfc', doc=doc, time=day_before_first_date)
ineligible.add(p) # RFC is out of the eligibility window

p = PersonFactory()
da = WgDocumentAuthorFactory(person=p)
da = WgDocumentAuthorFactory(
person=p,
document__states=[("draft", "active"), ("draft-rfceditor", "ref")],
)
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)
doc = WgRfcFactory(authors=[p])
DocEventFactory(type='published_rfc', doc=doc, time=middle_date)
ineligible.add(p) # iesg approval is outside the eligibility window

for person in eligible:
self.assertTrue(is_eligible(person,nomcom))
Expand Down Expand Up @@ -2945,10 +3048,10 @@ def test_decorate_volunteers_with_qualifications(self):

author_person = PersonFactory()
for i in range(2):
da = WgDocumentAuthorFactory(person=author_person)
doc = WgRfcFactory(authors=[author_person])
DocEventFactory(
type='published_rfc',
doc=da.document,
doc=doc,
time=datetime.datetime(
elig_date.year - 3,
elig_date.month,
Expand Down
79 changes: 68 additions & 11 deletions ietf/nomcom/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from email.utils import parseaddr
from textwrap import dedent

from django.db.models import Q, Count
from django.db.models import Q, Count, F, QuerySet
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.exceptions import ObjectDoesNotExist
Expand All @@ -27,15 +27,15 @@
from django.shortcuts import get_object_or_404

from ietf.dbtemplate.models import DBTemplate
from ietf.doc.models import DocEvent, NewRevisionDocEvent
from ietf.doc.models import DocEvent, NewRevisionDocEvent, State, Document
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.utils import participants_for_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
from ietf.utils.log import assertion, log
from ietf.person.name import unidecode_name
from ietf.utils.timezone import date_today, datetime_from_date, DEADLINE_TZINFO

Expand Down Expand Up @@ -576,6 +576,70 @@ def get_8989_eligibility_querysets(date, base_qs):
def get_9389_eligibility_querysets(date, base_qs):
return get_threerule_eligibility_querysets(date, base_qs, three_of_five_callable=three_of_five_eligible_9389)


def get_qualified_author_queryset(
base_qs: QuerySet[Person],
eligibility_period_start: datetime.datetime,
eligibility_period_end: datetime.datetime,
):
"""Filter a Person queryset, keeping those qualified by RFC 8989's author path

The author path is defined by "path 3" in section 4 of RFC 8989. It qualifies
a person who has been a front-page listed author or editor of at least two IETF-
stream RFCs within the last five years. An I-D in the RFC Editor queue that was
approved by the IESG is treated as an RFC, using the date of entry to the RFC
Editor queue as the date for qualification.

This method does not strictly enforce "in the RFC Editor queue" for IESG-approved
drafts when computing eligibility. In the overwhelming majority of cases, an IESG-
approved draft immediately enters the queue and goes on to be published, so this
simplification makes the calculation much easier and virtually never affects
eligibility.

Arguments eligibility_period_start and eligibility_period_end are datetimes that
mark the start and end of the eligibility period. These should be five years apart.
"""
# First, get the RFCs using publication date
qualifying_rfc_pub_events = DocEvent.objects.filter(
type='published_rfc',
time__gte=eligibility_period_start,
time__lte=eligibility_period_end,
)
qualifying_rfcs = Document.objects.filter(
type_id="rfc",
docevent__in=qualifying_rfc_pub_events
).annotate(
rfcauthor_count=Count("rfcauthor")
)
rfcs_with_rfcauthors = qualifying_rfcs.filter(rfcauthor_count__gt=0).distinct()
rfcs_without_rfcauthors = qualifying_rfcs.filter(rfcauthor_count=0).distinct()

# Second, get the IESG-approved I-Ds excluding any we're already counting as rfcs
qualifying_approval_events = DocEvent.objects.filter(
type='iesg_approved',
time__gte=eligibility_period_start,
time__lte=eligibility_period_end,
)
qualifying_drafts = Document.objects.filter(
type_id="draft",
docevent__in=qualifying_approval_events,
).exclude(
relateddocument__relationship_id="became_rfc",
relateddocument__target__in=qualifying_rfcs,
).distinct()

return base_qs.filter(
Q(documentauthor__document__in=qualifying_drafts)
| Q(rfcauthor__document__in=rfcs_with_rfcauthors)
| Q(documentauthor__document__in=rfcs_without_rfcauthors)
).annotate(
document_author_count=Count('documentauthor'),
rfc_author_count=Count("rfcauthor")
).annotate(
authorship_count=F("document_author_count") + F("rfc_author_count")
).filter(authorship_count__gte=2)


def get_threerule_eligibility_querysets(date, base_qs, three_of_five_callable):
if not base_qs:
base_qs = Person.objects.all()
Expand Down Expand Up @@ -608,14 +672,7 @@ def get_threerule_eligibility_querysets(date, base_qs, three_of_five_callable):
)
).distinct()

rfc_pks = set(DocEvent.objects.filter(type='published_rfc', time__gte=five_years_ago, time__lte=date_as_dt).values_list('doc__pk', flat=True))
iesgappr_pks = set(DocEvent.objects.filter(type='iesg_approved', time__gte=five_years_ago, time__lte=date_as_dt).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 # BIG TODO: make sure rfcauthor gets counted here - overqualify if necessary
).annotate(
document_author_count = Count('documentauthor')
).filter(document_author_count__gte=2)
author_qs = get_qualified_author_queryset(base_qs, five_years_ago, date_as_dt)
return three_of_five_qs, officer_qs, author_qs

def list_eligible_8989(date, base_qs=None):
Expand Down