Made Publication Request (for documents from IETF working groups) an explicit action rather than a side-effect.

Simplified the working group state edit form.
Added hints to the WG state edit form to use the document's main page to request publication.
If a document is moved into IESG processing directly by the secretariat or an AD (old processing path), set working group state accordingly.
 - Legacy-Id: 6119
This commit is contained in:
Robert Sparks 2013-09-09 18:25:44 +00:00
parent 2a2389d17f
commit 63139ab860
11 changed files with 340 additions and 114 deletions

View file

@ -328,6 +328,24 @@ class EditInfoTestCase(django.test.TestCase):
self.assertEquals(events[-3].type, "started_iesg_process")
self.assertEquals(len(outbox), mailbox_before)
# Redo, starting in publication requested to make sure WG state is also set
draft.unset_state('draft-iesg')
draft.set_state(State.objects.get(type='draft-stream-ietf',slug='writeupw'))
draft.stream = StreamName.objects.get(slug='ietf')
draft.save()
r = self.client.post(url,
dict(intended_std_level=str(draft.intended_std_level_id),
ad=ad.pk,
create_in_state=State.objects.get(used=True, type="draft-iesg", slug="pub-req").pk,
notify="test@example.com",
note="This is a note",
telechat_date="",
))
self.assertEquals(r.status_code, 302)
draft = Document.objects.get(name=draft.name)
self.assertEquals(draft.get_state_slug('draft-iesg'),'pub-req')
self.assertEquals(draft.get_state_slug('draft-stream-ietf'),'sub-pub')
def test_edit_consensus(self):
draft = make_test_data()
@ -867,6 +885,66 @@ class IndividualInfoFormsTestCase(django.test.TestCase):
self.docname='draft-ietf-mars-test'
self.doc = Document.objects.get(name=self.docname)
class SubmitToIesgTestCase(django.test.TestCase):
fixtures = ['names']
def verify_permissions(self):
def verify_fail(remote_user):
if remote_user:
self.client.login(remote_user=remote_user)
r = self.client.get(url)
self.assertEquals(r.status_code,404)
def verify_can_see(remote_user):
self.client.login(remote_user=remote_user)
r = self.client.get(url)
self.assertEquals(r.status_code,200)
q = PyQuery(r.content)
self.assertEquals(len(q('form input[name="confirm"]')),1)
url = urlreverse('doc_to_iesg', kwargs=dict(name=self.docname))
for username in [None,'plain','iana','iab chair']:
verify_fail(username)
for username in ['marschairman','secretary','ad']:
verify_can_see(username)
def cancel_submission(self):
url = urlreverse('doc_to_iesg', kwargs=dict(name=self.docname))
self.client.login(remote_user='marschairman')
r = self.client.post(url, dict(cancel="1"))
self.assertEquals(r.status_code, 302)
doc = Document.objects.get(pk=self.doc.pk)
self.assertTrue(doc.get_state('draft-iesg')==None)
def confirm_submission(self):
url = urlreverse('doc_to_iesg', kwargs=dict(name=self.docname))
self.client.login(remote_user='marschairman')
docevent_count_pre = self.doc.docevent_set.count()
mailbox_before = len(outbox)
r = self.client.post(url, dict(confirm="1"))
self.assertEquals(r.status_code, 302)
doc = Document.objects.get(pk=self.doc.pk)
self.assertTrue(doc.get_state('draft-iesg').slug=='pub-req')
self.assertTrue(doc.get_state('draft-stream-ietf').slug=='sub-pub')
self.assertTrue(doc.ad!=None)
self.assertTrue(doc.docevent_set.count() != docevent_count_pre)
self.assertEquals(len(outbox), mailbox_before + 1)
self.assertTrue("Publication has been requested" in outbox[-1]['Subject'])
def setUp(self):
make_test_data()
self.docname='draft-ietf-mars-test'
self.doc = Document.objects.get(name=self.docname)
self.doc.unset_state('draft-iesg')
class RequestPublicationTestCase(django.test.TestCase):
fixtures = ['names']

View file

@ -69,6 +69,7 @@ urlpatterns = patterns('',
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/state/(?P<state_type>iana-action|iana-review)/$', views_draft.change_iana_state, name='doc_change_iana_state'),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/info/$', views_draft.edit_info, name='doc_edit_info'),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/requestresurrect/$', views_draft.request_resurrect, name='doc_request_resurrect'),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/submit-to-iesg/$', views_draft.to_iesg, name='doc_to_iesg'),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/resurrect/$', views_draft.resurrect, name='doc_resurrect'),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/addcomment/$', views_doc.add_comment, name='doc_add_comment'),

