Skip to content

Commit

Permalink
feat: remove "AD is watching" state (#7960)
Browse files Browse the repository at this point in the history
* feat: remove "AD is watching" state

* chore: update names.json

* refactor: use idexists state, not dead

* fix: remove guidance to use watching state

* chore: remove references to 'watching' state

* feat: remove create_in_state from edit_info view

* test: update test

* style: Black + move class closer to use

* refactor: remove lint

* fix: restore missing admonition

---------

Co-authored-by: Robert Sparks <[email protected]>
  • Loading branch information
jennifer-richards and rjsparks committed Sep 24, 2024
1 parent 5ea4cc1 commit fbcfa19
Show file tree
Hide file tree
Showing 13 changed files with 311 additions and 146 deletions.
4 changes: 2 additions & 2 deletions ietf/doc/expire.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ def expirable_drafts(queryset=None):

# Populate this first time through (but after django has been set up)
if nonexpirable_states is None:
# all IESG states except I-D Exists, AD Watching, and Dead block expiry
nonexpirable_states = list(State.objects.filter(used=True, type="draft-iesg").exclude(slug__in=("idexists","watching", "dead")))
# all IESG states except I-D Exists and Dead block expiry
nonexpirable_states = list(State.objects.filter(used=True, type="draft-iesg").exclude(slug__in=("idexists", "dead")))
# sent to RFC Editor and RFC Published block expiry (the latter
# shouldn't be possible for an active draft, though)
nonexpirable_states += list(State.objects.filter(used=True, type__in=("draft-stream-iab", "draft-stream-irtf", "draft-stream-ise"), slug__in=("rfc-edit", "pub")))
Expand Down
121 changes: 121 additions & 0 deletions ietf/doc/migrations/0024_remove_ad_is_watching_states.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Copyright The IETF Trust 2024, All Rights Reserved

from django.db import migrations


def get_helper(DocHistory, RelatedDocument, RelatedDocHistory, DocumentAuthor, DocHistoryAuthor):
"""Dependency injection wrapper"""

def save_document_in_history(doc):
"""Save a snapshot of document and related objects in the database.
Local copy of ietf.doc.utils.save_document_in_history() to avoid depending on the
code base in a migration.
"""

def get_model_fields_as_dict(obj):
return dict((field.name, getattr(obj, field.name))
for field in obj._meta.fields
if field is not obj._meta.pk)

# copy fields
fields = get_model_fields_as_dict(doc)
fields["doc"] = doc
fields["name"] = doc.name

dochist = DocHistory(**fields)
dochist.save()

# copy many to many
for field in doc._meta.many_to_many:
if field.remote_field.through and field.remote_field.through._meta.auto_created:
hist_field = getattr(dochist, field.name)
hist_field.clear()
hist_field.set(getattr(doc, field.name).all())

# copy remaining tricky many to many
def transfer_fields(obj, HistModel):
mfields = get_model_fields_as_dict(item)
# map doc -> dochist
for k, v in mfields.items():
if v == doc:
mfields[k] = dochist
HistModel.objects.create(**mfields)

for item in RelatedDocument.objects.filter(source=doc):
transfer_fields(item, RelatedDocHistory)

for item in DocumentAuthor.objects.filter(document=doc):
transfer_fields(item, DocHistoryAuthor)

return dochist

return save_document_in_history


def forward(apps, schema_editor):
"""Mark watching draft-iesg state unused after removing it from Documents"""
StateDocEvent = apps.get_model("doc", "StateDocEvent")
Document = apps.get_model("doc", "Document")
State = apps.get_model("doc", "State")
StateType = apps.get_model("doc", "StateType")
Person = apps.get_model("person", "Person")

save_document_in_history = get_helper(
DocHistory=apps.get_model("doc", "DocHistory"),
RelatedDocument=apps.get_model("doc", "RelatedDocument"),
RelatedDocHistory=apps.get_model("doc", "RelatedDocHistory"),
DocumentAuthor=apps.get_model("doc", "DocumentAuthor"),
DocHistoryAuthor=apps.get_model("doc", "DocHistoryAuthor"),
)

draft_iesg_state_type = StateType.objects.get(slug="draft-iesg")
idexists_state = State.objects.get(type=draft_iesg_state_type, slug="idexists")
watching_state = State.objects.get(type=draft_iesg_state_type, slug="watching")
system_person = Person.objects.get(name="(System)")

# Remove state from documents that currently have it
for doc in Document.objects.filter(states=watching_state):
assert doc.type_id == "draft"
doc.states.remove(watching_state)
doc.states.add(idexists_state)
e = StateDocEvent.objects.create(
type="changed_state",
by=system_person,
doc=doc,
rev=doc.rev,
desc=f"{draft_iesg_state_type.label} changed to <b>{idexists_state.name}</b> from {watching_state.name}",
state_type=draft_iesg_state_type,
state=idexists_state,
)
doc.time = e.time
doc.save()
save_document_in_history(doc)
assert not Document.objects.filter(states=watching_state).exists()

# Mark state as unused
watching_state.used = False
watching_state.save()


def reverse(apps, schema_editor):
"""Mark watching draft-iesg state as used
Does not try to re-apply the state to Documents modified by the forward migration. This
could be done in theory, but would either require dangerous history rewriting or add a
lot of history junk.
"""
State = apps.get_model("doc", "State")
StateType = apps.get_model("doc", "StateType")
State.objects.filter(
type=StateType.objects.get(slug="draft-iesg"), slug="watching"
).update(used=True)


class Migration(migrations.Migration):

dependencies = [
("doc", "0023_bofreqspamstate"),
]

operations = [migrations.RunPython(forward, reverse)]
2 changes: 1 addition & 1 deletion ietf/doc/templatetags/ballot_icon.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ def state_age_colored(doc):
if not iesg_state:
return ""

if iesg_state in ["dead", "watching", "pub", "idexists"]:
if iesg_state in ["dead", "pub", "idexists"]:
return ""
try:
state_datetime = (
Expand Down
2 changes: 1 addition & 1 deletion ietf/doc/tests_ballot.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ def test_issue_ballot_warn_if_early(self):
q = PyQuery(r.content)
self.assertFalse(q('[class=text-danger]:contains("not completed IETF Last Call")'))

for state_slug in ["lc", "watching", "ad-eval"]:
for state_slug in ["lc", "ad-eval"]:
draft.set_state(State.objects.get(type="draft-iesg",slug=state_slug))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
Expand Down
86 changes: 37 additions & 49 deletions ietf/doc/tests_draft.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
WriteupDocEvent, DocRelationshipName, IanaExpertDocEvent )
from ietf.doc.utils import get_tags_for_stream_id, create_ballot_if_not_open
from ietf.doc.views_draft import AdoptDraftForm
from ietf.name.models import StreamName, DocTagName, RoleName
from ietf.name.models import DocTagName, RoleName
from ietf.group.factories import GroupFactory, RoleFactory
from ietf.group.models import Group, Role
from ietf.person.factories import PersonFactory, EmailFactory
Expand Down Expand Up @@ -471,69 +471,61 @@ def test_edit_telechat_date(self):
self.assertIn("may not leave enough time", get_payload_text(outbox[-1]))

def test_start_iesg_process_on_draft(self):

draft = WgDraftFactory(
name="draft-ietf-mars-test2",
group__acronym='mars',
group__acronym="mars",
intended_std_level_id="ps",
authors=[Person.objects.get(user__username='ad')],
)
url = urlreverse('ietf.doc.views_draft.edit_info', kwargs=dict(name=draft.name))
authors=[Person.objects.get(user__username="ad")],
)

