Merge remote-tracking branch 'origin/main' into feat/postgres
This commit is contained in:
commit
2a1602d9bb
4
.github/ISSUE_TEMPLATE/report-a-bug.yml
vendored
4
.github/ISSUE_TEMPLATE/report-a-bug.yml
vendored
|
@ -22,3 +22,7 @@ body:
|
|||
options:
|
||||
- label: I agree to follow the [IETF's Code of Conduct](https://github.com/ietf-tools/.github/blob/main/CODE_OF_CONDUCT.md)
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
If you are having trouble logging into the datatracker, please do not open an issue here. Instead, please send email to support@ietf.org providing your name and username.
|
||||
|
|
|
@ -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):
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
@ -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 <iesg-secretary@ietf.org>'
|
||||
|
||||
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
|
||||
|
|
51
ietf/doc/migrations/0049_add_rsab_doc_positions.py
Normal file
51
ietf/doc/migrations/0049_add_rsab_doc_positions.py
Normal file
|
@ -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),
|
||||
]
|
43
ietf/doc/migrations/0050_editorial_stream_states.py
Normal file
43
ietf/doc/migrations/0050_editorial_stream_states.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
# 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),
|
||||
("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,
|
||||
)
|
||||
State.objects.filter(type_id="draft-stream-editorial", slug="rsab_review").delete()
|
||||
|
||||
|
||||
|
||||
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()
|
||||
# Intentionally not trying to return broken rsab_review State object
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("doc", "0049_add_rsab_doc_positions"),
|
||||
]
|
||||
|
||||
operations = [migrations.RunPython(forward, reverse)]
|
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -1384,7 +1384,7 @@ class BallotDocEvent(DocEvent):
|
|||
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:
|
||||
|
|
|
@ -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"):
|
||||
|
@ -105,8 +107,10 @@ def ballot_icon(context, doc):
|
|||
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"
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import debug # pyflakes:ignore
|
|||
|
||||
from ietf.doc.models import BallotDocEvent, DocAlias
|
||||
from ietf.doc.models import ConsensusDocEvent
|
||||
from ietf.ietfauth.utils import can_request_rfc_publication as utils_can_request_rfc_publication
|
||||
from ietf.utils.html import sanitize_fragment
|
||||
from ietf.utils import log
|
||||
from ietf.doc.utils import prettify_std_name
|
||||
|
@ -577,6 +578,8 @@ def pos_to_label_format(text):
|
|||
'Recuse': 'bg-recuse text-light',
|
||||
'Not Ready': 'bg-discuss text-light',
|
||||
'Need More Time': 'bg-discuss text-light',
|
||||
'Concern': 'bg-discuss text-light',
|
||||
|
||||
}.get(str(text), 'bg-norecord text-dark')
|
||||
|
||||
@register.filter
|
||||
|
@ -591,6 +594,7 @@ def pos_to_border_format(text):
|
|||
'Recuse': 'border-recuse',
|
||||
'Not Ready': 'border-discuss',
|
||||
'Need More Time': 'border-discuss',
|
||||
'Concern': 'border-discuss',
|
||||
}.get(str(text), 'border-norecord')
|
||||
|
||||
@register.filter
|
||||
|
@ -664,17 +668,25 @@ def charter_minor_rev(rev):
|
|||
@register.filter()
|
||||
def can_defer(user,doc):
|
||||
ballot = doc.latest_event(BallotDocEvent, type="created_ballot")
|
||||
if ballot and (doc.type_id == "draft" or doc.type_id == "conflrev") and doc.stream_id == 'ietf' and has_role(user, 'Area Director,Secretariat'):
|
||||
if ballot and (doc.type_id == "draft" or doc.type_id == "conflrev" or doc.type_id=="statchg") and doc.stream_id == 'ietf' and has_role(user, 'Area Director,Secretariat'):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@register.filter()
|
||||
def can_clear_ballot(user, doc):
|
||||
return can_defer(user, doc)
|
||||
|
||||
@register.filter()
|
||||
def can_request_rfc_publication(user, doc):
|
||||
return utils_can_request_rfc_publication(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")
|
||||
|
||||
|
|
|
@ -17,7 +17,8 @@ from django.utils import timezone
|
|||
from ietf.doc.models import (Document, State, DocEvent,
|
||||
BallotPositionDocEvent, LastCallDocEvent, WriteupDocEvent, TelechatDocEvent)
|
||||
from ietf.doc.factories import (DocumentFactory, IndividualDraftFactory, IndividualRfcFactory, WgDraftFactory,
|
||||
BallotPositionDocEventFactory, BallotDocEventFactory)
|
||||
BallotPositionDocEventFactory, BallotDocEventFactory, IRSGBallotDocEventFactory)
|
||||
from ietf.doc.templatetags.ietf_filters import can_defer
|
||||
from ietf.doc.utils import create_ballot_if_not_open
|
||||
from ietf.doc.views_doc import document_ballot_content
|
||||
from ietf.group.models import Group, Role
|
||||
|
@ -1069,6 +1070,35 @@ class DeferUndeferTestCase(TestCase):
|
|||
DocumentFactory(type_id='statchg',name='status-change-imaginary-mid-review',states=[('statchg','iesgeval')])
|
||||
DocumentFactory(type_id='conflrev',name='conflict-review-imaginary-irtf-submission',states=[('conflrev','iesgeval')])
|
||||
|
||||
class IetfFiltersTests(TestCase):
|
||||
def test_can_defer(self):
|
||||
secretariat = Person.objects.get(user__username="secretary").user
|
||||
ad = Person.objects.get(user__username="ad").user
|
||||
irtf_chair = Person.objects.get(user__username="irtf-chair").user
|
||||
rsab_chair = Person.objects.get(user__username="rsab-chair").user
|
||||
irsg_member = RoleFactory(group__type_id="rg", name_id="chair").person.user
|
||||
rsab_member = RoleFactory(group=Group.objects.get(acronym="rsab"), name_id="member").person.user
|
||||
nobody = PersonFactory().user
|
||||
|
||||
users = set([secretariat, ad, irtf_chair, rsab_chair, irsg_member, rsab_member, nobody])
|
||||
|
||||
iesg_ballot = BallotDocEventFactory(doc__stream_id='ietf')
|
||||
self.assertTrue(can_defer(secretariat, iesg_ballot.doc))
|
||||
self.assertTrue(can_defer(ad, iesg_ballot.doc))
|
||||
for user in users - set([secretariat, ad]):
|
||||
self.assertFalse(can_defer(user, iesg_ballot.doc))
|
||||
|
||||
irsg_ballot = IRSGBallotDocEventFactory(doc__stream_id='irtf')
|
||||
for user in users:
|
||||
self.assertFalse(can_defer(user, irsg_ballot.doc))
|
||||
|
||||
rsab_ballot = BallotDocEventFactory(ballot_type__slug='rsab-approve', doc__stream_id='editorial')
|
||||
for user in users:
|
||||
self.assertFalse(can_defer(user, rsab_ballot.doc))
|
||||
|
||||
def test_can_clear_ballot(self):
|
||||
pass # Right now, can_clear_ballot is implemented by can_defer
|
||||
|
||||
class RegenerateLastCallTestCase(TestCase):
|
||||
|
||||
def test_regenerate_last_call(self):
|
||||
|
|
|
@ -11,6 +11,7 @@ from collections import Counter
|
|||
from pathlib import Path
|
||||
from pyquery import PyQuery
|
||||
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse as urlreverse
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
@ -24,6 +25,7 @@ from ietf.doc.models import ( Document, DocReminder, DocEvent,
|
|||
ConsensusDocEvent, LastCallDocEvent, RelatedDocument, State, TelechatDocEvent,
|
||||
WriteupDocEvent, DocRelationshipName, IanaExpertDocEvent )
|
||||
from ietf.doc.utils import get_tags_for_stream_id, create_ballot_if_not_open
|
||||
from ietf.doc.views_draft import AdoptDraftForm
|
||||
from ietf.name.models import StreamName, DocTagName
|
||||
from ietf.group.factories import GroupFactory, RoleFactory
|
||||
from ietf.group.models import Group, Role
|
||||
|
@ -1598,84 +1600,206 @@ class ReleaseDraftTests(TestCase):
|
|||
|
||||
class AdoptDraftTests(TestCase):
|
||||
def test_adopt_document(self):
|
||||
RoleFactory(group__acronym='mars',group__list_email='mars-wg@ietf.org',person__user__username='marschairman',name_id='chair')
|
||||
draft = IndividualDraftFactory(name='draft-ietf-mars-test',notify='aliens@example.mars')
|
||||
stream_state_type_slug = {
|
||||
"wg": "draft-stream-ietf",
|
||||
"ag": "draft-stream-ietf",
|
||||
"rg": "draft-stream-irtf",
|
||||
"rag": "draft-stream-irtf",
|
||||
"edwg": "draft-stream-editorial",
|
||||
}
|
||||
for type_id in ("wg", "ag", "rg", "rag", "edwg"):
|
||||
chair_role = RoleFactory(group__type_id=type_id,name_id='chair')
|
||||
draft = IndividualDraftFactory(notify=f'{type_id}group@example.mars')
|
||||
|
||||
url = urlreverse('ietf.doc.views_draft.adopt_draft', kwargs=dict(name=draft.name))
|
||||
login_testing_unauthorized(self, "marschairman", url)
|
||||
|
||||
# get
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
q = PyQuery(r.content)
|
||||
self.assertEqual(len(q('form select[name="group"] option')), 1) # we can only select "mars"
|
||||
url = urlreverse('ietf.doc.views_draft.adopt_draft', kwargs=dict(name=draft.name))
|
||||
self.client.logout()
|
||||
login_testing_unauthorized(self, chair_role.person.user.username, url)
|
||||
|
||||
# adopt in mars WG
|
||||
mailbox_before = len(outbox)
|
||||
events_before = draft.docevent_set.count()
|
||||
mars = Group.objects.get(acronym="mars")
|
||||
call_issued = State.objects.get(type='draft-stream-ietf',slug='c-adopt')
|
||||
r = self.client.post(url,
|
||||
dict(comment="some comment",
|
||||
group=mars.pk,
|
||||
newstate=call_issued.pk,
|
||||
weeks="10"))
|
||||
self.assertEqual(r.status_code, 302)
|
||||
# get
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
draft = Document.objects.get(pk=draft.pk)
|
||||
self.assertEqual(draft.group.acronym, "mars")
|
||||
self.assertEqual(draft.stream_id, "ietf")
|
||||
self.assertEqual(draft.docevent_set.count() - events_before, 5)
|
||||
self.assertEqual(draft.notify,"aliens@example.mars")
|
||||
self.assertEqual(len(outbox), mailbox_before + 1)
|
||||
self.assertTrue("Call For Adoption" in outbox[-1]["Subject"])
|
||||
self.assertTrue("mars-chairs@ietf.org" in outbox[-1]['To'])
|
||||
self.assertTrue("draft-ietf-mars-test@" in outbox[-1]['To'])
|
||||
self.assertTrue("mars-wg@" in outbox[-1]['To'])
|
||||
# call for adoption
|
||||
group_type_can_call_for_adoption = State.objects.filter(type_id=stream_state_type_slug[type_id],slug="c-adopt").exists()
|
||||
if group_type_can_call_for_adoption:
|
||||
empty_outbox()
|
||||
events_before = draft.docevent_set.count()
|
||||
call_issued = State.objects.get(type=stream_state_type_slug[type_id],slug='c-adopt')
|
||||
r = self.client.post(url,
|
||||
dict(comment="some comment",
|
||||
group=chair_role.group.pk,
|
||||
newstate=call_issued.pk,
|
||||
weeks="10"))
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
self.assertFalse(mars.list_email in draft.notify)
|
||||
draft = Document.objects.get(pk=draft.pk)
|
||||
self.assertEqual(draft.get_state_slug(stream_state_type_slug[type_id]), "c-adopt")
|
||||
self.assertEqual(draft.group, chair_role.group)
|
||||
self.assertEqual(draft.stream_id, stream_state_type_slug[type_id][13:]) # trim off "draft-stream-"
|
||||
self.assertEqual(draft.docevent_set.count() - events_before, 5)
|
||||
self.assertEqual(len(outbox), 1)
|
||||
self.assertTrue("Call For Adoption" in outbox[-1]["Subject"])
|
||||
self.assertTrue(f"{chair_role.group.acronym}-chairs@" in outbox[-1]['To'])
|
||||
self.assertTrue(f"{draft.name}@" in outbox[-1]['To'])
|
||||
self.assertTrue(f"{chair_role.group.acronym}@" in outbox[-1]['To'])
|
||||
|
||||
def test_right_state_choices_offered(self):
|
||||
draft = IndividualDraftFactory()
|
||||
wg = GroupFactory(type_id='wg',state_id='active')
|
||||
rg = GroupFactory(type_id='rg',state_id='active')
|
||||
person = PersonFactory(user__username='person')
|
||||
# adopt
|
||||
empty_outbox()
|
||||
events_before = draft.docevent_set.count()
|
||||
# There are several possible states that a stream can adopt into - we will only test one per stream
|
||||
stream_adopt_state_slug = "wg-doc" if type_id in ("wg", "ag") else "active"
|
||||
stream_adopt_state = State.objects.get(type=stream_state_type_slug[type_id],slug=stream_adopt_state_slug)
|
||||
r = self.client.post(url,
|
||||
dict(comment="some comment",
|
||||
group=chair_role.group.pk,
|
||||
newstate=stream_adopt_state.pk,
|
||||
weeks="10"))
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
self.client.login(username='person',password='person+password')
|
||||
url = urlreverse('ietf.doc.views_draft.adopt_draft', kwargs=dict(name=draft.name))
|
||||
draft = Document.objects.get(pk=draft.pk)
|
||||
self.assertEqual(draft.get_state_slug(stream_state_type_slug[type_id]), stream_adopt_state_slug)
|
||||
self.assertEqual(draft.group, chair_role.group)
|
||||
self.assertEqual(draft.stream_id, stream_state_type_slug[type_id][13:]) # trim off "draft-stream-"
|
||||
if type_id in ("wg", "ag"):
|
||||
self.assertEqual(
|
||||
Counter(list(draft.docevent_set.values_list('type',flat=True))[events_before:]),
|
||||
Counter({'changed_group': 1, 'changed_stream': 1, 'new_revision': 1})
|
||||
)
|
||||
else:
|
||||
self.assertEqual(
|
||||
Counter(list(draft.docevent_set.values_list('type',flat=True))[events_before:]),
|
||||
Counter({'changed_state': 1, 'added_comment': 1, 'changed_group': 1, 'changed_document': 1, 'changed_stream': 1, 'new_revision': 1})
|
||||
)
|
||||
self.assertEqual(len(outbox), 1 if type_id in ["wg", "ag"] else 2)
|
||||
self.assertTrue(stream_adopt_state.name in outbox[-1]["Subject"])
|
||||
self.assertTrue(f"{chair_role.group.acronym}-chairs@" in outbox[-1]['To'])
|
||||
self.assertTrue(f"{draft.name}@" in outbox[-1]['To'])
|
||||
self.assertTrue(f"{chair_role.group.acronym}@" in outbox[-1]['To'])
|
||||
if type_id not in ["wg", "ag"]:
|
||||
self.assertTrue(outbox[-2]["Subject"].endswith("to Informational"))
|
||||
# recipient fields tested elsewhere
|
||||
|
||||
person.role_set.create(name_id='chair',group=wg,email=person.email())
|
||||
r = self.client.get(url)
|
||||
q = PyQuery(r.content)
|
||||
self.assertTrue('(IETF)' in q('#id_newstate option').text())
|
||||
self.assertFalse('(IRTF)' in q('#id_newstate option').text())
|
||||
|
||||
person.role_set.create(name_id='chair',group=Group.objects.get(acronym='irtf'),email=person.email())
|
||||
r = self.client.get(url)
|
||||
q = PyQuery(r.content)
|
||||
self.assertTrue('(IETF)' in q('#id_newstate option').text())
|
||||
self.assertTrue('(IRTF)' in q('#id_newstate option').text())
|
||||
class AdoptDraftFormTests(TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
# test_data.py made a WG already, and made all the GroupFeatures
|
||||
# This will detect changes in that assumption
|
||||
self.chair_roles = {
|
||||
"wg": Group.objects.filter(
|
||||
type__features__acts_like_wg=True, state="active"
|
||||
)
|
||||
.get()
|
||||
.role_set.get(name_id="chair")
|
||||
}
|
||||
# This set of tests currently assumes all document adopting group types have "chair" in thier docman roles,
|
||||
# and only tests that the form acts correctly for chairs. It should be expanded to use all the roles it finds
|
||||
# in the group of docman roles (which comes from the production database by way of ietf/name/fixtures/names.json)
|
||||
for type_id in ["ag", "rg", "rag", "edwg"]:
|
||||
self.chair_roles[type_id] = RoleFactory(
|
||||
group__type_id=type_id, name_id="chair"
|
||||
)
|
||||
|
||||
person.role_set.filter(group__acronym='irtf').delete()
|
||||
person.role_set.create(name_id='chair',group=rg,email=person.email())
|
||||
r = self.client.get(url)
|
||||
q = PyQuery(r.content)
|
||||
self.assertTrue('(IETF)' in q('#id_newstate option').text())
|
||||
self.assertTrue('(IRTF)' in q('#id_newstate option').text())
|
||||
def test_form_init(self):
|
||||
secretariat = Person.objects.get(user__username="secretary")
|
||||
f = AdoptDraftForm(user=secretariat.user)
|
||||
form_offers_groups = f.fields["group"].queryset
|
||||
self.assertEqual(
|
||||
set(form_offers_groups.all()),
|
||||
set(
|
||||
Group.objects.filter(type__features__acts_like_wg=True, state="active")
|
||||
),
|
||||
)
|
||||
self.assertEqual(form_offers_groups.count(), 5)
|
||||
form_offers_states = State.objects.filter(
|
||||
pk__in=[t[0] for t in f.fields["newstate"].choices[1:]]
|
||||
)
|
||||
self.assertEqual(
|
||||
Counter(form_offers_states.values_list("type_id", flat=True)),
|
||||
Counter(
|
||||
{
|
||||
"draft-stream-irtf": 14,
|
||||
"draft-stream-ietf": 12,
|
||||
"draft-stream-editorial": 5,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
person.role_set.filter(group=wg).delete()
|
||||
r = self.client.get(url)
|
||||
q = PyQuery(r.content)
|
||||
self.assertFalse('(IETF)' in q('#id_newstate option').text())
|
||||
self.assertTrue('(IRTF)' in q('#id_newstate option').text())
|
||||
irtf_chair = Person.objects.get(user__username="irtf-chair")
|
||||
f = AdoptDraftForm(user=irtf_chair.user)
|
||||
form_offers_groups = f.fields["group"].queryset
|
||||
self.assertEqual(
|
||||
set(form_offers_groups.all()),
|
||||
set(Group.objects.filter(type_id__in=("rag", "rg"), state="active")),
|
||||
)
|
||||
self.assertEqual(form_offers_groups.count(), 2)
|
||||
form_offers_states = State.objects.filter(
|
||||
pk__in=[t[0] for t in f.fields["newstate"].choices[1:]]
|
||||
)
|
||||
self.assertEqual(
|
||||
set(form_offers_states.values_list("type_id", flat=True)),
|
||||
set(["draft-stream-irtf"]),
|
||||
)
|
||||
|
||||
person.role_set.all().delete()
|
||||
person.role_set.create(name_id='secr',group=Group.objects.get(acronym='secretariat'),email=person.email())
|
||||
r = self.client.get(url)
|
||||
q = PyQuery(r.content)
|
||||
self.assertTrue('(IETF)' in q('#id_newstate option').text())
|
||||
self.assertTrue('(IRTF)' in q('#id_newstate option').text())
|
||||
stream_state_type_slug = {
|
||||
"wg": "draft-stream-ietf",
|
||||
"ag": "draft-stream-ietf",
|
||||
"rg": "draft-stream-irtf",
|
||||
"rag": "draft-stream-irtf",
|
||||
"edwg": "draft-stream-editorial",
|
||||
}
|
||||
for type_id in self.chair_roles:
|
||||
f = AdoptDraftForm(user=self.chair_roles[type_id].person.user)
|
||||
form_offers_groups = f.fields["group"].queryset
|
||||
self.assertEqual(form_offers_groups.get(), self.chair_roles[type_id].group)
|
||||
form_offers_states = State.objects.filter(
|
||||
pk__in=[t[0] for t in f.fields["newstate"].choices[1:]]
|
||||
)
|
||||
self.assertEqual(
|
||||
set(form_offers_states.values_list("type_id", flat=True)),
|
||||
set([stream_state_type_slug[type_id]]),
|
||||
)
|
||||
|
||||
edwgchair_role = self.chair_roles["edwg"]
|
||||
RoleFactory(group__type_id="wg", person=edwgchair_role.person, name_id="chair")
|
||||
RoleFactory(group__type_id="rg", person=edwgchair_role.person, name_id="chair")
|
||||
f = AdoptDraftForm(user=edwgchair_role.person.user)
|
||||
form_offers_groups = f.fields["group"].queryset
|
||||
self.assertEqual(
|
||||
set(form_offers_groups.values_list("type_id", flat=True)),
|
||||
set(["edwg", "wg", "rg"]),
|
||||
)
|
||||
self.assertEqual(form_offers_groups.count(), 3)
|
||||
form_offers_states = State.objects.filter(
|
||||
pk__in=[t[0] for t in f.fields["newstate"].choices[1:]]
|
||||
)
|
||||
self.assertEqual(
|
||||
set(form_offers_states.values_list("type_id", flat=True)),
|
||||
set(["draft-stream-irtf", "draft-stream-ietf", "draft-stream-editorial"]),
|
||||
)
|
||||
|
||||
also_chairs_wg = RoleFactory(
|
||||
group__type_id="wg", person=irtf_chair, name_id="chair"
|
||||
)
|
||||
f = AdoptDraftForm(user=irtf_chair.user)
|
||||
form_offers_groups = f.fields["group"].queryset
|
||||
self.assertEqual(
|
||||
set(form_offers_groups.all()),
|
||||
set(
|
||||
Group.objects.filter(
|
||||
Q(type_id__in=("rag", "rg")) | Q(pk=also_chairs_wg.group.pk),
|
||||
state="active",
|
||||
)
|
||||
),
|
||||
)
|
||||
self.assertEqual(form_offers_groups.count(), 4)
|
||||
form_offers_states = State.objects.filter(
|
||||
pk__in=[t[0] for t in f.fields["newstate"].choices[1:]]
|
||||
)
|
||||
self.assertEqual(
|
||||
set(form_offers_states.values_list("type_id", flat=True)),
|
||||
set(["draft-stream-irtf", "draft-stream-ietf"]),
|
||||
)
|
||||
|
||||
class ChangeStreamStateTests(TestCase):
|
||||
def test_set_tags(self):
|
||||
|
|
|
@ -446,7 +446,7 @@ class IRSGMemberTests(TestCase):
|
|||
def test_cant_issue_irsg_ballot(self):
|
||||
draft = RgDraftFactory()
|
||||
due = datetime_today(DEADLINE_TZINFO) + datetime.timedelta(days=14)
|
||||
url = urlreverse('ietf.doc.views_ballot.close_irsg_ballot', kwargs=dict(name=draft.name))
|
||||
url = urlreverse('ietf.doc.views_ballot.issue_irsg_ballot', kwargs=dict(name=draft.name))
|
||||
|
||||
self.client.login(username = self.username, password = self.username+'+password')
|
||||
r = self.client.get(url)
|
||||
|
|
601
ietf/doc/tests_rsab_ballot.py
Normal file
601
ietf/doc/tests_rsab_ballot.py
Normal file
|
@ -0,0 +1,601 @@
|
|||
# Copyright The IETF Trust 2022, All Rights Reserved
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# import datetime
|
||||
# from pyquery import PyQuery
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
from django.urls import reverse as urlreverse
|
||||
|
||||
from ietf.utils.mail import outbox, empty_outbox, get_payload_text
|
||||
from ietf.utils.test_utils import TestCase, unicontent, login_testing_unauthorized
|
||||
from ietf.doc.factories import (
|
||||
EditorialDraftFactory,
|
||||
EditorialRfcFactory,
|
||||
IndividualDraftFactory,
|
||||
WgDraftFactory,
|
||||
RgDraftFactory,
|
||||
BallotDocEventFactory,
|
||||
IRSGBallotDocEventFactory,
|
||||
BallotPositionDocEventFactory,
|
||||
)
|
||||
from ietf.doc.models import BallotPositionDocEvent
|
||||
from ietf.doc.utils import create_ballot_if_not_open, close_ballot
|
||||
from ietf.person.utils import get_active_rsab, get_active_ads, get_active_irsg
|
||||
from ietf.group.factories import RoleFactory
|
||||
from ietf.group.models import Group, Role
|
||||
from ietf.person.models import Person
|
||||
|
||||
|
||||
class IssueRSABBallotTests(TestCase):
|
||||
def test_issue_ballot_button_presence(self):
|
||||
|
||||
individual_draft = IndividualDraftFactory()
|
||||
wg_draft = WgDraftFactory()
|
||||
rg_draft = RgDraftFactory()
|
||||
ed_draft = EditorialDraftFactory()
|
||||
ed_rfc = EditorialRfcFactory()
|
||||
|
||||
# login as an RSAB chair
|
||||
self.client.login(username="rsab-chair", password="rsab-chair+password")
|
||||
|
||||
for name in [
|
||||
doc.canonical_name()
|
||||
for doc in (individual_draft, wg_draft, rg_draft, ed_rfc)
|
||||
]:
|
||||
url = urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=name))
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertNotIn("Issue RSAB ballot", unicontent(r))
|
||||
|
||||
url = urlreverse(
|
||||
"ietf.doc.views_doc.document_main", kwargs=dict(name=ed_draft.name)
|
||||
)
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertIn("Issue RSAB ballot", unicontent(r))
|
||||
|
||||
self.client.logout()
|
||||
url = urlreverse(
|
||||
"ietf.doc.views_doc.document_main", kwargs=dict(name=ed_draft.name)
|
||||
)
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertNotIn("Issue RSAB ballot", unicontent(r))
|
||||
|
||||
def test_close_ballot_button_presence(self):
|
||||
individual_draft = IndividualDraftFactory()
|
||||
wg_draft = WgDraftFactory()
|
||||
rg_draft = RgDraftFactory()
|
||||
ed_draft = EditorialDraftFactory()
|
||||
ed_rfc = EditorialRfcFactory()
|
||||
iesgmember = get_active_ads()[0]
|
||||
irsgmember = get_active_irsg()[0]
|
||||
|
||||
BallotDocEventFactory(doc=ed_draft, ballot_type__slug="rsab-approve")
|
||||
|
||||
url = urlreverse(
|
||||
"ietf.doc.views_doc.document_main", kwargs=dict(name=ed_draft.name)
|
||||
)
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertNotIn("Close RSAB ballot", unicontent(r))
|
||||
|
||||
# Login as other body balloters to see if the ballot close button appears
|
||||
for member in (iesgmember, irsgmember):
|
||||
url = urlreverse(
|
||||
"ietf.doc.views_doc.document_main", kwargs=dict(name=ed_draft.name)
|
||||
)
|
||||
self.client.login(
|
||||
username=member.user.username,
|
||||
password=member.user.username + "+password",
|
||||
)
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertNotIn("Close RSAB ballot", unicontent(r))
|
||||
|
||||
# Try to get the ballot closing page directly
|
||||
url = urlreverse(
|
||||
"ietf.doc.views_ballot.close_rsab_ballot",
|
||||
kwargs=dict(name=ed_draft.name),
|
||||
)
|
||||
r = self.client.get(url)
|
||||
self.assertNotEqual(r.status_code, 200)
|
||||
self.client.logout()
|
||||
|
||||
# Login as the RSAB chair
|
||||
self.client.login(username="rsab-chair", password="rsab-chair+password")
|
||||
|
||||
# The close button should now be available
|
||||
url = urlreverse(
|
||||
"ietf.doc.views_doc.document_main", kwargs=dict(name=ed_draft.name)
|
||||
)
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertIn("Close RSAB ballot", unicontent(r))
|
||||
|
||||
# Get the page with the Close RSAB Ballot Yes/No buttons
|
||||
url = urlreverse(
|
||||
"ietf.doc.views_ballot.close_rsab_ballot", kwargs=dict(name=ed_draft.name)
|
||||
)
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
# Login as the Secretariat
|
||||
self.client.logout()
|
||||
self.client.login(username="secretary", password="secretary+password")
|
||||
|
||||
# The close button should be available
|
||||
url = urlreverse(
|
||||
"ietf.doc.views_doc.document_main", kwargs=dict(name=ed_draft.name)
|
||||
)
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertIn("Close RSAB ballot", unicontent(r))
|
||||
|
||||
# Individual, IETF, and RFC docs should not show the Close button.
|
||||
for draft in (individual_draft, wg_draft, rg_draft, ed_rfc):
|
||||
url = urlreverse(
|
||||
"ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)
|
||||
)
|
||||
r = self.client.get(url, follow=True)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertNotIn("Close RSAB ballot", unicontent(r))
|
||||
|
||||
# TODO: This has a lot of redundancy with the BaseManipulation tests that should be refactored to speed tests up.
|
||||
def test_issue_ballot(self):
|
||||
|
||||
ed_draft1 = EditorialDraftFactory()
|
||||
ed_draft2 = EditorialDraftFactory()
|
||||
iesgmember = get_active_ads()[0]
|
||||
|
||||
self.assertFalse(ed_draft1.ballot_open("rsab-approve"))
|
||||
|
||||
self.client.login(username="rsab-chair", password="rsab-chair+password")
|
||||
url = urlreverse(
|
||||
"ietf.doc.views_ballot.issue_rsab_ballot", kwargs=dict(name=ed_draft1.name)
|
||||
)
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
# Buttons present?
|
||||
self.assertIn("rsab_button", unicontent(r))
|
||||
|
||||
# Press the No button - expect nothing but a redirect back to the draft's main page
|
||||
r = self.client.post(url, dict(rsab_button="No"))
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
# Press the Yes button
|
||||
r = self.client.post(url, dict(rsab_button="Yes"))
|
||||
self.assertEqual(r.status_code, 302)
|
||||
self.assertTrue(ed_draft1.ballot_open("rsab-approve"))
|
||||
|
||||
# Having issued a ballot, the Issue RSAB ballot button should be gone
|
||||
url = urlreverse(
|
||||
"ietf.doc.views_doc.document_main", kwargs=dict(name=ed_draft1.name)
|
||||
)
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertNotIn("Issue RSAB ballot", unicontent(r))
|
||||
|
||||
# The RSAB evaluation record tab should exist
|
||||
self.assertIn("RSAB evaluation record", unicontent(r))
|
||||
# The RSAB evaluation record tab should not indicate unavailability
|
||||
self.assertNotIn(
|
||||
"RSAB Evaluation Ballot has not been created yet", unicontent(r)
|
||||
) # TODO: why is this a thing? We don't ever show the tab unless there's a ballot. May need to reconsider how we treat the IESG.
|
||||
|
||||
# We should find an RSAB member's name on the RSAB evaluation tab regardless of any positions taken or not
|
||||
url = urlreverse(
|
||||
"ietf.doc.views_doc.document_rsab_ballot", kwargs=dict(name=ed_draft1.name)
|
||||
)
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
rsabmembers = get_active_rsab()
|
||||
self.assertNotEqual(len(rsabmembers), 0)
|
||||
for member in rsabmembers:
|
||||
self.assertIn(member.name, unicontent(r))
|
||||
|
||||
# Having issued a ballot, it should appear on the RSAB Ballot Status page
|
||||
url = urlreverse("ietf.doc.views_ballot.rsab_ballot_status")
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
# Does the draft name appear on the page?
|
||||
self.assertIn(ed_draft1.name, unicontent(r))
|
||||
|
||||
self.client.logout()
|
||||
|
||||
# Test that an IESG member cannot issue an RSAB ballot
|
||||
self.client.login(
|
||||
username=iesgmember.user.username,
|
||||
password=iesgmember.user.username + "password",
|
||||
)
|
||||
|
||||
url = urlreverse(
|
||||
"ietf.doc.views_ballot.issue_rsab_ballot", kwargs=dict(name=ed_draft2.name)
|
||||
)
|
||||
r = self.client.get(url)
|
||||
self.assertNotEqual(r.status_code, 200)
|
||||
# Buttons present?
|
||||
self.assertNotIn("rsab_button", unicontent(r))
|
||||
|
||||
# Attempt to press the Yes button anyway
|
||||
r = self.client.post(url, dict(rsab_button="Yes"))
|
||||
self.assertTrue(r.status_code == 302 and "/accounts/login" in r["Location"])
|
||||
|
||||
def test_edit_ballot_position_permissions(self):
|
||||
ed_draft = EditorialDraftFactory()
|
||||
ad = RoleFactory(group__type_id="area", name_id="ad")
|
||||
pre_ad = RoleFactory(group__type_id="area", name_id="pre-ad")
|
||||
irsgmember = get_active_irsg()[0]
|
||||
rsab_chair = Role.objects.get(group__acronym="rsab", name="chair")
|
||||
ballot = create_ballot_if_not_open(
|
||||
None, ed_draft, rsab_chair.person, "rsab-approve"
|
||||
)
|
||||
|
||||
url = urlreverse(
|
||||
"ietf.doc.views_ballot.edit_position",
|
||||
kwargs=dict(name=ed_draft.name, ballot_id=ballot.pk),
|
||||
)
|
||||
|
||||
for person in (ad.person, pre_ad.person, irsgmember):
|
||||
self.client.login(
|
||||
username=person.user.username,
|
||||
password=f"{person.user.username}+password",
|
||||
)
|
||||
r = self.client.post(
|
||||
url, dict(position="concern", discuss="Test discuss text")
|
||||
)
|
||||
self.assertEqual(r.status_code, 403)
|
||||
self.client.logout()
|
||||
|
||||
def test_iesg_ballot_no_rsab_actions(self):
|
||||
ad = Person.objects.get(user__username="ad")
|
||||
wg_draft = IndividualDraftFactory(ad=ad)
|
||||
RoleFactory.create_batch(
|
||||
2, name_id="member", group=Group.objects.get(acronym="rsab")
|
||||
)
|
||||
rsabmember = get_active_rsab()[0]
|
||||
|
||||
url = urlreverse(
|
||||
"ietf.doc.views_ballot.ballot_writeupnotes", kwargs=dict(name=wg_draft.name)
|
||||
)
|
||||
|
||||
# RSAB members should not be able to issue IESG ballots
|
||||
login_testing_unauthorized(self, rsabmember.user.username, url)
|
||||
r = self.client.post(
|
||||
url, dict(ballot_writeup="This is a test.", issue_ballot="1")
|
||||
)
|
||||
self.assertNotEqual(r.status_code, 200)
|
||||
|
||||
self.client.logout()
|
||||
login_testing_unauthorized(self, "ad", url)
|
||||
|
||||
BallotDocEventFactory(doc=wg_draft)
|
||||
|
||||
# rsab members (who are not also IESG members) can't take positions
|
||||
ballot = wg_draft.active_ballot()
|
||||
url = urlreverse(
|
||||
"ietf.doc.views_ballot.edit_position",
|
||||
kwargs=dict(name=wg_draft.name, ballot_id=ballot.pk),
|
||||
)
|
||||
self.client.logout()
|
||||
login_testing_unauthorized(self, rsabmember.user.username, url)
|
||||
|
||||
r = self.client.post(url, dict(position="discuss", discuss="Test discuss text"))
|
||||
self.assertEqual(r.status_code, 403)
|
||||
|
||||
|
||||
class BaseManipulationTests:
|
||||
def test_issue_ballot(self):
|
||||
draft = EditorialDraftFactory()
|
||||
url = urlreverse(
|
||||
"ietf.doc.views_ballot.issue_rsab_ballot", kwargs=dict(name=draft.name)
|
||||
)
|
||||
empty_outbox()
|
||||
|
||||
login_testing_unauthorized(self, self.username, url)
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
r = self.client.post(url, {"rsab_button": "No"})
|
||||
self.assertEqual(r.status_code, 302)
|
||||
self.assertIsNone(draft.ballot_open("rsab-approve"))
|
||||
|
||||
# No notifications should have been generated yet
|
||||
self.assertEqual(len(outbox), 0)
|
||||
|
||||
r = self.client.post(url, {"rsab_button": "Yes"})
|
||||
self.assertEqual(r.status_code, 302)
|
||||
self.assertIsNotNone(draft.ballot_open("rsab-approve"))
|
||||
|
||||
# Should have sent a notification about the new ballot
|
||||
self.assertEqual(len(outbox), 1)
|
||||
msg = outbox[0]
|
||||
self.assertIn("RSAB ballot issued", msg["Subject"])
|
||||
self.assertIn("iesg-secretary@ietf.org", msg["From"])
|
||||
self.assertIn(draft.name, msg["Cc"])
|
||||
self.assertIn("rsab@rfc-editor.org", msg["To"])
|
||||
|
||||
def test_take_and_email_position(self):
|
||||
draft = EditorialDraftFactory()
|
||||
ballot = BallotDocEventFactory(doc=draft, ballot_type__slug="rsab-approve")
|
||||
url = (
|
||||
urlreverse(
|
||||
"ietf.doc.views_ballot.edit_position",
|
||||
kwargs=dict(name=draft.name, ballot_id=ballot.pk),
|
||||
)
|
||||
+ self.balloter
|
||||
)
|
||||
empty_outbox()
|
||||
|
||||
login_testing_unauthorized(self, self.username, url)
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
r = self.client.post(
|
||||
url,
|
||||
dict(position="yes", comment="oib239sb", send_mail="Save and send email"),
|
||||
)
|
||||
self.assertEqual(r.status_code, 302)
|
||||
e = draft.latest_event(BallotPositionDocEvent)
|
||||
self.assertEqual(e.pos.slug, "yes")
|
||||
self.assertEqual(e.comment, "oib239sb")
|
||||
|
||||
url = (
|
||||
urlreverse(
|
||||
"ietf.doc.views_ballot.send_ballot_comment",
|
||||
kwargs=dict(name=draft.name, ballot_id=ballot.pk),
|
||||
)
|
||||
+ self.balloter
|
||||
)
|
||||
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
r = self.client.post(
|
||||
url,
|
||||
dict(
|
||||
cc_choices=["doc_authors", "doc_group_chairs", "doc_group_mail_list"],
|
||||
body="Stuff",
|
||||
),
|
||||
)
|
||||
self.assertEqual(r.status_code, 302)
|
||||
self.assertEqual(len(outbox), 1)
|
||||
self.assertNotIn("discuss-criteria", get_payload_text(outbox[0]))
|
||||
|
||||
def test_close_ballot(self):
|
||||
draft = EditorialDraftFactory()
|
||||
BallotDocEventFactory(doc=draft, ballot_type__slug="rsab-approve")
|
||||
url = urlreverse(
|
||||
"ietf.doc.views_ballot.close_rsab_ballot", kwargs=dict(name=draft.name)
|
||||
)
|
||||
empty_outbox()
|
||||
|
||||
login_testing_unauthorized(self, self.username, url)
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
r = self.client.post(url, dict(rsab_button="No"))
|
||||
self.assertEqual(r.status_code, 302)
|
||||
self.assertIsNotNone(draft.ballot_open("rsab-approve"))
|
||||
|
||||
# Should not have generated a notification yet
|
||||
self.assertEqual(len(outbox), 0)
|
||||
|
||||
r = self.client.post(url, dict(rsab_button="Yes"))
|
||||
self.assertEqual(r.status_code, 302)
|
||||
self.assertIsNone(draft.ballot_open("rsab-approve"))
|
||||
|
||||
# Closing the ballot should have generated a notification
|
||||
self.assertEqual(len(outbox), 1)
|
||||
msg = outbox[0]
|
||||
self.assertIn("RSAB ballot closed", msg["Subject"])
|
||||
self.assertIn("iesg-secretary@ietf.org", msg["From"])
|
||||
self.assertIn("rsab@rfc-editor.org", msg["To"])
|
||||
self.assertIn(f"{draft.name}@ietf.org", msg["Cc"])
|
||||
|
||||
def test_view_outstanding_ballots(self):
|
||||
draft = EditorialDraftFactory()
|
||||
BallotDocEventFactory(doc=draft, ballot_type__slug="rsab-approve")
|
||||
url = urlreverse("ietf.doc.views_ballot.rsab_ballot_status")
|
||||
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertIn(draft.name, unicontent(r))
|
||||
|
||||
close_ballot(
|
||||
draft, Person.objects.get(user__username=self.username), "rsab-approve"
|
||||
)
|
||||
r = self.client.get(url)
|
||||
self.assertNotIn(draft.name, unicontent(r))
|
||||
|
||||
|
||||
class RSABChairTests(BaseManipulationTests, TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.username = "rsab-chair"
|
||||
self.balloter = ""
|
||||
|
||||
|
||||
class SecretariatTests(BaseManipulationTests, TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.username = "secretary"
|
||||
self.balloter = "?balloter={}".format(
|
||||
Person.objects.get(user__username="rsab-chair").pk
|
||||
)
|
||||
|
||||
|
||||
class RSABMemberTests(TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.username = RoleFactory(
|
||||
group__acronym="rsab", name_id="member"
|
||||
).person.user.username
|
||||
|
||||
def test_cant_issue_rsab_ballot(self):
|
||||
draft = EditorialDraftFactory()
|
||||
url = urlreverse(
|
||||
"ietf.doc.views_ballot.issue_rsab_ballot", kwargs=dict(name=draft.name)
|
||||
)
|
||||
|
||||
self.client.login(username=self.username, password=self.username + "+password")
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 403)
|
||||
|
||||
r = self.client.post(url, {"rsab_button": "Yes"})
|
||||
self.assertEqual(r.status_code, 403)
|
||||
|
||||
def test_cant_close_rsab_ballot(self):
|
||||
draft = EditorialDraftFactory()
|
||||
BallotDocEventFactory(doc=draft, ballot_type__slug="rsab-approve")
|
||||
url = urlreverse(
|
||||
"ietf.doc.views_ballot.close_rsab_ballot", kwargs=dict(name=draft.name)
|
||||
)
|
||||
|
||||
self.client.login(username=self.username, password=self.username + "+password")
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 403)
|
||||
|
||||
r = self.client.post(url, dict(rsab_button="Yes"))
|
||||
self.assertEqual(r.status_code, 403)
|
||||
|
||||
def test_cant_act_on_other_bodies_ballots(self):
|
||||
ietf_doc = WgDraftFactory()
|
||||
irtf_doc = RgDraftFactory()
|
||||
|
||||
self.client.login(username=self.username, password=f"{self.username}+password")
|
||||
|
||||
url = urlreverse(
|
||||
"ietf.doc.views_ballot.ballot_writeupnotes", kwargs=dict(name=ietf_doc.name)
|
||||
)
|
||||
self.assertEqual(self.client.get(url).status_code, 403)
|
||||
self.assertEqual(
|
||||
self.client.post(
|
||||
url,
|
||||
dict(ballot_writeup="This is a simple test.", save_ballot_writeup="1"),
|
||||
).status_code,
|
||||
403,
|
||||
)
|
||||
|
||||
url = urlreverse(
|
||||
"ietf.doc.views_ballot.issue_irsg_ballot", kwargs=dict(name=irtf_doc.name)
|
||||
)
|
||||
self.assertEqual(self.client.get(url).status_code, 403)
|
||||
self.assertEqual(
|
||||
self.client.post(
|
||||
url, dict(irsg_button="Yes", duedate="2038-01-19")
|
||||
).status_code,
|
||||
403,
|
||||
)
|
||||
|
||||
for name, ballot_id in [
|
||||
(ietf_doc.name, BallotDocEventFactory(doc=ietf_doc).pk),
|
||||
(irtf_doc.name, IRSGBallotDocEventFactory(doc=irtf_doc).pk),
|
||||
]:
|
||||
url = urlreverse(
|
||||
"ietf.doc.views_ballot.edit_position",
|
||||
kwargs=dict(name=name, ballot_id=ballot_id),
|
||||
)
|
||||
self.assertEqual(
|
||||
self.client.get(url).status_code, 200
|
||||
) # TODO : WHAT?! : This is strange, and probably tied up badly with pre-ad, and it should change.
|
||||
self.assertEqual(
|
||||
self.client.post(
|
||||
url,
|
||||
dict(position="yes"),
|
||||
).status_code,
|
||||
403,
|
||||
)
|
||||
|
||||
url = urlreverse(
|
||||
"ietf.doc.views_ballot.close_irsg_ballot", kwargs=dict(name=irtf_doc.name)
|
||||
)
|
||||
self.assertEqual(self.client.get(url).status_code, 403)
|
||||
self.assertEqual(
|
||||
self.client.post(url, dict(irsg_button="Yes")).status_code, 403
|
||||
)
|
||||
|
||||
# Closing iesg ballots happens as a side-effect of secretariat actions with access testing done elsewhere
|
||||
|
||||
def test_take_and_email_position(self):
|
||||
draft = EditorialDraftFactory()
|
||||
ballot = BallotDocEventFactory(doc=draft, ballot_type__slug="rsab-approve")
|
||||
url = urlreverse(
|
||||
"ietf.doc.views_ballot.edit_position",
|
||||
kwargs=dict(name=draft.name, ballot_id=ballot.pk),
|
||||
)
|
||||
empty_outbox()
|
||||
|
||||
login_testing_unauthorized(self, self.username, url)
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
r = self.client.post(
|
||||
url,
|
||||
dict(position="yes", comment="oib239sb", send_mail="Save and send email"),
|
||||
)
|
||||
self.assertEqual(r.status_code, 302)
|
||||
e = draft.latest_event(BallotPositionDocEvent)
|
||||
self.assertEqual(e.pos.slug, "yes")
|
||||
self.assertEqual(e.comment, "oib239sb")
|
||||
|
||||
url = urlreverse(
|
||||
"ietf.doc.views_ballot.send_ballot_comment",
|
||||
kwargs=dict(name=draft.name, ballot_id=ballot.pk),
|
||||
)
|
||||
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
r = self.client.post(
|
||||
url,
|
||||
dict(
|
||||
cc_choices=["doc_authors", "doc_group_chairs", "doc_group_mail_list"],
|
||||
body="Stuff",
|
||||
),
|
||||
)
|
||||
self.assertEqual(r.status_code, 302)
|
||||
self.assertEqual(len(outbox), 1)
|
||||
|
||||
|
||||
class NobodyTests(TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.draft = EditorialDraftFactory()
|
||||
self.ballot = BallotDocEventFactory(
|
||||
doc=self.draft, ballot_type__slug="rsab-approve"
|
||||
)
|
||||
BallotPositionDocEventFactory(
|
||||
ballot=self.ballot,
|
||||
by=get_active_rsab()[0],
|
||||
pos_id="yes",
|
||||
comment="b2390sn3",
|
||||
)
|
||||
|
||||
def can_see_RSAB_tab(self):
|
||||
url = urlreverse(
|
||||
"ietf.doc.views_doc.document_rsab_ballot", kwargs=dict(name=self.draft.name)
|
||||
)
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertIn("b2390sn3", unicontent(r))
|
||||
|
||||
def test_cant_take_position_on_irtf_ballot(self):
|
||||
|
||||
url = urlreverse(
|
||||
"ietf.doc.views_ballot.edit_position",
|
||||
kwargs=dict(name=self.draft.name, ballot_id=self.ballot.pk),
|
||||
)
|
||||
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 302)
|
||||
self.assertIn("/accounts/login", r["Location"])
|
||||
|
||||
r = self.client.post(
|
||||
url,
|
||||
dict(position="yes", comment="oib239sb", send_mail="Save and send email"),
|
||||
)
|
||||
self.assertEqual(r.status_code, 302)
|
||||
self.assertIn("/accounts/login", r["Location"])
|
|
@ -96,6 +96,7 @@ urlpatterns = [
|
|||
url(r'^recent/?$', views_search.recent_drafts),
|
||||
url(r'^select2search/(?P<model_name>(document|docalias))/(?P<doc_type>draft)/$', views_search.ajax_select2_search_docs),
|
||||
url(r'^ballots/irsg/$', views_ballot.irsg_ballot_status),
|
||||
url(r'^ballots/rsab/$', views_ballot.rsab_ballot_status),
|
||||
|
||||
url(r'^%(name)s(?:/%(rev)s)?/$' % settings.URL_REGEXPS, views_doc.document_main),
|
||||
url(r'^%(name)s(?:/%(rev)s)?/bibtex/$' % settings.URL_REGEXPS, views_doc.document_bibtex),
|
||||
|
@ -110,6 +111,7 @@ urlpatterns = [
|
|||
url(r'^%(name)s/referencedby/$' % settings.URL_REGEXPS, views_doc.document_referenced_by),
|
||||
url(r'^%(name)s/ballot/(iesg/)?$' % settings.URL_REGEXPS, views_doc.document_ballot),
|
||||
url(r'^%(name)s/ballot/irsg/$' % settings.URL_REGEXPS, views_doc.document_irsg_ballot),
|
||||
url(r'^%(name)s/ballot/rsab/$' % settings.URL_REGEXPS, views_doc.document_rsab_ballot),
|
||||
url(r'^%(name)s/ballot/(?P<ballot_id>[0-9]+)/$' % settings.URL_REGEXPS, views_doc.document_ballot),
|
||||
url(r'^%(name)s/ballot/(?P<ballot_id>[0-9]+)/position/$' % settings.URL_REGEXPS, views_ballot.edit_position),
|
||||
url(r'^%(name)s/ballot/(?P<ballot_id>[0-9]+)/emailposition/$' % settings.URL_REGEXPS, views_ballot.send_ballot_comment),
|
||||
|
@ -161,6 +163,8 @@ urlpatterns = [
|
|||
url(r'^%(name)s/edit/resources/$' % settings.URL_REGEXPS, views_draft.edit_doc_extresources),
|
||||
url(r'^%(name)s/edit/issueballot/irsg/$' % settings.URL_REGEXPS, views_ballot.issue_irsg_ballot),
|
||||
url(r'^%(name)s/edit/closeballot/irsg/$' % settings.URL_REGEXPS, views_ballot.close_irsg_ballot),
|
||||
url(r'^%(name)s/edit/issueballot/rsab/$' % settings.URL_REGEXPS, views_ballot.issue_rsab_ballot),
|
||||
url(r'^%(name)s/edit/closeballot/rsab/$' % settings.URL_REGEXPS, views_ballot.close_rsab_ballot),
|
||||
|
||||
url(r'^help/state/(?P<type>[\w-]+)/$', views_help.state_help),
|
||||
url(r'^help/relationships/$', views_help.relationship_help),
|
||||
|
|
|
@ -11,7 +11,7 @@ import os
|
|||
import re
|
||||
import textwrap
|
||||
|
||||
from collections import defaultdict, namedtuple
|
||||
from collections import defaultdict, namedtuple, Counter
|
||||
from typing import Union
|
||||
from urllib.parse import quote
|
||||
from zoneinfo import ZoneInfo
|
||||
|
@ -35,7 +35,7 @@ from ietf.doc.models import DocAlias, RelatedDocument, RelatedDocHistory, Ballot
|
|||
from ietf.doc.models import DocEvent, ConsensusDocEvent, BallotDocEvent, IRSGBallotDocEvent, NewRevisionDocEvent, StateDocEvent
|
||||
from ietf.doc.models import TelechatDocEvent, DocumentActionHolder, EditedAuthorsDocEvent
|
||||
from ietf.name.models import DocReminderTypeName, DocRelationshipName
|
||||
from ietf.group.models import Role, Group
|
||||
from ietf.group.models import Role, Group, GroupFeatures
|
||||
from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream, is_individual_draft_author, is_bofreq_editor
|
||||
from ietf.person.models import Person
|
||||
from ietf.review.models import ReviewWish
|
||||
|
@ -118,6 +118,10 @@ def get_tags_for_stream_id(stream_id):
|
|||
return []
|
||||
|
||||
def can_adopt_draft(user, doc):
|
||||
"""Answers whether a user can adopt a given draft into some stream/group.
|
||||
|
||||
This does not answer, even by implicaiton, which streams/groups the user has authority to adopt into."""
|
||||
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
|
||||
|
@ -129,17 +133,29 @@ def can_adopt_draft(user, doc):
|
|||
return (doc.stream_id in (None, "irtf")
|
||||
and doc.group.type_id == "individ")
|
||||
|
||||
roles = Role.objects.filter(name__in=("chair", "delegate", "secr"),
|
||||
group__type__in=("wg", "rg", "ag", "rag"),
|
||||
group__state="active",
|
||||
person__user=user)
|
||||
role_groups = [ r.group for r in roles ]
|
||||
for type_id, allowed_stream in (
|
||||
("wg", "ietf"),
|
||||
("rg", "irtf"),
|
||||
("ag", "ietf"),
|
||||
("rag", "irtf"),
|
||||
("edwg", "editorial"),
|
||||
):
|
||||
if doc.stream_id in (None, allowed_stream):
|
||||
if doc.group.type_id in ("individ", type_id):
|
||||
if Role.objects.filter(
|
||||
name__in=GroupFeatures.objects.get(type_id=type_id).docman_roles,
|
||||
group__type_id = type_id,
|
||||
group__state = "active",
|
||||
person__user = user,
|
||||
).exists():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
return (doc.stream_id in (None, "ietf", "irtf")
|
||||
and (doc.group.type_id == "individ" or (doc.group in role_groups and len(role_groups)>1))
|
||||
and roles.exists())
|
||||
|
||||
def can_unadopt_draft(user, doc):
|
||||
# TODO: This should use docman_roles, and this implementation probably returns wrong answers
|
||||
# For instance, should any WG chair be able to unadopt a group from any other WG
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
if has_role(user, "Secretariat"):
|
||||
|
@ -155,6 +171,8 @@ def can_unadopt_draft(user, doc):
|
|||
elif doc.stream_id == 'iab':
|
||||
return False # Right now only the secretariat can add a document to the IAB stream, so we'll
|
||||
# leave it where only the secretariat can take it out.
|
||||
elif doc.stream_id == 'editorial':
|
||||
return user.person.role_set.filter(name='chair', group__acronym='rswg').exists()
|
||||
else:
|
||||
return False
|
||||
|
||||
|
@ -222,7 +240,6 @@ def needed_ballot_positions(doc, active_positions):
|
|||
|
||||
return " ".join(answer)
|
||||
|
||||
# Not done yet - modified version of above needed_ballot_positions
|
||||
def irsg_needed_ballot_positions(doc, active_positions):
|
||||
'''Returns text answering the question "what does this document
|
||||
need to pass?". The return value is only useful if the document
|
||||
|
@ -250,6 +267,21 @@ def irsg_needed_ballot_positions(doc, active_positions):
|
|||
|
||||
return " ".join(answer)
|
||||
|
||||
def rsab_needed_ballot_positions(doc, active_positions):
|
||||
count = Counter([p.pos_id if p else 'none' for p in active_positions])
|
||||
answer = []
|
||||
if count["concern"] > 0:
|
||||
answer.append("Has a Concern position.")
|
||||
# note RFC9280 section 3.2.2 item 12
|
||||
# the "vote" mentioned there is a separate thing from ballot position.
|
||||
if count["yes"] == 0:
|
||||
# This is _implied_ by 9280 - a document shouldn't be
|
||||
# approved if all RSAB members recuse
|
||||
answer.append("Needs a YES position.")
|
||||
if count["none"] > 0:
|
||||
answer.append("Some members have have not taken a position.")
|
||||
return " ".join(answer)
|
||||
|
||||
def create_ballot(request, doc, by, ballot_slug, time=None):
|
||||
closed = close_open_ballots(doc, by)
|
||||
for e in closed:
|
||||
|
@ -265,16 +297,14 @@ def create_ballot(request, doc, by, ballot_slug, time=None):
|
|||
def create_ballot_if_not_open(request, doc, by, ballot_slug, time=None, duedate=None):
|
||||
ballot_type = BallotType.objects.get(doc_type=doc.type, slug=ballot_slug)
|
||||
if not doc.ballot_open(ballot_slug):
|
||||
kwargs = dict(type="created_ballot", by=by, doc=doc, rev=doc.rev)
|
||||
if time:
|
||||
if duedate:
|
||||
e = IRSGBallotDocEvent(type="created_ballot", by=by, doc=doc, rev=doc.rev, time=time, duedate=duedate)
|
||||
else:
|
||||
e = BallotDocEvent(type="created_ballot", by=by, doc=doc, rev=doc.rev, time=time)
|
||||
kwargs['time'] = time
|
||||
if doc.stream_id == 'irtf':
|
||||
kwargs['duedate'] = duedate
|
||||
e = IRSGBallotDocEvent(**kwargs)
|
||||
else:
|
||||
if duedate:
|
||||
e = IRSGBallotDocEvent(type="created_ballot", by=by, doc=doc, rev=doc.rev, duedate=duedate)
|
||||
else:
|
||||
e = BallotDocEvent(type="created_ballot", by=by, doc=doc, rev=doc.rev)
|
||||
e = BallotDocEvent(**kwargs)
|
||||
e.ballot_type = ballot_type
|
||||
e.desc = 'Created "%s" ballot' % e.ballot_type.name
|
||||
e.save()
|
||||
|
|
|
@ -27,6 +27,7 @@ from ietf.doc.mails import ( email_ballot_deferred, email_ballot_undeferred,
|
|||
extra_automation_headers, generate_last_call_announcement,
|
||||
generate_issue_ballot_mail, generate_ballot_writeup, generate_ballot_rfceditornote,
|
||||
generate_approval_mail, email_irsg_ballot_closed, email_irsg_ballot_issued,
|
||||
email_rsab_ballot_issued, email_rsab_ballot_closed,
|
||||
email_lc_to_yang_doctors )
|
||||
from ietf.doc.lastcall import request_last_call
|
||||
from ietf.doc.templatetags.ietf_filters import can_ballot
|
||||
|
@ -43,16 +44,6 @@ from ietf.utils.response import permission_denied
|
|||
from ietf.utils.timezone import date_today, datetime_from_date, DEADLINE_TZINFO
|
||||
|
||||
|
||||
BALLOT_CHOICES = (("yes", "Yes"),
|
||||
("noobj", "No Objection"),
|
||||
("discuss", "Discuss"),
|
||||
("abstain", "Abstain"),
|
||||
("recuse", "Recuse"),
|
||||
("moretime", "Need More Time"),
|
||||
("notready", "Not Ready"),
|
||||
("", "No Record"),
|
||||
)
|
||||
|
||||
# -------------------------------------------------
|
||||
# Helper Functions
|
||||
# -------------------------------------------------
|
||||
|
@ -106,6 +97,8 @@ class EditPositionForm(forms.Form):
|
|||
ballot_type = kwargs.pop("ballot_type")
|
||||
super(EditPositionForm, self).__init__(*args, **kwargs)
|
||||
self.fields['position'].queryset = ballot_type.positions.order_by('order')
|
||||
if ballot_type.positions.filter(blocking=True).exists():
|
||||
self.fields['discuss'].label = ballot_type.positions.get(blocking=True).name
|
||||
|
||||
def clean_discuss(self):
|
||||
entered_discuss = self.cleaned_data["discuss"]
|
||||
|
@ -183,9 +176,9 @@ def save_position(form, doc, ballot, balloter, login=None, send_email=False):
|
|||
|
||||
return pos
|
||||
|
||||
@role_required('Area Director','Secretariat','IRSG Member')
|
||||
@role_required("Area Director", "Secretariat", "IRSG Member", "RSAB Member")
|
||||
def edit_position(request, name, ballot_id):
|
||||
"""Vote and edit discuss and comment on document as Area Director."""
|
||||
"""Vote and edit discuss and comment on document"""
|
||||
doc = get_object_or_404(Document, docalias__name=name)
|
||||
ballot = get_object_or_404(BallotDocEvent, type="created_ballot", pk=ballot_id, doc=doc)
|
||||
|
||||
|
@ -196,8 +189,7 @@ def edit_position(request, name, ballot_id):
|
|||
else:
|
||||
return_to_url = urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=doc.name, ballot_id=ballot_id))
|
||||
|
||||
# if we're in the Secretariat, we can select an AD to act as stand-in for
|
||||
# or we can select an IRSG member
|
||||
# if we're in the Secretariat, we can select a balloter to act as stand-in for
|
||||
if has_role(request.user, "Secretariat"):
|
||||
balloter_id = request.GET.get('balloter')
|
||||
if not balloter_id:
|
||||
|
@ -207,8 +199,8 @@ def edit_position(request, name, ballot_id):
|
|||
if request.method == 'POST':
|
||||
old_pos = None
|
||||
if not has_role(request.user, "Secretariat") and not can_ballot(request.user, doc):
|
||||
# prevent pre-ADs from voting
|
||||
permission_denied(request, "Must be a proper Area Director in an active area or IRSG Member to cast ballot")
|
||||
# prevent pre-ADs from taking a position
|
||||
permission_denied(request, "Must be an active member (not a pre-AD for example) of the balloting body to take a position")
|
||||
|
||||
form = EditPositionForm(request.POST, ballot_type=ballot.ballot_type)
|
||||
if form.is_valid():
|
||||
|
@ -328,16 +320,18 @@ def build_position_email(balloter, doc, pos):
|
|||
|
||||
return addrs, frm, subject, body
|
||||
|
||||
@role_required('Area Director','Secretariat','IRSG Member')
|
||||
@role_required('Area Director','Secretariat','IRSG Member', 'RSAB Member')
|
||||
def send_ballot_comment(request, name, ballot_id):
|
||||
"""Email document ballot position discuss/comment for Area Director."""
|
||||
doc = get_object_or_404(Document, docalias__name=name)
|
||||
ballot = get_object_or_404(BallotDocEvent, type="created_ballot", pk=ballot_id, doc=doc)
|
||||
|
||||
if not has_role(request.user, 'Secretariat'):
|
||||
if doc.stream_id == 'irtf' and not has_role(request.user, 'IRSG Member'):
|
||||
raise Http404
|
||||
if doc.stream_id == 'ietf' and not has_role(request.user, 'Area Director'):
|
||||
if any([
|
||||
doc.stream_id == 'ietf' and not has_role(request.user, 'Area Director'),
|
||||
doc.stream_id == 'irtf' and not has_role(request.user, 'IRSG Member'),
|
||||
doc.stream_id == 'editorial' and not has_role(request.user, 'RSAB Member'),
|
||||
]):
|
||||
raise Http404
|
||||
|
||||
balloter = request.user.person
|
||||
|
@ -352,7 +346,7 @@ def send_ballot_comment(request, name, ballot_id):
|
|||
else:
|
||||
back_url = urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=doc.name, ballot_id=ballot_id))
|
||||
|
||||
# if we're in the Secretariat, we can select an AD to act as stand-in for
|
||||
# if we're in the Secretariat, we can select a balloter (such as an AD) to act as stand-in for
|
||||
if has_role(request.user, "Secretariat"):
|
||||
balloter_id = request.GET.get('balloter')
|
||||
if not balloter_id:
|
||||
|
@ -367,6 +361,8 @@ def send_ballot_comment(request, name, ballot_id):
|
|||
|
||||
if doc.stream_id == 'irtf':
|
||||
mailtrigger_slug='irsg_ballot_saved'
|
||||
elif doc.stream_id == 'editorial':
|
||||
mailtrigger_slug='rsab_ballot_saved'
|
||||
else:
|
||||
mailtrigger_slug='iesg_ballot_saved'
|
||||
|
||||
|
@ -1200,3 +1196,90 @@ def irsg_ballot_status(request):
|
|||
docs.append(doc)
|
||||
|
||||
return render(request, 'doc/irsg_ballot_status.html', {'docs':docs})
|
||||
|
||||
@role_required('Secretariat', 'RSAB Chair')
|
||||
def issue_rsab_ballot(request, name):
|
||||
doc = get_object_or_404(Document, docalias__name=name)
|
||||
if doc.stream.slug != "editorial" or doc.type != DocTypeName.objects.get(slug="draft"):
|
||||
raise Http404
|
||||
|
||||
by = request.user.person
|
||||
|
||||
if request.method == 'POST':
|
||||
button = request.POST.get("rsab_button") # TODO: Really? There's an irsg button? The templates should be generalized.
|
||||
if button == 'Yes':
|
||||
e = BallotDocEvent(doc=doc, rev=doc.rev, by=request.user.person)
|
||||
e.type = "created_ballot"
|
||||
e.desc = "Created RSAB Ballot"
|
||||
ballot_type = BallotType.objects.get(doc_type=doc.type, slug="rsab-approve")
|
||||
e.ballot_type = ballot_type
|
||||
e.save()
|
||||
new_state = doc.get_state()
|
||||
prev_tags = []
|
||||
new_tags = []
|
||||
|
||||
email_rsab_ballot_issued(request, doc, ballot=e) # Send notification email
|
||||
|
||||
if doc.type_id == 'draft':
|
||||
new_state = State.objects.get(used=True, type="draft-stream-editorial", slug='rsabpoll')
|
||||
|
||||
prev_state = doc.get_state(new_state.type_id if new_state else None)
|
||||
|
||||
doc.set_state(new_state)
|
||||
doc.tags.remove(*prev_tags)
|
||||
|
||||
events = []
|
||||
e = add_state_change_event(doc, by, prev_state, new_state, prev_tags=prev_tags, new_tags=new_tags)
|
||||
if e:
|
||||
events.append(e)
|
||||
e = update_action_holders(doc, prev_state, new_state, prev_tags=prev_tags, new_tags=new_tags)
|
||||
if e:
|
||||
events.append(e)
|
||||
|
||||
if events:
|
||||
doc.save_with_history(events)
|
||||
|
||||
return HttpResponseRedirect(doc.get_absolute_url())
|
||||
else:
|
||||
templ = 'doc/ballot/rsab_ballot_approve.html'
|
||||
|
||||
question = "Confirm issuing a ballot for " + name + "?"
|
||||
return render(request, templ, dict(doc=doc, question=question))
|
||||
|
||||
@role_required('Secretariat', 'RSAB Chair')
|
||||
def close_rsab_ballot(request, name):
|
||||
doc = get_object_or_404(Document, docalias__name=name)
|
||||
if doc.stream.slug != "editorial" or doc.type_id != "draft":
|
||||
raise Http404
|
||||
|
||||
by = request.user.person
|
||||
|
||||
if request.method == 'POST':
|
||||
button = request.POST.get("rsab_button")
|
||||
if button == 'Yes':
|
||||
ballot = close_ballot(doc, by, "rsab-approve")
|
||||
email_rsab_ballot_closed(
|
||||
request,
|
||||
doc=doc,
|
||||
ballot=BallotDocEvent.objects.get(pk=ballot.pk)
|
||||
)
|
||||
return HttpResponseRedirect(doc.get_absolute_url())
|
||||
|
||||
templ = 'doc/ballot/rsab_ballot_close.html'
|
||||
question = "Confirm closing the ballot for " + name + "?"
|
||||
return render(request, templ, dict(doc=doc, question=question))
|
||||
|
||||
def rsab_ballot_status(request):
|
||||
possible_docs = Document.objects.filter(docevent__ballotdocevent__isnull=False)
|
||||
docs = []
|
||||
for doc in possible_docs:
|
||||
if doc.ballot_open("rsab-approve"):
|
||||
ballot = doc.active_ballot()
|
||||
if ballot:
|
||||
doc.ballot = ballot
|
||||
|
||||
docs.append(doc)
|
||||
return render(request, 'doc/rsab_ballot_status.html', {'docs':docs})
|
||||
# Possible TODO: add a menu item to show this? Maybe only if you're in rsab or an rswg chair?
|
||||
# There will be so few of these that the general community would follow them from the rswg docs page.
|
||||
# Maybe the view isn't actually needed at all...
|
||||
|
|
|
@ -70,7 +70,7 @@ from ietf.doc.utils_bofreq import bofreq_editors, bofreq_responsible
|
|||
from ietf.group.models import Role, Group
|
||||
from ietf.group.utils import can_manage_all_groups_of_type, can_manage_materials, group_features_role_filter
|
||||
from ietf.ietfauth.utils import ( has_role, is_authorized_in_doc_stream, user_is_person,
|
||||
role_required, is_individual_draft_author)
|
||||
role_required, is_individual_draft_author, can_request_rfc_publication)
|
||||
from ietf.name.models import StreamName, BallotPositionName
|
||||
from ietf.utils.history import find_history_active_at
|
||||
from ietf.doc.forms import TelechatForm, NotifyForm, ActionHoldersForm, DocAuthorForm, DocAuthorChangeBasisForm
|
||||
|
@ -92,13 +92,28 @@ def render_document_top(request, doc, tab, name):
|
|||
tabs = []
|
||||
tabs.append(("Status", "status", urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=name)), True, None))
|
||||
|
||||
iesg_type_slugs = set(BallotType.objects.values_list('slug',flat=True))
|
||||
iesg_type_slugs.discard('irsg-approve')
|
||||
iesg_type_slugs = set(BallotType.objects.exclude(slug__in=("irsg-approve","rsab-approve")).values_list('slug',flat=True))
|
||||
iesg_ballot = doc.latest_event(BallotDocEvent, type="created_ballot", ballot_type__slug__in=iesg_type_slugs)
|
||||
irsg_ballot = doc.latest_event(BallotDocEvent, type="created_ballot", ballot_type__slug='irsg-approve')
|
||||
rsab_ballot = doc.latest_event(BallotDocEvent, type="created_ballot", ballot_type__slug='rsab-approve')
|
||||
|
||||
if doc.type_id == "draft" and doc.get_state("draft-stream-irtf"):
|
||||
tabs.append(("IRSG Evaluation Record", "irsgballot", urlreverse("ietf.doc.views_doc.document_irsg_ballot", kwargs=dict(name=name)), irsg_ballot, None if irsg_ballot else "IRSG Evaluation Ballot has not been created yet"))
|
||||
if doc.type_id == "draft":
|
||||
if doc.get_state("draft-stream-irtf"):
|
||||
tabs.append((
|
||||
"IRSG Evaluation Record",
|
||||
"irsgballot",
|
||||
urlreverse("ietf.doc.views_doc.document_irsg_ballot", kwargs=dict(name=name)),
|
||||
irsg_ballot,
|
||||
None if irsg_ballot else "IRSG Evaluation Ballot has not been created yet"
|
||||
))
|
||||
if doc.get_state("draft-stream-editorial"):
|
||||
tabs.append((
|
||||
"RSAB Evaluation Record",
|
||||
"rsabballot",
|
||||
urlreverse("ietf.doc.views_doc.document_rsab_ballot", kwargs=dict(name=name)),
|
||||
rsab_ballot,
|
||||
None if rsab_ballot else "RSAB Evaluation Ballot has not been created yet"
|
||||
))
|
||||
if doc.type_id in ("draft","conflrev", "statchg"):
|
||||
tabs.append(("IESG Evaluation Record", "ballot", urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=name)), iesg_ballot, None if iesg_ballot else "IESG Evaluation Ballot has not been created yet"))
|
||||
elif doc.type_id == "charter" and doc.group.type_id == "wg":
|
||||
|
@ -269,13 +284,11 @@ def document_main(request, name, rev=None, document_html=False):
|
|||
|
||||
# ballot
|
||||
iesg_ballot_summary = None
|
||||
irsg_ballot_summary = None
|
||||
due_date = None
|
||||
if (iesg_state_slug in IESG_BALLOT_ACTIVE_STATES) or irsg_state:
|
||||
active_ballot = doc.active_ballot()
|
||||
if active_ballot:
|
||||
if irsg_state:
|
||||
irsg_ballot_summary = irsg_needed_ballot_positions(doc, list(active_ballot.active_balloter_positions().values()))
|
||||
due_date=active_ballot.irsgballotdocevent.duedate
|
||||
else:
|
||||
iesg_ballot_summary = needed_ballot_positions(doc, list(active_ballot.active_balloter_positions().values()))
|
||||
|
@ -289,7 +302,7 @@ def document_main(request, name, rev=None, document_html=False):
|
|||
elif group.type_id == "area" and doc.stream_id == "ietf":
|
||||
submission = "individual in %s area" % group.acronym
|
||||
else:
|
||||
if group.features.acts_like_wg:
|
||||
if group.features.acts_like_wg and not group.type_id=="edwg":
|
||||
submission = "%s %s" % (group.acronym, group.type)
|
||||
else:
|
||||
submission = group.acronym
|
||||
|
@ -390,12 +403,29 @@ def document_main(request, name, rev=None, document_html=False):
|
|||
if doc.get_state_slug() == "expired" and has_role(request.user, ("Secretariat",)) and not snapshot:
|
||||
actions.append(("Resurrect", urlreverse('ietf.doc.views_draft.resurrect', kwargs=dict(name=doc.name))))
|
||||
|
||||
if (doc.get_state_slug() not in ["rfc", "expired"] and doc.stream_id in ("irtf",) and not snapshot and not doc.ballot_open('irsg-approve') and has_role(request.user, ("Secretariat", "IRTF Chair"))):
|
||||
label = "Issue IRSG Ballot"
|
||||
actions.append((label, urlreverse('ietf.doc.views_ballot.issue_irsg_ballot', kwargs=dict(name=doc.name))))
|
||||
if (doc.get_state_slug() not in ["rfc", "expired"] and doc.stream_id in ("irtf",) and not snapshot and doc.ballot_open('irsg-approve') and has_role(request.user, ("Secretariat", "IRTF Chair"))):
|
||||
label = "Close IRSG Ballot"
|
||||
actions.append((label, urlreverse('ietf.doc.views_ballot.close_irsg_ballot', kwargs=dict(name=doc.name))))
|
||||
if doc.get_state_slug() not in ["rfc", "expired"] and not snapshot:
|
||||
if doc.stream_id == "irtf" and has_role(request.user, ("Secretariat", "IRTF Chair")):
|
||||
if not doc.ballot_open('irsg-approve'):
|
||||
actions.append((
|
||||
"Issue IRSG Ballot",
|
||||
urlreverse('ietf.doc.views_ballot.issue_irsg_ballot', kwargs=dict(name=doc.name))
|
||||
))
|
||||
else:
|
||||
actions.append((
|
||||
"Close IRSG Ballot",
|
||||
urlreverse('ietf.doc.views_ballot.close_irsg_ballot', kwargs=dict(name=doc.name))
|
||||
))
|
||||
elif doc.stream_id == "editorial" and has_role(request.user, ("Secretariat", "RSAB Chair")):
|
||||
if not doc.ballot_open('rsab-approve'):
|
||||
actions.append((
|
||||
"Issue RSAB Ballot",
|
||||
urlreverse('ietf.doc.views_ballot.issue_rsab_ballot', kwargs=dict(name=doc.name))
|
||||
))
|
||||
else:
|
||||
actions.append((
|
||||
"Close RSAB Ballot",
|
||||
urlreverse('ietf.doc.views_ballot.close_rsab_ballot', kwargs=dict(name=doc.name))
|
||||
))
|
||||
|
||||
if (doc.get_state_slug() not in ["rfc", "expired"] and doc.stream_id in ("ise", "irtf")
|
||||
and has_role(request.user, ("Secretariat", "IRTF Chair")) and not conflict_reviews and not snapshot):
|
||||
|
@ -404,15 +434,15 @@ def document_main(request, name, rev=None, document_html=False):
|
|||
label += " (note that intended status is not set)"
|
||||
actions.append((label, urlreverse('ietf.doc.views_conflict_review.start_review', kwargs=dict(name=doc.name))))
|
||||
|
||||
if (doc.get_state_slug() not in ["rfc", "expired"] and doc.stream_id in ("iab", "ise", "irtf")
|
||||
and (has_role(request.user, ("Secretariat", "IRTF Chair")) if doc.stream_id=="irtf" else can_edit_stream_info) and not snapshot):
|
||||
if doc.get_state_slug('draft-stream-%s' % doc.stream_id) not in ('rfc-edit', 'pub', 'dead'):
|
||||
label = "Request Publication"
|
||||
if not doc.intended_std_level:
|
||||
label += " (note that intended status is not set)"
|
||||
if iesg_state and iesg_state_slug not in ('idexists','dead'):
|
||||
label += " (Warning: the IESG state indicates ongoing IESG processing)"
|
||||
actions.append((label, urlreverse('ietf.doc.views_draft.request_publication', kwargs=dict(name=doc.name))))
|
||||
if doc.get_state_slug() not in ["rfc", "expired"] and not snapshot:
|
||||
if can_request_rfc_publication(request.user, doc):
|
||||
if doc.get_state_slug('draft-stream-%s' % doc.stream_id) not in ('rfc-edit', 'pub', 'dead'):
|
||||
label = "Request Publication"
|
||||
if not doc.intended_std_level:
|
||||
label += " (note that intended status is not set)"
|
||||
if iesg_state and iesg_state_slug not in ('idexists','dead'):
|
||||
label += " (Warning: the IESG state indicates ongoing IESG processing)"
|
||||
actions.append((label, urlreverse('ietf.doc.views_draft.request_publication', kwargs=dict(name=doc.name))))
|
||||
|
||||
if doc.get_state_slug() not in ["rfc", "expired"] and doc.stream_id in ("ietf",) and not snapshot:
|
||||
if iesg_state_slug == 'idexists' and can_edit:
|
||||
|
@ -491,8 +521,6 @@ def document_main(request, name, rev=None, document_html=False):
|
|||
draft_name=draft_name,
|
||||
telechat=telechat,
|
||||
iesg_ballot_summary=iesg_ballot_summary,
|
||||
# PEY: Currently not using irsg_ballot_summary in the template, but it should be. That will take a new box for IRSG data.
|
||||
irsg_ballot_summary=irsg_ballot_summary,
|
||||
submission=submission,
|
||||
resurrected_by=resurrected_by,
|
||||
|
||||
|
@ -1315,6 +1343,28 @@ def document_irsg_ballot(request, name, ballot_id=None):
|
|||
# ballot_type_slug=ballot.ballot_type.slug,
|
||||
))
|
||||
|
||||
def document_rsab_ballot(request, name, ballot_id=None):
|
||||
doc = get_object_or_404(Document, docalias__name=name)
|
||||
top = render_document_top(request, doc, "rsabballot", name)
|
||||
if not ballot_id:
|
||||
ballot = doc.latest_event(BallotDocEvent, type="created_ballot", ballot_type__slug='rsab-approve')
|
||||
if ballot:
|
||||
ballot_id = ballot.id
|
||||
|
||||
c = document_ballot_content(request, doc, ballot_id, editable=True)
|
||||
|
||||
request.session['ballot_edit_return_point'] = request.path_info
|
||||
|
||||
return render(
|
||||
request,
|
||||
"doc/document_ballot.html",
|
||||
dict(
|
||||
doc=doc,
|
||||
top=top,
|
||||
ballot_content=c,
|
||||
)
|
||||
)
|
||||
|
||||
def ballot_popup(request, name, ballot_id):
|
||||
doc = get_object_or_404(Document, docalias__name=name)
|
||||
c = document_ballot_content(request, doc, ballot_id=ballot_id, editable=False)
|
||||
|
|
|
@ -43,7 +43,7 @@ from ietf.doc.forms import ExtResourceForm
|
|||
from ietf.group.models import Group, Role, GroupFeatures
|
||||
from ietf.iesg.models import TelechatDate
|
||||
from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream, user_is_person
|
||||
from ietf.ietfauth.utils import role_required
|
||||
from ietf.ietfauth.utils import role_required, can_request_rfc_publication
|
||||
from ietf.mailtrigger.utils import gather_address_lists
|
||||
from ietf.message.models import Message
|
||||
from ietf.name.models import IntendedStdLevelName, DocTagName, StreamName
|
||||
|
@ -459,8 +459,6 @@ def change_intention(request, name):
|
|||
or is_authorized_in_doc_stream(request.user, doc)):
|
||||
permission_denied(request, "You do not have the necessary permissions to view this page.")
|
||||
|
||||
login = request.user.person
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ChangeIntentionForm(request.POST)
|
||||
if form.is_valid():
|
||||
|
@ -468,36 +466,7 @@ def change_intention(request, name):
|
|||
comment = form.cleaned_data['comment'].strip()
|
||||
old_level = doc.intended_std_level
|
||||
|
||||
if new_level != old_level:
|
||||
doc.intended_std_level = new_level
|
||||
|
||||
events = []
|
||||
e = DocEvent(doc=doc, rev=doc.rev, by=login, type='changed_document')
|
||||
e.desc = "Intended Status changed to <b>%s</b> from %s"% (new_level,old_level)
|
||||
e.save()
|
||||
events.append(e)
|
||||
|
||||
if comment:
|
||||
c = DocEvent(doc=doc, rev=doc.rev, by=login, type="added_comment")
|
||||
c.desc = comment
|
||||
c.save()
|
||||
events.append(c)
|
||||
|
||||
de = doc.latest_event(ConsensusDocEvent, type="changed_consensus")
|
||||
prev_consensus = de and de.consensus
|
||||
if not prev_consensus and doc.intended_std_level_id in ("std", "ds", "ps", "bcp"):
|
||||
ce = ConsensusDocEvent(doc=doc, rev=doc.rev, by=login, type="changed_consensus")
|
||||
ce.consensus = True
|
||||
ce.desc = "Changed consensus to <b>%s</b> from %s" % (nice_consensus(True),
|
||||
nice_consensus(prev_consensus))
|
||||
ce.save()
|
||||
events.append(ce)
|
||||
|
||||
doc.save_with_history(events)
|
||||
|
||||
msg = "\n".join(e.desc for e in events)
|
||||
|
||||
email_intended_status_changed(request, doc, msg)
|
||||
set_intended_status_level(request=request, doc=doc, new_level=new_level, old_level=old_level, comment=comment)
|
||||
|
||||
return HttpResponseRedirect(doc.get_absolute_url())
|
||||
|
||||
|
@ -1290,14 +1259,11 @@ def request_publication(request, name):
|
|||
subject = forms.CharField(max_length=200, required=True)
|
||||
body = forms.CharField(widget=forms.Textarea, required=True, strip=False)
|
||||
|
||||
doc = get_object_or_404(Document, type="draft", name=name, stream__in=("iab", "ise", "irtf"))
|
||||
doc = get_object_or_404(Document, type="draft", name=name, stream__in=("iab", "ise", "irtf", "editorial"))
|
||||
|
||||
if doc.stream_id == "irtf":
|
||||
if not has_role(request.user, ("Secretariat", "IRTF Chair")):
|
||||
permission_denied(request, "You do not have the necessary permissions to view this page.")
|
||||
elif not is_authorized_in_doc_stream(request.user, doc):
|
||||
if not can_request_rfc_publication(request.user, doc):
|
||||
permission_denied(request, "You do not have the necessary permissions to view this page.")
|
||||
|
||||
|
||||
consensus_event = doc.latest_event(ConsensusDocEvent, type="changed_consensus")
|
||||
|
||||
m = Message()
|
||||
|
@ -1378,63 +1344,163 @@ def request_publication(request, name):
|
|||
)
|
||||
|
||||
class AdoptDraftForm(forms.Form):
|
||||
group = forms.ModelChoiceField(queryset=Group.objects.filter(type__features__acts_like_wg=True, state="active").order_by("-type", "acronym"), required=True, empty_label=None)
|
||||
newstate = forms.ModelChoiceField(queryset=State.objects.filter(type__in=['draft-stream-ietf','draft-stream-irtf'], used=True).exclude(slug__in=settings.GROUP_STATES_WITH_EXTRA_PROCESSING), required=True, label="State")
|
||||
comment = forms.CharField(widget=forms.Textarea, required=False, label="Comment", help_text="Optional comment explaining the reasons for the adoption.", strip=False)
|
||||
group = forms.ModelChoiceField(
|
||||
queryset=Group.objects.filter(type__features__acts_like_wg=True, state="active")
|
||||
.order_by("-type", "acronym")
|
||||
.distinct(),
|
||||
required=True,
|
||||
empty_label=None,
|
||||
)
|
||||
newstate = forms.ModelChoiceField(
|
||||
queryset=State.objects.filter(
|
||||
type__in=[
|
||||
"draft-stream-ietf",
|
||||
"draft-stream-irtf",
|
||||
"draft-stream-editorial",
|
||||
],
|
||||
used=True,
|
||||
).exclude(slug__in=settings.GROUP_STATES_WITH_EXTRA_PROCESSING),
|
||||
required=True,
|
||||
label="State",
|
||||
)
|
||||
comment = forms.CharField(
|
||||
widget=forms.Textarea,
|
||||
required=False,
|
||||
label="Comment",
|
||||
help_text="Optional comment explaining the reasons for the adoption.",
|
||||
strip=False,
|
||||
)
|
||||
weeks = forms.IntegerField(required=False, label="Expected weeks in adoption state")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
user = kwargs.pop("user")
|
||||
rg_features = GroupFeatures.objects.get(type_id='rg')
|
||||
wg_features = GroupFeatures.objects.get(type_id='wg')
|
||||
|
||||
super(AdoptDraftForm, self).__init__(*args, **kwargs)
|
||||
|
||||
docman_roles = {}
|
||||
for group_type in ("wg", "ag", "rg", "rag", "edwg"):
|
||||
docman_roles[group_type] = GroupFeatures.objects.get(
|
||||
type_id=group_type
|
||||
).docman_roles
|
||||
|
||||
state_types = set()
|
||||
if has_role(user, "Secretariat"):
|
||||
state_types.update(['draft-stream-ietf','draft-stream-irtf'])
|
||||
else:
|
||||
if (has_role(user, "IRTF Chair")
|
||||
or Group.objects.filter(type="rg",
|
||||
state="active",
|
||||
role__person__user=user,
|
||||
role__name__in=rg_features.docman_roles).exists()):
|
||||
state_types.add('draft-stream-irtf')
|
||||
if Group.objects.filter( type="wg",
|
||||
state="active",
|
||||
role__person__user=user,
|
||||
role__name__in=wg_features.docman_roles).exists():
|
||||
state_types.add('draft-stream-ietf')
|
||||
state_types.update(
|
||||
["draft-stream-ietf", "draft-stream-irtf", "draft-stream-editorial"]
|
||||
)
|
||||
else:
|
||||
if has_role(user, "IRTF Chair") or any(
|
||||
[
|
||||
Group.objects.filter(
|
||||
type=type_id,
|
||||
state="active",
|
||||
role__person__user=user,
|
||||
role__name__in=docman_roles[type_id],
|
||||
).exists()
|
||||
for type_id in ("rg", "rag")
|
||||
]
|
||||
):
|
||||
state_types.add("draft-stream-irtf")
|
||||
if any(
|
||||
[
|
||||
Group.objects.filter(
|
||||
type=type_id,
|
||||
state="active",
|
||||
role__person__user=user,
|
||||
role__name__in=docman_roles[type_id],
|
||||
).exists()
|
||||
for type_id in ("wg", "ag")
|
||||
]
|
||||
):
|
||||
state_types.add("draft-stream-ietf")
|
||||
if Group.objects.filter(
|
||||
type="edwg",
|
||||
state="active",
|
||||
role__person__user=user,
|
||||
role__name__in=docman_roles["edwg"],
|
||||
).exists():
|
||||
state_types.add("draft-stream-editorial")
|
||||
|
||||
state_choices = State.objects.filter(type__in=state_types, used=True).exclude(slug__in=settings.GROUP_STATES_WITH_EXTRA_PROCESSING)
|
||||
state_choices = State.objects.filter(type__in=state_types, used=True).exclude(
|
||||
slug__in=settings.GROUP_STATES_WITH_EXTRA_PROCESSING
|
||||
)
|
||||
|
||||
if not has_role(user, "Secretariat"):
|
||||
allow_matching_groups = []
|
||||
if has_role(user, "IRTF Chair"):
|
||||
group_queryset = self.fields["group"].queryset.filter(Q(role__person__user=user, role__name__in=rg_features.docman_roles)|Q(type="rg", state="active")).distinct()
|
||||
else:
|
||||
group_queryset = self.fields["group"].queryset.filter(role__person__user=user, role__name__in=wg_features.docman_roles).distinct()
|
||||
self.fields["group"].queryset = group_queryset
|
||||
allow_matching_groups.append(Q(type__in=["rg", "rag"]))
|
||||
for type_id in docman_roles:
|
||||
allow_matching_groups.append(
|
||||
Q(
|
||||
role__person__user=user,
|
||||
role__name__in=docman_roles[type_id],
|
||||
type_id=type_id,
|
||||
)
|
||||
)
|
||||
combined_query = Q(pk__in=[]) # Never use Q() here when following this pattern
|
||||
for query in allow_matching_groups:
|
||||
combined_query |= query
|
||||
|
||||
self.fields["group"].queryset = self.fields["group"].queryset.filter(combined_query)
|
||||
|
||||
self.fields['group'].choices = [(g.pk, '%s - %s' % (g.acronym, g.name)) for g in self.fields["group"].queryset]
|
||||
self.fields['newstate'].choices = [('','-- Pick a state --')]
|
||||
self.fields['newstate'].choices.extend([(x.pk,x.name + " (IETF)") for x in state_choices if x.type_id == 'draft-stream-ietf'])
|
||||
self.fields['newstate'].choices.extend([(x.pk,x.name + " (IRTF)") for x in state_choices if x.type_id == 'draft-stream-irtf'])
|
||||
self.fields["group"].choices = [
|
||||
(g.pk, "%s - %s" % (g.acronym, g.name))
|
||||
for g in self.fields["group"].queryset
|
||||
]
|
||||
self.fields["newstate"].choices = [("", "-- Pick a state --")]
|
||||
self.fields["newstate"].choices.extend(
|
||||
[
|
||||
(x.pk, x.name + " (IETF)")
|
||||
for x in state_choices
|
||||
if x.type_id == "draft-stream-ietf"
|
||||
]
|
||||
)
|
||||
self.fields["newstate"].choices.extend(
|
||||
[
|
||||
(x.pk, x.name + " (IRTF)")
|
||||
for x in state_choices
|
||||
if x.type_id == "draft-stream-irtf"
|
||||
]
|
||||
)
|
||||
self.fields["newstate"].choices.extend(
|
||||
[
|
||||
(x.pk, x.name + " (Editorial)")
|
||||
for x in state_choices
|
||||
if x.type_id == "draft-stream-editorial"
|
||||
]
|
||||
)
|
||||
|
||||
def clean_newstate(self):
|
||||
group = self.cleaned_data['group']
|
||||
newstate = self.cleaned_data['newstate']
|
||||
|
||||
if (newstate.type_id == 'draft-stream-ietf') and (group.type_id == 'rg'):
|
||||
raise forms.ValidationError('Cannot assign IETF WG state to IRTF group')
|
||||
elif (newstate.type_id == 'draft-stream-irtf') and (group.type_id == 'wg'):
|
||||
raise forms.ValidationError('Cannot assign IRTF RG state to IETF group')
|
||||
else:
|
||||
return newstate
|
||||
|
||||
group = self.cleaned_data["group"]
|
||||
newstate = self.cleaned_data["newstate"]
|
||||
|
||||
ok_to_assign = (
|
||||
("draft-stream-ietf", ("wg", "ag")),
|
||||
("draft-stream-irtf", ("rg", "rag")),
|
||||
("draft-stream-editorial", ("edwg",)),
|
||||
)
|
||||
ok = True
|
||||
for stream, types in ok_to_assign:
|
||||
if newstate.type_id == stream and group.type_id not in types:
|
||||
ok = False
|
||||
break
|
||||
if not ok:
|
||||
state_type_text = newstate.type_id.split("-")[-1].upper()
|
||||
group_type_text = {
|
||||
"wg": "IETF Working Group",
|
||||
"ag": "IETF Area Group",
|
||||
"rg": "IRTF Research Group",
|
||||
"rag": "IRTF Area Group",
|
||||
"edwg": "Editorial Stream Working Group",
|
||||
}[group.type_id]
|
||||
raise forms.ValidationError(
|
||||
f"Cannot assign {state_type_text} state to a {group_type_text}"
|
||||
)
|
||||
return newstate
|
||||
|
||||
|
||||
@login_required
|
||||
def adopt_draft(request, name):
|
||||
doc = get_object_or_404(Document, type="draft", name=name)
|
||||
|
||||
if not can_adopt_draft(request.user, doc):
|
||||
permission_denied(request, "You don't have permission to access this page.")
|
||||
|
||||
|
@ -1447,8 +1513,10 @@ def adopt_draft(request, name):
|
|||
events = []
|
||||
|
||||
group = form.cleaned_data["group"]
|
||||
if group.type.slug == "rg":
|
||||
new_stream = StreamName.objects.get(slug="irtf")
|
||||
if group.type.slug in ("rg", "rag"):
|
||||
new_stream = StreamName.objects.get(slug="irtf")
|
||||
elif group.type.slug =="edwg":
|
||||
new_stream = StreamName.objects.get(slug="editorial")
|
||||
else:
|
||||
new_stream = StreamName.objects.get(slug="ietf")
|
||||
|
||||
|
@ -1467,6 +1535,13 @@ def adopt_draft(request, name):
|
|||
if old_stream != None:
|
||||
email_stream_changed(request, doc, old_stream, new_stream)
|
||||
|
||||
# Force intended std level here if stream isn't ietf
|
||||
if new_stream.slug != "ietf":
|
||||
old_level = doc.intended_std_level
|
||||
new_level = IntendedStdLevelName.objects.get(slug="inf", used=True)
|
||||
set_intended_status_level(request=request, doc=doc, new_level=new_level, old_level=old_level, comment="")
|
||||
|
||||
|
||||
# group
|
||||
if group != doc.group:
|
||||
e = DocEvent(type="changed_group", doc=doc, rev=doc.rev, by=by)
|
||||
|
@ -1737,3 +1812,36 @@ def change_stream_state(request, name, state_type):
|
|||
"state_type": state_type,
|
||||
"next_states": next_states,
|
||||
})
|
||||
|
||||
# This should be in ietf.doc.utils, but placing it there brings a circular import issue with ietf.doc.mail
|
||||
def set_intended_status_level(request, doc, new_level, old_level, comment):
|
||||
if new_level != old_level:
|
||||
doc.intended_std_level = new_level
|
||||
|
||||
events = []
|
||||
e = DocEvent(doc=doc, rev=doc.rev, by=request.user.person, type='changed_document')
|
||||
e.desc = "Intended Status changed to <b>%s</b> from %s"% (new_level,old_level)
|
||||
e.save()
|
||||
events.append(e)
|
||||
|
||||
if comment:
|
||||
c = DocEvent(doc=doc, rev=doc.rev, by=request.user.person, type="added_comment")
|
||||
c.desc = comment
|
||||
c.save()
|
||||
events.append(c)
|
||||
|
||||
de = doc.latest_event(ConsensusDocEvent, type="changed_consensus")
|
||||
prev_consensus = de and de.consensus
|
||||
if not prev_consensus and doc.intended_std_level_id in ("std", "ds", "ps", "bcp"):
|
||||
ce = ConsensusDocEvent(doc=doc, rev=doc.rev, by=request.user.person, type="changed_consensus")
|
||||
ce.consensus = True
|
||||
ce.desc = "Changed consensus to <b>%s</b> from %s" % (nice_consensus(True),
|
||||
nice_consensus(prev_consensus))
|
||||
ce.save()
|
||||
events.append(ce)
|
||||
|
||||
doc.save_with_history(events)
|
||||
|
||||
msg = "\n".join(e.desc for e in events)
|
||||
|
||||
email_intended_status_changed(request, doc, msg)
|
||||
|
|
152
ietf/group/migrations/0060_editoral_refactor.py
Executable file
152
ietf/group/migrations/0060_editoral_refactor.py
Executable file
|
@ -0,0 +1,152 @@
|
|||
# Copyright The IETF Trust 2023, All Rights Reserved
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def forward(apps, schema_editor):
|
||||
Group = apps.get_model("group", "Group")
|
||||
GroupFeatures = apps.get_model("group", "GroupFeatures")
|
||||
GroupTypeName = apps.get_model("name", "GroupTypeName")
|
||||
|
||||
GroupTypeName.objects.create(
|
||||
slug="edwg",
|
||||
name="Editorial Stream Working Group",
|
||||
desc="Editorial Stream Working Group",
|
||||
used=True,
|
||||
)
|
||||
GroupTypeName.objects.create(
|
||||
slug="edappr",
|
||||
name="Editorial Stream Approval Group",
|
||||
desc="Editorial Stream Approval Group",
|
||||
used=True,
|
||||
)
|
||||
Group.objects.filter(acronym="rswg").update(type_id="edwg")
|
||||
Group.objects.filter(acronym="rsab").update(type_id="edappr")
|
||||
Group.objects.filter(acronym="editorial").delete()
|
||||
GroupFeatures.objects.create(
|
||||
type_id="edwg",
|
||||
need_parent=False,
|
||||
has_milestones=False,
|
||||
has_chartering_process=False,
|
||||
has_documents=True,
|
||||
has_session_materials=True,
|
||||
has_meetings=True,
|
||||
has_reviews=False,
|
||||
has_default_chat=True,
|
||||
acts_like_wg=True,
|
||||
create_wiki=False,
|
||||
custom_group_roles=False,
|
||||
customize_workflow=True,
|
||||
is_schedulable=True,
|
||||
show_on_agenda=True,
|
||||
agenda_filter_type_id="normal",
|
||||
req_subm_approval=True,
|
||||
agenda_type_id="ietf",
|
||||
about_page="ietf.group.views.group_about",
|
||||
default_tab="ietf.group.views.group_documents",
|
||||
material_types=["slides"],
|
||||
default_used_roles=["chair"],
|
||||
admin_roles=["chair"],
|
||||
docman_roles=["chair"],
|
||||
groupman_roles=["chair"],
|
||||
groupman_authroles=["Secretariat"],
|
||||
matman_roles=["chair"],
|
||||
role_order=["chair"],
|
||||
session_purposes=["regular"],
|
||||
)
|
||||
# Create edappr GroupFeature
|
||||
GroupFeatures.objects.create(
|
||||
type_id="edappr",
|
||||
need_parent=False,
|
||||
has_milestones=False,
|
||||
has_chartering_process=False,
|
||||
has_documents=False,
|
||||
has_session_materials=True,
|
||||
has_meetings=True,
|
||||
has_reviews=False,
|
||||
has_default_chat=True,
|
||||
acts_like_wg=False,
|
||||
create_wiki=False,
|
||||
custom_group_roles=False,
|
||||
customize_workflow=False,
|
||||
is_schedulable=True,
|
||||
show_on_agenda=True,
|
||||
agenda_filter_type_id="normal",
|
||||
req_subm_approval=False,
|
||||
agenda_type_id="ietf",
|
||||
about_page="ietf.group.views.group_about",
|
||||
default_tab="ietf.group.views.group_about",
|
||||
material_types=["slides"],
|
||||
default_used_roles=["chair", "member"],
|
||||
admin_roles=["chair"],
|
||||
docman_roles=["chair"],
|
||||
groupman_roles=["chair"],
|
||||
groupman_authroles=["Secretariat"],
|
||||
matman_roles=["chair"],
|
||||
role_order=["chair", "member"],
|
||||
session_purposes=["officehourse", "regular"],
|
||||
)
|
||||
GroupFeatures.objects.filter(type_id="editorial").delete()
|
||||
GroupTypeName.objects.filter(slug="editorial").delete()
|
||||
|
||||
|
||||
def reverse(apps, schema_editor):
|
||||
Group = apps.get_model("group", "Group")
|
||||
GroupFeatures = apps.get_model("group", "GroupFeatures")
|
||||
GroupTypeName = apps.get_model("name", "GroupTypeName")
|
||||
GroupTypeName.objects.filter(slug="editorial").update(name="Editorial")
|
||||
Group.objects.create(
|
||||
acronym="editorial",
|
||||
name="Editorial Stream",
|
||||
state_id="active",
|
||||
type_id="editorial",
|
||||
parent=None,
|
||||
)
|
||||
GroupFeatures.objects.create(
|
||||
type_id="editorial",
|
||||
need_parent=False,
|
||||
has_milestones=False,
|
||||
has_chartering_process=False,
|
||||
has_documents=False,
|
||||
has_session_materials=False,
|
||||
has_meetings=False,
|
||||
has_reviews=False,
|
||||
has_default_chat=False,
|
||||
acts_like_wg=False,
|
||||
create_wiki=False,
|
||||
custom_group_roles=True,
|
||||
customize_workflow=False,
|
||||
is_schedulable=False,
|
||||
show_on_agenda=False,
|
||||
agenda_filter_type_id="none",
|
||||
req_subm_approval=False,
|
||||
agenda_type_id="side",
|
||||
about_page="ietf.group.views.group_about",
|
||||
default_tab="ietf.group.views.group_about",
|
||||
material_types=["slides"],
|
||||
default_used_roles=["auth", "chair"],
|
||||
admin_roles=["chair"],
|
||||
docman_roles=[],
|
||||
groupman_roles=[],
|
||||
matman_roles=[],
|
||||
role_order=["chair", "secr"],
|
||||
session_purposes=["officehours"],
|
||||
)
|
||||
Group.objects.filter(acronym__in=["rswg", "rsab"]).update(type_id="rfcedtyp")
|
||||
GroupTypeName.objects.create(
|
||||
slug="editorial",
|
||||
name="Editorial",
|
||||
desc="Editorial Stream Group",
|
||||
used=True,
|
||||
)
|
||||
GroupFeatures.objects.filter(type_id__in=["edwg", "edappr"]).delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("group", "0059_use_timezone_now_for_group_models"),
|
||||
("name", "0045_polls_and_chatlogs"),
|
||||
]
|
||||
|
||||
operations = [migrations.RunPython(forward, reverse)]
|
|
@ -87,7 +87,7 @@ class GroupPagesTests(TestCase):
|
|||
self.assertContains(r, "Directorate")
|
||||
self.assertContains(r, "AG")
|
||||
|
||||
for slug in GroupTypeName.objects.exclude(slug__in=['wg','rg','ag','rag','area','dir','review','team','program','adhoc','ise','adm','iabasg','rfcedtyp']).values_list('slug',flat=True):
|
||||
for slug in GroupTypeName.objects.exclude(slug__in=['wg','rg','ag','rag','area','dir','review','team','program','adhoc','ise','adm','iabasg','rfcedtyp', 'edwg', 'edappr']).values_list('slug',flat=True):
|
||||
with self.assertRaises(NoReverseMatch):
|
||||
url=urlreverse('ietf.group.views.active_groups', kwargs=dict(group_type=slug))
|
||||
|
||||
|
@ -1826,7 +1826,7 @@ class StatusUpdateTests(TestCase):
|
|||
self.assertEqual(response.status_code, 404)
|
||||
self.client.logout()
|
||||
|
||||
for type_id in GroupTypeName.objects.exclude(slug__in=('wg','rg','ag','rag','team')).values_list('slug',flat=True):
|
||||
for type_id in GroupTypeName.objects.exclude(slug__in=('wg','rg','ag','rag','team','edwg')).values_list('slug',flat=True):
|
||||
group = GroupFactory.create(type_id=type_id)
|
||||
for user in (None,User.objects.get(username='secretary')):
|
||||
ensure_updates_dont_show(group,user)
|
||||
|
|
|
@ -334,7 +334,7 @@ def active_adm(request):
|
|||
return render(request, 'group/active_adm.html', {'adm' : adm })
|
||||
|
||||
def active_rfced(request):
|
||||
rfced = Group.objects.filter(type="rfcedtyp", state="active").order_by("parent", "name")
|
||||
rfced = Group.objects.filter(type__in=["rfcedtyp", "edwg", "edappr"], state="active").order_by("parent", "name")
|
||||
return render(request, 'group/active_rfced.html', {'rfced' : rfced})
|
||||
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ from ietf.person.models import Person, Email
|
|||
from ietf.mailinglists.models import Allowlisted
|
||||
from ietf.utils.text import isascii
|
||||
|
||||
|
||||
class RegistrationForm(forms.Form):
|
||||
email = forms.EmailField(label="Your email (lowercase)")
|
||||
|
||||
|
@ -193,10 +194,18 @@ class ResetPasswordForm(forms.Form):
|
|||
username = forms.EmailField(label="Your email (lowercase)")
|
||||
|
||||
def clean_username(self):
|
||||
import ietf.ietfauth.views
|
||||
"""Verify that the username is valid
|
||||
|
||||
In addition to EmailField's checks, verifies that a User matching the username exists.
|
||||
"""
|
||||
username = self.cleaned_data["username"]
|
||||
if not User.objects.filter(username=username).exists():
|
||||
raise forms.ValidationError(mark_safe("Didn't find a matching account. If you don't have an account yet, you can <a href=\"{}\">create one</a>.".format(urlreverse(ietf.ietfauth.views.create_account))))
|
||||
raise forms.ValidationError(mark_safe(
|
||||
"Didn't find a matching account. "
|
||||
"If you don't have an account yet, you can <a href=\"{}\">create one</a>.".format(
|
||||
urlreverse('ietf.ietfauth.views.create_account')
|
||||
)
|
||||
))
|
||||
return username
|
||||
|
||||
|
||||
|
|
|
@ -429,12 +429,10 @@ class IetfAuthTests(TestCase):
|
|||
email = 'someone@example.com'
|
||||
password = 'foobar'
|
||||
|
||||
user = User.objects.create(username=email, email=email)
|
||||
user = PersonFactory(user__email=email).user
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
p = Person.objects.create(name="Some One", ascii="Some One", user=user)
|
||||
Email.objects.create(address=user.username, person=p, origin=user.username)
|
||||
|
||||
|
||||
# get
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
@ -512,6 +510,39 @@ class IetfAuthTests(TestCase):
|
|||
r = self.client.get(confirm_url)
|
||||
self.assertEqual(r.status_code, 404)
|
||||
|
||||
def test_reset_password_without_person(self):
|
||||
"""No password reset for account without a person"""
|
||||
url = urlreverse('ietf.ietfauth.views.password_reset')
|
||||
user = UserFactory()
|
||||
user.set_password('some password')
|
||||
user.save()
|
||||
empty_outbox()
|
||||
r = self.client.post(url, { 'username': user.username})
|
||||
self.assertContains(r, 'No known active email addresses', status_code=200)
|
||||
q = PyQuery(r.content)
|
||||
self.assertTrue(len(q("form .is-invalid")) > 0)
|
||||
self.assertEqual(len(outbox), 0)
|
||||
|
||||
def test_reset_password_address_handling(self):
|
||||
"""Reset password links are only sent to known, active addresses"""
|
||||
url = urlreverse('ietf.ietfauth.views.password_reset')
|
||||
person = PersonFactory()
|
||||
person.email_set.update(active=False)
|
||||
empty_outbox()
|
||||
r = self.client.post(url, { 'username': person.user.username})
|
||||
self.assertContains(r, 'No known active email addresses', status_code=200)
|
||||
q = PyQuery(r.content)
|
||||
self.assertTrue(len(q("form .is-invalid")) > 0)
|
||||
self.assertEqual(len(outbox), 0)
|
||||
|
||||
active_address = EmailFactory(person=person).address
|
||||
r = self.client.post(url, {'username': person.user.username})
|
||||
self.assertNotContains(r, 'No known active email addresses', status_code=200)
|
||||
self.assertEqual(len(outbox), 1)
|
||||
to = outbox[0].get('To')
|
||||
self.assertIn(active_address, to)
|
||||
self.assertNotIn(person.user.username, to)
|
||||
|
||||
def test_review_overview(self):
|
||||
review_req = ReviewRequestFactory()
|
||||
assignment = ReviewAssignmentFactory(review_request=review_req,reviewer=EmailFactory(person__user__username='reviewer'))
|
||||
|
|
|
@ -67,6 +67,7 @@ def has_role(user, role_names, *args, **kwargs):
|
|||
"IETF Chair": Q(person=person, name="chair", group__acronym="ietf"),
|
||||
"IETF Trust Chair": Q(person=person, name="chair", group__acronym="ietf-trust"),
|
||||
"IRTF Chair": Q(person=person, name="chair", group__acronym="irtf"),
|
||||
"RSAB Chair": Q(person=person, name="chair", group__acronym="rsab"),
|
||||
"IAB Chair": Q(person=person, name="chair", group__acronym="iab"),
|
||||
"IAB Executive Director": Q(person=person, name="execdir", group__acronym="iab"),
|
||||
"IAB Group Chair": Q(person=person, name="chair", group__type="iab", group__state="active"),
|
||||
|
@ -90,6 +91,7 @@ def has_role(user, role_names, *args, **kwargs):
|
|||
"Reviewer": Q(person=person, name="reviewer", group__state="active"),
|
||||
"Review Team Secretary": Q(person=person, name="secr", group__reviewteamsettings__isnull=False,group__state="active", ),
|
||||
"IRSG Member": (Q(person=person, name="member", group__acronym="irsg") | Q(person=person, name="chair", group__acronym="irtf") | Q(person=person, name="atlarge", group__acronym="irsg")),
|
||||
"RSAB Member": Q(person=person, name="member", group__acronym="rsab"),
|
||||
"Robot": Q(person=person, name="robot", group__acronym="secretariat"),
|
||||
}
|
||||
|
||||
|
@ -163,6 +165,10 @@ def is_authorized_in_doc_stream(user, doc):
|
|||
if doc.group.type.slug == 'individ':
|
||||
docman_roles = GroupFeatures.objects.get(type_id="ietf").docman_roles
|
||||
group_req = Q(group__acronym=doc.stream.slug)
|
||||
elif doc.stream.slug == "editorial":
|
||||
group_req = Q(group=doc.group) | Q(group__acronym='rsab')
|
||||
if doc.group.type.slug in ("individ", "rfcedtype"):
|
||||
docman_roles = GroupFeatures.objects.get(type_id="rfcedtyp").docman_roles
|
||||
else:
|
||||
group_req = Q() # no group constraint for other cases
|
||||
|
||||
|
@ -295,3 +301,24 @@ class OidcExtraScopeClaims(oidc_provider.lib.claims.ScopeClaims):
|
|||
|
||||
return info
|
||||
|
||||
def can_request_rfc_publication(user, doc):
|
||||
"""Answers whether this user has an appropriate role to send this document to the RFC Editor for publication as an RFC.
|
||||
|
||||
This not take anything but the stream of the document into account.
|
||||
|
||||
NOTE: This intentionally always returns False for IETF stream documents.
|
||||
The publication request process for the IETF stream is handled by the
|
||||
secretariat at ietf.doc.views_ballot.approve_ballot"""
|
||||
|
||||
if doc.stream_id == "irtf":
|
||||
return has_role(user, ("Secretariat", "IRTF Chair"))
|
||||
elif doc.stream_id == "editorial":
|
||||
return has_role(user, ("Secretariat", "RSAB Chair"))
|
||||
elif doc.stream_id == "ise":
|
||||
return has_role(user, ("Secretariat", "ISE"))
|
||||
elif doc.stream_id == "iab":
|
||||
return has_role(user, ("Secretariat", "IAB Chair"))
|
||||
elif doc.stream_id == "ietf":
|
||||
return False # See the docstring
|
||||
else:
|
||||
return False
|
||||
|
|
|
@ -413,32 +413,39 @@ def password_reset(request):
|
|||
if request.method == 'POST':
|
||||
form = ResetPasswordForm(request.POST)
|
||||
if form.is_valid():
|
||||
username = form.cleaned_data['username']
|
||||
submitted_username = form.cleaned_data['username']
|
||||
# The form validation checks that a matching User exists. Add the person__isnull check
|
||||
# because the OneToOne field does not gracefully handle checks for user.person is Null.
|
||||
# If we don't get a User here, we know it's because there's no related Person.
|
||||
user = User.objects.filter(username=submitted_username, person__isnull=False).first()
|
||||
if not (user and user.person.email_set.filter(active=True).exists()):
|
||||
form.add_error(
|
||||
'username',
|
||||
'No known active email addresses are associated with this account. '
|
||||
'Please contact the secretariat for assistance.',
|
||||
)
|
||||
else:
|
||||
data = {
|
||||
'username': user.username,
|
||||
'password': user.password and user.password[-4:],
|
||||
'last_login': user.last_login.timestamp() if user.last_login else None,
|
||||
}
|
||||
auth = django.core.signing.dumps(data, salt="password_reset")
|
||||
|
||||
data = { 'username': username }
|
||||
if User.objects.filter(username=username).exists():
|
||||
user = User.objects.get(username=username)
|
||||
data['password'] = user.password and user.password[-4:]
|
||||
if user.last_login:
|
||||
data['last_login'] = user.last_login.timestamp()
|
||||
else:
|
||||
data['last_login'] = None
|
||||
|
||||
auth = django.core.signing.dumps(data, salt="password_reset")
|
||||
|
||||
domain = Site.objects.get_current().domain
|
||||
subject = 'Confirm password reset at %s' % domain
|
||||
from_email = settings.DEFAULT_FROM_EMAIL
|
||||
to_email = username # form validation makes sure that this is an email address
|
||||
|
||||
send_mail(request, to_email, from_email, subject, 'registration/password_reset_email.txt', {
|
||||
'domain': domain,
|
||||
'auth': auth,
|
||||
'username': username,
|
||||
'expire': settings.MINUTES_TO_EXPIRE_RESET_PASSWORD_LINK,
|
||||
})
|
||||
|
||||
success = True
|
||||
domain = Site.objects.get_current().domain
|
||||
subject = 'Confirm password reset at %s' % domain
|
||||
from_email = settings.DEFAULT_FROM_EMAIL
|
||||
# Send email to addresses from the database, NOT to the address from the form.
|
||||
# This prevents unicode spoofing tricks (https://nvd.nist.gov/vuln/detail/CVE-2019-19844).
|
||||
to_emails = list(set(email.address for email in user.person.email_set.filter(active=True)))
|
||||
to_emails.sort()
|
||||
send_mail(request, to_emails, from_email, subject, 'registration/password_reset_email.txt', {
|
||||
'domain': domain,
|
||||
'auth': auth,
|
||||
'username': submitted_username,
|
||||
'expire': settings.MINUTES_TO_EXPIRE_RESET_PASSWORD_LINK,
|
||||
})
|
||||
success = True
|
||||
else:
|
||||
form = ResetPasswordForm()
|
||||
return render(request, 'registration/password_reset.html', {
|
||||
|
|
|
@ -710,7 +710,7 @@ Subject: test
|
|||
post_data = {
|
||||
'iprdocrel_set-TOTAL_FORMS' : 1,
|
||||
'iprdocrel_set-INITIAL_FORMS' : 0,
|
||||
'iprdocrel_set-0-id': disclosure.pk,
|
||||
'iprdocrel_set-0-id': '',
|
||||
"iprdocrel_set-0-document": disclosure.docs.first().pk,
|
||||
"iprdocrel_set-0-revisions": disclosure.docs.first().document.rev,
|
||||
'holder_legal_name': disclosure.holder_legal_name,
|
||||
|
|
72
ietf/mailtrigger/migrations/0024_rsab_ballots.py
Normal file
72
ietf/mailtrigger/migrations/0024_rsab_ballots.py
Normal file
|
@ -0,0 +1,72 @@
|
|||
# Copyright The IETF Trust 2022, All Rights Reserved# Generated by Django 2.2.28 on 2022-12-22 22:41
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def forward(apps, schema_editor):
|
||||
Recipient = apps.get_model("mailtrigger", "Recipient")
|
||||
MailTrigger = apps.get_model("mailtrigger", "MailTrigger")
|
||||
|
||||
rsab = Recipient.objects.create(
|
||||
slug="rsab",
|
||||
desc="The RFC Series Approval Board",
|
||||
template="The RSAB <rsab@rfc-editor.org>",
|
||||
)
|
||||
|
||||
rsab_ballot_saved = MailTrigger.objects.create(
|
||||
slug="rsab_ballot_saved",
|
||||
desc="Recipients when a new RSAB ballot position with comments is saved",
|
||||
)
|
||||
rsab_ballot_saved.to.add(rsab)
|
||||
rsab_ballot_saved.cc.set(
|
||||
Recipient.objects.filter(
|
||||
slug__in=[
|
||||
"doc_affecteddoc_authors",
|
||||
"doc_affecteddoc_group_chairs",
|
||||
"doc_affecteddoc_notify",
|
||||
"doc_authors",
|
||||
"doc_group_chairs",
|
||||
"doc_group_mail_list",
|
||||
"doc_notify",
|
||||
"doc_shepherd",
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
rsab_ballot_issued = MailTrigger.objects.create(
|
||||
slug="rsab_ballot_issued",
|
||||
desc="Recipients when a new RSAB ballot is issued",
|
||||
)
|
||||
rsab_ballot_issued.to.add(rsab)
|
||||
rsab_ballot_issued.cc.set(
|
||||
Recipient.objects.filter(
|
||||
slug__in=[
|
||||
"doc_affecteddoc_authors",
|
||||
"doc_affecteddoc_group_chairs",
|
||||
"doc_affecteddoc_notify",
|
||||
"doc_authors",
|
||||
"doc_group_chairs",
|
||||
"doc_group_mail_list",
|
||||
"doc_notify",
|
||||
"doc_shepherd",
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def reverse(apps, schema_editor):
|
||||
Recipient = apps.get_model("mailtrigger", "Recipient")
|
||||
MailTrigger = apps.get_model("mailtrigger", "MailTrigger")
|
||||
MailTrigger.objects.filter(
|
||||
slug__in=("rsab_ballot_issued", "rsab_ballot_saved")
|
||||
).delete()
|
||||
Recipient.objects.filter(slug="rsab").delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("mailtrigger", "0023_bofreq_triggers"),
|
||||
]
|
||||
|
||||
operations = [migrations.RunPython(forward, reverse)]
|
|
@ -9,7 +9,7 @@ from email.utils import parseaddr
|
|||
|
||||
from ietf.doc.utils_bofreq import bofreq_editors, bofreq_responsible
|
||||
from ietf.utils.mail import formataddr, get_email_addresses_from_text
|
||||
from ietf.group.models import Group
|
||||
from ietf.group.models import Group, Role
|
||||
from ietf.person.models import Email, Alias
|
||||
from ietf.review.models import ReviewTeamSettings
|
||||
|
||||
|
@ -137,10 +137,13 @@ class Recipient(models.Model):
|
|||
|
||||
def gather_stream_managers(self, **kwargs):
|
||||
addrs = []
|
||||
manager_map = dict(ise = '<rfc-ise@rfc-editor.org>',
|
||||
irtf = '<irtf-chair@irtf.org>',
|
||||
ietf = '<iesg@ietf.org>',
|
||||
iab = '<iab-chair@iab.org>')
|
||||
manager_map = dict(
|
||||
ise = '<rfc-ise@rfc-editor.org>',
|
||||
irtf = '<irtf-chair@irtf.org>',
|
||||
ietf = '<iesg@ietf.org>',
|
||||
iab = '<iab-chair@iab.org>',
|
||||
editorial = Role.objects.filter(group__acronym="rsab",name_id="chair").values_list("email__address", flat=True),
|
||||
)
|
||||
if 'streams' in kwargs:
|
||||
for stream in kwargs['streams']:
|
||||
if stream in manager_map:
|
||||
|
|
|
@ -25,6 +25,8 @@ import debug # pyflakes:ignore
|
|||
from ietf.person.models import Person
|
||||
from ietf.meeting import models
|
||||
from ietf.meeting.helpers import get_person_by_email
|
||||
from ietf.name.models import SessionPurposeName
|
||||
|
||||
|
||||
# 40 runs of the optimiser for IETF 106 with cycles=160 resulted in 16
|
||||
# zero-violation invocations, with a mean number of runs of 91 and
|
||||
|
@ -72,18 +74,31 @@ class Command(BaseCommand):
|
|||
'Base schedule for generated schedule, specified as "[owner/]name"'
|
||||
' (default is no base schedule; owner not required if name is unique)'
|
||||
))
|
||||
parser.add_argument('-p', '--purpose',
|
||||
dest='purposes',
|
||||
action='append',
|
||||
choices=[
|
||||
spn.slug for spn in SessionPurposeName.objects.all()
|
||||
if 'regular' in spn.timeslot_types # scheduler only works with "regular" timeslots
|
||||
],
|
||||
default=None,
|
||||
help=(
|
||||
'Limit scheduling to specified purpose '
|
||||
'(use option multiple times to specify more than one purpose; default is all purposes)'
|
||||
))
|
||||
|
||||
def handle(self, meeting, name, max_cycles, verbosity, base_id, *args, **kwargs):
|
||||
ScheduleHandler(self.stdout, meeting, name, max_cycles, verbosity, base_id).run()
|
||||
def handle(self, meeting, name, max_cycles, verbosity, base_id, purposes, *args, **kwargs):
|
||||
ScheduleHandler(self.stdout, meeting, name, max_cycles, verbosity, base_id, purposes).run()
|
||||
|
||||
|
||||
class ScheduleHandler(object):
|
||||
def __init__(self, stdout, meeting_number, name=None, max_cycles=OPTIMISER_MAX_CYCLES,
|
||||
verbosity=1, base_id=None):
|
||||
verbosity=1, base_id=None, session_purposes=None):
|
||||
self.stdout = stdout
|
||||
self.verbosity = verbosity
|
||||
self.name = name
|
||||
self.max_cycles = max_cycles
|
||||
self.session_purposes = session_purposes
|
||||
if meeting_number:
|
||||
try:
|
||||
self.meeting = models.Meeting.objects.get(type="ietf", number=meeting_number)
|
||||
|
@ -114,6 +129,10 @@ class ScheduleHandler(object):
|
|||
msgs.append('Applying schedule {} as base schedule'.format(ScheduleId.from_schedule(self.base_schedule)))
|
||||
self.stdout.write('\n{}\n\n'.format('\n'.join(msgs)))
|
||||
self._load_meeting()
|
||||
if len(self.schedule.sessions) == 0:
|
||||
raise CommandError('No sessions found to schedule')
|
||||
if len(self.schedule.timeslots) == 0:
|
||||
raise CommandError('No timeslots found for schedule')
|
||||
|
||||
def run(self):
|
||||
"""Schedule all sessions"""
|
||||
|
@ -194,6 +213,8 @@ class ScheduleHandler(object):
|
|||
Extra arguments are passed to the Session constructor.
|
||||
"""
|
||||
sessions_db = self.meeting.session_set.that_can_be_scheduled().filter(type_id='regular')
|
||||
if self.session_purposes is not None:
|
||||
sessions_db = sessions_db.filter(purpose__slug__in=self.session_purposes)
|
||||
if self.base_schedule is None:
|
||||
fixed_sessions = models.Session.objects.none()
|
||||
else:
|
||||
|
|
|
@ -134,7 +134,7 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
|
|||
self.assertEqual(session_info_container.find_element(By.CSS_SELECTOR, ".other-session .time").text, "not yet scheduled")
|
||||
|
||||
# deselect
|
||||
self.driver.find_element(By.CSS_SELECTOR, '.drop-target').click()
|
||||
self.driver.find_element(By.CSS_SELECTOR, '.timeslot[data-type="regular"] .drop-target').click()
|
||||
|
||||
self.assertEqual(session_info_container.find_elements(By.CSS_SELECTOR, ".title"), [])
|
||||
self.assertNotIn('other-session-selected', s2b_element.get_attribute('class'))
|
||||
|
@ -193,9 +193,9 @@ class EditMeetingScheduleTests(IetfSeleniumTestCase):
|
|||
|
||||
# violated due to constraints - both the timeslot and its timeslot label
|
||||
self.assertTrue(self.driver.find_elements(By.CSS_SELECTOR, '#timeslot{}.would-violate-hint'.format(slot1.pk)))
|
||||
# Find the timeslot label for slot1 - it's the first timeslot in the first room group
|
||||
# Find the timeslot label for slot1 - it's the first timeslot in the room group containing room 1
|
||||
slot1_roomgroup_elt = self.driver.find_element(By.CSS_SELECTOR,
|
||||
'.day-flow .day:first-child .room-group:nth-child(2)' # count from 2 - first-child is the day label
|
||||
'.day-flow .day:first-child .room-group[data-rooms="1"]'
|
||||
)
|
||||
self.assertTrue(
|
||||
slot1_roomgroup_elt.find_elements(By.CSS_SELECTOR,
|
||||
|
|
|
@ -6369,7 +6369,7 @@ class SessionTests(TestCase):
|
|||
# a couple non-wg group types, confirm that their has_meetings features are as expected
|
||||
group_type_with_meetings = 'adhoc'
|
||||
self.assertTrue(GroupFeatures.objects.get(pk=group_type_with_meetings).has_meetings)
|
||||
group_type_without_meetings = 'editorial'
|
||||
group_type_without_meetings = 'sdo'
|
||||
self.assertFalse(GroupFeatures.objects.get(pk=group_type_without_meetings).has_meetings)
|
||||
|
||||
area = GroupFactory(type_id='area', acronym='area')
|
||||
|
|
|
@ -659,7 +659,9 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
|
|||
sorted_rooms = sorted(
|
||||
rooms_with_timeslots,
|
||||
key=lambda room: (
|
||||
# First, sort regular session rooms ahead of others - these will usually
|
||||
# Sort higher capacity rooms first.
|
||||
-room.capacity if room.capacity is not None else 1, # sort rooms with capacity = None at end
|
||||
# Sort regular session rooms ahead of others - these will usually
|
||||
# have more timeslots than other room types.
|
||||
0 if room_data[room.pk]['timeslot_count'] == max_timeslots else 1,
|
||||
# Sort rooms with earlier timeslots ahead of later
|
||||
|
@ -669,8 +671,6 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
|
|||
# Sort by list of starting time and duration so that groups with identical
|
||||
# timeslot structure will be neighbors. The grouping algorithm relies on this!
|
||||
room_data[room.pk]['start_and_duration'],
|
||||
# Within each group, sort higher capacity rooms first.
|
||||
-room.capacity if room.capacity is not None else 1, # sort rooms with capacity = None at end
|
||||
# Finally, sort alphabetically by name
|
||||
room.name
|
||||
)
|
||||
|
|
|
@ -147,6 +147,23 @@
|
|||
"model": "doc.ballottype",
|
||||
"pk": 7
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"doc_type": "draft",
|
||||
"name": "RSAB Approve",
|
||||
"order": 0,
|
||||
"positions": [
|
||||
"concern",
|
||||
"yes",
|
||||
"recuse"
|
||||
],
|
||||
"question": "Is this draft ready for publication in the Editorial stream?",
|
||||
"slug": "rsab-approve",
|
||||
"used": true
|
||||
},
|
||||
"model": "doc.ballottype",
|
||||
"pk": 8
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "",
|
||||
|
@ -2457,6 +2474,71 @@
|
|||
"model": "doc.state",
|
||||
"pk": 168
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "",
|
||||
"name": "Replaced editorial stream document",
|
||||
"next_states": [],
|
||||
"order": 0,
|
||||
"slug": "repl",
|
||||
"type": "draft-stream-editorial",
|
||||
"used": true
|
||||
},
|
||||
"model": "doc.state",
|
||||
"pk": 170
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "",
|
||||
"name": "Active editorial stream document",
|
||||
"next_states": [],
|
||||
"order": 2,
|
||||
"slug": "active",
|
||||
"type": "draft-stream-editorial",
|
||||
"used": true
|
||||
},
|
||||
"model": "doc.state",
|
||||
"pk": 171
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "",
|
||||
"name": "Editorial stream document under RSAB review",
|
||||
"next_states": [],
|
||||
"order": 3,
|
||||
"slug": "rsabpoll",
|
||||
"type": "draft-stream-editorial",
|
||||
"used": true
|
||||
},
|
||||
"model": "doc.state",
|
||||
"pk": 172
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "",
|
||||
"name": "Published RFC",
|
||||
"next_states": [],
|
||||
"order": 4,
|
||||
"slug": "pub",
|
||||
"type": "draft-stream-editorial",
|
||||
"used": true
|
||||
},
|
||||
"model": "doc.state",
|
||||
"pk": 173
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "",
|
||||
"name": "Dead editorial stream document",
|
||||
"next_states": [],
|
||||
"order": 5,
|
||||
"slug": "dead",
|
||||
"type": "draft-stream-editorial",
|
||||
"used": true
|
||||
},
|
||||
"model": "doc.state",
|
||||
"pk": 174
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"label": "State"
|
||||
|
@ -2548,6 +2630,13 @@
|
|||
"model": "doc.statetype",
|
||||
"pk": "draft-rfceditor"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"label": "Editorial stream state"
|
||||
},
|
||||
"model": "doc.statetype",
|
||||
"pk": "draft-stream-editorial"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"label": "IAB state"
|
||||
|
@ -2852,37 +2941,74 @@
|
|||
"about_page": "ietf.group.views.group_about",
|
||||
"acts_like_wg": false,
|
||||
"admin_roles": "[\n \"chair\"\n]",
|
||||
"agenda_filter_type": "none",
|
||||
"agenda_type": "side",
|
||||
"agenda_filter_type": "normal",
|
||||
"agenda_type": "ietf",
|
||||
"create_wiki": false,
|
||||
"custom_group_roles": true,
|
||||
"custom_group_roles": false,
|
||||
"customize_workflow": false,
|
||||
"default_parent": "",
|
||||
"default_tab": "ietf.group.views.group_about",
|
||||
"default_used_roles": "[\n \"auth\",\n \"chair\"\n]",
|
||||
"docman_roles": "[]",
|
||||
"default_used_roles": "[\n \"chair\",\n \"member\"\n]",
|
||||
"docman_roles": "[\n \"chair\"\n]",
|
||||
"groupman_authroles": "[\n \"Secretariat\"\n]",
|
||||
"groupman_roles": "[]",
|
||||
"groupman_roles": "[\n \"chair\"\n]",
|
||||
"has_chartering_process": false,
|
||||
"has_default_chat": false,
|
||||
"has_default_chat": true,
|
||||
"has_documents": false,
|
||||
"has_meetings": false,
|
||||
"has_meetings": true,
|
||||
"has_milestones": false,
|
||||
"has_nonsession_materials": false,
|
||||
"has_reviews": false,
|
||||
"has_session_materials": false,
|
||||
"is_schedulable": false,
|
||||
"has_session_materials": true,
|
||||
"is_schedulable": true,
|
||||
"material_types": "[\n \"slides\"\n]",
|
||||
"matman_roles": "[]",
|
||||
"matman_roles": "[\n \"chair\"\n]",
|
||||
"need_parent": false,
|
||||
"parent_types": [],
|
||||
"req_subm_approval": false,
|
||||
"role_order": "[\n \"chair\",\n \"member\"\n]",
|
||||
"session_purposes": "[\n \"officehourse\",\n \"regular\"\n]",
|
||||
"show_on_agenda": true
|
||||
},
|
||||
"model": "group.groupfeatures",
|
||||
"pk": "edappr"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"about_page": "ietf.group.views.group_about",
|
||||
"acts_like_wg": true,
|
||||
"admin_roles": "[\n \"chair\"\n]",
|
||||
"agenda_filter_type": "normal",
|
||||
"agenda_type": "ietf",
|
||||
"create_wiki": false,
|
||||
"custom_group_roles": false,
|
||||
"customize_workflow": true,
|
||||
"default_parent": "",
|
||||
"default_tab": "ietf.group.views.group_documents",
|
||||
"default_used_roles": "[\n \"chair\"\n]",
|
||||
"docman_roles": "[\n \"chair\"\n]",
|
||||
"groupman_authroles": "[\n \"Secretariat\"\n]",
|
||||
"groupman_roles": "[\n \"chair\"\n]",
|
||||
"has_chartering_process": false,
|
||||
"has_default_chat": true,
|
||||
"has_documents": true,
|
||||
"has_meetings": true,
|
||||
"has_milestones": false,
|
||||
"has_nonsession_materials": false,
|
||||
"has_reviews": false,
|
||||
"has_session_materials": true,
|
||||
"is_schedulable": true,
|
||||
"material_types": "[\n \"slides\"\n]",
|
||||
"matman_roles": "[\n \"chair\"\n]",
|
||||
"need_parent": false,
|
||||
"parent_types": [],
|
||||
"req_subm_approval": true,
|
||||
"role_order": "[\n \"chair\",\n \"secr\"\n]",
|
||||
"session_purposes": "[\n \"officehours\"\n]",
|
||||
"show_on_agenda": false
|
||||
"role_order": "[\n \"chair\"\n]",
|
||||
"session_purposes": "[\n \"regular\"\n]",
|
||||
"show_on_agenda": true
|
||||
},
|
||||
"model": "group.groupfeatures",
|
||||
"pk": "editorial"
|
||||
"pk": "edwg"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
|
@ -3398,7 +3524,7 @@
|
|||
"default_parent": "",
|
||||
"default_tab": "ietf.group.views.group_about",
|
||||
"default_used_roles": "[\n \"auth\",\n \"chair\"\n]",
|
||||
"docman_roles": "[]",
|
||||
"docman_roles": "[\n \"chair\"\n]",
|
||||
"groupman_authroles": "[\n \"Secretariat\"\n]",
|
||||
"groupman_roles": "[\n \"chair\"\n]",
|
||||
"has_chartering_process": false,
|
||||
|
@ -5268,6 +5394,46 @@
|
|||
"model": "mailtrigger.mailtrigger",
|
||||
"pk": "review_req_changed"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"cc": [
|
||||
"doc_affecteddoc_authors",
|
||||
"doc_affecteddoc_group_chairs",
|
||||
"doc_affecteddoc_notify",
|
||||
"doc_authors",
|
||||
"doc_group_chairs",
|
||||
"doc_group_mail_list",
|
||||
"doc_notify",
|
||||
"doc_shepherd"
|
||||
],
|
||||
"desc": "Recipients when a new RSAB ballot is issued",
|
||||
"to": [
|
||||
"rsab"
|
||||
]
|
||||
},
|
||||
"model": "mailtrigger.mailtrigger",
|
||||
"pk": "rsab_ballot_issued"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"cc": [
|
||||
"doc_affecteddoc_authors",
|
||||
"doc_affecteddoc_group_chairs",
|
||||
"doc_affecteddoc_notify",
|
||||
"doc_authors",
|
||||
"doc_group_chairs",
|
||||
"doc_group_mail_list",
|
||||
"doc_notify",
|
||||
"doc_shepherd"
|
||||
],
|
||||
"desc": "Recipients when a new RSAB ballot position with comments is saved",
|
||||
"to": [
|
||||
"rsab"
|
||||
]
|
||||
},
|
||||
"model": "mailtrigger.mailtrigger",
|
||||
"pk": "rsab_ballot_saved"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"cc": [
|
||||
|
@ -6129,6 +6295,14 @@
|
|||
"model": "mailtrigger.recipient",
|
||||
"pk": "rfc_editor_if_doc_in_queue"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "The RFC Series Approval Board",
|
||||
"template": "The RSAB <rsab@rfc-editor.org>"
|
||||
},
|
||||
"model": "mailtrigger.recipient",
|
||||
"pk": "rsab"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "The person that requested a meeting slot for a given group",
|
||||
|
@ -6391,6 +6565,17 @@
|
|||
"model": "name.ballotpositionname",
|
||||
"pk": "block"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"blocking": true,
|
||||
"desc": "",
|
||||
"name": "Concern",
|
||||
"order": 0,
|
||||
"used": true
|
||||
},
|
||||
"model": "name.ballotpositionname",
|
||||
"pk": "concern"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"blocking": true,
|
||||
|
@ -11139,14 +11324,25 @@
|
|||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "Editorial Stream Group",
|
||||
"name": "Editorial",
|
||||
"desc": "Editorial Stream Approval Group",
|
||||
"name": "Editorial Stream Approval Group",
|
||||
"order": 0,
|
||||
"used": true,
|
||||
"verbose_name": ""
|
||||
},
|
||||
"model": "name.grouptypename",
|
||||
"pk": "editorial"
|
||||
"pk": "edappr"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "Editorial Stream Working Group",
|
||||
"name": "Editorial Stream Working Group",
|
||||
"order": 0,
|
||||
"used": true,
|
||||
"verbose_name": ""
|
||||
},
|
||||
"model": "name.grouptypename",
|
||||
"pk": "edwg"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
|
@ -16130,7 +16326,7 @@
|
|||
"fields": {
|
||||
"command": "xym",
|
||||
"switch": "--version",
|
||||
"time": "2022-12-14T08:09:37.183Z",
|
||||
"time": "2023-01-24T08:09:45.071Z",
|
||||
"used": true,
|
||||
"version": "xym 0.6.2"
|
||||
},
|
||||
|
@ -16141,7 +16337,7 @@
|
|||
"fields": {
|
||||
"command": "pyang",
|
||||
"switch": "--version",
|
||||
"time": "2022-12-14T08:09:37.496Z",
|
||||
"time": "2023-01-24T08:09:45.405Z",
|
||||
"used": true,
|
||||
"version": "pyang 2.5.3"
|
||||
},
|
||||
|
@ -16152,7 +16348,7 @@
|
|||
"fields": {
|
||||
"command": "yanglint",
|
||||
"switch": "--version",
|
||||
"time": "2022-12-14T08:09:37.549Z",
|
||||
"time": "2023-01-24T08:09:45.421Z",
|
||||
"used": true,
|
||||
"version": "yanglint SO 1.9.2"
|
||||
},
|
||||
|
@ -16163,9 +16359,9 @@
|
|||
"fields": {
|
||||
"command": "xml2rfc",
|
||||
"switch": "--version",
|
||||
"time": "2022-12-14T08:09:38.461Z",
|
||||
"time": "2023-01-24T08:09:46.384Z",
|
||||
"used": true,
|
||||
"version": "xml2rfc 3.15.3"
|
||||
"version": "xml2rfc 3.16.0"
|
||||
},
|
||||
"model": "utils.versioninfo",
|
||||
"pk": 4
|
||||
|
|
|
@ -199,11 +199,13 @@ def determine_merge_order(source,target):
|
|||
return source,target
|
||||
|
||||
def get_active_balloters(ballot_type):
|
||||
if (ballot_type.slug != "irsg-approve"):
|
||||
active_balloters = get_active_ads()
|
||||
if ballot_type.slug == 'irsg-approve':
|
||||
return get_active_irsg()
|
||||
elif ballot_type.slug == 'rsab-approve':
|
||||
return get_active_rsab()
|
||||
else:
|
||||
active_balloters = get_active_irsg()
|
||||
return active_balloters
|
||||
return get_active_ads()
|
||||
|
||||
|
||||
def get_active_ads():
|
||||
cache_key = "doc:active_ads"
|
||||
|
@ -219,7 +221,15 @@ def get_active_irsg():
|
|||
if not active_irsg_balloters:
|
||||
active_irsg_balloters = list(Person.objects.filter(role__group__acronym='irsg',role__name__in=['chair','member','atlarge']).distinct())
|
||||
cache.set(cache_key, active_irsg_balloters)
|
||||
return active_irsg_balloters
|
||||
return active_irsg_balloters
|
||||
|
||||
def get_active_rsab():
|
||||
cache_key = "doc:active_rsab_balloters"
|
||||
active_rsab_balloters = cache.get(cache_key)
|
||||
if not active_rsab_balloters:
|
||||
active_rsab_balloters = list(Person.objects.filter(role__group__acronym='rsab', role__name="member").distinct())
|
||||
cache.set(cache_key, active_rsab_balloters)
|
||||
return active_rsab_balloters
|
||||
|
||||
def get_dots(person):
|
||||
roles = person.role_set.filter(group__state_id__in=('active','bof','proposed'))
|
||||
|
|
|
@ -44,7 +44,7 @@ class SessionRequestTestCase(TestCase):
|
|||
meeting = MeetingFactory(type_id='ietf', date=date_today())
|
||||
SessionFactory.create_batch(2, meeting=meeting, status_id='sched')
|
||||
SessionFactory.create_batch(2, meeting=meeting, status_id='disappr')
|
||||
# An additional unscheduled group comes from make_immutable_base_data
|
||||
# Several unscheduled groups come from make_immutable_base_data
|
||||
url = reverse('ietf.secr.sreq.views.main')
|
||||
self.client.login(username="secretary", password="secretary+password")
|
||||
r = self.client.get(url)
|
||||
|
@ -52,7 +52,7 @@ class SessionRequestTestCase(TestCase):
|
|||
sched = r.context['scheduled_groups']
|
||||
self.assertEqual(len(sched), 2)
|
||||
unsched = r.context['unscheduled_groups']
|
||||
self.assertEqual(len(unsched), 11)
|
||||
self.assertEqual(len(unsched), 12)
|
||||
|
||||
def test_approve(self):
|
||||
meeting = MeetingFactory(type_id='ietf', date=date_today())
|
||||
|
|
|
@ -647,12 +647,9 @@ $(function () {
|
|||
|
||||
function updateTimeSlotDurationViolations() {
|
||||
timeslots.each(function () {
|
||||
let total = 0;
|
||||
jQuery(this).find(".session").each(function () {
|
||||
total += +jQuery(this).data("duration");
|
||||
});
|
||||
|
||||
jQuery(this).toggleClass("overfull", total > +jQuery(this).data("duration"));
|
||||
const sessionsInSlot = Array.from(this.getElementsByClassName('session'));
|
||||
const requiredDuration = Math.max(sessionsInSlot.map(elt => Number(elt.dataset.duration)));
|
||||
this.classList.toggle('overfull', requiredDuration > Number(this.dataset.duration));
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ When responding, please keep the subject line intact and reply to all
|
|||
email addresses included in the To and CC lines. (Feel free to cut this
|
||||
introductory paragraph, however.)
|
||||
|
||||
{% if doc.type_id == "draft" and doc.stream_id != "irtf" %}
|
||||
{% if doc.type_id == "draft" and doc.stream_id == "ietf" %}
|
||||
Please refer to https://www.ietf.org/about/groups/iesg/statements/handling-ballot-positions/
|
||||
for more information about how to handle DISCUSS and COMMENT positions.
|
||||
{% endif %}
|
||||
|
|
23
ietf/templates/doc/ballot/rsab_ballot_approve.html
Normal file
23
ietf/templates/doc/ballot/rsab_ballot_approve.html
Normal file
|
@ -0,0 +1,23 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2022, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
{% load static %}
|
||||
{% load django_bootstrap5 %}
|
||||
{% block title %}Issue ballot for {{ doc }}?{% endblock %}
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
<h1>
|
||||
Issue ballot
|
||||
<br>
|
||||
<small class="text-muted">{{ doc }}</small>
|
||||
</h1>
|
||||
<p class="mt-3">
|
||||
{{ question }}
|
||||
</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{# curly percent bootstrap_form approval_text_form curly percent #}
|
||||
<button type="submit" class="btn btn-primary" name="rsab_button" value="Yes">Issue ballot</button>
|
||||
<button type="submit" class="btn btn-secondary float-end" name="rsab_button" value="No">Back</button>
|
||||
</form>
|
||||
{% endblock %}
|
22
ietf/templates/doc/ballot/rsab_ballot_close.html
Normal file
22
ietf/templates/doc/ballot/rsab_ballot_close.html
Normal file
|
@ -0,0 +1,22 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2022, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
{% load django_bootstrap5 %}
|
||||
{% block title %}Close ballot for {{ doc }}{% endblock %}
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
<h1>
|
||||
Close ballot
|
||||
<br>
|
||||
<small class="text-muted">{{ doc }}</small>
|
||||
</h1>
|
||||
<p>
|
||||
{{ question }}
|
||||
</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{# curly percent bootstrap_form approval_text_form curly percent #}
|
||||
<button type="submit" class="btn btn-primary" name="rsab_button" value="Yes">Yes</button>
|
||||
<button type="submit" class="btn btn-primary" name="rsab_button" value="No">No</button>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -1,4 +1,4 @@
|
|||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||
{# Copyright The IETF Trust 2015-2022, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
{% origin %}
|
||||
{% load ietf_filters %}
|
||||
|
@ -24,7 +24,7 @@
|
|||
<div class="modal-body">{{ ballot_content }}</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
{% if editable and user|has_role:"Area Director,Secretariat,IRSG Member" %}
|
||||
{% if editable and user|has_role:"Area Director,Secretariat,IRSG Member,RSAB Member" %}
|
||||
{% if user|can_ballot:doc %}
|
||||
<a class="btn btn-primary"
|
||||
href="{% url "ietf.doc.views_ballot.edit_position" name=doc.name ballot_id=ballot_id %}">
|
||||
|
@ -39,7 +39,7 @@
|
|||
<a class="btn btn-warning"
|
||||
href="{% url 'ietf.doc.views_ballot.defer_ballot' name=doc.name %}">Defer ballot</a>
|
||||
{% endif %}
|
||||
{% if user|has_role:"Secretariat" and ballot_type_slug != "irsg-approve" %}
|
||||
{% if user|has_role:"Secretariat" %}
|
||||
<a class="btn btn-danger"
|
||||
href="{% url 'ietf.doc.views_ballot.clear_ballot' name=doc.name ballot_type_slug=ballot_type_slug %}">
|
||||
Clear ballot
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||
{# Copyright The IETF Trust 2015-2022, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
{% origin %}
|
||||
{% load ietf_filters %}
|
||||
|
@ -53,9 +53,9 @@
|
|||
<b>Ballot question:</b> "{{ ballot.ballot_type.question }}"
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if editable and user|has_role:"Area Director,Secretariat,IRSG Member" %}
|
||||
{% if editable and user|has_role:"Area Director,Secretariat,IRSG Member,RSAB Member" %}
|
||||
<a class="btn btn-primary my-3"
|
||||
href="https://mailarchive.ietf.org/arch/search/?q=subject:{{ doc.name }}+AND+subject:(discuss+OR+comment+OR+review)">
|
||||
href="https://mailarchive.ietf.org/arch/search/?q=subject:{{ doc.name }}+AND+subject:(discuss+OR+comment+OR+review+OR+concern)">
|
||||
Search Mailarchive
|
||||
</a>
|
||||
{% if user|can_ballot:doc %}
|
||||
|
@ -64,22 +64,22 @@
|
|||
Edit position
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if doc.type_id == "draft" or doc.type_id == "conflrev" or doc.type_id == "statchg" %}
|
||||
{% if user|can_defer:doc %}
|
||||
{% if deferred %}
|
||||
<a class="btn btn-warning"
|
||||
href="{% url 'ietf.doc.views_ballot.undefer_ballot' name=doc.name %}">Undefer ballot</a>
|
||||
href="{% url 'ietf.doc.views_ballot.undefer_ballot' name=doc.name %}">Undefer ballot</a>
|
||||
{% else %}
|
||||
{% if doc.telechat_date %}
|
||||
<a class="btn btn-warning"
|
||||
href="{% url 'ietf.doc.views_ballot.defer_ballot' name=doc.name %}">Defer ballot</a>
|
||||
href="{% url 'ietf.doc.views_ballot.defer_ballot' name=doc.name %}">Defer ballot</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if user|has_role:"Area Director,Secretariat" and ballot.ballot_type.slug != "irsg-approve" %}
|
||||
{% endif %}
|
||||
{% if user|can_clear_ballot:doc %}
|
||||
<a class="btn btn-danger"
|
||||
href="{% url 'ietf.doc.views_ballot.clear_ballot' name=doc.name ballot_type_slug=ballot.ballot_type.slug %}">
|
||||
href="{% url 'ietf.doc.views_ballot.clear_ballot' name=doc.name ballot_type_slug=ballot.ballot_type.slug %}">
|
||||
Clear ballot
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% for n, positions in position_groups %}
|
||||
|
|
|
@ -12,10 +12,7 @@
|
|||
</h1>
|
||||
<p class="my-3 alert alert-info">
|
||||
You can begin managing the group state of this draft.
|
||||
</p>
|
||||
<p class="alert alert-info my-3">
|
||||
For a WG, the draft enters the IETF stream.
|
||||
For an RG, the draft enters the IRTF stream.
|
||||
The document will be moved into the stream of the adopting group.
|
||||
</p>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
|
3
ietf/templates/doc/mail/close_rsab_ballot_mail.txt
Normal file
3
ietf/templates/doc/mail/close_rsab_ballot_mail.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
{% load ietf_filters %}{% load mail_filters %}{% autoescape off %} {% filter wordwrap:78 %}The RSAB ballot for {{ doc.file_tag }} has been closed. The evaluation for this document can be found at {{ doc_url }}{% endfilter %}
|
||||
|
||||
{% endautoescape%}
|
6
ietf/templates/doc/mail/issue_rsab_ballot_mail.txt
Normal file
6
ietf/templates/doc/mail/issue_rsab_ballot_mail.txt
Normal file
|
@ -0,0 +1,6 @@
|
|||
{% load ietf_filters %}{% load mail_filters %}{% autoescape off %}{% filter wordwrap:78 %}Evaluation for {{ doc.file_tag }} can be found at {{ doc_url }}
|
||||
|
||||
{% endfilter %}
|
||||
{% filter wordwrap:78 %}{{ needed_ballot_positions }}{% endfilter %}
|
||||
|
||||
{% endautoescape%}
|
38
ietf/templates/doc/rsab_ballot_status.html
Executable file
38
ietf/templates/doc/rsab_ballot_status.html
Executable file
|
@ -0,0 +1,38 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2023, All Rights Reserved #}
|
||||
{% load origin static %}
|
||||
{% load ballot_icon %}
|
||||
{% load ietf_filters %}
|
||||
{% block pagehead %}
|
||||
<link rel="stylesheet" href="{% static "ietf/css/list.css" %}">
|
||||
{% endblock %}
|
||||
{% block title %}RSAB ballot status{% endblock %}
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
<h1>RSAB ballot status</h1>
|
||||
{% if docs %}
|
||||
<table class="table table-sm table-striped tablesorter">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" data-sort="doc">Document</th>
|
||||
<th scope="col" data-sort="status">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for doc in docs %}
|
||||
<tr>
|
||||
<td>{{ doc.displayname_with_link }}</td>
|
||||
{% include "doc/search/status_columns.html" %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="alert alert-info my-3">
|
||||
No open RSAB ballots.
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block js %}
|
||||
<script src="{% static "ietf/js/list.js" %}"></script>
|
||||
{% endblock %}
|
|
@ -87,6 +87,7 @@
|
|||
{% csrf_token %}
|
||||
<button class="btn btn-warning btn-sm"
|
||||
type="submit"
|
||||
value="Mark as action taken"
|
||||
name="do_action_taken">
|
||||
Mark as action taken
|
||||
</button>
|
||||
|
|
|
@ -67,7 +67,7 @@ urlpatterns = [
|
|||
url(r'^submit/', include('ietf.submit.urls')),
|
||||
url(r'^sync/', include('ietf.sync.urls')),
|
||||
url(r'^templates/', include('ietf.dbtemplate.urls')),
|
||||
url(r'^(?P<group_type>(wg|rg|ag|rag|team|dir|review|area|program|iabasg|adhoc|ise|adm|rfcedtyp))/', include(grouptype_urls)),
|
||||
url(r'^(?P<group_type>(wg|rg|ag|rag|team|dir|review|area|program|iabasg|adhoc|ise|adm|rfcedtyp|edwg|edappr))/', include(grouptype_urls)),
|
||||
|
||||
# Redirects
|
||||
url(r'^(?P<path>public)/', include('ietf.redirects.urls')),
|
||||
|
|
|
@ -72,6 +72,10 @@ def make_immutable_base_data():
|
|||
irtf = create_group(name="IRTF", acronym="irtf", type_id="irtf")
|
||||
create_person(irtf, "chair")
|
||||
|
||||
rsab = create_group(name="RSAB", acronym="rsab", type_id="rfcedtyp")
|
||||
p = create_person(rsab, "chair")
|
||||
p.role_set.create(group=rsab, name_id="member", email=p.email())
|
||||
|
||||
secretariat = create_group(name="IETF Secretariat", acronym="secretariat", type_id="ietf")
|
||||
create_person(secretariat, "secr", name="Sec Retary", username="secretary", is_staff=True, is_superuser=True)
|
||||
|
||||
|
|
Loading…
Reference in a new issue