12/13 merged into 6.113.1.dev0 with migration ordering failure

- Legacy-Id: 17149
This commit is contained in:
Peter E. Yee 2019-12-13 19:53:45 +00:00
parent cbe8da6a71
commit bb7e504d14
52 changed files with 15567 additions and 14357 deletions

View file

@ -12,7 +12,7 @@ from .models import (StateType, State, RelatedDocument, DocumentAuthor, Document
StateDocEvent, ConsensusDocEvent, BallotType, BallotDocEvent, WriteupDocEvent, LastCallDocEvent,
TelechatDocEvent, BallotPositionDocEvent, ReviewRequestDocEvent, InitialReviewDocEvent,
AddedMessageEvent, SubmissionDocEvent, DeletedEvent, EditedAuthorsDocEvent, DocumentURL,
ReviewAssignmentDocEvent, IanaExpertDocEvent )
ReviewAssignmentDocEvent, IanaExpertDocEvent, IRSGBallotDocEvent )
class StateTypeAdmin(admin.ModelAdmin):
@ -164,8 +164,12 @@ class DeletedEventAdmin(admin.ModelAdmin):
admin.site.register(DeletedEvent, DeletedEventAdmin)
class BallotPositionDocEventAdmin(DocEventAdmin):
raw_id_fields = ["doc", "by", "ad", "ballot"]
raw_id_fields = ["doc", "by", "pos_by", "ballot"]
admin.site.register(BallotPositionDocEvent, BallotPositionDocEventAdmin)
class IRSGBallotDocEventAdmin(DocEventAdmin):
raw_id_fields = ["doc", "by"]
admin.site.register(IRSGBallotDocEvent, IRSGBallotDocEventAdmin)
class DocumentUrlAdmin(admin.ModelAdmin):
list_display = ['id', 'doc', 'tag', 'url', 'desc', ]

View file

@ -14,7 +14,7 @@ if six.PY3:
from django.conf import settings
from ietf.doc.models import Document, DocEvent, NewRevisionDocEvent, DocAlias, State, DocumentAuthor, StateDocEvent, BallotPositionDocEvent, BallotDocEvent, BallotType
from ietf.doc.models import Document, DocEvent, NewRevisionDocEvent, DocAlias, State, DocumentAuthor, StateDocEvent, BallotPositionDocEvent, BallotDocEvent, BallotType, IRSGBallotDocEvent
from ietf.group.models import Group
def draft_name_generator(type_id,group,n):
@ -192,6 +192,26 @@ class RgDraftFactory(BaseDocumentFactory):
obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists'))
class RgRfcFactory(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-irtf'):
obj.set_state(State.objects.get(type_id='draft-stream-irtf', slug='pub'))
else:
obj.set_state(State.objects.get(type_id='draft',slug='rfc'))
obj.set_state(State.objects.get(type_id='draft-stream-irtf', slug='pub'))
class CharterFactory(BaseDocumentFactory):
type_id = 'charter'
@ -296,9 +316,10 @@ class StateDocEventFactory(DocEventFactory):
class BallotTypeFactory(factory.DjangoModelFactory):
class Meta:
model = BallotType
django_get_or_create = ('slug','doc_type_id')
doc_type_id = 'draft'
slug = 'approve'
doc_type_id = 'draft'
class BallotDocEventFactory(DocEventFactory):
@ -308,6 +329,13 @@ class BallotDocEventFactory(DocEventFactory):
ballot_type = factory.SubFactory(BallotTypeFactory)
type = 'created_ballot'
class IRSGBallotDocEventFactory(BallotDocEventFactory):
class Meta:
model = IRSGBallotDocEvent
duedate = datetime.datetime.now() + datetime.timedelta(days=14)
ballot_type = factory.SubFactory(BallotTypeFactory, slug='irsg-approve')
class BallotPositionDocEventFactory(DocEventFactory):
class Meta:
model = BallotPositionDocEvent
@ -319,6 +347,6 @@ class BallotPositionDocEventFactory(DocEventFactory):
# separately and passing the same doc into thier factories.
ballot = factory.SubFactory(BallotDocEventFactory)
ad = factory.SubFactory('ietf.person.factories.PersonFactory')
pos_by = factory.SubFactory('ietf.person.factories.PersonFactory')
pos_id = 'discuss'

View file

@ -382,7 +382,7 @@ def generate_issue_ballot_mail(request, doc, ballot):
last_call_has_expired=last_call_has_expired,
needed_ballot_positions=
needed_ballot_positions(doc,
list(doc.active_ballot().active_ad_positions().values())
list(doc.active_ballot().active_balloteer_positions().values())
),
)
)

View file

@ -0,0 +1,62 @@
# Copyright The IETF Trust 2019, All Rights Reserved
# -*- coding: utf-8 -*-
# Generated by Django 1.11.22 on 2019-08-03 10:09
from __future__ import unicode_literals
from django.db import migrations
# forward, reverse initially copied from migration 0004
def forward(apps, schema_editor):
State = apps.get_model('doc','State')
State.objects.create(type_id='draft-stream-irtf',
slug='irsg_review',
name='IRSG Review',
desc='IRSG Review',
used=True,
)
BallotPositionName = apps.get_model('name','BallotPositionName')
# desc, used, order, and blocking all have suitable defaults
BallotPositionName.objects.create(slug="moretime",
name="Need More Time",
)
BallotPositionName.objects.create(slug="notready",
name="Not Ready",
)
# Create a new ballot type for IRSG ballot
# include positions for the ballot type
BallotType = apps.get_model('doc','BallotType')
bt = BallotType.objects.create(doc_type_id="draft",
slug="irsg-approve",
name="IRSG Approve",
question="Is this draft ready for publication in the IRTF stream?",
)
bt.positions.set(['yes','noobj','recuse','notready','moretime'])
def reverse(apps, schema_editor):
State = apps.get_model('doc','State')
State.objects.filter(type_id__in=('draft-stream-irtf',), slug='irsg_review').delete()
Position = apps.get_model('name','BallotPositionName')
for pos in ("moretime", "notready"):
Position.objects.filter(slug=pos).delete()
IRSGBallot = apps.get_model('doc','BallotType')
IRSGBallot.objects.filter(slug="irsg-approve").delete()
class Migration(migrations.Migration):
dependencies = [
('doc', '0026_add_draft_rfceditor_state'),
('name', '0007_fix_m2m_slug_id_length'),
]
operations = [
migrations.RunPython(forward,reverse),
migrations.RenameField(
model_name='ballotpositiondocevent',
old_name='ad',
new_name='pos_by',
),
]

View file

@ -0,0 +1,25 @@
# Copyright The IETF Trust 2019, All Rights Reserved
# -*- coding: utf-8 -*-
# Generated by Django 1.11.25 on 2019-10-10 10:37
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('doc', '0027_add_irsg_doc_positions'),
]
operations = [
migrations.CreateModel(
name='IRSGBallotDocEvent',
fields=[
('ballotdocevent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='doc.BallotDocEvent')),
('duedate', models.DateTimeField(blank=True, null=True)),
],
bases=('doc.ballotdocevent',),
),
]

View file

@ -27,7 +27,7 @@ from ietf.name.models import ( DocTypeName, DocTagName, StreamName, IntendedStdL
DocRelationshipName, DocReminderTypeName, BallotPositionName, ReviewRequestStateName, ReviewAssignmentStateName, FormalLanguageName,
DocUrlTagName)
from ietf.person.models import Email, Person
from ietf.person.utils import get_active_ads
from ietf.person.utils import get_active_balloteers
from ietf.utils import log
from ietf.utils.admin import admin_link
from ietf.utils.decorators import memoize
@ -656,7 +656,7 @@ class Document(DocumentInfo):
def latest_event(self, *args, **filter_args):
"""Get latest event of optional Python type and with filter
arguments, e.g. d.latest_event(type="xyz") returns an DocEvent
arguments, e.g. d.latest_event(type="xyz") returns a DocEvent
while d.latest_event(WriteupDocEvent, type="xyz") returns a
WriteupDocEvent event."""
model = args[0] if args else DocEvent
@ -1082,20 +1082,20 @@ class BallotType(models.Model):
class BallotDocEvent(DocEvent):
ballot_type = ForeignKey(BallotType)
def active_ad_positions(self):
"""Return dict mapping each active AD to a current ballot position (or None if they haven't voted)."""
def active_balloteer_positions(self):
"""Return dict mapping each active AD or IRSG member to a current ballot position (or None if they haven't voted)."""
res = {}
active_ads = get_active_ads()
positions = BallotPositionDocEvent.objects.filter(type="changed_ballot_position",ad__in=active_ads, ballot=self).select_related('ad', 'pos').order_by("-time", "-id")
active_balloteers = get_active_balloteers(self.ballot_type)
positions = BallotPositionDocEvent.objects.filter(type="changed_ballot_position",pos_by__in=active_balloteers, ballot=self).select_related('pos_by', 'pos').order_by("-time", "-id")
for pos in positions:
if pos.ad not in res:
res[pos.ad] = pos
if pos.pos_by not in res:
res[pos.pos_by] = pos
for ad in active_ads:
if ad not in res:
res[ad] = None
for balloteer in active_balloteers:
if balloteer not in res:
res[balloteer] = None
return res
def all_positions(self):
@ -1103,15 +1103,15 @@ class BallotDocEvent(DocEvent):
positions = []
seen = {}
active_ads = get_active_ads()
for e in BallotPositionDocEvent.objects.filter(type="changed_ballot_position", ballot=self).select_related('ad', 'pos').order_by("-time", '-id'):
if e.ad not in seen:
e.old_ad = e.ad not in active_ads
active_balloteers = get_active_balloteers(self.ballot_type)
for e in BallotPositionDocEvent.objects.filter(type="changed_ballot_position", ballot=self).select_related('pos_by', 'pos').order_by("-time", '-id'):
if e.pos_by not in seen:
e.old_pos_by = e.pos_by not in active_balloteers
e.old_positions = []
positions.append(e)
seen[e.ad] = e
seen[e.pos_by] = e
else:
latest = seen[e.ad]
latest = seen[e.pos_by]
if latest.old_positions:
prev = latest.old_positions[-1]
else:
@ -1126,25 +1126,27 @@ class BallotDocEvent(DocEvent):
while p.old_positions and p.old_positions[-1].slug == "norecord":
p.old_positions.pop()
# add any missing ADs through fake No Record events
# add any missing ADs/IRSGers through fake No Record events
if self.doc.active_ballot() == self:
norecord = BallotPositionName.objects.get(slug="norecord")
for ad in active_ads:
if ad not in seen:
e = BallotPositionDocEvent(type="changed_ballot_position", doc=self.doc, rev=self.doc.rev, ad=ad)
e.by = ad
for balloteer in active_balloteers:
if balloteer not in seen:
e = BallotPositionDocEvent(type="changed_ballot_position", doc=self.doc, rev=self.doc.rev, pos_by=balloteer)
e.by = balloteer
e.pos = norecord
e.old_ad = False
e.old_pos_by = False
e.old_positions = []
positions.append(e)
positions.sort(key=lambda p: (p.old_ad, p.ad.last_name()))
positions.sort(key=lambda p: (p.old_pos_by, p.pos_by.last_name()))
return positions
class IRSGBallotDocEvent(BallotDocEvent):
duedate = models.DateTimeField(blank=True, null=True)
class BallotPositionDocEvent(DocEvent):
ballot = ForeignKey(BallotDocEvent, null=True, default=None) # default=None is a temporary migration period fix, should be removed when charter branch is live
ad = ForeignKey(Person)
pos_by = ForeignKey(Person)
pos = ForeignKey(BallotPositionName, verbose_name="position", default="norecord")
discuss = models.TextField(help_text="Discuss text if position is discuss", blank=True)
discuss_time = models.DateTimeField(help_text="Time discuss text was written", blank=True, null=True)

View file

@ -17,7 +17,7 @@ from ietf.doc.models import (BallotType, DeletedEvent, StateType, State, Documen
InitialReviewDocEvent, DocHistoryAuthor, BallotDocEvent, RelatedDocument,
RelatedDocHistory, BallotPositionDocEvent, AddedMessageEvent, SubmissionDocEvent,
ReviewRequestDocEvent, ReviewAssignmentDocEvent, EditedAuthorsDocEvent, DocumentURL,
IanaExpertDocEvent )
IanaExpertDocEvent, IRSGBallotDocEvent )
from ietf.name.resources import BallotPositionNameResource, DocTypeNameResource
class BallotTypeResource(ModelResource):
@ -531,7 +531,7 @@ class BallotPositionDocEventResource(ModelResource):
doc = ToOneField(DocumentResource, 'doc')
docevent_ptr = ToOneField(DocEventResource, 'docevent_ptr')
ballot = ToOneField(BallotDocEventResource, 'ballot', null=True)
ad = ToOneField(PersonResource, 'ad')
pos_by = ToOneField(PersonResource, 'pos_by')
pos = ToOneField(BallotPositionNameResource, 'pos')
class Meta:
cache = SimpleCache()
@ -553,7 +553,7 @@ class BallotPositionDocEventResource(ModelResource):
"doc": ALL_WITH_RELATIONS,
"docevent_ptr": ALL_WITH_RELATIONS,
"ballot": ALL_WITH_RELATIONS,
"ad": ALL_WITH_RELATIONS,
"pos_by": ALL_WITH_RELATIONS,
"pos": ALL_WITH_RELATIONS,
}
api.doc.register(BallotPositionDocEventResource())
@ -738,3 +738,32 @@ class IanaExpertDocEventResource(ModelResource):
"docevent_ptr": ALL_WITH_RELATIONS,
}
api.doc.register(IanaExpertDocEventResource())
from ietf.person.resources import PersonResource
class IRSGBallotDocEventResource(ModelResource):
by = ToOneField(PersonResource, 'by')
doc = ToOneField(DocumentResource, 'doc')
docevent_ptr = ToOneField(DocEventResource, 'docevent_ptr')
ballot_type = ToOneField(BallotTypeResource, 'ballot_type')
ballotdocevent_ptr = ToOneField(BallotDocEventResource, 'ballotdocevent_ptr')
class Meta:
queryset = IRSGBallotDocEvent.objects.all()
serializer = api.Serializer()
cache = SimpleCache()
#resource_name = 'irsgballotdocevent'
ordering = ['ballotdocevent_ptr', ]
filtering = {
"id": ALL,
"time": ALL,
"type": ALL,
"rev": ALL,
"desc": ALL,
"duedate": ALL,
"by": ALL_WITH_RELATIONS,
"doc": ALL_WITH_RELATIONS,
"docevent_ptr": ALL_WITH_RELATIONS,
"ballot_type": ALL_WITH_RELATIONS,
"ballotdocevent_ptr": ALL_WITH_RELATIONS,
}
api.doc.register(IRSGBallotDocEventResource())

