Skip to content

Commit fbcfa19

Browse files
feat: remove "AD is watching" state (ietf-tools#7960)
* 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 <rjsparks@nostrum.com>
1 parent 5ea4cc1 commit fbcfa19

13 files changed

Lines changed: 311 additions & 146 deletions

File tree

ietf/doc/expire.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ def expirable_drafts(queryset=None):
3434

3535
# Populate this first time through (but after django has been set up)
3636
if nonexpirable_states is None:
37-
# all IESG states except I-D Exists, AD Watching, and Dead block expiry
38-
nonexpirable_states = list(State.objects.filter(used=True, type="draft-iesg").exclude(slug__in=("idexists","watching", "dead")))
37+
# all IESG states except I-D Exists and Dead block expiry
38+
nonexpirable_states = list(State.objects.filter(used=True, type="draft-iesg").exclude(slug__in=("idexists", "dead")))
3939
# sent to RFC Editor and RFC Published block expiry (the latter
4040
# shouldn't be possible for an active draft, though)
4141
nonexpirable_states += list(State.objects.filter(used=True, type__in=("draft-stream-iab", "draft-stream-irtf", "draft-stream-ise"), slug__in=("rfc-edit", "pub")))
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# Copyright The IETF Trust 2024, All Rights Reserved
2+
3+
from django.db import migrations
4+
5+
6+
def get_helper(DocHistory, RelatedDocument, RelatedDocHistory, DocumentAuthor, DocHistoryAuthor):
7+
"""Dependency injection wrapper"""
8+
9+
def save_document_in_history(doc):
10+
"""Save a snapshot of document and related objects in the database.
11+
12+
Local copy of ietf.doc.utils.save_document_in_history() to avoid depending on the
13+
code base in a migration.
14+
"""
15+
16+
def get_model_fields_as_dict(obj):
17+
return dict((field.name, getattr(obj, field.name))
18+
for field in obj._meta.fields
19+
if field is not obj._meta.pk)
20+
21+
# copy fields
22+
fields = get_model_fields_as_dict(doc)
23+
fields["doc"] = doc
24+
fields["name"] = doc.name
25+
26+
dochist = DocHistory(**fields)
27+
dochist.save()
28+
29+
# copy many to many
30+
for field in doc._meta.many_to_many:
31+
if field.remote_field.through and field.remote_field.through._meta.auto_created:
32+
hist_field = getattr(dochist, field.name)
33+
hist_field.clear()
34+
hist_field.set(getattr(doc, field.name).all())
35+
36+
# copy remaining tricky many to many
37+
def transfer_fields(obj, HistModel):
38+
mfields = get_model_fields_as_dict(item)
39+
# map doc -> dochist
40+
for k, v in mfields.items():
41+
if v == doc:
42+
mfields[k] = dochist
43+
HistModel.objects.create(**mfields)
44+
45+
for item in RelatedDocument.objects.filter(source=doc):
46+
transfer_fields(item, RelatedDocHistory)
47+
48+
for item in DocumentAuthor.objects.filter(document=doc):
49+
transfer_fields(item, DocHistoryAuthor)
50+
51+
return dochist
52+
53+
return save_document_in_history
54+
55+
56+
def forward(apps, schema_editor):
57+
"""Mark watching draft-iesg state unused after removing it from Documents"""
58+
StateDocEvent = apps.get_model("doc", "StateDocEvent")
59+
Document = apps.get_model("doc", "Document")
60+
State = apps.get_model("doc", "State")
61+
StateType = apps.get_model("doc", "StateType")
62+
Person = apps.get_model("person", "Person")
63+
64+
save_document_in_history = get_helper(
65+
DocHistory=apps.get_model("doc", "DocHistory"),
66+
RelatedDocument=apps.get_model("doc", "RelatedDocument"),
67+
RelatedDocHistory=apps.get_model("doc", "RelatedDocHistory"),
68+
DocumentAuthor=apps.get_model("doc", "DocumentAuthor"),
69+
DocHistoryAuthor=apps.get_model("doc", "DocHistoryAuthor"),
70+
)
71+
72+
draft_iesg_state_type = StateType.objects.get(slug="draft-iesg")
73+
idexists_state = State.objects.get(type=draft_iesg_state_type, slug="idexists")
74+
watching_state = State.objects.get(type=draft_iesg_state_type, slug="watching")
75+
system_person = Person.objects.get(name="(System)")
76+
77+
# Remove state from documents that currently have it
78+
for doc in Document.objects.filter(states=watching_state):
79+
assert doc.type_id == "draft"
80+
doc.states.remove(watching_state)
81+
doc.states.add(idexists_state)
82+
e = StateDocEvent.objects.create(
83+
type="changed_state",
84+
by=system_person,
85+
doc=doc,
86+
rev=doc.rev,
87+
desc=f"{draft_iesg_state_type.label} changed to <b>{idexists_state.name}</b> from {watching_state.name}",
88+
state_type=draft_iesg_state_type,
89+
state=idexists_state,
90+
)
91+
doc.time = e.time
92+
doc.save()
93+
save_document_in_history(doc)
94+
assert not Document.objects.filter(states=watching_state).exists()
95+
96+
# Mark state as unused
97+
watching_state.used = False
98+
watching_state.save()
99+
100+
101+
def reverse(apps, schema_editor):
102+
"""Mark watching draft-iesg state as used
103+
104+
Does not try to re-apply the state to Documents modified by the forward migration. This
105+
could be done in theory, but would either require dangerous history rewriting or add a
106+
lot of history junk.
107+
"""
108+
State = apps.get_model("doc", "State")
109+
StateType = apps.get_model("doc", "StateType")
110+
State.objects.filter(
111+
type=StateType.objects.get(slug="draft-iesg"), slug="watching"
112+
).update(used=True)
113+
114+
115+
class Migration(migrations.Migration):
116+
117+
dependencies = [
118+
("doc", "0023_bofreqspamstate"),
119+
]
120+
121+
operations = [migrations.RunPython(forward, reverse)]

ietf/doc/templatetags/ballot_icon.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ def state_age_colored(doc):
184184
if not iesg_state:
185185
return ""
186186

187-
if iesg_state in ["dead", "watching", "pub", "idexists"]:
187+
if iesg_state in ["dead", "pub", "idexists"]:
188188
return ""
189189
try:
190190
state_datetime = (

ietf/doc/tests_ballot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,7 @@ def test_issue_ballot_warn_if_early(self):
559559
q = PyQuery(r.content)
560560
self.assertFalse(q('[class=text-danger]:contains("not completed IETF Last Call")'))
561561

562-
for state_slug in ["lc", "watching", "ad-eval"]:
562+
for state_slug in ["lc", "ad-eval"]:
563563
draft.set_state(State.objects.get(type="draft-iesg",slug=state_slug))
564564
r = self.client.get(url)
565565
self.assertEqual(r.status_code, 200)

ietf/doc/tests_draft.py

Lines changed: 37 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
WriteupDocEvent, DocRelationshipName, IanaExpertDocEvent )
2727
from ietf.doc.utils import get_tags_for_stream_id, create_ballot_if_not_open
2828
from ietf.doc.views_draft import AdoptDraftForm
29-
from ietf.name.models import StreamName, DocTagName, RoleName
29+
from ietf.name.models import DocTagName, RoleName
3030
from ietf.group.factories import GroupFactory, RoleFactory
3131
from ietf.group.models import Group, Role
3232
from ietf.person.factories import PersonFactory, EmailFactory
@@ -471,69 +471,61 @@ def test_edit_telechat_date(self):
471471
self.assertIn("may not leave enough time", get_payload_text(outbox[-1]))
472472

473473
def test_start_iesg_process_on_draft(self):
474-
475474
draft = WgDraftFactory(
476475
name="draft-ietf-mars-test2",
477-
group__acronym='mars',
476+
group__acronym="mars",
478477
intended_std_level_id="ps",
479-
authors=[Person.objects.get(user__username='ad')],
480-
)
481-
482-
url = urlreverse('ietf.doc.views_draft.edit_info', kwargs=dict(name=draft.name))
478+
authors=[Person.objects.get(user__username="ad")],
479+
)
480+
481+
url = urlreverse("ietf.doc.views_draft.edit_info", kwargs=dict(name=draft.name))
483482
login_testing_unauthorized(self, "secretary", url)
484483

