diff --git a/ietf/dbtemplate/fixtures/nomcom_templates.xml b/ietf/dbtemplate/fixtures/nomcom_templates.xml index cc0eb32c6..afffb9bb4 100644 --- a/ietf/dbtemplate/fixtures/nomcom_templates.xml +++ b/ietf/dbtemplate/fixtures/nomcom_templates.xml @@ -172,4 +172,14 @@ The questionaire is repeated below for your convenience. -------- + + /nomcom/defaults/topic/description + Description of Topic + $topic: Topic' + rst + This is a description of the topic "$topic" + +Describe the topic and add any information/instructions for the responder here. + + diff --git a/ietf/dbtemplate/migrations/0005_add_default_topic_description.py b/ietf/dbtemplate/migrations/0005_add_default_topic_description.py new file mode 100644 index 000000000..47dc1efad --- /dev/null +++ b/ietf/dbtemplate/migrations/0005_add_default_topic_description.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-05-24 09:30 +from __future__ import unicode_literals + +from django.db import migrations + +def forward(apps, schema_editor): + DBTemplate = apps.get_model('dbtemplate','DBTemplate') + DBTemplate.objects.create( + path='/nomcom/defaults/topic/description', + title='Description of Topic', + variables='$topic: Topic', + type_id='rst', + content="""This is a description of the topic "$topic" + +Describe the topic and add any information/instructions for the responder here. +""", + ) + +def reverse(apps, schema_editor): + DBTemplate = apps.get_model('dbtemplate','DBTemplate') + DBTemplate.objects.filter(path='/nomcom/defaults/topic/description').delete() + +class Migration(migrations.Migration): + + dependencies = [ + ('dbtemplate', '0004_team_review_content_templates'), + ] + + operations = [ + migrations.RunPython(forward,reverse), + ] diff --git a/ietf/dbtemplate/migrations/0006_adjust_feedback_receipt_template.py b/ietf/dbtemplate/migrations/0006_adjust_feedback_receipt_template.py new file mode 100644 index 000000000..fc19c21b5 --- /dev/null +++ b/ietf/dbtemplate/migrations/0006_adjust_feedback_receipt_template.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-05-24 14:54 +from __future__ import unicode_literals + +from django.db import migrations + +def forward(apps, schema_migration): + DBTemplate = apps.get_model('dbtemplate','DBTemplate') + t = DBTemplate.objects.get(path='/nomcom/defaults/email/feedback_receipt.txt') + t.variables="""$about: What this feedback is about +$comments: Comments on whatever the feedback is about +""" + t.content="""Hi, + +Your input regarding $about has been received and registered. + +The following comments have been registered: + +-------------------------------------------------------------------------- +$comments +-------------------------------------------------------------------------- + +Thank you, +""" + t.save() + +def reverse(apps, schema_migration): + DBTemplate = apps.get_model('dbtemplate','DBTemplate') + t = DBTemplate.objects.get(path='/nomcom/defaults/email/feedback_receipt.txt') + t.variables="""$nominee: Full name of the nominee +$position: Nomination position +$comments: Comments on this candidate +""" + t.content="""Hi, + +Your input regarding $nominee for the position of +$position has been received and registered. + +The following comments have been registered: + +-------------------------------------------------------------------------- +$comments +-------------------------------------------------------------------------- + +Thank you, +""" + t.save() +class Migration(migrations.Migration): + + dependencies = [ + ('dbtemplate', '0005_add_default_topic_description'), + ] + + operations = [ + migrations.RunPython(forward, reverse) + ] diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index d7e154042..9f29ed30e 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -2787,6 +2787,7 @@ ], "desc": "Recipients when an interim meeting is announced", "to": [ + "group_stream_announce", "ietf_announce" ] }, @@ -2813,6 +2814,7 @@ ], "desc": "Recipients when an interim meeting is cancelled", "to": [ + "group_stream_announce", "ietf_announce" ] }, @@ -6184,17 +6186,27 @@ "desc": "Experimental room setup (boardroom and classroom) subject to availability", "name": "Boardroom Layout", "order": 0, - "used": true + "used": false }, "model": "name.roomresourcename", "pk": "boardroom" }, + { + "fields": { + "desc": "Flipchars", + "name": "Flipcharts", + "order": 0, + "used": true + }, + "model": "name.roomresourcename", + "pk": "flipcharts" + }, { "fields": { "desc": "The room will have a meetecho wrangler", "name": "Meetecho Support", "order": 0, - "used": true + "used": false }, "model": "name.roomresourcename", "pk": "meetecho" @@ -6204,7 +6216,7 @@ "desc": "The room will have a second computer projector", "name": "second LCD projector", "order": 0, - "used": true + "used": false }, "model": "name.roomresourcename", "pk": "proj2" @@ -6214,11 +6226,21 @@ "desc": "The room will have a computer projector", "name": "LCD projector", "order": 0, - "used": true + "used": false }, "model": "name.roomresourcename", "pk": "project" }, + { + "fields": { + "desc": "Experimental Room Setup (U-Shape and classroom, subject to availability)", + "name": "Experimental Room Setup (U-Shape and classroom)", + "order": 0, + "used": true + }, + "model": "name.roomresourcename", + "pk": "u-shape" + }, { "fields": { "desc": "", @@ -6538,5 +6560,35 @@ }, "model": "name.timeslottypename", "pk": "unavail" + }, + { + "fields": { + "desc": "Anyone who can log in", + "name": "General", + "order": 0, + "used": true + }, + "model": "name.topicaudiencename", + "pk": "general" + }, + { + "fields": { + "desc": "Members of this nomcom", + "name": "Nomcom Members", + "order": 0, + "used": true + }, + "model": "name.topicaudiencename", + "pk": "nomcom" + }, + { + "fields": { + "desc": "Anyone who has accepted a Nomination for an open position", + "name": "Nominees", + "order": 0, + "used": true + }, + "model": "name.topicaudiencename", + "pk": "nominees" } ] diff --git a/ietf/name/migrations/0020_add_topics.py b/ietf/name/migrations/0020_add_topics.py new file mode 100644 index 000000000..f84bf46c7 --- /dev/null +++ b/ietf/name/migrations/0020_add_topics.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-05-24 08:25 +from __future__ import unicode_literals + +from django.db import migrations, models + +def forward(apps, schema_editor): + TopicAudienceName = apps.get_model('name','TopicAudienceName') + # """General, Nominee, Nomcom Member""" + TopicAudienceName.objects.create( + slug='general', + name='General', + desc='Anyone who can log in', + ) + TopicAudienceName.objects.create( + slug='nominees', + name='Nominees', + desc='Anyone who has accepted a Nomination for an open position', + ) + TopicAudienceName.objects.create( + slug='nomcom', + name='Nomcom Members', + desc='Members of this nomcom', + ) + +def reverse(apps, schema_editor): + pass + +class Migration(migrations.Migration): + + dependencies = [ + ('name', '0019_add_docrelationshipname_downref_approval'), + ] + + operations = [ + migrations.CreateModel( + name='TopicAudienceName', + fields=[ + ('slug', models.CharField(max_length=32, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=255)), + ('desc', models.TextField(blank=True)), + ('used', models.BooleanField(default=True)), + ('order', models.IntegerField(default=0)), + ], + options={ + 'ordering': ['order', 'name'], + 'abstract': False, + }, + ), + migrations.RunPython(forward,reverse), + ] diff --git a/ietf/name/models.py b/ietf/name/models.py index c1c69b4c6..ec0b6905c 100644 --- a/ietf/name/models.py +++ b/ietf/name/models.py @@ -97,4 +97,5 @@ class ReviewResultName(NameModel): """Almost ready, Has issues, Has nits, Not Ready, On the right track, Ready, Ready with issues, Ready with nits, Serious Issues""" - +class TopicAudienceName(NameModel): + """General, Nominee, Nomcom Member""" diff --git a/ietf/name/resources.py b/ietf/name/resources.py index 15bc6d378..fc4253685 100644 --- a/ietf/name/resources.py +++ b/ietf/name/resources.py @@ -14,7 +14,8 @@ from ietf.name.models import (TimeSlotTypeName, GroupStateName, DocTagName, Inte ConstraintName, MeetingTypeName, DocRelationshipName, RoomResourceName, IprLicenseTypeName, LiaisonStatementTagName, FeedbackTypeName, LiaisonStatementState, StreamName, BallotPositionName, DBTemplateTypeName, NomineePositionStateName, - ReviewRequestStateName, ReviewTypeName, ReviewResultName) + ReviewRequestStateName, ReviewTypeName, ReviewResultName, + TopicAudienceName, ) class TimeSlotTypeNameResource(ModelResource): @@ -456,3 +457,17 @@ class ReviewResultNameResource(ModelResource): } api.name.register(ReviewResultNameResource()) +class TopicAudienceNameResource(ModelResource): + class Meta: + cache = SimpleCache() + queryset = TopicAudienceName.objects.all() + #resource_name = 'topicaudiencename' + filtering = { + "slug": ALL, + "name": ALL, + "desc": ALL, + "used": ALL, + "order": ALL, + } +api.name.register(TopicAudienceNameResource()) + diff --git a/ietf/nomcom/factories.py b/ietf/nomcom/factories.py index 5c9c523cc..364d4f971 100644 --- a/ietf/nomcom/factories.py +++ b/ietf/nomcom/factories.py @@ -1,7 +1,7 @@ import factory import random -from ietf.nomcom.models import NomCom, Position, Feedback, Nominee, NomineePosition +from ietf.nomcom.models import NomCom, Position, Feedback, Nominee, NomineePosition, Topic from ietf.group.factories import GroupFactory from ietf.person.factories import PersonFactory @@ -122,6 +122,17 @@ class NomComFactory(factory.DjangoModelFactory): p = PersonFactory() obj.group.role_set.create(name_id=role,person=p,email=p.email_set.first()) + @factory.post_generation + def populate_topics(obj, create, extracted, **kwargs): # pylint: disable=no-self-argument + ''' + Create a set of topics unless the factory is called with populate_topics=False + ''' + if extracted is None: + extracted = True + if create and extracted: + for i in range(3): + TopicFactory(nomcom=obj) + class PositionFactory(factory.DjangoModelFactory): class Meta: model = Position @@ -155,3 +166,13 @@ class FeedbackFactory(factory.DjangoModelFactory): subject = factory.Faker('sentence') comments = factory.Faker('paragraph') type_id = 'comment' + +class TopicFactory(factory.DjangoModelFactory): + class Meta: + model = Topic + + nomcom = factory.SubFactory(NomComFactory) + subject = factory.Faker('sentence') + accepting_feedback = True + audience_id = 'general' + diff --git a/ietf/nomcom/forms.py b/ietf/nomcom/forms.py index 3fde106b5..7d9d1f9c8 100644 --- a/ietf/nomcom/forms.py +++ b/ietf/nomcom/forms.py @@ -12,7 +12,7 @@ from ietf.group.models import Group, Role from ietf.ietfauth.utils import role_required from ietf.name.models import RoleName, FeedbackTypeName, NomineePositionStateName from ietf.nomcom.models import ( NomCom, Nomination, Nominee, NomineePosition, - Position, Feedback, ReminderDates ) + Position, Feedback, ReminderDates, Topic ) from ietf.nomcom.utils import (NOMINATION_RECEIPT_TEMPLATE, FEEDBACK_RECEIPT_TEMPLATE, get_user_email, validate_private_key, validate_public_key, make_nomineeposition, make_nomineeposition_for_newperson, @@ -565,6 +565,7 @@ class FeedbackForm(forms.ModelForm): self.public = kwargs.pop('public', None) self.position = kwargs.pop('position', None) self.nominee = kwargs.pop('nominee', None) + self.topic = kwargs.pop('topic', None) super(FeedbackForm, self).__init__(*args, **kwargs) @@ -583,10 +584,11 @@ class FeedbackForm(forms.ModelForm): def clean(self): - if not NomineePosition.objects.accepted().filter(nominee=self.nominee, - position=self.position): - msg = "There isn't a accepted nomination for %s on the %s position" % (self.nominee, self.position) - self._errors["comments"] = self.error_class([msg]) + if self.nominee and self.position: + if not NomineePosition.objects.accepted().filter(nominee=self.nominee, + position=self.position): + msg = "There isn't a accepted nomination for %s on the %s position" % (self.nominee, self.position) + self._errors["comments"] = self.error_class([msg]) return self.cleaned_data def save(self, commit=True): @@ -611,8 +613,11 @@ class FeedbackForm(forms.ModelForm): feedback.user = self.user feedback.type = FeedbackTypeName.objects.get(slug='comment') feedback.save() - feedback.positions.add(self.position) - feedback.nominees.add(self.nominee) + if self.nominee and self.position: + feedback.positions.add(self.position) + feedback.nominees.add(self.nominee) + if self.topic: + feedback.topics.add(self.topic) # send receipt email to feedback author if confirmation: @@ -620,10 +625,14 @@ class FeedbackForm(forms.ModelForm): subject = "NomCom comment confirmation" from_email = settings.NOMCOM_FROM_EMAIL (to_email, cc) = gather_address_lists('nomcom_comment_receipt_requested',commenter=author.address) - context = {'nominee': self.nominee.email.person.name, - 'comments': comments, - 'position': self.position.name} + if self.nominee and self.position: + about = '%s for the position of\n%s'%(self.nominee.email.person.name, self.position.name) + elif self.topic: + about = self.topic.subject + context = {'about': about, + 'comments': comments, } path = nomcom_template_path + FEEDBACK_RECEIPT_TEMPLATE + # TODO - make the thing above more generic send_mail(None, to_email, from_email, subject, path, context, cc=cc) class Meta: @@ -694,6 +703,19 @@ class PositionForm(forms.ModelForm): self.instance.nomcom = self.nomcom super(PositionForm, self).save(*args, **kwargs) +class TopicForm(forms.ModelForm): + + class Meta: + model = Topic + fields = ('subject', 'accepting_feedback','audience') + + def __init__(self, *args, **kwargs): + self.nomcom = kwargs.pop('nomcom', None) + super(TopicForm, self).__init__(*args, **kwargs) + + def save(self, *args, **kwargs): + self.instance.nomcom = self.nomcom + super(TopicForm, self).save(*args, **kwargs) class PrivateKeyForm(forms.Form): @@ -766,9 +788,13 @@ class MutableFeedbackForm(forms.ModelForm): if self.feedback_type.slug != 'nomina': self.fields['nominee'] = MultiplePositionNomineeField(nomcom=self.nomcom, - required=True, widget=forms.SelectMultiple(attrs={'class':'nominee_multi_select','size':'12'}), + required= self.feedback_type.slug != 'comment', help_text='Hold down "Control", or "Command" on a Mac, to select more than one.') + if self.feedback_type.slug == 'comment': + self.fields['topic'] = forms.ModelMultipleChoiceField(queryset=self.nomcom.topic_set.all(), + help_text='Hold down "Control" or "Command" on a Mac, to select more than one.', + required=False,) else: self.fields['position'] = forms.ModelChoiceField(queryset=Position.objects.get_by_nomcom(self.nomcom).filter(is_open=True), label="Position") self.fields['searched_email'] = SearchableEmailField(only_users=False,help_text="Try to find the candidate you are classifying with this field first. Only use the name and email fields below if this search does not find the candidate.",label="Candidate",required=False) @@ -793,6 +819,11 @@ class MutableFeedbackForm(forms.ModelForm): raise forms.ValidationError("You must identify either an existing person (by searching with the candidate field) and leave the name and email fields blank, or leave the search field blank and provide both a name and email address.") if candidate_email and Email.objects.filter(address=candidate_email).exists(): raise forms.ValidationError("%s already exists in the datatracker. Please search within the candidate field to find it and leave both the name and email fields blank." % candidate_email) + elif self.feedback_type.slug == 'comment': + nominees = self.cleaned_data.get('nominee') + topics = self.cleaned_data.get('topic') + if not (nominees or topics): + raise forms.ValidationError("You must choose at least one Nominee or Topic to associate with this comment") return cleaned_data def save(self, commit=True): @@ -832,6 +863,9 @@ class MutableFeedbackForm(forms.ModelForm): for (position, nominee) in self.cleaned_data['nominee']: feedback.nominees.add(nominee) feedback.positions.add(position) + if self.instance.type.slug=='comment': + for topic in self.cleaned_data['topic']: + feedback.topics.add(topic) return feedback FullFeedbackFormSet = forms.modelformset_factory( diff --git a/ietf/nomcom/migrations/0016_add_topics.py b/ietf/nomcom/migrations/0016_add_topics.py new file mode 100644 index 000000000..e293b0b6b --- /dev/null +++ b/ietf/nomcom/migrations/0016_add_topics.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-05-25 12:34 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('name', '0020_add_topics'), + ('dbtemplate', '0006_adjust_feedback_receipt_template'), + ('person', '0019_add_discovered_people'), + ('nomcom', '0015_show_pictures'), + ] + + operations = [ + migrations.CreateModel( + name='Topic', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('subject', models.CharField(help_text=b'This short description will appear on the Feedback pages.', max_length=255, verbose_name=b'Subject')), + ('accepting_feedback', models.BooleanField(default=False, verbose_name=b'Is accepting feedback')), + ('audience', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='name.TopicAudienceName')), + ('description', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='description', to='dbtemplate.DBTemplate')), + ('nomcom', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='nomcom.NomCom')), + ], + options={ + 'verbose_name_plural': 'Topics', + }, + ), + migrations.CreateModel( + name='TopicFeedbackLastSeen', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('time', models.DateTimeField(auto_now=True)), + ('reviewer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='person.Person')), + ('topic', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='nomcom.Topic')), + ], + ), + migrations.AddField( + model_name='feedback', + name='topics', + field=models.ManyToManyField(blank=True, to='nomcom.Topic'), + ), + ] diff --git a/ietf/nomcom/models.py b/ietf/nomcom/models.py index 1652436e0..18447f5d4 100644 --- a/ietf/nomcom/models.py +++ b/ietf/nomcom/models.py @@ -11,7 +11,7 @@ from django.template.defaultfilters import linebreaks from ietf.nomcom.fields import EncryptedTextField from ietf.person.models import Person,Email from ietf.group.models import Group -from ietf.name.models import NomineePositionStateName, FeedbackTypeName +from ietf.name.models import NomineePositionStateName, FeedbackTypeName, TopicAudienceName from ietf.dbtemplate.models import DBTemplate from ietf.nomcom.managers import NomineePositionManager, NomineeManager, \ @@ -19,6 +19,7 @@ from ietf.nomcom.managers import NomineePositionManager, NomineeManager, \ from ietf.nomcom.utils import (initialize_templates_for_group, initialize_questionnaire_for_position, initialize_requirements_for_position, + initialize_description_for_topic, delete_nomcom_templates) from ietf.utils.storage import NoLocationMigrationFileSystemStorage @@ -206,12 +207,41 @@ class Position(models.Model): rendered = linebreaks(rendered) return rendered +class Topic(models.Model): + nomcom = models.ForeignKey('NomCom') + subject = models.CharField(verbose_name='Name', max_length=255, help_text='This short description will appear on the Feedback pages.') + description = models.ForeignKey(DBTemplate, related_name='description', null=True, editable=False) + accepting_feedback = models.BooleanField(verbose_name='Is accepting feedback', default=False) + audience = models.ForeignKey(TopicAudienceName) + + class Meta: + verbose_name_plural = 'Topics' + + def __unicode__(self): + return self.subject + + def save(self, *args, **kwargs): + created = not self.id + super(Topic, self).save(*args, **kwargs) + changed = False + if created and self.id and not self.description_id: + self.description = initialize_description_for_topic(self) + changed = True + if changed: + self.save() + + def get_description(self): + rendered = render_to_string(self.description.path, {'topic': self}) + if self.description.type_id=='plain': + rendered = linebreaks(rendered) + return rendered class Feedback(models.Model): nomcom = models.ForeignKey('NomCom') author = models.EmailField(verbose_name='Author', blank=True) positions = models.ManyToManyField('Position', blank=True) nominees = models.ManyToManyField('Nominee', blank=True) + topics = models.ManyToManyField('Topic', blank=True) subject = models.TextField(verbose_name='Subject', blank=True) comments = EncryptedTextField(verbose_name='Comments') type = models.ForeignKey(FeedbackTypeName, blank=True, null=True) @@ -230,3 +260,9 @@ class FeedbackLastSeen(models.Model): reviewer = models.ForeignKey(Person) nominee = models.ForeignKey(Nominee) time = models.DateTimeField(auto_now=True) + +class TopicFeedbackLastSeen(models.Model): + reviewer = models.ForeignKey(Person) + topic = models.ForeignKey(Topic) + time = models.DateTimeField(auto_now=True) + diff --git a/ietf/nomcom/resources.py b/ietf/nomcom/resources.py index 9de9b8249..c967d470f 100644 --- a/ietf/nomcom/resources.py +++ b/ietf/nomcom/resources.py @@ -8,7 +8,7 @@ from tastypie.cache import SimpleCache from ietf import api from ietf.nomcom.models import (NomCom, Position, Nominee, ReminderDates, NomineePosition, - Feedback, Nomination, FeedbackLastSeen ) + Feedback, Nomination, FeedbackLastSeen, Topic, TopicFeedbackLastSeen, ) from ietf.group.resources import GroupResource class NomComResource(ModelResource): @@ -170,3 +170,43 @@ class FeedbackLastSeenResource(ModelResource): "nominee": ALL_WITH_RELATIONS, } api.nomcom.register(FeedbackLastSeenResource()) + + +from ietf.name.resources import TopicAudienceNameResource +from ietf.dbtemplate.resources import DBTemplateResource +class TopicResource(ModelResource): + nomcom = ToOneField(NomComResource, 'nomcom') + description = ToOneField(DBTemplateResource, 'description', null=True) + audience = ToOneField(TopicAudienceNameResource, 'audience') + class Meta: + queryset = Topic.objects.all() + serializer = api.Serializer() + cache = SimpleCache() + #resource_name = 'topic' + filtering = { + "id": ALL, + "subject": ALL, + "accepting_feedback": ALL, + "nomcom": ALL_WITH_RELATIONS, + "description": ALL_WITH_RELATIONS, + "audience": ALL_WITH_RELATIONS, + } +api.nomcom.register(TopicResource()) + + +from ietf.person.resources import PersonResource +class TopicFeedbackLastSeenResource(ModelResource): + reviewer = ToOneField(PersonResource, 'reviewer') + topic = ToOneField(TopicResource, 'topic') + class Meta: + queryset = TopicFeedbackLastSeen.objects.all() + serializer = api.Serializer() + cache = SimpleCache() + #resource_name = 'topicfeedbacklastseen' + filtering = { + "id": ALL, + "time": ALL, + "reviewer": ALL_WITH_RELATIONS, + "topic": ALL_WITH_RELATIONS, + } +api.nomcom.register(TopicFeedbackLastSeenResource()) diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py index 434163ffe..bca4e96c5 100644 --- a/ietf/nomcom/tests.py +++ b/ietf/nomcom/tests.py @@ -29,12 +29,12 @@ from ietf.nomcom.test_data import nomcom_test_data, generate_cert, check_comment MEMBER_USER, SECRETARIAT_USER, EMAIL_DOMAIN, NOMCOM_YEAR from ietf.nomcom.models import NomineePosition, Position, Nominee, \ NomineePositionStateName, Feedback, FeedbackTypeName, \ - Nomination, FeedbackLastSeen + Nomination, FeedbackLastSeen, TopicFeedbackLastSeen from ietf.nomcom.forms import EditMembersForm, EditMembersFormPreview from ietf.nomcom.utils import get_nomcom_by_year, make_nomineeposition, get_hash_nominee_position from ietf.nomcom.management.commands.send_reminders import Command, is_time_to_send -from ietf.nomcom.factories import NomComFactory, FeedbackFactory, \ +from ietf.nomcom.factories import NomComFactory, FeedbackFactory, TopicFactory, \ nomcom_kwargs_for_year, provide_private_key_to_test_client, \ key from ietf.person.factories import PersonFactory, EmailFactory, UserFactory @@ -1246,10 +1246,13 @@ class FeedbackLastSeenTests(TestCase): self.member = self.nc.group.role_set.filter(name='member').first().person self.nominee = self.nc.nominee_set.order_by('pk').first() self.position = self.nc.position_set.first() + self.topic = self.nc.topic_set.first() for type_id in ['comment','nomina','questio']: f = FeedbackFactory.create(author=self.author,nomcom=self.nc,type_id=type_id) f.positions.add(self.position) f.nominees.add(self.nominee) + f = FeedbackFactory.create(author=self.author,nomcom=self.nc,type_id='comment') + f.topics.add(self.topic) now = datetime.datetime.now() self.hour_ago = now - datetime.timedelta(hours=1) self.half_hour_ago = now - datetime.timedelta(minutes=30) @@ -1265,7 +1268,7 @@ class FeedbackLastSeenTests(TestCase): response = self.client.get(url) self.assertEqual(response.status_code,200) q = PyQuery(response.content) - self.assertEqual( len(q('.label-success')), 3 ) + self.assertEqual( len(q('.label-success')), 4 ) f = self.nc.feedback_set.first() f.time = self.hour_ago @@ -1275,12 +1278,19 @@ class FeedbackLastSeenTests(TestCase): response = self.client.get(url) self.assertEqual(response.status_code,200) q = PyQuery(response.content) - self.assertEqual( len(q('.label-success')), 2 ) + self.assertEqual( len(q('.label-success')), 3 ) FeedbackLastSeen.objects.update(time=self.second_from_now) response = self.client.get(url) self.assertEqual(response.status_code,200) q = PyQuery(response.content) + self.assertEqual( len(q('.label-success')), 1 ) + + TopicFeedbackLastSeen.objects.create(reviewer=self.member,topic=self.topic) + TopicFeedbackLastSeen.objects.update(time=self.second_from_now) + response = self.client.get(url) + self.assertEqual(response.status_code,200) + q = PyQuery(response.content) self.assertEqual( len(q('.label-success')), 0 ) def test_feedback_nominee_badges(self): @@ -1308,6 +1318,31 @@ class FeedbackLastSeenTests(TestCase): q = PyQuery(response.content) self.assertEqual( len(q('.label-success')), 0 ) + def test_feedback_topic_badges(self): + url = reverse('ietf.nomcom.views.view_feedback_topic', kwargs={'year':self.nc.year(), 'topic_id':self.topic.id}) + login_testing_unauthorized(self, self.member.user.username, url) + provide_private_key_to_test_client(self) + response = self.client.get(url) + self.assertEqual(response.status_code,200) + q = PyQuery(response.content) + self.assertEqual( len(q('.label-success')), 1 ) + + f = self.topic.feedback_set.first() + f.time = self.hour_ago + f.save() + TopicFeedbackLastSeen.objects.create(reviewer=self.member,topic=self.topic) + TopicFeedbackLastSeen.objects.update(time=self.half_hour_ago) + response = self.client.get(url) + self.assertEqual(response.status_code,200) + q = PyQuery(response.content) + self.assertEqual( len(q('.label-success')), 0 ) + + TopicFeedbackLastSeen.objects.update(time=self.second_from_now) + response = self.client.get(url) + self.assertEqual(response.status_code,200) + q = PyQuery(response.content) + self.assertEqual( len(q('.label-success')), 0 ) + class NewActiveNomComTests(TestCase): def setUp(self): @@ -1819,7 +1854,7 @@ class AcceptingTests(TestCase): login_testing_unauthorized(self,self.plain_person.user.username,url) response = self.client.get(url) q=PyQuery(response.content) - self.assertEqual( len(q('.badge')) , 3 ) + self.assertEqual( len(q('.badge')) , 6 ) pos = self.nc.position_set.first() pos.accepting_feedback=False @@ -1827,10 +1862,18 @@ class AcceptingTests(TestCase): response = self.client.get(url) q=PyQuery(response.content) - self.assertEqual( len(q('.badge')) , 2 ) + self.assertEqual( len(q('.badge')) , 5 ) - url += "?nominee=%d&position=%d" % (pos.nominee_set.first().pk, pos.pk) + topic = self.nc.topic_set.first() + topic.accepting_feedback=False + topic.save() + response = self.client.get(url) + q=PyQuery(response.content) + self.assertEqual( len(q('.badge')) , 4 ) + + posurl = url+ "?nominee=%d&position=%d" % (pos.nominee_set.first().pk, pos.pk) + response = self.client.get(posurl) self.assertTrue('not currently accepting feedback' in unicontent(response)) test_data = {'comments': 'junk', @@ -1841,7 +1884,17 @@ class AcceptingTests(TestCase): 'nominator_email': self.plain_person.email().address, 'nominator_name': self.plain_person.plain_name(), } - response = self.client.post(url, test_data) + response = self.client.post(posurl, test_data) + self.assertTrue('not currently accepting feedback' in unicontent(response)) + + topicurl = url+ "?topic=%d" % (topic.pk, ) + response = self.client.get(topicurl) + self.assertTrue('not currently accepting feedback' in unicontent(response)) + + test_data = {'comments': 'junk', + 'confirmation': False, + } + response = self.client.post(topicurl, test_data) self.assertTrue('not currently accepting feedback' in unicontent(response)) def test_private_accepting_feedback(self): @@ -1850,7 +1903,7 @@ class AcceptingTests(TestCase): login_testing_unauthorized(self,self.member.user.username,url) response = self.client.get(url) q=PyQuery(response.content) - self.assertEqual( len(q('.badge')) , 3 ) + self.assertEqual( len(q('.badge')) , 6 ) pos = self.nc.position_set.first() pos.accepting_feedback=False @@ -1858,7 +1911,7 @@ class AcceptingTests(TestCase): response = self.client.get(url) q=PyQuery(response.content) - self.assertEqual( len(q('.badge')) , 3 ) + self.assertEqual( len(q('.badge')) , 6 ) class FeedbackPictureTests(TestCase): @@ -1881,3 +1934,92 @@ class FeedbackPictureTests(TestCase): response = self.client.get(url) q = PyQuery(response.content) self.assertFalse(q('.photo')) + +class TopicTests(TestCase): + def setUp(self): + build_test_public_keys_dir(self) + self.nc = NomComFactory(**nomcom_kwargs_for_year(populate_topics=False)) + self.plain_person = PersonFactory.create() + self.chair = self.nc.group.role_set.filter(name='chair').first().person + + def tearDown(self): + clean_test_public_keys_dir(self) + + def testAddEditListRemoveTopic(self): + self.assertFalse(self.nc.topic_set.exists()) + + url = reverse('ietf.nomcom.views.edit_topic', kwargs={'year':self.nc.year()}) + login_testing_unauthorized(self,self.chair.user.username,url) + + response = self.client.post(url,{'subject':'Test Topic', 'accepting_feedback':True, 'audience':'general'}) + self.assertEqual(response.status_code,302) + self.assertEqual(self.nc.topic_set.first().subject,'Test Topic') + self.assertEqual(self.nc.topic_set.first().accepting_feedback, True) + self.assertEqual(self.nc.topic_set.first().audience.slug,'general') + + url = reverse('ietf.nomcom.views.edit_topic', kwargs={'year':self.nc.year(),'topic_id':self.nc.topic_set.first().pk}) + response = self.client.get(url) + self.assertEqual(response.status_code,200) + q = PyQuery(response.content) + self.assertEqual(q('#id_subject').attr['value'],'Test Topic') + + response = self.client.post(url,{'subject':'Test Topic Modified', 'accepting_feedback':False, 'audience':'nominees'}) + self.assertEqual(response.status_code,302) + self.assertEqual(self.nc.topic_set.first().subject,'Test Topic Modified') + self.assertEqual(self.nc.topic_set.first().accepting_feedback, False) + self.assertEqual(self.nc.topic_set.first().audience.slug,'nominees') + + self.client.logout() + url = reverse('ietf.nomcom.views.list_topics',kwargs={'year':self.nc.year()}) + login_testing_unauthorized(self,self.chair.user.username,url) + response=self.client.get(url) + self.assertEqual(response.status_code,200) + self.assertTrue('Test Topic Modified' in unicontent(response)) + + self.client.logout() + url = reverse('ietf.nomcom.views.remove_topic', kwargs={'year':self.nc.year(),'topic_id':self.nc.topic_set.first().pk}) + login_testing_unauthorized(self,self.chair.user.username,url) + response=self.client.get(url) + self.assertEqual(response.status_code,200) + self.assertTrue('Test Topic Modified' in unicontent(response)) + response=self.client.post(url,{'remove':1}) + self.assertEqual(response.status_code,302) + self.assertFalse(self.nc.topic_set.exists()) + + def testClassifyTopicFeedback(self): + topic = TopicFactory(nomcom=self.nc) + feedback = FeedbackFactory(nomcom=self.nc,type_id=None) + + url = reverse('ietf.nomcom.views.view_feedback_pending',kwargs={'year':self.nc.year() }) + login_testing_unauthorized(self, self.chair.user.username, url) + provide_private_key_to_test_client(self) + + response = self.client.post(url, {'form-TOTAL_FORMS':1, + 'form-INITIAL_FORMS':1, + 'end':'Save feedback', + 'form-0-id': feedback.id, + 'form-0-type': 'comment', + }) + self.assertTrue('You must choose at least one Nominee or Topic' in unicontent(response)) + response = self.client.post(url, {'form-TOTAL_FORMS':1, + 'form-INITIAL_FORMS':1, + 'end':'Save feedback', + 'form-0-id': feedback.id, + 'form-0-type': 'comment', + 'form-0-topic': '%s'%(topic.id,), + }) + self.assertEqual(response.status_code,302) + feedback = Feedback.objects.get(id=feedback.id) + self.assertEqual(feedback.type_id,'comment') + self.assertEqual(topic.feedback_set.count(),1) + + def testTopicFeedback(self): + topic = TopicFactory(nomcom=self.nc) + url = reverse('ietf.nomcom.views.public_feedback',kwargs={'year':self.nc.year() }) + url += '?topic=%d'%topic.pk + login_testing_unauthorized(self, self.plain_person.user.username, url) + response=self.client.post(url, {'comments':'junk', 'confirmation':False}) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "alert-success") + self.assertNotContains(response, "feedbackform") + self.assertEqual(topic.feedback_set.count(),1) diff --git a/ietf/nomcom/urls.py b/ietf/nomcom/urls.py index 5323ead1e..50519ca59 100644 --- a/ietf/nomcom/urls.py +++ b/ietf/nomcom/urls.py @@ -19,6 +19,7 @@ urlpatterns = [ url(r'^(?P\d{4})/private/view-feedback/unrelated/$', views.view_feedback_unrelated), url(r'^(?P\d{4})/private/view-feedback/pending/$', views.view_feedback_pending), url(r'^(?P\d{4})/private/view-feedback/nominee/(?P\d+)$', views.view_feedback_nominee), + url(r'^(?P\d{4})/private/view-feedback/topic/(?P\d+)$', views.view_feedback_topic), url(r'^(?P\d{4})/private/edit/nominee/(?P\d+)$', views.edit_nominee), url(r'^(?P\d{4})/private/merge-nominee/?$', views.private_merge_nominee), url(r'^(?P\d{4})/private/merge-person/?$', views.private_merge_person), @@ -31,6 +32,10 @@ urlpatterns = [ url(r'^(?P\d{4})/private/chair/position/add/$', views.edit_position), url(r'^(?P\d{4})/private/chair/position/(?P\d+)/$', views.edit_position), url(r'^(?P\d{4})/private/chair/position/(?P\d+)/remove/$', views.remove_position), + url(r'^(?P\d{4})/private/chair/topic/$', views.list_topics), + url(r'^(?P\d{4})/private/chair/topic/add/$', views.edit_topic), + url(r'^(?P\d{4})/private/chair/topic/(?P\d+)/$', views.edit_topic), + url(r'^(?P\d{4})/private/chair/topic/(?P\d+)/remove/$', views.remove_topic), url(r'^(?P\d{4})/$', views.year_index), url(r'^(?P\d{4})/requirements/$', views.requirements), diff --git a/ietf/nomcom/utils.py b/ietf/nomcom/utils.py index 28172df7f..b311c070a 100644 --- a/ietf/nomcom/utils.py +++ b/ietf/nomcom/utils.py @@ -38,6 +38,7 @@ NOMINEE_ACCEPT_REMINDER_TEMPLATE = 'email/nomination_accept_reminder.txt' NOMINEE_QUESTIONNAIRE_REMINDER_TEMPLATE = 'email/questionnaire_reminder.txt' NOMINATION_RECEIPT_TEMPLATE = 'email/nomination_receipt.txt' FEEDBACK_RECEIPT_TEMPLATE = 'email/feedback_receipt.txt' +DESCRIPTION_TEMPLATE = 'topic/description' DEFAULT_NOMCOM_TEMPLATES = [HOME_TEMPLATE, INEXISTENT_PERSON_TEMPLATE, @@ -133,6 +134,16 @@ def initialize_requirements_for_position(position): type_id=template.type_id, content=template.content) +def initialize_description_for_topic(topic): + description_path = MAIN_NOMCOM_TEMPLATE_PATH + DESCRIPTION_TEMPLATE + template = DBTemplate.objects.get(path=description_path) + return DBTemplate.objects.create( + group=topic.nomcom.group, + title=template.title + ' [%s]' % topic.subject, + path='/nomcom/' + topic.nomcom.group.acronym + '/topic/' + str(topic.id) + '/' + DESCRIPTION_TEMPLATE, + variables=template.variables, + type_id=template.type_id, + content=template.content) def delete_nomcom_templates(nomcom): nomcom_template_path = '/nomcom/' + nomcom.group.acronym diff --git a/ietf/nomcom/views.py b/ietf/nomcom/views.py index 12e3754ec..428170503 100644 --- a/ietf/nomcom/views.py +++ b/ietf/nomcom/views.py @@ -24,8 +24,9 @@ from ietf.nomcom.forms import (NominateForm, NominateNewPersonForm, FeedbackForm MergeNomineeForm, MergePersonForm, NomComTemplateForm, PositionForm, PrivateKeyForm, EditNomcomForm, EditNomineeForm, PendingFeedbackForm, ReminderDatesForm, FullFeedbackFormSet, - FeedbackEmailForm, NominationResponseCommentForm) -from ietf.nomcom.models import Position, NomineePosition, Nominee, Feedback, NomCom, ReminderDates, FeedbackLastSeen + FeedbackEmailForm, NominationResponseCommentForm, TopicForm) +from ietf.nomcom.models import (Position, NomineePosition, Nominee, Feedback, NomCom, ReminderDates, + FeedbackLastSeen, Topic, TopicFeedbackLastSeen, ) from ietf.nomcom.utils import (get_nomcom_by_year, store_nomcom_private_key, get_hash_nominee_position, send_reminder_to_nominees, HOME_TEMPLATE, NOMINEE_ACCEPT_REMINDER_TEMPLATE,NOMINEE_QUESTIONNAIRE_REMINDER_TEMPLATE) @@ -415,17 +416,23 @@ def feedback(request, year, public): has_publickey = nomcom.public_key and True or False nominee = None position = None + topic = None if nomcom.group.state_id != 'conclude': selected_nominee = request.GET.get('nominee') selected_position = request.GET.get('position') if selected_nominee and selected_position: nominee = get_object_or_404(Nominee, id=selected_nominee) position = get_object_or_404(Position, id=selected_position) + selected_topic = request.GET.get('topic') + if selected_topic: + topic = get_object_or_404(Topic,id=selected_topic) if public: positions = Position.objects.get_by_nomcom(nomcom=nomcom).filter(is_open=True,accepting_feedback=True) + topics = Topic.objects.filter(nomcom=nomcom,accepting_feedback=True) else: positions = Position.objects.get_by_nomcom(nomcom=nomcom).filter(is_open=True) + topics = Topic.objects.filter(nomcom=nomcom) user_comments = Feedback.objects.filter(nomcom=nomcom, type='comment', @@ -434,6 +441,9 @@ def feedback(request, year, public): counts = dict() for pos,nom in counter: counts.setdefault(pos,dict())[nom] = counter[(pos,nom)] + + topic_counts = Counter(user_comments.values_list('topics',flat=True)) + if public: base_template = "nomcom/nomcom_public_base.html" else: @@ -457,25 +467,56 @@ def feedback(request, year, public): 'year': year, 'selected': 'feedback', 'positions': positions, + 'topics': topics, 'counts' : counts, + 'topic_counts' : topic_counts, 'base_template': base_template }) - if nominee and position and request.method == 'POST': - form = FeedbackForm(data=request.POST, - nomcom=nomcom, user=request.user, - public=public, position=position, nominee=nominee) - if form.is_valid(): + if public and topic and not topic.accepting_feedback: + messages.warning(request, "This Nomcom is not currently accepting feedback for "+topic.subject) + return render(request, 'nomcom/feedback.html', { + 'form': None, + 'nomcom': nomcom, + 'year': year, + 'selected': 'feedback', + 'positions': positions, + 'topics': topics, + 'counts' : counts, + 'topic_counts' : topic_counts, + 'base_template': base_template + }) + if request.method == 'POST': + if nominee and position: + form = FeedbackForm(data=request.POST, + nomcom=nomcom, user=request.user, + public=public, position=position, nominee=nominee) + elif topic: + form = FeedbackForm(data=request.POST, + nomcom=nomcom, user=request.user, + topic=topic) + else: + form = None + if form and form.is_valid(): form.save() messages.success(request, 'Your feedback has been registered.') form = None - counts.setdefault(position.pk,dict()) - counts[position.pk].setdefault(nominee.pk,0) - counts[position.pk][nominee.pk] += 1 + if position: + counts.setdefault(position.pk,dict()) + counts[position.pk].setdefault(nominee.pk,0) + counts[position.pk][nominee.pk] += 1 + elif topic: + topic_counts.setdefault(topic.pk,0) + topic_counts[topic.pk] += 1 + else: + pass else: if nominee and position: form = FeedbackForm(nomcom=nomcom, user=request.user, public=public, position=position, nominee=nominee) + elif topic: + form = FeedbackForm(nomcom=nomcom, user=request.user, public=public, + topic=topic) else: form = None @@ -484,8 +525,10 @@ def feedback(request, year, public): 'nomcom': nomcom, 'year': year, 'positions': positions, + 'topics': topics, 'selected': 'feedback', 'counts': counts, + 'topic_counts': topic_counts, 'base_template': base_template }) @@ -633,13 +676,15 @@ def view_feedback(request, year): nomcom = get_nomcom_by_year(year) nominees = Nominee.objects.get_by_nomcom(nomcom).not_duplicated().distinct() independent_feedback_types = [] - feedback_types = [] + nominee_feedback_types = [] for ft in FeedbackTypeName.objects.all(): if ft.slug in settings.NOMINEE_FEEDBACK_TYPES: - feedback_types.append(ft) + nominee_feedback_types.append(ft) else: independent_feedback_types.append(ft) + topic_feedback_types=FeedbackTypeName.objects.filter(slug='comment') nominees_feedback = [] + topics_feedback = [] def nominee_staterank(nominee): states=nominee.nomineeposition_set.values_list('state_id',flat=True) @@ -658,7 +703,7 @@ def view_feedback(request, year): for nominee in sorted_nominees: last_seen = FeedbackLastSeen.objects.filter(reviewer=request.user.person,nominee=nominee).first() nominee_feedback = [] - for ft in feedback_types: + for ft in nominee_feedback_types: qs = nominee.feedback_set.by_type(ft.slug) count = qs.count() if not count: @@ -670,13 +715,29 @@ def view_feedback(request, year): nominee_feedback.append( (ft.name,count,newflag) ) nominees_feedback.append( {'nominee':nominee, 'feedback':nominee_feedback} ) independent_feedback = [ft.feedback_set.get_by_nomcom(nomcom).count() for ft in independent_feedback_types] + for topic in nomcom.topic_set.all(): + last_seen = TopicFeedbackLastSeen.objects.filter(reviewer=request.user.person,topic=topic).first() + topic_feedback = [] + for ft in topic_feedback_types: + qs = topic.feedback_set.by_type(ft.slug) + count = qs.count() + if not count: + newflag = False + elif not last_seen: + newflag = True + else: + newflag = qs.filter(time__gt=last_seen.time).exists() + topic_feedback.append( (ft.name,count,newflag) ) + topics_feedback.append ( {'topic':topic, 'feedback':topic_feedback} ) return render(request, 'nomcom/view_feedback.html', {'year': year, 'selected': 'view_feedback', 'nominees': nominees, - 'feedback_types': feedback_types, + 'nominee_feedback_types': nominee_feedback_types, 'independent_feedback_types': independent_feedback_types, + 'topic_feedback_types': topic_feedback_types, + 'topics_feedback': topics_feedback, 'independent_feedback': independent_feedback, 'nominees_feedback': nominees_feedback, 'nomcom': nomcom}) @@ -796,6 +857,27 @@ def view_feedback_unrelated(request, year): 'feedback_types': feedback_types, 'nomcom': nomcom}) +@role_required("Nomcom") +@nomcom_private_key_required +def view_feedback_topic(request, year, topic_id): + nomcom = get_nomcom_by_year(year) + topic = get_object_or_404(Topic, id=topic_id) + feedback_types = FeedbackTypeName.objects.filter(slug__in=['comment',]) + + last_seen = TopicFeedbackLastSeen.objects.filter(reviewer=request.user.person,topic=topic).first() + last_seen_time = (last_seen and last_seen.time) or datetime.datetime(year=1,month=1,day=1) + if last_seen: + last_seen.save() + else: + TopicFeedbackLastSeen.objects.create(reviewer=request.user.person,topic=topic) + + return render(request, 'nomcom/view_feedback_topic.html', + {'year': year, + 'selected': 'view_feedback', + 'topic': topic, + 'feedback_types': feedback_types, + 'last_seen_time' : last_seen_time, + 'nomcom': nomcom}) @role_required("Nomcom") @nomcom_private_key_required @@ -884,12 +966,10 @@ def edit_nomcom(request, year): @role_required("Nomcom Chair", "Nomcom Advisor") def list_templates(request, year): nomcom = get_nomcom_by_year(year) - positions = nomcom.position_set.all() - template_list = DBTemplate.objects.filter(group=nomcom.group).exclude(path__contains='/position/') + template_list = DBTemplate.objects.filter(group=nomcom.group).exclude(path__contains='/position/').exclude(path__contains='/topic/') return render(request, 'nomcom/list_templates.html', {'template_list': template_list, - 'positions': positions, 'year': year, 'selected': 'edit_templates', 'nomcom': nomcom, @@ -987,6 +1067,73 @@ def edit_position(request, year, position_id=None): 'is_chair_task' : True, }) + +@role_required("Nomcom Chair", "Nomcom Advisor") +def list_topics(request, year): + nomcom = get_nomcom_by_year(year) + topics = nomcom.topic_set.all() + + return render(request, 'nomcom/list_topics.html', + {'topics': topics, + 'year': year, + 'selected': 'edit_topics', + 'nomcom': nomcom, + 'is_chair_task' : True, + }) + + +@role_required("Nomcom Chair", "Nomcom Advisor") +def remove_topic(request, year, topic_id): + nomcom = get_nomcom_by_year(year) + if nomcom.group.state_id=='conclude': + return HttpResponseForbidden('This nomcom is closed.') + try: + topic = nomcom.topic_set.get(id=topic_id) + except Topic.DoesNotExist: + raise Http404 + + if request.POST.get('remove', None): + topic.delete() + return redirect('ietf.nomcom.views.list_topics', year=year) + return render(request, 'nomcom/remove_topic.html', + {'year': year, + 'topic': topic, + 'nomcom': nomcom, + 'is_chair_task' : True, + }) + + +@role_required("Nomcom Chair", "Nomcom Advisor") +def edit_topic(request, year, topic_id=None): + nomcom = get_nomcom_by_year(year) + + if nomcom.group.state_id=='conclude': + return HttpResponseForbidden('This nomcom is closed.') + + if topic_id: + try: + topic = nomcom.topic_set.get(id=topic_id) + except Topic.DoesNotExist: + raise Http404 + else: + topic = None + + if request.method == 'POST': + form = TopicForm(request.POST, instance=topic, nomcom=nomcom) + if form.is_valid(): + form.save() + return redirect('ietf.nomcom.views.list_topics', year=year) + else: + form = TopicForm(instance=topic, nomcom=nomcom,initial={'accepting_feedback':True,'audience':'general'} if not topic else {}) + + return render(request, 'nomcom/edit_topic.html', + {'form': form, + 'topic': topic, + 'year': year, + 'nomcom': nomcom, + 'is_chair_task' : True, + }) + @role_required("Nomcom Chair", "Nomcom Advisor") def configuration_help(request, year): return render(request,'nomcom/chair_help.html',{'year':year}) diff --git a/ietf/secr/sreq/tests.py b/ietf/secr/sreq/tests.py index 982ac9c2f..fcb56126f 100644 --- a/ietf/secr/sreq/tests.py +++ b/ietf/secr/sreq/tests.py @@ -77,6 +77,9 @@ class SubmitRequestCase(TestCase): group = Group.objects.get(acronym='ames') ad = group.parent.role_set.filter(name='ad').first().person resource = ResourceAssociation.objects.first() + # Bit of a test data hack - the fixture now has no used resources to pick from + resource.name.used=True + resource.name.save() url = reverse('ietf.secr.sreq.views.new',kwargs={'acronym':group.acronym}) confirm_url = reverse('ietf.secr.sreq.views.confirm',kwargs={'acronym':group.acronym}) len_before = len(outbox) diff --git a/ietf/templates/nomcom/edit_topic.html b/ietf/templates/nomcom/edit_topic.html new file mode 100644 index 000000000..d9ea3cee3 --- /dev/null +++ b/ietf/templates/nomcom/edit_topic.html @@ -0,0 +1,32 @@ +{% extends "nomcom/nomcom_private_base.html" %} +{# Copyright The IETF Trust 2017, All Rights Reserved #} +{% load origin %} +{% load staticfiles %} +{% block pagehead %} + + +{% endblock %} + +{% load bootstrap3 %} + +{% block subtitle %} - {% if topic %}Edit{% else %}Add{% endif %} topics{% endblock %} + +{% block nomcom_content %} + {% origin %} +

{% if topic %}Edit{% else %}Add{% endif %} topic

+ +
+ {% csrf_token %} + {% bootstrap_form form %} + + {% buttons %} + + Back + {% endbuttons %} +
+{% endblock %} + +{% block content_end %} + + +{% endblock %} diff --git a/ietf/templates/nomcom/feedback.html b/ietf/templates/nomcom/feedback.html index 5759f09e7..7f36c0aac 100644 --- a/ietf/templates/nomcom/feedback.html +++ b/ietf/templates/nomcom/feedback.html @@ -87,18 +87,35 @@ the badge, for more information about this nominee.

+ +

Topics

+
{% if form %} -

Provide feedback - {% if form.position %} - about {{form.nominee.email.person.name}} ({{form.nominee.email.address}}) for the {{form.position.name}} position. - {% if nomcom.show_nominee_pictures and form.nominee.email.person.photo_thumb %} - - {% endif %} - {% endif %} -

+ {% if form.position %} +

Provide feedback about {{form.nominee.email.person.name}} ({{form.nominee.email.address}}) for the {{form.position.name}} position. + {% if nomcom.show_nominee_pictures and form.nominee.email.person.photo_thumb %} + + {% endif %} +

+ {% elif form.topic %} +

Provide feedback about "{{form.topic.subject}}"

+
A description of this topic is at the end of the page.
+ {% endif %}

This feedback will only be available to NomCom {{year}}. You may have the feedback mailed back to you by selecting the option below.

@@ -108,8 +125,14 @@ {% buttons %} {% endbuttons %} - + + {% if form.topic %} +
+
Description: {{form.topic.subject}}
+
{{form.topic.get_description|safe}}
+
{% endif %} + {% endif %}
{% endif %} diff --git a/ietf/templates/nomcom/list_templates.html b/ietf/templates/nomcom/list_templates.html index aa9fddb6b..4c3f0cbe7 100644 --- a/ietf/templates/nomcom/list_templates.html +++ b/ietf/templates/nomcom/list_templates.html @@ -19,16 +19,24 @@

Defined templates for positions

- {% if positions %} - {% for position in positions %} + {% for position in nomcom.position_set.all %}

{{ position.name }}

    {% for template in position.get_templates %}
  • {{ template }}
  • {% endfor %}
- {% endfor %} - {% else %} + {% empty %}

There are no positions defined.

- {% endif %} + {% endfor %} + +

Defined templates for topics

+ {% for topic in nomcom.topic_set.all %} +

{{ topic.subject }}

+ + {% empty %} +

There are no topics defined.

+ {% endfor %} {% endblock nomcom_content %} diff --git a/ietf/templates/nomcom/list_topics.html b/ietf/templates/nomcom/list_topics.html new file mode 100644 index 000000000..c3f468807 --- /dev/null +++ b/ietf/templates/nomcom/list_topics.html @@ -0,0 +1,41 @@ +{% extends "nomcom/nomcom_private_base.html" %} +{# Copyright The IETF Trust 2017, All Rights Reserved #} +{% load origin %} + +{% block subtitle %} - Topics{% endblock %} + +{% block nomcom_content %} + {% origin %} +

Topics in {{ nomcom.group }}

+ + {% if nomcom.group.state_id == 'active' %} + Add new topic + {% endif %} + + {% if topics %} +
+ {% for topic in topics %} +

{{ topic.subject }}

+
+
Accepting feedback
+
{{topic.accepting_feedback|yesno}}
+
Description
+
+ {{ topic.description }}
+
+
Audience
+
{{topic.audience}}
+ {% if nomcom.group.state_id == 'active' %} +
Actions
+
+ Edit + Remove +
+ {% endif %} +
+ {% endfor %} +
+ {% else %} +

There are no topics defined.

+ {% endif %} +{% endblock nomcom_content %} diff --git a/ietf/templates/nomcom/nomcom_private_base.html b/ietf/templates/nomcom/nomcom_private_base.html index 4b93d362f..2f7bdc021 100644 --- a/ietf/templates/nomcom/nomcom_private_base.html +++ b/ietf/templates/nomcom/nomcom_private_base.html @@ -40,6 +40,7 @@
  • Edit Settings
  • Edit Pages
  • Edit Positions
  • +
  • Edit Topics
  • {% if nomcom.group.state_id == 'active' %}
  • Edit Members
  • {% endif %} diff --git a/ietf/templates/nomcom/remove_topic.html b/ietf/templates/nomcom/remove_topic.html new file mode 100644 index 000000000..88afa1857 --- /dev/null +++ b/ietf/templates/nomcom/remove_topic.html @@ -0,0 +1,35 @@ +{% extends "nomcom/nomcom_private_base.html" %} +{# Copyright The IETF Trust 2017, All Rights Reserved #} +{% load origin %} + +{% load bootstrap3 %} + +{% block nomcom_content %} + {% origin %} +

    Position: {{ topic }}

    + +

    This topic has {{topic.feedback_set.count|default:"no"}} feedback objects associated with it.

    + {% if topic.feedback_set.count %} +

    + Unless this is a topic created only for testing, deleting it is likely to be harmful. All of the feedback will also be deleted. +

    +

    + If you are just wanting the topic to disappear from the lists available to the community for providing feedback, instead of deleting the topic, edit the topic and change accepting_feedback to False. +

    +

    If this is just a test topic, it is ok to delete it.

    + {% else %} +

    This topic is safe to delete.

    + {% endif %} + +
    + {% csrf_token %} + + + + {% buttons %} + + Cancel + {% endbuttons %} +
    + +{% endblock nomcom_content %} diff --git a/ietf/templates/nomcom/view_feedback.html b/ietf/templates/nomcom/view_feedback.html index bf6853cd5..4b33721cc 100644 --- a/ietf/templates/nomcom/view_feedback.html +++ b/ietf/templates/nomcom/view_feedback.html @@ -27,7 +27,7 @@ Nominee - {% for ft in feedback_types %} + {% for ft in nominee_feedback_types %} {{ ft.name }} {% endfor %} @@ -53,6 +53,38 @@ {% endfor %} +

    Feedback related to topics

    + +
    +
    + + + + + {% for ft in topic_feedback_types %} + + {% endfor %} + + + + {% for fb_dict in topics_feedback %} + + + {% for fbtype_name, fbtype_count, fbtype_newflag in fb_dict.feedback %} + + {% endfor %} + + {% endfor %} + +
    Topic{{ ft.name }}
    + {{ fb_dict.topic.subject }} + + {% if fbtype_newflag %}New{% endif %} + {{ fbtype_count }} +
    +
    +
    + {% if independent_feedback_types %}

    Feedback not related to Nominees

    diff --git a/ietf/templates/nomcom/view_feedback_pending.html b/ietf/templates/nomcom/view_feedback_pending.html index 960860718..9d3196549 100644 --- a/ietf/templates/nomcom/view_feedback_pending.html +++ b/ietf/templates/nomcom/view_feedback_pending.html @@ -33,7 +33,7 @@ {% bootstrap_form formset.management_form %} {% if extra_step %} -

    Please, provide the following information about nominees to complete the classification of this feedback.

    +

    Please indicate which nominees and/or topics this feedback should be associated with.

    {% for form in formset.forms %}
    diff --git a/ietf/templates/nomcom/view_feedback_topic.html b/ietf/templates/nomcom/view_feedback_topic.html new file mode 100644 index 000000000..22a6d88a3 --- /dev/null +++ b/ietf/templates/nomcom/view_feedback_topic.html @@ -0,0 +1,42 @@ +{% extends "nomcom/nomcom_private_base.html" %} +{# Copyright The IETF Trust 2017, All Rights Reserved #} +{% load origin %}{% origin %} + +{% load nomcom_tags %} + +{% block subtitle %} - View feedback about {{ topic.subject }}{% endblock %} + +{% block nomcom_content %} + +

    Feedback about {{ topic.subject }}

    + + + +
    + {% for ft in feedback_types %} +
    + {% for feedback in topic.feedback_set.all %} + {% if feedback.type.slug == ft.slug %} + {% if forloop.first %}

    {% else %}
    {% endif %} +
    +
    {% if feedback.time > last_seen_time %}New{% endif %}From
    +
    {{ feedback.author|formatted_email|default:"Anonymous" }} +
    +
    Date
    +
    {{ feedback.time|date:"Y-m-d" }}
    +
    Body
    +
    {% decrypt feedback.comments request year 1 %}
    +
    + {% endif %} + {% endfor %} +
    + {% endfor %} +
    + + Back + +{% endblock %}