url = urlreverse("ietf.doc.views_draft.edit_info", kwargs=dict(name=draft.name))
login_testing_unauthorized(self, "secretary", url)

# normal get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('form select[name=intended_std_level]')), 1)
self.assertEqual("", q('form textarea[name=notify]')[0].value.strip())
self.assertEqual(len(q("form select[name=intended_std_level]")), 1)
self.assertEqual("", q("form textarea[name=notify]")[0].value.strip())

# add
events_before = draft.docevent_set.count()
events_before = list(draft.docevent_set.values_list("id", flat=True))
mailbox_before = len(outbox)

ad = Person.objects.get(name="Areað Irector")

r = self.client.post(url,
dict(intended_std_level=str(draft.intended_std_level_id),
ad=ad.pk,
create_in_state=State.objects.get(used=True, type="draft-iesg", slug="watching").pk,
notify="[email protected]",
telechat_date="",
))
r = self.client.post(
url,
dict(
intended_std_level=str(draft.intended_std_level_id),
ad=ad.pk,
notify="[email protected]",
telechat_date="",
),
)
self.assertEqual(r.status_code, 302)

draft = Document.objects.get(name=draft.name)
self.assertEqual(draft.get_state_slug("draft-iesg"), "watching")
self.assertEqual(draft.get_state_slug("draft-iesg"), "pub-req")
self.assertEqual(draft.get_state_slug("draft-stream-ietf"), "sub-pub")
self.assertEqual(draft.ad, ad)
self.assertTrue(not draft.latest_event(TelechatDocEvent, type="scheduled_for_telechat"))
self.assertEqual(draft.docevent_set.count(), events_before + 4)
self.assertCountEqual(draft.action_holders.all(), [draft.ad])
events = list(draft.docevent_set.order_by('time', 'id'))
self.assertEqual(events[-4].type, "started_iesg_process")
self.assertEqual(len(outbox), mailbox_before+1)
self.assertTrue('IESG processing' in outbox[-1]['Subject'])
self.assertTrue('draft-ietf-mars-test2@' in outbox[-1]['To'])

