This set of changes focuses on improvements to the nomcom portion of the datatracker.

These changes:

Simplify the nomcom form for comments. Make it more obvious who receives mail when a comment is supplied. Fixes # 1849.

Simplify the nomcom form for nominations. Provide a primary workflow where nominations choose an existing Person, and a secondary workflow for nominating new people. 

Allow nominees to add a comment when accepting or declining a nomination. Fixes #1845.

Organize the list of nominees on the feedback page. Fixes #1786 and #1809.

Simplify the mechanisms used to display feedback message counts.

Regroup the feedback view to make it easier to see where to spend review effort. Fixes #1866.

Capture when nomcom members last reviewed feedback for a given nominee. Add badges when new feedback is avaliable. Improve the layout of the feedback index page. Fixes #1850.

Reorganize the tab navigation on the nomcom private pages. Made it more obvious when the chair is doing something that only the chair gets to see. Fixes #1788 and #1795.

Regroup multiselect options to make classifying pending feedback simpler. Make the control larger and resizable. Fixes #1854.

Simplify the chair's views for editing nominee records. Replace the merge nominee form with a request to the secretariat to merge Person records. Fixes #1847. 

Added merging nominees to the secretariat's persson merging script. 

Show information for concluded nomcoms. Close feedback and nomination for concluded nomcoms. Fixes #1856.

Improve the questionnaire templates, reminding the nominee that receiving the questionnaire does not imply they have accepted a nomination. Fixes #1807.

Remove the description field from Postion. Simplify the Position list and the Position edit form. Make the nomcom pages more self documenting. Add a page to help nomcom chiars through setting up a new nomcom. Fixes #1867 and #1768.

Remove the type from the template pathname for the requirements templates. Make the requirements views work for both types plain and rst. Changed the default type for new nomcom requirement templates to rst.

Remove 'incumbent' from the models. Fixes #1771.

Adjust the models for Nominee and Nomination to better associate Nominee objects with Person objects.

Remove BaseNomcomForm and the implementation of custom fieldsets.

Replace the custom message framework with the django provided messages framework.

Improve SearchablePersonField to show the primary email address for any search result where a name appears more than once.

Add the use of factory-boy for generating test data. Normalize management of a test directory for test nomcom public keys. Significantly improve test coverage of the nomcom related code.


Commit ready for merge.
 - Legacy-Id: 10629
This commit is contained in:
Robert Sparks 2015-12-23 21:00:15 +00:00
commit c8bbfbad78
67 changed files with 2648 additions and 1143 deletions

View file

@ -12,11 +12,10 @@ import django
django.setup() django.setup()
import argparse import argparse
import pprint
from django.contrib import admin
from django.contrib.auth.models import User
from ietf.person.models import Person from ietf.person.models import Person
from ietf.person.utils import merge_persons
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("source_id",type=int) parser.add_argument("source_id",type=int)
parser.add_argument("target_id",type=int) parser.add_argument("target_id",type=int)
@ -30,62 +29,4 @@ response = raw_input('Ok to continue y/n? ')
if response.lower() != 'y': if response.lower() != 'y':
sys.exit() sys.exit()
# merge emails merge_persons(source,target,sys.stdout)
for email in source.email_set.all():
print "Merging email: {}".format(email.address)
email.person = target
email.save()
# merge aliases
target_aliases = [ a.name for a in target.alias_set.all() ]
for alias in source.alias_set.all():
if alias.name in target_aliases:
alias.delete()
else:
print "Merging alias: {}".format(alias.name)
alias.person = target
alias.save()
# merge DocEvents
for docevent in source.docevent_set.all():
docevent.by = target
docevent.save()
# merge SubmissionEvents
for subevent in source.submissionevent_set.all():
subevent.by = target
subevent.save()
# merge Messages
for message in source.message_set.all():
message.by = target
message.save()
# merge Constraints
for constraint in source.constraint_set.all():
constraint.person = target
constraint.save()
# merge Roles
for role in source.role_set.all():
role.person = target
role.save()
# check for any remaining relationships and delete if none
objs = [source]
opts = Person._meta
user = User.objects.filter(is_superuser=True).first()
admin_site = admin.site
using = 'default'
deletable_objects, perms_needed, protected = admin.utils.get_deleted_objects(
objs, opts, user, admin_site, using)
if len(deletable_objects) > 1:
print "Not Deleting Person: {}({})".format(source.ascii,source.pk)
print "Related objects remain:"
pprint.pprint(deletable_objects[1])
else:
print "Deleting Person: {}({})".format(source.ascii,source.pk)
source.delete()

View file

@ -0,0 +1,8 @@
import factory
from ietf.dbtemplate.models import DBTemplate
class DBTemplateFactory(factory.DjangoModelFactory):
class Meta:
model = DBTemplate

View file

@ -76,10 +76,10 @@ Questionnaire</field>
<field to="group.group" name="group" rel="ManyToOneRel"><None></None></field> <field to="group.group" name="group" rel="ManyToOneRel"><None></None></field>
</object> </object>
<object pk="6" model="dbtemplate.dbtemplate"> <object pk="6" model="dbtemplate.dbtemplate">
<field type="CharField" name="path">/nomcom/defaults/position/requirements.txt</field> <field type="CharField" name="path">/nomcom/defaults/position/requirements</field>
<field type="CharField" name="title">Position requirements</field> <field type="CharField" name="title">Position requirements</field>
<field type="TextField" name="variables">$position: Position</field> <field type="TextField" name="variables">$position: Position</field>
<field to="name.dbtemplatetypename" name="type" rel="ManyToOneRel">plain</field> <field to="name.dbtemplatetypename" name="type" rel="ManyToOneRel">rst</field>
<field type="TextField" name="content">These are the requirements for the position $position: <field type="TextField" name="content">These are the requirements for the position $position:
Requirements.</field> Requirements.</field>

View file

@ -0,0 +1,40 @@
{% extends "ietf.html" %}
{% load bootstrap3 %}
{% block content %}
<h1>Template: {{ template }}</h1>
<h2>Meta information</h2>
<dl>
<dt>Title</dt>
<dd>{{ template.title }}</dt>
<dt>Group</dt>
<dd>{{ template.group }}</dd>
<dt>Template type</dt>
<dd>{{ template.type.name }}
{% if template.type.slug == "rst" %}
<p class="help-block">This template uses the syntax of reStructuredText. Get a quick reference at <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">http://docutils.sourceforge.net/docs/user/rst/quickref.html</a>.</p>
<p class="help-block">You can do variable interpolation with $varialbe if the template allows any variable.</p>
{% endif %}
{% if template.type.slug == "django" %}
<p class="help-block">This template uses the syntax of the default django template framework. Get more info at <a href="https://docs.djangoproject.com/en/dev/topics/templates/">https://docs.djangoproject.com/en/dev/topics/templates/</a>.</p>
<p class="help-block">You can do variable interpolation with the current django markup &#123;&#123;variable&#125;&#125; if the template allows any variable.</p>
{% endif %}
{% if template.type.slug == "plain" %}
<p class="help-block">This template uses plain text, so no markup is used. You can do variable interpolation with $variable if the template allows any variable.</p>
{% endif %}
</dd>
{% if template.variables %}
<dt>Variables allowed in this template</dt>
<dd>{{ template.variables|linebreaks }}</dd>
{% endif %}
</dl>
<h2>Template content</h2>
<div class = "panel panel-default">
<p class='pasted'>{{ template.content }}</p>
</div>
{% endblock content %}

View file

@ -43,3 +43,19 @@ def template_edit(request, acronym, template_id, base_template='dbtemplate/templ
} }
context.update(extra_context) context.update(extra_context)
return render(request, base_template, context) return render(request, base_template, context)
def template_show(request, acronym, template_id, base_template='dbtemplate/template_edit.html', extra_context=None):
group = get_object_or_404(Group, acronym=acronym)
chairs = group.role_set.filter(name__slug='chair')
extra_context = extra_context or {}
if not has_role(request.user, "Secretariat") and not chairs.filter(person__user=request.user).count():
return HttpResponseForbidden("You are not authorized to access this view")
template = get_object_or_404(DBTemplate, id=template_id, group=group)
context = {'template': template,
'group': group,
}
context.update(extra_context)
return render(request, base_template, context)

View file

@ -1302,6 +1302,7 @@ class ChangeReplacesTests(TestCase):
expires=datetime.datetime.now() + datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE), expires=datetime.datetime.now() + datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE),
group=mars_wg, group=mars_wg,
) )
self.basea.documentauthor_set.create(author=Email.objects.create(address="basea_author@example.com"),order=1)
self.baseb = Document.objects.create( self.baseb = Document.objects.create(
name="draft-test-base-b", name="draft-test-base-b",
@ -1312,6 +1313,7 @@ class ChangeReplacesTests(TestCase):
expires=datetime.datetime.now() - datetime.timedelta(days = 365 - settings.INTERNET_DRAFT_DAYS_TO_EXPIRE), expires=datetime.datetime.now() - datetime.timedelta(days = 365 - settings.INTERNET_DRAFT_DAYS_TO_EXPIRE),
group=mars_wg, group=mars_wg,
) )
self.baseb.documentauthor_set.create(author=Email.objects.create(address="baseb_author@example.com"),order=1)
self.replacea = Document.objects.create( self.replacea = Document.objects.create(
name="draft-test-replace-a", name="draft-test-replace-a",
@ -1322,6 +1324,7 @@ class ChangeReplacesTests(TestCase):
expires=datetime.datetime.now() + datetime.timedelta(days = settings.INTERNET_DRAFT_DAYS_TO_EXPIRE), expires=datetime.datetime.now() + datetime.timedelta(days = settings.INTERNET_DRAFT_DAYS_TO_EXPIRE),
group=mars_wg, group=mars_wg,
) )
self.replacea.documentauthor_set.create(author=Email.objects.create(address="replacea_author@example.com"),order=1)
self.replaceboth = Document.objects.create( self.replaceboth = Document.objects.create(
name="draft-test-replace-both", name="draft-test-replace-both",
@ -1332,6 +1335,7 @@ class ChangeReplacesTests(TestCase):
expires=datetime.datetime.now() + datetime.timedelta(days = settings.INTERNET_DRAFT_DAYS_TO_EXPIRE), expires=datetime.datetime.now() + datetime.timedelta(days = settings.INTERNET_DRAFT_DAYS_TO_EXPIRE),
group=mars_wg, group=mars_wg,
) )
self.replaceboth.documentauthor_set.create(author=Email.objects.create(address="replaceboth_author@example.com"),order=1)
self.basea.set_state(State.objects.get(used=True, type="draft", slug="active")) self.basea.set_state(State.objects.get(used=True, type="draft", slug="active"))
self.baseb.set_state(State.objects.get(used=True, type="draft", slug="expired")) self.baseb.set_state(State.objects.get(used=True, type="draft", slug="expired"))
@ -1366,8 +1370,8 @@ class ChangeReplacesTests(TestCase):
self.assertTrue(not RelatedDocument.objects.filter(relationship='possibly-replaces', source=self.replacea)) self.assertTrue(not RelatedDocument.objects.filter(relationship='possibly-replaces', source=self.replacea))
self.assertEqual(len(outbox), 1) self.assertEqual(len(outbox), 1)
self.assertTrue('replacement status updated' in outbox[-1]['Subject']) self.assertTrue('replacement status updated' in outbox[-1]['Subject'])
self.assertTrue('base-a@' in outbox[-1]['To']) self.assertTrue('replacea_author@' in outbox[-1]['To'])
self.assertTrue('replace-a@' in outbox[-1]['To']) self.assertTrue('basea_author@' in outbox[-1]['To'])
empty_outbox() empty_outbox()
# Post that says replaceboth replaces both base a and base b # Post that says replaceboth replaces both base a and base b
@ -1378,9 +1382,9 @@ class ChangeReplacesTests(TestCase):
self.assertEqual(Document.objects.get(name='draft-test-base-a').get_state().slug,'repl') self.assertEqual(Document.objects.get(name='draft-test-base-a').get_state().slug,'repl')
self.assertEqual(Document.objects.get(name='draft-test-base-b').get_state().slug,'repl') self.assertEqual(Document.objects.get(name='draft-test-base-b').get_state().slug,'repl')
self.assertEqual(len(outbox), 1) self.assertEqual(len(outbox), 1)
self.assertTrue('base-a@' in outbox[-1]['To']) self.assertTrue('basea_author@' in outbox[-1]['To'])
self.assertTrue('base-b@' in outbox[-1]['To']) self.assertTrue('baseb_author@' in outbox[-1]['To'])
self.assertTrue('replace-both@' in outbox[-1]['To']) self.assertTrue('replaceboth_author@' in outbox[-1]['To'])
# Post that undoes replaceboth # Post that undoes replaceboth
empty_outbox() empty_outbox()
@ -1389,9 +1393,9 @@ class ChangeReplacesTests(TestCase):
self.assertEqual(Document.objects.get(name='draft-test-base-a').get_state().slug,'repl') # Because A is still also replaced by replacea self.assertEqual(Document.objects.get(name='draft-test-base-a').get_state().slug,'repl') # Because A is still also replaced by replacea
self.assertEqual(Document.objects.get(name='draft-test-base-b').get_state().slug,'expired') self.assertEqual(Document.objects.get(name='draft-test-base-b').get_state().slug,'expired')
self.assertEqual(len(outbox), 1) self.assertEqual(len(outbox), 1)
self.assertTrue('base-a@' in outbox[-1]['To']) self.assertTrue('basea_author@' in outbox[-1]['To'])
self.assertTrue('base-b@' in outbox[-1]['To']) self.assertTrue('baseb_author@' in outbox[-1]['To'])
self.assertTrue('replace-both@' in outbox[-1]['To']) self.assertTrue('replaceboth_author@' in outbox[-1]['To'])
# Post that undoes replacea # Post that undoes replacea
empty_outbox() empty_outbox()
@ -1399,8 +1403,8 @@ class ChangeReplacesTests(TestCase):
r = self.client.post(url, dict(replaces="")) r = self.client.post(url, dict(replaces=""))
self.assertEqual(r.status_code, 302) self.assertEqual(r.status_code, 302)
self.assertEqual(Document.objects.get(name='draft-test-base-a').get_state().slug,'active') self.assertEqual(Document.objects.get(name='draft-test-base-a').get_state().slug,'active')
self.assertTrue('base-a@' in outbox[-1]['To']) self.assertTrue('basea_author@' in outbox[-1]['To'])
self.assertTrue('replace-a@' in outbox[-1]['To']) self.assertTrue('replacea_author@' in outbox[-1]['To'])
def test_review_possibly_replaces(self): def test_review_possibly_replaces(self):

View file

@ -20,6 +20,8 @@ from ietf.utils import draft, markup_txt
from ietf.utils.mail import send_mail from ietf.utils.mail import send_mail
from ietf.mailtrigger.utils import gather_address_lists from ietf.mailtrigger.utils import gather_address_lists
import debug # pyflakes:ignore
#TODO FIXME - it would be better if this lived in ietf/doc/mails.py, but there's #TODO FIXME - it would be better if this lived in ietf/doc/mails.py, but there's
# an import order issue to work out. # an import order issue to work out.
def email_update_telechat(request, doc, text): def email_update_telechat(request, doc, text):

10
ietf/group/factories.py Normal file
View file

@ -0,0 +1,10 @@
import factory
from ietf.group.models import Group
class GroupFactory(factory.DjangoModelFactory):
class Meta:
model = Group
name = factory.Faker('sentence',nb_words=6)
acronym = factory.Faker('word')

View file

@ -64,9 +64,9 @@ def has_role(user, role_names, *args, **kwargs):
"RG Secretary": Q(person=person,name="secr", group__type="rg", group__state__in=["active","proposed"]), "RG Secretary": Q(person=person,name="secr", group__type="rg", group__state__in=["active","proposed"]),
"AG Secretary": Q(person=person,name="secr", group__type="ag", group__state__in=["active"]), "AG Secretary": Q(person=person,name="secr", group__type="ag", group__state__in=["active"]),
"Team Chair": Q(person=person,name="chair", group__type="team", group__state="active"), "Team Chair": Q(person=person,name="chair", group__type="team", group__state="active"),
"Nomcom Chair": Q(person=person, name="chair", group__type="nomcom", group__state="active", group__acronym__icontains=kwargs.get('year', '0000')), "Nomcom Chair": Q(person=person, name="chair", group__type="nomcom", group__acronym__icontains=kwargs.get('year', '0000')),
"Nomcom Advisor": Q(person=person, name="advisor", group__type="nomcom", group__state="active", group__acronym__icontains=kwargs.get('year', '0000')), "Nomcom Advisor": Q(person=person, name="advisor", group__type="nomcom", group__acronym__icontains=kwargs.get('year', '0000')),
"Nomcom": Q(person=person, group__type="nomcom", group__state="active", group__acronym__icontains=kwargs.get('year', '0000')), "Nomcom": Q(person=person, group__type="nomcom", group__acronym__icontains=kwargs.get('year', '0000')),
"Liaison Manager": Q(person=person,name="liaiman",group__type="sdo",group__state="active", ), "Liaison Manager": Q(person=person,name="liaiman",group__type="sdo",group__state="active", ),
"Authorized Individual": Q(person=person,name="auth",group__type="sdo",group__state="active", ), "Authorized Individual": Q(person=person,name="auth",group__type="sdo",group__state="active", ),
} }

View file

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
def forward(apps, schema_editor):
Recipient=apps.get_model('mailtrigger','Recipient')
MailTrigger=apps.get_model('mailtrigger','MailTrigger')
m = MailTrigger.objects.create(
slug='person_merge_requested',
desc="Recipients for a message requesting that duplicated Person records be merged ")
m.to = Recipient.objects.filter(slug__in=['ietf_secretariat', ])
def reverse(apps, schema_editor):
MailTrigger=apps.get_model('mailtrigger','MailTrigger')
MailTrigger.objects.filter(slug='person_merge_requested').delete()
class Migration(migrations.Migration):
dependencies = [
('mailtrigger', '0002_auto_20150809_1314'),
]
operations = [
migrations.RunPython(forward, reverse)
]

View file

