From a741642717fb4c5fb5abfa7bd040bf06346dea4a Mon Sep 17 00:00:00 2001 From: Robert Sparks Date: Sat, 1 Jun 2013 21:01:45 +0000 Subject: [PATCH] Added editing last call messages, requesting, issuing and tracking IETF LCs to status-change documents Added a Cancel button to the form that allows editing the relations for status-change documents Added instructions to the agenda section 3.3 This adds states to status-change- documents and has a migration that must be applied. This fixes bug #1039 - Legacy-Id: 5770 --- .../migrations/0010_more_statchg_states.py | 477 ++++++++++++++++++ ietf/doc/tests_status_change.py | 59 ++- ietf/doc/urls_status_change.py | 1 + ietf/doc/views_status_change.py | 102 +++- ietf/idrfc/views_ballot.py | 63 ++- ietf/idrfc/views_search.py | 9 + ietf/name/fixtures/names.xml | 60 ++- .../doc/status_change/edit_relations.html | 3 +- .../doc/status_change/initial_template.txt | 2 +- .../doc/status_change/last_call.html | 42 ++ .../status_change/last_call_announcement.txt | 25 + .../doc/status_change/make_last_call.html | 40 ++ .../idrfc/document_status_change.html | 7 +- ietf/templates/iesg/agenda_doc.html | 8 + ietf/utils/test_data.py | 3 +- 15 files changed, 846 insertions(+), 55 deletions(-) create mode 100644 ietf/doc/migrations/0010_more_statchg_states.py create mode 100644 ietf/templates/doc/status_change/last_call.html create mode 100644 ietf/templates/doc/status_change/last_call_announcement.txt create mode 100644 ietf/templates/doc/status_change/make_last_call.html diff --git a/ietf/doc/migrations/0010_more_statchg_states.py b/ietf/doc/migrations/0010_more_statchg_states.py new file mode 100644 index 0000000000..0fca886a0c --- /dev/null +++ b/ietf/doc/migrations/0010_more_statchg_states.py @@ -0,0 +1,477 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +from ietf.doc.models import State,StateType + +class Migration(DataMigration): + + def forwards(self, orm): + + statchg = StateType.objects.get(slug='statchg') + + lc_req = State( + type=statchg,slug="lc-req", + name="Last Call Requested",used=True,order=3, + desc="Last Call has been requested for this proposed status change") + lc_req.save() + + in_lc = State( + type=statchg,slug="in-lc", + name="In Last Call",used=True,order=4, + desc="This proposed status change is in IETF Last Call") + in_lc.save() + + goahead = State( + type=statchg,slug="goahead", + name="Waiting for AD Go-Ahead",used=True,order=5, + desc="The AD is following up on IETF LC comments") + goahead.save() + + adrev=State.objects.get(type=statchg,slug='adrev') + iesgeval=State.objects.get(type=statchg,slug='iesgeval') + defer=State.objects.get(type=statchg,slug='defer') + appr_pr=State.objects.get(type=statchg,slug='appr-pr') + appr_pend=State.objects.get(type=statchg,slug='appr-pend') + appr_sent=State.objects.get(type=statchg,slug='appr-sent') + withdraw=State.objects.get(type=statchg,slug='withdraw') + dead=State.objects.get(type=statchg,slug='dead') + + adrev.next_states.add(lc_req) + adrev.save() + + lc_req.next_states.add(in_lc) + lc_req.save() + + in_lc.next_states.add(goahead) + in_lc.save() + + goahead.next_states.add(iesgeval,dead) + goahead.save() + + iesgeval.order=6 + iesgeval.save() + + defer.order=7 + defer.save() + + appr_pr.next_states.add(appr_pend,appr_sent) + appr_pr.order=8 + appr_pr.save() + + appr_pend.order=9 + appr_pend.save() + + appr_sent.order=10 + appr_sent.save() + + withdraw.delete() + + dead.order=11 + dead.save() + + def backwards(self, orm): + + statchg = StateType.objects.get(slug='statchg') + + State.objects.filter(type='statchg',slug__in=['lc-req','in-lc','goahead']).delete() + + needshep=State.objects.get(type=statchg,slug='needshep') + adrev=State.objects.get(type=statchg,slug='adrev') + iesgeval=State.objects.get(type=statchg,slug='iesgeval') + defer=State.objects.get(type=statchg,slug='defer') + appr_pr=State.objects.get(type=statchg,slug='appr-pr') + appr_pend=State.objects.get(type=statchg,slug='appr-pend') + appr_sent=State.objects.get(type=statchg,slug='appr-sent') + dead=State.objects.get(type=statchg,slug='dead') + + withdraw = State( + type=statchg,slug="withdraw", + name="Withdrawn",used=True,order=8, + desc="The request for RFC status changes was withdrawn") + withdraw.save() + + needshep.next_states.add(withdraw) + needshep.save() + + adrev.next_states.add(withdraw) + adrev.save() + + withdraw.next_states.add(needshep) + withdraw.save() + + iesgeval.order=3 + iesgeval.next_states.add(withdraw) + iesgeval.save() + + defer.order=4 + defer.next_states.add(withdraw) + defer.save() + + appr_pr.next_states.remove(appr_pend,appr_sent) + appr_pr.order=5 + appr_pr.save() + + appr_pend.order=6 + appr_pend.next_states.add(withdraw) + appr_pend.save() + + appr_sent.order=7 + appr_sent.save() + + dead.order=9 + dead.save() + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'doc.ballotdocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'BallotDocEvent', '_ormbases': ['doc.DocEvent']}, + 'ballot_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.BallotType']"}), + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'doc.ballotpositiondocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'BallotPositionDocEvent', '_ormbases': ['doc.DocEvent']}, + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}), + 'ballot': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['doc.BallotDocEvent']", 'null': 'True'}), + 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'comment_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'discuss': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'discuss_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'pos': ('django.db.models.fields.related.ForeignKey', [], {'default': "'norecord'", 'to': "orm['name.BallotPositionName']"}) + }, + 'doc.ballottype': { + 'Meta': {'ordering': "['order']", 'object_name': 'BallotType'}, + 'doc_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.DocTypeName']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'positions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['name.BallotPositionName']", 'symmetrical': 'False', 'blank': 'True'}), + 'question': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'doc.consensusdocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'ConsensusDocEvent', '_ormbases': ['doc.DocEvent']}, + 'consensus': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'doc.deletedevent': { + 'Meta': {'object_name': 'DeletedEvent'}, + 'by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'json': ('django.db.models.fields.TextField', [], {}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}) + }, + 'doc.docalias': { + 'Meta': {'object_name': 'DocAlias'}, + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.Document']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}) + }, + 'doc.docevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'DocEvent'}, + 'by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}), + 'desc': ('django.db.models.fields.TextField', [], {}), + 'doc': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.Document']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'doc.dochistory': { + 'Meta': {'object_name': 'DocHistory'}, + 'abstract': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'ad_dochistory_set'", 'null': 'True', 'to': "orm['person.Person']"}), + 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['person.Email']", 'symmetrical': 'False', 'through': "orm['doc.DocHistoryAuthor']", 'blank': 'True'}), + 'doc': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'history_set'", 'to': "orm['doc.Document']"}), + 'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'external_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'intended_std_level': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.IntendedStdLevelName']", 'null': 'True', 'blank': 'True'}), + 'internal_comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'note': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'notify': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '1', 'blank': 'True'}), + 'pages': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'related': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['doc.DocAlias']", 'symmetrical': 'False', 'through': "orm['doc.RelatedDocHistory']", 'blank': 'True'}), + 'rev': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'shepherd': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'shepherd_dochistory_set'", 'null': 'True', 'to': "orm['person.Person']"}), + 'states': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['doc.State']", 'symmetrical': 'False', 'blank': 'True'}), + 'std_level': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.StdLevelName']", 'null': 'True', 'blank': 'True'}), + 'stream': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.StreamName']", 'null': 'True', 'blank': 'True'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['name.DocTagName']", 'null': 'True', 'blank': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.DocTypeName']", 'null': 'True', 'blank': 'True'}) + }, + 'doc.dochistoryauthor': { + 'Meta': {'ordering': "['document', 'order']", 'object_name': 'DocHistoryAuthor'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Email']"}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.DocHistory']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.IntegerField', [], {}) + }, + 'doc.docreminder': { + 'Meta': {'object_name': 'DocReminder'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'due': ('django.db.models.fields.DateTimeField', [], {}), + 'event': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.DocEvent']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.DocReminderTypeName']"}) + }, + 'doc.document': { + 'Meta': {'object_name': 'Document'}, + 'abstract': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'ad_document_set'", 'null': 'True', 'to': "orm['person.Person']"}), + 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['person.Email']", 'symmetrical': 'False', 'through': "orm['doc.DocumentAuthor']", 'blank': 'True'}), + 'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'external_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']", 'null': 'True', 'blank': 'True'}), + 'intended_std_level': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.IntendedStdLevelName']", 'null': 'True', 'blank': 'True'}), + 'internal_comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}), + 'note': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'notify': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '1', 'blank': 'True'}), + 'pages': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'related': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'reversely_related_document_set'", 'blank': 'True', 'through': "orm['doc.RelatedDocument']", 'to': "orm['doc.DocAlias']"}), + 'rev': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'shepherd': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'shepherd_document_set'", 'null': 'True', 'to': "orm['person.Person']"}), + 'states': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['doc.State']", 'symmetrical': 'False', 'blank': 'True'}), + 'std_level': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.StdLevelName']", 'null': 'True', 'blank': 'True'}), + 'stream': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.StreamName']", 'null': 'True', 'blank': 'True'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['name.DocTagName']", 'null': 'True', 'blank': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.DocTypeName']", 'null': 'True', 'blank': 'True'}) + }, + 'doc.documentauthor': { + 'Meta': {'ordering': "['document', 'order']", 'object_name': 'DocumentAuthor'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Email']"}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.Document']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '1'}) + }, + 'doc.initialreviewdocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'InitialReviewDocEvent', '_ormbases': ['doc.DocEvent']}, + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'doc.lastcalldocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'LastCallDocEvent', '_ormbases': ['doc.DocEvent']}, + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'doc.newrevisiondocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'NewRevisionDocEvent', '_ormbases': ['doc.DocEvent']}, + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'rev': ('django.db.models.fields.CharField', [], {'max_length': '16'}) + }, + 'doc.relateddochistory': { + 'Meta': {'object_name': 'RelatedDocHistory'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'relationship': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.DocRelationshipName']"}), + 'source': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.DocHistory']"}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reversely_related_document_history_set'", 'to': "orm['doc.DocAlias']"}) + }, + 'doc.relateddocument': { + 'Meta': {'object_name': 'RelatedDocument'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'relationship': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.DocRelationshipName']"}), + 'source': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.Document']"}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.DocAlias']"}) + }, + 'doc.state': { + 'Meta': {'ordering': "['type', 'order']", 'object_name': 'State'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'next_states': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'previous_states'", 'blank': 'True', 'to': "orm['doc.State']"}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.StateType']"}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'doc.statedocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'StateDocEvent', '_ormbases': ['doc.DocEvent']}, + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.State']", 'null': 'True', 'blank': 'True'}), + 'state_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.StateType']"}) + }, + 'doc.statetype': { + 'Meta': {'object_name': 'StateType'}, + 'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '30', 'primary_key': 'True'}) + }, + 'doc.telechatdocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'TelechatDocEvent', '_ormbases': ['doc.DocEvent']}, + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'returning_item': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'telechat_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}) + }, + 'doc.writeupdocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'WriteupDocEvent', '_ormbases': ['doc.DocEvent']}, + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {'blank': 'True'}) + }, + 'group.group': { + 'Meta': {'object_name': 'Group'}, + 'acronym': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}), + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']", 'null': 'True', 'blank': 'True'}), + 'charter': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'chartered_group'", 'unique': 'True', 'null': 'True', 'to': "orm['doc.Document']"}), + 'comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'list_archive': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'list_email': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'list_subscribe': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']", 'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.GroupStateName']", 'null': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.GroupTypeName']", 'null': 'True'}), + 'unused_states': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['doc.State']", 'symmetrical': 'False', 'blank': 'True'}), + 'unused_tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['name.DocTagName']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'name.ballotpositionname': { + 'Meta': {'ordering': "['order']", 'object_name': 'BallotPositionName'}, + 'blocking': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.docrelationshipname': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocRelationshipName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'revname': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.docremindertypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocReminderTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.doctagname': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocTagName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.doctypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.groupstatename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupStateName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.grouptypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.intendedstdlevelname': { + 'Meta': {'ordering': "['order']", 'object_name': 'IntendedStdLevelName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.stdlevelname': { + 'Meta': {'ordering': "['order']", 'object_name': 'StdLevelName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.streamname': { + 'Meta': {'ordering': "['order']", 'object_name': 'StreamName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'person.email': { + 'Meta': {'object_name': 'Email'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'address': ('django.db.models.fields.CharField', [], {'max_length': '64', 'primary_key': 'True'}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']", 'null': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}) + }, + 'person.person': { + 'Meta': {'object_name': 'Person'}, + 'address': ('django.db.models.fields.TextField', [], {'max_length': '255', 'blank': 'True'}), + 'affiliation': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'ascii': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'ascii_short': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True', 'null': 'True', 'blank': 'True'}) + } + } + + complete_apps = ['doc'] diff --git a/ietf/doc/tests_status_change.py b/ietf/doc/tests_status_change.py index 90b55e5036..bcd10d181d 100644 --- a/ietf/doc/tests_status_change.py +++ b/ietf/doc/tests_status_change.py @@ -16,7 +16,7 @@ from ietf.doc.utils import create_ballot_if_not_open from ietf.doc.views_status_change import default_approval_text -from ietf.doc.models import Document,DocEvent,NewRevisionDocEvent,BallotPositionDocEvent,TelechatDocEvent,DocAlias,State +from ietf.doc.models import Document,DocEvent,NewRevisionDocEvent,BallotPositionDocEvent,TelechatDocEvent,WriteupDocEvent,DocAlias,State from ietf.name.models import StreamName from ietf.group.models import Person from ietf.iesg.models import TelechatDate @@ -202,6 +202,51 @@ def test_edit_telechat_date(self): doc = Document.objects.get(name='status-change-imaginary-mid-review') self.assertEquals(doc.latest_event(TelechatDocEvent, "scheduled_for_telechat").telechat_date,None) + def test_edit_lc(self): + doc = Document.objects.get(name='status-change-imaginary-mid-review') + url = urlreverse('status_change_last_call',kwargs=dict(name=doc.name)) + + login_testing_unauthorized(self, "ad", url) + + # additional setup + doc.relateddocument_set.create(target=DocAlias.objects.get(name='rfc9999'),relationship_id='tois') + doc.relateddocument_set.create(target=DocAlias.objects.get(name='rfc9998'),relationship_id='tohist') + doc.ad = Person.objects.get(name='Ad No2') + doc.save() + + # get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertEquals(len(q('form.edit-last-call-text')),1) + + self.assertTrue( 'RFC9999 from Proposed Standard to Internet Standard' in ''.join(wrap(r.content,2**16))) + self.assertTrue( 'RFC9998 from Informational to Historic' in ''.join(wrap(r.content,2**16))) + + # save + r = self.client.post(url,dict(last_call_text="Bogus last call text",save_last_call_text="1")) + self.assertEquals(r.status_code, 200) + + last_call_event = doc.latest_event(WriteupDocEvent, type="changed_last_call_text") + self.assertEquals(last_call_event.text,"Bogus last call text") + + # reset + r = self.client.post(url,dict(regenerate_last_call_text="1")) + self.assertEquals(r.status_code,200) + self.assertTrue( 'RFC9999 from Proposed Standard to Internet Standard' in ''.join(wrap(r.content,2**16))) + self.assertTrue( 'RFC9998 from Informational to Historic' in ''.join(wrap(r.content,2**16))) + + # request last call + messages_before = len(outbox) + r = self.client.post(url,dict(last_call_text='stuff',send_last_call_request='Save+and+Request+Last+Call')) + self.assertEquals(r.status_code,200) + self.assertTrue( 'Last Call Requested' in ''.join(wrap(r.content,2**16))) + self.assertEquals(len(outbox), messages_before + 1) + self.assertTrue('iesg-secretary' in outbox[-1]['To']) + self.assertTrue('Last Call:' in outbox[-1]['Subject']) + self.assertTrue('Last Call Request has been submitted' in ''.join(wrap(unicode(outbox[-1]),2**16))) + + def test_approve(self): doc = Document.objects.get(name='status-change-imaginary-mid-review') url = urlreverse('status_change_approve',kwargs=dict(name=doc.name)) @@ -263,21 +308,24 @@ def test_edit_relations(self): # Try to add a relation to an RFC that doesn't exist r = self.client.post(url,dict(new_relation_row_blah="rfc9997", - statchg_relation_row_blah="tois")) + statchg_relation_row_blah="tois", + Submit="Submit")) self.assertEquals(r.status_code, 200) q = PyQuery(r.content) self.assertTrue(len(q('form ul.errorlist')) > 0) # Try to add a relation leaving the relation type blank r = self.client.post(url,dict(new_relation_row_blah="rfc9999", - statchg_relation_row_blah="")) + statchg_relation_row_blah="", + Submit="Submit")) self.assertEquals(r.status_code, 200) q = PyQuery(r.content) self.assertTrue(len(q('form ul.errorlist')) > 0) # Try to add a relation with an unknown relationship type r = self.client.post(url,dict(new_relation_row_blah="rfc9999", - statchg_relation_row_blah="badslug")) + statchg_relation_row_blah="badslug", + Submit="Submit")) self.assertEquals(r.status_code, 200) q = PyQuery(r.content) self.assertTrue(len(q('form ul.errorlist')) > 0) @@ -286,7 +334,8 @@ def test_edit_relations(self): r = self.client.post(url,dict(new_relation_row_blah="rfc9999", statchg_relation_row_blah="toexp", new_relation_row_foo="rfc9998", - statchg_relation_row_foo="tobcp")) + statchg_relation_row_foo="tobcp", + Submit="Submit")) self.assertEquals(r.status_code, 302) doc = Document.objects.get(name='status-change-imaginary-mid-review') self.assertEquals(doc.relateddocument_set.count(),2) diff --git a/ietf/doc/urls_status_change.py b/ietf/doc/urls_status_change.py index 91bbea9bb7..573835833d 100644 --- a/ietf/doc/urls_status_change.py +++ b/ietf/doc/urls_status_change.py @@ -8,5 +8,6 @@ url(r'^approve/$', "approve", name='status_change_approve'), url(r'^telechat/$', "telechat_date", name='status_change_telechat_date'), url(r'^relations/$', "edit_relations", name='status_change_relations'), + url(r'^last-call/$', "last_call", name='status_change_last_call'), ) diff --git a/ietf/doc/views_status_change.py b/ietf/doc/views_status_change.py index 560f2e9fe5..2e7ff3bcc6 100644 --- a/ietf/doc/views_status_change.py +++ b/ietf/doc/views_status_change.py @@ -25,6 +25,9 @@ from ietf.doc.forms import TelechatForm, AdForm, NotifyForm +from ietf.idrfc.views_ballot import LastCallTextForm +from ietf.idrfc.lastcall import request_last_call + class ChangeStateForm(forms.Form): new_state = forms.ModelChoiceField(State.objects.filter(type="statchg", used=True), label="Status Change Evaluation State", empty_label=None, required=True) comment = forms.CharField(widget=forms.Textarea, help_text="Optional comment for the review history", required=False) @@ -588,8 +591,8 @@ def edit_relations(request, name): if request.method == 'POST': form = EditStatusChangeForm(request.POST) - if form.is_valid(): - + if 'Submit' in request.POST and form.is_valid(): + old_relations={} for rel in status_change.relateddocument_set.filter(relationship__slug__in=RELATION_SLUGS): old_relations[rel.target.document.canonical_name()]=rel.relationship.slug @@ -605,13 +608,14 @@ def edit_relations(request, name): c.desc += "\nNEW:" for relname,relslug in (set(new_relations.items())-set(old_relations.items())): c.desc += "\n "+relname+": "+DocRelationshipName.objects.get(slug=relslug).name - #for rel in status_change.relateddocument_set.filter(relationship__slug__in=RELATION_SLUGS): - # c.desc +="\n"+rel.relationship.name+": "+rel.target.document.canonical_name() c.desc += "\n" c.save() return HttpResponseRedirect(status_change.get_absolute_url()) + elif 'Cancel' in request.POST: + return HttpResponseRedirect(status_change.get_absolute_url()) + else: relations={} for rel in status_change.relateddocument_set.filter(relationship__slug__in=RELATION_SLUGS): @@ -627,3 +631,93 @@ def edit_relations(request, name): 'relation_slugs': relation_slugs, }, context_instance = RequestContext(request)) + +def generate_last_call_text(request, doc): + + # requester should be set based on doc.group once the group for a status change can be set to something other than the IESG + # and when groups are set, vary the expiration time accordingly + + requester = "an individual participant" + expiration_date = datetime.date.today() + datetime.timedelta(days=28) + cc = [] + + new_text = render_to_string("doc/status_change/last_call_announcement.txt", + dict(doc=doc, + settings=settings, + requester=requester, + expiration_date=expiration_date.strftime("%Y-%m-%d"), + changes=['%s from %s to %s'%(rel.target.name.upper(),rel.target.document.std_level.name,newstatus(rel)) for rel in doc.relateddocument_set.filter(relationship__slug__in=RELATION_SLUGS)], + urls=[rel.target.document.get_absolute_url() for rel in doc.relateddocument_set.filter(relationship__slug__in=RELATION_SLUGS)], + cc=cc + ) + ) + + e = WriteupDocEvent() + e.type = 'changed_last_call_text' + e.by = request.user.get_profile() + e.doc = doc + e.desc = 'Last call announcement was generated' + e.text = unicode(new_text) + e.save() + + return e + +@role_required("Area Director", "Secretariat") +def last_call(request, name): + """Edit the Last Call Text for this status change and possibly request IETF LC""" + + status_change = get_object_or_404(Document, type="statchg", name=name) + + login = request.user.get_profile() + + last_call_event = status_change.latest_event(WriteupDocEvent, type="changed_last_call_text") + if not last_call_event: + last_call_event = generate_last_call_text(request, status_change) + + form = LastCallTextForm(initial=dict(last_call_text=last_call_event.text)) + + if request.method == 'POST': + if "save_last_call_text" in request.POST or "send_last_call_request" in request.POST: + form = LastCallTextForm(request.POST) + if form.is_valid(): + t = form.cleaned_data['last_call_text'] + if t != last_call_event.text: + e = WriteupDocEvent(doc=status_change, by=login) + e.by = login + e.type = "changed_last_call_text" + e.desc = "Last call announcement was changed" + e.text = t + e.save() + + if "send_last_call_request" in request.POST: + save_document_in_history(status_change) + + old_description = status_change.friendly_state() + status_change.set_state(State.objects.get(type='statchg', slug='lc-req')) + new_description = status_change.friendly_state() + + e = log_state_changed(request, status_change, login, new_description, old_description) + + status_change.time = e.time + status_change.save() + + request_last_call(request, status_change) + + return render_to_response('idrfc/last_call_requested.html', + dict(doc=status_change, + url = status_change.get_absolute_url(), + ), + context_instance=RequestContext(request)) + + if "regenerate_last_call_text" in request.POST: + e = generate_last_call_text(request,status_change) + form = LastCallTextForm(initial=dict(last_call_text=e.text)) + + return render_to_response('doc/status_change/last_call.html', + dict(doc=status_change, + back_url = status_change.get_absolute_url(), + last_call_event = last_call_event, + last_call_form = form, + ), + context_instance = RequestContext(request)) + diff --git a/ietf/idrfc/views_ballot.py b/ietf/idrfc/views_ballot.py index f7d16e652b..918c9e834e 100644 --- a/ietf/idrfc/views_ballot.py +++ b/ietf/idrfc/views_ballot.py @@ -1094,13 +1094,15 @@ class MakeLastCallForm(forms.Form): def make_last_call(request, name): """Make last call for Internet Draft, sending out announcement.""" doc = get_object_or_404(Document, docalias__name=name) - if not doc.get_state("draft-iesg"): + if not (doc.get_state("draft-iesg") or doc.get_state("statchg")): raise Http404 login = request.user.get_profile() e = doc.latest_event(WriteupDocEvent, type="changed_last_call_text") if not e: + if doc.type.slug != 'draft': + raise Http404 e = generate_last_call_announcement(request, doc) announcement = e.text @@ -1108,8 +1110,9 @@ def make_last_call(request, name): form = MakeLastCallForm(request.POST) if form.is_valid(): send_mail_preformatted(request, announcement) - send_mail_preformatted(request, announcement, extra=extra_automation_headers(doc), - override={ "To": "IANA ", "CC": None, "Bcc": None, "Reply-To": None}) + if doc.type.slug == 'draft': + send_mail_preformatted(request, announcement, extra=extra_automation_headers(doc), + override={ "To": "IANA ", "CC": None, "Bcc": None, "Reply-To": None}) msg = infer_message(announcement) msg.by = login @@ -1118,20 +1121,29 @@ def make_last_call(request, name): save_document_in_history(doc) - prev = doc.get_state("draft-iesg") - doc.set_state(State.objects.get(used=True, type="draft-iesg", slug='lc')) + if doc.type.slug == 'draft': - prev_tag = doc.tags.filter(slug__in=('point', 'ad-f-up', 'need-rev', 'extpty')) - prev_tag = prev_tag[0] if prev_tag else None - if prev_tag: - doc.tags.remove(prev_tag) + prev = doc.get_state("draft-iesg") + doc.set_state(State.objects.get(used=True, type="draft-iesg", slug='lc')) - e = idrfcutil_log_state_changed(request, doc, login, prev, prev_tag) + prev_tag = doc.tags.filter(slug__in=('point', 'ad-f-up', 'need-rev', 'extpty')) + prev_tag = prev_tag[0] if prev_tag else None + if prev_tag: + doc.tags.remove(prev_tag) + + e = idrfcutil_log_state_changed(request, doc, login, prev, prev_tag) + change_description = "Last call has been made for %s and state has been changed to %s" % (doc.name, doc.get_state("draft-iesg").name) + + elif doc.type.slug == 'statchg': + + prev = doc.friendly_state() + doc.set_state(State.objects.get(used=True, type="statchg", slug='in-lc')) + e = docutil_log_state_changed(request, doc, login, doc.friendly_state(), prev) + change_description = "Last call has been made for %s and state has been changed to %s" % (doc.name, doc.friendly_state()) doc.time = e.time doc.save() - change_description = "Last call has been made for %s and state has been changed to %s" % (doc.name, doc.get_state("draft-iesg").name) email_state_changed(request, doc, change_description) email_owner(request, doc, doc.ad, login, change_description) @@ -1146,25 +1158,34 @@ def make_last_call(request, name): e.save() # update IANA Review state - prev_state = doc.get_state("draft-iana-review") - if not prev_state: - next_state = State.objects.get(used=True, type="draft-iana-review", slug="need-rev") - doc.set_state(next_state) - add_state_change_event(doc, login, prev_state, next_state) + if doc.type.slug == 'draft': + prev_state = doc.get_state("draft-iana-review") + if not prev_state: + next_state = State.objects.get(used=True, type="draft-iana-review", slug="need-rev") + doc.set_state(next_state) + add_state_change_event(doc, login, prev_state, next_state) return HttpResponseRedirect(doc.get_absolute_url()) else: initial = {} initial["last_call_sent_date"] = date.today() - expire_days = 14 - if doc.group.type_id in ("individ", "area"): - expire_days = 28 + if doc.type.slug == 'draft': + # This logic is repeated in the code that edits last call text - why? + expire_days = 14 + if doc.group.type_id in ("individ", "area"): + expire_days = 28 + templ = 'idrfc/make_last_callREDESIGN.html' + else: + expire_days=28 + templ = 'doc/status_change/make_last_call.html' initial["last_call_expiration_date"] = date.today() + timedelta(days=expire_days) form = MakeLastCallForm(initial=initial) - return render_to_response('idrfc/make_last_callREDESIGN.html', + return render_to_response(templ, dict(doc=doc, - form=form), + form=form, + announcement=announcement, + ), context_instance=RequestContext(request)) diff --git a/ietf/idrfc/views_search.py b/ietf/idrfc/views_search.py index 4ce8f11f4c..7acc084ce2 100644 --- a/ietf/idrfc/views_search.py +++ b/ietf/idrfc/views_search.py @@ -640,6 +640,10 @@ def ad_dashboard_sort_key(doc): elif doc.get_state_slug('charter') == 'iesgrev': state = State.objects.get(type__slug='draft-iesg',slug='iesg-eva') return "1%d%s" % (state.order,seed) + + if doc.type.slug=='statchg' and doc.get_state_slug('statchg') == 'adrev': + state = State.objects.get(type__slug='draft-iesg',slug='ad-eval') + return "1%d%s" % (state.order,seed) if seed.startswith('Needs Shepherd'): return "100%s" % seed @@ -664,6 +668,7 @@ def ad_dashboard_sort_key(doc): def by_ad2(request, name): responsible = Document.objects.values_list('ad', flat=True).distinct() + ad_id = None for p in Person.objects.filter(Q(role__name__in=("pre-ad", "ad"), role__group__type="area", role__group__state="active") @@ -672,6 +677,10 @@ def by_ad2(request, name): ad_id = p.id ad_name = p.plain_name() break + + if not ad_id: + raise Http404 + docqueryset = Document.objects.filter(ad__id=ad_id) docs=[] for doc in docqueryset: diff --git a/ietf/name/fixtures/names.xml b/ietf/name/fixtures/names.xml index 592204e078..966f855763 100644 --- a/ietf/name/fixtures/names.xml +++ b/ietf/name/fixtures/names.xml @@ -2053,7 +2053,7 @@ True An RFC status change has been requested, but a shepherding AD has not yet been assigned 1 - + statchg @@ -2062,7 +2062,34 @@ True The sponsoring AD is preparing an RFC status change document 2 - + + + + statchg + lc-req + Last Call Requested + True + Last Call has been requested for this proposed status change + 3 + + + + statchg + in-lc + In Last Call + True + This proposed status change is in IETF Last Call + 4 + + + + statchg + goahead + Waiting for AD Go-Ahead + True + The AD is following up on IETF LC comments + 5 + statchg @@ -2070,8 +2097,8 @@ IESG Evaluation True The IESG is considering the proposed RFC status changes - 3 - + 6 + statchg @@ -2079,8 +2106,8 @@ IESG Evaluation - Defer True The evaluation of the proposed RFC status changes have been deferred to the next telechat - 4 - + 7 + statchg @@ -2088,8 +2115,8 @@ Approved - point raised True The IESG has approved the RFC status changes, but a point has been raised that should be cleared before proceeding to announcement to be sent - 5 - + 8 + statchg @@ -2097,8 +2124,8 @@ Approved - announcement to be sent True The IESG has approved the RFC status changes, but the secretariat has not yet sent the announcement - 6 - + 9 + statchg @@ -2106,25 +2133,16 @@ Approved - announcement sent True The secretariat has announced the IESG's approved RFC status changes - 7 + 10 - - statchg - withdraw - Withdrawn - True - The request for RFC status changes was withdrawn - 8 - - statchg dead Dead True The RFC status changes have been abandoned - 9 + 11 diff --git a/ietf/templates/doc/status_change/edit_relations.html b/ietf/templates/doc/status_change/edit_relations.html index f83d3e6481..ef708cca04 100644 --- a/ietf/templates/doc/status_change/edit_relations.html +++ b/ietf/templates/doc/status_change/edit_relations.html @@ -65,7 +65,8 @@

