Skip to content

Commit a741642

Browse files
committed
Added editing last call messages, requesting, issuing and tracking IETF LCs to status-change documents
Added a Cancel button to the form that allows editing the relations for status-change documents Added instructions to the agenda section 3.3 This adds states to status-change- documents and has a migration that must be applied. This fixes bug ietf-tools#1039 - Legacy-Id: 5770
1 parent 5bee7ac commit a741642

15 files changed

Lines changed: 846 additions & 55 deletions

ietf/doc/migrations/0010_more_statchg_states.py

Lines changed: 477 additions & 0 deletions
Large diffs are not rendered by default.

ietf/doc/tests_status_change.py

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from ietf.doc.utils import create_ballot_if_not_open
1717
from ietf.doc.views_status_change import default_approval_text
1818

19-
from ietf.doc.models import Document,DocEvent,NewRevisionDocEvent,BallotPositionDocEvent,TelechatDocEvent,DocAlias,State
19+
from ietf.doc.models import Document,DocEvent,NewRevisionDocEvent,BallotPositionDocEvent,TelechatDocEvent,WriteupDocEvent,DocAlias,State
2020
from ietf.name.models import StreamName
2121
from ietf.group.models import Person
2222
from ietf.iesg.models import TelechatDate
@@ -202,6 +202,51 @@ def test_edit_telechat_date(self):
202202
doc = Document.objects.get(name='status-change-imaginary-mid-review')
203203
self.assertEquals(doc.latest_event(TelechatDocEvent, "scheduled_for_telechat").telechat_date,None)
204204

205+
def test_edit_lc(self):
206+
doc = Document.objects.get(name='status-change-imaginary-mid-review')
207+
url = urlreverse('status_change_last_call',kwargs=dict(name=doc.name))
208+
209+
login_testing_unauthorized(self, "ad", url)
210+
211+
# additional setup
212+
doc.relateddocument_set.create(target=DocAlias.objects.get(name='rfc9999'),relationship_id='tois')
213+
doc.relateddocument_set.create(target=DocAlias.objects.get(name='rfc9998'),relationship_id='tohist')
214+
doc.ad = Person.objects.get(name='Ad No2')
215+
doc.save()
216+
217+
# get
218+
r = self.client.get(url)
219+
self.assertEquals(r.status_code, 200)
220+
q = PyQuery(r.content)
221+
self.assertEquals(len(q('form.edit-last-call-text')),1)
222+
223+
self.assertTrue( 'RFC9999 from Proposed Standard to Internet Standard' in ''.join(wrap(r.content,2**16)))
224+
self.assertTrue( 'RFC9998 from Informational to Historic' in ''.join(wrap(r.content,2**16)))
225+
226+
# save
227+
r = self.client.post(url,dict(last_call_text="Bogus last call text",save_last_call_text="1"))
228+
self.assertEquals(r.status_code, 200)
229+
230+
last_call_event = doc.latest_event(WriteupDocEvent, type="changed_last_call_text")
231+
self.assertEquals(last_call_event.text,"Bogus last call text")
232+
233+
# reset
234+
r = self.client.post(url,dict(regenerate_last_call_text="1"))
235+
self.assertEquals(r.status_code,200)
236+
self.assertTrue( 'RFC9999 from Proposed Standard to Internet Standard' in ''.join(wrap(r.content,2**16)))
237+
self.assertTrue( 'RFC9998 from Informational to Historic' in ''.join(wrap(r.content,2**16)))
238+
239+
# request last call
240+
messages_before = len(outbox)
241+
r = self.client.post(url,dict(last_call_text='stuff',send_last_call_request='Save+and+Request+Last+Call'))
242+
self.assertEquals(r.status_code,200)
243+
self.assertTrue( 'Last Call Requested' in ''.join(wrap(r.content,2**16)))
244+
self.assertEquals(len(outbox), messages_before + 1)
245+
self.assertTrue('iesg-secretary' in outbox[-1]['To'])
246+
self.assertTrue('Last Call:' in outbox[-1]['Subject'])
247+
self.assertTrue('Last Call Request has been submitted' in ''.join(wrap(unicode(outbox[-1]),2**16)))
248+
249+
205250
def test_approve(self):
206251
doc = Document.objects.get(name='status-change-imaginary-mid-review')
207252
url = urlreverse('status_change_approve',kwargs=dict(name=doc.name))
@@ -263,21 +308,24 @@ def test_edit_relations(self):
263308

