Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
from ietf.name.models import DocTagName
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="test@example.com",
telechat_date="",
))
r = self.client.post(
url,
dict(
intended_std_level=str(draft.intended_std_level_id),
ad=ad.pk,
notify="test@example.com",
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="test@example.com",
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 @@ -582,7 +582,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