Merged in [16126] from housley@vigilsec.com:

Allow Secretariat to handle downrefs when they approve a document
 - Legacy-Id: 16134
Note: SVN reference [16126] has been migrated to Git commit 8c7e75101d
This commit is contained in:
Henrik Levkowetz 2019-04-08 17:02:02 +00:00
commit 33e0f8c35d
7 changed files with 190 additions and 10 deletions

View file

@ -702,6 +702,60 @@ class ApproveBallotTests(TestCase):
self.assertEqual(ballot.ballotpositiondocevent_set.count(),0)
self.assertNotEqual(old_ballot_id, ballot.id)
def test_ballot_downref_approve(self):
ad = Person.objects.get(name="Areað Irector")
draft = IndividualDraftFactory(ad=ad, intended_std_level_id='ps')
draft.set_state(State.objects.get(used=True, type="draft-iesg", slug="ann")) # make sure it's approved
LastCallDocEvent.objects.create(
by=Person.objects.get(name='(System)'),
type='sent_last_call',
doc=draft,
rev=draft.rev,
desc='issued last call',
expires = datetime.datetime.now()-datetime.timedelta(days=14) )
WriteupDocEvent.objects.create(
by=Person.objects.get(name='(System)'),
doc=draft,
rev=draft.rev,
type='changed_last_call_text',
desc='Last call announcement was changed',
text='this is simple last call text.' )
rfc = IndividualRfcFactory.create(
stream_id='ise',
other_aliases=['rfc6666',],
states=[('draft','rfc'),('draft-iesg','pub')],
std_level_id='inf', )
url = urlreverse('ietf.doc.views_ballot.approve_downrefs', kwargs=dict(name=draft.name))
# Only Secretariat can use this URL
login_testing_unauthorized(self, "ad", url)
r = self.client.get(url)
self.assertEqual(r.status_code, 403)
self.assertTrue("Restricted to role Secretariat" in r.content)
# There are no downrefs, the page should say so
login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue("No downward references for" in r.content)
# Add a downref, the page should ask if it should be added to the registry
rel = draft.relateddocument_set.create(target=rfc.docalias_set.get(name='rfc6666'),relationship_id='refnorm')
d = [rdoc for rdoc in draft.relateddocument_set.all() if rel.is_approved_downref()]
original_len = len(d)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue("normatively references rfc6666" in r.content)
# POST with the downref checked
r = self.client.post(url, dict(checkboxes=rel.pk))
self.assertEqual(r.status_code, 302)
# Confirm an entry was added to the downref registry
d = [rdoc for rdoc in draft.relateddocument_set.all() if rel.is_approved_downref()]
self.assertTrue(len(d) > original_len, "The downref approval was not added")
class MakeLastCallTests(TestCase):
def test_make_last_call(self):
ad = Person.objects.get(user__username="ad")

View file

@ -118,6 +118,7 @@ urlpatterns = [
url(r'^%(name)s/edit/ballotrfceditornote/$' % settings.URL_REGEXPS, views_ballot.ballot_rfceditornote),
url(r'^%(name)s/edit/approvaltext/$' % settings.URL_REGEXPS, views_ballot.ballot_approvaltext),
url(r'^%(name)s/edit/approveballot/$' % settings.URL_REGEXPS, views_ballot.approve_ballot),
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),

View file

