Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enable editorial stream adoption and balloting #5011

Merged
merged 12 commits into from
Jan 31, 2023
47 changes: 47 additions & 0 deletions ietf/doc/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -531,3 +531,50 @@ class DocExtResourceFactory(factory.django.DjangoModelFactory):
class Meta:
model = DocExtResource

class EditorialDraftFactory(BaseDocumentFactory):

type_id = 'draft'
group = factory.SubFactory('ietf.group.factories.GroupFactory',acronym='rswg', type_id='rfcedtyp')
stream_id = 'editorial'

@factory.post_generation
def states(obj, create, extracted, **kwargs):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the linter no longer complain about having the first parameter as obj instead of self?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've seen no complaints, and functionally, self makes less sense than obj given that this is a factory, and this is called by the factory engine at untypical times.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok - some places have pylint directives on these lines, but I guess they're obsolete

if not create:
return
if extracted:
for (state_type_id,state_slug) in extracted:
obj.set_state(State.objects.get(type_id=state_type_id,slug=state_slug))
if not obj.get_state('draft-iesg'):
obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists'))
else:
obj.set_state(State.objects.get(type_id='draft',slug='active'))
obj.set_state(State.objects.get(type_id='draft-stream-editorial',slug='active'))
obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists'))

class EditorialRfcFactory(RgDraftFactory):

alias2 = factory.RelatedFactory('ietf.doc.factories.DocAliasFactory','document',name=factory.Sequence(lambda n: 'rfc%04d'%(n+1000)))

std_level_id = 'inf'

@factory.post_generation
def states(obj, create, extracted, **kwargs):
if not create:
return
if extracted:
for (state_type_id,state_slug) in extracted:
obj.set_state(State.objects.get(type_id=state_type_id,slug=state_slug))
if not obj.get_state('draft-stream-editorial'):
obj.set_state(State.objects.get(type_id='draft-stream-editorial', slug='pub'))
if not obj.get_state('draft-iesg'):
obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists'))
else:
obj.set_state(State.objects.get(type_id='draft',slug='rfc'))
obj.set_state(State.objects.get(type_id='draft-stream-editorial', slug='pub'))
obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists'))

@factory.post_generation
def reset_canonical_name(obj, create, extracted, **kwargs):
if hasattr(obj, '_canonical_name'):
del obj._canonical_name
return None
51 changes: 51 additions & 0 deletions ietf/doc/mails.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,9 @@ def generate_publication_request(request, doc):
if doc.stream_id == "irtf":
approving_body = "IRSG"
consensus_body = doc.group.acronym.upper()
if doc.stream_id == "editorial":
approving_body = "RSAB"
consensus_body = doc.group.acronym.upper()
else:
approving_body = str(doc.stream)
consensus_body = approving_body
Expand Down Expand Up @@ -486,6 +489,54 @@ def email_irsg_ballot_closed(request, doc, ballot):
"doc/mail/close_irsg_ballot_mail.txt",
)

def _send_rsab_ballot_email(request, doc, ballot, subject, template):
"""Send email notification when IRSG ballot is issued"""
(to, cc) = gather_address_lists('rsab_ballot_issued', doc=doc)
sender = 'IESG Secretary <[email protected]>'

active_ballot = doc.active_ballot()
if active_ballot is None:
needed_bps = ''
else:
needed_bps = needed_ballot_positions(
doc,
list(active_ballot.active_balloter_positions().values())
)

return send_mail(
request=request,
frm=sender,
to=to,
cc=cc,
subject=subject,
extra={'Reply-To': [sender]},
template=template,
context=dict(
doc=doc,
doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(),
needed_ballot_positions=needed_bps,
))

def email_rsab_ballot_issued(request, doc, ballot):
"""Send email notification when RSAB ballot is issued"""
return _send_rsab_ballot_email(
request,
doc,
ballot,
'RSAB ballot issued: %s to %s'%(doc.file_tag(), std_level_prompt(doc)),
'doc/mail/issue_rsab_ballot_mail.txt',
)

def email_rsab_ballot_closed(request, doc, ballot):
"""Send email notification when RSAB ballot is closed"""
return _send_rsab_ballot_email(
request,
doc,
ballot,
'RSAB ballot closed: %s to %s'%(doc.file_tag(), std_level_prompt(doc)),
"doc/mail/close_rsab_ballot_mail.txt",
)

def email_iana(request, doc, to, msg, cc=None):
# fix up message and send it with extra info on doc in headers
import email
Expand Down
51 changes: 51 additions & 0 deletions ietf/doc/migrations/0049_add_rsab_doc_positions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Copyright The IETF Trust 2022, All Rights Reserved
# -*- coding: utf-8 -*-

from django.db import migrations


def forward(apps, schema_editor):
State = apps.get_model("doc", "State")
State.objects.create(
type_id="draft-stream-editorial",
slug="rsab_review",
name="RSAB Review",
desc="RSAB Review",
used=True,
)
BallotPositionName = apps.get_model("name", "BallotPositionName")
BallotPositionName.objects.create(slug="concern", name="Concern", blocking=True)

BallotType = apps.get_model("doc", "BallotType")
bt = BallotType.objects.create(
doc_type_id="draft",
slug="rsab-approve",
name="RSAB Approve",
question="Is this draft ready for publication in the Editorial stream?",
)
bt.positions.set(
["yes", "concern", "recuse"]
) # See RFC9280 section 3.2.2 list item 9.


def reverse(apps, schema_editor):
State = apps.get_model("doc", "State")
State.objects.filter(type_id="draft-stream-editorial", slug="rsab_review").delete()

