From 6c1e87b773d8846c3057d3db503ecfe191154c73 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Tue, 26 Jun 2012 17:47:53 +0000 Subject: [PATCH 01/30] Move milestones branch out of the way to prepare for merge with trunk to get latest charter changes in [[Split portion of a mixed commit.]] - Legacy-Id: 4514.1 --- ietf/doc/models.py | 1 + ietf/group/admin.py | 4 +- ietf/group/migrations/0001_initial.py | 481 ++++++++++++++++++ ...del_field_groupmilestone_expected_due_d.py | 361 +++++++++++++ ietf/group/migrations/0003_fixup_milestone.py | 331 ++++++++++++ ...done_date__del_field_groupmilestone_don.py | 322 ++++++++++++ .../0005_auto__add_groupmilestonehistory.py | 349 +++++++++++++ ietf/group/migrations/__init__.py | 0 ietf/group/models.py | 30 +- ietf/group/utils.py | 31 +- ietf/idrfc/views_doc.py | 16 +- ietf/name/fixtures/names.xml | 67 +-- ...sitionname__add_groupmilestonestatename.py | 181 +++++++ .../0004_add_groupmilestonestatenames.py | 170 +++++++ ietf/name/models.py | 4 +- ietf/person/forms.py | 16 +- ietf/templates/idrfc/document_charter.html | 18 +- ietf/templates/wginfo/edit.html | 2 +- ietf/templates/wginfo/edit_milestones.html | 91 ++++ ietf/templates/wginfo/milestone_form.html | 45 ++ ietf/templates/wginfo/milestones.html | 12 + .../wginfo/reset_charter_milestones.html | 36 ++ ietf/templates/wginfo/wg-charterREDESIGN.txt | 2 +- ietf/templates/wginfo/wg_charter.html | 198 +++---- ietf/templates/wginfo/wg_charterREDESIGN.html | 167 ------ ietf/utils/history.py | 39 +- ietf/wginfo/milestones.py | 341 +++++++++++++ ietf/wginfo/tests.py | 249 +++++++++ ietf/wginfo/urls.py | 12 +- ietf/wginfo/views.py | 32 +- static/css/token-input.css | 14 +- static/js/edit-milestones.js | 100 ++++ static/js/emails-field.js | 13 - static/js/lib/jquery.maskedinput.js | 258 ++++++++++ static/js/tokenized-field.js | 19 + 35 files changed, 3639 insertions(+), 373 deletions(-) create mode 100644 ietf/group/migrations/0001_initial.py create mode 100644 ietf/group/migrations/0002_auto__add_milestonegroupevent__del_field_groupmilestone_expected_due_d.py create mode 100644 ietf/group/migrations/0003_fixup_milestone.py create mode 100644 ietf/group/migrations/0004_auto__del_field_groupmilestone_done_date__del_field_groupmilestone_don.py create mode 100644 ietf/group/migrations/0005_auto__add_groupmilestonehistory.py create mode 100644 ietf/group/migrations/__init__.py create mode 100644 ietf/name/migrations/0003_auto__del_groupballotpositionname__add_groupmilestonestatename.py create mode 100644 ietf/name/migrations/0004_add_groupmilestonestatenames.py create mode 100644 ietf/templates/wginfo/edit_milestones.html create mode 100644 ietf/templates/wginfo/milestone_form.html create mode 100644 ietf/templates/wginfo/milestones.html create mode 100644 ietf/templates/wginfo/reset_charter_milestones.html delete mode 100644 ietf/templates/wginfo/wg_charterREDESIGN.html create mode 100644 ietf/wginfo/milestones.py create mode 100644 static/js/edit-milestones.js delete mode 100644 static/js/emails-field.js create mode 100644 static/js/lib/jquery.maskedinput.js create mode 100644 static/js/tokenized-field.js diff --git a/ietf/doc/models.py b/ietf/doc/models.py index 8b4e27f27..4a6baab54 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -311,6 +311,7 @@ EVENT_TYPES = [ # WG events ("changed_group", "Changed group"), ("changed_protocol_writeup", "Changed protocol writeup"), + ("changed_charter_milestone", "Changed charter milestone"), # charter events ("initial_review", "Set initial review time"), diff --git a/ietf/group/admin.py b/ietf/group/admin.py index ed08b74e9..2538dbfa8 100644 --- a/ietf/group/admin.py +++ b/ietf/group/admin.py @@ -116,8 +116,8 @@ class GroupHistoryAdmin(admin.ModelAdmin): admin.site.register(GroupHistory, GroupHistoryAdmin) class GroupMilestoneAdmin(admin.ModelAdmin): - list_display = ["group", "desc", "expected_due_date", "time"] - search_fields = ["group__name", "group__acronym", "desc"] + list_display = ["group", "desc", "due", "resolved", "time"] + search_fields = ["group__name", "group__acronym", "desc", "resolved"] raw_id_fields = ["group"] admin.site.register(GroupMilestone, GroupMilestoneAdmin) diff --git a/ietf/group/migrations/0001_initial.py b/ietf/group/migrations/0001_initial.py new file mode 100644 index 000000000..6e9852f5d --- /dev/null +++ b/ietf/group/migrations/0001_initial.py @@ -0,0 +1,481 @@ +# 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 'Group' + db.create_table('group_group', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('time', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=80)), + ('state', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['name.GroupStateName'], null=True)), + ('type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['name.GroupTypeName'], null=True)), + ('parent', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['group.Group'], null=True, blank=True)), + ('ad', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['person.Person'], null=True, blank=True)), + ('list_email', self.gf('django.db.models.fields.CharField')(max_length=64, blank=True)), + ('list_subscribe', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)), + ('list_archive', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)), + ('comments', self.gf('django.db.models.fields.TextField')(blank=True)), + ('acronym', self.gf('django.db.models.fields.SlugField')(unique=True, max_length=40, db_index=True)), + ('charter', self.gf('django.db.models.fields.related.OneToOneField')(blank=True, related_name='chartered_group', unique=True, null=True, to=orm['doc.Document'])), + )) + db.send_create_signal('group', ['Group']) + + # Adding M2M table for field unused_states on 'Group' + db.create_table('group_group_unused_states', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('group', models.ForeignKey(orm['group.group'], null=False)), + ('state', models.ForeignKey(orm['doc.state'], null=False)) + )) + db.create_unique('group_group_unused_states', ['group_id', 'state_id']) + + # Adding M2M table for field unused_tags on 'Group' + db.create_table('group_group_unused_tags', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('group', models.ForeignKey(orm['group.group'], null=False)), + ('doctagname', models.ForeignKey(orm['name.doctagname'], null=False)) + )) + db.create_unique('group_group_unused_tags', ['group_id', 'doctagname_id']) + + # Adding model 'GroupHistory' + db.create_table('group_grouphistory', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('time', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=80)), + ('state', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['name.GroupStateName'], null=True)), + ('type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['name.GroupTypeName'], null=True)), + ('parent', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['group.Group'], null=True, blank=True)), + ('ad', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['person.Person'], null=True, blank=True)), + ('list_email', self.gf('django.db.models.fields.CharField')(max_length=64, blank=True)), + ('list_subscribe', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)), + ('list_archive', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)), + ('comments', self.gf('django.db.models.fields.TextField')(blank=True)), + ('group', self.gf('django.db.models.fields.related.ForeignKey')(related_name='history_set', to=orm['group.Group'])), + ('acronym', self.gf('django.db.models.fields.CharField')(max_length=40)), + )) + db.send_create_signal('group', ['GroupHistory']) + + # Adding M2M table for field unused_states on 'GroupHistory' + db.create_table('group_grouphistory_unused_states', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('grouphistory', models.ForeignKey(orm['group.grouphistory'], null=False)), + ('state', models.ForeignKey(orm['doc.state'], null=False)) + )) + db.create_unique('group_grouphistory_unused_states', ['grouphistory_id', 'state_id']) + + # Adding M2M table for field unused_tags on 'GroupHistory' + db.create_table('group_grouphistory_unused_tags', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('grouphistory', models.ForeignKey(orm['group.grouphistory'], null=False)), + ('doctagname', models.ForeignKey(orm['name.doctagname'], null=False)) + )) + db.create_unique('group_grouphistory_unused_tags', ['grouphistory_id', 'doctagname_id']) + + # Adding model 'GroupURL' + db.create_table('group_groupurl', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('group', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['group.Group'])), + ('name', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('url', self.gf('django.db.models.fields.URLField')(max_length=200)), + )) + db.send_create_signal('group', ['GroupURL']) + + # Adding model 'GroupMilestone' + db.create_table('group_groupmilestone', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('group', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['group.Group'])), + ('desc', self.gf('django.db.models.fields.TextField')()), + ('expected_due_date', self.gf('django.db.models.fields.DateField')()), + ('done', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('done_date', self.gf('django.db.models.fields.DateField')(null=True, blank=True)), + ('time', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), + )) + db.send_create_signal('group', ['GroupMilestone']) + + # Adding model 'GroupStateTransitions' + db.create_table('group_groupstatetransitions', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('group', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['group.Group'])), + ('state', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['doc.State'])), + )) + db.send_create_signal('group', ['GroupStateTransitions']) + + # Adding M2M table for field next_states on 'GroupStateTransitions' + db.create_table('group_groupstatetransitions_next_states', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('groupstatetransitions', models.ForeignKey(orm['group.groupstatetransitions'], null=False)), + ('state', models.ForeignKey(orm['doc.state'], null=False)) + )) + db.create_unique('group_groupstatetransitions_next_states', ['groupstatetransitions_id', 'state_id']) + + # Adding model 'GroupEvent' + db.create_table('group_groupevent', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('group', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['group.Group'])), + ('time', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)), + ('type', self.gf('django.db.models.fields.CharField')(max_length=50)), + ('by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['person.Person'])), + ('desc', self.gf('django.db.models.fields.TextField')()), + )) + db.send_create_signal('group', ['GroupEvent']) + + # Adding model 'ChangeStateGroupEvent' + db.create_table('group_changestategroupevent', ( + ('groupevent_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['group.GroupEvent'], unique=True, primary_key=True)), + ('state', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['name.GroupStateName'])), + )) + db.send_create_signal('group', ['ChangeStateGroupEvent']) + + # Adding model 'Role' + db.create_table('group_role', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['name.RoleName'])), + ('group', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['group.Group'])), + ('person', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['person.Person'])), + ('email', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['person.Email'])), + )) + db.send_create_signal('group', ['Role']) + + # Adding model 'RoleHistory' + db.create_table('group_rolehistory', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['name.RoleName'])), + ('group', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['group.GroupHistory'])), + ('person', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['person.Person'])), + ('email', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['person.Email'])), + )) + db.send_create_signal('group', ['RoleHistory']) + + + def backwards(self, orm): + + # Deleting model 'Group' + db.delete_table('group_group') + + # Removing M2M table for field unused_states on 'Group' + db.delete_table('group_group_unused_states') + + # Removing M2M table for field unused_tags on 'Group' + db.delete_table('group_group_unused_tags') + + # Deleting model 'GroupHistory' + db.delete_table('group_grouphistory') + + # Removing M2M table for field unused_states on 'GroupHistory' + db.delete_table('group_grouphistory_unused_states') + + # Removing M2M table for field unused_tags on 'GroupHistory' + db.delete_table('group_grouphistory_unused_tags') + + # Deleting model 'GroupURL' + db.delete_table('group_groupurl') + + # Deleting model 'GroupMilestone' + db.delete_table('group_groupmilestone') + + # Deleting model 'GroupStateTransitions' + db.delete_table('group_groupstatetransitions') + + # Removing M2M table for field next_states on 'GroupStateTransitions' + db.delete_table('group_groupstatetransitions_next_states') + + # Deleting model 'GroupEvent' + db.delete_table('group_groupevent') + + # Deleting model 'ChangeStateGroupEvent' + db.delete_table('group_changestategroupevent') + + # Deleting model 'Role' + db.delete_table('group_role') + + # Deleting model 'RoleHistory' + db.delete_table('group_rolehistory') + + + 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.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.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.relateddocument': { + 'Meta': {'object_name': 'RelatedDocument'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'relationship': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.DocRelationshipName']"}), + 'source': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.Document']"}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.DocAlias']"}) + }, + 'doc.state': { + 'Meta': {'ordering': "['type', 'order']", 'object_name': 'State'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'next_states': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'previous_states'", 'symmetrical': 'False', 'to': "orm['doc.State']"}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.StateType']"}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'doc.statetype': { + 'Meta': {'object_name': 'StateType'}, + 'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '30', 'primary_key': 'True'}) + }, + 'group.changestategroupevent': { + 'Meta': {'ordering': "['-time', 'id']", 'object_name': 'ChangeStateGroupEvent', '_ormbases': ['group.GroupEvent']}, + 'groupevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['group.GroupEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.GroupStateName']"}) + }, + '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'}) + }, + 'group.groupevent': { + 'Meta': {'ordering': "['-time', 'id']", 'object_name': 'GroupEvent'}, + 'by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}), + 'desc': ('django.db.models.fields.TextField', [], {}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + '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'}) + }, + 'group.grouphistory': { + 'Meta': {'object_name': 'GroupHistory'}, + 'acronym': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']", 'null': 'True', 'blank': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'history_set'", 'to': "orm['group.Group']"}), + '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'}) + }, + 'group.groupmilestone': { + 'Meta': {'ordering': "['expected_due_date']", 'object_name': 'GroupMilestone'}, + 'desc': ('django.db.models.fields.TextField', [], {}), + 'done': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'done_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'expected_due_date': ('django.db.models.fields.DateField', [], {}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'time': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'group.groupstatetransitions': { + 'Meta': {'object_name': 'GroupStateTransitions'}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'next_states': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'previous_groupstatetransitions_states'", 'symmetrical': 'False', 'to': "orm['doc.State']"}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.State']"}) + }, + 'group.groupurl': { + 'Meta': {'object_name': 'GroupURL'}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '200'}) + }, + 'group.role': { + 'Meta': {'object_name': 'Role'}, + 'email': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Email']"}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.RoleName']"}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}) + }, + 'group.rolehistory': { + 'Meta': {'object_name': 'RoleHistory'}, + 'email': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Email']"}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.GroupHistory']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.RoleName']"}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}) + }, + '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.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.rolename': { + 'Meta': {'ordering': "['order']", 'object_name': 'RoleName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.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 = ['group'] diff --git a/ietf/group/migrations/0002_auto__add_milestonegroupevent__del_field_groupmilestone_expected_due_d.py b/ietf/group/migrations/0002_auto__add_milestonegroupevent__del_field_groupmilestone_expected_due_d.py new file mode 100644 index 000000000..f79b03b18 --- /dev/null +++ b/ietf/group/migrations/0002_auto__add_milestonegroupevent__del_field_groupmilestone_expected_due_d.py @@ -0,0 +1,361 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + depends_on = ( + ("name", "0004_add_groupmilestonestatenames"), + ) + + def forwards(self, orm): + + # Adding model 'MilestoneGroupEvent' + db.create_table('group_milestonegroupevent', ( + ('groupevent_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['group.GroupEvent'], unique=True, primary_key=True)), + ('milestone', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['group.GroupMilestone'])), + )) + db.send_create_signal('group', ['MilestoneGroupEvent']) + + # rename expected_due_date + db.rename_column('group_groupmilestone', 'expected_due_date', "due") + + # Adding field 'GroupMilestone.resolved' + db.add_column('group_groupmilestone', 'resolved', self.gf('django.db.models.fields.CharField')(default='', max_length=50, blank=True), keep_default=False) + + # Adding field 'GroupMilestone.state' + db.add_column('group_groupmilestone', 'state', self.gf('django.db.models.fields.related.ForeignKey')(default='active', to=orm['name.GroupMilestoneStateName']), keep_default=False) + + # Adding M2M table for field docs on 'GroupMilestone' + db.create_table('group_groupmilestone_docs', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('groupmilestone', models.ForeignKey(orm['group.groupmilestone'], null=False)), + ('document', models.ForeignKey(orm['doc.document'], null=False)) + )) + db.create_unique('group_groupmilestone_docs', ['groupmilestone_id', 'document_id']) + + # Changing field 'GroupMilestone.desc' + db.alter_column('group_groupmilestone', 'desc', self.gf('django.db.models.fields.CharField')(max_length=500)) + + + def backwards(self, orm): + + # Deleting model 'MilestoneGroupEvent' + db.delete_table('group_milestonegroupevent') + + # rename due + db.rename_column('group_groupmilestone', 'due', "expected_due_date") + + # Deleting field 'GroupMilestone.resolved' + db.delete_column('group_groupmilestone', 'resolved') + + # Deleting field 'GroupMilestone.state' + db.delete_column('group_groupmilestone', 'state_id') + + # Removing M2M table for field docs on 'GroupMilestone' + db.delete_table('group_groupmilestone_docs') + + # Changing field 'GroupMilestone.desc' + db.alter_column('group_groupmilestone', 'desc', self.gf('django.db.models.fields.TextField')()) + + + 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.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.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.relateddocument': { + 'Meta': {'object_name': 'RelatedDocument'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'relationship': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.DocRelationshipName']"}), + 'source': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.Document']"}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.DocAlias']"}) + }, + 'doc.state': { + 'Meta': {'ordering': "['type', 'order']", 'object_name': 'State'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'next_states': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'previous_states'", 'symmetrical': 'False', 'to': "orm['doc.State']"}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.StateType']"}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'doc.statetype': { + 'Meta': {'object_name': 'StateType'}, + 'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '30', 'primary_key': 'True'}) + }, + 'group.changestategroupevent': { + 'Meta': {'ordering': "['-time', 'id']", 'object_name': 'ChangeStateGroupEvent', '_ormbases': ['group.GroupEvent']}, + 'groupevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['group.GroupEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.GroupStateName']"}) + }, + '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'}) + }, + 'group.groupevent': { + 'Meta': {'ordering': "['-time', 'id']", 'object_name': 'GroupEvent'}, + 'by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}), + 'desc': ('django.db.models.fields.TextField', [], {}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + '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'}) + }, + 'group.grouphistory': { + 'Meta': {'object_name': 'GroupHistory'}, + 'acronym': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']", 'null': 'True', 'blank': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'history_set'", 'to': "orm['group.Group']"}), + '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'}) + }, + 'group.groupmilestone': { + 'Meta': {'ordering': "['due', 'id']", 'object_name': 'GroupMilestone'}, + 'desc': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'docs': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['doc.Document']", 'symmetrical': 'False', 'blank': 'True'}), + 'done': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'done_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'due': ('django.db.models.fields.DateField', [], {}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'resolved': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.GroupMilestoneStateName']"}), + 'time': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'group.groupstatetransitions': { + 'Meta': {'object_name': 'GroupStateTransitions'}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'next_states': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'previous_groupstatetransitions_states'", 'symmetrical': 'False', 'to': "orm['doc.State']"}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.State']"}) + }, + 'group.groupurl': { + 'Meta': {'object_name': 'GroupURL'}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '200'}) + }, + 'group.milestonegroupevent': { + 'Meta': {'ordering': "['-time', 'id']", 'object_name': 'MilestoneGroupEvent', '_ormbases': ['group.GroupEvent']}, + 'groupevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['group.GroupEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'milestone': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.GroupMilestone']"}) + }, + 'group.role': { + 'Meta': {'object_name': 'Role'}, + 'email': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Email']"}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.RoleName']"}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}) + }, + 'group.rolehistory': { + 'Meta': {'object_name': 'RoleHistory'}, + 'email': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Email']"}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.GroupHistory']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.RoleName']"}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}) + }, + '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.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.groupmilestonestatename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupMilestoneStateName'}, + '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.rolename': { + 'Meta': {'ordering': "['order']", 'object_name': 'RoleName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.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 = ['group'] diff --git a/ietf/group/migrations/0003_fixup_milestone.py b/ietf/group/migrations/0003_fixup_milestone.py new file mode 100644 index 000000000..8abdc3d6f --- /dev/null +++ b/ietf/group/migrations/0003_fixup_milestone.py @@ -0,0 +1,331 @@ +# 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): + "Write your forwards methods here." + + system_person = orm["person.Person"].objects.get(name="(System)") + + for m in orm.GroupMilestone.objects.all(): + if m.done: + m.resolved = "Done" + m.save() + + orm.MilestoneGroupEvent.objects.get_or_create( + group_id=m.group_id, + type="changed_milestone", + time=datetime.datetime.combine(m.done_date, datetime.time(0, 0, 0)), + by=system_person, + desc='Changed milestone "%s", resolved as "%s"' % (m.desc, m.resolved), + milestone=m, + ) + + + 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.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.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.relateddocument': { + 'Meta': {'object_name': 'RelatedDocument'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'relationship': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.DocRelationshipName']"}), + 'source': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.Document']"}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.DocAlias']"}) + }, + 'doc.state': { + 'Meta': {'ordering': "['type', 'order']", 'object_name': 'State'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'next_states': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'previous_states'", 'symmetrical': 'False', 'to': "orm['doc.State']"}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.StateType']"}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'doc.statetype': { + 'Meta': {'object_name': 'StateType'}, + 'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '30', 'primary_key': 'True'}) + }, + 'group.changestategroupevent': { + 'Meta': {'ordering': "['-time', 'id']", 'object_name': 'ChangeStateGroupEvent', '_ormbases': ['group.GroupEvent']}, + 'groupevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['group.GroupEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.GroupStateName']"}) + }, + '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'}) + }, + 'group.groupevent': { + 'Meta': {'ordering': "['-time', 'id']", 'object_name': 'GroupEvent'}, + 'by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}), + 'desc': ('django.db.models.fields.TextField', [], {}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + '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'}) + }, + 'group.grouphistory': { + 'Meta': {'object_name': 'GroupHistory'}, + 'acronym': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']", 'null': 'True', 'blank': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'history_set'", 'to': "orm['group.Group']"}), + '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'}) + }, + 'group.groupmilestone': { + 'Meta': {'ordering': "['due', 'id']", 'object_name': 'GroupMilestone'}, + 'desc': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'docs': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['doc.Document']", 'symmetrical': 'False', 'blank': 'True'}), + 'done': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'done_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}), + 'due': ('django.db.models.fields.DateField', [], {}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'resolved': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.GroupMilestoneStateName']"}), + 'time': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'group.groupstatetransitions': { + 'Meta': {'object_name': 'GroupStateTransitions'}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'next_states': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'previous_groupstatetransitions_states'", 'symmetrical': 'False', 'to': "orm['doc.State']"}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.State']"}) + }, + 'group.groupurl': { + 'Meta': {'object_name': 'GroupURL'}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '200'}) + }, + 'group.milestonegroupevent': { + 'Meta': {'ordering': "['-time', 'id']", 'object_name': 'MilestoneGroupEvent', '_ormbases': ['group.GroupEvent']}, + 'groupevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['group.GroupEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'milestone': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.GroupMilestone']"}) + }, + 'group.role': { + 'Meta': {'object_name': 'Role'}, + 'email': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Email']"}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.RoleName']"}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}) + }, + 'group.rolehistory': { + 'Meta': {'object_name': 'RoleHistory'}, + 'email': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Email']"}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.GroupHistory']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.RoleName']"}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}) + }, + '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.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.groupmilestonestatename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupMilestoneStateName'}, + '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.rolename': { + 'Meta': {'ordering': "['order']", 'object_name': 'RoleName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.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 = ['group'] diff --git a/ietf/group/migrations/0004_auto__del_field_groupmilestone_done_date__del_field_groupmilestone_don.py b/ietf/group/migrations/0004_auto__del_field_groupmilestone_done_date__del_field_groupmilestone_don.py new file mode 100644 index 000000000..e08a26e94 --- /dev/null +++ b/ietf/group/migrations/0004_auto__del_field_groupmilestone_done_date__del_field_groupmilestone_don.py @@ -0,0 +1,322 @@ +# 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): + + # Deleting field 'GroupMilestone.done_date' + db.delete_column('group_groupmilestone', 'done_date') + + # Deleting field 'GroupMilestone.done' + db.delete_column('group_groupmilestone', 'done') + + + def backwards(self, orm): + + # Adding field 'GroupMilestone.done_date' + db.add_column('group_groupmilestone', 'done_date', self.gf('django.db.models.fields.DateField')(null=True, blank=True), keep_default=False) + + # Adding field 'GroupMilestone.done' + db.add_column('group_groupmilestone', 'done', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=False) + + + 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.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.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.relateddocument': { + 'Meta': {'object_name': 'RelatedDocument'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'relationship': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.DocRelationshipName']"}), + 'source': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.Document']"}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.DocAlias']"}) + }, + 'doc.state': { + 'Meta': {'ordering': "['type', 'order']", 'object_name': 'State'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'next_states': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'previous_states'", 'symmetrical': 'False', 'to': "orm['doc.State']"}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.StateType']"}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'doc.statetype': { + 'Meta': {'object_name': 'StateType'}, + 'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '30', 'primary_key': 'True'}) + }, + 'group.changestategroupevent': { + 'Meta': {'ordering': "['-time', 'id']", 'object_name': 'ChangeStateGroupEvent', '_ormbases': ['group.GroupEvent']}, + 'groupevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['group.GroupEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.GroupStateName']"}) + }, + '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'}) + }, + 'group.groupevent': { + 'Meta': {'ordering': "['-time', 'id']", 'object_name': 'GroupEvent'}, + 'by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}), + 'desc': ('django.db.models.fields.TextField', [], {}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + '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'}) + }, + 'group.grouphistory': { + 'Meta': {'object_name': 'GroupHistory'}, + 'acronym': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']", 'null': 'True', 'blank': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'history_set'", 'to': "orm['group.Group']"}), + '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'}) + }, + 'group.groupmilestone': { + 'Meta': {'ordering': "['due', 'id']", 'object_name': 'GroupMilestone'}, + 'desc': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'docs': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['doc.Document']", 'symmetrical': 'False', 'blank': 'True'}), + 'due': ('django.db.models.fields.DateField', [], {}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'resolved': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.GroupMilestoneStateName']"}), + 'time': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'group.groupstatetransitions': { + 'Meta': {'object_name': 'GroupStateTransitions'}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'next_states': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'previous_groupstatetransitions_states'", 'symmetrical': 'False', 'to': "orm['doc.State']"}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.State']"}) + }, + 'group.groupurl': { + 'Meta': {'object_name': 'GroupURL'}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '200'}) + }, + 'group.milestonegroupevent': { + 'Meta': {'ordering': "['-time', 'id']", 'object_name': 'MilestoneGroupEvent', '_ormbases': ['group.GroupEvent']}, + 'groupevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['group.GroupEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'milestone': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.GroupMilestone']"}) + }, + 'group.role': { + 'Meta': {'object_name': 'Role'}, + 'email': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Email']"}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.RoleName']"}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}) + }, + 'group.rolehistory': { + 'Meta': {'object_name': 'RoleHistory'}, + 'email': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Email']"}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.GroupHistory']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.RoleName']"}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}) + }, + '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.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.groupmilestonestatename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupMilestoneStateName'}, + '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.rolename': { + 'Meta': {'ordering': "['order']", 'object_name': 'RoleName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.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 = ['group'] diff --git a/ietf/group/migrations/0005_auto__add_groupmilestonehistory.py b/ietf/group/migrations/0005_auto__add_groupmilestonehistory.py new file mode 100644 index 000000000..394e83cd3 --- /dev/null +++ b/ietf/group/migrations/0005_auto__add_groupmilestonehistory.py @@ -0,0 +1,349 @@ +# 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 'GroupMilestoneHistory' + db.create_table('group_groupmilestonehistory', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('group', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['group.Group'])), + ('state', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['name.GroupMilestoneStateName'])), + ('desc', self.gf('django.db.models.fields.CharField')(max_length=500)), + ('due', self.gf('django.db.models.fields.DateField')()), + ('resolved', self.gf('django.db.models.fields.CharField')(max_length=50, blank=True)), + ('time', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), + ('milestone', self.gf('django.db.models.fields.related.ForeignKey')(related_name='history_set', to=orm['group.GroupMilestone'])), + )) + db.send_create_signal('group', ['GroupMilestoneHistory']) + + # Adding M2M table for field docs on 'GroupMilestoneHistory' + db.create_table('group_groupmilestonehistory_docs', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('groupmilestonehistory', models.ForeignKey(orm['group.groupmilestonehistory'], null=False)), + ('document', models.ForeignKey(orm['doc.document'], null=False)) + )) + db.create_unique('group_groupmilestonehistory_docs', ['groupmilestonehistory_id', 'document_id']) + + + def backwards(self, orm): + + # Deleting model 'GroupMilestoneHistory' + db.delete_table('group_groupmilestonehistory') + + # Removing M2M table for field docs on 'GroupMilestoneHistory' + db.delete_table('group_groupmilestonehistory_docs') + + + 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.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.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.relateddocument': { + 'Meta': {'object_name': 'RelatedDocument'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'relationship': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.DocRelationshipName']"}), + 'source': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.Document']"}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.DocAlias']"}) + }, + 'doc.state': { + 'Meta': {'ordering': "['type', 'order']", 'object_name': 'State'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'next_states': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'previous_states'", 'symmetrical': 'False', 'to': "orm['doc.State']"}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}), + 'type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.StateType']"}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'doc.statetype': { + 'Meta': {'object_name': 'StateType'}, + 'label': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '30', 'primary_key': 'True'}) + }, + 'group.changestategroupevent': { + 'Meta': {'ordering': "['-time', 'id']", 'object_name': 'ChangeStateGroupEvent', '_ormbases': ['group.GroupEvent']}, + 'groupevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['group.GroupEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.GroupStateName']"}) + }, + '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'}) + }, + 'group.groupevent': { + 'Meta': {'ordering': "['-time', 'id']", 'object_name': 'GroupEvent'}, + 'by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}), + 'desc': ('django.db.models.fields.TextField', [], {}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + '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'}) + }, + 'group.grouphistory': { + 'Meta': {'object_name': 'GroupHistory'}, + 'acronym': ('django.db.models.fields.CharField', [], {'max_length': '40'}), + 'ad': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']", 'null': 'True', 'blank': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'history_set'", 'to': "orm['group.Group']"}), + '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'}) + }, + 'group.groupmilestone': { + 'Meta': {'ordering': "['due', 'id']", 'object_name': 'GroupMilestone'}, + 'desc': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'docs': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['doc.Document']", 'symmetrical': 'False', 'blank': 'True'}), + 'due': ('django.db.models.fields.DateField', [], {}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'resolved': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.GroupMilestoneStateName']"}), + 'time': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'group.groupmilestonehistory': { + 'Meta': {'ordering': "['due', 'id']", 'object_name': 'GroupMilestoneHistory'}, + 'desc': ('django.db.models.fields.CharField', [], {'max_length': '500'}), + 'docs': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['doc.Document']", 'symmetrical': 'False', 'blank': 'True'}), + 'due': ('django.db.models.fields.DateField', [], {}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'milestone': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'history_set'", 'to': "orm['group.GroupMilestone']"}), + 'resolved': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.GroupMilestoneStateName']"}), + 'time': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'group.groupstatetransitions': { + 'Meta': {'object_name': 'GroupStateTransitions'}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'next_states': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'previous_groupstatetransitions_states'", 'symmetrical': 'False', 'to': "orm['doc.State']"}), + 'state': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['doc.State']"}) + }, + 'group.groupurl': { + 'Meta': {'object_name': 'GroupURL'}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '200'}) + }, + 'group.milestonegroupevent': { + 'Meta': {'ordering': "['-time', 'id']", 'object_name': 'MilestoneGroupEvent', '_ormbases': ['group.GroupEvent']}, + 'groupevent_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['group.GroupEvent']", 'unique': 'True', 'primary_key': 'True'}), + 'milestone': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.GroupMilestone']"}) + }, + 'group.role': { + 'Meta': {'object_name': 'Role'}, + 'email': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Email']"}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.Group']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.RoleName']"}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}) + }, + 'group.rolehistory': { + 'Meta': {'object_name': 'RoleHistory'}, + 'email': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Email']"}), + 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['group.GroupHistory']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['name.RoleName']"}), + 'person': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['person.Person']"}) + }, + '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.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.groupmilestonestatename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupMilestoneStateName'}, + '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.rolename': { + 'Meta': {'ordering': "['order']", 'object_name': 'RoleName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.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 = ['group'] diff --git a/ietf/group/migrations/__init__.py b/ietf/group/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ietf/group/models.py b/ietf/group/models.py index 723a74238..e003314ff 100644 --- a/ietf/group/models.py +++ b/ietf/group/models.py @@ -60,17 +60,31 @@ class GroupURL(models.Model): def __unicode__(self): return u"%s (%s)" % (self.url, self.name) -class GroupMilestone(models.Model): +class GroupMilestoneInfo(models.Model): group = models.ForeignKey(Group) - desc = models.TextField(verbose_name="Description") - expected_due_date = models.DateField() - done = models.BooleanField() - done_date = models.DateField(null=True, blank=True) + # a group has two sets of milestones, current milestones + # (active/under review/deleted) and charter milestones (active + # during a charter/recharter event), events for charter milestones + # are stored on the charter document + state = models.ForeignKey(GroupMilestoneStateName) + desc = models.CharField(verbose_name="Description", max_length=500) + due = models.DateField() + resolved = models.CharField(max_length=50, blank=True, help_text="Explanation of why milestone is resolved (usually \"Done\"), or empty if still due") time = models.DateTimeField(auto_now=True) + + docs = models.ManyToManyField('doc.Document', blank=True) + def __unicode__(self): return self.desc[:20] + "..." class Meta: - ordering = ['expected_due_date'] + abstract = True + ordering = ['due', 'id'] + +class GroupMilestone(GroupMilestoneInfo): + pass + +class GroupMilestoneHistory(GroupMilestoneInfo): + milestone = models.ForeignKey(GroupMilestone, related_name="history_set") class GroupStateTransitions(models.Model): """Captures that a group has overriden the default available @@ -86,6 +100,7 @@ GROUP_EVENT_CHOICES = [ ("changed_state", "Changed state"), ("added_comment", "Added comment"), ("info_changed", "Changed metadata"), + ("changed_milestone", "Changed milestone"), ] class GroupEvent(models.Model): @@ -105,6 +120,9 @@ class GroupEvent(models.Model): class ChangeStateGroupEvent(GroupEvent): state = models.ForeignKey(GroupStateName) +class MilestoneGroupEvent(GroupEvent): + milestone = models.ForeignKey(GroupMilestone) + class Role(models.Model): name = models.ForeignKey(RoleName) group = models.ForeignKey(Group) diff --git a/ietf/group/utils.py b/ietf/group/utils.py index e0a10ef54..fe69c85d3 100644 --- a/ietf/group/utils.py +++ b/ietf/group/utils.py @@ -3,33 +3,21 @@ import os from django.conf import settings from ietf.group.models import * +from ietf.utils.history import get_history_object_for, copy_many_to_many_for_history def save_group_in_history(group): - def get_model_fields_as_dict(obj): - return dict((field.name, getattr(obj, field.name)) - for field in obj._meta.fields - if field is not obj._meta.pk) - - # copy fields - fields = get_model_fields_as_dict(group) - del fields["charter"] # Charter is saved canonically on Group - fields["group"] = group - - grouphist = GroupHistory(**fields) - grouphist.save() + h = get_history_object_for(group) + h.save() # save RoleHistory for role in group.role_set.all(): rh = RoleHistory(name=role.name, group=grouphist, email=role.email, person=role.person) rh.save() - # copy many to many - for field in group._meta.many_to_many: - if field.rel.through and field.rel.through._meta.auto_created: - setattr(grouphist, field.name, getattr(group, field.name).all()) + copy_many_to_many_for_history(h, group) - return grouphist + return h def get_charter_text(group): # get file path from settings. Syntesize file name from path, acronym, and suffix @@ -55,3 +43,12 @@ def get_charter_text(group): except BaseException: desc = 'Error Loading Work Group Description' return desc + +def save_milestone_in_history(milestone): + h = get_history_object_for(milestone) + h.milestone = milestone + h.save() + + copy_many_to_many_for_history(h, milestone) + + return h diff --git a/ietf/idrfc/views_doc.py b/ietf/idrfc/views_doc.py index 5bbb0e4ad..476ed1a82 100644 --- a/ietf/idrfc/views_doc.py +++ b/ietf/idrfc/views_doc.py @@ -89,13 +89,9 @@ def document_main(request, name, rev=None): raise Http404() return document_main_idrfc(request, name, tab="document") - print name - print Document.objects.filter(name=name) doc = get_object_or_404(Document, docalias__name=name) - print doc group = doc.group - print group - + revisions = [] for h in doc.history_set.order_by("time", "id"): if h.rev and not h.rev in revisions: @@ -140,10 +136,17 @@ def document_main(request, name, rev=None): if doc.get_state_slug() in ("intrev", "iesgrev"): ballot_summary = needed_ballot_positions(doc, active_ballot_positions(doc).values()) + chartering = get_chartering_type(doc) + + # inject milestones from group + milestones = None + if chartering and not snapshot: + milestones = doc.group.groupmilestone_set.filter(state="charter") + return render_to_response("idrfc/document_charter.html", dict(doc=doc, top=top, - chartering=get_chartering_type(doc), + chartering=chartering, content=content, txt_url=settings.CHARTER_TXT_URL + filename, revisions=revisions, @@ -151,6 +154,7 @@ def document_main(request, name, rev=None): telechat=telechat, ballot_summary=ballot_summary, group=group, + milestones=milestones, ), context_instance=RequestContext(request)) diff --git a/ietf/name/fixtures/names.xml b/ietf/name/fixtures/names.xml index dad67e3a5..bb3a387dc 100644 --- a/ietf/name/fixtures/names.xml +++ b/ietf/name/fixtures/names.xml @@ -19,7 +19,7 @@ True 3 - False + True Block @@ -307,35 +307,29 @@ True 0 - - No + + Active True - 0 + 1 - - Yes + + Deleted True - 0 + 2 - - Abstain + + For review True - 0 + 3 - - Block + + Chartering/rechartering True - 0 - - - No record - - True - 0 + 4 BOF @@ -628,7 +622,7 @@ Draft Standard - True + False 0 @@ -1630,25 +1624,34 @@ Ready for external review Is this charter ready for external review? True - 0 + 1 - - charter - approve - Approve - Do we approve of this charter? - True - 0 - - - + draft approve Approve True - 0 + 1 + + charter + r-wo-ext + Ready w/o external review + Is this charter ready for external review? Is this charter ready for approval without external review? + True + 2 + + + + charter + approve + Approve + Do we approve of this charter? + True + 3 + + \ No newline at end of file diff --git a/ietf/name/migrations/0003_auto__del_groupballotpositionname__add_groupmilestonestatename.py b/ietf/name/migrations/0003_auto__del_groupballotpositionname__add_groupmilestonestatename.py new file mode 100644 index 000000000..c7ab6cd25 --- /dev/null +++ b/ietf/name/migrations/0003_auto__del_groupballotpositionname__add_groupmilestonestatename.py @@ -0,0 +1,181 @@ +# 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): + + # Deleting model 'GroupBallotPositionName' + db.delete_table('name_groupballotpositionname') + + # looks like this is actually included at the moment + # Adding model 'GroupMilestoneStateName' + # db.create_table('name_groupmilestonestatename', ( + # ('slug', self.gf('django.db.models.fields.CharField')(max_length=8, primary_key=True)), + # ('name', self.gf('django.db.models.fields.CharField')(max_length=255)), + # ('desc', self.gf('django.db.models.fields.TextField')(blank=True)), + # ('used', self.gf('django.db.models.fields.BooleanField')(default=True)), + # ('order', self.gf('django.db.models.fields.IntegerField')(default=0)), + # )) + # db.send_create_signal('name', ['GroupMilestoneStateName']) + + def backwards(self, orm): + + # Adding model 'GroupBallotPositionName' + db.create_table('name_groupballotpositionname', ( + ('used', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('slug', self.gf('django.db.models.fields.CharField')(max_length=8, primary_key=True)), + ('order', self.gf('django.db.models.fields.IntegerField')(default=0)), + ('desc', self.gf('django.db.models.fields.TextField')(blank=True)), + )) + db.send_create_signal('name', ['GroupBallotPositionName']) + + # Deleting model 'GroupMilestoneStateName' + # db.delete_table('name_groupmilestonestatename') + + + models = { + 'name.ballotpositionname': { + 'Meta': {'ordering': "['order']", 'object_name': 'BallotPositionName'}, + 'blocking': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.constraintname': { + 'Meta': {'ordering': "['order']", 'object_name': 'ConstraintName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.docrelationshipname': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocRelationshipName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.docremindertypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocReminderTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.doctagname': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocTagName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.doctypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.groupmilestonestatename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupMilestoneStateName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.groupstatename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupStateName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.grouptypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.intendedstdlevelname': { + 'Meta': {'ordering': "['order']", 'object_name': 'IntendedStdLevelName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.liaisonstatementpurposename': { + 'Meta': {'ordering': "['order']", 'object_name': 'LiaisonStatementPurposeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.meetingtypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'MeetingTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.rolename': { + 'Meta': {'ordering': "['order']", 'object_name': 'RoleName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.sessionstatusname': { + 'Meta': {'ordering': "['order']", 'object_name': 'SessionStatusName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.stdlevelname': { + 'Meta': {'ordering': "['order']", 'object_name': 'StdLevelName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.streamname': { + 'Meta': {'ordering': "['order']", 'object_name': 'StreamName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.timeslottypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'TimeSlotTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + } + } + + complete_apps = ['name'] diff --git a/ietf/name/migrations/0004_add_groupmilestonestatenames.py b/ietf/name/migrations/0004_add_groupmilestonestatenames.py new file mode 100644 index 000000000..323f20f7a --- /dev/null +++ b/ietf/name/migrations/0004_add_groupmilestonestatenames.py @@ -0,0 +1,170 @@ +# 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): + # add names + orm.GroupMilestoneStateName.objects.get_or_create(slug="active", + name="Active", + order=1) + orm.GroupMilestoneStateName.objects.get_or_create(slug="deleted", + name="Deleted", + order=2) + orm.GroupMilestoneStateName.objects.get_or_create(slug="review", + name="For review", + order=3) + orm.GroupMilestoneStateName.objects.get_or_create(slug="charter", + name="Chartering/rechartering", + order=4) + + + def backwards(self, orm): + # remove names + orm.GroupMilestoneStateName.objects.filter(slug__in=("active", "deleted", "review", "charter")).delete() + + + models = { + 'name.ballotpositionname': { + 'Meta': {'ordering': "['order']", 'object_name': 'BallotPositionName'}, + 'blocking': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.constraintname': { + 'Meta': {'ordering': "['order']", 'object_name': 'ConstraintName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.docrelationshipname': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocRelationshipName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.docremindertypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocReminderTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.doctagname': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocTagName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.doctypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'DocTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.groupmilestonestatename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupMilestoneStateName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.groupstatename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupStateName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.grouptypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'GroupTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.intendedstdlevelname': { + 'Meta': {'ordering': "['order']", 'object_name': 'IntendedStdLevelName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.liaisonstatementpurposename': { + 'Meta': {'ordering': "['order']", 'object_name': 'LiaisonStatementPurposeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.meetingtypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'MeetingTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.rolename': { + 'Meta': {'ordering': "['order']", 'object_name': 'RoleName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.sessionstatusname': { + 'Meta': {'ordering': "['order']", 'object_name': 'SessionStatusName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.stdlevelname': { + 'Meta': {'ordering': "['order']", 'object_name': 'StdLevelName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.streamname': { + 'Meta': {'ordering': "['order']", 'object_name': 'StreamName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + }, + 'name.timeslottypename': { + 'Meta': {'ordering': "['order']", 'object_name': 'TimeSlotTypeName'}, + 'desc': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'slug': ('django.db.models.fields.CharField', [], {'max_length': '8', 'primary_key': 'True'}), + 'used': ('django.db.models.fields.BooleanField', [], {'default': 'True'}) + } + } + + complete_apps = ['name'] diff --git a/ietf/name/models.py b/ietf/name/models.py index 672b57004..2c8b15e3d 100644 --- a/ietf/name/models.py +++ b/ietf/name/models.py @@ -20,6 +20,8 @@ class GroupStateName(NameModel): """BOF, Proposed, Active, Dormant, Concluded""" class GroupTypeName(NameModel): """IETF, Area, WG, RG, Team, etc.""" +class GroupMilestoneStateName(NameModel): + """Active, Deleted, For Review, Chartering""" class RoleName(NameModel): """AD, Chair""" class StreamName(NameModel): @@ -44,8 +46,6 @@ class DocReminderTypeName(NameModel): class BallotPositionName(NameModel): """ Yes, No Objection, Abstain, Discuss, Block, Recuse """ blocking = models.BooleanField(default=False) -class GroupBallotPositionName(NameModel): - """ Yes, No, Block, Abstain """ class MeetingTypeName(NameModel): """IETF, Interim""" class SessionStatusName(NameModel): diff --git a/ietf/person/forms.py b/ietf/person/forms.py index 039d0cb1b..22bc2d0ea 100644 --- a/ietf/person/forms.py +++ b/ietf/person/forms.py @@ -10,22 +10,30 @@ def json_emails(emails): return simplejson.dumps([{"id": e.address + "", "name": escape(u"%s <%s>" % (e.person.name, e.address))} for e in emails]) class EmailsField(forms.CharField): + """Multi-select field using jquery.tokeninput.js. Since the API of + tokeninput" is asymmetric, we have to pass it a JSON + representation on the way out and parse the ids coming back as a + comma-separated list on the way in.""" + def __init__(self, *args, **kwargs): kwargs["max_length"] = 1000 if not "help_text" in kwargs: kwargs["help_text"] = "Type in name to search for person" super(EmailsField, self).__init__(*args, **kwargs) - self.widget.attrs["class"] = "emails-field" + self.widget.attrs["class"] = "tokenized-field" self.widget.attrs["data-ajax-url"] = lazy(urlreverse, str)("ajax_search_emails") # make this lazy to prevent initialization problem + def parse_tokenized_value(self, value): + return Email.objects.filter(address__in=[x.strip() for x in value.split(",") if x.strip()]).select_related("person") + def prepare_value(self, value): if not value: return "" - if isinstance(value, str): - return value + if isinstance(value, str) or isinstance(value, unicode): + value = self.parse_tokenized_value(value) return json_emails(value) def clean(self, value): value = super(EmailsField, self).clean(value) - return Email.objects.filter(address__in=[x.strip() for x in value.split(",") if x.strip()]).select_related("person") + return self.parse_tokenized_value(value) diff --git a/ietf/templates/idrfc/document_charter.html b/ietf/templates/idrfc/document_charter.html index ce208f79a..00a8e82b9 100644 --- a/ietf/templates/idrfc/document_charter.html +++ b/ietf/templates/idrfc/document_charter.html @@ -44,7 +44,7 @@ {% if not snapshot and user|has_role:"Area Director,Secretariat" %} - {% if chartering %} + {% if chartering %} - Abandon effort {% if request.user|has_role:"Secretariat" %} @@ -110,11 +110,25 @@ {% endif %} -{% if doc.rev %} +{% if doc.rev != "" %}
{{ content|fill:"80"|safe|linebreaksbr|keep_spacing|sanitize_html|safe }}
{% endif %} +{% if not snapshot and chartering %} +

Proposed Milestones +{% if user|has_role:"Area Director,Secretariat" %} +Edit charter milestones +{% endif %} +

+ +{% if milestones %} +{% include "wginfo/milestones.html" %} +{% else %} +

No milestones for charter found.

+{% endif %} +{% endif %} + {% endblock %} diff --git a/ietf/templates/wginfo/edit.html b/ietf/templates/wginfo/edit.html index 82fea836f..1c21b32c4 100644 --- a/ietf/templates/wginfo/edit.html +++ b/ietf/templates/wginfo/edit.html @@ -82,5 +82,5 @@ Create WG {% block content_end %} - + {% endblock %} diff --git a/ietf/templates/wginfo/edit_milestones.html b/ietf/templates/wginfo/edit_milestones.html new file mode 100644 index 000000000..8667e39ed --- /dev/null +++ b/ietf/templates/wginfo/edit_milestones.html @@ -0,0 +1,91 @@ +{% extends "base.html" %} + +{% block title %}{{ title }}{% endblock %} + +{% block morecss %} +tr.milestone td { padding: 0.2em 0; cursor: pointer; vertical-align: top; } +tr.milestone:hover { background-color: #e8f0fa; } +td.due { width: 5em; } +.milestone .needs-accept { font-style: italic; display: inline-block; margin-left: 0.5em; color: #2647a0; } +.edit-milestone { display: none; } +.edit-milestone.delete, .edit-milestone.delete input { color: #aaa !important; } +.edit-milestone table { margin: 0.3em 0; } +.edit-milestone table td { padding: 0.1em; } +.edit-milestone .desc input { width: 50em; } +.edit-milestone .due input { width: 6em; } +.edit-milestone input[type=checkbox] { vertical-align: middle; margin: 0 0.2em 0 0.8em;} +.edit-milestone .resolved label { vertical-align: middle; } +.edit-milestone .delete label { vertical-align: middle; } +.edit-milestone .accept label { vertical-align: middle; } +.edit-milestone .docs td { vertical-align: top; } + +ul.errorlist { border-width: 0px; padding: 0px; margin: 0px; display: inline-block; } +ul.errorlist li { color: #a00; margin: 0px; padding: 0px; list-style: none; } +p.help { font-style: italic; } +p.error { color: #a00; font-size: larger; } +tr.milestone.add { font-style: italic; } +{% endblock %} + +{% block pagehead %} + +{% endblock %} + +{% block content %} +{% load ietf_filters %} +

{{ title }}

+ +

{% if forms %}Click a milestone to edit it.{% endif %} + +{% if needs_review %} +Note that as {{ group.type.name }} Chair you cannot edit descriptions of existing +milestones and milestones you add are subject to review by the Area +Director. +{% endif %} +

+ +{% if can_reset %} +

+You can reset +this list to the currently in-use milestones for the {{ group.acronym }} {{ group.type.name }}. +

+{% endif %} + +{% if form_errors %} +

There were errors, see below.

+{% endif %} + +
+ +{% for form in forms %} + + + + + + +{% endfor %} + + +
{% if form.milestone.resolved %}{{ form.milestone.resolved }}{% else %}{{ form.milestone.due|date:"M Y" }}{% endif %} + {{ form.milestone.desc }} + {% if form.needs_review %}awaiting accept{% endif %} +
{% include "wginfo/milestone_form.html" %}
Add {% if milestone_set == "chartering" %}charter{% endif%} milestone {% if needs_review %}for AD review{% endif %}
{% with empty_form as form %}{% include "wginfo/milestone_form.html" %}{% endwith %}
+ +
+ Back + +
+ +
+{% endblock %} + +{% block content_end %} + + + + + + +{% endblock %} diff --git a/ietf/templates/wginfo/milestone_form.html b/ietf/templates/wginfo/milestone_form.html new file mode 100644 index 000000000..f08c898c5 --- /dev/null +++ b/ietf/templates/wginfo/milestone_form.html @@ -0,0 +1,45 @@ +{# assumes group, form, needs_review are in the context #} + +{{ form.id }} +{{ form.expanded_for_editing }} + + + + + + + {% if form.desc.errors %}{% endif %} + + + + + + + + + {% if form.needs_review %} + + + + + {% endif %} +
{{ form.desc.label_tag }}: + + {% if needs_review and form.milestone and form.milestone.state_id != "review" %} + {{ form.milestone.desc }} {{ form.desc.as_hidden }} + {% else %} + {{ form.desc }} + {% endif %} + + {{ form.delete }} {{ form.delete.label_tag }} +
{{ form.desc.errors }}
{{ form.due.label_tag }}:{{ form.due }} {{ form.due.errors }} + {{ form.resolved_checkbox }} {{ form.resolved_checkbox.label_tag }} {{ form.resolved }} + {{ form.resolved.errors }} +
Drafts: + {{ form.docs.errors }} +
Review:This milestone is not public yet, awaiting AD acceptance. + + {% if not needs_review %} + {{ form.accept }} {{ form.accept.label_tag }} + {% endif %} +
diff --git a/ietf/templates/wginfo/milestones.html b/ietf/templates/wginfo/milestones.html new file mode 100644 index 000000000..8ff043f9d --- /dev/null +++ b/ietf/templates/wginfo/milestones.html @@ -0,0 +1,12 @@ +{# assumes milestones is in context #} + + +{% for milestone in milestones %} + + + + +{% endfor %} +
+ {% if milestone.resolved %}{{ milestone.resolved }}{% else %}{{ milestone.due|date:"M Y" }}{% endif %} + {{ milestone.desc|escape }}
diff --git a/ietf/templates/wginfo/reset_charter_milestones.html b/ietf/templates/wginfo/reset_charter_milestones.html new file mode 100644 index 000000000..eb7b08212 --- /dev/null +++ b/ietf/templates/wginfo/reset_charter_milestones.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} + +{% block title %}Reset Charter Milestones for {{ group.acronym }} {{ group.type.name }}{% endblock %} + +{% block morecss %} +#reset-form .date { display: inline-block; min-width: 5em; } +{% endblock %} + +{% block content %} +{% load ietf_filters %} +

Reset Charter Milestones for {{ group.acronym }} {{ group.type.name }}

+ +

Select which of the current {{ group.type.name }} milestones you would like to copy to the charter. + +{% if charter_milestones %}This will discard {{ charter_milestones|length }} existing charter milestone{{ charter_milestones|pluralize }}{% endif %} +

+ +
+{% for milestone in current_milestones %} +
+ +
+{% endfor %} + +
+ Back + +
+
+{% endblock %} diff --git a/ietf/templates/wginfo/wg-charterREDESIGN.txt b/ietf/templates/wginfo/wg-charterREDESIGN.txt index ef99aa2ef..a46bb0b08 100644 --- a/ietf/templates/wginfo/wg-charterREDESIGN.txt +++ b/ietf/templates/wginfo/wg-charterREDESIGN.txt @@ -34,7 +34,7 @@ Description of Working Group: {{ wg.charter_text|indent|safe }} Goals and Milestones: -{% for milestone in wg.milestones %} {% if milestone.done %}Done {% else %}{{ milestone.expected_due_date|date:"M Y" }}{% endif %} - {{ milestone.desc|safe }} +{% for milestone in wg.milestones %} {% if milestone.resolved %}{{ milestone.resolved }} {% else %}{{ milestone.due|date:"M Y" }}{% endif %} - {{ milestone.desc|safe }} {% endfor %} Internet-Drafts: {% for alias in wg.drafts %} - {{alias.document.title|safe}} [{{alias.name}}-{{alias.document.rev}}] ({{ alias.document.pages }} pages) diff --git a/ietf/templates/wginfo/wg_charter.html b/ietf/templates/wginfo/wg_charter.html index c4aa2fcaa..dff518d48 100644 --- a/ietf/templates/wginfo/wg_charter.html +++ b/ietf/templates/wginfo/wg_charter.html @@ -35,87 +35,122 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. {% load ietf_filters %} {% block wg_titledetail %}Charter{% endblock %} -{% block wg_content %} +{% block morecss %} +{{ block.super }} +h2 a.edit { font-weight: normal; font-size: 13px; display: inline-block; margin-left: 0.5em; } +{% endblock %} +{% block wg_content %}
{% if concluded %} Note: The data for concluded WGs is occasionally incorrect. {% endif %} - - - + - - - - - - -{% if wg.wgtechadvisor_set.count %} - - - -{% endif %} -{% if wg.wgeditor_set.count %} - - -{% endif %} -{% if wg.secretaries %} - - -{% endif %} + + + + - - - + - - - + {% if wg.parent %} + + {% endif %} -{% if not concluded %} - - - + - - -{% endif %} + + + + + + + + + + + + + + + + {% if wg.techadvisors %} + + + + + {% endif %} + {% if wg.editors %} + + + {% endif %} + {% if wg.secretaries %} + + + + + {% endif %} + + + + + + + + {% if not concluded %} + + + + + {% endif %}
-Personnel -
Group
Chair{{ wg.chairs.count|pluralize:",s" }}: -{% for chair in wg.chairs %} -{{ chair.person|escape }} <{{ chair.person.email.1 }}>
-{% endfor %} -
Area Director: -{% ifequal wg.area_director.person.email.1 "noreply@ietf.org" %}?{%else%} -{{ wg.area_director.person }} <{{wg.area_director.person.email.1 }}>{% endifequal %} -
Tech Advisor{{ wg.wgtechadvisor_set.count|pluralize:",s" }}: -{% for techadvisor in wg.wgtechadvisor_set.all %} -{{ techadvisor.person }} <{{ techadvisor.person.email.1 }}>
-{% endfor %} -
Editor{{ wg.wgeditor_set.count|pluralize:",s" }}: -{% for editor in wg.wgeditor_set.all %} -{{ editor.person }} <{{ editor.person.email.1 }}>
-{% endfor %} -
Secretar{{ wg.secretaries.count|pluralize:"y,ies" }}: -{% for secretary in wg.secretaries %} -{{ secretary.person }} <{{ secretary.person.email.1 }}>
-{% endfor %} -
Name:{{ wg.name }}
-
Mailing List -
Acronym:{{ wg.acronym }}
Address:{{ wg.email_address|urlize }}
To Subscribe:{{ wg.email_subscribe|urlize }}
Archive:{{ wg.clean_email_archive|urlize }}
Area:{{ wg.parent.name }} ({{ wg.parent.acronym }})
-
Jabber Chat -
State:{{ wg.state.name }}
Room Address:xmpp:{{wg}}@jabber.ietf.org
Logs:http://jabber.ietf.org/logs/{{wg}}/
Charter: + {% if wg.charter %} + {{ wg.charter.name }}-{{ wg.charter.rev }} ({{ wg.charter.get_state.name }}) + {% else %} + none + {% if user|has_role:"Area Director,Secretariat" %} + - Submit Charter + {% endif %} + {% endif %} +
Personnel
Chair{{ wg.chairs|pluralize }}: + {% for chair in wg.chairs %} + {{ chair.person.plain_name }} <{{ chair.address }}>
+ {% endfor %} +
Area Director: + {% if not wg.ad %}?{% else %} + {{ wg.ad.plain_name }} <{{ wg.areadirector.address }}>{% endif %} +
Tech Advisor{{ wg.techadvisors|pluralize }}: + {% for techadvisor in wg.techadvisors %} + {{ techadvisor.person.plain_name }} <{{ techadvisor.address }}>
+ {% endfor %} +
Editor{{ wg.editors|pluralize }}: + {% for editor in wg.editors %} + {{ editor.person.plain_name }} <{{ editor.address }}>
+ {% endfor %} +
Secretar{{ wg.secretaries|pluralize:"y,ies" }}: + {% for secretary in wg.secretaries %} + {{ secretary.person.plain_name }} <{{ secretary.address }}>
+ {% endfor %} +
Mailing List
Address:{{ wg.email_address|urlize }}
To Subscribe:{{ wg.email_subscribe|urlize }}
Archive:{{ wg.clean_email_archive|urlize }}
Jabber Chat
Room Address:xmpp:{{ wg.acronym }}@jabber.ietf.org
Logs:http://jabber.ietf.org/logs/{{ wg.acronym }}/
+ +{% if user|has_role:"Area Director,Secretariat" %} +
+ {% for name, url in actions %} + {{ name }} + {% if not forloop.last %}|{% endif %} + {% endfor %} +
+{% endif %}
{% if wg.additional_urls %}

In addition to the charter maintained by the IETF Secretariat, there is additional information about this working group on the Web at: {% for url in wg.additional_urls %} -{{ url.description}}{% if not forloop.last %}, {% endif %} +{{ url.name }}{% if not forloop.last %}, {% endif %} {% endfor %}

{% endif %} @@ -123,31 +158,16 @@ is occasionally incorrect.

Description of Working Group

{{ wg.charter_text|escape|format_charter|safe }}

-

Goals and Milestones

- -{% for milestone in wg.milestones %} - - - -{% endfor %} -
- {% ifequal milestone.done 'Done' %} Done - {% else %} - {%ifequal milestone.expected_due_date.month 1 %}Jan{% endifequal %} - {%ifequal milestone.expected_due_date.month 2 %}Feb{% endifequal %} - {%ifequal milestone.expected_due_date.month 3 %}Mar{% endifequal %} - {%ifequal milestone.expected_due_date.month 4 %}Apr{% endifequal %} - {%ifequal milestone.expected_due_date.month 5 %}May{% endifequal %} - {%ifequal milestone.expected_due_date.month 6 %}Jun{% endifequal %} - {%ifequal milestone.expected_due_date.month 7 %}Jul{% endifequal %} - {%ifequal milestone.expected_due_date.month 8 %}Aug{% endifequal %} - {%ifequal milestone.expected_due_date.month 9 %}Sep{% endifequal %} - {%ifequal milestone.expected_due_date.month 10 %}Oct{% endifequal %} - {%ifequal milestone.expected_due_date.month 11 %}Nov{% endifequal %} - {%ifequal milestone.expected_due_date.month 12 %}Dec{% endifequal %} - {{ milestone.expected_due_date.year }} - {% endifequal %} - {{ milestone.description|escape }} -
-{% endblock wg_content %} +

Goals and Milestones +{% if user|has_role:"Area Director,Secretariat" or is_chair %} +Add or edit milestones +{% endif %} +

+{% with wg.milestones as milestones %}{% include "wginfo/milestones.html" %}{% endwith %} + +{% if milestones_in_review %} +

{{ milestones_in_review|length }} new milestone{{ milestones_in_review|pluralize }} +under review.

+{% endif %} +{% endblock wg_content %} diff --git a/ietf/templates/wginfo/wg_charterREDESIGN.html b/ietf/templates/wginfo/wg_charterREDESIGN.html deleted file mode 100644 index 60f87e077..000000000 --- a/ietf/templates/wginfo/wg_charterREDESIGN.html +++ /dev/null @@ -1,167 +0,0 @@ -{% extends "wginfo/wg_base.html" %} -{% comment %} -Copyright (C) 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. -{% endcomment %} -{% load ietf_filters %} -{% block wg_titledetail %}Charter{% endblock %} - -{% block wg_content %} -
-{% if concluded %} -Note: The data for concluded WGs -is occasionally incorrect. -{% endif %} - - - - - - - - - - - {% if wg.parent %} - - {% endif %} - - - - - - - - - - - - - - - - - - - {% if wg.techadvisors %} - - - - - {% endif %} - {% if wg.editors %} - - - {% endif %} - {% if wg.secretaries %} - - - - - {% endif %} - - - - - - - - {% if not concluded %} - - - - - {% endif %} - -
Group
Name:{{ wg.name }}
Acronym:{{ wg.acronym }}
Area:{{ wg.parent.name }} ({{ wg.parent.acronym }})
State:{{ wg.state.name }}
Charter: - {% if wg.charter %} - {{ wg.charter.name }}-{{ wg.charter.rev }} ({{ wg.charter.get_state.name }}) - {% else %} - none - {% if user|has_role:"Area Director,Secretariat" %} - - Submit Charter - {% endif %} - {% endif %} -
Personnel
Chair{{ wg.chairs|pluralize }}: - {% for chair in wg.chairs %} - {{ chair.person.plain_name }} <{{ chair.address }}>
- {% endfor %} -
Area Director: - {% if not wg.ad %}?{% else %} - {{ wg.ad.plain_name }} <{{ wg.areadirector.address }}>{% endif %} -
Tech Advisor{{ wg.techadvisors|pluralize }}: - {% for techadvisor in wg.techadvisors %} - {{ techadvisor.person.plain_name }} <{{ techadvisor.address }}>
- {% endfor %} -
Editor{{ wg.editors|pluralize }}: - {% for editor in wg.editors %} - {{ editor.person.plain_name }} <{{ editor.address }}>
- {% endfor %} -
Secretar{{ wg.secretaries|pluralize:"y,ies" }}: - {% for secretary in wg.secretaries %} - {{ secretary.person.plain_name }} <{{ secretary.address }}>
- {% endfor %} -
Mailing List
Address:{{ wg.email_address|urlize }}
To Subscribe:{{ wg.email_subscribe|urlize }}
Archive:{{ wg.clean_email_archive|urlize }}
Jabber Chat
Room Address:xmpp:{{ wg.acronym }}@jabber.ietf.org
Logs:http://jabber.ietf.org/logs/{{ wg.acronym }}/
- -{% if user|has_role:"Area Director,Secretariat" %} -
- {% for name, url in actions %} - {{ name }} - {% if not forloop.last %}|{% endif %} - {% endfor %} -
-{% endif %} -
- -{% if wg.additional_urls %} -

In addition to the charter maintained by the IETF Secretariat, there is additional information about this working group on the Web at: -{% for url in wg.additional_urls %} -{{ url.name }}{% if not forloop.last %}, {% endif %} -{% endfor %} -

-{% endif %} - -

Description of Working Group

-

{{ wg.charter_text|escape|format_charter|safe }}

- -

Goals and Milestones

- -{% for milestone in wg.milestones %} - - - -{% endfor %} -
- {% if milestone.done %}Done{% else %}{{ milestone.expected_due_date|date:"M Y" }}{% endif %} - {{ milestone.desc|escape }} -
-{% endblock wg_content %} diff --git a/ietf/utils/history.py b/ietf/utils/history.py index e0b790044..09e1adaa4 100644 --- a/ietf/utils/history.py +++ b/ietf/utils/history.py @@ -8,7 +8,7 @@ def find_history_active_at(obj, time): related_name="history_set" for the foreign key connecting to the live model, both models must have a "time" DateTimeField and a history object must be saved with a copy of the old values and - time when the time field changes. + old time when the time field changes. """ if obj.time <= time: return None @@ -20,3 +20,40 @@ def find_history_active_at(obj, time): return h return None + +def get_history_object_for(obj): + """Construct history object for obj, i.e. instantiate history + object, copy relevant attributes and set a link to obj, but done + save. Any customizations can be done by the caller afterwards. + Many-to-many fields are not copied. + + The history model must use related_name="history_set" for the + foreign key connecting to the live model for this function to be + able to discover it.""" + + history_model = obj.history_set.model + h = history_model() + + # copy attributes shared between history and obj + history_field_names = set(f.name for f in history_model._meta.fields) + + for field in obj._meta.fields: + if field is not obj._meta.pk and field.name in history_field_names: + setattr(h, field.name, getattr(obj, field.name)) + + # try setting foreign key to obj + key_name = obj._meta.object_name.lower() + if key_name in history_field_names: + setattr(h, key_name, obj) + + # we can't copy many-to-many fields as h isn't saved yet, leave + # that to caller + + return h + +def copy_many_to_many_for_history(history_obj, obj): + """Copy basic many-to-many fields from obj to history_obj.""" + # copy many to many + for field in obj._meta.many_to_many: + if field.rel.through and field.rel.through._meta.auto_created: + setattr(history_obj, field.name, getattr(obj, field.name).all()) diff --git a/ietf/wginfo/milestones.py b/ietf/wginfo/milestones.py new file mode 100644 index 000000000..a95712429 --- /dev/null +++ b/ietf/wginfo/milestones.py @@ -0,0 +1,341 @@ +# WG milestone editing views + +import re, os, string, datetime, shutil + +from django.shortcuts import render_to_response, get_object_or_404, redirect +from django.core.urlresolvers import reverse +from django.template import RequestContext +from django import forms +from django.http import HttpResponse, HttpResponseForbidden, HttpResponseBadRequest +from django.utils import simplejson +from django.utils.html import mark_safe, escape +from django.utils.functional import lazy +from django.core.urlresolvers import reverse as urlreverse + +from ietf.ietfauth.decorators import role_required, has_role +from ietf.doc.models import Document, DocEvent +from ietf.doc.utils import get_chartering_type +from ietf.group.models import * +from ietf.group.utils import save_group_in_history, save_milestone_in_history + +def json_doc_names(docs): + return simplejson.dumps([{"id": doc.pk, "name": doc.name } for doc in docs]) + +def parse_doc_names(s): + return Document.objects.filter(pk__in=[x.strip() for x in s.split(",") if x.strip()], type="draft") + +class MilestoneForm(forms.Form): + id = forms.IntegerField(required=True, widget=forms.HiddenInput) + + desc = forms.CharField(max_length=500, label="Milestone", required=True) + due = forms.DateField(required=True, label="Due date") + resolved_checkbox = forms.BooleanField(required=False, label="Resolved") + resolved = forms.CharField(max_length=50, required=False) + + delete = forms.BooleanField(required=False, initial=False) + + docs = forms.CharField(max_length=10000, required=False) + + accept = forms.BooleanField(required=False, initial=False) + + expanded_for_editing = forms.BooleanField(required=False, initial=False, widget=forms.HiddenInput) + + def __init__(self, *args, **kwargs): + m = self.milestone = kwargs.pop("instance", None) + + self.needs_review = kwargs.pop("needs_review", False) + + if m: + if not "initial" in kwargs: + kwargs["initial"] = {} + kwargs["initial"].update(dict(id=m.pk, + desc=m.desc, + due=m.due, + resolved_checkbox=bool(m.resolved), + resolved=m.resolved, + docs=",".join(m.docs.values_list("pk", flat=True)), + )) + + kwargs["prefix"] = "m%s" % m.pk + + self.needs_review = m.state_id == "review" + + super(MilestoneForm, self).__init__(*args, **kwargs) + + pre = "" + if not self.is_bound: + pre = self.initial.get("docs", "") + else: + pre = self["docs"].data or "" + + self.fields["docs"].prepopulate = json_doc_names(parse_doc_names(pre)) + + def clean_docs(self): + s = self.cleaned_data["docs"] + return Document.objects.filter(pk__in=[x.strip() for x in s.split(",") if x.strip()], type="draft") + + def clean_resolved(self): + r = self.cleaned_data["resolved"].strip() + + if self.cleaned_data["resolved_checkbox"]: + if not r: + raise forms.ValidationError('Please provide explanation (like "Done") for why the milestone is no longer due.') + else: + r = "" + + return r + + +@role_required('WG Chair', 'Area Director', 'Secretariat') +def edit_milestones(request, acronym, milestone_set="current"): + # milestones_set + needs_review: we have several paths into this view + # AD/Secr. -> all actions on current + add new + # group chair -> limited actions on current + add new for review + # (re)charter -> all actions on existing in state charter + add new in state charter + # + # For charters we store the history on the charter document to not confuse people. + + login = request.user.get_profile() + + group = get_object_or_404(Group, acronym=acronym) + + needs_review = False + if not has_role(request.user, ("Area Director", "Secretariat")): + if group.role_set.filter(name="chair", person=login): + if milestone_set == "current": + needs_review = True + else: + return HttpResponseForbidden("You are not chair of this group.") + + if milestone_set == "current": + title = "Edit milestones for %s %s" % (group.acronym, group.type.name) + milestones = group.groupmilestone_set.filter(state__in=("active", "review")) + elif milestone_set == "charter": + title = "Edit charter milestones for %s %s" % (group.acronym, group.type.name) + milestones = group.groupmilestone_set.filter(state="charter") + + forms = [] + + milestones_dict = dict((str(m.id), m) for m in milestones) + + def add_event(m, desc): + if milestone_set == "charter": + DocEvent.objects.create(doc=group.charter, type="changed_charter_milestone", + by=login, desc=desc) + else: + MilestoneGroupEvent.objects.create(group=group, type="changed_milestone", + by=login, desc=desc, milestone=m) + + finished_milestone_text = "Done" + + form_errors = False + + if request.method == 'POST': + for prefix in request.POST.getlist("prefix"): + if not prefix: # empty form + continue + + # new milestones have non-existing ids so instance end up as None + instance = milestones_dict.get(request.POST.get(prefix + "-id", ""), None) + f = MilestoneForm(request.POST, prefix=prefix, instance=instance, + needs_review=needs_review) + forms.append(f) + + form_errors = form_errors or not f.is_valid() + + if not form_errors: + for f in forms: + c = f.cleaned_data + + if f.milestone: + m = f.milestone + + named_milestone = 'milestone "%s"' % m.desc + if milestone_set == "charter": + named_milestone = "charter " + named_milestone + + if c["delete"]: + save_milestone_in_history(m) + + m.time = datetime.datetime.now() + m.state_id = "deleted" + m.save() + + add_event(m, 'Deleted %s' % named_milestone) + + continue + + # compute changes + history = None + + changes = ['Changed %s' % named_milestone] + + if m.state_id == "review" and not needs_review and c["accept"]: + if not history: + history = save_milestone_in_history(m) + m.state_id = "active" + changes.append("changed state from review to active") + + + if c["desc"] != m.desc and not needs_review: + if not history: + history = save_milestone_in_history(m) + m.desc = c["desc"] + changes.append('changed description to "%s"' % m.desc) + + if c["due"] != m.due: + if not history: + history = save_milestone_in_history(m) + m.due = c["due"] + changes.append('changed due date to %s' % m.due.strftime("%Y-%m-%d")) + + resolved = c["resolved"] + if resolved != m.resolved: + if resolved and not m.resolved: + changes.append('resolved as "%s"' % resolved) + elif not resolved and m.resolved: + changes.append("reverted to not being resolved") + elif resolved and m.resolved: + changes.append('changed resolution to "%s"' % resolved) + + if not history: + history = save_milestone_in_history(m) + + m.resolved = resolved + + new_docs = set(c["docs"]) + old_docs = set(m.docs.all()) + if new_docs != old_docs: + added = new_docs - old_docs + if added: + changes.append('added %s to milestone' % ", ".join(d.name for d in added)) + + removed = old_docs - new_docs + if removed: + changes.append('removed %s from milestone' % ", ".join(d.name for d in removed)) + + if not history: + history = save_milestone_in_history(m) + + m.docs = new_docs + + if len(changes) > 1: + add_event(m, ", ".join(changes)) + + m.save() + + else: # new milestone + m = GroupMilestone() + m.group = group + if milestone_set == "current": + if needs_review: + m.state = GroupMilestoneStateName.objects.get(slug="review") + else: + m.state = GroupMilestoneStateName.objects.get(slug="active") + elif milestone_set == "charter": + m.state = GroupMilestoneStateName.objects.get(slug="charter") + m.desc = c["desc"] + m.due = c["due"] + m.resolved = c["resolved"] + m.save() + + m.docs = c["docs"] + + named_milestone = 'milestone "%s"' % m.desc + if milestone_set == "charter": + named_milestone = "charter " + named_milestone + + if m.state_id in ("active", "charter"): + add_event(m, 'Added %s, due %s' % (named_milestone, m.due.strftime("%Y-%m-%d"))) + elif m.state_id == "review": + add_event(m, 'Added %s for review, due %s' % (named_milestone, m.due.strftime("%Y-%m-%d"))) + + if milestone_set == "charter": + return redirect('doc_view', name=group.charter.canonical_name()) + else: + return redirect('wg_charter', acronym=group.acronym) + else: + for m in milestones: + forms.append(MilestoneForm(instance=m, needs_review=needs_review)) + + can_reset = milestone_set == "charter" and get_chartering_type(group.charter) == "rechartering" + + empty_form = MilestoneForm(needs_review=needs_review) + + return render_to_response('wginfo/edit_milestones.html', + dict(group=group, + title=title, + forms=forms, + form_errors=form_errors, + empty_form=empty_form, + milestone_set=milestone_set, + finished_milestone_text=finished_milestone_text, + needs_review=needs_review, + can_reset=can_reset), + context_instance=RequestContext(request)) + +@role_required('WG Chair', 'Area Director', 'Secretariat') +def reset_charter_milestones(request, acronym): + """Reset charter milestones to the currently in-use milestones.""" + login = request.user.get_profile() + + group = get_object_or_404(Group, acronym=acronym) + + if (not has_role(request.user, ("Area Director", "Secretariat")) and + not group.role_set.filter(name="chair", person=login)): + return HttpResponseForbidden("You are not chair of this group.") + + current_milestones = group.groupmilestone_set.filter(state="active") + charter_milestones = group.groupmilestone_set.filter(state="charter") + + if request.method == 'POST': + try: + milestone_ids = [int(v) for v in request.POST.getlist("milestone")] + except ValueError as e: + return HttpResponseBadRequest("errror in list of ids - %s" % e) + + # delete existing + for m in charter_milestones: + save_milestone_in_history(m) + + m.time = datetime.datetime.now() + m.state_id = "deleted" + m.save() + + DocEvent.objects.create(type="changed_charter_milestone", + doc=group.charter, + desc='Deleted milestone "%s"' % m.desc, + by=login, + ) + + # add current + for m in current_milestones.filter(id__in=milestone_ids): + m = GroupMilestone.objects.create(group=m.group, + state_id="charter", + desc=m.desc, + due=m.due, + resolved=m.resolved, + time=datetime.datetime.now(), + ) + m.docs = m.docs.all() + + DocEvent.objects.create(type="changed_charter_milestone", + doc=group.charter, + desc='Added milestone "%s", due %s, from current group milestones' % (m.desc, m.due.strftime("%Y-%m-%d")), + by=login, + ) + + + return redirect('wg_edit_charter_milestones', acronym=group.acronym) + + return render_to_response('wginfo/reset_charter_milestones.html', + dict(group=group, + charter_milestones=charter_milestones, + current_milestones=current_milestones, + ), + context_instance=RequestContext(request)) + + +def ajax_search_docs(request, acronym): + docs = Document.objects.filter(name__icontains=request.GET.get('q',''), group__acronym=acronym, type="draft").order_by('name').distinct()[:20] + return HttpResponse(json_doc_names(docs), mimetype='application/json') diff --git a/ietf/wginfo/tests.py b/ietf/wginfo/tests.py index 51e34d0b2..88357705a 100644 --- a/ietf/wginfo/tests.py +++ b/ietf/wginfo/tests.py @@ -217,3 +217,252 @@ class WgEditTestCase(django.test.TestCase): # the WG remains active until the Secretariat takes action group = Group.objects.get(acronym=group.acronym) self.assertEquals(group.state_id, "active") + +class MilestoneTestCase(django.test.TestCase): + fixtures = ["names"] + + def create_test_milestones(self): + draft = make_test_data() + + group = Group.objects.get(acronym="mars") + + m1 = GroupMilestone.objects.create(group=group, + desc="Test 1", + due=datetime.date.today(), + resolved="", + state_id="active") + m1.docs = [draft] + + m2 = GroupMilestone.objects.create(group=group, + desc="Test 2", + due=datetime.date.today(), + resolved="", + state_id="charter") + m2.docs = [draft] + + return (m1, m2, group) + + + def test_milestone_sets(self): + m1, m2, group = self.create_test_milestones() + + url = urlreverse('wg_edit_milestones', kwargs=dict(acronym=group.acronym)) + login_testing_unauthorized(self, "secretary", url) + + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + self.assertTrue(m1.desc in r.content) + self.assertTrue(m2.desc not in r.content) + + url = urlreverse('wg_edit_charter_milestones', kwargs=dict(acronym=group.acronym)) + + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + self.assertTrue(m1.desc not in r.content) + self.assertTrue(m2.desc in r.content) + + def test_add_milestone(self): + m1, m2, group = self.create_test_milestones() + + url = urlreverse('wg_edit_milestones', kwargs=dict(acronym=group.acronym)) + login_testing_unauthorized(self, "secretary", url) + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + + milestones_before = GroupMilestone.objects.count() + events_before = group.groupevent_set.count() + docs = Document.objects.filter(type="draft").values_list("name", flat=True) + + due = datetime.date.today() + datetime.timedelta(days=365) + + # faulty post + r = self.client.post(url, { 'prefix': "m-1", + 'm-1-id': -1, + 'm-1-desc': "", # no description + 'm-1-due': due.strftime("%Y-%m-%d"), + 'm-1-resolved': "", + 'm-1-docs': ",".join(docs), + }) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue(len(q('form ul.errorlist')) > 0) + self.assertEquals(GroupMilestone.objects.count(), milestones_before) + + # add + r = self.client.post(url, { 'prefix': "m-1", + 'm-1-id': -1, + 'm-1-desc': "Test 3", + 'm-1-due': due.strftime("%Y-%m-%d"), + 'm-1-resolved': "", + 'm-1-docs': ",".join(docs), + }) + self.assertEquals(r.status_code, 302) + self.assertEquals(GroupMilestone.objects.count(), milestones_before + 1) + self.assertEquals(group.groupevent_set.count(), events_before + 1) + + m = GroupMilestone.objects.get(desc="Test 3") + self.assertEquals(m.state_id, "active") + self.assertEquals(m.due, due) + self.assertEquals(m.resolved, "") + self.assertEquals(set(m.docs.values_list("name", flat=True)), set(docs)) + self.assertTrue("Added milestone" in m.milestonegroupevent_set.all()[0].desc) + + def test_add_milestone_as_chair(self): + m1, m2, group = self.create_test_milestones() + + url = urlreverse('wg_edit_milestones', kwargs=dict(acronym=group.acronym)) + login_testing_unauthorized(self, "marschairman", url) + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + + milestones_before = GroupMilestone.objects.count() + events_before = group.groupevent_set.count() + due = datetime.date.today() + datetime.timedelta(days=365) + + # add + r = self.client.post(url, { 'prefix': "m-1", + 'm-1-id': -1, + 'm-1-desc': "Test 3", + 'm-1-due': due.strftime("%Y-%m-%d"), + 'm-1-resolved': "", + 'm-1-docs': "", + }) + self.assertEquals(r.status_code, 302) + self.assertEquals(GroupMilestone.objects.count(), milestones_before + 1) + + m = GroupMilestone.objects.get(desc="Test 3") + self.assertEquals(m.state_id, "review") + self.assertEquals(group.groupevent_set.count(), events_before + 1) + self.assertTrue("for review" in m.milestonegroupevent_set.all()[0].desc) + + def test_accept_milestone(self): + m1, m2, group = self.create_test_milestones() + m1.state_id = "review" + m1.save() + + url = urlreverse('wg_edit_milestones', kwargs=dict(acronym=group.acronym)) + login_testing_unauthorized(self, "ad", url) + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + + events_before = group.groupevent_set.count() + due = datetime.date.today() + datetime.timedelta(days=365) + + # add + r = self.client.post(url, { 'prefix': "m1", + 'm1-id': m1.id, + 'm1-desc': m1.desc, + 'm1-due': m1.due.strftime("%Y-%m-%d"), + 'm1-resolved': m1.resolved, + 'm1-docs': ",".join(m1.docs.values_list("name", flat=True)), + 'm1-accept': "checked", + }) + self.assertEquals(r.status_code, 302) + + m = GroupMilestone.objects.get(pk=m1.pk) + self.assertEquals(m.state_id, "active") + self.assertEquals(group.groupevent_set.count(), events_before + 1) + self.assertTrue("from review to active" in m.milestonegroupevent_set.all()[0].desc) + + def test_delete_milestone(self): + m1, m2, group = self.create_test_milestones() + + url = urlreverse('wg_edit_milestones', kwargs=dict(acronym=group.acronym)) + login_testing_unauthorized(self, "secretary", url) + + milestones_before = GroupMilestone.objects.count() + events_before = group.groupevent_set.count() + + # delete + r = self.client.post(url, { 'prefix': "m1", + 'm1-id': m1.id, + 'm1-desc': m1.desc, + 'm1-due': m1.due.strftime("%Y-%m-%d"), + 'm1-resolved': "", + 'm1-docs': ",".join(m1.docs.values_list("name", flat=True)), + 'm1-delete': "checked", + }) + self.assertEquals(r.status_code, 302) + self.assertEquals(GroupMilestone.objects.count(), milestones_before) + self.assertEquals(group.groupevent_set.count(), events_before + 1) + + m = GroupMilestone.objects.get(pk=m1.pk) + self.assertEquals(m.state_id, "deleted") + self.assertTrue("Deleted milestone" in m.milestonegroupevent_set.all()[0].desc) + + def test_edit_milestone(self): + m1, m2, group = self.create_test_milestones() + + url = urlreverse('wg_edit_milestones', kwargs=dict(acronym=group.acronym)) + login_testing_unauthorized(self, "secretary", url) + + milestones_before = GroupMilestone.objects.count() + events_before = group.groupevent_set.count() + docs = Document.objects.filter(type="draft").values_list("name", flat=True) + + due = datetime.date.today() + datetime.timedelta(days=365) + + # faulty post + r = self.client.post(url, { 'prefix': "m1", + 'm1-id': m1.id, + 'm1-desc': "", # no description + 'm1-due': due.strftime("%Y-%m-%d"), + 'm1-resolved': "", + 'm1-docs': ",".join(docs), + }) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue(len(q('form ul.errorlist')) > 0) + m = GroupMilestone.objects.get(pk=m1.pk) + self.assertEquals(GroupMilestone.objects.count(), milestones_before) + self.assertEquals(m.due, m1.due) + + # add + r = self.client.post(url, { 'prefix': "m1", + 'm1-id': m1.id, + 'm1-desc': "Test 2 - changed", + 'm1-due': due.strftime("%Y-%m-%d"), + 'm1-resolved': "Done", + 'm1-resolved_checkbox': "checked", + 'm1-docs': ",".join(docs), + }) + self.assertEquals(r.status_code, 302) + self.assertEquals(GroupMilestone.objects.count(), milestones_before) + self.assertEquals(group.groupevent_set.count(), events_before + 1) + + m = GroupMilestone.objects.get(pk=m1.pk) + self.assertEquals(m.state_id, "active") + self.assertEquals(m.due, due) + self.assertEquals(m.resolved, "Done") + self.assertEquals(set(m.docs.values_list("name", flat=True)), set(docs)) + self.assertTrue("Changed milestone" in m.milestonegroupevent_set.all()[0].desc) + + def test_reset_charter_milestones(self): + m1, m2, group = self.create_test_milestones() + + url = urlreverse('wg_reset_charter_milestones', kwargs=dict(acronym=group.acronym)) + login_testing_unauthorized(self, "secretary", url) + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertEquals(q('input[name=milestone]').val(), str(m1.pk)) + + events_before = group.charter.docevent_set.count() + + # reset + r = self.client.post(url, dict(milestone=[str(m1.pk)])) + self.assertEquals(r.status_code, 302) + + self.assertEquals(GroupMilestone.objects.get(pk=m1.pk).state_id, "active") + self.assertEquals(GroupMilestone.objects.get(pk=m2.pk).state_id, "deleted") + self.assertEquals(GroupMilestone.objects.filter(due=m1.due, desc=m1.desc, state="charter").count(), 1) + + self.assertEquals(group.charter.docevent_set.count(), events_before + 2) # 1 delete, 1 add diff --git a/ietf/wginfo/urls.py b/ietf/wginfo/urls.py index 5979386a9..dc3721a18 100644 --- a/ietf/wginfo/urls.py +++ b/ietf/wginfo/urls.py @@ -1,7 +1,7 @@ # Copyright The IETF Trust 2008, All Rights Reserved from django.conf.urls.defaults import patterns, include -from ietf.wginfo import views, edit +from ietf.wginfo import views, edit, milestones from django.views.generic.simple import redirect_to @@ -19,8 +19,12 @@ urlpatterns = patterns('', (r'^(?P[a-zA-Z0-9-]+)/documents/txt/$', views.wg_documents_txt), (r'^(?P[a-zA-Z0-9-]+)/$', views.wg_documents_html), (r'^(?P[a-zA-Z0-9-]+)/charter/$', views.wg_charter, None, 'wg_charter'), - (r'^(?P[a-zA-Z0-9-]+)/history/', views.history), - (r'^(?P[a-zA-Z0-9-]+)/edit/', edit.edit, {'action': "edit"}, "wg_edit"), - (r'^(?P[a-zA-Z0-9-]+)/conclude/', edit.conclude, None, "wg_conclude"), + (r'^(?P[a-zA-Z0-9-]+)/history/$', views.history), + (r'^(?P[a-zA-Z0-9-]+)/edit/$', edit.edit, {'action': "edit"}, "wg_edit"), + (r'^(?P[a-zA-Z0-9-]+)/conclude/$', edit.conclude, None, "wg_conclude"), + (r'^(?P[a-zA-Z0-9-]+)/milestones/$', milestones.edit_milestones, {'milestone_set': "current"}, "wg_edit_milestones"), + (r'^(?P[a-zA-Z0-9-]+)/milestones/charter/$', milestones.edit_milestones, {'milestone_set': "charter"}, "wg_edit_charter_milestones"), + (r'^(?P[a-zA-Z0-9-]+)/milestones/charter/reset/$', milestones.reset_charter_milestones, None, "wg_reset_charter_milestones"), + (r'^(?P[a-zA-Z0-9-]+)/ajax/searchdocs/$', milestones.ajax_search_docs, None, "wg_ajax_search_docs"), (r'^(?P[^/]+)/management/', include('ietf.wgchairs.urls')), ) diff --git a/ietf/wginfo/views.py b/ietf/wginfo/views.py index a9be75bfd..623b4d179 100644 --- a/ietf/wginfo/views.py +++ b/ietf/wginfo/views.py @@ -55,7 +55,7 @@ def fill_in_charter_info(wg, include_drafts=False): wg.techadvisors = Email.objects.filter(role__group=wg, role__name="techadv") wg.editors = Email.objects.filter(role__group=wg, role__name="editor") wg.secretaries = Email.objects.filter(role__group=wg, role__name="secr") - wg.milestones = wg.groupmilestone_set.all().order_by('expected_due_date') + wg.milestones = wg.groupmilestone_set.filter(state="active").order_by('due') if include_drafts: aliases = DocAlias.objects.filter(document__type="draft", document__group=wg).select_related('document').order_by("name") @@ -191,24 +191,24 @@ def wg_charter(request, acronym): concluded = wg.status_id in [ 2, 3, ] proposed = (wg.status_id == 4) - if settings.USE_DB_REDESIGN_PROXY_CLASSES: - fill_in_charter_info(wg) - actions = [] - if wg.state_id != "conclude": - actions.append(("Edit WG", urlreverse("wg_edit", kwargs=dict(acronym=wg.acronym)))) + fill_in_charter_info(wg) + actions = [] + if wg.state_id != "conclude": + actions.append(("Edit WG", urlreverse("wg_edit", kwargs=dict(acronym=wg.acronym)))) - if wg.state_id == "active" and (not wg.charter or wg.charter.get_state_slug() == "approved"): - actions.append(("Conclude WG", urlreverse("wg_conclude", kwargs=dict(acronym=wg.acronym)))) + if wg.state_id == "active" and (not wg.charter or wg.charter.get_state_slug() == "approved"): + actions.append(("Conclude WG", urlreverse("wg_conclude", kwargs=dict(acronym=wg.acronym)))) - context = get_wg_menu_context(wg, "charter") - context.update(dict( - actions=actions)) + context = get_wg_menu_context(wg, "charter") + context.update(dict( + actions=actions, + is_chair=wg.role_set.filter(name="chair", person__user=request.user), + milestones_in_review=wg.groupmilestone_set.filter(state="review"), + )) - return render_to_response('wginfo/wg_charterREDESIGN.html', - context, - RequestContext(request)) - - return render_to_response('wginfo/wg_charter.html', {'wg': wg, 'concluded':concluded, 'proposed': proposed, 'selected':'charter'}, RequestContext(request)) + return render_to_response('wginfo/wg_charter.html', + context, + RequestContext(request)) def get_wg_menu_context(wg, selected): # it would probably be better to refactor wginfo into rendering diff --git a/static/css/token-input.css b/static/css/token-input.css index 03bb01c4d..39fdc5d4d 100644 --- a/static/css/token-input.css +++ b/static/css/token-input.css @@ -6,8 +6,6 @@ ul.token-input-list { width: 400px; border: 1px solid #999; cursor: text; - font-size: 12px; - font-family: Verdana; z-index: 999; margin: 0; padding: 0; @@ -34,9 +32,8 @@ li.token-input-token { height: 1%; margin: 3px; padding: 3px 5px; - background-color: #d0efa0; + background-color: #e8f0fa; color: #000; - font-weight: bold; cursor: default; display: block; } @@ -54,8 +51,8 @@ li.token-input-token span { } li.token-input-selected-token { - background-color: #08844e; - color: #fff; + background-color: #d5dde6; + color: #000; } li.token-input-selected-token span { @@ -71,15 +68,12 @@ div.token-input-dropdown { border-right: 1px solid #ccc; border-bottom: 1px solid #ccc; cursor: default; - font-size: 12px; - font-family: Verdana; z-index: 1; } div.token-input-dropdown p { margin: 0; padding: 5px; - font-weight: bold; color: #777; } @@ -108,6 +102,6 @@ div.token-input-dropdown ul li em { } div.token-input-dropdown ul li.token-input-selected-dropdown-item { - background-color: #d0efa0; + background-color: #d5dde6; } diff --git a/static/js/edit-milestones.js b/static/js/edit-milestones.js new file mode 100644 index 000000000..6a2867b76 --- /dev/null +++ b/static/js/edit-milestones.js @@ -0,0 +1,100 @@ +jQuery(function () { + var idCounter = -1; + + // make sure we got the lowest number for idCounter + jQuery('#milestones-form .edit-milestone input[name$="-id"]').each(function () { + var v = +this.value; + if (!isNaN(v) && v < idCounter) + idCounter = v - 1; + }); + + + jQuery("tr.milestone").click(function () { + var row = jQuery(this), editRow = row.next("tr.edit-milestone"); + + if (row.hasClass("add")) { + // move Add milestone row and duplicate hidden template + row.closest("table").append(row).append(editRow.clone()); + + // fixup template + var newId = idCounter; + --idCounter; + + var prefix = "m" + newId; + editRow.find('input[name="prefix"]').val(prefix); + + editRow.find("input,select,textarea").each(function () { + if (this.name == "prefix") + return; + + if (this.name == "id") + this.value = "" + idCounter; + + this.name = prefix + "-" + this.name; + }); + + editRow.removeClass("template"); + setupTokenizedField(editRow.find(".tokenized-field")); // from tokenized-field.js + setInputMasks(editRow); + editRow.show(); + } + else { + row.hide(); + editRow.show(); + } + + editRow.find('input[name$="expanded_for_editing"]').val("True"); + editRow.find('input[name$="desc"]').focus(); + }); + + function setResolvedState() { + var resolved = jQuery(this).is(":checked"); + var label = jQuery(this).siblings("label"); + var reason = jQuery(this).siblings("input[type=text]"); + if (resolved) { + if (label.text().indexOf(":") == -1) + label.text(label.text() + ":"); + reason.show(); + if (!reason.val()) + reason.val(finishedMilestoneText); + } + else { + if (label.text().indexOf(":") != -1) + label.text(label.text().replace(":", "")); + reason.hide(); + reason.val(""); + } + } + + jQuery("#milestones-form .edit-milestone .resolved input[type=checkbox]") + .each(setResolvedState) + .live("change", setResolvedState); + + function setDeleteState() { + var top = jQuery(this).closest(".edit-milestone"); + + if (jQuery(this).is(":checked")) { + if (+top.find('input[name$="id"]').val() < 0) + top.remove(); + else + top.addClass("delete") + } + else + top.removeClass("delete") + } + + jQuery("#milestones-form .edit-milestone .delete input[type=checkbox]") + .each(setDeleteState) + .live("change", setDeleteState); + + function setInputMasks(editRows) { + editRows.find(".due input").mask("9999-99-99"); + } + + setInputMasks(jQuery("#milestone-form .edit-milestone").not(".template")); + + jQuery('#milestones-form .edit-milestone input[name$="expanded_for_editing"]').each(function () { + if (this.value == "True") + jQuery(this).closest(".edit-milestone").prev().click(); + }); +}); diff --git a/static/js/emails-field.js b/static/js/emails-field.js deleted file mode 100644 index 6c5c770cb..000000000 --- a/static/js/emails-field.js +++ /dev/null @@ -1,13 +0,0 @@ -jQuery(function () { - jQuery(".emails-field").each(function () { - var e = jQuery(this); - var pre = []; - if (e.val()) - pre = JSON.parse(e.val()); - e.tokenInput(e.data("ajax-url"), { - hintText: "", - preventDuplicates: true, - prePopulate: pre - }); - }); -}); diff --git a/static/js/lib/jquery.maskedinput.js b/static/js/lib/jquery.maskedinput.js new file mode 100644 index 000000000..af6760426 --- /dev/null +++ b/static/js/lib/jquery.maskedinput.js @@ -0,0 +1,258 @@ +/* + Masked Input plugin for jQuery + Copyright (c) 2007-@Year Josh Bush (digitalbush.com) + Licensed under the MIT license (http://digitalbush.com/projects/masked-input-plugin/#license) + Version: @version +*/ +(function($) { + var pasteEventName = ($.browser.msie ? 'paste' : 'input') + ".mask"; + var iPhone = (window.orientation != undefined); + + $.mask = { + //Predefined character definitions + definitions: { + '9': "[0-9]", + 'a': "[A-Za-z]", + '*': "[A-Za-z0-9]" + }, + dataName:"rawMaskFn" + }; + + $.fn.extend({ + //Helper Function for Caret positioning + caret: function(begin, end) { + if (this.length == 0) return; + if (typeof begin == 'number') { + end = (typeof end == 'number') ? end : begin; + return this.each(function() { + if (this.setSelectionRange) { + this.setSelectionRange(begin, end); + } else if (this.createTextRange) { + var range = this.createTextRange(); + range.collapse(true); + range.moveEnd('character', end); + range.moveStart('character', begin); + range.select(); + } + }); + } else { + if (this[0].setSelectionRange) { + begin = this[0].selectionStart; + end = this[0].selectionEnd; + } else if (document.selection && document.selection.createRange) { + var range = document.selection.createRange(); + begin = 0 - range.duplicate().moveStart('character', -100000); + end = begin + range.text.length; + } + return { begin: begin, end: end }; + } + }, + unmask: function() { return this.trigger("unmask"); }, + mask: function(mask, settings) { + if (!mask && this.length > 0) { + var input = $(this[0]); + return input.data($.mask.dataName)(); + } + settings = $.extend({ + placeholder: "_", + completed: null + }, settings); + + var defs = $.mask.definitions; + var tests = []; + var partialPosition = mask.length; + var firstNonMaskPos = null; + var len = mask.length; + + $.each(mask.split(""), function(i, c) { + if (c == '?') { + len--; + partialPosition = i; + } else if (defs[c]) { + tests.push(new RegExp(defs[c])); + if(firstNonMaskPos==null) + firstNonMaskPos = tests.length - 1; + } else { + tests.push(null); + } + }); + + return this.trigger("unmask").each(function() { + var input = $(this); + var buffer = $.map(mask.split(""), function(c, i) { if (c != '?') return defs[c] ? settings.placeholder : c }); + var focusText = input.val(); + + function seekNext(pos) { + while (++pos <= len && !tests[pos]); + return pos; + }; + function seekPrev(pos) { + while (--pos >= 0 && !tests[pos]); + return pos; + }; + + function shiftL(begin,end) { + if(begin<0) + return; + for (var i = begin,j = seekNext(end); i < len; i++) { + if (tests[i]) { + if (j < len && tests[i].test(buffer[j])) { + buffer[i] = buffer[j]; + buffer[j] = settings.placeholder; + } else + break; + j = seekNext(j); + } + } + writeBuffer(); + input.caret(Math.max(firstNonMaskPos, begin)); + }; + + function shiftR(pos) { + for (var i = pos, c = settings.placeholder; i < len; i++) { + if (tests[i]) { + var j = seekNext(i); + var t = buffer[i]; + buffer[i] = c; + if (j < len && tests[j].test(t)) + c = t; + else + break; + } + } + }; + + function keydownEvent(e) { + var k=e.which; + + //backspace, delete, and escape get special treatment + if(k == 8 || k == 46 || (iPhone && k == 127)){ + var pos = input.caret(), + begin = pos.begin, + end = pos.end; + + if(end-begin==0){ + begin=k!=46?seekPrev(begin):(end=seekNext(begin-1)); + end=k==46?seekNext(end):end; + } + clearBuffer(begin, end); + shiftL(begin,end-1); + + return false; + } else if (k == 27) {//escape + input.val(focusText); + input.caret(0, checkVal()); + return false; + } + }; + + function keypressEvent(e) { + var k = e.which, + pos = input.caret(); + if (e.ctrlKey || e.altKey || e.metaKey || k<32) {//Ignore + return true; + } else if (k) { + if(pos.end-pos.begin!=0){ + clearBuffer(pos.begin, pos.end); + shiftL(pos.begin, pos.end-1); + } + + var p = seekNext(pos.begin - 1); + if (p < len) { + var c = String.fromCharCode(k); + if (tests[p].test(c)) { + shiftR(p); + buffer[p] = c; + writeBuffer(); + var next = seekNext(p); + input.caret(next); + if (settings.completed && next >= len) + settings.completed.call(input); + } + } + return false; + } + }; + + function clearBuffer(start, end) { + for (var i = start; i < end && i < len; i++) { + if (tests[i]) + buffer[i] = settings.placeholder; + } + }; + + function writeBuffer() { return input.val(buffer.join('')).val(); }; + + function checkVal(allow) { + //try to place characters where they belong + var test = input.val(); + var lastMatch = -1; + for (var i = 0, pos = 0; i < len; i++) { + if (tests[i]) { + buffer[i] = settings.placeholder; + while (pos++ < test.length) { + var c = test.charAt(pos - 1); + if (tests[i].test(c)) { + buffer[i] = c; + lastMatch = i; + break; + } + } + if (pos > test.length) + break; + } else if (buffer[i] == test.charAt(pos) && i!=partialPosition) { + pos++; + lastMatch = i; + } + } + if (!allow && lastMatch + 1 < partialPosition) { + input.val(""); + clearBuffer(0, len); + } else if (allow || lastMatch + 1 >= partialPosition) { + writeBuffer(); + if (!allow) input.val(input.val().substring(0, lastMatch + 1)); + } + return (partialPosition ? i : firstNonMaskPos); + }; + + input.data($.mask.dataName,function(){ + return $.map(buffer, function(c, i) { + return tests[i]&&c!=settings.placeholder ? c : null; + }).join(''); + }) + + if (!input.attr("readonly")) + input + .one("unmask", function() { + input + .unbind(".mask") + .removeData($.mask.dataName); + }) + .bind("focus.mask", function() { + focusText = input.val(); + var pos = checkVal(); + writeBuffer(); + var moveCaret=function(){ + if (pos == mask.length) + input.caret(0, pos); + else + input.caret(pos); + }; + ($.browser.msie ? moveCaret:function(){setTimeout(moveCaret,0)})(); + }) + .bind("blur.mask", function() { + checkVal(); + if (input.val() != focusText) + input.change(); + }) + .bind("keydown.mask", keydownEvent) + .bind("keypress.mask", keypressEvent) + .bind(pasteEventName, function() { + setTimeout(function() { input.caret(checkVal(true)); }, 0); + }); + + checkVal(); //Perform initial check for existing values + }); + } + }); +})(jQuery); diff --git a/static/js/tokenized-field.js b/static/js/tokenized-field.js new file mode 100644 index 000000000..962f64369 --- /dev/null +++ b/static/js/tokenized-field.js @@ -0,0 +1,19 @@ +function setupTokenizedField(field) { + if (field.parents(".template").length > 0) + return; // don't tokenize hidden template snippets + + var pre = []; + if (field.val()) + pre = JSON.parse(field.val()); + else if (field.data("pre")) + pre = JSON.parse(field.data("pre")); + field.tokenInput(field.data("ajax-url"), { + hintText: "", + preventDuplicates: true, + prePopulate: pre + }); +} + +jQuery(function () { + jQuery(".tokenized-field").each(function () { setupTokenizedField(jQuery(this)); }); +}); From 319a202900c597f9098a2144d6e7c091fab51a44 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Wed, 27 Jun 2012 16:24:04 +0000 Subject: [PATCH 02/30] Finish charter milestones support so they're merged in after approval of the charter, also replace references to WGs in wgcharter with a more generic counterpart so will be easier to add support for RGs if someone ever gets that far (will still need testing and probably some minor adjustments though). - Legacy-Id: 4534 --- ietf/group/utils.py | 2 +- ietf/templates/wgcharter/action_text.txt | 10 +- .../templates/wgcharter/email_secretariat.txt | 2 +- ietf/templates/wgcharter/group_info.txt | 28 ++++ ietf/templates/wgcharter/review_text.txt | 10 +- .../wgcharter/search_result_row.html | 13 -- ietf/templates/wgcharter/submit.html | 8 +- ietf/templates/wgcharter/wg_info.txt | 28 ---- ietf/wgcharter/mails.py | 46 +++--- ietf/wgcharter/tests.py | 29 +++- ietf/wgcharter/views.py | 147 ++++++++++++------ ietf/wginfo/milestones.py | 3 - 12 files changed, 194 insertions(+), 132 deletions(-) create mode 100644 ietf/templates/wgcharter/group_info.txt delete mode 100644 ietf/templates/wgcharter/search_result_row.html delete mode 100644 ietf/templates/wgcharter/wg_info.txt diff --git a/ietf/group/utils.py b/ietf/group/utils.py index fe69c85d3..0ab62eafe 100644 --- a/ietf/group/utils.py +++ b/ietf/group/utils.py @@ -12,7 +12,7 @@ def save_group_in_history(group): # save RoleHistory for role in group.role_set.all(): - rh = RoleHistory(name=role.name, group=grouphist, email=role.email, person=role.person) + rh = RoleHistory(name=role.name, group=h, email=role.email, person=role.person) rh.save() copy_many_to_many_for_history(h, group) diff --git a/ietf/templates/wgcharter/action_text.txt b/ietf/templates/wgcharter/action_text.txt index 30f7435de..bee83bc6c 100644 --- a/ietf/templates/wgcharter/action_text.txt +++ b/ietf/templates/wgcharter/action_text.txt @@ -1,8 +1,8 @@ {% load ietf_filters %}{% autoescape off %}From: The IESG -To: IETF-Announce {% if wg.list_email %} -Cc: {{ wg.acronym }} WG <{{ wg.list_email }}> {% endif %} -Subject: WG Action: {{ action_type }} {{ wg.name }} ({{ wg.acronym }}) +To: IETF-Announce {% if group.list_email %} +Cc: {{ group.acronym }} {{ group.type.name }} <{{ group.list_email }}> {% endif %} +Subject: WG Action: {{ action_type }} {{ group.name }} ({{ group.acronym }}) -{% filter wordwrap:73 %}{% ifequal action_type "Formed" %}A new IETF working group has been formed in the {{ wg.parent.name }}.{% endifequal %}{% ifequal action_type "Rechartered" %}The {{ wg.name }} ({{ wg.acronym }}) working group in the {{ wg.parent.name }} of the IETF has been rechartered.{% endifequal %} For additional information please contact the Area Directors or the WG Chair{{ chairs|pluralize}}. +{% filter wordwrap:73 %}{% ifequal action_type "Formed" %}A new IETF working group has been formed in the {{ group.parent.name }}.{% endifequal %}{% ifequal action_type "Rechartered" %}The {{ group.name }} ({{ group.acronym }}) working group in the {{ group.parent.name }} of the IETF has been rechartered.{% endifequal %} For additional information please contact the Area Directors or the {{ group.type.name }} Chair{{ chairs|pluralize}}. -{% include "wgcharter/wg_info.txt" %}{% endfilter %}{% endautoescape %} +{% include "wgcharter/group_info.txt" %}{% endfilter %}{% endautoescape %} diff --git a/ietf/templates/wgcharter/email_secretariat.txt b/ietf/templates/wgcharter/email_secretariat.txt index 140714df6..4b8480fe7 100644 --- a/ietf/templates/wgcharter/email_secretariat.txt +++ b/ietf/templates/wgcharter/email_secretariat.txt @@ -1,6 +1,6 @@ {% load ietf_filters %}{% autoescape off %} {{ text|fill:70 }} -WG: {{ wg_url }} +{{ group.type.name}}: {{ group_url }} Charter: {{ charter_url }} {% endautoescape %} diff --git a/ietf/templates/wgcharter/group_info.txt b/ietf/templates/wgcharter/group_info.txt new file mode 100644 index 000000000..4d21461ec --- /dev/null +++ b/ietf/templates/wgcharter/group_info.txt @@ -0,0 +1,28 @@ +{{ group.name }} ({{ group.acronym }}) +------------------------------------------------ +Current Status: {{ group.state.name }} {{ group.type.name }} + +{% if chairs %}Chairs: +{% for r in chairs %} {{ r.person.plain_name }} <{{r.email.address}}> +{% endfor %} +{% endif %}{% if secr %}Secretaries: +{% for r in secr %} {{ r.person.plain_name }} <{{r.email.address}}> +{% endfor %} +{% endif %}{% if techadv %}Technical advisors: +{% for r in techadv %} {{ r.person.plain_name }} <{{r.email.address}}> +{% endfor %} +{% endif %}{% if group.ad %}Assigned Area Director: + {{ group.ad.plain_name }} <{{ ad_email }}> + +{% endif %}{% if group.list_email %}Mailing list + Address: {{ group.list_email }} + To Subscribe: {{ group.list_subscribe }} + Archive: {{ group.list_archive }} +{% endif %} +Charter: + +{{ charter_text }} + +Milestones: +{% for milestone in milestones %} {% if milestone.resolved %}{{ milestone.resolved }} {% else %}{{ milestone.due|date:"M Y" }}{% endif %} - {{ milestone.desc|safe }} +{% endfor %} diff --git a/ietf/templates/wgcharter/review_text.txt b/ietf/templates/wgcharter/review_text.txt index a683516e9..de8c83c28 100644 --- a/ietf/templates/wgcharter/review_text.txt +++ b/ietf/templates/wgcharter/review_text.txt @@ -1,8 +1,8 @@ {% load ietf_filters %}{% autoescape off %}From: The IESG -To: IETF-Announce {% if wg.list_email %} -Cc: {{ wg.acronym }} WG <{{ wg.list_email }}> {% endif %} -Subject: WG Review: {{ wg.name }} ({{ wg.acronym }}) +To: IETF-Announce {% if group.list_email %} +Cc: {{ group.acronym }} {{ group.type.name }} <{{ group.list_email }}> {% endif %} +Subject: WG Review: {{ group.name }} ({{ group.acronym }}) -{% filter wordwrap:73 %}{% ifequal review_type "new" %}A new IETF working group has been proposed in the {{ wg.parent.name }}.{% endifequal %}{% ifequal review_type "recharter" %}The {{ wg.name }} ({{wg.acronym}}) working group in the {{ wg.parent.name }} of the IETF is undergoing rechartering.{% endifequal %} The IESG has not made any determination yet. The following draft charter was submitted, and is provided for informational purposes only. Please send your comments to the IESG mailing list (iesg at ietf.org) by {{ review_date }}. +{% filter wordwrap:73 %}{% ifequal review_type "new" %}A new IETF working group has been proposed in the {{ group.parent.name }}.{% endifequal %}{% ifequal review_type "recharter" %}The {{ group.name }} ({{group.acronym}}) working group in the {{ group.parent.name }} of the IETF is undergoing rechartering.{% endifequal %} The IESG has not made any determination yet. The following draft charter was submitted, and is provided for informational purposes only. Please send your comments to the IESG mailing list (iesg at ietf.org) by {{ review_date }}. -{% include "wgcharter/wg_info.txt" %}{% endfilter %}{% endautoescape %} +{% include "wgcharter/group_info.txt" %}{% endfilter %}{% endautoescape %} diff --git a/ietf/templates/wgcharter/search_result_row.html b/ietf/templates/wgcharter/search_result_row.html deleted file mode 100644 index babfb2343..000000000 --- a/ietf/templates/wgcharter/search_result_row.html +++ /dev/null @@ -1,13 +0,0 @@ -{% comment %} -Copyright The IETF Trust 2011, All Rights Reserved -{% endcomment %} - -{% load ietf_filters %} - - -{{ wg.acronym|safe }} - -{{ wg.name }} -{% include "wgcharter/date_column.html" %} -{% include "wgcharter/status_columns.html" %} - diff --git a/ietf/templates/wgcharter/submit.html b/ietf/templates/wgcharter/submit.html index 9646d452a..8cae45f6b 100644 --- a/ietf/templates/wgcharter/submit.html +++ b/ietf/templates/wgcharter/submit.html @@ -8,13 +8,13 @@ form #id_content { {% endblock %} {% block title %} -Charter submission for {{ wg.acronym }} +Charter submission for {{ group.acronym }} {{ group.type.name }} {% endblock %} {% block content %} -

Charter submission for {{ wg.acronym }}

+

Charter submission for {{ group.acronym }} {{ group.type.name }}

-

The text will be submitted as charter-ietf-{{ wg.acronym }}-{{ next_rev }}

+

The text will be submitted as charter-ietf-{{ group.acronym }}-{{ next_rev }}

{% for field in form.visible_fields %} @@ -30,7 +30,7 @@ Charter submission for {{ wg.acronym }} diff --git a/ietf/templates/wgcharter/wg_info.txt b/ietf/templates/wgcharter/wg_info.txt deleted file mode 100644 index 209768f97..000000000 --- a/ietf/templates/wgcharter/wg_info.txt +++ /dev/null @@ -1,28 +0,0 @@ -{{ wg.name }} ({{ wg.acronym }}) ------------------------------------------------- -Current Status: {{ wg.state.name }} Working Group - -{% if chairs %}Chairs: -{% for r in chairs %} {{ r.person.plain_name }} <{{r.email.address}}> -{% endfor %} -{% endif %}{% if secr %}Secretaries: -{% for r in secr %} {{ r.person.plain_name }} <{{r.email.address}}> -{% endfor %} -{% endif %}{% if techadv %}Technical advisors: -{% for r in techadv %} {{ r.person.plain_name }} <{{r.email.address}}> -{% endfor %} -{% endif %}{% if wg.ad %}Assigned Area Director: - {{ wg.ad.plain_name }} <{{ ad_email }}> - -{% endif %}{% if wg.list_email %}Mailing list - Address: {{ wg.list_email }} - To Subscribe: {{ wg.list_subscribe }} - Archive: {{ wg.list_archive }} -{% endif %} -Charter of Working Group: - -{{ charter_text }} - -Milestones: -{% for milestone in milestones %} {% if milestone.done %}Done {% else %}{{ milestone.expected_due_date|date:"M Y" }}{% endif %} - {{ milestone.desc|safe }} -{% endfor %} diff --git a/ietf/wgcharter/mails.py b/ietf/wgcharter/mails.py index e59da36cc..e48ed0841 100644 --- a/ietf/wgcharter/mails.py +++ b/ietf/wgcharter/mails.py @@ -13,7 +13,7 @@ from ietf.doc.models import WriteupDocEvent, DocAlias, BallotPositionDocEvent from ietf.person.models import Person from ietf.wgcharter.utils import * -def email_secretariat(request, wg, type, text): +def email_secretariat(request, group, type, text): to = ["iesg-secretary@ietf.org"] types = {} @@ -24,16 +24,16 @@ def email_secretariat(request, wg, type, text): types['state-extrev'] = "State changed to External review" types['state-iesgrev'] = "State changed to IESG review" types['state-approved'] = "Charter approved" - types['conclude'] = "Request closing of WG" + types['conclude'] = "Request closing of group" - subject = u"Regarding WG %s: %s" % (wg.acronym, types[type]) + subject = u"Regarding %s %s: %s" % (group.type.name, group.acronym, types[type]) text = strip_tags(text) send_mail(request, to, None, subject, "wgcharter/email_secretariat.txt", dict(text=text, - wg_url=settings.IDTRACKER_BASE_URL + urlreverse('wg_charter', kwargs=dict(acronym=wg.acronym)), - charter_url=settings.IDTRACKER_BASE_URL + urlreverse('doc_view', kwargs=dict(name=wg.charter.name)), + group_url=settings.IDTRACKER_BASE_URL + urlreverse('wg_charter', kwargs=dict(acronym=group.acronym)), + charter_url=settings.IDTRACKER_BASE_URL + urlreverse('doc_view', kwargs=dict(name=group.charter.name)), ) ) @@ -62,8 +62,8 @@ def generate_ballot_writeup(request, doc): return e -def default_action_text(wg, charter, user): - if next_approved_revision(wg.charter.rev) == "01": +def default_action_text(group, charter, user): + if next_approved_revision(group.charter.rev) == "01": action = "Formed" else: action = "Rechartered" @@ -71,38 +71,38 @@ def default_action_text(wg, charter, user): e = WriteupDocEvent(doc=charter, by=user) e.by = user e.type = "changed_action_announcement" - e.desc = "WG action text was changed" + e.desc = "%s action text was changed" % group.type.name e.text = render_to_string("wgcharter/action_text.txt", - dict(wg=wg, + dict(group=group, charter_url=settings.IDTRACKER_BASE_URL + charter.get_absolute_url(), charter_text=read_charter_text(charter), - chairs=wg.role_set.filter(name="chair"), - secr=wg.role_set.filter(name="secr"), - techadv=wg.role_set.filter(name="techadv"), - milestones=wg.groupmilestone_set.all(), - ad_email=wg.ad.role_email("ad") if wg.ad else None, + chairs=group.role_set.filter(name="chair"), + secr=group.role_set.filter(name="secr"), + techadv=group.role_set.filter(name="techadv"), + milestones=group.groupmilestone_set.filter(state="charter"), + ad_email=group.ad.role_email("ad") if group.ad else None, action_type=action, )) e.save() return e -def default_review_text(wg, charter, user): +def default_review_text(group, charter, user): e = WriteupDocEvent(doc=charter, by=user) e.by = user e.type = "changed_review_announcement" - e.desc = "WG review text was changed" + e.desc = "%s review text was changed" % group.type.name e.text = render_to_string("wgcharter/review_text.txt", - dict(wg=wg, + dict(group=group, charter_url=settings.IDTRACKER_BASE_URL + charter.get_absolute_url(), charter_text=read_charter_text(charter), - chairs=wg.role_set.filter(name="chair"), - secr=wg.role_set.filter(name="secr"), - techadv=wg.role_set.filter(name="techadv"), - milestones=wg.groupmilestone_set.all(), - ad_email=wg.ad.role_email("ad") if wg.ad else None, + chairs=group.role_set.filter(name="chair"), + secr=group.role_set.filter(name="secr"), + techadv=group.role_set.filter(name="techadv"), + milestones=group.groupmilestone_set.filter(state="charter"), + ad_email=group.ad.role_email("ad") if group.ad else None, review_date=(datetime.date.today() + datetime.timedelta(weeks=1)).isoformat(), - review_type="new" if wg.state_id == "proposed" else "recharter", + review_type="new" if group.state_id == "proposed" else "recharter", ) ) e.save() diff --git a/ietf/wgcharter/tests.py b/ietf/wgcharter/tests.py index 309afe314..3d61ac4e8 100644 --- a/ietf/wgcharter/tests.py +++ b/ietf/wgcharter/tests.py @@ -174,7 +174,7 @@ class EditCharterTestCase(django.test.TestCase): self.assertEquals(f.read(), "Windows line\nMac line\nUnix line\n" + utf_8_snippet) -class CharterApproveBallotTestCase(django.test.TestCase): +class ApproveCharterTestCase(django.test.TestCase): fixtures = ['names'] def setUp(self): @@ -209,6 +209,28 @@ class CharterApproveBallotTestCase(django.test.TestCase): charter.set_state(State.objects.get(type="charter", slug="iesgrev")) + due_date = datetime.date.today() + datetime.timedelta(days=180) + m1 = GroupMilestone.objects.create(group=group, + state_id="active", + desc="Has been copied", + due=due_date, + resolved="") + m2 = GroupMilestone.objects.create(group=group, + state_id="active", + desc="To be deleted", + due=due_date, + resolved="") + m3 = GroupMilestone.objects.create(group=group, + state_id="charter", + desc="Has been copied", + due=due_date, + resolved="") + m4 = GroupMilestone.objects.create(group=group, + state_id="charter", + desc="New charter milestone", + due=due_date, + resolved="") + # normal get r = self.client.get(url) self.assertEquals(r.status_code, 200) @@ -232,3 +254,8 @@ class CharterApproveBallotTestCase(django.test.TestCase): self.assertEquals(len(outbox), mailbox_before + 2) self.assertTrue("WG Action" in outbox[-1]['Subject']) self.assertTrue("Charter approved" in outbox[-2]['Subject']) + + self.assertEquals(group.groupmilestone_set.filter(state="charter").count(), 0) + self.assertEquals(group.groupmilestone_set.filter(state="active").count(), 2) + self.assertEquals(group.groupmilestone_set.filter(state="active", desc=m1.desc).count(), 1) + self.assertEquals(group.groupmilestone_set.filter(state="active", desc=m4.desc).count(), 1) diff --git a/ietf/wgcharter/views.py b/ietf/wgcharter/views.py index 2ec85cd40..97d09166f 100644 --- a/ietf/wgcharter/views.py +++ b/ietf/wgcharter/views.py @@ -22,7 +22,7 @@ from ietf.doc.utils import * from ietf.name.models import * from ietf.person.models import * from ietf.group.models import * -from ietf.group.utils import save_group_in_history +from ietf.group.utils import save_group_in_history, save_milestone_in_history from ietf.wgcharter.mails import * from ietf.wgcharter.utils import * @@ -42,10 +42,10 @@ class ChangeStateForm(forms.Form): @role_required("Area Director", "Secretariat") def change_state(request, name, option=None): - """Change state of WG and charter, notifying parties as necessary - and logging the change as a comment.""" + """Change state of charter, notifying parties as necessary and + logging the change as a comment.""" charter = get_object_or_404(Document, type="charter", name=name) - wg = charter.group + group = charter.group chartering_type = get_chartering_type(charter) @@ -73,7 +73,7 @@ def change_state(request, name, option=None): if "-" not in charter_rev: charter_rev = charter_rev + "-00" elif option == "abandon": - if wg.state_id == "proposed": + if group.state_id == "proposed": charter_state = State.objects.get(type="charter", slug="notrev") else: charter_state = State.objects.get(type="charter", slug="approved") @@ -112,7 +112,7 @@ def change_state(request, name, option=None): charter.save() if message: - email_secretariat(request, wg, "state-%s" % charter_state.slug, message) + email_secretariat(request, group, "state-%s" % charter_state.slug, message) email_state_changed(request, charter, "State changed to %s." % charter_state) @@ -121,8 +121,8 @@ def change_state(request, name, option=None): create_ballot_if_not_open(charter, login, "r-wo-ext") else: create_ballot_if_not_open(charter, login, "r-extrev") - default_review_text(wg, charter, login) - default_action_text(wg, charter, login) + default_review_text(group, charter, login) + default_action_text(group, charter, login) elif charter_state.slug == "iesgrev": create_ballot_if_not_open(charter, login, "approve") @@ -139,10 +139,10 @@ def change_state(request, name, option=None): init = dict() elif option == "initcharter": hide = ['charter_state'] - init = dict(initial_time=1, message='%s has initiated chartering of the proposed WG:\n "%s" (%s).' % (login.plain_name(), wg.name, wg.acronym)) + init = dict(initial_time=1, message='%s has initiated chartering of the proposed %s:\n "%s" (%s).' % (login.plain_name(), group.type.name, group.name, group.acronym)) elif option == "abandon": hide = ['initial_time', 'charter_state'] - init = dict(message='%s has abandoned the chartering effort on the WG:\n "%s" (%s).' % (login.plain_name(), wg.name, wg.acronym)) + init = dict(message='%s has abandoned the chartering effort on the %s:\n "%s" (%s).' % (login.plain_name(), group.type.name, group.name, group.acronym)) else: hide = ['initial_time'] s = charter.get_state() @@ -155,27 +155,27 @@ def change_state(request, name, option=None): prev_charter_state = charter_hists[0].get_state() title = { - "initcharter": "Initiate chartering of WG %s" % wg.acronym, - "recharter": "Recharter WG %s" % wg.acronym, - "abandon": "Abandon effort on WG %s" % wg.acronym, + "initcharter": "Initiate chartering of %s %s" % (group.acronym, group.type.name), + "recharter": "Recharter %s %s" % (group.acronym, group.type.name), + "abandon": "Abandon effort on %s %s" % (group.acronym, group.type.name), }.get(option) if not title: - title = "Change state of WG %s" % wg.acronym + title = "Change state of %s %s" % (group.acronym, group.type.name) def state_pk(slug): return State.objects.get(type="charter", slug=slug).pk messages = { - state_pk("infrev"): 'The WG "%s" (%s) has been set to Informal IESG review by %s.' % (wg.name, wg.acronym, login.plain_name()), - state_pk("intrev"): 'The WG "%s" (%s) has been set to Internal review by %s.\nPlease place it on the next IESG telechat and inform the IAB.' % (wg.name, wg.acronym, login.plain_name()), - state_pk("extrev"): 'The WG "%s" (%s) has been set to External review by %s.\nPlease send out the external review announcement to the appropriate lists.\n\nSend the announcement to other SDOs: Yes\nAdditional recipients of the announcement: ' % (wg.name, wg.acronym, login.plain_name()), + state_pk("infrev"): 'The %s "%s" (%s) has been set to Informal IESG review by %s.' % (group.type.name, group.name, group.acronym, login.plain_name()), + state_pk("intrev"): 'The %s "%s" (%s) has been set to Internal review by %s.\nPlease place it on the next IESG telechat and inform the IAB.' % (group.type.name, group.name, group.acronym, login.plain_name()), + state_pk("extrev"): 'The %s "%s" (%s) has been set to External review by %s.\nPlease send out the external review announcement to the appropriate lists.\n\nSend the announcement to other SDOs: Yes\nAdditional recipients of the announcement: ' % (group.type.name, group.name, group.acronym, login.plain_name()), } states_for_ballot_wo_extern = State.objects.filter(type="charter", slug="intrev").values_list("pk", flat=True) return render_to_response('wgcharter/change_state.html', dict(form=form, - doc=wg.charter, + doc=group.charter, login=login, option=option, prev_charter_state=prev_charter_state, @@ -278,8 +278,8 @@ class UploadForm(forms.Form): def clean_txt(self): return get_cleaned_text_file_content(self.cleaned_data["txt"]) - def save(self, wg, rev): - filename = os.path.join(settings.CHARTER_PATH, '%s-%s.txt' % (wg.charter.canonical_name(), rev)) + def save(self, group, rev): + filename = os.path.join(settings.CHARTER_PATH, '%s-%s.txt' % (group.charter.canonical_name(), rev)) with open(filename, 'wb') as destination: if self.cleaned_data['txt']: destination.write(self.cleaned_data['txt']) @@ -291,7 +291,7 @@ def submit(request, name=None, acronym=None, option=None): if acronym and not name: name = "charter-ietf-" + acronym charter = get_object_or_404(Document, type="charter", name=name) - wg = charter.group + group = charter.group login = request.user.get_profile() @@ -313,7 +313,7 @@ def submit(request, name=None, acronym=None, option=None): if form.is_valid(): save_document_in_history(charter) # Also save group history so we can search for it - save_group_in_history(wg) + save_group_in_history(group) charter.rev = next_rev @@ -323,7 +323,7 @@ def submit(request, name=None, acronym=None, option=None): e.save() # Save file on disk - form.save(wg, charter.rev) + form.save(group, charter.rev) charter.time = datetime.datetime.now() charter.save() @@ -354,7 +354,7 @@ def submit(request, name=None, acronym=None, option=None): return render_to_response('wgcharter/submit.html', {'form': form, 'next_rev': next_rev, - 'wg': wg }, + 'group': group }, context_instance=RequestContext(request)) class AnnouncementTextForm(forms.Form): @@ -367,19 +367,20 @@ class AnnouncementTextForm(forms.Form): def announcement_text(request, name, ann): """Editing of announcement text""" charter = get_object_or_404(Document, type="charter", name=name) - wg = charter.group + group = charter.group login = request.user.get_profile() - if ann == "action": - existing = charter.latest_event(WriteupDocEvent, type="changed_action_announcement") - elif ann == "review": - existing = charter.latest_event(WriteupDocEvent, type="changed_review_announcement") + if ann in ("action", "review"): + existing = charter.latest_event(WriteupDocEvent, type="changed_%s_announcement" % ann) if not existing: if ann == "action": - existing = default_action_text(wg, charter, login) + existing = default_action_text(group, charter, login) elif ann == "review": - existing = default_review_text(wg, charter, login) + existing = default_review_text(group, charter, login) + + if not existing: + raise Http404 form = AnnouncementTextForm(initial=dict(announcement_text=existing.text)) @@ -391,7 +392,7 @@ def announcement_text(request, name, ann): e = WriteupDocEvent(doc=charter, by=login) e.by = login e.type = "changed_%s_announcement" % ann - e.desc = "WG %s text was changed" % ann + e.desc = "%s %s text was changed" % (group.type.name, ann) e.text = t e.save() @@ -405,9 +406,9 @@ def announcement_text(request, name, ann): if "regenerate_text" in request.POST: if ann == "action": - e = default_action_text(wg, charter, login) + e = default_action_text(group, charter, login) elif ann == "review": - e = default_review_text(wg, charter, login) + e = default_review_text(group, charter, login) # make sure form has the updated text form = AnnouncementTextForm(initial=dict(announcement_text=e.text)) @@ -439,7 +440,6 @@ class BallotWriteupForm(forms.Form): def ballot_writeupnotes(request, name): """Editing of ballot write-up and notes""" charter = get_object_or_404(Document, type="charter", name=name) - wg = charter.group ballot = charter.latest_event(BallotDocEvent, type="created_ballot") if not ballot: @@ -507,13 +507,13 @@ def ballot_writeupnotes(request, name): def approve(request, name): """Approve charter, changing state, fixing revision, copying file to final location.""" charter = get_object_or_404(Document, type="charter", name=name) - wg = charter.group + group = charter.group login = request.user.get_profile() e = charter.latest_event(WriteupDocEvent, type="changed_action_announcement") if not e: - announcement = default_action_text(wg, charter, login).text + announcement = default_action_text(group, charter, login).text else: announcement = e.text @@ -535,13 +535,13 @@ def approve(request, name): change_description = e.desc new_state = GroupStateName.objects.get(slug="active") - if wg.state != new_state: - save_group_in_history(wg) - prev_state = wg.state - wg.state = new_state - wg.time = e.time - wg.save() - change_description += " and WG state has been changed to %s" % new_state.name + if group.state != new_state: + save_group_in_history(group) + prev_state = group.state + group.state = new_state + group.time = e.time + group.save() + change_description += " and %s state has been changed to %s" % (group.type.name, new_state.name) e = log_state_changed(request, charter, login, prev_charter_state) @@ -551,7 +551,9 @@ def approve(request, name): new = os.path.join(charter.get_file_path(), '%s-%s.txt' % (charter.canonical_name(), next_approved_revision(charter.rev))) shutil.copy(old, new) except IOError: - raise Http404("Charter text %s" % filename) + return HttpResponse("There was an error copying %s to %s" % + ('%s-%s.txt' % (charter.canonical_name(), charter.rev), + '%s-%s.txt' % (charter.canonical_name(), next_approved_revision(charter.rev)))) e = NewRevisionDocEvent(doc=charter, by=login, type="new_revision") e.rev = next_approved_revision(charter.rev) @@ -562,7 +564,57 @@ def approve(request, name): charter.time = e.time charter.save() - email_secretariat(request, wg, "state-%s" % new_charter_state.slug, change_description) + email_secretariat(request, group, "state-%s" % new_charter_state.slug, change_description) + + # move milestones over + milestones_to_delete = list(group.groupmilestone_set.filter(state__in=("active", "review"))) + + for m in group.groupmilestone_set.filter(state="charter"): + # see if we got this milestone already (i.e. it was copied + # verbatim to the charter) + found = False + for i, o in enumerate(milestones_to_delete): + if o.desc == m.desc and o.due == m.due and set(o.docs.all()) == set(m.docs.all()): + found = True + break + + if found: + # keep existing, whack charter milestone + if not o.state_id == "active": + save_milestone_in_history(o) + o.state_id = "active" + o.save() + MilestoneGroupEvent.objects.create( + group=group, type="changed_milestone", by=login, + desc="Changed milestone \"%s\", changed state from review to active" % o.desc, + milestone=o) + + del milestones_to_delete[i] + + # don't generate a DocEvent for this, it's implicit in the approval event + save_milestone_in_history(m) + m.state_id = "deleted" + m.save() + else: + # move charter milestone + save_milestone_in_history(m) + m.state_id = "active" + m.save() + + MilestoneGroupEvent.objects.create( + group=group, type="changed_milestone", by=login, + desc="Added milestone \"%s\", due %s, from approved charter" % (m.desc, m.due), + milestone=m) + + for m in milestones_to_delete: + save_milestone_in_history(m) + m.state_id = "deleted" + m.save() + + MilestoneGroupEvent.objects.create( + group=group, type="changed_milestone", by=login, + desc="Deleted milestone \"%s\", not present in approved charter" % m.desc, + milestone=m) # send announcement send_mail_preformatted(request, announcement) @@ -571,7 +623,6 @@ def approve(request, name): return render_to_response('wgcharter/approve.html', dict(charter=charter, - announcement=announcement, - wg=wg), + announcement=announcement), context_instance=RequestContext(request)) diff --git a/ietf/wginfo/milestones.py b/ietf/wginfo/milestones.py index a95712429..d8f3a9c11 100644 --- a/ietf/wginfo/milestones.py +++ b/ietf/wginfo/milestones.py @@ -157,7 +157,6 @@ def edit_milestones(request, acronym, milestone_set="current"): if c["delete"]: save_milestone_in_history(m) - m.time = datetime.datetime.now() m.state_id = "deleted" m.save() @@ -298,7 +297,6 @@ def reset_charter_milestones(request, acronym): for m in charter_milestones: save_milestone_in_history(m) - m.time = datetime.datetime.now() m.state_id = "deleted" m.save() @@ -315,7 +313,6 @@ def reset_charter_milestones(request, acronym): desc=m.desc, due=m.due, resolved=m.resolved, - time=datetime.datetime.now(), ) m.docs = m.docs.all() From bbee25b924bb1f71bb3998f8be923d4b5d1916ab Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Wed, 27 Jun 2012 16:47:31 +0000 Subject: [PATCH 03/30] Include associated drafts in milestone overview on WG charter page. - Legacy-Id: 4535 --- ietf/templates/wginfo/milestones.html | 11 ++++++++--- static/css/base2.css | 2 ++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/ietf/templates/wginfo/milestones.html b/ietf/templates/wginfo/milestones.html index 8ff043f9d..3cebdab49 100644 --- a/ietf/templates/wginfo/milestones.html +++ b/ietf/templates/wginfo/milestones.html @@ -1,12 +1,17 @@ {# assumes milestones is in context #} -
- Back + Back
+
{% for milestone in milestones %} - - + {% endfor %}
+ {% if milestone.resolved %}{{ milestone.resolved }}{% else %}{{ milestone.due|date:"M Y" }}{% endif %} {{ milestone.desc|escape }} +
{{ milestone.desc|escape }}
+ {% for d in milestone.docs.all %} + {{ d.name }} + {% endfor %} +
diff --git a/static/css/base2.css b/static/css/base2.css index 669c5955d..6e1627a9c 100644 --- a/static/css/base2.css +++ b/static/css/base2.css @@ -191,3 +191,5 @@ form table .help { .ballot-content h2.ad-ballot-comment { background: #2647A0; color: #fff; padding: 2px 4px; font-size: 108%; margin-top: 0;} +table.milestones td.due { vertical-align: top; width: 80px; } +table.milestones .doc { display: block; padding-left: 1em; } From 1653633afaba139a8635a10a6423c182b4a18326 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Wed, 27 Jun 2012 16:51:40 +0000 Subject: [PATCH 04/30] Fix some terminology on charter page to be more in line with what's actually displayed these days - Legacy-Id: 4536 --- ietf/templates/wginfo/wg_charter.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ietf/templates/wginfo/wg_charter.html b/ietf/templates/wginfo/wg_charter.html index 56635994c..3f172b13c 100644 --- a/ietf/templates/wginfo/wg_charter.html +++ b/ietf/templates/wginfo/wg_charter.html @@ -162,10 +162,10 @@ is occasionally incorrect.

{% endif %} -

Description of Working Group

+

Charter for Working Group

{{ wg.charter_text|escape|format_charter|safe }}

-

Goals and Milestones +

Milestones {% if user|has_role:"Area Director,Secretariat" or is_chair %} Add or edit milestones {% endif %} @@ -174,7 +174,7 @@ is occasionally incorrect. {% with wg.milestones as milestones %}{% include "wginfo/milestones.html" %}{% endwith %} {% if milestones_in_review %} -

{{ milestones_in_review|length }} new milestone{{ milestones_in_review|pluralize }} -under review.

+

+ {{ milestones_in_review|length }} new milestone{{ milestones_in_review|pluralize }} +currently in Area Director review.

{% endif %} {% endblock wg_content %} From 22ce2e709ae6012b9a57cfdbb38af2d9a8f66931 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Thu, 28 Jun 2012 13:57:48 +0000 Subject: [PATCH 05/30] Make sure milestone.docs is also raw edit to avoid biggish drop down - Legacy-Id: 4541 --- ietf/group/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/group/admin.py b/ietf/group/admin.py index 2538dbfa8..a458d822e 100644 --- a/ietf/group/admin.py +++ b/ietf/group/admin.py @@ -118,7 +118,7 @@ admin.site.register(GroupHistory, GroupHistoryAdmin) class GroupMilestoneAdmin(admin.ModelAdmin): list_display = ["group", "desc", "due", "resolved", "time"] search_fields = ["group__name", "group__acronym", "desc", "resolved"] - raw_id_fields = ["group"] + raw_id_fields = ["group", "docs"] admin.site.register(GroupMilestone, GroupMilestoneAdmin) From 4ef6cd75435d34512428f2cb10e30eeda6cb2ec0 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Thu, 28 Jun 2012 13:58:34 +0000 Subject: [PATCH 06/30] Add option to reject review milestone, not only accept it, for ADs - Legacy-Id: 4542 --- ietf/templates/wginfo/edit_milestones.html | 5 ++++- ietf/templates/wginfo/milestone_form.html | 8 +++----- ietf/wginfo/milestones.py | 14 ++++++++++---- ietf/wginfo/tests.py | 2 +- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/ietf/templates/wginfo/edit_milestones.html b/ietf/templates/wginfo/edit_milestones.html index 8667e39ed..a8a17caa7 100644 --- a/ietf/templates/wginfo/edit_milestones.html +++ b/ietf/templates/wginfo/edit_milestones.html @@ -16,7 +16,10 @@ td.due { width: 5em; } .edit-milestone input[type=checkbox] { vertical-align: middle; margin: 0 0.2em 0 0.8em;} .edit-milestone .resolved label { vertical-align: middle; } .edit-milestone .delete label { vertical-align: middle; } -.edit-milestone .accept label { vertical-align: middle; } +.edit-milestone .accept ul { display: inline-block; margin: 0; padding: 0; } +.edit-milestone .accept ul li { list-style: none; display: inline-block; margin: 0; padding: 0; padding-left: 0.4em; } +.edit-milestone .accept ul li label { vertical-align: middle; } +.edit-milestone .accept ul li input { margin: 0; padding: 0; vertical-align: middle; } .edit-milestone .docs td { vertical-align: top; } ul.errorlist { border-width: 0px; padding: 0px; margin: 0px; display: inline-block; } diff --git a/ietf/templates/wginfo/milestone_form.html b/ietf/templates/wginfo/milestone_form.html index f08c898c5..83ddcc82c 100644 --- a/ietf/templates/wginfo/milestone_form.html +++ b/ietf/templates/wginfo/milestone_form.html @@ -34,11 +34,9 @@ {% if form.needs_review %} Review: - This milestone is not public yet, awaiting AD acceptance. - - {% if not needs_review %} - {{ form.accept }} {{ form.accept.label_tag }} - {% endif %} + + This milestone is not active yet, awaiting + AD acceptance{% if needs_review %}.{% else %}: {{ form.accept }}{% endif %} {% endif %} diff --git a/ietf/wginfo/milestones.py b/ietf/wginfo/milestones.py index d8f3a9c11..c281313e5 100644 --- a/ietf/wginfo/milestones.py +++ b/ietf/wginfo/milestones.py @@ -36,7 +36,8 @@ class MilestoneForm(forms.Form): docs = forms.CharField(max_length=10000, required=False) - accept = forms.BooleanField(required=False, initial=False) + accept = forms.ChoiceField(choices=(("accept", "Accept"), ("reject", "Reject and delete"), ("noaction", "No action")), + required=False, initial="noaction", widget=forms.RadioSelect) expanded_for_editing = forms.BooleanField(required=False, initial=False, widget=forms.HiddenInput) @@ -169,11 +170,16 @@ def edit_milestones(request, acronym, milestone_set="current"): changes = ['Changed %s' % named_milestone] - if m.state_id == "review" and not needs_review and c["accept"]: + if m.state_id == "review" and not needs_review and c["accept"] != "noaction": if not history: history = save_milestone_in_history(m) - m.state_id = "active" - changes.append("changed state from review to active") + + if c["accept"] == "accept": + m.state_id = "active" + changes.append("changed state from review to active, accepting new milestone") + elif c["accept"] == "reject": + m.state_id = "deleted" + changes.append("changed state from review to deleted, rejecting new milestone") if c["desc"] != m.desc and not needs_review: diff --git a/ietf/wginfo/tests.py b/ietf/wginfo/tests.py index 04a5a8385..e676dfa41 100644 --- a/ietf/wginfo/tests.py +++ b/ietf/wginfo/tests.py @@ -393,7 +393,7 @@ class MilestoneTestCase(django.test.TestCase): 'm1-due': m1.due.strftime("%Y-%m-%d"), 'm1-resolved': m1.resolved, 'm1-docs': ",".join(m1.docs.values_list("name", flat=True)), - 'm1-accept': "checked", + 'm1-accept': "accept", }) self.assertEquals(r.status_code, 302) From 471d5de21e14038df4dcc58580d91e2f788cf08f Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Thu, 28 Jun 2012 17:33:52 +0000 Subject: [PATCH 07/30] Add a review step when submitting milestones to try to help people catch errors - Legacy-Id: 4543 --- ietf/templates/wginfo/edit_milestones.html | 5 +- ietf/templates/wginfo/milestone_form.html | 1 - ietf/wginfo/milestones.py | 248 +++++++++++---------- ietf/wginfo/tests.py | 7 + static/js/edit-milestones.js | 24 +- 5 files changed, 163 insertions(+), 122 deletions(-) diff --git a/ietf/templates/wginfo/edit_milestones.html b/ietf/templates/wginfo/edit_milestones.html index a8a17caa7..18ea6f1a1 100644 --- a/ietf/templates/wginfo/edit_milestones.html +++ b/ietf/templates/wginfo/edit_milestones.html @@ -37,6 +37,8 @@ tr.milestone.add { font-style: italic; } {% load ietf_filters %}

{{ title }}

+ +

{% if forms %}Click a milestone to edit it.{% endif %} {% if needs_review %} @@ -76,7 +78,8 @@ this list to the currently in-use milestones for the {{ group.acronym }} {{

Back - + +
diff --git a/ietf/templates/wginfo/milestone_form.html b/ietf/templates/wginfo/milestone_form.html index 83ddcc82c..bb85d1d3d 100644 --- a/ietf/templates/wginfo/milestone_form.html +++ b/ietf/templates/wginfo/milestone_form.html @@ -1,7 +1,6 @@ {# assumes group, form, needs_review are in the context #} {{ form.id }} -{{ form.expanded_for_editing }} diff --git a/ietf/wginfo/milestones.py b/ietf/wginfo/milestones.py index c281313e5..2850c1d98 100644 --- a/ietf/wginfo/milestones.py +++ b/ietf/wginfo/milestones.py @@ -39,8 +39,6 @@ class MilestoneForm(forms.Form): accept = forms.ChoiceField(choices=(("accept", "Accept"), ("reject", "Reject and delete"), ("noaction", "No action")), required=False, initial="noaction", widget=forms.RadioSelect) - expanded_for_editing = forms.BooleanField(required=False, initial=False, widget=forms.HiddenInput) - def __init__(self, *args, **kwargs): m = self.milestone = kwargs.pop("instance", None) @@ -119,19 +117,124 @@ def edit_milestones(request, acronym, milestone_set="current"): milestones_dict = dict((str(m.id), m) for m in milestones) - def add_event(m, desc): - if milestone_set == "charter": - DocEvent.objects.create(doc=group.charter, type="changed_charter_milestone", - by=login, desc=desc) - else: - MilestoneGroupEvent.objects.create(group=group, type="changed_milestone", - by=login, desc=desc, milestone=m) + def set_attributes_from_form(f, m): + c = f.cleaned_data + m.group = group + if milestone_set == "current": + if needs_review: + m.state = GroupMilestoneStateName.objects.get(slug="review") + else: + m.state = GroupMilestoneStateName.objects.get(slug="active") + elif milestone_set == "charter": + m.state = GroupMilestoneStateName.objects.get(slug="charter") + m.desc = c["desc"] + m.due = c["due"] + m.resolved = c["resolved"] + + def save_milestone_form(f): + c = f.cleaned_data + + if f.milestone: + m = f.milestone + + named_milestone = 'milestone "%s"' % m.desc + if milestone_set == "charter": + named_milestone = "charter " + named_milestone + + if c["delete"]: + save_milestone_in_history(m) + + m.state_id = "deleted" + m.save() + + return 'Deleted %s' % named_milestone + + # compute changes + history = None + + changes = ['Changed %s' % named_milestone] + + if m.state_id == "review" and not needs_review and c["accept"] != "noaction": + if not history: + history = save_milestone_in_history(m) + + if c["accept"] == "accept": + m.state_id = "active" + changes.append("changed state from review to active, accepting new milestone") + elif c["accept"] == "reject": + m.state_id = "deleted" + changes.append("changed state from review to deleted, rejecting new milestone") + + + if c["desc"] != m.desc and not needs_review: + if not history: + history = save_milestone_in_history(m) + m.desc = c["desc"] + changes.append('changed description to "%s"' % m.desc) + + if c["due"] != m.due: + if not history: + history = save_milestone_in_history(m) + m.due = c["due"] + changes.append('changed due date to %s' % m.due.strftime("%Y-%m-%d")) + + resolved = c["resolved"] + if resolved != m.resolved: + if resolved and not m.resolved: + changes.append('resolved as "%s"' % resolved) + elif not resolved and m.resolved: + changes.append("reverted to not being resolved") + elif resolved and m.resolved: + changes.append('changed resolution to "%s"' % resolved) + + if not history: + history = save_milestone_in_history(m) + + m.resolved = resolved + + new_docs = set(c["docs"]) + old_docs = set(m.docs.all()) + if new_docs != old_docs: + added = new_docs - old_docs + if added: + changes.append('added %s to milestone' % ", ".join(d.name for d in added)) + + removed = old_docs - new_docs + if removed: + changes.append('removed %s from milestone' % ", ".join(d.name for d in removed)) + + if not history: + history = save_milestone_in_history(m) + + m.docs = new_docs + + if len(changes) > 1: + m.save() + + return ", ".join(changes) + + else: # new milestone + m = f.milestone = GroupMilestone() + set_attributes_from_form(f, m) + m.save() + + m.docs = c["docs"] + + named_milestone = 'milestone "%s"' % m.desc + if milestone_set == "charter": + named_milestone = "charter " + named_milestone + + if m.state_id in ("active", "charter"): + return 'Added %s, due %s' % (named_milestone, m.due.strftime("%Y-%m-%d")) + elif m.state_id == "review": + return 'Added %s for review, due %s' % (named_milestone, m.due.strftime("%Y-%m-%d")) finished_milestone_text = "Done" form_errors = False if request.method == 'POST': + # parse out individual milestone forms for prefix in request.POST.getlist("prefix"): if not prefix: # empty form continue @@ -144,116 +247,29 @@ def edit_milestones(request, acronym, milestone_set="current"): form_errors = form_errors or not f.is_valid() - if not form_errors: + action = request.POST.get("action", "review") + if action == "review": for f in forms: - c = f.cleaned_data + if not f.is_valid(): + continue - if f.milestone: - m = f.milestone + # let's fill in the form milestone so we can output it in the template + if not f.milestone: + f.milestone = GroupMilestone() + set_attributes_from_form(f, f.milestone) + elif action == "save" and not form_errors: + for f in forms: + change = save_milestone_form(f) - named_milestone = 'milestone "%s"' % m.desc - if milestone_set == "charter": - named_milestone = "charter " + named_milestone + if not change: + continue - if c["delete"]: - save_milestone_in_history(m) - - m.state_id = "deleted" - m.save() - - add_event(m, 'Deleted %s' % named_milestone) - - continue - - # compute changes - history = None - - changes = ['Changed %s' % named_milestone] - - if m.state_id == "review" and not needs_review and c["accept"] != "noaction": - if not history: - history = save_milestone_in_history(m) - - if c["accept"] == "accept": - m.state_id = "active" - changes.append("changed state from review to active, accepting new milestone") - elif c["accept"] == "reject": - m.state_id = "deleted" - changes.append("changed state from review to deleted, rejecting new milestone") - - - if c["desc"] != m.desc and not needs_review: - if not history: - history = save_milestone_in_history(m) - m.desc = c["desc"] - changes.append('changed description to "%s"' % m.desc) - - if c["due"] != m.due: - if not history: - history = save_milestone_in_history(m) - m.due = c["due"] - changes.append('changed due date to %s' % m.due.strftime("%Y-%m-%d")) - - resolved = c["resolved"] - if resolved != m.resolved: - if resolved and not m.resolved: - changes.append('resolved as "%s"' % resolved) - elif not resolved and m.resolved: - changes.append("reverted to not being resolved") - elif resolved and m.resolved: - changes.append('changed resolution to "%s"' % resolved) - - if not history: - history = save_milestone_in_history(m) - - m.resolved = resolved - - new_docs = set(c["docs"]) - old_docs = set(m.docs.all()) - if new_docs != old_docs: - added = new_docs - old_docs - if added: - changes.append('added %s to milestone' % ", ".join(d.name for d in added)) - - removed = old_docs - new_docs - if removed: - changes.append('removed %s from milestone' % ", ".join(d.name for d in removed)) - - if not history: - history = save_milestone_in_history(m) - - m.docs = new_docs - - if len(changes) > 1: - add_event(m, ", ".join(changes)) - - m.save() - - else: # new milestone - m = GroupMilestone() - m.group = group - if milestone_set == "current": - if needs_review: - m.state = GroupMilestoneStateName.objects.get(slug="review") - else: - m.state = GroupMilestoneStateName.objects.get(slug="active") - elif milestone_set == "charter": - m.state = GroupMilestoneStateName.objects.get(slug="charter") - m.desc = c["desc"] - m.due = c["due"] - m.resolved = c["resolved"] - m.save() - - m.docs = c["docs"] - - named_milestone = 'milestone "%s"' % m.desc - if milestone_set == "charter": - named_milestone = "charter " + named_milestone - - if m.state_id in ("active", "charter"): - add_event(m, 'Added %s, due %s' % (named_milestone, m.due.strftime("%Y-%m-%d"))) - elif m.state_id == "review": - add_event(m, 'Added %s for review, due %s' % (named_milestone, m.due.strftime("%Y-%m-%d"))) + if milestone_set == "charter": + DocEvent.objects.create(doc=group.charter, type="changed_charter_milestone", + by=login, desc=change) + else: + MilestoneGroupEvent.objects.create(group=group, type="changed_milestone", + by=login, desc=change, milestone=f.milestone) if milestone_set == "charter": return redirect('doc_view', name=group.charter.canonical_name()) @@ -267,6 +283,8 @@ def edit_milestones(request, acronym, milestone_set="current"): empty_form = MilestoneForm(needs_review=needs_review) + forms.sort(key=lambda f: f.milestone.due if f.milestone else datetime.date.max) + return render_to_response('wginfo/edit_milestones.html', dict(group=group, title=title, diff --git a/ietf/wginfo/tests.py b/ietf/wginfo/tests.py index e676dfa41..9aadc78bb 100644 --- a/ietf/wginfo/tests.py +++ b/ietf/wginfo/tests.py @@ -316,6 +316,7 @@ class MilestoneTestCase(django.test.TestCase): 'm-1-due': due.strftime("%Y-%m-%d"), 'm-1-resolved': "", 'm-1-docs': ",".join(docs), + 'action': "save", }) self.assertEquals(r.status_code, 200) q = PyQuery(r.content) @@ -329,6 +330,7 @@ class MilestoneTestCase(django.test.TestCase): 'm-1-due': due.strftime("%Y-%m-%d"), 'm-1-resolved': "", 'm-1-docs': ",".join(docs), + 'action': "save", }) self.assertEquals(r.status_code, 302) self.assertEquals(GroupMilestone.objects.count(), milestones_before + 1) @@ -362,6 +364,7 @@ class MilestoneTestCase(django.test.TestCase): 'm-1-due': due.strftime("%Y-%m-%d"), 'm-1-resolved': "", 'm-1-docs': "", + 'action': "save", }) self.assertEquals(r.status_code, 302) self.assertEquals(GroupMilestone.objects.count(), milestones_before + 1) @@ -394,6 +397,7 @@ class MilestoneTestCase(django.test.TestCase): 'm1-resolved': m1.resolved, 'm1-docs': ",".join(m1.docs.values_list("name", flat=True)), 'm1-accept': "accept", + 'action': "save", }) self.assertEquals(r.status_code, 302) @@ -419,6 +423,7 @@ class MilestoneTestCase(django.test.TestCase): 'm1-resolved': "", 'm1-docs': ",".join(m1.docs.values_list("name", flat=True)), 'm1-delete': "checked", + 'action': "save", }) self.assertEquals(r.status_code, 302) self.assertEquals(GroupMilestone.objects.count(), milestones_before) @@ -447,6 +452,7 @@ class MilestoneTestCase(django.test.TestCase): 'm1-due': due.strftime("%Y-%m-%d"), 'm1-resolved': "", 'm1-docs': ",".join(docs), + 'action': "save", }) self.assertEquals(r.status_code, 200) q = PyQuery(r.content) @@ -463,6 +469,7 @@ class MilestoneTestCase(django.test.TestCase): 'm1-resolved': "Done", 'm1-resolved_checkbox': "checked", 'm1-docs': ",".join(docs), + 'action': "save", }) self.assertEquals(r.status_code, 302) self.assertEquals(GroupMilestone.objects.count(), milestones_before) diff --git a/static/js/edit-milestones.js b/static/js/edit-milestones.js index 6a2867b76..afc3e1edb 100644 --- a/static/js/edit-milestones.js +++ b/static/js/edit-milestones.js @@ -8,8 +8,19 @@ jQuery(function () { idCounter = v - 1; }); + function setSubmitButtonState() { + var action, label; + if (jQuery("#milestones-form input[name$=desc]:visible").length > 0) + action = "review"; + else + action = "save"; - jQuery("tr.milestone").click(function () { + var submit = jQuery("#milestones-form input[type=submit]"); + submit.val(submit.data("label-" + action)); + jQuery("#milestones-form input[name=action]").val(action); + } + + jQuery("#milestones-form tr.milestone").click(function () { var row = jQuery(this), editRow = row.next("tr.edit-milestone"); if (row.hasClass("add")) { @@ -45,6 +56,8 @@ jQuery(function () { editRow.find('input[name$="expanded_for_editing"]').val("True"); editRow.find('input[name$="desc"]').focus(); + + setSubmitButtonState(); }); function setResolvedState() { @@ -74,8 +87,10 @@ jQuery(function () { var top = jQuery(this).closest(".edit-milestone"); if (jQuery(this).is(":checked")) { - if (+top.find('input[name$="id"]').val() < 0) + if (+top.find('input[name$="id"]').val() < 0) { top.remove(); + setSubmitButtonState(); + } else top.addClass("delete") } @@ -93,8 +108,7 @@ jQuery(function () { setInputMasks(jQuery("#milestone-form .edit-milestone").not(".template")); - jQuery('#milestones-form .edit-milestone input[name$="expanded_for_editing"]').each(function () { - if (this.value == "True") - jQuery(this).closest(".edit-milestone").prev().click(); + jQuery('#milestones-form .edit-milestone .errorlist').each(function () { + jQuery(this).closest(".edit-milestone").prev().click(); }); }); From 2b5345cf670ee338c986ea4f8b05fe4917d45af1 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Thu, 28 Jun 2012 19:00:24 +0000 Subject: [PATCH 08/30] Add some polish, send email notifications on milestone changes - Legacy-Id: 4544 --- ietf/wgcharter/views.py | 2 +- ietf/wginfo/mails.py | 27 +++++++++++++++++++++++++++ ietf/wginfo/milestones.py | 17 ++++++++++++----- ietf/wginfo/tests.py | 3 +++ 4 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 ietf/wginfo/mails.py diff --git a/ietf/wgcharter/views.py b/ietf/wgcharter/views.py index 97d09166f..129c8e363 100644 --- a/ietf/wgcharter/views.py +++ b/ietf/wgcharter/views.py @@ -586,7 +586,7 @@ def approve(request, name): o.save() MilestoneGroupEvent.objects.create( group=group, type="changed_milestone", by=login, - desc="Changed milestone \"%s\", changed state from review to active" % o.desc, + desc="Changed milestone \"%s\", set state to active from review" % o.desc, milestone=o) del milestones_to_delete[i] diff --git a/ietf/wginfo/mails.py b/ietf/wginfo/mails.py new file mode 100644 index 000000000..3c346301f --- /dev/null +++ b/ietf/wginfo/mails.py @@ -0,0 +1,27 @@ +# generation of mails + +import textwrap, datetime + +from django.template.loader import render_to_string +from django.utils.html import strip_tags +from django.utils.text import wrap +from django.conf import settings +from django.core.urlresolvers import reverse as urlreverse + +from ietf.utils.mail import send_mail, send_mail_text + +def email_milestones_changed(request, group, text): + to = [] + if group.ad: + to.append(group.ad.role_email("ad").formatted_email()) + + for r in group.role_set.filter(name="chair"): + to.append(r.formatted_email()) + + text = wrap(strip_tags(text), 70) + text += "\n\n" + text += "URL: %s" % (settings.IDTRACKER_BASE_URL + urlreverse("wg_charter", kwargs=dict(acronym=group.acronym))) + + send_mail_text(request, to, None, + "Milestones changed for %s %s" % (group.acronym, group.type.name), + text) diff --git a/ietf/wginfo/milestones.py b/ietf/wginfo/milestones.py index 2850c1d98..c52a69142 100644 --- a/ietf/wginfo/milestones.py +++ b/ietf/wginfo/milestones.py @@ -17,6 +17,7 @@ from ietf.doc.models import Document, DocEvent from ietf.doc.utils import get_chartering_type from ietf.group.models import * from ietf.group.utils import save_group_in_history, save_milestone_in_history +from ietf.wginfo.mails import email_milestones_changed def json_doc_names(docs): return simplejson.dumps([{"id": doc.pk, "name": doc.name } for doc in docs]) @@ -160,23 +161,23 @@ def edit_milestones(request, acronym, milestone_set="current"): if c["accept"] == "accept": m.state_id = "active" - changes.append("changed state from review to active, accepting new milestone") + changes.append("set state to active from review, accepting new milestone") elif c["accept"] == "reject": m.state_id = "deleted" - changes.append("changed state from review to deleted, rejecting new milestone") + changes.append("set state to deleted from review, rejecting new milestone") if c["desc"] != m.desc and not needs_review: if not history: history = save_milestone_in_history(m) m.desc = c["desc"] - changes.append('changed description to "%s"' % m.desc) + changes.append('set description to "%s"' % m.desc) if c["due"] != m.due: if not history: history = save_milestone_in_history(m) + changes.append('set due date to %s from %s' % (c["due"].strftime("%Y-%m-%d"), m.due.strftime("%Y-%m-%d"))) m.due = c["due"] - changes.append('changed due date to %s' % m.due.strftime("%Y-%m-%d")) resolved = c["resolved"] if resolved != m.resolved: @@ -185,7 +186,7 @@ def edit_milestones(request, acronym, milestone_set="current"): elif not resolved and m.resolved: changes.append("reverted to not being resolved") elif resolved and m.resolved: - changes.append('changed resolution to "%s"' % resolved) + changes.append('set resolution to "%s"' % resolved) if not history: history = save_milestone_in_history(m) @@ -258,6 +259,7 @@ def edit_milestones(request, acronym, milestone_set="current"): f.milestone = GroupMilestone() set_attributes_from_form(f, f.milestone) elif action == "save" and not form_errors: + changes = [] for f in forms: change = save_milestone_form(f) @@ -271,6 +273,11 @@ def edit_milestones(request, acronym, milestone_set="current"): MilestoneGroupEvent.objects.create(group=group, type="changed_milestone", by=login, desc=change, milestone=f.milestone) + changes.append(change) + + if milestone_set == "current": + email_milestones_changed(request, group, u"\n\n".join(c + "." for c in changes)) + if milestone_set == "charter": return redirect('doc_view', name=group.charter.canonical_name()) else: diff --git a/ietf/wginfo/tests.py b/ietf/wginfo/tests.py index 9aadc78bb..094071066 100644 --- a/ietf/wginfo/tests.py +++ b/ietf/wginfo/tests.py @@ -462,6 +462,7 @@ class MilestoneTestCase(django.test.TestCase): self.assertEquals(m.due, m1.due) # add + mailbox_before = len(outbox) r = self.client.post(url, { 'prefix': "m1", 'm1-id': m1.id, 'm1-desc': "Test 2 - changed", @@ -481,6 +482,8 @@ class MilestoneTestCase(django.test.TestCase): self.assertEquals(m.resolved, "Done") self.assertEquals(set(m.docs.values_list("name", flat=True)), set(docs)) self.assertTrue("Changed milestone" in m.milestonegroupevent_set.all()[0].desc) + self.assertEquals(len(outbox), mailbox_before + 1) + self.assertTrue("Milestones changed" in outbox[-1]["Subject"]) def test_reset_charter_milestones(self): m1, m2, group = self.create_test_milestones() From 488add5004807d6fbe3802fdee26400545a63f84 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Sat, 30 Jun 2012 16:46:31 +0000 Subject: [PATCH 09/30] Add script and code (and tests) to send milestone reminders to ADs (for reviews) and group chairs. - Legacy-Id: 4557 --- ietf/bin/send-milestone-reminders | 45 ++++++ .../wginfo/reminder_milestones_due.txt | 7 + .../reminder_milestones_need_review.txt | 10 ++ .../wginfo/reminder_milestones_overdue.txt | 7 + ietf/utils/test_data.py | 3 + ietf/wginfo/mails.py | 94 ++++++++++++- ietf/wginfo/tests.py | 130 +++++++++++++++++- 7 files changed, 293 insertions(+), 3 deletions(-) create mode 100755 ietf/bin/send-milestone-reminders create mode 100644 ietf/templates/wginfo/reminder_milestones_due.txt create mode 100644 ietf/templates/wginfo/reminder_milestones_need_review.txt create mode 100644 ietf/templates/wginfo/reminder_milestones_overdue.txt diff --git a/ietf/bin/send-milestone-reminders b/ietf/bin/send-milestone-reminders new file mode 100755 index 000000000..a23a7bcb9 --- /dev/null +++ b/ietf/bin/send-milestone-reminders @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# +# This script will send various milestone reminders. It's supposed to +# be run daily, and will then send reminders weekly/monthly as +# appropriate. + +import datetime, os +import syslog + +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.wginfo.mails import * + +today = datetime.date.today() + +MONDAY = 1 +FIRST_DAY_OF_MONTH = 1 + +if today.isoweekday() == MONDAY: + # send milestone review reminders - ideally we'd keep track of + # exactly when we sent one last time for a group, but it's a bit + # complicated because people can change the milestones in the mean + # time, so dodge all of this by simply sending once a week only + for g in groups_with_milestones_needing_review(): + mail_sent = email_milestone_review_reminder(g, grace_period=7) + if mail_sent: + syslog.syslog("Sent milestone review reminder for %s %s" % (g.acronym, g.type.name)) + + +early_warning_days = 30 + +# send any milestones due reminders +for g in groups_needing_milestones_due_reminder(early_warning_days): + email_milestones_due(g, early_warning_days) + syslog.syslog("Sent milestones due reminder for %s %s" % (g.acronym, g.type.name)) + +if today.day == FIRST_DAY_OF_MONTH: + # send milestone overdue reminders - once a month + for g in groups_needing_milestones_overdue_reminder(grace_period=30): + email_milestones_overdue(g) + syslog.syslog("Sent milestones overdue reminder for %s %s" % (g.acronym, g.type.name)) diff --git a/ietf/templates/wginfo/reminder_milestones_due.txt b/ietf/templates/wginfo/reminder_milestones_due.txt new file mode 100644 index 000000000..ad905e576 --- /dev/null +++ b/ietf/templates/wginfo/reminder_milestones_due.txt @@ -0,0 +1,7 @@ +{% autoescape off %}{% filter wordwrap:73 %}This is a reminder that milestones in "{{ group.name }}" are soon due. + +{% for m in milestones %}"{{ m.desc }}" is due {% if m.due == today %}today!{% else %}in {{ early_warning_days }} days.{% endif %} + +{% endfor %} +URL: {{ url }} +{% endfilter %}{% endautoescape %} diff --git a/ietf/templates/wginfo/reminder_milestones_need_review.txt b/ietf/templates/wginfo/reminder_milestones_need_review.txt new file mode 100644 index 000000000..e32dbfac7 --- /dev/null +++ b/ietf/templates/wginfo/reminder_milestones_need_review.txt @@ -0,0 +1,10 @@ +{% autoescape off %}{% filter wordwrap:73 %}{{ milestones|length }} new milestone{{ milestones|pluralize }} in "{{ group.name }}" {% if milestones|length > 1 %}need{% else %}needs{%endif %} an AD review: + +{% for m in milestones %}"{{ m.desc }}"{% if m.days_ready != None %} +Waiting for {{ m.days_ready }} day{{ m.days_ready|pluralize }}.{% endif %} + +{% endfor %} +Go here to either accept or reject the new milestones: + +{{ url }} +{% endfilter %}{% endautoescape %} diff --git a/ietf/templates/wginfo/reminder_milestones_overdue.txt b/ietf/templates/wginfo/reminder_milestones_overdue.txt new file mode 100644 index 000000000..9acaf096f --- /dev/null +++ b/ietf/templates/wginfo/reminder_milestones_overdue.txt @@ -0,0 +1,7 @@ +{% autoescape off %}{% filter wordwrap:73 %}This is a reminder that milestones in "{{ group.name }}" are overdue. + +{% for m in milestones %}"{{ m.desc }}" is overdue{% if m.months_overdue > 0 %} with {{ m.months_overdue }} month{{ m.months_overdue|pluralize }}{% endif %}! + +{% endfor %} +URL: {{ url }} +{% endfilter %}{% endautoescape %} diff --git a/ietf/utils/test_data.py b/ietf/utils/test_data.py index 0494be591..3663a9ce8 100644 --- a/ietf/utils/test_data.py +++ b/ietf/utils/test_data.py @@ -147,6 +147,9 @@ def make_test_data(): person=p, email=email) + mars_wg.ad = ad + mars_wg.save() + # create a bunch of ads for swarm tests for i in range(1, 10): u = User.objects.create(username="ad%s" % i) diff --git a/ietf/wginfo/mails.py b/ietf/wginfo/mails.py index 3c346301f..f967a2e4f 100644 --- a/ietf/wginfo/mails.py +++ b/ietf/wginfo/mails.py @@ -10,6 +10,8 @@ from django.core.urlresolvers import reverse as urlreverse from ietf.utils.mail import send_mail, send_mail_text +from ietf.group.models import * + def email_milestones_changed(request, group, text): to = [] if group.ad: @@ -20,8 +22,96 @@ def email_milestones_changed(request, group, text): text = wrap(strip_tags(text), 70) text += "\n\n" - text += "URL: %s" % (settings.IDTRACKER_BASE_URL + urlreverse("wg_charter", kwargs=dict(acronym=group.acronym))) + text += u"URL: %s" % (settings.IDTRACKER_BASE_URL + urlreverse("wg_charter", kwargs=dict(acronym=group.acronym))) send_mail_text(request, to, None, - "Milestones changed for %s %s" % (group.acronym, group.type.name), + u"Milestones changed for %s %s" % (group.acronym, group.type.name), text) + +def email_milestone_review_reminder(group, grace_period=7): + """Email reminders about milestones needing review to AD.""" + if not group.ad: + return False + + to = [group.ad.role_email("ad").formatted_email()] + cc = [r.formatted_email() for r in group.role_set.filter(name="chair")] + + now = datetime.datetime.now() + too_early = True + + milestones = group.groupmilestone_set.filter(state="review") + for m in milestones: + e = m.milestonegroupevent_set.filter(type="changed_milestone").order_by("-time")[:1] + m.days_ready = (now - e[0].time).days if e else None + + if m.days_ready == None or m.days_ready >= grace_period: + too_early = False + + if too_early: + return False + + subject = u"Reminder: Milestone%s needing review in %s %s" % ("s" if len(milestones) > 1 else "", group.acronym, group.type.name) + + send_mail(None, to, None, + subject, + "wginfo/reminder_milestones_need_review.txt", + dict(group=group, + milestones=milestones, + url=settings.IDTRACKER_BASE_URL + urlreverse("wg_edit_milestones", kwargs=dict(acronym=group.acronym)) + )) + + return True + +def groups_with_milestones_needing_review(): + return Group.objects.filter(groupmilestone__state="review").distinct() + +def email_milestones_due(group, early_warning_days): + to = [r.formatted_email() for r in group.role_set.filter(name="chair")] + + today = datetime.date.today() + early_warning = today + datetime.timedelta(days=early_warning_days) + + milestones = group.groupmilestone_set.filter(due__in=[today, early_warning], + resolved="", state="active") + + subject = u"Reminder: Milestone%s are soon due in %s %s" % ("s" if len(milestones) > 1 else "", group.acronym, group.type.name) + + send_mail(None, to, None, + subject, + "wginfo/reminder_milestones_due.txt", + dict(group=group, + milestones=milestones, + today=today, + early_warning_days=early_warning_days, + url=settings.IDTRACKER_BASE_URL + urlreverse("wg_charter", kwargs=dict(acronym=group.acronym)) + )) + +def groups_needing_milestones_due_reminder(early_warning_days): + """Return groups having milestones that are either + early_warning_days from being due or are due today.""" + today = datetime.date.today() + return Group.objects.filter(state="active", groupmilestone__due__in=[today, today + datetime.timedelta(days=early_warning_days)], groupmilestone__resolved="", groupmilestone__state="active").distinct() + +def email_milestones_overdue(group): + to = [r.formatted_email() for r in group.role_set.filter(name="chair")] + + today = datetime.date.today() + + milestones = group.groupmilestone_set.filter(due__lt=today, resolved="", state="active") + for m in milestones: + m.months_overdue = (today - m.due).days // 30 + + subject = u"Reminder: Milestone%s overdue in %s %s" % ("s" if len(milestones) > 1 else "", group.acronym, group.type.name) + + send_mail(None, to, None, + subject, + "wginfo/reminder_milestones_overdue.txt", + dict(group=group, + milestones=milestones, + url=settings.IDTRACKER_BASE_URL + urlreverse("wg_charter", kwargs=dict(acronym=group.acronym)) + )) + +def groups_needing_milestones_overdue_reminder(grace_period=30): + cut_off = datetime.date.today() - datetime.timedelta(days=grace_period) + return Group.objects.filter(state="active", groupmilestone__due__lt=cut_off, groupmilestone__resolved="", groupmilestone__state="active").distinct() + diff --git a/ietf/wginfo/tests.py b/ietf/wginfo/tests.py index 094071066..0c568ffbd 100644 --- a/ietf/wginfo/tests.py +++ b/ietf/wginfo/tests.py @@ -47,6 +47,7 @@ from ietf.group.models import * from ietf.group.utils import * from ietf.name.models import * from ietf.person.models import * +from ietf.wginfo.mails import * class WgInfoUrlTestCase(SimpleUrlTestCase): @@ -404,7 +405,7 @@ class MilestoneTestCase(django.test.TestCase): m = GroupMilestone.objects.get(pk=m1.pk) self.assertEquals(m.state_id, "active") self.assertEquals(group.groupevent_set.count(), events_before + 1) - self.assertTrue("from review to active" in m.milestonegroupevent_set.all()[0].desc) + self.assertTrue("to active from review" in m.milestonegroupevent_set.all()[0].desc) def test_delete_milestone(self): m1, m2, group = self.create_test_milestones() @@ -508,3 +509,130 @@ class MilestoneTestCase(django.test.TestCase): self.assertEquals(GroupMilestone.objects.filter(due=m1.due, desc=m1.desc, state="charter").count(), 1) self.assertEquals(group.charter.docevent_set.count(), events_before + 2) # 1 delete, 1 add + + def test_send_review_needed_reminders(self): + draft = make_test_data() + + group = Group.objects.get(acronym="mars") + person = Person.objects.get(user__username="marschairman") + + m1 = GroupMilestone.objects.create(group=group, + desc="Test 1", + due=datetime.date.today(), + resolved="", + state_id="review") + MilestoneGroupEvent.objects.create( + group=group, type="changed_milestone", + by=person, desc='Added milestone "%s"' % m1.desc, milestone=m1, + time=datetime.datetime.now() - datetime.timedelta(seconds=60)) + + # send + mailbox_before = len(outbox) + for g in groups_with_milestones_needing_review(): + email_milestone_review_reminder(g) + + self.assertEquals(len(outbox), mailbox_before) # too early to send reminder + + + # add earlier added milestone + m2 = GroupMilestone.objects.create(group=group, + desc="Test 2", + due=datetime.date.today(), + resolved="", + state_id="review") + MilestoneGroupEvent.objects.create( + group=group, type="changed_milestone", + by=person, desc='Added milestone "%s"' % m2.desc, milestone=m2, + time=datetime.datetime.now() - datetime.timedelta(days=10)) + + # send + mailbox_before = len(outbox) + for g in groups_with_milestones_needing_review(): + email_milestone_review_reminder(g) + + self.assertEquals(len(outbox), mailbox_before + 1) + self.assertTrue(group.acronym in outbox[-1]["Subject"]) + self.assertTrue(m1.desc in unicode(outbox[-1])) + self.assertTrue(m2.desc in unicode(outbox[-1])) + + def test_send_milestones_due_reminders(self): + draft = make_test_data() + + group = Group.objects.get(acronym="mars") + person = Person.objects.get(user__username="marschairman") + + early_warning_days = 30 + + m1 = GroupMilestone.objects.create(group=group, + desc="Test 1", + due=datetime.date.today(), + resolved="Done", + state_id="active") + m2 = GroupMilestone.objects.create(group=group, + desc="Test 2", + due=datetime.date.today() + datetime.timedelta(days=early_warning_days - 10), + resolved="", + state_id="active") + + # send + mailbox_before = len(outbox) + for g in groups_needing_milestones_due_reminder(early_warning_days): + email_milestones_due(g, early_warning_days) + + self.assertEquals(len(outbox), mailbox_before) # none found + + m1.resolved = "" + m1.save() + + m2.due = datetime.date.today() + datetime.timedelta(days=early_warning_days) + m2.save() + + # send + mailbox_before = len(outbox) + for g in groups_needing_milestones_due_reminder(early_warning_days): + email_milestones_due(g, early_warning_days) + + self.assertEquals(len(outbox), mailbox_before + 1) + self.assertTrue(group.acronym in outbox[-1]["Subject"]) + self.assertTrue(m1.desc in unicode(outbox[-1])) + self.assertTrue(m2.desc in unicode(outbox[-1])) + + def test_send_milestones_overdue_reminders(self): + draft = make_test_data() + + group = Group.objects.get(acronym="mars") + person = Person.objects.get(user__username="marschairman") + + m1 = GroupMilestone.objects.create(group=group, + desc="Test 1", + due=datetime.date.today() - datetime.timedelta(days=200), + resolved="Done", + state_id="active") + m2 = GroupMilestone.objects.create(group=group, + desc="Test 2", + due=datetime.date.today() - datetime.timedelta(days=10), + resolved="", + state_id="active") + + # send + mailbox_before = len(outbox) + for g in groups_needing_milestones_overdue_reminder(grace_period=30): + email_milestones_overdue(g) + + self.assertEquals(len(outbox), mailbox_before) # none found + + m1.resolved = "" + m1.save() + + m2.due = datetime.date.today() - datetime.timedelta(days=300) + m2.save() + + # send + mailbox_before = len(outbox) + for g in groups_needing_milestones_overdue_reminder(grace_period=30): + email_milestones_overdue(g) + + self.assertEquals(len(outbox), mailbox_before + 1) + self.assertTrue(group.acronym in outbox[-1]["Subject"]) + self.assertTrue(m1.desc in unicode(outbox[-1])) + self.assertTrue(m2.desc in unicode(outbox[-1])) From 02509a4d8c49851650ffbdb8fbf9a8f86a304ad2 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Sat, 30 Jun 2012 18:27:55 +0000 Subject: [PATCH 10/30] Fix a couple of bugs in handling editing documents associated with milestones, also include those documents in the initial output on the edit page - Legacy-Id: 4558 --- ietf/templates/wginfo/edit_milestones.html | 11 ++++++++--- ietf/templates/wginfo/milestone_form.html | 2 +- ietf/templates/wginfo/milestones.html | 2 +- ietf/wginfo/milestones.py | 8 ++++++-- static/js/edit-milestones.js | 2 +- static/js/tokenized-field.js | 7 ++++--- 6 files changed, 21 insertions(+), 11 deletions(-) diff --git a/ietf/templates/wginfo/edit_milestones.html b/ietf/templates/wginfo/edit_milestones.html index 18ea6f1a1..370ee0b30 100644 --- a/ietf/templates/wginfo/edit_milestones.html +++ b/ietf/templates/wginfo/edit_milestones.html @@ -7,6 +7,7 @@ tr.milestone td { padding: 0.2em 0; cursor: pointer; vertical-align: top; } tr.milestone:hover { background-color: #e8f0fa; } td.due { width: 5em; } .milestone .needs-accept { font-style: italic; display: inline-block; margin-left: 0.5em; color: #2647a0; } +.milestone .doc { display: block; padding-left: 1em; } .edit-milestone { display: none; } .edit-milestone.delete, .edit-milestone.delete input { color: #aaa !important; } .edit-milestone table { margin: 0.3em 0; } @@ -65,8 +66,12 @@ this list to the currently in-use milestones for the {{ group.acronym }} {{ @@ -78,7 +83,7 @@ this list to the currently in-use milestones for the {{ group.acronym }} {{
Back - +
diff --git a/ietf/templates/wginfo/milestone_form.html b/ietf/templates/wginfo/milestone_form.html index bb85d1d3d..1360dfc22 100644 --- a/ietf/templates/wginfo/milestone_form.html +++ b/ietf/templates/wginfo/milestone_form.html @@ -26,7 +26,7 @@ - diff --git a/ietf/templates/wginfo/milestones.html b/ietf/templates/wginfo/milestones.html index 3cebdab49..ee811c291 100644 --- a/ietf/templates/wginfo/milestones.html +++ b/ietf/templates/wginfo/milestones.html @@ -9,7 +9,7 @@ diff --git a/ietf/wginfo/milestones.py b/ietf/wginfo/milestones.py index c52a69142..63aa1206e 100644 --- a/ietf/wginfo/milestones.py +++ b/ietf/wginfo/milestones.py @@ -62,13 +62,17 @@ class MilestoneForm(forms.Form): super(MilestoneForm, self).__init__(*args, **kwargs) + # figure out what to prepopulate many-to-many field with pre = "" if not self.is_bound: pre = self.initial.get("docs", "") else: pre = self["docs"].data or "" - self.fields["docs"].prepopulate = json_doc_names(parse_doc_names(pre)) + # this is ugly, but putting it on self["docs"] is buggy with a + # bound/unbound form in Django 1.2 + self.docs_names = parse_doc_names(pre) + self.docs_prepopulate = json_doc_names(self.docs_names) def clean_docs(self): s = self.cleaned_data["docs"] @@ -322,7 +326,7 @@ def reset_charter_milestones(request, acronym): try: milestone_ids = [int(v) for v in request.POST.getlist("milestone")] except ValueError as e: - return HttpResponseBadRequest("errror in list of ids - %s" % e) + return HttpResponseBadRequest("error in list of ids - %s" % e) # delete existing for m in charter_milestones: diff --git a/static/js/edit-milestones.js b/static/js/edit-milestones.js index afc3e1edb..4d734db7c 100644 --- a/static/js/edit-milestones.js +++ b/static/js/edit-milestones.js @@ -16,7 +16,7 @@ jQuery(function () { action = "save"; var submit = jQuery("#milestones-form input[type=submit]"); - submit.val(submit.data("label-" + action)); + submit.val(submit.data("label" + action)); jQuery("#milestones-form input[name=action]").val(action); } diff --git a/static/js/tokenized-field.js b/static/js/tokenized-field.js index 962f64369..3e3ded052 100644 --- a/static/js/tokenized-field.js +++ b/static/js/tokenized-field.js @@ -4,10 +4,11 @@ function setupTokenizedField(field) { var pre = []; if (field.val()) - pre = JSON.parse(field.val()); + pre = JSON.parse((field.val() || "").replace(/"/g, '"')); else if (field.data("pre")) - pre = JSON.parse(field.data("pre")); - field.tokenInput(field.data("ajax-url"), { + pre = JSON.parse((field.attr("data-pre") || "").replace(/"/g, '"')); + + field.tokenInput(field.data("ajaxurl"), { hintText: "", preventDuplicates: true, prePopulate: pre From 7b8928fe017dbc3676b7fc16921d64b92434b4b0 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Sun, 1 Jul 2012 08:55:36 +0000 Subject: [PATCH 11/30] Make milestones visible in draft search results, and add notices to the draft state edit form for WG chairs, as reminders for the WG chairs to keep the milestones updated - Legacy-Id: 4559 --- ietf/idrfc/views_doc.py | 1 + ietf/ietfworkflows/templatetags/ietf_streams.py | 4 +++- ietf/ietfworkflows/views.py | 2 ++ ietf/templates/idrfc/doc_tab_document_id.html | 9 +++++---- ietf/templates/ietfworkflows/state_edit.html | 13 +++++++++++++ ietf/templates/ietfworkflows/stream_state.html | 5 +++-- static/css/base2.css | 2 ++ 7 files changed, 29 insertions(+), 7 deletions(-) diff --git a/ietf/idrfc/views_doc.py b/ietf/idrfc/views_doc.py index a852432a9..eb9960f80 100644 --- a/ietf/idrfc/views_doc.py +++ b/ietf/idrfc/views_doc.py @@ -439,6 +439,7 @@ def document_main_idrfc(request, name, tab): 'doc':doc, 'info':info, 'tab':tab, 'include_text':include_text(request), 'stream_info': get_full_info_for_draft(id), + 'milestones': id.groupmilestone_set.filter(state="active"), 'versions':versions, 'history':history}, context_instance=RequestContext(request)); diff --git a/ietf/ietfworkflows/templatetags/ietf_streams.py b/ietf/ietfworkflows/templatetags/ietf_streams.py index 13a040554..d01581e4d 100644 --- a/ietf/ietfworkflows/templatetags/ietf_streams.py +++ b/ietf/ietfworkflows/templatetags/ietf_streams.py @@ -40,7 +40,9 @@ def stream_state(context, doc): data.update({'workflow': workflow, 'draft': draft, - 'state': state}) + 'state': state, + 'milestones': draft.groupmilestone_set.filter(state="active") + }) return data diff --git a/ietf/ietfworkflows/views.py b/ietf/ietfworkflows/views.py index 62bc5ecd3..26dc6f859 100644 --- a/ietf/ietfworkflows/views.py +++ b/ietf/ietfworkflows/views.py @@ -83,6 +83,7 @@ def _edit_draft_stream(request, draft, form_class=DraftTagsStateForm): stream = get_stream_from_draft(draft) history = get_workflow_history_for_draft(draft, 'objectworkflowhistoryentry') tags = get_annotation_tags_for_draft(draft) + milestones = draft.groupmilestone_set.all() return render_to_response('ietfworkflows/state_edit.html', {'draft': draft, 'state': state, @@ -91,6 +92,7 @@ def _edit_draft_stream(request, draft, form_class=DraftTagsStateForm): 'history': history, 'tags': tags, 'form': form, + 'milestones': milestones, }, context_instance=RequestContext(request)) diff --git a/ietf/templates/idrfc/doc_tab_document_id.html b/ietf/templates/idrfc/doc_tab_document_id.html index 1997ff3d5..5e10b7441 100644 --- a/ietf/templates/idrfc/doc_tab_document_id.html +++ b/ietf/templates/idrfc/doc_tab_document_id.html @@ -71,9 +71,10 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. {% ifequal stream_info.stream.name "IETF" %} - + {% else %} {% if stream_info.stream %} @@ -81,7 +82,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. {% endif %} diff --git a/ietf/templates/ietfworkflows/state_edit.html b/ietf/templates/ietfworkflows/state_edit.html index b2c281fa4..19f0265a4 100644 --- a/ietf/templates/ietfworkflows/state_edit.html +++ b/ietf/templates/ietfworkflows/state_edit.html @@ -29,6 +29,19 @@ table.edit-form-tags ul li { padding: 0px; } Return to document view

+ +{% if state and state.slug == "wg-doc" and not milestones %} +

This document is not part of any milestone. You may wish to add it to one.

+{% endif %} + +{% if state and state.slug == "sub-pub" and milestones %} +

This document is part of {% if milestones|length > 1 %}{{ milestones|length }} +milestones{% else %}a milestone{% endif %}. Now that the draft is +submitted to IESG for publication, you may wish to +update the +milestone{{ milestones|pluralize }}.

+{% endif %} +
{% if form.milestone.resolved %}{{ form.milestone.resolved }}{% else %}{{ form.milestone.due|date:"M Y" }}{% endif %} - {{ form.milestone.desc }} - {% if form.needs_review %}awaiting accept{% endif %} +
{{ form.milestone.desc }} + {% if form.needs_review %}awaiting accept{% endif %}
+ + {% for d in form.docs_names %} +
{{ d }}
+ {% endfor %}
Drafts: + {{ form.docs.errors }}
{{ milestone.desc|escape }}
{% for d in milestone.docs.all %} - {{ d.name }} + {{ d.name }} {% endfor %}
IETF State:{{ stream_info.state.name }} ({{ stream_info.streamed.get_group }}) - {% if stream_info.tags %}
{% for tag in stream_info.tags %}{{ tag.name }}{% if not forloop.last %}, {% endif %}{% endfor %}{% endif %} -
{{ stream_info.state.name }} ({{ stream_info.streamed.get_group }}) + {% if stream_info.tags %}
{% for tag in stream_info.tags %}{{ tag.name }}{% if not forloop.last %}, {% endif %}{% endfor %}{% endif %} + {% if milestones %}{% for m in milestones %}{{ m.due|date:"M Y" }}{% endfor %}{% endif %} +
{{ stream_info.stream.name }} status: {{ stream_info.state.name }} {% if stream_info.streamed.get_group %}({{ stream_info.streamed.get_group }}) {% endif %} - {% if stream_info.tags %}
{% for tag in stream_info.tags %}{{ tag.name }}{% if not forloop.last %}, {% endif %}{% endfor %}{% endif %} + {% if stream_info.tags %}
{% for tag in stream_info.tags %}{{ tag.name }}{% if not forloop.last %}, {% endif %}{% endfor %}{% endif %}
diff --git a/ietf/templates/ietfworkflows/stream_state.html b/ietf/templates/ietfworkflows/stream_state.html index ec531781f..89f97b3d7 100644 --- a/ietf/templates/ietfworkflows/stream_state.html +++ b/ietf/templates/ietfworkflows/stream_state.html @@ -1,8 +1,9 @@ {% if draft %} -
-
+
+
{% if stream %} {% if state %}{{ state.name }}{% else %}{{ stream }}{% endif %} +{% if milestones %}{% for m in milestones %}{{ m.due|date:"M Y" }}{% endfor %}{% endif %} {% else %} No stream assigned {% endif %} diff --git a/static/css/base2.css b/static/css/base2.css index 6e1627a9c..29a70a5c2 100644 --- a/static/css/base2.css +++ b/static/css/base2.css @@ -193,3 +193,5 @@ form table .help { table.milestones td.due { vertical-align: top; width: 80px; } table.milestones .doc { display: block; padding-left: 1em; } + +.stream-state .milestone { display: inline-block; font-size: smaller; background-color: #d5dde6; padding: 0 0.2em; margin-left: 0.3em; } From 419f387bceaa2ed391f4afbfec8b3f7a3f346c3b Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Mon, 2 Jul 2012 18:09:42 +0000 Subject: [PATCH 12/30] Make find_history_active_at return the original object if it is currently active, otherwise we can't see from the return value whether a given time predates the history set. The downside is that the caller may now need to check whether it got a history object or the same object back. - Legacy-Id: 4560 --- ietf/meeting/views.py | 2 +- ietf/utils/history.py | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 5db717a8d..f89345226 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -163,7 +163,7 @@ def agenda_infoREDESIGN(num=None): meeting_time = datetime.datetime.combine(meeting.date, datetime.time(0, 0, 0)) for g in Group.objects.filter(type="area").order_by("acronym"): history = find_history_active_at(g, meeting_time) - if history: + if history and history != g: if history.state_id == "active": ads.extend(IESGHistory().from_role(x, meeting_time) for x in history.rolehistory_set.filter(name="ad").select_related()) else: diff --git a/ietf/utils/history.py b/ietf/utils/history.py index 09e1adaa4..a89ed2182 100644 --- a/ietf/utils/history.py +++ b/ietf/utils/history.py @@ -1,8 +1,9 @@ def find_history_active_at(obj, time): - """Assumes obj has a corresponding history object (e.g. obj could - be Person with a corresponding PersonHistory model), then returns - the history object active at time, or None if the object itself - was active at the time. + """Assumes obj has a corresponding history model (e.g. obj could + be Person with a corresponding PersonHistory model), then either + returns the object itself if it was active at time, or the history + object active at time, or None if time predates the object and its + history (assuming history is complete). For this to work, the history model must use related_name="history_set" for the foreign key connecting to the @@ -11,7 +12,7 @@ def find_history_active_at(obj, time): old time when the time field changes. """ if obj.time <= time: - return None + return obj histories = obj.history_set.order_by('-time') From b68bf9b0b8f1feaeddb5d15d22e40678a2614a8e Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Mon, 2 Jul 2012 18:10:41 +0000 Subject: [PATCH 13/30] Fix comment - Legacy-Id: 4561 --- ietf/utils/history.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ietf/utils/history.py b/ietf/utils/history.py index a89ed2182..24f72e55a 100644 --- a/ietf/utils/history.py +++ b/ietf/utils/history.py @@ -24,9 +24,9 @@ def find_history_active_at(obj, time): def get_history_object_for(obj): """Construct history object for obj, i.e. instantiate history - object, copy relevant attributes and set a link to obj, but done + object, copy relevant attributes and set a link to obj, but don't save. Any customizations can be done by the caller afterwards. - Many-to-many fields are not copied. + Many-to-many fields are not copied (impossible without save). The history model must use related_name="history_set" for the foreign key connecting to the live model for this function to be From bcf6ef0230e44ba4207ed268467432a0004c6776 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Tue, 3 Jul 2012 23:12:42 +0000 Subject: [PATCH 14/30] Add milestone support in charter diffs by outputting them on a special .txt generating page that looks up the milestones available at the time the charter was submitted (actually just before the next one was submitted) - Legacy-Id: 4578 --- ietf/idrfc/views_doc.py | 32 +++++----- .../wgcharter/charter_with_milestones.txt | 8 +++ ietf/wgcharter/feeds.py | 1 - ietf/wgcharter/urls.py | 3 +- ietf/wgcharter/utils.py | 1 - ietf/wgcharter/views.py | 60 ++++++++++++++++++- 6 files changed, 85 insertions(+), 20 deletions(-) create mode 100644 ietf/templates/wgcharter/charter_with_milestones.txt diff --git a/ietf/idrfc/views_doc.py b/ietf/idrfc/views_doc.py index eb9960f80..02d475ded 100644 --- a/ietf/idrfc/views_doc.py +++ b/ietf/idrfc/views_doc.py @@ -170,29 +170,31 @@ def document_history(request, name): doc = get_object_or_404(Document, docalias__name=name) top = render_document_top(request, doc, "history", name) - diff_documents = [ doc ] - diff_documents.extend(Document.objects.filter(docalias__relateddocument__source=doc, docalias__relateddocument__relationship="replaces")) - # pick up revisions from events diff_revisions = [] - seen = set() diffable = name.startswith("draft") or name.startswith("charter") - if diffable: + diff_documents = [ doc ] + diff_documents.extend(Document.objects.filter(docalias__relateddocument__source=doc, docalias__relateddocument__relationship="replaces")) + + seen = set() for e in NewRevisionDocEvent.objects.filter(type="new_revision", doc__in=diff_documents).select_related('doc').order_by("-time", "-id"): - if not (e.doc.name, e.rev) in seen: - seen.add((e.doc.name, e.rev)) + if (e.doc.name, e.rev) in seen: + continue - url = "" - if name.startswith("charter"): - h = find_history_active_at(e.doc, e.time) - url = settings.CHARTER_TXT_URL + ("%s-%s.txt" % ((h or doc).canonical_name(), e.rev)) - elif name.startswith("draft"): - # rfcdiff tool has special support for IDs - url = e.doc.name + "-" + e.rev + seen.add((e.doc.name, e.rev)) - diff_revisions.append((e.doc.name, e.rev, e.time, url)) + url = "" + if name.startswith("charter"): + #h = find_history_active_at(e.doc, e.time) + #url = settings.CHARTER_TXT_URL + ("%s-%s.txt" % ((h or doc).canonical_name(), e.rev)) + url = request.build_absolute_uri(urlreverse("charter_with_milestones_txt", kwargs=dict(name=e.doc.name, rev=e.rev))) + elif name.startswith("draft"): + # rfcdiff tool has special support for IDs + url = e.doc.name + "-" + e.rev + + diff_revisions.append((e.doc.name, e.rev, e.time, url)) # grab event history events = doc.docevent_set.all().order_by("-time", "-id").select_related("by") diff --git a/ietf/templates/wgcharter/charter_with_milestones.txt b/ietf/templates/wgcharter/charter_with_milestones.txt new file mode 100644 index 000000000..cfa74b052 --- /dev/null +++ b/ietf/templates/wgcharter/charter_with_milestones.txt @@ -0,0 +1,8 @@ +{% load ietf_filters %}{% autoescape off %}{% filter wrap_long_lines %}{{ charter_text }}{% endfilter %} + +Milestones + +{% for milestone in milestones %}{% if milestone.resolved %}{{ milestone.resolved|ljust:8 }}{% else %}{{ milestone.due|date:"M Y" }}{% endif %} - {{ milestone.desc_filled }}{% for d in milestone.docs.all %} + o {{ d.name }}{% endfor %} + +{% endfor %}{% endautoescape %} diff --git a/ietf/wgcharter/feeds.py b/ietf/wgcharter/feeds.py index 31369daf8..5abf7708c 100644 --- a/ietf/wgcharter/feeds.py +++ b/ietf/wgcharter/feeds.py @@ -9,7 +9,6 @@ from django.core.urlresolvers import reverse as urlreverse from django.utils.html import strip_tags from django.utils.text import truncate_words -from ietf.utils.history import find_history_active_at from ietf.group.models import Group, GroupEvent from ietf.doc.models import DocEvent diff --git a/ietf/wgcharter/urls.py b/ietf/wgcharter/urls.py index e9b04b5ec..e30d8ac22 100644 --- a/ietf/wgcharter/urls.py +++ b/ietf/wgcharter/urls.py @@ -11,5 +11,6 @@ urlpatterns = patterns('', url(r'^ballotwriteupnotes/$', "ietf.wgcharter.views.ballot_writeupnotes"), url(r'^approve/$', "ietf.wgcharter.views.approve", name='charter_approve'), url(r'^submit/$', "ietf.wgcharter.views.submit", name='charter_submit'), - url(r'^submit/(?P
Current stream
-{% for f in form %} - - - - - -{% endfor %} -
{{ f.label_tag }}{{ f }}{% if not f.field.thursday %}NOT THURSDAY{% endif %}
- -
- - - -
- - -{% endblock %} From 8d07791e236fecbceb937af8cada6ab47f7b534a Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Wed, 4 Jul 2012 18:03:59 +0000 Subject: [PATCH 19/30] Fix bug in milestones edit JS, check for due date rather than desc as WG Chairs can't edit the latter - Legacy-Id: 4590 --- static/js/edit-milestones.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/edit-milestones.js b/static/js/edit-milestones.js index 4d734db7c..58a2610e8 100644 --- a/static/js/edit-milestones.js +++ b/static/js/edit-milestones.js @@ -10,7 +10,7 @@ jQuery(function () { function setSubmitButtonState() { var action, label; - if (jQuery("#milestones-form input[name$=desc]:visible").length > 0) + if (jQuery("#milestones-form input[name$=due]:visible").length > 0) action = "review"; else action = "save"; From 7abb438424515d9a7b173381b81e0a5aa167b71f Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Wed, 4 Jul 2012 18:16:34 +0000 Subject: [PATCH 20/30] Add milestones needing review page for the IESG, replace simple telechat redirect view with a generic view in urls.py - Legacy-Id: 4591 --- ietf/iesg/urls.py | 3 ++- ietf/iesg/views.py | 25 ++++++++++++++--- ietf/templates/base_leftmenu.html | 7 ++--- .../iesg/milestones_needing_review.html | 27 +++++++++++++++++++ 4 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 ietf/templates/iesg/milestones_needing_review.html diff --git a/ietf/iesg/urls.py b/ietf/iesg/urls.py index 98c4a33e4..b709730ac 100644 --- a/ietf/iesg/urls.py +++ b/ietf/iesg/urls.py @@ -59,7 +59,8 @@ urlpatterns += patterns('', (r'^agenda/documents/$', views.agenda_documents), (r'^agenda/telechat-(?P\d+)-(?P\d+)-(?P\d+)-docs.tgz', views.telechat_docs_tarfile), (r'^discusses/$', views.discusses), - (r'^telechatdates/$', views.telechat_dates), + (r'^milestones', views.milestones_needing_review), + (r'^telechatdates/$', 'django.views.generic.simple.redirect_to', { 'url': '/admin/iesg/telechatdate/' }), url(r'^wgactions/$', views.working_group_actions, name="iesg_working_group_actions"), url(r'^wgactions/add/$', views.edit_working_group_action, { 'wga_id': None }, name="iesg_add_working_group_action"), url(r'^wgactions/(?P\d+)/$', views.edit_working_group_action, name="iesg_edit_working_group_action"), diff --git a/ietf/iesg/views.py b/ietf/iesg/views.py index 6c4b03759..8ab2fdec2 100644 --- a/ietf/iesg/views.py +++ b/ietf/iesg/views.py @@ -54,7 +54,7 @@ from ietf.ietfauth.decorators import group_required, role_required from ietf.idtracker.templatetags.ietf_filters import in_group from ietf.ipr.models import IprRfc, IprDraft, IprDetail from ietf.doc.models import Document, TelechatDocEvent -from ietf.group.models import Group +from ietf.group.models import Group, GroupMilestone def date_threshold(): """Return the first day of the month that is 185 days ago.""" @@ -509,9 +509,26 @@ def discusses(request): return direct_to_template(request, 'iesg/discusses.html', {'docs':res}) -@group_required('Secretariat') -def telechat_dates(request): - return HttpResponseRedirect("/admin/iesg/telechatdate/") +@role_required('Area Director', 'Secretariat') +def milestones_needing_review(request): + # collect milestones, grouped on AD and group + ads = {} + for m in GroupMilestone.objects.filter(state="review").exclude(group__state="concluded", group__ad=None).distinct().select_related("group", "group__ad"): + groups = ads.setdefault(m.group.ad, {}) + milestones = groups.setdefault(m.group, []) + milestones.append(m) + + ad_list = [] + for ad, groups in ads.iteritems(): + ad_list.append(ad) + ad.groups_needing_review = sorted(groups, key=lambda g: g.acronym) + for g, milestones in groups.iteritems(): + g.milestones_needing_review = sorted(milestones, key=lambda m: m.due) + + return render_to_response('iesg/milestones_needing_review.html', + dict(ads=sorted(ad_list, key=lambda ad: ad.plain_name()), + ), + context_instance=RequestContext(request)) def parse_wg_action_file(path): f = open(path, 'rU') diff --git a/ietf/templates/base_leftmenu.html b/ietf/templates/base_leftmenu.html index 544ba0c83..1d4463cd9 100644 --- a/ietf/templates/base_leftmenu.html +++ b/ietf/templates/base_leftmenu.html @@ -42,15 +42,16 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  • My Documents
  • Next Telechat
  • Discusses
  • +
  • Milestones
  • + {# FIXME: this link should be removed when the old WG Actions are completely dead #}
  • Working Groups
  • - {# FIXME: wgcharter
  • Working Groups
  • #} {% endif %} {% if user|in_group:"Secretariat" %}
  • Secretariat
  • -
  • Telechat Dates
  • +
  • Telechat Dates
  • Management Items
  • + {# FIXME: this link should be removed when the old WG Actions are completely dead #}
  • Working Groups
  • - {# FIXME: wgcharter
  • Working Groups
  • #} {% endif %} {% if user %} {% get_user_managed_streams user as stream_list %} diff --git a/ietf/templates/iesg/milestones_needing_review.html b/ietf/templates/iesg/milestones_needing_review.html new file mode 100644 index 000000000..3dfb16328 --- /dev/null +++ b/ietf/templates/iesg/milestones_needing_review.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} + +{% block title %}Milestones Needing Review{% endblock %} + +{% block morecss %} +h3.ad { margin-bottom: 0.5em; } +div.milestones-for-group { margin: 0.5em 0; } +{% endblock %} + +{% block content %} +

    Milestones Needing Review

    + +{% for ad in ads %} +

    {{ ad.plain_name }}

    + +{% for g in ad.groups_needing_review %} + +
    New milestones for {{ g.name }} ({{ g.acronym }}):
    + +{% with g.milestones_needing_review as milestones %} +{% include "wginfo/milestones.html" %} +{% endwith %} + +{% endfor %} +{% endfor %} + +{% endblock %} From 6050cbf76a9d0f5067ea11c389a7e7b55764075b Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Wed, 4 Jul 2012 18:29:53 +0000 Subject: [PATCH 21/30] Give test groups a list_email so we can email them - Legacy-Id: 4592 --- ietf/utils/test_data.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ietf/utils/test_data.py b/ietf/utils/test_data.py index 3663a9ce8..75d4eba61 100644 --- a/ietf/utils/test_data.py +++ b/ietf/utils/test_data.py @@ -58,6 +58,7 @@ def make_test_data(): state_id="active", type_id="wg", parent=area, + list_email="mars-wg@ietf.org", ) charter = Document.objects.create( name="charter-ietf-" + group.acronym, @@ -80,6 +81,7 @@ def make_test_data(): state_id="proposed", type_id="wg", parent=area, + list_email="ames-wg@ietf.org", ) charter = Document.objects.create( name="charter-ietf-" + group.acronym, From 95b144515ee27f5162d1b12e7b6f32d4dcb13cec Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Wed, 4 Jul 2012 18:31:18 +0000 Subject: [PATCH 22/30] Email the WG too when making changes to milestones, although filter out any "Adding milestone for review" changes - Legacy-Id: 4593 --- ietf/wginfo/mails.py | 28 ++++++++++++++++++++-------- ietf/wginfo/milestones.py | 2 +- ietf/wginfo/tests.py | 7 +++++-- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/ietf/wginfo/mails.py b/ietf/wginfo/mails.py index f967a2e4f..96b8bb6e8 100644 --- a/ietf/wginfo/mails.py +++ b/ietf/wginfo/mails.py @@ -1,6 +1,6 @@ # generation of mails -import textwrap, datetime +import textwrap, datetime, re from django.template.loader import render_to_string from django.utils.html import strip_tags @@ -12,7 +12,17 @@ from ietf.utils.mail import send_mail, send_mail_text from ietf.group.models import * -def email_milestones_changed(request, group, text): +def email_milestones_changed(request, group, changes): + def wrap_up_email(to, text): + text = wrap(strip_tags(text), 70) + text += "\n\n" + text += u"URL: %s" % (settings.IDTRACKER_BASE_URL + urlreverse("wg_charter", kwargs=dict(acronym=group.acronym))) + + send_mail_text(request, to, None, + u"Milestones changed for %s %s" % (group.acronym, group.type.name), + text) + + # first send to AD and chairs to = [] if group.ad: to.append(group.ad.role_email("ad").formatted_email()) @@ -20,13 +30,15 @@ def email_milestones_changed(request, group, text): for r in group.role_set.filter(name="chair"): to.append(r.formatted_email()) - text = wrap(strip_tags(text), 70) - text += "\n\n" - text += u"URL: %s" % (settings.IDTRACKER_BASE_URL + urlreverse("wg_charter", kwargs=dict(acronym=group.acronym))) + if to: + wrap_up_email(to, u"\n\n".join(c + "." for c in changes)) + + # then send to WG + if group.list_email: + review_re = re.compile("Added .* for review, due") + to = [ group.list_email ] + wrap_up_email(to, u"\n\n".join(c + "." for c in changes if not review_re.match(c))) - send_mail_text(request, to, None, - u"Milestones changed for %s %s" % (group.acronym, group.type.name), - text) def email_milestone_review_reminder(group, grace_period=7): """Email reminders about milestones needing review to AD.""" diff --git a/ietf/wginfo/milestones.py b/ietf/wginfo/milestones.py index eba2f5733..f5094be87 100644 --- a/ietf/wginfo/milestones.py +++ b/ietf/wginfo/milestones.py @@ -280,7 +280,7 @@ def edit_milestones(request, acronym, milestone_set="current"): changes.append(change) if milestone_set == "current": - email_milestones_changed(request, group, u"\n\n".join(c + "." for c in changes)) + email_milestones_changed(request, group, changes) if milestone_set == "charter": return redirect('doc_view', name=group.charter.canonical_name()) diff --git a/ietf/wginfo/tests.py b/ietf/wginfo/tests.py index 0c568ffbd..8e24413d8 100644 --- a/ietf/wginfo/tests.py +++ b/ietf/wginfo/tests.py @@ -462,7 +462,7 @@ class MilestoneTestCase(django.test.TestCase): self.assertEquals(GroupMilestone.objects.count(), milestones_before) self.assertEquals(m.due, m1.due) - # add + # edit mailbox_before = len(outbox) r = self.client.post(url, { 'prefix': "m1", 'm1-id': m1.id, @@ -483,8 +483,11 @@ class MilestoneTestCase(django.test.TestCase): self.assertEquals(m.resolved, "Done") self.assertEquals(set(m.docs.values_list("name", flat=True)), set(docs)) self.assertTrue("Changed milestone" in m.milestonegroupevent_set.all()[0].desc) - self.assertEquals(len(outbox), mailbox_before + 1) + self.assertEquals(len(outbox), mailbox_before + 2) + self.assertTrue("Milestones changed" in outbox[-2]["Subject"]) + self.assertTrue(group.ad.role_email("ad").address in str(outbox[-2])) self.assertTrue("Milestones changed" in outbox[-1]["Subject"]) + self.assertTrue(group.list_email in str(outbox[-1])) def test_reset_charter_milestones(self): m1, m2, group = self.create_test_milestones() From 27935fb7a2b12262abc5d6aba6fd10eda6c290f2 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Thu, 5 Jul 2012 18:57:15 +0000 Subject: [PATCH 23/30] Don't prompt for the day of the month in setting up milestones, instead set the deadline to the last day of the month, also remove any references to the day of the month from the output - Legacy-Id: 4596 --- ietf/templates/wginfo/edit_milestones.html | 1 - ietf/templates/wginfo/milestone_form.html | 4 +- ietf/wginfo/milestones.py | 43 +++- ietf/wginfo/tests.py | 47 ++-- static/js/edit-milestones.js | 9 +- static/js/lib/jquery.maskedinput.js | 258 --------------------- 6 files changed, 68 insertions(+), 294 deletions(-) delete mode 100644 static/js/lib/jquery.maskedinput.js diff --git a/ietf/templates/wginfo/edit_milestones.html b/ietf/templates/wginfo/edit_milestones.html index 370ee0b30..500ea2b2a 100644 --- a/ietf/templates/wginfo/edit_milestones.html +++ b/ietf/templates/wginfo/edit_milestones.html @@ -93,7 +93,6 @@ this list to the currently in-use milestones for the {{ group.acronym }} {{ {% block content_end %} -