Skip to content

Commit 6118975

Browse files
committed
Added an explicit ID-Exists state for the IESG state machine. Reworked code so that the IESG state machine always has a state. Added the ability to release a document from a working group, research group, or the independent stream. Releasing a document removes all stream state, and sets the document to have no stream.
- Legacy-Id: 15809
1 parent f69ad28 commit 6118975

25 files changed

Lines changed: 10935 additions & 10605 deletions

ietf/doc/expire.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import datetime, os, shutil, glob, re
66
from pathlib import Path
77

8+
from ietf.utils import log
89
from ietf.utils.mail import send_mail
910
from ietf.doc.models import Document, DocEvent, State, IESG_SUBSTATE_TAGS
1011
from ietf.person.models import Person
@@ -17,8 +18,11 @@ def expirable_draft(draft):
1718
"""Return whether draft is in an expirable state or not. This is
1819
the single draft version of the logic in expirable_drafts. These
1920
two functions need to be kept in sync."""
21+
if draft.type_id != 'draft':
22+
return False
23+
log.assertion('draft.get_state_slug("draft-iesg")')
2024
return (draft.expires and draft.get_state_slug() == "active"
21-
and draft.get_state_slug("draft-iesg") in (None, "watching", "dead")
25+
and draft.get_state_slug("draft-iesg") in ("idexists", "watching", "dead")
2226
and draft.get_state_slug("draft-stream-%s" % draft.stream_id) not in ("rfc-edit", "pub")
2327
and not draft.tags.filter(slug="rfc-rev"))
2428

@@ -29,8 +33,8 @@ def expirable_drafts():
2933
d = Document.objects.filter(states__type="draft", states__slug="active").exclude(expires=None)
3034

3135
nonexpirable_states = []
32-
# all IESG states except AD Watching and Dead block expiry
33-
nonexpirable_states += list(State.objects.filter(used=True, type="draft-iesg").exclude(slug__in=("watching", "dead")))
36+
# all IESG states except I-D Exists, AD Watching, and Dead block expiry
37+
nonexpirable_states += list(State.objects.filter(used=True, type="draft-iesg").exclude(slug__in=("idexists","watching", "dead")))
3438
# sent to RFC Editor and RFC Published block expiry (the latter
3539
# shouldn't be possible for an active draft, though)
3640
nonexpirable_states += list(State.objects.filter(used=True, type__in=("draft-stream-iab", "draft-stream-irtf", "draft-stream-ise"), slug__in=("rfc-edit", "pub")))
@@ -75,7 +79,8 @@ def send_expire_warning_for_draft(doc):
7579
(to,cc) = gather_address_lists('doc_expires_soon',doc=doc)
7680

7781
s = doc.get_state("draft-iesg")
78-
state = s.name if s else "I-D Exists"
82+
log.assertion('s')
83+
state = s.name if s else "I-D Exists" # TODO remove the if clause after some runtime shows no assertions
7984

8085
frm = None
8186
request = None
@@ -94,7 +99,8 @@ def send_expire_notice_for_draft(doc):
9499
return
95100

96101
s = doc.get_state("draft-iesg")
97-
state = s.name if s else "I-D Exists"
102+
log.assertion('s')
103+
state = s.name if s else "I-D Exists" # TODO remove the if clause after some rintime shows no assertions
98104

99105
request = None
100106
(to,cc) = gather_address_lists('doc_expired',doc=doc)

ietf/doc/factories.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ def states(obj, create, extracted, **kwargs): # pylint: disable=no-self-argument
4646
if create and extracted:
4747
for (state_type_id,state_slug) in extracted:
4848
obj.set_state(State.objects.get(type_id=state_type_id,slug=state_slug))
49+
if obj.type_id == 'draft':
50+
if not obj.states.filter(type_id='draft-iesg').exists():
51+
obj.set_state(State.objects.get(type_id='draft-iesg', slug='idexists'))
4952

5053
@factory.post_generation
5154
def authors(obj, create, extracted, **kwargs): # pylint: disable=no-self-argument
@@ -90,8 +93,11 @@ def states(obj, create, extracted, **kwargs):
9093
if extracted:
9194
for (state_type_id,state_slug) in extracted:
9295
obj.set_state(State.objects.get(type_id=state_type_id,slug=state_slug))
96+
if not obj.get_state('draft-iesg'):
97+
obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists'))
9398
else:
9499
obj.set_state(State.objects.get(type_id='draft',slug='active'))
100+
obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists'))
95101

