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
3 changes: 2 additions & 1 deletion ietf/doc/feeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import datetime
import unicodedata

from django.conf import settings
from django.contrib.syndication.views import Feed, FeedDoesNotExist
from django.utils.feedgenerator import Atom1Feed, Rss201rev2Feed
from django.urls import reverse as urlreverse
Expand Down Expand Up @@ -223,7 +224,7 @@ def item_extra_kwargs(self, item):
extra.update({"dcterms_accessRights": "gratis"})
extra.update({"dcterms_format": "text/html"})
media_contents = []
if item.rfc_number < 8650:
if item.rfc_number < settings.FIRST_V3_RFC:
if item.rfc_number not in [8, 9, 51, 418, 500, 530, 589]:
for fmt, media_type in [("txt", "text/plain"), ("html", "text/html")]:
media_contents.append(
Expand Down
122 changes: 122 additions & 0 deletions ietf/doc/tests_notprepped.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Copyright The IETF Trust 2026, All Rights Reserved

from django.conf import settings
from django.utils import timezone
from django.urls import reverse as urlreverse

from pyquery import PyQuery

from ietf.doc.factories import WgRfcFactory
from ietf.doc.models import StoredObject
from ietf.doc.storage_utils import store_bytes
from ietf.utils.test_utils import TestCase


class NotpreppedRfcXmlTests(TestCase):
def test_editor_source_button_visibility(self):
pre_v3 = WgRfcFactory(rfc_number=settings.FIRST_V3_RFC - 1)
first_v3 = WgRfcFactory(rfc_number=settings.FIRST_V3_RFC)
post_v3 = WgRfcFactory(rfc_number=settings.FIRST_V3_RFC + 1)

for rfc, expect_button in [(pre_v3, False), (first_v3, True), (post_v3, True)]:
r = self.client.get(
urlreverse(
"ietf.doc.views_doc.document_main", kwargs=dict(name=rfc.name)
)
)
self.assertEqual(r.status_code, 200)
buttons = PyQuery(r.content)('a.btn:contains("Get editor source")')
if expect_button:
self.assertEqual(len(buttons), 1, msg=f"rfc_number={rfc.rfc_number}")
expected_href = urlreverse(
"ietf.doc.views_doc.rfcxml_notprepped_wrapper",
kwargs=dict(number=rfc.rfc_number),
)
self.assertEqual(
buttons.attr("href"),
expected_href,
msg=f"rfc_number={rfc.rfc_number}",
)
else:
self.assertEqual(len(buttons), 0, msg=f"rfc_number={rfc.rfc_number}")

def test_rfcxml_notprepped(self):
number = settings.FIRST_V3_RFC
stored_name = f"notprepped/rfc{number}.notprepped.xml"
url = f"/doc/rfc{number}/notprepped/"

# 404 for pre-v3 RFC numbers (no document needed)
r = self.client.get(f"/doc/rfc{number - 1}/notprepped/")
self.assertEqual(r.status_code, 404)

# 404 when no RFC document exists in the database
r = self.client.get(url)
self.assertEqual(r.status_code, 404)

# 404 when RFC document exists but has no StoredObject
WgRfcFactory(rfc_number=number)
r = self.client.get(url)
self.assertEqual(r.status_code, 404)

# 404 when StoredObject exists but backing storage is missing (FileNotFoundError)
now = timezone.now()
StoredObject.objects.create(
store="rfc",
name=stored_name,
sha384="a" * 96,
len=0,
store_created=now,
created=now,
modified=now,
)
r = self.client.get(url)
self.assertEqual(r.status_code, 404)

# 200 with correct content-type, attachment disposition, and body when object is fully stored
xml_content = b"<rfc>test</rfc>"
store_bytes("rfc", stored_name, xml_content, allow_overwrite=True)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r["Content-Type"], "application/xml")
self.assertEqual(
r["Content-Disposition"],
f'attachment; filename="rfc{number}.notprepped.xml"',
)
self.assertEqual(b"".join(r.streaming_content), xml_content)