264309
# Try to add a relation to an RFC that doesn't exist
265310
r = self.client.post(url,dict(new_relation_row_blah="rfc9997",
266-
statchg_relation_row_blah="tois"))
311+
statchg_relation_row_blah="tois",
312+
Submit="Submit"))
267313
self.assertEquals(r.status_code, 200)
268314
q = PyQuery(r.content)
269315
self.assertTrue(len(q('form ul.errorlist')) > 0)
270316

271317
# Try to add a relation leaving the relation type blank
272318
r = self.client.post(url,dict(new_relation_row_blah="rfc9999",
273-
statchg_relation_row_blah=""))
319+
statchg_relation_row_blah="",
320+
Submit="Submit"))
274321
self.assertEquals(r.status_code, 200)
275322
q = PyQuery(r.content)
276323
self.assertTrue(len(q('form ul.errorlist')) > 0)
277324

278325
# Try to add a relation with an unknown relationship type
279326
r = self.client.post(url,dict(new_relation_row_blah="rfc9999",
280-
statchg_relation_row_blah="badslug"))
327+
statchg_relation_row_blah="badslug",
328+
Submit="Submit"))
281329
self.assertEquals(r.status_code, 200)
282330
q = PyQuery(r.content)
283331
self.assertTrue(len(q('form ul.errorlist')) > 0)
@@ -286,7 +334,8 @@ def test_edit_relations(self):
286334
r = self.client.post(url,dict(new_relation_row_blah="rfc9999",
287335
statchg_relation_row_blah="toexp",
288336
new_relation_row_foo="rfc9998",
289-
statchg_relation_row_foo="tobcp"))
337+
statchg_relation_row_foo="tobcp",
338+
Submit="Submit"))
290339
self.assertEquals(r.status_code, 302)
291340
doc = Document.objects.get(name='status-change-imaginary-mid-review')
292341
self.assertEquals(doc.relateddocument_set.count(),2)

ietf/doc/urls_status_change.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@
88
url(r'^approve/$', "approve", name='status_change_approve'),
99
url(r'^telechat/$', "telechat_date", name='status_change_telechat_date'),
1010
url(r'^relations/$', "edit_relations", name='status_change_relations'),
11+
url(r'^last-call/$', "last_call", name='status_change_last_call'),
1112
)
1213

ietf/doc/views_status_change.py

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525

2626
from ietf.doc.forms import TelechatForm, AdForm, NotifyForm
2727

28+
from ietf.idrfc.views_ballot import LastCallTextForm
29+
from ietf.idrfc.lastcall import request_last_call
30+
2831
class ChangeStateForm(forms.Form):
2932
new_state = forms.ModelChoiceField(State.objects.filter(type="statchg", used=True), label="Status Change Evaluation State", empty_label=None, required=True)
3033
comment = forms.CharField(widget=forms.Textarea, help_text="Optional comment for the review history", required=False)
@@ -588,8 +591,8 @@ def edit_relations(request, name):
588591