96102
class IndividualRfcFactory(IndividualDraftFactory):
97103

@@ -120,9 +126,12 @@ def states(obj, create, extracted, **kwargs):
120126
if extracted:
121127
for (state_type_id,state_slug) in extracted:
122128
obj.set_state(State.objects.get(type_id=state_type_id,slug=state_slug))
129+
if not obj.get_state('draft-iesg'):
130+
obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists'))
123131
else:
124132
obj.set_state(State.objects.get(type_id='draft',slug='active'))
125133
obj.set_state(State.objects.get(type_id='draft-stream-ietf',slug='wg-doc'))
134+
obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists'))
126135

127136
class WgRfcFactory(WgDraftFactory):
128137

@@ -137,8 +146,33 @@ def states(obj, create, extracted, **kwargs):
137146
if extracted:
138147
for (state_type_id,state_slug) in extracted:
139148
obj.set_state(State.objects.get(type_id=state_type_id,slug=state_slug))
149+
if not obj.get_state('draft-iesg'):
150+
obj.set_state(State.objects.get(type_id='draft-iesg', slug='pub'))
140151
else:
141152
obj.set_state(State.objects.get(type_id='draft',slug='rfc'))
153+
obj.set_state(State.objects.get(type_id='draft-iesg', slug='pub'))
154+
155+
156+
class RgDraftFactory(BaseDocumentFactory):
157+
158+
type_id = 'draft'
159+
group = factory.SubFactory('ietf.group.factories.GroupFactory',type_id='rg')
160+
stream_id = 'irtf'
161+
162+
@factory.post_generation
163+
def states(obj, create, extracted, **kwargs):
164+
if not create:
165+
return
166+
if extracted:
167+
for (state_type_id,state_slug) in extracted:
168+
obj.set_state(State.objects.get(type_id=state_type_id,slug=state_slug))
169+
if not obj.get_state('draft-iesg'):
170+
obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists'))
171+
else:
172+
obj.set_state(State.objects.get(type_id='draft',slug='active'))
173+
obj.set_state(State.objects.get(type_id='draft-stream-irtf',slug='active'))
174+
obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists'))
175+
142176

143177
class CharterFactory(BaseDocumentFactory):
144178

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# -*- coding: utf-8 -*-
2+
# Generated by Django 1.11.16 on 2018-11-04 10:56
3+
from __future__ import unicode_literals
4+
5+
from tqdm import tqdm
6+
7+
from django.db import migrations
8+
9+
10+
def forward(apps, schema_editor):
11+
State = apps.get_model('doc','State')
12+
Document = apps.get_model('doc','Document')
13+
DocHistory = apps.get_model('doc','DocHistory')
14+
15+
idexists = State.objects.create(
16+
type_id = 'draft-iesg',
17+
slug = 'idexists',
18+
name = 'I-D Exists',
19+
used = True,
20+
desc = 'The IESG has not started processing this draft, or has stopped processing it without publicastion.',
21+
order = 0,
22+
)
23+
idexists.next_states.set(State.objects.filter(type='draft-iesg',slug__in=['pub-req','watching']))
24+
25+
#for doc in tqdm(Document.objects.filter(type='draft'):
26+
# if not doc.states.filter(type='draft-iesg').exists():
27+
# doc.states.add(idexists)
28+
# for dh in doc.history_set.all():
29+
# if not dh.states.filter(type='draft-iesg').exists():
30+
# dh.states.add(idexists)
31+
32+
for doc in tqdm(Document.objects.filter(type_id='draft').exclude(states__type_id='draft-iesg')):
33+
doc.states.add(idexists)
34+
for history in tqdm(DocHistory.objects.filter(type_id='draft').exclude(states__type_id='draft-iesg')):
35+
history.states.add(idexists)
36+
37+
38+
def reverse(apps, schema_editor):
39+
State = apps.get_model('doc','State')
40+
State.objects.filter(slug='idexists').delete()
41+
42+
class Migration(migrations.Migration):
43+
44+
dependencies = [
45+
('doc', '0006_ballotpositiondocevent_send_email'),
46+
]
47+
48+
operations = [
49+
migrations.RunPython(forward, reverse)
50+
]

ietf/doc/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ def set_state(self, state):
267267

