Allow the nomcom to collect feedback on arbitrary topics. Fixes #2256 and #1846. Commit ready for merge.

- Legacy-Id: 13474
This commit is contained in:
Robert Sparks 2017-05-30 19:54:31 +00:00
parent 92b1c30b63
commit 4813177086
26 changed files with 7516 additions and 6599 deletions

View file

@ -172,4 +172,14 @@ The questionaire is repeated below for your convenience.
--------</field>
<field to="group.group" name="group" rel="ManyToOneRel"><None></None></field>
</object>
<object pk="12" model="dbtemplate.dbtemplate">
<field type="CharField" name="path">/nomcom/defaults/topic/description</field>
<field type="CharField" name="title">Description of Topic</field>
<field type="TextField" name="variables">$topic: Topic'</field>
<field to="name.dbtemplatetypename" name="type" rel="ManyToOneRel">rst</field>
<field type="TextField" name="content">This is a description of the topic "$topic"
Describe the topic and add any information/instructions for the responder here.
</field>
</object>
</django-objects>

View file

@ -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),
]

View file

@ -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)
]

File diff suppressed because it is too large Load diff

View file

@ -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),
]

View file

@ -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"""

View file

@ -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())

View file

@ -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'

View file

@ -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(

View file

@ -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'),
),
]

View file

@ -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)

View file

@ -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())

View file

@ -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)

View file

@ -19,6 +19,7 @@ urlpatterns = [
url(r'^(?P<year>\d{4})/private/view-feedback/unrelated/$', views.view_feedback_unrelated),
url(r'^(?P<year>\d{4})/private/view-feedback/pending/$', views.view_feedback_pending),
url(r'^(?P<year>\d{4})/private/view-feedback/nominee/(?P<nominee_id>\d+)$', views.view_feedback_nominee),
url(r'^(?P<year>\d{4})/private/view-feedback/topic/(?P<topic_id>\d+)$', views.view_feedback_topic),
url(r'^(?P<year>\d{4})/private/edit/nominee/(?P<nominee_id>\d+)$', views.edit_nominee),
url(r'^(?P<year>\d{4})/private/merge-nominee/?$', views.private_merge_nominee),
url(r'^(?P<year>\d{4})/private/merge-person/?$', views.private_merge_person),
@ -31,6 +32,10 @@ urlpatterns = [
url(r'^(?P<year>\d{4})/private/chair/position/add/$', views.edit_position),
url(r'^(?P<year>\d{4})/private/chair/position/(?P<position_id>\d+)/$', views.edit_position),
url(r'^(?P<year>\d{4})/private/chair/position/(?P<position_id>\d+)/remove/$', views.remove_position),
url(r'^(?P<year>\d{4})/private/chair/topic/$', views.list_topics),
url(r'^(?P<year>\d{4})/private/chair/topic/add/$', views.edit_topic),
url(r'^(?P<year>\d{4})/private/chair/topic/(?P<topic_id>\d+)/$', views.edit_topic),
url(r'^(?P<year>\d{4})/private/chair/topic/(?P<topic_id>\d+)/remove/$', views.remove_topic),
url(r'^(?P<year>\d{4})/$', views.year_index),
url(r'^(?P<year>\d{4})/requirements/$', views.requirements),

View file

@ -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

View file

@ -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})

View file

@ -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)

View file

@ -0,0 +1,32 @@
{% extends "nomcom/nomcom_private_base.html" %}
{# Copyright The IETF Trust 2017, All Rights Reserved #}
{% load origin %}
{% load staticfiles %}
{% block pagehead %}
<link rel="stylesheet" href="{% static 'select2/select2.css' %}">
<link rel="stylesheet" href="{% static 'select2-bootstrap-css/select2-bootstrap.min.css' %}">
{% endblock %}
{% load bootstrap3 %}
{% block subtitle %} - {% if topic %}Edit{% else %}Add{% endif %} topics{% endblock %}
{% block nomcom_content %}
{% origin %}
<h2>{% if topic %}Edit{% else %}Add{% endif %} topic</h2>
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<input class="btn btn-primary" type="submit" value="{% if topic %}Save{% else %}Add{% endif %}">
<a class="btn btn-default pull-right" href="../">Back</a>
{% endbuttons %}
</form>
{% endblock %}
{% block content_end %}
<script src="{% static 'select2/select2.min.js' %}"></script>
<script src="{% static 'ietf/js/select2-field.js' %}"></script>
{% endblock %}

View file

@ -87,18 +87,35 @@
the badge, for more information about this
nominee.
</p>
<h3>Topics</h3>
<div class="btn-group-vertical form-group">
{% for t in topics %}
<a class="btn btn-default btn-xs" {% if nomcom.group.state_id != 'conclude' %}href="?topic={{t.id}}"{% endif %}>
{{t.subject}}
{% with count=topic_counts|lookup:t.id %}
<span class="badge"
title="{% if count %}{{count}} earlier comment{{count|pluralize}} from you {% else %}You have not yet provided feedback {% endif %} on {{t.subject}}">
{{ count | default:"no feedback" }}
</span>&nbsp;
{% endwith %}
</a>
{% endfor %}
</div>
</div>
<div class="col-sm-8 col-sm-pull-4">
{% if form %}
<h3>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 %}
<span class="feedbackphoto"><img src="{{form.nominee.email.person.photo_thumb.url}}" width=100 /></span>
{% endif %}
{% endif %}
</h3>
{% if form.position %}
<h3> 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 %}
<span class="feedbackphoto"><img src="{{form.nominee.email.person.photo_thumb.url}}" width=100 /></span>
{% endif %}
</h3>
{% elif form.topic %}
<h3 >Provide feedback about "{{form.topic.subject}}"</h3>
<div>A description of this topic is at the end of the page.</div>
{% endif %}
<p>This feedback will only be available to <a href="{% url 'ietf.nomcom.views.year_index' year=year %}">NomCom {{year}}</a>.
You may have the feedback mailed back to you by selecting the option below.</p>
@ -108,8 +125,14 @@
{% buttons %}
<input class="btn btn-primary" type="submit" value="Save" name="save">
{% endbuttons %}
</form>
</form>
{% if form.topic %}
<div class="panel panel-default">
<div class="panel-heading">Description: {{form.topic.subject}}</div>
<div class="panel-body">{{form.topic.get_description|safe}}</div>
</div>
{% endif %}
{% endif %}
</div>
</div>
{% endif %}

View file

@ -19,16 +19,24 @@
<h2>Defined templates for positions</h2>
{% if positions %}
{% for position in positions %}
{% for position in nomcom.position_set.all %}
<h3>{{ position.name }}</h3>
<ul>
{% for template in position.get_templates %}
<li><a href="{% url 'ietf.nomcom.views.edit_template' year template.id %}">{{ template }}</a></li>
{% endfor %}
</ul>
{% endfor %}
{% else %}
{% empty %}
<p>There are no positions defined.</p>
{% endif %}
{% endfor %}
<h2>Defined templates for topics</h2>
{% for topic in nomcom.topic_set.all %}
<h3>{{ topic.subject }}</h3>
<ul>
<li><a href="{% url 'ietf.nomcom.views.edit_template' year topic.description.id %}">{{ topic.description }}</a></li>
</ul>
{% empty %}
<p>There are no topics defined.</p>
{% endfor %}
{% endblock nomcom_content %}

View file

@ -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 %}
<h2>Topics in {{ nomcom.group }}</h2>
{% if nomcom.group.state_id == 'active' %}
<a class="btn btn-default" href="{% url 'ietf.nomcom.views.edit_topic' year %}">Add new topic</a>
{% endif %}
{% if topics %}
<div>
{% for topic in topics %}
<h4>{{ topic.subject }}</h4>
<dl class="dl-horizontal">
<dt>Accepting feedback</dt>
<dd> {{topic.accepting_feedback|yesno}}</dd>
<dt>Description</dt>
<dd>
<a href="{% url 'ietf.nomcom.views.edit_template' year topic.description.id %}">{{ topic.description }}</a><br>
</dd>
<dt>Audience</dt>
<dd>{{topic.audience}}</dd>
{% if nomcom.group.state_id == 'active' %}
<dt>Actions</dt>
<dd>
<a class="btn btn-default" href="{% url 'ietf.nomcom.views.edit_topic' year topic.id %}">Edit</a>
<a class="btn btn-default" href="{% url 'ietf.nomcom.views.remove_topic' year topic.id %}">Remove</a>
</dd>
{% endif %}
</dl>
{% endfor %}
</div>
{% else %}
<p>There are no topics defined.</p>
{% endif %}
{% endblock nomcom_content %}

View file

@ -40,6 +40,7 @@
<li {% if selected == "edit_nomcom" %}class="active"{% endif %}><a href="{% url 'ietf.nomcom.views.edit_nomcom' year %}">Edit Settings</a></li>
<li {% if selected == "edit_templates" %}class="active"{% endif %}><a href="{% url 'ietf.nomcom.views.list_templates' year %}">Edit Pages</a></li>
<li {% if selected == "edit_positions" %}class="active"{% endif %}><a href="{% url 'ietf.nomcom.views.list_positions' year %}">Edit Positions</a></li>
<li {% if selected == "edit_topics" %}class="active"{% endif %}><a href="{% url 'ietf.nomcom.views.list_topics' year %}">Edit Topics</a></li>
{% if nomcom.group.state_id == 'active' %}
<li {% if selected == "edit_members" %}class="active"{% endif %}><a href="{% url 'ietf.nomcom.forms.EditMembersFormPreview' year %}">Edit Members</a></li>
{% endif %}

View file

@ -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 %}
<h2>Position: {{ topic }}</h2>
<p>This topic has {{topic.feedback_set.count|default:"no"}} feedback objects associated with it.</p>
{% if topic.feedback_set.count %}
<p>
<span class="alert alert-warning">Unless this is a topic created only for testing, deleting it is likely to be harmful. All of the feedback will also be deleted.</span>
</p>
<p>
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.
</p>
<p>If this is just a test topic, it is ok to delete it.</p>
{% else %}
<p>This topic is safe to delete.</p>
{% endif %}
<form method="post">
{% csrf_token %}
<input type="hidden" name="remove" value="1">
{% buttons %}
<button class="btn btn-primary btn-warning" type="submit">Delete</button>
<a class="btn btn-default" href="{% url 'ietf.nomcom.views.list_topics' year %}">Cancel</a>
{% endbuttons %}
</form>
{% endblock nomcom_content %}

View file

@ -27,7 +27,7 @@
<thead>
<tr>
<th class="col-sm-9">Nominee</th>
{% for ft in feedback_types %}
{% for ft in nominee_feedback_types %}
<th class="col-sm-1 text-center">{{ ft.name }}</th>
{% endfor %}
</tr>
@ -53,6 +53,38 @@
</div>
{% endfor %}
<h2>Feedback related to topics</h2>
<div class="panel panel-default">
<div class="panel-body">
<table class="table table-condensed table-striped">
<thead>
<tr>
<th class="col-sm-9">Topic</th>
{% for ft in topic_feedback_types %}
<th class="col-sm-1 text-center">{{ ft.name }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for fb_dict in topics_feedback %}
<tr>
<td>
<a href="{% url 'ietf.nomcom.views.view_feedback_topic' year=year topic_id=fb_dict.topic.id %}">{{ fb_dict.topic.subject }}</a>
</td>
{% for fbtype_name, fbtype_count, fbtype_newflag in fb_dict.feedback %}
<td class="text-right">
{% if fbtype_newflag %}<span class="label label-success">New</span>{% endif %}
{{ fbtype_count }}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% if independent_feedback_types %}
<h2>Feedback not related to Nominees</h2>

View file

@ -33,7 +33,7 @@
{% bootstrap_form formset.management_form %}
{% if extra_step %}
<p>Please, provide the following information about nominees to complete the classification of this feedback.</p>
<p>Please indicate which nominees and/or topics this feedback should be associated with.</p>
{% for form in formset.forms %}
<dl class="dl-horizontal">

View file

@ -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 %}
<h2>Feedback about {{ topic.subject }} </h2>
<ul class="nav nav-tabs" role="tablist">
{% for ft in feedback_types %}
<li {% if forloop.first %}class="active"{% endif %}><a href="#{{ ft.slug }}" role="tab" data-toggle="tab">{{ ft.name }}</a></li>
{% endfor %}
</ul>
<div class="tab-content">
{% for ft in feedback_types %}
<div class="tab-pane {% if forloop.first %}active{% endif %}" id="{{ ft.slug }}">
{% for feedback in topic.feedback_set.all %}
{% if feedback.type.slug == ft.slug %}
{% if forloop.first %}<p></p>{% else %}<hr>{% endif %}
<dl class="dl-horizontal">
<dt>{% if feedback.time > last_seen_time %}<span class="label label-success">New</span>{% endif %}From</dt>
<dd>{{ feedback.author|formatted_email|default:"Anonymous" }}
</dd>
<dt>Date</dt>
<dd>{{ feedback.time|date:"Y-m-d" }}</dd>
<dt>Body</dt>
<dd class="pasted">{% decrypt feedback.comments request year 1 %}</dd>
</dl>
{% endif %}
{% endfor %}
</div>
{% endfor %}
</div>
<a class="btn btn-default" href="{% url 'ietf.nomcom.views.view_feedback' year %}">Back</a>
{% endblock %}