Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions ietf/doc/templatetags/ietf_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -1018,9 +1018,10 @@ def is_in_stream(doc):
return True
return False


@register.filter
def can_issue_ietf_call_for_adoption(doc):
return all(
def is_doc_ietf_adoptable(doc):
return doc.stream_id is None or all(
[
doc.stream_id == "ietf",
doc.get_state_slug("draft-stream-ietf")
Expand All @@ -1039,6 +1040,7 @@ def can_issue_ietf_call_for_adoption(doc):
]
)


@register.filter
def can_issue_ietf_wg_lc(doc):
return all(
Expand Down
91 changes: 55 additions & 36 deletions ietf/doc/tests_draft.py
Original file line number Diff line number Diff line change
Expand Up @@ -2287,15 +2287,50 @@ def test_issue_wg_call_for_adoption_form(self):
)

def test_issue_wg_call_for_adoption(self):
def _assert_rejected(testcase, doc, person):
def _assert_rejected(testcase, doc, person, group=None):
target_acronym = group.acronym if group is not None else doc.group.acronym
url = urlreverse(
"ietf.doc.views_draft.issue_wg_call_for_adoption", kwargs=dict(name=doc.name)
"ietf.doc.views_draft.issue_wg_call_for_adoption",
kwargs=dict(name=doc.name, acronym=target_acronym),
)
login_testing_unauthorized(testcase, person.user.username, url)
r = testcase.client.get(url)
testcase.assertEqual(r.status_code, 404)
testcase.assertEqual(r.status_code, 403)
testcase.client.logout()

def _verify_call_issued(testcase, doc, chair_role):
url = urlreverse(
"ietf.doc.views_draft.issue_wg_call_for_adoption",
kwargs=dict(name=doc.name, acronym=chair_role.group.acronym),
)
login_testing_unauthorized(testcase, chair_role.person.user.username, url)
r = testcase.client.get(url)
testcase.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
postdict = dict()
postdict["end_date"] = q("input#id_end_date").attr("value")
postdict["to"] = q("input#id_to").attr("value")
cc = q("input#id_cc").attr("value")
if cc is not None:
postdict["cc"] = cc
postdict["subject"] = q("input#id_subject").attr("value")
postdict["body"] = q("textarea#id_body").text()
empty_outbox()
r = testcase.client.post(
url,
postdict,
)
testcase.assertEqual(r.status_code, 302)
doc.refresh_from_db()
self.assertEqual(doc.get_state_slug("draft-stream-ietf"), "c-adopt")
self.assertEqual(len(outbox), 2)
self.assertIn(f"{doc.group.acronym}@ietf.org", outbox[1]["To"])
self.assertIn("Call for adoption", outbox[1]["Subject"])
body = get_payload_text(outbox[1])
self.assertIn("disclosure obligations", body)
self.client.logout()
return doc

already_rfc = WgDraftFactory(states=[("draft", "rfc")])
rfc = WgRfcFactory(group=already_rfc.group)
already_rfc.relateddocument_set.create(relationship_id="became_rfc",target=rfc)
Expand All @@ -2307,33 +2342,25 @@ def _assert_rejected(testcase, doc, person):
inwglc_doc = WgDraftFactory(states=[("draft-stream-ietf", "wg-lc")])
inwglc_chair = RoleFactory(name_id="chair", group=inwglc_doc.group).person
_assert_rejected(self, inwglc_doc, inwglc_chair)
ind_doc = IndividualDraftFactory()
_assert_rejected(self, ind_doc, rg_chair, rg_doc.group)

# Successful call issued for doc already in WG
doc = WgDraftFactory(states=[("draft-stream-ietf","wg-cand")])
chair = RoleFactory(name_id="chair",group=doc.group).person
url = urlreverse("ietf.doc.views_draft.issue_wg_call_for_adoption", kwargs=dict(name=doc.name))
login_testing_unauthorized(self, chair.user.username, url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
postdict = dict()
postdict["end_date"] = q("input#id_end_date").attr("value")
postdict["to"] = q("input#id_to").attr("value")
cc = q("input#id_cc").attr("value")
if cc is not None:
postdict["cc"] = cc
postdict["subject"] = q("input#id_subject").attr("value")
postdict["body"] = q("textarea#id_body").text()
empty_outbox()
r = self.client.post(
url,
postdict,
)
self.assertEqual(r.status_code, 302)
chair_role = RoleFactory(name_id="chair",group=doc.group)
_ = _verify_call_issued(self, doc, chair_role)

# Successful call issued for doc not yet in WG
doc = IndividualDraftFactory()
chair_role = RoleFactory(name_id="chair",group__type_id="wg")
doc = _verify_call_issued(self, doc, chair_role)
self.assertEqual(doc.group, chair_role.group)
self.assertEqual(doc.stream_id, "ietf")
self.assertEqual(doc.get_state_slug("draft-stream-ietf"), "c-adopt")
self.assertEqual(len(outbox), 2)
self.assertIn(f"{doc.group.acronym}@ietf.org", outbox[1]["To"])
self.assertIn("Call for adoption", outbox[1]["Subject"])
body = get_payload_text(outbox[1])
self.assertIn("disclosure obligations", body)
self.assertCountEqual(
doc.docevent_set.values_list("type", flat=True),
["changed_state", "changed_group", "changed_stream", "new_revision"]
)

def test_pubreq_validation(self):
role = RoleFactory(name_id='chair',group__acronym='mars',group__list_email='mars-wg@ietf.org',person__user__username='marschairman',person__name='WG Cháir Man')
Expand Down Expand Up @@ -2606,14 +2633,6 @@ def test_ask_about_ietf_adoption_call(self):
self.assertEqual(r.status_code, 200)
r = self.client.post(url, {"group": group.pk})
self.assertEqual(r.status_code, 302)
doc.refresh_from_db()
self.assertEqual(doc.group, group)
self.assertEqual(doc.stream_id, "ietf")
self.assertEqual(doc.get_state_slug("draft-stream-ietf"), "wg-cand")
self.assertCountEqual(
doc.docevent_set.values_list("type", flat=True),
["changed_state", "changed_group", "changed_stream", "new_revision"],
)

def test_offer_wg_action_helpers(self):
def _assert_view_presents_buttons(testcase, response, expected):
Expand Down
3 changes: 1 addition & 2 deletions ietf/doc/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,7 @@
url(r'^%(name)s/edit/requestpublication/$' % settings.URL_REGEXPS, views_draft.request_publication),
url(r'^%(name)s/edit/ask-about-ietf-adoption/$' % settings.URL_REGEXPS, views_draft.ask_about_ietf_adoption_call),
url(r'^%(name)s/edit/adopt/$' % settings.URL_REGEXPS, views_draft.adopt_draft),
url(r'^%(name)s/edit/issue-wg-call-for-adoption/$' % settings.URL_REGEXPS, views_draft.issue_wg_call_for_adoption),

url(r'^%(name)s/edit/issue-wg-call-for-adoption/%(acronym)s/$' % settings.URL_REGEXPS, views_draft.issue_wg_call_for_adoption),

url(r'^%(name)s/edit/release/$' % settings.URL_REGEXPS, views_draft.release_draft),
url(r'^%(name)s/edit/state/(?P<state_type>draft-stream-[a-z]+)/$' % settings.URL_REGEXPS, views_draft.change_stream_state),
Expand Down
70 changes: 34 additions & 36 deletions ietf/doc/views_draft.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
email_iesg_processing_document, email_ad_approved_doc,
email_iana_expert_review_state_changed )
from ietf.doc.storage_utils import retrieve_bytes, store_bytes
from ietf.doc.templatetags.ietf_filters import can_issue_ietf_call_for_adoption
from ietf.doc.templatetags.ietf_filters import is_doc_ietf_adoptable
from ietf.doc.utils import ( add_state_change_event, can_adopt_draft, can_unadopt_draft,
get_tags_for_stream_id, nice_consensus, update_action_holders,
update_reminder, update_telechat, make_notify_changed_event, get_initial_notify,
Expand Down Expand Up @@ -2075,27 +2075,45 @@ def clean(self):
return cleaned_data

@login_required
def issue_wg_call_for_adoption(request, name):
def issue_wg_call_for_adoption(request, name, acronym):
doc = get_object_or_404(Document, name=name)

if doc.stream_id != "ietf":
raise Http404
if doc.group is None or doc.group.type_id != "wg":
raise Http404
if not can_issue_ietf_call_for_adoption(doc):
raise Http404

if not is_authorized_in_doc_stream(request.user, doc):
permission_denied(request, "You don't have permission to access this page.")
group = Group.objects.filter(acronym=acronym, type_id="wg").first()
reject = False
if group is None or doc.type_id != "draft" or not is_doc_ietf_adoptable(doc):
reject = True
if doc.stream is None:
if not can_adopt_draft(request.user, doc):
reject = True
elif doc.stream_id != "ietf":
reject = True
else: # doc.stream_id == "ietf"
if not is_authorized_in_doc_stream(request.user, doc):
reject = True
if reject:
raise permission_denied(request, f"You can't issue a {acronym} wg call for adoption for this document.")

if request.method == "POST":
form = IssueCallForAdoptionForm(request.POST)
if form.is_valid():
# Intentionally not changing tags or adding a comment
# those things can be done with other workflows
by = request.user.person
prev_state = doc.get_state("draft-stream-ietf")

events = []
if doc.stream_id != "ietf":
stream = StreamName.objects.get(slug="ietf")
doc.stream = stream
e = DocEvent(type="changed_stream", doc=doc, rev=doc.rev, by=by)
e.desc = f"Changed stream to <b>{stream.name}</b>" # Propogates embedding html in DocEvent.desc for consistency
e.save()
events.append(e)
if doc.group != group:
doc.group = group
e = DocEvent(type="changed_group", doc=doc, rev=doc.rev, by=by)
e.desc = f"Changed group to <b>{group.name} ({group.acronym.upper()})</b>" # Even if it makes the cats cry
e.save()
events.append(e)
prev_state = doc.get_state("draft-stream-ietf")
c_adopt_state = State.objects.get(type="draft-stream-ietf", slug="c-adopt")
doc.set_state(c_adopt_state)
e = add_state_change_event(doc, by, prev_state, c_adopt_state)
Expand All @@ -2104,9 +2122,9 @@ def issue_wg_call_for_adoption(request, name):
update_reminder(
doc, "stream-s", e, datetime_from_date(end_date, DEADLINE_TZINFO)
)
doc.save_with_history(events)
email_stream_state_changed(request, doc, prev_state, c_adopt_state, by)
email_wg_call_for_adoption_issued(request, doc, end_date)
doc.save_with_history(events)
return redirect("ietf.doc.views_doc.document_main", name=doc.name)
else:
end_date = date_today(DEADLINE_TZINFO) + datetime.timedelta(days=14)
Expand All @@ -2115,6 +2133,7 @@ def issue_wg_call_for_adoption(request, name):
"doc/mail/wg_call_for_adoption_issued.txt",
dict(
doc=doc,
group=group,
end_date=end_date,
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(),
wg_list=doc.group.list_email,
Expand Down Expand Up @@ -2172,29 +2191,8 @@ def ask_about_ietf_adoption_call(request, name):
if request.method == "POST":
form = WgForm(request.POST, user=request.user)
if form.is_valid():
by = request.user.person
events = []
group = form.cleaned_data["group"]
stream = StreamName.objects.get(slug="ietf")
doc.stream = stream
e = DocEvent(type="changed_stream", doc=doc, rev=doc.rev, by=by)
e.desc = f"Changed stream to <b>{stream.name}</b>" # Propogates embedding html in DocEvent.desc for consistency
e.save()
events.append(e)
e = DocEvent(type="changed_group", doc=doc, rev=doc.rev, by=by)
e.desc = f"Changed group to <b>{group.name} ({group.acronym.upper()})</b>" # Even if it makes the cats cry
e.save()
events.append(e)
doc.group = group
state = State.objects.get(
type_id="draft-stream-ietf", slug="wg-cand"
) # Stepping through this state
prev_state = doc.get_state("draft-stream-ietf")
doc.set_state(state)
e = add_state_change_event(doc, by, prev_state, state)
events.append(e)
doc.save_with_history(events)
return redirect(issue_wg_call_for_adoption, name=doc.name)
return redirect(issue_wg_call_for_adoption, name=doc.name, acronym=group.acronym)
else:
form = WgForm(initial={"group": None}, user=request.user)
return render(
Expand Down
4 changes: 2 additions & 2 deletions ietf/ietfauth/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,9 @@ def role_required(*role_names):

# specific permissions


def is_authorized_in_doc_stream(user, doc):
"""Return whether user is authorized to perform stream duties on
document."""
"""Is user authorized to perform stream duties on doc?"""
if has_role(user, ["Secretariat"]):
return True

Expand Down
2 changes: 1 addition & 1 deletion ietf/templates/doc/document_draft.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
<td class="edit">
{% if doc.stream and can_edit_stream_info and doc.stream.slug != "legacy" and not snapshot %}
<a class="btn btn-primary btn-sm"
{% if doc|can_issue_ietf_call_for_adoption or doc|can_issue_ietf_wg_lc or doc|can_submit_to_iesg %}
{% if doc|is_doc_ietf_adoptable or doc|can_issue_ietf_wg_lc or doc|can_submit_to_iesg %}
href="{% url 'ietf.doc.views_draft.offer_wg_action_helpers' name=doc.name %}"
{% else %}
href="{% url 'ietf.doc.views_draft.change_stream_state' name=doc.name state_type=stream_state_type_slug %}"
Expand Down
4 changes: 2 additions & 2 deletions ietf/templates/doc/draft/wg_action_helpers.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ <h1>
<small class="text-body-secondary">{{ doc }}</small>
</h1>
<div>
{% if doc|can_issue_ietf_call_for_adoption %}
<a class="btn btn-primary" href="{% url 'ietf.doc.views_draft.issue_wg_call_for_adoption' name=doc.name%}" id="id_wgadopt_button">Issue WG Call for Adoption</a>
{% if doc|is_doc_ietf_adoptable %}
<a class="btn btn-primary" href="{% url 'ietf.doc.views_draft.issue_wg_call_for_adoption' name=doc.name acronym=doc.group.acronym %}" id="id_wgadopt_button">Issue WG Call for Adoption</a>
{% endif %}
{% if doc|can_issue_ietf_wg_lc %}
<a class="btn btn-primary" href="{% url 'ietf.doc.views_draft.issue_wg_lc' name=doc.name %}" id="id_wglc_button">Issue{% if doc|has_had_ietf_wg_lc %} Another{% endif %} Working Group Last Call</a>
Expand Down
5 changes: 3 additions & 2 deletions ietf/templates/doc/mail/wg_call_for_adoption_issued.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{% load ietf_filters %}{% load mail_filters %}{% autoescape off %}{% filter wordwrap:78 %}This message starts a {{doc.group.acronym}} WG Call for Adoption of:
{% load ietf_filters %}{% load mail_filters %}{% autoescape off %}{% filter wordwrap:78 %}This message starts a {{group.acronym}} WG Call for Adoption of:
{{ doc.name }}-{{ doc.rev }}


This Working Group Call for Adoption ends on {{ end_date }}

Abstract:
Expand All @@ -9,7 +10,7 @@ Abstract:
File can be retrieved from:
{{ url }}

Please reply to this message keeping {{ wg_list }} in copy by indicating whether you support or not the adoption of this draft as a {{doc.group.acronym}} WG document. Comments to motivate your preference are highly appreciated.
Please reply to this message keeping {{ wg_list }} in copy by indicating whether you support or not the adoption of this draft as a {{group.acronym}} WG document. Comments to motivate your preference are highly appreciated.

Authors, and WG participants in general, are reminded of the Intellectual Property Rights (IPR) disclosure obligations described in BCP 79 [2]. Appropriate IPR disclosures required for full conformance with the provisions of BCP 78 [1] and BCP 79 [2] must be filed, if you are aware of any. Sanctions available for application to violators of IETF IPR Policy can be found at [3].

Expand Down