@ -648,6 +648,7 @@
{ {
"fields": { "fields": {
"order": 0, "order": 0,
"prefix": "charter",
"used": true, "used": true,
"name": "Charter", "name": "Charter",
"desc": "" "desc": ""
@ -658,6 +659,7 @@
{ {
"fields": { "fields": {
"order": 0, "order": 0,
"prefix": "agenda",
"used": true, "used": true,
"name": "Agenda", "name": "Agenda",
"desc": "" "desc": ""
@ -668,6 +670,7 @@
{ {
"fields": { "fields": {
"order": 0, "order": 0,
"prefix": "minutes",
"used": true, "used": true,
"name": "Minutes", "name": "Minutes",
"desc": "" "desc": ""
@ -678,6 +681,7 @@
{ {
"fields": { "fields": {
"order": 0, "order": 0,
"prefix": "slides",
"used": true, "used": true,
"name": "Slides", "name": "Slides",
"desc": "" "desc": ""
@ -688,6 +692,7 @@
{ {
"fields": { "fields": {
"order": 0, "order": 0,
"prefix": "draft",
"used": true, "used": true,
"name": "Draft", "name": "Draft",
"desc": "" "desc": ""
@ -698,6 +703,7 @@
{ {
"fields": { "fields": {
"order": 0, "order": 0,
"prefix": "liai-att",
"used": true, "used": true,
"name": "Liaison Attachment", "name": "Liaison Attachment",
"desc": "" "desc": ""
@ -708,6 +714,7 @@
{ {
"fields": { "fields": {
"order": 0, "order": 0,
"prefix": "conflict-review",
"used": true, "used": true,
"name": "Conflict Review", "name": "Conflict Review",
"desc": "" "desc": ""
@ -718,6 +725,7 @@
{ {
"fields": { "fields": {
"order": 0, "order": 0,
"prefix": "status-change",
"used": true, "used": true,
"name": "Status Change", "name": "Status Change",
"desc": "" "desc": ""
@ -728,6 +736,7 @@
{ {
"fields": { "fields": {
"order": 0, "order": 0,
"prefix": "",
"used": false, "used": false,
"name": "Shepherd's writeup", "name": "Shepherd's writeup",
"desc": "" "desc": ""
@ -738,6 +747,7 @@
{ {
"fields": { "fields": {
"order": 0, "order": 0,
"prefix": "",
"used": false, "used": false,
"name": "Liaison", "name": "Liaison",
"desc": "" "desc": ""
@ -748,6 +758,7 @@
{ {
"fields": { "fields": {
"order": 0, "order": 0,
"prefix": "recording",
"used": true, "used": true,
"name": "Recording", "name": "Recording",
"desc": "" "desc": ""
@ -758,6 +769,7 @@
{ {
"fields": { "fields": {
"order": 0, "order": 0,
"prefix": "bluesheets",
"used": true, "used": true,
"name": "Bluesheets", "name": "Bluesheets",
"desc": "" "desc": ""
@ -4551,6 +4563,14 @@
"model": "mailtrigger.recipient", "model": "mailtrigger.recipient",
"pk": "doc_authors" "pk": "doc_authors"
}, },
{
"fields": {
"template": "{{doc.author_list}}",
"desc": "The authors of the document, without using the draft aliases"
},
"model": "mailtrigger.recipient",
"pk": "doc_authors_expanded"
},
{ {
"fields": { "fields": {
"template": null, "template": null,
@ -4711,6 +4731,14 @@
"model": "mailtrigger.recipient", "model": "mailtrigger.recipient",
"pk": "iana_last_call" "pk": "iana_last_call"
}, },
{
"fields": {
"template": "<i-d-announce@ietf.org>",
"desc": "The I-D-Announce Email List"
},
"model": "mailtrigger.recipient",
"pk": "id_announce"
},
{ {
"fields": { "fields": {
"template": "The IESG <iesg@ietf.org>", "template": "The IESG <iesg@ietf.org>",
@ -5316,11 +5344,7 @@
"fields": { "fields": {
"cc": [], "cc": [],
"to": [ "to": [
"doc_authors", "doc_authors_expanded"
"doc_group_chairs",
"doc_group_responsible_directors",
"doc_notify",
"doc_shepherd"
], ],
"desc": "Recipients when what a document replaces or is replaced by changes" "desc": "Recipients when what a document replaces or is replaced by changes"
}, },
@ -5708,6 +5732,17 @@
"model": "mailtrigger.mailtrigger", "model": "mailtrigger.mailtrigger",
"pk": "nomination_received" "pk": "nomination_received"
}, },
{
"fields": {
"cc": [],
"to": [
"ietf_secretariat"
],
"desc": "Recipients for a message requesting that duplicated Person records be merged "
},
"model": "mailtrigger.mailtrigger",
"pk": "person_merge_requested"
},
{ {
"fields": { "fields": {
"cc": [ "cc": [
@ -5853,7 +5888,7 @@
"submission_group_mail_list" "submission_group_mail_list"
], ],
"to": [ "to": [
"ietf_announce" "id_announce"
], ],
"desc": "Recipients for the announcement of a successfully submitted draft" "desc": "Recipients for the announcement of a successfully submitted draft"
}, },

View file

@ -23,7 +23,7 @@ class NomineePositionAdmin(admin.ModelAdmin):
class PositionAdmin(admin.ModelAdmin): class PositionAdmin(admin.ModelAdmin):
list_display = ('name', 'nomcom', 'is_open', 'incumbent') list_display = ('name', 'nomcom', 'is_open')
list_filter = ('nomcom',) list_filter = ('nomcom',)

147
ietf/nomcom/factories.py Normal file
View file

@ -0,0 +1,147 @@
import factory
import random
from ietf.nomcom.models import NomCom, Position, Feedback, Nominee, NomineePosition
from ietf.group.factories import GroupFactory
from ietf.person.factories import PersonFactory
import debug # pyflakes:ignore
cert = '''-----BEGIN CERTIFICATE-----
MIIDHjCCAgagAwIBAgIJAKDCCjbQboJzMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNV
BAMMCE5vbUNvbTE1MB4XDTE0MDQwNDIxMTQxNFoXDTE2MDQwMzIxMTQxNFowEzER
MA8GA1UEAwwITm9tQ29tMTUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
AQC2QXCsAitYSOgPYor77zQnEeHuVqlcuhpH1wpKB+N6WcScA5N3AnX9uZEFOt6M
cJ+MCiHECdqDlH6npQTJlpCpIVgAD4B6xzjRBRww8d3lClA/kKwsKzuX93RS0Uv3
0hAD6q9wjqK/m6vR5Y1SsvJYV0y+Yu5j9xUEsojMH7O3NlXWAYOb6oH+f/X7PX27
IhtiCwfICMmVWh/hKeXuFx6HSOcH3gZ6Tlk1llfDbE/ArpsZ6JmnLn73+64yqIoO
ZOc4JJUPrdsmbNwXoxQSQhrpwjN8NpSkQaJbHGB3G+OWvP4fpqcweFHxlEq1Hhef
uR9E6jc3qwxVQfwjbcq6N/4JAgMBAAGjdTBzMB0GA1UdDgQWBBTJow+TJynRWsTQ
LzoS861FGb/rxDAOBgNVHQ8BAf8EBAMCBLAwDwYDVR0TAQH/BAUwAwEB/zAcBgNV
HREEFTATgRFub21jb20xNUBpZXRmLm9yZzATBgNVHSUEDDAKBggrBgEFBQcDBDAN
BgkqhkiG9w0BAQsFAAOCAQEAJwLapB9u5N3iK6SCTqh+PVkigZeB2YMVBW8WA3Ut
iRPBj+jHWOpF5pzZHTOcNaAxDEG9lyIlcWqc93A24K/Gen11Tx0hO4FAPOG0+PP8
4lx7F6xeeyUNR44pInrB93G2q0jl+3wjZH8uhBKlGji4UTMpDPpEl6uiyQCbkMMm
Vr7HZH5Dv/lsjGHHf8uJO7+mcMh+tqxLn3DzPrm61OfeWdkoVX2pTz0imRQ3Es+8
I7zNMk+fNNaEEyPnEyHfuWq0uD/qKeP27NZIoINy6E3INQ5QaE2uc1nQULg5y7uJ
toX3j+FUe2UiUak3ACXdrOPSsFP0KRrFwuMnuHHXkGj/Uw==
-----END CERTIFICATE-----
'''
key = '''-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC2QXCsAitYSOgP
Yor77zQnEeHuVqlcuhpH1wpKB+N6WcScA5N3AnX9uZEFOt6McJ+MCiHECdqDlH6n
pQTJlpCpIVgAD4B6xzjRBRww8d3lClA/kKwsKzuX93RS0Uv30hAD6q9wjqK/m6vR
5Y1SsvJYV0y+Yu5j9xUEsojMH7O3NlXWAYOb6oH+f/X7PX27IhtiCwfICMmVWh/h
KeXuFx6HSOcH3gZ6Tlk1llfDbE/ArpsZ6JmnLn73+64yqIoOZOc4JJUPrdsmbNwX
oxQSQhrpwjN8NpSkQaJbHGB3G+OWvP4fpqcweFHxlEq1HhefuR9E6jc3qwxVQfwj
bcq6N/4JAgMBAAECggEAb5SS4YwWc193S2v+QQ2KdVz6YEuINq/tRQw/TWGVACQT
PZzm3FaSXDsOsRAAjiSpWTgewgFyWVpBTGu4CZ73g8RZNvhGpWRwwW8KemCpg/8T
cEcnUYdKXdhuzAE9LETb7znwHM4Gj55DzCZopjfOLQ2Ne4XgAy2THaQcIjRKd6Bw
3mteJ2ityDj3iFN7cq9ntDzp+2BqLOi7AZmLntmUZxtkPCT6k5/dcKFYQW9Eb3bt
MON+BIYVzqhAijkP/cAWmbgZAP9EFng5PpE1lc/shl0W8eX4yvjNoMPRq3wphS4j
L16VncUeDep3vR0CECx7gnTfR0uCDEgKow50pzGQAQKBgQDaQWwK/o39zI3lCGzy
oSNJRNQJ/iZBkbbwpCCaka7VnBfd0ZH54VEWL3oMTkkWRSZtjsPAqT+ndwZitm0D
Kww9FUDMP7j/tMOwAUHYfjYFqFTn6ipkBuby9tbZtL7lgJO6Iu2Qk3afqADD0kcP
zRLxcYSLjrmp9NyUlNnpswR4CQKBgQDVxjwG/orCmiuyA1Bu4u1hdUD0w9CKnyjp
VTbkv8lxk5V3pYzms2Awb0X43W2OioYGBk5yw+9GCF//xCrfbGV7BLZnDTGShjkJ
8oTpLPGBsDSfaKVXE3Hko4LVLBMQIm0tDyuPD1Naia7ZknYn906skonEG8WgHUyp
c/BgkvzWAQKBgBdojuL6/FWtO8bFyZGYUMWJ+Uf9FzNPIpTatZh+aYcFj9W9pW9s
iBreCrQJLXOTBRUZC8u9G1Olw2yQ7k45rr1aazG83+WlCJv29o32s2qV7E1XYyaJ
SvniGZcN+K96w91h46Lu/fkPts1J309FinOU3kdtjmI5HfNdp6WWCrOpAoGBAMjc
TEaeIK8cwPWwG4E1A6pQy8mvu2Ckj4I+KSfh9FsdOpGDIdMas8SOqQZet7P5AFjk
0A0RgN8iu2DMZyQq62cdVG2bffqY1zs7fhrBueILOEaXwtMAWEFmSWYW1YqRbleq
K1luIvms6HdSIGcI/gk0XvG+zn/VR9ToNPHo6lwBAoGBAIrYGYPf+cjZ1V/tNqnL
IecEZb4Gkp1hVhOpNT4U+T2LROxrZtFxxsw2vuIRa5a5FtMbDq9Xyhkm0QppliBd
KQ38jTT0EaD2+vstTqL8vxupo25RQWV1XsmLL4pLbKnm2HnnwB3vEtsiokWKW0q0
Tdb0MiLc+r/zvx8oXtgDjDUa
-----END PRIVATE KEY-----
'''
def provide_private_key_to_test_client(testcase):
session = testcase.client.session
session['NOMCOM_PRIVATE_KEY_%s'%testcase.nc.year()] = key
session.save()
def nomcom_kwargs_for_year(year=None, *args, **kwargs):
if not year:
year = random.randint(1980,2100)
if 'group__state_id' not in kwargs:
kwargs['group__state_id']='active'
if 'group__acronym' not in kwargs:
kwargs['group__acronym'] = 'nomcom%d'%year
if 'group__name' not in kwargs:
kwargs['group__name'] = 'TEST VERSION of IAB/IESG Nominating Committee %d/%d'%(year,year+1)
return kwargs
class NomComFactory(factory.DjangoModelFactory):
class Meta:
model = NomCom
group = factory.SubFactory(GroupFactory,type_id='nomcom')
public_key = factory.django.FileField(data=cert)
@factory.post_generation
def populate_positions(self, create, extracted, **kwargs):
'''
Create a set of nominees and positions unless NomcomFactory is called
with populate_positions=False
'''
if extracted is None:
extracted = True
if create and extracted:
nominees = [NomineeFactory(nomcom=self) for i in range(4)]
positions = [PositionFactory(nomcom=self) for i in range(3)]
def npc(position,nominee,state_id):
return NomineePosition.objects.create(position=position,
nominee=nominee,
state_id=state_id)
# This gives us positions with 0, 1 and 2 nominees, and
# one person who's been nominated for more than one position
npc(positions[0],nominees[0],'accepted')
npc(positions[1],nominees[0],'accepted')
npc(positions[1],nominees[1],'accepted')
npc(positions[0],nominees[2],'pending')
npc(positions[0],nominees[3],'declined')
@factory.post_generation
def populate_personnel(self, create, extracted, **kwargs):
'''
Create a default set of role holders, unless the factory is called
with populate_personnel=False
'''
if extracted is None:
extracted = True
if create and extracted:
#roles= ['chair', 'advisor'] + ['member']*10
roles = ['chair', 'advisor', 'member']
for role in roles:
p = PersonFactory()
self.group.role_set.create(name_id=role,person=p,email=p.email_set.first())
class PositionFactory(factory.DjangoModelFactory):
class Meta:
model = Position
name = factory.Faker('sentence',nb_words=10)
is_open = True
class NomineeFactory(factory.DjangoModelFactory):
class Meta:
model = Nominee
nomcom = factory.SubFactory(NomComFactory)
person = factory.SubFactory(PersonFactory)
email = factory.LazyAttribute(lambda n: n.person.email())
class FeedbackFactory(factory.DjangoModelFactory):
class Meta:
model = Feedback
nomcom = factory.SubFactory(NomComFactory)
subject = factory.Faker('sentence')
comments = factory.Faker('paragraph')
type_id = 'comment'

View file

@ -2,26 +2,30 @@ from django.conf import settings
from django import forms from django import forms
from django.contrib.formtools.preview import FormPreview, AUTO_ID from django.contrib.formtools.preview import FormPreview, AUTO_ID
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.template.loader import render_to_string
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.shortcuts import render_to_response from django.shortcuts import render_to_response
from django.template.context import RequestContext from django.template.context import RequestContext
from django.core.urlresolvers import reverse
from django.utils.html import mark_safe
from ietf.dbtemplate.forms import DBTemplateForm from ietf.dbtemplate.forms import DBTemplateForm
from ietf.group.models import Group, Role from ietf.group.models import Group, Role
from ietf.ietfauth.utils import role_required from ietf.ietfauth.utils import role_required
from ietf.name.models import RoleName, FeedbackTypeName, NomineePositionStateName from ietf.name.models import RoleName, FeedbackTypeName
from ietf.nomcom.models import ( NomCom, Nomination, Nominee, NomineePosition, from ietf.nomcom.models import ( NomCom, Nomination, Nominee, NomineePosition,
Position, Feedback, ReminderDates ) Position, Feedback, ReminderDates )
from ietf.nomcom.utils import (NOMINATION_RECEIPT_TEMPLATE, FEEDBACK_RECEIPT_TEMPLATE, from ietf.nomcom.utils import (NOMINATION_RECEIPT_TEMPLATE, FEEDBACK_RECEIPT_TEMPLATE,
get_user_email, validate_private_key, validate_public_key, get_user_email, validate_private_key, validate_public_key,
get_or_create_nominee, create_feedback_email) make_nomineeposition, make_nomineeposition_for_newperson,
create_feedback_email)
from ietf.person.models import Email from ietf.person.models import Email
from ietf.person.fields import SearchableEmailField from ietf.person.fields import SearchableEmailField, SearchablePersonField, SearchablePersonsField
from ietf.utils.fields import MultiEmailField from ietf.utils.fields import MultiEmailField
from ietf.utils.mail import send_mail from ietf.utils.mail import send_mail
from ietf.mailtrigger.utils import gather_address_lists from ietf.mailtrigger.utils import gather_address_lists
import debug # pyflakes:ignore
ROLODEX_URL = getattr(settings, 'ROLODEX_URL', None) ROLODEX_URL = getattr(settings, 'ROLODEX_URL', None)
@ -40,9 +44,15 @@ class PositionNomineeField(forms.ChoiceField):
positions = Position.objects.get_by_nomcom(self.nomcom).opened().order_by('name') positions = Position.objects.get_by_nomcom(self.nomcom).opened().order_by('name')
results = [] results = []
for position in positions: for position in positions:
nominees = [('%s_%s' % (position.id, i.id), unicode(i)) for i in Nominee.objects.get_by_nomcom(self.nomcom).not_duplicated().filter(nominee_position=position).select_related("email", "email__person")] accepted_nominees = [np.nominee for np in NomineePosition.objects.filter(position=position,state='accepted').exclude(nominee__duplicated__isnull=False)]
nominees = [('%s_%s' % (position.id, i.id), unicode(i)) for i in accepted_nominees]
if nominees: if nominees:
results.append((position.name, nominees)) results.append((position.name+" (Accepted)", nominees))
for position in positions:
other_nominees = [np.nominee for np in NomineePosition.objects.filter(position=position).exclude(state='accepted').exclude(nominee__duplicated__isnull=False)]
nominees = [('%s_%s' % (position.id, i.id), unicode(i)) for i in other_nominees]
if nominees:
results.append((position.name+" (Declined or Pending)", nominees))
kwargs['choices'] = results kwargs['choices'] = results
super(PositionNomineeField, self).__init__(*args, **kwargs) super(PositionNomineeField, self).__init__(*args, **kwargs)
@ -83,35 +93,10 @@ class MultiplePositionNomineeField(forms.MultipleChoiceField, PositionNomineeFie
return result return result
class BaseNomcomForm(object): class EditMembersForm(forms.Form):
def __unicode__(self):
return self.as_div()
def as_div(self):
return render_to_string('nomcom/nomcomform.html', {'form': self})
def get_fieldsets(self):
if not self.fieldsets:
yield dict(name=None, fields=self)
else:
for fieldset, fields in self.fieldsets:
fieldset_dict = dict(name=fieldset, fields=[])
for field_name in fields:
if field_name in self.fields:
fieldset_dict['fields'].append(self[field_name])
if not fieldset_dict['fields']:
# if there is no fields in this fieldset, we continue to next fieldset
continue
yield fieldset_dict
class EditMembersForm(BaseNomcomForm, forms.Form):
members = MultiEmailField(label="Members email", required=False, widget=forms.Textarea) members = MultiEmailField(label="Members email", required=False, widget=forms.Textarea)
fieldsets = [('Members', ('members',))]
class EditMembersFormPreview(FormPreview): class EditMembersFormPreview(FormPreview):
form_template = 'nomcom/edit_members.html' form_template = 'nomcom/edit_members.html'
preview_template = 'nomcom/edit_members_preview.html' preview_template = 'nomcom/edit_members_preview.html'
@ -208,10 +193,8 @@ class EditMembersFormPreview(FormPreview):
return redirect('nomcom_edit_members', year=self.year) return redirect('nomcom_edit_members', year=self.year)
class EditNomcomForm(BaseNomcomForm, forms.ModelForm): class EditNomcomForm(forms.ModelForm):
fieldsets = [('Edit nomcom settings', ('public_key', 'initial_text',
'send_questionnaire', 'reminder_interval'))]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(EditNomcomForm, self).__init__(*args, **kwargs) super(EditNomcomForm, self).__init__(*args, **kwargs)
@ -238,100 +221,42 @@ class EditNomcomForm(BaseNomcomForm, forms.ModelForm):
raise forms.ValidationError('Invalid public key. Error was: %s' % error) raise forms.ValidationError('Invalid public key. Error was: %s' % error)
class MergeForm(BaseNomcomForm, forms.Form): class MergeForm(forms.Form):
secondary_emails = MultiEmailField(label="Secondary email addresses", primary_person = SearchablePersonField(help_text="Select the person you want the datatracker to keep")
help_text="Provide a comma separated list of email addresses. Nominations already received with any of these email address will be moved to show under the primary address.", widget=forms.Textarea) duplicate_persons = SearchablePersonsField(help_text="Select all the duplicates that should be merged into the primary person record")
primary_email = forms.EmailField(label="Primary email address",
widget=forms.TextInput(attrs={'size': '40'}))
fieldsets = [('Emails', ('primary_email', 'secondary_emails'))]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.nomcom = kwargs.pop('nomcom', None) self.nomcom = kwargs.pop('nomcom', None)
super(MergeForm, self).__init__(*args, **kwargs) super(MergeForm, self).__init__(*args, **kwargs)
def clean_primary_email(self):
email = self.cleaned_data['primary_email']
nominees = Nominee.objects.get_by_nomcom(self.nomcom).not_duplicated().filter(email__address=email)
if not nominees:
msg = "No nominee with this email exists"
self._errors["primary_email"] = self.error_class([msg])
return email
def clean_secondary_emails(self):
emails = self.cleaned_data['secondary_emails']
for email in emails:
nominees = Nominee.objects.get_by_nomcom(self.nomcom).not_duplicated().filter(email__address=email)
if not nominees:
msg = "No nominee with email %s exists" % email
self._errors["primary_email"] = self.error_class([msg])
break
return emails
def clean(self): def clean(self):
primary_email = self.cleaned_data.get("primary_email") primary_person = self.cleaned_data.get("primary_person")
secondary_emails = self.cleaned_data.get("secondary_emails") duplicate_persons = self.cleaned_data.get("duplicate_persons")
if primary_email and secondary_emails: if primary_person and duplicate_persons:
if primary_email in secondary_emails: if primary_person in duplicate_persons:
msg = "Primary and secondary email address must be differents" msg = "The primary person must not also be listed as a duplicate person"
self._errors["primary_email"] = self.error_class([msg]) self._errors["primary_person"] = self.error_class([msg])
return self.cleaned_data return self.cleaned_data
def save(self): def save(self):
primary_email = self.cleaned_data.get("primary_email") primary_person = self.cleaned_data.get("primary_person")
secondary_emails = self.cleaned_data.get("secondary_emails") duplicate_persons = self.cleaned_data.get("duplicate_persons")
primary_nominee = Nominee.objects.get_by_nomcom(self.nomcom).get(email__address=primary_email) subject = "Request to merge Person records"
while primary_nominee.duplicated: from_email = settings.NOMCOM_FROM_EMAIL
primary_nominee = primary_nominee.duplicated (to_email, cc) = gather_address_lists('person_merge_requested')
secondary_nominees = Nominee.objects.get_by_nomcom(self.nomcom).filter(email__address__in=secondary_emails) context = {'primary_person':primary_person, 'duplicate_persons':duplicate_persons}
for nominee in secondary_nominees: send_mail(None, to_email, from_email, subject, 'nomcom/merge_request.txt', context, cc=cc)
# move nominations
nominee.nomination_set.all().update(nominee=primary_nominee)
# move feedback
for fb in nominee.feedback_set.all():
fb.nominees.remove(nominee)
fb.nominees.add(primary_nominee)
# move nomineepositions
for nominee_position in nominee.nomineeposition_set.all():
primary_nominee_positions = NomineePosition.objects.filter(position=nominee_position.position,
nominee=primary_nominee)
primary_nominee_position = primary_nominee_positions and primary_nominee_positions[0] or None
if primary_nominee_position: class NominateForm(forms.ModelForm):
# if already a nomineeposition object for a position and nominee, searched_email = SearchableEmailField(only_users=False)
# update the nomineepostion of primary nominee with the state qualifications = forms.CharField(label="Candidate's qualifications for the position",
if nominee_position.state.slug == 'accepted' or primary_nominee_position.state.slug == 'accepted':
primary_nominee_position.state = NomineePositionStateName.objects.get(slug='accepted')
primary_nominee_position.save()
if nominee_position.state.slug == 'declined' and primary_nominee_position.state.slug == 'pending':
primary_nominee_position.state = NomineePositionStateName.objects.get(slug='declined')
primary_nominee_position.save()
else:
# It is not allowed two or more nomineeposition objects with same position and nominee
# move nominee_position object to primary nominee
nominee_position.nominee = primary_nominee
nominee_position.save()
nominee.duplicated = primary_nominee
nominee.save()
secondary_nominees.update(duplicated=primary_nominee)
class NominateForm(BaseNomcomForm, forms.ModelForm):
comments = forms.CharField(label="Candidate's qualifications for the position",
widget=forms.Textarea()) widget=forms.Textarea())
confirmation = forms.BooleanField(label='Email comments back to me as confirmation', confirmation = forms.BooleanField(label='Email comments back to me as confirmation.',
help_text="If you want to get a confirmation mail containing your feedback in cleartext, please check the 'email comments back to me as confirmation'.", help_text="If you want to get a confirmation mail containing your feedback in cleartext, please check the 'email comments back to me as confirmation'.",
required=False) required=False)
fieldsets = [('Candidate Nomination', ('share_nominator','position', 'candidate_name',
'candidate_email', 'candidate_phone', 'comments', 'confirmation'))]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.nomcom = kwargs.pop('nomcom', None) self.nomcom = kwargs.pop('nomcom', None)
self.user = kwargs.pop('user', None) self.user = kwargs.pop('user', None)
@ -339,19 +264,16 @@ class NominateForm(BaseNomcomForm, forms.ModelForm):
super(NominateForm, self).__init__(*args, **kwargs) super(NominateForm, self).__init__(*args, **kwargs)
fieldset = ['share_nominator', new_person_url_name = 'nomcom_%s_nominate_newperson' % ('public' if self.public else 'private' )
'position', self.fields['searched_email'].label = 'Candidate email'
'candidate_name', self.fields['searched_email'].help_text = 'Search by name or email address. Click <a href="%s">here</a> if the search does not find the candidate you want to nominate.' % reverse(new_person_url_name,kwargs={'year':self.nomcom.year()})
'candidate_email', 'candidate_phone',
'comments']
self.fields['nominator_email'].label = 'Nominator email' self.fields['nominator_email'].label = 'Nominator email'
if self.nomcom: if self.nomcom:
self.fields['position'].queryset = Position.objects.get_by_nomcom(self.nomcom).opened() self.fields['position'].queryset = Position.objects.get_by_nomcom(self.nomcom).opened()
self.fields['comments'].help_text = self.nomcom.initial_text self.fields['qualifications'].help_text = self.nomcom.initial_text
if not self.public: if not self.public:
fieldset = ['nominator_email'] + fieldset self.fields.pop('confirmation')
author = get_user_email(self.user) author = get_user_email(self.user)
if author: if author:
self.fields['nominator_email'].initial = author.address self.fields['nominator_email'].initial = author.address
@ -363,22 +285,23 @@ class NominateForm(BaseNomcomForm, forms.ModelForm):
has indicated they will allow NomCom to share their name as one of the people has indicated they will allow NomCom to share their name as one of the people
nominating this candidate.""" nominating this candidate."""
else: else:
fieldset.append('confirmation') self.fields.pop('nominator_email')
self.fieldsets = [('Candidate Nomination', fieldset)]
def save(self, commit=True): def save(self, commit=True):
# Create nomination # Create nomination
nomination = super(NominateForm, self).save(commit=False) nomination = super(NominateForm, self).save(commit=False)
nominator_email = self.cleaned_data.get('nominator_email', None) nominator_email = self.cleaned_data.get('nominator_email', None)
candidate_email = self.cleaned_data['candidate_email'] searched_email = self.cleaned_data['searched_email']
candidate_name = self.cleaned_data['candidate_name']
position = self.cleaned_data['position'] position = self.cleaned_data['position']
comments = self.cleaned_data['comments'] qualifications = self.cleaned_data['qualifications']
confirmation = self.cleaned_data['confirmation'] confirmation = self.cleaned_data.get('confirmation', False)
share_nominator = self.cleaned_data['share_nominator'] share_nominator = self.cleaned_data['share_nominator']
nomcom_template_path = '/nomcom/%s/' % self.nomcom.group.acronym nomcom_template_path = '/nomcom/%s/' % self.nomcom.group.acronym
nomination.candidate_name = searched_email.person.plain_name()
nomination.candidate_email = searched_email.address
author = None author = None
if self.public: if self.public:
author = get_user_email(self.user) author = get_user_email(self.user)
@ -386,11 +309,11 @@ class NominateForm(BaseNomcomForm, forms.ModelForm):
if nominator_email: if nominator_email:
emails = Email.objects.filter(address=nominator_email) emails = Email.objects.filter(address=nominator_email)
author = emails and emails[0] or None author = emails and emails[0] or None
nominee = get_or_create_nominee(self.nomcom, candidate_name, candidate_email, position, author) nominee = make_nomineeposition(self.nomcom, searched_email.person, position, author)
# Complete nomination data # Complete nomination data
feedback = Feedback.objects.create(nomcom=self.nomcom, feedback = Feedback.objects.create(nomcom=self.nomcom,
comments=comments, comments=qualifications,
type=FeedbackTypeName.objects.get(slug='nomina'), type=FeedbackTypeName.objects.get(slug='nomina'),
user=self.user) user=self.user)
feedback.positions.add(position) feedback.positions.add(position)
@ -416,7 +339,117 @@ class NominateForm(BaseNomcomForm, forms.ModelForm):
from_email = settings.NOMCOM_FROM_EMAIL from_email = settings.NOMCOM_FROM_EMAIL
(to_email, cc) = gather_address_lists('nomination_receipt_requested',nominator=author.address) (to_email, cc) = gather_address_lists('nomination_receipt_requested',nominator=author.address)
context = {'nominee': nominee.email.person.name, context = {'nominee': nominee.email.person.name,
'comments': comments, 'comments': qualifications,
'position': position.name}
path = nomcom_template_path + NOMINATION_RECEIPT_TEMPLATE
send_mail(None, to_email, from_email, subject, path, context, cc=cc)
return nomination
class Meta:
model = Nomination
fields = ('share_nominator', 'position', 'nominator_email', 'searched_email',
'candidate_phone', 'qualifications', 'confirmation')
class NominateNewPersonForm(forms.ModelForm):
qualifications = forms.CharField(label="Candidate's qualifications for the position",
widget=forms.Textarea())
confirmation = forms.BooleanField(label='Email comments back to me as confirmation.',
help_text="If you want to get a confirmation mail containing your feedback in cleartext, please check the 'email comments back to me as confirmation'.",
required=False)
def __init__(self, *args, **kwargs):
self.nomcom = kwargs.pop('nomcom', None)
self.user = kwargs.pop('user', None)
self.public = kwargs.pop('public', None)
super(NominateNewPersonForm, self).__init__(*args, **kwargs)
self.fields['nominator_email'].label = 'Nominator email'
if self.nomcom:
self.fields['position'].queryset = Position.objects.get_by_nomcom(self.nomcom).opened()
self.fields['qualifications'].help_text = self.nomcom.initial_text
if not self.public:
self.fields.pop('confirmation')
author = get_user_email(self.user)
if author:
self.fields['nominator_email'].initial = author.address
help_text = """(Nomcom Chair/Member: please fill this in. Use your own email address if the person making the
nomination wishes to be anonymous. The confirmation email will be sent to the address given here,
and the address will also be captured as part of the registered nomination.)"""
self.fields['nominator_email'].help_text = help_text
self.fields['share_nominator'].help_text = """(Nomcom Chair/Member: Check this box if the person providing this nomination
has indicated they will allow NomCom to share their name as one of the people
nominating this candidate."""
else:
self.fields.pop('nominator_email')
def clean_candidate_email(self):
candidate_email = self.cleaned_data['candidate_email']
if Email.objects.filter(address=candidate_email).exists():
normal_url_name = 'nomcom_%s_nominate' % 'public' if self.public else 'private'
msg = '%s is already in the datatracker. \
Use the <a href="%s">normal nomination form</a> to nominate the person \
with this address.\
' % (candidate_email,reverse(normal_url_name,kwargs={'year':self.nomcom.year()}))
raise forms.ValidationError(mark_safe(msg))
return candidate_email
def save(self, commit=True):
# Create nomination
nomination = super(NominateNewPersonForm, self).save(commit=False)
nominator_email = self.cleaned_data.get('nominator_email', None)
candidate_email = self.cleaned_data['candidate_email']
candidate_name = self.cleaned_data['candidate_name']
position = self.cleaned_data['position']
qualifications = self.cleaned_data['qualifications']
confirmation = self.cleaned_data.get('confirmation', False)
share_nominator = self.cleaned_data['share_nominator']
nomcom_template_path = '/nomcom/%s/' % self.nomcom.group.acronym
author = None
if self.public:
author = get_user_email(self.user)
else:
if nominator_email:
emails = Email.objects.filter(address=nominator_email)
author = emails and emails[0] or None
## This is where it should change - validation of the email field should fail if the email exists
## The function should become make_nominee_from_newperson)
nominee = make_nomineeposition_for_newperson(self.nomcom, candidate_name, candidate_email, position, author)
# Complete nomination data
feedback = Feedback.objects.create(nomcom=self.nomcom,
comments=qualifications,
type=FeedbackTypeName.objects.get(slug='nomina'),
user=self.user)
feedback.positions.add(position)
feedback.nominees.add(nominee)
if author:
nomination.nominator_email = author.address
feedback.author = author.address
feedback.save()
nomination.nominee = nominee
nomination.comments = feedback
nomination.share_nominator = share_nominator
nomination.user = self.user
if commit:
nomination.save()
# send receipt email to nominator
if confirmation:
if author:
subject = 'Nomination receipt'
from_email = settings.NOMCOM_FROM_EMAIL
(to_email, cc) = gather_address_lists('nomination_receipt_requested',nominator=author.address)
context = {'nominee': nominee.email.person.name,
'comments': qualifications,
'position': position.name} 'position': position.name}
path = nomcom_template_path + NOMINATION_RECEIPT_TEMPLATE path = nomcom_template_path + NOMINATION_RECEIPT_TEMPLATE
send_mail(None, to_email, from_email, subject, path, context, cc=cc) send_mail(None, to_email, from_email, subject, path, context, cc=cc)
@ -426,22 +459,15 @@ class NominateForm(BaseNomcomForm, forms.ModelForm):
class Meta: class Meta:
model = Nomination model = Nomination
fields = ('share_nominator', 'position', 'nominator_email', 'candidate_name', fields = ('share_nominator', 'position', 'nominator_email', 'candidate_name',
'candidate_email', 'candidate_phone') 'candidate_email', 'candidate_phone', 'qualifications', 'confirmation')
class FeedbackForm(BaseNomcomForm, forms.ModelForm): class FeedbackForm(forms.ModelForm):
position_name = forms.CharField(label='Position', nominator_email = forms.CharField(label='Commenter email',required=False)
widget=forms.TextInput(attrs={'size': '40'}))
nominee_name = forms.CharField(label='Nominee name',
widget=forms.TextInput(attrs={'size': '40'}))
nominee_email = forms.CharField(label='Nominee email',
widget=forms.TextInput(attrs={'size': '40'}))
nominator_email = forms.CharField(label='Commenter email')
comments = forms.CharField(label='Comments on this nominee', comments = forms.CharField(label='Comments',
widget=forms.Textarea()) widget=forms.Textarea())
confirmation = forms.BooleanField(label='Email comments back to me as confirmation', confirmation = forms.BooleanField(label='Email comments back to me as confirmation (if selected, your comments will be emailed to you in cleartext when you press Save).',
help_text="If you want to get a confirmation mail containing your feedback in cleartext, please check the 'email comments back to me as confirmation'.",
required=False) required=False)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -453,72 +479,44 @@ class FeedbackForm(BaseNomcomForm, forms.ModelForm):
super(FeedbackForm, self).__init__(*args, **kwargs) super(FeedbackForm, self).__init__(*args, **kwargs)
readonly_fields = ['position_name', author = get_user_email(self.user)
'nominee_name',
'nominee_email']
fieldset = ['position_name',
'nominee_name',
'nominee_email',
'nominator_email',
'comments']
if self.public: if self.public:
readonly_fields += ['nominator_email'] self.fields.pop('nominator_email')
fieldset.append('confirmation')
else: else:
help_text = """(Nomcom Chair/Member: please fill this in. Use your own email address if the person making the help_text = """(Nomcom Chair/Member: please fill this in. Use your own email address if the person making the
comments wishes to be anonymous. The confirmation email will be sent to the address given here, comments wishes to be anonymous. The confirmation email will be sent to the address given here,
and the address will also be captured as part of the registered nomination.)""" and the address will also be captured as part of the registered nomination.)"""
self.fields['nominator_email'].help_text = help_text self.fields['nominator_email'].help_text = help_text
self.fields['nominator_email'].required = False self.fields['confirmation'].label = 'Email these comments in cleartext to the provided commenter email address'
if author:
self.fields['nominator_email'].initial = author.address
author = get_user_email(self.user)
if author:
self.fields['nominator_email'].initial = author.address
if self.position and self.nominee:
self.fields['position_name'].initial = self.position.name
self.fields['nominee_name'].initial = self.nominee.email.person.name
self.fields['nominee_email'].initial = self.nominee.email.address
else:
help_text = "Please pick a name on the nominees list"
self.fields['position_name'].initial = help_text
self.fields['nominee_name'].initial = help_text
self.fields['nominee_email'].initial = help_text
self.fields['comments'].initial = help_text
readonly_fields += ['comments']
self.fields['confirmation'].widget.attrs['disabled'] = "disabled"
for field in readonly_fields:
self.fields[field].widget.attrs['readonly'] = True
self.fieldsets = [('Provide comments', fieldset)]
def clean(self): def clean(self):
if not NomineePosition.objects.accepted().filter(nominee=self.nominee, if not NomineePosition.objects.accepted().filter(nominee=self.nominee,
position=self.position): position=self.position):
msg = "There isn't a accepted nomination for %s on the %s position" % (self.nominee, self.position) msg = "There isn't a accepted nomination for %s on the %s position" % (self.nominee, self.position)
self._errors["nominee_email"] = self.error_class([msg]) self._errors["comments"] = self.error_class([msg])
return self.cleaned_data return self.cleaned_data
def save(self, commit=True): def save(self, commit=True):
feedback = super(FeedbackForm, self).save(commit=False) feedback = super(FeedbackForm, self).save(commit=False)
confirmation = self.cleaned_data['confirmation'] confirmation = self.cleaned_data['confirmation']
comments = self.cleaned_data['comments'] comments = self.cleaned_data['comments']
nominator_email = self.cleaned_data['nominator_email']
nomcom_template_path = '/nomcom/%s/' % self.nomcom.group.acronym nomcom_template_path = '/nomcom/%s/' % self.nomcom.group.acronym
author = None author = None
if self.public: if self.public:
author = get_user_email(self.user) author = get_user_email(self.user)
else: else:
nominator_email = self.cleaned_data['nominator_email']
if nominator_email: if nominator_email:
emails = Email.objects.filter(address=nominator_email) emails = Email.objects.filter(address=nominator_email)
author = emails and emails[0] or None author = emails and emails[0] or None
if author: if author:
feedback.author = author feedback.author = author.address
feedback.nomcom = self.nomcom feedback.nomcom = self.nomcom
feedback.user = self.user feedback.user = self.user
@ -541,18 +539,16 @@ class FeedbackForm(BaseNomcomForm, forms.ModelForm):
class Meta: class Meta:
model = Feedback model = Feedback
fields = ('nominee_name', fields = (
'nominee_email',
'nominator_email', 'nominator_email',
'comments',
'confirmation', 'confirmation',
'comments') )
class FeedbackEmailForm(BaseNomcomForm, forms.Form): class FeedbackEmailForm(forms.Form):
email_text = forms.CharField(label='Email text', widget=forms.Textarea()) email_text = forms.CharField(label='Email text', widget=forms.Textarea())
fieldsets = [('Feedback email', ('email_text',))]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.nomcom = kwargs.pop('nomcom', None) self.nomcom = kwargs.pop('nomcom', None)
super(FeedbackEmailForm, self).__init__(*args, **kwargs) super(FeedbackEmailForm, self).__init__(*args, **kwargs)
@ -560,12 +556,10 @@ class FeedbackEmailForm(BaseNomcomForm, forms.Form):
def save(self, commit=True): def save(self, commit=True):
create_feedback_email(self.nomcom, self.cleaned_data['email_text']) create_feedback_email(self.nomcom, self.cleaned_data['email_text'])
class QuestionnaireForm(BaseNomcomForm, forms.ModelForm): class QuestionnaireForm(forms.ModelForm):
comments = forms.CharField(label='Questionnaire response from this candidate', comments = forms.CharField(label='Questionnaire response from this candidate',
widget=forms.Textarea()) widget=forms.Textarea())
fieldsets = [('New questionnaire response', ('nominee', 'comments'))]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.nomcom = kwargs.pop('nomcom', None) self.nomcom = kwargs.pop('nomcom', None)
self.user = kwargs.pop('user', None) self.user = kwargs.pop('user', None)
@ -594,21 +588,14 @@ class QuestionnaireForm(BaseNomcomForm, forms.ModelForm):
model = Feedback model = Feedback
fields = ( 'comments', ) fields = ( 'comments', )
class NomComTemplateForm(BaseNomcomForm, DBTemplateForm): class NomComTemplateForm(DBTemplateForm):
content = forms.CharField(label="Text", widget=forms.Textarea(attrs={'cols': '120', 'rows':'40', })) content = forms.CharField(label="Text", widget=forms.Textarea(attrs={'cols': '120', 'rows':'40', }))
fieldsets = [('Template content', ('content', )), ]
class PositionForm(forms.ModelForm):
class PositionForm(BaseNomcomForm, forms.ModelForm):
fieldsets = [('Position', ('name', 'description',
'is_open', 'incumbent'))]
incumbent = SearchableEmailField(required=False)
class Meta: class Meta:
model = Position model = Position
fields = ('name', 'description', 'is_open', 'incumbent') fields = ('name', 'is_open')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.nomcom = kwargs.pop('nomcom', None) self.nomcom = kwargs.pop('nomcom', None)
@ -619,12 +606,10 @@ class PositionForm(BaseNomcomForm, forms.ModelForm):
super(PositionForm, self).save(*args, **kwargs) super(PositionForm, self).save(*args, **kwargs)
class PrivateKeyForm(BaseNomcomForm, forms.Form): class PrivateKeyForm(forms.Form):
key = forms.CharField(label='Private key', widget=forms.Textarea(), required=False) key = forms.CharField(label='Private key', widget=forms.Textarea(), required=False)
fieldsets = [('Private key', ('key',))]
def clean_key(self): def clean_key(self):
key = self.cleaned_data.get('key', None) key = self.cleaned_data.get('key', None)
if not key: if not key:
@ -635,7 +620,7 @@ class PrivateKeyForm(BaseNomcomForm, forms.Form):
raise forms.ValidationError('Invalid private key. Error was: %s' % error) raise forms.ValidationError('Invalid private key. Error was: %s' % error)
class PendingFeedbackForm(BaseNomcomForm, forms.ModelForm): class PendingFeedbackForm(forms.ModelForm):
type = forms.ModelChoiceField(queryset=FeedbackTypeName.objects.all().order_by('pk'), widget=forms.RadioSelect, empty_label='Unclassified', required=False) type = forms.ModelChoiceField(queryset=FeedbackTypeName.objects.all().order_by('pk'), widget=forms.RadioSelect, empty_label='Unclassified', required=False)
@ -643,13 +628,6 @@ class PendingFeedbackForm(BaseNomcomForm, forms.ModelForm):
model = Feedback model = Feedback
fields = ('type', ) fields = ('type', )
def __init__(self, *args, **kwargs):
super(PendingFeedbackForm, self).__init__(*args, **kwargs)
try:
self.default_type = FeedbackTypeName.objects.get(slug=settings.DEFAULT_FEEDBACK_TYPE)
except FeedbackTypeName.DoesNotExist:
self.default_type = None
def set_nomcom(self, nomcom, user): def set_nomcom(self, nomcom, user):
self.nomcom = nomcom self.nomcom = nomcom
self.user = user self.user = user
@ -665,17 +643,6 @@ class PendingFeedbackForm(BaseNomcomForm, forms.ModelForm):
feedback.save() feedback.save()
return feedback return feedback
def move_to_default(self):
if not self.default_type or self.cleaned_data.get('type', None):
return None
feedback = super(PendingFeedbackForm, self).save(commit=False)
feedback.nomcom = self.nomcom
feedback.user = self.user
feedback.type = self.default_type
feedback.save()
return feedback
class ReminderDatesForm(forms.ModelForm): class ReminderDatesForm(forms.ModelForm):
class Meta: class Meta:
@ -711,17 +678,38 @@ class MutableFeedbackForm(forms.ModelForm):
if self.feedback_type.slug != 'nomina': if self.feedback_type.slug != 'nomina':
self.fields['nominee'] = MultiplePositionNomineeField(nomcom=self.nomcom, self.fields['nominee'] = MultiplePositionNomineeField(nomcom=self.nomcom,
required=True, required=True,
widget=forms.SelectMultiple, widget=forms.SelectMultiple(attrs={'class':'nominee_multi_select','size':'12'}),
help_text='Hold down "Control", or "Command" on a Mac, to select more than one.') help_text='Hold down "Control", or "Command" on a Mac, to select more than one.')
else: else:
self.fields['position'] = forms.ModelChoiceField(queryset=Position.objects.get_by_nomcom(self.nomcom).opened(), label="Position") self.fields['position'] = forms.ModelChoiceField(queryset=Position.objects.get_by_nomcom(self.nomcom).opened(), label="Position")
self.fields['candidate_name'] = forms.CharField(label="Candidate name") 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)
self.fields['candidate_email'] = forms.EmailField(label="Candidate email") self.fields['candidate_name'] = forms.CharField(label="Candidate name",help_text="Only fill in this name field if the search doesn't find the person you are classifying",required=False)
self.fields['candidate_email'] = forms.EmailField(label="Candidate email",help_text="Only fill in this email field if the search doesn't find the person you are classifying",required=False)
self.fields['candidate_phone'] = forms.CharField(label="Candidate phone", required=False) self.fields['candidate_phone'] = forms.CharField(label="Candidate phone", required=False)
def clean(self):
cleaned_data = super(MutableFeedbackForm,self).clean()
if self.feedback_type.slug == 'nomina':
searched_email = self.cleaned_data.get('searched_email')
candidate_name = self.cleaned_data.get('candidate_name')
if candidate_name:
candidate_name = candidate_name.strip()
candidate_email = self.cleaned_data.get('candidate_email')
if candidate_email:
candidate_email = candidate_email.strip()
if not any([ searched_email and not candidate_name and not candidate_email,
not searched_email and candidate_name and candidate_email,
]):
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)
return cleaned_data
def save(self, commit=True): def save(self, commit=True):
feedback = super(MutableFeedbackForm, self).save(commit=False) feedback = super(MutableFeedbackForm, self).save(commit=False)
if self.instance.type.slug == 'nomina': if self.instance.type.slug == 'nomina':
searched_email = self.cleaned_data['searched_email']
candidate_email = self.cleaned_data['candidate_email'] candidate_email = self.cleaned_data['candidate_email']
candidate_name = self.cleaned_data['candidate_name'] candidate_name = self.cleaned_data['candidate_name']
candidate_phone = self.cleaned_data['candidate_phone'] candidate_phone = self.cleaned_data['candidate_phone']
@ -733,7 +721,10 @@ class MutableFeedbackForm(forms.ModelForm):
emails = Email.objects.filter(address=nominator_email) emails = Email.objects.filter(address=nominator_email)
author = emails and emails[0] or None author = emails and emails[0] or None
nominee = get_or_create_nominee(self.nomcom, candidate_name, candidate_email, position, author) if searched_email:
nominee = make_nomineeposition(self.nomcom, searched_email.person, position, author)
else:
nominee = make_nomineeposition_for_newperson(self.nomcom, candidate_name, candidate_email, position, author)
feedback.nominees.add(nominee) feedback.nominees.add(nominee)
feedback.positions.add(position) feedback.positions.add(position)
Nomination.objects.create( Nomination.objects.create(
@ -768,41 +759,25 @@ FullFeedbackFormSet = forms.modelformset_factory(
class EditNomineeForm(forms.ModelForm): class EditNomineeForm(forms.ModelForm):
nominee_email = forms.EmailField(label="Nominee email", nominee_email = forms.ModelChoiceField(queryset=Email.objects.none(),empty_label=None)
widget=forms.TextInput(attrs={'size': '40'}))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(EditNomineeForm, self).__init__(*args, **kwargs) super(EditNomineeForm, self).__init__(*args, **kwargs)
self.fields['nominee_email'].initial = self.instance.email.address self.fields['nominee_email'].queryset = Email.objects.filter(person=self.instance.person,active=True)
self.fields['nominee_email'].initial = self.instance.email
self.fields['nominee_email'].help_text = "If the address you are looking for does not appear in this list, ask the nominee (or the secretariat) to add the address to thier datatracker account and ensure it is marked as active."
def save(self, commit=True): def save(self, commit=True):
nominee = super(EditNomineeForm, self).save(commit=False) nominee = super(EditNomineeForm, self).save(commit=False)
nominee_email = self.cleaned_data.get("nominee_email") nominee_email = self.cleaned_data.get("nominee_email")
if nominee_email != nominee.email.address: nominee.email = nominee_email
# create a new nominee with the new email nominee.save()
new_email, created_email = Email.objects.get_or_create(address=nominee_email)
new_email.person = nominee.email.person
new_email.save()
# Chage emails between nominees
old_email = nominee.email
nominee.email = new_email
nominee.save()
new_nominee = Nominee.objects.create(email=old_email, nomcom=nominee.nomcom)
# new nominees point to old nominee
new_nominee.duplicated = nominee
new_nominee.save()
return nominee return nominee
class Meta: class Meta:
model = Nominee model = Nominee
fields = ('nominee_email',) fields = ('nominee_email',)
def clean_nominee_email(self): class NominationResponseCommentForm(forms.Form):
nominee_email = self.cleaned_data['nominee_email'] comments = forms.CharField(widget=forms.Textarea,required=False,help_text="Any comments provided will be encrytped and will only be visible to the NomCom.")
nominees = Nominee.objects.exclude(email__address=self.instance.email.address).filter(email__address=nominee_email)
if nominees:
raise forms.ValidationError('This emails already does exists in another nominee, please go to merge form')
return nominee_email

View file

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('nomcom', '0004_auto_20151027_0829'),
]
operations = [
migrations.RemoveField(
model_name='position',
name='incumbent',
),
]

View file

@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
def set_new_template_content(apps, schema_editor):
DBTemplate = apps.get_model('dbtemplate','DBTemplate')
h = DBTemplate.objects.get(path='/nomcom/defaults/position/header_questionnaire.txt')
h.content = """Hi $nominee, this is the questionnaire for the position $position.
Please follow the directions in the questionnaire closely - you may see
that some changes have been made from previous years, so please take note.
We look forward to reading your questionnaire response! If you have any
administrative questions, please send mail to nomcom-chair@ietf.org.
You may have received this questionnaire before accepting the nomination. A
separate message, sent at the time of nomination, provides instructions for
indicating whether you accept or decline. If you have not completed those
steps, please do so as soon as possible, or contact the nomcom chair.
Thank you!
"""
h.save()
h = DBTemplate.objects.get(path='/nomcom/defaults/position/questionnaire.txt')
h.content = """NomCom Chair: Replace this content with the appropriate questionnaire for the position $position.
"""
h.save()
def revert_to_old_template_content(apps, schema_editor):
DBTemplate = apps.get_model('dbtemplate','DBTemplate')
h = DBTemplate.objects.get(path='/nomcom/defaults/position/header_questionnaire.txt')
h.content = """Hi $nominee, this is the questionnaire for the position $position.
Please follow the directions in the questionnaire closely - you may see
that some changes have been made from previous years, so please take note.
We look forward to reading your questionnaire response! If you have any
administrative questions, please send mail to nomcom-chair@ietf.org.
Thank you!
"""
h.save()
h = DBTemplate.objects.get(path='/nomcom/defaults/position/questionnaire.txt')
h.content = """Enter here the questionnaire for the position $position:
Questionnaire
"""
h.save()
class Migration(migrations.Migration):
dependencies = [
('nomcom', '0005_remove_position_incumbent'),
]
operations = [
migrations.RunPython(set_new_template_content,revert_to_old_template_content)
]

View file

@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import datetime
from django.db import models, migrations
def create_lastseen(apps, schema_editor):
NomCom = apps.get_model('nomcom','NomCom')
FeedbackLastSeen = apps.get_model('nomcom','FeedbackLastSeen')
now = datetime.datetime.now()
for nc in NomCom.objects.all():
reviewers = [r.person for r in nc.group.role_set.all()]
nominees = nc.nominee_set.all()
for r in reviewers:
for n in nominees:
FeedbackLastSeen.objects.create(reviewer=r,nominee=n,time=now)
def remove_lastseen(apps, schema_editor):
FeedbackLastSeen = apps.get_model('nomcom','FeedbackLastSeen')
FeedbackLastSeen.objects.delete()
class Migration(migrations.Migration):
dependencies = [
('person', '0004_auto_20150308_0440'),
('group', '0006_auto_20150718_0509'),
('nomcom', '0006_improve_default_questionnaire_templates'),
]
operations = [
migrations.CreateModel(
name='FeedbackLastSeen',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('time', models.DateTimeField(auto_now=True)),
('nominee', models.ForeignKey(to='nomcom.Nominee')),
('reviewer', models.ForeignKey(to='person.Person')),
],
options={
},
bases=(models.Model,),
),
migrations.RunPython(create_lastseen,remove_lastseen)
]

View file

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('nomcom', '0007_feedbacklastseen'),
]
operations = [
migrations.RemoveField(
model_name='position',
name='description',
),
migrations.AlterField(
model_name='position',
name='name',
field=models.CharField(help_text=b'This short description will appear on the Nomination and Feedback pages. Be as descriptive as necessary. Past examples: "Transport AD", "IAB Member"', max_length=255, verbose_name=b'Name'),
preserve_default=True,
),
]

View file

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
def remove_extension(apps, schema_editor):
DBTemplate = apps.get_model('dbtemplate','DBTemplate')
for template in DBTemplate.objects.filter(path__endswith="requirements.txt"):
template.path = template.path[:-4]
template.save()
def restore_extension(apps, schema_editor):
DBTemplate = apps.get_model('dbtemplate','DBTemplate')
for template in DBTemplate.objects.filter(path__endswith="requirements"):
template.path = template.path+".txt"
template.save()
def default_rst(apps, schema_editor):
DBTemplate = apps.get_model('dbtemplate','DBTemplate')
default_req = DBTemplate.objects.get(path__startswith='/nomcom/defaults/position/requirements')
default_req.type_id = 'rst'
default_req.save()
def default_plain(apps, schema_editor):
DBTemplate = apps.get_model('dbtemplate','DBTemplate')
default_req = DBTemplate.objects.get(path__startswith='/nomcom/defaults/position/requirements')
default_req.type_id = 'plain'
default_req.save()
def rst_2015(apps, schema_editor):
DBTemplate = apps.get_model('dbtemplate','DBTemplate')
DBTemplate.objects.filter(path__startswith='/nomcom/nomcom2015/').filter(path__contains='position/requirements').exclude(path__contains='/27/').update(type_id='rst')
def plain_2015(apps, schema_editor):
DBTemplate = apps.get_model('dbtemplate','DBTemplate')
DBTemplate.objects.filter(path__startswith='/nomcom/nomcom2015/').filter(path__contains='position/requirements').update(type_id='plain')
class Migration(migrations.Migration):
dependencies = [
('nomcom', '0008_auto_20151209_1423'),
('dbtemplate', '0002_auto_20141222_1749'),
]
operations = [
migrations.RunPython(remove_extension,restore_extension),
migrations.RunPython(default_rst,default_plain),
migrations.RunPython(rst_2015,plain_2015),
]

View file

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
def populate_person(apps, schema_editor):
Nominee = apps.get_model('nomcom','Nominee')
for n in Nominee.objects.all():
n.person = n.email.person
n.save()
class Migration(migrations.Migration):
dependencies = [
('person', '0004_auto_20150308_0440'),
('nomcom', '0009_remove_requirements_dbtemplate_type_from_path'),
]
operations = [
migrations.AddField(
model_name='nominee',
name='person',
field=models.ForeignKey(blank=True, to='person.Person', null=True),
preserve_default=True,
),
migrations.RunPython(populate_person,None)
]

View file

@ -7,9 +7,10 @@ from django.conf import settings
from django.core.files.storage import FileSystemStorage from django.core.files.storage import FileSystemStorage
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.template.defaultfilters import linebreaks
from ietf.nomcom.fields import EncryptedTextField from ietf.nomcom.fields import EncryptedTextField
from ietf.person.models import Email from ietf.person.models import Person,Email
from ietf.group.models import Group from ietf.group.models import Group
from ietf.name.models import NomineePositionStateName, FeedbackTypeName from ietf.name.models import NomineePositionStateName, FeedbackTypeName
from ietf.dbtemplate.models import DBTemplate from ietf.dbtemplate.models import DBTemplate
@ -66,6 +67,14 @@ class NomCom(models.Model):
if created: if created:
initialize_templates_for_group(self) initialize_templates_for_group(self)
def year(self):
year = getattr(self,'_cached_year',None)
if year is None:
if self.group and self.group.acronym.startswith('nomcom'):
year = int(self.group.acronym[6:])
self._cached_year = year
return year
def delete_nomcom(sender, **kwargs): def delete_nomcom(sender, **kwargs):
nomcom = kwargs.get('instance', None) nomcom = kwargs.get('instance', None)
@ -102,6 +111,7 @@ class Nomination(models.Model):
class Nominee(models.Model): class Nominee(models.Model):
email = models.ForeignKey(Email) email = models.ForeignKey(Email)
person = models.ForeignKey(Person, blank=True, null=True)
nominee_position = models.ManyToManyField('Position', through='NomineePosition') nominee_position = models.ManyToManyField('Position', through='NomineePosition')
duplicated = models.ForeignKey('Nominee', blank=True, null=True) duplicated = models.ForeignKey('Nominee', blank=True, null=True)
nomcom = models.ForeignKey('NomCom') nomcom = models.ForeignKey('NomCom')
@ -118,6 +128,12 @@ class Nominee(models.Model):
else: else:
return self.email.address return self.email.address
def name(self):
if self.email.person and self.email.person.name:
return u'%s' % (self.email.person.plain_name(),)
else:
return self.email.address
class NomineePosition(models.Model): class NomineePosition(models.Model):
@ -150,12 +166,10 @@ class NomineePosition(models.Model):
class Position(models.Model): class Position(models.Model):
nomcom = models.ForeignKey('NomCom') nomcom = models.ForeignKey('NomCom')
name = models.CharField(verbose_name='Name', max_length=255) name = models.CharField(verbose_name='Name', max_length=255, help_text='This short description will appear on the Nomination and Feedback pages. Be as descriptive as necessary. Past examples: "Transport AD", "IAB Member"')
description = models.TextField(verbose_name='Description')
requirement = models.ForeignKey(DBTemplate, related_name='requirement', null=True, editable=False) requirement = models.ForeignKey(DBTemplate, related_name='requirement', null=True, editable=False)
questionnaire = models.ForeignKey(DBTemplate, related_name='questionnaire', null=True, editable=False) questionnaire = models.ForeignKey(DBTemplate, related_name='questionnaire', null=True, editable=False)
is_open = models.BooleanField(verbose_name='Is open', default=False) is_open = models.BooleanField(verbose_name='Is open', default=False)
incumbent = models.ForeignKey(Email, null=True, blank=True)
objects = PositionManager() objects = PositionManager()
@ -189,7 +203,10 @@ class Position(models.Model):
return render_to_string(self.questionnaire.path, {'position': self}) return render_to_string(self.questionnaire.path, {'position': self})
def get_requirement(self): def get_requirement(self):
return render_to_string(self.requirement.path, {'position': self}) rendered = render_to_string(self.requirement.path, {'position': self})
if self.requirement.type_id=='plain':
rendered = linebreaks(rendered)
return rendered
class Feedback(models.Model): class Feedback(models.Model):
@ -211,4 +228,7 @@ class Feedback(models.Model):
class Meta: class Meta:
ordering = ['time'] ordering = ['time']
class FeedbackLastSeen(models.Model):
reviewer = models.ForeignKey(Person)
nominee = models.ForeignKey(Nominee)
time = models.DateTimeField(auto_now=True)

View file

@ -25,13 +25,11 @@ class NomComResource(ModelResource):
} }
api.nomcom.register(NomComResource()) api.nomcom.register(NomComResource())
from ietf.person.resources import EmailResource
from ietf.dbtemplate.resources import DBTemplateResource from ietf.dbtemplate.resources import DBTemplateResource
class PositionResource(ModelResource): class PositionResource(ModelResource):
nomcom = ToOneField(NomComResource, 'nomcom') nomcom = ToOneField(NomComResource, 'nomcom')
requirement = ToOneField(DBTemplateResource, 'requirement', null=True) requirement = ToOneField(DBTemplateResource, 'requirement', null=True)
questionnaire = ToOneField(DBTemplateResource, 'questionnaire', null=True) questionnaire = ToOneField(DBTemplateResource, 'questionnaire', null=True)
incumbent = ToOneField(EmailResource, 'incumbent', null=True)
class Meta: class Meta:
queryset = Position.objects.all() queryset = Position.objects.all()
serializer = api.Serializer() serializer = api.Serializer()
@ -39,12 +37,10 @@ class PositionResource(ModelResource):
filtering = { filtering = {
"id": ALL, "id": ALL,
"name": ALL, "name": ALL,
"description": ALL,
"is_open": ALL, "is_open": ALL,
"nomcom": ALL_WITH_RELATIONS, "nomcom": ALL_WITH_RELATIONS,
"requirement": ALL_WITH_RELATIONS, "requirement": ALL_WITH_RELATIONS,
"questionnaire": ALL_WITH_RELATIONS, "questionnaire": ALL_WITH_RELATIONS,
"incumbent": ALL_WITH_RELATIONS,
} }
api.nomcom.register(PositionResource()) api.nomcom.register(PositionResource())
@ -148,3 +144,17 @@ class NominationResource(ModelResource):
} }
api.nomcom.register(NominationResource()) api.nomcom.register(NominationResource())
from ietf.person.resources import PersonResource
class FeedbackLastSeenResource(ModelResource):
reviewer = ToOneField(PersonResource, 'reviewer')
nominee = ToOneField(NomineeResource, 'nominee')
class Meta:
queryset = FeedbackLastSeen.objects.all()
serializer = api.Serializer()
filtering = {
"id": ALL,
"time": ALL,
"reviewer": ALL_WITH_RELATIONS,
"nominee": ALL_WITH_RELATIONS,
}
api.nomcom.register(FeedbackLastSeenResource())

View file

@ -7,43 +7,32 @@ from django.template.defaultfilters import linebreaksbr, force_escape
from ietf.utils.pipe import pipe from ietf.utils.pipe import pipe
from ietf.utils.log import log from ietf.utils.log import log
from ietf.ietfauth.utils import has_role
from ietf.doc.templatetags.ietf_filters import wrap_text from ietf.doc.templatetags.ietf_filters import wrap_text
from ietf.person.models import Person from ietf.person.models import Person
from ietf.nomcom.models import Feedback from ietf.nomcom.utils import get_nomcom_by_year, retrieve_nomcom_private_key
from ietf.nomcom.utils import get_nomcom_by_year, get_user_email, retrieve_nomcom_private_key
import debug # pyflakes:ignore
register = template.Library() register = template.Library()
@register.filter @register.filter
def is_chair(user, year): def is_chair_or_advisor(user, year):
if not user or not year: if not user or not year:
return False return False
nomcom = get_nomcom_by_year(year=year) nomcom = get_nomcom_by_year(year=year)
if has_role(user, "Secretariat"): return nomcom.group.has_role(user, ["chair","advisor"])
return True
return nomcom.group.has_role(user, "chair")
@register.filter @register.filter
def has_publickey(nomcom): def has_publickey(nomcom):
return nomcom and nomcom.public_key and True or False return nomcom and nomcom.public_key and True or False
@register.filter
@register.simple_tag def lookup(container,key):
def add_num_nominations(user, position, nominee): return container and container.get(key,None)
author = get_user_email(user)
count = Feedback.objects.filter(positions__in=[position],
nominees__in=[nominee],
author=author,
type='comment').count()
return '<span class="badge" title="%d earlier comments from you on %s as %s">%s</span>&nbsp;' % (count, nominee.email.address, position, count)
@register.filter @register.filter
def formatted_email(address): def formatted_email(address):

View file

@ -21,19 +21,19 @@ SECRETARIAT_USER = 'secretary'
EMAIL_DOMAIN = '@example.com' EMAIL_DOMAIN = '@example.com'
NOMCOM_YEAR = "2013" NOMCOM_YEAR = "2013"
POSITIONS = { POSITIONS = [
"GEN": "IETF Chair/Gen AD", "GEN",
"APP": "APP Area Director", "APP",
"INT": "INT Area Director", "INT",
"OAM": "OPS Area Director", "OAM",
"OPS": "OPS Area Director", "OPS",
"RAI": "RAI Area Director", "RAI",
"RTG": "RTG Area Director", "RTG",
"SEC": "SEC Area Director", "SEC",
"TSV": "TSV Area Director", "TSV",
"IAB": "IAB Member", "IAB",
"IAOC": "IAOC Member", "IAOC"
} ]
def generate_cert(): def generate_cert():
@ -127,12 +127,10 @@ def nomcom_test_data():
nominee, _ = Nominee.objects.get_or_create(email=email, nomcom=nomcom) nominee, _ = Nominee.objects.get_or_create(email=email, nomcom=nomcom)
# positions # positions
for name, description in POSITIONS.iteritems(): for name in POSITIONS:
position, created = Position.objects.get_or_create(nomcom=nomcom, position, created = Position.objects.get_or_create(nomcom=nomcom,
name=name, name=name,
description=description, is_open=True)
is_open=True,
incumbent=email)
ChangeStateGroupEvent.objects.get_or_create(group=group, ChangeStateGroupEvent.objects.get_or_create(group=group,
type="changed_state", type="changed_state",

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,4 @@
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from django.views.generic import TemplateView
from ietf.nomcom.forms import EditMembersForm, EditMembersFormPreview from ietf.nomcom.forms import EditMembersForm, EditMembersFormPreview
@ -8,7 +7,9 @@ urlpatterns = patterns('ietf.nomcom.views',
url(r'^ann/$', 'announcements'), url(r'^ann/$', 'announcements'),
url(r'^(?P<year>\d{4})/private/$', 'private_index', name='nomcom_private_index'), url(r'^(?P<year>\d{4})/private/$', 'private_index', name='nomcom_private_index'),
url(r'^(?P<year>\d{4})/private/key/$', 'private_key', name='nomcom_private_key'), url(r'^(?P<year>\d{4})/private/key/$', 'private_key', name='nomcom_private_key'),
url(r'^(?P<year>\d{4})/private/help/$', 'configuration_help', name='nomcom_chair_help'),
url(r'^(?P<year>\d{4})/private/nominate/$', 'private_nominate', name='nomcom_private_nominate'), url(r'^(?P<year>\d{4})/private/nominate/$', 'private_nominate', name='nomcom_private_nominate'),
url(r'^(?P<year>\d{4})/private/nominate/newperson$', 'private_nominate_newperson', name='nomcom_private_nominate_newperson'),
url(r'^(?P<year>\d{4})/private/feedback/$', 'private_feedback', name='nomcom_private_feedback'), url(r'^(?P<year>\d{4})/private/feedback/$', 'private_feedback', name='nomcom_private_feedback'),
url(r'^(?P<year>\d{4})/private/feedback-email/$', 'private_feedback_email', name='nomcom_private_feedback_email'), url(r'^(?P<year>\d{4})/private/feedback-email/$', 'private_feedback_email', name='nomcom_private_feedback_email'),
url(r'^(?P<year>\d{4})/private/questionnaire-response/$', 'private_questionnaire', name='nomcom_private_questionnaire'), url(r'^(?P<year>\d{4})/private/questionnaire-response/$', 'private_questionnaire', name='nomcom_private_questionnaire'),
@ -22,8 +23,6 @@ urlpatterns = patterns('ietf.nomcom.views',
url(r'^(?P<year>\d{4})/private/send-reminder-mail/(?P<type>\w+)/$', 'send_reminder_mail', name='nomcom_send_reminder_mail'), url(r'^(?P<year>\d{4})/private/send-reminder-mail/(?P<type>\w+)/$', 'send_reminder_mail', name='nomcom_send_reminder_mail'),
url(r'^(?P<year>\d{4})/private/edit-members/$', EditMembersFormPreview(EditMembersForm), name='nomcom_edit_members'), url(r'^(?P<year>\d{4})/private/edit-members/$', EditMembersFormPreview(EditMembersForm), name='nomcom_edit_members'),
url(r'^(?P<year>\d{4})/private/edit-nomcom/$', 'edit_nomcom', name='nomcom_edit_nomcom'), url(r'^(?P<year>\d{4})/private/edit-nomcom/$', 'edit_nomcom', name='nomcom_edit_nomcom'),
url(r'^(?P<year>\d{4})/private/delete-nomcom/$', 'delete_nomcom', name='nomcom_delete_nomcom'),
url(r'^deleted/$', TemplateView.as_view(template_name='nomcom/deleted.html'), name='nomcom_deleted'),
url(r'^(?P<year>\d{4})/private/chair/templates/$', 'list_templates', name='nomcom_list_templates'), url(r'^(?P<year>\d{4})/private/chair/templates/$', 'list_templates', name='nomcom_list_templates'),
url(r'^(?P<year>\d{4})/private/chair/templates/(?P<template_id>\d+)/$', 'edit_template', name='nomcom_edit_template'), url(r'^(?P<year>\d{4})/private/chair/templates/(?P<template_id>\d+)/$', 'edit_template', name='nomcom_edit_template'),
url(r'^(?P<year>\d{4})/private/chair/position/$', 'list_positions', name='nomcom_list_positions'), url(r'^(?P<year>\d{4})/private/chair/position/$', 'list_positions', name='nomcom_list_positions'),
@ -37,6 +36,7 @@ urlpatterns = patterns('ietf.nomcom.views',
url(r'^(?P<year>\d{4})/questionnaires/$', 'questionnaires', name='nomcom_questionnaires'), url(r'^(?P<year>\d{4})/questionnaires/$', 'questionnaires', name='nomcom_questionnaires'),
url(r'^(?P<year>\d{4})/feedback/$', 'public_feedback', name='nomcom_public_feedback'), url(r'^(?P<year>\d{4})/feedback/$', 'public_feedback', name='nomcom_public_feedback'),
url(r'^(?P<year>\d{4})/nominate/$', 'public_nominate', name='nomcom_public_nominate'), url(r'^(?P<year>\d{4})/nominate/$', 'public_nominate', name='nomcom_public_nominate'),
url(r'^(?P<year>\d{4})/nominate/newperson$', 'public_nominate_newperson', name='nomcom_public_nominate_newperson'),
url(r'^(?P<year>\d{4})/process-nomination-status/(?P<nominee_position_id>\d+)/(?P<state>[\w]+)/(?P<date>[\d]+)/(?P<hash>[a-f0-9]+)/$', 'process_nomination_status', name='nomcom_process_nomination_status'), url(r'^(?P<year>\d{4})/process-nomination-status/(?P<nominee_position_id>\d+)/(?P<state>[\w]+)/(?P<date>[\d]+)/(?P<hash>[a-f0-9]+)/$', 'process_nomination_status', name='nomcom_process_nomination_status'),
) )

View file

@ -29,7 +29,7 @@ import debug # pyflakes:ignore
MAIN_NOMCOM_TEMPLATE_PATH = '/nomcom/defaults/' MAIN_NOMCOM_TEMPLATE_PATH = '/nomcom/defaults/'
QUESTIONNAIRE_TEMPLATE = 'position/questionnaire.txt' QUESTIONNAIRE_TEMPLATE = 'position/questionnaire.txt'
HEADER_QUESTIONNAIRE_TEMPLATE = 'position/header_questionnaire.txt' HEADER_QUESTIONNAIRE_TEMPLATE = 'position/header_questionnaire.txt'
REQUIREMENTS_TEMPLATE = 'position/requirements.txt' REQUIREMENTS_TEMPLATE = 'position/requirements'
HOME_TEMPLATE = 'home.rst' HOME_TEMPLATE = 'home.rst'
INEXISTENT_PERSON_TEMPLATE = 'email/inexistent_person.txt' INEXISTENT_PERSON_TEMPLATE = 'email/inexistent_person.txt'
NOMINEE_EMAIL_TEMPLATE = 'email/new_nominee.txt' NOMINEE_EMAIL_TEMPLATE = 'email/new_nominee.txt'
@ -53,7 +53,7 @@ def get_nomcom_by_year(year):
from ietf.nomcom.models import NomCom from ietf.nomcom.models import NomCom
return get_object_or_404(NomCom, return get_object_or_404(NomCom,
group__acronym__icontains=year, group__acronym__icontains=year,
group__state__slug='active') )
def get_year_by_nomcom(nomcom): def get_year_by_nomcom(nomcom):
@ -271,42 +271,22 @@ def send_reminder_to_nominees(nominees,type):
return addrs return addrs
def get_or_create_nominee(nomcom, candidate_name, candidate_email, position, author): def make_nomineeposition(nomcom, candidate, position, author):
from ietf.nomcom.models import Nominee, NomineePosition from ietf.nomcom.models import Nominee, NomineePosition
nomcom_template_path = '/nomcom/%s/' % nomcom.group.acronym nomcom_template_path = '/nomcom/%s/' % nomcom.group.acronym
# Create person and email if candidate email does't exist and send email
email, created_email = Email.objects.get_or_create(address=candidate_email)
if created_email:
person = Person.objects.create(name=candidate_name,
ascii=unaccent.asciify(candidate_name),
address=candidate_email)
email.person = person
email.save()
# Add the nomination for a particular position # Add the nomination for a particular position
nominee, created = Nominee.objects.get_or_create(email=email, nomcom=nomcom) nominee, created = Nominee.objects.get_or_create(person=candidate,email=candidate.email(), nomcom=nomcom)
while nominee.duplicated: while nominee.duplicated:
nominee = nominee.duplicated nominee = nominee.duplicated
nominee_position, nominee_position_created = NomineePosition.objects.get_or_create(position=position, nominee=nominee) nominee_position, nominee_position_created = NomineePosition.objects.get_or_create(position=position, nominee=nominee)
if created_email:
# send email to secretariat and nomcomchair to warn about the new person
subject = 'New person is created'
from_email = settings.NOMCOM_FROM_EMAIL
(to_email, cc) = gather_address_lists('nomination_created_person',nomcom=nomcom)
context = {'email': email.address,
'fullname': email.person.name,
'person_id': email.person.id}
path = nomcom_template_path + INEXISTENT_PERSON_TEMPLATE
send_mail(None, to_email, from_email, subject, path, context, cc=cc)
if nominee_position_created: if nominee_position_created:
# send email to nominee # send email to nominee
subject = 'IETF Nomination Information' subject = 'IETF Nomination Information'
from_email = settings.NOMCOM_FROM_EMAIL from_email = settings.NOMCOM_FROM_EMAIL
(to_email, cc) = gather_address_lists('nomination_new_nominee',nominee=email.address) (to_email, cc) = gather_address_lists('nomination_new_nominee',nominee=nominee.email.address)
domain = Site.objects.get_current().domain domain = Site.objects.get_current().domain
today = datetime.date.today().strftime('%Y%m%d') today = datetime.date.today().strftime('%Y%m%d')
hash = get_hash_nominee_position(today, nominee_position.id) hash = get_hash_nominee_position(today, nominee_position.id)
@ -325,7 +305,7 @@ def get_or_create_nominee(nomcom, candidate_name, candidate_email, position, aut
today, today,
hash)) hash))
context = {'nominee': email.person.name, context = {'nominee': nominee.person.name,
'position': position.name, 'position': position.name,
'domain': domain, 'domain': domain,
'accept_url': accept_url, 'accept_url': accept_url,
@ -338,8 +318,8 @@ def get_or_create_nominee(nomcom, candidate_name, candidate_email, position, aut
if nomcom.send_questionnaire: if nomcom.send_questionnaire:
subject = '%s Questionnaire' % position subject = '%s Questionnaire' % position
from_email = settings.NOMCOM_FROM_EMAIL from_email = settings.NOMCOM_FROM_EMAIL
(to_email, cc) = gather_address_lists('nomcom_questionnaire',nominee=email.address) (to_email, cc) = gather_address_lists('nomcom_questionnaire',nominee=nominee.email.address)
context = {'nominee': email.person.name, context = {'nominee': nominee.person.name,
'position': position.name} 'position': position.name}
path = '%s%d/%s' % (nomcom_template_path, path = '%s%d/%s' % (nomcom_template_path,
position.id, HEADER_QUESTIONNAIRE_TEMPLATE) position.id, HEADER_QUESTIONNAIRE_TEMPLATE)
@ -353,8 +333,8 @@ def get_or_create_nominee(nomcom, candidate_name, candidate_email, position, aut
subject = 'Nomination Information' subject = 'Nomination Information'
from_email = settings.NOMCOM_FROM_EMAIL from_email = settings.NOMCOM_FROM_EMAIL
(to_email, cc) = gather_address_lists('nomination_received',nomcom=nomcom) (to_email, cc) = gather_address_lists('nomination_received',nomcom=nomcom)
context = {'nominee': email.person.name, context = {'nominee': nominee.person.name,
'nominee_email': email.address, 'nominee_email': nominee.email.address,
'position': position.name} 'position': position.name}
if author: if author:
@ -369,6 +349,28 @@ def get_or_create_nominee(nomcom, candidate_name, candidate_email, position, aut
return nominee return nominee
def make_nomineeposition_for_newperson(nomcom, candidate_name, candidate_email, position, author):
# This is expected to fail if called with an existing email address
email = Email.objects.create(address=candidate_email)
person = Person.objects.create(name=candidate_name,
ascii=unaccent.asciify(candidate_name),
address=candidate_email)
email.person = person
email.save()
# send email to secretariat and nomcomchair to warn about the new person
subject = 'New person is created'
from_email = settings.NOMCOM_FROM_EMAIL
(to_email, cc) = gather_address_lists('nomination_created_person',nomcom=nomcom)
context = {'email': email.address,
'fullname': email.person.name,
'person_id': email.person.id}
nomcom_template_path = '/nomcom/%s/' % nomcom.group.acronym
path = nomcom_template_path + INEXISTENT_PERSON_TEMPLATE
send_mail(None, to_email, from_email, subject, path, context, cc=cc)
return make_nomineeposition(nomcom, email.person, position, author)
def getheader(header_text, default="ascii"): def getheader(header_text, default="ascii"):
"""Decode the specified header""" """Decode the specified header"""

View file

@ -1,9 +1,10 @@
import datetime import datetime
import re import re
from collections import OrderedDict from collections import OrderedDict, Counter
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import AnonymousUser
from django.contrib import messages from django.contrib import messages
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import Http404, HttpResponseRedirect, HttpResponseForbidden from django.http import Http404, HttpResponseRedirect, HttpResponseForbidden
@ -15,31 +16,29 @@ from django.forms.models import modelformset_factory, inlineformset_factory
from ietf.dbtemplate.models import DBTemplate from ietf.dbtemplate.models import DBTemplate
from ietf.dbtemplate.views import template_edit from ietf.dbtemplate.views import template_edit, template_show
from ietf.name.models import NomineePositionStateName, FeedbackTypeName from ietf.name.models import NomineePositionStateName, FeedbackTypeName
from ietf.group.models import Group, GroupEvent from ietf.group.models import Group, GroupEvent
from ietf.message.models import Message from ietf.message.models import Message
from ietf.nomcom.decorators import nomcom_private_key_required from ietf.nomcom.decorators import nomcom_private_key_required
from ietf.nomcom.forms import (NominateForm, FeedbackForm, QuestionnaireForm, from ietf.nomcom.forms import (NominateForm, NominateNewPersonForm, FeedbackForm, QuestionnaireForm,
MergeForm, NomComTemplateForm, PositionForm, MergeForm, NomComTemplateForm, PositionForm,
PrivateKeyForm, EditNomcomForm, EditNomineeForm, PrivateKeyForm, EditNomcomForm, EditNomineeForm,
PendingFeedbackForm, ReminderDatesForm, FullFeedbackFormSet, PendingFeedbackForm, ReminderDatesForm, FullFeedbackFormSet,
FeedbackEmailForm) FeedbackEmailForm, NominationResponseCommentForm)
from ietf.nomcom.models import Position, NomineePosition, Nominee, Feedback, NomCom, ReminderDates from ietf.nomcom.models import Position, NomineePosition, Nominee, Feedback, NomCom, ReminderDates, FeedbackLastSeen
from ietf.nomcom.utils import (get_nomcom_by_year, store_nomcom_private_key, from ietf.nomcom.utils import (get_nomcom_by_year, store_nomcom_private_key,
get_hash_nominee_position, send_reminder_to_nominees, get_hash_nominee_position, send_reminder_to_nominees,
HOME_TEMPLATE, NOMINEE_ACCEPT_REMINDER_TEMPLATE,NOMINEE_QUESTIONNAIRE_REMINDER_TEMPLATE) HOME_TEMPLATE, NOMINEE_ACCEPT_REMINDER_TEMPLATE,NOMINEE_QUESTIONNAIRE_REMINDER_TEMPLATE)
from ietf.ietfauth.utils import role_required from ietf.ietfauth.utils import role_required
import debug # pyflakes:ignore
def index(request): def index(request):
nomcom_list = Group.objects.filter(type__slug='nomcom').order_by('acronym') nomcom_list = Group.objects.filter(type__slug='nomcom').order_by('acronym')
for nomcom in nomcom_list: for nomcom in nomcom_list:
year = nomcom.acronym[6:] year = int(nomcom.acronym[6:])
try:
year = int(year)
except ValueError:
year = None
nomcom.year = year nomcom.year = year
nomcom.label = "%s/%s" % (year, year+1) nomcom.label = "%s/%s" % (year, year+1)
if year in [ 2005, 2006, 2007, 2008, 2009, 2010 ]: if year in [ 2005, 2006, 2007, 2008, 2009, 2010 ]:
@ -107,11 +106,11 @@ def announcements(request):
@role_required("Nomcom") @role_required("Nomcom")
def private_key(request, year): def private_key(request, year):
nomcom = get_nomcom_by_year(year) nomcom = get_nomcom_by_year(year)
message = None
if request.session.get('NOMCOM_PRIVATE_KEY_%s' % year, None): if request.session.get('NOMCOM_PRIVATE_KEY_%s' % year, None):
message = ('warning', 'You already have a private decryption key set for this session.') messages.warning(request, 'You already have a private decryption key set for this session.')
else: else:
message = ('warning', "You don't have a private decryption key set for this session yet") messages.warning(request, "You don't have a private decryption key set for this session yet")
back_url = request.GET.get('back_to', reverse('nomcom_private_index', None, args=(year, ))) back_url = request.GET.get('back_to', reverse('nomcom_private_index', None, args=(year, )))
if request.method == 'POST': if request.method == 'POST':
@ -126,7 +125,6 @@ def private_key(request, year):
'year': year, 'year': year,
'back_url': back_url, 'back_url': back_url,
'form': form, 'form': form,
'message': message,
'selected': 'private_key'}, RequestContext(request)) 'selected': 'private_key'}, RequestContext(request))
@ -135,23 +133,25 @@ def private_index(request, year):
nomcom = get_nomcom_by_year(year) nomcom = get_nomcom_by_year(year)
all_nominee_positions = NomineePosition.objects.get_by_nomcom(nomcom).not_duplicated() all_nominee_positions = NomineePosition.objects.get_by_nomcom(nomcom).not_duplicated()
is_chair = nomcom.group.has_role(request.user, "chair") is_chair = nomcom.group.has_role(request.user, "chair")
message = None
if is_chair and request.method == 'POST': if is_chair and request.method == 'POST':
action = request.POST.get('action') if nomcom.group.state_id != 'active':
nominations_to_modify = request.POST.getlist('selected') messages.warning(request, "This nomcom is not active. Request administrative assistance if Nominee state needs to change.")
if nominations_to_modify:
nominations = all_nominee_positions.filter(id__in=nominations_to_modify)
if action == "set_as_accepted":
nominations.update(state='accepted')
message = ('success', 'The selected nominations have been set as accepted')
elif action == "set_as_declined":
nominations.update(state='declined')
message = ('success', 'The selected nominations have been set as declined')
elif action == "set_as_pending":
nominations.update(state='pending')
message = ('success', 'The selected nominations have been set as pending')
else: else:
message = ('warning', "Please, select some nominations to work with") action = request.POST.get('action')
nominations_to_modify = request.POST.getlist('selected')
if nominations_to_modify:
nominations = all_nominee_positions.filter(id__in=nominations_to_modify)
if action == "set_as_accepted":
nominations.update(state='accepted')
messages.success(request,'The selected nominations have been set as accepted')
elif action == "set_as_declined":
nominations.update(state='declined')
messages.success(request,'The selected nominations have been set as declined')
elif action == "set_as_pending":
nominations.update(state='pending')
messages.success(request,'The selected nominations have been set as pending')
else:
messages.warning(request, "Please, select some nominations to work with")
filters = {} filters = {}
questionnaire_state = "questionnaire" questionnaire_state = "questionnaire"
@ -193,7 +193,7 @@ def private_index(request, year):
'selected_position': selected_position and int(selected_position) or None, 'selected_position': selected_position and int(selected_position) or None,
'selected': 'index', 'selected': 'index',
'is_chair': is_chair, 'is_chair': is_chair,
'message': message}, RequestContext(request)) }, RequestContext(request))
@role_required("Nomcom Chair", "Nomcom Advisor") @role_required("Nomcom Chair", "Nomcom Advisor")
@ -201,6 +201,16 @@ def send_reminder_mail(request, year, type):
nomcom = get_nomcom_by_year(year) nomcom = get_nomcom_by_year(year)
nomcom_template_path = '/nomcom/%s/' % nomcom.group.acronym nomcom_template_path = '/nomcom/%s/' % nomcom.group.acronym
has_publickey = nomcom.public_key and True or False
if not has_publickey:
messages.warning(request, "This Nomcom does not yet have a public key.")
nomcom_ready = False
elif nomcom.group.state_id != 'active':
messages.warning(request, "This Nomcom is not active.")
nomcom_ready = False
else:
nomcom_ready = True
if type=='accept': if type=='accept':
interesting_state = 'pending' interesting_state = 'pending'
mail_path = nomcom_template_path + NOMINEE_ACCEPT_REMINDER_TEMPLATE mail_path = nomcom_template_path + NOMINEE_ACCEPT_REMINDER_TEMPLATE
@ -228,19 +238,19 @@ def send_reminder_mail(request, year, type):
mail_template = DBTemplate.objects.filter(group=nomcom.group, path=mail_path) mail_template = DBTemplate.objects.filter(group=nomcom.group, path=mail_path)
mail_template = mail_template and mail_template[0] or None mail_template = mail_template and mail_template[0] or None
message = None
if request.method == 'POST': if request.method == 'POST' and nomcom_ready:
selected_nominees = request.POST.getlist('selected') selected_nominees = request.POST.getlist('selected')
selected_nominees = nominees.filter(id__in=selected_nominees) selected_nominees = nominees.filter(id__in=selected_nominees)
if selected_nominees: if selected_nominees:
addrs = send_reminder_to_nominees(selected_nominees,type) addrs = send_reminder_to_nominees(selected_nominees,type)
if addrs: if addrs:
message = ('success', 'A copy of "%s" has been sent to %s'%(mail_template.title,", ".join(addrs))) messages.success(request, 'A copy of "%s" has been sent to %s'%(mail_template.title,", ".join(addrs)))
else: else:
message = ('warning', 'No messages were sent.') messages.warning(request, 'No messages were sent.')
else: else:
message = ('warning', "Please, select at least one nominee") messages.warning(request, "Please, select at least one nominee")
return render_to_response('nomcom/send_reminder_mail.html', return render_to_response('nomcom/send_reminder_mail.html',
{'nomcom': nomcom, {'nomcom': nomcom,
'year': year, 'year': year,
@ -249,27 +259,33 @@ def send_reminder_mail(request, year, type):
'selected': selected_tab, 'selected': selected_tab,
'reminder_description': reminder_description, 'reminder_description': reminder_description,
'state_description': state_description, 'state_description': state_description,
'message': message}, RequestContext(request)) 'is_chair_task' : True,
}, RequestContext(request))
@role_required("Nomcom Chair", "Nomcom Advisor") @role_required("Nomcom Chair", "Nomcom Advisor")
def private_merge(request, year): def private_merge(request, year):
nomcom = get_nomcom_by_year(year) nomcom = get_nomcom_by_year(year)
message = None if nomcom.group.state_id != 'active':
if request.method == 'POST': messages.warning(request, "This Nomcom is not active.")
form = MergeForm(request.POST, nomcom=nomcom) form = None
if form.is_valid():
form.save()
message = ('success', 'The emails have been unified')
else: else:
form = MergeForm(nomcom=nomcom) if request.method == 'POST':
form = MergeForm(request.POST, nomcom=nomcom )
if form.is_valid():
form.save()
messages.success(request, 'A merge request has been sent to the secretariat.')
return redirect('nomcom_private_index',year=year)
else:
form = MergeForm(nomcom=nomcom)
return render_to_response('nomcom/private_merge.html', return render_to_response('nomcom/private_merge.html',
{'nomcom': nomcom, {'nomcom': nomcom,
'year': year, 'year': year,
'form': form, 'form': form,
'message': message, 'selected': 'merge',
'selected': 'merge'}, RequestContext(request)) 'is_chair_task' : True,
}, RequestContext(request))
def requirements(request, year): def requirements(request, year):
@ -294,15 +310,24 @@ def questionnaires(request, year):
@login_required @login_required
def public_nominate(request, year): def public_nominate(request, year):
return nominate(request, year, True) return nominate(request=request, year=year, public=True, newperson=False)
@role_required("Nomcom") @role_required("Nomcom")
def private_nominate(request, year): def private_nominate(request, year):
return nominate(request, year, False) return nominate(request=request, year=year, public=False, newperson=False)
@login_required
def public_nominate_newperson(request, year):
return nominate(request=request, year=year, public=True, newperson=True)
def nominate(request, year, public): @role_required("Nomcom")
def private_nominate_newperson(request, year):
return nominate(request=request, year=year, public=False, newperson=True)
def nominate(request, year, public, newperson):
nomcom = get_nomcom_by_year(year) nomcom = get_nomcom_by_year(year)
has_publickey = nomcom.public_key and True or False has_publickey = nomcom.public_key and True or False
if public: if public:
@ -311,31 +336,43 @@ def nominate(request, year, public):
template = 'nomcom/private_nominate.html' template = 'nomcom/private_nominate.html'
if not has_publickey: if not has_publickey:
message = ('warning', "This Nomcom is not yet accepting nominations") messages.warning(request, "This Nomcom is not yet accepting nominations")
return render_to_response(template, return render_to_response(template,
{'message': message, {'nomcom': nomcom,
'nomcom': nomcom, 'year': year,
'selected': 'nominate'}, RequestContext(request))
if nomcom.group.state_id == 'conclude':
messages.warning(request, "Nominations to this Nomcom are closed.")
return render_to_response(template,
{'nomcom': nomcom,
'year': year, 'year': year,
'selected': 'nominate'}, RequestContext(request)) 'selected': 'nominate'}, RequestContext(request))
message = None
if request.method == 'POST': if request.method == 'POST':
form = NominateForm(data=request.POST, nomcom=nomcom, user=request.user, public=public) if newperson:
form = NominateNewPersonForm(data=request.POST, nomcom=nomcom, user=request.user, public=public)
else:
form = NominateForm(data=request.POST, nomcom=nomcom, user=request.user, public=public)
if form.is_valid(): if form.is_valid():
form.save() form.save()
message = ('success', 'Your nomination has been registered. Thank you for the nomination.') messages.success(request, 'Your nomination has been registered. Thank you for the nomination.')
form = NominateForm(nomcom=nomcom, user=request.user, public=public) if newperson:
return redirect('nomcom_%s_nominate' % ('public' if public else 'private'), year=year)
else:
form = NominateForm(nomcom=nomcom, user=request.user, public=public)
else: else:
form = NominateForm(nomcom=nomcom, user=request.user, public=public) if newperson:
form = NominateNewPersonForm(nomcom=nomcom, user=request.user, public=public)
else:
form = NominateForm(nomcom=nomcom, user=request.user, public=public)
return render_to_response(template, return render_to_response(template,
{'form': form, {'form': form,
'message': message,
'nomcom': nomcom, 'nomcom': nomcom,
'year': year, 'year': year,
'selected': 'nominate'}, RequestContext(request)) 'selected': 'nominate'}, RequestContext(request))
@login_required @login_required
def public_feedback(request, year): def public_feedback(request, year):
return feedback(request, year, True) return feedback(request, year, True)
@ -349,55 +386,61 @@ def private_feedback(request, year):
def feedback(request, year, public): def feedback(request, year, public):
nomcom = get_nomcom_by_year(year) nomcom = get_nomcom_by_year(year)
has_publickey = nomcom.public_key and True or False has_publickey = nomcom.public_key and True or False
submit_disabled = True
nominee = None nominee = None
position = None position = None
selected_nominee = request.GET.get('nominee') if nomcom.group.state_id != 'conclude':
selected_position = request.GET.get('position') selected_nominee = request.GET.get('nominee')
if selected_nominee and selected_position: selected_position = request.GET.get('position')
nominee = get_object_or_404(Nominee, id=selected_nominee) if selected_nominee and selected_position:
position = get_object_or_404(Position, id=selected_position) nominee = get_object_or_404(Nominee, id=selected_nominee)
submit_disabled = False position = get_object_or_404(Position, id=selected_position)
positions = Position.objects.get_by_nomcom(nomcom=nomcom).opened() positions = Position.objects.get_by_nomcom(nomcom=nomcom).opened()
user_comments = Feedback.objects.filter(nomcom=nomcom,
type='comment',
author__in=request.user.person.email_set.filter(active='True'))
counter = Counter(user_comments.values_list('positions','nominees'))
counts = dict()
for pos,nom in counter:
counts.setdefault(pos,dict())[nom] = counter[(pos,nom)]
if public: if public:
base_template = "nomcom/nomcom_public_base.html" base_template = "nomcom/nomcom_public_base.html"
else: else:
base_template = "nomcom/nomcom_private_base.html" base_template = "nomcom/nomcom_private_base.html"
if not has_publickey: if not has_publickey:
message = ('warning', "This Nomcom is not yet accepting comments") messages.warning(request, "This Nomcom is not yet accepting comments")
return render(request, 'nomcom/feedback.html', { return render(request, 'nomcom/feedback.html', {
'message': message,
'nomcom': nomcom, 'nomcom': nomcom,
'year': year, 'year': year,
'selected': 'feedback', 'selected': 'feedback',
'counts' : counts,
'base_template': base_template 'base_template': base_template
}) })
message = None if nominee and position and request.method == 'POST':
if request.method == 'POST':
form = FeedbackForm(data=request.POST, form = FeedbackForm(data=request.POST,
nomcom=nomcom, user=request.user, nomcom=nomcom, user=request.user,
public=public, position=position, nominee=nominee) public=public, position=position, nominee=nominee)
if form.is_valid(): if form.is_valid():
form.save() form.save()
message = ('success', 'Your feedback has been registered.') messages.success(request, 'Your feedback has been registered.')
form = None
else:
if nominee and position:
form = FeedbackForm(nomcom=nomcom, user=request.user, public=public, form = FeedbackForm(nomcom=nomcom, user=request.user, public=public,
position=position, nominee=nominee) position=position, nominee=nominee)
else: else:
form = FeedbackForm(nomcom=nomcom, user=request.user, public=public, form = None
position=position, nominee=nominee)
return render(request, 'nomcom/feedback.html', { return render(request, 'nomcom/feedback.html', {
'form': form, 'form': form,
'message': message,
'nomcom': nomcom, 'nomcom': nomcom,
'year': year, 'year': year,
'positions': positions, 'positions': positions,
'submit_disabled': submit_disabled,
'selected': 'feedback', 'selected': 'feedback',
'counts': counts,
'base_template': base_template 'base_template': base_template
}) })
@ -406,16 +449,24 @@ def feedback(request, year, public):
def private_feedback_email(request, year): def private_feedback_email(request, year):
nomcom = get_nomcom_by_year(year) nomcom = get_nomcom_by_year(year)
has_publickey = nomcom.public_key and True or False has_publickey = nomcom.public_key and True or False
message = None
template = 'nomcom/private_feedback_email.html' template = 'nomcom/private_feedback_email.html'
if not has_publickey: if not has_publickey:
message = ('warning', "This Nomcom is not yet accepting feedback email") messages.warning(request, "This Nomcom is not yet accepting feedback email.")
return render_to_response(template, nomcom_ready = False
{'message': message, elif nomcom.group.state_id != 'active':
'nomcom': nomcom, messages.warning(request, "This Nomcom is not active, and is not accepting feedback email.")
'year': year, nomcom_ready = False
'selected': 'feedback_email'}, RequestContext(request)) else:
nomcom_ready = True
if not nomcom_ready:
return render_to_response(template,
{'nomcom': nomcom,
'year': year,
'selected': 'feedback_email',
'is_chair_task' : True,
}, RequestContext(request))
form = FeedbackEmailForm(nomcom=nomcom) form = FeedbackEmailForm(nomcom=nomcom)
@ -425,38 +476,44 @@ def private_feedback_email(request, year):
if form.is_valid(): if form.is_valid():
form.save() form.save()
form = FeedbackEmailForm(nomcom=nomcom) form = FeedbackEmailForm(nomcom=nomcom)
message = ('success', 'The feedback email has been registered.') messages.success(request, 'The feedback email has been registered.')
return render_to_response(template, return render_to_response(template,
{'form': form, {'form': form,
'message': message,
'nomcom': nomcom, 'nomcom': nomcom,
'year': year, 'year': year,
'selected': 'feedback_email'}, RequestContext(request)) 'selected': 'feedback_email'}, RequestContext(request))
@role_required("Nomcom Chair", "Nomcom Advisor") @role_required("Nomcom Chair", "Nomcom Advisor")
def private_questionnaire(request, year): def private_questionnaire(request, year):
nomcom = get_nomcom_by_year(year) nomcom = get_nomcom_by_year(year)
has_publickey = nomcom.public_key and True or False has_publickey = nomcom.public_key and True or False
message = None
questionnaire_response = None questionnaire_response = None
template = 'nomcom/private_questionnaire.html' template = 'nomcom/private_questionnaire.html'
if not has_publickey: if not has_publickey:
message = ('warning', "This Nomcom is not yet accepting questionnaires") messages.warning(request, "This Nomcom is not yet accepting questionnaires.")
return render_to_response(template, nomcom_ready = False
{'message': message, elif nomcom.group.state_id != 'active':
'nomcom': nomcom, messages.warning(request, "This Nomcom is not active, and is not accepting questionnaires.")
'year': year, nomcom_ready = False
'selected': 'questionnaire'}, RequestContext(request)) else:
nomcom_ready = True
if not nomcom_ready:
return render_to_response(template,
{'nomcom': nomcom,
'year': year,
'selected': 'questionnaire',
'is_chair_task' : True,
}, RequestContext(request))
if request.method == 'POST': if request.method == 'POST':
form = QuestionnaireForm(data=request.POST, form = QuestionnaireForm(data=request.POST,
nomcom=nomcom, user=request.user) nomcom=nomcom, user=request.user)
if form.is_valid(): if form.is_valid():
form.save() form.save()
message = ('success', 'The questionnaire response has been registered.') messages.success(request, 'The questionnaire response has been registered.')
questionnaire_response = form.cleaned_data['comments'] questionnaire_response = form.cleaned_data['comments']
form = QuestionnaireForm(nomcom=nomcom, user=request.user) form = QuestionnaireForm(nomcom=nomcom, user=request.user)
else: else:
@ -465,7 +522,6 @@ def private_questionnaire(request, year):
return render_to_response(template, return render_to_response(template,
{'form': form, {'form': form,
'questionnaire_response': questionnaire_response, 'questionnaire_response': questionnaire_response,
'message': message,
'nomcom': nomcom, 'nomcom': nomcom,
'year': year, 'year': year,
'selected': 'questionnaire'}, RequestContext(request)) 'selected': 'questionnaire'}, RequestContext(request))
@ -478,35 +534,52 @@ def process_nomination_status(request, year, nominee_position_id, state, date, h
expiration_days = getattr(settings, 'DAYS_TO_EXPIRE_NOMINATION_LINK', None) expiration_days = getattr(settings, 'DAYS_TO_EXPIRE_NOMINATION_LINK', None)
if expiration_days: if expiration_days:
request_date = datetime.date(int(date[:4]), int(date[4:6]), int(date[6:])) request_date = datetime.date(int(date[:4]), int(date[4:6]), int(date[6:]))
if datetime.date.today() > (request_date + datetime.timedelta(days=settings.DAYS_TO_EXPIRE_REGISTRATION_LINK)): if datetime.date.today() > (request_date + datetime.timedelta(days=settings.DAYS_TO_EXPIRE_NOMINATION_LINK)):
return HttpResponseForbidden("Link expired") return HttpResponseForbidden("Link expired")
need_confirmation = True need_confirmation = True
nomcom = get_nomcom_by_year(year) nomcom = get_nomcom_by_year(year)
if nomcom.group.state_id == 'conclude':
return HttpResponseForbidden("This nomcom is concluded.")
nominee_position = get_object_or_404(NomineePosition, id=nominee_position_id) nominee_position = get_object_or_404(NomineePosition, id=nominee_position_id)
if nominee_position.state.slug != "pending": if nominee_position.state.slug != "pending":
return HttpResponseForbidden("The nomination already was %s" % nominee_position.state) return HttpResponseForbidden("The nomination already was %s" % nominee_position.state)
state = get_object_or_404(NomineePositionStateName, slug=state) state = get_object_or_404(NomineePositionStateName, slug=state)
message = ('warning', messages.info(request, "Click on 'Save' to set the state of your nomination to %s to %s (this is not a final commitment - you can notify us later if you need to change this)." % (nominee_position.position.name, state.name))
("Click on 'Save' to set the state of your nomination to %s to %s (this"+
"is not a final commitment - you can notify us later if you need to change this)") %
(nominee_position.position.name, state.name))
if request.method == 'POST': if request.method == 'POST':
nominee_position.state = state form = NominationResponseCommentForm(request.POST)
nominee_position.save() if form.is_valid():
need_confirmation = False nominee_position.state = state
message = message = ('success', 'Your nomination on %s has been set as %s' % (nominee_position.position.name, nominee_position.save()
state.name)) need_confirmation = False
if form.cleaned_data['comments']:
# This Feedback object is of type comment instead of nomina in order to not
# make answering "who nominated themselves" harder.
who = request.user
if isinstance(who,AnonymousUser):
who = None
f = Feedback.objects.create(nomcom = nomcom,
author = nominee_position.nominee.email,
subject = '%s nomination %s'%(nominee_position.nominee.name(),state),
comments = form.cleaned_data['comments'],
type_id = 'comment',
user = who,
)
f.positions.add(nominee_position.position)
f.nominees.add(nominee_position.nominee)
messages.success(request, 'Your nomination on %s has been set as %s' % (nominee_position.position.name, state.name))
else:
form = NominationResponseCommentForm()
return render_to_response('nomcom/process_nomination_status.html', return render_to_response('nomcom/process_nomination_status.html',
{'message': message, {'nomcom': nomcom,
'nomcom': nomcom,
'year': year, 'year': year,
'nominee_position': nominee_position, 'nominee_position': nominee_position,
'state': state, 'state': state,
'need_confirmation': need_confirmation, 'need_confirmation': need_confirmation,
'selected': 'feedback'}, RequestContext(request)) 'selected': 'feedback',
'form': form }, RequestContext(request))
@role_required("Nomcom") @role_required("Nomcom")
@ -521,10 +594,36 @@ def view_feedback(request, year):
feedback_types.append(ft) feedback_types.append(ft)
else: else:
independent_feedback_types.append(ft) independent_feedback_types.append(ft)
nominees_feedback = {} nominees_feedback = []
def nominee_staterank(nominee):
states=nominee.nomineeposition_set.values_list('state_id',flat=True)
if 'accepted' in states:
return 0
elif 'pending' in states:
return 1
else:
return 2
for nominee in nominees: for nominee in nominees:
nominee_feedback = [(ft.name, nominee.feedback_set.by_type(ft.slug).count()) for ft in feedback_types] nominee.staterank = nominee_staterank(nominee)
nominees_feedback.update({nominee: nominee_feedback})
sorted_nominees = sorted(nominees,key=lambda x:x.staterank)
for nominee in sorted_nominees:
last_seen = FeedbackLastSeen.objects.filter(reviewer=request.user.person,nominee=nominee).first()
nominee_feedback = []
for ft in feedback_types:
qs = nominee.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()
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] independent_feedback = [ft.feedback_set.get_by_nomcom(nomcom).count() for ft in independent_feedback_types]
return render_to_response('nomcom/view_feedback.html', return render_to_response('nomcom/view_feedback.html',
@ -542,33 +641,17 @@ def view_feedback(request, year):
@nomcom_private_key_required @nomcom_private_key_required
def view_feedback_pending(request, year): def view_feedback_pending(request, year):
nomcom = get_nomcom_by_year(year) nomcom = get_nomcom_by_year(year)
if nomcom.group.state_id == 'conclude':
return HttpResponseForbidden("This nomcom is concluded.")
extra_ids = None extra_ids = None
message = None
for message in messages.get_messages(request):
message = ('success', message.message)
FeedbackFormSet = modelformset_factory(Feedback, FeedbackFormSet = modelformset_factory(Feedback,
form=PendingFeedbackForm, form=PendingFeedbackForm,
extra=0) extra=0)
feedbacks = Feedback.objects.filter(type__isnull=True, nomcom=nomcom) feedbacks = Feedback.objects.filter(type__isnull=True, nomcom=nomcom)
try:
default_type = FeedbackTypeName.objects.get(slug=settings.DEFAULT_FEEDBACK_TYPE)
except FeedbackTypeName.DoesNotExist:
default_type = None
extra_step = False extra_step = False
if request.method == 'POST' and request.POST.get('move_to_default'): if request.method == 'POST' and request.POST.get('end'):
formset = FeedbackFormSet(request.POST)
if formset.is_valid():
for form in formset.forms:
form.set_nomcom(nomcom, request.user)
form.move_to_default()
formset = FeedbackFormSet(queryset=feedbacks)
for form in formset.forms:
form.set_nomcom(nomcom, request.user)
messages.success(request, 'Feedback saved')
return redirect('nomcom_view_feedback_pending', year=year)
elif request.method == 'POST' and request.POST.get('end'):
extra_ids = request.POST.get('extra_ids', None) extra_ids = request.POST.get('extra_ids', None)
extra_step = True extra_step = True
formset = FullFeedbackFormSet(request.POST) formset = FullFeedbackFormSet(request.POST)
@ -623,7 +706,7 @@ def view_feedback_pending(request, year):
for form in formset.forms: for form in formset.forms:
form.set_nomcom(nomcom, request.user, extra) form.set_nomcom(nomcom, request.user, extra)
if moved: if moved:
message = ('success', '%s messages classified. You must enter more information for the following feedback.' % moved) messages.success(request, '%s messages classified. You must enter more information for the following feedback.' % moved)
else: else:
messages.success(request, 'Feedback saved') messages.success(request, 'Feedback saved')
return redirect('nomcom_view_feedback_pending', year=year) return redirect('nomcom_view_feedback_pending', year=year)
@ -644,13 +727,13 @@ def view_feedback_pending(request, year):
{'year': year, {'year': year,
'selected': 'feedback_pending', 'selected': 'feedback_pending',
'formset': formset, 'formset': formset,
'message': message,
'extra_step': extra_step, 'extra_step': extra_step,
'default_type': default_type,
'type_dict': type_dict, 'type_dict': type_dict,
'extra_ids': extra_ids, 'extra_ids': extra_ids,
'types': FeedbackTypeName.objects.all().order_by('pk'), 'types': FeedbackTypeName.objects.all().order_by('pk'),
'nomcom': nomcom}, RequestContext(request)) 'nomcom': nomcom,
'is_chair_task' : True,
}, RequestContext(request))
@role_required("Nomcom") @role_required("Nomcom")
@ -676,11 +759,19 @@ def view_feedback_nominee(request, year, nominee_id):
nominee = get_object_or_404(Nominee, id=nominee_id) nominee = get_object_or_404(Nominee, id=nominee_id)
feedback_types = FeedbackTypeName.objects.filter(slug__in=settings.NOMINEE_FEEDBACK_TYPES) feedback_types = FeedbackTypeName.objects.filter(slug__in=settings.NOMINEE_FEEDBACK_TYPES)
last_seen = FeedbackLastSeen.objects.filter(reviewer=request.user.person,nominee=nominee).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:
FeedbackLastSeen.objects.create(reviewer=request.user.person,nominee=nominee)
return render_to_response('nomcom/view_feedback_nominee.html', return render_to_response('nomcom/view_feedback_nominee.html',
{'year': year, {'year': year,
'selected': 'view_feedback', 'selected': 'view_feedback',
'nominee': nominee, 'nominee': nominee,
'feedback_types': feedback_types, 'feedback_types': feedback_types,
'last_seen_time' : last_seen_time,
'nomcom': nomcom}, RequestContext(request)) 'nomcom': nomcom}, RequestContext(request))
@ -688,14 +779,14 @@ def view_feedback_nominee(request, year, nominee_id):
def edit_nominee(request, year, nominee_id): def edit_nominee(request, year, nominee_id):
nomcom = get_nomcom_by_year(year) nomcom = get_nomcom_by_year(year)
nominee = get_object_or_404(Nominee, id=nominee_id) nominee = get_object_or_404(Nominee, id=nominee_id)
message = None
if request.method == 'POST': if request.method == 'POST':
form = EditNomineeForm(request.POST, form = EditNomineeForm(request.POST,
instance=nominee) instance=nominee)
if form.is_valid(): if form.is_valid():
form.save() form.save()
message = ('success', 'The nominee has been changed') messages.success(request, 'The nomination address for %s has been changed to %s'%(nominee.name(),nominee.email.address))
return redirect('nomcom_private_index', year=year)
else: else:
form = EditNomineeForm(instance=nominee) form = EditNomineeForm(instance=nominee)
@ -704,8 +795,9 @@ def edit_nominee(request, year, nominee_id):
'selected': 'index', 'selected': 'index',
'nominee': nominee, 'nominee': nominee,
'form': form, 'form': form,
'message': message, 'nomcom': nomcom,
'nomcom': nomcom}, RequestContext(request)) 'is_chair_task' : True,
}, RequestContext(request))
@role_required("Nomcom Chair", "Nomcom Advisor") @role_required("Nomcom Chair", "Nomcom Advisor")
@ -713,14 +805,18 @@ def edit_nomcom(request, year):
nomcom = get_nomcom_by_year(year) nomcom = get_nomcom_by_year(year)
if nomcom.public_key: if nomcom.public_key:
message = ('warning', 'Previous data will remain encrypted with the old key') messages.warning(request, 'Previous data will remain encrypted with the old key')
else: else:
message = ('warning', 'The nomcom has not a public key yet') messages.warning(request, 'This Nomcom does not yet have a public key')
ReminderDateInlineFormSet = inlineformset_factory(parent_model=NomCom, ReminderDateInlineFormSet = inlineformset_factory(parent_model=NomCom,
model=ReminderDates, model=ReminderDates,
form=ReminderDatesForm) form=ReminderDatesForm)
if request.method == 'POST': if request.method == 'POST':
if nomcom.group.state_id=='conclude':
return HttpResponseForbidden('This nomcom is closed.')
formset = ReminderDateInlineFormSet(request.POST, instance=nomcom) formset = ReminderDateInlineFormSet(request.POST, instance=nomcom)
form = EditNomcomForm(request.POST, form = EditNomcomForm(request.POST,
request.FILES, request.FILES,
@ -729,7 +825,7 @@ def edit_nomcom(request, year):
form.save() form.save()
formset.save() formset.save()
formset = ReminderDateInlineFormSet(instance=nomcom) formset = ReminderDateInlineFormSet(instance=nomcom)
message = ('success', 'The nomcom has been changed') messages.success(request, 'The nomcom has been changed')
else: else:
formset = ReminderDateInlineFormSet(instance=nomcom) formset = ReminderDateInlineFormSet(instance=nomcom)
form = EditNomcomForm(instance=nomcom) form = EditNomcomForm(instance=nomcom)
@ -738,26 +834,12 @@ def edit_nomcom(request, year):
{'form': form, {'form': form,
'formset': formset, 'formset': formset,
'nomcom': nomcom, 'nomcom': nomcom,
'message': message,
'year': year, 'year': year,
'selected': 'edit_nomcom'}, RequestContext(request)) 'selected': 'edit_nomcom',
'is_chair_task' : True,
}, RequestContext(request))
@role_required("Nomcom Chair", "Nomcom Advisor")
def delete_nomcom(request, year):
nomcom = get_nomcom_by_year(year)
if request.method == 'POST':
nomcom.delete()
messages.success(request, "Deleted NomCom data")
return redirect('nomcom_deleted')
return render(request, 'nomcom/delete_nomcom.html', {
'year': year,
'selected': 'edit_nomcom',
'nomcom': nomcom,
})
@role_required("Nomcom Chair", "Nomcom Advisor") @role_required("Nomcom Chair", "Nomcom Advisor")
def list_templates(request, year): def list_templates(request, year):
@ -770,7 +852,9 @@ def list_templates(request, year):
'positions': positions, 'positions': positions,
'year': year, 'year': year,
'selected': 'edit_templates', 'selected': 'edit_templates',
'nomcom': nomcom}, RequestContext(request)) 'nomcom': nomcom,
'is_chair_task' : True,
}, RequestContext(request))
@role_required("Nomcom Chair", "Nomcom Advisor") @role_required("Nomcom Chair", "Nomcom Advisor")
@ -778,29 +862,44 @@ def edit_template(request, year, template_id):
nomcom = get_nomcom_by_year(year) nomcom = get_nomcom_by_year(year)
return_url = request.META.get('HTTP_REFERER', None) return_url = request.META.get('HTTP_REFERER', None)
return template_edit(request, nomcom.group.acronym, template_id, if nomcom.group.state_id=='conclude':
base_template='nomcom/edit_template.html', return template_show(request, nomcom.group.acronym, template_id,
formclass=NomComTemplateForm, base_template='nomcom/show_template.html',
extra_context={'year': year, extra_context={'year': year,
'return_url': return_url, 'return_url': return_url,
'nomcom': nomcom}) 'nomcom': nomcom,
'is_chair_task' : True,
})
else:
return template_edit(request, nomcom.group.acronym, template_id,
base_template='nomcom/edit_template.html',
formclass=NomComTemplateForm,
extra_context={'year': year,
'return_url': return_url,
'nomcom': nomcom,
'is_chair_task' : True,
})
@role_required("Nomcom Chair", "Nomcom Advisor") @role_required("Nomcom Chair", "Nomcom Advisor")
def list_positions(request, year): def list_positions(request, year):
nomcom = get_nomcom_by_year(year) nomcom = get_nomcom_by_year(year)
positions = nomcom.position_set.all() positions = nomcom.position_set.order_by('-is_open')
return render_to_response('nomcom/list_positions.html', return render_to_response('nomcom/list_positions.html',
{'positions': positions, {'positions': positions,
'year': year, 'year': year,
'selected': 'edit_positions', 'selected': 'edit_positions',
'nomcom': nomcom}, RequestContext(request)) 'nomcom': nomcom,
'is_chair_task' : True,
}, RequestContext(request))
@role_required("Nomcom Chair", "Nomcom Advisor") @role_required("Nomcom Chair", "Nomcom Advisor")
def remove_position(request, year, position_id): def remove_position(request, year, position_id):
nomcom = get_nomcom_by_year(year) nomcom = get_nomcom_by_year(year)
if nomcom.group.state_id=='conclude':
return HttpResponseForbidden('This nomcom is closed.')
try: try:
position = nomcom.position_set.get(id=position_id) position = nomcom.position_set.get(id=position_id)
except Position.DoesNotExist: except Position.DoesNotExist:
@ -812,12 +911,18 @@ def remove_position(request, year, position_id):
return render_to_response('nomcom/remove_position.html', return render_to_response('nomcom/remove_position.html',
{'year': year, {'year': year,
'position': position, 'position': position,
'nomcom': nomcom}, RequestContext(request)) 'nomcom': nomcom,
'is_chair_task' : True,
}, RequestContext(request))
@role_required("Nomcom Chair", "Nomcom Advisor") @role_required("Nomcom Chair", "Nomcom Advisor")
def edit_position(request, year, position_id=None): def edit_position(request, year, position_id=None):
nomcom = get_nomcom_by_year(year) nomcom = get_nomcom_by_year(year)
if nomcom.group.state_id=='conclude':
return HttpResponseForbidden('This nomcom is closed.')
if position_id: if position_id:
try: try:
position = nomcom.position_set.get(id=position_id) position = nomcom.position_set.get(id=position_id)
@ -838,4 +943,10 @@ def edit_position(request, year, position_id=None):
{'form': form, {'form': form,
'position': position, 'position': position,
'year': year, 'year': year,
'nomcom': nomcom}, RequestContext(request)) 'nomcom': nomcom,
'is_chair_task' : True,
}, RequestContext(request))
@role_required("Nomcom Chair", "Nomcom Advisor")
def configuration_help(request, year):
return render(request,'nomcom/chair_help.html',{'year':year})

58
ietf/person/factories.py Normal file
View file

@ -0,0 +1,58 @@
import factory
import faker
from unidecode import unidecode
from django.contrib.auth.models import User
from ietf.person.models import Person, Alias, Email
fake = faker.Factory.create()
class UserFactory(factory.DjangoModelFactory):
class Meta:
model = User
django_get_or_create = ('username',)
first_name = factory.Faker('first_name')
last_name = factory.Faker('last_name')
email = factory.LazyAttribute(lambda u: '%s.%s@%s'%(u.first_name,u.last_name,fake.domain_name()))
username = factory.LazyAttribute(lambda u: u.email)
@factory.post_generation
def set_password(self, create, extracted, **kwargs):
self.set_password( '%s+password' % self.username )
class PersonFactory(factory.DjangoModelFactory):
class Meta:
model = Person
user = factory.SubFactory(UserFactory)
name = factory.LazyAttribute(lambda p: '%s %s'%(p.user.first_name,p.user.last_name))
ascii = factory.LazyAttribute(lambda p: unidecode(p.name))
@factory.post_generation
def default_aliases(self, create, extracted, **kwargs):
make_alias = getattr(AliasFactory, 'create' if create else 'build')
make_alias(person=self,name=self.name)
make_alias(person=self,name=self.ascii)
@factory.post_generation
def default_emails(self, create, extracted, **kwargs):
make_email = getattr(EmailFactory, 'create' if create else 'build')
make_email(person=self,address=self.user.email)
class AliasFactory(factory.DjangoModelFactory):
class Meta:
model = Alias
django_get_or_create = ('name',)
name = factory.Faker('name')
class EmailFactory(factory.DjangoModelFactory):
class Meta:
model = Email
django_get_or_create = ('address',)
address = '%s.%s@%s' % (fake.first_name(),fake.last_name(),fake.domain_name())
active = True
primary = False

View file

@ -1,5 +1,7 @@
import json import json
from collections import Counter
from django.utils.html import escape from django.utils.html import escape
from django import forms from django import forms
from django.core.urlresolvers import reverse as urlreverse from django.core.urlresolvers import reverse as urlreverse
@ -12,7 +14,19 @@ def select2_id_name_json(objs):
def format_email(e): def format_email(e):
return escape(u"%s <%s>" % (e.person.name, e.address)) return escape(u"%s <%s>" % (e.person.name, e.address))
def format_person(p): def format_person(p):
return escape(p.name) if p.name_count > 1:
return escape('%s (%s)' % (p.name,p.email().address))
else:
return escape(p.name)
if objs and isinstance(objs[0], Email):
formatter = format_email
else:
formatter = format_person
c = Counter([p.name for p in objs])
for p in objs:
p.name_count = c[p.name]
formatter = format_email if objs and isinstance(objs[0], Email) else format_person formatter = format_email if objs and isinstance(objs[0], Email) else format_person

View file

@ -59,10 +59,13 @@ class PersonInfo(models.Model):
if e: if e:
return e[0] return e[0]
return None return None
def email_address(self): def email(self):
e = self.email_set.filter(primary=True).first() e = self.email_set.filter(primary=True).first()
if not e: if not e:
e = self.email_set.filter(active=True).order_by("-time").first() e = self.email_set.filter(active=True).order_by("-time").first()
return e
def email_address(self):
e = self.email()
if e: if e:
return e.address return e.address
else: else:

88
ietf/person/utils.py Executable file
View file

@ -0,0 +1,88 @@
import pprint
from django.contrib import admin
from django.contrib.auth.models import User
from ietf.person.models import Person
def merge_persons(source,target,stream):
# merge emails
for email in source.email_set.all():
print >>stream, "Merging email: {}".format(email.address)
email.person = target
email.save()
# merge aliases
target_aliases = [ a.name for a in target.alias_set.all() ]
for alias in source.alias_set.all():
if alias.name in target_aliases:
alias.delete()
else:
print >>stream,"Merging alias: {}".format(alias.name)
alias.person = target
alias.save()
# merge DocEvents
for docevent in source.docevent_set.all():
docevent.by = target
docevent.save()
# merge SubmissionEvents
for subevent in source.submissionevent_set.all():
subevent.by = target
subevent.save()
# merge Messages
for message in source.message_set.all():
message.by = target
message.save()
# merge Constraints
for constraint in source.constraint_set.all():
constraint.person = target
constraint.save()
# merge Roles
for role in source.role_set.all():
role.person = target
role.save()
# merge Nominees
for nominee in source.nominee_set.all():
target_nominee = target.nominee_set.get(nomcom=nominee.nomcom)
if not target_nominee:
target_nominee = target.nominee_set.create(nomcom=nominee.nomcom, email=target.email())
nominee.nomination_set.all().update(nominee=target_nominee)
for fb in nominee.feedback_set.all():
fb.nominees.remove(nominee)
fb.nominees.add(target_nominee)
for np in nominee.nomineeposition_set.all():
existing_target_np = target_nominee.nomineeposition_set.filter(position=np.position).first()
if existing_target_np:
if existing_target_np.state.slug=='pending':
existing_target_np.state = np.state
existing_target_np.save()
np.delete()
else:
np.nominee=target_nominee
np.save()
nominee.delete()
# check for any remaining relationships and delete if none
objs = [source]
opts = Person._meta
user = User.objects.filter(is_superuser=True).first()
admin_site = admin.site
using = 'default'
deletable_objects, perms_needed, protected = admin.utils.get_deleted_objects(
objs, opts, user, admin_site, using)
if len(deletable_objects) > 1:
print >>stream, "Not Deleting Person: {}({})".format(source.ascii,source.pk)
print >>stream, "Related objects remain:"
pprint.pprint(deletable_objects[1],stream=stream)
else:
print >>stream, "Deleting Person: {}({})".format(source.ascii,source.pk)
source.delete()

View file

@ -455,7 +455,6 @@ NOMCOM_PUBLIC_KEYS_DIR = '/a/www/nomcom/public_keys/'
NOMCOM_FROM_EMAIL = 'nomcom-chair@ietf.org' NOMCOM_FROM_EMAIL = 'nomcom-chair@ietf.org'
OPENSSL_COMMAND = '/usr/bin/openssl' OPENSSL_COMMAND = '/usr/bin/openssl'
DAYS_TO_EXPIRE_NOMINATION_LINK = '' DAYS_TO_EXPIRE_NOMINATION_LINK = ''
DEFAULT_FEEDBACK_TYPE = 'offtopic'
NOMINEE_FEEDBACK_TYPES = ['comment', 'questio', 'nomina'] NOMINEE_FEEDBACK_TYPES = ['comment', 'questio', 'nomina']
# ID Submission Tool settings # ID Submission Tool settings

View file

@ -5,6 +5,7 @@
# ./manage.py test --settings=settings_sqlitetest doc.ChangeStateTestCase # ./manage.py test --settings=settings_sqlitetest doc.ChangeStateTestCase
# #
import os
from settings import * # pyflakes:ignore from settings import * # pyflakes:ignore
# Workaround to avoid spending minutes stepping through the migrations in # Workaround to avoid spending minutes stepping through the migrations in
@ -38,4 +39,5 @@ DATABASES = {
if TEST_CODE_COVERAGE_CHECKER and not TEST_CODE_COVERAGE_CHECKER._started: if TEST_CODE_COVERAGE_CHECKER and not TEST_CODE_COVERAGE_CHECKER._started:
TEST_CODE_COVERAGE_CHECKER.start() TEST_CODE_COVERAGE_CHECKER.start()
NOMCOM_PUBLIC_KEYS_DIR=os.path.abspath("tmp-nomcom-public-keys-dir")

View file

@ -324,7 +324,7 @@ class SubmitTests(TestCase):
self.assertTrue((u"I-D Action: %s" % name) in outbox[-3]["Subject"]) self.assertTrue((u"I-D Action: %s" % name) in outbox[-3]["Subject"])
self.assertTrue((u"I-D Action: %s" % name) in draft.message_set.order_by("-time")[0].subject) self.assertTrue((u"I-D Action: %s" % name) in draft.message_set.order_by("-time")[0].subject)
self.assertTrue("Author Name" in unicode(outbox[-3])) self.assertTrue("Author Name" in unicode(outbox[-3]))
self.assertTrue("ietf-announce@" in outbox[-3]['To']) self.assertTrue("i-d-announce@" in outbox[-3]['To'])
self.assertTrue("New Version Notification" in outbox[-2]["Subject"]) self.assertTrue("New Version Notification" in outbox[-2]["Subject"])
self.assertTrue(name in unicode(outbox[-2])) self.assertTrue(name in unicode(outbox[-2]))
self.assertTrue("mars" in unicode(outbox[-2])) self.assertTrue("mars" in unicode(outbox[-2]))

View file

@ -1,6 +1,7 @@
<!DOCTYPE html> {% load ietf_filters staticfiles %} <!DOCTYPE html> {% load ietf_filters staticfiles %}
{# Copyright The IETF Trust 2015, All Rights Reserved #} {# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}{% origin %} {% load origin %}{% origin %}
{% load bootstrap3 %}
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
@ -83,15 +84,7 @@
</nav> </nav>
{% endwith %} {% endwith %}
<div class="container-fluid"> <div class="container-fluid">
{% if messages %} {% bootstrap_messages %}
<div class="row">
<div class="col-lg-12">
{% for message in messages %}
<p class="alert alert-info {% if message.tags %}{{ message.tags }}{% endif %}">{{ message }}</p>
{% endfor %}
</div>
</div>
{% endif %}
{% if request.COOKIES.left_menu != "off" and not hide_menu %} {# ugly hack for the more or less unported meeting agenda edit pages #} {% if request.COOKIES.left_menu != "off" and not hide_menu %} {# ugly hack for the more or less unported meeting agenda edit pages #}
<div class="row"> <div class="row">
<div class="col-md-2 visible-md visible-lg leftmenu"> <div class="col-md-2 visible-md visible-lg leftmenu">

View file

@ -0,0 +1,98 @@
{% extends "nomcom/nomcom_private_base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load bootstrap3 %}
{% load ietf_filters %}
{% block bodyAttrs %}data-spy="scroll" data-target="#nav-instructions"{% endblock %}
{% block subtitle %} - Configuration Help {% endblock %}
{% block nomcom_content %}
{% origin %}
<div class="col-sm-2 col-sm-offset-10 hidden-xs hidden-print bs-docs-sidebar" id="nav-instructions">
<ul class="nav nav-pills nav-stacked small" data-spy="affix">
<li><a href="#keys">Keypair</a></li>
<li><a href="#configure">Configuration</a></li>
<li><a href="#positions">Positions</a></li>
<li><a href="#templates">Templates</a></li>
</ul>
</div>
<div id="instructions" class="col-sm-10">
<h2>Help for Configuring a New NomCom</h2>
<h3 class="anchor-target" id="keys">Generate a keypair for the nomcom</h3>
<p> The Datatracker uses a public/private keypair to encrypt any feedback entered by the community
before storing it in the database. Only persons with the private key can view this feedback.
The private key is provided by using a datatracker page to store a blowfish-encrypted cookie in a browser session.
The blowfish-encrypted private key is sent to the server and used to decrypt feedback. The private key is never
stored on the server, but if the server is compromised, it would be possible for an attacker to grab the private key
by modifying the datatracker code. The NomCom chair generates the keypair for each NomCom and manages its secure
distribution.
</p>
<p>To generate the keypair:
<ol>
<li>
Create a config file for openssl, named nomcom-config.cnf, with the following contents:
<pre>[ req ]
distinguished_name = req_distinguished_name
string_mask = utf8only
x509_extensions = ss_v3_ca
[ req_distinguished_name ]
commonName = Common Name (e.g. NomComYY)
commonName_default = NomCom{{year|slice:"2:"}}
[ ss_v3_ca ]
subjectKeyIdentifier = hash
keyUsage = critical, digitalSignature, keyEncipherment, dataEncipherment
basicConstraints = critical, CA:true
subjectAltName = email:nomcom{{year|slice:"2:"}}@ietf.org
extendedKeyUsage= emailProtection
</pre></li>
<li>Generate a private key and corresponding certificate:
<pre>openssl req -config nomcom-config.cnf -x509 -new -newkey rsa:2048 -sha256 -days 730 -nodes -keyout privateKey-nomcom{{year|slice:"2:"}}.pem -out nomcom{{year|slice:"2:"}}.cert</pre>
(Just press Enter when presented with "Common Name (e.g. NomComYY) [NomCom15]:")
</li>
</ol>
<p>
You will upload the certificate to the datatracker (and make it available to people wishing to send mail) in the steps below.
</p>
<p>Securely distribute privateKey-nomcom{{year|slice:"2:"}} to your NomCom advisor(s), liaisons, and members, as they become known.</p>
<h3 class="anchor-target" id="configure">Configure the Datatracker NomCom</h3>
<p>Sign into the datatracker and go to the <a href="{% url 'nomcom_edit_nomcom' year %}">NomCom Configuration Page</a>.</p>
<p>Use the Browse button to select the public nomcom{{year|slice:"2:"}}.cert file created above.</p>
<p>Enter any special instructions you want to appear on the nomination entry form in the "Help text for nomination form" box. These will appear on the form immediately below the field labeled "Candidate's qualifications for the position".</p>
<p>Choose whether to have the datatracker send questionnares, and whether to automatically remind people to accept nominations and return questionnaires, according to the instructions on the form.</p>
<p>Press the save button.</p>
<p>You can return to this page and change your mind on any of the settings, even towards the end of your nomcom cycle. However, be wary of uploading a new public key once one feedback has been received. That step should only be taken in the case of a compromised keypair. Old feedback will remain encrypted with the old key, and will not be accessible through the datatracker.</p>
<h3 class="anchor-target" id="positions">Configure the Positions to be filled</h3>
<p>Add the positions this nomcom needs to fill.</p>
<p>Only create one Position for those roles having multiple seats to fill,
such as the IAB, or the IESG areas where multiple ADs in that area are at the end of their term. </p>
<p>Note the "Is open" checkbox. When this is set to True, the Position will appear on the Nomination and Feedback pages. You will set this to False when you do not want any more feedback (that is, the position is filled, or otherwise closed for some reason). It is a good idea to start with the "Is open" value set to False. After you edit the templates for the position and are ready for the community to provide nominations and feedback, set the "Is open" value to True.</p>
<p>You might need to close some positions and open others as your nomcom progresses. For example, the 2014 Nomcom was called back after it had finished work on its usual selections to fill a IAOC position that had been vacated mid-term. Before making the call for nominations and feedback for this additional IAOC position, the chair would mark the already filled positions as not open, leaving only the new IAOC position open for consideration. At that point, only that IAOC position would be available on the Nomination and Feedback pages. </p>
<h3 class="anchor-target" id="templates">Customize the web-form and email templates</h3>
<p>Edit each of the templates at {% url 'nomcom_list_templates' year %}. The "Home page of group" template is where to put information about the current nomcom members and policies. It is also a good place to list incumbents in positions, and information about whether the incumbents will stand again. See the home page of past nomcoms for examples.</p>
<h3 class="anchor-target" id="test">Test the results</h3>
<p> Before advertising that your nomcom pages are ready for the community to use, test your configuration. Create a dummy nominee for at least one position, and give it some feedback. You will be able to move this out of the way later. Once you've marked positions as open, ask your nomcom members to look over the expertise and questionnaires tab (which show rendered view of each of the templates for each position) to ensure they contain what you want the community to see. Please don't assume that everything is all right without looking. It's a good idea to give the secretariat and the tools team a heads up a few (preferably 3 to 5) days notice before announcing that your pages are ready for community use.
</div>
{% endblock %}

View file

@ -1,13 +0,0 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% block title %}NomCom deleted{% endblock %}
{% block content %}
{% origin %}
<h1>NomCom deleted</h1>
<p class="alert alert-success">All data about the NomCom has been removed.</p>
{% endblock %}

View file

@ -10,8 +10,6 @@
{% origin %} {% origin %}
<h2>Edit members</h2> <h2>Edit members</h2>
{% bootstrap_messages %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% bootstrap_form form %} {% bootstrap_form form %}

View file

@ -14,12 +14,6 @@
{% origin %} {% origin %}
<h2>Settings</h2> <h2>Settings</h2>
{% if message %}
<div class="alert alert-{{ message.0 }}">{{ message.1 }}</div>
{% endif %}
{% bootstrap_messages %}
<form enctype="multipart/form-data" method="post"> <form enctype="multipart/form-data" method="post">
{% csrf_token %} {% csrf_token %}
{% bootstrap_form form %} {% bootstrap_form form %}
@ -37,12 +31,6 @@
{% endbuttons %} {% endbuttons %}
</form> </form>
<h2>Delete Nomcom</h2>
<p>
<a class="btn btn-danger" href="{% url "nomcom_delete_nomcom" year %}" class="deletelink">Delete NomCom</a>
</p>
{% endblock %} {% endblock %}
{% block js %} {% block js %}

View file

@ -8,14 +8,9 @@
{% block nomcom_content %} {% block nomcom_content %}
{% origin %} {% origin %}
{% bootstrap_messages %}
<h2>Edit email<br><small>{{ nominee }}</small></h2> <h2>Edit email<br><small>{{ nominee }}</small></h2>
{% if message %}
<p class="alert alert-{{ message.0 }}">{{ message.1 }}</p>
{% endif %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% bootstrap_form form %} {% bootstrap_form form %}

View file

@ -15,8 +15,6 @@
{% origin %} {% origin %}
<h2>{% if position %}Edit{% else %}Add{% endif %} position</h2> <h2>{% if position %}Edit{% else %}Add{% endif %} position</h2>
{% bootstrap_messages %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% bootstrap_form form %} {% bootstrap_form form %}

View file

@ -31,7 +31,7 @@
{% endif %} {% endif %}
</dl> </dl>
<form method="post"> <form id="templateform" method="post">
{% csrf_token %} {% csrf_token %}
{% bootstrap_form form %} {% bootstrap_form form %}

View file

@ -5,24 +5,30 @@
{% load bootstrap3 %} {% load bootstrap3 %}
{% load nomcom_tags %} {% load nomcom_tags %}
{% block morecss %}
.btn-group-vertical .btn {
text-align: left;
}
.btn-group-vertical .btn .badge {
float:right; margin-top: -1.3em;
}
{% endblock %}
{% block subtitle %} - Feedback{% endblock %} {% block subtitle %} - Feedback{% endblock %}
{% block nomcom_content %} {% block nomcom_content %}
{% origin %} {% origin %}
<p class="alert alert-info"> <p id="instructions" class="alert alert-info">
First select a nominee from the list of nominees to provide input about that nominee. {% if nomcom.group.state_id == 'conclude' %}
This will fill in the non-editable fields in the form. Feedback to this nomcom is closed.
{% else %}
Select a nominee from the list of nominees to the right to obtain a new feedback form.
{% endif %}
</p> </p>
{% if message %}
<p class="alert alert-{{ message.0 }}">{{ message.1 }}</p>
{% endif %}
{% bootstrap_messages %}
{% if nomcom|has_publickey %} {% if nomcom|has_publickey %}
<div class="row"> <div class="row">
<div class="col-sm-4 col-sm-push-8"> <div id="nominees" class="col-sm-4 col-sm-push-8">
<h3>Nominees</h3> <h3>Nominees</h3>
{% for p in positions %} {% for p in positions %}
@ -30,9 +36,14 @@
<h4>{{ p.name }}</h4> <h4>{{ p.name }}</h4>
<div class="btn-group-vertical form-group"> <div class="btn-group-vertical form-group">
{% for np in p.nomineeposition_set.accepted.not_duplicated %} {% for np in p.nomineeposition_set.accepted.not_duplicated %}
<a class="btn btn-default btn-xs" href="?nominee={{np.nominee.id}}&position={{ np.position.id}}"> <a class="btn btn-default btn-xs" {% if nomcom.group.state_id != 'conclude' %}href="?nominee={{np.nominee.id}}&position={{ np.position.id}}"{% endif %}>
{{ np.nominee }} {{ np.nominee.name }}
{% add_num_nominations user np.position np.nominee %} {% with count=counts|lookup:np.position.id|lookup:np.nominee.id %}
<span class="badge"
title="{% if count %}{{count}} earlier comment{{count|pluralize}} from you {% else %}You have not yet provided feedback {% endif %} on {{np.nominee.email.address}} as {{np.position}}">
{{ count | default:"no feedback" }}
</span>&nbsp;
{% endwith %}
</a> </a>
{% endfor %} {% endfor %}
</div> </div>
@ -42,22 +53,30 @@
<p> <p>
An number after a name indicates An number after a name indicates
that you have given comments on this nominee that you have given comments on this nominee
earlier. If you position the mouse pointer over earlier. Position the mouse pointer over
it, you should see how many comments the badge, for more information about this
exist from you for this nominee. nominee.
</p> </p>
</div> </div>
<div class="col-sm-8 col-sm-pull-4"> <div class="col-sm-8 col-sm-pull-4">
<h3>Provide feedback</h3> {% if form %}
<h3>Provide feedback
{% if form.position %}
about {{form.nominee.email.person.name}} ({{form.nominee.email.address}}) for the {{form.position.name}} position.</p>
{% endif %}
</h3>
<p>This feedback will only be available to <a href="{% url 'nomcom_year_index' year=year %}">NomCom {{year}}</a>.
You may have the feedback mailed back to you by selecting the option below.</p>
<form id="feedbackform" method="post"> <form id="feedbackform" method="post">
{% csrf_token %} {% csrf_token %}
{% bootstrap_form form %} {% bootstrap_form form %}
{% buttons %} {% buttons %}
<input class="btn btn-primary" type="submit" value="Save" name="save" {% if submit_disabled %}disabled="disabled"{% endif %}> <input class="btn btn-primary" type="submit" value="Save" name="save">
{% endbuttons %} {% endbuttons %}
</form> </form>
{% endif %}
</div> </div>
</div> </div>
{% endif %} {% endif %}

View file

@ -8,31 +8,38 @@
{% origin %} {% origin %}
<h2>Positions in {{ nomcom.group }}</h2> <h2>Positions in {{ nomcom.group }}</h2>
<a class="btn btn-default" href="{% url "nomcom_add_position" year %}">Add new position</a> {% if nomcom.group.state_id == 'active' %}
<a class="btn btn-default" href="{% url "nomcom_add_position" year %}">Add new position</a>
<p></p>
{% endif %}
{% if positions %} {% if positions %}
{% for position in positions %} {% regroup positions by is_open as posgroups %}
<h3>{{ position.name }}</h3> {% for group in posgroups %}
<div class="panel panel-default">
<div class="panel-heading"><h3>{{ group.grouper| yesno:"Open Positions,Closed Positions"}}</h3></div>
<div class="panel-body">
{% for position in group.list %}
<h4>{{ position.name }}</h4>
<dl class="dl-horizontal"> <dl class="dl-horizontal">
<dt>Description</dt>
<dd>{{ position.description }}</dd>
<dt>Incumbent</dt>
<dd>{% if position.incumbent %}{{ position.incumbent.person }} &lt;{{ position.incumbent.address }}&gt;{% else %}None{% endif %}</dd>
<dt>Is open</dt>
<dd>{{ position.is_open }}</dd>
<dt>Templates</dt> <dt>Templates</dt>
<dd> <dd>
{% for template in position.get_templates %} {% for template in position.get_templates %}
<a href="{% url "nomcom_edit_template" year template.id %}">{{ template }}</a><br> <a href="{% url "nomcom_edit_template" year template.id %}">{{ template }}</a><br>
{% endfor %} {% endfor %}
</dd> </dd>
{% if nomcom.group.state_id == 'active' %}
<dt>Actions</dt> <dt>Actions</dt>
<dd> <dd>
<a class="btn btn-default" href="{% url "nomcom_edit_position" year position.id %}">Edit</a> <a class="btn btn-default" href="{% url "nomcom_edit_position" year position.id %}">Edit</a>
<a class="btn btn-default" href="{% url "nomcom_remove_position" year position.id %}">Remove</a> <a class="btn btn-default" href="{% url "nomcom_remove_position" year position.id %}">Remove</a>
</dd> </dd>
{% endif %}
</dl> </dl>
{% endfor %} {% endfor %}
</div>
</div>
{% endfor %}
{% else %} {% else %}
<p>There are no positions defined.</p> <p>There are no positions defined.</p>
{% endif %} {% endif %}

View file

@ -0,0 +1,11 @@
The following Person records have been identified by the NomCom chair as potential duplicates.
The following records:{% for p in duplicate_persons %}
{{p.name}} ({{p.id}}) [{{p.email_set.all|join:", "}}]{% endfor %}
appear to be duplicates of this person (which should be kept)
{{primary_person.name}} ({{primary_person.id}}) [{{primary_person.email_set.all|join:", "}}]
Please verify that these are indeed duplicates, and if so, merge them.
Thanks in advance.

View file

@ -9,7 +9,7 @@
{% block content %} {% block content %}
{% origin %} {% origin %}
<h1>NomCom {{ year }} <small>Private area</small></h1> <h1>NomCom {{ year }} {% if nomcom.group.state_id == 'conclude' %}(Concluded){% endif %} <small>Private area {% if is_chair_task %}- Chair/Advisors only{% endif %}</small></h1>
<ul class="nav nav-tabs" role="tablist"> <ul class="nav nav-tabs" role="tablist">
<li {% if selected == "index" %}class="active"{% endif %}><a href="{% url "nomcom_private_index" year %}">Nominees</a></li> <li {% if selected == "index" %}class="active"{% endif %}><a href="{% url "nomcom_private_index" year %}">Nominees</a></li>
@ -17,31 +17,32 @@
{% if nomcom|has_publickey %} {% if nomcom|has_publickey %}
<li {% if selected == "nominate" %}class="active"{% endif %}><a href="{% url "nomcom_private_nominate" year %}">Nominate</a></li> <li {% if selected == "nominate" %}class="active"{% endif %}><a href="{% url "nomcom_private_nominate" year %}">Nominate</a></li>
<li {% if selected == "feedback" %}class="active"{% endif %}><a href="{% url "nomcom_private_feedback" year %}">Enter feedback</a></li> <li {% if selected == "feedback" %}class="active"{% endif %}><a href="{% url "nomcom_private_feedback" year %}">Enter feedback</a></li>
<li {% if selected == "questionnaire" %}class="active"{% endif %}><a href="{% url "nomcom_private_questionnaire" year %}">Questionnaire response</a></li>
{% endif %} {% endif %}
<li {% if selected == "view_feedback" %}class="active"{% endif %}><a href="{% url "nomcom_view_feedback" year %}">View feedback</a></li> <li {% if selected == "view_feedback" %}class="active"{% endif %}><a href="{% url "nomcom_view_feedback" year %}">View feedback</a></li>
<li {% if selected == "private_key" %}class="active"{% endif %}><a href="{% url "nomcom_private_key" year %}">Private key</a></li> <li {% if selected == "private_key" %}class="active"{% endif %}><a href="{% url "nomcom_private_key" year %}">Private key</a></li>
{% if user|is_chair:year %} {% if user|is_chair_or_advisor:year %}
<li class="dropdown"> <li class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">Chair <span class="caret"></span></a> <a class="dropdown-toggle" data-toggle="dropdown" href="#">Chair/Advisor Tasks <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu"> <ul class="dropdown-menu" role="menu">
<li {% if selected == "feedback_pending" %}class="active"{% endif %}><a href="{% url "nomcom_view_feedback_pending" year %}">Pending feedback</a></li> {% if nomcom.group.state_id == 'active' %}
<li {% if selected == "feedback_email" %}class="active"{% endif %}><a href="{% url "nomcom_private_feedback_email" year %}">Enter email feedback</a></li> <li role = "presentation" class = "dropdown-header">Feedback Management</li>
<li {% if selected == "send_accept_reminder" %}class="active"{% endif %}><a href="{% url "nomcom_send_reminder_mail" year "accept" %}">Send accept reminder</a></li> <li {% if selected == "feedback_pending" %}class="active"{% endif %}><a href="{% url "nomcom_view_feedback_pending" year %}">Classify pending feedback</a></li>
<li {% if selected == "send_questionnaire_reminder" %}class="active"{% endif %}><a href="{% url "nomcom_send_reminder_mail" year "questionnaire" %}">Send questionnaire reminder</a></li> <li {% if selected == "feedback_email" %}class="active"{% endif %}><a href="{% url "nomcom_private_feedback_email" year %}">Enter email feedback</a></li>
</ul> <li {% if selected == "questionnaire" %}class="active"{% endif %}><a href="{% url "nomcom_private_questionnaire" year %}">Enter questionnaire response</a></li>
</li> <li {% if selected == "send_accept_reminder" %}class="active"{% endif %}><a href="{% url "nomcom_send_reminder_mail" year "accept" %}">Send accept reminder</a></li>
<li {% if selected == "send_questionnaire_reminder" %}class="active"{% endif %}><a href="{% url "nomcom_send_reminder_mail" year "questionnaire" %}">Send questionnaire reminder</a></li>
<li class="dropdown"> <li {% if selected == "merge" %}class="active"{% endif %}><a href="{% url "nomcom_private_merge" year %}">Request Nominee Merge</a></li>
<a class="dropdown-toggle" data-toggle="dropdown" href="#">Edit <span class="caret"></span></a> {% endif %}
<ul class="dropdown-menu" role="menu"> <li role = "presentation" class = "dropdown-header">Nomcom Configuration</li>
<li {% if selected == "edit_nomcom" %}class="active"{% endif %}><a href="{% url "nomcom_edit_nomcom" year %}">Edit Settings</a></li> <li {% if selected == "edit_nomcom" %}class="active"{% endif %}><a href="{% url "nomcom_edit_nomcom" year %}">Edit Settings</a></li>
<li {% if selected == "edit_templates" %}class="active"{% endif %}><a href="{% url "nomcom_list_templates" year %}">Edit Pages</a></li> <li {% if selected == "edit_templates" %}class="active"{% endif %}><a href="{% url "nomcom_list_templates" year %}">Edit Pages</a></li>
<li {% if selected == "edit_positions" %}class="active"{% endif %}><a href="{% url "nomcom_list_positions" year %}">Edit Positions</a></li> <li {% if selected == "edit_positions" %}class="active"{% endif %}><a href="{% url "nomcom_list_positions" year %}">Edit Positions</a></li>
<li {% if selected == "merge" %}class="active"{% endif %}><a href="{% url "nomcom_private_merge" year %}">Merge Email Addresses</a></li> {% if nomcom.group.state_id == 'active' %}
<li {% if selected == "edit_members" %}class="active"{% endif %}><a href="{% url "nomcom_edit_members" year %}">Edit Nomcom Members</a></li> <li {% if selected == "edit_members" %}class="active"{% endif %}><a href="{% url "nomcom_edit_members" year %}">Edit Members</a></li>
{% endif %}
<li {% if selected == "help" %}class="active"{% endif %}><a href="{% url "nomcom_chair_help" year %}">Configuration Help</a></li>
</ul> </ul>
</li> </li>
{% endif %} {% endif %}

View file

@ -9,7 +9,7 @@
{% block content %} {% block content %}
{% origin %} {% origin %}
<h1>NomCom {{ year }}</h1> <h1>NomCom {{ year }} {% if nomcom.group.state_id == 'conclude' %}(Concluded){% endif %}</h1>
<ul class="nav nav-tabs" role="tablist"> <ul class="nav nav-tabs" role="tablist">
<li {% if selected == "index" %}class="active"{% endif %}><a href="{% url "nomcom_year_index" year %}">Home</a></li> <li {% if selected == "index" %}class="active"{% endif %}><a href="{% url "nomcom_year_index" year %}">Home</a></li>

View file

@ -1,33 +0,0 @@
{# Copyright The IETF Trust 2015, All Rights Reserved #}{% load origin %}{% origin %}
<div class="baseform">
{% for fieldset in form.get_fieldsets %}
{% if fieldset.name %}
<div class="fieldset">
<h2>{{ fieldset.name }}</h2>
{% endif %}
{% for field in fieldset.fields %}
<div id="baseform-fieldname-{{ field.html_name }}"
{% if field.field.widget.is_hidden %}style="display: none;"{% endif %}
class="{% if field.errors %}fieldError {% endif %}field BaseFormStringWidget{% if field.field.column_style %} {{ field.field.column_style }}{% endif %}">
<label for="id_{{ field.html_name }}">{{ field.label }}
{% if field.field.required %}
<span class="fieldRequired" title="Required">*</span>
{% endif %}
</label>
<div class="fieldWidget">
<div id="{{ field.html_name }}_help" class="formHelp"> {{ field.help_text }}</div>
{{ field }}
<pre>{{ field.errors }}</pre>
</div>
<div class="endfield"></div>
</div>
{% endfor %}
{% if fieldset.name %}
</div>
{% endif %}
{% endfor %}
</div>

View file

@ -8,14 +8,9 @@
{% block nomcom_content %} {% block nomcom_content %}
{% origin %} {% origin %}
{% if message %}
<div class="alert alert-{{ message.0 }}">{{ message.1 }}</div>
{% endif %}
{% if nomcom|has_publickey %} {% if form %}
{% bootstrap_messages %} <form id="paste-email-feedback-form" method="post">
<form id="questionnnaireform" method="post">
{% csrf_token %} {% csrf_token %}
{% bootstrap_form form %} {% bootstrap_form form %}
{% buttons %} {% buttons %}

View file

@ -66,14 +66,14 @@
</form> </form>
{% if is_chair %} {% if is_chair and nomcom.group.state_id == 'active' %}
<form class="form-inline" id="batch-action-form" method="post">{% csrf_token %} <form class="form-inline" id="batch-action-form" method="post">{% csrf_token %}
{% endif %} {% endif %}
<table class="table table-condensed table-striped"> <table class="table table-condensed table-striped">
<thead> <thead>
<tr> <tr>
{% if is_chair %}<th colspan="2"><span class="fa fa-check"></span></th>{% endif %} {% if is_chair and nomcom.group.state_id == 'active' %}<th colspan="2"><span class="fa fa-check"></span></th>{% endif %}
<th>Nominee</th> <th>Nominee</th>
<th>Position</th> <th>Position</th>
<th>State</th> <th>State</th>
@ -83,7 +83,7 @@
<tbody> <tbody>
{% for np in nominee_positions %} {% for np in nominee_positions %}
<tr> <tr>
{% if is_chair %} {% if is_chair and nomcom.group.state_id == 'active' %}
<td><input class="batch-select" type="checkbox" value="{{ np.id }}" name="selected"></td> <td><input class="batch-select" type="checkbox" value="{{ np.id }}" name="selected"></td>
<td class="edit"><a class="btn btn-default btn-xs" href="{% url "nomcom_edit_nominee" year np.nominee.id %}">Edit</a></td> <td class="edit"><a class="btn btn-default btn-xs" href="{% url "nomcom_edit_nominee" year np.nominee.id %}">Edit</a></td>
{% endif %} {% endif %}
@ -101,23 +101,21 @@
{% if is_chair %} {% if is_chair %}
{% if message %} {% if nomcom.group.state_id == 'active' %}
<p class="alert alert-{{ message.0 }}">{{ message.1 }}</p> <div class="form-group">
<label>Action:</label>
<select class="form-control" name="action">
<option value="" selected="selected">---------</option>
<option value="set_as_accepted">Set as accepted</option>
<option value="set_as_pending">Set as pending</option>
<option value="set_as_declined">Set as declined</option>
</select>
</div>
<button class="btn btn-warning" type="submit" title="Run action">Apply</button>
</form>
{% endif %} {% endif %}
<div class="form-group">
<label>Action:</label>
<select class="form-control" name="action">
<option value="" selected="selected">---------</option>
<option value="set_as_accepted">Set as accepted</option>
<option value="set_as_pending">Set as pending</option>
<option value="set_as_declined">Set as declined</option>
</select>
</div>
<button class="btn btn-warning" type="submit" title="Run action">Apply</button>
</form>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View file

@ -10,10 +10,6 @@
{% origin %} {% origin %}
<h2>Enter private key</h2> <h2>Enter private key</h2>
{% if message %}
<p class="alert alert-{{ message.0 }}">{{ message.1 }}</p>
{% endif %}
<p>In order to access the {{ nomcom.group }} data you have to enter your private key. Please paste it in the text area below. The key must be in the following format:</p> <p>In order to access the {{ nomcom.group }} data you have to enter your private key. Please paste it in the text area below. The key must be in the following format:</p>
<pre> <pre>

View file

@ -1,44 +1,53 @@
{% extends "nomcom/nomcom_private_base.html" %} {% extends "nomcom/nomcom_private_base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #} {# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %} {% load origin %}
{% load staticfiles %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% block subtitle %} - Merging emails {% endblock %} {% block pagehead %}
<link rel="stylesheet" href="{% static 'select2/select2.css' %}">
<link rel="stylesheet" href="{% static 'select2-bootstrap-css/select2-bootstrap.min.css' %}">
{% endblock %}
{% block subtitle %} - Request Nominee Merge {% endblock %}
{% block nomcom_content %} {% block nomcom_content %}
{% origin %} {% origin %}
<h2>Merging nominee email addresses</h2> <h2>Request Nominee Merge</h2>
<p> <p>
If a nominee has been nominated with multiple email addresses, the nominee will The nomination system encourages the community to nominate people by selecting
appear multiple times in the nomination list, as the email address is used as their email address from the set of addresses the tracker already knows. In order
the unique identifier for each nominee. In order to permit comments and nominations to allow a person who does not yet have a datatracker account to be nominated, the
to be submitted under multiple email addresses, there is a list of secondary email system also provides a way for the community to nominate people with a new,
addresses which needs to be kept up-to-date. When nominations of one particular nominee previously unknown email address. When this option is chosen, a new Person record
have already been made under different email addresses, the nomination comments from the is created and associated with the new address.
secondary address also needs to be merged with those under the primary address.
</p> </p>
<p> <p>
It doesn't matter particularly which email address is used as primary, as far as the Occasionally, this new address should have been associated with an existing person
nominee information maintenance goes, but it's probably handier for the nomcom if the instead. This will happen particularly if the community member uses a slightly incorrect
primary address is the one which the nominee prefers at the time. address (such as a typo), or knows the person they want to nominate by a very old or very
new address that is not yet in the tracker. When this happens, you can use this form to
ask the secretariat to merge the two Person records. The secretariat has a process
for verifying that the addresses both belong to the same person, and a tool that
can correct the relevant data.
</p> </p>
{% if message %} {% if form %}
<div class="alert alert-{{ message.0 }}">{{ message.1 }}</div> <form id="mergeform" method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<input class="btn btn-primary" type="submit" value="Save" name="save">
{% endbuttons %}
</form>
{% endif %} {% endif %}
{% bootstrap_messages %} {% endblock %}
<form id="nominateform" method="post"> {% block js %}
{% csrf_token %} <script src="{% static 'select2/select2.min.js' %}"></script>
{% bootstrap_form form %} <script src="{% static 'ietf/js/select2-field.js' %}"></script>
{% buttons %}
<input class="btn btn-primary" type="submit" value="Save" name="save">
{% endbuttons %}
</form>
{% endblock %} {% endblock %}

View file

@ -1,23 +1,23 @@
{% extends "nomcom/nomcom_private_base.html" %} {% extends "nomcom/nomcom_private_base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #} {# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %} {% load origin %}
{% load staticfiles %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load nomcom_tags %} {% load nomcom_tags %}
{% block pagehead %}
<link rel="stylesheet" href="{% static 'select2/select2.css' %}">
<link rel="stylesheet" href="{% static 'select2-bootstrap-css/select2-bootstrap.min.css' %}">
{% endblock %}
{% block subtitle %} - Nominate{% endblock %} {% block subtitle %} - Nominate{% endblock %}
{% block nomcom_content %} {% block nomcom_content %}
{% origin %} {% origin %}
<h2>Candidate nomination</h2> <h2>Candidate nomination</h2>
{% bootstrap_messages %} {% if form %}
{% if message %}
<p class="alert alert-{{ message.0 }}">{{ message.1 }}</p>
{% endif %}
{% if nomcom|has_publickey %}
<form id="nominate-form" method="post"> <form id="nominate-form" method="post">
{% csrf_token %} {% csrf_token %}
@ -30,3 +30,8 @@
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block js %}
<script src="{% static 'select2/select2.min.js' %}"></script>
<script src="{% static 'ietf/js/select2-field.js' %}"></script>
{% endblock %}

View file

@ -9,18 +9,12 @@
{% block nomcom_content %} {% block nomcom_content %}
{% if message %} {% if form %}
<p class="alert alert-{{ message.0 }}">{{ message.1 }}</p>
{% endif %}
{% if nomcom|has_publickey %}
{% if questionnaire_response %} {% if questionnaire_response %}
<h2>Questionnaire response</h2> <h2>Questionnaire response</h2>
{{ questionnaire_response }} {{ questionnaire_response }}
{% endif %} {% endif %}
{% bootstrap_messages %}
<form id="questionnnaireform" method="post"> <form id="questionnnaireform" method="post">
{% csrf_token %} {% csrf_token %}
{% bootstrap_form form %} {% bootstrap_form form %}

View file

@ -8,14 +8,13 @@
{% block nomcom_content %} {% block nomcom_content %}
{% origin %} {% origin %}
{% if message %}
<div class="alert alert-{{ message.0 }}">{{ message.1 }}</div>
{% endif %}
{% if need_confirmation %} {% if need_confirmation %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
{% bootstrap_form form %}
{% buttons %} {% buttons %}
<button class="btn btn-primary" type="submit">Save</button> <button class="btn btn-primary" type="submit">Save</button>
{% endbuttons %} {% endbuttons %}

View file

@ -1,21 +1,22 @@
{% extends "nomcom/nomcom_public_base.html" %} {% extends "nomcom/nomcom_public_base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #} {# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %} {% load origin %}
{% load staticfiles %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load nomcom_tags %} {% load nomcom_tags %}
{% block pagehead %}
<link rel="stylesheet" href="{% static 'select2/select2.css' %}">
<link rel="stylesheet" href="{% static 'select2-bootstrap-css/select2-bootstrap.min.css' %}">
{% endblock %}
{% block subtitle %} - Nominate{% endblock %} {% block subtitle %} - Nominate{% endblock %}
{% block nomcom_content %} {% block nomcom_content %}
{% origin %} {% origin %}
{% if message %}
<p class="alert alert-{{ message.0 }}">{{ message.1 }}</p>
{% endif %}
{% bootstrap_messages %} {% if form %}
{% if nomcom|has_publickey %}
<form id="nominate-form" method="post"> <form id="nominate-form" method="post">
{% csrf_token %} {% csrf_token %}
{% bootstrap_form form %} {% bootstrap_form form %}
@ -26,3 +27,8 @@
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block js %}
<script src="{% static 'select2/select2.min.js' %}"></script>
<script src="{% static 'ietf/js/select2-field.js' %}"></script>
{% endblock %}

View file

@ -19,7 +19,6 @@
<div class="tab-content"> <div class="tab-content">
{% for position in positions %} {% for position in positions %}
<div class="tab-pane {% if forloop.first %}active{% endif %}" id="{{ position.name|slugify }}"> <div class="tab-pane {% if forloop.first %}active{% endif %}" id="{{ position.name|slugify }}">
<h3>{{ position.description }}</h3>
{{ position.get_questionnaire|linebreaks}} {{ position.get_questionnaire|linebreaks}}
</div> </div>
{% endfor %} {% endfor %}

View file

@ -7,16 +7,24 @@
{% block nomcom_content %} {% block nomcom_content %}
{% origin %} {% origin %}
<h2>Position: {{ position }}</h2> <h2>Position: {{ position }}</h2>
<dl>
<dt>Description:</dt>
<dd>{{ position.description }}</dd>
<dt>Incumbent:</dt>
<dd>{{ position.incumbent }}</dd>
<dt>Is open:</dt>
<dd>{{ position.is_open }}</dd>
</dl>
<h3>Do you want to remove it?</h3> <p>This position is currently {{position.is_open|yesno:"open,closed"}}.</p>
<p>It has {{position.feedback_set.count|default:"no"}} feedback objects associated with it.</p>
{% if position.feedback_set.count %}
<p>
<span class="alert alert-warning">Unless this is a position created only for testing, deleting it is likely to be harmful. All of the feedback will also be deleted.</span>
</p>
<p>
{% if position.is_open %}
If you are just wanting the position to disappear from the lists available to the community for providing nominations and feedback, instead of deleting the position, edit the position and change is_open to False.
{% else %}
Since the position is closed, it will not appear on the lists available to the community for providing nominations and feedback.
{% endif %}
</p>
<p>If this is just a test position, it is ok to delete it.</p>
{% else %}
<p>This position is safe to delete.</p>
{% endif %}
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
@ -24,8 +32,8 @@
<input type="hidden" name="remove" value="1"> <input type="hidden" name="remove" value="1">
{% buttons %} {% buttons %}
<a class="btn btn-default pull-right" href="../">No, get me out of here</a> <button class="btn btn-primary btn-warning" type="submit">Delete</button>
<button class="btn btn-primary" type="submit">Yes, remove it</button> <a class="btn btn-default" href="{% url 'nomcom_list_positions' year %}">Cancel</a>
{% endbuttons %} {% endbuttons %}
</form> </form>

View file

@ -26,8 +26,7 @@
<div class="tab-content"> <div class="tab-content">
{% for position in positions %} {% for position in positions %}
<div class="tab-pane {% if forloop.first %}active{% endif %}" id="{{ position.name|slugify }}"> <div class="tab-pane {% if forloop.first %}active{% endif %}" id="{{ position.name|slugify }}">
<h3>{{ position.description }}</h3> {{ position.get_requirement|safe }}
{{ position.get_requirement|linebreaks}}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>

View file

@ -11,51 +11,48 @@
{% origin %} {% origin %}
<h2>Send remider to {{reminder_description}}</h2> <h2>Send remider to {{reminder_description}}</h2>
<p>The message that will be sent is as follows:</p> {% if nomcom.group.state_id == 'active' %}
<pre>{{ mail_template.content|wrap_text:80 }}</pre> <p>The message that will be sent is as follows:</p>
<pre>{{ mail_template.content|wrap_text:80 }}</pre>
{% if mail_template %} {% if mail_template %}
<p> <p>
<a class="btn btn-default" href="{% url "nomcom_edit_template" year mail_template.id %}">Edit the message</a> <a class="btn btn-default" href="{% url "nomcom_edit_template" year mail_template.id %}">Edit the message</a>
</p> </p>
{% endif %}
<p>These are the nominees that are in the '{{state_description}}' state for the listed positions. </p>
<p>The message that will be sent is shown below the list of nominees. </p>
{% endif %} {% endif %}
<p>These are the nominees that are in the '{{state_description}}' state for the listed positions. </p> {% if nomcom.group.state_id == 'active' %}
<form id="reminderform " method="post">
<p>The message that will be sent is shown below the list of nominees. </p> {% csrf_token %}
<table class="table table-condensed table-striped">
{% bootstrap_messages %} <thead>
{% if message %}
<div class="alert alert-{{ message.0 }}">{{ message.1 }}</div>
{% endif %}
<form method="post">
{% csrf_token %}
<table class="table table-condensed table-striped">
<thead>
<tr>
<th><span class="fa fa-check"></span></th>
<th>Nominee</th>
<th>Positions</th>
</tr>
</thead>
<tbody>
{% for nominee in nominees %}
<tr> <tr>
<td> <th><span class="fa fa-check"></span></th>
<input class="batch-select" type="checkbox" value="{{ nominee.id }}" name="selected" checked="checked"> <th>Nominee</th>
</td> <th>Positions</th>
<td>{{ nominee }}</td>
<td>{{nominee.interesting_positions|join:", "}}</td>
</tr> </tr>
{% endfor %} </thead>
</tbody> <tbody>
</table> {% for nominee in nominees %}
<tr>
{% buttons %} <td>
<input class="btn btn-primary" type="submit" name="submit" value="Submit request"> <input class="batch-select" type="checkbox" value="{{ nominee.id }}" name="selected" checked="checked">
{% endbuttons %} </td>
</form> <td>{{ nominee }}</td>
<td>{{nominee.interesting_positions|join:", "}}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% buttons %}
<input class="btn btn-primary" type="submit" name="submit" value="Submit request">
{% endbuttons %}
</form>
{% endif %}
{% endblock %} {% endblock %}

View file

@ -0,0 +1,41 @@
{% extends "nomcom/nomcom_private_base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% block subtitle %} - Template: {{ template }}{% endblock %}
{% load bootstrap3 %}
{% block nomcom_content %}
{% origin %}
<h2>Template: {{ template }}</h2>
<dl>
<dt>Title</dt>
<dd>{{ template.title }}</dt>
<dt>Group</dt>
<dd>{{ template.group }}</dd>
<dt>Template type</dt>
<dd>{{ template.type.name }}:
{% if template.type.slug == "rst" %}
This template uses the syntax of reStructuredText. Get a quick reference at <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">http://docutils.sourceforge.net/docs/user/rst/quickref.html</a>. You can do variable interpolation with $variable if the template allows any variable.
{% elif template.type.slug == "django" %}
This template uses the syntax of the default django template framework. Get more info at <a href="https://docs.djangoproject.com/en/dev/topics/templates/">https://docs.djangoproject.com/en/dev/topics/templates/</a>. You can do variable interpolation with the current django markup &#123;&#123;variable&#125;&#125; if the template allows any variable.
{% elif template.type.slug == "plain" %}
This template uses plain text, so no markup is used. You can do variable interpolation with $variable if the template allows any variable.
{% endif %}
</dd>
{% if template.variables %}
<dt>Variables allowed in this template</dt>
<dd>{{ template.variables|linebreaks }}</dd>
{% endif %}
</dl>
<div class = "panel panel-default">
<p class='pasted'>{{ template.content }}</p>
</div>
{% buttons %}
<a class="btn btn-default pull-right" href="{% if return_url %}{{ return_url }}{% else %}../{% endif %}">Back</a>
{% endbuttons %}
{% endblock nomcom_content %}

View file

@ -10,28 +10,48 @@
{% origin %} {% origin %}
<h2>Feedback related to nominees</h2> <h2>Feedback related to nominees</h2>
<table class="table table-condensed table-striped"> {% regroup nominees_feedback by nominee.staterank as stateranked_nominees %}
<thead> {% for staterank in stateranked_nominees %}
<tr> <div class="panel panel-default">
<th>Nominee</th> <div class="panel-heading">
{% for ft in feedback_types %} {% if staterank.grouper == 0 %}
<th>{{ ft.name }}</th> Accepted nomination for at least one position
{% endfor %} {% elif staterank.grouper == 1 %}
</tr> Pending for at least one position and has not accepted any nomination
</thead> {% else %}
<tbody> Declined each nominated position
{% for nominee, feedback in nominees_feedback.items %} {% endif %}
<tr> </div>
<td> <div class="panel-body">
<a href="{% url "nomcom_view_feedback_nominee" year nominee.id %}#comment">{{ nominee }} <table class="table table-condensed table-striped">
</td> <thead>
{% for f in feedback %} <tr>
<td>{{ f.1 }}</td> <th class="col-sm-9">Nominee</th>
{% for ft in feedback_types %}
<th class="col-sm-1 text-center">{{ ft.name }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for fb_dict in staterank.list %}
<tr>
<td>
<a href="{% url "nomcom_view_feedback_nominee" year fb_dict.nominee.id %}">{{ fb_dict.nominee.name }}</a>
<span class="hidden-xs">&lt;{{fb_dict.nominee.email.address}}&gt;</span>
</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 %} {% endfor %}
</tr> </tbody>
{% endfor %} </table>
</tbody> </div>
</table> </div>
{% endfor %}
{% if independent_feedback_types %} {% if independent_feedback_types %}
<h2>Feedback not related to Nominees</h2> <h2>Feedback not related to Nominees</h2>

View file

@ -23,7 +23,7 @@
{% if feedback.type.slug == ft.slug %} {% if feedback.type.slug == ft.slug %}
{% if forloop.first %}<p></p>{% else %}<hr>{% endif %} {% if forloop.first %}<p></p>{% else %}<hr>{% endif %}
<dl class="dl-horizontal"> <dl class="dl-horizontal">
<dt>From</dt> <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>{{ feedback.author|formatted_email|default:"Anonymous" }}
{% if ft.slug == "nomina" and feedback.nomination_set.first.share_nominator %} {% if ft.slug == "nomina" and feedback.nomination_set.first.share_nominator %}
<span class="bg-info"> OK to share name with nominee</span> <span class="bg-info"> OK to share name with nominee</span>

View file

@ -3,17 +3,24 @@
{% load origin %} {% load origin %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% load staticfiles %}
{% load nomcom_tags %} {% load nomcom_tags %}
{% block pagehead %}
<link rel="stylesheet" href="{% static 'select2/select2.css' %}">
<link rel="stylesheet" href="{% static 'select2-bootstrap-css/select2-bootstrap.min.css' %}">
{% endblock %}
{% block subtitle %} - Feeback pending{% endblock %} {% block subtitle %} - Feeback pending{% endblock %}
{% block morecss %}
.nominee_multi_select { resize: vertical; }
{% endblock %}
{% block nomcom_content %} {% block nomcom_content %}
{% origin %} {% origin %}
<h2>Feedback pending from email list</h2> <h2>Feedback pending from email list</h2>
{% if message %}
<p class="alert alert-{{ message.0 }}">{{ message.1 }}</p>
{% endif %}
{% if formset.forms %} {% if formset.forms %}
<form method="post"> <form method="post">
@ -145,9 +152,6 @@
{% buttons %} {% buttons %}
<input class="btn btn-primary" type="submit" value="Classify"> <input class="btn btn-primary" type="submit" value="Classify">
{% if default_type %}
<input class="btn btn-default" type="submit" name="move_to_default" value="Move all unclassified feedback to {{ default_type }}">
{% endif %}
{% endbuttons %} {% endbuttons %}
{% endif %} {% endif %}
</form> </form>
@ -157,3 +161,8 @@
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block js %}
<script src="{% static 'select2/select2.min.js' %}"></script>
<script src="{% static 'ietf/js/select2-field.js' %}"></script>
{% endblock %}

View file

@ -25,23 +25,7 @@
<dt>From</dt> <dt>From</dt>
<dd>{{ feedback.author|formatted_email|default:"Anonymous" }}</dd> <dd>{{ feedback.author|formatted_email|default:"Anonymous" }}</dd>
<dt>Date</dt> <dt>Date</dt>
<dd>{{ feedback.time|date:"Y-m-d" }})</dd> <dd>{{ feedback.time|date:"Y-m-d" }}</dd>
{% if ft.slug == "nomina" %}
{% for fn in feedback.nomination_set.all %}
{% if fn.candidate_name %}
<dt>Nominee</dt>
<dd>{{ fn.candidate_name }}</dd>
{% endif %}
{% if fn.candidate_phone %}
<dt>Nominee phone</dt>
<dd>{{ fn.candidate_phone }}</dd>
{% endif %}
{% endfor %}
{% endif %}
<dt>Positions</dt>
<dd>{{ feedback.positions.all|join:"," }}</dd>
<dt>Body</dt> <dt>Body</dt>
<dd class="pasted">{% decrypt feedback.comments request year 1 %}</dd> <dd class="pasted">{% decrypt feedback.comments request year 1 %}</dd>
</dl> </dl>

View file

@ -25,3 +25,5 @@ six>=1.8.0
wsgiref>=0.1.2 wsgiref>=0.1.2
xml2rfc>=2.5.0 xml2rfc>=2.5.0
django>=1.7.10,<1.8 django>=1.7.10,<1.8
factory-boy>=2.6.0
Unidecode>=0.4.18