589592
if request.method == 'POST':
590593
form = EditStatusChangeForm(request.POST)
591-
if form.is_valid():
592-
594+
if 'Submit' in request.POST and form.is_valid():
595+
593596
old_relations={}
594597
for rel in status_change.relateddocument_set.filter(relationship__slug__in=RELATION_SLUGS):
595598
old_relations[rel.target.document.canonical_name()]=rel.relationship.slug
@@ -605,13 +608,14 @@ def edit_relations(request, name):
605608
c.desc += "\nNEW:"
606609
for relname,relslug in (set(new_relations.items())-set(old_relations.items())):
607610
c.desc += "\n "+relname+": "+DocRelationshipName.objects.get(slug=relslug).name
608-
#for rel in status_change.relateddocument_set.filter(relationship__slug__in=RELATION_SLUGS):
609-
# c.desc +="\n"+rel.relationship.name+": "+rel.target.document.canonical_name()
610611
c.desc += "\n"
611612
c.save()
612613

613614
return HttpResponseRedirect(status_change.get_absolute_url())
614615

616+
elif 'Cancel' in request.POST:
617+
return HttpResponseRedirect(status_change.get_absolute_url())
618+
615619
else:
616620
relations={}
617621
for rel in status_change.relateddocument_set.filter(relationship__slug__in=RELATION_SLUGS):
@@ -627,3 +631,93 @@ def edit_relations(request, name):
627631
'relation_slugs': relation_slugs,
628632
},
629633
context_instance = RequestContext(request))
634+
635+
def generate_last_call_text(request, doc):
636+
637+
# requester should be set based on doc.group once the group for a status change can be set to something other than the IESG
638+
# and when groups are set, vary the expiration time accordingly
639+
640+
requester = "an individual participant"
641+
expiration_date = datetime.date.today() + datetime.timedelta(days=28)
642+
cc = []
643+
644+
new_text = render_to_string("doc/status_change/last_call_announcement.txt",
645+
dict(doc=doc,
646+
settings=settings,
647+
requester=requester,
648+
expiration_date=expiration_date.strftime("%Y-%m-%d"),
649+
changes=['%s from %s to %s'%(rel.target.name.upper(),rel.target.document.std_level.name,newstatus(rel)) for rel in doc.relateddocument_set.filter(relationship__slug__in=RELATION_SLUGS)],
650+
urls=[rel.target.document.get_absolute_url() for rel in doc.relateddocument_set.filter(relationship__slug__in=RELATION_SLUGS)],
651+
cc=cc
652+
)
653+
)
654+
655+
e = WriteupDocEvent()
656+
e.type = 'changed_last_call_text'
657+
e.by = request.user.get_profile()
658+
e.doc = doc
659+
e.desc = 'Last call announcement was generated'
660+
e.text = unicode(new_text)
661+
e.save()
662+
663+
return e
664+
665+
@role_required("Area Director", "Secretariat")
666+
def last_call(request, name):
667+
"""Edit the Last Call Text for this status change and possibly request IETF LC"""
668+
669+
status_change = get_object_or_404(Document, type="statchg", name=name)
670+
671+
login = request.user.get_profile()
672+
673+
last_call_event = status_change.latest_event(WriteupDocEvent, type="changed_last_call_text")
674+
if not last_call_event:
675+
last_call_event = generate_last_call_text(request, status_change)
676+
677+
form = LastCallTextForm(initial=dict(last_call_text=last_call_event.text))
678+
679+
if request.method == 'POST':
680+
if "save_last_call_text" in request.POST or "send_last_call_request" in request.POST:
681+
form = LastCallTextForm(request.POST)
682+
if form.is_valid():
683+
t = form.cleaned_data['last_call_text']
684+
if t != last_call_event.text:
685+
e = WriteupDocEvent(doc=status_change, by=login)
686+
e.by = login
687+
e.type = "changed_last_call_text"
688+
e.desc = "Last call announcement was changed"
689+
e.text = t
690+
e.save()
691+
692+
if "send_last_call_request" in request.POST:
693+
save_document_in_history(status_change)
694+
695+
old_description = status_change.friendly_state()
696+
status_change.set_state(State.objects.get(type='statchg', slug='lc-req'))
697+
new_description = status_change.friendly_state()
698+
699+
e = log_state_changed(request, status_change, login, new_description, old_description)
700+
701+
status_change.time = e.time
702+
status_change.save()
703+
704+
request_last_call(request, status_change)
705+
706+
return render_to_response('idrfc/last_call_requested.html',
707+
dict(doc=status_change,
708+
url = status_change.get_absolute_url(),
709+
),
710+
context_instance=RequestContext(request))
711+
712+
if "regenerate_last_call_text" in request.POST:
713+
e = generate_last_call_text(request,status_change)
714+
form = LastCallTextForm(initial=dict(last_call_text=e.text))
715+
716+
return render_to_response('doc/status_change/last_call.html',
717+
dict(doc=status_change,
718+
back_url = status_change.get_absolute_url(),
719+
last_call_event = last_call_event,
720+
last_call_form = form,
721+
),
722+
context_instance = RequestContext(request))
723+