485484
# normal get
486485
r = self.client.get(url)
487486
self.assertEqual(r.status_code, 200)
488487
q = PyQuery(r.content)
489-
self.assertEqual(len(q('form select[name=intended_std_level]')), 1)
490-
self.assertEqual("", q('form textarea[name=notify]')[0].value.strip())
488+
self.assertEqual(len(q("form select[name=intended_std_level]")), 1)
489+
self.assertEqual("", q("form textarea[name=notify]")[0].value.strip())
491490

492-
# add
493-
events_before = draft.docevent_set.count()
491+
events_before = list(draft.docevent_set.values_list("id", flat=True))
494492
mailbox_before = len(outbox)
495493

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

498-
r = self.client.post(url,
499-
dict(intended_std_level=str(draft.intended_std_level_id),
500-
ad=ad.pk,
501-
create_in_state=State.objects.get(used=True, type="draft-iesg", slug="watching").pk,
502-
notify="test@example.com",
503-
telechat_date="",
504-
))
496+
r = self.client.post(
497+
url,
498+
dict(
499+
intended_std_level=str(draft.intended_std_level_id),
500+
ad=ad.pk,
501+
notify="test@example.com",
502+
telechat_date="",
503+
),
504+
)
505505
self.assertEqual(r.status_code, 302)
506506

