8686from ietf .utils .test_utils import TestCase
8787from ietf .utils .text import normalize_text , texescape
8888from ietf .utils .timezone import date_today , datetime_today , DEADLINE_TZINFO , RPC_TZINFO
89- from ietf .doc .utils_search import AD_WORKLOAD , fill_in_telechat_date , prepare_document_table
89+ from ietf .doc .utils_search import (AD_WORKLOAD , fill_in_rfc_editor_queue_status ,
90+ fill_in_telechat_date , prepare_document_table )
9091
9192
9293class SearchTests (TestCase ):
@@ -308,21 +309,38 @@ def test_search_query_count_does_not_grow_with_results(self):
308309 so doubling the number of rows must not change the number of queries. A lookup
309310 that slipped back into the per-row path shows up here as a count that grows.
310311
311- Mind the blind spots: these documents have no IESG state, ballot, last call,
312- action holders, telechat or obsoleting RFCs, so the per-row work the columns
313- driven by those still do is not covered. Widen the fixtures rather than reading
314- a pass here as "the table does no per-row queries".
312+ Mind the blind spots: these documents have no ballot, last call, action holders,
313+ telechat, obsoleting RFCs, or IESG state under way, so the per-row work the
314+ columns driven by those still do is not covered -- state_age_colored and the
315+ action holder list each still cost a query for a document being processed by the
316+ IESG. Widen the fixtures rather than reading a pass here as "the table does no
317+ per-row queries".
315318 """
316319 group = GroupFactory (type_id = "wg" )
317320 url = urlreverse ('ietf.doc.views_search.search' ) + (
318321 f"?activedrafts=on&olddrafts=on&rfcs=on&by=group&group={ group .acronym } "
319322 )
323+ system = Person .objects .get (name = "(System)" )
320324
321325 def add_documents (count ):
322326 for _ in range (count ):
323327 WgDraftFactory (group = group , authors = [PersonFactory ()], ad = PersonFactory (),
324328 shepherd = EmailFactory ())
325329 WgRfcFactory (group = group )
330+ # A draft in the RFC Editor queue, whose row also shows the queue status.
331+ # Not one the IESG is processing, to keep the blind spots above out of
332+ # the count.
333+ queued = WgDraftFactory (
334+ group = group ,
335+ states = [("draft" , "active" ), ("draft-iesg" , "idexists" ),
336+ ("draft-rfceditor" , "in_progress" )],
337+ )
338+ RpcAssignmentDocEvent .objects .create (
339+ doc = queued , rev = queued .rev , by = system ,
340+ type = "changed_rpc_assignments" ,
341+ assignments = "In Progress (First Edit)" ,
342+ desc = "RPC status changed to In Progress (First Edit)" ,
343+ )
326344
327345 def count_queries ():
328346 with CaptureQueriesContext (connection ) as context :
@@ -335,7 +353,7 @@ def count_queries():
335353 add_documents (4 )
336354 doubled = count_queries ()
337355
338- # A per-row lookup would add at least one query for each of the 8 new documents.
356+ # A per-row lookup would add at least one query for each of the 12 new documents.
339357 self .assertLessEqual (
340358 doubled , baseline + 2 ,
341359 f"query count grew from { baseline } to { doubled } when the result set tripled" ,
@@ -387,6 +405,15 @@ def test_prepared_documents_are_picklable(self):
387405 shepherd = EmailFactory ())
388406 TelechatDocEventFactory (doc = draft )
389407 WgRfcFactory ()
408+ queued = WgDraftFactory (
409+ states = [("draft" , "active" ), ("draft-iesg" , "rfcqueue" ),
410+ ("draft-rfceditor" , "in_progress" )],
411+ )
412+ RpcAssignmentDocEvent .objects .create (
413+ doc = queued , rev = queued .rev , by = Person .objects .get (name = "(System)" ),
414+ type = "changed_rpc_assignments" , assignments = "In Progress (First Edit)" ,
415+ desc = "RPC status changed to In Progress (First Edit)" ,
416+ )
390417
391418 request = RequestFactory ().get ("/doc/recent/" )
392419 request .user = AnonymousUser ()
@@ -396,6 +423,9 @@ def test_prepared_documents_are_picklable(self):
396423 restored , _ = pickle .loads (pickle .dumps ([results , meta ]))
397424 for before , after in zip (results , restored ):
398425 self .assertEqual (after .telechat_date (), before .telechat_date ())
426+ self .assertEqual (
427+ after .rfc_editor_queue_status (), before .rfc_editor_queue_status ()
428+ )
399429
400430 def test_fill_in_telechat_date_matches_the_method (self ):
401431 """The precomputed value has to equal what Document.telechat_date() returns.
@@ -433,6 +463,49 @@ def test_fill_in_telechat_date_matches_the_method(self):
433463 self .assertIsNotNone (expected [future .name ])
434464 self .assertIsNone (expected [past .name ])
435465
466+ def test_fill_in_rfc_editor_queue_status_matches_the_method (self ):
467+ """The precomputed value has to equal what Document.rfc_editor_queue_status() returns.
468+
469+ The document page renders the method and the document tables render the
470+ precomputed value, so a mismatch shows the same document two different statuses.
471+ """
472+ system = Person .objects .get (name = "(System)" )
473+
474+ def queued (state_slug , * statuses ):
475+ doc = WgDraftFactory (
476+ states = [("draft" , "active" ), ("draft-iesg" , "rfcqueue" ),
477+ ("draft-rfceditor" , state_slug )],
478+ )
479+ for status in statuses :
480+ RpcAssignmentDocEvent .objects .create (
481+ doc = doc , rev = doc .rev , by = system , type = "changed_rpc_assignments" ,
482+ assignments = status , desc = f"RPC status changed to { status } " ,
483+ )
484+ return doc
485+
486+ in_progress = queued ("in_progress" , "In Progress (First Edit)" )
487+ # A document that has moved on has an event for every status it has held.
488+ moved_on = queued ("in_progress" , "Awaiting First editor" , "In Final Review" )
489+ blocked = queued ("blocked" , "blocked: Manual Hold" )
490+ # No queue status: a state from before the queue integration, and no state at all.
491+ legacy = queued ("rfc-edit" , "In Progress (First Edit)" )
492+ not_queued = WgDraftFactory ()
493+
494+ fixtures = (in_progress , moved_on , blocked , legacy , not_queued )
495+ expected = {
496+ d .name : Document .objects .get (pk = d .pk ).rfc_editor_queue_status ()
497+ for d in fixtures
498+ }
499+ self .assertEqual (expected [moved_on .name ], "In Final Review" )
500+ self .assertIsNone (expected [legacy .name ])
501+
502+ docs = list (Document .objects .filter (name__in = [d .name for d in fixtures ]))
503+ doc_dict = {d .pk : d for d in docs }
504+ fill_in_rfc_editor_queue_status (docs , doc_dict , list (doc_dict ))
505+
506+ for doc in docs :
507+ self .assertEqual (doc .rfc_editor_queue_status (), expected [doc .name ], doc .name )
508+
436509 def test_search_for_name (self ):
437510 draft = WgDraftFactory (name = 'draft-ietf-mars-test' ,group = GroupFactory (acronym = 'mars' ,parent = Group .objects .get (acronym = 'farfut' )),authors = [PersonFactory ()],ad = PersonFactory ())
438511 draft .set_state (State .objects .get (used = True , type = "draft-iesg" , slug = "pub-req" ))
@@ -688,6 +761,90 @@ def test_auth48_doc_for_ad(self):
688761 self .assertContains (r , draft .name )
689762 self .assertContains (r , 'title="AUTH48"' ) # title attribute of AUTH48 badge in auth48_alert_badge filter
690763
764+ def _status_column_text (self , draft ):
765+ r = self .client .get (
766+ urlreverse ("ietf.doc.views_search.search" )
767+ + f"?activedrafts=on&olddrafts=on&rfcs=on&name={ draft .name } "
768+ )
769+ self .assertEqual (r .status_code , 200 )
770+ return PyQuery (r .content )("td.status" ).text ()
771+
772+ def test_search_labels_the_iesg_state_of_a_queued_draft (self ):
773+ """The RFC Ed Queue state is labeled as the IESG's, because the row also shows
774+ the RFC Editor's own status. Other states are left to speak for themselves.
775+ """
776+ queued = IndividualDraftFactory (
777+ states = [("draft" , "active" ), ("draft-iesg" , "rfcqueue" ),
778+ ("draft-rfceditor" , "in_progress" )]
779+ )
780+ queue_state_name = queued .get_state ("draft-iesg" ).name
781+ self .assertIn (f"IESG: { queue_state_name } " , self ._status_column_text (queued ))
782+ # The document page has a row labeled "IESG state" already, so the value there
783+ # is the bare state name.
784+ r = self .client .get (
785+ urlreverse ("ietf.doc.views_doc.document_main" ,
786+ kwargs = dict (name = queued .name ))
787+ )
788+ self .assertEqual (r .status_code , 200 )
789+ self .assertContains (r , queue_state_name )
790+ self .assertNotContains (r , f"IESG: { queue_state_name } " )
791+
792+ for slug , states in (
793+ ("iesg-eva" , [("draft" , "active" ), ("draft-iesg" , "iesg-eva" )]),
794+ ("idexists" , [("draft" , "active" ), ("draft-iesg" , "idexists" )]),
795+ ("expired" , [("draft" , "expired" ), ("draft-iesg" , "idexists" )]),
796+ ):
797+ with self .subTest (slug = slug ):
798+ draft = IndividualDraftFactory (states = states )
799+ self .assertNotIn ("IESG: " , self ._status_column_text (draft ))
800+
801+ # friendly_state labels this one itself; it must not be labeled twice.
802+ dead = IndividualDraftFactory (
803+ states = [("draft" , "active" ), ("draft-iesg" , "dead" )]
804+ )
805+ self .assertIn ("I-D Exists (IESG: Dead)" , self ._status_column_text (dead ))
806+
807+ def test_search_shows_rfc_editor_queue_status (self ):
808+ """A queued draft's row shows the publication queue Status, as its document page does.
809+
810+ The status column carries two state machines at once, so each state is labeled
811+ with the body it belongs to.
812+ """
813+ draft = IndividualDraftFactory (
814+ states = [
815+ ("draft" , "active" ),
816+ ("draft-iesg" , "rfcqueue" ),
817+ ("draft-rfceditor" , "in_progress" ),
818+ ]
819+ )
820+ RpcAssignmentDocEvent .objects .create (
821+ doc = draft ,
822+ rev = draft .rev ,
823+ by = Person .objects .get (name = "(System)" ),
824+ type = "changed_rpc_assignments" ,
825+ assignments = "In Progress (First Edit)" ,
826+ desc = "RPC status changed to In Progress (First Edit)" ,
827+ )
828+ status = self ._status_column_text (draft )
829+ self .assertIn (f"IESG: { draft .get_state ('draft-iesg' ).name } " , status )
830+ self .assertIn ("RFC Editor: In Progress (First Edit)" , status )
831+
832+ def test_search_falls_back_to_rfc_editor_state_name (self ):
833+ """A document with no queue status shows its draft-rfceditor state name.
834+
835+ Documents that went through the RFC Editor before the publication queue
836+ integration have a draft-rfceditor state but no RpcAssignmentDocEvent.
837+ """
838+ draft = IndividualDraftFactory (
839+ states = [
840+ ("draft" , "active" ),
841+ ("draft-iesg" , "rfcqueue" ),
842+ ("draft-rfceditor" , "rfc-edit" ),
843+ ]
844+ )
845+ status = self ._status_column_text (draft )
846+ self .assertIn (f"RFC Editor: { draft .get_state ('draft-rfceditor' ).name } " , status )
847+
691848 def test_drafts_in_last_call (self ):
692849 draft = IndividualDraftFactory (pages = 1 )
693850 draft .action_holders .set ([PersonFactory ()])
0 commit comments