View file

@ -318,8 +318,11 @@ def document_main(request, name, rev=None):
label += " (note that intended status is not set)"
actions.append((label, urlreverse('doc_request_publication', kwargs=dict(name=doc.name))))
if doc.get_state_slug() != "expired" and doc.stream_id in ("ietf",) and can_edit and not iesg_state:
actions.append(("Begin IESG Processing", urlreverse('doc_edit_info', kwargs=dict(name=doc.name)) + "?new=1"))
if doc.get_state_slug() != "expired" and doc.stream_id in ("ietf",):
if not iesg_state and can_edit:
actions.append(("Begin IESG Processing", urlreverse('doc_edit_info', kwargs=dict(name=doc.name)) + "?new=1"))
elif can_edit_stream_info and (not iesg_state or iesg_state.slug == 'watching'):
actions.append(("Submit to IESG for Publication", urlreverse('doc_to_iesg', kwargs=dict(name=doc.name))))
return render_to_response("doc/document_draft.html",
dict(doc=doc,

View file

@ -374,6 +374,105 @@ def get_initial_notify(doc):
receivers.append("%s@%s" % (doc.name, settings.TOOLS_SERVER))
return ", ".join(receivers)
def to_iesg(request,name):
""" Submit an IETF stream document to the IESG for publication """
doc = get_object_or_404(Document, docalias__name=name, stream='ietf')
if doc.get_state_slug('draft') == "expired" or doc.get_state_slug('draft-iesg') == 'pub-req' :
raise Http404()
if not is_authorized_in_doc_stream(request.user, doc):
raise Http404()
target_state={
'iesg' : State.objects.get(type='draft-iesg',slug='pub-req'),
'wg' : State.objects.get(type='draft-stream-ietf',slug='sub-pub'),
}
warn={}
if not doc.intended_std_level:
warn['intended_std_level'] = True
if not doc.shepherd:
warn['shepherd'] = True
shepherd_writeup = doc.latest_event(WriteupDocEvent, type="changed_protocol_writeup")
if not shepherd_writeup:
warn['shepherd_writeup'] = True
tags = doc.tags.filter(slug__in=get_tags_for_stream_id(doc.stream_id))
if tags:
warn['tags'] = True
notify = doc.notify
if not notify:
notify = get_initial_notify(doc)
ad = doc.ad or doc.group.ad
if request.method == 'POST':
if request.POST.get("confirm", ""):
save_document_in_history(doc)
login = request.user.get_profile()
changes = []
if not doc.get_state("draft-iesg"):
e = DocEvent()
e.type = "started_iesg_process"
e.by = login
e.doc = doc
e.desc = "IESG process started in state <b>%s</b>" % target_state['iesg'].name
e.save()
if not doc.get_state('draft-iesg')==target_state['iesg']:
doc.set_state(target_state['iesg'])
changes.append("IESG state set to %s" % target_state['iesg'].name)
if not doc.get_state('draft-ietf-stream')==target_state['wg']:
doc.set_state(target_state['wg'])
changes.append("Working group state set to %s" % target_state['wg'].name)
if not doc.ad == ad :
doc.ad = ad
changes.append("Responsible AD changed to %s" % doc.ad)
if not doc.notify == notify :
doc.notify = notify
changes.append("State Change Notice email list changed to %s" % doc.notify)
for c in changes:
e = DocEvent(doc=doc, by=login)
e.desc = c
e.type = "changed_document"
e.save()
# Is this still necessary? I remember Henrik planning to have the model take care of this.
doc.time = datetime.datetime.now()
doc.save()
extra = {}
extra['Cc'] = "%s-chairs@tools.ietf.org, iesg-secretary@ietf.org, %s" % (doc.group.acronym,doc.notify)
send_mail(request=request,
to = doc.ad.email_address(),
frm = login.formatted_email(),
subject = "Publication has been requested for %s-%s" % (doc.name,doc.rev),
template = "doc/submit_to_iesg_email.txt",
context = dict(doc=doc,login=login,url="%s%s"%(settings.IDTRACKER_BASE_URL,doc.get_absolute_url()),),
extra = extra)
return HttpResponseRedirect(doc.get_absolute_url())
return render_to_response('doc/submit_to_iesg.html',
dict(doc=doc,
warn=warn,
target_state=target_state,
ad=ad,
shepherd_writeup=shepherd_writeup,
tags=tags,
notify=notify,
),
context_instance=RequestContext(request))
@role_required('Area Director','Secretariat')
def edit_info(request, name):
"""Edit various Internet Draft attributes, notifying parties as
@ -404,6 +503,18 @@ def edit_info(request, name):
if new_document:
doc.set_state(r['create_in_state'])
# Is setting the WG state here too much of a hidden side-effect?
if r['create_in_state'].slug=='pub-req':
if doc.stream and ( doc.stream.slug=='ietf' ) and doc.group and ( doc.group.type.name=='WG'):
submitted_state = State.objects.get(type='draft-stream-ietf',slug='sub-pub')
doc.set_state(submitted_state)
e = DocEvent()
e.type = "changed_document"
e.by = login
e.doc = doc
e.desc = "Working group state set to %s" % submitted_state.name
e.save()
# fix so Django doesn't barf in the diff below because these
# fields can't be NULL
doc.ad = r['ad']

View file

@ -156,9 +156,9 @@ class NoWorkflowStateForm(StreamDraftForm):
class DraftTagsStateForm(StreamDraftForm):
new_state = forms.ChoiceField(label='State')
weeks = forms.IntegerField(label='Expected weeks in state',required=False)
comment = forms.CharField(widget=forms.Textarea, required=False)
new_state = forms.ChoiceField()
weeks = forms.IntegerField(required=False)
tags = forms.MultipleChoiceField(widget=forms.CheckboxSelectMultiple, required=False)
template = 'ietfworkflows/state_form.html'
@ -167,6 +167,9 @@ class DraftTagsStateForm(StreamDraftForm):
super(DraftTagsStateForm, self).__init__(*args, **kwargs)
self.state = get_state_for_draft(self.draft)
self.fields['new_state'].choices = self.get_states()
self.fields['new_state'].initial = self.state.pk
if self.draft.stream_id == 'ietf':
self.fields['new_state'].help_text = "Only select 'Submitted to IESG for Publication' to correct errors. Use the document's main page to request publication."
if self.is_bound:
for key, value in self.data.items():
if key.startswith('transition_'):

View file

@ -72,7 +72,6 @@ class EditStreamInfoTestCase(django.test.TestCase):
unused = draft.group.unused_tags.values_list("slug", flat=True)
for t in q("input[name=tags]"):
self.assertTrue(t.attrib["value"] not in unused)
self.assertEquals(len(q('form input[type=submit][name=only_tags]')), 1)
# set tags
mailbox_before = len(outbox)
@ -115,10 +114,26 @@ class EditStreamInfoTestCase(django.test.TestCase):
self.assertTrue(t.attrib["value"] not in unused)
self.assertEquals(len(q('select[name=new_state]')), 1)
# set state
old_state = draft.get_state("draft-stream-%s" % draft.stream_id )
new_state = State.objects.get(used=True, type="draft-stream-%s" % draft.stream_id, slug="parked")
self.assertTrue(old_state!=new_state)
mailbox_before = len(outbox)
events_before = draft.docevent_set.count()
# First make sure cancel doesn't change anything
r = self.client.post(url,
dict(comment="some comment",
weeks="10",
tags=[x.pk for x in draft.tags.filter(slug__in=get_tags_for_stream_id(draft.stream_id))],
new_state=new_state.pk,
cancel="1",
))
self.assertEquals(r.status_code, 302)
draft = Document.objects.get(pk=draft.pk)
self.assertEquals(draft.get_state("draft-stream-%s" % draft.stream_id), old_state)
# Set new state
r = self.client.post(url,
dict(comment="some comment",
weeks="10",

View file

@ -70,12 +70,16 @@ def _edit_draft_stream(request, draft, form_class=DraftTagsStateForm):
if request.method == 'POST':
form = form_class(user=user, draft=draft, data=request.POST)
form.request = request
if request.POST.get("cancel",""):
return HttpResponseRedirect(draft.get_absolute_url())
if form.is_valid():
form.save()
if form_class == NoWorkflowStateForm and settings.USE_DB_REDESIGN_PROXY_CLASSES:
return HttpResponseRedirect(urlreverse('ietf.ietfworkflows.views.edit_state', kwargs={ 'name': draft.filename } ))
# This behavior surprises folks. Let's try running awhile without it.
#if form_class == NoWorkflowStateForm and settings.USE_DB_REDESIGN_PROXY_CLASSES:
# return HttpResponseRedirect(urlreverse('ietf.ietfworkflows.views.edit_state', kwargs={ 'name': draft.filename } ))
return HttpResponseRedirect('.')
return HttpResponseRedirect(draft.get_absolute_url())
else:
form = form_class(user=user, draft=draft)
form.request = request

View file

@ -0,0 +1,80 @@
{% extends "base.html" %}
{% block title %}
Publication Request for {{doc.name}}-{{doc.rev}}
{% endblock %}
{% block content %}
<h1>Publication Request for {{doc.name}}-{{doc.rev}}</h1>
<div>
Please verify the following information:
</div>
<div style="margin-left:10px;margin-top:20px;margin-bottom:20px;">
<table>
<tr>
<td>Intended Status Level:</td>
<td>{% if warn.intended_std_level %}<img src="/images/warning.png"/>{% endif %}</td>
<td>{{doc.intended_std_level}}</td>
</tr>
<tr>
<td>Responsible AD:</td>
<td></td>
<td>{{ad}}</td>
</tr>
<tr>
<td>Document Shepherd:</td>
<td>{% if warn.shepherd %}<img src="/images/warning.png"/>{% endif %}</td>
<td>{{doc.shepherd}}</td>
</tr>
<tr>
<td>Shepherd Write-Up Exists:</td>
<td>{% if warn.shepherd_writeup %}<img src="/images/warning.png"/>{% endif %}</td>
<td>{%if shepherd_writeup %}Yes{%else%}No{%endif%}</td>
</tr>
<tr>
<td>Also Notify:</td>
<td></td>
<td>{% if notify %}{{notify}}{%else%}(None){%endif%}</td>
</tr>
<tr>
<td>Annotation Tags:</td>
<td>{% if warn.tags %}<img src="/images/warning.png"/>{% endif %}</td>
<td>{% if not tags %}(None){%else%}{% for tag in tags %}{{ tag }}{% if not forloop.last%}, {%endif%}{% endfor %}{% endif %}</td>
</tr>
</table>
</div>
{% if warn %}
<div style="margin-top:20px; margin-bottom:20px;">
<img src="/images/warning.png"/> indicates the document might not be ready for submission. Please check each instance carefully to see if changes need to be made to the document's state before submitting.
</div>
{% endif %}
<div>
Upon submission:
<ul>
<li> the document will be placed into the IESG '{{target_state.iesg}}' state</li>
<li> the document will be placed into the working group '{{target_state.wg}}' state</li>
{% if not ad == doc.ad %}<li> the responsible AD will be set as above </li>{% endif %}
{% if not notify == doc.notify %}<li> the document's state change notification list will be set as above </li>{% endif %}
<li> an entry will be made noting the publication request in the document's history</li>
<li> an email message will be sent to the working group chairs, the secretariat, and everyone listed above</li>
</div>
<form action="" method="POST">
<input type="submit" name="confirm" value="Request Publication"/>
<input type="submit" name="cancel" value="Cancel"/>
</form>
{% endblock %}

View file

@ -0,0 +1,4 @@
{{login}} has requested publication of {{doc.name}}-{{doc.rev}} as {{doc.intended_std_level}} on behalf of the {{doc.group.acronym|upper}} working group.
Please verify the document's state at {{url}}

View file

@ -17,6 +17,16 @@ table.edit-form-tags tr { vertical-align: top; }
table.edit-form-tags textarea { height: 200px; }
table.edit-form-tags ul { border-width: 0px; padding: 0px 2em; }
table.edit-form-tags ul li { padding: 0px; }
#new-edit-form { clear:both; padding-top: 1em; }
#new-edit-form th { padding-left:0; max-width: 8em;}
#new-edit-form #id_comment { width: 30em; height: 5em; }
#new-edit-form #id_weeks { width: 2em;}
#new-edit-form ul {list-style-type: none; padding-left:0; margin-top:0; margin-bottom:0; }
.rec-state-header { float:left; padding-left:2px; padding-right:.5em; }
.rec-statelist {list-style-type: none; padding-left:0; margin-top:0; margin-bottom:0; }
.rec-state { float:left; padding-right:.5em; }
{% endblock morecss %}
{% block title %}Change state for {{ draft }}{% endblock %}
@ -24,12 +34,6 @@ table.edit-form-tags ul li { padding: 0px; }
{% block content %}
<h1>Change state for {{ draft }}</h1>
<div class="return-to-document">
<p>
<a href="{% url doc_view draft.filename %}">Return to document view</a>
</p>
</div>
{% if state and state.slug == "wg-doc" and not milestones %}
<p>This document is not part of any milestone. You may wish to <a href="{% url wg_edit_milestones acronym=draft.group.acronym %}">add it to one</a>.</p>
{% endif %}
@ -42,42 +46,6 @@ submitted to IESG for publication, you may wish to
milestone{{ milestones|pluralize }}</a>.</p>
{% endif %}
<table class="ietf-table" style="width: 100%;">
<tr>
<th>Current stream</th>
<th>Current state</th>
<th>Annotation tags</th>
</tr>
<tr><td style="width: 25%;">
{{ stream.name|default:"None" }}
</td>
<td style="width: 25%;">
{{ state.name|default:"None" }}
</td><td>
<ul style="list-style-type: none; padding: 0px;">
{% for tag in tags%}
<li>{{ tag.annotation_tag }}</li>
{% endfor %}
</ul>
</td></tr></table>
<br />
{{ form }}
<br />
<strong>State history</strong>
<table class="ietf-table state-history" style="width: 100%">
{% if history %}
<tr><th>Date</th><th>Person</th><th>Change</th><th>Comment</th></tr>
{% for baseentry in history %}
{% with baseentry.get_real_instance as entry %}
<tr class="{% cycle oddrow,evenrow %}"><td>{{ entry.date }}</td><td>{{ entry.person }}</td>
<td>{{ entry.describe_change|safe }}</td><td>{{ entry.comment }}</td>
</tr>
{% endwith %}
{% endfor %}
{% else %}
<tr><td>There is no state history for this document.</td></th>
{% endif %}
</table>
{% endblock %}

View file

@ -1,73 +1,32 @@
<form action="" method="post">
<table class="ietf-table edit-form" style="width: 100%;">
<tr>
<th>1. Input information about change</th>
<th>2. Change annotation tags if needed</th>
</tr>
<tr style="vertical-align: top;"><td style="width: 50%;">
<div class="field{% if form.comment.errors %} error{% endif %}">
{{ form.comment.errors }}
Comment: <br/>
<textarea name="comment">{{ form.data.comment }}</textarea>
</div>
<div class="field{% if form.weeks.errors %} error{% endif %}">
{{ form.weeks.errors }}
Estimated time in next status:<br />
<input type="text" name="weeks" value="{{ form.data.weeks }}" /> (in weeks)
</div>
</td><td>
<div class="field">
{{ form.tags }}
</div>
</td></tr>
</table>
<br />
<table class="ietf-table edit-form edit-form-tags" style="width: 100%;">
<tr>
<th>3. Select one action</th>
</tr>
<tr><td>
<div class="only-tags field">
<ul>
<li><input type="submit" name="only_tags" value="Update annotation tags" /> State remains unchanged: <strong>{{ form.state.name }}</strong></li>
</ul>
</div>
{% with form.get_transitions as transitions %}
{% if transitions %}
<ul>
{% for transition in transitions %}
<li class="{% cycle oddrow,evenrow %}">
<input type="submit" name="transition_{{ transition.pk }}" value="{{ transition.name }}" />
Changes state to: <strong>{{ transition.destination.name }}</strong>
</li>
{% endfor %}
{% endif %}
</ul>
{% endwith %}
<div class="rec-states">
{% with form.get_next_states as next_states %}
{% if next_states %}
<ul>
Jump to the next state:
{% for state in next_states %}
<input type="submit" name="new_state_{{ state.pk }}" value="{{ state.name }}" />
<span class="rec-state-header">Recommended next state{{next_states|pluralize}}:</span>
<ul class="rec-statelist">
{% for state in next_states %}
<li class="rec-state">'{{ state.name }}'{% if not forloop.last %}, {% endif %}</li>
{% endfor %}
</ul>
{% endif %}
</ul>
{% endwith %}
</div>
<div class="free-change field{% if form.new_state.errors %} error{% endif %}">
{{ form.new_state.errors }}
Change to another state:
<select name="new_state">
{% for value, name in form.get_states %}
<option value="{{ value }}">{{ name }}</option>
{% endfor %}
</select>
<input type="submit" name="change" value="Change state" />
</div>
</td></tr>
<table id="new-edit-form">
{% for field in form.visible_fields %}
<tr>
<th>{{ field.label_tag }}:</th>
<td>{{ field }}
{% if field.help_text %}<div class="help">{{ field.help_text }}</div>{% endif %}
{{ field.errors }}
</td>
</tr>
{% endfor %}
</table>
<div class="actions">
<input type="submit" name="save" value="Save" />
<input type="submit" name="cancel" value="Cancel" />
</div>
</form>