ietf/idrfc/views_ballot.py

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,22 +1094,25 @@ class MakeLastCallForm(forms.Form):
10941094
def make_last_call(request, name):
10951095
"""Make last call for Internet Draft, sending out announcement."""
10961096
doc = get_object_or_404(Document, docalias__name=name)
1097-
if not doc.get_state("draft-iesg"):
1097+
if not (doc.get_state("draft-iesg") or doc.get_state("statchg")):
10981098
raise Http404
10991099

11001100
login = request.user.get_profile()
11011101

11021102
e = doc.latest_event(WriteupDocEvent, type="changed_last_call_text")
11031103
if not e:
1104+
if doc.type.slug != 'draft':
1105+
raise Http404
11041106
e = generate_last_call_announcement(request, doc)
11051107
announcement = e.text
11061108

11071109
if request.method == 'POST':
11081110
form = MakeLastCallForm(request.POST)
11091111
if form.is_valid():
11101112
send_mail_preformatted(request, announcement)
1111-
send_mail_preformatted(request, announcement, extra=extra_automation_headers(doc),
1112-
override={ "To": "IANA <drafts-lastcall@icann.org>", "CC": None, "Bcc": None, "Reply-To": None})
1113+
if doc.type.slug == 'draft':
1114+
send_mail_preformatted(request, announcement, extra=extra_automation_headers(doc),
1115+
override={ "To": "IANA <drafts-lastcall@icann.org>", "CC": None, "Bcc": None, "Reply-To": None})
11131116

11141117
msg = infer_message(announcement)
11151118
msg.by = login
@@ -1118,20 +1121,29 @@ def make_last_call(request, name):
11181121

11191122
save_document_in_history(doc)
11201123

1121-
prev = doc.get_state("draft-iesg")
1122-
doc.set_state(State.objects.get(used=True, type="draft-iesg", slug='lc'))
1124+
if doc.type.slug == 'draft':
11231125

1124-
prev_tag = doc.tags.filter(slug__in=('point', 'ad-f-up', 'need-rev', 'extpty'))
1125-
prev_tag = prev_tag[0] if prev_tag else None
1126-
if prev_tag:
1127-
doc.tags.remove(prev_tag)
1126+
prev = doc.get_state("draft-iesg")
1127+
doc.set_state(State.objects.get(used=True, type="draft-iesg", slug='lc'))
11281128

1129-
e = idrfcutil_log_state_changed(request, doc, login, prev, prev_tag)
1129+
prev_tag = doc.tags.filter(slug__in=('point', 'ad-f-up', 'need-rev', 'extpty'))
1130+
prev_tag = prev_tag[0] if prev_tag else None
1131+
if prev_tag:
1132+
doc.tags.remove(prev_tag)
1133+
1134+
e = idrfcutil_log_state_changed(request, doc, login, prev, prev_tag)
1135+
change_description = "Last call has been made for %s and state has been changed to %s" % (doc.name, doc.get_state("draft-iesg").name)
1136+
1137+
elif doc.type.slug == 'statchg':
1138+
1139+
prev = doc.friendly_state()
1140+
doc.set_state(State.objects.get(used=True, type="statchg", slug='in-lc'))
1141+
e = docutil_log_state_changed(request, doc, login, doc.friendly_state(), prev)
1142+
change_description = "Last call has been made for %s and state has been changed to %s" % (doc.name, doc.friendly_state())
11301143

