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
41 changes: 40 additions & 1 deletion ietf/doc/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@

from ietf.doc.models import (Document, DocRelationshipName, RelatedDocument, State,
DocEvent, BallotPositionDocEvent, LastCallDocEvent, WriteupDocEvent, NewRevisionDocEvent, BallotType,
EditedAuthorsDocEvent, StateType, RfcAuthor)
EditedAuthorsDocEvent, StateType, RfcAuthor, RpcAssignmentDocEvent)
from ietf.doc.factories import (DocumentFactory, DocEventFactory, CharterFactory,
ConflictReviewFactory, WgDraftFactory,
IndividualDraftFactory, WgRfcFactory,
Expand Down Expand Up @@ -1688,6 +1688,28 @@ def _change_state(doc, state):
self.assertEqual(r.status_code, 200)
self.assertNotContains(r, 'Auth48 status')

def test_rfceditor_queue_status_shown(self):
"""A queued draft shows its publication-queue Status in place of the state name."""
draft = IndividualDraftFactory()
event = StateDocEventFactory(doc=draft, state=('draft-rfceditor', 'in_progress'))
draft.set_state(event.state)
draft.save_with_history([event])
RpcAssignmentDocEvent.objects.create(
doc=draft,
rev=draft.rev,
by=Person.objects.get(name="(System)"),
type="changed_rpc_assignments",
assignments="In Progress (First Edit)",
desc="RPC status changed to In Progress (First Edit)",
)

r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name)))
self.assertEqual(r.status_code, 200)
# The composite queue Status (which only comes from the RpcAssignmentDocEvent,
# not from the state name) is shown, confirming it replaces the raw state name.
self.assertContains(r, "In Progress (First Edit)")
self.assertContains(r, "Publication queue entry")


class DocTestCase(TestCase):
def test_status_change(self):
Expand Down Expand Up @@ -2103,6 +2125,23 @@ def test_state_help(self):
self.assertEqual(r.status_code, 200)
self.assertContains(r, State.objects.get(type="draft-iesg", slug="lc").name)

def test_rfceditor_state_help_has_queue_status_and_legacy_sections(self):
url = urlreverse('ietf.doc.views_help.state_help', kwargs=dict(type="draft-rfceditor"))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
# New "Queue status" section describing the queue Status values.
self.assertContains(r, "Queue status")
self.assertContains(r, "In Progress (First Edit)")
# Legacy states are moved to their own section with the history note.
self.assertContains(r, "Legacy states")
self.assertContains(r, "appear in the change history")
self.assertContains(r, State.objects.get(type="draft-rfceditor", slug="auth48").name)
# The queue-backing states are not listed among the legacy states.
q = PyQuery(r.content)
legacy_ids = [row.get("id") for row in q("tbody tr")]
self.assertNotIn("in_progress", legacy_ids)
self.assertNotIn("blocked", legacy_ids)

def test_document_nonietf_pubreq_button(self):
doc = IndividualDraftFactory()