268268
def unset_state(self, state_type):
269269
"""Unset state of type so no state of that type is any longer set."""
270+
log.assertion('state_type != "draft-iesg"')
270271
self.states.remove(*self.states.filter(type=state_type))
271272
self.state_cache = None # invalidate cache
272273
self._cached_state_slug = {}
@@ -325,6 +326,7 @@ def friendly_state(self):
325326
else:
326327
return "Replaced"
327328
elif state.slug == "active":
329+
log.assertion('iesg_state')
328330
if iesg_state:
329331
if iesg_state.slug == "dead":
330332
# Many drafts in the draft-iesg "Dead" state are not dead

ietf/doc/templatetags/ballot_icon.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
from ietf.ietfauth.utils import user_is_person, has_role
4343
from ietf.doc.models import BallotPositionDocEvent, IESG_BALLOT_ACTIVE_STATES
4444
from ietf.name.models import BallotPositionName
45+
from ietf.utils import log
4546

4647

4748
register = template.Library()
@@ -157,10 +158,11 @@ def state_age_colored(doc):
157158
# Don't show anything for expired/withdrawn/replaced drafts
158159
return ""
159160
iesg_state = doc.get_state_slug('draft-iesg')
161+
log.assertion('iesg_state')
160162
if not iesg_state:
161163
return ""
162164