11311144
doc.time = e.time
11321145
doc.save()
11331146

1134-
change_description = "Last call has been made for %s and state has been changed to %s" % (doc.name, doc.get_state("draft-iesg").name)
11351147
email_state_changed(request, doc, change_description)
11361148
email_owner(request, doc, doc.ad, login, change_description)
11371149

@@ -1146,25 +1158,34 @@ def make_last_call(request, name):
11461158
e.save()
11471159

11481160
# update IANA Review state
1149-
prev_state = doc.get_state("draft-iana-review")
1150-
if not prev_state:
1151-
next_state = State.objects.get(used=True, type="draft-iana-review", slug="need-rev")
1152-
doc.set_state(next_state)
1153-
add_state_change_event(doc, login, prev_state, next_state)
1161+
if doc.type.slug == 'draft':
1162+
prev_state = doc.get_state("draft-iana-review")
1163+
if not prev_state:
1164+
next_state = State.objects.get(used=True, type="draft-iana-review", slug="need-rev")
1165+
doc.set_state(next_state)
1166+
add_state_change_event(doc, login, prev_state, next_state)
11541167

11551168
return HttpResponseRedirect(doc.get_absolute_url())
11561169
else:
11571170
initial = {}
11581171
initial["last_call_sent_date"] = date.today()
1159-
expire_days = 14
1160-
if doc.group.type_id in ("individ", "area"):
1161-
expire_days = 28
1172+
if doc.type.slug == 'draft':
1173+
# This logic is repeated in the code that edits last call text - why?
1174+
expire_days = 14
1175+
if doc.group.type_id in ("individ", "area"):
1176+
expire_days = 28
1177+
templ = 'idrfc/make_last_callREDESIGN.html'
1178+
else:
1179+
expire_days=28
1180+
templ = 'doc/status_change/make_last_call.html'
11621181

11631182
initial["last_call_expiration_date"] = date.today() + timedelta(days=expire_days)
11641183

11651184
form = MakeLastCallForm(initial=initial)
11661185

1167-
return render_to_response('idrfc/make_last_callREDESIGN.html',
1186+
return render_to_response(templ,
11681187
dict(doc=doc,
1169-
form=form),
1188+
form=form,
1189+
announcement=announcement,
1190+
),
11701191
context_instance=RequestContext(request))

ietf/idrfc/views_search.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,10 @@ def ad_dashboard_sort_key(doc):
640640
elif doc.get_state_slug('charter') == 'iesgrev':
641641
state = State.objects.get(type__slug='draft-iesg',slug='iesg-eva')
642642
return "1%d%s" % (state.order,seed)
643+
644+
if doc.type.slug=='statchg' and doc.get_state_slug('statchg') == 'adrev':
645+
state = State.objects.get(type__slug='draft-iesg',slug='ad-eval')
646+
return "1%d%s" % (state.order,seed)
643647

644648
if seed.startswith('Needs Shepherd'):
645649
return "100%s" % seed
@@ -664,6 +668,7 @@ def ad_dashboard_sort_key(doc):
664668

665669
def by_ad2(request, name):
666670
responsible = Document.objects.values_list('ad', flat=True).distinct()
671+
ad_id = None
667672
for p in Person.objects.filter(Q(role__name__in=("pre-ad", "ad"),
668673
role__group__type="area",
669674
role__group__state="active")
@@ -672,6 +677,10 @@ def by_ad2(request, name):
672677
ad_id = p.id
673678
ad_name = p.plain_name()
674679
break
680+
681+
if not ad_id:
682+
raise Http404
683+
675684
docqueryset = Document.objects.filter(ad__id=ad_id)
676685
docs=[]
677686
for doc in docqueryset:

0 commit comments

Comments
 (0)