Expand Down
20 changes: 19 additions & 1 deletion ietf/doc/views_doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
from ietf.doc.models import ( Document, DocHistory, DocEvent, BallotDocEvent, BallotType,
ConsensusDocEvent, NewRevisionDocEvent, StoredObject, TelechatDocEvent, WriteupDocEvent, IanaExpertDocEvent,
IESG_BALLOT_ACTIVE_STATES, STATUSCHANGE_RELATIONS, DocumentActionHolder, DocumentAuthor,
RelatedDocument, RelatedDocHistory)
RelatedDocument, RelatedDocHistory, RpcAssignmentDocEvent)
from ietf.doc.tasks import investigate_fragment_task
from ietf.doc.utils import (augment_events_with_revision,
can_adopt_draft, can_unadopt_draft, get_chartering_type, get_tags_for_stream_id,
Expand Down Expand Up @@ -197,6 +197,22 @@ def interesting_doc_relations(doc):

return interesting_relations_that, interesting_relations_that_doc


def rfc_editor_queue_status(doc):
"""Human-readable RPC publication queue "Status" for the document, or None.

While a document is in the RFC Editor queue (draft-rfceditor state
"in_progress" or "blocked"), this is the status text pushed by the RFC
Production Center, matching what the queue website shows. It is displayed in
place of the raw draft-rfceditor state name. Returns None for documents whose
draft-rfceditor state predates the queue integration (they fall back to the
state name).
"""
if doc.get_state_slug("draft-rfceditor") not in ("in_progress", "blocked"):
return None
event = doc.latest_event(RpcAssignmentDocEvent, type="changed_rpc_assignments")
return event.assignments if event else None

def document_main(request, name, rev=None, document_html=False):

doc = get_object_or_404(Document.objects.select_related(), name=name)
Expand Down Expand Up @@ -364,6 +380,7 @@ def document_main(request, name, rev=None, document_html=False):
has_errata=doc.pk and doc.tags.filter(slug="errata"), # doc.pk == None if using a fake_history_obj
file_urls=file_urls,
rfc_editor_state=doc.get_state("draft-rfceditor"),
rfc_editor_queue_status=rfc_editor_queue_status(doc),
iana_review_state=doc.get_state("draft-iana-review"),
iana_action_state=doc.get_state("draft-iana-action"),
iana_experts_state=doc.get_state("draft-iana-experts"),
Expand Down Expand Up @@ -707,6 +724,7 @@ def document_main(request, name, rev=None, document_html=False):
iesg_state=iesg_state,
iesg_state_summary=iesg_state_summary,
rfc_editor_state=doc.get_state("draft-rfceditor"),
rfc_editor_queue_status=rfc_editor_queue_status(doc),
rfc_editor_auth48_url=auth48_url,
iana_review_state=doc.get_state("draft-iana-review"),
iana_action_state=doc.get_state("draft-iana-action"),
Expand Down
55 changes: 55 additions & 0 deletions ietf/doc/views_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,43 @@
from ietf.name.models import DocRelationshipName, DocTagName
from ietf.doc.utils import get_tags_for_stream_id

# Documentation of the values shown in the RFC Editor queue "Status" field. This
# status is not a stored state; it is derived by the RFC Production Center's
# publication queue from the active editor assignments, pending activities,
# blocking reasons and IANA status of a document, and is rendered to match the
# publication queue site (https://queue.rfc-editor.org/). Keep in sync with
# ietf.sync.tasks.format_rpc_queue_status.
RFC_EDITOR_QUEUE_STATUS_VALUES = [
("In Progress (First Edit)",
"The document is being copyedited by the first editor."),
("In Progress (Second Edit)",
"The document is getting a second review, focusing on complex issues and IANA "
"actions."),
("In Final Review",
"Awaiting final approval(s) from authors and/or action holders."),
("<activity> (e.g. “formatting”)",
"Another RPC activity is currently underway for the document; the activity is "
"shown by name (for example “formatting”). Reference checking and "
"publication are not shown as their own status."),
("Awaiting <activity>",
"The document is in the queue waiting for the named activity to begin. Values "
"include “Awaiting Formatting”, “Awaiting Reference Checker”, "
"“Awaiting First editor”, “Awaiting Second editor”, "
"“Awaiting Final review editor”, and “Awaiting Publisher”."),
("Awaiting Editor Assignment",
"The document is in the queue but nothing has been assigned to it yet."),
("IANA hold",
"First editing is underway but is held pending completion of IANA actions. (This "
"is distinct from the “IANA Hold” blocking reason below.)"),
("blocked: <reason>",
"Progress is blocked; one or more blocking reasons are listed after the colon. "
"The possible reasons are: Waiting for Action Holder, Stream Hold, External "
"Reference Hold, Author Input Required, IANA Hold, Reference Not Received, "
"Reference Not Received (2nd Generation), Reference Not Received (3rd Generation), "
"Reference: Second Edit Incomplete, Reference: Publish Incomplete, Final Approval "
"Pending, Tools Issue, and Manual Hold."),
]

def state_index(request):
types = StateType.objects.all()
names = [ type.slug for type in types ]
Expand Down Expand Up @@ -67,6 +104,22 @@ def state_help(request, type=None):

states = State.objects.filter(used=True, type=state_type).order_by("order")

# The RFC Editor queue status is now driven by the RFC Production Center's
# publication queue rather than by the legacy draft-rfceditor states. The
# "in_progress"/"blocked" states back the queue-status display; the remaining
# states are legacy and only appear in the history of older documents.
queue_status_values = None
legacy_states_note = None
if state_type.slug == "draft-rfceditor":
states = states.exclude(slug__in=("in_progress", "blocked"))
queue_status_values = RFC_EDITOR_QUEUE_STATUS_VALUES
legacy_states_note = (
"These states predate the current RFC Editor publication queue and are "
"no longer assigned to documents. They are documented here because they "
"still appear in the change history of documents that were processed "
"before the queue integration."
)

has_next_states = False
for state in states:
if state.next_states.all():
Expand All @@ -88,6 +141,8 @@ def state_help(request, type=None):
"states": states,
"has_next_states": has_next_states,
"tags": tags,
"queue_status_values": queue_status_values,
"legacy_states_note": legacy_states_note,
} )

