Repository: ietf-tools/datatracker

Issue:
IRTF state doesn't change when draft sent for IESG conflict review
`type_defect`   |    by csp@csperkins.org

___


I have a draft in IRTF state "Waiting for IRTF Chair" and press "Begin IETF Conflict Review". The conflict review is correctly started and appears in the datatracker, but the IRTF state remains unchanged as "Waiting for IRTF chair". I would expect the state to change – probably to "In IESG review"?

Then, when the IESG conflict review is completed, the IRTF state should change back to "Waiting for IRTF Chair" with tag "IESG Review Completed".

___
_Issue migrated from trac:3469 at 2022-03-04 09:20:16 +0000_


Discussion:
_@rjsparks@nostrum.com_ _changed status from `new` to `under_review`_
_@rjsparks@nostrum.com_ _commented_


___
"In IESG Conflict Review" might be more illuminating, but the state machines aren't really that tightly coupled - is it not possible that the IRTF could be doing other things with the document?

I understand you're looking for clear signals of where you need to apply attention. Will research.
_@csp@csperkins.org_ _commented_


___
Thanks. This is one area where I think the state machines should be a little more tightly coupled. It's easy to miss things or to get out of sync. 
A friendly bump on this: the IESG conflict review states are decoupled from the IRTF document states but should be linked. Specifically:

- When an IRTF stream document enters IESG conflict review it should move to IRTF state "In IESG Review".

- When an IRTF stream document completes IESG conflict review it should move to state "Waiting for IRTF Chair" with tag "IESG Review Completed". This should happen when the document moves to a conflict review state of either "Approved Request to Not Publish - announcement sent", "Approved No Problem - announcement sent", "Withdrawn", or "Dead".

At present the IRTF state has to be updated manually, which leads to updates being missed. No other IRTF state change is possible when these events occur, so there is no reason to require a manual IRTF state change.

Resolving patch:
diff --git a/ietf/doc/views_conflict_review.py b/ietf/doc/views_conflict_review.py
--- a/ietf/doc/views_conflict_review.py
+++ b/ietf/doc/views_conflict_review.py
@@ -1,4 +1,4 @@
-# Copyright The IETF Trust 2012-2020, All Rights Reserved
+# Copyright The IETF Trust 2012-2023, All Rights Reserved
 # -*- coding: utf-8 -*-
 
 
@@ -25,6 +25,7 @@
 from ietf.group.models import Role, Group
 from ietf.iesg.models import TelechatDate
 from ietf.ietfauth.utils import has_role, role_required, is_authorized_in_doc_stream
+from ietf.name.models import DocTagName
 from ietf.person.models import Person
 from ietf.utils import log
 from ietf.utils.mail import send_mail_preformatted
@@ -90,6 +91,10 @@ def change_state(request, name, option=None):
                                                       review,
                                                       ok_to_publish)
 
+                if new_state.slug in ["appr-reqnopub-sent", "appr-noprob-sent", "withdraw", "dead"]:
+                    doc = review.related_that_doc("conflrev")[0].document
+                    if doc.stream_id == "irtf":
+                        close_review_irtf_state(doc, login)
 
             return redirect('ietf.doc.views_doc.document_main', name=review.name)
     else:
@@ -355,6 +360,10 @@ def approve_conflict_review(request, name):
             c.desc = "The following approval message was sent\n"+form.cleaned_data['announcement_text']
             c.save()
 
+            doc = review.related_that_doc("conflrev")[0].document
+            if doc.stream_id == "irtf":
+                close_review_irtf_state(doc, login)
+
             return HttpResponseRedirect(review.get_absolute_url())
 
     else:
@@ -488,6 +497,9 @@ def start_review_as_secretariat(request, name):
 
             send_conflict_review_started_email(request, conflict_review)
 
+            if doc_to_review.stream_id == 'irtf':
+                start_review_irtf_state(doc_to_review, login)
+
             return HttpResponseRedirect(conflict_review.get_absolute_url())
     else: 
         notify_addresses = build_notify_addresses(doc_to_review)
@@ -522,6 +534,9 @@ def start_review_as_stream_owner(request, name):
 
             send_conflict_review_started_email(request, conflict_review)
 
+            if doc_to_review.stream_id == 'irtf':
+                start_review_irtf_state(doc_to_review, login)
+
             return HttpResponseRedirect(conflict_review.get_absolute_url())
     else: 
         notify_addresses = build_notify_addresses(doc_to_review)
@@ -536,3 +551,26 @@ def start_review_as_stream_owner(request, name):
                                'doc_to_review': doc_to_review,
                               },
                           )
+
+def start_review_irtf_state(doc, by):
+    prev_state = doc.get_state('draft-stream-irtf')
+    new_state = State.objects.get(type_id='draft-stream-irtf', slug='iesg-rev')
+
+    if new_state != prev_state:
+        doc.set_state(new_state)
+        events = []
+        events.append(add_state_change_event(doc, by, prev_state, new_state))
+        doc.save_with_history(events)
+
+def close_review_irtf_state(doc, by):
+    prev_state = doc.get_state("draft-stream-irtf")
+    new_state = State.objects.get(type_id="draft-stream-irtf", slug="chair-w")
+    prev_tags = set(doc.tags.all())
+    new_tags = set(DocTagName.objects.filter(pk="iesg-com"))
+
+    if new_state != prev_state:
+        doc.set_state(new_state)
+        doc.tags.clear()
+        doc.tags.set(new_tags)
+        events = [add_state_change_event(doc, by, prev_state, new_state, prev_tags, new_tags)]
+        doc.save_with_history(events)


