From 63139ab860ba2ca31d6f93fc3855ed0820374987 Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Mon, 9 Sep 2013 18:25:44 +0000 Subject: [PATCH] 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 --- ietf/doc/tests_draft.py | 78 +++++++++++++ ietf/doc/urls.py | 1 + ietf/doc/views_doc.py | 7 +- ietf/doc/views_draft.py | 111 +++++++++++++++++++ ietf/ietfworkflows/forms.py | 7 +- ietf/ietfworkflows/tests.py | 19 +++- ietf/ietfworkflows/views.py | 10 +- ietf/templates/doc/submit_to_iesg.html | 80 +++++++++++++ ietf/templates/doc/submit_to_iesg_email.txt | 4 + ietf/templates/ietfworkflows/state_edit.html | 52 ++------- ietf/templates/ietfworkflows/state_form.html | 85 ++++---------- 11 files changed, 340 insertions(+), 114 deletions(-) create mode 100644 ietf/templates/doc/submit_to_iesg.html create mode 100644 ietf/templates/doc/submit_to_iesg_email.txt diff --git a/ietf/doc/tests_draft.py b/ietf/doc/tests_draft.py index 0fc63b589..03ca24c72 100644 --- a/ietf/doc/tests_draft.py +++ b/ietf/doc/tests_draft.py @@ -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'] diff --git a/ietf/doc/urls.py b/ietf/doc/urls.py index e68517f71..8a8cb2ae7 100644 --- a/ietf/doc/urls.py +++ b/ietf/doc/urls.py @@ -69,6 +69,7 @@ urlpatterns = patterns('', url(r'^(?P[A-Za-z0-9._+-]+)/edit/state/(?Piana-action|iana-review)/$', views_draft.change_iana_state, name='doc_change_iana_state'), url(r'^(?P[A-Za-z0-9._+-]+)/edit/info/$', views_draft.edit_info, name='doc_edit_info'), url(r'^(?P[A-Za-z0-9._+-]+)/edit/requestresurrect/$', views_draft.request_resurrect, name='doc_request_resurrect'), + url(r'^(?P[A-Za-z0-9._+-]+)/edit/submit-to-iesg/$', views_draft.to_iesg, name='doc_to_iesg'), url(r'^(?P[A-Za-z0-9._+-]+)/edit/resurrect/$', views_draft.resurrect, name='doc_resurrect'), url(r'^(?P[A-Za-z0-9._+-]+)/edit/addcomment/$', views_doc.add_comment, name='doc_add_comment'), diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index 9be513d0d..6328fcc1c 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -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, diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py index 8095155ba..24ae62f08 100644 --- a/ietf/doc/views_draft.py +++ b/ietf/doc/views_draft.py @@ -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 %s" % 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'] diff --git a/ietf/ietfworkflows/forms.py b/ietf/ietfworkflows/forms.py index 7f9dd1fdf..b4db79bda 100644 --- a/ietf/ietfworkflows/forms.py +++ b/ietf/ietfworkflows/forms.py @@ -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_'): diff --git a/ietf/ietfworkflows/tests.py b/ietf/ietfworkflows/tests.py index 56721b156..ad467a9e7 100644 --- a/ietf/ietfworkflows/tests.py +++ b/ietf/ietfworkflows/tests.py @@ -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", diff --git a/ietf/ietfworkflows/views.py b/ietf/ietfworkflows/views.py index 26dc6f859..c976c634b 100644 --- a/ietf/ietfworkflows/views.py +++ b/ietf/ietfworkflows/views.py @@ -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 diff --git a/ietf/templates/doc/submit_to_iesg.html b/ietf/templates/doc/submit_to_iesg.html new file mode 100644 index 000000000..6dfa0561c --- /dev/null +++ b/ietf/templates/doc/submit_to_iesg.html @@ -0,0 +1,80 @@ +{% extends "base.html" %} + +{% block title %} +Publication Request for {{doc.name}}-{{doc.rev}} +{% endblock %} + +{% block content %} +

Publication Request for {{doc.name}}-{{doc.rev}}

