diff --git a/ietf/doc/forms.py b/ietf/doc/forms.py new file mode 100644 index 000000000..2058d86bd --- /dev/null +++ b/ietf/doc/forms.py @@ -0,0 +1,37 @@ +import datetime + +from django import forms + +from ietf.iesg.models import TelechatDate + +class TelechatForm(forms.Form): + telechat_date = forms.TypedChoiceField(coerce=lambda x: datetime.datetime.strptime(x, '%Y-%m-%d').date(), empty_value=None, required=False) + returning_item = forms.BooleanField(required=False) + + def __init__(self, *args, **kwargs): + super(self.__class__, self).__init__(*args, **kwargs) + + dates = [d.date for d in TelechatDate.objects.active().order_by('date')] + init = kwargs['initial'].get("telechat_date") + if init and init not in dates: + dates.insert(0, init) + + self.fields['telechat_date'].choices = [("", "(not on agenda)")] + [(d, d.strftime("%Y-%m-%d")) for d in dates] + +from ietf.person.models import Person + +class AdForm(forms.Form): + ad = forms.ModelChoiceField(Person.objects.filter(role__name="ad", role__group__state="active").order_by('name'), + label="Shepherding AD", empty_label="(None)", required=True) + + def __init__(self, *args, **kwargs): + super(self.__class__, self).__init__(*args, **kwargs) + + # if previous AD is now ex-AD, append that person to the list + ad_pk = self.initial.get('ad') + choices = self.fields['ad'].choices + if ad_pk and ad_pk not in [pk for pk, name in choices]: + self.fields['ad'].choices = list(choices) + [("", "-------"), (ad_pk, Person.objects.get(pk=ad_pk).plain_name())] + +class NotifyForm(forms.Form): + notify = forms.CharField(max_length=255, label="Notice emails", help_text="Separate email addresses with commas", required=False) diff --git a/ietf/doc/migrations/0005_add_statchg.py b/ietf/doc/migrations/0005_add_statchg.py new file mode 100644 index 000000000..93d59a634 --- /dev/null +++ b/ietf/doc/migrations/0005_add_statchg.py @@ -0,0 +1,427 @@ +# 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 StateType, State, BallotType, DocTypeName +from ietf.name.models import BallotPositionName + +class Migration(DataMigration): + + def forwards(self, orm): + statchg = StateType(slug='statchg',label='RFC Status Change') + statchg.save() + + needshep = State( + type=statchg,slug="needshep", + name="Needs Shepherd",used=True,order=1, + desc="An RFC status change has been requested, but a shepherding AD has not yet been assigned") + needshep.save() + + adrev = State( + type=statchg,slug="adrev", + name="AD Review",used=True,order=2, + desc="The sponsoring AD is preparing an RFC status change document") + adrev.save() + + iesgeval = State( + type=statchg,slug="iesgeval", + name="IESG Evaluation",used=True,order=3, + desc="The IESG is considering the proposed RFC status changes") + iesgeval.save() + + defer = State( + type=statchg,slug="defer", + name="IESG Evaluation - Defer",used=True, order=4, + desc="The evaluation of the proposed RFC status changes have been deferred to the next telechat") + defer.save() + + appr_pr = State( + type=statchg,slug="appr-pr", + name="Approved - point raised", used=True,order=5, + desc="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") + appr_pr.save() + + appr_pend = State( + type=statchg,slug="appr-pend", + name="Approved - announcement to be sent", used=True,order=6, + desc="The IESG has approved the RFC status changes, but the secretariat has not yet sent the announcement") + appr_pend.save() + + appr_sent = State( + type=statchg,slug="appr-sent", + name="Approved - announcement sent",used=True,order=7, + desc="The secretariat has announced the IESG's approved RFC status changes") + appr_sent.save() + + withdraw = State( + type=statchg,slug="withdraw", + name="Withdrawn",used=True,order=8, + desc="The request for RFC status changes was withdrawn") + withdraw.save() + + dead = State( + type=statchg,slug="dead", + name="Dead",used=True,order=9, + desc="The RFC status changes have been abandoned") + dead.save() + + needshep.next_states.add(adrev,withdraw,dead) + needshep.save() + adrev.next_states.add(iesgeval,withdraw,dead) + adrev.save() + iesgeval.next_states.add(appr_pr,appr_pend,defer,withdraw,dead) + iesgeval.save() + defer.next_states.add(iesgeval,appr_pend,appr_pr,withdraw,dead) + defer.save() + appr_pend.next_states.add(appr_sent,withdraw) + appr_pend.save() + withdraw.next_states.add(needshep) + withdraw.save() + dead.next_states.add(needshep) + dead.save() + + statchg_ballot = BallotType(doc_type=DocTypeName.objects.get(slug='statchg'), + slug='statchg',name="Approve",used=True, + question="Do we approve these RFC status changes?") + statchg_ballot.save() + statchg_ballot.positions.add('yes','noobj','discuss','abstain','recuse','norecord') + statchg_ballot.save() + + def backwards(self, orm): + StateType.objects.filter(slug='statchg').delete() + StateType.objects.filter(slug='statchg').delete() + BallotType.objects.filter(slug='statchg').delete() + + 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.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', [], {'related_name': "'previous_states'", 'symmetrical': 'False', '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.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'}), + '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/models.py b/ietf/doc/models.py index 18c937ac4..1cefeda24 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -150,8 +150,7 @@ class RelatedDocument(models.Model): def action(self): return self.relationship.name def inverse_action(): - infinitive = self.relationship.name[:-1] - return u"%sd by" % infinitive + return self.relationship.revname def __unicode__(self): return u"%s %s %s" % (self.source.name, self.relationship.name.lower(), self.target.name) diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py index 6ffbe2985..c527fa6c4 100644 --- a/ietf/doc/tests.py +++ b/ietf/doc/tests.py @@ -1,3 +1,4 @@ from ietf.doc.tests_conflict_review import * +from ietf.doc.tests_status_change import * diff --git a/ietf/doc/tests_status_change.py b/ietf/doc/tests_status_change.py new file mode 100644 index 000000000..9e7ae64fe --- /dev/null +++ b/ietf/doc/tests_status_change.py @@ -0,0 +1,392 @@ +import os +import shutil + +from pyquery import PyQuery +from StringIO import StringIO +from textwrap import wrap + + +import django.test +from django.conf import settings +from django.core.urlresolvers import reverse as urlreverse + +from ietf.utils.test_utils import login_testing_unauthorized +from ietf.utils.test_data import make_test_data +from ietf.utils.mail import outbox +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.name.models import StreamName +from ietf.group.models import Person +from ietf.iesg.models import TelechatDate + + +class StatusChangeTestCase(django.test.TestCase): + + fixtures = ['names'] + + def test_start_review(self): + + url = urlreverse('start_rfc_status_change') + login_testing_unauthorized(self, "secretary", url) + + # normal get should succeed and get a reasonable form + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertEquals(len(q('form select[name=create_in_state]')),1) + + ad_strpk = str(Person.objects.get(name='Aread Irector').pk) + state_strpk = str(State.objects.get(slug='adrev',type__slug='statchg').pk) + + # faulty posts + + ## Must set a responsible AD + r = self.client.post(url,dict(document_name="bogus",title="Bogus Title",ad="",create_in_state=state_strpk,notify='ipu@ietf.org')) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue(len(q('form ul.errorlist')) > 0) + + ## Must set a name + r = self.client.post(url,dict(document_name="",title="Bogus Title",ad=ad_strpk,create_in_state=state_strpk,notify='ipu@ietf.org')) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue(len(q('form ul.errorlist')) > 0) + + ## Must not choose a document name that already exists + r = self.client.post(url,dict(document_name="imaginary-mid-review",title="Bogus Title",ad=ad_strpk,create_in_state=state_strpk,notify='ipu@ietf.org')) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue(len(q('form ul.errorlist')) > 0) + + ## Must set a title + r = self.client.post(url,dict(document_name="bogus",title="",ad=ad_strpk,create_in_state=state_strpk,notify='ipu@ietf.org')) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue(len(q('form ul.errorlist')) > 0) + + # successful status change start + r = self.client.post(url,dict(document_name="imaginary-new",title="A new imaginary status change",ad=ad_strpk, + create_in_state=state_strpk,notify='ipu@ietf.org',new_relation_row_blah="rfc9999", + statchg_relation_row_blah="tois")) + self.assertEquals(r.status_code, 302) + status_change = Document.objects.get(name='status-change-imaginary-new') + self.assertEquals(status_change.get_state('statchg').slug,'adrev') + self.assertEquals(status_change.rev,u'00') + self.assertEquals(status_change.ad.name,u'Aread Irector') + self.assertEquals(status_change.notify,u'ipu@ietf.org') + self.assertTrue(status_change.relateddocument_set.filter(relationship__slug='tois',target__document__name='draft-ietf-random-thing')) + + def test_change_state(self): + + doc = Document.objects.get(name='status-change-imaginary-mid-review') + url = urlreverse('status_change_change_state',kwargs=dict(name=doc.name)) + + login_testing_unauthorized(self, "ad", url) + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertEquals(len(q('form select[name=new_state]')),1) + + # faulty post + r = self.client.post(url,dict(new_state="")) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue(len(q('form ul.errorlist')) > 0) + + # successful change to AD Review + adrev_pk = str(State.objects.get(slug='adrev',type__slug='statchg').pk) + r = self.client.post(url,dict(new_state=adrev_pk,comment='RDNK84ZD')) + self.assertEquals(r.status_code, 302) + doc = Document.objects.get(name='status-change-imaginary-mid-review') + self.assertEquals(doc.get_state('statchg').slug,'adrev') + self.assertTrue(doc.latest_event(DocEvent,type="added_comment").desc.startswith('RDNK84ZD')) + self.assertFalse(doc.active_ballot()) + + # successful change to IESG Evaluation + iesgeval_pk = str(State.objects.get(slug='iesgeval',type__slug='statchg').pk) + r = self.client.post(url,dict(new_state=iesgeval_pk,comment='TGmZtEjt')) + self.assertEquals(r.status_code, 302) + doc = Document.objects.get(name='status-change-imaginary-mid-review') + self.assertEquals(doc.get_state('statchg').slug,'iesgeval') + self.assertTrue(doc.latest_event(DocEvent,type="added_comment").desc.startswith('TGmZtEjt')) + self.assertTrue(doc.active_ballot()) + self.assertEquals(doc.latest_event(BallotPositionDocEvent, type="changed_ballot_position").pos_id,'yes') + + def test_edit_notices(self): + doc = Document.objects.get(name='status-change-imaginary-mid-review') + url = urlreverse('status_change_notices',kwargs=dict(name=doc.name)) + + login_testing_unauthorized(self, "ad", url) + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertEquals(len(q('form input[name=notify]')),1) + self.assertEquals(doc.notify,q('form input[name=notify]')[0].value) + + # change notice list + newlist = '"Foo Bar" ' + r = self.client.post(url,dict(notify=newlist)) + self.assertEquals(r.status_code,302) + doc = Document.objects.get(name='status-change-imaginary-mid-review') + self.assertEquals(doc.notify,newlist) + self.assertTrue(doc.latest_event(DocEvent,type="added_comment").desc.startswith('Notification list changed')) + + def test_edit_ad(self): + doc = Document.objects.get(name='status-change-imaginary-mid-review') + url = urlreverse('status_change_ad',kwargs=dict(name=doc.name)) + + login_testing_unauthorized(self, "ad", url) + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertEquals(len(q('select[name=ad]')),1) + + # change ads + ad2 = Person.objects.get(name='Ad No2') + r = self.client.post(url,dict(ad=str(ad2.pk))) + self.assertEquals(r.status_code,302) + doc = Document.objects.get(name='status-change-imaginary-mid-review') + self.assertEquals(doc.ad,ad2) + self.assertTrue(doc.latest_event(DocEvent,type="added_comment").desc.startswith('Shepherding AD changed')) + + def test_edit_telechat_date(self): + doc = Document.objects.get(name='status-change-imaginary-mid-review') + url = urlreverse('status_change_telechat_date',kwargs=dict(name=doc.name)) + + login_testing_unauthorized(self, "ad", url) + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertEquals(len(q('select[name=telechat_date]')),1) + + # set a date + self.assertFalse(doc.latest_event(TelechatDocEvent, "scheduled_for_telechat")) + telechat_date = TelechatDate.objects.active().order_by('date')[0].date + r = self.client.post(url,dict(telechat_date=telechat_date.isoformat())) + self.assertEquals(r.status_code,302) + doc = Document.objects.get(name='status-change-imaginary-mid-review') + self.assertEquals(doc.latest_event(TelechatDocEvent, "scheduled_for_telechat").telechat_date,telechat_date) + + # move it forward a telechat (this should set the returning item bit) + telechat_date = TelechatDate.objects.active().order_by('date')[1].date + r = self.client.post(url,dict(telechat_date=telechat_date.isoformat())) + self.assertEquals(r.status_code,302) + doc = Document.objects.get(name='status-change-imaginary-mid-review') + self.assertTrue(doc.returning_item()) + + # clear the returning item bit + r = self.client.post(url,dict(telechat_date=telechat_date.isoformat())) + self.assertEquals(r.status_code,302) + doc = Document.objects.get(name='status-change-imaginary-mid-review') + self.assertFalse(doc.returning_item()) + + # set the returning item bit without changing the date + r = self.client.post(url,dict(telechat_date=telechat_date.isoformat(),returning_item="on")) + self.assertEquals(r.status_code,302) + doc = Document.objects.get(name='status-change-imaginary-mid-review') + self.assertTrue(doc.returning_item()) + + # Take the doc back off any telechat + r = self.client.post(url,dict(telechat_date="")) + self.assertEquals(r.status_code, 302) + doc = Document.objects.get(name='status-change-imaginary-mid-review') + self.assertEquals(doc.latest_event(TelechatDocEvent, "scheduled_for_telechat").telechat_date,None) + + def test_approve(self): + doc = Document.objects.get(name='status-change-imaginary-mid-review') + url = urlreverse('status_change_approve',kwargs=dict(name=doc.name)) + + login_testing_unauthorized(self, "secretary", url) + + # Some 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') + create_ballot_if_not_open(doc,Person.objects.get(name="Sec Retary"),"statchg") + doc.set_state(State.objects.get(slug='appr-pend',type='statchg')) + doc.save() + + # get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertEquals(len(q('form.approve')),1) + # There should be two messages to edit + self.assertEquals(q('input#id_form-TOTAL_FORMS').val(),'2') + self.assertTrue( '(rfc9999) to Internet Standard' in ''.join(wrap(r.content,2**16))) + self.assertTrue( '(rfc9998) to Historic' in ''.join(wrap(r.content,2**16))) + + # submit + messages_before = len(outbox) + msg0=default_approval_text(doc,doc.relateddocument_set.all()[0]) + msg1=default_approval_text(doc,doc.relateddocument_set.all()[1]) + r = self.client.post(url,{'form-0-announcement_text':msg0,'form-1-announcement_text':msg1,'form-TOTAL_FORMS':'2','form-INITIAL_FORMS':'2','form-MAX_NUM_FORMS':''}) + self.assertEquals(r.status_code, 302) + + doc = Document.objects.get(name='status-change-imaginary-mid-review') + self.assertEquals(doc.get_state_slug(),'appr-sent') + self.assertFalse(doc.ballot_open("statchg")) + + self.assertEquals(len(outbox), messages_before + 2) + self.assertTrue('Action:' in outbox[-1]['Subject']) + self.assertTrue('(rfc9999) to Internet Standard' in ''.join(wrap(unicode(outbox[-1])+unicode(outbox[-2]),2**16))) + self.assertTrue('(rfc9998) to Historic' in ''.join(wrap(unicode(outbox[-1])+unicode(outbox[-2]),2**16))) + + self.assertTrue(doc.latest_event(DocEvent,type="added_comment").desc.startswith('The following approval message was sent')) + + def test_edit_relations(self): + doc = Document.objects.get(name='status-change-imaginary-mid-review') + url = urlreverse('status_change_relations',kwargs=dict(name=doc.name)) + + login_testing_unauthorized(self, "secretary", url) + + # Some 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') + + # get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertEquals(len(q('form.edit-status-change-rfcs')),1) + # There should be three rows on the form + self.assertEquals(len(q('tr[id^=relation_row]')),3) + + # 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")) + 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="")) + 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")) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue(len(q('form ul.errorlist')) > 0) + + # Successful change of relations + 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")) + self.assertEquals(r.status_code, 302) + doc = Document.objects.get(name='status-change-imaginary-mid-review') + self.assertEquals(doc.relateddocument_set.count(),2) + verify9999 = doc.relateddocument_set.filter(target__name='rfc9999') + self.assertTrue(verify9999) + self.assertEquals(verify9999.count(),1) + self.assertEquals(verify9999[0].relationship.slug,'toexp') + verify9998 = doc.relateddocument_set.filter(target__name='rfc9998') + self.assertTrue(verify9998) + self.assertEquals(verify9998.count(),1) + self.assertEquals(verify9998[0].relationship.slug,'tobcp') + self.assertTrue(doc.latest_event(DocEvent,type="added_comment").desc.startswith('Affected RFC list changed.')) + + def setUp(self): + make_test_data() + + +class StatusChangeSubmitTestCase(django.test.TestCase): + + fixtures = ['names'] + + def test_initial_submission(self): + doc = Document.objects.get(name='status-change-imaginary-mid-review') + url = urlreverse('status_change_submit',kwargs=dict(name=doc.name)) + login_testing_unauthorized(self, "ad", url) + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code,200) + q = PyQuery(r.content) + self.assertTrue(q('textarea')[0].text.startswith("Provide a description")) + + # Faulty posts using textbox + # Right now, nothing to test - we let people put whatever the web browser will let them put into that textbox + + # sane post using textbox + path = os.path.join(settings.STATUS_CHANGE_PATH, '%s-%s.txt' % (doc.canonical_name(), doc.rev)) + self.assertEquals(doc.rev,u'00') + self.assertFalse(os.path.exists(path)) + r = self.client.post(url,dict(content="Some initial review text\n",submit_response="1")) + self.assertEquals(r.status_code,302) + doc = Document.objects.get(name='status-change-imaginary-mid-review') + self.assertEquals(doc.rev,u'00') + with open(path) as f: + self.assertEquals(f.read(),"Some initial review text\n") + f.close() + self.assertTrue( "mid-review-00" in doc.latest_event(NewRevisionDocEvent).desc) + + def test_subsequent_submission(self): + doc = Document.objects.get(name='status-change-imaginary-mid-review') + url = urlreverse('status_change_submit',kwargs=dict(name=doc.name)) + login_testing_unauthorized(self, "ad", url) + + # A little additional setup + # doc.rev is u'00' per the test setup - double-checking that here - if it fails, the breakage is in setUp + self.assertEquals(doc.rev,u'00') + path = os.path.join(settings.STATUS_CHANGE_PATH, '%s-%s.txt' % (doc.canonical_name(), doc.rev)) + with open(path,'w') as f: + f.write('This is the old proposal.') + f.close() + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code,200) + q = PyQuery(r.content) + self.assertTrue(q('textarea')[0].text.startswith("This is the old proposal.")) + + # faulty posts trying to use file upload + # Copied from wgtracker tests - is this really testing the server code, or is it testing + # how client.post populates Content-Type? + test_file = StringIO("\x10\x11\x12") # post binary file + test_file.name = "unnamed" + r = self.client.post(url, dict(txt=test_file,submit_response="1")) + self.assertEquals(r.status_code, 200) + self.assertTrue("does not appear to be a text file" in r.content) + + # sane post uploading a file + test_file = StringIO("This is a new proposal.") + test_file.name = "unnamed" + r = self.client.post(url,dict(txt=test_file,submit_response="1")) + self.assertEquals(r.status_code, 302) + doc = Document.objects.get(name='status-change-imaginary-mid-review') + self.assertEquals(doc.rev,u'01') + path = os.path.join(settings.STATUS_CHANGE_PATH, '%s-%s.txt' % (doc.canonical_name(), doc.rev)) + with open(path) as f: + self.assertEquals(f.read(),"This is a new proposal.") + f.close() + self.assertTrue( "mid-review-01" in doc.latest_event(NewRevisionDocEvent).desc) + + # verify reset text button works + r = self.client.post(url,dict(reset_text="1")) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue(q('textarea')[0].text.startswith("Provide a description")) + + def setUp(self): + make_test_data() + self.test_dir = os.path.abspath("tmp-status-change-testdir") + os.mkdir(self.test_dir) + settings.STATUS_CHANGE_PATH = self.test_dir + + def tearDown(self): + shutil.rmtree(self.test_dir) diff --git a/ietf/doc/urls_status_change.py b/ietf/doc/urls_status_change.py new file mode 100644 index 000000000..91bbea9bb --- /dev/null +++ b/ietf/doc/urls_status_change.py @@ -0,0 +1,12 @@ +from django.conf.urls.defaults import patterns, url + +urlpatterns = patterns('ietf.doc.views_status_change', + url(r'^state/$', "change_state", name='status_change_change_state'), + url(r'^submit/$', "submit", name='status_change_submit'), + url(r'^notices/$', "edit_notices", name='status_change_notices'), + url(r'^ad/$', "edit_ad", name='status_change_ad'), + 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'), +) + diff --git a/ietf/doc/views_conflict_review.py b/ietf/doc/views_conflict_review.py index 34f99a540..f5d42fb52 100644 --- a/ietf/doc/views_conflict_review.py +++ b/ietf/doc/views_conflict_review.py @@ -25,20 +25,15 @@ from ietf.person.models import Person from ietf.iesg.models import TelechatDate from ietf.group.models import Role, Group +from ietf.doc.forms import TelechatForm, AdForm, NotifyForm + class ChangeStateForm(forms.Form): review_state = forms.ModelChoiceField(State.objects.filter(type="conflrev", used=True), label="Conflict review state", empty_label=None, required=True) comment = forms.CharField(widget=forms.Textarea, help_text="Optional comment for the review history", required=False) - def __init__(self, *args, **kwargs): - self.hide = kwargs.pop('hide', None) - super(ChangeStateForm, self).__init__(*args, **kwargs) - # hide requested fields - if self.hide: - for f in self.hide: - self.fields[f].widget = forms.HiddenInput @role_required("Area Director", "Secretariat") def change_state(request, name, option=None): - """Change state of and IESG review for IETF conflicts in other stream's documents, notifying parties as necessary + """Change state of an IESG review for IETF conflicts in other stream's documents, notifying parties as necessary and logging the change as a comment.""" review = get_object_or_404(Document, type="conflrev", name=name) @@ -86,22 +81,22 @@ def change_state(request, name, option=None): return redirect('doc_view', name=review.name) else: - hide = [] s = review.get_state() init = dict(review_state=s.pk if s else None) - form = ChangeStateForm(hide=hide, initial=init) + form = ChangeStateForm(initial=init) - return render_to_response('doc/conflict_review/change_state.html', + return render_to_response('doc/change_state.html', dict(form=form, doc=review, login=login, + help_url=reverse('help_conflict_review_states'), ), context_instance=RequestContext(request)) def send_conflict_eval_email(request,review): - msg = render_to_string("doc/conflict_review/eval_email.txt", - dict(review=review, - review_url = settings.IDTRACKER_BASE_URL+review.get_absolute_url(), + msg = render_to_string("doc/eval_email.txt", + dict(doc=review, + doc_url = settings.IDTRACKER_BASE_URL+review.get_absolute_url(), ) ) send_mail_preformatted(request,msg) @@ -202,9 +197,6 @@ def submit(request, name): }, context_instance=RequestContext(request)) -class NotifyForm(forms.Form): - notify = forms.CharField(max_length=255, label="Notice emails", help_text="Separate email addresses with commas", required=False) - @role_required("Area Director", "Secretariat") def edit_notices(request, name): @@ -231,28 +223,15 @@ def edit_notices(request, name): init = { "notify" : review.notify } form = NotifyForm(initial=init) - return render_to_response('doc/conflict_review/notify.html', - {'form': form, - 'review': review, - 'conflictdoc' : review.relateddocument_set.get(relationship__slug='conflrev').target.document, + conflictdoc = review.relateddocument_set.get(relationship__slug='conflrev').target.document + titletext = 'the conflict review of %s-%s' % (conflictdoc.canonical_name(),conflictdoc.rev) + return render_to_response('doc/notify.html', + {'form': form, + 'doc': review, + 'titletext' : titletext }, context_instance = RequestContext(request)) -class AdForm(forms.Form): - ad = forms.ModelChoiceField(Person.objects.filter(role__name="ad", role__group__state="active").order_by('name'), - label="Shepherding AD", empty_label="(None)", required=True) - - def __init__(self, *args, **kwargs): - super(self.__class__, self).__init__(*args, **kwargs) - - # if previous AD is now ex-AD, append that person to the list - ad_pk = self.initial.get('ad') - choices = self.fields['ad'].choices - if ad_pk and ad_pk not in [pk for pk, name in choices]: - self.fields['ad'].choices = list(choices) + [("", "-------"), (ad_pk, Person.objects.get(pk=ad_pk).plain_name())] - - - @role_required("Area Director", "Secretariat") def edit_ad(request, name): """Change the shepherding Area Director for this review.""" @@ -277,10 +256,13 @@ def edit_ad(request, name): init = { "ad" : review.ad_id } form = AdForm(initial=init) - return render_to_response('doc/conflict_review/change_ad.html', - {'form': form, - 'review': review, - 'conflictdoc' : review.relateddocument_set.get(relationship__slug='conflrev').target.document, + + conflictdoc = review.relateddocument_set.get(relationship__slug='conflrev').target.document + titletext = 'the conflict review of %s-%s' % (conflictdoc.canonical_name(),conflictdoc.rev) + return render_to_response('doc/change_ad.html', + {'form': form, + 'doc': review, + 'titletext': titletext }, context_instance = RequestContext(request)) @@ -318,7 +300,7 @@ def approve(request, name): review = get_object_or_404(Document, type="conflrev", name=name) if review.get_state('conflrev').slug not in ('appr-reqnopub-pend','appr-noprob-pend'): - return Http404() + raise Http404 login = request.user.get_profile() @@ -465,22 +447,6 @@ def start_review(request, name): context_instance = RequestContext(request)) -# There should really only be one of these living in Doc instead of it being spread between idrfc,charter, and here -class TelechatForm(forms.Form): - telechat_date = forms.TypedChoiceField(coerce=lambda x: datetime.datetime.strptime(x, '%Y-%m-%d').date(), empty_value=None, required=False) - returning_item = forms.BooleanField(required=False) - - def __init__(self, *args, **kwargs): - super(self.__class__, self).__init__(*args, **kwargs) - - dates = [d.date for d in TelechatDate.objects.active().order_by('date')] - init = kwargs['initial'].get("telechat_date") - if init and init not in dates: - dates.insert(0, init) - - self.fields['telechat_date'].choices = [("", "(not on agenda)")] + [(d, d.strftime("%Y-%m-%d")) for d in dates] - - @role_required("Area Director", "Secretariat") def telechat_date(request, name): doc = get_object_or_404(Document, type="conflrev", name=name) @@ -501,7 +467,7 @@ def telechat_date(request, name): else: form = TelechatForm(initial=initial) - return render_to_response('doc/conflict_review/edit_telechat_date.html', + return render_to_response('doc/edit_telechat_date.html', dict(doc=doc, form=form, user=request.user, diff --git a/ietf/doc/views_status_change.py b/ietf/doc/views_status_change.py new file mode 100644 index 000000000..ad053dfba --- /dev/null +++ b/ietf/doc/views_status_change.py @@ -0,0 +1,596 @@ +import datetime, os, re + +from django import forms +from django.shortcuts import render_to_response, get_object_or_404, redirect +from django.http import HttpResponseRedirect, Http404 +from django.core.urlresolvers import reverse +from django.template import RequestContext +from django.template.loader import render_to_string +from django.conf import settings + +from ietf.idrfc.utils import update_telechat +from ietf.doc.utils import log_state_changed +from ietf.doc.models import save_document_in_history + +from ietf.doc.utils import create_ballot_if_not_open, close_open_ballots, get_document_content +from ietf.ietfauth.decorators import has_role, role_required +from ietf.utils.textupload import get_cleaned_text_file_content +from ietf.utils.mail import send_mail_preformatted +from ietf.doc.models import State, Document, DocHistory, DocAlias +from ietf.doc.models import DocEvent, NewRevisionDocEvent, WriteupDocEvent, TelechatDocEvent, BallotDocEvent, BallotPositionDocEvent +from ietf.person.models import Person +from ietf.iesg.models import TelechatDate +from ietf.group.models import Group +from ietf.name.models import DocRelationshipName, StdLevelName + +from ietf.doc.forms import TelechatForm, AdForm, NotifyForm + +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) + + +@role_required("Area Director", "Secretariat") +def change_state(request, name, option=None): + """Change state of an status-change document, notifying parties as necessary + and logging the change as a comment.""" + status_change = get_object_or_404(Document, type="statchg", name=name) + + login = request.user.get_profile() + + if request.method == 'POST': + form = ChangeStateForm(request.POST) + if form.is_valid(): + clean = form.cleaned_data + new_state = clean['new_state'] + comment = clean['comment'].rstrip() + + if comment: + c = DocEvent(type="added_comment", doc=status_change, by=login) + c.desc = comment + c.save() + + if new_state != status_change.get_state(): + save_document_in_history(status_change) + + old_description = status_change.friendly_state() + status_change.set_state(new_state) + new_description = status_change.friendly_state() + + log_state_changed(request, status_change, login, new_description, old_description) + + status_change.time = datetime.datetime.now() + status_change.save() + + if new_state.slug == "iesgeval": + create_ballot_if_not_open(status_change, login, "statchg") + ballot = status_change.latest_event(BallotDocEvent, type="created_ballot") + if has_role(request.user, "Area Director") and not status_change.latest_event(BallotPositionDocEvent, ad=login, ballot=ballot, type="changed_ballot_position"): + + # The AD putting a status change into iesgeval who doesn't already have a position is saying "yes" + pos = BallotPositionDocEvent(doc=status_change, by=login) + pos.ballot = ballot + pos.type = "changed_ballot_position" + pos.ad = login + pos.pos_id = "yes" + pos.desc = "[Ballot Position Update] New position, %s, has been recorded for %s" % (pos.pos.name, pos.ad.plain_name()) + pos.save() + + send_status_change_eval_email(request,status_change) + + + return redirect('doc_view', name=status_change.name) + else: + s = status_change.get_state() + init = dict(new_state=s.pk if s else None, + type='statchg', + label='Status Change Evaluation State', + ) + form = ChangeStateForm(initial=init) + + return render_to_response('doc/change_state.html', + dict(form=form, + doc=status_change, + login=login, + help_url=reverse('help_status_change_states') + ), + context_instance=RequestContext(request)) + +def send_status_change_eval_email(request,doc): + msg = render_to_string("doc/eval_email.txt", + dict(doc=doc, + doc_url = settings.IDTRACKER_BASE_URL+doc.get_absolute_url(), + ) + ) + send_mail_preformatted(request,msg) + +class UploadForm(forms.Form): + content = forms.CharField(widget=forms.Textarea, label="Status change text", help_text="Edit the status change text", required=False) + txt = forms.FileField(label=".txt format", help_text="Or upload a .txt file", required=False) + + def clean_content(self): + return self.cleaned_data["content"].replace("\r", "") + + def clean_txt(self): + return get_cleaned_text_file_content(self.cleaned_data["txt"]) + + def save(self, doc): + filename = os.path.join(settings.STATUS_CHANGE_PATH, '%s-%s.txt' % (doc.canonical_name(), doc.rev)) + with open(filename, 'wb') as destination: + if self.cleaned_data['txt']: + destination.write(self.cleaned_data['txt']) + else: + destination.write(self.cleaned_data['content']) + +#This is very close to submit on charter - can we get better reuse? +@role_required('Area Director','Secretariat') +def submit(request, name): + doc = get_object_or_404(Document, type="statchg", name=name) + + login = request.user.get_profile() + + path = os.path.join(settings.STATUS_CHANGE_PATH, '%s-%s.txt' % (doc.canonical_name(), doc.rev)) + not_uploaded_yet = doc.rev == "00" and not os.path.exists(path) + + if not_uploaded_yet: + # this case is special - the status change text document doesn't actually exist yet + next_rev = doc.rev + else: + next_rev = "%02d" % (int(doc.rev)+1) + + if request.method == 'POST': + if "submit_response" in request.POST: + form = UploadForm(request.POST, request.FILES) + if form.is_valid(): + save_document_in_history(doc) + + doc.rev = next_rev + + e = NewRevisionDocEvent(doc=doc, by=login, type="new_revision") + e.desc = "New version available: %s-%s.txt" % (doc.canonical_name(), doc.rev) + e.rev = doc.rev + e.save() + + # Save file on disk + form.save(doc) + + doc.time = datetime.datetime.now() + doc.save() + + return HttpResponseRedirect(reverse('doc_view', kwargs={'name': doc.name})) + + elif "reset_text" in request.POST: + + init = { "content": render_to_string("doc/status_change/initial_template.txt",dict())} + form = UploadForm(initial=init) + + # Protect against handcrufted malicious posts + else: + form = None + + else: + form = None + + if not form: + init = { "content": ""} + + if not_uploaded_yet: + init["content"] = render_to_string("doc/status_change/initial_template.txt", + dict(), + ) + else: + filename = os.path.join(settings.STATUS_CHANGE_PATH, '%s-%s.txt' % (doc.canonical_name(), doc.rev)) + try: + with open(filename, 'r') as f: + init["content"] = f.read() + except IOError: + pass + + form = UploadForm(initial=init) + + return render_to_response('doc/status_change/submit.html', + {'form': form, + 'next_rev': next_rev, + 'doc' : doc, + }, + context_instance=RequestContext(request)) + +@role_required("Area Director", "Secretariat") +def edit_notices(request, name): + """Change the set of email addresses document change notificaitions go to.""" + + status_change = get_object_or_404(Document, type="statchg", name=name) + + if request.method == 'POST': + form = NotifyForm(request.POST) + if form.is_valid(): + + status_change.notify = form.cleaned_data['notify'] + status_change.save() + + login = request.user.get_profile() + c = DocEvent(type="added_comment", doc=status_change, by=login) + c.desc = "Notification list changed to : "+status_change.notify + c.save() + + return HttpResponseRedirect(reverse('doc_view', kwargs={'name': status_change.name})) + + else: + + init = { "notify" : status_change.notify } + form = NotifyForm(initial=init) + + return render_to_response('doc/notify.html', + {'form': form, + 'doc': status_change, + 'titletext' : '%s-%s.txt' % (status_change.canonical_name(),status_change.rev) + }, + context_instance = RequestContext(request)) + +@role_required("Area Director", "Secretariat") +def edit_ad(request, name): + """Change the shepherding Area Director for this status_change.""" + + status_change = get_object_or_404(Document, type="statchg", name=name) + + if request.method == 'POST': + form = AdForm(request.POST) + if form.is_valid(): + + status_change.ad = form.cleaned_data['ad'] + status_change.save() + + login = request.user.get_profile() + c = DocEvent(type="added_comment", doc=status_change, by=login) + c.desc = "Shepherding AD changed to "+status_change.ad.name + c.save() + + return redirect("doc_view", name=status_change.name) + + else: + init = { "ad" : status_change.ad_id } + form = AdForm(initial=init) + + titletext = '%s-%s.txt' % (status_change.canonical_name(),status_change.rev) + return render_to_response('doc/change_ad.html', + {'form': form, + 'doc': status_change, + 'titletext' : titletext, + }, + context_instance = RequestContext(request)) + +def newstatus(relateddoc): + + level_map = { + 'tops' : 'ps', + 'tois' : 'std', + 'tohist' : 'hist', + 'toinf' : 'inf', + 'tobcp' : 'bcp', + 'toexp' : 'exp', + } + + return StdLevelName.objects.get(slug=level_map[relateddoc.relationship.slug]) + +def default_approval_text(status_change,relateddoc): + + filename = "%s-%s.txt" % (status_change.canonical_name(), status_change.rev) + current_text = get_document_content(filename, os.path.join(settings.STATUS_CHANGE_PATH, filename), split=False, markup=False) + + if relateddoc.target.document.std_level.slug in ('std','ps','ds','bcp',): + action = "Protocol Action" + else: + action = "Document Action" + + + text = render_to_string("doc/status_change/approval_text.txt", + dict(status_change=status_change, + status_change_url = settings.IDTRACKER_BASE_URL+status_change.get_absolute_url(), + relateddoc= relateddoc, + relateddoc_url = settings.IDTRACKER_BASE_URL+relateddoc.target.document.get_absolute_url(), + approved_text = current_text, + action=action, + newstatus=newstatus(relateddoc), + ) + ) + + return text + +from django.forms.formsets import formset_factory + +class AnnouncementForm(forms.Form): + announcement_text = forms.CharField(widget=forms.Textarea, label="Status Change Announcement", help_text="Edit the announcement message", required=True) + label = None + + def __init__(self, *args, **kwargs): + super(self.__class__, self).__init__(*args, **kwargs) + self.label = self.initial.get('label') + +@role_required("Secretariat") +def approve(request, name): + """Approve this status change, setting the appropriate state and send the announcements to the right parties.""" + status_change = get_object_or_404(Document, type="statchg", name=name) + + if status_change.get_state('statchg').slug not in ('appr-pend'): + raise Http404 + + login = request.user.get_profile() + + AnnouncementFormSet = formset_factory(AnnouncementForm,extra=0) + + if request.method == 'POST': + + formset = AnnouncementFormSet(request.POST) + + if formset.is_valid(): + + save_document_in_history(status_change) + + old_description = status_change.friendly_state() + status_change.set_state(State.objects.get(type='statchg', slug='appr-sent')) + new_description = status_change.friendly_state() + log_state_changed(request, status_change, login, new_description, old_description) + + close_open_ballots(status_change, login) + + e = DocEvent(doc=status_change, by=login) + e.type = "iesg_approved" + e.desc = "IESG has approved the status change" + e.save() + + status_change.time = e.time + status_change.save() + + + for form in formset.forms: + + send_mail_preformatted(request,form.cleaned_data['announcement_text']) + + c = DocEvent(type="added_comment", doc=status_change, by=login) + c.desc = "The following approval message was sent\n"+form.cleaned_data['announcement_text'] + c.save() + + for rel in status_change.relateddocument_set.filter(relationship__slug__in=RELATION_SLUGS): + # Add a document event to each target + c = DocEvent(type="added_comment", doc=rel.target.document, by=login) + c.desc = "New status of %s approved by the IESG\n%s%s" % (newstatus(rel), settings.IDTRACKER_BASE_URL,reverse('doc_view', kwargs={'name': status_change.name})) + c.save() + + return HttpResponseRedirect(status_change.get_absolute_url()) + + else: + + init = [] + for rel in status_change.relateddocument_set.filter(relationship__slug__in=RELATION_SLUGS): + init.append({"announcement_text" : default_approval_text(status_change,rel), + "label": "Announcement text for %s to %s"%(rel.target.document.canonical_name(),newstatus(rel)), + }) + formset = AnnouncementFormSet(initial=init) + for form in formset.forms: + form.fields['announcement_text'].label=form.label + + return render_to_response('doc/status_change/approve.html', + dict( + doc = status_change, + formset = formset, + ), + context_instance=RequestContext(request)) + +RELATION_SLUGS = ('tops','tois','tohist','toinf','tobcp','toexp') + +def clean_helper(form, formtype): + cleaned_data = super(formtype, form).clean() + + new_relations = {} + rfc_fields = {} + status_fields={} + for k in sorted(form.data.iterkeys()): + v = form.data[k] + if k.startswith('new_relation_row'): + if re.match('\d{4}',v): + v = 'rfc'+v + rfc_fields[k[17:]]=v + elif k.startswith('statchg_relation_row'): + status_fields[k[21:]]=v + for key in rfc_fields: + if rfc_fields[key]!="": + new_relations[rfc_fields[key]]=status_fields[key] + + form.relations = new_relations + + errors=[] + for key in new_relations: + + if not re.match('(?i)rfc\d{4}',key): + errors.append(key+" is not a valid RFC - please use the form RFCxxxx\n") + elif not DocAlias.objects.filter(name=key): + errors.append(key+" does not exist\n") + + if new_relations[key] not in RELATION_SLUGS: + errors.append("Please choose a new status level for "+key+"\n") + + if errors: + raise forms.ValidationError(errors) + + cleaned_data['relations']=new_relations + + return cleaned_data + +class EditStatusChangeForm(forms.Form): + relations={} + + def __init__(self, *args, **kwargs): + super(self.__class__, self).__init__(*args, **kwargs) + self.relations = self.initial.get('relations') + + def clean(self): + return clean_helper(self,EditStatusChangeForm) + +class StartStatusChangeForm(forms.Form): + document_name = forms.CharField(max_length=255, label="Document name", help_text="A descriptive name such as status-change-md2-to-historic is better than status-change-rfc1319", required=True) + title = forms.CharField(max_length=255, label="Title", required=True) + ad = forms.ModelChoiceField(Person.objects.filter(role__name="ad", role__group__state="active").order_by('name'), + label="Shepherding AD", empty_label="(None)", required=True) + create_in_state = forms.ModelChoiceField(State.objects.filter(type="statchg", slug__in=("needshep", "adrev")), empty_label=None, required=False) + notify = forms.CharField(max_length=255, label="Notice emails", help_text="Separate email addresses with commas", required=False) + telechat_date = forms.TypedChoiceField(coerce=lambda x: datetime.datetime.strptime(x, '%Y-%m-%d').date(), empty_value=None, required=False, widget=forms.Select(attrs={'onchange':'make_bold()'})) + relations={} + + def __init__(self, *args, **kwargs): + super(self.__class__, self).__init__(*args, **kwargs) + + # telechat choices + dates = [d.date for d in TelechatDate.objects.active().order_by('date')] + self.fields['telechat_date'].choices = [("", "(not on agenda)")] + [(d, d.strftime("%Y-%m-%d")) for d in dates] + + def clean_document_name(self): + name = self.cleaned_data['document_name'] + if Document.objects.filter(name='status-change-%s'%name): + raise forms.ValidationError("status-change-%s already exists"%name) + return name + + def clean(self): + return clean_helper(self,StartStatusChangeForm) + +#TODO - cleaned data, especially on document_name + +def rfc_status_changes(request): + """Show the rfc status changes that are under consideration, and those that are completed.""" + + docs=Document.objects.filter(type__slug='statchg') + doclist=[x for x in docs] + doclist.sort(key=lambda obj: obj.get_state().order) + return render_to_response('doc/status_change/status_changes.html', + {'docs' : doclist, + }, + context_instance = RequestContext(request)) + +@role_required("Area Director","Secretariat") +def start_rfc_status_change(request): + """Start the RFC status change review process, setting the initial shepherding AD, and possibly putting the review on a telechat.""" + + login = request.user.get_profile() + + relation_slugs = DocRelationshipName.objects.filter(slug__in=RELATION_SLUGS) + + if request.method == 'POST': + form = StartStatusChangeForm(request.POST) + if form.is_valid(): + + iesg_group = Group.objects.get(acronym='iesg') + + status_change=Document( type_id = "statchg", + name = 'status-change-'+form.cleaned_data['document_name'], + title = form.cleaned_data['title'], + rev = "00", + ad = form.cleaned_data['ad'], + notify = form.cleaned_data['notify'], + stream_id = 'ietf', + group = iesg_group, + ) + status_change.set_state(form.cleaned_data['create_in_state']) + + status_change.save() + + DocAlias.objects.create( name= 'status-change-'+form.cleaned_data['document_name'], document=status_change ) + + for key in form.cleaned_data['relations']: + status_change.relateddocument_set.create(target=DocAlias.objects.get(name=key), + relationship_id=form.cleaned_data['relations'][key]) + + + tc_date = form.cleaned_data['telechat_date'] + if tc_date: + update_telechat(request, status_change, login, tc_date) + + return HttpResponseRedirect(status_change.get_absolute_url()) + else: + init = { + } + form = StartStatusChangeForm(initial=init) + + return render_to_response('doc/status_change/start.html', + {'form': form, + 'relation_slugs': relation_slugs, + }, + context_instance = RequestContext(request)) + + +@role_required("Area Director", "Secretariat") +def telechat_date(request, name): + doc = get_object_or_404(Document, type="statchg", name=name) + login = request.user.get_profile() + + e = doc.latest_event(TelechatDocEvent, type="scheduled_for_telechat") + initial_returning_item = bool(e and e.returning_item) + + initial = dict(telechat_date=e.telechat_date if e else None, + returning_item = initial_returning_item, + ) + if request.method == "POST": + form = TelechatForm(request.POST, initial=initial) + + if form.is_valid(): + update_telechat(request, doc, login, form.cleaned_data['telechat_date'], form.cleaned_data['returning_item']) + return redirect("doc_view", name=doc.name) + else: + form = TelechatForm(initial=initial) + + return render_to_response('doc/edit_telechat_date.html', + dict(doc=doc, + form=form, + user=request.user, + login=login), + context_instance=RequestContext(request)) + +@role_required("Area Director", "Secretariat") +def edit_relations(request, name): + """Change the affected set of RFCs""" + + status_change = get_object_or_404(Document, type="statchg", name=name) + + login = request.user.get_profile() + + relation_slugs = DocRelationshipName.objects.filter(slug__in=RELATION_SLUGS) + + if request.method == 'POST': + form = EditStatusChangeForm(request.POST) + if 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 + new_relations=form.cleaned_data['relations'] + status_change.relateddocument_set.filter(relationship__slug__in=RELATION_SLUGS).delete() + for key in new_relations: + status_change.relateddocument_set.create(target=DocAlias.objects.get(name=key), + relationship_id=new_relations[key]) + c = DocEvent(type="added_comment", doc=status_change, by=login) + c.desc = "Affected RFC list changed.\nOLD:" + for relname,relslug in (set(old_relations.items())-set(new_relations.items())): + c.desc += "\n "+relname+": "+DocRelationshipName.objects.get(slug=relslug).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()) + + else: + relations={} + for rel in status_change.relateddocument_set.filter(relationship__slug__in=RELATION_SLUGS): + relations[rel.target.document.canonical_name()]=rel.relationship.slug + init = { "relations":relations, + } + form = EditStatusChangeForm(initial=init) + + return render_to_response('doc/status_change/edit_relations.html', + { + 'doc': status_change, + 'form': form, + 'relation_slugs': relation_slugs, + }, + context_instance = RequestContext(request)) diff --git a/ietf/idrfc/urls.py b/ietf/idrfc/urls.py index 2a0477be8..4e2e83b04 100644 --- a/ietf/idrfc/urls.py +++ b/ietf/idrfc/urls.py @@ -33,6 +33,7 @@ from django.conf.urls.defaults import patterns, url, include from ietf.idrfc import views_doc, views_search, views_edit, views_ballot, views from ietf.doc.models import State +from ietf.doc import views_status_change urlpatterns = patterns('', (r'^/?$', views_search.search_main), @@ -40,6 +41,8 @@ urlpatterns = patterns('', (r'^all/$', views_search.all), (r'^active/$', views_search.active), (r'^in-last-call/$', views_search.in_last_call), + url(r'^rfc-status-changes/$', views_status_change.rfc_status_changes, name='rfc_status_changes'), + url(r'^start-rfc-status-change/$', views_status_change.start_rfc_status_change, name='start_rfc_status_change'), url(r'^ad/(?P[A-Za-z0-9.-]+)/$', views_search.by_ad, name="doc_search_by_ad"), url(r'^(?P[A-Za-z0-9._+-]+)/((?P[0-9-]+)/)?$', views_doc.document_main, name="doc_view"), @@ -81,11 +84,13 @@ urlpatterns = patterns('', (r'^(?Pcharter-[A-Za-z0-9._+-]+)/', include('ietf.wgcharter.urls')), (r'^(?P[A-Za-z0-9._+-]+)/conflict-review/', include('ietf.doc.urls_conflict_review')), + (r'^(?P[A-Za-z0-9._+-]+)/status-change/', include('ietf.doc.urls_status_change')), ) urlpatterns += patterns('django.views.generic.simple', url(r'^help/state/charter/$', 'direct_to_template', { 'template': 'doc/states.html', 'extra_context': { 'states': State.objects.filter(type="charter"),'title':"Charter" } }, name='help_charter_states'), url(r'^help/state/conflict-review/$', 'direct_to_template', { 'template': 'doc/states.html', 'extra_context': { 'states': State.objects.filter(type="conflrev").order_by("order"),'title':"Conflict Review" } }, name='help_conflict_review_states'), + url(r'^help/state/status-change/$', 'direct_to_template', { 'template': 'doc/states.html', 'extra_context': { 'states': State.objects.filter(type="statchg").order_by("order"),'title':"RFC Status Change" } }, name='help_status_change_states'), ) diff --git a/ietf/idrfc/views_doc.py b/ietf/idrfc/views_doc.py index 7fc7372e7..6f5a76541 100644 --- a/ietf/idrfc/views_doc.py +++ b/ietf/idrfc/views_doc.py @@ -60,14 +60,14 @@ def render_document_top(request, doc, tab, name): tabs.append(("Document", "document", urlreverse("ietf.idrfc.views_doc.document_main", kwargs=dict(name=name)), True)) ballot = doc.latest_event(BallotDocEvent, type="created_ballot") - if doc.type_id in ("draft","conflrev"): + if doc.type_id in ("draft","conflrev","statchg"): # if doc.in_ietf_process and doc.ietf_process.has_iesg_ballot: tabs.append(("IESG Evaluation Record", "ballot", urlreverse("ietf.idrfc.views_doc.document_ballot", kwargs=dict(name=name)), ballot)) elif doc.type_id == "charter": tabs.append(("IESG Review", "ballot", urlreverse("ietf.idrfc.views_doc.document_ballot", kwargs=dict(name=name)), ballot)) # FIXME: if doc.in_ietf_process and doc.ietf_process.has_iesg_ballot: - if doc.type_id != "conflrev": + if doc.type_id not in ("conflrev","statchg"): tabs.append(("IESG Writeups", "writeup", urlreverse("ietf.idrfc.views_doc.document_writeup", kwargs=dict(name=doc.name)), True)) tabs.append(("History", "history", urlreverse("ietf.idrfc.views_doc.document_history", kwargs=dict(name=doc.name)), True)) @@ -190,6 +190,33 @@ def document_main(request, name, rev=None): ), context_instance=RequestContext(request)) + if doc.type_id == "statchg": + filename = "%s-%s.txt" % (doc.canonical_name(), doc.rev) + pathname = os.path.join(settings.STATUS_CHANGE_PATH,filename) + + if doc.rev == "00" and not os.path.isfile(pathname): + # This could move to a template + content = "Status change text has not yet been proposed." + else: + content = _get_html(filename, pathname, split=False) + + ballot_summary = None + if doc.get_state_slug() in ("iesgeval"): + ballot_summary = needed_ballot_positions(doc, doc.active_ballot().active_ad_positions().values()) + + return render_to_response("idrfc/document_status_change.html", + dict(doc=doc, + top=top, + content=content, + revisions=revisions, + snapshot=snapshot, + telechat=telechat, + ballot_summary=ballot_summary, + approved_states=('appr-pend','appr-sent'), + sorted_relations=doc.relateddocument_set.all().order_by('relationship__name'), + ), + context_instance=RequestContext(request)) + raise Http404() diff --git a/ietf/name/admin.py b/ietf/name/admin.py index 60b1b952a..7e09d4dab 100644 --- a/ietf/name/admin.py +++ b/ietf/name/admin.py @@ -4,12 +4,15 @@ from models import * class NameAdmin(admin.ModelAdmin): list_display = ["slug", "name", "desc", "used"] prepopulate_from = { "slug": ("name",) } + +class DocRelationshipNameAdmin(NameAdmin): + list_display = ["slug", "name", "revname", "desc", "used"] admin.site.register(GroupTypeName, NameAdmin) admin.site.register(GroupStateName, NameAdmin) admin.site.register(RoleName, NameAdmin) admin.site.register(StreamName, NameAdmin) -admin.site.register(DocRelationshipName, NameAdmin) +admin.site.register(DocRelationshipName, DocRelationshipNameAdmin) admin.site.register(DocTypeName, NameAdmin) admin.site.register(DocTagName, NameAdmin) admin.site.register(StdLevelName, NameAdmin) diff --git a/ietf/name/fixtures/names.xml b/ietf/name/fixtures/names.xml index a979ab819..9c3a0206a 100644 --- a/ietf/name/fixtures/names.xml +++ b/ietf/name/fixtures/names.xml @@ -72,24 +72,70 @@ True 0 + Obsoleted by Updates True 0 + Updated by Replaces True 0 + Replaced by conflict reviews True 0 + Conflict reviewed by + + + Moves to Proposed Standard + + True + 0 + Moved to Proposed Standard by + + + Moves to Internet Standard + + True + 0 + Moved to Internet Standard by + + + Moves to Historic + + True + 0 + Moved to Historic by + + + Moves to Informational + + True + 0 + Moved to Informational by + + + Moves to BCP + + True + 0 + Moved to BCP by + + + Moves to Experimental + + True + 0 + Moved to Experimental by Stream state should change @@ -134,7 +180,7 @@ 0 - Approved in minute + Approved in minutes True 0 @@ -313,14 +359,14 @@ True 0 - - Maturity Change + + Conflict Review True 0 - - Conflict Review + + Status Change True 0 @@ -361,6 +407,12 @@ True 0 + + Abandonded + Formation of the group (most likely a BoF or Proposed WG) was abandoned + True + 0 + IETF @@ -415,6 +467,12 @@ True 0 + + RFC Editor + + True + 0 + Proposed Standard @@ -559,6 +617,12 @@ True 0 + + At Large Member + + True + 0 + Waiting for Scheduling @@ -760,6 +824,9 @@ Conflict Review State + + RFC Status Change + agenda active @@ -832,95 +899,113 @@ 0 - + conflrev needshep Needs Shepherd True A conflict review has been requested, but a shepherding AD has not yet been assigned 1 - + - + conflrev adrev AD Review True The sponsoring AD is reviewing the document and preparing a proposed response 2 - + - + conflrev iesgeval IESG Evaluation True The IESG is considering the proposed conflict review response 3 - + - + conflrev defer IESG Evaluation - Defer True The evaluation of the proposed conflict review response has been deferred to the next telechat 4 - + - + + conflrev + appr-reqnopub-pr + Approved Request to Not Publish - point raised + True + The IESG has approved the conflict review response (a request to not publish), but a point has been raised that should be cleared before moving to announcement to be sent + 5 + + + + conflrev + appr-noprob-pr + Approved No Problem - point raised + True + The IESG has approved the conflict review response, but a point has been raised that should be cleared before proceeding to announcement to be sent + 6 + + + conflrev appr-reqnopub-pend Approved Request to Not Publish - announcement to be sent True The IESG has approved the conflict review response (a request to not publish), but the secretariat has not yet sent the response - 5 - + 7 + - + conflrev appr-noprob-pend Approved No Problem - announcement to be sent True The IESG has approved the conflict review response, but the secretariat has not yet sent the response - 6 - + 8 + - + conflrev appr-reqnopub-sent Approved Request to Not Publish - announcement sent True The secretariat has delivered the IESG's approved conflict review response (a request to not publish) to the requester - 7 - + 9 + - + conflrev appr-noprob-sent Approved No Problem - announcement sent True The secretariat has delivered the IESG's approved conflict review response to the requester - 8 - + 10 + - + conflrev withdraw Withdrawn True The request for conflict review was withdrawn - 9 - + 11 + - + conflrev dead Dead True The conflict review has been abandoned - 10 - + 12 + draft @@ -1363,6 +1448,15 @@ 0 + + draft-rfceditor + auth48done + AUTH48-DONE + True + Final approvals are complete + 0 + + draft-stream-iab candidat @@ -1843,6 +1937,87 @@ 2 + + statchg + needshep + Needs Shepherd + True + An RFC status change has been requested, but a shepherding AD has not yet been assigned + 1 + + + + statchg + adrev + AD Review + True + The sponsoring AD is preparing an RFC status change document + 2 + + + + statchg + iesgeval + IESG Evaluation + True + The IESG is considering the proposed RFC status changes + 3 + + + + statchg + defer + IESG Evaluation - Defer + True + The evaluation of the proposed RFC status changes have been deferred to the next telechat + 4 + + + + statchg + appr-pr + 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 + + + + statchg + appr-pend + Approved - announcement to be sent + True + The IESG has approved the RFC status changes, but the secretariat has not yet sent the announcement + 6 + + + + statchg + appr-sent + Approved - announcement sent + True + The secretariat has announced the IESG's approved RFC status changes + 7 + + + + 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 + + conflrev conflrev @@ -1852,6 +2027,15 @@ 0 + + statchg + statchg + Approve + Do we approve these RFC status changes? + True + 0 + + charter r-extrev diff --git a/ietf/name/migrations/0005_add_newstat.py b/ietf/name/migrations/0005_add_newstat.py new file mode 100644 index 000000000..aabf93f67 --- /dev/null +++ b/ietf/name/migrations/0005_add_newstat.py @@ -0,0 +1,157 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +from ietf.name.models import DocTypeName, DocRelationshipName + +class Migration(DataMigration): + + def forwards(self, orm): + DocTypeName(slug='statchg',name='Status Change',used=True).save() + + def backwards(self, orm): + pass + + models = { + '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.constraintname': { + 'Meta': {'ordering': "['order']", 'object_name': 'ConstraintName'}, + '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'}), + '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.groupballotpositionname': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupBallotPositionName'}, + '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.liaisonstatementpurposename': { + 'Meta': {'ordering': "['order']", 'object_name': 'LiaisonStatementPurposeName'}, + '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.meetingtypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'MeetingTypeName'}, + '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.rolename': { + 'Meta': {'ordering': "['order']", 'object_name': 'RoleName'}, + '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.sessionstatusname': { + 'Meta': {'ordering': "['order']", 'object_name': 'SessionStatusName'}, + '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'}) + }, + 'name.timeslottypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'TimeSlotTypeName'}, + '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'}) + } + } + + complete_apps = ['name'] diff --git a/ietf/name/migrations/0006_add_revname_column.py b/ietf/name/migrations/0006_add_revname_column.py new file mode 100644 index 000000000..98a832082 --- /dev/null +++ b/ietf/name/migrations/0006_add_revname_column.py @@ -0,0 +1,161 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding field 'DocRelationshipName.revname' + db.add_column('name_docrelationshipname', 'revname', self.gf('django.db.models.fields.CharField')(default='fixme', max_length=255), keep_default=False) + + def backwards(self, orm): + + # Deleting field 'DocRelationshipName.revname' + db.delete_column('name_docrelationshipname', 'revname') + + + models = { + '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'}), + '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.constraintname': { + 'Meta': {'ordering': "['order']", 'object_name': 'ConstraintName'}, + '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'}), + '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.groupballotpositionname': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupBallotPositionName'}, + '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.liaisonstatementpurposename': { + 'Meta': {'ordering': "['order']", 'object_name': 'LiaisonStatementPurposeName'}, + '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.meetingtypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'MeetingTypeName'}, + '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.rolename': { + 'Meta': {'ordering': "['order']", 'object_name': 'RoleName'}, + '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.sessionstatusname': { + 'Meta': {'ordering': "['order']", 'object_name': 'SessionStatusName'}, + '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'}) + }, + 'name.timeslottypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'TimeSlotTypeName'}, + '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'}) + } + } + + complete_apps = ['name'] diff --git a/ietf/name/migrations/0007_reverse_relation_names.py b/ietf/name/migrations/0007_reverse_relation_names.py new file mode 100644 index 000000000..d81e734a2 --- /dev/null +++ b/ietf/name/migrations/0007_reverse_relation_names.py @@ -0,0 +1,177 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +from ietf.name.models import DocRelationshipName + +class Migration(DataMigration): + + def update_reverse_name(self,slug,revname): + relation = DocRelationshipName.objects.get(slug=slug) + relation.revname = revname + relation.save() + + def forwards(self, orm): + + revnames = { 'obs' : 'Obsoleted by', + 'updates' : 'Updated by', + 'replaces': 'Replaced by', + 'conflrev': 'Conflict reviewed by', + } + for key in revnames: + self.update_reverse_name(key,revnames[key]) + + DocRelationshipName(slug='tops', name='Moves to Proposed Standard', revname='Moved to Proposed Standard by', used=True).save() + DocRelationshipName(slug='tois', name='Moves to Internet Standard', revname='Moved to Internet Standard by', used=True).save() + DocRelationshipName(slug='tohist', name='Moves to Historic', revname='Moved to Historic by', used=True).save() + DocRelationshipName(slug='toinf', name='Moves to Informational', revname='Moved to Informational by', used=True).save() + DocRelationshipName(slug='tobcp', name='Moves to BCP', revname='Moved to BCP by', used=True).save() + DocRelationshipName(slug='toexp', name='Moves to Experimental', revname='Moved to Experimental by', used=True).save() + + def backwards(self, orm): + pass + + models = { + '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.constraintname': { + 'Meta': {'ordering': "['order']", 'object_name': 'ConstraintName'}, + '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.groupballotpositionname': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupBallotPositionName'}, + '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.liaisonstatementpurposename': { + 'Meta': {'ordering': "['order']", 'object_name': 'LiaisonStatementPurposeName'}, + '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.meetingtypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'MeetingTypeName'}, + '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.rolename': { + 'Meta': {'ordering': "['order']", 'object_name': 'RoleName'}, + '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.sessionstatusname': { + 'Meta': {'ordering': "['order']", 'object_name': 'SessionStatusName'}, + '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'}) + }, + 'name.timeslottypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'TimeSlotTypeName'}, + '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'}) + } + } + + complete_apps = ['name'] diff --git a/ietf/name/models.py b/ietf/name/models.py index 9568ed078..e3d1e8fce 100644 --- a/ietf/name/models.py +++ b/ietf/name/models.py @@ -24,9 +24,12 @@ class RoleName(NameModel): """AD, Chair""" class StreamName(NameModel): """IETF, IAB, IRTF, ISE, Legacy""" + class DocRelationshipName(NameModel): """Updates, Replaces, Obsoletes, Reviews, ... The relationship is always recorded in one direction.""" + revname = models.CharField(max_length=255) + class DocTypeName(NameModel): """Draft, Agenda, Minutes, Charter, Discuss, Guideline, Email, Review, Issue, Wiki""" diff --git a/ietf/settings.py b/ietf/settings.py index 2ffde5d0c..89c6447e1 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -206,6 +206,8 @@ CHARTER_PATH = '/a/www/ietf-ftp/charters/' CHARTER_TXT_URL = 'http://www.ietf.org/charter/' CONFLICT_REVIEW_PATH = '/a/www/ietf-ftp/conflict-reviews' CONFLICT_REVIEW_TXT_URL = 'http://www.ietf.org/cr/' +STATUS_CHANGE_PATH = '/a/www/ietf-ftp/status-changes' +STATUS_CHANGE_TXT_URL = 'http://www.ietf.org/sc/' AGENDA_PATH = '/a/www/www6s/proceedings/' AGENDA_PATH_PATTERN = '/a/www/www6s/proceedings/%(meeting)s/agenda/%(wg)s.%(ext)s' MINUTES_PATH_PATTERN = '/a/www/www6s/proceedings/%(meeting)s/minutes/%(wg)s.%(ext)s' diff --git a/ietf/templates/base_leftmenu.html b/ietf/templates/base_leftmenu.html index cba8d1370..fce7514fd 100644 --- a/ietf/templates/base_leftmenu.html +++ b/ietf/templates/base_leftmenu.html @@ -102,6 +102,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. {% else %}
  • Sign in to track drafts
  • {% endif %} +{% if user|in_group:"Area_Director,Secretariat" %} +
  • RFC status changes
  • +{% endif %}
  • Meetings
  • diff --git a/ietf/templates/doc/conflict_review/change_ad.html b/ietf/templates/doc/change_ad.html similarity index 69% rename from ietf/templates/doc/conflict_review/change_ad.html rename to ietf/templates/doc/change_ad.html index 233b01f01..8fab6800f 100644 --- a/ietf/templates/doc/conflict_review/change_ad.html +++ b/ietf/templates/doc/change_ad.html @@ -8,11 +8,11 @@ {% endblock %} {% block title %} -Change the shepherding AD for the conflict review of {{ conflictdoc.canonical_name }}-{{ conflictdoc.rev }} +Change the shepherding AD for {{titletext}} {% endblock %} {% block content %} -

    Change the shepherding AD for the conflict review of {{ conflictdoc.canonical_name }}-{{ conflictdoc.rev }}

    +

    Change the shepherding AD for {{titletext}}

    @@ -29,7 +29,7 @@ Change the shepherding AD for the conflict review of {{ conflictdoc.canonical_na diff --git a/ietf/templates/doc/conflict_review/change_state.html b/ietf/templates/doc/change_state.html similarity index 87% rename from ietf/templates/doc/conflict_review/change_state.html rename to ietf/templates/doc/change_state.html index a6633e371..424699d7a 100644 --- a/ietf/templates/doc/conflict_review/change_state.html +++ b/ietf/templates/doc/change_state.html @@ -20,7 +20,7 @@ form.change-state .actions { {% block content %}

    Change State: {{doc.title}}

    -

    For help on the states, see the state table.

    +

    For help on the states, see the state table.

    - Back + Back
    diff --git a/ietf/templates/doc/conflict_review/edit_telechat_date.html b/ietf/templates/doc/edit_telechat_date.html similarity index 100% rename from ietf/templates/doc/conflict_review/edit_telechat_date.html rename to ietf/templates/doc/edit_telechat_date.html diff --git a/ietf/templates/doc/conflict_review/eval_email.txt b/ietf/templates/doc/eval_email.txt similarity index 67% rename from ietf/templates/doc/conflict_review/eval_email.txt rename to ietf/templates/doc/eval_email.txt index c0009bc3f..9c15e9638 100644 --- a/ietf/templates/doc/conflict_review/eval_email.txt +++ b/ietf/templates/doc/eval_email.txt @@ -1,8 +1,8 @@ {% load mail_filters %}{% autoescape off %}To: Internet Engineering Steering Group From: IESG Secretary Reply-To: IESG Secretary -Subject: Evaluation: {{review.title}} +Subject: Evaluation: {{doc.title}} -Evaluation for {{ review.title }} can be found at <{{ review_url }}> +Evaluation for {{ doc.title }} can be found at <{{ doc_url }}> {% endautoescape%} diff --git a/ietf/templates/doc/conflict_review/notify.html b/ietf/templates/doc/notify.html similarity index 70% rename from ietf/templates/doc/conflict_review/notify.html rename to ietf/templates/doc/notify.html index 14aad867c..0dc7d8d59 100644 --- a/ietf/templates/doc/conflict_review/notify.html +++ b/ietf/templates/doc/notify.html @@ -11,11 +11,11 @@ form.edit-info #id_notify { {% endblock %} {% block title %} -Edit notification addresses for the conflict review of {{ conflictdoc.canonical_name }}-{{ conflictdoc.rev }} +Edit notification addresses for {{titletext}} {% endblock %} {% block content %} -

    Edit notification addresses for the conflict review of {{ conflictdoc.canonical_name }}-{{ conflictdoc.rev }}

    +

    Edit notification addresses for {{titletext}}

    @@ -32,7 +32,7 @@ Edit notification addresses for the conflict review of {{ conflictdoc.canonical_ diff --git a/ietf/templates/doc/status_change/approval_text.txt b/ietf/templates/doc/status_change/approval_text.txt new file mode 100644 index 000000000..25310852e --- /dev/null +++ b/ietf/templates/doc/status_change/approval_text.txt @@ -0,0 +1,25 @@ +{% load mail_filters %}{% autoescape off %}From: The IESG +To: IETF-Announce +Cc: RFC Editor , {{status_change.notify}} +Subject: {{action}}: {{relateddoc.target.document.title}} to {{newstatus}} + +{% filter wordwrap:73 %}The IESG has approved changing the status of the following document: +- {{relateddoc.target.document.title }} + ({{relateddoc.target.document.canonical_name }}) to {{ newstatus }} + +This {{action|lower}} is documented at: +{{status_change_url}} + +A URL of the affected document is: +{{relateddoc_url}} + +Status Change Details: + +{{ approved_text }} + +Personnel + + {{status_change.ad.plain_name}} is the responsible Area Director. + +{% endfilter %} +{% endautoescape %} diff --git a/ietf/templates/doc/status_change/approve.html b/ietf/templates/doc/status_change/approve.html new file mode 100644 index 000000000..5c4e97ead --- /dev/null +++ b/ietf/templates/doc/status_change/approve.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} + +{% block title %}Approve {{doc.canonical_name }}{% endblock %} + +{% block morecss %} +textarea[id^="id_form-"][id$="-announcement_text"] { + overflow-x: auto; + overflow-y: scroll; + width: 800px; + height: 400px; + border: 1px solid #bbb; +} +{% endblock %} + +{% block content %} +

    Approve {{ doc.canonical_name }}

    + + + {{formset.management_form}} +
    - Back + Back
    + {% for form in formset.forms %} + {% for field in form.visible_fields %} + + + + {% endfor %} + {% endfor %} + + + +
    +
    {{ field.label_tag }}:
    + {{ field }} + {% if field.help_text %}
    {{ field.help_text }}
    {% endif %} + {{ field.errors }} +
    + Back + +
    +
    +{% endblock %} diff --git a/ietf/templates/doc/status_change/edit_relations.html b/ietf/templates/doc/status_change/edit_relations.html new file mode 100644 index 000000000..f83d3e648 --- /dev/null +++ b/ietf/templates/doc/status_change/edit_relations.html @@ -0,0 +1,73 @@ +{% extends "base.html" %} + +{% block title %}Edit List of RFCs Affected By Status Change{% endblock %} + +{% block morecss %} +form.start-rfc-status-change-review #id_notify { + width: 600px; +} +form.start-rfc-status-change-review #id_document_name { + width: 510px; +} +form.start-rfc-status-change-review .actions { + padding-top: 20px; +} +.warning { + font-weight: bold; + color: #a00; +} +{% endblock %} + +{% block pagehead %} +{% include "doc/status_change/status-change-edit-relations-js.html" %} +{% endblock %} + +{% block content %} + +

    Edit List of RFCs Affected By Status Change

    + +
    + + + + + + + +
    Affects RFCs: + {% for rfc,choice_slug in form.relations.items %} + + + + {% endfor %} + + + +
    + + + + +
    + +
    +
    Enter one of the affected RFC as RFCXXXX
    + {{ form.non_field_errors }} +
    + +
    +
    +{% endblock %} diff --git a/ietf/templates/doc/status_change/initial_template.txt b/ietf/templates/doc/status_change/initial_template.txt new file mode 100644 index 000000000..a66d68f8a --- /dev/null +++ b/ietf/templates/doc/status_change/initial_template.txt @@ -0,0 +1,17 @@ +Provide a description of what RFCs status are changed and any necessary rational for the change. + +This is a good place to document how the RFC6410 criteria for advancing to Internet Standard are met: + + (1) There are at least two independent interoperating implementations + with widespread deployment and successful operational experience. + + (2) There are no errata against the specification that would cause a + new implementation to fail to interoperate with deployed ones. + + (3) There are no unused features in the specification that greatly + increase implementation complexity. + + (4) If the technology required to implement the specification + requires patented or otherwise controlled technology, then the + set of implementations must demonstrate at least two independent, + separate and successful uses of the licensing process. diff --git a/ietf/templates/doc/status_change/start.html b/ietf/templates/doc/status_change/start.html new file mode 100644 index 000000000..07fb2f6f3 --- /dev/null +++ b/ietf/templates/doc/status_change/start.html @@ -0,0 +1,89 @@ +{% extends "base.html" %} + +{% block title %}Begin RFC status change review {% endblock %} + +{% block morecss %} +form.start-rfc-status-change-review #id_notify { + width: 600px; +} +form.start-rfc-status-change-review #id_document_name { + width: 510px; +} +form.start-rfc-status-change-review .actions { + padding-top: 20px; +} +.warning { + font-weight: bold; + color: #a00; +} +{% endblock %} + +{% block pagehead %} +{% include "doc/status_change/status-change-edit-relations-js.html" %} +{% endblock %} + +{% block content %} +

    Begin RFC status change review

    + +

    For help on the initial state choice, see the state table.

    + +
    + + {% for field in form.visible_fields %} + + + + + {% if field.label == "Document name" %} + + + + + {% endif %} + {% endfor %} + + + +
    {{ field.label_tag }}: + {% if field.label == "Document name" %}status-change-{% endif %} + {{ field }} + {% if field.help_text %}
    {{ field.help_text }}
    {% endif %} + + {{ field.errors }} +
    Affects RFCs: + {% for rfc,choice_slug in form.relations.items %} + + + + {% endfor %} + + + +
    + + + + +
    + +
    +
    Enter one of the affected RFC as RFCXXXX
    + {{ form.non_field_errors }} +
    + +
    +
    +{% endblock %} diff --git a/ietf/templates/doc/status_change/status-change-edit-relations-js.html b/ietf/templates/doc/status_change/status-change-edit-relations-js.html new file mode 100644 index 000000000..99b08e31b --- /dev/null +++ b/ietf/templates/doc/status_change/status-change-edit-relations-js.html @@ -0,0 +1,78 @@ + diff --git a/ietf/templates/doc/status_change/status_changes.html b/ietf/templates/doc/status_change/status_changes.html new file mode 100644 index 000000000..8106fecb0 --- /dev/null +++ b/ietf/templates/doc/status_change/status_changes.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} +{% load ietf_filters %} +{% block title %}RFC Status Changes{% endblock %} +{% block content %} +

    RFC Status Changes

    + +{% if user|in_group:"Area_Director,Secretariat" %} +

    Start new RFC status change document

    +{% endif %} +{% regroup docs by get_state as state_groups %} + + +{% for state in state_groups %} + +{% for doc in state.list %} + + + + +{% endfor %} +{% endfor %} +{% endblock content %} + diff --git a/ietf/templates/doc/status_change/submit.html b/ietf/templates/doc/status_change/submit.html new file mode 100644 index 000000000..f2e77b128 --- /dev/null +++ b/ietf/templates/doc/status_change/submit.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} + +{% block morecss %} +form #id_content { + width: 40em; + height: 450px; +} +{% endblock %} + +{% block title %} +Edit status change text for {{doc.title}} +{% endblock %} + +{% block content %} +

    Edit status change text for {{doc.title}}

    + +

    The text will be submitted as {{ doc.canonical_name }}-{{ next_rev }}

    + +
    DocumentTitle
    {{state.grouper}}
    {{ doc.displayname_with_link|safe }}{{ doc.title }}
    + {% for field in form.visible_fields %} + + + + + {% endfor %} + + + + +
    {{ field.label_tag }}: + {{ field }} + {% if field.help_text %}
    {{ field.help_text }}
    {% endif %} + {{ field.errors }} +
    + Back + + +
    + + +{% endblock %} diff --git a/ietf/templates/idrfc/document_conflict_review.html b/ietf/templates/idrfc/document_conflict_review.html index 0cb182033..41d437f6c 100644 --- a/ietf/templates/idrfc/document_conflict_review.html +++ b/ietf/templates/idrfc/document_conflict_review.html @@ -42,25 +42,26 @@ {% endif %} - - {% if not snapshot %} - - {% if ballot_summary %}
    ({{ ballot_summary }})
    {% endif %} - {% endif %} + {% endif %} Shepherding AD: diff --git a/ietf/templates/idrfc/document_status_change.html b/ietf/templates/idrfc/document_status_change.html new file mode 100644 index 000000000..c45f7ae25 --- /dev/null +++ b/ietf/templates/idrfc/document_status_change.html @@ -0,0 +1,135 @@ +{% extends "idrfc/doc_main.html" %} + +{% load ietf_filters %} + +{% block title %}{{ doc.canonical_name }}-{{ doc.rev }}{% endblock %} + +{% block pagehead %} + +{% endblock %} + +{% block content %} +{{ top|safe }} + +
    + Versions: + + {% for rev in revisions %} + {{ rev }} + {% endfor %} + +
    + +
    +
    + {% if snapshot %}Snapshot of{% endif %} + {% if doc.get_state_slug not in approved_states %}Proposed{% endif %} + Status change : {{ doc.title }} +
    + + + {% regroup sorted_relations by relationship.name as relation_groups %} + {% for relation_group in relation_groups %} + + + + + {% endfor %} + + + + + + + + + + + + + + + + + + + + + + + + + {% if not snapshot and user|has_role:"Area Director,Secretariat" and doc.get_state_slug not in approved_states %} + + {% endif %} + + {% comment %} + + {% endcomment %} + +
    {{relation_group.grouper}}:{% for rel in relation_group.list %}{{rel.target.document.canonical_name|upper|urlize_ietf_docs}}{% if not forloop.last %}, {% endif %}{% endfor %}
    Review State: +
    + {{ doc.get_state.name }} + + {% if not snapshot and user|has_role:"Area Director,Secretariat" %} + + {% if request.user|has_role:"Secretariat" %}{% if doc.get_state_slug = 'appr-pend' %} + - Approve RFC status changes + {% endif %}{% endif %} + + {% endif %} +
    +
    Telechat Date: + {% if not snapshot %} + + {% if not telechat %}Not on agenda of an IESG telechat{% else %}On agenda of {{ telechat.telechat_date|date:"Y-m-d" }} IESG telechat{% if doc.returning_item %} (returning item){% endif %}{% endif %} + + + {% if ballot_summary %} +
    + ({{ ballot_summary }}) +
    + {% endif %} + + {% endif %} +
    Shepherding AD: + + {{doc.ad}} + +
    Send notices to: + + {{doc.notify}} + +

    Last updated: {{ doc.time|date:"Y-m-d" }}
    + + + Edit Affected RFC List + + +

    + +
    + +

    RFC Status Change : {{ doc.title }} + +{% if not snapshot and user|has_role:"Area Director,Secretariat" and doc.get_state_slug != 'apprsent' %} +Change status change text +{% endif %} +

    + +{% if doc.rev %} +
    +{{ content|fill:"80"|safe|linebreaksbr|keep_spacing|sanitize_html|safe }} +
    +{% endif %} + +{% endblock %} + diff --git a/ietf/templates/registration/edit_profile.html b/ietf/templates/registration/edit_profile.html index d28efb9eb..e14f60566 100644 --- a/ietf/templates/registration/edit_profile.html +++ b/ietf/templates/registration/edit_profile.html @@ -126,7 +126,7 @@ th { {{person_form.ascii_short.label}}: - {{person_form.ascii_short}} - Short form, if any, of your name as renedered in ASCII (blank is okay) + {{person_form.ascii_short}} - Short form, if any, of your name as rendered in ASCII (blank is okay) {{person_form.affiliation.label}}: diff --git a/ietf/utils/test_data.py b/ietf/utils/test_data.py index 3a8e61bdf..336677090 100644 --- a/ietf/utils/test_data.py +++ b/ietf/utils/test_data.py @@ -400,4 +400,22 @@ def make_test_data(): crdoc.save() 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") + 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) + + # Some things for a status change to affect + target_rfc = Document.objects.create(name='draft-ietf-random-thing', type_id='draft', std_level_id='ps') + target_rfc.set_state(State.objects.get(slug='rfc',type__slug='draft')) + target_rfc.save() + docalias = DocAlias.objects.create(name='draft-ietf-random-thing',document=target_rfc) + docalias = DocAlias.objects.create(name='rfc9999',document=target_rfc) + target_rfc = Document.objects.create(name='draft-ietf-random-otherthing', type_id='draft', std_level_id='inf') + target_rfc.set_state(State.objects.get(slug='rfc',type__slug='draft')) + target_rfc.save() + docalias = DocAlias.objects.create(name='draft-ietf-random-otherthing',document=target_rfc) + docalias = DocAlias.objects.create(name='rfc9998',document=target_rfc) + return draft