From 1ba87890ba9451bb696aef4c8d049bcefa85b71b Mon Sep 17 00:00:00 2001 From: Lars Eggert Date: Fri, 22 Jul 2022 20:43:02 +0200 Subject: [PATCH] feat: Render the document shepherd writeup templates at two new URLs (#4225) * feat: Render the document shepherd writeup templates at two new URL. Those being `/doc/shepherdwriteuptemplate/group` and `/doc/shepherdwriteuptemplate/individual`. * Address review comments from @jennifer-richards * Fixes * Remove debug statement * Make bleach sanitizer not strip the `start` attribute of `ol` tags Also rearrange the code a bit * Don't sanitize the `python_markdown` output, it destroys wanted formatting * Restore bleach * Don't bleach tag `id`s. --- ietf/doc/tests_draft.py | 22 ++++- ietf/doc/urls.py | 5 ++ ietf/doc/views_doc.py | 19 ++++ ietf/doc/views_draft.py | 88 +++++++++++++------ ietf/templates/doc/shepherd_writeup.txt | 10 +-- .../doc/shepherd_writeup_template.html | 10 +++ ietf/utils/markdown.py | 3 +- ietf/utils/text.py | 43 +++++---- 8 files changed, 149 insertions(+), 51 deletions(-) create mode 100644 ietf/templates/doc/shepherd_writeup_template.html diff --git a/ietf/doc/tests_draft.py b/ietf/doc/tests_draft.py index 61fe2f6c7..6efa627f3 100644 --- a/ietf/doc/tests_draft.py +++ b/ietf/doc/tests_draft.py @@ -1163,7 +1163,27 @@ class IndividualInfoFormsTests(TestCase): self.assertEqual(r.status_code, 302) doc = Document.objects.get(name=self.docname) self.assertEqual(comment_event, doc.latest_event(DocEvent, type="added_comment")) - + + def test_doc_view_shepherd_writeup_templates(self): + url = urlreverse( + "ietf.doc.views_doc.document_shepherd_writeup_template", + kwargs=dict(type="group"), + ) + + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertEqual(len(q('h1:contains("for Group Documents")')), 1) + + url = urlreverse( + "ietf.doc.views_doc.document_shepherd_writeup_template", + kwargs=dict(type="individual"), + ) + + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + q = PyQuery(r.content) + self.assertEqual(len(q('h1:contains("for Individual Documents")')), 1) def test_doc_view_shepherd_writeup(self): url = urlreverse('ietf.doc.views_doc.document_shepherd_writeup',kwargs=dict(name=self.docname)) diff --git a/ietf/doc/urls.py b/ietf/doc/urls.py index e5614503d..ad8ee81b2 100644 --- a/ietf/doc/urls.py +++ b/ietf/doc/urls.py @@ -60,6 +60,11 @@ urlpatterns = [ url(r'^email-aliases/?$', views_doc.email_aliases), url(r'^downref/?$', views_downref.downref_registry), url(r'^downref/add/?$', views_downref.downref_registry_add), + url( + r"^shepherdwriteup-template/(?P\w+)/?$", + views_doc.document_shepherd_writeup_template, + ), + url(r'^stats/newrevisiondocevent/?$', views_stats.chart_newrevisiondocevent), url(r'^stats/newrevisiondocevent/conf/?$', views_stats.chart_conf_newrevisiondocevent), url(r'^stats/newrevisiondocevent/data/?$', views_stats.chart_data_newrevisiondocevent), diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index d08eda3f0..fb1a0d009 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -1139,6 +1139,25 @@ def document_shepherd_writeup(request, name): ), ) + +def document_shepherd_writeup_template(request, type): + writeup = markdown.markdown( + render_to_string( + "doc/shepherd_writeup.txt", + dict(stream="ietf", type="individ" if type == "individual" else "group"), + ) + ) + return render( + request, + "doc/shepherd_writeup_template.html", + dict( + writeup=writeup, + stream="ietf", + type="individ" if type == "individual" else "group", + ), + ) + + def document_references(request, name): doc = get_object_or_404(Document,docalias__name=name) refs = doc.references() diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py index 3d8567e89..9f73e3b5c 100644 --- a/ietf/doc/views_draft.py +++ b/ietf/doc/views_draft.py @@ -940,49 +940,68 @@ class ShepherdWriteupUploadForm(forms.Form): def clean_txt(self): return get_cleaned_text_file_content(self.cleaned_data["txt"]) + @login_required def edit_shepherd_writeup(request, name): """Change this document's shepherd writeup""" doc = get_object_or_404(Document, type="draft", name=name) can_edit_stream_info = is_authorized_in_doc_stream(request.user, doc) - can_edit_shepherd_writeup = ( can_edit_stream_info + can_edit_shepherd_writeup = ( + can_edit_stream_info or (doc.shepherd and user_is_person(request.user, doc.shepherd.person)) - or has_role(request.user, ["Area Director"])) + or has_role(request.user, ["Area Director"]) + ) if not can_edit_shepherd_writeup: - permission_denied(request, "You do not have the necessary permissions to view this page") + permission_denied( + request, "You do not have the necessary permissions to view this page" + ) login = request.user.person - if request.method == 'POST': + if request.method == "POST": if "submit_response" in request.POST: form = ShepherdWriteupUploadForm(request.POST, request.FILES) if form.is_valid(): - - from_file = form.cleaned_data['txt'] - if from_file: - writeup = from_file - else: - writeup = form.cleaned_data['content'] - e = WriteupDocEvent(doc=doc, rev=doc.rev, by=login, type="changed_protocol_writeup") - # Add the shepherd writeup to description if the document is in submitted for publication state + from_file = form.cleaned_data["txt"] + if from_file: + writeup = from_file + else: + writeup = form.cleaned_data["content"] + e = WriteupDocEvent( + doc=doc, rev=doc.rev, by=login, type="changed_protocol_writeup" + ) + + # Add the shepherd writeup to description, + # if the document is in submitted for publication state stream_state = doc.get_state("draft-stream-%s" % doc.stream_id) - iesg_state = doc.get_state("draft-iesg") - if (iesg_state or (stream_state and stream_state.slug=='sub-pub')): + iesg_state = doc.get_state("draft-iesg") + if iesg_state or (stream_state and stream_state.slug == "sub-pub"): e.desc = writeup else: e.desc = "Changed document writeup" e.text = writeup e.save() - - return redirect('ietf.doc.views_doc.document_main', name=doc.name) + + return redirect("ietf.doc.views_doc.document_main", name=doc.name) elif "reset_text" in request.POST: - - init = { "content": render_to_string("doc/shepherd_writeup.txt",dict(doc=doc))} + init = { + "content": render_to_string( + "doc/shepherd_writeup.txt", + dict( + doc=doc, + type="individ" + if not doc.group.type.slug or doc.group.type.slug != "ietf" + else "group", + stream=doc.stream.slug, + group=doc.group.type.slug, + ), + ) + } form = ShepherdWriteupUploadForm(initial=init) # Protect against handcrufted malicious posts @@ -993,21 +1012,36 @@ def edit_shepherd_writeup(request, name): form = None if not form: - init = { "content": ""} + init = {"content": ""} - previous_writeup = doc.latest_event(WriteupDocEvent,type="changed_protocol_writeup") + previous_writeup = doc.latest_event( + WriteupDocEvent, type="changed_protocol_writeup" + ) if previous_writeup: init["content"] = previous_writeup.text else: - init["content"] = render_to_string("doc/shepherd_writeup.txt", - dict(doc=doc), - ) + init["content"] = render_to_string( + "doc/shepherd_writeup.txt", + dict( + doc=doc, + type="individ" + if not doc.group.type.slug or doc.group.type.slug != "ietf" + else "group", + stream=doc.stream.slug, + group=doc.group.type.slug, + ), + ) form = ShepherdWriteupUploadForm(initial=init) - return render(request, 'doc/draft/change_shepherd_writeup.html', - {'form': form, - 'doc' : doc, - }) + return render( + request, + "doc/draft/change_shepherd_writeup.html", + { + "form": form, + "doc": doc, + }, + ) + class ShepherdForm(forms.Form): shepherd = SearchableEmailField(required=False, only_users=True) diff --git a/ietf/templates/doc/shepherd_writeup.txt b/ietf/templates/doc/shepherd_writeup.txt index 05cdc7d4e..7c81ef3dd 100644 --- a/ietf/templates/doc/shepherd_writeup.txt +++ b/ietf/templates/doc/shepherd_writeup.txt @@ -1,4 +1,4 @@ -{# Keep in sync with https://github.com/ietf-chairs/chairs.ietf.org/blob/main/documents/qa-style-writeup-template.md #}{% if doc.stream %}{% if doc.stream.slug == 'ietf' %}# Document Shepherd Write-Up +{# Keep in sync with https://github.com/ietf-chairs/chairs.ietf.org/blob/main/documents/qa-style-writeup-template.md #}{% if stream %}{% if stream == 'ietf' %}# Document Shepherd Write-Up{% if type != "individ" %} for Group Documents{% else %} for Individual Documents{% endif %} *This version is dated 4 July 2022.* @@ -13,7 +13,7 @@ Note that some numbered items contain multiple related questions; please be sure to answer all of them. ## Document History -{% if doc.group.type.slug == 'individ' %} +{% if type == 'individ' %} 1. Was the document considered in any WG, and if so, why was it not adopted as a work item there? @@ -37,7 +37,7 @@ to answer all of them. either in the document itself (as [RFC 7942][3] recommends) or elsewhere (where)? -### Additional Reviews +## Additional Reviews 5. Do the contents of this document closely interact with technologies in other IETF working groups or external organizations, and would it therefore benefit @@ -58,7 +58,7 @@ to answer all of them. final version of the document written in a formal language, such as XML code, BNF rules, MIB definitions, CBOR's CDDL, etc. -### Document Shepherd Checks +## Document Shepherd Checks 9. Based on the shepherd's review of the document, is it their opinion that this document is needed, clearly written, complete, correctly designed, and ready @@ -139,5 +139,5 @@ to answer all of them. [15]: https://authors.ietf.org/en/content-guidelines-overview [16]: https://www.ietf.org/about/groups/iesg/statements/normative-informative-references/ [17]: https://datatracker.ietf.org/doc/downref/ -{% else %}There is no default shepherd write-up template for the {{doc.stream}} stream. +{% else %}There is no default shepherd write-up template for the {{stream}} stream. {% endif %}{% else %}There is no stream set for this document (thus, no default shepherd write-up template).{% endif %} diff --git a/ietf/templates/doc/shepherd_writeup_template.html b/ietf/templates/doc/shepherd_writeup_template.html new file mode 100644 index 000000000..1ed4954cc --- /dev/null +++ b/ietf/templates/doc/shepherd_writeup_template.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} +{# Copyright The IETF Trust 2015, All Rights Reserved #} +{% load origin %} +{% load ietf_filters %} +{% load textfilters htmlfilters %} +{% block title %}Document Shepherd Write-Up{% if type == "group" %} for Group Documents{% elif type == "individual" %} for Individual Documents{% endif %}{% endblock %} +{% block content %} + {% origin %} + {{ writeup|urlize_ietf_docs|linkify }} +{% endblock %} \ No newline at end of file diff --git a/ietf/utils/markdown.py b/ietf/utils/markdown.py index 9818b26a3..3b7c60cae 100644 --- a/ietf/utils/markdown.py +++ b/ietf/utils/markdown.py @@ -8,7 +8,6 @@ the datatracker. import markdown as python_markdown from django.utils.safestring import mark_safe -from markdown.extensions.extra import ExtraExtension from ietf.doc.templatetags.ietf_filters import urlize_ietf_docs from ietf.utils.text import bleach_cleaner, bleach_linker @@ -20,7 +19,7 @@ def markdown(text): urlize_ietf_docs( bleach_cleaner.clean( python_markdown.markdown( - text, extensions=[ExtraExtension(), "nl2br"] + text, extensions=["extra", "nl2br", "sane_lists", "toc"] ) ) ) diff --git a/ietf/utils/text.py b/ietf/utils/text.py index 007fe2704..39989d5e0 100644 --- a/ietf/utils/text.py +++ b/ietf/utils/text.py @@ -23,13 +23,36 @@ tlds_sorted = sorted(tlds.tld_set, key=len, reverse=True) protocols = copy.copy(bleach.sanitizer.ALLOWED_PROTOCOLS) protocols.append("ftp") # we still have some ftp links protocols.append("xmpp") # we still have some xmpp links + +tags = set(copy.copy(bleach.sanitizer.ALLOWED_TAGS)).union( + { + # fmt: off + 'a', 'abbr', 'acronym', 'address', 'b', 'big', + 'blockquote', 'body', 'br', 'caption', 'center', 'cite', 'code', 'col', + 'colgroup', 'dd', 'del', 'dfn', 'dir', 'div', 'dl', 'dt', 'em', 'font', + 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'html', 'i', 'ins', 'kbd', + 'li', 'ol', 'p', 'pre', 'q', 's', 'samp', 'small', 'span', 'strike', 'style', + 'strong', 'sub', 'sup', 'table', 'title', 'tbody', 'td', 'tfoot', 'th', 'thead', + 'tr', 'tt', 'u', 'ul', 'var' + # fmt: on + } +) + +attributes = copy.copy(bleach.sanitizer.ALLOWED_ATTRIBUTES) +attributes["*"] = ["id"] +attributes["ol"] = ["start"] + +bleach_cleaner = bleach.sanitizer.Cleaner( + tags=tags, attributes=attributes, protocols=protocols, strip=True +) + validate_url = URLValidator() def check_url_validity(attrs, new=False): - if (None, 'href') not in attrs: + if (None, "href") not in attrs: return None - url = attrs[(None, 'href')] + url = attrs[(None, "href")] try: if url.startswith("http"): validate_url(url) @@ -41,22 +64,10 @@ def check_url_validity(attrs, new=False): bleach_linker = bleach.Linker( callbacks=[check_url_validity], url_re=bleach.linkifier.build_url_re(tlds=tlds_sorted, protocols=protocols), - email_re=bleach.linkifier.build_email_re(tlds=tlds_sorted), # type: ignore - parse_email=True + email_re=bleach.linkifier.build_email_re(tlds=tlds_sorted), # type: ignore + parse_email=True, ) -tags = ( - 'a', 'abbr', 'acronym', 'address', 'b', 'big', - 'blockquote', 'body', 'br', 'caption', 'center', 'cite', 'code', 'col', - 'colgroup', 'dd', 'del', 'dfn', 'dir', 'div', 'dl', 'dt', 'em', 'font', - 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'hr', 'html', 'i', 'ins', 'kbd', - 'li', 'ol', 'p', 'pre', 'q', 's', 'samp', 'small', 'span', 'strike', 'style', - 'strong', 'sub', 'sup', 'table', 'title', 'tbody', 'td', 'tfoot', 'th', 'thead', - 'tr', 'tt', 'u', 'ul', 'var' -) - -bleach_cleaner = bleach.sanitizer.Cleaner(tags=tags, protocols=protocols, strip=True) - @keep_lazy(str) def xslugify(value):