Position = apps.get_model("name", "BallotPositionName")
Position.objects.filter(slug="concern").delete()

BallotType = apps.get_model("doc", "BallotType")
BallotType.objects.filter(slug="irsg-approve").delete()


class Migration(migrations.Migration):

dependencies = [
("doc", "0048_allow_longer_notify"),
("name", "0045_polls_and_chatlogs"),
]

operations = [
migrations.RunPython(forward, reverse),
]
40 changes: 40 additions & 0 deletions ietf/doc/migrations/0050_editorial_stream_states.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Copyright The IETF Trust 2022, All Rights Reserved
from django.db import migrations


def forward(apps, schema_editor):
State = apps.get_model("doc", "State")
StateType = apps.get_model("doc", "StateType")
StateType.objects.create(
slug="draft-stream-editorial", label="Editorial stream state"
)
for slug, name, order in (
("repl", "Replaced editorial stream document", 0),
("active", "Active editorial stream document", 2),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No order=1 state? I guess it's a side effect of creating then deleting the rsab_review state and doesn't matter aside from the order.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

order is almost a nuisance field. I think we'll have less confusion with this migration as is. We can make order look prettier (to no good effect otherwise) later.

("rsabpoll", "Editorial stream document under RSAB review", 3),
("pub", "Published RFC", 4),
("dead", "Dead editorial stream document", 5),
):
State.objects.create(
type_id="draft-stream-editorial",
slug=slug,
name=name,
order=order,
used=True,
)


def reverse(apps, schema_editor):
State = apps.get_model("doc", "State")
StateType = apps.get_model("doc", "StateType")
State.objects.filter(type_id="draft-stream-editorial").delete()
StateType.objects.filter(slug="draft-stream-editorial").delete()


class Migration(migrations.Migration):

dependencies = [
("doc", "0049_add_rsab_doc_positions"),
]

operations = [migrations.RunPython(forward, reverse)]
6 changes: 3 additions & 3 deletions ietf/doc/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ class DocumentInfo(models.Model):

states = models.ManyToManyField(State, blank=True) # plain state (Active/Expired/...), IESG state, stream state
tags = models.ManyToManyField(DocTagName, blank=True) # Revised ID Needed, ExternalParty, AD Followup, ...
stream = ForeignKey(StreamName, blank=True, null=True) # IETF, IAB, IRTF, Independent Submission
stream = ForeignKey(StreamName, blank=True, null=True) # IETF, IAB, IRTF, Independent Submission, Editorial
group = ForeignKey(Group, blank=True, null=True) # WG, RG, IAB, IESG, Edu, Tools

abstract = models.TextField(blank=True)
Expand Down Expand Up @@ -1341,7 +1341,7 @@ class BallotDocEvent(DocEvent):
ballot_type = ForeignKey(BallotType)

def active_balloter_positions(self):
"""Return dict mapping each active AD or IRSG member to a current ballot position (or None if they haven't voted)."""
"""Return dict mapping each active member of the balloting body to a current ballot position (or None if they haven't voted)."""
res = {}

active_balloters = get_active_balloters(self.ballot_type)
Expand Down Expand Up @@ -1384,7 +1384,7 @@ def all_positions(self):
while p.old_positions and p.old_positions[-1].slug == "norecord":
p.old_positions.pop()

# add any missing ADs/IRSGers through fake No Record events
# add any missing balloters through fake No Record events
if self.doc.active_ballot() == self:
norecord = BallotPositionName.objects.get(slug="norecord")
for balloter in active_balloters:
Expand Down
8 changes: 6 additions & 2 deletions ietf/doc/templatetags/ballot_icon.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ def showballoticon(doc):
if doc.type_id == "draft":
if doc.stream_id == 'ietf' and doc.get_state_slug("draft-iesg") not in IESG_BALLOT_ACTIVE_STATES:
return False
elif doc.stream_id == 'irtf' and doc.get_state_slug("draft-stream-irtf") not in ['irsgpoll']:
elif doc.stream_id == 'irtf' and doc.get_state_slug("draft-stream-irtf") != "irsgpoll":
return False
elif doc.stream_id == 'editorial' and doc.get_state_slug("draft-stream-rsab") != "rsabpoll":
return False
elif doc.type_id == "charter":
if doc.get_state_slug() not in ("intrev", "extrev", "iesgrev"):
Expand Down Expand Up @@ -105,8 +107,10 @@ def sort_key(t):
break

typename = "Unknown"
if ballot.ballot_type.slug=='irsg-approve':
if ballot.ballot_type.slug == "irsg-approve":
typename = "IRSG"
elif ballot.ballot_type.slug == "rsab-approve":
typename = "RSAB"
else:
typename = "IESG"

Expand Down
8 changes: 4 additions & 4 deletions ietf/doc/templatetags/ietf_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -671,10 +671,10 @@ def can_defer(user,doc):

@register.filter()
def can_ballot(user,doc):
# Only IRSG members (and the secretariat, handled by code separately) can take positions on IRTF documents
# Otherwise, an AD can take a position on anything that has a ballot open
if doc.type_id == 'draft' and doc.stream_id == 'irtf':
return has_role(user,'IRSG Member')
if doc.stream_id == "irtf" and doc.type_id == "draft":
return has_role(user,"IRSG Member")
elif doc.stream_id == "editorial" and doc.type_id == "draft":
return has_role(user,"RSAB Member")
else:
return user.person.role_set.filter(name="ad", group__type="area", group__state="active")

Expand Down
Loading