@ -16,7 +16,7 @@ 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 )
LastCallDocEvent, WriteupDocEvent, IESG_SUBSTATE_TAGS, RelatedDocument )
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,
@ -769,6 +769,7 @@ def ballot_approvaltext(request, name):
need_intended_status=need_intended_status,
))
@role_required('Secretariat')
def approve_ballot(request, name):
"""Approve ballot, sending out announcement, changing state."""
@ -872,7 +873,11 @@ def approve_ballot(request, name):
msg.save()
msg.related_docs.add(doc)
return HttpResponseRedirect(doc.get_absolute_url())
downrefs = [rel for rel in doc.relateddocument_set.all() if rel.is_downref() and not rel.is_approved_downref()]
if not downrefs:
return HttpResponseRedirect(doc.get_absolute_url())
else:
return HttpResponseRedirect(doc.get_absolute_url()+'edit/approvedownrefs/')
return render(request, 'doc/ballot/approve_ballot.html',
dict(doc=doc,
@ -880,6 +885,64 @@ def approve_ballot(request, name):
announcement=announcement))
class ApproveDownrefsForm(forms.Form):
checkboxes = forms.ModelMultipleChoiceField(
widget = forms.CheckboxSelectMultiple,
queryset = RelatedDocument.objects.none(), )
def __init__(self, queryset, *args, **kwargs):
super(ApproveDownrefsForm, self).__init__(*args, **kwargs)
self.fields['checkboxes'].queryset = queryset
def clean(self):
if 'checkboxes' not in self.cleaned_data:
raise forms.ValidationError("No RFCs were selected")
@role_required('Secretariat')
def approve_downrefs(request, name):
"""Document ballot was just approved; add the checked downwared references to the downref registry."""
doc = get_object_or_404(Document, docalias__name=name)
if not doc.get_state("draft-iesg"):
raise Http404
login = request.user.person
downrefs_to_rfc = [rel for rel in doc.relateddocument_set.all() if rel.is_downref() and not rel.is_approved_downref() and rel.target.document.is_rfc()]
downrefs_to_rfc_qs = RelatedDocument.objects.filter(pk__in=[r.pk for r in downrefs_to_rfc])
last_call_text = doc.latest_event(WriteupDocEvent, type="changed_last_call_text").text.strip()
if request.method == 'POST':
form = ApproveDownrefsForm(downrefs_to_rfc_qs, request.POST)
if form.is_valid():
for rel in form.cleaned_data['checkboxes']:
RelatedDocument.objects.create(source=rel.source,
target=rel.target, relationship_id='downref-approval')
c = DocEvent(type="downref_approved", doc=rel.source,
rev=rel.source.rev, by=login)
c.desc = "Downref to RFC %s approved by Last Call for %s-%s" % (
rel.target.document.rfc_number(), rel.source, rel.source.rev)
c.save()
c = DocEvent(type="downref_approved", doc=rel.target.document,
rev=rel.target.document.rev, by=login)
c.desc = "Downref to RFC %s approved by Last Call for %s-%s" % (
rel.target.document.rfc_number(), rel.source, rel.source.rev)
c.save()
return HttpResponseRedirect(doc.get_absolute_url())
else:
form = ApproveDownrefsForm(downrefs_to_rfc_qs)
return render(request, 'doc/ballot/approve_downrefs.html',
dict(doc=doc,
approve_downrefs_form=form,
last_call_text=last_call_text,
downrefs_to_rfc=downrefs_to_rfc))
class MakeLastCallForm(forms.Form):
last_call_sent_date = forms.DateField(required=True)
last_call_expiration_date = forms.DateField(required=True)

View file

@ -12,7 +12,7 @@ import debug # pyflakes:ignore
from ietf.doc.models import DocEvent, BallotPositionDocEvent, TelechatDocEvent
from ietf.doc.models import Document, DocAlias, State, RelatedDocument
from ietf.doc.factories import WgDraftFactory, IndividualDraftFactory, ConflictReviewFactory, BaseDocumentFactory, CharterFactory, WgRfcFactory
from ietf.doc.factories import WgDraftFactory, IndividualDraftFactory, ConflictReviewFactory, BaseDocumentFactory, CharterFactory, WgRfcFactory, IndividualRfcFactory
from ietf.doc.utils import create_ballot_if_not_open
from ietf.group.factories import RoleFactory, GroupFactory
from ietf.group.models import Group, GroupMilestone, Role
@ -90,7 +90,9 @@ class IESGTests(TestCase):
class IESGAgendaTests(TestCase):
def setUp(self):
mars = GroupFactory(acronym='mars',parent=Group.objects.get(acronym='farfut'))
WgDraftFactory(name='draft-ietf-mars-test',group=mars)
wgdraft = WgDraftFactory(name='draft-ietf-mars-test', group=mars, intended_std_level_id='ps')
rfc = IndividualRfcFactory.create(stream_id='irtf', other_aliases=['rfc6666',], states=[('draft','rfc'),('draft-iesg','pub')], std_level_id='inf', )
wgdraft.relateddocument_set.create(target=rfc.docalias_set.get(name='rfc6666'), relationship_id='refnorm')
ise_draft = IndividualDraftFactory(name='draft-imaginary-independent-submission')
ise_draft.stream = StreamName.objects.get(slug="ise")
ise_draft.save_with_history([DocEvent(doc=ise_draft, rev=ise_draft.rev, type="changed_stream", by=Person.objects.get(user__username="secretary"), desc="Test")])
@ -364,8 +366,14 @@ class IESGAgendaTests(TestCase):
self.assertTrue(d.group.name in unicontent(r), "%s not in response" % k)
self.assertTrue(d.group.acronym in unicontent(r), "%s acronym not in response" % k)
else:
self.assertTrue(d.name in unicontent(r), "%s not in response" % k)
self.assertTrue(d.title in unicontent(r), "%s title not in response" % k)
if d.type_id == "draft" and d.name == "draft-ietf-mars-test":
self.assertTrue(d.name in unicontent(r), "%s not in response" % k)
self.assertTrue(d.title in unicontent(r), "%s title not in response" % k)
self.assertTrue("Has downref: Yes" in unicontent(r), "%s downref not in response" % k)
self.assertTrue("Add rfc6666" in unicontent(r), "%s downref not in response" % k)
else:
self.assertTrue(d.name in unicontent(r), "%s not in response" % k)
self.assertTrue(d.title in unicontent(r), "%s title not in response" % k)
def test_agenda_package(self):
url = urlreverse("ietf.iesg.views.agenda_package")