View file

@ -1,4 +1,4 @@
# Copyright The IETF Trust 2019, All rights reserved.
# Copyright The IETF Trust 2019, All Rights Reserved
# Copyright (C) 2009-2010 Nokia Corporation and/or its subsidiary(-ies).
# All rights reserved. Contact: Pasi Eronen <pasi.eronen@nokia.com>
#
@ -91,7 +91,7 @@ def ballot_icon(context, doc):
else:
return (1, pos.pos.order)
positions = list(ballot.active_ad_positions().items())
positions = list(ballot.active_balloteer_positions().items())
positions.sort(key=sort_key)
right_click_string = ''
@ -99,8 +99,8 @@ def ballot_icon(context, doc):
right_click_string = 'oncontextmenu="window.location.href=\'%s\';return false;"' % urlreverse('ietf.doc.views_ballot.edit_position', kwargs=dict(name=doc.name, ballot_id=ballot.pk))
my_blocking = False
for i, (ad, pos) in enumerate(positions):
if user_is_person(user,ad) and pos and pos.pos.blocking:
for i, (pos_by, pos) in enumerate(positions):
if user_is_person(user,pos_by) and pos and pos.pos.blocking:
my_blocking = True
break
@ -153,7 +153,7 @@ def ballotposition(doc, user):
if not ballot:
return None
changed_pos = doc.latest_event(BallotPositionDocEvent, type="changed_ballot_position", ad__user=user, ballot=ballot)
changed_pos = doc.latest_event(BallotPositionDocEvent, type="changed_ballot_position", pos_by__user=user, ballot=ballot)
if changed_pos:
pos = changed_pos.pos
else:

View file

@ -25,6 +25,7 @@ import debug # pyflakes:ignore
from ietf.doc.models import ConsensusDocEvent
from ietf.utils.text import wordwrap, fill, wrap_text_if_unwrapped
from ietf.utils.html import sanitize_fragment
from ietf.doc.models import BallotDocEvent
register = template.Library()
@ -539,3 +540,20 @@ def charter_major_rev(rev):
@stringfilter
def charter_minor_rev(rev):
return rev[3:5]
@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'):
return True
else:
return False
@register.filter()
def can_ballot(user,doc):
if doc.stream_id == 'ietf' and user.person.role_set.filter(name="ad", group__type="area", group__state="active"):
return True
elif doc.stream_id == 'irtf' and has_role(user,'IRSG Member'):
return True
else:
return False

View file

@ -240,7 +240,7 @@ class SearchTests(TestCase):
ballot_type = BallotType.objects.get(doc_type_id='draft',slug='approve')
ballot = BallotDocEventFactory(ballot_type=ballot_type, doc__states=[('draft-iesg','iesg-eva')])
discuss_pos = BallotPositionName.objects.get(slug='discuss')
discuss_other = BallotPositionDocEventFactory(ballot=ballot, doc=ballot.doc, ad=ad, pos=discuss_pos)
discuss_other = BallotPositionDocEventFactory(ballot=ballot, doc=ballot.doc, pos_by=ad, pos=discuss_pos)
r = self.client.get(urlreverse('ietf.doc.views_search.docs_for_ad', kwargs=dict(name=ad.full_name_as_key())))
self.assertEqual(r.status_code, 200)
@ -740,7 +740,7 @@ class DocTestCase(TestCase):
pos_id="yes",
comment="Looks fine to me",
comment_time=datetime.datetime.now(),
ad=Person.objects.get(user__username="ad"),
pos_by=Person.objects.get(user__username="ad"),
by=Person.objects.get(name="(System)"))
r = self.client.get(urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=doc.name)))
@ -1103,7 +1103,7 @@ expand-draft-ietf-ames-test.all@virtual.ietf.org ames-author@example.ames, ames
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertContains(r, 'draft-ietf-mars-test.all@ietf.org')
self.assertContains(r, 'ballot_saved')
self.assertContains(r, 'iesg_ballot_saved')
class DocumentMeetingTests(TestCase):

View file