163-
if iesg_state in ["dead", "watching", "pub"]:
165+
if iesg_state in ["dead", "watching", "pub", "idexists"]:
164166
return ""
165167
try:
166168
state_date = doc.docevent_set.filter(

ietf/doc/tests_draft.py

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@
1010

1111
import debug # pyflakes:ignore
1212

13-
from ietf.doc.factories import IndividualDraftFactory, WgDraftFactory, DocEventFactory
13+
from ietf.doc.factories import IndividualDraftFactory, WgDraftFactory, RgDraftFactory, DocEventFactory
1414
from ietf.doc.models import ( Document, DocReminder, DocEvent,
1515
ConsensusDocEvent, LastCallDocEvent, RelatedDocument, State, TelechatDocEvent,
1616
WriteupDocEvent, DocRelationshipName)
1717
from ietf.doc.utils import get_tags_for_stream_id, create_ballot_if_not_open
1818
from ietf.name.models import StreamName, DocTagName
1919
from ietf.group.factories import GroupFactory, RoleFactory
20-
from ietf.group.models import Group
20+
from ietf.group.models import Group, Role
2121
from ietf.person.factories import PersonFactory
2222
from ietf.person.models import Person, Email
2323
from ietf.meeting.models import Meeting, MeetingTypeName
@@ -419,7 +419,7 @@ def test_start_iesg_process_on_draft(self):
419419
self.assertTrue('draft-ietf-mars-test2@' in outbox[-1]['To'])
420420

421421
# Redo, starting in publication requested to make sure WG state is also set
422-
draft.unset_state('draft-iesg')
422+
draft.set_state(State.objects.get(type_id='draft-iesg', slug='idexists'))
423423
draft.set_state(State.objects.get(type='draft-stream-ietf',slug='writeupw'))
424424
draft.stream = StreamName.objects.get(slug='ietf')
425425
draft.save_with_history([DocEvent.objects.create(doc=draft, rev=draft.rev, type="changed_stream", by=Person.objects.get(user__username="secretary"), desc="Test")])
@@ -589,7 +589,7 @@ def test_warn_expirable_drafts(self):
589589
self.assertEqual(len(list(get_soon_to_expire_drafts(14))), 0)
590590

591591
# hack into expirable state
592-
draft.unset_state("draft-iesg")
592+
draft.set_state(State.objects.get(type_id='draft-iesg',slug='idexists'))
593593
draft.expires = datetime.datetime.now() + datetime.timedelta(days=10)
594594
draft.save_with_history([DocEvent.objects.create(doc=draft, rev=draft.rev, type="changed_document", by=Person.objects.get(user__username="secretary"), desc="Test")])
595595

@@ -616,7 +616,7 @@ def test_expire_drafts(self):
616616
self.assertEqual(len(list(get_expired_drafts())), 0)
617617

618618
# hack into expirable state
619-
draft.unset_state("draft-iesg")
619+
draft.set_state(State.objects.get(type_id='draft-iesg',slug='idexists'))
620620
draft.expires = datetime.datetime.now()
621621
draft.save_with_history([DocEvent.objects.create(doc=draft, rev=draft.rev, type="changed_document", by=Person.objects.get(user__username="secretary"), desc="Test")])
622622

@@ -1114,7 +1114,7 @@ def test_cancel_submission(self):
11141114
self.assertEqual(r.status_code, 302)
11151115

11161116
doc = Document.objects.get(name=self.docname)
1117-
self.assertTrue(doc.get_state('draft-iesg')==None)
1117+
self.assertEqual(doc.get_state_slug('draft-iesg'),'idexists')
11181118

11191119
def test_confirm_submission(self):
11201120
url = urlreverse('ietf.doc.views_draft.to_iesg', kwargs=dict(name=self.docname))
@@ -1180,6 +1180,88 @@ def test_request_publication(self):
11801180

11811181
self.assertTrue("Document Action" in draft.message_set.order_by("-time")[0].subject)
11821182

1183+
class ReleaseDraftTests(TestCase):
1184+
def test_release_wg_draft(self):
1185+
chair_role = RoleFactory(group__type_id='wg',name_id='chair')
1186+
draft = WgDraftFactory(group = chair_role.group)
1187+
draft.tags.set(DocTagName.objects.filter(slug__in=('sh-f-up','w-merge')))
1188+
other_chair_role = RoleFactory(group__type_id='wg',name_id='chair')
1189+
1190+
url = urlreverse('ietf.doc.views_draft.release_draft', kwargs=dict(name=draft.name))
1191+
1192+
r = self.client.get(url)
1193+
self.assertEqual(r.status_code, 302) # redirect to login
1194+
1195+
self.client.login(username=other_chair_role.person.user.username,password=other_chair_role.person.user.username+"+password")
1196+
r = self.client.get(url)
1197+
self.assertEqual(r.status_code, 403)
1198+
1199+
self.client.logout()
1200+
self.client.login(username=chair_role.person.user.username, password=chair_role.person.user.username+"+password")
1201+
r = self.client.get(url)
1202+
self.assertEqual(r.status_code, 200)
1203+
1204+
events_before = list(draft.docevent_set.all())
1205+
empty_outbox()
1206+
r = self.client.post(url,{"comment": "Here are some comments"})
1207+
self.assertEqual(r.status_code, 302)
1208+
draft = Document.objects.get(pk=draft.pk)
1209+
self.assertEqual(draft.stream, None)
1210+
self.assertEqual(draft.group.type_id, "individ")
1211+
self.assertFalse(draft.get_state('draft-stream-ietf'))
1212+
self.assertEqual(len(outbox),3)
1213+
subjects = [msg["Subject"] for msg in outbox]
1214+
cat_subjects = "".join(subjects)
1215+
self.assertIn("Tags changed", cat_subjects)
1216+
self.assertIn("State Update", cat_subjects)
1217+
self.assertIn("Stream Change", cat_subjects)
1218+
descs = sorted([event.desc for event in set(list(draft.docevent_set.all())) - set(events_before)])
1219+
self.assertEqual("Changed stream to <b>None</b> from IETF",descs[0])
1220+
self.assertEqual("Here are some comments",descs[1])
1221+
self.assertEqual("State changed to <b>None</b> from WG Document",descs[2])
1222+
self.assertEqual("Tags Awaiting Merge with Other Document, Document Shepherd Followup cleared.",descs[3])
1223+
1224+
def test_release_rg_draft(self):
1225+
chair_role = RoleFactory(group__type_id='rg',name_id='chair')
1226+
draft = RgDraftFactory(group = chair_role.group)
1227+
url = urlreverse('ietf.doc.views_draft.release_draft', kwargs=dict(name=draft.name))
1228+
self.client.login(username=chair_role.person.user.username, password=chair_role.person.user.username+"+password")
1229+
r = self.client.post(url,{"comment": "Here are some comments"})
1230+
self.assertEqual(r.status_code, 302)
1231+
draft = Document.objects.get(pk=draft.pk)
1232+
self.assertEqual(draft.stream, None)
1233+
self.assertEqual(draft.group.type_id, "individ")
1234+
self.assertFalse(draft.get_state('draft-stream-irtf'))
1235+
1236+
def test_release_ise_draft(self):
1237+
ise = Role.objects.get(name_id='chair', group__acronym='ise')
1238+
draft = IndividualDraftFactory(stream_id='ise')
1239+
draft.set_state(State.objects.get(type_id='draft-stream-ise',slug='ise-rev'))
1240+
draft.tags.set(DocTagName.objects.filter(slug='w-dep'))
1241+
url = urlreverse('ietf.doc.views_draft.release_draft', kwargs=dict(name=draft.name))
1242+
1243+
self.client.login(username=ise.person.user.username, password=ise.person.user.username+'+password')
1244+
1245+
events_before = list(draft.docevent_set.all())
1246+
empty_outbox()
1247+
r = self.client.post(url,{"comment": "Here are some comments"})
1248+
self.assertEqual(r.status_code, 302)
1249+
draft = Document.objects.get(pk=draft.pk)
1250+
self.assertEqual(draft.stream, None)
1251+
self.assertEqual(draft.group.type_id, "individ")
1252+
self.assertFalse(draft.get_state('draft-stream-ise'))
1253+
self.assertEqual(len(outbox),3)
1254+
subjects = [msg["Subject"] for msg in outbox]
1255+
cat_subjects = "".join(subjects)
1256+
self.assertIn("Tags changed", cat_subjects)
1257+
self.assertIn("State Update", cat_subjects)
1258+
self.assertIn("Stream Change", cat_subjects)
1259+
descs = sorted([event.desc for event in set(list(draft.docevent_set.all())) - set(events_before)])
1260+
self.assertEqual("Changed stream to <b>None</b> from ISE",descs[0])
1261+
self.assertEqual("Here are some comments",descs[1])
1262+
self.assertEqual("State changed to <b>None</b> from In ISE Review",descs[2])
1263+
self.assertEqual("Tag Waiting for Dependency on Other Document cleared.",descs[3])
1264+
11831265
class AdoptDraftTests(TestCase):
11841266
def test_adopt_document(self):
11851267
RoleFactory(group__acronym='mars',group__list_email='mars-wg@ietf.org',person__user__username='marschairman',name_id='chair')

ietf/doc/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
url(r'^%(name)s/edit/shepherdwriteup/$' % settings.URL_REGEXPS, views_draft.edit_shepherd_writeup),
108108
url(r'^%(name)s/edit/requestpublication/$' % settings.URL_REGEXPS, views_draft.request_publication),
109109
url(r'^%(name)s/edit/adopt/$' % settings.URL_REGEXPS, views_draft.adopt_draft),
110+
url(r'^%(name)s/edit/release/$' % settings.URL_REGEXPS, views_draft.release_draft),
110111
url(r'^%(name)s/edit/state/(?P<state_type>draft-stream-[a-z]+)/$' % settings.URL_REGEXPS, views_draft.change_stream_state),
111112

112113
url(r'^%(name)s/edit/clearballot/(?P<ballot_type_slug>[\w-]+)/$' % settings.URL_REGEXPS, views_ballot.clear_ballot),

ietf/doc/utils.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,25 @@ def can_adopt_draft(user, doc):
120120
and (doc.group.type_id == "individ" or (doc.group in role_groups and len(role_groups)>1))
121121
and roles.exists())
122122