Test patch:
diff --git a/ietf/doc/tests_conflict_review.py b/ietf/doc/tests_conflict_review.py
--- a/ietf/doc/tests_conflict_review.py
+++ b/ietf/doc/tests_conflict_review.py
@@ -1,4 +1,4 @@
-# Copyright The IETF Trust 2012-2020, All Rights Reserved
+# Copyright The IETF Trust 2012-2023, All Rights Reserved
 # -*- coding: utf-8 -*-
 
 
@@ -13,8 +13,8 @@
 
 import debug    # pyflakes:ignore
 
-from ietf.doc.factories import IndividualDraftFactory, ConflictReviewFactory
-from ietf.doc.models import Document, DocEvent, NewRevisionDocEvent, BallotPositionDocEvent, TelechatDocEvent, State
+from ietf.doc.factories import IndividualDraftFactory, ConflictReviewFactory, RgDraftFactory
+from ietf.doc.models import Document, DocEvent, NewRevisionDocEvent, BallotPositionDocEvent, TelechatDocEvent, State, DocTagName
 from ietf.doc.utils import create_ballot_if_not_open
 from ietf.doc.views_conflict_review import default_approval_text
 from ietf.group.models import Person
@@ -450,3 +450,65 @@ def test_subsequent_submission(self):
     def setUp(self):
         super().setUp()
         ConflictReviewFactory(name='conflict-review-imaginary-irtf-submission',review_of=IndividualDraftFactory(name='draft-imaginary-irtf-submission',stream_id='irtf'),notify='notifyme@example.net')
+
+class ConflictReviewIrtfStateTests(TestCase):
+
+    def start_review(self, role, kwargs=None):
+        doc = RgDraftFactory()
+        url = urlreverse('ietf.doc.views_conflict_review.start_review', kwargs=dict(name=doc.name))
+        login_testing_unauthorized(self, role, url)
+
+        r = self.client.post(url, kwargs)
+        self.assertEqual(r.status_code, 302)
+        self.assertEqual(doc.get_state('draft-stream-irtf').slug, 'iesg-rev')
+
+    def test_start_review_as_secretary(self):
+        ad_strpk = str(Person.objects.get(name='Areað Irector').pk)
+        state_strpk = str(State.objects.get(used=True, slug='needshep', type__slug='conflrev').pk)
+        self.start_review('secretary', kwargs=dict(ad=ad_strpk, create_in_state=state_strpk))
+
+    def test_start_review_as_stream_owner(self):
+        self.start_review('irtf-chair')
+
+    def close_review(self, close_type, role):
+        doc = RgDraftFactory()
+        review = ConflictReviewFactory(review_of=doc)
+        url = urlreverse('ietf.doc.views_conflict_review.change_state', kwargs=dict(name=review.name))
+        login_testing_unauthorized(self, role, url)
+
+        strpk = str(State.objects.get(used=True, slug=close_type, type__slug='conflrev').pk)
+        r = self.client.post(url, dict(review_state=strpk))
+        self.assertEqual(r.status_code, 302)
+        self.assertEqual(doc.get_state('draft-stream-irtf').slug, 'chair-w')
+        self.assertIn(DocTagName.objects.get(pk='iesg-com'), doc.tags.all())
+
+    def test_close_review_reqnopub_as_secretary(self):
+        self.close_review('appr-reqnopub-sent', 'secretary')
+
+    def test_close_review_noprob_as_secretary(self):
+        self.close_review('appr-noprob-sent', 'secretary')
+
+    def test_close_review_withdraw_as_secretary(self):
+        self.close_review('withdraw', 'secretary')
+
+    def test_close_review_dead_as_secretary(self):
+        self.close_review('dead', 'secretary')
+
+    def test_close_review_withdraw_as_ad(self):
+        self.close_review('withdraw', 'ad')
+
+    def test_close_review_dead_as_ad(self):
+        self.close_review('dead', 'ad')
+
+    def test_approve_review(self):
+        doc = RgDraftFactory()
+        review = ConflictReviewFactory(review_of=doc)
+        review.set_state(State.objects.get(used=True, slug='appr-noprob-pend', type='conflrev'))
+
+        url = urlreverse('ietf.doc.views_conflict_review.approve_conflict_review', kwargs=dict(name=review.name))
+        login_testing_unauthorized(self, 'secretary', url)
+
+        r = self.client.post(url, dict(announcement_text=default_approval_text(review)))
+        self.assertEqual(r.status_code, 302)
+        self.assertEqual(doc.get_state('draft-stream-irtf').slug, 'chair-w')
+        self.assertIn(DocTagName.objects.get(pk='iesg-com'), doc.tags.all())