def relationship_help(request,subset=None):
Expand Down
94 changes: 84 additions & 10 deletions ietf/sync/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,89 @@ def refresh_rfc_index_task():
mark_rfcindex_as_processed(new_processed_time)


# Human-readable labels for the RPC publication queue "Status", mirroring the
# ietf-tools/queue website (website/app/utils/queue.ts, renderAssignmentsByRoles)
# so the datatracker shows the same status text that appears at
# https://queue.rfc-editor.org/. The queue "Status" is not a stored field; it is
# derived from the active assignment roles, pending activities, blocking reasons
# and IANA status carried in the purple pubq queue payload.
RPC_QUEUE_ROLE_LABELS = {
"first_editor": "In Progress (First Edit)",
"second_editor": "In Progress (Second Edit)",
"final_review_editor": "In Final Review",
}
# Roles the queue site does not surface in the Status column.
RPC_QUEUE_HIDDEN_ROLES = {"ref_checker", "publisher"}


def _humanize_slug(slug):
return slug.replace("_", " ")


def _rpc_role_label(role):
return RPC_QUEUE_ROLE_LABELS.get(role, _humanize_slug(role))


def _rpc_blocking_reason_label(name):
# Special case mirrored from the queue site's humanFriendlyBlockingReason().
if name == "Reference: First Edit Incomplete":
return "Author Input Required"
return _humanize_slug(name)


def format_rpc_queue_status(obj):
"""Render the RPC publication queue "Status" for a single queue entry.

Mirrors renderAssignmentsByRoles() from the ietf-tools/queue website so the
datatracker presents the same status text. ``obj`` is one entry of the purple
pubq queue payload. Roles, pending activities and blocking reasons are sorted
so the result is stable (a change to the string is what triggers a new
RpcAssignmentDocEvent).
"""
roles = {
a["role"] for a in (obj.get("assignment_set") or []) if a.get("role")
}
is_blocked = "blocked" in roles

parts = []

# IANA hold: iana_status "not_completed" while a first_editor is assigned.
iana_status = obj.get("iana_status") or {}
if iana_status.get("slug") == "not_completed" and "first_editor" in roles:
parts.append("IANA hold")

# Pending activities (only when not blocked): "Awaiting <role>", skipping any
# role that is already a current assignment. Note the queue site does NOT hide
# ref_checker/publisher here (only for current-role badges below), so e.g.
# "Awaiting Reference Checker" can appear.
if not is_blocked:
for activity in sorted(
obj.get("pending_activities") or [],
key=lambda a: (a.get("name") or a.get("slug") or ""),
):
slug = activity.get("slug")
if not slug or slug in roles:
continue
parts.append(f"Awaiting {activity.get('name') or _humanize_slug(slug)}")

# Current assignment roles (ref_checker/publisher hidden). Blocking reason
# names are appended to the "blocked" role.
blocking_names = sorted(
_rpc_blocking_reason_label(br["reason"]["name"])
for br in (obj.get("blocking_reasons") or [])
if br.get("reason", {}).get("name")
)
for role in sorted(roles - RPC_QUEUE_HIDDEN_ROLES):
label = _rpc_role_label(role)
if role == "blocked" and blocking_names:
label += ": " + ", ".join(blocking_names)
parts.append(label)

if not parts:
return "Awaiting Editor Assignment"
return ", ".join(parts)


@shared_task
def process_rpc_queue_task(data: list):
in_progress_state = State.objects.get(
Expand Down Expand Up @@ -366,16 +449,7 @@ def process_rpc_queue_task(data: list):
e.save()
events.append(e)

roles = sorted(a["role"] for a in obj.get("assignment_set", []))
next_assignments = ", ".join(roles)
blocking_names = sorted(
br["reason"]["name"] for br in obj.get("blocking_reasons", [])
)
if blocking_names:
next_assignments += ": " + ", ".join(blocking_names)

if next_assignments == "":
next_assignments = "Awaiting Editor Assignment"
next_assignments = format_rpc_queue_status(obj)

prev_assignments_event = d.latest_event(
RpcAssignmentDocEvent, type="changed_rpc_assignments"
Expand Down
Loading
Loading