@ -1,4 +1,4 @@
# Copyright The IETF Trust 2013-2019, All Rights Reserved
#ad Copyright The IETF Trust 2013-2019, All Rights Reserved
# -*- coding: utf-8 -*-
@ -28,7 +28,7 @@ from ietf.utils.text import unwrap
class EditPositionTests(TestCase):
def test_edit_position(self):
ad = Person.objects.get(user__username="ad")
draft = IndividualDraftFactory(ad=ad)
draft = IndividualDraftFactory(ad=ad,stream_id='ietf')
ballot = create_ballot_if_not_open(None, draft, ad, 'approve')
url = urlreverse('ietf.doc.views_ballot.edit_position', kwargs=dict(name=draft.name,
ballot_id=ballot.pk))
@ -51,7 +51,7 @@ class EditPositionTests(TestCase):
comment=" This is a test. \n "))
self.assertEqual(r.status_code, 302)
pos = draft.latest_event(BallotPositionDocEvent, ad=ad)
pos = draft.latest_event(BallotPositionDocEvent, pos_by=ad)
self.assertEqual(pos.pos.slug, "discuss")
self.assertTrue(" This is a discussion test." in pos.discuss)
self.assertTrue(pos.discuss_time != None)
@ -66,7 +66,7 @@ class EditPositionTests(TestCase):
self.assertEqual(r.status_code, 302)
draft = Document.objects.get(name=draft.name)
pos = draft.latest_event(BallotPositionDocEvent, ad=ad)
pos = draft.latest_event(BallotPositionDocEvent, pos_by=ad)
self.assertEqual(pos.pos.slug, "noobj")
self.assertEqual(draft.docevent_set.count(), events_before + 1)
self.assertTrue("Position for" in pos.desc)
@ -77,7 +77,7 @@ class EditPositionTests(TestCase):
self.assertEqual(r.status_code, 302)
draft = Document.objects.get(name=draft.name)
pos = draft.latest_event(BallotPositionDocEvent, ad=ad)
pos = draft.latest_event(BallotPositionDocEvent, pos_by=ad)
self.assertEqual(pos.pos.slug, "norecord")
self.assertEqual(draft.docevent_set.count(), events_before + 1)
self.assertTrue("Position for" in pos.desc)
@ -88,7 +88,7 @@ class EditPositionTests(TestCase):
self.assertEqual(r.status_code, 302)
draft = Document.objects.get(name=draft.name)
pos = draft.latest_event(BallotPositionDocEvent, ad=ad)
pos = draft.latest_event(BallotPositionDocEvent, pos_by=ad)
self.assertEqual(pos.pos.slug, "norecord")
self.assertEqual(draft.docevent_set.count(), events_before + 2)
self.assertTrue("Ballot comment text updated" in pos.desc)
@ -115,7 +115,7 @@ class EditPositionTests(TestCase):
)
self.assertContains(r, "Done")
pos = draft.latest_event(BallotPositionDocEvent, ad=ad)
pos = draft.latest_event(BallotPositionDocEvent, pos_by=ad)
self.assertEqual(pos.pos.slug, "discuss")
self.assertTrue(" This is a discussion test." in pos.discuss)
self.assertTrue(pos.discuss_time != None)
@ -132,7 +132,7 @@ class EditPositionTests(TestCase):
self.assertEqual(r.status_code, 200)
draft = Document.objects.get(name=draft.name)
pos = draft.latest_event(BallotPositionDocEvent, ad=ad)
pos = draft.latest_event(BallotPositionDocEvent, pos_by=ad)
self.assertEqual(pos.pos.slug, "noobj")
self.assertEqual(draft.docevent_set.count(), events_before + 1)
self.assertTrue("Position for" in pos.desc)
@ -150,7 +150,7 @@ class EditPositionTests(TestCase):
self.assertEqual(r.status_code, 200)
draft = Document.objects.get(name=draft.name)
pos = draft.latest_event(BallotPositionDocEvent, ad=ad)
pos = draft.latest_event(BallotPositionDocEvent, pos_by=ad)
self.assertEqual(pos.pos.slug, "norecord")
self.assertEqual(draft.docevent_set.count(), events_before + 1)
self.assertTrue("Position for" in pos.desc)
@ -165,7 +165,7 @@ class EditPositionTests(TestCase):
self.assertEqual(r.status_code, 200)
draft = Document.objects.get(name=draft.name)
pos = draft.latest_event(BallotPositionDocEvent, ad=ad)
pos = draft.latest_event(BallotPositionDocEvent, pos_by=ad)
self.assertEqual(pos.pos.slug, "norecord")
self.assertEqual(draft.docevent_set.count(), events_before + 2)
self.assertTrue("Ballot comment text updated" in pos.desc)
@ -181,7 +181,7 @@ class EditPositionTests(TestCase):
ballot = create_ballot_if_not_open(None, draft, ad, 'approve')
url = urlreverse('ietf.doc.views_ballot.edit_position', kwargs=dict(name=draft.name, ballot_id=ballot.pk))
ad = Person.objects.get(name="Areað Irector")
url += "?ad=%s" % ad.pk
url += "?pos_by=%s" % ad.pk
login_testing_unauthorized(self, "secretary", url)
# normal get
@ -195,7 +195,7 @@ class EditPositionTests(TestCase):
r = self.client.post(url, dict(position="discuss", discuss="Test discuss text"))
self.assertEqual(r.status_code, 302)
pos = draft.latest_event(BallotPositionDocEvent, ad=ad)
pos = draft.latest_event(BallotPositionDocEvent, pos_by=ad)
self.assertEqual(pos.pos.slug, "discuss")
self.assertEqual(pos.discuss, "Test discuss text")
self.assertTrue("New position" in pos.desc)
@ -229,7 +229,7 @@ class EditPositionTests(TestCase):
BallotPositionDocEvent.objects.create(
doc=draft, rev=draft.rev, type="changed_ballot_position",
by=ad, ad=ad, ballot=ballot, pos=BallotPositionName.objects.get(slug="discuss"),
by=ad, pos_by=ad, ballot=ballot, pos=BallotPositionName.objects.get(slug="discuss"),
discuss="This draft seems to be lacking a clearer title?",
discuss_time=datetime.datetime.now(),
comment="Test!",

View file

@ -0,0 +1,529 @@
# Copyright The IETF Trust 2019, All Rights Reserved
# -*- coding: utf-8 -*-
# import datetime
# from pyquery import PyQuery
import debug # pyflakes:ignore
import datetime
from django.urls import reverse as urlreverse
from ietf.utils.mail import outbox, empty_outbox, get_payload
from ietf.utils.test_utils import TestCase, unicontent, login_testing_unauthorized
from ietf.doc.factories import IndividualDraftFactory, WgDraftFactory, RgDraftFactory, RgRfcFactory, BallotDocEventFactory, IRSGBallotDocEventFactory, BallotPositionDocEventFactory
from ietf.doc.models import BallotDocEvent, BallotPositionDocEvent
from ietf.doc.utils import create_ballot_if_not_open, close_ballot
from ietf.person.utils import get_active_irsg, get_active_ads
from ietf.group.factories import RoleFactory
from ietf.person.models import Person
class IssueIRSGBallotTests(TestCase):
def test_issue_ballot_button(self):
# creates empty drafts with lots of values filled in
individual_draft = IndividualDraftFactory()
wg_draft = WgDraftFactory()
rg_draft = RgDraftFactory()
rg_rfc = RgRfcFactory()
# login as an IRTF chair
self.client.login(username='irtf-chair', password='irtf-chair+password')
url = urlreverse('ietf.doc.views_doc.document_main',kwargs=dict(name=individual_draft.name))
r = self.client.get(url)
self.assertEqual(r.status_code,200)
self.assertNotIn("Issue IRSG ballot", unicontent(r))
url = urlreverse('ietf.doc.views_doc.document_main',kwargs=dict(name=wg_draft.name))
r = self.client.get(url)
self.assertEqual(r.status_code,200)
self.assertNotIn("Issue IRSG ballot", unicontent(r))
url = urlreverse('ietf.doc.views_doc.document_main',kwargs=dict(name=rg_draft.name))
r = self.client.get(url)
self.assertEqual(r.status_code,200)
self.assertIn("Issue IRSG ballot", unicontent(r))
url = urlreverse('ietf.doc.views_doc.document_main',kwargs=dict(name=rg_rfc.name))
r = self.client.get(url, follow = True)
self.assertEqual(r.status_code,200)
self.assertNotIn("Issue IRSG ballot", unicontent(r))
self.client.logout()
url = urlreverse('ietf.doc.views_doc.document_main',kwargs=dict(name=rg_draft.name))
r = self.client.get(url)
self.assertEqual(r.status_code,200)
self.assertNotIn("Issue IRSG ballot", unicontent(r))
def test_close_ballot_button(self):
# creates empty drafts with lots of values filled in
rg_draft1 = RgDraftFactory()
rg_draft2 = RgDraftFactory()
rg_rfc = RgRfcFactory()
iesgmember = get_active_ads()[0]
# Login as the IRTF chair
self.client.login(username='irtf-chair', password='irtf-chair+password')
# Set the two IRTF ballots in motion
# Get the page with the Issue IRSG Ballot Yes/No buttons
url = urlreverse('ietf.doc.views_ballot.issue_irsg_ballot',kwargs=dict(name=rg_draft1.name))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# Press the Yes button
r = self.client.post(url,dict(irsg_button="Yes", duedate="2038-01-19"))
self.assertEqual(r.status_code, 302)
self.assertTrue(rg_draft1.ballot_open('irsg-approve'))
# Get the page with the Issue IRSG Ballot Yes/No buttons
url = urlreverse('ietf.doc.views_ballot.issue_irsg_ballot',kwargs=dict(name=rg_draft2.name))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# Press the Yes button
r = self.client.post(url,dict(irsg_button="Yes", duedate="2038-01-18"))
self.assertEqual(r.status_code, 302)
self.assertTrue(rg_draft2.ballot_open('irsg-approve'))
# Logout - the Close button should not be available
self.client.logout()
url = urlreverse('ietf.doc.views_doc.document_main',kwargs=dict(name=rg_draft1.name))
r = self.client.get(url)
self.assertEqual(r.status_code,200)
self.assertNotIn("Close IRSG ballot", unicontent(r))
# Login as an IESG member to see if the ballot close button appears
self.client.login(username=iesgmember.user.username, password=iesgmember.user.username+"password")
r = self.client.get(url)
self.assertEqual(r.status_code,200)
self.assertNotIn("Close IRSG ballot", unicontent(r))
# Try to get the ballot closing page directly
url = urlreverse('ietf.doc.views_ballot.close_irsg_ballot',kwargs=dict(name=rg_draft1.name))
r = self.client.get(url)
self.assertNotEqual(r.status_code, 200)
self.client.logout()
# Login again as the IRTF chair
self.client.login(username='irtf-chair', password='irtf-chair+password')
# The close button should now be available
url = urlreverse('ietf.doc.views_doc.document_main',kwargs=dict(name=rg_draft1.name))
r = self.client.get(url)
self.assertEqual(r.status_code,200)
self.assertIn("Close IRSG ballot", unicontent(r))
# Get the page with the Close IRSG Ballot Yes/No buttons
url = urlreverse('ietf.doc.views_ballot.close_irsg_ballot',kwargs=dict(name=rg_draft1.name))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# Press the Yes button
r = self.client.post(url,dict(irsg_button="Yes"))
self.assertEqual(r.status_code,302)
# Expect the draft not to have an open IRSG ballot anymore
self.assertFalse(rg_draft1.ballot_open('irsg-approve'))
# Login as the Secretariat
self.client.login(username='secretary', password='secretary+password')
# The close button should now be available
url = urlreverse('ietf.doc.views_doc.document_main',kwargs=dict(name=rg_draft2.name))
r = self.client.get(url)
self.assertEqual(r.status_code,200)
self.assertIn("Close IRSG ballot", unicontent(r))
# Get the page with the Close IRSG Ballot Yes/No buttons
url = urlreverse('ietf.doc.views_ballot.close_irsg_ballot',kwargs=dict(name=rg_draft2.name))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# Press the Yes button
r = self.client.post(url,dict(irsg_button="Yes"))
self.assertEqual(r.status_code,302)
# Expect the draft not to have an open IRSG ballot anymore
self.assertFalse(rg_draft2.ballot_open('irsg-approve'))
# Individual, IETF, and RFC docs should not show the Close button. Sample test using IRTF RFC:
url = urlreverse('ietf.doc.views_doc.document_main',kwargs=dict(name=rg_rfc.name))
r = self.client.get(url, follow = True)
self.assertEqual(r.status_code,200)
self.assertNotIn("Close IRSG ballot", unicontent(r))
def test_issue_ballot(self):
# Just testing IRTF drafts
rg_draft1 = RgDraftFactory()
rg_draft2 = RgDraftFactory()
iesgmember = get_active_ads()[0]
# login as an IRTF chair (who is a user who can issue an IRSG ballot)
self.client.login(username='irtf-chair', password='irtf-chair+password')
# Get the page with the Issue IRSG Ballot Yes/No buttons
url = urlreverse('ietf.doc.views_ballot.issue_irsg_ballot',kwargs=dict(name=rg_draft1.name))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# Buttons present?
self.assertIn("irsg_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(irsg_button="No"))
self.assertEqual(r.status_code, 302)
# PEY: Insert assertion about the redirect URL
# Press the Yes button
r = self.client.post(url,dict(irsg_button="Yes", duedate="2038-01-19"))
self.assertEqual(r.status_code, 302)
# PEY: Check on whether the ballot is reflected in the BallotDocEvents table
# Can't get ballot_type to work in the filter below, so commented out for now
# ballot_type = BallotType.objects.get(doc_type=rg_draft.type,slug='irsg-approve')
# debug.show("ballot_type")
ballot_created = list(BallotDocEvent.objects.filter(doc=rg_draft1,
type="created_ballot"))
self.assertNotEqual(len(ballot_created), 0)
# Having issued a ballot, the Issue IRSG ballot button should be gone
url = urlreverse('ietf.doc.views_doc.document_main',kwargs=dict(name=rg_draft1.name))
r = self.client.get(url)
self.assertEqual(r.status_code,200)
self.assertNotIn("Issue IRSG ballot", unicontent(r))
# The IRSG evaluation record tab should exist
self.assertIn("IRSG evaluation record", unicontent(r))
# The IRSG evaluation record tab should not indicate unavailability
self.assertNotIn("IRSG Evaluation Ballot has not been created yet", unicontent(r))
# We should find an IRSG member's name on the IRSG evaluation tab regardless of any positions taken or not
url = urlreverse('ietf.doc.views_doc.document_irsg_ballot',kwargs=dict(name=rg_draft1.name))
r = self.client.get(url)
self.assertEqual(r.status_code,200)
irsgmembers = get_active_irsg()
self.assertNotEqual(len(irsgmembers), 0)
self.assertIn(irsgmembers[0].name, unicontent(r))
# Having issued a ballot, it should appear on the IRSG Ballot Status page
url = urlreverse('ietf.doc.views_ballot.irsg_ballot_status')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# Does the draft name appear on the page?
self.assertIn(rg_draft1.name, unicontent(r))
self.client.logout()
# Test that an IESG member cannot issue an IRSG ballot
self.client.login(username=iesgmember.user.username, password=iesgmember.user.username+"password")
url = urlreverse('ietf.doc.views_ballot.issue_irsg_ballot',kwargs=dict(name=rg_draft2.name))
r = self.client.get(url)
self.assertNotEqual(r.status_code, 200)
# Buttons present?
self.assertNotIn("irsg_button", unicontent(r))
# Attempt to press the Yes button anyway
r = self.client.post(url,dict(irsg_button="Yes", duedate="2038-01-19"))
self.assertTrue(r.status_code == 302 and "/accounts/login" in r['Location'])
self.client.logout()
# Test that the Secretariat can issue an IRSG ballot
self.client.login(username="secretary", password="secretary+password")
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# Buttons present?
self.assertIn("irsg_button", unicontent(r))
# Press the Yes button
r = self.client.post(url,dict(irsg_button="Yes", duedate="2038-01-19"))
self.assertEqual(r.status_code, 302)
self.client.logout()
def test_edit_ballot_position_permissions(self):
rg_draft = RgDraftFactory()
wg_draft = WgDraftFactory()
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]
secr = RoleFactory(group__acronym='secretariat',name_id='secr')
wg_ballot = create_ballot_if_not_open(None, wg_draft, ad.person, 'approve')
due = datetime.date.today()+datetime.timedelta(days=14)
rg_ballot = create_ballot_if_not_open(None, rg_draft, secr.person, 'irsg-approve', due)
url = urlreverse('ietf.doc.views_ballot.edit_position', kwargs=dict(name=wg_draft.name, ballot_id=wg_ballot.pk))
# Pre-ADs can see
login_testing_unauthorized(self, pre_ad.person.user.username, url)
# But Pre-ADs cannot take a position
r = self.client.post(url, dict(position="discuss", discuss="Test discuss text"))
self.assertEqual(r.status_code, 403)
self.client.logout()
# ADs can see and take a position
login_testing_unauthorized(self, ad.person.user.username, url)
r = self.client.post(url, dict(position="discuss", discuss="Test discuss text"))
self.assertTrue(r.status_code == 302 and "/accounts/login" not in r['Location'])
# IESG members should not be able to take positions on IRSG ballots
url = urlreverse('ietf.doc.views_ballot.edit_position', kwargs=dict(name=rg_draft.name, ballot_id=rg_ballot.pk))
r = self.client.post(url, dict(position="yes"))
self.assertEqual(r.status_code, 403)
self.client.logout()
# IRSG members should be able to enter a position on IRSG ballots
login_testing_unauthorized(self, irsgmember.user.username, url)
r = self.client.post(url, dict(position="yes"))
self.assertTrue(r.status_code == 302 and "/accounts/login" not in r['Location'])
def test_iesg_ballot_no_irsg_actions(self):
ad = Person.objects.get(user__username="ad")
wg_draft = IndividualDraftFactory(ad=ad)
irsgmember = get_active_irsg()[0]
url = urlreverse('ietf.doc.views_ballot.ballot_writeupnotes', kwargs=dict(name=wg_draft.name))
# IRSG members should not be able to issue IESG ballots
login_testing_unauthorized(self, irsgmember.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)
# But IESG members can
r = self.client.post(url, dict(
ballot_writeup="This is a test.",
issue_ballot="1"))
self.assertEqual(r.status_code, 200)
self.client.logout()
# Now that the ballot is issued, see if an IRSG member can take a position or close the ballot
ballot = wg_draft.active_ballot()
url = urlreverse('ietf.doc.views_ballot.edit_position', kwargs=dict(name=wg_draft.name, ballot_id=ballot.pk))
login_testing_unauthorized(self, irsgmember.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 = RgDraftFactory()
url = urlreverse('ietf.doc.views_ballot.issue_irsg_ballot',kwargs=dict(name=draft.name))
due = datetime.date.today()+datetime.timedelta(days=14)
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,{'irsg_button':'No', 'duedate':due })
self.assertEqual(r.status_code, 302)
self.assertIsNone(draft.ballot_open('irsg-approve'))
r = self.client.post(url,{'irsg_button':'Yes', 'duedate':due })
self.assertEqual(r.status_code,302)
self.assertIsNotNone(draft.ballot_open('irsg-approve'))
self.assertEqual(len(outbox),0)
def test_take_and_email_position(self):
draft = RgDraftFactory()
ballot = IRSGBallotDocEventFactory(doc=draft)
url = urlreverse('ietf.doc.views_ballot.edit_position', kwargs=dict(name=draft.name, ballot_id=ballot.pk)) + self.pos_by
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.pos_by
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(outbox[0]))
def test_close_ballot(self):
draft = RgDraftFactory()
IRSGBallotDocEventFactory(doc=draft)
url = urlreverse('ietf.doc.views_ballot.close_irsg_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(irsg_button='No'))
self.assertEqual(r.status_code, 302)
self.assertIsNotNone(draft.ballot_open('irsg-approve'))
r = self.client.post(url,dict(irsg_button='Yes'))
self.assertEqual(r.status_code, 302)
self.assertIsNone(draft.ballot_open('irsg-approve'))
self.assertEqual(len(outbox), 0)
def test_view_outstanding_ballots(self):
draft = RgDraftFactory()
# PEY: Commented out RJS' following line., Will need this in the future when irsg_ballot_status changes to take a ballot not a doc
# ballot = IRSGBallotDocEventFactory(doc=draft)
IRSGBallotDocEventFactory(doc=draft)
url = urlreverse('ietf.doc.views_ballot.irsg_ballot_status')
login_testing_unauthorized(self, self.username, url)
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), 'irsg-approve')
r = self.client.get(url)
self.assertNotIn(draft.name, unicontent(r))
class IRTFChairTests(BaseManipulationTests, TestCase):
def setUp(self):
self.username = 'irtf-chair'
self.pos_by = ''
class SecretariatTests(BaseManipulationTests, TestCase):
def setUp(self):
self.username = 'secretary'
self.pos_by = '?pos_by={}'.format(Person.objects.get(user__username='irtf-chair').pk)
class IRSGMemberTests(TestCase):
def setUp(self):
self.username = get_active_irsg()[0].user.username
def test_cant_issue_irsg_ballot(self):
draft = RgDraftFactory()
due = datetime.date.today()+datetime.timedelta(days=14)
url = urlreverse('ietf.doc.views_ballot.close_irsg_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,{'irsg_button':'Yes', 'duedate':due })
self.assertEqual(r.status_code, 403)
def test_cant_close_irsg_ballot(self):
draft = RgDraftFactory()
# PEY: Commented out RJS' following line. Will need this in the future when close_irsg_ballot changes to taking a ballot not a doc
# ballot = IRSGBallotDocEventFactory(doc=draft)
IRSGBallotDocEventFactory(doc=draft)
url = urlreverse('ietf.doc.views_ballot.close_irsg_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(irsg_button='Yes'))
self.assertEqual(r.status_code, 403)
def test_cant_take_position_on_iesg_ballot(self):
draft = WgDraftFactory()
ballot = BallotDocEventFactory(doc=draft)
url = urlreverse('ietf.doc.views_ballot.edit_position', kwargs=dict(name=draft.name, ballot_id=ballot.pk))
self.client.login(username = self.username, password = self.username+'+password')
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, 403)
def test_take_and_email_position(self):
draft = RgDraftFactory()
ballot = IRSGBallotDocEventFactory(doc=draft)
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 IESGMemberTests(TestCase):
def test_cant_take_position_on_irtf_ballot(self):
draft = RgDraftFactory()
ballot = IRSGBallotDocEventFactory(doc=draft)
url = urlreverse('ietf.doc.views_ballot.edit_position', kwargs=dict(name=draft.name, ballot_id=ballot.pk))
self.assertEqual(self.client.login(username = 'ad', password = 'ad+password'), True)
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, 403)
class NobodyTests(TestCase):
def can_see_IRSG_tab(self):
draft=RgDraftFactory()
ballot = IRSGBallotDocEventFactory(doc=draft)
BallotPositionDocEventFactory(ballot=ballot, by=get_active_irsg()[0], pos_id='yes', comment='b2390sn3')
url = urlreverse('ietf.doc.views_doc.document_irsg_ballot',kwargs=dict(name=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):
draft = RgDraftFactory()
ballot = IRSGBallotDocEventFactory(doc=draft)
url = urlreverse('ietf.doc.views_ballot.edit_position', kwargs=dict(name=draft.name, ballot_id=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'])

View file

@ -71,6 +71,7 @@ urlpatterns = [
url(r'^active/?$', views_search.index_active_drafts),
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'^irsgballots/?$', views_ballot.irsg_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),
@ -83,6 +84,7 @@ urlpatterns = [
url(r'^%(name)s/references/$' % settings.URL_REGEXPS, views_doc.document_references),
url(r'^%(name)s/referencedby/$' % settings.URL_REGEXPS, views_doc.document_referenced_by),
url(r'^%(name)s/ballot/$' % settings.URL_REGEXPS, views_doc.document_ballot),
url(r'^%(name)s/irsgballot/$' % settings.URL_REGEXPS, views_doc.document_irsg_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),
@ -129,7 +131,9 @@ urlpatterns = [
url(r'^%(name)s/edit/approvedownrefs/$' % settings.URL_REGEXPS, views_ballot.approve_downrefs),
url(r'^%(name)s/edit/makelastcall/$' % settings.URL_REGEXPS, views_ballot.make_last_call),
url(r'^%(name)s/edit/urls/$' % settings.URL_REGEXPS, views_draft.edit_document_urls),
url(r'^%(name)s/edit/issueirsgballot/$' % settings.URL_REGEXPS, views_ballot.issue_irsg_ballot),
url(r'^%(name)s/edit/closeirsgballot/$' % settings.URL_REGEXPS, views_ballot.close_irsg_ballot),
url(r'^help/state/(?P<type>[\w-]+)/$', views_help.state_help),
url(r'^help/relationships/$', views_help.relationship_help),
url(r'^help/relationships/(?P<subset>\w+)/$', views_help.relationship_help),

View file

@ -28,7 +28,7 @@ from ietf.community.utils import docs_tracked_by_community_list
from ietf.doc.models import Document, DocHistory, State, DocumentAuthor, DocHistoryAuthor
from ietf.doc.models import DocAlias, RelatedDocument, RelatedDocHistory, BallotType, DocReminder
from ietf.doc.models import DocEvent, ConsensusDocEvent, BallotDocEvent, NewRevisionDocEvent, StateDocEvent
from ietf.doc.models import DocEvent, ConsensusDocEvent, BallotDocEvent, IRSGBallotDocEvent, NewRevisionDocEvent, StateDocEvent
from ietf.doc.models import TelechatDocEvent
from ietf.name.models import DocReminderTypeName, DocRelationshipName
from ietf.group.models import Role, Group
@ -210,6 +210,34 @@ 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
is currently in IRSG evaluation.'''
yes = [p for p in active_positions if p and p.pos_id == "yes"]
needmoretime = [p for p in active_positions if p and p.pos_id == "moretime"]
notready = [p for p in active_positions if p and p.pos_id == "notready"]
answer = []
needed = 2
have = len(yes)
if len(notready) > 0:
answer.append("Has a Not Ready position.")
if have < needed:
more = needed - have
if more == 1:
answer.append("Needs one more YES position to pass.")
else:
answer.append("Needs %d more YES positions to pass." % more)
else:
answer.append("Has enough positions to pass.")
if len(needmoretime) > 0:
answer.append("Has a Need More Time position.")
return " ".join(answer)
def create_ballot(request, doc, by, ballot_slug, time=None):
closed = close_open_ballots(doc, by)
for e in closed:
@ -222,13 +250,19 @@ def create_ballot(request, doc, by, ballot_slug, time=None):
e.desc = 'Created "%s" ballot' % e.ballot_type.name
e.save()
def create_ballot_if_not_open(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):
if time:
e = BallotDocEvent(type="created_ballot", by=by, doc=doc, rev=doc.rev, time=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)
else:
e = BallotDocEvent(type="created_ballot", by=by, doc=doc, rev=doc.rev)
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.ballot_type = ballot_type
e.desc = 'Created "%s" ballot' % e.ballot_type.name
e.save()

View file

@ -20,8 +20,9 @@ from django.views.decorators.csrf import csrf_exempt
import debug # pyflakes:ignore
from ietf.doc.models import ( Document, State, DocEvent, BallotDocEvent, BallotPositionDocEvent,
LastCallDocEvent, WriteupDocEvent, IESG_SUBSTATE_TAGS, RelatedDocument )
from ietf.doc.models import ( Document, State, DocEvent, BallotDocEvent,
IRSGBallotDocEvent, BallotPositionDocEvent, LastCallDocEvent, WriteupDocEvent,
IESG_SUBSTATE_TAGS, RelatedDocument, BallotType )
from ietf.doc.utils import ( add_state_change_event, close_ballot, close_open_ballots,
create_ballot_if_not_open, update_telechat )
from ietf.doc.mails import ( email_ballot_deferred, email_ballot_undeferred,
@ -34,17 +35,20 @@ from ietf.ietfauth.utils import has_role, role_required, is_authorized_in_doc_st
from ietf.mailtrigger.utils import gather_address_lists
from ietf.mailtrigger.forms import CcSelectForm
from ietf.message.utils import infer_message
from ietf.name.models import BallotPositionName
from ietf.name.models import BallotPositionName, DocTypeName
from ietf.person.models import Person
from ietf.utils import log
from ietf.utils.mail import send_mail_text, send_mail_preformatted
from ietf.utils.decorators import require_api_key
from ietf.doc.templatetags.ietf_filters import can_ballot
BALLOT_CHOICES = (("yes", "Yes"),
("noobj", "No Objection"),
("discuss", "Discuss"),
("abstain", "Abstain"),
("recuse", "Recuse"),
("moretime", "Need More Time"),
("notready", "Not Ready"),
("", "No Record"),
)
@ -117,17 +121,17 @@ class EditPositionForm(forms.Form):
raise forms.ValidationError("You must enter a non-empty discuss")
return entered_discuss
def save_position(form, doc, ballot, ad, login=None, send_email=False):
def save_position(form, doc, ballot, pos_by, login=None, send_email=False):
# save the vote
if login is None:
login = ad
login = pos_by
clean = form.cleaned_data
old_pos = doc.latest_event(BallotPositionDocEvent, type="changed_ballot_position", ad=ad, ballot=ballot)
old_pos = doc.latest_event(BallotPositionDocEvent, type="changed_ballot_position", pos_by=pos_by, ballot=ballot)
pos = BallotPositionDocEvent(doc=doc, rev=doc.rev, by=login)
pos.type = "changed_ballot_position"
pos.ballot = ballot
pos.ad = ad
pos.pos_by = pos_by
pos.pos = clean["position"]
pos.comment = clean["comment"].rstrip()
pos.comment_time = old_pos.comment_time if old_pos else None
@ -147,7 +151,7 @@ def save_position(form, doc, ballot, ad, login=None, send_email=False):
changes.append("comment")
if pos.comment:
e = DocEvent(doc=doc, rev=doc.rev, by=ad)
e = DocEvent(doc=doc, rev=doc.rev, by=pos_by)
e.type = "added_comment"
e.desc = "[Ballot comment]\n" + pos.comment
@ -159,7 +163,7 @@ def save_position(form, doc, ballot, ad, login=None, send_email=False):
changes.append("discuss")
if pos.pos.blocking:
e = DocEvent(doc=doc, rev=doc.rev, by=ad)
e = DocEvent(doc=doc, rev=doc.rev, by=pos_by)
e.type = "added_comment"
e.desc = "[Ballot %s]\n" % pos.pos.name.lower()
e.desc += pos.discuss
@ -167,16 +171,16 @@ def save_position(form, doc, ballot, ad, login=None, send_email=False):
# figure out a description
if not old_pos and pos.pos.slug != "norecord":
pos.desc = "[Ballot Position Update] New position, %s, has been recorded for %s" % (pos.pos.name, pos.ad.plain_name())
pos.desc = "[Ballot Position Update] New position, %s, has been recorded for %s" % (pos.pos.name, pos.pos_by.plain_name())
elif old_pos and pos.pos != old_pos.pos:
pos.desc = "[Ballot Position Update] Position for %s has been changed to %s from %s" % (pos.ad.plain_name(), pos.pos.name, old_pos.pos.name)
pos.desc = "[Ballot Position Update] Position for %s has been changed to %s from %s" % (pos.pos_by.plain_name(), pos.pos.name, old_pos.pos.name)
if not pos.desc and changes:
pos.desc = "Ballot %s text updated for %s" % (" and ".join(changes), ad.plain_name())
pos.desc = "Ballot %s text updated for %s" % (" and ".join(changes), pos_by.plain_name())
# only add new event if we actually got a change
if pos.desc:
if login != ad:
if login != pos_by:
pos.desc += " by %s" % login.plain_name()
pos.save()
@ -186,13 +190,13 @@ def save_position(form, doc, ballot, ad, login=None, send_email=False):
return pos
@role_required('Area Director','Secretariat')
@role_required('Area Director','Secretariat','IRSG Member')
def edit_position(request, name, ballot_id):
"""Vote and edit discuss and comment on document as 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)
ad = login = request.user.person
pos_by = login = request.user.person
if 'ballot_edit_return_point' in request.session:
return_to_url = request.session['ballot_edit_return_point']
@ -200,37 +204,39 @@ def edit_position(request, name, ballot_id):
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 has_role(request.user, "Secretariat"):
ad_id = request.GET.get('ad')
if not ad_id:
pos_by_id = request.GET.get('pos_by')
if not pos_by_id:
raise Http404
ad = get_object_or_404(Person, pk=ad_id)
pos_by = get_object_or_404(Person, pk=pos_by_id)
if request.method == 'POST':
old_pos = None
if not has_role(request.user, "Secretariat") and not ad.role_set.filter(name="ad", group__type="area", group__state="active"):
# PEY: if not has_role(request.user, "Secretariat") and not pos_by.role_set.filter(name="ad", group__type="area", group__state="active"):
if not has_role(request.user, "Secretariat") and not can_ballot(request.user, doc):
# prevent pre-ADs from voting
return HttpResponseForbidden("Must be a proper Area Director in an active area to cast ballot")
return HttpResponseForbidden("Must be a proper Area Director in an active area or IRSG Member to cast ballot")
form = EditPositionForm(request.POST, ballot_type=ballot.ballot_type)
if form.is_valid():
send_mail = True if request.POST.get("send_mail") else False
save_position(form, doc, ballot, ad, login, send_mail)
save_position(form, doc, ballot, pos_by, login, send_mail)
if send_mail:
qstr=""
if request.GET.get('ad'):
qstr += "?ad=%s" % request.GET.get('ad')
if request.GET.get('pos_by'):
qstr += "?pos_by=%s" % request.GET.get('pos_by')
return HttpResponseRedirect(urlreverse('ietf.doc.views_ballot.send_ballot_comment', kwargs=dict(name=doc.name, ballot_id=ballot_id)) + qstr)
elif request.POST.get("Defer"):
elif request.POST.get("Defer") and doc.stream.slug != "irtf":
return redirect('ietf.doc.views_ballot.defer_ballot', name=doc)
elif request.POST.get("Undefer"):
elif request.POST.get("Undefer") and doc.stream.slug != "irtf":
return redirect('ietf.doc.views_ballot.undefer_ballot', name=doc)
else:
return HttpResponseRedirect(return_to_url)
else:
initial = {}
old_pos = doc.latest_event(BallotPositionDocEvent, type="changed_ballot_position", ad=ad, ballot=ballot)
old_pos = doc.latest_event(BallotPositionDocEvent, type="changed_ballot_position", pos_by=pos_by, ballot=ballot)
if old_pos:
initial['position'] = old_pos.pos.slug
initial['discuss'] = old_pos.discuss
@ -245,7 +251,7 @@ def edit_position(request, name, ballot_id):
return render(request, 'doc/ballot/edit_position.html',
dict(doc=doc,
form=form,
ad=ad,
pos_by=pos_by,
return_to_url=return_to_url,
old_pos=old_pos,
ballot_deferred=ballot_deferred,
@ -295,7 +301,7 @@ def api_set_position(request):
return HttpResponse("Done", status=200, content_type='text/plain')
def build_position_email(ad, doc, pos):
def build_position_email(pos_by, doc, pos):
subj = []
d = ""
blocking_name = "DISCUSS"
@ -308,32 +314,42 @@ def build_position_email(ad, doc, pos):
c = pos.comment
subj.append("COMMENT")
ad_name_genitive = ad.plain_name() + "'" if ad.plain_name().endswith('s') else ad.plain_name() + "'s"
subject = "%s %s on %s" % (ad_name_genitive, pos.pos.name if pos.pos else "No Position", doc.name + "-" + doc.rev)
pos_by_name_genitive = pos_by.plain_name() + "'" if pos_by.plain_name().endswith('s') else pos_by.plain_name() + "'s"
subject = "%s %s on %s" % (pos_by_name_genitive, pos.pos.name if pos.pos else "No Position", doc.name + "-" + doc.rev)
if subj:
subject += ": (with %s)" % " and ".join(subj)
body = render_to_string("doc/ballot/ballot_comment_mail.txt",
dict(discuss=d,
comment=c,
ad=ad.plain_name(),
pos_by=pos_by.plain_name(),
doc=doc,
pos=pos.pos,
blocking_name=blocking_name,
settings=settings))
frm = ad.role_email("ad").formatted_email()
addrs = gather_address_lists('ballot_saved',doc=doc)
# PEY: This doesn't work properly for IRSG members, since they don't have the "ad" role. It still manages to get an address so it doesn't have to be fixed as a first priority.
frm = pos_by.role_email("ad").formatted_email()
if doc.stream_id == "irtf":
addrs = gather_address_lists('irsg_ballot_saved',doc=doc)
else:
addrs = gather_address_lists('iesg_ballot_saved',doc=doc)
return addrs, frm, subject, body
@role_required('Area Director','Secretariat')
@role_required('Area Director','Secretariat','IRSG 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)
ad = request.user.person
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'):
raise Http404
pos_by = request.user.person
if 'ballot_edit_return_point' in request.session:
return_to_url = request.session['ballot_edit_return_point']
@ -346,21 +362,26 @@ def send_ballot_comment(request, name, ballot_id):
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 not has_role(request.user, "Area Director"):
ad_id = request.GET.get('ad')
if not ad_id:
if has_role(request.user, "Secretariat"):
pos_by_id = request.GET.get('pos_by')
if not pos_by_id:
raise Http404
ad = get_object_or_404(Person, pk=ad_id)
pos_by = get_object_or_404(Person, pk=pos_by_id)
pos = doc.latest_event(BallotPositionDocEvent, type="changed_ballot_position", ad=ad, ballot=ballot)
pos = doc.latest_event(BallotPositionDocEvent, type="changed_ballot_position", pos_by=pos_by, ballot=ballot)
if not pos:
raise Http404
addrs, frm, subject, body = build_position_email(ad, doc, pos)
addrs, frm, subject, body = build_position_email(pos_by, doc, pos)
if doc.stream_id == 'irtf':
mailtrigger_slug='irsg_ballot_saved'
else:
mailtrigger_slug='iesg_ballot_saved'
if request.method == 'POST':
cc = []
cc_select_form = CcSelectForm(data=request.POST,mailtrigger_slug='ballot_saved',mailtrigger_context={'doc':doc})
cc_select_form = CcSelectForm(data=request.POST,mailtrigger_slug=mailtrigger_slug,mailtrigger_context={'doc':doc})
if cc_select_form.is_valid():
cc.extend(cc_select_form.get_selected_addresses())
extra_cc = [x.strip() for x in request.POST.get("extra_cc","").split(',') if x.strip()]
@ -373,7 +394,7 @@ def send_ballot_comment(request, name, ballot_id):
else:
cc_select_form = CcSelectForm(mailtrigger_slug='ballot_saved',mailtrigger_context={'doc':doc})
cc_select_form = CcSelectForm(mailtrigger_slug=mailtrigger_slug,mailtrigger_context={'doc':doc})
return render(request, 'doc/ballot/send_ballot_comment.html',
dict(doc=doc,
@ -381,7 +402,7 @@ def send_ballot_comment(request, name, ballot_id):
body=body,
frm=frm,
to=addrs.as_strings().to,
ad=ad,
pos_by=pos_by,
back_url=back_url,
cc_select_form = cc_select_form,
))
@ -599,17 +620,17 @@ def ballot_writeupnotes(request, name):
if "issue_ballot" in request.POST:
e = create_ballot_if_not_open(request, doc, login, "approve") # pyflakes:ignore
ballot = doc.latest_event(BallotDocEvent, type="created_ballot")
if has_role(request.user, "Area Director") and not doc.latest_event(BallotPositionDocEvent, ad=login, ballot=ballot):
if has_role(request.user, "Area Director") and not doc.latest_event(BallotPositionDocEvent, pos_by=login, ballot=ballot):
# sending the ballot counts as a yes
pos = BallotPositionDocEvent(doc=doc, rev=doc.rev, by=login)
pos.ballot = ballot
pos.type = "changed_ballot_position"
pos.ad = login
pos.pos_by = login
pos.pos_id = "yes"
pos.desc = "[Ballot Position Update] New position, %s, has been recorded for %s" % (pos.pos.name, pos.ad.plain_name())
pos.desc = "[Ballot Position Update] New position, %s, has been recorded for %s" % (pos.pos.name, pos.pos_by.plain_name())
pos.save()
# Consider mailing this position to 'ballot_saved'
# Consider mailing this position to 'iesg_ballot_saved'
approval = doc.latest_event(WriteupDocEvent, type="changed_ballot_approval_text")
if not approval:
@ -1051,3 +1072,119 @@ def make_last_call(request, name):
form=form,
announcement=announcement,
))
@role_required('Secretariat', 'IRTF Chair')
def issue_irsg_ballot(request, name):
doc = get_object_or_404(Document, docalias__name=name)
if doc.stream.slug != "irtf" or doc.type != DocTypeName.objects.get(slug="draft"):
raise Http404
by = request.user.person
fillerdate = datetime.date.today() + datetime.timedelta(weeks=2)
if request.method == 'POST':
button = request.POST.__getitem__("irsg_button")
if button == 'Yes':
duedate = request.POST.__getitem__("duedate")
e = IRSGBallotDocEvent(doc=doc, rev=doc.rev, by=request.user.person)
if (duedate == None or duedate==""):
duedate = str(fillerdate)
e.duedate = datetime.datetime.strptime(duedate, '%Y-%m-%d')
# PEY: What's the best thing to do for "unreasonable" dates?
e.type = "created_ballot"
e.desc = "Created IRSG Ballot"
ballot_type = BallotType.objects.get(doc_type=doc.type, slug="irsg-approve")
e.ballot_type = ballot_type
e.save()
# PEY: This is probably not enough state setting/cleanup. I should review the IESG version more to see what happens.
new_state = doc.get_state()
prev_tags = []
new_tags = []
if doc.type_id == 'draft':
new_state = State.objects.get(used=True, type="draft-stream-irtf", slug='irsgpoll')
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 = []
state_change_event = add_state_change_event(doc, by, prev_state, new_state, prev_tags=prev_tags, new_tags=new_tags)
if state_change_event:
events.append(state_change_event)
if events:
doc.save_with_history(events)
return HttpResponseRedirect(doc.get_absolute_url())
else:
templ = 'doc/ballot/irsg_ballot_approve.html'
question = "Are you sure you really want to issue a ballot for " + name + "?"
return render(request, templ, dict(doc=doc,
question=question, fillerdate=fillerdate))
@role_required('Secretariat', 'IRTF Chair')
def close_irsg_ballot(request, name):
doc = get_object_or_404(Document, docalias__name=name)
if doc.stream.slug != "irtf" or doc.type != DocTypeName.objects.get(slug="draft"):
raise Http404
by = request.user.person
if request.method == 'POST':
button = request.POST.__getitem__("irsg_button")
if button == 'Yes':
e = BallotDocEvent(doc=doc, rev=doc.rev, by=request.user.person)
e.type = "closed_ballot"
e.desc = "Closed IRSG Ballot"
ballot_type = BallotType.objects.get(doc_type=doc.type, slug="irsg-approve")
e.ballot_type = ballot_type
e.save()
# PEY: This is probably not enough state setting/cleanup. I should review the IESG version more to see what happens.
new_state = doc.get_state()
prev_tags = []
new_tags = []
# PEY: Need to determine what the correct state to transition to is.
if doc.type_id == 'draft':
new_state = State.objects.get(used=True, type="draft-stream-irtf", slug='active')
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 = []
state_change_event = add_state_change_event(doc, by, prev_state, new_state, prev_tags=prev_tags, new_tags=new_tags)
if state_change_event:
events.append(state_change_event)
if events:
doc.save_with_history(events)
return HttpResponseRedirect(doc.get_absolute_url())
templ = 'doc/ballot/irsg_ballot_close.html'
question = "Are you sure you really want to close the ballot for " + name + "?"
return render(request, templ, dict(doc=doc,
question=question))
@role_required('Secretariat', 'IRTF Chair')
def irsg_ballot_status(request):
possible_docs = Document.objects.filter(docevent__ballotdocevent__irsgballotdocevent__isnull=False)
docs = []
for doc in possible_docs:
if doc.ballot_open("irsg-approve"):
ballot = doc.active_ballot()
if ballot:
doc.ballot = ballot
# PEY: Need to figure how to work the duedate into status_columns.html
# PEY: Also, how is it I can add duedate to doc just like that?
doc.duedate=datetime.datetime.strftime(ballot.irsgballotdocevent.duedate, '%Y-%m-%d')
docs.append(doc)
return render(request, 'doc/irsg_ballot_status.html', {'docs':docs})

View file

@ -665,15 +665,15 @@ def ballot_writeupnotes(request, name):
existing.save()
if "send_ballot" in request.POST and approval:
if has_role(request.user, "Area Director") and not charter.latest_event(BallotPositionDocEvent, type="changed_ballot_position", ad=by, ballot=ballot):
if has_role(request.user, "Area Director") and not charter.latest_event(BallotPositionDocEvent, type="changed_ballot_position", pos_by=by, ballot=ballot):
# sending the ballot counts as a yes
pos = BallotPositionDocEvent(doc=charter, rev=charter.rev, by=by)
pos.type = "changed_ballot_position"
pos.ad = by
pos.pos_by = by
pos.pos_id = "yes"
pos.desc = "[Ballot Position Update] New position, %s, has been recorded for %s" % (pos.pos.name, pos.ad.plain_name())
pos.save()
# Consider mailing this position to 'ballot_saved'
# Consider mailing this position to 'iesg_ballot_saved'
msg = generate_issue_ballot_mail(request, charter, ballot)
send_mail_preformatted(request, msg)

View file

@ -69,17 +69,17 @@ def change_state(request, name, option=None):
e = create_ballot_if_not_open(request, review, login, "conflrev") # pyflakes:ignore
ballot = review.latest_event(BallotDocEvent, type="created_ballot")
log.assertion('ballot == e')
if has_role(request.user, "Area Director") and not review.latest_event(BallotPositionDocEvent, ad=login, ballot=ballot, type="changed_ballot_position"):
if has_role(request.user, "Area Director") and not review.latest_event(BallotPositionDocEvent, pos_by=login, ballot=ballot, type="changed_ballot_position"):
# The AD putting a conflict review into iesgeval who doesn't already have a position is saying "yes"
pos = BallotPositionDocEvent(doc=review, rev=review.rev, by=login)
pos.ballot = ballot
pos.type = "changed_ballot_position"
pos.ad = login
pos.pos_by = login
pos.pos_id = "yes"
pos.desc = "[Ballot Position Update] New position, %s, has been recorded for %s" % (pos.pos.name, pos.ad.plain_name())
pos.desc = "[Ballot Position Update] New position, %s, has been recorded for %s" % (pos.pos.name, pos.pos_by.plain_name())
pos.save()
# Consider mailing that position to 'ballot_saved'
# Consider mailing that position to 'iesg_ballot_saved'
send_conflict_eval_email(request,review)

View file

@ -63,7 +63,7 @@ from ietf.doc.utils import (add_links_in_new_revision_events, augment_events_wit
needed_ballot_positions, nice_consensus, prettify_std_name, update_telechat, has_same_ballot,
get_initial_notify, make_notify_changed_event, make_rev_history, default_consensus,
add_events_message_info, get_unicode_document_content, build_doc_meta_block,
augment_docs_and_user_with_user_info)
irsg_needed_ballot_positions )
from ietf.group.models import Role, Group
from ietf.group.utils import can_manage_group_type, can_manage_materials, group_features_role_filter
from ietf.ietfauth.utils import ( has_role, is_authorized_in_doc_stream, user_is_person,
@ -83,15 +83,22 @@ from ietf.utils.text import maybe_split
def render_document_top(request, doc, tab, name):
# PEY: Figuring out what tab value is
tabs = []
tabs.append(("Status", "status", urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=name)), True, None))
ballot = doc.latest_event(BallotDocEvent, type="created_ballot")
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)), ballot, None if ballot else "IESG Evaluation Ballot has not been created yet"))
elif doc.type_id == "charter" and doc.group.type_id == "wg":
tabs.append(("IESG Review", "ballot", urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=name)), ballot, None if ballot else "IESG Review Ballot has not been created yet"))
# ballot = doc.latest_event(BallotDocEvent, type="created_ballot")
# ballot_type = BallotType.objects.get(doc_type=doc.type, slug="irsg-approve")
iesg_ballot = doc.latest_event(BallotDocEvent, type="created_ballot", ballot_type__slug='approve')
irsg_ballot = doc.latest_event(BallotDocEvent, type="created_ballot",ballot_type__slug='irsg-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 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":
tabs.append(("IESG Review", "ballot", urlreverse("ietf.doc.views_doc.document_ballot", kwargs=dict(name=name)), iesg_ballot, None if iesg_ballot else "IESG Review Ballot has not been created yet"))
if doc.type_id == "draft" or (doc.type_id == "charter" and doc.group.type_id == "wg"):
tabs.append(("IESG Writeups", "writeup", urlreverse('ietf.doc.views_doc.document_writeup', kwargs=dict(name=name)), True, None))
@ -170,6 +177,8 @@ def document_main(request, name, rev=None):
iesg_state = doc.get_state("draft-iesg")
iesg_state_summary = doc.friendly_state()
irsg_state = doc.get_state("draft-stream-irtf")
can_edit = has_role(request.user, ("Area Director", "Secretariat"))
stream_slugs = StreamName.objects.values_list("slug", flat=True)
# For some reason, AnonymousUser has __iter__, but is not iterable,
@ -260,11 +269,17 @@ def document_main(request, name, rev=None):
file_urls.append(("bibtex", "bibtex"))
# ballot
ballot_summary = None
if iesg_state and iesg_state.slug in IESG_BALLOT_ACTIVE_STATES:
iesg_ballot_summary = None
irsg_ballot_summary = None
due_date = None
if (iesg_state and iesg_state.slug in IESG_BALLOT_ACTIVE_STATES) or irsg_state:
active_ballot = doc.active_ballot()
if active_ballot:
ballot_summary = needed_ballot_positions(doc, list(active_ballot.active_ad_positions().values()))
if irsg_state:
irsg_ballot_summary = irsg_needed_ballot_positions(doc, list(active_ballot.active_balloteer_positions().values()))
due_date=active_ballot.irsgballotdocevent.duedate
else:
iesg_ballot_summary = needed_ballot_positions(doc, list(active_ballot.active_balloteer_positions().values()))
# submission
submission = ""
@ -365,6 +380,13 @@ def document_main(request, name, rev=None):
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 can_edit_stream_info):
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 can_edit_stream_info):
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 doc.stream_id in ("ise", "irtf")
and can_edit_stream_info and not conflict_reviews and not snapshot):
@ -390,7 +412,7 @@ def document_main(request, name, rev=None):
elif can_edit_stream_info and (iesg_state.slug in ('idexists','watching')):
actions.append(("Submit to IESG for Publication", urlreverse('ietf.doc.views_draft.to_iesg', kwargs=dict(name=doc.name))))
augment_docs_and_user_with_user_info([doc], request.user)
#augment_docs_and_user_with_user_info([doc], request.user)
replaces = [d.name for d in doc.related_that_doc("replaces")]
replaced_by = [d.name for d in doc.related_that("replaces")]
@ -433,7 +455,9 @@ def document_main(request, name, rev=None):
rfc_number=rfc_number,
draft_name=draft_name,
telechat=telechat,
ballot_summary=ballot_summary,
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,
@ -471,6 +495,7 @@ def document_main(request, name, rev=None):
presentations=presentations,
review_assignments=review_assignments,
no_review_from_teams=no_review_from_teams,
due_date=due_date,
))
if doc.type_id == "charter":
@ -479,9 +504,10 @@ def document_main(request, name, rev=None):
ballot_summary = None
if doc.get_state_slug() in ("intrev", "iesgrev"):
# PEY: Need to adjust this so that I check draft-stream-irtf state as well and generate an irsg_ballot_summary as needed...
active_ballot = doc.active_ballot()
if active_ballot:
ballot_summary = needed_ballot_positions(doc, list(active_ballot.active_ad_positions().values()))
ballot_summary = needed_ballot_positions(doc, list(active_ballot.active_balloteer_positions().values()))
else:
ballot_summary = "No active ballot found."
@ -522,8 +548,9 @@ def document_main(request, name, rev=None):
content = markup_txt.markup(content)
ballot_summary = None
# PEY: Need to work in irsg_ballot_summary here as well
if doc.get_state_slug() in ("iesgeval") and doc.active_ballot():
ballot_summary = needed_ballot_positions(doc, list(doc.active_ballot().active_ad_positions().values()))
ballot_summary = needed_ballot_positions(doc, list(doc.active_ballot().active_balloteer_positions().values()))
return render(request, "doc/document_conflict_review.html",
dict(doc=doc,
@ -549,8 +576,9 @@ def document_main(request, name, rev=None):
content = doc.text_or_error() # pyflakes:ignore
ballot_summary = None
# PEY: work in irsg_ballot_summary here too
if doc.get_state_slug() in ("iesgeval"):
ballot_summary = needed_ballot_positions(doc, list(doc.active_ballot().active_ad_positions().values()))
ballot_summary = needed_ballot_positions(doc, list(doc.active_ballot().active_balloteer_positions().values()))
if isinstance(doc,Document):
sorted_relations=doc.relateddocument_set.all().order_by('relationship__name')
@ -998,16 +1026,20 @@ def document_ballot_content(request, doc, ballot_id, editable=True):
position_groups = []
for n in BallotPositionName.objects.filter(slug__in=[p.pos_id for p in positions]).order_by('order'):
g = (n, [p for p in positions if p.pos_id == n.slug])
g[1].sort(key=lambda p: (p.old_ad, p.ad.plain_name()))
g[1].sort(key=lambda p: (p.old_pos_by, p.pos_by.plain_name()))
if n.blocking:
position_groups.insert(0, g)
else:
position_groups.append(g)
summary = needed_ballot_positions(doc, [p for p in positions if not p.old_ad])
# PEY: Need to integrate irsg_needed_ballot_positions here as well.
if (ballot.ballot_type.slug == "irsg-approve"):
summary = irsg_needed_ballot_positions(doc, [p for p in positions if not p.old_pos_by])
else:
summary = needed_ballot_positions(doc, [p for p in positions if not p.old_pos_by])
text_positions = [p for p in positions if p.discuss or p.comment]
text_positions.sort(key=lambda p: (p.old_ad, p.ad.plain_name()))
text_positions.sort(key=lambda p: (p.old_pos_by, p.pos_by.plain_name()))
ballot_open = not BallotDocEvent.objects.filter(doc=doc,
type__in=("closed_ballot", "created_ballot"),
@ -1031,7 +1063,51 @@ def document_ballot_content(request, doc, ballot_id, editable=True):
def document_ballot(request, name, ballot_id=None):
doc = get_object_or_404(Document, docalias__name=name)
top = render_document_top(request, doc, "ballot", name)
all_ballots = list(BallotDocEvent.objects.filter(doc=doc, type="created_ballot").order_by("time"))
if not ballot_id:
if all_ballots:
ballot = all_ballots[-1]
else:
# PEY: What should I do if I somehow got here without any ballots existing? Can that happen? Passing for now.
pass
ballot_id = ballot.id
else:
ballot_id = int(ballot_id)
for b in all_ballots:
if b.id == ballot_id:
ballot = b
break
if not ballot_id or not ballot:
# PEY: Something bad happened. How do I gracefully bail out?
pass
if ballot.ballot_type.slug == "approve":
ballot_tab = "ballot"
elif ballot.ballot_type.slug == "irsg-approve":
ballot_tab = "irsgballot"
else:
ballot_tab = None
top = render_document_top(request, doc, ballot_tab, name)
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,
# ballot_type_slug=ballot.ballot_type.slug,
))
def document_irsg_ballot(request, name, ballot_id=None):
doc = get_object_or_404(Document, docalias__name=name)
top = render_document_top(request, doc, "irsgballot", name)
if not ballot_id:
ballot = doc.latest_event(BallotDocEvent, type="created_ballot", ballot_type__slug='irsg-approve')
if ballot:
ballot_id = ballot.id
c = document_ballot_content(request, doc, ballot_id, editable=True)
@ -1041,6 +1117,7 @@ def document_ballot(request, name, ballot_id=None):
dict(doc=doc,
top=top,
ballot_content=c,
# ballot_type_slug=ballot.ballot_type.slug,
))
def ballot_popup(request, name, ballot_id):

View file

@ -425,7 +425,7 @@ def docs_for_ad(request, name):
Q(states__type__in=("statchg", "conflrev"),
states__slug__in=("iesgeval", "defer")),
docevent__ballotpositiondocevent__pos__blocking=True,
docevent__ballotpositiondocevent__ad=ad).distinct()
docevent__ballotpositiondocevent__pos_by=ad)
for doc in possible_docs:
ballot = doc.active_ballot()
if not ballot:
@ -433,7 +433,7 @@ def docs_for_ad(request, name):
blocking_positions = [p for p in ballot.all_positions() if p.pos.blocking]
if not blocking_positions or not any( p.ad==ad for p in blocking_positions ):
if not blocking_positions or not any( p.pos_by==ad for p in blocking_positions ):
continue
augment_events_with_revision(doc, blocking_positions)
@ -445,7 +445,7 @@ def docs_for_ad(request, name):
# latest first
if blocked_docs:
blocked_docs.sort(key=lambda d: min(p.time for p in d.blocking_positions if p.ad==ad), reverse=True)
blocked_docs.sort(key=lambda d: min(p.time for p in d.blocking_positions if p.pos_by==ad), reverse=True)
return render(request, 'doc/drafts_for_ad.html', {
'form':form, 'docs':results, 'meta':meta, 'ad_name': ad.plain_name(), 'blocked_docs': blocked_docs

View file

@ -73,15 +73,15 @@ def change_state(request, name, option=None):
if new_state.slug == "iesgeval":
e = create_ballot_if_not_open(request, status_change, login, "statchg", status_change.time) # pyflakes:ignore
ballot = status_change.latest_event(BallotDocEvent, type="created_ballot")
if has_role(request.user, "Area Director") and not status_change.latest_event(BallotPositionDocEvent, ad=login, ballot=ballot, type="changed_ballot_position"):
if has_role(request.user, "Area Director") and not status_change.latest_event(BallotPositionDocEvent, pos_by=login, ballot=ballot, type="changed_ballot_position"):
# The AD putting a status change into iesgeval who doesn't already have a position is saying "yes"
pos = BallotPositionDocEvent(doc=status_change, rev=status_change.rev, by=login)
pos.ballot = ballot
pos.type = "changed_ballot_position"
pos.ad = login
pos.pos_by = login
pos.pos_id = "yes"
pos.desc = "[Ballot Position Update] New position, %s, has been recorded for %s" % (pos.pos.name, pos.ad.plain_name())
pos.desc = "[Ballot Position Update] New position, %s, has been recorded for %s" % (pos.pos.name, pos.pos_by.plain_name())
pos.save()
send_status_change_eval_email(request,status_change)

View file

@ -42,14 +42,14 @@ class IESGTests(TestCase):
pos.type = "changed_ballot_position"
pos.doc = draft
pos.rev = draft.rev
pos.ad = pos.by = Person.objects.get(user__username="ad")
pos.pos_by = pos.by = Person.objects.get(user__username="ad")
pos.save()
r = self.client.get(urlreverse("ietf.iesg.views.discusses"))
self.assertEqual(r.status_code, 200)
self.assertContains(r, draft.name)
self.assertContains(r, pos.ad.plain_name())
self.assertContains(r, pos.pos_by.plain_name())
def test_milestones_needing_review(self):
draft = WgDraftFactory()

View file

@ -428,7 +428,7 @@ def past_documents(request):
if blocking_positions:
augment_events_with_revision(doc, blocking_positions)
doc.by_me = bool([p for p in blocking_positions if user_is_person(request.user, p.ad)])
doc.by_me = bool([p for p in blocking_positions if user_is_person(request.user, p.pos_by)])
doc.for_me = user_is_person(request.user, doc.ad)
doc.milestones = doc.groupmilestone_set.filter(state="active").order_by("time").select_related("group")
doc.blocking_positions = blocking_positions
@ -506,7 +506,7 @@ def discusses(request):
augment_events_with_revision(doc, blocking_positions)
doc.by_me = bool([p for p in blocking_positions if user_is_person(request.user, p.ad)])
doc.by_me = bool([p for p in blocking_positions if user_is_person(request.user, p.pos_by)])
doc.for_me = user_is_person(request.user, doc.ad)
doc.milestones = doc.groupmilestone_set.filter(state="active").order_by("time").select_related("group")
doc.blocking_positions = blocking_positions

View file

@ -80,6 +80,7 @@ def has_role(user, role_names, *args, **kwargs):
"Recording Manager": Q(person=person,name="recman",group__type="ietf",group__state="active", ),
"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")),
}

View file

@ -0,0 +1,44 @@
# Copyright The IETF Trust 2019, All Rights Reserved
# -*- coding: utf-8 -*-
# Generated by Django 1.11.25 on 2019-10-04 13:12
from __future__ import unicode_literals
from django.db import migrations
def forward(apps, shema_editor):
Recipient = apps.get_model('mailtrigger','Recipient')
irsg = Recipient.objects.create(
slug = 'irsg',
desc = 'The IRSG',
template = 'The IRSG <irsg@irtf.org>'
)
MailTrigger = apps.get_model('mailtrigger', 'MailTrigger')
slug = 'irsg_ballot_saved'
desc = 'Recipients when a new IRSG ballot position with comments is saved'
irsg_ballot_saved = MailTrigger.objects.create(
slug=slug,
desc=desc
)
irsg_ballot_saved.to.add(irsg)
irsg_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']))
MailTrigger.objects.filter(slug='ballot_saved').update(slug='iesg_ballot_saved')
def reverse(apps, shema_editor):
MailTrigger = apps.get_model('mailtrigger', 'MailTrigger')
MailTrigger.objects.filter(slug='irsg_ballot_saved').delete()
MailTrigger.objects.filter(slug='iesg_ballot_saved').update(slug='ballot_saved')
Recipient = apps.get_model('mailtrigger','Recipient')
Recipient.objects.filter(slug='irsg').delete()
class Migration(migrations.Migration):
dependencies = [
('mailtrigger', '0012_dont_last_call_early_reviews'),
]
operations = [
migrations.RunPython(forward, reverse),
]

View file

@ -304,9 +304,9 @@ class Recipient(models.Model):
doc = kwargs['doc']
active_ballot = doc.active_ballot()
if active_ballot:
for ad, pos in active_ballot.active_ad_positions().items():
for balloteer, pos in active_ballot.active_balloteer_positions().items():
if pos and pos.pos_id == "discuss":
addrs.append(ad.role_email("ad").address)
addrs.append(balloteer.role_email("ad").address)
return addrs
def gather_ipr_updatedipr_contacts(self, **kwargs):

View file

@ -15,12 +15,12 @@ class EventMailTests(TestCase):
url = urlreverse('ietf.mailtrigger.views.show_triggers')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertContains(r, 'ballot_saved')
self.assertContains(r, 'iesg_ballot_saved')
url = urlreverse('ietf.mailtrigger.views.show_triggers',kwargs=dict(mailtrigger_slug='ballot_saved'))
url = urlreverse('ietf.mailtrigger.views.show_triggers',kwargs=dict(mailtrigger_slug='iesg_ballot_saved'))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertContains(r, 'ballot_saved')
self.assertContains(r, 'iesg_ballot_saved')
def test_show_recipients(self):

View file

@ -68,7 +68,8 @@ def gather_relevant_expansions(**kwargs):
doc = kwargs['doc']
relevant.update(['doc_state_edited','doc_telechat_details_changed','ballot_deferred','ballot_saved'])
# PEY: does this need to include irsg_ballot_saved as well?
relevant.update(['doc_state_edited','doc_telechat_details_changed','ballot_deferred','iesg_ballot_saved'])
if doc.type_id in ['draft','statchg']:
relevant.update(starts_with('last_call_'))

File diff suppressed because it is too large Load diff

View file

@ -59,7 +59,8 @@ class FormalLanguageName(NameModel):
class DocReminderTypeName(NameModel):
"Stream state"
class BallotPositionName(NameModel):
""" Yes, No Objection, Abstain, Discuss, Block, Recuse """
""" Yes, No Objection, Abstain, Discuss, Block, Recuse, Need More Time,
Not Ready """
blocking = models.BooleanField(default=False)
class MeetingTypeName(NameModel):
"""IETF, Interim"""

View file

@ -189,6 +189,13 @@ def determine_merge_order(source,target):
source,target = sorted([source,target],key=lambda a: a.user.last_login if a.user.last_login else datetime.datetime.min)
return source,target
def get_active_balloteers(ballot_type):
if (ballot_type.slug != "irsg-approve"):
active_balloteers = get_active_ads()
else:
active_balloteers = get_active_irsg()
return active_balloteers
def get_active_ads():
from ietf.person.models import Person
cache_key = "doc:active_ads"
@ -197,3 +204,13 @@ def get_active_ads():
active_ads = list(Person.objects.filter(role__name="ad", role__group__state="active", role__group__type="area").distinct())
cache.set(cache_key, active_ads)
return active_ads
def get_active_irsg():
from ietf.person.models import Person
cache_key = "doc:active_irsg_balloteers"
active_irsg_balloteers = cache.get(cache_key)
if not active_irsg_balloteers:
active_irsg_balloteers = list(Person.objects.filter(role__group__acronym='irsg',role__name__in=['chair','member','atlarge']).distinct())
cache.set(cache_key, active_irsg_balloteers)
return active_irsg_balloteers

View file

@ -158,7 +158,7 @@ class SecrTelechatTestCase(TestCase):
}
)
self.assertEqual(response.status_code,302)
self.assertTrue(BallotPositionDocEvent.objects.filter(doc=charter, ad_id=13, pos__slug='noobj').exists())
self.assertTrue(BallotPositionDocEvent.objects.filter(doc=charter, pos_by_id=13, pos__slug='noobj').exists())
def test_doc_detail_post_update_state(self):
by=Person.objects.get(name="(System)")

View file

@ -35,7 +35,7 @@ x consolidate views (get rid of get_group_header,group,group_navigate)
'''
active_ballot_positions: takes one argument, doc. returns a dictionary with a key for each ad Person object
NOTE: this function has been deprecated as of Datatracker 4.34. Should now use methods on the Document.
For example: doc.active_ballot().active_ad_positions()
For example: doc.active_ballot().active_balloteer_positions()
agenda_data: takes a date string in the format YYYY-MM-DD.
'''
@ -191,7 +191,7 @@ def doc_detail(request, date, name):
login = request.user.person
if doc.active_ballot():
ballots = doc.active_ballot().active_ad_positions() # returns dict of ad:ballotpositiondocevent
ballots = doc.active_ballot().active_balloteer_positions() # returns dict of ad:ballotpositiondocevent
else:
ballots = []
@ -246,16 +246,16 @@ def doc_detail(request, date, name):
if form.is_valid() and form.changed_data:
# create new BallotPositionDocEvent
clean = form.cleaned_data
ad = Person.objects.get(id=clean['id'])
pos_by = Person.objects.get(id=clean['id'])
pos = BallotPositionDocEvent(doc=doc, rev=doc.rev, by=login)
pos.type = "changed_ballot_position"
pos.ad = ad
pos.pos_by = pos_by
pos.ballot = doc.latest_event(BallotDocEvent, type="created_ballot")
pos.pos = clean['position']
if form.initial['position'] == None:
pos.desc = '[Ballot Position Update] New position, %s, has been recorded for %s by %s' % (pos.pos.name, ad.name, login.name)
pos.desc = '[Ballot Position Update] New position, %s, has been recorded for %s by %s' % (pos.pos.name, pos_by.name, login.name)
else:
pos.desc = '[Ballot Position Update] Position for %s has been changed to %s by %s' % (ad.name, pos.pos.name, login.name)
pos.desc = '[Ballot Position Update] Position for %s has been changed to %s by %s' % (pos_by.name, pos.pos.name, login.name)
pos.save()
has_changed = True

View file

@ -461,7 +461,7 @@ td.area-director div { border-bottom: solid #ccc 1px; }
.milestone { font-style: italic; }
.areadirector-name { padding-bottom: .5em; line-height: 1em;}
.balloteer-name { padding-bottom: .5em; line-height: 1em;}
.changebar { width: 0.3em; }

View file

@ -365,7 +365,7 @@ class SubmitTests(TestCase):
ballot_position.type = "changed_ballot_position"
ballot_position.doc = draft
ballot_position.rev = draft.rev
ballot_position.ad = ballot_position.by = Person.objects.get(user__username="ad2")
ballot_position.pos_by = ballot_position.by = Person.objects.get(user__username="ad2")
ballot_position.save()
elif stream_type == 'irtf':
@ -508,7 +508,7 @@ class SubmitTests(TestCase):
self.assertTrue(interesting_address in force_text(outbox[-2].as_string()))
if draft.stream_id == 'ietf':
self.assertTrue(draft.ad.role_email("ad").address in force_text(outbox[-2].as_string()))
self.assertTrue(ballot_position.ad.role_email("ad").address in force_text(outbox[-2].as_string()))
self.assertTrue(ballot_position.pos_by.role_email("ad").address in force_text(outbox[-2].as_string()))
self.assertTrue("New Version Notification" in outbox[-1]["Subject"])
self.assertTrue(name in get_payload(outbox[-1]))
r = self.client.get(urlreverse('ietf.doc.views_search.recent_drafts'))

View file

@ -55,6 +55,11 @@
<li><a href="{% url 'ietf.doc.views_status_change.rfc_status_changes' %}">RFC status changes</a></li>
{% endif %}
{% if user|has_role:"IRTF Chair,Secretariat" %}
{% if flavor == "top" %}<li class="divider hidden-xs"></li>{% endif %}
<li><a href="{% url 'ietf.doc.views_ballot.irsg_ballot_status' %}">IRSG ballot status</a></li>
{% endif %}
{% if user|has_role:"WG Chair,RG Chair" %}
{% if flavor == "top" %}<li class="divider hidden-xs"></li>{% endif %}
<li {%if flavor == "top" %}class="dropdown-header hidden-xs"{% else %}class="nav-header"{% endif %}>WG chair</li>

View file

@ -1,11 +1,11 @@
{% load ietf_filters %}{% autoescape off %}{{ ad }} has entered the following ballot position for
{% load ietf_filters %}{% autoescape off %}{{ pos_by }} has entered the following ballot position for
{{ doc.name }}-{{ doc.rev }}: {{ pos.name }}
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" %}
{% if doc.type_id == "draft" and doc.stream_id != "irtf" %}
Please refer to https://www.ietf.org/iesg/statement/discuss-criteria.html
for more information about IESG DISCUSS and COMMENT positions.
{% endif %}

View file

@ -3,11 +3,11 @@
{% load origin %}
{% load bootstrap3 %}
{% block title %}Change position for {{ ad.plain_name }}{% endblock %}
{% block title %}Change position for {{ pos_by.plain_name }}{% endblock %}
{% block content %}
{% origin %}
<h1>Change position for {{ ad.plain_name }} regarding <br><small><a href="{% url "ietf.doc.views_doc.document_main" name=doc.canonical_name %}">{{ doc }}</a></small></h1>
<h1>Change position for {{ pos_by.plain_name }} regarding <br><small><a href="{% url "ietf.doc.views_doc.document_main" name=doc.canonical_name %}">{{ doc }}</a></small></h1>
<div class="question">{{ ballot.ballot_type.question }}</div>
@ -36,9 +36,9 @@
<input type="submit" class="btn btn-default" value="Save">
{% if doc.type_id == "draft" or doc.type_id == "conflrev" %}
{% if ballot_deferred %}
{% if ballot_deferred and doc.stream.slug != "irtf" %}
<input type="submit" class="btn btn-warning" name="Undefer" value="Undefer ballot">
{% else %}
{% elif doc.stream.slug != "irtf" %}
<input type="submit" class="btn btn-danger" name="Defer" value="Defer ballot">
{% endif %}
{% endif %}

View file

@ -0,0 +1,26 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2019, All Rights Reserved #}
{% load origin %}
{% load bootstrap3 %}
{% block title %}Issue ballot for {{ doc }}?{% endblock %}
{% block content %}
{% origin %}
<h1>{{ question }}</h1>
<form method="post">
{% csrf_token %}
{# curly percent bootstrap_form approval_text_form curly percent #}
Due date for this ballot:
<input type="text" placeholder={{ fillerdate }} name="duedate">
{% buttons %}
<button type="submit" class="btn btn-primary" name="irsg_button" value="Yes">Yes</button>
<button type="submit" class="btn btn-default" name="irsg_button" value="No">No</button>
{% endbuttons %}
</form>
{% endblock %}

View file

@ -0,0 +1,23 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2019, All Rights Reserved #}
{% load origin %}
{% load bootstrap3 %}
{% block title %}Close ballot for {{ doc }}?{% endblock %}
{% block content %}
{% origin %}
<h1>{{ question }}</h1>
<form method="post">
{% csrf_token %}
{# curly percent bootstrap_form approval_text_form curly percent #}
{% buttons %}
<button type="submit" class="btn btn-primary" name="irsg_button" value="Yes">Yes</button>
<button type="submit" class="btn btn-default" name="irsg_button" value="No">No</button>
{% endbuttons %}
</form>
{% endblock %}

View file

@ -6,11 +6,11 @@
{% load ietf_filters %}
{% block title %}Send ballot position for {{ ad }}{% endblock %}
{% block title %}Send ballot position for {{ pos_by }}{% endblock %}
{% block content %}
{% origin %}
<h1>Send ballot position for {{ ad }} on <a href="{% url "ietf.doc.views_doc.document_main" name=doc.canonical_name %}">{{ doc }}</a></h1>
<h1>Send ballot position for {{ pos_by }} on <a href="{% url "ietf.doc.views_doc.document_main" name=doc.canonical_name %}">{{ doc }}</a></h1>
<form method="post">
{% csrf_token %}

View file

@ -25,19 +25,19 @@
</div>
<div class="modal-footer">
{% if editable and user|has_role:"Area Director,Secretariat" %}
{% if user|has_role:"Area Director" %}
{% if editable and user|has_role:"Area Director,Secretariat,IRSG Member" %}
{% if user|can_ballot:doc %}
<a class="btn btn-default" href="{% url "ietf.doc.views_ballot.edit_position" name=doc.name ballot_id=ballot_id %}">Edit position</a>
{% endif %}
{% if doc.type_id == "draft" or doc.type_id == "conflrev" %}
{% if user|can_defer:doc %}
{% if deferred %}
<a class="btn btn-default" href="{% url 'ietf.doc.views_ballot.undefer_ballot' name=doc.name %}">Undefer ballot</a>
{% else %}
<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" %}
{% if user|has_role:"Secretariat" and ballot_type_slug != "irsg-approve" %}
<a class="btn btn-danger" href="{% url 'ietf.doc.views_ballot.clear_ballot' name=doc.name ballot_type_slug=ballot_type_slug %}">Clear ballot</a>
{% endif %}
{% endif %}

View file

@ -7,9 +7,9 @@
{% for n, positions in position_groups %}
<h4><span class="label label-{{ n|pos_to_label }}"> {{ n.name }}</span></h4>
{% for p in positions|dictsort:"ad.last_name" %}
<div class="areadirector-name">
{% if p.old_ad %}<span class="text-muted">({% endif %}{% if p.comment or p.discuss %}<a href="#{{ p.ad.plain_name|slugify }}">{% endif %}{{ p.ad.plain_name }}{% if p.comment or p.discuss %}</a>{% endif %}{% if p.old_ad %})</span>{% endif %}
{% for p in positions|dictsort:"pos_by.last_name" %}
<div class="balloteer-name">
{% if p.old_pos_by %}<span class="text-muted">({% endif %}{% if p.comment or p.discuss %}<a href="#{{ p.pos_By.plain_name|slugify }}">{% endif %}{{ p.pos_by.plain_name }}{% if p.comment or p.discuss %}</a>{% endif %}{% if p.old_pos_by %})</span>{% endif %}
</div>
{% empty %}
(None)
@ -48,12 +48,12 @@
<p class="well"><b>Ballot question:</b> "{{ ballot.ballot_type.question }}"</p>
{% endif %}
{% if editable and user|has_role:"Area Director,Secretariat" %}
{% if editable and user|has_role:"Area Director,Secretariat,IRSG Member" %}
<a class="btn btn-default"
href="https://mailarchive.ietf.org/arch/search/?q=subject:{{doc.name}}+AND+subject:(discuss+OR+comment+OR+review)">
Search Mailarchive</a>
{% if user|has_role:"Area Director" %}
{% if user|can_ballot:doc %}
<a class="btn btn-primary" href="{% url "ietf.doc.views_ballot.edit_position" name=doc.name ballot_id=ballot.pk %}">Edit position</a>
{% endif %}
@ -66,23 +66,23 @@
{% endif %}
{% endif %}
{% if user|has_role:"Area Director,Secretariat" %}
{% if user|has_role:"Area Director,Secretariat" and ballot.ballot_type.slug != "irsg-approve" %}
<a class="btn btn-danger" 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 %}
{% for p in positions|dictsort:"ad.last_name" %}
<h4 class="anchor-target" id="{{ p.ad.plain_name|slugify }}">
{% if p.old_ad %}<span class="text-muted">({% endif %}{{ p.ad.plain_name }}{% if p.old_ad %})</span>{% endif %}
{% for p in positions|dictsort:"pos_by.last_name" %}
<h4 class="anchor-target" id="{{ p.pos_by.plain_name|slugify }}">
{% if p.old_pos_by %}<span class="text-muted">({% endif %}{{ p.pos_by.plain_name }}{% if p.old_pos_by %})</span>{% endif %}
<span class="pull-right">
{% if p.old_positions %}
<span class="text-muted small">(was {{ p.old_positions|join:", " }})</span>
{% endif %}
<span class="label label-{{ p.pos|pos_to_label }}">{{p.pos}}</span>
{% if user|has_role:"Secretariat" %}
<a href="{% url "ietf.doc.views_ballot.edit_position" name=doc.name ballot_id=ballot.pk %}?ad={{ p.ad.pk }}" title="Click to edit the position of {{ p.ad.plain_name }}" class="btn btn-default btn-xs">
<a href="{% url "ietf.doc.views_ballot.edit_position" name=doc.name ballot_id=ballot.pk %}?pos_by={{ p.pos_by.pk }}" title="Click to edit the position of {{ p.pos_by.plain_name }}" class="btn btn-default btn-xs">
Edit</a>
{% endif %}
</span>

View file

@ -305,6 +305,8 @@
{% if stream_tags %}
<div class="stream-tags">{% for tag in stream_tags %}{{ tag.name }}{% if not forloop.last %}, {% endif %}{% endfor %}</div>
{% endif %}
{# PEY: Move this to the IRSG section when built #}
{% if due_date %} [Due date: {{ due_date }}] {% endif %}
</td>
{% else %}
<th>Stream state</th>
@ -467,8 +469,8 @@
{% endif %}
{% endif %}
{% if ballot_summary %}
<br><i>{{ ballot_summary }}</i>
{% if iesg_ballot_summary %}
<br><i>{{ iesg_ballot_summary }}</i>
{% endif %}
</td>
</tr>

View file

@ -31,11 +31,11 @@
<td>{{ doc.ad|default:"" }}</td>
<td>
{% for p in doc.blocking_positions %}
{{ p.ad }}
{{ p.pos_by }}
({% if p.discuss_time %}{{ p.discuss_time|timesince_days }}{% endif %}
days ago{% if doc.get_state_url != "rfc" and p.rev != doc.rev %}
for -{{ p.rev }}{% endif %})<br>
{% if p.old_ad %}
{% if p.old_pos_by %}
</span>
{% endif %}
{% endfor %}

View file

@ -0,0 +1,42 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2019, All Rights Reserved #}
{% load origin staticfiles %}
{% load ballot_icon %}
{% load ietf_filters %}
{% block pagehead %}
<link rel="stylesheet" href="{% static "jquery.tablesorter/css/theme.bootstrap.min.css" %}">
{% endblock %}
{% block title %}IRSG ballot status{% endblock %}
{% block content %}
{% origin %}
<h1>IRSG ballot status</h1>
<table class="table table-condensed table-striped tablesorter">
<thead>
<tr>
<th>Document</th>
<th>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>
{% endblock %}
{% block js %}
<script src="{% static "jquery.tablesorter/js/jquery.tablesorter.combined.min.js" %}"></script>
{% endblock %}

View file

@ -32,9 +32,9 @@
{% if doc.stream %}
<br>
{% if doc|state:"stream" %}
{{ doc|state:"stream" }}{% if doc.intended_std_level %}:<wbr>{% endif %}
{{ doc|state:"stream" }} {% if doc.intended_std_level %}:<wbr>{% endif %}
{% else %}
{{ doc.stream }} stream{% if doc.intended_std_level %}:<wbr>{% endif %}
{{ doc.stream }} stream {% if doc.intended_std_level %}:<wbr>{% endif %}
{% endif %}
{% endif %}
@ -42,6 +42,11 @@
{{ doc.intended_std_level }}
{% endif %}
{% if doc.duedate %}
<br>
Due date: {{ doc.duedate }}
{%endif %}
{% if doc.reviewed_by_teams %}
<br>Reviews:
{% for acronym in doc.reviewed_by_teams %}

View file

@ -59,14 +59,14 @@
<td>{{ doc.ad|default:"" }}</td>
<td>
{% for p in doc.blocking_positions %}
{% if p.old_ad %}
{% if p.old_pos_by %}
<span class="text-muted">
{% endif %}
{{ p.ad }}
{{ p.pos_by }}
({% if p.discuss_time %}{{ p.discuss_time|timesince_days }}{% endif %}
days ago{% if doc.get_state_url != "rfc" and p.rev != doc.rev %}
for -{{ p.rev }}{% endif %})<br>
{% if p.old_ad %}
{% if p.old_pos_by %}
</span>
{% endif %}
{% endfor %}

View file

@ -64,7 +64,7 @@ Some parts Copyright (c) 2009 The IETF Trust, all rights reserved.
{% if ballot %}
<small><pre>
Yes No-Objection Discuss Abstain Recuse
{% for pos in ballot.all_positions %}{% if pos.old_ad %}{{pos.ad.plain_name|bracket|ljust:"22"}}{%else%}{{pos.ad.plain_name|ljust:"22"}}{%endif%} {{pos|bracketpos:"yes"}} {{pos|bracketpos:"noobj"}} {{pos|bracketpos:"discuss"}} {{pos|bracketpos:"abstain"}} {{pos|bracketpos:"recuse"}}
{% for pos in ballot.all_positions %}{% if pos.old_pos_by %}{{pos.pos_by.plain_name|bracket|ljust:"22"}}{%else%}{{pos.pos_by.plain_name|ljust:"22"}}{%endif%} {{pos|bracketpos:"yes"}} {{pos|bracketpos:"noobj"}} {{pos|bracketpos:"discuss"}} {{pos|bracketpos:"abstain"}} {{pos|bracketpos:"recuse"}}
{% endfor %}
</pre></small>

View file

@ -66,14 +66,14 @@
<td>{{ doc.ad|default:"" }}</td>
<td>
{% for p in doc.blocking_positions %}
{% if p.old_ad %}
{% if p.old_pos_by %}
<span class="text-muted">
{% endif %}
{{ p.ad }}
{{ p.pos_by }}
({% if p.discuss_time %}{{ p.discuss_time|timesince_days }}{% endif %}
days ago{% if doc.get_state_url != "rfc" and p.rev != doc.rev %}
for -{{ p.rev }}{% endif %})<br>
{% if p.old_ad %}
{% if p.old_pos_by %}
</span>
{% endif %}
{% endfor %}

View file

@ -3,17 +3,17 @@
{% if ballot %}
<br><b>Discusses/comments</b> <a href="https://datatracker.ietf.org/doc/{{doc.canonical_name}}/ballot/">[ballot]</a>:
<ul>
{% for p in ballot.active_ad_positions.values %}
{% for p in ballot.active_balloteer_positions.values %}
{% if p.pos %}
{% if p.discuss %}
<li>
<a href="#{{ doc.name }}+{{ p.ad|slugify }}+discuss">{{ p.ad }}: Discuss [{{ p.discuss_time }}]</a>:
<a href="#{{ doc.name }}+{{ p.pos_by|slugify }}+discuss">{{ p.pos_by }}: Discuss [{{ p.discuss_time }}]</a>:
<br>{{ p.discuss }}
</li>
{% endif %}
{% if p.comment %}
<li>
<a href="#{{ doc.name }}+{{ p.ad|slugify }}+comment">{{ p.ad }}: Comment [{{ p.comment_time }}]</a>:
<a href="#{{ doc.name }}+{{ p.pos_by|slugify }}+comment">{{ p.pos_by }}: Comment [{{ p.comment_time }}]</a>:
<br>{{ p.comment }}
</li>
{% endif %}

View file

@ -67,16 +67,16 @@ Some parts Copyright (c) 2009 The IETF Trust, all rights reserved.
{% with doc.active_ballot as ballot %}
{% if ballot %}
<ul>{% for p in ballot.active_ad_positions.values %}
<ul>{% for p in ballot.active_balloteer_positions.values %}
{% if p.pos and p.discuss %}
<li>
<a name="{{ doc.name }}+{{ p.ad.plain_name|slugify }}+discuss">{{ p.ad.plain_name }}: Discuss [{{ p.discuss_time }}]</a>:
<a name="{{ doc.name }}+{{ p.pos_by.plain_name|slugify }}+discuss">{{ p.pos_by.plain_name }}: Discuss [{{ p.discuss_time }}]</a>:
<br><pre>{{ p.discuss|wordwrap:80 }}</pre>
</li>
{% endif %}
{% if p.pos and p.comment %}
<li>
<a name="{{ doc.name }}+{{ p.ad.plain_name|slugify }}+comment">{{ p.ad.plain_name }}: Comment [{{ p.comment_time }}]</a>:
<a name="{{ doc.name }}+{{ p.pos_by.plain_name|slugify }}+comment">{{ p.pos_by.plain_name }}: Comment [{{ p.comment_time }}]</a>:
<br><pre>{{ p.comment|wordwrap:80 }}</pre>
</li>
{% endif %}{% endfor %}

View file

@ -95,6 +95,7 @@ def make_immutable_base_data():
create_person(rfc_editor, "auth", name="Rfc Editor", username="rfc", email_address="rfc@edit.or")
iesg = create_group(name="Internet Engineering Steering Group", acronym="iesg", type_id="ietf", parent=ietf) # pyflakes:ignore
irsg = create_group(name="Internet Research Steering Group", acronym="irsg", type_id="irtf", parent=irtf) # pyflakes:ignore
individ = create_group(name="Individual submissions", acronym="none", type_id="individ") # pyflakes:ignore
@ -134,6 +135,17 @@ def make_immutable_base_data():
person=p,
email=email)
# Create some IRSG members (really should add some atlarge and the chair,
# but this isn't currently essential)
create_person(irsg, "member", name="R. Searcher", username="rsearcher", email_address="rsearcher@example.org")
# Create a bunch of IRSG members for swarm tests
for i in range(1, 5):
u = User.objects.create(username="irsgmember%s" % i)
p = Person.objects.create(name="IRSG Member No%s" % i, ascii="IRSG Member No%s" % i, user=u)
email = Email.objects.create(address="irsgmember%s@example.org" % i, person=p, origin=u.username)
Role.objects.create(name_id="member", group=irsg, person=p, email=email)
def make_test_data():
area = Group.objects.get(acronym="farfut")
ad = Person.objects.get(user__username="ad")