def test_rfcxml_notprepped_wrapper(self):
number = settings.FIRST_V3_RFC

# 404 for pre-v3 RFC numbers (no document needed)
r = self.client.get(
urlreverse(
"ietf.doc.views_doc.rfcxml_notprepped_wrapper",
kwargs=dict(number=number - 1),
)
)
self.assertEqual(r.status_code, 404)

# 404 when no RFC document exists in the database
r = self.client.get(
urlreverse(
"ietf.doc.views_doc.rfcxml_notprepped_wrapper",
kwargs=dict(number=number),
)
)
self.assertEqual(r.status_code, 404)

# 200 with rendered template when RFC document exists
rfc = WgRfcFactory(rfc_number=number)
r = self.client.get(
urlreverse(
"ietf.doc.views_doc.rfcxml_notprepped_wrapper",
kwargs=dict(number=number),
)
)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertIn(str(rfc.rfc_number), q("h1").text())
download_url = urlreverse(
"ietf.doc.views_doc.rfcxml_notprepped", kwargs=dict(number=number)
)
self.assertEqual(len(q(f'a.btn[href="{download_url}"]')), 1)
2 changes: 2 additions & 0 deletions ietf/doc/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@

url(r'^%(name)s(?:/%(rev)s)?/$' % settings.URL_REGEXPS, views_doc.document_main),
url(r'^%(name)s(?:/%(rev)s)?/bibtex/$' % settings.URL_REGEXPS, views_doc.document_bibtex),
url(r'^rfc(?P<number>[0-9]+)/notprepped/$' , views_doc.rfcxml_notprepped),
url(r'^rfc(?P<number>[0-9]+)/notprepped-wrapper/$', views_doc.rfcxml_notprepped_wrapper),
url(r'^%(name)s(?:/%(rev)s)?/idnits2-state/$' % settings.URL_REGEXPS, views_doc.idnits2_state),
url(r'^bibxml3/reference.I-D.%(name)s(?:-%(rev)s)?.xml$' % settings.URL_REGEXPS, views_doc.document_bibxml_ref),
url(r'^bibxml3/%(name)s(?:-%(rev)s)?.xml$' % settings.URL_REGEXPS, views_doc.document_bibxml),
Expand Down
34 changes: 31 additions & 3 deletions ietf/doc/views_doc.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright The IETF Trust 2009-2024, All Rights Reserved
# Copyright The IETF Trust 2009-2026, All Rights Reserved
# -*- coding: utf-8 -*-
#
# Parts Copyright (C) 2009-2010 Nokia Corporation and/or its subsidiary(-ies).
Expand Down Expand Up @@ -43,9 +43,10 @@

from celery.result import AsyncResult
from django.core.cache import caches
from django.core.files.base import ContentFile
from django.core.exceptions import PermissionDenied
from django.db.models import Max
from django.http import HttpResponse, Http404, HttpResponseBadRequest, JsonResponse
from django.http import FileResponse, HttpResponse, Http404, HttpResponseBadRequest, JsonResponse
from django.shortcuts import render, get_object_or_404, redirect
from django.template.loader import render_to_string
from django.urls import reverse as urlreverse
Expand All @@ -57,7 +58,7 @@
import debug # pyflakes:ignore

from ietf.doc.models import ( Document, DocHistory, DocEvent, BallotDocEvent, BallotType,
ConsensusDocEvent, NewRevisionDocEvent, TelechatDocEvent, WriteupDocEvent, IanaExpertDocEvent,
ConsensusDocEvent, NewRevisionDocEvent, StoredObject, TelechatDocEvent, WriteupDocEvent, IanaExpertDocEvent,
IESG_BALLOT_ACTIVE_STATES, STATUSCHANGE_RELATIONS, DocumentActionHolder, DocumentAuthor,
RelatedDocument, RelatedDocHistory)
from ietf.doc.tasks import investigate_fragment_task
Expand Down Expand Up @@ -86,6 +87,7 @@
from ietf.review.models import ReviewAssignment
from ietf.review.utils import can_request_review_of_doc, review_assignments_to_list_for_docs, review_requests_to_list_for_docs
from ietf.review.utils import no_review_from_teams_on_doc
from ietf.doc.storage_utils import retrieve_bytes
from ietf.utils import markup_txt, log, markdown
from ietf.utils.draft import get_status_from_draft_text
from ietf.utils.meetecho import MeetechoAPIError, SlidesManager
Expand Down Expand Up @@ -2356,3 +2358,29 @@ def investigate(request):
"results": results,
},
)

