Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: remove "AD is watching" state #7960

Merged
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}",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cringing at the html tags, but keeping the set of things to mutate later regular was a good call

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I share the cringe

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
Loading