Edit List of RFCs Affected By Status Change

- + + diff --git a/ietf/templates/doc/status_change/initial_template.txt b/ietf/templates/doc/status_change/initial_template.txt index a66d68f8ac..5c58ae3abd 100644 --- a/ietf/templates/doc/status_change/initial_template.txt +++ b/ietf/templates/doc/status_change/initial_template.txt @@ -1,4 +1,4 @@ -Provide a description of what RFCs status are changed and any necessary rational for the change. +Provide a description of what RFCs status are changed and any necessary rationale for the change. This is a good place to document how the RFC6410 criteria for advancing to Internet Standard are met: diff --git a/ietf/templates/doc/status_change/last_call.html b/ietf/templates/doc/status_change/last_call.html new file mode 100644 index 0000000000..6da9ec714b --- /dev/null +++ b/ietf/templates/doc/status_change/last_call.html @@ -0,0 +1,42 @@ +{% extends "base.html" %} + +{% block title %}Last Call text for {{ doc }}{% endblock %} + +{% block morecss %} +form #id_last_call_text { + width: 700px; + height: 600px; +} +{% endblock %} + +{% block content %} +

Last Call text for {{ doc }}

+ +
+ + {{ last_call_form.last_call_text }} + +

{{ last_call_form.last_call_text.errors }}

