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
This commit is contained in:
parent
f69ad28e4c
commit
6118975620
|
@ -5,6 +5,7 @@ from django.conf import settings
|
|||
import datetime, os, shutil, glob, re
|
||||
from pathlib import Path
|
||||
|
||||
from ietf.utils import log
|
||||
from ietf.utils.mail import send_mail
|
||||
from ietf.doc.models import Document, DocEvent, State, IESG_SUBSTATE_TAGS
|
||||
from ietf.person.models import Person
|
||||
|
@ -17,8 +18,11 @@ def expirable_draft(draft):
|
|||
"""Return whether draft is in an expirable state or not. This is
|
||||
the single draft version of the logic in expirable_drafts. These
|
||||
two functions need to be kept in sync."""
|
||||
if draft.type_id != 'draft':
|
||||
return False
|
||||
log.assertion('draft.get_state_slug("draft-iesg")')
|
||||
return (draft.expires and draft.get_state_slug() == "active"
|
||||
and draft.get_state_slug("draft-iesg") in (None, "watching", "dead")
|
||||
and draft.get_state_slug("draft-iesg") in ("idexists", "watching", "dead")
|
||||
and draft.get_state_slug("draft-stream-%s" % draft.stream_id) not in ("rfc-edit", "pub")
|
||||
and not draft.tags.filter(slug="rfc-rev"))
|
||||
|
||||
|
@ -29,8 +33,8 @@ def expirable_drafts():
|
|||
d = Document.objects.filter(states__type="draft", states__slug="active").exclude(expires=None)
|
||||
|
||||
nonexpirable_states = []
|
||||
# all IESG states except AD Watching and Dead block expiry
|
||||
nonexpirable_states += list(State.objects.filter(used=True, type="draft-iesg").exclude(slug__in=("watching", "dead")))
|
||||
# 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")))
|
||||
# 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")))
|
||||
|
@ -75,7 +79,8 @@ def send_expire_warning_for_draft(doc):
|
|||
(to,cc) = gather_address_lists('doc_expires_soon',doc=doc)
|
||||
|
||||
s = doc.get_state("draft-iesg")
|
||||
state = s.name if s else "I-D Exists"
|
||||
log.assertion('s')
|
||||
state = s.name if s else "I-D Exists" # TODO remove the if clause after some runtime shows no assertions
|
||||
|
||||
frm = None
|
||||
request = None
|
||||
|
@ -94,7 +99,8 @@ def send_expire_notice_for_draft(doc):
|
|||
return
|
||||
|
||||
s = doc.get_state("draft-iesg")
|
||||
state = s.name if s else "I-D Exists"
|
||||
log.assertion('s')
|
||||
state = s.name if s else "I-D Exists" # TODO remove the if clause after some rintime shows no assertions
|
||||
|
||||
request = None
|
||||
(to,cc) = gather_address_lists('doc_expired',doc=doc)
|
||||
|
|
|
@ -46,6 +46,9 @@ class BaseDocumentFactory(factory.DjangoModelFactory):
|
|||
if create and extracted:
|
||||
for (state_type_id,state_slug) in extracted:
|
||||
obj.set_state(State.objects.get(type_id=state_type_id,slug=state_slug))
|
||||
if obj.type_id == 'draft':
|
||||
if not obj.states.filter(type_id='draft-iesg').exists():
|
||||
obj.set_state(State.objects.get(type_id='draft-iesg', slug='idexists'))
|
||||
|
||||
@factory.post_generation
|
||||
def authors(obj, create, extracted, **kwargs): # pylint: disable=no-self-argument
|
||||
|
@ -90,8 +93,11 @@ class IndividualDraftFactory(BaseDocumentFactory):
|
|||
if extracted:
|
||||
for (state_type_id,state_slug) in extracted:
|
||||
obj.set_state(State.objects.get(type_id=state_type_id,slug=state_slug))
|
||||
if not obj.get_state('draft-iesg'):
|
||||
obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists'))
|
||||
else:
|
||||
obj.set_state(State.objects.get(type_id='draft',slug='active'))
|
||||
obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists'))
|
||||
|
||||
class IndividualRfcFactory(IndividualDraftFactory):
|
||||
|
||||
|
@ -120,9 +126,12 @@ class WgDraftFactory(BaseDocumentFactory):
|
|||
if extracted:
|
||||
for (state_type_id,state_slug) in extracted:
|
||||
obj.set_state(State.objects.get(type_id=state_type_id,slug=state_slug))
|
||||
if not obj.get_state('draft-iesg'):
|
||||
obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists'))
|
||||
else:
|
||||
obj.set_state(State.objects.get(type_id='draft',slug='active'))
|
||||
obj.set_state(State.objects.get(type_id='draft-stream-ietf',slug='wg-doc'))
|
||||
obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists'))
|
||||
|
||||
class WgRfcFactory(WgDraftFactory):
|
||||
|
||||
|
@ -137,8 +146,33 @@ class WgRfcFactory(WgDraftFactory):
|
|||
if extracted:
|
||||
for (state_type_id,state_slug) in extracted:
|
||||
obj.set_state(State.objects.get(type_id=state_type_id,slug=state_slug))
|
||||
if not obj.get_state('draft-iesg'):
|
||||
obj.set_state(State.objects.get(type_id='draft-iesg', slug='pub'))
|
||||
else:
|
||||
obj.set_state(State.objects.get(type_id='draft',slug='rfc'))
|
||||
obj.set_state(State.objects.get(type_id='draft-iesg', slug='pub'))
|
||||
|
||||
|
||||
class RgDraftFactory(BaseDocumentFactory):
|
||||
|
||||
type_id = 'draft'
|
||||
group = factory.SubFactory('ietf.group.factories.GroupFactory',type_id='rg')
|
||||
stream_id = 'irtf'
|
||||
|
||||
@factory.post_generation
|
||||
def states(obj, create, extracted, **kwargs):
|
||||
if not create:
|
||||
return
|
||||
if extracted:
|
||||
for (state_type_id,state_slug) in extracted:
|
||||
obj.set_state(State.objects.get(type_id=state_type_id,slug=state_slug))
|
||||
if not obj.get_state('draft-iesg'):
|
||||
obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists'))
|
||||
else:
|
||||
obj.set_state(State.objects.get(type_id='draft',slug='active'))
|
||||
obj.set_state(State.objects.get(type_id='draft-stream-irtf',slug='active'))
|
||||
obj.set_state(State.objects.get(type_id='draft-iesg',slug='idexists'))
|
||||
|
||||
|
||||
class CharterFactory(BaseDocumentFactory):
|
||||
|
||||
|
|
50
ietf/doc/migrations/0007_idexists.py
Normal file
50
ietf/doc/migrations/0007_idexists.py
Normal file
|
@ -0,0 +1,50 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.16 on 2018-11-04 10:56
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def forward(apps, schema_editor):
|
||||
State = apps.get_model('doc','State')
|
||||
Document = apps.get_model('doc','Document')
|
||||
DocHistory = apps.get_model('doc','DocHistory')
|
||||
|
||||
idexists = State.objects.create(
|
||||
type_id = 'draft-iesg',
|
||||
slug = 'idexists',
|
||||
name = 'I-D Exists',
|
||||
used = True,
|
||||
desc = 'The IESG has not started processing this draft, or has stopped processing it without publicastion.',
|
||||
order = 0,
|
||||
)
|
||||
idexists.next_states.set(State.objects.filter(type='draft-iesg',slug__in=['pub-req','watching']))
|
||||
|
||||
#for doc in tqdm(Document.objects.filter(type='draft'):
|
||||
# if not doc.states.filter(type='draft-iesg').exists():
|
||||
# doc.states.add(idexists)
|
||||
# for dh in doc.history_set.all():
|
||||
# if not dh.states.filter(type='draft-iesg').exists():
|
||||
# dh.states.add(idexists)
|
||||
|
||||
for doc in tqdm(Document.objects.filter(type_id='draft').exclude(states__type_id='draft-iesg')):
|
||||
doc.states.add(idexists)
|
||||
for history in tqdm(DocHistory.objects.filter(type_id='draft').exclude(states__type_id='draft-iesg')):
|
||||
history.states.add(idexists)
|
||||
|
||||
|
||||
def reverse(apps, schema_editor):
|
||||
State = apps.get_model('doc','State')
|
||||
State.objects.filter(slug='idexists').delete()
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('doc', '0006_ballotpositiondocevent_send_email'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(forward, reverse)
|
||||
]
|
|
@ -267,6 +267,7 @@ class DocumentInfo(models.Model):
|
|||
|
||||
def unset_state(self, state_type):
|
||||
"""Unset state of type so no state of that type is any longer set."""
|
||||
log.assertion('state_type != "draft-iesg"')
|
||||
self.states.remove(*self.states.filter(type=state_type))
|
||||
self.state_cache = None # invalidate cache
|
||||
self._cached_state_slug = {}
|
||||
|
@ -325,6 +326,7 @@ class DocumentInfo(models.Model):
|
|||
else:
|
||||
return "Replaced"
|
||||
elif state.slug == "active":
|
||||
log.assertion('iesg_state')
|
||||
if iesg_state:
|
||||
if iesg_state.slug == "dead":
|
||||
# Many drafts in the draft-iesg "Dead" state are not dead
|
||||
|
|
|
@ -42,6 +42,7 @@ from django.utils.safestring import mark_safe
|
|||
from ietf.ietfauth.utils import user_is_person, has_role
|
||||
from ietf.doc.models import BallotPositionDocEvent, IESG_BALLOT_ACTIVE_STATES
|
||||
from ietf.name.models import BallotPositionName
|
||||
from ietf.utils import log
|
||||
|
||||
|
||||
register = template.Library()
|
||||
|
@ -157,10 +158,11 @@ def state_age_colored(doc):
|
|||
# Don't show anything for expired/withdrawn/replaced drafts
|
||||
return ""
|
||||
iesg_state = doc.get_state_slug('draft-iesg')
|
||||
log.assertion('iesg_state')
|
||||
if not iesg_state:
|
||||
return ""
|
||||
|
||||
if iesg_state in ["dead", "watching", "pub"]:
|
||||
if iesg_state in ["dead", "watching", "pub", "idexists"]:
|
||||
return ""
|
||||
try:
|
||||
state_date = doc.docevent_set.filter(
|
||||
|
|
|
@ -10,14 +10,14 @@ from django.conf import settings
|
|||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
from ietf.doc.factories import IndividualDraftFactory, WgDraftFactory, DocEventFactory
|
||||
from ietf.doc.factories import IndividualDraftFactory, WgDraftFactory, RgDraftFactory, DocEventFactory
|
||||
from ietf.doc.models import ( Document, DocReminder, DocEvent,
|
||||
ConsensusDocEvent, LastCallDocEvent, RelatedDocument, State, TelechatDocEvent,
|
||||
WriteupDocEvent, DocRelationshipName)
|
||||
from ietf.doc.utils import get_tags_for_stream_id, create_ballot_if_not_open
|
||||
from ietf.name.models import StreamName, DocTagName
|
||||
from ietf.group.factories import GroupFactory, RoleFactory
|
||||
from ietf.group.models import Group
|
||||
from ietf.group.models import Group, Role
|
||||
from ietf.person.factories import PersonFactory
|
||||
from ietf.person.models import Person, Email
|
||||
from ietf.meeting.models import Meeting, MeetingTypeName
|
||||
|
@ -419,7 +419,7 @@ class EditInfoTests(TestCase):
|
|||
self.assertTrue('draft-ietf-mars-test2@' in outbox[-1]['To'])
|
||||
|
||||
# Redo, starting in publication requested to make sure WG state is also set
|
||||
draft.unset_state('draft-iesg')
|
||||
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.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 @@ class ExpireIDsTests(TestCase):
|
|||
self.assertEqual(len(list(get_soon_to_expire_drafts(14))), 0)
|
||||
|
||||
# hack into expirable state
|
||||
draft.unset_state("draft-iesg")
|
||||
draft.set_state(State.objects.get(type_id='draft-iesg',slug='idexists'))
|
||||
draft.expires = datetime.datetime.now() + datetime.timedelta(days=10)
|
||||
draft.save_with_history([DocEvent.objects.create(doc=draft, rev=draft.rev, type="changed_document", by=Person.objects.get(user__username="secretary"), desc="Test")])
|
||||
|
||||
|
@ -616,7 +616,7 @@ class ExpireIDsTests(TestCase):
|
|||
self.assertEqual(len(list(get_expired_drafts())), 0)
|
||||
|
||||
# hack into expirable state
|
||||
draft.unset_state("draft-iesg")
|
||||
draft.set_state(State.objects.get(type_id='draft-iesg',slug='idexists'))
|
||||
draft.expires = datetime.datetime.now()
|
||||
draft.save_with_history([DocEvent.objects.create(doc=draft, rev=draft.rev, type="changed_document", by=Person.objects.get(user__username="secretary"), desc="Test")])
|
||||
|
||||
|
@ -1114,7 +1114,7 @@ class SubmitToIesgTests(TestCase):
|
|||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
doc = Document.objects.get(name=self.docname)
|
||||
self.assertTrue(doc.get_state('draft-iesg')==None)
|
||||
self.assertEqual(doc.get_state_slug('draft-iesg'),'idexists')
|
||||
|
||||
def test_confirm_submission(self):
|
||||
url = urlreverse('ietf.doc.views_draft.to_iesg', kwargs=dict(name=self.docname))
|
||||
|
@ -1180,6 +1180,88 @@ class RequestPublicationTests(TestCase):
|
|||
|
||||
self.assertTrue("Document Action" in draft.message_set.order_by("-time")[0].subject)
|
||||
|
||||
class ReleaseDraftTests(TestCase):
|
||||
def test_release_wg_draft(self):
|
||||
chair_role = RoleFactory(group__type_id='wg',name_id='chair')
|
||||
draft = WgDraftFactory(group = chair_role.group)
|
||||
draft.tags.set(DocTagName.objects.filter(slug__in=('sh-f-up','w-merge')))
|
||||
other_chair_role = RoleFactory(group__type_id='wg',name_id='chair')
|
||||
|
||||
url = urlreverse('ietf.doc.views_draft.release_draft', kwargs=dict(name=draft.name))
|
||||
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 302) # redirect to login
|
||||
|
||||
self.client.login(username=other_chair_role.person.user.username,password=other_chair_role.person.user.username+"+password")
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 403)
|
||||
|
||||
self.client.logout()
|
||||
self.client.login(username=chair_role.person.user.username, password=chair_role.person.user.username+"+password")
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
events_before = list(draft.docevent_set.all())
|
||||
empty_outbox()
|
||||
r = self.client.post(url,{"comment": "Here are some comments"})
|
||||
self.assertEqual(r.status_code, 302)
|
||||
draft = Document.objects.get(pk=draft.pk)
|
||||
self.assertEqual(draft.stream, None)
|
||||
self.assertEqual(draft.group.type_id, "individ")
|
||||
self.assertFalse(draft.get_state('draft-stream-ietf'))
|
||||
self.assertEqual(len(outbox),3)
|
||||
subjects = [msg["Subject"] for msg in outbox]
|
||||
cat_subjects = "".join(subjects)
|
||||
self.assertIn("Tags changed", cat_subjects)
|
||||
self.assertIn("State Update", cat_subjects)
|
||||
self.assertIn("Stream Change", cat_subjects)
|
||||
descs = sorted([event.desc for event in set(list(draft.docevent_set.all())) - set(events_before)])
|
||||
self.assertEqual("Changed stream to <b>None</b> from IETF",descs[0])
|
||||
self.assertEqual("Here are some comments",descs[1])
|
||||
self.assertEqual("State changed to <b>None</b> from WG Document",descs[2])
|
||||
self.assertEqual("Tags Awaiting Merge with Other Document, Document Shepherd Followup cleared.",descs[3])
|
||||
|
||||
def test_release_rg_draft(self):
|
||||
chair_role = RoleFactory(group__type_id='rg',name_id='chair')
|
||||
draft = RgDraftFactory(group = chair_role.group)
|
||||
url = urlreverse('ietf.doc.views_draft.release_draft', kwargs=dict(name=draft.name))
|
||||
self.client.login(username=chair_role.person.user.username, password=chair_role.person.user.username+"+password")
|
||||
r = self.client.post(url,{"comment": "Here are some comments"})
|
||||
self.assertEqual(r.status_code, 302)
|
||||
draft = Document.objects.get(pk=draft.pk)
|
||||
self.assertEqual(draft.stream, None)
|
||||
self.assertEqual(draft.group.type_id, "individ")
|
||||
self.assertFalse(draft.get_state('draft-stream-irtf'))
|
||||
|
||||
def test_release_ise_draft(self):
|
||||
ise = Role.objects.get(name_id='chair', group__acronym='ise')
|
||||
draft = IndividualDraftFactory(stream_id='ise')
|
||||
draft.set_state(State.objects.get(type_id='draft-stream-ise',slug='ise-rev'))
|
||||
draft.tags.set(DocTagName.objects.filter(slug='w-dep'))
|
||||
url = urlreverse('ietf.doc.views_draft.release_draft', kwargs=dict(name=draft.name))
|
||||
|
||||
self.client.login(username=ise.person.user.username, password=ise.person.user.username+'+password')
|
||||
|
||||
events_before = list(draft.docevent_set.all())
|
||||
empty_outbox()
|
||||
r = self.client.post(url,{"comment": "Here are some comments"})
|
||||
self.assertEqual(r.status_code, 302)
|
||||
draft = Document.objects.get(pk=draft.pk)
|
||||
self.assertEqual(draft.stream, None)
|
||||
self.assertEqual(draft.group.type_id, "individ")
|
||||
self.assertFalse(draft.get_state('draft-stream-ise'))
|
||||
self.assertEqual(len(outbox),3)
|
||||
subjects = [msg["Subject"] for msg in outbox]
|
||||
cat_subjects = "".join(subjects)
|
||||
self.assertIn("Tags changed", cat_subjects)
|
||||
self.assertIn("State Update", cat_subjects)
|
||||
self.assertIn("Stream Change", cat_subjects)
|
||||
descs = sorted([event.desc for event in set(list(draft.docevent_set.all())) - set(events_before)])
|
||||
self.assertEqual("Changed stream to <b>None</b> from ISE",descs[0])
|
||||
self.assertEqual("Here are some comments",descs[1])
|
||||
self.assertEqual("State changed to <b>None</b> from In ISE Review",descs[2])
|
||||
self.assertEqual("Tag Waiting for Dependency on Other Document cleared.",descs[3])
|
||||
|
||||
class AdoptDraftTests(TestCase):
|
||||
def test_adopt_document(self):
|
||||
RoleFactory(group__acronym='mars',group__list_email='mars-wg@ietf.org',person__user__username='marschairman',name_id='chair')
|
||||
|
|
|
@ -107,6 +107,7 @@ urlpatterns = [
|
|||
url(r'^%(name)s/edit/shepherdwriteup/$' % settings.URL_REGEXPS, views_draft.edit_shepherd_writeup),
|
||||
url(r'^%(name)s/edit/requestpublication/$' % settings.URL_REGEXPS, views_draft.request_publication),
|
||||
url(r'^%(name)s/edit/adopt/$' % settings.URL_REGEXPS, views_draft.adopt_draft),
|
||||
url(r'^%(name)s/edit/release/$' % settings.URL_REGEXPS, views_draft.release_draft),
|
||||
url(r'^%(name)s/edit/state/(?P<state_type>draft-stream-[a-z]+)/$' % settings.URL_REGEXPS, views_draft.change_stream_state),
|
||||
|
||||
url(r'^%(name)s/edit/clearballot/(?P<ballot_type_slug>[\w-]+)/$' % settings.URL_REGEXPS, views_ballot.clear_ballot),
|
||||
|
|
|
@ -120,6 +120,25 @@ def can_adopt_draft(user, doc):
|
|||
and (doc.group.type_id == "individ" or (doc.group in role_groups and len(role_groups)>1))
|
||||
and roles.exists())
|
||||
|
||||
def can_unadopt_draft(user, doc):
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
if has_role(user, "Secretariat"):
|
||||
return True
|
||||
if doc.stream_id == 'irtf':
|
||||
if has_role(user, "IRTF Chair"):
|
||||
return True
|
||||
return user.person.role_set.filter(name__in=('chair','delegate','secr'),group=doc.group).exists()
|
||||
elif doc.stream_id == 'ietf':
|
||||
return user.person.role_set.filter(name__in=('chair','delegate','secr'),group=doc.group).exists()
|
||||
elif doc.stream_id == 'ise':
|
||||
return user.person.role_set.filter(name='chair',group__acronym='ise').exists()
|
||||
elif doc.stream_id == 'iab':
|
||||
return False # Right now only the secretariat can add a document to the IAB stream, so we'll
|
||||
# leave it where only the secretariat can take it out.
|
||||
else:
|
||||
return False
|
||||
|
||||
def two_thirds_rule( recused=0 ):
|
||||
# For standards-track, need positions from 2/3 of the non-recused current IESG.
|
||||
active = Role.objects.filter(name="ad",group__type="area",group__state="active").count()
|
||||
|
|
|
@ -44,6 +44,7 @@ def fill_in_document_sessions(docs, doc_dict, doc_ids):
|
|||
def fill_in_document_table_attributes(docs, have_telechat_date=False):
|
||||
# fill in some attributes for the document table results to save
|
||||
# some hairy template code and avoid repeated SQL queries
|
||||
# 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
|
||||
|
||||
doc_dict = dict((d.pk, d) for d in docs)
|
||||
doc_ids = doc_dict.keys()
|
||||
|
|
|
@ -47,7 +47,7 @@ from ietf.doc.models import ( Document, DocAlias, DocHistory, DocEvent, BallotDo
|
|||
ConsensusDocEvent, NewRevisionDocEvent, TelechatDocEvent, WriteupDocEvent,
|
||||
IESG_BALLOT_ACTIVE_STATES, STATUSCHANGE_RELATIONS )
|
||||
from ietf.doc.utils import ( add_links_in_new_revision_events, augment_events_with_revision,
|
||||
can_adopt_draft, get_chartering_type, get_tags_for_stream_id,
|
||||
can_adopt_draft, can_unadopt_draft, get_chartering_type, get_tags_for_stream_id,
|
||||
needed_ballot_positions, nice_consensus, prettify_std_name, update_telechat, has_same_ballot,
|
||||
get_initial_notify, make_notify_changed_event, make_rev_history, default_consensus,
|
||||
add_events_message_info, get_unicode_document_content, build_doc_meta_block)
|
||||
|
@ -66,7 +66,7 @@ from ietf.meeting.utils import group_sessions, get_upcoming_manageable_sessions,
|
|||
from ietf.review.models import ReviewRequest
|
||||
from ietf.review.utils import can_request_review_of_doc, review_requests_to_list_for_docs
|
||||
from ietf.review.utils import no_review_from_teams_on_doc
|
||||
from ietf.utils import markup_txt
|
||||
from ietf.utils import markup_txt, log
|
||||
from ietf.utils.text import maybe_split
|
||||
|
||||
|
||||
|
@ -329,6 +329,15 @@ def document_main(request, name, rev=None):
|
|||
button_text = "Manage Document Adoption in Group"
|
||||
actions.append((button_text, urlreverse('ietf.doc.views_draft.adopt_draft', kwargs=dict(name=doc.name))))
|
||||
|
||||
if can_unadopt_draft(request.user, doc) and not doc.get_state_slug() in ["rfc"] and not snapshot:
|
||||
if doc.get_state_slug('draft-iesg') == 'idexists':
|
||||
if doc.group and doc.group.acronym != 'none':
|
||||
actions.append(("Release document from group", urlreverse('ietf.doc.views_draft.release_draft', kwargs=dict(name=doc.name))))
|
||||
elif doc.stream_id == 'ise':
|
||||
actions.append(("Release document from stream", urlreverse('ietf.doc.views_draft.release_draft', kwargs=dict(name=doc.name))))
|
||||
else:
|
||||
pass
|
||||
|
||||
if doc.get_state_slug() == "expired" and not resurrected_by and can_edit and not snapshot:
|
||||
actions.append(("Request Resurrect", urlreverse('ietf.doc.views_draft.request_resurrect', kwargs=dict(name=doc.name))))
|
||||
|
||||
|
@ -348,14 +357,15 @@ def document_main(request, name, rev=None):
|
|||
label = "Request Publication"
|
||||
if not doc.intended_std_level:
|
||||
label += " (note that intended status is not set)"
|
||||
if iesg_state and iesg_state.slug != 'dead':
|
||||
if iesg_state and iesg_state.slug not in ('idexists','dead'):
|
||||
label += " (Warning: the IESG state indicates ongoing IESG processing)"
|
||||
actions.append((label, urlreverse('ietf.doc.views_draft.request_publication', kwargs=dict(name=doc.name))))
|
||||
|
||||
if doc.get_state_slug() not in ["rfc", "expired"] and doc.stream_id in ("ietf",) and not snapshot:
|
||||
if not iesg_state and can_edit:
|
||||
log.assertion('iesg_state')
|
||||
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 or is_shepherd) and (not iesg_state or iesg_state.slug == 'watching'):
|
||||
elif (can_edit_stream_info or is_shepherd) and (iesg_state.slug in ('idexists','watching')):
|
||||
actions.append(("Submit to IESG for Publication", urlreverse('ietf.doc.views_draft.to_iesg', kwargs=dict(name=doc.name))))
|
||||
|
||||
augment_docs_with_tracking_info([doc], request.user)
|
||||
|
|
|
@ -18,13 +18,14 @@ from django.template.defaultfilters import pluralize
|
|||
import debug # pyflakes:ignore
|
||||
|
||||
from ietf.doc.models import ( Document, DocAlias, RelatedDocument, State,
|
||||
StateType, DocEvent, ConsensusDocEvent, TelechatDocEvent, WriteupDocEvent, IESG_SUBSTATE_TAGS)
|
||||
StateType, DocEvent, ConsensusDocEvent, TelechatDocEvent, WriteupDocEvent, StateDocEvent,
|
||||
IESG_SUBSTATE_TAGS)
|
||||
from ietf.doc.mails import ( email_pulled_from_rfc_queue, email_resurrect_requested,
|
||||
email_resurrection_completed, email_state_changed, email_stream_changed,
|
||||
email_stream_state_changed, email_stream_tags_changed, extra_automation_headers,
|
||||
generate_publication_request, email_adopted, email_intended_status_changed,
|
||||
email_iesg_processing_document, email_ad_approved_doc )
|
||||
from ietf.doc.utils import ( add_state_change_event, can_adopt_draft,
|
||||
from ietf.doc.utils import ( add_state_change_event, can_adopt_draft, can_unadopt_draft,
|
||||
get_tags_for_stream_id, nice_consensus,
|
||||
update_reminder, update_telechat, make_notify_changed_event, get_initial_notify,
|
||||
set_replaces_for_document, default_consensus, tags_suffix, )
|
||||
|
@ -62,6 +63,10 @@ class ChangeStateForm(forms.Form):
|
|||
|
||||
if state == prev and tag == prev_tag:
|
||||
self._errors['comment'] = ErrorList([u'State not changed. Comments entered will be lost with no state change. Please go back and use the Add Comment feature on the history tab to add comments without changing state.'])
|
||||
|
||||
if state != '(None)' and state.slug == 'idexists' and tag:
|
||||
self._errors['substate'] = ErrorList([u'Clear substate before setting the document to the idexists state.'])
|
||||
|
||||
return retclean
|
||||
|
||||
@role_required('Area Director','Secretariat')
|
||||
|
@ -141,6 +146,10 @@ def change_state(request, name):
|
|||
return render(request, 'doc/draft/last_call_requested.html',
|
||||
dict(doc=doc,
|
||||
url=doc.get_absolute_url()))
|
||||
|
||||
if new_state.slug == "idexists" and doc.stream:
|
||||
msg = "Note that this document is still in the %s stream. Please ensure the stream state settings make sense, or consider removing the document from the stream." % doc.stream.name
|
||||
messages.info(request, msg)
|
||||
|
||||
return HttpResponseRedirect(doc.get_absolute_url())
|
||||
|
||||
|
@ -612,7 +621,7 @@ def edit_info(request, name):
|
|||
raise Http404
|
||||
|
||||
new_document = False
|
||||
if not doc.get_state("draft-iesg"): # FIXME: should probably receive "new document" as argument to view instead of this
|
||||
if doc.get_state_slug("draft-iesg") == "idexists": # FIXME: should probably receive "new document" as argument to view instead of this
|
||||
new_document = True
|
||||
doc.notify = get_initial_notify(doc)
|
||||
|
||||
|
@ -1413,6 +1422,79 @@ def adopt_draft(request, name):
|
|||
'form': form,
|
||||
})
|
||||
|
||||
class ReleaseDraftForm(forms.Form):
|
||||
comment = forms.CharField(widget=forms.Textarea, required=False, label="Comment", help_text="Optional comment explaining the reasons for releasing the document." )
|
||||
|
||||
@login_required
|
||||
def release_draft(request, name):
|
||||
doc = get_object_or_404(Document, type="draft", name=name)
|
||||
|
||||
if doc.get_state_slug('draft-iesg') != 'idexists':
|
||||
raise Http404
|
||||
|
||||
if not can_unadopt_draft(request.user, doc):
|
||||
return HttpResponseForbidden("You don't have permission to access this page")
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ReleaseDraftForm(request.POST)
|
||||
if form.is_valid():
|
||||
comment = form.cleaned_data["comment"]
|
||||
by = request.user.person
|
||||
events = []
|
||||
|
||||
if doc.stream.slug == 'ise' or doc.group.type_id != 'individ':
|
||||
existing_tags = set(doc.tags.all())
|
||||
if existing_tags:
|
||||
doc.tags.clear()
|
||||
e = DocEvent(type="changed_document", doc=doc, rev=doc.rev, by=by)
|
||||
l = []
|
||||
l.append(u"Tag%s %s cleared." % (pluralize(existing_tags), ", ".join(t.name for t in existing_tags)))
|
||||
e.desc = " ".join(l)
|
||||
e.save()
|
||||
events.append(e)
|
||||
email_stream_tags_changed(request, doc, set(), existing_tags, by, comment)
|
||||
|
||||
prev_state = doc.get_state("draft-stream-%s" % doc.stream_id)
|
||||
if prev_state:
|
||||
doc.unset_state("draft-stream-%s" % doc.stream_id)
|
||||
e = StateDocEvent(doc=doc, rev=doc.rev, by=by)
|
||||
e.type = "changed_state"
|
||||
e.state_type = (prev_state).type
|
||||
e.state = None
|
||||
e.desc = "State changed to <b>None</b> from %s" % prev_state.name
|
||||
e.save()
|
||||
events.append(e)
|
||||
email_state_changed(request,doc,e.desc)
|
||||
|
||||
if doc.stream.slug != 'ise':
|
||||
old_group = doc.group
|
||||
doc.group = Group.objects.get(acronym='none')
|
||||
e = DocEvent(type="changed_document", doc=doc, rev=doc.rev, by=by)
|
||||
e.desc = "Document removed from group %s." % old_group.acronym.upper()
|
||||
|
||||
if doc.stream:
|
||||
e = DocEvent(type="changed_stream", doc=doc, rev=doc.rev, by=by)
|
||||
e.desc = u"Changed stream to <b>None</b> from %s" % doc.stream.name
|
||||
e.save()
|
||||
events.append(e)
|
||||
old_stream = doc.stream
|
||||
doc.stream = None
|
||||
email_stream_changed(request, doc, old_stream, None)
|
||||
|
||||
if comment:
|
||||
e = DocEvent(type="added_comment", doc=doc, rev=doc.rev, by=by)
|
||||
e.desc = comment
|
||||
e.save()
|
||||
events.append(e)
|
||||
|
||||
doc.save_with_history(events)
|
||||
return HttpResponseRedirect(doc.get_absolute_url())
|
||||
else:
|
||||
form = ReleaseDraftForm()
|
||||
|
||||
return render(request, 'doc/draft/release_draft.html', {'doc':doc, 'form':form })
|
||||
|
||||
|
||||
class ChangeStreamStateForm(forms.Form):
|
||||
new_state = forms.ModelChoiceField(queryset=State.objects.filter(used=True), label='State' )
|
||||
weeks = forms.IntegerField(label='Expected weeks in state',required=False)
|
||||
|
|
|
@ -31,14 +31,6 @@ def state_help(request, type):
|
|||
tags = []
|
||||
|
||||
if state_type.slug == "draft-iesg":
|
||||
# legacy hack
|
||||
states = list(states)
|
||||
fake_state = dict(name="I-D Exists",
|
||||
desc="Initial (default) state for all internet drafts. Such documents are not being tracked by the IESG as no request has been made of the IESG to do anything with the document.",
|
||||
next_states=dict(all=State.objects.filter(type="draft-iesg", slug__in=("watching", "pub-req")))
|
||||
)
|
||||
states.insert(0, fake_state)
|
||||
|
||||
tags = DocTagName.objects.filter(slug__in=IESG_SUBSTATE_TAGS)
|
||||
elif state_type.slug.startswith("draft-stream-"):
|
||||
possible = get_tags_for_stream_id(state_type.slug.replace("draft-stream-", ""))
|
||||
|
|
|
@ -426,7 +426,7 @@ def drafts_in_last_call(request):
|
|||
})
|
||||
|
||||
def drafts_in_iesg_process(request):
|
||||
states = State.objects.filter(type="draft-iesg").exclude(slug__in=('pub', 'dead', 'watching', 'rfcqueue'))
|
||||
states = State.objects.filter(type="draft-iesg").exclude(slug__in=('idexists', 'pub', 'dead', 'watching', 'rfcqueue'))
|
||||
title = "Documents in IESG process"
|
||||
|
||||
grouped_docs = []
|
||||
|
|
|
@ -6,6 +6,7 @@ from django.db.models import Q
|
|||
from django.template.loader import render_to_string
|
||||
|
||||
from ietf.doc.models import RelatedDocument
|
||||
from ietf.utils import log
|
||||
|
||||
|
||||
class Edge(object):
|
||||
|
@ -54,9 +55,10 @@ def get_node_styles(node, group):
|
|||
|
||||
styles['style'] = 'filled'
|
||||
|
||||
log.assertion('node.get_state("draft_iesg")')
|
||||
if node.get_state('draft').slug == 'rfc':
|
||||
styles['shape'] = 'box'
|
||||
elif node.get_state('draft-iesg') and not node.get_state('draft-iesg').slug in ['watching', 'dead']:
|
||||
elif not node.get_state('draft-iesg').slug in ['idexists', 'watching', 'dead']:
|
||||
styles['shape'] = 'parallelogram'
|
||||
elif node.get_state('draft').slug == 'expired':
|
||||
styles['shape'] = 'house'
|
||||
|
|
|
@ -16,6 +16,7 @@ from ietf.doc.models import LastCallDocEvent, NewRevisionDocEvent
|
|||
from ietf.doc.models import IESG_SUBSTATE_TAGS
|
||||
from ietf.group.models import Group
|
||||
from ietf.person.models import Person, Email
|
||||
from ietf.utils import log
|
||||
|
||||
def all_id_txt():
|
||||
# this returns a lot of data so try to be efficient
|
||||
|
@ -44,7 +45,7 @@ def all_id_txt():
|
|||
res.append(f1 + "\t" + f2 + "\t" + f3 + "\t" + f4)
|
||||
|
||||
|
||||
inactive_states = ["pub", "watching", "dead"]
|
||||
inactive_states = ["idexists", "pub", "watching", "dead"]
|
||||
|
||||
excludes = list(State.objects.filter(type="draft", slug__in=["rfc","repl"]))
|
||||
includes = list(State.objects.filter(type="draft-iesg").exclude(slug__in=inactive_states))
|
||||
|
@ -149,6 +150,7 @@ def all_id2_txt():
|
|||
# 3
|
||||
if state == "active":
|
||||
s = "I-D Exists"
|
||||
log.assertion('iesg_state')
|
||||
if iesg_state:
|
||||
s = iesg_state.name
|
||||
tags = d.tags.filter(slug__in=IESG_SUBSTATE_TAGS).values_list("name", flat=True)
|
||||
|
|
|
@ -39,7 +39,7 @@ class IndexTests(TestCase):
|
|||
self.assertTrue(draft.get_state("draft-iesg").name in txt)
|
||||
|
||||
# not active in IESG process
|
||||
draft.unset_state("draft-iesg")
|
||||
draft.set_state(State.objects.get(type_id="draft-iesg", slug="idexists"))
|
||||
|
||||
txt = all_id_txt()
|
||||
self.assertTrue(draft.name + "-" + draft.rev in txt)
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -35,6 +35,7 @@ from ietf.nomcom.utils import (get_nomcom_by_year, store_nomcom_private_key,
|
|||
HOME_TEMPLATE, NOMINEE_ACCEPT_REMINDER_TEMPLATE,NOMINEE_QUESTIONNAIRE_REMINDER_TEMPLATE)
|
||||
from ietf.ietfauth.utils import role_required
|
||||
from ietf.person.models import Person
|
||||
from ietf.utils import log
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
|
@ -1238,10 +1239,7 @@ def eligible(request, year):
|
|||
# the date of the announcement of the Call for Volunteers, instead
|
||||
date = datetime.date.today()
|
||||
previous_five = Meeting.objects.filter(type='ietf',date__lte=date).order_by('-date')[:5]
|
||||
if not len(previous_five) == 5:
|
||||
debug.show('year')
|
||||
debug.show('date')
|
||||
debug.show('previous_five')
|
||||
log.assertion("len(previous_five) == 5")
|
||||
attendees = {}
|
||||
potentials = set()
|
||||
for m in previous_five:
|
||||
|
|
|
@ -114,7 +114,7 @@ class AuthorForm(forms.Form):
|
|||
class EditModelForm(forms.ModelForm):
|
||||
#expiration_date = forms.DateField(required=False)
|
||||
state = forms.ModelChoiceField(queryset=State.objects.filter(type='draft'),empty_label=None)
|
||||
iesg_state = forms.ModelChoiceField(queryset=State.objects.filter(type='draft-iesg'),required=False)
|
||||
iesg_state = forms.ModelChoiceField(queryset=State.objects.filter(type='draft-iesg'),empty_label=None)
|
||||
group = GroupModelChoiceField(required=True)
|
||||
review_by_rfc_editor = forms.BooleanField(required=False)
|
||||
shepherd = SearchableEmailField(required=False, only_users=True)
|
||||
|
@ -132,8 +132,7 @@ class EditModelForm(forms.ModelForm):
|
|||
self.fields['rev'].widget.attrs['size'] = 2
|
||||
self.fields['abstract'].widget.attrs['cols'] = 72
|
||||
self.initial['state'] = self.instance.get_state().pk
|
||||
if self.instance.get_state('draft-iesg'):
|
||||
self.initial['iesg_state'] = self.instance.get_state('draft-iesg').pk
|
||||
self.initial['iesg_state'] = self.instance.get_state('draft-iesg').pk
|
||||
|
||||
# setup special fields
|
||||
if self.instance:
|
||||
|
@ -150,10 +149,7 @@ class EditModelForm(forms.ModelForm):
|
|||
|
||||
# note we're not sending notices here, is this desired
|
||||
if 'iesg_state' in self.changed_data:
|
||||
if iesg_state == None:
|
||||
m.unset_state('draft-iesg')
|
||||
else:
|
||||
m.set_state(iesg_state)
|
||||
m.set_state(iesg_state)
|
||||
|
||||
if 'review_by_rfc_editor' in self.changed_data:
|
||||
if self.cleaned_data.get('review_by_rfc_editor',''):
|
||||
|
|
|
@ -69,8 +69,7 @@ class SecrDraftsTestCase(TestCase):
|
|||
self.client.login(username="secretary", password="secretary+password")
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.post(url,{'title':draft.title,'name':draft.name,'rev':draft.rev,'state':4,'group':draft.group.pk})
|
||||
#debug.show('response')
|
||||
response = self.client.post(url,{'title':draft.title,'name':draft.name,'rev':draft.rev,'state':4,'group':draft.group.pk,'iesg_state':draft.get_state('draft-iesg').pk})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
draft = Document.objects.get(pk=draft.pk)
|
||||
self.assertEqual(draft.get_state().slug,'repl')
|
||||
|
|
|
@ -331,6 +331,9 @@ def post_submission(request, submission, approvedDesc):
|
|||
desc = settings.SUBMIT_YANG_CATALOG_MODULE_DESC.format(module=module)
|
||||
draft.documenturl_set.create(url=url, tag_id='yang-module-metadata', desc=desc)
|
||||
|
||||
if not draft.get_state('draft-iesg'):
|
||||
draft.states.add(State.objects.get(type_id='draft-iesg', slug='idexists'))
|
||||
|
||||
# save history now that we're done with changes to the draft itself
|
||||
draft.save_with_history(events)
|
||||
|
||||
|
|
|
@ -444,10 +444,12 @@ def update_docs_from_rfc_index(data, skip_older_than_date=None):
|
|||
|
||||
for t in ("draft-iesg", "draft-stream-iab", "draft-stream-irtf", "draft-stream-ise"):
|
||||
slug = doc.get_state_slug(t)
|
||||
if slug and slug != "pub":
|
||||
if slug and slug not in ("pub", "idexists"):
|
||||
new_state = State.objects.select_related("type").get(used=True, type=t, slug="pub")
|
||||
doc.set_state(new_state)
|
||||
changes.append("changed %s to %s" % (new_state.type.label, new_state))
|
||||
if t == 'draft-iesg' and not slug:
|
||||
doc.set_state(State.objects.get(type_id='draft-iesg', slug='idexists'))
|
||||
|
||||
def parse_relation_list(l):
|
||||
res = []
|
||||
|
|
|
@ -404,6 +404,7 @@ class RFCSyncTests(TestCase):
|
|||
self.assertEqual(len(changed), 1)
|
||||
self.assertEqual(len(warnings), 0)
|
||||
|
||||
draft = Document.objects.get(pk=draft.pk)
|
||||
self.assertEqual(draft.get_state_slug("draft-rfceditor"), "edit")
|
||||
self.assertEqual(draft.get_state_slug("draft-iesg"), "rfcqueue")
|
||||
self.assertEqual(set(draft.tags.all()), set(DocTagName.objects.filter(slug__in=("iana", "ref"))))
|
||||
|
|
|
@ -418,7 +418,7 @@
|
|||
<th>IESG</th>
|
||||
<th><a href="{% url "ietf.help.views.state" doc=doc.type.slug type="iesg" %}">IESG state</a></th>
|
||||
<td class="edit">
|
||||
{% if iesg_state and can_edit %}
|
||||
{% if iesg_state.slug != 'idexists' and can_edit %}
|
||||
<a class="btn btn-default btn-xs" href="{% url 'ietf.doc.views_draft.change_state' name=doc.name %}">Edit</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
@ -481,7 +481,7 @@
|
|||
</td>
|
||||
</tr>
|
||||
|
||||
{% if iesg_state %}
|
||||
{% if iesg_state.slug != 'idexists' %}
|
||||
{% if doc.note or can_edit %}
|
||||
<tr>
|
||||
<th></th>
|
||||
|
@ -583,7 +583,7 @@
|
|||
<a class="btn btn-default btn-xs track-untrack-doc {% if doc.tracked_in_personal_community_list %}hide{% endif %}" href="{% url "ietf.community.views.track_document" username=user.username name=doc.name %}" title="Add to your personal ID list"><span class="fa fa-bookmark-o"></span> Track</a>
|
||||
{% endif %}
|
||||
|
||||
{% if can_edit and iesg_state %}
|
||||
{% if can_edit and iesg_state.slug != 'idexists' %}
|
||||
<a class="btn btn-default btn-xs" href="{% url 'ietf.doc.views_ballot.lastcalltext' name=doc.name %}">Last call text</a>
|
||||
<a class="btn btn-default btn-xs" href="{% url 'ietf.doc.views_ballot.ballot_writeupnotes' name=doc.name %}">Ballot text</a>
|
||||
<a class="btn btn-default btn-xs" href="{% url 'ietf.doc.views_ballot.ballot_approvaltext' name=doc.name %}">Announcement text</a>
|
||||
|
|
29
ietf/templates/doc/draft/release_draft.html
Normal file
29
ietf/templates/doc/draft/release_draft.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
{% extends "base.html" %}
|
||||
{% load origin %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block morecss %}
|
||||
.center-button-text { text-align: center;}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class='col-md-12 center-button-text button btn-warning'>Warning</div>
|
||||
<div class='col-md-12'>
|
||||
{% if doc.stream.slug == 'ise' %}
|
||||
This action will unset all Independent stream state and remove the document from the Independent stream.
|
||||
{% else %}
|
||||
This action will unset all {{doc.group.acronym|upper}} group state, and remove the document from the {{doc.stream.name}} stream.<br>
|
||||
This is appropriate, for example, if the group decided not to adopt a document after considering it.<br>
|
||||
It may also be appropriate if the group is abandoning the document.
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class='col-md-12 center-button-text button btn-warning'>Warning</div>
|
||||
<div>
|
||||
<form method='POST'>
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
<button type="submit" class="btn btn-primary">Proceed</button>
|
||||
<a class="btn btn-default pull-right" href="{% url "ietf.doc.views_doc.document_main" name=doc.name %}">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
Loading…
Reference in a new issue