+ +
+Please verify the following information: +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Intended Status Level:{% if warn.intended_std_level %}{% endif %}{{doc.intended_std_level}}
Responsible AD:{{ad}}
Document Shepherd:{% if warn.shepherd %}{% endif %}{{doc.shepherd}}
Shepherd Write-Up Exists:{% if warn.shepherd_writeup %}{% endif %}{%if shepherd_writeup %}Yes{%else%}No{%endif%}
Also Notify:{% if notify %}{{notify}}{%else%}(None){%endif%}
Annotation Tags:{% if warn.tags %}{% endif %}{% if not tags %}(None){%else%}{% for tag in tags %}{{ tag }}{% if not forloop.last%}, {%endif%}{% endfor %}{% endif %}
+
+ + + +{% if warn %} +
+ 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. +
+{% endif %} + +
+Upon submission: +
    +
  • the document will be placed into the IESG '{{target_state.iesg}}' state
  • +
  • the document will be placed into the working group '{{target_state.wg}}' state
  • +{% if not ad == doc.ad %}
  • the responsible AD will be set as above
  • {% endif %} +{% if not notify == doc.notify %}
  • the document's state change notification list will be set as above
  • {% endif %} +
  • an entry will be made noting the publication request in the document's history
  • +
  • an email message will be sent to the working group chairs, the secretariat, and everyone listed above
  • +
+ +
+ + +
+ +{% endblock %} diff --git a/ietf/templates/doc/submit_to_iesg_email.txt b/ietf/templates/doc/submit_to_iesg_email.txt new file mode 100644 index 000000000..807cdd351 --- /dev/null +++ b/ietf/templates/doc/submit_to_iesg_email.txt @@ -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}} + diff --git a/ietf/templates/ietfworkflows/state_edit.html b/ietf/templates/ietfworkflows/state_edit.html index 19f0265a4..5cef5eb00 100644 --- a/ietf/templates/ietfworkflows/state_edit.html +++ b/ietf/templates/ietfworkflows/state_edit.html @@ -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 %}

Change state for {{ draft }}

- - {% if state and state.slug == "wg-doc" and not milestones %}

This document is not part of any milestone. You may wish to add it to one.

{% endif %} @@ -42,42 +46,6 @@ submitted to IESG for publication, you may wish to milestone{{ milestones|pluralize }}.

{% endif %} - - - - - - - -
Current streamCurrent stateAnnotation tags
- {{ stream.name|default:"None" }} - - {{ state.name|default:"None" }} - -
    - {% for tag in tags%} -
  • {{ tag.annotation_tag }}
  • - {% endfor %} -
-
-
- {{ form }} -
-State history - - {% if history %} - - {% for baseentry in history %} - {% with baseentry.get_real_instance as entry %} - - - - {% endwith %} - {% endfor %} - {% else %} - - {% endif %} -
DatePersonChangeComment
{{ entry.date }}{{ entry.person }}{{ entry.describe_change|safe }}{{ entry.comment }}
There is no state history for this document.
{% endblock %} diff --git a/ietf/templates/ietfworkflows/state_form.html b/ietf/templates/ietfworkflows/state_form.html index 15fa7a0fd..a96b20f3e 100644 --- a/ietf/templates/ietfworkflows/state_form.html +++ b/ietf/templates/ietfworkflows/state_form.html @@ -1,73 +1,32 @@
- - - - - - -
1. Input information about change2. Change annotation tags if needed
-
- {{ form.comment.errors }} - Comment:
- -
-
- {{ form.weeks.errors }} - Estimated time in next status:
- (in weeks) -
-
-
- {{ form.tags }} -
-
- -
- - - - - - +
3. Select one action
-
-
    -
  • State remains unchanged: {{ form.state.name }}
  • -
-
- {% with form.get_transitions as transitions %} - {% if transitions %} -
    - {% for transition in transitions %} -
  • - - Changes state to: {{ transition.destination.name }} -
  • - {% endfor %} - {% endif %} -
- {% endwith %} +
{% with form.get_next_states as next_states %} {% if next_states %} -
    - Jump to the next state: - {% for state in next_states %} - + Recommended next state{{next_states|pluralize}}: +
      + {% for state in next_states %} +
    • '{{ state.name }}'{% if not forloop.last %}, {% endif %}
    • {% endfor %} +
    {% endif %} -
{% endwith %} +
-
- {{ form.new_state.errors }} - Change to another state: - - -
-
+ {% for field in form.visible_fields %} + + + + + {% endfor %}
{{ field.label_tag }}:{{ field }} + {% if field.help_text %}
{{ field.help_text }}
{% endif %} + + {{ field.errors }} +
+
+ + +