507507
draft = Document.objects.get(name=draft.name)
508-
self.assertEqual(draft.get_state_slug("draft-iesg"), "watching")
508+
self.assertEqual(draft.get_state_slug("draft-iesg"), "pub-req")
509+
self.assertEqual(draft.get_state_slug("draft-stream-ietf"), "sub-pub")
509510
self.assertEqual(draft.ad, ad)
510-
self.assertTrue(not draft.latest_event(TelechatDocEvent, type="scheduled_for_telechat"))
511-
self.assertEqual(draft.docevent_set.count(), events_before + 4)
512-
self.assertCountEqual(draft.action_holders.all(), [draft.ad])
513-
events = list(draft.docevent_set.order_by('time', 'id'))
514-
self.assertEqual(events[-4].type, "started_iesg_process")
515-
self.assertEqual(len(outbox), mailbox_before+1)
516-
self.assertTrue('IESG processing' in outbox[-1]['Subject'])
517-
self.assertTrue('draft-ietf-mars-test2@' in outbox[-1]['To'])
518-
519-
# Redo, starting in publication requested to make sure WG state is also set
520-
draft.set_state(State.objects.get(type_id='draft-iesg', slug='idexists'))
521-
draft.set_state(State.objects.get(type='draft-stream-ietf',slug='writeupw'))
522-
draft.stream = StreamName.objects.get(slug='ietf')
523-
draft.action_holders.clear()
524-
draft.save_with_history([DocEvent.objects.create(doc=draft, rev=draft.rev, type="changed_stream", by=Person.objects.get(user__username="secretary"), desc="Test")])
525-
r = self.client.post(url,
526-
dict(intended_std_level=str(draft.intended_std_level_id),
527-
ad=ad.pk,
528-
create_in_state=State.objects.get(used=True, type="draft-iesg", slug="pub-req").pk,
529-
notify="test@example.com",
530-
telechat_date="",
531-
))
532-
self.assertEqual(r.status_code, 302)
533-
draft = Document.objects.get(name=draft.name)
534-
self.assertEqual(draft.get_state_slug('draft-iesg'),'pub-req')
535-
self.assertEqual(draft.get_state_slug('draft-stream-ietf'),'sub-pub')
511+
self.assertTrue(
512+
not draft.latest_event(TelechatDocEvent, type="scheduled_for_telechat")
513+
)
514+
# check that the expected events were created (don't insist on ordering)
515+
self.assertCountEqual(
516+
draft.docevent_set.exclude(id__in=events_before).values_list("type", flat=True),
517+
[
518+
"changed_action_holders", # action holders set to AD
519+
"changed_document", # WG state set to sub-pub
520+
"changed_document", # AD set
521+
"changed_document", # state change notice email set
522+
"started_iesg_process", # IESG state is now pub-req
523+
],
524+
)
536525
self.assertCountEqual(draft.action_holders.all(), [draft.ad])
526+
self.assertEqual(len(outbox), mailbox_before + 1)
527+
self.assertTrue("IESG processing" in outbox[-1]["Subject"])
528+
self.assertTrue("draft-ietf-mars-test2@" in outbox[-1]["To"])
537529