View file

@ -6,7 +6,7 @@ import debug # pyflakes:ignore
from django.urls import reverse
from ietf.doc.factories import WgDraftFactory, CharterFactory
from ietf.doc.factories import WgDraftFactory, IndividualRfcFactory, CharterFactory
from ietf.doc.models import BallotDocEvent, BallotType, BallotPositionDocEvent
from ietf.doc.utils import update_telechat, create_ballot_if_not_open
from ietf.utils.test_utils import TestCase
@ -58,6 +58,26 @@ class SecrTelechatTestCase(TestCase):
self.assertEqual(q("#telechat-positions-table").find("th:contains('Recuse')").length,1)
self.assertEqual(q("#telechat-positions-table").find("th:contains('No Record')").length,1)
def test_doc_detail_draft_with_downref(self):
ad = Person.objects.get(user__username="ad")
draft = WgDraftFactory(ad=ad, intended_std_level_id='ps', states=[('draft-iesg','pub-req'),])
rfc = IndividualRfcFactory.create(stream_id='irtf', other_aliases=['rfc6666',],
states=[('draft','rfc'),('draft-iesg','pub')], std_level_id='inf', )
draft.relateddocument_set.create(target=rfc.docalias_set.get(name='rfc6666'),
relationship_id='refnorm')
create_ballot_if_not_open(None, draft, ad, 'approve')
d = get_next_telechat_date()
date = d.strftime('%Y-%m-%d')
by=Person.objects.get(name="(System)")
update_telechat(None, draft, by, d)
url = reverse('ietf.secr.telechat.views.doc_detail', kwargs={'date':date, 'name':draft.name})
self.client.login(username="secretary", password="secretary+password")
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertTrue("Has downref: Yes" in response.content)
self.assertTrue("Add rfc6666" in response.content)
self.assertTrue("to downref registry" in response.content)
def test_doc_detail_draft_invalid(self):
'''Test using a document not on telechat agenda'''
draft = WgDraftFactory(states=[('draft-iesg','pub-req'),])

View file

@ -16,11 +16,11 @@
{% buttons %}
{% if action == "to_announcement_list" %}
<button class="btn btn-warningprimary" type="submit">Notify RFC Editor, send announcement & close ballot</button>
<button class="btn btn-primary" type="submit">Notify RFC Editor, send announcement & close ballot</button>
{% elif action == "to_rfc_editor" %}
<button class="btn btn-warning" type="submit">Email RFC Editor & close ballot</button>
<button class="btn btn-primary" type="submit">Email RFC Editor & close ballot</button>
{% elif action == "do_not_publish" %}
<button class="btn btn-warning" type="submit">Email RFC Editor (DNP) & close ballot"/>
<button class="btn btn-primary" type="submit">Email RFC Editor (DNP) & close ballot"/>
{% endif %}
{% endbuttons %}
</form>

View file

@ -0,0 +1,34 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2019, All Rights Reserved #}
{% load origin %}
{% load bootstrap3 %}
{% block title %}Approve downward references for {{ doc }}{% endblock %}
{% block content %}
{% origin %}
<h1>Approve downward references<br><small>The ballot for <a href="{% url "ietf.doc.views_doc.document_main" name=doc.canonical_name %}">{{ doc }}</a> was just approved</small></h1>
{% if not downrefs_to_rfc %}
<p>No downward references for <a href="{% url "ietf.doc.views_doc.document_main" name=doc.canonical_name %}">{{ doc }}</a></p>
{% else %}
<p>Add downward references to RFCs to the downref registry, if they were identified in the IETF Last Call and approved by the Sponsoring Area Director.<p>
<p><b>Last Call text for this document:</b><p>
<pre>
{{ last_call_text }}
</pre>
<p><b>This document has downward references to the following RFCs.<br>Which downward references, if any, are to be added to the downref registry?</b></p>
<form action="" method="post">
{% csrf_token %}
{% bootstrap_form approve_downrefs_form %}
{% buttons %}
<p>
<a class="btn btn-primary" href="{% url "ietf.doc.views_doc.document_main" name=doc.canonical_name %}">Add no downref entries</a>
<button type="submit" class="btn btn-warning" value="Save checked downrefs">Add checked downref entries</button>
</p>
{% endbuttons %}
</form>
{% endif %}
{% endblock %}