def rfcxml_notprepped(request, number):
number = int(number)
if number < settings.FIRST_V3_RFC:
raise Http404
rfc = Document.objects.filter(type="rfc", rfc_number=number).first()
if rfc is None:
raise Http404
name = f"notprepped/rfc{number}.notprepped.xml"
if not StoredObject.objects.filter(name=name).exists():
raise Http404
try:
bytes = retrieve_bytes("rfc", name)
except FileNotFoundError:
raise Http404
return FileResponse(ContentFile(bytes, name=f"rfc{number}.notprepped.xml"), as_attachment=True)


def rfcxml_notprepped_wrapper(request, number):
number = int(number)
if number < settings.FIRST_V3_RFC:
raise Http404
rfc = Document.objects.filter(type="rfc", rfc_number=number).first()
if rfc is None:
raise Http404
return render(request, "doc/notprepped_wrapper.html", context={"rfc": rfc})
4 changes: 3 additions & 1 deletion ietf/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,9 @@

AUTHENTICATION_BACKENDS = ( 'ietf.ietfauth.backends.CaseInsensitiveModelBackend', )

FILE_UPLOAD_PERMISSIONS = 0o644
FILE_UPLOAD_PERMISSIONS = 0o644

FIRST_V3_RFC = 8650


#
Expand Down
2 changes: 1 addition & 1 deletion ietf/sync/rfcindex.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ def get_publication_std_levels() -> dict[int, StdLevelName]:


def format_ordering(rfc_number):
if rfc_number < 8650:
if rfc_number < settings.FIRST_V3_RFC:
ordering = ["txt", "ps", "pdf", "html", "xml"]
else:
ordering = ["html", "txt", "ps", "pdf", "xml"]
Expand Down
9 changes: 9 additions & 0 deletions ietf/templates/doc/document_rfc.html
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@
</i>
Referenced by
</a>
{% if doc.rfc_number >= settings.FIRST_V3_RFC %}
<a class="btn btn-primary btn-sm"
href="{% url 'ietf.doc.views_doc.rfcxml_notprepped_wrapper' number=doc.rfc_number %}"
rel="nofollow">
<i class="bi bi-vector-pen">
</i>
Get editor source
</a>
{% endif %}
<a class="btn btn-primary btn-sm"
href="https://mailarchive.ietf.org/arch/search?q=%22{{ doc.name }}%22"
rel="nofollow"
Expand Down
26 changes: 26 additions & 0 deletions ietf/templates/doc/notprepped_wrapper.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2026, All Rights Reserved #}
{% load origin %}
{% block title %}RFC {{ rfc.rfc_number }} — Not-prepped XML{% endblock %}
{% block content %}
{% origin %}
<h1>RFC {{ rfc.rfc_number }} — Not-prepped XML</h1>
<p>
The <em>not-prepped</em> XML
is the RFC XML v3 source for an RFC at the moment in the publication process
just before the prep tool was used to expand default
values, generate section numbers, resolve cross-references, and embed
boilerplate.
</p><p>
It is useful for authors who want to begin a new draft based on
the RFC's text, such as when creating a bis-draft, and for tools that process
author-facing RFC XML.
</p>
<p>
<a class="btn btn-primary"
href="{% url 'ietf.doc.views_doc.rfcxml_notprepped' number=rfc.rfc_number %}">
<i class="bi bi-download"></i>
Download not-prepped XML for RFC {{ rfc.rfc_number }}
</a>
</p>
{% endblock %}
Loading