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
+
+
+{% 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 %}
+
+
+
+{% 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
+
+
+
+
+
+
+ Topic
+ {% for ft in topic_feedback_types %}
+ {{ ft.name }}
+ {% endfor %}
+
+
+
+ {% for fb_dict in topics_feedback %}
+
+
+ {{ fb_dict.topic.subject }}
+
+ {% for fbtype_name, fbtype_count, fbtype_newflag in fb_dict.feedback %}
+
+ {% if fbtype_newflag %}New {% endif %}
+ {{ fbtype_count }}
+
+ {% endfor %}
+
+ {% endfor %}
+
+
+
+
+
{% 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 %}
+ {{ ft.name }}
+ {% endfor %}
+
+
+
+ {% 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 %}