# Redo, starting in publication requested to make sure WG state is also set
draft.set_state(State.objects.get(type_id='draft-iesg', slug='idexists'))
draft.set_state(State.objects.get(type='draft-stream-ietf',slug='writeupw'))
draft.stream = StreamName.objects.get(slug='ietf')
draft.action_holders.clear()
draft.save_with_history([DocEvent.objects.create(doc=draft, rev=draft.rev, type="changed_stream", by=Person.objects.get(user__username="secretary"), desc="Test")])
r = self.client.post(url,
dict(intended_std_level=str(draft.intended_std_level_id),
ad=ad.pk,
create_in_state=State.objects.get(used=True, type="draft-iesg", slug="pub-req").pk,
notify="[email protected]",
telechat_date="",
))
self.assertEqual(r.status_code, 302)
draft = Document.objects.get(name=draft.name)
self.assertEqual(draft.get_state_slug('draft-iesg'),'pub-req')
self.assertEqual(draft.get_state_slug('draft-stream-ietf'),'sub-pub')
self.assertTrue(
not draft.latest_event(TelechatDocEvent, type="scheduled_for_telechat")
)
# check that the expected events were created (don't insist on ordering)
self.assertCountEqual(
draft.docevent_set.exclude(id__in=events_before).values_list("type", flat=True),
[
"changed_action_holders", # action holders set to AD
"changed_document", # WG state set to sub-pub
"changed_document", # AD set
"changed_document", # state change notice email set
"started_iesg_process", # IESG state is now pub-req
],
)
self.assertCountEqual(draft.action_holders.all(), [draft.ad])
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue("IESG processing" in outbox[-1]["Subject"])
self.assertTrue("draft-ietf-mars-test2@" in outbox[-1]["To"])

def test_edit_consensus(self):
draft = WgDraftFactory()
Expand Down Expand Up @@ -750,10 +742,6 @@ def test_expire_drafts(self):

self.assertEqual(len(list(get_expired_drafts())), 1)

draft.set_state(State.objects.get(used=True, type="draft-iesg", slug="watching"))

self.assertEqual(len(list(get_expired_drafts())), 1)

draft.set_state(State.objects.get(used=True, type="draft-iesg", slug="iesg-eva"))

self.assertEqual(len(list(get_expired_drafts())), 0)
Expand Down
4 changes: 2 additions & 2 deletions ietf/doc/views_ballot.py
Original file line number Diff line number Diff line change
Expand Up @@ -641,7 +641,7 @@ def ballot_writeupnotes(request, name):
existing.save()

if "issue_ballot" in request.POST and not ballot_already_approved:
if prev_state.slug in ['watching', 'writeupw', 'goaheadw']:
if prev_state.slug in ['writeupw', 'goaheadw']:
new_state = State.objects.get(used=True, type="draft-iesg", slug='iesg-eva')
prev_tags = doc.tags.filter(slug__in=IESG_SUBSTATE_TAGS)
doc.set_state(new_state)
Expand Down Expand Up @@ -708,7 +708,7 @@ def ballot_writeupnotes(request, name):
back_url=doc.get_absolute_url(),
ballot_issued=bool(doc.latest_event(type="sent_ballot_announcement")),
warn_lc = not doc.docevent_set.filter(lastcalldocevent__expires__date__lt=date_today(DEADLINE_TZINFO)).exists(),
warn_unexpected_state= prev_state if bool(prev_state.slug in ['watching', 'ad-eval', 'lc']) else None,
warn_unexpected_state= prev_state if bool(prev_state.slug in ['ad-eval', 'lc']) else None,
ballot_writeup_form=form,
need_intended_status=need_intended_status,
))
Expand Down
2 changes: 1 addition & 1 deletion ietf/doc/views_doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,7 +587,7 @@ def document_main(request, name, rev=None, document_html=False):
if doc.get_state_slug() not in ["rfc", "expired"] and doc.stream_id in ("ietf",) and not snapshot:
if iesg_state_slug == 'idexists' and can_edit:
actions.append(("Begin IESG Processing", urlreverse('ietf.doc.views_draft.edit_info', kwargs=dict(name=doc.name)) + "?new=1"))
elif can_edit_stream_info and (iesg_state_slug in ('idexists','watching')):
elif can_edit_stream_info and (iesg_state_slug == 'idexists'):
actions.append(("Submit to IESG for Publication", urlreverse('ietf.doc.views_draft.to_iesg', kwargs=dict(name=doc.name))))

if request.user.is_authenticated and hasattr(request.user, "person"):
Expand Down
Loading

0 comments on commit fbcfa19

Please sign in to comment.