Skip to content

Commit e37303b

Browse files
committed
Merged in [15822] and [15809] from rjsparks@nostrum.com:
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: 15823 Note: SVN reference [15809] has been migrated to Git commit 6118975 Note: SVN reference [15822] has been migrated to Git commit f6886f6
2 parents 5d87de6 + f6886f6 commit e37303b

26 files changed

Lines changed: 10947 additions & 10610 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: 98 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,21 @@
44
import datetime
55
import StringIO
66
from pyquery import PyQuery
7+
from collections import Counter
78

89
from django.urls import reverse as urlreverse
910
from django.conf import settings
1011

1112
import debug # pyflakes:ignore
1213

13-
from ietf.doc.factories import IndividualDraftFactory, WgDraftFactory, DocEventFactory
14+
from ietf.doc.factories import IndividualDraftFactory, WgDraftFactory, RgDraftFactory, DocEventFactory
1415
from ietf.doc.models import ( Document, DocReminder, DocEvent,
1516
ConsensusDocEvent, LastCallDocEvent, RelatedDocument, State, TelechatDocEvent,
1617
WriteupDocEvent, DocRelationshipName)
1718
from ietf.doc.utils import get_tags_for_stream_id, create_ballot_if_not_open
1819
from ietf.name.models import StreamName, DocTagName
1920
from ietf.group.factories import GroupFactory, RoleFactory
20-
from ietf.group.models import Group
21+
from ietf.group.models import Group, Role
2122
from ietf.person.factories import PersonFactory
2223
from ietf.person.models import Person, Email
2324
from ietf.meeting.models import Meeting, MeetingTypeName
@@ -419,7 +420,7 @@ def test_start_iesg_process_on_draft(self):
419420
self.assertTrue('draft-ietf-mars-test2@' in outbox[-1]['To'])
420421

421422
# Redo, starting in publication requested to make sure WG state is also set
422-
draft.unset_state('draft-iesg')
423+
draft.set_state(State.objects.get(type_id='draft-iesg', slug='idexists'))
423424
draft.set_state(State.objects.get(type='draft-stream-ietf',slug='writeupw'))
424425
draft.stream = StreamName.objects.get(slug='ietf')
425426
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 +590,7 @@ def test_warn_expirable_drafts(self):
589590
self.assertEqual(len(list(get_soon_to_expire_drafts(14))), 0)
590591

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

@@ -616,7 +617,7 @@ def test_expire_drafts(self):
616617
self.assertEqual(len(list(get_expired_drafts())), 0)
617618

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

@@ -1110,21 +1111,21 @@ def test_cancel_submission(self):
11101111
url = urlreverse('ietf.doc.views_draft.to_iesg', kwargs=dict(name=self.docname))
11111112
self.client.login(username="marschairman", password="marschairman+password")
11121113

1113-
r = self.client.post(url, dict(cancel="1"))
1114+
r = self.client.post(url, dict(cancel="1"))
11141115
self.assertEqual(r.status_code, 302)
11151116

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

11191120
def test_confirm_submission(self):
11201121
url = urlreverse('ietf.doc.views_draft.to_iesg', kwargs=dict(name=self.docname))
11211122
self.client.login(username="marschairman", password="marschairman+password")
11221123

11231124
doc = Document.objects.get(name=self.docname)
1124-
docevent_count_pre = doc.docevent_set.count()
1125+
docevents_pre = set(doc.docevent_set.all())
11251126
mailbox_before = len(outbox)
11261127

1127-
r = self.client.post(url, dict(confirm="1"))
1128+
r = self.client.post(url, dict(confirm="1"))
11281129
self.assertEqual(r.status_code, 302)
11291130

11301131
doc = Document.objects.get(name=self.docname)
@@ -1136,7 +1137,12 @@ def test_confirm_submission(self):
11361137
# checks whether the setup document had an ad or not.
11371138
self.assertTrue(doc.ad!=None)
11381139

1139-
self.assertTrue(doc.docevent_set.count() != docevent_count_pre)
1140+
new_docevents = set(doc.docevent_set.all()) - docevents_pre
1141+
self.assertEqual(len(new_docevents),3)
1142+
new_docevent_type_count = Counter([e.type for e in new_docevents])
1143+
self.assertEqual(new_docevent_type_count['changed_state'],2)
1144+
self.assertEqual(new_docevent_type_count['started_iesg_process'],1)
1145+
11401146
self.assertEqual(len(outbox), mailbox_before + 1)
11411147
self.assertTrue("Publication has been requested" in outbox[-1]['Subject'])
11421148
self.assertTrue("aread@" in outbox[-1]['To'])
@@ -1180,6 +1186,88 @@ def test_request_publication(self):
11801186

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

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

0 commit comments

Comments
 (0)