123+
def can_unadopt_draft(user, doc):
124+
if not user.is_authenticated:
125+
return False
126+
if has_role(user, "Secretariat"):
127+
return True
128+
if doc.stream_id == 'irtf':
129+
if has_role(user, "IRTF Chair"):
130+
return True
131+
return user.person.role_set.filter(name__in=('chair','delegate','secr'),group=doc.group).exists()
132+
elif doc.stream_id == 'ietf':
133+
return user.person.role_set.filter(name__in=('chair','delegate','secr'),group=doc.group).exists()
134+
elif doc.stream_id == 'ise':
135+
return user.person.role_set.filter(name='chair',group__acronym='ise').exists()
136+
elif doc.stream_id == 'iab':
137+
return False # Right now only the secretariat can add a document to the IAB stream, so we'll
138+
# leave it where only the secretariat can take it out.
139+
else:
140+
return False
141+
123142
def two_thirds_rule( recused=0 ):
124143
# For standards-track, need positions from 2/3 of the non-recused current IESG.
125144
active = Role.objects.filter(name="ad",group__type="area",group__state="active").count()

ietf/doc/utils_search.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ def fill_in_document_sessions(docs, doc_dict, doc_ids):
4444
def fill_in_document_table_attributes(docs, have_telechat_date=False):
4545
# fill in some attributes for the document table results to save
4646
# some hairy template code and avoid repeated SQL queries
47+
# TODO - this function evolved from something that assumed it was handling only drafts. It still has places where it assumes all docs are drafts where that is not a correct assumption
4748

4849
doc_dict = dict((d.pk, d) for d in docs)
4950
doc_ids = doc_dict.keys()

0 commit comments

Comments
 (0)