538530
def test_edit_consensus(self):
539531
draft = WgDraftFactory()
@@ -750,10 +742,6 @@ def test_expire_drafts(self):
750742

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

753-
draft.set_state(State.objects.get(used=True, type="draft-iesg", slug="watching"))
754-
755-
self.assertEqual(len(list(get_expired_drafts())), 1)
756-
757745
draft.set_state(State.objects.get(used=True, type="draft-iesg", slug="iesg-eva"))
758746

759747
self.assertEqual(len(list(get_expired_drafts())), 0)

ietf/doc/views_ballot.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -641,7 +641,7 @@ def ballot_writeupnotes(request, name):
641641
existing.save()
642642

643643
if "issue_ballot" in request.POST and not ballot_already_approved:
644-
if prev_state.slug in ['watching', 'writeupw', 'goaheadw']:
644+
if prev_state.slug in ['writeupw', 'goaheadw']:
645645
new_state = State.objects.get(used=True, type="draft-iesg", slug='iesg-eva')
646646
prev_tags = doc.tags.filter(slug__in=IESG_SUBSTATE_TAGS)
647647
doc.set_state(new_state)
@@ -708,7 +708,7 @@ def ballot_writeupnotes(request, name):
708708
back_url=doc.get_absolute_url(),
709709
ballot_issued=bool(doc.latest_event(type="sent_ballot_announcement")),
710710
warn_lc = not doc.docevent_set.filter(lastcalldocevent__expires__date__lt=date_today(DEADLINE_TZINFO)).exists(),
711-
warn_unexpected_state= prev_state if bool(prev_state.slug in ['watching', 'ad-eval', 'lc']) else None,
711+
warn_unexpected_state= prev_state if bool(prev_state.slug in ['ad-eval', 'lc']) else None,
712712
ballot_writeup_form=form,
713713
need_intended_status=need_intended_status,
714714
))

ietf/doc/views_doc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -587,7 +587,7 @@ def document_main(request, name, rev=None, document_html=False):
587587
if doc.get_state_slug() not in ["rfc", "expired"] and doc.stream_id in ("ietf",) and not snapshot:
588588
if iesg_state_slug == 'idexists' and can_edit:
589589
actions.append(("Begin IESG Processing", urlreverse('ietf.doc.views_draft.edit_info', kwargs=dict(name=doc.name)) + "?new=1"))
590-
elif can_edit_stream_info and (iesg_state_slug in ('idexists','watching')):
590+
elif can_edit_stream_info and (iesg_state_slug == 'idexists'):
591591
actions.append(("Submit to IESG for Publication", urlreverse('ietf.doc.views_draft.to_iesg', kwargs=dict(name=doc.name))))
592592

593593
if request.user.is_authenticated and hasattr(request.user, "person"):

0 commit comments

Comments
 (0)