From 07490fcb838985268c16c34e0a313efdd023c18d Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Mon, 17 Sep 2012 10:00:56 +0000 Subject: [PATCH 01/47] Add new IANA states, add consensus event, add a state change event, add migrations for these that also clean up the RFC Editor states a little given the recent changes - Legacy-Id: 4846 --- ...dd_statedocevent__add_consensusdocevent.py | 377 +++++++++++++++++ .../0005_add_iana_rfc_editor_states.py | 387 ++++++++++++++++++ ietf/doc/models.py | 24 +- ietf/doc/utils.py | 24 +- 4 files changed, 804 insertions(+), 8 deletions(-) create mode 100644 ietf/doc/migrations/0004_auto__add_statedocevent__add_consensusdocevent.py create mode 100644 ietf/doc/migrations/0005_add_iana_rfc_editor_states.py diff --git a/ietf/doc/migrations/0004_auto__add_statedocevent__add_consensusdocevent.py b/ietf/doc/migrations/0004_auto__add_statedocevent__add_consensusdocevent.py new file mode 100644 index 000000000..c6d1981cf --- /dev/null +++ b/ietf/doc/migrations/0004_auto__add_statedocevent__add_consensusdocevent.py @@ -0,0 +1,377 @@ +# 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 model 'StateDocEvent' + db.create_table('doc_statedocevent', ( + ('docevent_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['doc.DocEvent'], unique=True, primary_key=True)), + ('state_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['doc.StateType'])), + ('state', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['doc.State'], null=True, blank=True)), + )) + db.send_create_signal('doc', ['StateDocEvent']) + + # Adding model 'ConsensusDocEvent' + db.create_table('doc_consensusdocevent', ( + ('docevent_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['doc.DocEvent'], unique=True, primary_key=True)), + ('consensus', self.gf('django.db.models.fields.BooleanField')(default=False)), + )) + db.send_create_signal('doc', ['ConsensusDocEvent']) + + + def backwards(self, orm): + + # Deleting model 'StateDocEvent' + db.delete_table('doc_statedocevent') + + # Deleting model 'ConsensusDocEvent' + db.delete_table('doc_consensusdocevent') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'doc.ballotdocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'BallotDocEvent', '_ormbases': ['doc.DocEvent']}, + 'ballot_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.BallotType']"}), + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'doc.ballotpositiondocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'BallotPositionDocEvent', '_ormbases': ['doc.DocEvent']}, + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}), + 'ballot': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['doc.BallotDocEvent']", 'null': 'True'}), + 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'comment_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'discuss': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'discuss_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'pos': ('django.db.models.fields.related.ForeignKey', [], {'default': "'norecord'", 'to': "orm['name.BallotPositionName']"}) + }, + 'doc.ballottype': { + 'Meta': {'ordering': "['order']", 'object_name': 'BallotType'}, + 'doc_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.DocTypeName']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'positions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['name.BallotPositionName']", 'symmetrical': 'False', 'blank': 'True'}), + 'question': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'doc.consensusdocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'ConsensusDocEvent', '_ormbases': ['doc.DocEvent']}, + 'consensus': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'doc.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.statedocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'StateDocEvent', '_ormbases': ['doc.DocEvent']}, + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.State']", 'null': 'True', 'blank': 'True'}), + 'state_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.StateType']"}) + }, + 'doc.statetype': { + 'Meta': {'object_name': 'StateType'}, + 'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '30', 'primary_key': 'True'}) + }, + 'doc.telechatdocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'TelechatDocEvent', '_ormbases': ['doc.DocEvent']}, + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'returning_item': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'telechat_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}) + }, + 'doc.writeupdocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'WriteupDocEvent', '_ormbases': ['doc.DocEvent']}, + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {'blank': 'True'}) + }, + 'group.group': { + 'Meta': {'object_name': 'Group'}, + 'acronym': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}), + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']", 'null': 'True', 'blank': 'True'}), + 'charter': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'chartered_group'", 'unique': 'True', 'null': 'True', 'to': "orm['doc.Document']"}), + 'comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'list_archive': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'list_email': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'list_subscribe': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']", 'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.GroupStateName']", 'null': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.GroupTypeName']", 'null': 'True'}), + 'unused_states': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['doc.State']", 'symmetrical': 'False', 'blank': 'True'}), + 'unused_tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['name.DocTagName']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'name.ballotpositionname': { + 'Meta': {'ordering': "['order']", 'object_name': 'BallotPositionName'}, + 'blocking': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.docrelationshipname': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocRelationshipName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + '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/migrations/0005_add_iana_rfc_editor_states.py b/ietf/doc/migrations/0005_add_iana_rfc_editor_states.py new file mode 100644 index 000000000..cf4d992be --- /dev/null +++ b/ietf/doc/migrations/0005_add_iana_rfc_editor_states.py @@ -0,0 +1,387 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + t = orm.StateType.objects.get_or_create(slug="draft-iana-action", label="IANA Action state")[0] + orm.State.objects.get_or_create(type=t, slug='newdoc', name='New Document', desc="A new document has been received by IANA, but no actions have been taken") + orm.State.objects.get_or_create(type=t, slug='inprog', name='In Progress', desc="IANA is currently processing the actions for this document") + orm.State.objects.get_or_create(type=t, slug='waitauth', name='Waiting on Authors', desc="IANA is waiting on the document's authors to respond") + orm.State.objects.get_or_create(type=t, slug='waitad', name='Waiting on ADs', desc="IANA is waiting on the IETF Area Directors to respond") + orm.State.objects.get_or_create(type=t, slug='waitwgc', name='Waiting on WGC', desc="IANA is waiting on the IETF Working Group Chairs to respond") + orm.State.objects.get_or_create(type=t, slug='waitrfc', name='Waiting on RFC Editor', desc="IANA has notified the RFC Editor that the actions have been completed") + orm.State.objects.get_or_create(type=t, slug='rfcedack', name='RFC-Ed-Ack', desc="Request completed. The RFC Editor has acknowledged receipt of IANA's message that the actions have been completed") + orm.State.objects.get_or_create(type=t, slug='onhold', name='On Hold', desc="IANA has suspended work on the document") + orm.State.objects.get_or_create(type=t, slug='noic', name='No IC', desc="Request completed. There were no IANA actions for this document") + + t = orm.StateType.objects.get_or_create(slug="draft-iana-review", label="IANA Review state")[0] + orm.State.objects.get_or_create(type=t, slug="need-rev", name='IANA Review Needed', desc="Document has not yet been reviewed by IANA.", order=1) + orm.State.objects.get_or_create(type=t, slug="ok-act", name='IANA OK - Actions Needed', desc="Document requires IANA actions, and the IANA Considerations section indicates the details of the actions correctly.", order=2) + orm.State.objects.get_or_create(type=t, slug="ok-noact", name='IANA OK - No Actions Needed', desc="Document requires no IANA action, and the IANA Considerations section indicates this correctly.", order=3) + orm.State.objects.get_or_create(type=t, slug="not-ok", name='IANA Not OK', desc="IANA has issues with the text of the IANA Considerations section of the document.", order=4) + orm.State.objects.get_or_create(type=t, slug="changed", name='Version Changed - Review Needed', desc="Document revision has changed after review by IANA.", order=5) + + # fixup RFC Editor states/tags + orm.State.objects.filter(type="draft-rfceditor", slug="edit").update(desc="Awaiting editing or being edited") + orm.State.objects.filter(type="draft-rfceditor", slug="iesg").update(desc="Awaiting IESG action") + orm.State.objects.filter(type="draft-rfceditor", slug="isr-auth").update(desc="Independent submission awaiting author action, or in discussion between author and ISE") + orm.State.objects.filter(type="draft-rfceditor", slug="iana-crd").update(slug="iana", desc="Document has been edited, but is holding for completion of IANA actions") + orm.State.objects.get_or_create(type_id="draft-rfceditor", slug="auth48-done", defaults=dict(name="AUTH48-DONE", desc="Final approvals are complete")) + + orm["name.DocTagName"].objects.get_or_create(slug="iana", name="IANA", desc="The document has IANA actions that are not yet completed.") + for d in orm.Document.objects.filter(type="draft", tags="iana-crd"): + d.tags.remove("iana-crd") + d.tags.add("iana") + + orm["name.DocTagName"].objects.filter(slug="iana-crd").delete() + + def backwards(self, orm): + "Write your backwards methods here." + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'doc.ballotdocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'BallotDocEvent', '_ormbases': ['doc.DocEvent']}, + 'ballot_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.BallotType']"}), + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'doc.ballotpositiondocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'BallotPositionDocEvent', '_ormbases': ['doc.DocEvent']}, + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}), + 'ballot': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['doc.BallotDocEvent']", 'null': 'True'}), + 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'comment_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'discuss': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'discuss_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'pos': ('django.db.models.fields.related.ForeignKey', [], {'default': "'norecord'", 'to': "orm['name.BallotPositionName']"}) + }, + 'doc.ballottype': { + 'Meta': {'ordering': "['order']", 'object_name': 'BallotType'}, + 'doc_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.DocTypeName']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'positions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['name.BallotPositionName']", 'symmetrical': 'False', 'blank': 'True'}), + 'question': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'doc.consensusdocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'ConsensusDocEvent', '_ormbases': ['doc.DocEvent']}, + 'consensus': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'doc.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.statedocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'StateDocEvent', '_ormbases': ['doc.DocEvent']}, + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.State']", 'null': 'True', 'blank': 'True'}), + 'state_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.StateType']"}) + }, + 'doc.statetype': { + 'Meta': {'object_name': 'StateType'}, + 'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '30', 'primary_key': 'True'}) + }, + 'doc.telechatdocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'TelechatDocEvent', '_ormbases': ['doc.DocEvent']}, + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'returning_item': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'telechat_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}) + }, + 'doc.writeupdocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'WriteupDocEvent', '_ormbases': ['doc.DocEvent']}, + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {'blank': 'True'}) + }, + 'group.group': { + 'Meta': {'object_name': 'Group'}, + 'acronym': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}), + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']", 'null': 'True', 'blank': 'True'}), + 'charter': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'chartered_group'", 'unique': 'True', 'null': 'True', 'to': "orm['doc.Document']"}), + 'comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'list_archive': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'list_email': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'list_subscribe': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']", 'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.GroupStateName']", 'null': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.GroupTypeName']", 'null': 'True'}), + 'unused_states': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['doc.State']", 'symmetrical': 'False', 'blank': 'True'}), + 'unused_tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['name.DocTagName']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'name.ballotpositionname': { + 'Meta': {'ordering': "['order']", 'object_name': 'BallotPositionName'}, + 'blocking': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.docrelationshipname': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocRelationshipName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + '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 c42b54c3f..6af0dcbff 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -450,12 +450,15 @@ EVENT_TYPES = [ ("deleted", "Deleted document"), + ("changed_state", "Changed state"), + # misc draft/RFC events ("changed_stream", "Changed document stream"), ("expired_document", "Expired document"), ("extended_expiry", "Extended expiry of document"), ("requested_resurrect", "Requested resurrect"), ("completed_resurrect", "Completed resurrect"), + ("changed_consensus", "Changed consensus"), ("published_rfc", "Published RFC"), # WG events @@ -481,13 +484,21 @@ EVENT_TYPES = [ ("changed_last_call_text", "Changed last call text"), ("requested_last_call", "Requested last call"), ("sent_last_call", "Sent last call"), - + ("scheduled_for_telechat", "Scheduled for telechat"), ("iesg_approved", "IESG approved document (no problem)"), ("iesg_disapproved", "IESG disapproved document (do not publish)"), ("approved_in_minute", "Approved in minute"), + + # IANA events + ("iana_review", "IANA review comment"), + ("rfc_in_iana_registry", "RFC is in IANA registry"), + + # RFC Editor + ("rfc_editor_received_announcement", "Announcement was received by RFC Editor"), + ("requested_publication", "Publication at RFC Editor requested") ] class DocEvent(models.Model): @@ -506,7 +517,14 @@ class DocEvent(models.Model): class NewRevisionDocEvent(DocEvent): rev = models.CharField(max_length=16) - + +class StateDocEvent(DocEvent): + state_type = models.ForeignKey(StateType) + state = models.ForeignKey(State, blank=True, null=True) + +class ConsensusDocEvent(DocEvent): + consensus = models.BooleanField() + # IESG events class BallotType(models.Model): doc_type = models.ForeignKey(DocTypeName, blank=True, null=True) @@ -534,7 +552,7 @@ class BallotPositionDocEvent(DocEvent): discuss_time = models.DateTimeField(help_text="Time discuss text was written", blank=True, null=True) comment = models.TextField(help_text="Optional comment", blank=True) comment_time = models.DateTimeField(help_text="Time optional comment was written", blank=True, null=True) - + class WriteupDocEvent(DocEvent): text = models.TextField(blank=True) diff --git a/ietf/doc/utils.py b/ietf/doc/utils.py index 43a3fb78d..dd5abd95d 100644 --- a/ietf/doc/utils.py +++ b/ietf/doc/utils.py @@ -19,7 +19,8 @@ def get_state_types(doc): res.append("draft-stream-%s" % doc.stream_id) res.append("draft-iesg") - res.append("draft-iana") + res.append("draft-iana-review") + res.append("draft-iana-action") res.append("draft-rfceditor") return res @@ -137,8 +138,6 @@ def augment_with_start_time(docs): if e.doc_id in seen: continue - print e.time, e.doc_id - docs_dict[e.doc_id].start_time = e.time seen.add(e.doc_id) @@ -196,11 +195,26 @@ def get_document_content(key, filename, split=True, markup=True): return raw_content def log_state_changed(request, doc, by, new_description, old_description): - from ietf.doc.models import DocEvent - e = DocEvent(doc=doc, by=by) e.type = "changed_document" e.desc = u"State changed to %s from %s" % (new_description, old_description) e.save() return e +def add_state_change_event(doc, by, prev_state, new_state, timestamp=None): + """Add doc event to explain that state change just happened.""" + if prev_state == new_state: + return + + e = StateDocEvent(doc=doc, by=by) + e.type = "changed_state" + e.state_type = (prev_state or new_state).type + e.state = new_state + e.desc = "%s changed to %s" % (e.state_type.label, new_state.name) + if prev_state: + e.desc += " from %s" % prev_state.name + if timestamp: + e.time = timestamp + e.save() + return e + From 8cbdc0b03d387d080e8ac56171fa7a54e6fafa4f Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Mon, 17 Sep 2012 10:02:38 +0000 Subject: [PATCH 02/47] Augment expiry rules so that alternate stream documents aren't expired when they've been sent to the RFC Editor (but not yet published) - Legacy-Id: 4847 --- ietf/idrfc/expire.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/ietf/idrfc/expire.py b/ietf/idrfc/expire.py index cd5b732a8..1faf135c9 100644 --- a/ietf/idrfc/expire.py +++ b/ietf/idrfc/expire.py @@ -34,11 +34,19 @@ def in_id_expire_freeze(when=None): return second_cut_off <= when < ietf_monday def expirable_documents(): + # the general rule is that each active draft is expirable, unless + # it's in a state where we shouldn't touch it + d = Document.objects.filter(states__type="draft", states__slug="active").exclude(tags="rfc-rev") - # we need to get those that either don't have a state or have a - # state >= 42 (AD watching), unfortunately that doesn't appear to - # be possible to get to work directly in Django 1.1 - return itertools.chain(d.exclude(states__type="draft-iesg").distinct(), d.filter(states__type="draft-iesg", states__slug__in=("watching", "dead")).distinct()) + + nonexpirable_states = [] + # all IESG states except AD Watching and Dead block expiry + nonexpirable_states += list(State.objects.filter(type="draft-iesg").exclude(slug__in=("watching", "dead"))) + # Sent to RFC Editor and RFC Published block expiry (the latter + # shouldn't be possible for an active draft, though) + nonexpirable_states += list(State.objects.filter(type__in=("draft-stream-iab", "draft-stream-irtf", "draft-stream-ise"), slug__in=("rfc-edit", "pub"))) + + return d.exclude(states__in=nonexpirable_states).distinct() def get_soon_to_expire_ids(days): start_date = datetime.date.today() - datetime.timedelta(InternetDraft.DAYS_TO_EXPIRE - 1) From 09e6203f18be563821470c14329acffd3ed9ff33 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Mon, 17 Sep 2012 15:45:27 +0000 Subject: [PATCH 03/47] Revamp sync from RFC Editor to include more info, fix a couple of bugs and email the Secretariet when a draft sent to the RFC Editor ends up in the queue, split it up so it's easier to test; also moved the location of the binaries to bin/ - Legacy-Id: 4848 --- ietf/bin/rfc-editor-index-updates | 30 ++ ietf/bin/rfc-editor-queue-updates | 35 ++ ietf/idrfc/idrfc_wrapper.py | 6 +- ietf/idrfc/mirror_rfc_editor_queue.py | 293 ----------------- ietf/idrfc/mirror_rfc_index.py | 365 --------------------- ietf/settings.py | 1 + ietf/sync/__init__.py | 0 ietf/sync/models.py | 0 ietf/sync/rfceditor.py | 446 ++++++++++++++++++++++++++ 9 files changed, 515 insertions(+), 661 deletions(-) create mode 100755 ietf/bin/rfc-editor-index-updates create mode 100755 ietf/bin/rfc-editor-queue-updates delete mode 100644 ietf/idrfc/mirror_rfc_editor_queue.py delete mode 100644 ietf/idrfc/mirror_rfc_index.py create mode 100644 ietf/sync/__init__.py create mode 100644 ietf/sync/models.py create mode 100644 ietf/sync/rfceditor.py diff --git a/ietf/bin/rfc-editor-index-updates b/ietf/bin/rfc-editor-index-updates new file mode 100755 index 000000000..1d2c9ee6c --- /dev/null +++ b/ietf/bin/rfc-editor-index-updates @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +import os, sys, re, json, datetime +import syslog + +syslog.openlog(os.path.basename(__file__), syslog.LOG_PID, syslog.LOG_LOCAL0) + +# boilerplate +basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) +sys.path = [ basedir ] + sys.path + +from ietf import settings +from django.core import management +management.setup_environ(settings) + + +from ietf.sync.rfceditor import * + +syslog.syslog("Updating document metadata from RFC index from %s" % QUEUE_URL) + +response = fetch_index_xml(INDEX_URL) +data = parse_index(response) + +if len(data) < MIN_INDEX_RESULTS: + syslog.syslog("Not enough results, only %s" % len(data)) + sys.exit(1) + +changed = update_docs_from_rfc_index(data) +for c in changed: + syslog.syslog(c) diff --git a/ietf/bin/rfc-editor-queue-updates b/ietf/bin/rfc-editor-queue-updates new file mode 100755 index 000000000..22562a629 --- /dev/null +++ b/ietf/bin/rfc-editor-queue-updates @@ -0,0 +1,35 @@ +#!/usr/bin/env python + +import os, sys, re, json, datetime +import syslog + +syslog.openlog(os.path.basename(__file__), syslog.LOG_PID, syslog.LOG_LOCAL0) + +# boilerplate +basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) +sys.path = [ basedir ] + sys.path + +from ietf import settings +from django.core import management +management.setup_environ(settings) + + +from ietf.sync.rfceditor import * + +syslog.syslog("Updating RFC Editor queue states from %s" % QUEUE_URL) + +response = fetch_queue_xml(QUEUE_URL) +drafts, warnings = parse_queue(response) +for w in warnings: + syslog.syslog(u"WARNING: %s" % w) + +if len(drafts) < MIN_QUEUE_RESULTS: + syslog.syslog("Not enough results, only %s" % len(drafts)) + sys.exit(1) + +changed, warnings = update_drafts_from_queue(drafts) +for w in warnings: + syslog.syslog(u"WARNING: %s" % w) + +for c in changed: + syslog.syslog(u"Updated %s" % c) diff --git a/ietf/idrfc/idrfc_wrapper.py b/ietf/idrfc/idrfc_wrapper.py index 48e806d58..2c1a136c2 100644 --- a/ietf/idrfc/idrfc_wrapper.py +++ b/ietf/idrfc/idrfc_wrapper.py @@ -125,9 +125,9 @@ class IdWrapper: if settings.USE_DB_REDESIGN_PROXY_CLASSES: s = self._draft.get_state("draft-rfceditor") if s: - # extract possible extra states - tags = self._draft.tags.filter(slug__in=("iana-crd", "ref", "missref")) - return " ".join([s.name] + [t.slug.replace("-crd", "").upper() for t in tags]) + # extract possible extra annotations + tags = self._draft.tags.filter(slug__in=("iana", "ref")) + return "*".join([s.name] + [t.slug.upper() for t in tags]) else: return None diff --git a/ietf/idrfc/mirror_rfc_editor_queue.py b/ietf/idrfc/mirror_rfc_editor_queue.py deleted file mode 100644 index 0faf941cb..000000000 --- a/ietf/idrfc/mirror_rfc_editor_queue.py +++ /dev/null @@ -1,293 +0,0 @@ -# Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). -# All rights reserved. Contact: Pasi Eronen -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# -# * Neither the name of the Nokia Corporation and/or its -# subsidiary(-ies) nor the names of its contributors may be used -# to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -from ietf import settings -from django.core import management -management.setup_environ(settings) -from django import db - -from xml.dom import pulldom, Node -import re -import urllib2 -from datetime import datetime -import socket -import sys - -QUEUE_URL = "http://www.rfc-editor.org/queue2.xml" -TABLE = "rfc_editor_queue_mirror" -REF_TABLE = "rfc_editor_queue_mirror_refs" - -log_data = "" -def log(line): - global log_data - if __name__ == '__main__' and len(sys.argv) > 1: - print line - else: - log_data += line + "\n" - -def parse(response): - def getChildText(parentNode, tagName): - for node in parentNode.childNodes: - if node.nodeType == Node.ELEMENT_NODE and node.localName == tagName: - return node.firstChild.data - return None - - events = pulldom.parse(response) - drafts = [] - refs = [] - for (event, node) in events: - if event == pulldom.START_ELEMENT and node.tagName == "entry": - events.expandNode(node) - node.normalize() - draft_name = getChildText(node, "draft").strip() - draft_name = re.sub("(-\d\d)?(.txt){1,2}$", "", draft_name) - date_received = getChildText(node, "date-received") - - states = [] - for child in node.childNodes: - if child.nodeType == Node.ELEMENT_NODE and child.localName == "state": - states.append(child.firstChild.data) - - has_refs = False - for child in node.childNodes: - if child.nodeType == Node.ELEMENT_NODE and child.localName == "normRef": - ref_name = getChildText(child, "ref-name") - ref_state = getChildText(child, "ref-state") - in_queue = ref_state.startswith("IN-QUEUE") - refs.append([draft_name, ref_name, in_queue, True]) - has_refs = True - if has_refs and not "MISSREF" in states: - states.append("REF") - - if len(states) == 0: - state = "?" - else: - state = " ".join(states) - drafts.append([draft_name, date_received, state, stream]) - - elif event == pulldom.START_ELEMENT and node.tagName == "section": - name = node.getAttribute('name') - if name.startswith("IETF"): - stream = 1 - elif name.startswith("IAB"): - stream = 2 - elif name.startswith("IRTF"): - stream = 3 - elif name.startswith("INDEPENDENT"): - stream = 4 - else: - stream = 0 - log("WARNING: unrecognized section "+name) - return (drafts, refs) - -# Find set of all normative references (whether direct or via some -# other normative reference) -def find_indirect_refs(drafts, refs): - result = [] - draft_names = set() - for draft in drafts: - draft_names.add(draft[0]) - - def recurse(draft_name, ref_set, level): - for (source, destination, in_queue, direct) in refs: - if source == draft_name: - if destination not in ref_set: - ref_set.add(destination) - recurse(destination, ref_set, level+1) - if level == 0: - # Remove self-reference - ref_set.remove(draft_name) - # Remove direct references - for (source, destination, in_queue, direct) in refs: - if source == draft_name: - if destination in ref_set: - ref_set.remove(destination) - # The rest are indirect references - for ref in ref_set: - if draft_name != ref: - result.append([draft_name, ref, ref in draft_names, False]) - - for draft_name in draft_names: - recurse(draft_name, set([draft_name]), 0) - return result - -# Convert filenames to id_document_tags -def find_document_ids(cursor, drafts, refs): - draft_ids = {} - drafts2 = [] - for draft in drafts: - cursor.execute("SELECT id_document_tag FROM internet_drafts WHERE filename=%s", [draft[0]]) - row = cursor.fetchone() - if not row: - log("WARNING: cannot find id for "+draft[0]) - else: - draft_ids[draft[0]] = row[0] - drafts2.append([row[0]]+draft[1:]) - refs2 = [] - for ref in refs: - if ref[0] in draft_ids: - refs2.append([draft_ids[ref[0]]]+ref[1:]) - return (drafts2, refs2) - -def parse_all(response): - log("parsing...") - (drafts, refs) = parse(response) - log("got "+ str(len(drafts)) + " drafts and "+str(len(refs))+" direct refs") - - indirect_refs = find_indirect_refs(drafts, refs) - log("found " + str(len(indirect_refs)) + " indirect refs") - refs.extend(indirect_refs) - del(indirect_refs) - - if settings.USE_DB_REDESIGN_PROXY_CLASSES: # note: return before id lookup - return (drafts, refs) - - # convert filenames to id_document_tags - log("connecting to database...") - cursor = db.connection.cursor() - log("finding id_document_tags...") - (drafts, refs) = find_document_ids(cursor, drafts, refs) - cursor.close() - return (drafts, refs) - -def insert_into_database(drafts, refs): - log("connecting to database...") - cursor = db.connection.cursor() - log("removing old data...") - cursor.execute("DELETE FROM "+TABLE) - cursor.execute("DELETE FROM "+REF_TABLE) - log("inserting new data...") - cursor.executemany("INSERT INTO "+TABLE+" (id_document_tag, date_received, state, stream) VALUES (%s, %s, %s, %s)", drafts) - cursor.execute("DELETE FROM "+REF_TABLE) - cursor.executemany("INSERT INTO "+REF_TABLE+" (source, destination, in_queue, direct) VALUES (%s, %s, %s, %s)", refs) - cursor.close() - db.connection._commit() - db.connection.close() - -import django.db.transaction - -def get_rfc_tag_mapping(): - """Return dict with RFC Editor state name -> DocTagName""" - from ietf.name.models import DocTagName - from ietf.name.utils import name - - return { - 'IANA': name(DocTagName, 'iana-crd', 'IANA coordination', "RFC-Editor/IANA Registration Coordination"), - 'REF': name(DocTagName, 'ref', 'Holding for references', "Holding for normative reference"), - 'MISSREF': name(DocTagName, 'missref', 'Missing references', "Awaiting missing normative reference"), - } - -def get_rfc_state_mapping(): - """Return dict with RFC Editor state name -> State""" - from ietf.doc.models import State, StateType - t = StateType.objects.get(slug="draft-rfceditor") - return { - 'AUTH': State.objects.get_or_create(type=t, slug='auth', name='AUTH', desc="Awaiting author action")[0], - 'AUTH48': State.objects.get_or_create(type=t, slug='auth48', name="AUTH48", desc="Awaiting final author approval")[0], - 'AUTH48-DONE': State.objects.get_or_create(type=t, slug='auth48done', name="AUTH48-DONE", desc="Final approvals are complete")[0], - 'EDIT': State.objects.get_or_create(type=t, slug='edit', name='EDIT', desc="Approved by the stream manager (e.g., IESG, IAB, IRSG, ISE), awaiting processing and publishing")[0], - 'IANA': State.objects.get_or_create(type=t, slug='iana-crd', name='IANA', desc="RFC-Editor/IANA Registration Coordination")[0], - 'IESG': State.objects.get_or_create(type=t, slug='iesg', name='IESG', desc="Holding for IESG action")[0], - 'ISR': State.objects.get_or_create(type=t, slug='isr', name='ISR', desc="Independent Submission Review by the ISE ")[0], - 'ISR-AUTH': State.objects.get_or_create(type=t, slug='isr-auth', name='ISR-AUTH', desc="Independent Submission awaiting author update, or in discussion between author and ISE")[0], - 'REF': State.objects.get_or_create(type=t, slug='ref', name='REF', desc="Holding for normative reference")[0], - 'RFC-EDITOR': State.objects.get_or_create(type=t, slug='rfc-edit', name='RFC-EDITOR', desc="Awaiting final RFC Editor review before AUTH48")[0], - 'TO': State.objects.get_or_create(type=t, slug='timeout', name='TO', desc="Time-out period during which the IESG reviews document for conflict/concurrence with other IETF working group work")[0], - 'MISSREF': State.objects.get_or_create(type=t, slug='missref', name='MISSREF', desc="Awaiting missing normative reference")[0], - } - - -@django.db.transaction.commit_on_success -def insert_into_databaseREDESIGN(drafts, refs): - from ietf.doc.models import Document - from ietf.name.models import DocTagName - - tags = get_rfc_tag_mapping() - state_map = get_rfc_state_mapping() - - rfc_editor_tags = tags.values() - - log("removing old data...") - for d in Document.objects.filter(states__type="draft-rfceditor").distinct(): - d.tags.remove(*rfc_editor_tags) - d.unset_state("draft-rfceditor") - - log("inserting new data...") - - for name, date_received, state_info, stream_id in drafts: - try: - d = Document.objects.get(name=name) - except Document.DoesNotExist: - log("unknown document %s" % name) - continue - - state_list = state_info.split(" ") - if state_list: - state = state_list[0] - # For now, ignore the '*R...' that's appeared for some states. - # FIXME : see if we need to add some refinement for this. - if '*' in state: - state = state.split("*")[0] - # first is state - d.set_state(state_map[state]) - - # remainding are tags - for x in state_list[1:]: - d.tags.add(tags[x]) - -if settings.USE_DB_REDESIGN_PROXY_CLASSES: - insert_into_database = insert_into_databaseREDESIGN - - -if __name__ == '__main__': - try: - log("output from mirror_rfc_editor_queue.py:\n") - log("time: "+str(datetime.now())) - log("host: "+socket.gethostname()) - log("url: "+QUEUE_URL) - - log("downloading...") - socket.setdefaulttimeout(30) - response = urllib2.urlopen(QUEUE_URL) - - (drafts, refs) = parse_all(response) - if len(drafts) < 10 or len(refs) < 10: - raise Exception('not enough data') - - insert_into_database(drafts, refs) - - log("all done!") - if log_data.find("WARNING") < 0: - log_data = "" - finally: - if len(log_data) > 0: - print log_data diff --git a/ietf/idrfc/mirror_rfc_index.py b/ietf/idrfc/mirror_rfc_index.py deleted file mode 100644 index afb81cec0..000000000 --- a/ietf/idrfc/mirror_rfc_index.py +++ /dev/null @@ -1,365 +0,0 @@ -# Copyright (C) 2009-2010 Nokia Corporation and/or its subsidiary(-ies). -# All rights reserved. Contact: Pasi Eronen -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above -# copyright notice, this list of conditions and the following -# disclaimer in the documentation and/or other materials provided -# with the distribution. -# -# * Neither the name of the Nokia Corporation and/or its -# subsidiary(-ies) nor the names of its contributors may be used -# to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -from ietf import settings -from django.core import management -management.setup_environ(settings) -from django import db - -from xml.dom import pulldom, Node -import re -import urllib2 -from datetime import datetime, date, timedelta -import socket -import sys - -INDEX_URL = "http://www.rfc-editor.org/rfc/rfc-index.xml" -TABLE = "rfc_index_mirror" - -log_data = "" -def log(line): - global log_data - if __name__ == '__main__' and len(sys.argv) > 1: - print line - else: - log_data += line + "\n" - -# python before 2.7 doesn't have the total_seconds method on datetime.timedelta. -def total_seconds(td): - return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6 - -def parse(response): - def getChildText(parentNode, tagName): - for node in parentNode.childNodes: - if node.nodeType == Node.ELEMENT_NODE and node.localName == tagName: - return node.firstChild.data - return None - - def getDocList(parentNode, tagName): - l = [] - for u in parentNode.getElementsByTagName(tagName): - for d in u.getElementsByTagName("doc-id"): - l.append(d.firstChild.data) - if len(l) == 0: - return None - else: - return ",".join(l) - - also_list = {} - data = [] - events = pulldom.parse(response) - for (event, node) in events: - if event == pulldom.START_ELEMENT and node.tagName in ["bcp-entry", "fyi-entry", "std-entry"]: - events.expandNode(node) - node.normalize() - bcpid = getChildText(node, "doc-id") - doclist = getDocList(node, "is-also") - if doclist: - for docid in doclist.split(","): - if docid in also_list: - also_list[docid].append(bcpid) - else: - also_list[docid] = [bcpid] - - elif event == pulldom.START_ELEMENT and node.tagName == "rfc-entry": - events.expandNode(node) - node.normalize() - rfc_number = int(getChildText(node, "doc-id")[3:]) - title = getChildText(node, "title") - - l = [] - for author in node.getElementsByTagName("author"): - l.append(getChildText(author, "name")) - authors = "; ".join(l) - - d = node.getElementsByTagName("date")[0] - year = int(getChildText(d, "year")) - month = getChildText(d, "month") - month = ["January","February","March","April","May","June","July","August","September","October","November","December"].index(month)+1 - rfc_published_date = ("%d-%02d-01" % (year, month)) - - current_status = getChildText(node, "current-status").title() - - updates = getDocList(node, "updates") - updated_by = getDocList(node, "updated-by") - obsoletes = getDocList(node, "obsoletes") - obsoleted_by = getDocList(node, "obsoleted-by") - stream = getChildText(node, "stream") - wg = getChildText(node, "wg_acronym") - if wg and ((wg == "NON WORKING GROUP") or len(wg) > 15): - wg = None - - l = [] - for format in node.getElementsByTagName("format"): - l.append(getChildText(format, "file-format")) - file_formats = (",".join(l)).lower() - - draft = getChildText(node, "draft") - if draft and re.search("-\d\d$", draft): - draft = draft[0:-3] - - if len(node.getElementsByTagName("errata-url")) > 0: - has_errata = 1 - else: - has_errata = 0 - - data.append([rfc_number,title,authors,rfc_published_date,current_status,updates,updated_by,obsoletes,obsoleted_by,None,draft,has_errata,stream,wg,file_formats]) - - for d in data: - k = "RFC%04d" % d[0] - if k in also_list: - d[9] = ",".join(also_list[k]) - return data - -def insert_to_database(data): - log("connecting to database...") - cursor = db.connection.cursor() - log("removing old data...") - cursor.execute("DELETE FROM "+TABLE) - log("inserting new data...") - cursor.executemany("INSERT INTO "+TABLE+" (rfc_number, title, authors, rfc_published_date, current_status,updates,updated_by,obsoletes,obsoleted_by,also,draft,has_errata,stream,wg,file_formats) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)", data) - cursor.close() - db.connection._commit() - db.connection.close() - -def get_std_level_mapping(): - from ietf.name.models import StdLevelName - from ietf.name.utils import name - return { - "Standard": name(StdLevelName, "std", "Standard"), - "Draft Standard": name(StdLevelName, "ds", "Draft Standard"), - "Proposed Standard": name(StdLevelName, "ps", "Proposed Standard"), - "Informational": name(StdLevelName, "inf", "Informational"), - "Experimental": name(StdLevelName, "exp", "Experimental"), - "Best Current Practice": name(StdLevelName, "bcp", "Best Current Practice"), - "Historic": name(StdLevelName, "hist", "Historic"), - "Unknown": name(StdLevelName, "unkn", "Unknown"), - } - -def get_stream_mapping(): - from ietf.name.models import StreamName - from ietf.name.utils import name - - return { - "IETF": name(StreamName, "ietf", "IETF", desc="IETF stream", order=1), - "INDEPENDENT": name(StreamName, "ise", "ISE", desc="Independent Submission Editor stream", order=2), - "IRTF": name(StreamName, "irtf", "IRTF", desc="Independent Submission Editor stream", order=3), - "IAB": name(StreamName, "iab", "IAB", desc="IAB stream", order=4), - "Legacy": name(StreamName, "legacy", "Legacy", desc="Legacy stream", order=5), - } - - -import django.db.transaction - -@django.db.transaction.commit_on_success -def insert_to_databaseREDESIGN(data): - from ietf.person.models import Person - from ietf.doc.models import Document, DocAlias, DocEvent, RelatedDocument, State, save_document_in_history - from ietf.group.models import Group - from ietf.name.models import DocTagName, DocRelationshipName - from ietf.name.utils import name - - system = Person.objects.get(name="(System)") - std_level_mapping = get_std_level_mapping() - stream_mapping = get_stream_mapping() - tag_has_errata = name(DocTagName, 'errata', "Has errata") - relationship_obsoletes = name(DocRelationshipName, "obs", "Obsoletes") - relationship_updates = name(DocRelationshipName, "updates", "Updates") - - skip_older_than_date = (date.today() - timedelta(days=365)).strftime("%Y-%m-%d") - - log("updating data...") - for d in data: - rfc_number, title, authors, rfc_published_date, current_status, updates, updated_by, obsoletes, obsoleted_by, also, draft, has_errata, stream, wg, file_formats = d - - if rfc_published_date < skip_older_than_date: - # speed up the process by skipping old entries - continue - - # we assume two things can happen: we get a new RFC, or an - # attribute has been updated at the RFC Editor (RFC Editor - # attributes currently take precedence over our local - # attributes) - - # make sure we got the document and alias - created = False - doc = None - name = "rfc%s" % rfc_number - a = DocAlias.objects.filter(name=name) - if a: - doc = a[0].document - else: - if draft: - try: - doc = Document.objects.get(name=draft) - except Document.DoesNotExist: - pass - - if not doc: - created = True - log("created document %s" % name) - doc = Document.objects.create(name=name) - - # add alias - DocAlias.objects.create(name=name, document=doc) - if not created: - created = True - log("created alias %s to %s" % (name, doc.name)) - - - # check attributes - changed_attributes = {} - changed_states = [] - created_relations = [] - other_changes = False - if title != doc.title: - changed_attributes["title"] = title - - if std_level_mapping[current_status] != doc.std_level: - changed_attributes["std_level"] = std_level_mapping[current_status] - - if doc.get_state_slug() != "rfc": - changed_states.append(State.objects.get(type="draft", slug="rfc")) - - if doc.stream != stream_mapping[stream]: - changed_attributes["stream"] = stream_mapping[stream] - - if not doc.group and wg: - changed_attributes["group"] = Group.objects.get(acronym=wg) - - if not doc.latest_event(type="published_rfc"): - e = DocEvent(doc=doc, type="published_rfc") - pubdate = datetime.strptime(rfc_published_date, "%Y-%m-%d") - # unfortunately, pubdate doesn't include the correct day - # at the moment because the data only has month/year, so - # try to deduce it - synthesized = datetime.now() - if abs(pubdate - synthesized) > timedelta(days=60): - synthesized = pubdate - else: - direction = -1 if total_seconds(pubdate - synthesized) < 0 else +1 - while synthesized.month != pubdate.month or synthesized.year != pubdate.year: - synthesized += timedelta(days=direction) - e.time = synthesized - e.by = system - e.desc = "RFC published" - e.save() - other_changes = True - - if doc.get_state_slug("draft-iesg") == "rfcqueue": - changed_states.append(State.objects.get(type="draft-iesg", slug="pub")) - - def parse_relation_list(s): - if not s: - return [] - res = [] - for x in s.split(","): - if x[:3] in ("NIC", "IEN", "STD", "RTR"): - # try translating this to RFCs that we can handle - # sensibly; otherwise we'll have to ignore them - l = DocAlias.objects.filter(name__startswith="rfc", document__docalias__name=x.lower()) - else: - l = DocAlias.objects.filter(name=x.lower()) - - for a in l: - if a not in res: - res.append(a) - return res - - for x in parse_relation_list(obsoletes): - if not RelatedDocument.objects.filter(source=doc, target=x, relationship=relationship_obsoletes): - created_relations.append(RelatedDocument(source=doc, target=x, relationship=relationship_obsoletes)) - - for x in parse_relation_list(updates): - if not RelatedDocument.objects.filter(source=doc, target=x, relationship=relationship_updates): - created_relations.append(RelatedDocument(source=doc, target=x, relationship=relationship_updates)) - - if also: - for a in also.lower().split(","): - if not DocAlias.objects.filter(name=a): - DocAlias.objects.create(name=a, document=doc) - other_changes = True - - if has_errata: - if not doc.tags.filter(pk=tag_has_errata.pk): - changed_attributes["tags"] = list(doc.tags.all()) + [tag_has_errata] - else: - if doc.tags.filter(pk=tag_has_errata.pk): - changed_attributes["tags"] = set(doc.tags.all()) - set([tag_has_errata]) - - if changed_attributes or changed_states or created_relations or other_changes: - # apply changes - save_document_in_history(doc) - for k, v in changed_attributes.iteritems(): - setattr(doc, k, v) - - for s in changed_states: - doc.set_state(s) - - for o in created_relations: - o.save() - - doc.time = datetime.now() - doc.save() - - if not created: - log("%s changed" % name) - - -if settings.USE_DB_REDESIGN_PROXY_CLASSES: - insert_to_database = insert_to_databaseREDESIGN - -if __name__ == '__main__': - try: - log("output from mirror_rfc_index.py:\n") - log("time: "+str(datetime.now())) - log("host: "+socket.gethostname()) - log("url: "+INDEX_URL) - - log("downloading...") - socket.setdefaulttimeout(30) - response = urllib2.urlopen(INDEX_URL) - log("parsing...") - data = parse(response) - - log("got " + str(len(data)) + " entries") - if len(data) < 5000: - raise Exception('not enough data') - - insert_to_database(data) - - log("all done!") - log_data = "" - - finally: - if len(log_data) > 0: - print log_data diff --git a/ietf/settings.py b/ietf/settings.py index 4b6eefa8b..2e1e6b124 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -159,6 +159,7 @@ INSTALLED_APPS = ( 'ietf.ietfworkflows', 'ietf.wgchairs', 'ietf.wgcharter', + 'ietf.sync', 'ietf.community', ) diff --git a/ietf/sync/__init__.py b/ietf/sync/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ietf/sync/models.py b/ietf/sync/models.py new file mode 100644 index 000000000..e69de29bb diff --git a/ietf/sync/rfceditor.py b/ietf/sync/rfceditor.py new file mode 100644 index 000000000..0dbc08ff4 --- /dev/null +++ b/ietf/sync/rfceditor.py @@ -0,0 +1,446 @@ +import re, urllib2, json, email, socket +from xml.dom import pulldom, Node + +from django.utils.http import urlquote + +from ietf.utils.mail import send_mail_text + +from ietf.doc.models import * +from ietf.person.models import * +from ietf.name.models import * +from ietf.doc.utils import add_state_change_event + +QUEUE_URL = "http://www.rfc-editor.org/queue2.xml" +INDEX_URL = "http://www.rfc-editor.org/rfc/rfc-index.xml" + +MIN_QUEUE_RESULTS = 10 +MIN_INDEX_RESULTS = 5000 + +# Python < 2.7 doesn't have the total_seconds method on datetime.timedelta. +def total_seconds(td): + return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6 + +def get_child_text(parent_node, tag_name): + for node in parent_node.childNodes: + if node.nodeType == Node.ELEMENT_NODE and node.localName == tag_name: + return node.firstChild.data + return None + + +def fetch_queue_xml(url): + socket.setdefaulttimeout(30) + return urllib2.urlopen(url) + +def parse_queue(response): + events = pulldom.parse(response) + drafts = [] + warnings = [] + + for event, node in events: + if event == pulldom.START_ELEMENT and node.tagName == "entry": + events.expandNode(node) + node.normalize() + draft_name = get_child_text(node, "draft").strip() + draft_name = re.sub("(-\d\d)?(.txt){1,2}$", "", draft_name) + date_received = get_child_text(node, "date-received") + + state = "" + tags = [] + missref_generation = "" + for child in node.childNodes: + if child.nodeType == Node.ELEMENT_NODE and child.localName == "state": + state = child.firstChild.data + # state has some extra annotations encoded, parse + # them out + if '*R' in state: + tags.append("ref") + state = state.replace("*R", "") + if '*A' in state: + tags.append("iana") + state = state.replace("*A", "") + m = re.search(r"\(([0-9]+)G\)", state) + if m: + missref_generation = m.group(1) + state = state.replace("(%sG)" % missref_generation, "") + + # AUTH48 link + auth48 = "" + for child in node.childNodes: + if child.nodeType == Node.ELEMENT_NODE and child.localName == "auth48-url": + auth48 = child.firstChild.data + + # cluster link (if it ever gets implemented) + cluster = "" + for child in node.childNodes: + if child.nodeType == Node.ELEMENT_NODE and child.localName == "cluster-url": + cluster = child.firstChild.data + + refs = [] + for child in node.childNodes: + if child.nodeType == Node.ELEMENT_NODE and child.localName == "normRef": + ref_name = get_child_text(child, "ref-name") + ref_state = get_child_text(child, "ref-state") + in_queue = ref_state.startswith("IN-QUEUE") + refs.append((ref_name, ref_state, in_queue)) + + drafts.append((draft_name, date_received, state, tags, missref_generation, stream, auth48, cluster, refs)) + + elif event == pulldom.START_ELEMENT and node.tagName == "section": + name = node.getAttribute('name') + if name.startswith("IETF"): + stream = "ietf" + elif name.startswith("IAB"): + stream = "iab" + elif name.startswith("IRTF"): + stream = "irtf" + elif name.startswith("INDEPENDENT"): + stream = "ise" + else: + stream = None + warnings.append("unrecognized section " + name) + + return drafts, warnings + +def update_drafts_from_queue(drafts): + tag_mapping = { + 'IANA': DocTagName.objects.get(slug='iana'), + 'REF': DocTagName.objects.get(slug='ref') + } + + slookup = dict((s.slug, s) + for s in State.objects.filter(type=StateType.objects.get(slug="draft-rfceditor"))) + state_mapping = { + 'AUTH': slookup['auth'], + 'AUTH48': slookup['auth48'], + 'AUTH48-DONE': slookup['auth48-done'], + 'EDIT': slookup['edit'], + 'IANA': slookup['iana'], + 'IESG': slookup['iesg'], + 'ISR': slookup['isr'], + 'ISR-AUTH': slookup['isr-auth'], + 'REF': slookup['ref'], + 'RFC-EDITOR': slookup['rfc-edit'], + 'TO': slookup['timeout'], + 'MISSREF': slookup['missref'], + } + + system = Person.objects.get(name="(System)") + + warnings = [] + + names = [t[0] for t in drafts] + + drafts_in_db = dict((d.name, d) + for d in Document.objects.filter(type="draft", docalias__name__in=names)) + + changed = set() + + for name, date_received, state, tags, missref_generation, stream, auth48, cluster, refs in drafts: + if name not in drafts_in_db: + warnings.append("unknown document %s" % name) + continue + + if not state or state not in state_mapping: + warnings.append("unknown state '%s'" % state) + continue + + d = drafts_in_db[name] + + prev_state = d.get_state("draft-rfceditor") + next_state = state_mapping[state] + + # check if we've noted it's been received + if d.get_state_slug("draft-iesg") == "ann" and not prev_state and not d.latest_event(DocEvent, type="rfc_editor_received_announcement"): + e = DocEvent(doc=d, by=system, type="rfc_editor_received_announcement") + e.desc = "Announcement was received by RFC Editor" + e.save() + send_mail_text(None, "iesg-secretary@ietf.org", None, + '%s in RFC Editor queue' % d.name, + 'The announcement for %s has been received by the RFC Editor.' % d.name) + + + if prev_state != next_state: + save_document_in_history(d) + + d.set_state(next_state) + + e = add_state_change_event(d, system, prev_state, next_state) + + if auth48: + e.desc = re.sub(r"(.*)", r"\1" % auth48, e.desc) + e.save() + + changed.add(name) + + t = DocTagName.objects.filter(slug__in=tags) + if set(t) != set(d.tags.all()): + d.tags = t + changed.add(name) + + + # remove tags and states for those not in the queue anymore + for d in Document.objects.exclude(docalias__name__in=names).filter(states__type="draft-rfceditor").distinct(): + d.tags.remove(*tag_mapping.values()) + d.unset_state("draft-rfceditor") + # we do not add a history entry here - most likely we already + # have something that explains what happened + changed.add(name) + + return changed, warnings + + +def fetch_index_xml(url): + socket.setdefaulttimeout(30) + return urllib2.urlopen(url) + +def parse_index(response): + def getDocList(parentNode, tagName): + l = [] + for u in parentNode.getElementsByTagName(tagName): + for d in u.getElementsByTagName("doc-id"): + l.append(d.firstChild.data) + return l + + also_list = {} + data = [] + events = pulldom.parse(response) + for event, node in events: + if event == pulldom.START_ELEMENT and node.tagName in ["bcp-entry", "fyi-entry", "std-entry"]: + events.expandNode(node) + node.normalize() + bcpid = get_child_text(node, "doc-id") + doclist = getDocList(node, "is-also") + for docid in doclist: + if docid in also_list: + also_list[docid].append(bcpid) + else: + also_list[docid] = [bcpid] + + elif event == pulldom.START_ELEMENT and node.tagName == "rfc-entry": + events.expandNode(node) + node.normalize() + rfc_number = int(get_child_text(node, "doc-id")[3:]) + title = get_child_text(node, "title") + + authors = [] + for author in node.getElementsByTagName("author"): + authors.append(get_child_text(author, "name")) + + d = node.getElementsByTagName("date")[0] + year = int(get_child_text(d, "year")) + month = get_child_text(d, "month") + month = ["January","February","March","April","May","June","July","August","September","October","November","December"].index(month)+1 + rfc_published_date = datetime.date(year, month, 1) + + current_status = get_child_text(node, "current-status").title() + + updates = getDocList(node, "updates") + updated_by = getDocList(node, "updated-by") + obsoletes = getDocList(node, "obsoletes") + obsoleted_by = getDocList(node, "obsoleted-by") + stream = get_child_text(node, "stream") + wg = get_child_text(node, "wg_acronym") + if wg and ((wg == "NON WORKING GROUP") or len(wg) > 15): + wg = None + + l = [] + pages = "" + for fmt in node.getElementsByTagName("format"): + l.append(get_child_text(fmt, "file-format")) + if get_child_text(fmt, "file-format") == "ASCII": + pages = get_child_text(fmt, "page-count") + file_formats = (",".join(l)).lower() + + abstract = "" + for abstract in node.getElementsByTagName("abstract"): + abstract = get_child_text(abstract, "p") + + draft = get_child_text(node, "draft") + if draft and re.search("-\d\d$", draft): + draft = draft[0:-3] + + if len(node.getElementsByTagName("errata-url")) > 0: + has_errata = 1 + else: + has_errata = 0 + + data.append((rfc_number,title,authors,rfc_published_date,current_status,updates,updated_by,obsoletes,obsoleted_by,[],draft,has_errata,stream,wg,file_formats,pages,abstract)) + + for d in data: + k = "RFC%04d" % d[0] + if k in also_list: + d[9].extend(also_list[k]) + return data + + + #skip_older_than_date = date.today() - timedelta(days=365) +def update_docs_from_rfc_index(data, skip_older_than_date=None): + std_level_mapping = { + "Standard": StdLevelName.objects.get(slug="std"), + "Draft Standard": StdLevelName.objects.get(slug="ds"), + "Proposed Standard": StdLevelName.objects.get(slug="ps"), + "Informational": StdLevelName.objects.get(slug="inf"), + "Experimental": StdLevelName.objects.get(slug="exp"), + "Best Current Practice": StdLevelName.objects.get(slug="bcp"), + "Historic": StdLevelName.objects.get(slug="hist"), + "Unknown": StdLevelName.objects.get(slug="unkn"), + } + + stream_mapping = { + "IETF": StreamName.objects.get(slug="ietf"), + "INDEPENDENT": StreamName.objects.get(slug="ise"), + "IRTF": StreamName.objects.get(slug="irtf"), + "IAB": StreamName.objects.get(slug="iab"), + "Legacy": StreamName.objects.get(slug="legacy"), + } + + tag_has_errata = DocTagName.objects.get(slug='errata') + relationship_obsoletes = DocRelationshipName.objects.get(slug="obs") + relationship_updates = DocRelationshipName.objects.get(slug="updates") + + system = Person.objects.get(name="(System)") + + results = [] + + for rfc_number, title, authors, rfc_published_date, current_status, updates, updated_by, obsoletes, obsoleted_by, also, draft, has_errata, stream, wg, file_formats, pages, abstract in data: + + if skip_older_than_date and rfc_published_date < skip_older_than_date: + # speed up the process by skipping old entries + continue + + # we assume two things can happen: we get a new RFC, or an + # attribute has been updated at the RFC Editor (RFC Editor + # attributes take precedence over our local attributes) + + # make sure we got the document and alias + created = False + doc = None + name = "rfc%s" % rfc_number + a = DocAlias.objects.filter(name=name).select_related("document") + if a: + doc = a[0].document + else: + if draft: + try: + doc = Document.objects.get(name=draft) + except Document.DoesNotExist: + pass + + if not doc: + results.append("created document %s" % name) + doc = Document.objects.get_or_create(name=name)[0] + + # add alias + DocAlias.objects.get_or_create(name=name, document=doc) + results.append("created alias %s to %s" % (name, doc.name)) + created = True + + + # check attributes + changed_attributes = {} + changed_states = [] + created_relations = [] + other_changes = False + if title != doc.title: + changed_attributes["title"] = title + + if abstract and abstract != doc.abstract: + changed_attributes["abstract"] = abstract + + if int(pages) != doc.pages: + changed_attributes["pages"] = int(pages) + + if std_level_mapping[current_status] != doc.std_level: + changed_attributes["std_level"] = std_level_mapping[current_status] + + if doc.get_state_slug() != "rfc": + changed_states.append(State.objects.get(type="draft", slug="rfc")) + + if doc.stream != stream_mapping[stream]: + changed_attributes["stream"] = stream_mapping[stream] + + if not doc.group and wg: + changed_attributes["group"] = Group.objects.get(acronym=wg) + + if not doc.latest_event(type="published_rfc"): + e = DocEvent(doc=doc, type="published_rfc") + # unfortunately, rfc_published_date doesn't include the correct day + # at the moment because the data only has month/year, so + # try to deduce it + d = datetime.datetime.combine(rfc_published_date, datetime.time()) + synthesized = datetime.datetime.now() + if abs(d - synthesized) > datetime.timedelta(days=60): + synthesized = d + else: + direction = -1 if total_seconds(d - synthesized) < 0 else +1 + while synthesized.month != d.month or synthesized.year != d.year: + synthesized += datetime.timedelta(days=direction) + e.time = synthesized + e.by = system + e.desc = "RFC published" + e.save() + other_changes = True + + results.append("Added RFC published event: %s" % e.time.strftime("%Y-%m-%d")) + + for t in ("draft-iesg", "draft-stream-iab", "draft-stream-irtf", "draft-stream-ise"): + if doc.get_state_slug(t) != "pub": + changed_states.append(State.objects.get(type=t, slug="pub")) + + def parse_relation_list(l): + res = [] + for x in l: + if x[:3] in ("NIC", "IEN", "STD", "RTR"): + # try translating this to RFCs that we can handle + # sensibly; otherwise we'll have to ignore them + l = DocAlias.objects.filter(name__startswith="rfc", document__docalias__name=x.lower()) + else: + l = DocAlias.objects.filter(name=x.lower()) + + for a in l: + if a not in res: + res.append(a) + return res + + for x in parse_relation_list(obsoletes): + if not RelatedDocument.objects.filter(source=doc, target=x, relationship=relationship_obsoletes): + created_relations.append(RelatedDocument(source=doc, target=x, relationship=relationship_obsoletes)) + + for x in parse_relation_list(updates): + if not RelatedDocument.objects.filter(source=doc, target=x, relationship=relationship_updates): + created_relations.append(RelatedDocument(source=doc, target=x, relationship=relationship_updates)) + + if also: + for a in also: + a = a.lower() + if not DocAlias.objects.filter(name=a): + DocAlias.objects.create(name=a, document=doc) + other_changes = True + results.append("Created alias %s to %s" % (a, doc.name)) + + if has_errata: + if not doc.tags.filter(pk=tag_has_errata.pk): + changed_attributes["tags"] = list(doc.tags.all()) + [tag_has_errata] + else: + if doc.tags.filter(pk=tag_has_errata.pk): + changed_attributes["tags"] = set(doc.tags.all()) - set([tag_has_errata]) + + if changed_attributes or changed_states or created_relations or other_changes: + # apply changes + save_document_in_history(doc) + for k, v in changed_attributes.iteritems(): + setattr(doc, k, v) + results.append("Changed %s to %s on %s" % (k, v, doc.name)) + + for s in changed_states: + doc.set_state(s) + results.append("Set state %s on %s" % (s, doc.name)) + + for o in created_relations: + o.save() + results.append("Created %s" % o) + + doc.time = datetime.datetime.now() + doc.save() + + return results From 9fdcbc38abbbed91efddd044a163f4a8009e29f5 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Mon, 17 Sep 2012 15:51:33 +0000 Subject: [PATCH 04/47] Add time zone helpers for converting between local IETF db time and UTC - Legacy-Id: 4849 --- ietf/utils/timezone.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 ietf/utils/timezone.py diff --git a/ietf/utils/timezone.py b/ietf/utils/timezone.py new file mode 100644 index 000000000..2d0c3db1d --- /dev/null +++ b/ietf/utils/timezone.py @@ -0,0 +1,37 @@ +import pytz +import email.utils +import datetime + +from django.conf import settings + +def local_timezone_to_utc(d): + """Takes a naive datetime in the local timezone and returns a + naive datetime with the corresponding UTC time.""" + local_timezone = pytz.timezone(settings.TIME_ZONE) + + d = local_timezone.localize(d).astimezone(pytz.utc) + + return d.replace(tzinfo=None) + +def utc_to_local_timezone(d): + """Takes a naive datetime UTC and returns a naive datetime in the + local time zone.""" + local_timezone = pytz.timezone(settings.TIME_ZONE) + + d = local_timezone.normalize(d.replace(tzinfo=pytz.utc).astimezone(local_timezone)) + + return d.replace(tzinfo=None) + +def email_time_to_local_timezone(date_string): + """Takes a time string from an email and returns a naive datetime + in the local time zone.""" + + t = email.utils.parsedate_tz(date_string) + d = datetime.datetime(*t[:6]) + + if t[7] != None: + d += datetime.timedelta(seconds=t[9]) + + return utc_to_local_timezone(d) + + From 5282bd1d07b085fc2de5b24a04c80c34df80df6e Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Mon, 17 Sep 2012 15:54:22 +0000 Subject: [PATCH 05/47] Add sync scripts for reading from IANA changes API, reading from the protocols page (to see when references to newly published RFCs have been updated) and parsing IANA review emails to be included as comments - Legacy-Id: 4850 --- ietf/bin/iana-changes-updates | 67 ++++++++ ietf/bin/iana-protocols-updates | 34 +++++ ietf/bin/iana-review-email | 27 ++++ ietf/sync/iana.py | 260 ++++++++++++++++++++++++++++++++ 4 files changed, 388 insertions(+) create mode 100755 ietf/bin/iana-changes-updates create mode 100755 ietf/bin/iana-protocols-updates create mode 100755 ietf/bin/iana-review-email create mode 100644 ietf/sync/iana.py diff --git a/ietf/bin/iana-changes-updates b/ietf/bin/iana-changes-updates new file mode 100755 index 000000000..4627e6890 --- /dev/null +++ b/ietf/bin/iana-changes-updates @@ -0,0 +1,67 @@ +#!/usr/bin/env python + +import os, sys, re, json, datetime, optparse +import syslog + +# boilerplate +basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) +sys.path = [ basedir ] + sys.path + +from ietf import settings +from django.core import management +management.setup_environ(settings) + + +from optparse import OptionParser + +parser = OptionParser() +parser.add_option("-f", "--from", dest="start", + help="Start time, defaults to a little less than 23 hours ago", metavar="YYYY-MM-DD HH:MM:SS") +parser.add_option("-t", "--to", dest="end", + help="End time, defaults to 23 hours later than from", metavar="YYYY-MM-DD HH:MM:SS") +parser.add_option("", "--no-email", dest="send_email", default=True, action="store_false", + help="Skip sending emails") + +options, args = parser.parse_args() + +# compensate to avoid we ask for something that happened now and then +# don't get it back because our request interval is slightly off +CLOCK_SKEW_COMPENSATION = 5 # seconds + +# actually the interface accepts 24 hours, but then we get into +# trouble with daylights savings - meh +MAX_INTERVAL_ACCEPTED_BY_IANA = datetime.timedelta(hours=23) + + +start = datetime.datetime.now() - datetime.timedelta(hours=23) + CLOCK_SKEW_COMPENSATION +if options.start: + start = datetime.datetime.strptime(options.start, "%Y-%m-%d %H:%M:%S") + +end = start + datetime.timedelta(hours=23) +if options.end: + end = datetime.datetime.strptime(options.end, "%Y-%m-%d %H:%M:%S") + +syslog.openlog(os.path.basename(__file__), syslog.LOG_PID, syslog.LOG_LOCAL0) + + +from ietf.sync.iana import * + +syslog.syslog("Updating history log with new changes from IANA from %s, period %s - %s" % (CHANGES_URL, start, end)) + +t = start +while t < end: + # the IANA server doesn't allow us to fetch more than a certain + # period, so loop over the requested period and make multiple + # requests if necessary + + text = fetch_changes_json(CHANGES_URL, t, min(end, t + MAX_INTERVAL_ACCEPTED_BY_IANA)) + changes = parse_changes_json(text) + added_events, warnings = update_history_with_changes(changes, send_email=options.send_email) + + for e in added_events: + syslog.syslog("Added event for %s %s: %s" % (e.doc_id, e.time, e.desc)) + + for w in warnings: + syslog.syslog("WARNING: %s" % w) + + t += MAX_INTERVAL_ACCEPTED_BY_IANA diff --git a/ietf/bin/iana-protocols-updates b/ietf/bin/iana-protocols-updates new file mode 100755 index 000000000..257b0e7cf --- /dev/null +++ b/ietf/bin/iana-protocols-updates @@ -0,0 +1,34 @@ +#!/usr/bin/env python + +import os, sys, re, json, datetime +import syslog + +syslog.openlog(os.path.basename(__file__), syslog.LOG_PID, syslog.LOG_LOCAL0) + +# boilerplate +basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) +sys.path = [ basedir ] + sys.path + +from ietf import settings +from django.core import management +management.setup_environ(settings) + + +from ietf.sync.iana import * + +def chunks(l, n): + """Split list l up in chunks of max size n.""" + return (l[i:i+n] for i in xrange(0, len(l), n)) + +syslog.syslog("Updating history log with new RFC entries from IANA protocols page %s" % PROTOCOLS_URL) + +# FIXME: this needs to be the date where this tool is first deployed +rfc_must_published_later_than = datetime.datetime(2012, 8, 30, 0, 0, 0) + +text = fetch_protocol_page(PROTOCOLS_URL) +rfc_numbers = parse_protocol_page(text) +for chunk in chunks(rfc_numbers, 100): + updated = update_rfc_log_from_protocol_page(chunk, rfc_must_published_later_than) + + for d in updated: + syslog.syslog("Added history entry for %s" % d.display_name()) diff --git a/ietf/bin/iana-review-email b/ietf/bin/iana-review-email new file mode 100755 index 000000000..85f4daa2e --- /dev/null +++ b/ietf/bin/iana-review-email @@ -0,0 +1,27 @@ +#!/usr/bin/env python + +import os, sys, re, json, datetime, optparse +import syslog + +# boilerplate +basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) +sys.path = [ basedir ] + sys.path + +from ietf import settings +from django.core import management +management.setup_environ(settings) + + +syslog.openlog(os.path.basename(__file__), syslog.LOG_PID, syslog.LOG_LOCAL0) + +from ietf.sync.iana import * + +msg = sys.stdin.read() + +syslog.syslog("Reading IANA review email") + +doc_name, review_time, by, comment = parse_review_email(msg) +add_review_comment(doc_name, review_time, by, comment) + +if by.name == "(System)": + syslog.syslog("WARNING: person responsible for email does not have a IANA role") diff --git a/ietf/sync/iana.py b/ietf/sync/iana.py new file mode 100644 index 000000000..30c13260b --- /dev/null +++ b/ietf/sync/iana.py @@ -0,0 +1,260 @@ +import re, urllib2, json, email + +from django.utils.http import urlquote + +from ietf.doc.models import * +from ietf.doc.utils import add_state_change_event +from ietf.person.models import * +from ietf.idrfc.mails import email_owner, email_state_changed, email_authors +from ietf.utils.timezone import * + +PROTOCOLS_URL = "http://www.iana.org/protocols/" +CHANGES_URL = "http://datatracker.dev.icann.org:8080/data-tracker/changes" + +def fetch_protocol_page(url): + f = urllib2.urlopen(PROTOCOLS_URL) + text = f.read() + f.close() + return text + +def parse_protocol_page(text): + """Parse IANA protocols page to extract referenced RFCs (as + rfcXXXX document names).""" + matches = re.findall('RFC [0-9]+', text) + res = set() + for m in matches: + res.add("rfc" + m[len("RFC "):]) + + return list(res) + +def update_rfc_log_from_protocol_page(rfc_names, rfc_must_published_later_than): + """Add notices to RFC history log that IANA is now referencing the RFC.""" + system = Person.objects.get(name="(System)") + + updated = [] + + docs = Document.objects.filter(docalias__name__in=rfc_names).exclude( + docevent__type="rfc_in_iana_registry").filter( + # only take those that were published after cutoff since we + # have a big bunch of old RFCs that we unfortunately don't have data for + docevent__type="published_rfc", docevent__time__gte=rfc_must_published_later_than + ).distinct() + + for d in docs: + e = DocEvent(doc=d) + e.by = system + e.type = "rfc_in_iana_registry" + e.desc = "IANA registries were updated to include %s" % d.display_name() + e.save() + + updated.append(d) + + return updated + + + +def fetch_changes_json(url, start, end): + url += "?start=%s&end=%s" % (urlquote(local_timezone_to_utc(start).strftime("%Y-%m-%d %H:%M:%S")), + urlquote(local_timezone_to_utc(end).strftime("%Y-%m-%d %H:%M:%S"))) + f = urllib2.urlopen(url) + text = f.read() + f.close() + return text + +def parse_changes_json(text): + response = json.loads(text) + + if "error" in response: + raise Exception("IANA server returned error: %s" % response["error"]) + + changes = response["changes"] + + # do some rudimentary validation + for i in changes: + for f in ['doc', 'type', 'time']: + if f not in i: + raise Exception('Error in response: Field %s missing in input: %s - %s' % (f, json.dumps(i), json.dumps(changes))) + + # a little bit of cleaning + i["doc"] = i["doc"].strip() + if i["doc"].startswith("http://www.ietf.org/internet-drafts/"): + i["doc"] = i["doc"][len("http://www.ietf.org/internet-drafts/"):] + + # make sure we process oldest entries first + changes.sort(key=lambda c: c["time"]) + + return changes + +def update_history_with_changes(changes, send_email=True): + """Take parsed changes from IANA and apply them. Note that we + expect to get these in chronologically sorted, otherwise the + change descriptions generated may not be right.""" + + # build up state lookup + states = {} + + slookup = dict((s.slug, s) + for s in State.objects.filter(type=StateType.objects.get(slug="draft-iana-action"))) + states["action"] = { + "": slookup["newdoc"], + "In Progress": slookup["inprog"], + "Open": slookup["inprog"], + "pre-approval In Progress": slookup["inprog"], + "Waiting on Authors": slookup["waitauth"], + "Author": slookup["waitauth"], + "Waiting on ADs": slookup["waitad"], + "Waiting on AD": slookup["waitad"], + "AD": slookup["waitad"], + "Waiting on WGC": slookup["waitwgc"], + "WGC": slookup["waitwgc"], + "Waiting on RFC-Editor": slookup["waitrfc"], + "Waiting on RFC Editor": slookup["waitrfc"], + "RFC-Editor": slookup["waitrfc"], + "RFC-Ed-ACK": slookup["rfcedack"], + "RFC-Editor-ACK": slookup["rfcedack"], + "Completed": slookup["rfcedack"], + "On Hold": slookup["onhold"], + "No IC": slookup["noic"], + } + + slookup = dict((s.slug, s) + for s in State.objects.filter(type=StateType.objects.get(slug="draft-iana-review"))) + states["review"] = { + "IANA Review Needed": slookup["need-rev"], + "IANA OK - Actions Needed": slookup["ok-act"], + "IANA OK - No Actions Needed": slookup["ok-noact"], + "IANA Not OK": slookup["not-ok"], + "Version Changed - Review Needed": slookup["changed"], + } + + # so it turns out IANA has made a mistake and are including some + # wrong states, we'll have to skip those + wrong_action_states = ("Waiting on Reviewer", "Review Complete", "Last Call", + "Last Call - Questions", "Evaluation", "Evaluation - Questions", + "With Reviewer", "IESG Notification Received", "Watiing on Last Call", + "IANA Comments Submitted", "Waiting on Last Call") + + system = Person.objects.get(name="(System)") + + added_events = [] + warnings = [] + + for c in changes: + docname = c['doc'] + timestamp = datetime.datetime.strptime(c["time"], "%Y-%m-%d %H:%M:%S") + timestamp = utc_to_local_timezone(timestamp) # timestamps are in UTC + + if c['type'] in ("iana_state", "iana_review"): + if c['type'] == "iana_state": + kind = "action" + + if c["state"] in wrong_action_states: + warnings.append("Wrong action state '%s' encountered in changes from IANA" % c["state"]) + continue + else: + kind = "review" + + if c["state"] not in states[kind]: + warnings.append("Unknown IANA %s state %s (%s)" % (kind, c["state"], timestamp)) + print "Unknown IANA %s state %s" % (kind, c["state"]) + continue + + state = states[kind][c["state"]] + state_type = "draft-iana-%s" % kind + + e = StateDocEvent.objects.filter(type="changed_state", time=timestamp, + state_type=state_type, state=state) + if not e: + try: + doc = Document.objects.get(docalias__name=docname) + except Document.DoesNotExist: + warnings.append("Document %s not found" % docname) + continue + + # the naive way of extracting prev_state here means + # that we assume these changes are cronologically + # applied + prev_state = doc.get_state(state_type) + e = add_state_change_event(doc, system, prev_state, state, timestamp) + + added_events.append(e) + + if not StateDocEvent.objects.filter(doc=doc, time__gt=timestamp, state_type=state_type): + save_document_in_history(doc) + doc.set_state(state) + + if send_email: + email_state_changed(None, doc, "IANA %s state changed to %s" % (kind, state.name)) + email_owner(None, doc, doc.ad, system, "IANA %s state changed to %s" % (kind, state.name)) + + if doc.time < timestamp: + doc.time = timestamp + doc.save() + + return added_events, warnings + + +def parse_review_email(text): + msg = email.message_from_string(text) + + # doc + doc_name = "" + m = re.search(r"<([^>]+)>", msg["Subject"]) + if m: + doc_name = m.group(1).lower() + if re.search(r"\.\w{3}$", doc_name): # strip off extension + doc_name = doc_name[:-4] + + if re.search(r"-\d{2}$", doc_name): # strip off revision + doc_name = doc_name[:-3] + + # date + review_time = datetime.datetime.now() + if "Date" in msg: + review_time = email_time_to_local_timezone(msg["Date"]) + + # by + by = None + m = re.search(r"\"(.*)\"", msg["From"]) + if m: + name = m.group(1).strip() + if name.endswith(" via RT"): + name = name[:-len(" via RT")] + + try: + by = Person.objects.get(alias__name=name, role__group__acronym="iana") + except Person.DoesNotExist: + pass + + if not by: + by = Person.objects.get(name="(System)") + + # comment + body = msg.get_payload().decode('quoted-printable').replace("\r", "") + b = body.find("(BEGIN IANA LAST CALL COMMENTS)") + e = body.find("(END IANA LAST CALL COMMENTS)") + + comment = body[b + len("(BEGIN IANA LAST CALL COMMENTS)"):e].strip() + + # strip leading IESG: + if comment.startswith("IESG:"): + comment = comment[len("IESG:"):].lstrip() + + # strip ending Thanks, followed by signature + m = re.compile(r"^Thanks,\n\n", re.MULTILINE).search(comment) + if m: + comment = comment[:m.start()].rstrip() + + return doc_name, review_time, by, comment + +def add_review_comment(doc_name, review_time, by, comment): + try: + e = DocEvent.objects.get(doc__name=doc_name, time=review_time, type="iana_review") + except DocEvent.DoesNotExist: + doc = Document.objects.get(name=doc_name) + e = DocEvent(doc=doc, time=review_time, type="iana_review") + + e.desc = comment + e.by = by + + e.save() From 85d2cdddc20d7c0b0abaae273810dcc115bc3ca3 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Mon, 17 Sep 2012 15:57:48 +0000 Subject: [PATCH 06/47] Add POST interface for IANA/RFC Editor for triggering updates, add discrepancies report for seeing differences between the Datatracker/RFC Editor/IANA, add tests of IANA/RFC Editor integration, add script for weekly discrepancies emails - Legacy-Id: 4851 --- ietf/bin/email-sync-discrepancies | 35 ++ ietf/sync/discrepancies.py | 37 ++ ietf/sync/mails.py | 21 + ietf/sync/tests.py | 401 +++++++++++++++++++ ietf/sync/urls.py | 8 + ietf/sync/views.py | 79 ++++ ietf/templates/sync/discrepancies.html | 37 ++ ietf/templates/sync/discrepancies_report.txt | 18 + ietf/templates/sync/update.html | 27 ++ ietf/urls.py | 1 + ietf/utils/test_data.py | 52 +++ 11 files changed, 716 insertions(+) create mode 100755 ietf/bin/email-sync-discrepancies create mode 100644 ietf/sync/discrepancies.py create mode 100644 ietf/sync/mails.py create mode 100644 ietf/sync/tests.py create mode 100644 ietf/sync/urls.py create mode 100644 ietf/sync/views.py create mode 100644 ietf/templates/sync/discrepancies.html create mode 100644 ietf/templates/sync/discrepancies_report.txt create mode 100644 ietf/templates/sync/update.html diff --git a/ietf/bin/email-sync-discrepancies b/ietf/bin/email-sync-discrepancies new file mode 100755 index 000000000..8f01fcbcf --- /dev/null +++ b/ietf/bin/email-sync-discrepancies @@ -0,0 +1,35 @@ +#!/usr/bin/env python + +import sys, os +import syslog + +# boilerplate +basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) +sys.path = [ basedir ] + sys.path + +from ietf import settings +from django.core import management +management.setup_environ(settings) + + +from optparse import OptionParser + +parser = OptionParser() +parser.add_option("-t", "--to", dest="to", + help="Email address to send report to", metavar="EMAIL") + +options, args = parser.parse_args() + + +syslog.openlog(os.path.basename(__file__), syslog.LOG_PID, syslog.LOG_LOCAL0) + +from ietf.sync.mails import email_discrepancies + +receivers = ["iesg-secretary@ietf.org"] + +if options.to: + receivers = [options.to] + +email_discrepancies(receivers) + +syslog.syslog("Emailed sync discrepancies to %s" % receivers) diff --git a/ietf/sync/discrepancies.py b/ietf/sync/discrepancies.py new file mode 100644 index 000000000..9c0db606d --- /dev/null +++ b/ietf/sync/discrepancies.py @@ -0,0 +1,37 @@ +from ietf.doc.models import * + +def find_discrepancies(): + res = [] + + title = "Drafts that have been sent to the RFC Editor but do not have an RFC Editor state" + + docs = Document.objects.filter(states__in=list(State.objects.filter(type="draft-iesg", slug__in=("ann", "rfcqueue")))).exclude(states__in=list(State.objects.filter(type="draft-rfceditor"))) + + res.append((title, docs)) + + title = "Drafts that have the IANA Action state \"In Progress\" but do not have a \"IANA\" RFC-Editor state/tag" + + docs = Document.objects.filter(states__in=list(State.objects.filter(type="draft-iana-action", slug__in=("inprog",)))).exclude(tags="iana").exclude(states__in=list(State.objects.filter(type="draft-rfceditor", slug="iana"))) + + res.append((title, docs)) + + title = "Drafts that have the IANA Action state \"Waiting on RFC Editor\" or \"RFC-Ed-Ack\" but are in the RFC Editor state \"IANA\"/tagged with \"IANA\"" + + docs = Document.objects.filter(states__in=list(State.objects.filter(type="draft-iana-action", slug__in=("waitrfc", "rfcedack")))).filter(models.Q(tags="iana") | models.Q(states__in=list(State.objects.filter(type="draft-rfceditor", slug="iana")))) + + res.append((title, docs)) + + title = "Drafts that have a state other than \"RFC Ed Queue\", \"RFC Published\" or \"Sent to the RFC Editor\" and have an RFC Editor or IANA Action state" + + docs = Document.objects.exclude(states__in=list(State.objects.filter(type="draft-iesg", slug__in=("rfcqueue", "pub"))) + list(State.objects.filter(type__in=("draft-stream-iab", "draft-stream-ise", "draft-stream-irtf"), slug="rfc-edit"))).filter(states__in=list(State.objects.filter(type__in=("draft-iana-action", "draft-rfceditor")))) + + res.append((title, docs)) + + for _, docs in res: + for d in docs: + d.iesg_state = d.get_state("draft-iesg") + d.rfc_state = d.get_state("draft-rfceditor") + d.iana_action_state = d.get_state("draft-iana-action") + + return res + diff --git a/ietf/sync/mails.py b/ietf/sync/mails.py new file mode 100644 index 000000000..480fd1b40 --- /dev/null +++ b/ietf/sync/mails.py @@ -0,0 +1,21 @@ +from django.core.urlresolvers import reverse as urlreverse +from django.conf import settings + +from ietf.utils.mail import send_mail + +from ietf.sync.discrepancies import find_discrepancies + +def email_discrepancies(receivers): + sections = find_discrepancies() + + send_mail(None, + receivers, + None, + "Datatracker Sync Discrepancies Report", + "sync/discrepancies_report.txt", + dict(sections=sections, + url=settings.IDTRACKER_BASE_URL + urlreverse("ietf.sync.views.discrepancies"), + base_url=settings.IDTRACKER_BASE_URL, + )) + + diff --git a/ietf/sync/tests.py b/ietf/sync/tests.py new file mode 100644 index 000000000..8aa97da85 --- /dev/null +++ b/ietf/sync/tests.py @@ -0,0 +1,401 @@ +import unittest, re, json, datetime, StringIO +import django.test +from django.conf import settings +from django.core.urlresolvers import reverse as urlreverse + +from ietf.utils.mail import outbox +from ietf.utils.test_data import make_test_data +from ietf.utils.test_utils import login_testing_unauthorized + +from ietf.doc.models import * +from ietf.person.models import * + +from ietf.sync import iana, rfceditor + +from pyquery import PyQuery + +class IANASyncTestCase(django.test.TestCase): + fixtures = ['names'] + + def test_protocol_page_sync(self): + draft = make_test_data() + DocAlias.objects.create(name="rfc1234", document=draft) + DocEvent.objects.create(doc=draft, type="published_rfc", by=Person.objects.get(name="(System)")) + + rfc_names = iana.parse_protocol_page('RFC 1234') + self.assertEqual(len(rfc_names), 1) + self.assertEqual(rfc_names[0], "rfc1234") + + iana.update_rfc_log_from_protocol_page(rfc_names, datetime.datetime.now() - datetime.timedelta(days=1)) + self.assertEqual(DocEvent.objects.filter(doc=draft, type="rfc_in_iana_registry").count(), 1) + + # make sure it doesn't create duplicates + iana.update_rfc_log_from_protocol_page(rfc_names, datetime.datetime.now() - datetime.timedelta(days=1)) + self.assertEqual(DocEvent.objects.filter(doc=draft, type="rfc_in_iana_registry").count(), 1) + + def test_changes_sync(self): + draft = make_test_data() + + data = json.dumps({ + "changes": [ + { + "time": "2011-10-09 12:00:01", + "doc": draft.name, + "state": "IANA Not OK", + "type": "iana_review", + }, + { + "time": "2011-10-09 12:00:00", + "doc": draft.name, + "state": "Waiting on RFC-Editor", + "type": "iana_state", + }, + { + "time": "2011-10-09 11:00:00", + "doc": draft.name, + "state": "In Progress", + "type": "iana_state", + } + ] + }) + + changes = iana.parse_changes_json(data) + # check sorting + self.assertEqual(changes[0]["time"], "2011-10-09 11:00:00") + + mailbox_before = len(outbox) + added_events, warnings = iana.update_history_with_changes(changes) + + self.assertEqual(len(added_events), 3) + self.assertEqual(len(warnings), 0) + self.assertEqual(draft.get_state_slug("draft-iana-review"), "not-ok") + self.assertEqual(draft.get_state_slug("draft-iana-action"), "waitrfc") + e = draft.latest_event(StateDocEvent, type="changed_state", state_type="draft-iana-action") + self.assertEqual(e.desc, "IANA Action state changed to Waiting on RFC Editor from In Progress") + self.assertEqual(e.time, datetime.datetime(2011, 10, 9, 5, 0)) # check timezone handling + self.assertEqual(len(outbox), mailbox_before + 3 * 2) + + # make sure it doesn't create duplicates + added_events, warnings = iana.update_history_with_changes(changes) + self.assertEqual(len(added_events), 0) + self.assertEqual(len(warnings), 0) + + def test_changes_sync_errors(self): + draft = make_test_data() + + # missing "type" + data = json.dumps({ + "changes": [ + { + "time": "2011-10-09 12:00:01", + "doc": draft.name, + "state": "IANA Not OK", + }, + ] + }) + + self.assertRaises(Exception, iana.parse_changes_json, data) + + # error response + data = json.dumps({ + "error": "I am in error." + }) + + self.assertRaises(Exception, iana.parse_changes_json, data) + + # missing document from database + data = json.dumps({ + "changes": [ + { + "time": "2011-10-09 12:00:01", + "doc": "draft-this-does-not-exist", + "state": "IANA Not OK", + "type": "iana_review", + }, + ] + }) + + changes = iana.parse_changes_json(data) + added_events, warnings = iana.update_history_with_changes(changes) + self.assertEqual(len(added_events), 0) + self.assertEqual(len(warnings), 1) + + def test_iana_review_mail(self): + draft = make_test_data() + + msg = """From: "%(person)s via RT" +Date: Thu, 10 May 2012 12:00:00 +0000 +Subject: [IANA #12345] Last Call: <%(draft)s-%(rev)s.txt> (Long text) to Informational RFC +(BEGIN IANA LAST CALL COMMENTS) + +IESG: + +IANA has reviewed %(draft)s-%(rev)s, which is=20 +currently in Last Call, and has the following comments: + +IANA understands that, upon approval of this document, there are no=20 +IANA Actions that need completion. + +Thanks, + +%(person)s +IANA Fake Test Person +ICANN + +(END IANA LAST CALL COMMENTS) +""" + + msg = msg % dict(person=Person.objects.get(user__username="iana").name, + draft=draft.name, + rev=draft.rev) + + doc_name, review_time, by, comment = iana.parse_review_email(msg) + + self.assertEqual(doc_name, draft.name) + self.assertEqual(review_time, datetime.datetime(2012, 5, 10, 5, 0, 0)) + self.assertEqual(by, Person.objects.get(user__username="iana")) + self.assertTrue("there are no IANA Actions" in comment.replace("\n", "")) + + iana.add_review_comment(doc_name, review_time, by, comment) + + e = draft.latest_event(type="iana_review") + self.assertTrue(e) + self.assertEqual(e.desc, comment) + self.assertEqual(e.by, by) + + # make sure it doesn't create duplicates + iana.add_review_comment(doc_name, review_time, by, comment) + self.assertEqual(DocEvent.objects.filter(doc=draft, type="iana_review").count(), 1) + + +class RFCSyncTestCase(django.test.TestCase): + fixtures = ['names'] + + def test_rfc_index(self): + doc = make_test_data() + doc.set_state(State.objects.get(type="draft-iesg", slug="rfcqueue")) + # it's a bit strange to have this set when draft-iesg is set + # too, but for testing purposes ... + doc.set_state(State.objects.get(type="draft-stream-ise", slug="rfc-edit")) + + updated_doc = Document.objects.create(name="draft-ietf-something") + DocAlias.objects.create(name=updated_doc.name, document=updated_doc) + DocAlias.objects.create(name="rfc123", document=updated_doc) + + today = datetime.date.today() + + t = ''' + + + BCP0001 + + RFC1234 + RFC2345 + + + + FYI0001 + + RFC1234 + + + + STD0001 + Test + + RFC1234 + + + + RFC1234 + A Testing RFC + + A. Irector + + + %(month)s + %(year)s + + + ASCII + 12345 + 42 + + + test + +