+ +{% comment %} + {% if can_request_last_call and need_intended_status %} +

You need to select intended status of {{ need_intended_status }} and regenerate last call text to request last call.

+ {% endif %} +{% endcomment %} + +
+ Back + + + +
+
+ +{% load ietf_filters %} +{% if user|in_group:"Secretariat" %} +

+Make Last Call +

+{% endif %} + +{% endblock%} diff --git a/ietf/templates/doc/status_change/last_call_announcement.txt b/ietf/templates/doc/status_change/last_call_announcement.txt new file mode 100644 index 0000000000..52f63a2dbc --- /dev/null +++ b/ietf/templates/doc/status_change/last_call_announcement.txt @@ -0,0 +1,25 @@ +{% load ietf_filters %}{% load mail_filters %}{% autoescape off %}From: The IESG +To: IETF-Announce {% if cc %} +CC: {{ cc }}{% endif %} +Reply-To: ietf@ietf.org +Sender: +Subject: Last Call: {{ doc.title|clean_whitespace }} + +{% filter wordwrap:73 %} +The IESG has received a request from {{ requester }} to make the following status changes: +{%for change in changes %} +- {{ change }} +{% endfor %} +The supporting document for this request can be found here: + +{{ settings.IDTRACKER_BASE_URL }}{{ doc.get_absolute_url }} + +The IESG plans to make a decision in the next few weeks, and solicits final comments on this action. Please send substantive comments to the ietf@ietf.org mailing lists by {{ expiration_date }}. Exceptionally, comments may be sent to iesg@ietf.org instead. In either case, please retain the beginning of the Subject line to allow automated sorting.{% endfilter %} + +The affected document{{ urls|pluralize }} can be obtained via +{% for u in urls %}{{ settings.IDTRACKER_BASE_URL}}{{ u }} +{% endfor %} +IESG discussion of this request can be tracked via +{{ settings.IDTRACKER_BASE_URL }}{% url doc_ballot name=doc %} + +{% endautoescape %} diff --git a/ietf/templates/doc/status_change/make_last_call.html b/ietf/templates/doc/status_change/make_last_call.html new file mode 100644 index 0000000000..ca079a6543 --- /dev/null +++ b/ietf/templates/doc/status_change/make_last_call.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} + +{% block title %}Make Last Call for {{ doc.name }}{% endblock %} + +{% block morecss %} +form.approve-ballot pre { + margin: 0; + padding: 4px; + border-top: 4px solid #eee; + border-bottom: 4px solid #eee; +} +form.approve-ballot .announcement { + overflow-x: auto; + overflow-y: scroll; + width: 800px; + height: 400px; + border: 1px solid #bbb; +} +{% endblock %} + +{% block content %} +

Make Last Call for {{ doc.file_tag }}

+

({{doc.title}})

+

Announcement Text:

+
+{{ announcement }} +
+ +
+ + {{ form.as_table }} +
+ +
+ Back + + +
+
+{% endblock %} diff --git a/ietf/templates/idrfc/document_status_change.html b/ietf/templates/idrfc/document_status_change.html index c45f7ae257..4361e44339 100644 --- a/ietf/templates/idrfc/document_status_change.html +++ b/ietf/templates/idrfc/document_status_change.html @@ -102,11 +102,16 @@ Last updated: {{ doc.time|date:"Y-m-d" }} {% if not snapshot and user|has_role:"Area Director,Secretariat" and doc.get_state_slug not in approved_states %} - + Edit Affected RFC List + + + Edit Last Call Text + + {% endif %} diff --git a/ietf/templates/iesg/agenda_doc.html b/ietf/templates/iesg/agenda_doc.html index 6e017e6db8..ecb2a45e4b 100644 --- a/ietf/templates/iesg/agenda_doc.html +++ b/ietf/templates/iesg/agenda_doc.html @@ -53,6 +53,14 @@

{{ title2 }}

which it covers? If not, what changes would make it so?" {% endif %} +{% if title2|startswith:"3.3" %} +
+ Reviews should focus on these questions: "Are the proposed + changes to document status appropriate? Have all requirements + for such a change been met? If not, what changes to the proposal + would make it appropriate?" +
+{% endif %} {% endif %}

{{ title3 }}

{% for doc in section_docs %} diff --git a/ietf/utils/test_data.py b/ietf/utils/test_data.py index 0fa40ed9ee..91e5689eeb 100644 --- a/ietf/utils/test_data.py +++ b/ietf/utils/test_data.py @@ -406,7 +406,8 @@ def make_test_data(): crdoc.relateddocument_set.create(target=docalias,relationship_id='conflrev') # A status change mid review - doc = Document.objects.create(name='status-change-imaginary-mid-review',type_id='statchg', rev='00', notify="fsm@ietf.org") + iesg = Group.objects.get(acronym='iesg') + doc = Document.objects.create(name='status-change-imaginary-mid-review',type_id='statchg', rev='00', notify="fsm@ietf.org",group=iesg) doc.set_state(State.objects.get(slug='needshep',type__slug='statchg')) doc.save() docalias = DocAlias.objects.create(name='status-change-imaginary-mid-review',document=doc)