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:
parent
92b1c30b63
commit
4813177086
|
@ -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>
|
||||
|
|
|
@ -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),
|
||||
]
|
|
@ -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
51
ietf/name/migrations/0020_add_topics.py
Normal file
51
ietf/name/migrations/0020_add_topics.py
Normal 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),
|
||||
]
|
|
@ -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"""
|
||||
|
|
|
@ -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())
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
47
ietf/nomcom/migrations/0016_add_topics.py
Normal file
47
ietf/nomcom/migrations/0016_add_topics.py
Normal 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'),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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)
|
||||
|
|
32
ietf/templates/nomcom/edit_topic.html
Normal file
32
ietf/templates/nomcom/edit_topic.html
Normal 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 %}
|
|
@ -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>
|
||||
{% 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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
41
ietf/templates/nomcom/list_topics.html
Normal file
41
ietf/templates/nomcom/list_topics.html
Normal 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 %}
|
|
@ -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 %}
|
||||
|
|
35
ietf/templates/nomcom/remove_topic.html
Normal file
35
ietf/templates/nomcom/remove_topic.html
Normal 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 %}
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
42
ietf/templates/nomcom/view_feedback_topic.html
Normal file
42
ietf/templates/nomcom/view_feedback_topic.html
Normal 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 %}
|
Loading…
Reference in a new issue