This is some interesting text.

+ %(name)s-%(rev)s + + RFC123 + + + BCP0001 + + PROPOSED STANDARD + PROPOSED STANDARD + IETF + %(area)s + %(group)s + http://www.rfc-editor.org/errata_search.php?rfc=1234 +
+
''' % dict(year=today.strftime("%Y"), + month=today.strftime("%B"), + name=doc.name, + rev=doc.rev, + area=doc.group.parent.acronym, + group=doc.group.acronym) + + data = rfceditor.parse_index(StringIO.StringIO(t)) + self.assertEqual(len(data), 1) + + rfc_number, title, authors, rfc_published_date, current_status, updates, updated_by, obsoletes, obsoleted_by, also, draft, has_errata, stream, wg, file_formats, pages, abstract = data[0] + + # currently, we only check what we actually use + self.assertEqual(rfc_number, 1234) + self.assertEqual(title, "A Testing RFC") + self.assertEqual(rfc_published_date.year, today.year) + self.assertEqual(rfc_published_date.month, today.month) + self.assertEqual(current_status, "Proposed Standard") + self.assertEqual(updates, ["RFC123"]) + self.assertEqual(set(also), set(["BCP0001", "FYI0001", "STD0001"])) + self.assertEqual(draft, doc.name) + self.assertEqual(wg, doc.group.acronym) + self.assertEqual(has_errata, True) + self.assertEqual(stream, "IETF") + self.assertEqual(pages, "42") + self.assertEqual(abstract, "This is some interesting text.") + + + mailbox_before = len(outbox) + + changed = rfceditor.update_docs_from_rfc_index(data, today - datetime.timedelta(days=30)) + + doc = Document.objects.get(name=doc.name) + + self.assertEqual(doc.docevent_set.all()[0].type, "published_rfc") + self.assertEqual(doc.docevent_set.all()[0].time.date(), today) + self.assertTrue("errata" in doc.tags.all().values_list("slug", flat=True)) + self.assertTrue(DocAlias.objects.filter(name="rfc1234", document=doc)) + self.assertTrue(DocAlias.objects.filter(name="bcp0001", document=doc)) + self.assertTrue(DocAlias.objects.filter(name="fyi0001", document=doc)) + self.assertTrue(DocAlias.objects.filter(name="std0001", document=doc)) + self.assertTrue(RelatedDocument.objects.filter(source=doc, target__name="rfc123", relationship="updates")) + self.assertEqual(doc.title, "A Testing RFC") + self.assertEqual(doc.abstract, "This is some interesting text.") + self.assertEqual(doc.get_state_slug(), "rfc") + self.assertEqual(doc.get_state_slug("draft-iesg"), "pub") + self.assertEqual(doc.get_state_slug("draft-stream-ise"), "pub") + self.assertEqual(doc.std_level_id, "ps") + self.assertEqual(doc.pages, 42) + + # make sure we can apply it again with no changes + changed = rfceditor.update_docs_from_rfc_index(data, today - datetime.timedelta(days=30)) + self.assertEquals(len(changed), 0) + + + def test_rfc_queue(self): + draft = make_test_data() + + draft.set_state(State.objects.get(type="draft-iesg", slug="ann")) + + t = ''' +
+ +%(name)s-%(rev)s.txt +2010-09-08 +EDIT*R*A(1G) +http://www.rfc-editor.org/auth48/rfc1234 + +%(ref)s +IN-QUEUE + +A. Author + +%(title)s + +10000000 +%(group)s + +
+
''' % dict(name=draft.name, + rev=draft.rev, + title=draft.title, + group=draft.group.name, + ref="draft-ietf-test") + + drafts, warnings = rfceditor.parse_queue(StringIO.StringIO(t)) + self.assertEqual(len(drafts), 1) + self.assertEqual(len(warnings), 0) + + draft_name, date_received, state, tags, missref_generation, stream, auth48, cluster, refs = drafts[0] + + # currently, we only check what we actually use + self.assertEqual(draft_name, draft.name) + self.assertEqual(state, "EDIT") + self.assertEqual(set(tags), set(["iana", "ref"])) + self.assertEqual(auth48, "http://www.rfc-editor.org/auth48/rfc1234") + + + mailbox_before = len(outbox) + + changed, warnings = rfceditor.update_drafts_from_queue(drafts) + self.assertEqual(len(changed), 1) + self.assertEqual(len(warnings), 0) + + self.assertEqual(draft.get_state_slug("draft-rfceditor"), "edit") + self.assertEqual(set(draft.tags.all()), set(DocTagName.objects.filter(slug__in=("iana", "ref")))) + self.assertEqual(draft.docevent_set.all()[0].type, "changed_state") + self.assertEqual(draft.docevent_set.all()[1].type, "rfc_editor_received_announcement") + + self.assertEqual(len(outbox), mailbox_before + 1) + self.assertTrue("RFC Editor queue" in outbox[-1]["Subject"]) + + # make sure we can apply it again with no changes + changed, warnings = rfceditor.update_drafts_from_queue(drafts) + self.assertEquals(len(changed), 0) + self.assertEquals(len(warnings), 0) + +class DiscrepanciesTestCase(django.test.TestCase): + fixtures = ['names'] + + def test_discrepancies(self): + make_test_data() + + # draft approved but no RFC Editor state + doc = Document.objects.create(name="draft-ietf-test1", type_id="draft") + doc.set_state(State.objects.get(type="draft-iesg", slug="ann")) + + r = self.client.get(urlreverse("ietf.sync.views.discrepancies")) + self.assertTrue(doc.name in r.content) + + # draft with IANA state "In Progress" but RFC Editor state not IANA + doc = Document.objects.create(name="draft-ietf-test2", type_id="draft") + doc.set_state(State.objects.get(type="draft-iesg", slug="rfcqueue")) + doc.set_state(State.objects.get(type="draft-iana-action", slug="inprog")) + doc.set_state(State.objects.get(type="draft-rfceditor", slug="auth")) + + r = self.client.get(urlreverse("ietf.sync.views.discrepancies")) + self.assertTrue(doc.name in r.content) + + # draft with IANA state "Waiting on RFC Editor" or "RFC-Ed-Ack" + # but RFC Editor state is IANA + doc = Document.objects.create(name="draft-ietf-test3", type_id="draft") + doc.set_state(State.objects.get(type="draft-iesg", slug="rfcqueue")) + doc.set_state(State.objects.get(type="draft-iana-action", slug="waitrfc")) + doc.set_state(State.objects.get(type="draft-rfceditor", slug="iana")) + + r = self.client.get(urlreverse("ietf.sync.views.discrepancies")) + self.assertTrue(doc.name in r.content) + + # draft with state other than "RFC Ed Queue" or "RFC Published" + # that are in RFC Editor or IANA queues + doc = Document.objects.create(name="draft-ietf-test4", type_id="draft") + doc.set_state(State.objects.get(type="draft-iesg", slug="ann")) + doc.set_state(State.objects.get(type="draft-rfceditor", slug="auth")) + + r = self.client.get(urlreverse("ietf.sync.views.discrepancies")) + self.assertTrue(doc.name in r.content) diff --git a/ietf/sync/urls.py b/ietf/sync/urls.py new file mode 100644 index 000000000..f99cc76c1 --- /dev/null +++ b/ietf/sync/urls.py @@ -0,0 +1,8 @@ +from django.conf.urls.defaults import patterns, url + +urlpatterns = patterns('', + url(r'^discrepancies/$', 'ietf.sync.views.discrepancies'), + url(r'^iana/update/$', 'ietf.sync.views.update_iana'), + url(r'^rfc-editor/update/$', 'ietf.sync.views.update_rfc_editor'), +) + diff --git a/ietf/sync/views.py b/ietf/sync/views.py new file mode 100644 index 000000000..c6b9c7fc1 --- /dev/null +++ b/ietf/sync/views.py @@ -0,0 +1,79 @@ +import subprocess, os + +from django.http import HttpResponse +from django.shortcuts import render_to_response +from django.template import RequestContext +from django.template.loader import render_to_string +from django import forms +from django.db.models import Q + +from ietf.ietfauth.decorators import role_required +from ietf.doc.models import * +from ietf.sync import iana, rfceditor +from ietf.sync.discrepancies import find_discrepancies + +SYNC_BIN_PATH = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "../bin")) + +#@role_required('Secretariat', 'IANA', 'RFC Editor') +def discrepancies(request): + sections = find_discrepancies() + + return render_to_response("sync/discrepancies.html", + dict(sections=sections), + context_instance=RequestContext(request)) + + +class UpdateIanaForm(forms.Form): + protocols_page = forms.BooleanField(initial=False, required=False, help_text="For when a reference to an RFC has been added to the IANA protocols page" % iana.PROTOCOLS_URL) + changes = forms.BooleanField(initial=False, required=False, help_text="For new changes at the changes JSON dump" % iana.CHANGES_URL) + +def update_iana(request): + if request.method == 'POST': + form = UpdateIanaForm(request.POST) + if form.is_valid(): + failed = False + if form.cleaned_data["protocols_page"]: + failed = failed or subprocess.call(["python", os.path.join(SYNC_BIN_PATH, "iana-protocols-updates")]) + if form.cleaned_data["changes"]: + failed = failed or subprocess.call(["python", os.path.join(SYNC_BIN_PATH, "iana-changes-updates")]) + + if failed: + return HttpResponse("FAIL") + else: + return HttpResponse("OK") + else: + form = UpdateIanaForm() + + return render_to_response('sync/update.html', + dict(form=form, + org="IANA", + ), + context_instance=RequestContext(request)) + + +class UpdateRFCEditorForm(forms.Form): + queue = forms.BooleanField(initial=False, required=False, help_text="For when queue2.xml has been updated" % rfceditor.QUEUE_URL) + index = forms.BooleanField(initial=False, required=False, help_text="For when rfc-index.xml has been updated" % rfceditor.INDEX_URL) + +def update_rfc_editor(request): + if request.method == 'POST': + form = UpdateRFCEditorForm(request.POST) + if form.is_valid(): + failed = False + if form.cleaned_data["queue"]: + failed = failed or subprocess.call(["python", os.path.join(SYNC_BIN_PATH, "rfc-editor-queue-updates")]) + if form.cleaned_data["index"]: + failed = failed or subprocess.call(["python", os.path.join(SYNC_BIN_PATH, "rfc-editor-index-updates")]) + + if failed: + return HttpResponse("FAIL") + else: + return HttpResponse("OK") + else: + form = UpdateRFCEditorForm() + + return render_to_response('sync/update.html', + dict(form=form, + org="RFC Editor", + ), + context_instance=RequestContext(request)) diff --git a/ietf/templates/sync/discrepancies.html b/ietf/templates/sync/discrepancies.html new file mode 100644 index 000000000..fdf966e0e --- /dev/null +++ b/ietf/templates/sync/discrepancies.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} + +{% block title %}Sync discrepancies{% endblock %} + +{% block morecss %} +table.discrepancies td { padding-right: 0.2em; } +{% endblock %} + +{% block content %} +

Sync discrepancies

+ +{% for title, docs in sections %} +

{{ title }}

+ +{% if docs %} + + + + + + + +{% for d in docs %} + + + + + + +{% endfor %} +
Draft NameIESG stateRFC Editor stateIANA Action state
{{ d.name }}{{ d.iesg_state|default:"-" }}{{ d.rfc_state|default:"-" }}{{ d.iana_action_state|default:"-" }}
+{% else %} +

None found.

+{% endif %} + +{% endfor %} +{% endblock %} diff --git a/ietf/templates/sync/discrepancies_report.txt b/ietf/templates/sync/discrepancies_report.txt new file mode 100644 index 000000000..0fb324668 --- /dev/null +++ b/ietf/templates/sync/discrepancies_report.txt @@ -0,0 +1,18 @@ +{% autoescape off %} +This is an automated report of current Datatracker sync discrepancies, +also available at: + +{{ url }} +{% for title, docs in sections %} + +{{ title|wordwrap:73 }} +{% if docs %}{% for d in docs %} + {{ d.name }} + IESG: {{ d.iesg_state|default:"-" }} + RFC Ed: {{ d.rfc_state|default:"-" }} + IANA: {{ d.iana_action_state|default:"-" }} + {{ base_url }}{{ d.get_absolute_url }} +{% endfor %}{% else %} +None found. +{% endif %}{% endfor %} +{% endautoescape %} diff --git a/ietf/templates/sync/update.html b/ietf/templates/sync/update.html new file mode 100644 index 000000000..86b6a73f8 --- /dev/null +++ b/ietf/templates/sync/update.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} + +{% block title %}Trigger sync for {{ org }}{% endblock %} + +{% block morecss %} +.sync-form .help { font-style: italic; padding-left: 2em; } +.sync-form input[type=submit] { margin-top: 1em; } +{% endblock %} + + +{% block content %} +

Trigger sync for {{ org }}

+ +

Update the Datatracker with information from {{ org }}. Select +which parts to trigger a sync for:

+ +
+{% for field in form %} +
+ {{ field }} + {{ field.label_tag }} + {% if field.help_text %}{{ field.help_text|safe }}{% endif %} +
+{% endfor %} + +
+{% endblock %} diff --git a/ietf/urls.py b/ietf/urls.py index 3edfa9388..0f2bce922 100644 --- a/ietf/urls.py +++ b/ietf/urls.py @@ -66,6 +66,7 @@ urlpatterns = patterns('', (r'^submit/', include('ietf.submit.urls')), (r'^streams/', include('ietf.ietfworkflows.urls')), (r'^community/', include('ietf.community.urls')), + (r'^sync/', include('ietf.sync.urls')), (r'^$', 'ietf.idrfc.views.main'), (r'^admin/doc/', include('django.contrib.admindocs.urls')), diff --git a/ietf/utils/test_data.py b/ietf/utils/test_data.py index 7e22f88cf..1c8fd9bfe 100644 --- a/ietf/utils/test_data.py +++ b/ietf/utils/test_data.py @@ -114,6 +114,22 @@ def make_test_data(): ascii="(System)", address="", ) + + # IANA and RFC Editor groups + iana = Group.objects.create( + name="IANA", + acronym="iana", + state_id="active", + type_id="ietf", + parent=None, + ) + rfc_editor = Group.objects.create( + name="RFC Editor", + acronym="rfc-edit", + state_id="active", + type_id="ietf", + parent=None, + ) if system_person.id != 0: # work around bug in Django Person.objects.filter(id=system_person.id).update(id=0) @@ -248,6 +264,42 @@ def make_test_data(): email=email, ) + # IANA user + u = User.objects.create(username="iana") + p = Person.objects.create( + name="Ina Iana", + ascii="Ina Iana", + user=u) + Alias.objects.create( + name=p.name, + person=p) + email = Email.objects.create( + address="iana@ia.na", + person=p) + Role.objects.create( + name_id="auth", + group=iana, + email=email, + person=p, + ) + + # RFC Editor user + u = User.objects.create(username="rfc") + p = Person.objects.create( + name="Rfc Editor", + ascii="Rfc Editor", + user=u) + email = Email.objects.create( + address="rfc@edit.or", + person=p) + Role.objects.create( + name_id="auth", + group=rfc_editor, + email=email, + person=p, + ) + + # draft draft = Document.objects.create( name="draft-ietf-mars-test", From 0ec0fa58f3d6645aa09e03309f5b39d0ded643b6 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Mon, 17 Sep 2012 15:59:35 +0000 Subject: [PATCH 07/47] Update IESG agenda pages with IANA Review state, consensus and last call expiration (if applicable) - Legacy-Id: 4852 --- ietf/iesg/views.py | 47 +++++++++++++++++++++++------ ietf/templates/iesg/agenda.html | 1 + ietf/templates/iesg/agenda_doc.html | 15 +++++++++ ietf/templates/iesg/agenda_doc.txt | 7 +++-- ietf/templates/iesg/scribe_doc.html | 11 ++++++- 5 files changed, 69 insertions(+), 12 deletions(-) diff --git a/ietf/iesg/views.py b/ietf/iesg/views.py index 40e4a99c4..cc9cf37a6 100644 --- a/ietf/iesg/views.py +++ b/ietf/iesg/views.py @@ -56,7 +56,7 @@ from ietf.idrfc.utils import update_telechat from ietf.ietfauth.decorators import group_required from ietf.idtracker.templatetags.ietf_filters import in_group from ietf.ipr.models import IprDocAlias -from ietf.doc.models import Document, TelechatDocEvent +from ietf.doc.models import Document, TelechatDocEvent, LastCallDocEvent, ConsensusDocEvent from ietf.group.models import Group def date_threshold(): @@ -193,20 +193,33 @@ if settings.USE_DB_REDESIGN_PROXY_CLASSES: get_doc_section = get_doc_sectionREDESIGN def agenda_docs(date, next_agenda): - from ietf.doc.models import TelechatDocEvent - - matches = Document.objects.filter(docevent__telechatdocevent__telechat_date=date).distinct() + matches = Document.objects.filter(docevent__telechatdocevent__telechat_date=date).select_related("stream").distinct() docmatches = [] - for m in matches: - if m.latest_event(TelechatDocEvent, type="scheduled_for_telechat").telechat_date != date: + for doc in matches: + if doc.latest_event(TelechatDocEvent, type="scheduled_for_telechat").telechat_date != date: continue - e = m.latest_event(type="started_iesg_process") - m.balloting_started = e.time if e else datetime.datetime.min + e = doc.latest_event(type="started_iesg_process") + doc.balloting_started = e.time if e else datetime.datetime.min - docmatches.append(m) + if doc.type_id == "draft": + s = doc.get_state("draft-iana-review") + if s and s.slug in ("not-ok", "changed", "need-rev"): + doc.iana_review_state = str(s) + + if doc.get_state_slug("draft-iesg") == "lc": + e = doc.latest_event(LastCallDocEvent, type="sent_last_call") + if e: + doc.lastcall_expires = e.expires + + doc.consensus = "Unknown" + e = doc.latest_event(ConsensusDocEvent, type="changed_consensus") + if e: + doc.consensus = "Yes" if e.consensus else "No" + + docmatches.append(doc) res = dict(("s%s%s%s" % (i, j, k), []) for i in range(2, 5) for j in range (1, 4) for k in range(1, 4)) for id in docmatches: @@ -314,6 +327,22 @@ def _agenda_json(request, date=None): if defer: docinfo['defer-by'] = defer.by.name docinfo['defer-at'] = str(defer.time) + + if doc.type_id == "draft": + iana_state = doc.get_state("draft-iana-review") + if iana_state.slug in ("not-ok", "changed", "need-rev"): + docinfo['iana_review_state'] = str(iana_state) + + if doc.get_state_slug("draft-iesg") == "lc": + e = doc.latest_event(LastCallDocEvent, type="sent_last_call") + if e: + docinfo['lastcall_expires'] = e.expires + + docinfo['consensus'] = None + e = doc.latest_event(ConsensusDocEvent, type="changed_consensus") + if e: + docinfo['consensus'] = e.consensus + data['sections'][s]['docs'] += [docinfo, ] wgs = agenda_wg_actions(date) diff --git a/ietf/templates/iesg/agenda.html b/ietf/templates/iesg/agenda.html index d08f5645f..012ae6b79 100644 --- a/ietf/templates/iesg/agenda.html +++ b/ietf/templates/iesg/agenda.html @@ -46,6 +46,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #} .agenda blockquote { margin-left: 30px; width: 70ex; font-style:italic;} table.agenda-doc { margin-left: 30px; margin-top:0.5em; margin-bottom: 0.5em; width: 95%; } table.agenda-doc > tbody > tr { vertical-align:top; } +.agenda .stream { padding-left: 0.5em; } {% endblock morecss %} {% block pagehead %} diff --git a/ietf/templates/iesg/agenda_doc.html b/ietf/templates/iesg/agenda_doc.html index ec9530ca5..da89ae2e8 100644 --- a/ietf/templates/iesg/agenda_doc.html +++ b/ietf/templates/iesg/agenda_doc.html @@ -106,6 +106,8 @@ Some parts Copyright (c) 2009 The IETF Trust, all rights reserved. {% endif %} {% endwith %} +{% if doc.obj.stream %} - {{ doc.obj.stream }} stream{% endif %} +
{{ doc.obj.title|escape }} ({{ doc.obj.intended_std_level }}) @@ -132,6 +134,19 @@ Some parts Copyright (c) 2009 The IETF Trust, all rights reserved.
Was deferred by {{defer.by}} on {{defer.time|date:"Y-m-d"}} {% endif %} {% endwith %} + +{% if doc.obj.iana_review_state %} +
IANA Review: {{ doc.obj.iana_review_state }} +{% endif %} + +{% if doc.obj.consensus %} +
Consensus: {{ doc.obj.consensus }} +{% endif %} + +{% if doc.obj.lastcall_expires %} +
Last call expires: {{ doc.obj.lastcall_expires|date:"Y-m-d" }} +{% endif %} + {% ballot_icon doc.obj %} diff --git a/ietf/templates/iesg/agenda_doc.txt b/ietf/templates/iesg/agenda_doc.txt index 143fe8632..abb6706db 100644 --- a/ietf/templates/iesg/agenda_doc.txt +++ b/ietf/templates/iesg/agenda_doc.txt @@ -39,10 +39,13 @@ Some parts Copyright (c) 2009 The IETF Trust, all rights reserved. {{ title2 }} {% endif %}{{ title3 }} {% for doc in section_docs %}{% with doc.obj.rfc_number as rfc_number %} - o {{doc.obj.canonical_name}}{% if not rfc_number %}-{{doc.obj.rev}}{% endif %}{% endwith %} + o {{doc.obj.canonical_name}}{% if not rfc_number %}-{{doc.obj.rev}}{% endif %}{% endwith %}{% if doc.obj.stream %} - {{ doc.obj.stream }} stream{% endif %} {% filter wordwrap:"68"|indent|indent %}{{ doc.obj.title }} ({{ doc.obj.intended_std_level }}){% endfilter %} {% if doc.obj.note %}{# note: note is not escaped #} {% filter wordwrap:"68"|indent|indent %}Note: {{ doc.obj.note|striptags }}{% endfilter %} -{% endif %} Token: {{ doc.obj.ad }} +{% endif %} Token: {{ doc.obj.ad }}{% if doc.obj.iana_review_state %} + IANA Review: {{ doc.obj.iana_review_state }}{% endif %}{% if doc.obj.consensus %} + Consensus: {{ doc.obj.consensus }}{% endif %}{% if doc.obj.lastcall_expires %} + Last call expires: {{ doc.obj.lastcall_expires|date:"Y-m-d" }}{% endif %} {% with doc.obj.active_defer_event as defer %}{% if defer %} Was deferred by {{defer.by}} on {{defer.time|date:"Y-m-d"}}{% endif %}{% endwith %} {% empty %} NONE diff --git a/ietf/templates/iesg/scribe_doc.html b/ietf/templates/iesg/scribe_doc.html index 9ef60865a..32570a1e0 100644 --- a/ietf/templates/iesg/scribe_doc.html +++ b/ietf/templates/iesg/scribe_doc.html @@ -53,10 +53,19 @@ Some parts Copyright (c) 2009 The IETF Trust, all rights reserved. [txt] {% endif %} {% endwith %} + + {% if doc.obj.stream %} - {{ doc.obj.stream }} stream{% endif %} +
Token: {{ doc.obj.ad|escape }} ({{doc.obj.area_acronym}} area) - {% if doc.obj.note %}{# note: note is not escaped #} + {% if doc.obj.note %}{# note: note is not escaped #}
Note: {{ doc.obj.note|safe }} {% endif %} + {% if doc.obj.iana_review_state %} +
IANA Review: {{ doc.obj.iana_review_state }} + {% endif %} + {% if doc.obj.consensus %} +
Consensus: {{ doc.obj.consensus }} + {% endif %} {% for ipr in doc.obj.ipr %} {% ifequal ipr.ipr.status 1 %}
IPR: {{ ipr.ipr.title|escape }} From 2ada703a7f6604ab478cfdecd9bc575431b88c0f Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Mon, 17 Sep 2012 15:59:55 +0000 Subject: [PATCH 08/47] Add optional argument to indent filter - Legacy-Id: 4853 --- ietf/idtracker/templatetags/ietf_filters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ietf/idtracker/templatetags/ietf_filters.py b/ietf/idtracker/templatetags/ietf_filters.py index 73220b7d5..1006ca197 100644 --- a/ietf/idtracker/templatetags/ietf_filters.py +++ b/ietf/idtracker/templatetags/ietf_filters.py @@ -31,8 +31,8 @@ def format_charter(value): return value.replace("\n\n", "

").replace("\n","
\n") @register.filter(name='indent') -def indent(value): - return value.replace("\n", "\n "); +def indent(value, amount=2): + return value.replace("\n", "\n" + " " * amount); @register.filter(name='parse_email_list') def parse_email_list(value): From fa6aa1f67271ec6d5a5958dc0f3660cd0dc0377e Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Mon, 17 Sep 2012 16:01:16 +0000 Subject: [PATCH 09/47] Add link to discrepancies to the left menu for IANA, RFC Editor and Secretariat - Legacy-Id: 4854 --- ietf/templates/base_leftmenu.html | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/ietf/templates/base_leftmenu.html b/ietf/templates/base_leftmenu.html index f1d7919e6..12b0a591a 100644 --- a/ietf/templates/base_leftmenu.html +++ b/ietf/templates/base_leftmenu.html @@ -44,14 +44,13 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

  • Next Telechat
  • Discusses
  • Working Groups
  • - {# FIXME: wgcharter
  • Working Groups
  • #} {% endif %} {% if user|in_group:"Secretariat" %}
  • Secretariat
  • Telechat Dates
  • Management Items
  • Working Groups
  • - {# FIXME: wgcharter
  • Working Groups
  • #} +
  • Sync discrepancies {% endif %} {% if user %} {% get_user_managed_streams user as stream_list %} @@ -62,6 +61,14 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. {% endfor %} {% endif %} {% endif %} +{% if user|has_role:"IANA" %} +
  • IANA
  • +
  • Sync discrepancies
  • +{% endif %} +{% if user|has_role:"RFC Editor" %} +
  • RFC Editor
  • +
  • Sync discrepancies
  • +{% endif %}
  • Working Groups
  • From 513c2bcf4cc52974e50dfc4310a700ec4d16f84d Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Mon, 17 Sep 2012 16:06:21 +0000 Subject: [PATCH 10/47] Handle IANA Review state intelligently upon draft submission, email IRSG/ISE/IAB on uploads to alternate streams, email RFC Editor in case a draft under publication has a new upload - Legacy-Id: 4855 --- ietf/submit/tests.py | 20 ++++++++++++----- ietf/submit/utils.py | 53 +++++++++++++++++++++++++++++--------------- 2 files changed, 50 insertions(+), 23 deletions(-) diff --git a/ietf/submit/tests.py b/ietf/submit/tests.py index 7a980d8d9..09b1116b5 100644 --- a/ietf/submit/tests.py +++ b/ietf/submit/tests.py @@ -16,7 +16,7 @@ from ietf.utils.mail import outbox from ietf.person.models import Person, Email from ietf.group.models import Group, Role -from ietf.doc.models import Document, BallotDocEvent, BallotPositionDocEvent +from ietf.doc.models import * from ietf.submit.models import IdSubmissionDetail, Preapproval class SubmitTestCase(django.test.TestCase): @@ -161,6 +161,16 @@ class SubmitTestCase(django.test.TestCase): # submit new revision of existing -> supply submitter info -> confirm draft = make_test_data() + # pretend IANA reviewed it + draft.set_state(State.objects.get(type="draft-iana-review", slug="not-ok")) + + # pretend it was approved to check that we notify the RFC Editor + e = DocEvent(type="iesg_approved", doc=draft) + e.time = draft.time + e.by = Person.objects.get(name="(System)") + e.desc = "The IESG approved the document" + e.save() + # make a discuss to see if the AD gets an email ballot_position = BallotPositionDocEvent() ballot_position.ballot = draft.latest_event(BallotDocEvent, type="created_ballot") @@ -214,16 +224,16 @@ class SubmitTestCase(django.test.TestCase): draft = Document.objects.get(docalias__name=name) self.assertEquals(draft.rev, rev) - new_revision = draft.latest_event() - self.assertEquals(new_revision.type, "new_revision") - self.assertEquals(new_revision.by.name, "Test Name") + self.assertEquals(draft.docevent_set.all()[1].type, "new_revision") + self.assertEquals(draft.docevent_set.all()[1].by.name, "Test Name") self.assertTrue(not os.path.exists(os.path.join(self.repository_dir, "%s-%s.txt" % (name, old_rev)))) self.assertTrue(os.path.exists(os.path.join(self.archive_dir, "%s-%s.txt" % (name, old_rev)))) self.assertTrue(not os.path.exists(os.path.join(self.staging_dir, u"%s-%s.txt" % (name, rev)))) self.assertTrue(os.path.exists(os.path.join(self.repository_dir, u"%s-%s.txt" % (name, rev)))) self.assertEquals(draft.type_id, "draft") self.assertEquals(draft.stream_id, "ietf") - self.assertEquals(draft.get_state("draft-stream-%s" % draft.stream_id).slug, "wg-doc") + self.assertEquals(draft.get_state_slug("draft-stream-%s" % draft.stream_id), "wg-doc") + self.assertEquals(draft.get_state_slug("draft-iana-review"), "changed") self.assertEquals(draft.authors.count(), 1) self.assertEquals(draft.authors.all()[0].get_name(), "Test Name") self.assertEquals(draft.authors.all()[0].address, "testname@example.com") diff --git a/ietf/submit/utils.py b/ietf/submit/utils.py index ca4b09623..8f6f97eaf 100644 --- a/ietf/submit/utils.py +++ b/ietf/submit/utils.py @@ -17,7 +17,7 @@ from ietf.ietfauth.decorators import has_role from ietf.doc.models import * from ietf.person.models import Person, Alias, Email -from ietf.doc.utils import active_ballot_positions +from ietf.doc.utils import active_ballot_positions, add_state_change_event from ietf.message.models import Message # Some useful states @@ -133,28 +133,34 @@ def perform_postREDESIGN(request, submission): draft.expires = datetime.datetime.now() + datetime.timedelta(settings.INTERNET_DRAFT_DAYS_TO_EXPIRE) draft.save() - draft.set_state(State.objects.get(type="draft", slug="active")) - if draft.stream_id == "ietf" and draft.group.type_id == "wg" and draft.rev == "00": - # automatically set state "WG Document" - draft.set_state(State.objects.get(type="draft-stream-%s" % draft.stream_id, slug="wg-doc")) - - DocAlias.objects.get_or_create(name=submission.filename, document=draft) - - update_authors(draft, submission) - - # new revision event a = submission.tempidauthors_set.filter(author_order=0) if a: submitter = ensure_person_email_info_exists(a[0]).person else: submitter = system + draft.set_state(State.objects.get(type="draft", slug="active")) + DocAlias.objects.get_or_create(name=submission.filename, document=draft) + + update_authors(draft, submission) + + # new revision event e = NewRevisionDocEvent(type="new_revision", doc=draft, rev=draft.rev) e.time = draft.time #submission.submission_date e.by = submitter e.desc = "New revision available" e.save() + if draft.stream_id == "ietf" and draft.group.type_id == "wg" and draft.rev == "00": + # automatically set state "WG Document" + draft.set_state(State.objects.get(type="draft-stream-%s" % draft.stream_id, slug="wg-doc")) + + if draft.get_state_slug("draft-iana-review") in ("ok-act", "ok-noact", "not-ok"): + prev_state = draft.get_state("draft-iana-review") + next_state = State.objects.get(type="draft-iana-review", slug="changed") + draft.set_state(next_state) + add_state_change_event(draft, submitter, prev_state, next_state) + # clean up old files if prev_rev != draft.rev: from ietf.idrfc.expire import move_draft_files_to_archive @@ -178,8 +184,7 @@ def perform_postREDESIGN(request, submission): submission.status_id = POSTED announce_to_lists(request, submission) - if draft.get_state("draft-iesg") != None and not was_rfc: - announce_new_version(request, submission, draft, state_change_msg) + announce_new_version(request, submission, draft, state_change_msg) announce_to_authors(request, submission) submission.save() @@ -262,15 +267,27 @@ def announce_new_versionREDESIGN(request, submission, draft, state_change_msg): if draft.ad: to_email.append(draft.ad.role_email("ad").address) + if draft.stream_id == "iab": + to_email.append("IAB Chair ") + elif draft.stream_id == "ise": + to_email.append("Independent Submission Editor ") + elif draft.stream_id == "irtf": + to_email.append("IRSG ") + + # if it has been sent to the RFC Editor, keep them in the loop + if draft.get_state_slug("draft-iesg") in ("ann", "rfcqueue"): + to_email.append("RFC Editor ") + for ad, pos in active_ballot_positions(draft).iteritems(): if pos and pos.pos_id == "discuss": to_email.append(ad.role_email("ad").address) - subject = 'New Version Notification - %s-%s.txt' % (submission.filename, submission.revision) - from_email = settings.IDSUBMIT_ANNOUNCE_FROM_EMAIL - send_mail(request, to_email, from_email, subject, 'submit/announce_new_version.txt', - {'submission': submission, - 'msg': state_change_msg}) + if to_email: + subject = 'New Version Notification - %s-%s.txt' % (submission.filename, submission.revision) + from_email = settings.IDSUBMIT_ANNOUNCE_FROM_EMAIL + send_mail(request, to_email, from_email, subject, 'submit/announce_new_version.txt', + {'submission': submission, + 'msg': state_change_msg}) if settings.USE_DB_REDESIGN_PROXY_CLASSES: announce_new_version = announce_new_versionREDESIGN From 5c89b8a51d7eb7cc502c492b2bae981aee27532b Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Mon, 17 Sep 2012 16:07:24 +0000 Subject: [PATCH 11/47] Add RFC Editor role handling - Legacy-Id: 4856 --- ietf/ietfauth/decorators.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ietf/ietfauth/decorators.py b/ietf/ietfauth/decorators.py index 933338a43..a49b1bc03 100644 --- a/ietf/ietfauth/decorators.py +++ b/ietf/ietfauth/decorators.py @@ -89,6 +89,7 @@ def has_role(user, role_names): "Area Director": Q(person=person, name__in=("pre-ad", "ad"), group__type="area", group__state="active"), "Secretariat": Q(person=person, name="secr", group__acronym="secretariat"), "IANA": Q(person=person, name="auth", group__acronym="iana"), + "RFC Editor": Q(email__person=person, name="auth", group__acronym="rfceditor"), "IAD": Q(person=person, name="admdir", group__acronym="ietf"), "IETF Chair": Q(person=person, name="chair", group__acronym="ietf"), "IAB Chair": Q(person=person, name="chair", group__acronym="iab"), From 6062e16c2d39ba5b7aa804154462f4b5cf9d5635 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Mon, 17 Sep 2012 16:15:45 +0000 Subject: [PATCH 12/47] Email IANA and RFC Editor when a draft is pulled from the queue at the Datatracker, add page for editing IANA states, add more info to the /doc/draft-XXXXX/doc.json dump for the RFC Editor, add page for editing consensus, add page for requesting publication at the RFC Editor for alternate streams (this will email the RFC Editor and set the draft in the appropriate state), make it possible for alternate streams to change the intended RFC status of a draft in the stream, refactor how IANA copies are handled slightly so it's less code, put drafts automatically in IANA Review "Need Review" state upon last call, fix a bug in ballot issuing, remove a bit of dead code - Legacy-Id: 4857 --- ietf/idrfc/mails.py | 69 +-- ietf/idrfc/testsREDESIGN.py | 470 +++++------------- ietf/idrfc/urls.py | 5 +- ietf/idrfc/utils.py | 22 + ietf/idrfc/views_ballot.py | 77 +-- ietf/idrfc/views_doc.py | 44 +- ietf/idrfc/views_edit.py | 200 +++++++- ietf/ietfworkflows/accounts.py | 8 - .../templatetags/ietf_streams.py | 2 + ietf/name/fixtures/names.xml | 178 +++++-- ietf/templates/idrfc/ballot_writeup.txt | 4 +- ietf/templates/idrfc/change_consensus.html | 21 + ietf/templates/idrfc/change_iana_state.html | 24 + .../templates/idrfc/change_stateREDESIGN.html | 14 +- ietf/templates/idrfc/doc_tab_document_id.html | 14 +- ietf/templates/idrfc/doc_tab_history.html | 2 +- ietf/templates/idrfc/document_history.html | 2 +- .../idrfc/issue_ballot_mailREDESIGN.txt | 2 +- ietf/templates/idrfc/publication_request.txt | 16 + .../idrfc/pulled_from_rfc_queue_email.txt | 9 + ietf/templates/idrfc/request_publication.html | 37 ++ ietf/utils/mail.py | 34 +- 22 files changed, 740 insertions(+), 514 deletions(-) create mode 100644 ietf/templates/idrfc/change_consensus.html create mode 100644 ietf/templates/idrfc/change_iana_state.html create mode 100644 ietf/templates/idrfc/publication_request.txt create mode 100644 ietf/templates/idrfc/pulled_from_rfc_queue_email.txt create mode 100644 ietf/templates/idrfc/request_publication.html diff --git a/ietf/idrfc/mails.py b/ietf/idrfc/mails.py index b75e7af95..33edd1137 100644 --- a/ietf/idrfc/mails.py +++ b/ietf/idrfc/mails.py @@ -62,8 +62,26 @@ def email_stream_changed(request, doc, old_stream, new_stream, text=""): "idrfc/stream_changed_email.txt", dict(text=text, url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url())) + +def email_pulled_from_rfc_queue(request, doc, comment, prev_state, next_state): + send_mail(request, ['IANA ', 'rfc-editor@rfc-editor.org'], None, + "%s changed state from %s to %s" % (doc.name, prev_state.name, next_state.name), + "idrfc/pulled_from_rfc_queue_email.txt", + dict(doc=doc, + prev_state=prev_state, + next_state=next_state, + comment=comment, + url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), + extra=extra_automation_headers(doc)) + + +def email_authors(request, doc, subject, text): + to = [x.strip() for x in doc.author_list().split(',')] + if not to: + return - + send_mail_text(request, to, None, subject, text) + def html_to_text(html): return strip_tags(html.replace("<", "<").replace(">", ">").replace("&", "&").replace("
    ", "\n")) @@ -98,12 +116,15 @@ if settings.USE_DB_REDESIGN_PROXY_CLASSES: def generate_ballot_writeup(request, doc): + e = doc.latest_event(type="iana_review") + iana = e.desc if e else "" + e = WriteupDocEvent() e.type = "changed_ballot_writeup_text" e.by = request.user.get_profile() e.doc = doc e.desc = u"Ballot writeup was generated" - e.text = unicode(render_to_string("idrfc/ballot_writeup.txt")) + e.text = unicode(render_to_string("idrfc/ballot_writeup.txt", {'iana': iana})) e.save() return e @@ -263,6 +284,20 @@ if settings.USE_DB_REDESIGN_PROXY_CLASSES: generate_approval_mail = generate_approval_mailREDESIGN generate_approval_mail_rfc_editor = generate_approval_mail_rfc_editorREDESIGN +def generate_publication_request(request, doc): + group_description = "" + if doc.group and doc.group.acronym != "none": + group_description = doc.group.name + if doc.group.type_id in ("wg", "rg", "area"): + group_description += " %s (%s)" % (doc.group.type, doc.group.acronym) + + + return render_to_string("idrfc/publication_request.txt", + dict(doc=doc, + doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), + group_description=group_description, + ) + ) def send_last_call_request(request, doc, ballot): to = "iesg-secretary@ietf.org" @@ -425,39 +460,13 @@ def generate_issue_ballot_mailREDESIGN(request, doc, ballot): if settings.USE_DB_REDESIGN_PROXY_CLASSES: generate_issue_ballot_mail = generate_issue_ballot_mailREDESIGN -def email_iana(request, doc, to, msg): - # fix up message and send message to IANA for each in ballot set - import email - parsed_msg = email.message_from_string(msg.encode("utf-8")) - - for i in doc.idinternal.ballot_set(): - extra = {} - extra["Reply-To"] = "noreply@ietf.org" - extra["X-IETF-Draft-string"] = i.document().filename - extra["X-IETF-Draft-revision"] = i.document().revision_display() - - send_mail_text(request, "To: IANA <%s>" % to, - parsed_msg["From"], parsed_msg["Subject"], - parsed_msg.get_payload(), - extra=extra) - -def email_ianaREDESIGN(request, doc, to, msg): - # fix up message and send it with extra info on doc in headers - import email - parsed_msg = email.message_from_string(msg.encode("utf-8")) - +def extra_automation_headers(doc): extra = {} extra["Reply-To"] = "noreply@ietf.org" extra["X-IETF-Draft-string"] = doc.name extra["X-IETF-Draft-revision"] = doc.rev - - send_mail_text(request, "IANA <%s>" % to, - parsed_msg["From"], parsed_msg["Subject"], - parsed_msg.get_payload(), - extra=extra) -if settings.USE_DB_REDESIGN_PROXY_CLASSES: - email_iana = email_ianaREDESIGN + return extra def email_last_call_expired(doc): text = "IETF Last Call has ended, and the state has been changed to\n%s." % doc.idinternal.cur_state.state diff --git a/ietf/idrfc/testsREDESIGN.py b/ietf/idrfc/testsREDESIGN.py index 734cd6bb7..0cc597415 100644 --- a/ietf/idrfc/testsREDESIGN.py +++ b/ietf/idrfc/testsREDESIGN.py @@ -119,7 +119,61 @@ class ChangeStateTestCase(django.test.TestCase): q = PyQuery(r.content) self.assertEquals(len(q('.prev-state form input[name="state"]')), 1) + def test_pull_from_rfc_queue(self): + draft = make_test_data() + draft.set_state(State.objects.get(type="draft-iesg", slug="rfcqueue")) + + url = urlreverse('doc_change_state', kwargs=dict(name=draft.name)) + login_testing_unauthorized(self, "secretary", url) + + # change state + mailbox_before = len(outbox) + + r = self.client.post(url, + dict(state=State.objects.get(type="draft-iesg", slug="review-e").pk, + substate="", + comment="Test comment")) + self.assertEquals(r.status_code, 302) + + draft = Document.objects.get(name=draft.name) + self.assertEquals(draft.get_state_slug("draft-iesg"), "review-e") + self.assertEquals(len(outbox), mailbox_before + 2 + 1) + self.assertTrue(draft.name in outbox[-1]['Subject']) + self.assertTrue("changed state" in outbox[-1]['Subject']) + self.assertTrue("is no longer" in str(outbox[-1])) + self.assertTrue("Test comment" in str(outbox[-1])) + + def test_change_iana_state(self): + draft = make_test_data() + + first_state = State.objects.get(type="draft-iana-review", slug="need-rev") + next_state = State.objects.get(type="draft-iana-review", slug="ok-noact") + draft.set_state(first_state) + + url = urlreverse('doc_change_iana_state', kwargs=dict(name=draft.name, state_type="iana-review")) + login_testing_unauthorized(self, "iana", 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=state]')), 1) + # faulty post + r = self.client.post(url, dict(state="foobarbaz")) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue(len(q('form ul.errorlist')) > 0) + draft = Document.objects.get(name=draft.name) + self.assertEquals(draft.get_state("draft-iana-review"), first_state) + + # change state + r = self.client.post(url, dict(state=next_state.pk)) + self.assertEquals(r.status_code, 302) + + draft = Document.objects.get(name=draft.name) + self.assertEquals(draft.get_state("draft-iana-review"), next_state) + def test_request_last_call(self): draft = make_test_data() draft.set_state(State.objects.get(type="draft-iesg", slug="ad-eval")) @@ -202,7 +256,7 @@ class EditInfoTestCase(django.test.TestCase): draft = Document.objects.get(name=draft.name) self.assertEquals(draft.ad, new_ad) self.assertEquals(draft.note, "New note") - self.assertTrue(not draft.latest_event(TelechatDocEvent, type="telechat_date")) + self.assertTrue(not draft.latest_event(TelechatDocEvent, type="scheduled_for_telechat")) self.assertEquals(draft.docevent_set.count(), events_before + 3) self.assertEquals(len(outbox), mailbox_before + 1) self.assertTrue(draft.name in outbox[-1]['Subject']) @@ -221,14 +275,14 @@ class EditInfoTestCase(django.test.TestCase): ) # add to telechat - self.assertTrue(not draft.latest_event(TelechatDocEvent, "scheduled_for_telechat")) + self.assertTrue(not draft.latest_event(TelechatDocEvent, type="scheduled_for_telechat")) data["telechat_date"] = TelechatDate.objects.active()[0].date.isoformat() r = self.client.post(url, data) self.assertEquals(r.status_code, 302) draft = Document.objects.get(name=draft.name) - self.assertTrue(draft.latest_event(TelechatDocEvent, "scheduled_for_telechat")) - self.assertEquals(draft.latest_event(TelechatDocEvent, "scheduled_for_telechat").telechat_date, TelechatDate.objects.active()[0].date) + self.assertTrue(draft.latest_event(TelechatDocEvent, type="scheduled_for_telechat")) + self.assertEqual(draft.latest_event(TelechatDocEvent, type="scheduled_for_telechat").telechat_date, TelechatDate.objects.active()[0].date) # change telechat data["telechat_date"] = TelechatDate.objects.active()[1].date.isoformat() @@ -236,7 +290,7 @@ class EditInfoTestCase(django.test.TestCase): self.assertEquals(r.status_code, 302) draft = Document.objects.get(name=draft.name) - self.assertEquals(draft.latest_event(TelechatDocEvent, "scheduled_for_telechat").telechat_date, TelechatDate.objects.active()[1].date) + self.assertEqual(draft.latest_event(TelechatDocEvent, type="scheduled_for_telechat").telechat_date, TelechatDate.objects.active()[1].date) # remove from agenda data["telechat_date"] = "" @@ -244,7 +298,7 @@ class EditInfoTestCase(django.test.TestCase): self.assertEquals(r.status_code, 302) draft = Document.objects.get(name=draft.name) - self.assertTrue(not draft.latest_event(TelechatDocEvent, "scheduled_for_telechat").telechat_date) + self.assertTrue(not draft.latest_event(TelechatDocEvent, type="scheduled_for_telechat").telechat_date) def test_start_iesg_process_on_draft(self): make_test_data() @@ -311,6 +365,18 @@ class EditInfoTestCase(django.test.TestCase): self.assertEquals(events[-3].type, "started_iesg_process") self.assertEquals(len(outbox), mailbox_before) + def test_edit_consensus(self): + draft = make_test_data() + + url = urlreverse('doc_edit_consensus', kwargs=dict(name=draft.name)) + login_testing_unauthorized(self, "secretary", url) + + self.assertTrue(not draft.latest_event(ConsensusDocEvent, type="changed_consensus")) + r = self.client.post(url, dict(consensus="Yes")) + self.assertEquals(r.status_code, 302) + + self.assertEqual(draft.latest_event(ConsensusDocEvent, type="changed_consensus").consensus, True) + class ResurrectTestCase(django.test.TestCase): fixtures = ['names'] @@ -406,6 +472,16 @@ class AddCommentTestCase(django.test.TestCase): self.assertTrue("updated" in outbox[-1]['Subject']) self.assertTrue(draft.name in outbox[-1]['Subject']) + # Make sure we can also do it as IANA + self.client.login(remote_user="iana") + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertEquals(len(q('form textarea[name=comment]')), 1) + + class EditPositionTestCase(django.test.TestCase): fixtures = ['names'] @@ -670,12 +746,21 @@ class BallotWriteupsTestCase(django.test.TestCase): url = urlreverse('doc_ballot_writeupnotes', kwargs=dict(name=draft.name)) login_testing_unauthorized(self, "secretary", url) + # add a IANA review note + draft.set_state(State.objects.get(type="draft-iana-review", slug="not-ok")) + DocEvent.objects.create(type="iana_review", + doc=draft, + by=Person.objects.get(user__username="iana"), + desc="IANA does not approve of this document, it does not make sense.", + ) + # normal get r = self.client.get(url) self.assertEquals(r.status_code, 200) q = PyQuery(r.content) self.assertEquals(len(q('textarea[name=ballot_writeup]')), 1) self.assertEquals(len(q('input[type=submit][value*="Save Ballot Writeup"]')), 1) + self.assertTrue("IANA does not" in r.content) # save r = self.client.post(url, dict( @@ -869,6 +954,44 @@ class MakeLastCallTestCase(django.test.TestCase): self.assertTrue("Last Call" in outbox[-3]['Subject']) self.assertTrue("Last Call" in draft.message_set.order_by("-time")[0].subject) +class RequestPublicationTestCase(django.test.TestCase): + fixtures = ['names'] + + def test_request_publication(self): + draft = make_test_data() + draft.stream = StreamName.objects.get(slug="iab") + draft.group = Group.objects.get(acronym="iab") + draft.intended_std_level = IntendedStdLevelName.objects.get(slug="inf") + draft.save() + draft.set_state(State.objects.get(type="draft-stream-iab", slug="approved")) + + url = urlreverse('doc_request_publication', kwargs=dict(name=draft.name)) + login_testing_unauthorized(self, "iabchair", url) + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + subject = q('input#id_subject')[0].get("value") + self.assertTrue("Document Action" in subject) + body = q('.request-publication #id_body').text() + self.assertTrue("Informational" in body) + self.assertTrue("IAB" in body) + + # approve + mailbox_before = len(outbox) + + r = self.client.post(url, dict(subject=subject, body=body)) + self.assertEquals(r.status_code, 302) + + draft = Document.objects.get(name=draft.name) + self.assertEquals(draft.get_state_slug("draft-stream-iab"), "rfc-edit") + self.assertEquals(len(outbox), mailbox_before + 2) + self.assertTrue("Document Action" in outbox[-2]['Subject']) + self.assertTrue("Document Action" in draft.message_set.order_by("-time")[0].subject) + # the IANA copy + self.assertTrue("Document Action" in outbox[-1]['Subject']) + class ExpireIDsTestCase(django.test.TestCase): fixtures = ['names'] @@ -1099,341 +1222,6 @@ class ExpireLastCallTestCase(django.test.TestCase): self.assertEquals(draft.docevent_set.count(), events_before + 1) self.assertEquals(len(outbox), mailbox_before + 1) self.assertTrue("Last Call Expired" in outbox[-1]["Subject"]) - - - -TEST_RFC_INDEX = ''' - - - BCP0110 - - RFC4170 - - - - BCP0111 - - RFC4181 - RFC4841 - - - - FYI0038 - - RFC3098 - - - - RFC1938 - A One-Time Password System - - N. Haller - - - C. Metz - - - May - 1996 - - - ASCII - 44844 - 18 - - - OTP - authentication - S/KEY - -

    This document describes a one-time password authentication system (OTP). [STANDARDS-TRACK]

    - - RFC2289 - - PROPOSED STANDARD - PROPOSED STANDARD - Legacy -
    - - RFC2289 - A One-Time Password System - - N. Haller - - - C. Metz - - - P. Nesser - - - M. Straw - - - February - 1998 - - - ASCII - 56495 - 25 - - - ONE-PASS - authentication - OTP - replay - attach - -

    This document describes a one-time password authentication system (OTP). The system provides authentication for system access (login) and other applications requiring authentication that is secure against passive attacks based on replaying captured reusable passwords. [STANDARDS- TRACK]

    - - RFC1938 - - - STD0061 - - STANDARD - DRAFT STANDARD - Legacy -
    - - RFC3098 - How to Advertise Responsibly Using E-Mail and Newsgroups or - how NOT to $$$$$ MAKE ENEMIES FAST! $$$$$ - - T. Gavin - - - D. Eastlake 3rd - - - S. Hambridge - - - April - 2001 - - - ASCII - 64687 - 28 - - - internet - marketing - users - service - providers - isps - -

    This memo offers useful suggestions for responsible advertising techniques that can be used via the internet in an environment where the advertiser, recipients, and the Internet Community can coexist in a productive and mutually respectful fashion. This memo provides information for the Internet community.

    - draft-ietf-run-adverts-02 - - FYI0038 - - INFORMATIONAL - INFORMATIONAL - Legacy -
    - - RFC4170 - Tunneling Multiplexed Compressed RTP (TCRTP) - - B. Thompson - - - T. Koren - - - D. Wing - - - November - 2005 - - - ASCII - 48990 - 24 - - - real-time transport protocol - -

    This document describes a method to improve the bandwidth utilization of RTP streams over network paths that carry multiple Real-time Transport Protocol (RTP) streams in parallel between two endpoints, as in voice trunking. The method combines standard protocols that provide compression, multiplexing, and tunneling over a network path for the purpose of reducing the bandwidth used when multiple RTP streams are carried over that path. This document specifies an Internet Best Current Practices for the Internet Community, and requests discussion and suggestions for improvements.

    - draft-ietf-avt-tcrtp-08 - - BCP0110 - - BEST CURRENT PRACTICE - BEST CURRENT PRACTICE - IETF - rai - avt -
    - - RFC4181 - Guidelines for Authors and Reviewers of MIB Documents - - C. Heard - Editor - - - September - 2005 - - - ASCII - 102521 - 42 - - - standards-track specifications - management information base - review - -

    This memo provides guidelines for authors and reviewers of IETF standards-track specifications containing MIB modules. Applicable portions may be used as a basis for reviews of other MIB documents. This document specifies an Internet Best Current Practices for the Internet Community, and requests discussion and suggestions for improvements.

    - draft-ietf-ops-mib-review-guidelines-04 - - RFC4841 - - - BCP0111 - - BEST CURRENT PRACTICE - BEST CURRENT PRACTICE - IETF - rtg - ospf - http://www.rfc-editor.org/errata_search.php?rfc=4181 -
    - - RFC4841 - RFC 4181 Update to Recognize the IETF Trust - - C. Heard - Editor - - - March - 2007 - - - ASCII - 4414 - 3 - - - management information base - standards-track specifications - mib review - -

    This document updates RFC 4181, "Guidelines for Authors and Reviewers of MIB Documents", to recognize the creation of the IETF Trust. This document specifies an Internet Best Current Practices for the Internet Community, and requests discussion and suggestions for improvements.

    - draft-heard-rfc4181-update-00 - - RFC4181 - - - BCP0111 - - BEST CURRENT PRACTICE - BEST CURRENT PRACTICE - IETF - NON WORKING GROUP -
    - - STD0061 - A One-Time Password System - - RFC2289 - - -
    -''' - -TEST_QUEUE = ''' -
    - -draft-ietf-sipping-app-interaction-framework-05.txt -2005-10-17 -EDIT - -draft-ietf-sip-gruu -IN-QUEUE - -J. Rosenberg - -A Framework for Application Interaction in the Session Initiation Protocol (SIP) - -94672 -Session Initiation Proposal Investigation - -
    -
    - -draft-ietf-sip-gruu-15.txt -2007-10-15 -MISSREF - -draft-ietf-sip-outbound -NOT-RECEIVED - -J. Rosenberg - -Obtaining and Using Globally Routable User Agent (UA) URIs (GRUU) in the Session Initiation Protocol (SIP) - -95501 -Session Initiation Protocol - -
    -
    -
    -
    - -draft-thomson-beep-async-02.txt -2009-05-12 -EDIT -IANA -M. Thomson - -Asynchronous Channels for the Blocks Extensible Exchange Protocol (BEEP) - -17237 -IETF - NON WORKING GROUP - -
    -
    -
    -
    -
    -
    -
    -
    -''' - -class MirrorScriptTestCases(unittest.TestCase,RealDatabaseTest): - - def setUp(self): - self.setUpRealDatabase() - def tearDown(self): - self.tearDownRealDatabase() - - def testRfcIndex(self): - print " Testing rfc-index.xml parsing" - from ietf.idrfc.mirror_rfc_index import parse - data = parse(StringIO.StringIO(TEST_RFC_INDEX)) - self.assertEquals(len(data), 6) - print "OK" - - def testRfcEditorQueue(self): - print " Testing queue2.xml parsing" - from ietf.idrfc.mirror_rfc_editor_queue import parse_all - (drafts,refs) = parse_all(StringIO.StringIO(TEST_QUEUE)) - self.assertEquals(len(drafts), 3) - self.assertEquals(len(refs), 3) - print "OK" - class IndividualInfoFormsTestCase(django.test.TestCase): diff --git a/ietf/idrfc/urls.py b/ietf/idrfc/urls.py index b520d98ea..2a0477be8 100644 --- a/ietf/idrfc/urls.py +++ b/ietf/idrfc/urls.py @@ -49,12 +49,13 @@ urlpatterns = patterns('', url(r'^(?P[A-Za-z0-9._+-]+)/ballot/(?P[0-9]+)/emailposition/$', views_ballot.send_ballot_comment, name='doc_send_ballot_comment'), url(r'^(?P[A-Za-z0-9._+-]+)/ballot/(?P[0-9]+)/$', views_doc.document_ballot, name="doc_ballot"), url(r'^(?P[A-Za-z0-9._+-]+)/ballot/$', views_doc.document_ballot, name="doc_ballot"), - (r'^(?P[A-Za-z0-9._+-]+)/doc.json$', views_doc.document_debug), + (r'^(?P[A-Za-z0-9._+-]+)/doc.json$', views_doc.document_json), (r'^(?P[A-Za-z0-9._+-]+)/ballotpopup/$', views_doc.ballot_for_popup), (r'^(?P[A-Za-z0-9._+-]+)/ballot.tsv$', views_doc.ballot_tsv), (r'^(?P[A-Za-z0-9._+-]+)/ballot.json$', views_doc.ballot_json), url(r'^(?P[A-Za-z0-9._+-]+)/edit/state/$', views_edit.change_state, name='doc_change_state'), # IESG state + url(r'^(?P[A-Za-z0-9._+-]+)/edit/state/(?Piana-action|iana-review)/$', views_edit.change_iana_state, name='doc_change_iana_state'), url(r'^(?P[A-Za-z0-9._+-]+)/edit/info/$', views_edit.edit_info, name='doc_edit_info'), url(r'^(?P[A-Za-z0-9._+-]+)/edit/requestresurrect/$', views_edit.request_resurrect, name='doc_request_resurrect'), url(r'^(?P[A-Za-z0-9._+-]+)/edit/resurrect/$', views_edit.resurrect, name='doc_resurrect'), @@ -66,6 +67,8 @@ urlpatterns = patterns('', url(r'^(?P[A-Za-z0-9._+-]+)/edit/telechat/$', views_edit.telechat_date, name='doc_change_telechat_date'), url(r'^(?P[A-Za-z0-9._+-]+)/edit/iesgnote/$', views_edit.edit_iesg_note, name='doc_change_iesg_note'), url(r'^(?P[A-Za-z0-9._+-]+)/edit/ad/$', views_edit.edit_ad, name='doc_change_ad'), + url(r'^(?P[A-Za-z0-9._+-]+)/edit/consensus/$', views_edit.edit_consensus, name='doc_edit_consensus'), + url(r'^(?P[A-Za-z0-9._+-]+)/edit/requestpublication/$', views_edit.request_publication, name='doc_request_publication'), url(r'^(?P[A-Za-z0-9._+-]+)/edit/clearballot/$', views_ballot.clear_ballot, name='doc_clear_ballot'), url(r'^(?P[A-Za-z0-9._+-]+)/edit/deferballot/$', views_ballot.defer_ballot, name='doc_defer_ballot'), diff --git a/ietf/idrfc/utils.py b/ietf/idrfc/utils.py index 9fc376753..318880493 100644 --- a/ietf/idrfc/utils.py +++ b/ietf/idrfc/utils.py @@ -2,6 +2,7 @@ from django.conf import settings from ietf.idtracker.models import InternetDraft, DocumentComment, BallotInfo, IESGLogin from ietf.idrfc.mails import * +from ietf.ietfauth.decorators import has_role def add_document_comment(request, doc, text, ballot=None): if request: @@ -175,3 +176,24 @@ def update_telechatREDESIGN(request, doc, by, new_telechat_date, new_returning_i if settings.USE_DB_REDESIGN_PROXY_CLASSES: update_telechat = update_telechatREDESIGN + +def can_edit_intended_std_level(doc, user): + return user.is_authenticated() and ( + has_role(user, ["Secretariat", "Area Director"]) or + doc.group.role_set.filter(name__in=("chair", "auth", "delegate"), person__user=user) + ) + +def can_edit_consensus(doc, user): + return user.is_authenticated() and ( + has_role(user, ["Secretariat", "Area Director"]) or + doc.group.role_set.filter(name__in=("chair", "auth", "delegate"), person__user=user) + ) + +def nice_consensus(consensus): + mapping = { + None: "Unknown", + True: "Yes", + False: "No" + } + return mapping[consensus] + diff --git a/ietf/idrfc/views_ballot.py b/ietf/idrfc/views_ballot.py index 2afc08bab..503bda8bf 100644 --- a/ietf/idrfc/views_ballot.py +++ b/ietf/idrfc/views_ballot.py @@ -818,8 +818,8 @@ def ballot_writeupnotesREDESIGN(request, name): msg = generate_issue_ballot_mail(request, doc, ballot) send_mail_preformatted(request, msg) - - email_iana(request, doc, 'drafts-eval@icann.org', msg) + send_mail_preformatted(request, msg, extra=extra_automation_headers(doc), + override={ "To": "IANA " }) e = DocEvent(doc=doc, by=login) e.by = login @@ -1106,7 +1106,8 @@ def approve_ballotREDESIGN(request, name): send_mail_preformatted(request, announcement) if action == "to_announcement_list": - email_iana(request, doc, "drafts-approval@icann.org", announcement) + send_mail_preformatted(request, announcement, extra=extra_automation_headers(doc), + override={ "To": "IANA " }) msg = infer_message(announcement) msg.by = login @@ -1131,62 +1132,10 @@ class MakeLastCallForm(forms.Form): @group_required('Secretariat') def make_last_call(request, name): - """Make last call for Internet Draft, sending out announcement.""" - doc = get_object_or_404(InternetDraft, filename=name) - if not doc.idinternal: - raise Http404() - - login = IESGLogin.objects.get(login_name=request.user.username) - - ballot = doc.idinternal.ballot - docs = [i.document() for i in doc.idinternal.ballot_set()] - - announcement = ballot.last_call_text - - if request.method == 'POST': - form = MakeLastCallForm(request.POST) - if form.is_valid(): - send_mail_preformatted(request, announcement) - email_iana(request, doc, "drafts-lastcall@icann.org", announcement) - - doc.idinternal.change_state(IDState.objects.get(document_state_id=IDState.IN_LAST_CALL), None) - doc.idinternal.event_date = date.today() - doc.idinternal.save() - - log_state_changed(request, doc, login) - - doc.lc_sent_date = form.cleaned_data['last_call_sent_date'] - doc.lc_expiration_date = form.cleaned_data['last_call_expiration_date'] - doc.save() - - comment = "Last call has been made for %s ballot and state has been changed to %s" % (doc.filename, doc.idinternal.cur_state.state) - email_owner(request, doc, doc.idinternal.job_owner, login, comment) - - return HttpResponseRedirect(doc.idinternal.get_absolute_url()) - else: - initial = {} - initial["last_call_sent_date"] = date.today() - expire_days = 14 - if doc.group_id == Acronym.INDIVIDUAL_SUBMITTER: - expire_days = 28 - - initial["last_call_expiration_date"] = date.today() + timedelta(days=expire_days) - - form = MakeLastCallForm(initial=initial) - - return render_to_response('idrfc/make_last_call.html', - dict(doc=doc, - docs=docs, - form=form), - context_instance=RequestContext(request)) - - -@group_required('Secretariat') -def make_last_callREDESIGN(request, name): """Make last call for Internet Draft, sending out announcement.""" doc = get_object_or_404(Document, docalias__name=name) if not doc.get_state("draft-iesg"): - raise Http404() + raise Http404 login = request.user.get_profile() @@ -1199,7 +1148,8 @@ def make_last_callREDESIGN(request, name): form = MakeLastCallForm(request.POST) if form.is_valid(): send_mail_preformatted(request, announcement) - email_iana(request, doc, "drafts-lastcall@icann.org", announcement) + send_mail_preformatted(request, announcement, extra=extra_automation_headers(doc), + override={ "To": "IANA " }) msg = infer_message(announcement) msg.by = login @@ -1234,7 +1184,14 @@ def make_last_callREDESIGN(request, name): e.time = datetime.datetime.combine(form.cleaned_data['last_call_sent_date'], e.time.time()) e.expires = form.cleaned_data['last_call_expiration_date'] e.save() - + + # update IANA Review state + prev_state = doc.get_state("draft-iana-review") + if not prev_state: + next_state = State.objects.get(type="draft-iana-review", slug="need-rev") + doc.set_state(next_state) + add_state_change_event(doc, login, prev_state, next_state) + return HttpResponseRedirect(doc.get_absolute_url()) else: initial = {} @@ -1251,7 +1208,3 @@ def make_last_callREDESIGN(request, name): dict(doc=doc, form=form), context_instance=RequestContext(request)) - - -if settings.USE_DB_REDESIGN_PROXY_CLASSES: - make_last_call = make_last_callREDESIGN diff --git a/ietf/idrfc/views_doc.py b/ietf/idrfc/views_doc.py index c96bd9800..5e9c43451 100644 --- a/ietf/idrfc/views_doc.py +++ b/ietf/idrfc/views_doc.py @@ -46,6 +46,7 @@ from django.conf import settings from ietf.idtracker.models import InternetDraft, IDInternal, BallotInfo, DocumentComment from ietf.idtracker.templatetags.ietf_filters import format_textarea, fill from ietf.idrfc import markup_txt +from ietf.idrfc.utils import * from ietf.idrfc.models import RfcIndex, DraftVersions from ietf.idrfc.idrfc_wrapper import BallotWrapper, IdWrapper, RfcWrapper from ietf.ietfworkflows.utils import get_full_info_for_draft @@ -371,7 +372,8 @@ def document_ballot(request, name, ballot_id=None): ), context_instance=RequestContext(request)) -def document_debug(request, name): +def document_json(request, name): + # old interface r = re.compile("^rfc([1-9][0-9]*)$") m = r.match(name) if m: @@ -381,7 +383,36 @@ def document_debug(request, name): else: id = get_object_or_404(InternetDraft, filename=name) doc = IdWrapper(draft=id) - return HttpResponse(doc.to_json(), mimetype='text/plain') + + from idrfc_wrapper import jsonify_helper + + if isinstance(doc, RfcWrapper): + data = jsonify_helper(doc, ['rfc_number', 'title', 'publication_date', 'maturity_level', 'obsoleted_by','obsoletes','updated_by','updates','also','has_errata','stream_name','file_types','in_ietf_process', 'friendly_state']) + else: + data = jsonify_helper(doc, ['draft_name', 'draft_status', 'latest_revision', 'rfc_number', 'title', 'tracker_id', 'publication_date','rfc_editor_state', 'replaced_by', 'replaces', 'in_ietf_process', 'file_types', 'group_acronym', 'stream_id','friendly_state', 'abstract', 'ad_name']) + if doc.in_ietf_process(): + data['ietf_process'] = doc.ietf_process.dict() + + # add some more fields using the new interface + d = get_object_or_404(Document, docalias__name=name) + data["authors"] = [ + dict(name=e.person.name, + email=e.address, + affiliation=e.person.affiliation) + for e in Email.objects.filter(documentauthor__document=d).select_related("person").order_by("documentauthor__order") + ] + e = d.latest_event(ConsensusDocEvent, type="changed_consensus") + data["consensus"] = e.consensus if e else None + data["stream"] = d.stream.name if d.stream else None + data["shepherd"] = d.shepherd.formatted_email() if d.shepherd else None + + def state_name(s): + return s.name if s else None + + data["iana_review_state"] = state_name(d.get_state("draft-iana-review")) + data["iana_action_state"] = state_name(d.get_state("draft-iana-action")) + + return HttpResponse(json.dumps(data, indent=2), mimetype='text/plain') def _get_html(key, filename, split=True): return get_document_content(key, filename, split=split, markup=True) @@ -437,7 +468,14 @@ def document_main_idrfc(request, name, tab): info['is_rfc'] = False info['conflict_reviews'] = [ rel.source for alias in id.docalias_set.all() for rel in alias.relateddocument_set.filter(relationship='conflrev') ] - + info['rfc_editor_state'] = id.get_state("draft-rfceditor") + info['iana_review_state'] = id.get_state("draft-iana-review") + info['iana_action_state'] = id.get_state("draft-iana-action") + e = id.latest_event(ConsensusDocEvent, type="changed_consensus") + info["consensus"] = nice_consensus(e and e.consensus) + info["can_edit_consensus"] = can_edit_consensus(id, request.user) + info["can_edit_intended_std_level"] = can_edit_intended_std_level(id, request.user) + (content1, content2) = _get_html( str(name)+","+str(id.revision)+",html", os.path.join(settings.INTERNET_DRAFT_PATH, name+"-"+id.revision+".txt")) diff --git a/ietf/idrfc/views_edit.py b/ietf/idrfc/views_edit.py index 60b57014d..1e4ecb5e6 100644 --- a/ietf/idrfc/views_edit.py +++ b/ietf/idrfc/views_edit.py @@ -3,7 +3,7 @@ import re, os from datetime import datetime, date, time, timedelta -from django.http import HttpResponse, HttpResponseRedirect, Http404 +from django.http import HttpResponse, HttpResponseRedirect, HttpResponseForbidden, Http404 from django.shortcuts import render_to_response, get_object_or_404 from django.core.urlresolvers import reverse as urlreverse from django.template.loader import render_to_string @@ -13,10 +13,10 @@ from django.utils.html import strip_tags from django.db.models import Max from django.conf import settings -from ietf.utils.mail import send_mail_text +from ietf.utils.mail import send_mail_text, send_mail_message from ietf.ietfauth.decorators import group_required from ietf.idtracker.templatetags.ietf_filters import in_group -from ietf.ietfauth.decorators import has_role +from ietf.ietfauth.decorators import has_role, role_required from ietf.idtracker.models import * from ietf.iesg.models import * from ietf.idrfc.mails import * @@ -26,10 +26,13 @@ from ietf.idrfc.lastcall import request_last_call from ietf.ietfworkflows.models import Stream from ietf.ietfworkflows.utils import update_stream from ietf.ietfworkflows.streams import get_stream_from_draft +from ietf.ietfworkflows.accounts import can_edit_state from ietf.doc.models import * +from ietf.doc.utils import * from ietf.name.models import IntendedStdLevelName, DocTagName, StreamName from ietf.person.models import Person, Email +from ietf.message.models import Message class ChangeStateForm(forms.Form): pass @@ -58,20 +61,21 @@ def change_stateREDESIGN(request, name): if request.method == 'POST': form = ChangeStateForm(request.POST) if form.is_valid(): - state = form.cleaned_data['state'] + next_state = form.cleaned_data['state'] + prev_state = doc.get_state("draft-iesg") + tag = form.cleaned_data['substate'] comment = form.cleaned_data['comment'].strip() - prev = doc.get_state("draft-iesg") # tag handling is a bit awkward since the UI still works # as if IESG tags are a substate prev_tag = doc.tags.filter(slug__in=('point', 'ad-f-up', 'need-rev', 'extpty')) prev_tag = prev_tag[0] if prev_tag else None - if state != prev or tag != prev_tag: + if next_state != prev_state or tag != prev_tag: save_document_in_history(doc) - doc.set_state(state) + doc.set_state(next_state) if prev_tag: doc.tags.remove(prev_tag) @@ -79,7 +83,7 @@ def change_stateREDESIGN(request, name): if tag: doc.tags.add(tag) - e = log_state_changed(request, doc, login, prev, prev_tag) + e = log_state_changed(request, doc, login, prev_state, prev_tag) if comment: c = DocEvent(type="added_comment") @@ -96,7 +100,15 @@ def change_stateREDESIGN(request, name): email_state_changed(request, doc, e.desc) email_owner(request, doc, doc.ad, login, e.desc) - if state.slug == "lc-req": + + if prev_state and prev_state.slug in ("ann", "rfcqueue") and next_state.slug not in ("rfcqueue", "pub"): + email_pulled_from_rfc_queue(request, doc, comment, prev_state, next_state) + + if next_state.slug in ("iesg-eva", "lc"): + if not doc.get_state_slug("draft-iana-review"): + doc.set_state(State.objects.get(type="draft-iana-review", slug="rev-need")) + + if next_state.slug == "lc-req": request_last_call(request, doc) return render_to_response('idrfc/last_call_requested.html', @@ -129,6 +141,7 @@ def change_stateREDESIGN(request, name): return render_to_response('idrfc/change_stateREDESIGN.html', dict(form=form, doc=doc, + state=state, prev_state=prev_state, next_states=next_states, to_iesg_eval=to_iesg_eval), @@ -138,6 +151,52 @@ if settings.USE_DB_REDESIGN_PROXY_CLASSES: change_state = change_stateREDESIGN ChangeStateForm = ChangeStateFormREDESIGN +class ChangeIanaStateForm(forms.Form): + state = forms.ModelChoiceField(State.objects.all(), required=False) + + def __init__(self, state_type, *args, **kwargs): + super(self.__class__, self).__init__(*args, **kwargs) + + choices = State.objects.filter(type=state_type).order_by("order").values_list("pk", "name") + self.fields['state'].choices = [("", "-------")] + list(choices) + +@role_required('Secretariat', 'IANA') +def change_iana_state(request, name, state_type): + """Change IANA review state of Internet Draft. Normally, this is done via + automatic sync, but this form allows one to set it manually.""" + doc = get_object_or_404(Document, docalias__name=name) + + state_type = doc.type_id + "-" + state_type + + prev_state = doc.get_state(state_type) + + if request.method == 'POST': + form = ChangeIanaStateForm(state_type, request.POST) + if form.is_valid(): + next_state = form.cleaned_data['state'] + + if next_state != prev_state: + save_document_in_history(doc) + + doc.set_state(next_state) + + e = add_state_change_event(doc, request.user.get_profile(), prev_state, next_state) + + doc.time = e.time + doc.save() + + return HttpResponseRedirect(doc.get_absolute_url()) + + else: + form = ChangeIanaStateForm(state_type, initial=dict(state=prev_state.pk if prev_state else None)) + + return render_to_response('idrfc/change_iana_state.html', + dict(form=form, + doc=doc), + context_instance=RequestContext(request)) + + + class ChangeStreamForm(forms.Form): stream = forms.ModelChoiceField(StreamName.objects.exclude(slug="legacy"), required=False) comment = forms.CharField(widget=forms.Textarea, required=False) @@ -197,13 +256,15 @@ class ChangeIntentionForm(forms.Form): intended_std_level = forms.ModelChoiceField(IntendedStdLevelName.objects.filter(used=True), empty_label="(None)", required=True, label="Intended RFC status") comment = forms.CharField(widget=forms.Textarea, required=False) -@group_required('Area_Director','Secretariat') def change_intention(request, name): """Change the intended publication status of a Document of type 'draft' , notifying parties as necessary and logging the change as a comment.""" doc = get_object_or_404(Document, docalias__name=name) - if not doc.type_id=='draft': - raise Http404() + if doc.type_id != 'draft': + raise Http404 + + if not can_edit_intended_std_level(doc, request.user): + return HttpResponseForbidden("You do not have the necessary permissions to view this page") login = request.user.get_profile() @@ -617,7 +678,7 @@ def add_comment(request, name): back_url=doc.idinternal.get_absolute_url()), context_instance=RequestContext(request)) -@group_required('Area_Director', 'Secretariat', 'IANA') +@group_required('Area_Director', 'Secretariat', 'IANA', 'RFC Editor') def add_commentREDESIGN(request, name): """Add comment to history of document.""" doc = get_object_or_404(Document, docalias__name=name) @@ -827,3 +888,116 @@ def edit_ad(request, name): }, context_instance = RequestContext(request)) +class ConsensusForm(forms.Form): + consensus = forms.ChoiceField(choices=(("", "Unknown"), ("Yes", "Yes"), ("No", "No")), required=True) + +def edit_consensus(request, name): + """Change whether the draft is a consensus document or not.""" + + doc = get_object_or_404(Document, type="draft", name=name) + + if not can_edit_consensus(doc, request.user): + return HttpResponseForbidden("You do not have the necessary permissions to view this page") + + e = doc.latest_event(ConsensusDocEvent, type="changed_consensus") + prev_consensus = e and e.consensus + + if request.method == 'POST': + form = ConsensusForm(request.POST) + if form.is_valid(): + if form.cleaned_data["consensus"] != bool(prev_consensus): + e = ConsensusDocEvent(doc=doc, type="changed_consensus", by=request.user.get_profile()) + e.consensus = form.cleaned_data["consensus"] == "Yes" + + e.desc = "Changed consensus to %s from %s" % (nice_consensus(e.consensus), + nice_consensus(prev_consensus)) + + e.save() + + return HttpResponseRedirect(urlreverse('doc_view', kwargs={'name': doc.name})) + + else: + form = ConsensusForm(initial=dict(consensus=nice_consensus(prev_consensus).replace("Unknown", ""))) + + return render_to_response('idrfc/change_consensus.html', + {'form': form, + 'doc': doc, + }, + context_instance = RequestContext(request)) + +class PublicationForm(forms.Form): + subject = forms.CharField(max_length=200, required=True) + body = forms.CharField(widget=forms.Textarea, required=True) + +def request_publication(request, name): + """Request publication by RFC Editor for a document which hasn't + been through the IESG ballot process.""" + + doc = get_object_or_404(Document, type="draft", name=name, stream__in=("iab", "ise", "irtf")) + + if not can_edit_state(request.user, doc): + return HttpResponseForbidden("You do not have the necessary permissions to view this page") + + m = Message() + m.frm = request.user.get_profile().formatted_email() + m.to = "RFC Editor " + m.by = request.user.get_profile() + + next_state = State.objects.get(type="draft-stream-%s" % doc.stream.slug, slug="rfc-edit") + + if request.method == 'POST' and not request.POST.get("reset"): + form = PublicationForm(request.POST) + if form.is_valid(): + m.subject = form.cleaned_data["subject"] + m.body = form.cleaned_data["body"] + m.save() + + if doc.group.acronym != "none": + m.related_groups = [doc.group] + m.related_docs = [doc] + + send_mail_message(request, m) + + # IANA copy + m.to = "IANA " + send_mail_message(request, m, extra=extra_automation_headers(doc)) + + e = DocEvent(doc=doc, type="requested_publication", by=request.user.get_profile()) + e.desc = "Sent request for publication to the RFC Editor" + e.save() + + # change state + prev_state = doc.get_state(next_state.type) + + doc.set_state(next_state) + + e = add_state_change_event(doc, request.user.get_profile(), prev_state, next_state) + + doc.time = e.time + doc.save() + + return HttpResponseRedirect(urlreverse('doc_view', kwargs={'name': doc.name})) + + else: + if doc.intended_std_level_id in ("std", "ds", "ps", "bcp"): + action = "Protocol Action" + else: + action = "Document Action" + + from ietf.idrfc.templatetags.mail_filters import std_level_prompt + + subject = "%s: '%s' to %s (%s-%s.txt)" % (action, doc.title, std_level_prompt(doc), doc.name, doc.rev) + + body = generate_publication_request(request, doc) + + form = PublicationForm(initial=dict(subject=subject, + body=body)) + + return render_to_response('idrfc/request_publication.html', + dict(form=form, + doc=doc, + message=m, + next_state=next_state, + ), + context_instance = RequestContext(request)) + diff --git a/ietf/ietfworkflows/accounts.py b/ietf/ietfworkflows/accounts.py index 4b7046462..1e0762d17 100644 --- a/ietf/ietfworkflows/accounts.py +++ b/ietf/ietfworkflows/accounts.py @@ -108,14 +108,6 @@ if settings.USE_DB_REDESIGN_PROXY_CLASSES: def can_edit_state(user, draft): - streamed = get_streamed_draft(draft) - if not settings.USE_DB_REDESIGN_PROXY_CLASSES and (not streamed or not streamed.stream): - person = get_person_for_user(user) - if not person: - return False - return (is_secretariat(user) or - is_wgchair(person) or - is_wgdelegate(person)) return (is_secretariat(user) or is_authorized_in_draft_stream(user, draft)) diff --git a/ietf/ietfworkflows/templatetags/ietf_streams.py b/ietf/ietfworkflows/templatetags/ietf_streams.py index 13a040554..25b0fc959 100644 --- a/ietf/ietfworkflows/templatetags/ietf_streams.py +++ b/ietf/ietfworkflows/templatetags/ietf_streams.py @@ -74,6 +74,8 @@ def edit_actions(context, wrapper): if can_edit_state(user, draft): actions.append(("Change stream state", urlreverse('edit_state', kwargs=dict(name=doc.draft_name)))) + if draft.stream_id in ("iab", "ise", "irtf"): + actions.append(("Request publication", urlreverse('doc_request_publication', kwargs=dict(name=doc.draft_name)))) if can_manage_shepherd_of_a_document(user, draft): actions.append(("Change shepherd", urlreverse('doc_managing_shepherd', kwargs=dict(acronym=draft.group.acronym, name=draft.filename)))) diff --git a/ietf/name/fixtures/names.xml b/ietf/name/fixtures/names.xml index 09393cf04..a979ab819 100644 --- a/ietf/name/fixtures/names.xml +++ b/ietf/name/fixtures/names.xml @@ -97,7 +97,7 @@ True 0 - + IANA coordination RFC-Editor/IANA Registration Coordination True @@ -325,36 +325,6 @@ True 0 - - No - - True - 0 - - - Yes - - True - 0 - - - Abstain - - True - 0 - - - Block - - True - 0 - - - No record - - True - 0 - BOF @@ -751,8 +721,11 @@ IESG state - - IANA state + + IANA Action state + + + IANA Review state RFC Editor state @@ -1003,6 +976,141 @@ 6 + + draft-iana-action + rfcedack + RFC-Ed-Ack + True + Request completed. The RFC Editor has acknowledged receipt of IANA's message that the actions have been completed + 0 + + + + draft-iana-action + waitrfc + Waiting on RFC Editor + True + IANA has notified the RFC Editor that the actions have been completed + 0 + + + + draft-iana-action + waitwgc + Waiting on WGC + True + IANA is waiting on the IETF Working Group Chairs to respond + 0 + + + + draft-iana-action + waitad + Waiting on ADs + True + IANA is waiting on the IETF Area Directors to respond + 0 + + + + draft-iana-action + waitauth + Waiting on Authors + True + IANA is waiting on the document's authors to respond + 0 + + + + draft-iana-action + inprog + In Progress + True + IANA is currently processing the actions for this document + 0 + + + + draft-iana-action + newdoc + New Document + True + A new document has been received by IANA, but no actions have been taken + 0 + + + + draft-iana-action + onhold + On Hold + True + IANA has suspended work on the document + 0 + + + + draft-iana-action + noic + No IC + True + Request completed. There were no IANA actions for this document + 0 + + + + draft-iana-review + need-rev + IANA Review Needed + True + + 1 + + + + draft-iana-review + ok-act + IANA OK - Actions Needed + True + Document requires IANA actions, and the IANA Considerations section indicates the details of the actions correctly. + 2 + + + + draft-iana-review + ok-noact + IANA OK - No Actions Needed + True + Document requires no IANA action, and the IANA Considerations section indicates this correctly. + 3 + + + + draft-iana-review + not-ok + IANA Not OK + True + IANA has issues with the text of the IANA Considerations section of the document. + 4 + + + + draft-iana-review + changed + Version Changed - Review Needed + True + Document revision has changed after review by IANA. + 5 + + + + draft-rfceditor + auth48-done + AUTH48-DONE + True + Final approvals are complete + 0 + + draft-iesg pub-req @@ -1185,7 +1293,7 @@ draft-rfceditor - iana-crd + iana IANA True RFC-Editor/IANA Registration Coordination @@ -1780,4 +1888,4 @@ 3 - \ No newline at end of file + diff --git a/ietf/templates/idrfc/ballot_writeup.txt b/ietf/templates/idrfc/ballot_writeup.txt index df5814020..02888c631 100644 --- a/ietf/templates/idrfc/ballot_writeup.txt +++ b/ietf/templates/idrfc/ballot_writeup.txt @@ -45,6 +45,8 @@ IESG Note (Insert IESG Note here or remove section) IANA Note - +{% if iana %} + {% load ietf_filters %}{% filter wordwrap:"68"|indent:2 %}{{ iana }}{% endfilter %} +{% endif %} (Insert IANA Note here or remove section) {% endautoescape%} diff --git a/ietf/templates/idrfc/change_consensus.html b/ietf/templates/idrfc/change_consensus.html new file mode 100644 index 000000000..782318cef --- /dev/null +++ b/ietf/templates/idrfc/change_consensus.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} + +{% block title %}Change whether {{ doc.name }}-{{ doc.rev }} is the result of a consensus process{% endblock %} + +{% block content %} +

    Change whether {{ doc.name }}-{{ doc.rev }} is the result of a consensus process

    + +
    + + {{ form.as_table }} + + + + +
    + Back + +
    +
    + +{% endblock %} diff --git a/ietf/templates/idrfc/change_iana_state.html b/ietf/templates/idrfc/change_iana_state.html new file mode 100644 index 000000000..479e918a9 --- /dev/null +++ b/ietf/templates/idrfc/change_iana_state.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block title %}Change IANA state of {{ doc }}{% endblock %} + +{% block morecss %} +form table .actions { text-align: right; padding-top: 1em; } +{% endblock %} + +{% block content %} +

    Change IANA state of {{ doc }}

    + +
    + + {{ form.as_table }} + + + +
    + Back + +
    +
    + +{% endblock %} diff --git a/ietf/templates/idrfc/change_stateREDESIGN.html b/ietf/templates/idrfc/change_stateREDESIGN.html index 863620afa..4cc755bb1 100644 --- a/ietf/templates/idrfc/change_stateREDESIGN.html +++ b/ietf/templates/idrfc/change_stateREDESIGN.html @@ -3,9 +3,9 @@ {% block title %}Change state of {{ doc }}{% endblock %} {% block morecss %} -form.change-state select { - width: 22em; -} +form.change-state select { width: 22em; } +form.change-state #id_comment { width: 30em; } +form.change-state .cancel-pub-note { width: 30em; color: #a00; } form.change-state .actions { text-align: right; padding-top: 10px; @@ -29,6 +29,14 @@ form.change-state .actions {
    {{ form.as_table }} + {% if state and state.slug == "rfcqueue" %} + + + + + {% endif %} {% endif %} {% endwith %} + + {% ifequal doc.draft_status "Active" %} @@ -118,6 +116,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. {% endif %} + + @@ -127,7 +127,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. {% else %} {{ doc.friendly_state|safe }} {% endif %} -{% if doc.rfc_editor_state %}
    RFC Editor State: {{ doc.rfc_editor_state|escape }}{% endif %} +{% if info.iana_review_state %}
    IANA Review State: {{ info.iana_review_state.name|escape }}{% endif %} +{% if info.iana_action_state %}
    IANA Action State: {{ info.iana_action_state.name|escape }}{% endif %} +{% if info.rfc_editor_state %}
    RFC Editor State: {{ info.rfc_editor_state|escape }}{% endif %} {% ifequal doc.draft_status "Expired" %} {% if doc.resurrect_requested_by %}(resurrect requested by {{ doc.resurrect_requested_by }}){% endif %} {% endifequal %} diff --git a/ietf/templates/idrfc/doc_tab_history.html b/ietf/templates/idrfc/doc_tab_history.html index 381d08ca9..11b45965d 100644 --- a/ietf/templates/idrfc/doc_tab_history.html +++ b/ietf/templates/idrfc/doc_tab_history.html @@ -42,7 +42,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. {% include "idrfc/doc_diffs.html" %} {% endif %}

    Document history

    -{% if user|in_group:"Area_Director,Secretariat,IANA" and doc.in_ietf_process %} +{% if user|in_group:"Area_Director,Secretariat,IANA,RFC Editor" and doc.in_ietf_process %} diff --git a/ietf/templates/idrfc/document_history.html b/ietf/templates/idrfc/document_history.html index bd193df2d..283689a63 100644 --- a/ietf/templates/idrfc/document_history.html +++ b/ietf/templates/idrfc/document_history.html @@ -53,7 +53,7 @@ {% endif %}

    Document history

    -{% if user|has_role:"Area Director,Secretariat,IANA" %} +{% if user|has_role:"Area Director,Secretariat,IANA,RFC Editor" %} diff --git a/ietf/templates/idrfc/issue_ballot_mailREDESIGN.txt b/ietf/templates/idrfc/issue_ballot_mailREDESIGN.txt index b5623968e..bf3c662b6 100644 --- a/ietf/templates/idrfc/issue_ballot_mailREDESIGN.txt +++ b/ietf/templates/idrfc/issue_ballot_mailREDESIGN.txt @@ -22,7 +22,7 @@ with no "Discuss" positions, are needed for approval. DISCUSSES AND COMMENTS ====================== -{% filter wordwrap:79 %}{% for pos in ad_feedback %}{{ pos.ad.get_name }}: +{% filter wordwrap:79 %}{% for pos in ad_feedback %}{{ pos.ad }}: {% if pos.discuss %}Discuss [{{ pos.discuss_time|date:"Y-m-d" }}]: {{ pos.discuss }} diff --git a/ietf/templates/idrfc/publication_request.txt b/ietf/templates/idrfc/publication_request.txt new file mode 100644 index 000000000..e0de3222d --- /dev/null +++ b/ietf/templates/idrfc/publication_request.txt @@ -0,0 +1,16 @@ +{% load mail_filters %}{% autoescape off %}{% filter wordwrap:73 %} +The document "{{ doc.title }}" <{{ doc.name }}> from the {{ doc.stream }} stream is ready for publication as {{ doc|std_level_prompt }}.{% if group_description %} + +This document is the product of the {{ group_description }}.{% endif %}{% endfilter %} + +URL: {{ doc_url }} + +No IANA allocation in the document requires IETF Consensus or Standards Action. + + +[OPTIONAL: include summary of related discussion of this document in an IETF WG or in the IESG.] + +[OPTIONAL: include statement of the purpose of publishing this document, its intended audience, its merits and significance.] + +[OPTIONAL: include suggested names and contact information for one or more competent and independent potential reviewers for the document (this can speed the review and approval process).] +{% endautoescape %} diff --git a/ietf/templates/idrfc/pulled_from_rfc_queue_email.txt b/ietf/templates/idrfc/pulled_from_rfc_queue_email.txt new file mode 100644 index 000000000..b4f3eb016 --- /dev/null +++ b/ietf/templates/idrfc/pulled_from_rfc_queue_email.txt @@ -0,0 +1,9 @@ +{% autoescape off %}{{ doc.name }} is no longer in the {{ prev_state.name }} state. + +{% if comment %}Explanation: + +{{ comment }} +{% endif %} + +URL: {{ url }} +{% endautoescape %} diff --git a/ietf/templates/idrfc/request_publication.html b/ietf/templates/idrfc/request_publication.html new file mode 100644 index 000000000..c379aed36 --- /dev/null +++ b/ietf/templates/idrfc/request_publication.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} + +{% block title %}Request publication for {{ doc }}{% endblock %} + +{% block morecss %} +form.request-publication #id_subject, +form.request-publication #id_body { width: 50em; } + +form.request-publication #id_body { height: 30em; } +{% endblock %} + +{% block content %} +

    Request publication for {{ doc }}

    + +

    Send a publication request to the RFC Editor for {{ doc }} and move +it to the {{ next_state.name }} stream state.

    + +

    Please edit the message and remove any parts in brackets you do not +fill in. For independent submissions, see the guidelines.

    + + +
    Note: if you pull the draft out of the + {{ state.name }} state, the RFC Editor and IANA will be notified + by email with this comment so they can update their queues.
    Back diff --git a/ietf/templates/idrfc/doc_tab_document_id.html b/ietf/templates/idrfc/doc_tab_document_id.html index ab4a9cf3b..524ca9df6 100644 --- a/ietf/templates/idrfc/doc_tab_document_id.html +++ b/ietf/templates/idrfc/doc_tab_document_id.html @@ -56,12 +56,10 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. {% with info.conflict_reviews as r %}{% if r %}
    IETF Conflict Review: {% filter urlize_ietf_docs %}{{ r|join:","}}{% endfilter %}
    Intended RFC status: -{% with user|in_group:"Area_Director,Secretariat" as add_link %} -{% if add_link %}{% endif %} + {{ doc.underlying_document.intended_std_level|default:"-" }} -{% if add_link %}{% endif %} -{% endwith %} -
    Other versions:
    Document shepherd:{{ stream_info.shepherd|default:"" }}
    Consensus:{{ info.consensus }}

    + + + + + + + +
    From: {{ message.frm }}
    To: {{ message.to }}
    Subject: {{ form.subject }} {{ form.subject.errors }}
    Message: {{ form.body }} {{ form.body.errors }}
    + Back + + +
    +
    +{% endblock %} diff --git a/ietf/utils/mail.py b/ietf/utils/mail.py index 29c3808e0..07c9d8745 100644 --- a/ietf/utils/mail.py +++ b/ietf/utils/mail.py @@ -26,7 +26,7 @@ test_mode = False outbox = [] def empty_outbox(): - outbox[:] = [] + outbox[:] = [] def add_headers(msg): if not(msg.has_key('Message-ID')): @@ -187,16 +187,34 @@ def send_mail_mime(request, to, frm, subject, msg, cc=None, extra=None, toUser=F msg['X-Tracker-Bcc']=bcc copy_email(msg, copy_to) -def send_mail_preformatted(request, preformatted): +def send_mail_preformatted(request, preformatted, extra={}, override={}): """Parse preformatted string containing mail with From:, To:, ..., and send it through the standard IETF mail interface (inserting extra headers as needed).""" msg = message_from_string(preformatted.encode("utf-8")) - extra = copy.copy(msg) - for key in ['To', 'From', 'Subject', ]: - del extra[key] - send_mail_text(request, msg['To'], msg["From"], msg["Subject"], msg.get_payload(), extra=extra) -def send_mail_message(request, message): + for k, v in override.iteritems(): + if k in msg: + del msg[k] + msg[k] = v + + headers = copy.copy(msg) + for key in ['To', 'From', 'Subject']: + del headers[key] + for k, v in extra.iteritems(): + if k in headers: + del headers[k] + headers[k] = v + + send_mail_text(request, msg['To'], msg["From"], msg["Subject"], msg.get_payload(), extra=headers) + +def send_mail_message(request, message, extra={}): + """Send a Message object.""" # note that this doesn't handle MIME messages at the moment - send_mail_text(request, message.to, message.frm, message.subject, message.body, cc=message.cc, bcc=message.bcc, extra={ 'Reply-to': message.reply_to }) + + e = extra.copy() + if message.reply_to: + e['Reply-to'] = message.reply_to + + send_mail_text(request, message.to, message.frm, message.subject, + message.body, cc=message.cc, bcc=message.bcc, extra=e) From 444a57a26646be41b50930075c3c18c099c1e401 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Thu, 20 Sep 2012 10:42:55 +0000 Subject: [PATCH 13/47] Notify iab@iab.org rather than the IAB Chair on new versions of IAB stream drafts - Legacy-Id: 4867 --- ietf/submit/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/submit/utils.py b/ietf/submit/utils.py index 8f6f97eaf..6152bc43c 100644 --- a/ietf/submit/utils.py +++ b/ietf/submit/utils.py @@ -268,7 +268,7 @@ def announce_new_versionREDESIGN(request, submission, draft, state_change_msg): to_email.append(draft.ad.role_email("ad").address) if draft.stream_id == "iab": - to_email.append("IAB Chair ") + to_email.append("IAB ") elif draft.stream_id == "ise": to_email.append("Independent Submission Editor ") elif draft.stream_id == "irtf": From ff077f4b1f8be2be523518d8c25ce1adb47ecd99 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Thu, 20 Sep 2012 11:53:56 +0000 Subject: [PATCH 14/47] Rename /sync/iana|rfceditor/update/ to /sync/iana|rfceditor/notify/, and put the thing to notify in the URL, like /sync/iana/notify/changes/, instead of using POST parameters; refactor the two sync views to use the same for IANA and RFC Editor; require Secretariat or IANA/RFC Editor roles for triggering the updates and support passing username and password for auth to make it easier in the other end once we move away from HTTP Auth - Legacy-Id: 4868 --- ietf/sync/urls.py | 3 +- ietf/sync/views.py | 125 +++++++++++++++++++------------- ietf/templates/sync/notify.html | 13 ++++ ietf/templates/sync/update.html | 27 ------- 4 files changed, 88 insertions(+), 80 deletions(-) create mode 100644 ietf/templates/sync/notify.html delete mode 100644 ietf/templates/sync/update.html diff --git a/ietf/sync/urls.py b/ietf/sync/urls.py index f99cc76c1..d04919822 100644 --- a/ietf/sync/urls.py +++ b/ietf/sync/urls.py @@ -2,7 +2,6 @@ from django.conf.urls.defaults import patterns, url urlpatterns = patterns('', url(r'^discrepancies/$', 'ietf.sync.views.discrepancies'), - url(r'^iana/update/$', 'ietf.sync.views.update_iana'), - url(r'^rfc-editor/update/$', 'ietf.sync.views.update_rfc_editor'), + url(r'^(?P\w+)/notify/(?P\w+)/$', 'ietf.sync.views.notify'), ) diff --git a/ietf/sync/views.py b/ietf/sync/views.py index c6b9c7fc1..58d793c56 100644 --- a/ietf/sync/views.py +++ b/ietf/sync/views.py @@ -1,13 +1,15 @@ import subprocess, os -from django.http import HttpResponse +from django.http import HttpResponse, HttpResponseForbidden from django.shortcuts import render_to_response from django.template import RequestContext from django.template.loader import render_to_string +from django.conf import settings from django import forms from django.db.models import Q +from django.contrib.auth.models import User -from ietf.ietfauth.decorators import role_required +from ietf.ietfauth.decorators import role_required, has_role from ietf.doc.models import * from ietf.sync import iana, rfceditor from ietf.sync.discrepancies import find_discrepancies @@ -22,58 +24,79 @@ def discrepancies(request): dict(sections=sections), context_instance=RequestContext(request)) +def notify(request, org, notification): + """Notify that something has changed at another site to trigger a + run of one of the sync scripts.""" -class UpdateIanaForm(forms.Form): - protocols_page = forms.BooleanField(initial=False, required=False, help_text="For when a reference to an RFC has been added to the IANA protocols page" % iana.PROTOCOLS_URL) - changes = forms.BooleanField(initial=False, required=False, help_text="For new changes at the changes JSON dump" % iana.CHANGES_URL) + known_orgs = { + "iana": "IANA", + "rfceditor": "RFC Editor", + } -def update_iana(request): - if request.method == 'POST': - form = UpdateIanaForm(request.POST) - if form.is_valid(): - failed = False - if form.cleaned_data["protocols_page"]: - failed = failed or subprocess.call(["python", os.path.join(SYNC_BIN_PATH, "iana-protocols-updates")]) - if form.cleaned_data["changes"]: - failed = failed or subprocess.call(["python", os.path.join(SYNC_BIN_PATH, "iana-changes-updates")]) + if org not in known_orgs: + raise Http404 - if failed: - return HttpResponse("FAIL") - else: - return HttpResponse("OK") - else: - form = UpdateIanaForm() + # handle auth, to make it easier for the other end, you can send + # the username/password as POST parameters instead of having to + # visit the login page + user = request.user - return render_to_response('sync/update.html', - dict(form=form, - org="IANA", - ), - context_instance=RequestContext(request)) - - -class UpdateRFCEditorForm(forms.Form): - queue = forms.BooleanField(initial=False, required=False, help_text="For when queue2.xml has been updated" % rfceditor.QUEUE_URL) - index = forms.BooleanField(initial=False, required=False, help_text="For when rfc-index.xml has been updated" % rfceditor.INDEX_URL) - -def update_rfc_editor(request): - if request.method == 'POST': - form = UpdateRFCEditorForm(request.POST) - if form.is_valid(): - failed = False - if form.cleaned_data["queue"]: - failed = failed or subprocess.call(["python", os.path.join(SYNC_BIN_PATH, "rfc-editor-queue-updates")]) - if form.cleaned_data["index"]: - failed = failed or subprocess.call(["python", os.path.join(SYNC_BIN_PATH, "rfc-editor-index-updates")]) - - if failed: - return HttpResponse("FAIL") - else: - return HttpResponse("OK") - else: - form = UpdateRFCEditorForm() - - return render_to_response('sync/update.html', - dict(form=form, - org="RFC Editor", + username = request.POST.get("username") or request.GET.get("username") + password = request.POST.get("password") or request.GET.get("password") + + if username and password: + if settings.SERVER_MODE == "production" and not request.is_secure(): + return HttpResponseForbidden("You must use HTTPS when sending username/password") + + if not user.is_authenticated(): + try: + user = User.objects.get(username=username) + except User.DoesNotExist: + return HttpResponse("Invalid username/password") + + if not user.check_password(password): + return HttpResponse("Invalid username/password") + + if not has_role(user, ("Secretariat", known_orgs[org])): + return HttpResponseForbidden("You do not have the necessary permissions to view this page") + + known_notifications = { + "protocols": "an added reference to an RFC at the IANA protocols page" % iana.PROTOCOLS_URL, + "changes": "new changes at the changes JSON dump" % iana.CHANGES_URL, + "queue": "new changes to queue2.xml" % rfceditor.QUEUE_URL, + "index": "new changes to rfc-index.xml" % rfceditor.INDEX_URL, + } + + if notification not in known_notifications: + raise Http404 + + if request.method == "POST": + def runscript(name): + p = subprocess.Popen(["python", os.path.join(SYNC_BIN_PATH, name)], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + out, _ = p.communicate() + return (p.returncode, out) + + if notification == "protocols": + failed, out = runscript("iana-protocols-updates") + + if notification == "changes": + failed, out = runscript("iana-changes-updates") + + if notification == "queue": + failed, out = runscript("rfc-editor-queue-updates") + + if notification == "index": + failed, out = runscript("rfc-editor-index-updates") + + if failed: + return HttpResponse("FAIL\n\n" + out, content_type="text/plain") + else: + return HttpResponse("OK", content_type="text/plain") + + return render_to_response('sync/notify.html', + dict(org=known_orgs[org], + notification=notification, + help_text=known_notifications[notification], ), context_instance=RequestContext(request)) diff --git a/ietf/templates/sync/notify.html b/ietf/templates/sync/notify.html new file mode 100644 index 000000000..4092bb535 --- /dev/null +++ b/ietf/templates/sync/notify.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block title %}Trigger {{ notification }} sync for {{ org }}{% endblock %} + +{% block content %} +

    Trigger {{ notification }} sync for {{ org }}

    + +

    Update the Datatracker with {{ help_text|safe }} at {{ org }}.

    + +
    + +
    +{% endblock %} diff --git a/ietf/templates/sync/update.html b/ietf/templates/sync/update.html deleted file mode 100644 index 86b6a73f8..000000000 --- a/ietf/templates/sync/update.html +++ /dev/null @@ -1,27 +0,0 @@ -{% extends "base.html" %} - -{% block title %}Trigger sync for {{ org }}{% endblock %} - -{% block morecss %} -.sync-form .help { font-style: italic; padding-left: 2em; } -.sync-form input[type=submit] { margin-top: 1em; } -{% endblock %} - - -{% block content %} -

    Trigger sync for {{ org }}

    - -

    Update the Datatracker with information from {{ org }}. Select -which parts to trigger a sync for:

    - -
    -{% for field in form %} -
    - {{ field }} - {{ field.label_tag }} - {% if field.help_text %}{{ field.help_text|safe }}{% endif %} -
    -{% endfor %} - -
    -{% endblock %} From 49fc493526b9a8c02e9239d138bf0364118ca482 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Thu, 20 Sep 2012 12:29:14 +0000 Subject: [PATCH 15/47] Clean up doc.json view, only export plain fields and fields requested in RFC 6359, more can be added later if necessary; export more info on the group - Legacy-Id: 4869 --- ietf/group/models.py | 6 +++++ ietf/idrfc/mails.py | 5 +--- ietf/idrfc/views_doc.py | 60 +++++++++++++++++++++-------------------- 3 files changed, 38 insertions(+), 33 deletions(-) diff --git a/ietf/group/models.py b/ietf/group/models.py index b44fa98a1..7546fd37b 100644 --- a/ietf/group/models.py +++ b/ietf/group/models.py @@ -24,6 +24,12 @@ class GroupInfo(models.Model): def __unicode__(self): return self.name + def name_with_acronym(self): + res = self.name + if self.type_id in ("wg", "rg", "area"): + res += " %s (%s)" % (self.type, self.acronym) + return res + class Meta: abstract = True diff --git a/ietf/idrfc/mails.py b/ietf/idrfc/mails.py index 33edd1137..0703a4815 100644 --- a/ietf/idrfc/mails.py +++ b/ietf/idrfc/mails.py @@ -287,10 +287,7 @@ if settings.USE_DB_REDESIGN_PROXY_CLASSES: def generate_publication_request(request, doc): group_description = "" if doc.group and doc.group.acronym != "none": - group_description = doc.group.name - if doc.group.type_id in ("wg", "rg", "area"): - group_description += " %s (%s)" % (doc.group.type, doc.group.acronym) - + group_description = doc.group.name_with_acronym() return render_to_string("idrfc/publication_request.txt", dict(doc=doc, diff --git a/ietf/idrfc/views_doc.py b/ietf/idrfc/views_doc.py index 5e9c43451..4c8c42649 100644 --- a/ietf/idrfc/views_doc.py +++ b/ietf/idrfc/views_doc.py @@ -55,7 +55,6 @@ from ietf.doc.utils import * from ietf.utils.history import find_history_active_at from ietf.ietfauth.decorators import has_role - def render_document_top(request, doc, tab, name): tabs = [] tabs.append(("Document", "document", urlreverse("ietf.idrfc.views_doc.document_main", kwargs=dict(name=name)), True)) @@ -373,44 +372,47 @@ def document_ballot(request, name, ballot_id=None): context_instance=RequestContext(request)) def document_json(request, name): - # old interface - r = re.compile("^rfc([1-9][0-9]*)$") - m = r.match(name) - if m: - rfc_number = int(m.group(1)) - rfci = get_object_or_404(RfcIndex, rfc_number=rfc_number) - doc = RfcWrapper(rfci) - else: - id = get_object_or_404(InternetDraft, filename=name) - doc = IdWrapper(draft=id) + doc = get_object_or_404(Document, docalias__name=name) - from idrfc_wrapper import jsonify_helper + def extract_name(s): + return s.name if s else None - if isinstance(doc, RfcWrapper): - data = jsonify_helper(doc, ['rfc_number', 'title', 'publication_date', 'maturity_level', 'obsoleted_by','obsoletes','updated_by','updates','also','has_errata','stream_name','file_types','in_ietf_process', 'friendly_state']) - else: - data = jsonify_helper(doc, ['draft_name', 'draft_status', 'latest_revision', 'rfc_number', 'title', 'tracker_id', 'publication_date','rfc_editor_state', 'replaced_by', 'replaces', 'in_ietf_process', 'file_types', 'group_acronym', 'stream_id','friendly_state', 'abstract', 'ad_name']) - if doc.in_ietf_process(): - data['ietf_process'] = doc.ietf_process.dict() + data = {} - # add some more fields using the new interface - d = get_object_or_404(Document, docalias__name=name) + data["name"] = doc.name + data["rev"] = doc.rev + data["time"] = doc.time.strftime("%Y-%m-%d %H:%M:%S") + data["group"] = None + if doc.group: + data["group"] = dict( + name=doc.group.name, + type=extract_name(doc.group.type), + acronym=doc.group.acronym) + data["expires"] = doc.expires.strftime("%Y-%m-%d %H:%M:%S") + data["title"] = doc.title + data["abstract"] = doc.abstract + data["aliases"] = list(doc.docalias_set.values_list("name", flat=True)) + data["state"] = extract_name(doc.get_state()) + data["intended_std_level"] = extract_name(doc.intended_std_level) + data["std_level"] = extract_name(doc.std_level) data["authors"] = [ dict(name=e.person.name, email=e.address, affiliation=e.person.affiliation) - for e in Email.objects.filter(documentauthor__document=d).select_related("person").order_by("documentauthor__order") + for e in Email.objects.filter(documentauthor__document=doc).select_related("person").order_by("documentauthor__order") ] - e = d.latest_event(ConsensusDocEvent, type="changed_consensus") - data["consensus"] = e.consensus if e else None - data["stream"] = d.stream.name if d.stream else None - data["shepherd"] = d.shepherd.formatted_email() if d.shepherd else None + data["shepherd"] = doc.shepherd.formatted_email() if doc.shepherd else None + data["ad"] = doc.ad.role_email("ad").formatted_email() if doc.ad else None - def state_name(s): - return s.name if s else None + if doc.type_id == "draft": + data["iesg_state"] = extract_name(doc.get_state("draft-iesg")) + data["rfceditor_state"] = extract_name(doc.get_state("draft-rfceditor")) + data["iana_review_state"] = extract_name(doc.get_state("draft-iana-review")) + data["iana_action_state"] = extract_name(doc.get_state("draft-iana-action")) - data["iana_review_state"] = state_name(d.get_state("draft-iana-review")) - data["iana_action_state"] = state_name(d.get_state("draft-iana-action")) + e = doc.latest_event(ConsensusDocEvent, type="changed_consensus") + data["consensus"] = e.consensus if e else None + data["stream"] = extract_name(doc.stream) return HttpResponse(json.dumps(data, indent=2), mimetype='text/plain') From 1072f3b5e4e89c93bc64a78fb87c7cf5ff2a9e75 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Thu, 20 Sep 2012 13:30:32 +0000 Subject: [PATCH 16/47] Fix bitrot bug in RFC Editor branch in has_role - Legacy-Id: 4870 --- ietf/ietfauth/decorators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/ietfauth/decorators.py b/ietf/ietfauth/decorators.py index a49b1bc03..973362c5b 100644 --- a/ietf/ietfauth/decorators.py +++ b/ietf/ietfauth/decorators.py @@ -89,7 +89,7 @@ def has_role(user, role_names): "Area Director": Q(person=person, name__in=("pre-ad", "ad"), group__type="area", group__state="active"), "Secretariat": Q(person=person, name="secr", group__acronym="secretariat"), "IANA": Q(person=person, name="auth", group__acronym="iana"), - "RFC Editor": Q(email__person=person, name="auth", group__acronym="rfceditor"), + "RFC Editor": Q(person=person, name="auth", group__acronym="rfceditor"), "IAD": Q(person=person, name="admdir", group__acronym="ietf"), "IETF Chair": Q(person=person, name="chair", group__acronym="ietf"), "IAB Chair": Q(person=person, name="chair", group__acronym="iab"), From 79a16d913b9b614de762cff90aada3ed5ab2e337 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Thu, 20 Sep 2012 14:16:08 +0000 Subject: [PATCH 17/47] Add order on IANA Action states - Legacy-Id: 4871 --- .../0005_add_iana_rfc_editor_states.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/ietf/doc/migrations/0005_add_iana_rfc_editor_states.py b/ietf/doc/migrations/0005_add_iana_rfc_editor_states.py index cf4d992be..5a17ab484 100644 --- a/ietf/doc/migrations/0005_add_iana_rfc_editor_states.py +++ b/ietf/doc/migrations/0005_add_iana_rfc_editor_states.py @@ -8,15 +8,15 @@ class Migration(DataMigration): def forwards(self, orm): t = orm.StateType.objects.get_or_create(slug="draft-iana-action", label="IANA Action state")[0] - orm.State.objects.get_or_create(type=t, slug='newdoc', name='New Document', desc="A new document has been received by IANA, but no actions have been taken") - orm.State.objects.get_or_create(type=t, slug='inprog', name='In Progress', desc="IANA is currently processing the actions for this document") - orm.State.objects.get_or_create(type=t, slug='waitauth', name='Waiting on Authors', desc="IANA is waiting on the document's authors to respond") - orm.State.objects.get_or_create(type=t, slug='waitad', name='Waiting on ADs', desc="IANA is waiting on the IETF Area Directors to respond") - orm.State.objects.get_or_create(type=t, slug='waitwgc', name='Waiting on WGC', desc="IANA is waiting on the IETF Working Group Chairs to respond") - orm.State.objects.get_or_create(type=t, slug='waitrfc', name='Waiting on RFC Editor', desc="IANA has notified the RFC Editor that the actions have been completed") - orm.State.objects.get_or_create(type=t, slug='rfcedack', name='RFC-Ed-Ack', desc="Request completed. The RFC Editor has acknowledged receipt of IANA's message that the actions have been completed") - orm.State.objects.get_or_create(type=t, slug='onhold', name='On Hold', desc="IANA has suspended work on the document") - orm.State.objects.get_or_create(type=t, slug='noic', name='No IC', desc="Request completed. There were no IANA actions for this document") + orm.State.objects.get_or_create(type=t, slug='newdoc', name='New Document', desc="A new document has been received by IANA, but no actions have been taken", order=1) + orm.State.objects.get_or_create(type=t, slug='inprog', name='In Progress', desc="IANA is currently processing the actions for this document", order=2) + orm.State.objects.get_or_create(type=t, slug='waitauth', name='Waiting on Authors', desc="IANA is waiting on the document's authors to respond", order=3) + orm.State.objects.get_or_create(type=t, slug='waitad', name='Waiting on ADs', desc="IANA is waiting on the IETF Area Directors to respond", order=4) + orm.State.objects.get_or_create(type=t, slug='waitwgc', name='Waiting on WGC', desc="IANA is waiting on the IETF Working Group Chairs to respond", order=5) + orm.State.objects.get_or_create(type=t, slug='waitrfc', name='Waiting on RFC Editor', desc="IANA has notified the RFC Editor that the actions have been completed", order=6) + orm.State.objects.get_or_create(type=t, slug='rfcedack', name='RFC-Ed-Ack', desc="Request completed. The RFC Editor has acknowledged receipt of IANA's message that the actions have been completed", order=7) + orm.State.objects.get_or_create(type=t, slug='onhold', name='On Hold', desc="IANA has suspended work on the document", order=8) + orm.State.objects.get_or_create(type=t, slug='noic', name='No IC', desc="Request completed. There were no IANA actions for this document", order=9) t = orm.StateType.objects.get_or_create(slug="draft-iana-review", label="IANA Review state")[0] orm.State.objects.get_or_create(type=t, slug="need-rev", name='IANA Review Needed', desc="Document has not yet been reviewed by IANA.", order=1) From 2415ae8687e75168b820574a99d2e4d2aa6a6bab Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Fri, 21 Sep 2012 09:01:00 +0000 Subject: [PATCH 18/47] Support blank password in sync notify view (for testing) - Legacy-Id: 4874 --- ietf/sync/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/sync/views.py b/ietf/sync/views.py index 58d793c56..ecfa9f2f8 100644 --- a/ietf/sync/views.py +++ b/ietf/sync/views.py @@ -44,7 +44,7 @@ def notify(request, org, notification): username = request.POST.get("username") or request.GET.get("username") password = request.POST.get("password") or request.GET.get("password") - if username and password: + if username != None and password != None: if settings.SERVER_MODE == "production" and not request.is_secure(): return HttpResponseForbidden("You must use HTTPS when sending username/password") From 915908c203d05278ebb235473787c546fa81acb4 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Fri, 21 Sep 2012 09:15:35 +0000 Subject: [PATCH 19/47] Add HTTP auth to IANA sync, password is supposed to be saved in settings_local.py - Legacy-Id: 4875 --- ietf/bin/iana-changes-updates | 2 +- ietf/settings.py | 3 ++- ietf/sync/iana.py | 10 ++++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/ietf/bin/iana-changes-updates b/ietf/bin/iana-changes-updates index 4627e6890..2c913b2fa 100755 --- a/ietf/bin/iana-changes-updates +++ b/ietf/bin/iana-changes-updates @@ -33,7 +33,7 @@ CLOCK_SKEW_COMPENSATION = 5 # seconds MAX_INTERVAL_ACCEPTED_BY_IANA = datetime.timedelta(hours=23) -start = datetime.datetime.now() - datetime.timedelta(hours=23) + CLOCK_SKEW_COMPENSATION +start = datetime.datetime.now() - datetime.timedelta(hours=23) + datetime.timedelta(seconds=CLOCK_SKEW_COMPENSATION) if options.start: start = datetime.datetime.strptime(options.start, "%Y-%m-%d %H:%M:%S") diff --git a/ietf/settings.py b/ietf/settings.py index 2e1e6b124..3d7154877 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -244,7 +244,8 @@ else: IPR_EMAIL_TO = ['ietf-ipr@ietf.org', ] DOC_APPROVAL_EMAIL_CC = ["RFC Editor ", ] - +# Put real password in settings_local.py +IANA_SYNC_PASSWORD = "secret" # Liaison Statement Tool settings LIAISON_UNIVERSAL_FROM = 'Liaison Statement Management Tool ' diff --git a/ietf/sync/iana.py b/ietf/sync/iana.py index 30c13260b..df8ad6d1d 100644 --- a/ietf/sync/iana.py +++ b/ietf/sync/iana.py @@ -1,6 +1,7 @@ -import re, urllib2, json, email +import re, urllib2, json, email, base64 from django.utils.http import urlquote +from django.conf import settings from ietf.doc.models import * from ietf.doc.utils import add_state_change_event @@ -56,7 +57,12 @@ def update_rfc_log_from_protocol_page(rfc_names, rfc_must_published_later_than): def fetch_changes_json(url, start, end): url += "?start=%s&end=%s" % (urlquote(local_timezone_to_utc(start).strftime("%Y-%m-%d %H:%M:%S")), urlquote(local_timezone_to_utc(end).strftime("%Y-%m-%d %H:%M:%S"))) - f = urllib2.urlopen(url) + request = urllib2.Request(url) + # HTTP basic auth + username = "ietfsync" + password = settings.IANA_SYNC_PASSWORD + request.add_header("Authorization", "Basic %s" % base64.encodestring("%s:%s" % (username, password)).replace("\n", "")) + f = urllib2.urlopen(request) text = f.read() f.close() return text From e9c6c9e34774273776e3b1c27bcd9fd632a65676 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Fri, 21 Sep 2012 10:23:45 +0000 Subject: [PATCH 20/47] Only show new consensus field for IETF and IRTF stream documents - Legacy-Id: 4876 --- ietf/idrfc/views_doc.py | 13 ++++++++----- ietf/iesg/views.py | 12 ++++++------ ietf/templates/idrfc/doc_tab_document_id.html | 2 ++ 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/ietf/idrfc/views_doc.py b/ietf/idrfc/views_doc.py index 4c8c42649..46dc0ba0f 100644 --- a/ietf/idrfc/views_doc.py +++ b/ietf/idrfc/views_doc.py @@ -410,8 +410,9 @@ def document_json(request, name): data["iana_review_state"] = extract_name(doc.get_state("draft-iana-review")) data["iana_action_state"] = extract_name(doc.get_state("draft-iana-action")) - e = doc.latest_event(ConsensusDocEvent, type="changed_consensus") - data["consensus"] = e.consensus if e else None + if doc.stream_id in ("ietf", "irtf"): + e = doc.latest_event(ConsensusDocEvent, type="changed_consensus") + data["consensus"] = e.consensus if e else None data["stream"] = extract_name(doc.stream) return HttpResponse(json.dumps(data, indent=2), mimetype='text/plain') @@ -473,9 +474,11 @@ def document_main_idrfc(request, name, tab): info['rfc_editor_state'] = id.get_state("draft-rfceditor") info['iana_review_state'] = id.get_state("draft-iana-review") info['iana_action_state'] = id.get_state("draft-iana-action") - e = id.latest_event(ConsensusDocEvent, type="changed_consensus") - info["consensus"] = nice_consensus(e and e.consensus) - info["can_edit_consensus"] = can_edit_consensus(id, request.user) + info["consensus"] = None + if id.stream_id in ("ietf", "irtf"): + e = id.latest_event(ConsensusDocEvent, type="changed_consensus") + info["consensus"] = nice_consensus(e and e.consensus) + info["can_edit_consensus"] = can_edit_consensus(id, request.user) info["can_edit_intended_std_level"] = can_edit_intended_std_level(id, request.user) (content1, content2) = _get_html( diff --git a/ietf/iesg/views.py b/ietf/iesg/views.py index cc9cf37a6..e8e0cd505 100644 --- a/ietf/iesg/views.py +++ b/ietf/iesg/views.py @@ -141,7 +141,6 @@ def get_doc_section(id): def get_doc_sectionREDESIGN(doc): if doc.type_id == 'draft': - states = [16,17,18,19,20,21] if doc.intended_std_level_id in ["bcp", "ds", "ps", "std"]: s = "2" else: @@ -154,7 +153,7 @@ def get_doc_sectionREDESIGN(doc): s = s + "3" else: s = s + "2" - if not doc.get_state_slug=="rfc" and doc.get_state('draft-iesg').order not in states: + if not doc.get_state_slug=="rfc" and doc.get_state_slug('draft-iesg') not in ("lc", "writeupw", "goaheadw", "iesg-eva", "defer"): s = s + "3" elif doc.returning_item(): s = s + "2" @@ -214,10 +213,11 @@ def agenda_docs(date, next_agenda): if e: doc.lastcall_expires = e.expires - doc.consensus = "Unknown" - e = doc.latest_event(ConsensusDocEvent, type="changed_consensus") - if e: - doc.consensus = "Yes" if e.consensus else "No" + if doc.stream_id in ("ietf", "irtf"): + doc.consensus = "Unknown" + e = doc.latest_event(ConsensusDocEvent, type="changed_consensus") + if e: + doc.consensus = "Yes" if e.consensus else "No" docmatches.append(doc) diff --git a/ietf/templates/idrfc/doc_tab_document_id.html b/ietf/templates/idrfc/doc_tab_document_id.html index 524ca9df6..5847aca0d 100644 --- a/ietf/templates/idrfc/doc_tab_document_id.html +++ b/ietf/templates/idrfc/doc_tab_document_id.html @@ -116,7 +116,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. {% endif %} Document shepherd:{{ stream_info.shepherd|default:"" }} +{% if info.consensus %} Consensus:{{ info.consensus }} +{% endif %}
    From 2ab4161daf9677ce4937c22f327a3e9ba40f881d Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Fri, 21 Sep 2012 13:22:04 +0000 Subject: [PATCH 21/47] Return 500 on sync notify errors - Legacy-Id: 4877 --- ietf/sync/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ietf/sync/views.py b/ietf/sync/views.py index ecfa9f2f8..12d5ae1a0 100644 --- a/ietf/sync/views.py +++ b/ietf/sync/views.py @@ -1,6 +1,6 @@ import subprocess, os -from django.http import HttpResponse, HttpResponseForbidden +from django.http import HttpResponse, HttpResponseForbidden, HttpResponseServerError from django.shortcuts import render_to_response from django.template import RequestContext from django.template.loader import render_to_string @@ -90,7 +90,7 @@ def notify(request, org, notification): failed, out = runscript("rfc-editor-index-updates") if failed: - return HttpResponse("FAIL\n\n" + out, content_type="text/plain") + return HttpResponseServerError("FAIL\n\n" + out, content_type="text/plain") else: return HttpResponse("OK", content_type="text/plain") From e569bf3f9009bda5e42ad2152f01dd0d432d4223 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Mon, 24 Sep 2012 10:08:41 +0000 Subject: [PATCH 22/47] Improve the syslog output for the IANA review email script, also handle error cases more graceful - Legacy-Id: 4880 --- ietf/bin/iana-review-email | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/ietf/bin/iana-review-email b/ietf/bin/iana-review-email index 85f4daa2e..71e4100e9 100755 --- a/ietf/bin/iana-review-email +++ b/ietf/bin/iana-review-email @@ -15,13 +15,19 @@ management.setup_environ(settings) syslog.openlog(os.path.basename(__file__), syslog.LOG_PID, syslog.LOG_LOCAL0) from ietf.sync.iana import * +from ietf.doc.models import Document msg = sys.stdin.read() -syslog.syslog("Reading IANA review email") - doc_name, review_time, by, comment = parse_review_email(msg) -add_review_comment(doc_name, review_time, by, comment) + +syslog.syslog(u"Read IANA review email for %s at %s by %s" % (doc_name, review_time, by)) if by.name == "(System)": syslog.syslog("WARNING: person responsible for email does not have a IANA role") + +try: + add_review_comment(doc_name, review_time, by, comment) +except Document.DoesNotExist: + syslog.syslog("ERROR: unknown document %s" % doc_name) + From 91bea253827f6c12d3c71a8b0c8c722479813eab Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Mon, 24 Sep 2012 11:52:23 +0000 Subject: [PATCH 23/47] Fix bug in the /accounts/testmail/ thing (missing import, probably broken by a refactor some time ago) - Legacy-Id: 4881 --- ietf/ietfauth/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/ietfauth/views.py b/ietf/ietfauth/views.py index eee1e85ab..9c737186c 100644 --- a/ietf/ietfauth/views.py +++ b/ietf/ietfauth/views.py @@ -46,7 +46,7 @@ from django.utils.http import urlquote from django.utils import simplejson as json from django.utils.translation import ugettext as _ -from ietf.ietfauth.forms import RegistrationForm, PasswordForm, RecoverPasswordForm +from ietf.ietfauth.forms import RegistrationForm, PasswordForm, RecoverPasswordForm, TestEmailForm def index(request): return render_to_response('registration/index.html', context_instance=RequestContext(request)) From d47878656f8cac57a3fc86ab63ade66944cc6b51 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Mon, 24 Sep 2012 11:53:30 +0000 Subject: [PATCH 24/47] Fix a bug in the IANA changes logging, fix spelling mistake, remove debug print - Legacy-Id: 4882 --- ietf/sync/iana.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ietf/sync/iana.py b/ietf/sync/iana.py index df8ad6d1d..f588fb02d 100644 --- a/ietf/sync/iana.py +++ b/ietf/sync/iana.py @@ -93,8 +93,8 @@ def parse_changes_json(text): def update_history_with_changes(changes, send_email=True): """Take parsed changes from IANA and apply them. Note that we - expect to get these in chronologically sorted, otherwise the - change descriptions generated may not be right.""" + expect to get these chronologically sorted, otherwise the change + descriptions generated may not be right.""" # build up state lookup states = {} @@ -162,7 +162,6 @@ def update_history_with_changes(changes, send_email=True): if c["state"] not in states[kind]: warnings.append("Unknown IANA %s state %s (%s)" % (kind, c["state"], timestamp)) - print "Unknown IANA %s state %s" % (kind, c["state"]) continue state = states[kind][c["state"]] @@ -183,7 +182,8 @@ def update_history_with_changes(changes, send_email=True): prev_state = doc.get_state(state_type) e = add_state_change_event(doc, system, prev_state, state, timestamp) - added_events.append(e) + if e: + added_events.append(e) if not StateDocEvent.objects.filter(doc=doc, time__gt=timestamp, state_type=state_type): save_document_in_history(doc) From a8671798fdfcac44e9e4e4daba8f8ad99a349a36 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Fri, 28 Sep 2012 14:49:37 +0000 Subject: [PATCH 25/47] Return None explicitly rather than implicitly - Legacy-Id: 4886 --- ietf/doc/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/doc/utils.py b/ietf/doc/utils.py index dd5abd95d..79b1ea978 100644 --- a/ietf/doc/utils.py +++ b/ietf/doc/utils.py @@ -204,7 +204,7 @@ def log_state_changed(request, doc, by, new_description, old_description): def add_state_change_event(doc, by, prev_state, new_state, timestamp=None): """Add doc event to explain that state change just happened.""" if prev_state == new_state: - return + return None e = StateDocEvent(doc=doc, by=by) e.type = "changed_state" From 33baa215b8af89b42362a814854ec22be4e0eddb Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Wed, 3 Oct 2012 08:52:24 +0000 Subject: [PATCH 26/47] Send new draft revision notifications to iab-stream@iab.org as per IABs request - Legacy-Id: 4889 --- ietf/submit/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/submit/utils.py b/ietf/submit/utils.py index 6152bc43c..09eb0a34a 100644 --- a/ietf/submit/utils.py +++ b/ietf/submit/utils.py @@ -268,7 +268,7 @@ def announce_new_versionREDESIGN(request, submission, draft, state_change_msg): to_email.append(draft.ad.role_email("ad").address) if draft.stream_id == "iab": - to_email.append("IAB ") + to_email.append("IAB Stream ") elif draft.stream_id == "ise": to_email.append("Independent Submission Editor ") elif draft.stream_id == "irtf": From 3b9bdbcecce31baa9191a1b0aaccfa818a83f5fd Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Thu, 4 Oct 2012 08:41:09 +0000 Subject: [PATCH 27/47] According to Alice Russo IAB has recently added the consensus option for their drafts, so make it possible for them to edit it too - Legacy-Id: 4890 --- ietf/idrfc/views_doc.py | 4 ++-- ietf/iesg/views.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ietf/idrfc/views_doc.py b/ietf/idrfc/views_doc.py index 46dc0ba0f..9fdca872c 100644 --- a/ietf/idrfc/views_doc.py +++ b/ietf/idrfc/views_doc.py @@ -410,7 +410,7 @@ def document_json(request, name): data["iana_review_state"] = extract_name(doc.get_state("draft-iana-review")) data["iana_action_state"] = extract_name(doc.get_state("draft-iana-action")) - if doc.stream_id in ("ietf", "irtf"): + if doc.stream_id in ("ietf", "irtf", "iab"): e = doc.latest_event(ConsensusDocEvent, type="changed_consensus") data["consensus"] = e.consensus if e else None data["stream"] = extract_name(doc.stream) @@ -475,7 +475,7 @@ def document_main_idrfc(request, name, tab): info['iana_review_state'] = id.get_state("draft-iana-review") info['iana_action_state'] = id.get_state("draft-iana-action") info["consensus"] = None - if id.stream_id in ("ietf", "irtf"): + if id.stream_id in ("ietf", "irtf", "iab"): e = id.latest_event(ConsensusDocEvent, type="changed_consensus") info["consensus"] = nice_consensus(e and e.consensus) info["can_edit_consensus"] = can_edit_consensus(id, request.user) diff --git a/ietf/iesg/views.py b/ietf/iesg/views.py index e8e0cd505..3c3dd7242 100644 --- a/ietf/iesg/views.py +++ b/ietf/iesg/views.py @@ -213,7 +213,7 @@ def agenda_docs(date, next_agenda): if e: doc.lastcall_expires = e.expires - if doc.stream_id in ("ietf", "irtf"): + if doc.stream_id in ("ietf", "irtf", "iab"): doc.consensus = "Unknown" e = doc.latest_event(ConsensusDocEvent, type="changed_consensus") if e: From 400218f40b0dc29ce01602de01c20c732a0c8093 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Fri, 5 Oct 2012 10:30:54 +0000 Subject: [PATCH 28/47] Reword publication request email from alternate streams from suggestion by Sandy Ginoza and include consensus info - Legacy-Id: 4894 --- ietf/idrfc/mails.py | 6 +++++- ietf/templates/idrfc/publication_request.txt | 14 +++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/ietf/idrfc/mails.py b/ietf/idrfc/mails.py index 0703a4815..fc57effc1 100644 --- a/ietf/idrfc/mails.py +++ b/ietf/idrfc/mails.py @@ -11,7 +11,7 @@ from django.core.urlresolvers import reverse as urlreverse from ietf.utils.mail import send_mail, send_mail_text from ietf.idtracker.models import * from ietf.ipr.search import iprs_from_docs -from ietf.doc.models import WriteupDocEvent, BallotPositionDocEvent, LastCallDocEvent, DocAlias +from ietf.doc.models import * from ietf.person.models import Person from ietf.group.models import Group @@ -289,10 +289,14 @@ def generate_publication_request(request, doc): if doc.group and doc.group.acronym != "none": group_description = doc.group.name_with_acronym() + e = doc.latest_event(ConsensusDocEvent, type="changed_consensus") + consensus = e.consensus if e else None + return render_to_string("idrfc/publication_request.txt", dict(doc=doc, doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), group_description=group_description, + consensus=consensus, ) ) diff --git a/ietf/templates/idrfc/publication_request.txt b/ietf/templates/idrfc/publication_request.txt index e0de3222d..14bc62648 100644 --- a/ietf/templates/idrfc/publication_request.txt +++ b/ietf/templates/idrfc/publication_request.txt @@ -1,16 +1,20 @@ {% load mail_filters %}{% autoescape off %}{% filter wordwrap:73 %} -The document "{{ doc.title }}" <{{ doc.name }}> from the {{ doc.stream }} stream is ready for publication as {{ doc|std_level_prompt }}.{% if group_description %} +The {{ doc.stream }} has approved the following document: + +- "{{ doc.title }}" ({{ doc.name }}) as {{ doc|std_level_prompt }}.{% if group_description %} This document is the product of the {{ group_description }}.{% endif %}{% endfilter %} URL: {{ doc_url }} -No IANA allocation in the document requires IETF Consensus or Standards Action. +{% filter wordwrap:73 %}{% if consensus != None %}The document {% if consensus %}represents{% else %}does not necessarily represent{% endif%} the consensus of the {{ doc.stream }}. + +{% endif %}No IANA allocation in the document requires IETF Consensus or Standards Action.{% endfilter %} -[OPTIONAL: include summary of related discussion of this document in an IETF WG or in the IESG.] +[OPTIONAL: Include summary of related discussion of this document in an IETF WG or in the IESG.] -[OPTIONAL: include statement of the purpose of publishing this document, its intended audience, its merits and significance.] +[OPTIONAL: Include statement of the purpose of publishing this document, its intended audience, its merits and significance.] -[OPTIONAL: include suggested names and contact information for one or more competent and independent potential reviewers for the document (this can speed the review and approval process).] +[OPTIONAL: Include suggested names and contact information for one or more competent and independent potential reviewers for the document (this can speed the review and approval process).] {% endautoescape %} From 79212baffb4cb618f3c204ca8c61726874c6fe15 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Mon, 8 Oct 2012 09:35:37 +0000 Subject: [PATCH 29/47] Explicitly determine approving body and consensus body in publication requests for alternate streams so that they appear correctly for the IRTF - Legacy-Id: 4897 --- ietf/idrfc/mails.py | 11 ++++++++++- ietf/templates/idrfc/publication_request.txt | 4 ++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/ietf/idrfc/mails.py b/ietf/idrfc/mails.py index fc57effc1..cf4d1423c 100644 --- a/ietf/idrfc/mails.py +++ b/ietf/idrfc/mails.py @@ -291,11 +291,20 @@ def generate_publication_request(request, doc): e = doc.latest_event(ConsensusDocEvent, type="changed_consensus") consensus = e.consensus if e else None - + + if doc.stream_id == "irtf": + approving_body = "IRSG" + consensus_body = doc.group.acronym.upper() + else: + approving_body = str(doc.stream) + consensus_body = approving_body + return render_to_string("idrfc/publication_request.txt", dict(doc=doc, doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), group_description=group_description, + approving_body=approving_body, + consensus_body=consensus_body, consensus=consensus, ) ) diff --git a/ietf/templates/idrfc/publication_request.txt b/ietf/templates/idrfc/publication_request.txt index 14bc62648..f314d4190 100644 --- a/ietf/templates/idrfc/publication_request.txt +++ b/ietf/templates/idrfc/publication_request.txt @@ -1,5 +1,5 @@ {% load mail_filters %}{% autoescape off %}{% filter wordwrap:73 %} -The {{ doc.stream }} has approved the following document: +The {{ approving_body }} has approved the following document in the {{ doc.stream }} stream: - "{{ doc.title }}" ({{ doc.name }}) as {{ doc|std_level_prompt }}.{% if group_description %} @@ -7,7 +7,7 @@ This document is the product of the {{ group_description }}.{% endif %}{% endfil URL: {{ doc_url }} -{% filter wordwrap:73 %}{% if consensus != None %}The document {% if consensus %}represents{% else %}does not necessarily represent{% endif%} the consensus of the {{ doc.stream }}. +{% filter wordwrap:73 %}{% if consensus != None %}The document {% if consensus %}represents{% else %}does not necessarily represent{% endif%} the consensus of the {{ consensus_body }}. {% endif %}No IANA allocation in the document requires IETF Consensus or Standards Action.{% endfilter %} From 4aef9182d2d612348b61e7ea70c03e1b4c4ec8a1 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Wed, 10 Oct 2012 09:20:22 +0000 Subject: [PATCH 30/47] Small beautification: include "RFC Editor" in email address when emailing the RFC Editor - Legacy-Id: 4906 --- ietf/idrfc/mails.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/idrfc/mails.py b/ietf/idrfc/mails.py index cf4d1423c..66d6ac0bb 100644 --- a/ietf/idrfc/mails.py +++ b/ietf/idrfc/mails.py @@ -64,7 +64,7 @@ def email_stream_changed(request, doc, old_stream, new_stream, text=""): url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url())) def email_pulled_from_rfc_queue(request, doc, comment, prev_state, next_state): - send_mail(request, ['IANA ', 'rfc-editor@rfc-editor.org'], None, + send_mail(request, ["IANA ", "RFC Editor "], None, "%s changed state from %s to %s" % (doc.name, prev_state.name, next_state.name), "idrfc/pulled_from_rfc_queue_email.txt", dict(doc=doc, From 9c6fd95eabea49f1d615c3d1b101911262dde6e4 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Wed, 10 Oct 2012 09:22:26 +0000 Subject: [PATCH 31/47] Add date option to RFC Editor index mirroring script so one can control what the publication cut off date is, also fix crash problem with some old RFCs not having a page count - Legacy-Id: 4907 --- ietf/bin/rfc-editor-index-updates | 14 +++++++++++++- ietf/sync/rfceditor.py | 3 +-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/ietf/bin/rfc-editor-index-updates b/ietf/bin/rfc-editor-index-updates index 1d2c9ee6c..627ac91b0 100755 --- a/ietf/bin/rfc-editor-index-updates +++ b/ietf/bin/rfc-editor-index-updates @@ -14,6 +14,18 @@ from django.core import management management.setup_environ(settings) +from optparse import OptionParser + +parser = OptionParser() +parser.add_option("-d", dest="skip_date", + help="To speed up processing skip RFCs published before this date (default is one year ago)", metavar="YYYY-MM-DD") + +options, args = parser.parse_args() + +skip_date = datetime.date.today() - datetime.timedelta(days=365) +if options.skip_date: + skip_date = datetime.datetime.strptime(options.skip_date, "%Y-%m-%d").date() + from ietf.sync.rfceditor import * syslog.syslog("Updating document metadata from RFC index from %s" % QUEUE_URL) @@ -25,6 +37,6 @@ if len(data) < MIN_INDEX_RESULTS: syslog.syslog("Not enough results, only %s" % len(data)) sys.exit(1) -changed = update_docs_from_rfc_index(data) +changed = update_docs_from_rfc_index(data, skip_older_than_date=skip_date) for c in changed: syslog.syslog(c) diff --git a/ietf/sync/rfceditor.py b/ietf/sync/rfceditor.py index 0dbc08ff4..569baa423 100644 --- a/ietf/sync/rfceditor.py +++ b/ietf/sync/rfceditor.py @@ -273,7 +273,6 @@ def parse_index(response): return data - #skip_older_than_date = date.today() - timedelta(days=365) def update_docs_from_rfc_index(data, skip_older_than_date=None): std_level_mapping = { "Standard": StdLevelName.objects.get(slug="std"), @@ -347,7 +346,7 @@ def update_docs_from_rfc_index(data, skip_older_than_date=None): if abstract and abstract != doc.abstract: changed_attributes["abstract"] = abstract - if int(pages) != doc.pages: + if pages and int(pages) != doc.pages: changed_attributes["pages"] = int(pages) if std_level_mapping[current_status] != doc.std_level: From 49eb500692d44769d13ee3d3247b6223470dfcf4 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Wed, 10 Oct 2012 10:15:31 +0000 Subject: [PATCH 32/47] Only set published state for relevant state type - Legacy-Id: 4908 --- ietf/sync/rfceditor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ietf/sync/rfceditor.py b/ietf/sync/rfceditor.py index 569baa423..ed3415858 100644 --- a/ietf/sync/rfceditor.py +++ b/ietf/sync/rfceditor.py @@ -383,7 +383,8 @@ def update_docs_from_rfc_index(data, skip_older_than_date=None): results.append("Added RFC published event: %s" % e.time.strftime("%Y-%m-%d")) for t in ("draft-iesg", "draft-stream-iab", "draft-stream-irtf", "draft-stream-ise"): - if doc.get_state_slug(t) != "pub": + slug = doc.get_state_slug(t) + if slug and slug != "pub": changed_states.append(State.objects.get(type=t, slug="pub")) def parse_relation_list(l): From c20a5f6bf05d0b4067956fb442a928134b9f2e88 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Wed, 10 Oct 2012 16:15:09 +0000 Subject: [PATCH 33/47] Convert the submit tool to associate RGs and IAB with drafts instead of assigning them to individual submission, this is not a complete overhaul but at least basic support so submission works and doesn't say WG when it means RG. - Legacy-Id: 4910 --- ietf/submit/forms.py | 38 ++++++++++++------- ietf/submit/tests.py | 35 ++++++++++++++++- ietf/submit/utils.py | 2 +- ietf/submit/views.py | 2 +- ietf/templates/submit/announce_to_authors.txt | 2 +- ietf/templates/submit/draft_edit.html | 2 +- ietf/templates/submit/draft_status.html | 2 +- ietf/templates/submit/manual_post_mail.txt | 2 +- ietf/templates/submit/submission_approval.txt | 2 +- 9 files changed, 65 insertions(+), 22 deletions(-) diff --git a/ietf/submit/forms.py b/ietf/submit/forms.py index e12c0aef5..4fb53e166 100644 --- a/ietf/submit/forms.py +++ b/ietf/submit/forms.py @@ -12,6 +12,7 @@ from django.template.loader import render_to_string from django.utils.html import mark_safe from django.core.urlresolvers import reverse as urlreverse +from ietf.group.models import Group from ietf.idtracker.models import InternetDraft, IETFWG from ietf.proceedings.models import Meeting from ietf.submit.models import IdSubmissionDetail, TempIdAuthors, Preapproval @@ -224,27 +225,36 @@ class UploadForm(forms.Form): self.idnits_message = p.stdout.read() def get_working_group(self): - filename = self.draft.filename - existing_draft = InternetDraft.objects.filter(filename=filename) + name = self.draft.filename + existing_draft = InternetDraft.objects.filter(filename=name) if existing_draft: group = existing_draft[0].group and existing_draft[0].group.ietfwg or None - if group and group.pk != NONE_WG: - if settings.USE_DB_REDESIGN_PROXY_CLASSES and group.type_id == "area": - return None + if group and group.pk != NONE_WG and group.type_id != "area": return group else: return None else: - if filename.startswith('draft-ietf-'): - # Extra check for WG that contains dashes - for group in IETFWG.objects.filter(group_acronym__acronym__contains='-'): - if filename.startswith('draft-ietf-%s-' % group.group_acronym.acronym): - return group - group_acronym = filename.split('-')[2] + if name.startswith('draft-ietf-') or name.startswith("draft-irtf-"): + components = name.split("-") + if len(components) < 3: + raise forms.ValidationError("The draft name \"%s\" is missing a third part, please rename it") + + if components[1] == "ietf": + group_type = "wg" + else: + group_type = "rg" + + # first check groups with dashes + for g in Group.objects.filter(acronym__contains="-", type=group_type): + if name.startswith('draft-%s-%s-' % (components[1], g.acronym)): + return IETFWG().from_object(g) + try: - return IETFWG.objects.get(group_acronym__acronym=group_acronym) - except IETFWG.DoesNotExist: - raise forms.ValidationError('There is no active group with acronym \'%s\', please rename your draft' % group_acronym) + return IETFWG().from_object(Group.objects.get(acronym=components[2], type=group_type)) + except Group.DoesNotExist: + raise forms.ValidationError('There is no active group with acronym \'%s\', please rename your draft' % components[2]) + elif name.startswith("draft-iab-"): + return IETFWG().from_object(Group.objects.get(acronym="iab")) else: return None diff --git a/ietf/submit/tests.py b/ietf/submit/tests.py index 09b1116b5..071146f68 100644 --- a/ietf/submit/tests.py +++ b/ietf/submit/tests.py @@ -75,7 +75,6 @@ class SubmitTestCase(django.test.TestCase): self.assertTrue(os.path.exists(os.path.join(self.staging_dir, u"%s-%s.txt" % (name, rev)))) self.assertEquals(IdSubmissionDetail.objects.filter(filename=name).count(), 1) submission = IdSubmissionDetail.objects.get(filename=name) - self.assertEquals(submission.group_acronym.acronym, "mars") self.assertEquals(submission.tempidauthors_set.count(), 1) self.assertTrue(re.search('\s+Summary:\s+0\s+errors|No nits found', submission.idnits_message)) author = submission.tempidauthors_set.all()[0] @@ -139,6 +138,7 @@ class SubmitTestCase(django.test.TestCase): draft = Document.objects.get(docalias__name=name) self.assertEquals(draft.rev, rev) new_revision = draft.latest_event() + self.assertEquals(draft.group.acronym, "mars") self.assertEquals(new_revision.type, "new_revision") self.assertEquals(new_revision.by.name, "Test Name") self.assertTrue(not os.path.exists(os.path.join(self.staging_dir, u"%s-%s.txt" % (name, rev)))) @@ -224,6 +224,7 @@ class SubmitTestCase(django.test.TestCase): draft = Document.objects.get(docalias__name=name) self.assertEquals(draft.rev, rev) + self.assertEquals(draft.group.acronym, name.split("-")[2]) self.assertEquals(draft.docevent_set.all()[1].type, "new_revision") self.assertEquals(draft.docevent_set.all()[1].by.name, "Test Name") self.assertTrue(not os.path.exists(os.path.join(self.repository_dir, "%s-%s.txt" % (name, old_rev)))) @@ -250,6 +251,38 @@ class SubmitTestCase(django.test.TestCase): self.assertTrue(name in unicode(outbox[-1])) self.assertTrue("mars" in unicode(outbox[-1])) + def test_submit_new_wg_with_dash(self): + draft = make_test_data() + + group = Group.objects.create(acronym="mars-special", name="Mars Special", type_id="wg", state_id="active") + + name = "draft-ietf-%s-testing-tests" % group.acronym + + self.do_submission(name, "00") + + self.assertEquals(IdSubmissionDetail.objects.get(filename=name).group_acronym.acronym, group.acronym) + + def test_submit_new_irtf(self): + draft = make_test_data() + + group = Group.objects.create(acronym="saturnrg", name="Saturn", type_id="rg", state_id="active") + + name = "draft-irtf-%s-testing-tests" % group.acronym + + self.do_submission(name, "00") + + self.assertEquals(IdSubmissionDetail.objects.get(filename=name).group_acronym.acronym, group.acronym) + self.assertEquals(IdSubmissionDetail.objects.get(filename=name).group_acronym.type_id, group.type_id) + + def test_submit_new_iab(self): + draft = make_test_data() + + name = "draft-iab-testing-tests" + + self.do_submission(name, "00") + + self.assertEquals(IdSubmissionDetail.objects.get(filename=name).group_acronym.acronym, "iab") + def test_cancel_submission(self): # submit -> cancel draft = make_test_data() diff --git a/ietf/submit/utils.py b/ietf/submit/utils.py index 09eb0a34a..a247ffba8 100644 --- a/ietf/submit/utils.py +++ b/ietf/submit/utils.py @@ -579,7 +579,7 @@ class DraftValidation(object): def validate_wg(self): if self.wg and not self.wg.status_id == IETFWG.ACTIVE: - self.add_warning('group', 'Working Group exists but is not an active WG') + self.add_warning('group', 'Group exists but is not an active group') def validate_abstract(self): if not self.draft.abstract: diff --git a/ietf/submit/views.py b/ietf/submit/views.py index c5659e2c9..8b3e8410b 100644 --- a/ietf/submit/views.py +++ b/ietf/submit/views.py @@ -114,7 +114,7 @@ def draft_status(request, submission_id, submission_hash=None, message=None): except Preapproval.DoesNotExist: preapproval = None - if detail.revision == '00' and detail.group_acronym and not preapproval: + if detail.revision == '00' and detail.group_acronym and detail.group_acronym.type_id == "wg" and not preapproval: detail.status_id = INITIAL_VERSION_APPROVAL_REQUESTED detail.save() diff --git a/ietf/templates/submit/announce_to_authors.txt b/ietf/templates/submit/announce_to_authors.txt index e405724dd..c3e875f14 100644 --- a/ietf/templates/submit/announce_to_authors.txt +++ b/ietf/templates/submit/announce_to_authors.txt @@ -7,7 +7,7 @@ Filename: {{ submission.filename }} Revision: {{ submission.revision }} Title: {{ submission.id_document_name }} Creation date: {{ submission.creation_date|date:"Y-m-d" }} -WG ID: {{ wg }} +Group: {{ wg }} Number of pages: {{ submission.txt_page_count }} URL: http://www.ietf.org/internet-drafts/{{ submission.filename }}-{{ submission.revision }}.txt Status: http://datatracker.ietf.org/doc/{{ submission.filename }} diff --git a/ietf/templates/submit/draft_edit.html b/ietf/templates/submit/draft_edit.html index 3426ec3f2..a1d865374 100644 --- a/ietf/templates/submit/draft_edit.html +++ b/ietf/templates/submit/draft_edit.html @@ -93,7 +93,7 @@ table.ietf-table span.field-error { display: block; color: red; } {% show_submission_files detail %} Submission date{{ detail.submission_date }} -WG{{ validation.wg|default:"Individual Submission" }} +Group{{ validation.wg|default:"Individual Submission" }} {% if validation.warnings.group %}
    The secretariat will be notified that the working group is not active
    {% endif %} diff --git a/ietf/templates/submit/draft_status.html b/ietf/templates/submit/draft_status.html index 30bd84ee3..09c7c9aa0 100644 --- a/ietf/templates/submit/draft_status.html +++ b/ietf/templates/submit/draft_status.html @@ -149,7 +149,7 @@ returned to the submitter. Revision{{ detail.revision }}
    {{ validation.warnings.revision }}{% if validation.warnings.revision %}
    [View error]{% endif %}
    -WG{{ validation.wg|default:"Individual Submission" }}
    {{ validation.warnings.group }}
    +Group{{ validation.wg|default:"Individual Submission" }}
    {{ validation.warnings.group }}
    Document date{{ detail.creation_date }}
    {{ validation.warnings.creation_date }}
    Submission date{{ detail.submission_date }} Title{{ detail.id_document_name|default:"" }}
    {{ validation.warnings.title }}
    diff --git a/ietf/templates/submit/manual_post_mail.txt b/ietf/templates/submit/manual_post_mail.txt index 5c06f9cc0..7fd5ffd9c 100644 --- a/ietf/templates/submit/manual_post_mail.txt +++ b/ietf/templates/submit/manual_post_mail.txt @@ -9,7 +9,7 @@ I-D Submission Tool URL: File name : {{ draft.filename }} Version : {{ draft.revision }} Submission date : {{ draft.submission_date }} - WG : {{ draft.group_acronym|default:"Individual Submission" }} {% if form.validation.warnings.group %}*Please note that this WG is not an active one*{% endif %} + Group : {{ draft.group_acronym|default:"Individual Submission" }} {% if form.validation.warnings.group %}*Please note that this group is not an active one*{% endif %} Title : {{ draft.id_document_name }} Document date : {{ draft.creation_date }} diff --git a/ietf/templates/submit/submission_approval.txt b/ietf/templates/submit/submission_approval.txt index 51ea3e6b7..ddfa7fd94 100644 --- a/ietf/templates/submit/submission_approval.txt +++ b/ietf/templates/submit/submission_approval.txt @@ -9,7 +9,7 @@ To approve the draft, go to this URL (note: you need to login to be able to appr File name : {{ draft.filename }} Version : {{ draft.revision }} Submission date : {{ draft.submission_date }} - WG : {{ draft.group_acronym|default:"Individual Submission" }} + Group : {{ draft.group_acronym|default:"Individual Submission" }} Title : {{ draft.id_document_name }} Document date : {{ draft.creation_date }} From e87b8483e544e276f0ddf2a14cd9818678c02caa Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Thu, 11 Oct 2012 15:26:21 +0000 Subject: [PATCH 34/47] Add migration script for setting the group on IAB stream documents to IAB and IRTF stream documents to the corresponding RG (if it exists), instead of keeping it as individual submission - Legacy-Id: 4911 --- ...006_fix_groups_of_alternate_stream_docs.py | 374 ++++++++++++++++++ 1 file changed, 374 insertions(+) create mode 100644 ietf/doc/migrations/0006_fix_groups_of_alternate_stream_docs.py diff --git a/ietf/doc/migrations/0006_fix_groups_of_alternate_stream_docs.py b/ietf/doc/migrations/0006_fix_groups_of_alternate_stream_docs.py new file mode 100644 index 000000000..941098280 --- /dev/null +++ b/ietf/doc/migrations/0006_fix_groups_of_alternate_stream_docs.py @@ -0,0 +1,374 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + + def forwards(self, orm): + # IAB + iab = orm["group.Group"].objects.get(acronym="iab") + orm.Document.objects.filter(stream="iab").update(group=iab) + + # IRTF + rgs = {} + for d in orm.Document.objects.filter(stream="irtf"): + acronym = d.name.split("-")[2] + if acronym not in rgs: + try: + rgs[acronym] = orm["group.Group"].objects.get(acronym=acronym, type="rg") + except orm["group.Group"].DoesNotExist: + rgs[acronym] = None + + rg = rgs[acronym] + if rg: + d.group = rg + d.save() + + def backwards(self, orm): + "Write your backwards methods here." + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'doc.ballotdocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'BallotDocEvent', '_ormbases': ['doc.DocEvent']}, + 'ballot_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.BallotType']"}), + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'doc.ballotpositiondocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'BallotPositionDocEvent', '_ormbases': ['doc.DocEvent']}, + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}), + 'ballot': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['doc.BallotDocEvent']", 'null': 'True'}), + 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'comment_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'discuss': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'discuss_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'pos': ('django.db.models.fields.related.ForeignKey', [], {'default': "'norecord'", 'to': "orm['name.BallotPositionName']"}) + }, + 'doc.ballottype': { + 'Meta': {'ordering': "['order']", 'object_name': 'BallotType'}, + 'doc_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.DocTypeName']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'positions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['name.BallotPositionName']", 'symmetrical': 'False', 'blank': 'True'}), + 'question': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'doc.consensusdocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'ConsensusDocEvent', '_ormbases': ['doc.DocEvent']}, + 'consensus': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'doc.docalias': { + 'Meta': {'object_name': 'DocAlias'}, + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.Document']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}) + }, + 'doc.docevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'DocEvent'}, + 'by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}), + 'desc': ('django.db.models.fields.TextField', [], {}), + 'doc': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.Document']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'doc.dochistory': { + 'Meta': {'object_name': 'DocHistory'}, + 'abstract': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'ad_dochistory_set'", 'null': 'True', 'to': "orm['person.Person']"}), + 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['person.Email']", 'symmetrical': 'False', 'through': "orm['doc.DocHistoryAuthor']", 'blank': 'True'}), + 'doc': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'history_set'", 'to': "orm['doc.Document']"}), + 'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'external_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'intended_std_level': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.IntendedStdLevelName']", 'null': 'True', 'blank': 'True'}), + 'internal_comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'note': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'notify': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '1', 'blank': 'True'}), + 'pages': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'related': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['doc.DocAlias']", 'symmetrical': 'False', 'through': "orm['doc.RelatedDocHistory']", 'blank': 'True'}), + 'rev': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'shepherd': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'shepherd_dochistory_set'", 'null': 'True', 'to': "orm['person.Person']"}), + 'states': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['doc.State']", 'symmetrical': 'False', 'blank': 'True'}), + 'std_level': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.StdLevelName']", 'null': 'True', 'blank': 'True'}), + 'stream': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.StreamName']", 'null': 'True', 'blank': 'True'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['name.DocTagName']", 'null': 'True', 'blank': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.DocTypeName']", 'null': 'True', 'blank': 'True'}) + }, + 'doc.dochistoryauthor': { + 'Meta': {'ordering': "['document', 'order']", 'object_name': 'DocHistoryAuthor'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Email']"}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.DocHistory']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.IntegerField', [], {}) + }, + 'doc.docreminder': { + 'Meta': {'object_name': 'DocReminder'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'due': ('django.db.models.fields.DateTimeField', [], {}), + 'event': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.DocEvent']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.DocReminderTypeName']"}) + }, + 'doc.document': { + 'Meta': {'object_name': 'Document'}, + 'abstract': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'ad_document_set'", 'null': 'True', 'to': "orm['person.Person']"}), + 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['person.Email']", 'symmetrical': 'False', 'through': "orm['doc.DocumentAuthor']", 'blank': 'True'}), + 'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'external_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']", 'null': 'True', 'blank': 'True'}), + 'intended_std_level': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.IntendedStdLevelName']", 'null': 'True', 'blank': 'True'}), + 'internal_comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}), + 'note': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'notify': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '1', 'blank': 'True'}), + 'pages': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'related': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'reversely_related_document_set'", 'blank': 'True', 'through': "orm['doc.RelatedDocument']", 'to': "orm['doc.DocAlias']"}), + 'rev': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'shepherd': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'shepherd_document_set'", 'null': 'True', 'to': "orm['person.Person']"}), + 'states': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['doc.State']", 'symmetrical': 'False', 'blank': 'True'}), + 'std_level': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.StdLevelName']", 'null': 'True', 'blank': 'True'}), + 'stream': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.StreamName']", 'null': 'True', 'blank': 'True'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['name.DocTagName']", 'null': 'True', 'blank': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.DocTypeName']", 'null': 'True', 'blank': 'True'}) + }, + 'doc.documentauthor': { + 'Meta': {'ordering': "['document', 'order']", 'object_name': 'DocumentAuthor'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Email']"}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.Document']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '1'}) + }, + 'doc.initialreviewdocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'InitialReviewDocEvent', '_ormbases': ['doc.DocEvent']}, + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'doc.lastcalldocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'LastCallDocEvent', '_ormbases': ['doc.DocEvent']}, + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'doc.newrevisiondocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'NewRevisionDocEvent', '_ormbases': ['doc.DocEvent']}, + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'rev': ('django.db.models.fields.CharField', [], {'max_length': '16'}) + }, + 'doc.relateddochistory': { + 'Meta': {'object_name': 'RelatedDocHistory'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'relationship': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.DocRelationshipName']"}), + 'source': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.DocHistory']"}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reversely_related_document_history_set'", 'to': "orm['doc.DocAlias']"}) + }, + 'doc.relateddocument': { + 'Meta': {'object_name': 'RelatedDocument'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'relationship': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.DocRelationshipName']"}), + 'source': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.Document']"}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.DocAlias']"}) + }, + 'doc.state': { + 'Meta': {'ordering': "['type', 'order']", 'object_name': 'State'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'next_states': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'previous_states'", 'blank': 'True', 'to': "orm['doc.State']"}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.StateType']"}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'doc.statedocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'StateDocEvent', '_ormbases': ['doc.DocEvent']}, + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.State']", 'null': 'True', 'blank': 'True'}), + 'state_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.StateType']"}) + }, + 'doc.statetype': { + 'Meta': {'object_name': 'StateType'}, + 'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '30', 'primary_key': 'True'}) + }, + 'doc.telechatdocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'TelechatDocEvent', '_ormbases': ['doc.DocEvent']}, + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'returning_item': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'telechat_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}) + }, + 'doc.writeupdocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'WriteupDocEvent', '_ormbases': ['doc.DocEvent']}, + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {'blank': 'True'}) + }, + 'group.group': { + 'Meta': {'object_name': 'Group'}, + 'acronym': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}), + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']", 'null': 'True', 'blank': 'True'}), + 'charter': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'chartered_group'", 'unique': 'True', 'null': 'True', 'to': "orm['doc.Document']"}), + 'comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'list_archive': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'list_email': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'list_subscribe': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']", 'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.GroupStateName']", 'null': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.GroupTypeName']", 'null': 'True'}), + 'unused_states': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['doc.State']", 'symmetrical': 'False', 'blank': 'True'}), + 'unused_tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['name.DocTagName']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'name.ballotpositionname': { + 'Meta': {'ordering': "['order']", 'object_name': 'BallotPositionName'}, + 'blocking': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.docrelationshipname': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocRelationshipName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + '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'] From a77b2ae0abb9fdd6f68fc592acdd355bc403d96c Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Mon, 5 Nov 2012 13:32:31 +0000 Subject: [PATCH 35/47] Add missing admin entries for StateDocEvent and ConsensusDocEvent - Legacy-Id: 5006 --- ietf/doc/admin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ietf/doc/admin.py b/ietf/doc/admin.py index 2bb111400..63a457ac1 100644 --- a/ietf/doc/admin.py +++ b/ietf/doc/admin.py @@ -142,6 +142,8 @@ class DocEventAdmin(admin.ModelAdmin): admin.site.register(DocEvent, DocEventAdmin) admin.site.register(NewRevisionDocEvent, DocEventAdmin) +admin.site.register(StateDocEvent, DocEventAdmin) +admin.site.register(ConsensusDocEvent, DocEventAdmin) admin.site.register(BallotDocEvent, DocEventAdmin) admin.site.register(WriteupDocEvent, DocEventAdmin) admin.site.register(LastCallDocEvent, DocEventAdmin) From 63fb1cbf0c321cb0765e42fa6f32651eee98029c Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Tue, 6 Nov 2012 14:52:21 +0000 Subject: [PATCH 36/47] Remove dummy attribute from sync notify form - Legacy-Id: 5007 --- ietf/templates/sync/notify.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/templates/sync/notify.html b/ietf/templates/sync/notify.html index 4092bb535..2ebbf5fc1 100644 --- a/ietf/templates/sync/notify.html +++ b/ietf/templates/sync/notify.html @@ -7,7 +7,7 @@

    Update the Datatracker with {{ help_text|safe }} at {{ org }}.

    -
    +
    {% endblock %} From 793856c217c46e863843ddc62a6053b94a275bb5 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Tue, 6 Nov 2012 17:06:10 +0000 Subject: [PATCH 37/47] Add RFC Editor undo page, missing test and dump model - Legacy-Id: 5013 --- ietf/sync/urls.py | 1 + ietf/sync/views.py | 32 ++++++++++++++++++++++++- ietf/templates/sync/rfceditor_undo.html | 29 ++++++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 ietf/templates/sync/rfceditor_undo.html diff --git a/ietf/sync/urls.py b/ietf/sync/urls.py index d04919822..8b4b962cf 100644 --- a/ietf/sync/urls.py +++ b/ietf/sync/urls.py @@ -3,5 +3,6 @@ from django.conf.urls.defaults import patterns, url urlpatterns = patterns('', url(r'^discrepancies/$', 'ietf.sync.views.discrepancies'), url(r'^(?P\w+)/notify/(?P\w+)/$', 'ietf.sync.views.notify'), + url(r'^rfceditor/undo/', 'ietf.sync.views.rfceditor_undo') ) diff --git a/ietf/sync/views.py b/ietf/sync/views.py index 12d5ae1a0..f9183892c 100644 --- a/ietf/sync/views.py +++ b/ietf/sync/views.py @@ -1,6 +1,6 @@ import subprocess, os -from django.http import HttpResponse, HttpResponseForbidden, HttpResponseServerError +from django.http import HttpResponse, HttpResponseForbidden, HttpResponseServerError, HttpResponseRedirect from django.shortcuts import render_to_response from django.template import RequestContext from django.template.loader import render_to_string @@ -100,3 +100,33 @@ def notify(request, org, notification): help_text=known_notifications[notification], ), context_instance=RequestContext(request)) + +def rfceditor_undo(request): + """Undo a DocEvent.""" + + if not request.user.is_authenticated() or not has_role(request.user, ("Secretariat", "RFC Editor")): + return HttpResponseForbidden("You do not have the necessary permissions to view this page") + + events = StateDocEvent.objects.filter(state_type="draft-rfceditor", + time__gte=datetime.datetime.now() - datetime.timedelta(weeks=1) + ).order_by("-time", "-id") + + if request.method == "POST": + try: + eid = int(request.POST.get("event", "")) + except ValueError: + return HttpResponse("Could not parse event id") + + try: + e = events.get(id=eid) + except StateDocEvent.DoesNotExist: + return HttpResponse("Event does not exist") + + e.delete() + + return HttpResponseRedirect(urlreverse("ietf.sync.views.rfceditor_undo")) + + return render_to_response('sync/rfceditor_undo.html', + dict(events=events, + ), + context_instance=RequestContext(request)) diff --git a/ietf/templates/sync/rfceditor_undo.html b/ietf/templates/sync/rfceditor_undo.html new file mode 100644 index 000000000..e34e94f48 --- /dev/null +++ b/ietf/templates/sync/rfceditor_undo.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} + +{% block title %}Undo RFC Editor state events{% endblock %} + +{% block content %} +

    Undo RFC Editor state events

    + + + + + + + + + {% for e in events %} + + + + + + + {% endfor %} +
    TimeDocumentTextUndo
    {{ e.time|date:"Y-m-d H:i:s"}}{{ e.doc_id }}{{ e.desc|safe }} +
    + + +
    +
    +{% endblock %} From 730cd6056e86a8fb6cad5ab67af8e284775a49f3 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Wed, 7 Nov 2012 14:55:39 +0000 Subject: [PATCH 38/47] Add DeletedEvent for storing redacted events - Legacy-Id: 5021 --- ietf/doc/models.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ietf/doc/models.py b/ietf/doc/models.py index 6af0dcbff..2b14d09b7 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -2,6 +2,7 @@ from django.db import models from django.core.urlresolvers import reverse as urlreverse +from django.contrib.contenttypes.models import ContentType from django.conf import settings from ietf.group.models import * @@ -566,3 +567,14 @@ class TelechatDocEvent(DocEvent): # charter events class InitialReviewDocEvent(DocEvent): expires = models.DateTimeField(blank=True, null=True) + + +# dumping store for removed events +class DeletedEvent(models.Model): + content_type = models.ForeignKey(ContentType) + json = models.TextField(help_text="Deleted object in JSON format, with attribute names chosen to be suitable for passing into the relevant create method.") + by = models.ForeignKey(Person) + time = models.DateTimeField(default=datetime.datetime.now) + + def __unicode__(self): + return u"%s by %s %s" % (self.content_type, self.by, self.time) From 9b73aa30b9d1a838fd1e8f2660bf26cd18c763bd Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Wed, 7 Nov 2012 14:55:58 +0000 Subject: [PATCH 39/47] Add migration for DeletedEvent - Legacy-Id: 5022 --- .../migrations/0007_auto__add_deletedevent.py | 377 ++++++++++++++++++ 1 file changed, 377 insertions(+) create mode 100644 ietf/doc/migrations/0007_auto__add_deletedevent.py diff --git a/ietf/doc/migrations/0007_auto__add_deletedevent.py b/ietf/doc/migrations/0007_auto__add_deletedevent.py new file mode 100644 index 000000000..1ff1a99e4 --- /dev/null +++ b/ietf/doc/migrations/0007_auto__add_deletedevent.py @@ -0,0 +1,377 @@ +# 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 model 'DeletedEvent' + db.create_table('doc_deletedevent', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])), + ('json', self.gf('django.db.models.fields.TextField')()), + ('by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['person.Person'])), + ('time', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)), + )) + db.send_create_signal('doc', ['DeletedEvent']) + + + def backwards(self, orm): + + # Deleting model 'DeletedEvent' + db.delete_table('doc_deletedevent') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'doc.ballotdocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'BallotDocEvent', '_ormbases': ['doc.DocEvent']}, + 'ballot_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.BallotType']"}), + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'doc.ballotpositiondocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'BallotPositionDocEvent', '_ormbases': ['doc.DocEvent']}, + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}), + 'ballot': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['doc.BallotDocEvent']", 'null': 'True'}), + 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'comment_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'discuss': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'discuss_time': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'pos': ('django.db.models.fields.related.ForeignKey', [], {'default': "'norecord'", 'to': "orm['name.BallotPositionName']"}) + }, + 'doc.ballottype': { + 'Meta': {'ordering': "['order']", 'object_name': 'BallotType'}, + 'doc_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.DocTypeName']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'positions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['name.BallotPositionName']", 'symmetrical': 'False', 'blank': 'True'}), + 'question': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'doc.consensusdocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'ConsensusDocEvent', '_ormbases': ['doc.DocEvent']}, + 'consensus': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'doc.deletedevent': { + 'Meta': {'object_name': 'DeletedEvent'}, + 'by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'json': ('django.db.models.fields.TextField', [], {}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}) + }, + 'doc.docalias': { + 'Meta': {'object_name': 'DocAlias'}, + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.Document']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}) + }, + 'doc.docevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'DocEvent'}, + 'by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}), + 'desc': ('django.db.models.fields.TextField', [], {}), + 'doc': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.Document']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'type': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'doc.dochistory': { + 'Meta': {'object_name': 'DocHistory'}, + 'abstract': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'ad_dochistory_set'", 'null': 'True', 'to': "orm['person.Person']"}), + 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['person.Email']", 'symmetrical': 'False', 'through': "orm['doc.DocHistoryAuthor']", 'blank': 'True'}), + 'doc': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'history_set'", 'to': "orm['doc.Document']"}), + 'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'external_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']", 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'intended_std_level': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.IntendedStdLevelName']", 'null': 'True', 'blank': 'True'}), + 'internal_comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'note': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'notify': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '1', 'blank': 'True'}), + 'pages': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'related': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['doc.DocAlias']", 'symmetrical': 'False', 'through': "orm['doc.RelatedDocHistory']", 'blank': 'True'}), + 'rev': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'shepherd': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'shepherd_dochistory_set'", 'null': 'True', 'to': "orm['person.Person']"}), + 'states': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['doc.State']", 'symmetrical': 'False', 'blank': 'True'}), + 'std_level': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.StdLevelName']", 'null': 'True', 'blank': 'True'}), + 'stream': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.StreamName']", 'null': 'True', 'blank': 'True'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['name.DocTagName']", 'null': 'True', 'blank': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.DocTypeName']", 'null': 'True', 'blank': 'True'}) + }, + 'doc.dochistoryauthor': { + 'Meta': {'ordering': "['document', 'order']", 'object_name': 'DocHistoryAuthor'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Email']"}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.DocHistory']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.IntegerField', [], {}) + }, + 'doc.docreminder': { + 'Meta': {'object_name': 'DocReminder'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'due': ('django.db.models.fields.DateTimeField', [], {}), + 'event': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.DocEvent']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.DocReminderTypeName']"}) + }, + 'doc.document': { + 'Meta': {'object_name': 'Document'}, + 'abstract': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'ad_document_set'", 'null': 'True', 'to': "orm['person.Person']"}), + 'authors': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['person.Email']", 'symmetrical': 'False', 'through': "orm['doc.DocumentAuthor']", 'blank': 'True'}), + 'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'external_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']", 'null': 'True', 'blank': 'True'}), + 'intended_std_level': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.IntendedStdLevelName']", 'null': 'True', 'blank': 'True'}), + 'internal_comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'primary_key': 'True'}), + 'note': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'notify': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '1', 'blank': 'True'}), + 'pages': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'related': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'reversely_related_document_set'", 'blank': 'True', 'through': "orm['doc.RelatedDocument']", 'to': "orm['doc.DocAlias']"}), + 'rev': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'shepherd': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'shepherd_document_set'", 'null': 'True', 'to': "orm['person.Person']"}), + 'states': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['doc.State']", 'symmetrical': 'False', 'blank': 'True'}), + 'std_level': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.StdLevelName']", 'null': 'True', 'blank': 'True'}), + 'stream': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.StreamName']", 'null': 'True', 'blank': 'True'}), + 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['name.DocTagName']", 'null': 'True', 'blank': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.DocTypeName']", 'null': 'True', 'blank': 'True'}) + }, + 'doc.documentauthor': { + 'Meta': {'ordering': "['document', 'order']", 'object_name': 'DocumentAuthor'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Email']"}), + 'document': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.Document']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '1'}) + }, + 'doc.initialreviewdocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'InitialReviewDocEvent', '_ormbases': ['doc.DocEvent']}, + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'doc.lastcalldocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'LastCallDocEvent', '_ormbases': ['doc.DocEvent']}, + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}) + }, + 'doc.newrevisiondocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'NewRevisionDocEvent', '_ormbases': ['doc.DocEvent']}, + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'rev': ('django.db.models.fields.CharField', [], {'max_length': '16'}) + }, + 'doc.relateddochistory': { + 'Meta': {'object_name': 'RelatedDocHistory'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'relationship': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.DocRelationshipName']"}), + 'source': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.DocHistory']"}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reversely_related_document_history_set'", 'to': "orm['doc.DocAlias']"}) + }, + 'doc.relateddocument': { + 'Meta': {'object_name': 'RelatedDocument'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'relationship': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.DocRelationshipName']"}), + 'source': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.Document']"}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.DocAlias']"}) + }, + 'doc.state': { + 'Meta': {'ordering': "['type', 'order']", 'object_name': 'State'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'next_states': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'previous_states'", 'blank': 'True', 'to': "orm['doc.State']"}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.StateType']"}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'doc.statedocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'StateDocEvent', '_ormbases': ['doc.DocEvent']}, + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.State']", 'null': 'True', 'blank': 'True'}), + 'state_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.StateType']"}) + }, + 'doc.statetype': { + 'Meta': {'object_name': 'StateType'}, + 'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '30', 'primary_key': 'True'}) + }, + 'doc.telechatdocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'TelechatDocEvent', '_ormbases': ['doc.DocEvent']}, + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'returning_item': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'telechat_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}) + }, + 'doc.writeupdocevent': { + 'Meta': {'ordering': "['-time', '-id']", 'object_name': 'WriteupDocEvent', '_ormbases': ['doc.DocEvent']}, + 'docevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['doc.DocEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'text': ('django.db.models.fields.TextField', [], {'blank': 'True'}) + }, + 'group.group': { + 'Meta': {'object_name': 'Group'}, + 'acronym': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '40', 'db_index': 'True'}), + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']", 'null': 'True', 'blank': 'True'}), + 'charter': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'chartered_group'", 'unique': 'True', 'null': 'True', 'to': "orm['doc.Document']"}), + 'comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'list_archive': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'list_email': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'list_subscribe': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']", 'null': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.GroupStateName']", 'null': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.GroupTypeName']", 'null': 'True'}), + 'unused_states': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['doc.State']", 'symmetrical': 'False', 'blank': 'True'}), + 'unused_tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['name.DocTagName']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'name.ballotpositionname': { + 'Meta': {'ordering': "['order']", 'object_name': 'BallotPositionName'}, + 'blocking': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.docrelationshipname': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocRelationshipName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + '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'] From 6c91e93886524daa46930a3390c8288a0c06c6ab Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Wed, 7 Nov 2012 14:57:04 +0000 Subject: [PATCH 40/47] Allow State.next_states to be blank so the admin doesn't barf - Legacy-Id: 5023 --- ietf/doc/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/doc/models.py b/ietf/doc/models.py index 2b14d09b7..f5630d318 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -27,7 +27,7 @@ class State(models.Model): desc = models.TextField(blank=True) order = models.IntegerField(default=0) - next_states = models.ManyToManyField('State', related_name="previous_states") + next_states = models.ManyToManyField('State', related_name="previous_states", blank=True) def __unicode__(self): return self.name From daa5d49adb63e58c907b86e966a6cadf3e8f554d Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Wed, 7 Nov 2012 14:58:49 +0000 Subject: [PATCH 41/47] Fix RFC Editor group acronym to be rfceditor in test data - Legacy-Id: 5024 --- ietf/utils/test_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/utils/test_data.py b/ietf/utils/test_data.py index 1c8fd9bfe..3a8e61bdf 100644 --- a/ietf/utils/test_data.py +++ b/ietf/utils/test_data.py @@ -125,7 +125,7 @@ def make_test_data(): ) rfc_editor = Group.objects.create( name="RFC Editor", - acronym="rfc-edit", + acronym="rfceditor", state_id="active", type_id="ietf", parent=None, From 45cf853524e94c211f8ad9674fbcb113c5f5938d Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Wed, 7 Nov 2012 14:59:17 +0000 Subject: [PATCH 42/47] Dump redacted events in DeletedEvent table - Legacy-Id: 5025 --- ietf/sync/views.py | 14 +++++++++----- ietf/utils/serialize.py | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 ietf/utils/serialize.py diff --git a/ietf/sync/views.py b/ietf/sync/views.py index f9183892c..e9d734676 100644 --- a/ietf/sync/views.py +++ b/ietf/sync/views.py @@ -1,4 +1,4 @@ -import subprocess, os +import subprocess, os, json from django.http import HttpResponse, HttpResponseForbidden, HttpResponseServerError, HttpResponseRedirect from django.shortcuts import render_to_response @@ -13,6 +13,7 @@ from ietf.ietfauth.decorators import role_required, has_role from ietf.doc.models import * from ietf.sync import iana, rfceditor from ietf.sync.discrepancies import find_discrepancies +from ietf.utils.serialize import object_as_shallow_dict SYNC_BIN_PATH = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "../bin")) @@ -101,12 +102,9 @@ def notify(request, org, notification): ), context_instance=RequestContext(request)) +@role_required('Secretariat', 'RFC Editor') def rfceditor_undo(request): """Undo a DocEvent.""" - - if not request.user.is_authenticated() or not has_role(request.user, ("Secretariat", "RFC Editor")): - return HttpResponseForbidden("You do not have the necessary permissions to view this page") - events = StateDocEvent.objects.filter(state_type="draft-rfceditor", time__gte=datetime.datetime.now() - datetime.timedelta(weeks=1) ).order_by("-time", "-id") @@ -122,6 +120,12 @@ def rfceditor_undo(request): except StateDocEvent.DoesNotExist: return HttpResponse("Event does not exist") + dump = DeletedEvent() + dump.content_type = ContentType.objects.get_for_model(type(e)) + dump.json = json.dumps(object_as_shallow_dict(e), indent=2) + dump.by = request.user.person + dump.save() + e.delete() return HttpResponseRedirect(urlreverse("ietf.sync.views.rfceditor_undo")) diff --git a/ietf/utils/serialize.py b/ietf/utils/serialize.py new file mode 100644 index 000000000..64254300a --- /dev/null +++ b/ietf/utils/serialize.py @@ -0,0 +1,23 @@ +from django.db import models + +def object_as_shallow_dict(obj): + """Turn a Django model object into a dict suitable for passing to + create and for serializing to JSON.""" + + d = {} + for f in obj._meta.fields: + n = f.name + if isinstance(f, models.ForeignKey): + n = f.name + "_id" + + v = getattr(obj, n) + if isinstance(f, models.ManyToManyField): + v = list(v.values_list("pk", flat=True)) + elif isinstance(f, models.DateTimeField): + v = v.strftime('%Y-%m-%d %H:%M:%S') + elif isinstance(f, models.DateField): + v = v.strftime('%Y-%m-%d') + + d[n] = v + + return d From e29bd99283533a0f045dd5741b8bfdfd758b69ae Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Wed, 7 Nov 2012 15:19:45 +0000 Subject: [PATCH 43/47] Add test of RFC Editor redaction feature - Legacy-Id: 5027 --- ietf/sync/tests.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/ietf/sync/tests.py b/ietf/sync/tests.py index 8aa97da85..bb6403124 100644 --- a/ietf/sync/tests.py +++ b/ietf/sync/tests.py @@ -8,6 +8,7 @@ from ietf.utils.test_data import make_test_data from ietf.utils.test_utils import login_testing_unauthorized from ietf.doc.models import * +from ietf.doc.utils import add_state_change_event from ietf.person.models import * from ietf.sync import iana, rfceditor @@ -399,3 +400,37 @@ class DiscrepanciesTestCase(django.test.TestCase): r = self.client.get(urlreverse("ietf.sync.views.discrepancies")) self.assertTrue(doc.name in r.content) + +class RFCEditorUndoTestCase(django.test.TestCase): + fixtures = ['names'] + + def test_rfceditor_undo(self): + draft = make_test_data() + + e = add_state_change_event(draft, Person.objects.get(name="(System)"), None, + State.objects.get(type="draft-rfceditor", slug="auth")) + e.desc = "Test" + e.save() + + url = urlreverse('ietf.sync.views.rfceditor_undo') + login_testing_unauthorized(self, "rfc", url) + + # get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + self.assertTrue(e.doc_id in r.content) + + # delete + deleted_before = DeletedEvent.objects.count() + + r = self.client.post(url, dict(event=e.id)) + self.assertEquals(r.status_code, 302) + + self.assertEquals(StateDocEvent.objects.filter(id=e.id).count(), 0) + self.assertEquals(DeletedEvent.objects.count(), deleted_before + 1) + + # let's just test we can recover + e = DeletedEvent.objects.all().order_by("-time")[0] + + e.content_type.model_class().objects.create(**json.loads(e.json)) + self.assertEquals(draft.latest_event(type="changed_state").desc, "Test") From 8165048f11f77a4a41ac55677a9a1db9d6f2ff9c Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Tue, 27 Nov 2012 12:06:32 +0000 Subject: [PATCH 44/47] Add word wrap to charters for charters that aren't preformatted - Legacy-Id: 5078 --- ietf/templates/idrfc/document_charter.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/templates/idrfc/document_charter.html b/ietf/templates/idrfc/document_charter.html index e9f092b87..04d82c418 100644 --- a/ietf/templates/idrfc/document_charter.html +++ b/ietf/templates/idrfc/document_charter.html @@ -120,7 +120,7 @@ {% if doc.rev %}
    - {{ content|safe|linebreaksbr|keep_spacing|sanitize_html|safe }} + {{ content|safe|keep_spacing|sanitize_html|wordwrap:80|safe }}
    {% endif %} From 8eeef2def41971643a112bf66c4e0bc74e122e5d Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Tue, 27 Nov 2012 12:08:56 +0000 Subject: [PATCH 45/47] Send out expires as "null" in JSON rather than crashing if there's no expiry on a document - Legacy-Id: 5079 --- ietf/idrfc/views_doc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/idrfc/views_doc.py b/ietf/idrfc/views_doc.py index 9fdca872c..242a646ab 100644 --- a/ietf/idrfc/views_doc.py +++ b/ietf/idrfc/views_doc.py @@ -388,7 +388,7 @@ def document_json(request, name): name=doc.group.name, type=extract_name(doc.group.type), acronym=doc.group.acronym) - data["expires"] = doc.expires.strftime("%Y-%m-%d %H:%M:%S") + data["expires"] = doc.expires.strftime("%Y-%m-%d %H:%M:%S") if doc.expires else None data["title"] = doc.title data["abstract"] = doc.abstract data["aliases"] = list(doc.docalias_set.values_list("name", flat=True)) From 9c571e65efe74dba1cbefa12f5bffba5be4d62b2 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Tue, 11 Dec 2012 15:10:51 +0000 Subject: [PATCH 46/47] Undo actual state when doing an RFC Editor undo, this is only relevant if the removed entry is the latest entry - Legacy-Id: 5116 --- ietf/sync/tests.py | 26 ++++++++++++++++++-------- ietf/sync/views.py | 10 ++++++++++ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/ietf/sync/tests.py b/ietf/sync/tests.py index bb6403124..aed7eb60a 100644 --- a/ietf/sync/tests.py +++ b/ietf/sync/tests.py @@ -407,30 +407,40 @@ class RFCEditorUndoTestCase(django.test.TestCase): def test_rfceditor_undo(self): draft = make_test_data() - e = add_state_change_event(draft, Person.objects.get(name="(System)"), None, + e1 = add_state_change_event(draft, Person.objects.get(name="(System)"), None, State.objects.get(type="draft-rfceditor", slug="auth")) - e.desc = "Test" - e.save() + e1.desc = "First" + e1.save() + e2 = add_state_change_event(draft, Person.objects.get(name="(System)"), None, + State.objects.get(type="draft-rfceditor", slug="edit")) + e2.desc = "Second" + e2.save() + url = urlreverse('ietf.sync.views.rfceditor_undo') login_testing_unauthorized(self, "rfc", url) # get r = self.client.get(url) self.assertEquals(r.status_code, 200) - self.assertTrue(e.doc_id in r.content) + self.assertTrue(e2.doc_id in r.content) - # delete + # delete e2 deleted_before = DeletedEvent.objects.count() - r = self.client.post(url, dict(event=e.id)) + r = self.client.post(url, dict(event=e2.id)) self.assertEquals(r.status_code, 302) - self.assertEquals(StateDocEvent.objects.filter(id=e.id).count(), 0) + self.assertEquals(StateDocEvent.objects.filter(id=e2.id).count(), 0) + self.assertEquals(draft.get_state("draft-rfceditor").slug, "auth") self.assertEquals(DeletedEvent.objects.count(), deleted_before + 1) + # delete e1 + r = self.client.post(url, dict(event=e1.id)) + self.assertEquals(draft.get_state("draft-rfceditor"), None) + # let's just test we can recover e = DeletedEvent.objects.all().order_by("-time")[0] e.content_type.model_class().objects.create(**json.loads(e.json)) - self.assertEquals(draft.latest_event(type="changed_state").desc, "Test") + self.assertTrue(StateDocEvent.objects.filter(desc="First", doc=draft)) diff --git a/ietf/sync/views.py b/ietf/sync/views.py index e9d734676..47a372fbd 100644 --- a/ietf/sync/views.py +++ b/ietf/sync/views.py @@ -120,6 +120,16 @@ def rfceditor_undo(request): except StateDocEvent.DoesNotExist: return HttpResponse("Event does not exist") + doc = e.doc + + # possibly reset the state of the document + all_events = StateDocEvent.objects.filter(doc=doc, state_type="draft-rfceditor").order_by("-time", "-id") + if all_events and all_events[0] == e: + if len(all_events) > 1: + doc.set_state(all_events[1].state) + else: + doc.unset_state("draft-rfceditor") + dump = DeletedEvent() dump.content_type = ContentType.objects.get_for_model(type(e)) dump.json = json.dumps(object_as_shallow_dict(e), indent=2) From 819e80f033b447bf483a3b8c01433a7cecc18b16 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Tue, 11 Dec 2012 15:16:31 +0000 Subject: [PATCH 47/47] Fix quoting bug in AUTH48 links - Legacy-Id: 5118 --- ietf/sync/rfceditor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/sync/rfceditor.py b/ietf/sync/rfceditor.py index ed3415858..ccb7f9032 100644 --- a/ietf/sync/rfceditor.py +++ b/ietf/sync/rfceditor.py @@ -167,7 +167,7 @@ def update_drafts_from_queue(drafts): e = add_state_change_event(d, system, prev_state, next_state) if auth48: - e.desc = re.sub(r"(.*)", r"\1" % auth48, e.desc) + e.desc = re.sub(r"(.*)", "\\1" % auth48, e.desc) e.save() changed.add(name)