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()
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.utils import merge_persons
parser = argparse.ArgumentParser()
parser.add_argument("source_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':
sys.exit()
# merge emails
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()
merge_persons(source,target,sys.stdout)

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>
</object>
<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="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:
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)
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),
group=mars_wg,
)
self.basea.documentauthor_set.create(author=Email.objects.create(address="basea_author@example.com"),order=1)
self.baseb = Document.objects.create(
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),
group=mars_wg,
)
self.baseb.documentauthor_set.create(author=Email.objects.create(address="baseb_author@example.com"),order=1)
self.replacea = Document.objects.create(
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),
group=mars_wg,
)
self.replacea.documentauthor_set.create(author=Email.objects.create(address="replacea_author@example.com"),order=1)
self.replaceboth = Document.objects.create(
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),
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.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.assertEqual(len(outbox), 1)
self.assertTrue('replacement status updated' in outbox[-1]['Subject'])
self.assertTrue('base-a@' in outbox[-1]['To'])
self.assertTrue('replace-a@' in outbox[-1]['To'])
self.assertTrue('replacea_author@' in outbox[-1]['To'])
self.assertTrue('basea_author@' in outbox[-1]['To'])
empty_outbox()
# 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-b').get_state().slug,'repl')
self.assertEqual(len(outbox), 1)
self.assertTrue('base-a@' in outbox[-1]['To'])
self.assertTrue('base-b@' in outbox[-1]['To'])
self.assertTrue('replace-both@' in outbox[-1]['To'])
self.assertTrue('basea_author@' in outbox[-1]['To'])
self.assertTrue('baseb_author@' in outbox[-1]['To'])
self.assertTrue('replaceboth_author@' in outbox[-1]['To'])
# Post that undoes replaceboth
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-b').get_state().slug,'expired')
self.assertEqual(len(outbox), 1)
self.assertTrue('base-a@' in outbox[-1]['To'])
self.assertTrue('base-b@' in outbox[-1]['To'])
self.assertTrue('replace-both@' in outbox[-1]['To'])
self.assertTrue('basea_author@' in outbox[-1]['To'])
self.assertTrue('baseb_author@' in outbox[-1]['To'])
self.assertTrue('replaceboth_author@' in outbox[-1]['To'])
# Post that undoes replacea
empty_outbox()
@ -1399,8 +1403,8 @@ class ChangeReplacesTests(TestCase):
r = self.client.post(url, dict(replaces=""))
self.assertEqual(r.status_code, 302)
self.assertEqual(Document.objects.get(name='draft-test-base-a').get_state().slug,'active')
self.assertTrue('base-a@' in outbox[-1]['To'])
self.assertTrue('replace-a@' in outbox[-1]['To'])
self.assertTrue('basea_author@' in outbox[-1]['To'])
self.assertTrue('replacea_author@' in outbox[-1]['To'])
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.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
# an import order issue to work out.
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"]),
"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"),
"Nomcom Chair": Q(person=person, name="chair", group__type="nomcom", group__state="active", 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": Q(person=person, 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__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", ),
"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": {
"order": 0,
"prefix": "charter",
"used": true,
"name": "Charter",
"desc": ""
@ -658,6 +659,7 @@
{
"fields": {
"order": 0,
"prefix": "agenda",
"used": true,
"name": "Agenda",
"desc": ""
@ -668,6 +670,7 @@
{
"fields": {
"order": 0,
"prefix": "minutes",
"used": true,
"name": "Minutes",
"desc": ""
@ -678,6 +681,7 @@
{
"fields": {
"order": 0,
"prefix": "slides",
"used": true,
"name": "Slides",
"desc": ""
@ -688,6 +692,7 @@
{
"fields": {
"order": 0,
"prefix": "draft",
"used": true,
"name": "Draft",
"desc": ""
@ -698,6 +703,7 @@
{
"fields": {
"order": 0,
"prefix": "liai-att",
"used": true,
"name": "Liaison Attachment",
"desc": ""
@ -708,6 +714,7 @@
{
"fields": {
"order": 0,
"prefix": "conflict-review",
"used": true,
"name": "Conflict Review",
"desc": ""
@ -718,6 +725,7 @@
{
"fields": {
"order": 0,
"prefix": "status-change",
"used": true,
"name": "Status Change",
"desc": ""
@ -728,6 +736,7 @@
{
"fields": {
"order": 0,
"prefix": "",
"used": false,
"name": "Shepherd's writeup",
"desc": ""
@ -738,6 +747,7 @@
{
"fields": {
"order": 0,
"prefix": "",
"used": false,
"name": "Liaison",
"desc": ""
@ -748,6 +758,7 @@
{
"fields": {
"order": 0,
"prefix": "recording",
"used": true,
"name": "Recording",
"desc": ""
@ -758,6 +769,7 @@
{
"fields": {
"order": 0,
"prefix": "bluesheets",
"used": true,
"name": "Bluesheets",
"desc": ""
@ -4551,6 +4563,14 @@
"model": "mailtrigger.recipient",
"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": {
"template": null,
@ -4711,6 +4731,14 @@
"model": "mailtrigger.recipient",
"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": {
"template": "The IESG <iesg@ietf.org>",
@ -5316,11 +5344,7 @@
"fields": {
"cc": [],
"to": [
"doc_authors",
"doc_group_chairs",
"doc_group_responsible_directors",
"doc_notify",
"doc_shepherd"
"doc_authors_expanded"
],
"desc": "Recipients when what a document replaces or is replaced by changes"
},
@ -5708,6 +5732,17 @@
"model": "mailtrigger.mailtrigger",
"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": {
"cc": [
@ -5853,7 +5888,7 @@
"submission_group_mail_list"
],
"to": [
"ietf_announce"
"id_announce"
],
"desc": "Recipients for the announcement of a successfully submitted draft"
},

View file

@ -23,7 +23,7 @@ class NomineePositionAdmin(admin.ModelAdmin):
class PositionAdmin(admin.ModelAdmin):
list_display = ('name', 'nomcom', 'is_open', 'incumbent')
list_display = ('name', 'nomcom', 'is_open')
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.contrib.formtools.preview import FormPreview, AUTO_ID
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.shortcuts import render_to_response
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.group.models import Group, Role
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,
Position, Feedback, ReminderDates )
from ietf.nomcom.utils import (NOMINATION_RECEIPT_TEMPLATE, FEEDBACK_RECEIPT_TEMPLATE,
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.fields import SearchableEmailField
from ietf.person.fields import SearchableEmailField, SearchablePersonField, SearchablePersonsField
from ietf.utils.fields import MultiEmailField
from ietf.utils.mail import send_mail
from ietf.mailtrigger.utils import gather_address_lists
import debug # pyflakes:ignore
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')
results = []
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:
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
super(PositionNomineeField, self).__init__(*args, **kwargs)
@ -83,35 +93,10 @@ class MultiplePositionNomineeField(forms.MultipleChoiceField, PositionNomineeFie
return result
class BaseNomcomForm(object):
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):
class EditMembersForm(forms.Form):
members = MultiEmailField(label="Members email", required=False, widget=forms.Textarea)
fieldsets = [('Members', ('members',))]
class EditMembersFormPreview(FormPreview):
form_template = 'nomcom/edit_members.html'
preview_template = 'nomcom/edit_members_preview.html'
@ -208,10 +193,8 @@ class EditMembersFormPreview(FormPreview):
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):
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)
class MergeForm(BaseNomcomForm, forms.Form):
class MergeForm(forms.Form):
secondary_emails = MultiEmailField(label="Secondary email addresses",
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)
primary_email = forms.EmailField(label="Primary email address",
widget=forms.TextInput(attrs={'size': '40'}))
fieldsets = [('Emails', ('primary_email', 'secondary_emails'))]
primary_person = SearchablePersonField(help_text="Select the person you want the datatracker to keep")
duplicate_persons = SearchablePersonsField(help_text="Select all the duplicates that should be merged into the primary person record")
def __init__(self, *args, **kwargs):
self.nomcom = kwargs.pop('nomcom', None)
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):
primary_email = self.cleaned_data.get("primary_email")
secondary_emails = self.cleaned_data.get("secondary_emails")
if primary_email and secondary_emails:
if primary_email in secondary_emails:
msg = "Primary and secondary email address must be differents"
self._errors["primary_email"] = self.error_class([msg])
primary_person = self.cleaned_data.get("primary_person")
duplicate_persons = self.cleaned_data.get("duplicate_persons")
if primary_person and duplicate_persons:
if primary_person in duplicate_persons:
msg = "The primary person must not also be listed as a duplicate person"
self._errors["primary_person"] = self.error_class([msg])
return self.cleaned_data
def save(self):
primary_email = self.cleaned_data.get("primary_email")
secondary_emails = self.cleaned_data.get("secondary_emails")
primary_person = self.cleaned_data.get("primary_person")
duplicate_persons = self.cleaned_data.get("duplicate_persons")
primary_nominee = Nominee.objects.get_by_nomcom(self.nomcom).get(email__address=primary_email)
while primary_nominee.duplicated:
primary_nominee = primary_nominee.duplicated
secondary_nominees = Nominee.objects.get_by_nomcom(self.nomcom).filter(email__address__in=secondary_emails)
for nominee in secondary_nominees:
# 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
subject = "Request to merge Person records"
from_email = settings.NOMCOM_FROM_EMAIL
(to_email, cc) = gather_address_lists('person_merge_requested')
context = {'primary_person':primary_person, 'duplicate_persons':duplicate_persons}
send_mail(None, to_email, from_email, subject, 'nomcom/merge_request.txt', context, cc=cc)
if primary_nominee_position:
# if already a nomineeposition object for a position and nominee,
# update the nomineepostion of primary nominee with the state
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",
class NominateForm(forms.ModelForm):
searched_email = SearchableEmailField(only_users=False)
qualifications = forms.CharField(label="Candidate's qualifications for the position",
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'.",
required=False)
fieldsets = [('Candidate Nomination', ('share_nominator','position', 'candidate_name',
'candidate_email', 'candidate_phone', 'comments', 'confirmation'))]
def __init__(self, *args, **kwargs):
self.nomcom = kwargs.pop('nomcom', None)
self.user = kwargs.pop('user', None)
@ -339,19 +264,16 @@ class NominateForm(BaseNomcomForm, forms.ModelForm):
super(NominateForm, self).__init__(*args, **kwargs)
fieldset = ['share_nominator',
'position',
'candidate_name',
'candidate_email', 'candidate_phone',
'comments']
new_person_url_name = 'nomcom_%s_nominate_newperson' % ('public' if self.public else 'private' )
self.fields['searched_email'].label = 'Candidate email'
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()})
self.fields['nominator_email'].label = 'Nominator email'
if self.nomcom:
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:
fieldset = ['nominator_email'] + fieldset
self.fields.pop('confirmation')
author = get_user_email(self.user)
if author:
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
nominating this candidate."""
else:
fieldset.append('confirmation')
self.fields.pop('nominator_email')
self.fieldsets = [('Candidate Nomination', fieldset)]
def save(self, commit=True):
# Create nomination
nomination = super(NominateForm, 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']
searched_email = self.cleaned_data['searched_email']
position = self.cleaned_data['position']
comments = self.cleaned_data['comments']
confirmation = self.cleaned_data['confirmation']
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
nomination.candidate_name = searched_email.person.plain_name()
nomination.candidate_email = searched_email.address
author = None
if self.public:
author = get_user_email(self.user)
@ -386,11 +309,11 @@ class NominateForm(BaseNomcomForm, forms.ModelForm):
if nominator_email:
emails = Email.objects.filter(address=nominator_email)
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
feedback = Feedback.objects.create(nomcom=self.nomcom,
comments=comments,
comments=qualifications,
type=FeedbackTypeName.objects.get(slug='nomina'),
user=self.user)
feedback.positions.add(position)
@ -416,7 +339,117 @@ class NominateForm(BaseNomcomForm, forms.ModelForm):
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': 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}
path = nomcom_template_path + NOMINATION_RECEIPT_TEMPLATE
send_mail(None, to_email, from_email, subject, path, context, cc=cc)
@ -426,22 +459,15 @@ class NominateForm(BaseNomcomForm, forms.ModelForm):
class Meta:
model = Nomination
fields = ('share_nominator', 'position', 'nominator_email', 'candidate_name',
'candidate_email', 'candidate_phone')
'candidate_email', 'candidate_phone', 'qualifications', 'confirmation')
class FeedbackForm(BaseNomcomForm, forms.ModelForm):
position_name = forms.CharField(label='Position',
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')
class FeedbackForm(forms.ModelForm):
nominator_email = forms.CharField(label='Commenter email',required=False)
comments = forms.CharField(label='Comments on this nominee',
comments = forms.CharField(label='Comments',
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'.",
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).',
required=False)
def __init__(self, *args, **kwargs):
@ -453,72 +479,44 @@ class FeedbackForm(BaseNomcomForm, forms.ModelForm):
super(FeedbackForm, self).__init__(*args, **kwargs)
readonly_fields = ['position_name',
'nominee_name',
'nominee_email']
fieldset = ['position_name',
'nominee_name',
'nominee_email',
'nominator_email',
'comments']
author = get_user_email(self.user)
if self.public:
readonly_fields += ['nominator_email']
fieldset.append('confirmation')
self.fields.pop('nominator_email')
else:
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,
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'].required = False
author = get_user_email(self.user)
self.fields['confirmation'].label = 'Email these comments in cleartext to the provided commenter email address'
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):
if not NomineePosition.objects.accepted().filter(nominee=self.nominee,
position=self.position):
msg = "There isn't a accepted nomination for %s on the %s position" % (self.nominee, self.position)
self._errors["nominee_email"] = self.error_class([msg])
self._errors["comments"] = self.error_class([msg])
return self.cleaned_data
def save(self, commit=True):
feedback = super(FeedbackForm, self).save(commit=False)
confirmation = self.cleaned_data['confirmation']
comments = self.cleaned_data['comments']
nominator_email = self.cleaned_data['nominator_email']
nomcom_template_path = '/nomcom/%s/' % self.nomcom.group.acronym
author = None
if self.public:
author = get_user_email(self.user)
else:
nominator_email = self.cleaned_data['nominator_email']
if nominator_email:
emails = Email.objects.filter(address=nominator_email)
author = emails and emails[0] or None
if author:
feedback.author = author
feedback.author = author.address
feedback.nomcom = self.nomcom
feedback.user = self.user
@ -541,18 +539,16 @@ class FeedbackForm(BaseNomcomForm, forms.ModelForm):
class Meta:
model = Feedback
fields = ('nominee_name',
'nominee_email',
fields = (
'nominator_email',
'comments',
'confirmation',
'comments')
)
class FeedbackEmailForm(BaseNomcomForm, forms.Form):
class FeedbackEmailForm(forms.Form):
email_text = forms.CharField(label='Email text', widget=forms.Textarea())
fieldsets = [('Feedback email', ('email_text',))]
def __init__(self, *args, **kwargs):
self.nomcom = kwargs.pop('nomcom', None)
super(FeedbackEmailForm, self).__init__(*args, **kwargs)
@ -560,12 +556,10 @@ class FeedbackEmailForm(BaseNomcomForm, forms.Form):
def save(self, commit=True):
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',
widget=forms.Textarea())
fieldsets = [('New questionnaire response', ('nominee', 'comments'))]
def __init__(self, *args, **kwargs):
self.nomcom = kwargs.pop('nomcom', None)
self.user = kwargs.pop('user', None)
@ -594,21 +588,14 @@ class QuestionnaireForm(BaseNomcomForm, forms.ModelForm):
model = Feedback
fields = ( 'comments', )
class NomComTemplateForm(BaseNomcomForm, DBTemplateForm):
class NomComTemplateForm(DBTemplateForm):
content = forms.CharField(label="Text", widget=forms.Textarea(attrs={'cols': '120', 'rows':'40', }))
fieldsets = [('Template content', ('content', )), ]
class PositionForm(BaseNomcomForm, forms.ModelForm):
fieldsets = [('Position', ('name', 'description',
'is_open', 'incumbent'))]
incumbent = SearchableEmailField(required=False)
class PositionForm(forms.ModelForm):
class Meta:
model = Position
fields = ('name', 'description', 'is_open', 'incumbent')
fields = ('name', 'is_open')
def __init__(self, *args, **kwargs):
self.nomcom = kwargs.pop('nomcom', None)
@ -619,12 +606,10 @@ class PositionForm(BaseNomcomForm, forms.ModelForm):
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)
fieldsets = [('Private key', ('key',))]
def clean_key(self):
key = self.cleaned_data.get('key', None)
if not key:
@ -635,7 +620,7 @@ class PrivateKeyForm(BaseNomcomForm, forms.Form):
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)
@ -643,13 +628,6 @@ class PendingFeedbackForm(BaseNomcomForm, forms.ModelForm):
model = Feedback
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):
self.nomcom = nomcom
self.user = user
@ -665,17 +643,6 @@ class PendingFeedbackForm(BaseNomcomForm, forms.ModelForm):
feedback.save()
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 Meta:
@ -711,17 +678,38 @@ class MutableFeedbackForm(forms.ModelForm):
if self.feedback_type.slug != 'nomina':
self.fields['nominee'] = MultiplePositionNomineeField(nomcom=self.nomcom,
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.')
else:
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['candidate_email'] = forms.EmailField(label="Candidate email")
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_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)
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):
feedback = super(MutableFeedbackForm, self).save(commit=False)
if self.instance.type.slug == 'nomina':
searched_email = self.cleaned_data['searched_email']
candidate_email = self.cleaned_data['candidate_email']
candidate_name = self.cleaned_data['candidate_name']
candidate_phone = self.cleaned_data['candidate_phone']
@ -733,7 +721,10 @@ class MutableFeedbackForm(forms.ModelForm):
emails = Email.objects.filter(address=nominator_email)
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.positions.add(position)
Nomination.objects.create(
@ -768,41 +759,25 @@ FullFeedbackFormSet = forms.modelformset_factory(
class EditNomineeForm(forms.ModelForm):
nominee_email = forms.EmailField(label="Nominee email",
widget=forms.TextInput(attrs={'size': '40'}))
nominee_email = forms.ModelChoiceField(queryset=Email.objects.none(),empty_label=None)
def __init__(self, *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):
nominee = super(EditNomineeForm, self).save(commit=False)
nominee_email = self.cleaned_data.get("nominee_email")
if nominee_email != nominee.email.address:
# create a new nominee with the new email
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.email = nominee_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
class Meta:
model = Nominee
fields = ('nominee_email',)
def clean_nominee_email(self):
nominee_email = self.cleaned_data['nominee_email']
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
class NominationResponseCommentForm(forms.Form):
comments = forms.CharField(widget=forms.Textarea,required=False,help_text="Any comments provided will be encrytped and will only be visible to the NomCom.")

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.contrib.auth.models import User
from django.template.loader import render_to_string
from django.template.defaultfilters import linebreaks
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.name.models import NomineePositionStateName, FeedbackTypeName
from ietf.dbtemplate.models import DBTemplate
@ -66,6 +67,14 @@ class NomCom(models.Model):
if created:
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):
nomcom = kwargs.get('instance', None)
@ -102,6 +111,7 @@ class Nomination(models.Model):
class Nominee(models.Model):
email = models.ForeignKey(Email)
person = models.ForeignKey(Person, blank=True, null=True)
nominee_position = models.ManyToManyField('Position', through='NomineePosition')
duplicated = models.ForeignKey('Nominee', blank=True, null=True)
nomcom = models.ForeignKey('NomCom')
@ -118,6 +128,12 @@ class Nominee(models.Model):
else:
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):
@ -150,12 +166,10 @@ class NomineePosition(models.Model):
class Position(models.Model):
nomcom = models.ForeignKey('NomCom')
name = models.CharField(verbose_name='Name', max_length=255)
description = models.TextField(verbose_name='Description')
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"')
requirement = models.ForeignKey(DBTemplate, related_name='requirement', 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)
incumbent = models.ForeignKey(Email, null=True, blank=True)
objects = PositionManager()
@ -189,7 +203,10 @@ class Position(models.Model):
return render_to_string(self.questionnaire.path, {'position': 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):
@ -211,4 +228,7 @@ class Feedback(models.Model):
class Meta:
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())
from ietf.person.resources import EmailResource
from ietf.dbtemplate.resources import DBTemplateResource
class PositionResource(ModelResource):
nomcom = ToOneField(NomComResource, 'nomcom')
requirement = ToOneField(DBTemplateResource, 'requirement', null=True)
questionnaire = ToOneField(DBTemplateResource, 'questionnaire', null=True)
incumbent = ToOneField(EmailResource, 'incumbent', null=True)
class Meta:
queryset = Position.objects.all()
serializer = api.Serializer()
@ -39,12 +37,10 @@ class PositionResource(ModelResource):
filtering = {
"id": ALL,
"name": ALL,
"description": ALL,
"is_open": ALL,
"nomcom": ALL_WITH_RELATIONS,
"requirement": ALL_WITH_RELATIONS,
"questionnaire": ALL_WITH_RELATIONS,
"incumbent": ALL_WITH_RELATIONS,
}
api.nomcom.register(PositionResource())
@ -148,3 +144,17 @@ class NominationResource(ModelResource):
}
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.log import log
from ietf.ietfauth.utils import has_role
from ietf.doc.templatetags.ietf_filters import wrap_text
from ietf.person.models import Person
from ietf.nomcom.models import Feedback
from ietf.nomcom.utils import get_nomcom_by_year, get_user_email, retrieve_nomcom_private_key
from ietf.nomcom.utils import get_nomcom_by_year, retrieve_nomcom_private_key
import debug # pyflakes:ignore
register = template.Library()
@register.filter
def is_chair(user, year):
def is_chair_or_advisor(user, year):
if not user or not year:
return False
nomcom = get_nomcom_by_year(year=year)
if has_role(user, "Secretariat"):
return True
return nomcom.group.has_role(user, "chair")
return nomcom.group.has_role(user, ["chair","advisor"])
@register.filter
def has_publickey(nomcom):
return nomcom and nomcom.public_key and True or False
@register.simple_tag
def add_num_nominations(user, position, nominee):
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
def lookup(container,key):
return container and container.get(key,None)
@register.filter
def formatted_email(address):

View file

@ -21,19 +21,19 @@ SECRETARIAT_USER = 'secretary'
EMAIL_DOMAIN = '@example.com'
NOMCOM_YEAR = "2013"
POSITIONS = {
"GEN": "IETF Chair/Gen AD",
"APP": "APP Area Director",
"INT": "INT Area Director",
"OAM": "OPS Area Director",
"OPS": "OPS Area Director",
"RAI": "RAI Area Director",
"RTG": "RTG Area Director",
"SEC": "SEC Area Director",
"TSV": "TSV Area Director",
"IAB": "IAB Member",
"IAOC": "IAOC Member",
}
POSITIONS = [
"GEN",
"APP",
"INT",
"OAM",
"OPS",
"RAI",
"RTG",
"SEC",
"TSV",
"IAB",
"IAOC"
]
def generate_cert():
@ -127,12 +127,10 @@ def nomcom_test_data():
nominee, _ = Nominee.objects.get_or_create(email=email, nomcom=nomcom)
# positions
for name, description in POSITIONS.iteritems():
for name in POSITIONS:
position, created = Position.objects.get_or_create(nomcom=nomcom,
name=name,
description=description,
is_open=True,
incumbent=email)
is_open=True)
ChangeStateGroupEvent.objects.get_or_create(group=group,
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.views.generic import TemplateView
from ietf.nomcom.forms import EditMembersForm, EditMembersFormPreview
@ -8,7 +7,9 @@ urlpatterns = patterns('ietf.nomcom.views',
url(r'^ann/$', 'announcements'),
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/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/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-email/$', 'private_feedback_email', name='nomcom_private_feedback_email'),
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/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/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/(?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'),
@ -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})/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/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'),
)

View file

@ -29,7 +29,7 @@ import debug # pyflakes:ignore
MAIN_NOMCOM_TEMPLATE_PATH = '/nomcom/defaults/'
QUESTIONNAIRE_TEMPLATE = 'position/questionnaire.txt'
HEADER_QUESTIONNAIRE_TEMPLATE = 'position/header_questionnaire.txt'
REQUIREMENTS_TEMPLATE = 'position/requirements.txt'
REQUIREMENTS_TEMPLATE = 'position/requirements'
HOME_TEMPLATE = 'home.rst'
INEXISTENT_PERSON_TEMPLATE = 'email/inexistent_person.txt'
NOMINEE_EMAIL_TEMPLATE = 'email/new_nominee.txt'
@ -53,7 +53,7 @@ def get_nomcom_by_year(year):
from ietf.nomcom.models import NomCom
return get_object_or_404(NomCom,
group__acronym__icontains=year,
group__state__slug='active')
)
def get_year_by_nomcom(nomcom):
@ -271,42 +271,22 @@ def send_reminder_to_nominees(nominees,type):
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
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
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:
nominee = nominee.duplicated
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:
# send email to nominee
subject = 'IETF Nomination Information'
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
today = datetime.date.today().strftime('%Y%m%d')
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,
hash))
context = {'nominee': email.person.name,
context = {'nominee': nominee.person.name,
'position': position.name,
'domain': domain,
'accept_url': accept_url,
@ -338,8 +318,8 @@ def get_or_create_nominee(nomcom, candidate_name, candidate_email, position, aut
if nomcom.send_questionnaire:
subject = '%s Questionnaire' % position
from_email = settings.NOMCOM_FROM_EMAIL
(to_email, cc) = gather_address_lists('nomcom_questionnaire',nominee=email.address)
context = {'nominee': email.person.name,
(to_email, cc) = gather_address_lists('nomcom_questionnaire',nominee=nominee.email.address)
context = {'nominee': nominee.person.name,
'position': position.name}
path = '%s%d/%s' % (nomcom_template_path,
position.id, HEADER_QUESTIONNAIRE_TEMPLATE)
@ -353,8 +333,8 @@ def get_or_create_nominee(nomcom, candidate_name, candidate_email, position, aut
subject = 'Nomination Information'
from_email = settings.NOMCOM_FROM_EMAIL
(to_email, cc) = gather_address_lists('nomination_received',nomcom=nomcom)
context = {'nominee': email.person.name,
'nominee_email': email.address,
context = {'nominee': nominee.person.name,
'nominee_email': nominee.email.address,
'position': position.name}
if author:
@ -369,6 +349,28 @@ def get_or_create_nominee(nomcom, candidate_name, candidate_email, position, aut
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"):
"""Decode the specified header"""

View file

@ -1,9 +1,10 @@
import datetime
import re
from collections import OrderedDict
from collections import OrderedDict, Counter
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import AnonymousUser
from django.contrib import messages
from django.core.urlresolvers import reverse
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.views import template_edit
from ietf.dbtemplate.views import template_edit, template_show
from ietf.name.models import NomineePositionStateName, FeedbackTypeName
from ietf.group.models import Group, GroupEvent
from ietf.message.models import Message
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,
PrivateKeyForm, EditNomcomForm, EditNomineeForm,
PendingFeedbackForm, ReminderDatesForm, FullFeedbackFormSet,
FeedbackEmailForm)
from ietf.nomcom.models import Position, NomineePosition, Nominee, Feedback, NomCom, ReminderDates
FeedbackEmailForm, NominationResponseCommentForm)
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,
get_hash_nominee_position, send_reminder_to_nominees,
HOME_TEMPLATE, NOMINEE_ACCEPT_REMINDER_TEMPLATE,NOMINEE_QUESTIONNAIRE_REMINDER_TEMPLATE)
from ietf.ietfauth.utils import role_required
import debug # pyflakes:ignore
def index(request):
nomcom_list = Group.objects.filter(type__slug='nomcom').order_by('acronym')
for nomcom in nomcom_list:
year = nomcom.acronym[6:]
try:
year = int(year)
except ValueError:
year = None
year = int(nomcom.acronym[6:])
nomcom.year = year
nomcom.label = "%s/%s" % (year, year+1)
if year in [ 2005, 2006, 2007, 2008, 2009, 2010 ]:
@ -107,11 +106,11 @@ def announcements(request):
@role_required("Nomcom")
def private_key(request, year):
nomcom = get_nomcom_by_year(year)
message = 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:
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, )))
if request.method == 'POST':
@ -126,7 +125,6 @@ def private_key(request, year):
'year': year,
'back_url': back_url,
'form': form,
'message': message,
'selected': 'private_key'}, RequestContext(request))
@ -135,23 +133,25 @@ def private_index(request, year):
nomcom = get_nomcom_by_year(year)
all_nominee_positions = NomineePosition.objects.get_by_nomcom(nomcom).not_duplicated()
is_chair = nomcom.group.has_role(request.user, "chair")
message = None
if is_chair and request.method == 'POST':
if nomcom.group.state_id != 'active':
messages.warning(request, "This nomcom is not active. Request administrative assistance if Nominee state needs to change.")
else:
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')
message = ('success', 'The selected nominations have been set as accepted')
messages.success(request,'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')
messages.success(request,'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')
messages.success(request,'The selected nominations have been set as pending')
else:
message = ('warning', "Please, select some nominations to work with")
messages.warning(request, "Please, select some nominations to work with")
filters = {}
questionnaire_state = "questionnaire"
@ -193,7 +193,7 @@ def private_index(request, year):
'selected_position': selected_position and int(selected_position) or None,
'selected': 'index',
'is_chair': is_chair,
'message': message}, RequestContext(request))
}, RequestContext(request))
@role_required("Nomcom Chair", "Nomcom Advisor")
@ -201,6 +201,16 @@ def send_reminder_mail(request, year, type):
nomcom = get_nomcom_by_year(year)
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':
interesting_state = 'pending'
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 = 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 = nominees.filter(id__in=selected_nominees)
if selected_nominees:
addrs = send_reminder_to_nominees(selected_nominees,type)
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:
message = ('warning', 'No messages were sent.')
messages.warning(request, 'No messages were sent.')
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',
{'nomcom': nomcom,
'year': year,
@ -249,18 +259,23 @@ def send_reminder_mail(request, year, type):
'selected': selected_tab,
'reminder_description': reminder_description,
'state_description': state_description,
'message': message}, RequestContext(request))
'is_chair_task' : True,
}, RequestContext(request))
@role_required("Nomcom Chair", "Nomcom Advisor")
def private_merge(request, year):
nomcom = get_nomcom_by_year(year)
message = None
if nomcom.group.state_id != 'active':
messages.warning(request, "This Nomcom is not active.")
form = None
else:
if request.method == 'POST':
form = MergeForm(request.POST, nomcom=nomcom)
form = MergeForm(request.POST, nomcom=nomcom )
if form.is_valid():
form.save()
message = ('success', 'The emails have been unified')
messages.success(request, 'A merge request has been sent to the secretariat.')
return redirect('nomcom_private_index',year=year)
else:
form = MergeForm(nomcom=nomcom)
@ -268,8 +283,9 @@ def private_merge(request, year):
{'nomcom': nomcom,
'year': year,
'form': form,
'message': message,
'selected': 'merge'}, RequestContext(request))
'selected': 'merge',
'is_chair_task' : True,
}, RequestContext(request))
def requirements(request, year):
@ -294,15 +310,24 @@ def questionnaires(request, year):
@login_required
def public_nominate(request, year):
return nominate(request, year, True)
return nominate(request=request, year=year, public=True, newperson=False)
@role_required("Nomcom")
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)
has_publickey = nomcom.public_key and True or False
if public:
@ -311,31 +336,43 @@ def nominate(request, year, public):
template = 'nomcom/private_nominate.html'
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,
{'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,
'selected': 'nominate'}, RequestContext(request))
message = None
if request.method == 'POST':
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():
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.')
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:
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,
{'form': form,
'message': message,
'nomcom': nomcom,
'year': year,
'selected': 'nominate'}, RequestContext(request))
@login_required
def public_feedback(request, year):
return feedback(request, year, True)
@ -349,55 +386,61 @@ def private_feedback(request, year):
def feedback(request, year, public):
nomcom = get_nomcom_by_year(year)
has_publickey = nomcom.public_key and True or False
submit_disabled = True
nominee = None
position = None
if nomcom.group.state_id != 'conclude':
selected_nominee = request.GET.get('nominee')
selected_position = request.GET.get('position')
if selected_nominee and selected_position:
nominee = get_object_or_404(Nominee, id=selected_nominee)
position = get_object_or_404(Position, id=selected_position)
submit_disabled = False
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:
base_template = "nomcom/nomcom_public_base.html"
else:
base_template = "nomcom/nomcom_private_base.html"
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', {
'message': message,
'nomcom': nomcom,
'year': year,
'selected': 'feedback',
'counts' : counts,
'base_template': base_template
})
message = None
if request.method == 'POST':
if nominee and position and request.method == 'POST':
form = FeedbackForm(data=request.POST,
nomcom=nomcom, user=request.user,
public=public, position=position, nominee=nominee)
if form.is_valid():
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,
position=position, nominee=nominee)
else:
form = FeedbackForm(nomcom=nomcom, user=request.user, public=public,
position=position, nominee=nominee)
form = None
return render(request, 'nomcom/feedback.html', {
'form': form,
'message': message,
'nomcom': nomcom,
'year': year,
'positions': positions,
'submit_disabled': submit_disabled,
'selected': 'feedback',
'counts': counts,
'base_template': base_template
})
@ -406,16 +449,24 @@ def feedback(request, year, public):
def private_feedback_email(request, year):
nomcom = get_nomcom_by_year(year)
has_publickey = nomcom.public_key and True or False
message = None
template = 'nomcom/private_feedback_email.html'
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.")
nomcom_ready = False
elif nomcom.group.state_id != 'active':
messages.warning(request, "This Nomcom is not active, and is not accepting feedback email.")
nomcom_ready = False
else:
nomcom_ready = True
if not nomcom_ready:
return render_to_response(template,
{'message': message,
'nomcom': nomcom,
{'nomcom': nomcom,
'year': year,
'selected': 'feedback_email'}, RequestContext(request))
'selected': 'feedback_email',
'is_chair_task' : True,
}, RequestContext(request))
form = FeedbackEmailForm(nomcom=nomcom)
@ -425,38 +476,44 @@ def private_feedback_email(request, year):
if form.is_valid():
form.save()
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,
{'form': form,
'message': message,
'nomcom': nomcom,
'year': year,
'selected': 'feedback_email'}, RequestContext(request))
@role_required("Nomcom Chair", "Nomcom Advisor")
def private_questionnaire(request, year):
nomcom = get_nomcom_by_year(year)
has_publickey = nomcom.public_key and True or False
message = None
questionnaire_response = None
template = 'nomcom/private_questionnaire.html'
if not has_publickey:
message = ('warning', "This Nomcom is not yet accepting questionnaires")
messages.warning(request, "This Nomcom is not yet accepting questionnaires.")
nomcom_ready = False
elif nomcom.group.state_id != 'active':
messages.warning(request, "This Nomcom is not active, and is not accepting questionnaires.")
nomcom_ready = False
else:
nomcom_ready = True
if not nomcom_ready:
return render_to_response(template,
{'message': message,
'nomcom': nomcom,
{'nomcom': nomcom,
'year': year,
'selected': 'questionnaire'}, RequestContext(request))
'selected': 'questionnaire',
'is_chair_task' : True,
}, RequestContext(request))
if request.method == 'POST':
form = QuestionnaireForm(data=request.POST,
nomcom=nomcom, user=request.user)
if form.is_valid():
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']
form = QuestionnaireForm(nomcom=nomcom, user=request.user)
else:
@ -465,7 +522,6 @@ def private_questionnaire(request, year):
return render_to_response(template,
{'form': form,
'questionnaire_response': questionnaire_response,
'message': message,
'nomcom': nomcom,
'year': year,
'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)
if expiration_days:
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")
need_confirmation = True
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)
if nominee_position.state.slug != "pending":
return HttpResponseForbidden("The nomination already was %s" % nominee_position.state)
state = get_object_or_404(NomineePositionStateName, slug=state)
message = ('warning',
("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))
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))
if request.method == 'POST':
form = NominationResponseCommentForm(request.POST)
if form.is_valid():
nominee_position.state = state
nominee_position.save()
need_confirmation = False
message = message = ('success', 'Your nomination on %s has been set as %s' % (nominee_position.position.name,
state.name))
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',
{'message': message,
'nomcom': nomcom,
{'nomcom': nomcom,
'year': year,
'nominee_position': nominee_position,
'state': state,
'need_confirmation': need_confirmation,
'selected': 'feedback'}, RequestContext(request))
'selected': 'feedback',
'form': form }, RequestContext(request))
@role_required("Nomcom")
@ -521,10 +594,36 @@ def view_feedback(request, year):
feedback_types.append(ft)
else:
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:
nominee_feedback = [(ft.name, nominee.feedback_set.by_type(ft.slug).count()) for ft in feedback_types]
nominees_feedback.update({nominee: nominee_feedback})
nominee.staterank = nominee_staterank(nominee)
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]
return render_to_response('nomcom/view_feedback.html',
@ -542,33 +641,17 @@ def view_feedback(request, year):
@nomcom_private_key_required
def view_feedback_pending(request, year):
nomcom = get_nomcom_by_year(year)
if nomcom.group.state_id == 'conclude':
return HttpResponseForbidden("This nomcom is concluded.")
extra_ids = None
message = None
for message in messages.get_messages(request):
message = ('success', message.message)
FeedbackFormSet = modelformset_factory(Feedback,
form=PendingFeedbackForm,
extra=0)
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
if request.method == 'POST' and request.POST.get('move_to_default'):
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'):
if request.method == 'POST' and request.POST.get('end'):
extra_ids = request.POST.get('extra_ids', None)
extra_step = True
formset = FullFeedbackFormSet(request.POST)
@ -623,7 +706,7 @@ def view_feedback_pending(request, year):
for form in formset.forms:
form.set_nomcom(nomcom, request.user, extra)
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:
messages.success(request, 'Feedback saved')
return redirect('nomcom_view_feedback_pending', year=year)
@ -644,13 +727,13 @@ def view_feedback_pending(request, year):
{'year': year,
'selected': 'feedback_pending',
'formset': formset,
'message': message,
'extra_step': extra_step,
'default_type': default_type,
'type_dict': type_dict,
'extra_ids': extra_ids,
'types': FeedbackTypeName.objects.all().order_by('pk'),
'nomcom': nomcom}, RequestContext(request))
'nomcom': nomcom,
'is_chair_task' : True,
}, RequestContext(request))
@role_required("Nomcom")
@ -676,11 +759,19 @@ def view_feedback_nominee(request, year, nominee_id):
nominee = get_object_or_404(Nominee, id=nominee_id)
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',
{'year': year,
'selected': 'view_feedback',
'nominee': nominee,
'feedback_types': feedback_types,
'last_seen_time' : last_seen_time,
'nomcom': nomcom}, RequestContext(request))
@ -688,14 +779,14 @@ def view_feedback_nominee(request, year, nominee_id):
def edit_nominee(request, year, nominee_id):
nomcom = get_nomcom_by_year(year)
nominee = get_object_or_404(Nominee, id=nominee_id)
message = None
if request.method == 'POST':
form = EditNomineeForm(request.POST,
instance=nominee)
if form.is_valid():
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:
form = EditNomineeForm(instance=nominee)
@ -704,8 +795,9 @@ def edit_nominee(request, year, nominee_id):
'selected': 'index',
'nominee': nominee,
'form': form,
'message': message,
'nomcom': nomcom}, RequestContext(request))
'nomcom': nomcom,
'is_chair_task' : True,
}, RequestContext(request))
@role_required("Nomcom Chair", "Nomcom Advisor")
@ -713,14 +805,18 @@ def edit_nomcom(request, year):
nomcom = get_nomcom_by_year(year)
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:
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,
model=ReminderDates,
form=ReminderDatesForm)
if request.method == 'POST':
if nomcom.group.state_id=='conclude':
return HttpResponseForbidden('This nomcom is closed.')
formset = ReminderDateInlineFormSet(request.POST, instance=nomcom)
form = EditNomcomForm(request.POST,
request.FILES,
@ -729,7 +825,7 @@ def edit_nomcom(request, year):
form.save()
formset.save()
formset = ReminderDateInlineFormSet(instance=nomcom)
message = ('success', 'The nomcom has been changed')
messages.success(request, 'The nomcom has been changed')
else:
formset = ReminderDateInlineFormSet(instance=nomcom)
form = EditNomcomForm(instance=nomcom)
@ -738,25 +834,11 @@ def edit_nomcom(request, year):
{'form': form,
'formset': formset,
'nomcom': nomcom,
'message': message,
'year': year,
'selected': 'edit_nomcom'}, 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,
})
'is_chair_task' : True,
}, RequestContext(request))
@role_required("Nomcom Chair", "Nomcom Advisor")
@ -770,7 +852,9 @@ def list_templates(request, year):
'positions': positions,
'year': year,
'selected': 'edit_templates',
'nomcom': nomcom}, RequestContext(request))
'nomcom': nomcom,
'is_chair_task' : True,
}, RequestContext(request))
@role_required("Nomcom Chair", "Nomcom Advisor")
@ -778,29 +862,44 @@ def edit_template(request, year, template_id):
nomcom = get_nomcom_by_year(year)
return_url = request.META.get('HTTP_REFERER', None)
if nomcom.group.state_id=='conclude':
return template_show(request, nomcom.group.acronym, template_id,
base_template='nomcom/show_template.html',
extra_context={'year': year,
'return_url': return_url,
'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})
'nomcom': nomcom,
'is_chair_task' : True,
})
@role_required("Nomcom Chair", "Nomcom Advisor")
def list_positions(request, 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',
{'positions': positions,
'year': year,
'selected': 'edit_positions',
'nomcom': nomcom}, RequestContext(request))
'nomcom': nomcom,
'is_chair_task' : True,
}, RequestContext(request))
@role_required("Nomcom Chair", "Nomcom Advisor")
def remove_position(request, year, position_id):
nomcom = get_nomcom_by_year(year)
if nomcom.group.state_id=='conclude':
return HttpResponseForbidden('This nomcom is closed.')
try:
position = nomcom.position_set.get(id=position_id)
except Position.DoesNotExist:
@ -812,12 +911,18 @@ def remove_position(request, year, position_id):
return render_to_response('nomcom/remove_position.html',
{'year': year,
'position': position,
'nomcom': nomcom}, RequestContext(request))
'nomcom': nomcom,
'is_chair_task' : True,
}, RequestContext(request))
@role_required("Nomcom Chair", "Nomcom Advisor")
def edit_position(request, year, position_id=None):
nomcom = get_nomcom_by_year(year)
if nomcom.group.state_id=='conclude':
return HttpResponseForbidden('This nomcom is closed.')
if position_id:
try:
position = nomcom.position_set.get(id=position_id)
@ -838,4 +943,10 @@ def edit_position(request, year, position_id=None):
{'form': form,
'position': position,
'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
from collections import Counter
from django.utils.html import escape
from django import forms
from django.core.urlresolvers import reverse as urlreverse
@ -12,8 +14,20 @@ def select2_id_name_json(objs):
def format_email(e):
return escape(u"%s <%s>" % (e.person.name, e.address))
def format_person(p):
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
return json.dumps([{ "id": o.pk, "text": formatter(o) } for o in objs if o])

View file

@ -59,10 +59,13 @@ class PersonInfo(models.Model):
if e:
return e[0]
return None
def email_address(self):
def email(self):
e = self.email_set.filter(primary=True).first()
if not e:
e = self.email_set.filter(active=True).order_by("-time").first()
return e
def email_address(self):
e = self.email()
if e:
return e.address
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'
OPENSSL_COMMAND = '/usr/bin/openssl'
DAYS_TO_EXPIRE_NOMINATION_LINK = ''
DEFAULT_FEEDBACK_TYPE = 'offtopic'
NOMINEE_FEEDBACK_TYPES = ['comment', 'questio', 'nomina']
# ID Submission Tool settings

View file

@ -5,6 +5,7 @@
# ./manage.py test --settings=settings_sqlitetest doc.ChangeStateTestCase
#
import os
from settings import * # pyflakes:ignore
# Workaround to avoid spending minutes stepping through the migrations in
@ -39,3 +40,4 @@ DATABASES = {
if TEST_CODE_COVERAGE_CHECKER and not TEST_CODE_COVERAGE_CHECKER._started:
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 draft.message_set.order_by("-time")[0].subject)
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(name in unicode(outbox[-2]))
self.assertTrue("mars" in unicode(outbox[-2]))

View file

@ -1,6 +1,7 @@
<!DOCTYPE html> {% load ietf_filters staticfiles %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}{% origin %}
{% load bootstrap3 %}
<html lang="en">
<head>
<meta charset="utf-8">
@ -83,15 +84,7 @@
</nav>
{% endwith %}
<div class="container-fluid">
{% if 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 %}
{% bootstrap_messages %}
{% 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="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 %}
<h2>Edit members</h2>
{% bootstrap_messages %}
<form method="post">
{% csrf_token %}
{% bootstrap_form form %}

View file

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

View file

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

View file

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

View file

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

View file

@ -5,24 +5,30 @@
{% load bootstrap3 %}
{% 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 nomcom_content %}
{% origin %}
<p class="alert alert-info">
First select a nominee from the list of nominees to provide input about that nominee.
This will fill in the non-editable fields in the form.
</p>
{% if message %}
<p class="alert alert-{{ message.0 }}">{{ message.1 }}</p>
<p id="instructions" class="alert alert-info">
{% if nomcom.group.state_id == 'conclude' %}
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 %}
{% bootstrap_messages %}
</p>
{% if nomcom|has_publickey %}
<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>
{% for p in positions %}
@ -30,9 +36,14 @@
<h4>{{ p.name }}</h4>
<div class="btn-group-vertical form-group">
{% 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}}">
{{ np.nominee }}
{% add_num_nominations user np.position np.nominee %}
<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.name }}
{% 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>
{% endfor %}
</div>
@ -42,22 +53,30 @@
<p>
An number after a name indicates
that you have given comments on this nominee
earlier. If you position the mouse pointer over
it, you should see how many comments
exist from you for this nominee.
earlier. Position the mouse pointer over
the badge, for more information about this
nominee.
</p>
</div>
<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">
{% csrf_token %}
{% bootstrap_form form %}
{% 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 %}
</form>
{% endif %}
</div>
</div>
{% endif %}

View file

@ -8,31 +8,38 @@
{% origin %}
<h2>Positions in {{ nomcom.group }}</h2>
{% 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 %}
{% for position in positions %}
<h3>{{ position.name }}</h3>
{% regroup positions by is_open as posgroups %}
{% 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">
<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>
<dd>
{% for template in position.get_templates %}
<a href="{% url "nomcom_edit_template" year template.id %}">{{ template }}</a><br>
{% endfor %}
</dd>
{% if nomcom.group.state_id == 'active' %}
<dt>Actions</dt>
<dd>
<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>
</dd>
{% endif %}
</dl>
{% endfor %}
</div>
</div>
{% endfor %}
{% else %}
<p>There are no positions defined.</p>
{% 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 %}
{% 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">
<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 %}
<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 == "questionnaire" %}class="active"{% endif %}><a href="{% url "nomcom_private_questionnaire" year %}">Questionnaire response</a></li>
{% endif %}
<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>
{% if user|is_chair:year %}
{% if user|is_chair_or_advisor:year %}
<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">
<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 role = "presentation" class = "dropdown-header">Feedback Management</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 == "feedback_email" %}class="active"{% endif %}><a href="{% url "nomcom_private_feedback_email" year %}">Enter email feedback</a></li>
<li {% if selected == "questionnaire" %}class="active"{% endif %}><a href="{% url "nomcom_private_questionnaire" year %}">Enter questionnaire response</a></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>
</ul>
</li>
<li class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">Edit <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
<li {% if selected == "merge" %}class="active"{% endif %}><a href="{% url "nomcom_private_merge" year %}">Request Nominee Merge</a></li>
{% endif %}
<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_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 == "merge" %}class="active"{% endif %}><a href="{% url "nomcom_private_merge" year %}">Merge Email Addresses</a></li>
<li {% if selected == "edit_members" %}class="active"{% endif %}><a href="{% url "nomcom_edit_members" year %}">Edit Nomcom Members</a></li>
{% if nomcom.group.state_id == 'active' %}
<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>
</li>
{% endif %}

View file

@ -9,7 +9,7 @@
{% block content %}
{% origin %}
<h1>NomCom {{ year }}</h1>
<h1>NomCom {{ year }} {% if nomcom.group.state_id == 'conclude' %}(Concluded){% endif %}</h1>
<ul class="nav nav-tabs" role="tablist">
<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 %}
{% origin %}
{% if message %}
<div class="alert alert-{{ message.0 }}">{{ message.1 }}</div>
{% endif %}
{% if nomcom|has_publickey %}
{% bootstrap_messages %}
<form id="questionnnaireform" method="post">
{% if form %}
<form id="paste-email-feedback-form" method="post">
{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}

View file

@ -66,14 +66,14 @@
</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 %}
{% endif %}
<table class="table table-condensed table-striped">
<thead>
<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>Position</th>
<th>State</th>
@ -83,7 +83,7 @@
<tbody>
{% for np in nominee_positions %}
<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 class="edit"><a class="btn btn-default btn-xs" href="{% url "nomcom_edit_nominee" year np.nominee.id %}">Edit</a></td>
{% endif %}
@ -101,10 +101,7 @@
{% if is_chair %}
{% if message %}
<p class="alert alert-{{ message.0 }}">{{ message.1 }}</p>
{% endif %}
{% if nomcom.group.state_id == 'active' %}
<div class="form-group">
<label>Action:</label>
<select class="form-control" name="action">
@ -119,5 +116,6 @@
</form>
{% endif %}
{% endif %}
{% endblock %}

View file

@ -10,10 +10,6 @@
{% origin %}
<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>
<pre>

View file

@ -1,38 +1,41 @@
{% extends "nomcom/nomcom_private_base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load staticfiles %}
{% 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 %}
{% origin %}
<h2>Merging nominee email addresses</h2>
<h2>Request Nominee Merge</h2>
<p>
If a nominee has been nominated with multiple email addresses, the nominee will
appear multiple times in the nomination list, as the email address is used as
the unique identifier for each nominee. In order to permit comments and nominations
to be submitted under multiple email addresses, there is a list of secondary email
addresses which needs to be kept up-to-date. When nominations of one particular nominee
have already been made under different email addresses, the nomination comments from the
secondary address also needs to be merged with those under the primary address.
The nomination system encourages the community to nominate people by selecting
their email address from the set of addresses the tracker already knows. In order
to allow a person who does not yet have a datatracker account to be nominated, the
system also provides a way for the community to nominate people with a new,
previously unknown email address. When this option is chosen, a new Person record
is created and associated with the new address.
</p>
<p>
It doesn't matter particularly which email address is used as primary, as far as the
nominee information maintenance goes, but it's probably handier for the nomcom if the
primary address is the one which the nominee prefers at the time.
Occasionally, this new address should have been associated with an existing person
instead. This will happen particularly if the community member uses a slightly incorrect
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>
{% if message %}
<div class="alert alert-{{ message.0 }}">{{ message.1 }}</div>
{% endif %}
{% bootstrap_messages %}
<form id="nominateform" method="post">
{% if form %}
<form id="mergeform" method="post">
{% csrf_token %}
{% bootstrap_form form %}
@ -40,5 +43,11 @@
<input class="btn btn-primary" type="submit" value="Save" name="save">
{% endbuttons %}
</form>
{% endif %}
{% endblock %}
{% block js %}
<script src="{% static 'select2/select2.min.js' %}"></script>
<script src="{% static 'ietf/js/select2-field.js' %}"></script>
{% endblock %}

View file

@ -1,23 +1,23 @@
{% extends "nomcom/nomcom_private_base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load staticfiles %}
{% load bootstrap3 %}
{% 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 nomcom_content %}
{% origin %}
<h2>Candidate nomination</h2>
{% bootstrap_messages %}
{% if message %}
<p class="alert alert-{{ message.0 }}">{{ message.1 }}</p>
{% endif %}
{% if nomcom|has_publickey %}
{% if form %}
<form id="nominate-form" method="post">
{% csrf_token %}
@ -30,3 +30,8 @@
{% endif %}
{% 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 %}
{% if message %}
<p class="alert alert-{{ message.0 }}">{{ message.1 }}</p>
{% endif %}
{% if nomcom|has_publickey %}
{% if form %}
{% if questionnaire_response %}
<h2>Questionnaire response</h2>
{{ questionnaire_response }}
{% endif %}
{% bootstrap_messages %}
<form id="questionnnaireform" method="post">
{% csrf_token %}
{% bootstrap_form form %}

View file

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

View file

@ -1,21 +1,22 @@
{% extends "nomcom/nomcom_public_base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load staticfiles %}
{% load bootstrap3 %}
{% 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 nomcom_content %}
{% origin %}
{% if message %}
<p class="alert alert-{{ message.0 }}">{{ message.1 }}</p>
{% endif %}
{% bootstrap_messages %}
{% if nomcom|has_publickey %}
{% if form %}
<form id="nominate-form" method="post">
{% csrf_token %}
{% bootstrap_form form %}
@ -26,3 +27,8 @@
{% endif %}
{% 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">
{% for position in positions %}
<div class="tab-pane {% if forloop.first %}active{% endif %}" id="{{ position.name|slugify }}">
<h3>{{ position.description }}</h3>
{{ position.get_questionnaire|linebreaks}}
</div>
{% endfor %}

View file

@ -7,16 +7,24 @@
{% block nomcom_content %}
{% origin %}
<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">
{% csrf_token %}
@ -24,8 +32,8 @@
<input type="hidden" name="remove" value="1">
{% buttons %}
<a class="btn btn-default pull-right" href="../">No, get me out of here</a>
<button class="btn btn-primary" type="submit">Yes, remove it</button>
<button class="btn btn-primary btn-warning" type="submit">Delete</button>
<a class="btn btn-default" href="{% url 'nomcom_list_positions' year %}">Cancel</a>
{% endbuttons %}
</form>

View file

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

View file

@ -11,6 +11,7 @@
{% origin %}
<h2>Send remider to {{reminder_description}}</h2>
{% if nomcom.group.state_id == 'active' %}
<p>The message that will be sent is as follows:</p>
<pre>{{ mail_template.content|wrap_text:80 }}</pre>
@ -23,14 +24,10 @@
<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>
{% bootstrap_messages %}
{% if message %}
<div class="alert alert-{{ message.0 }}">{{ message.1 }}</div>
{% endif %}
<form method="post">
{% if nomcom.group.state_id == 'active' %}
<form id="reminderform " method="post">
{% csrf_token %}
<table class="table table-condensed table-striped">
<thead>
@ -57,5 +54,5 @@
<input class="btn btn-primary" type="submit" name="submit" value="Submit request">
{% endbuttons %}
</form>
{% endif %}
{% 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 %}
<h2>Feedback related to nominees</h2>
{% regroup nominees_feedback by nominee.staterank as stateranked_nominees %}
{% for staterank in stateranked_nominees %}
<div class="panel panel-default">
<div class="panel-heading">
{% if staterank.grouper == 0 %}
Accepted nomination for at least one position
{% elif staterank.grouper == 1 %}
Pending for at least one position and has not accepted any nomination
{% else %}
Declined each nominated position
{% endif %}
</div>
<div class="panel-body">
<table class="table table-condensed table-striped">
<thead>
<tr>
<th>Nominee</th>
<th class="col-sm-9">Nominee</th>
{% for ft in feedback_types %}
<th>{{ ft.name }}</th>
<th class="col-sm-1 text-center">{{ ft.name }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for nominee, feedback in nominees_feedback.items %}
{% for fb_dict in staterank.list %}
<tr>
<td>
<a href="{% url "nomcom_view_feedback_nominee" year nominee.id %}#comment">{{ nominee }}
<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>
{% for f in feedback %}
<td>{{ f.1 }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endfor %}
{% if independent_feedback_types %}
<h2>Feedback not related to Nominees</h2>

View file

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

View file

@ -3,17 +3,24 @@
{% load origin %}
{% load bootstrap3 %}
{% load staticfiles %}
{% 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 morecss %}
.nominee_multi_select { resize: vertical; }
{% endblock %}
{% block nomcom_content %}
{% origin %}
<h2>Feedback pending from email list</h2>
{% if message %}
<p class="alert alert-{{ message.0 }}">{{ message.1 }}</p>
{% endif %}
{% if formset.forms %}
<form method="post">
@ -145,9 +152,6 @@
{% buttons %}
<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 %}
{% endif %}
</form>
@ -157,3 +161,8 @@
{% endif %}
{% 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>
<dd>{{ feedback.author|formatted_email|default:"Anonymous" }}</dd>
<dt>Date</dt>
<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>
<dd>{{ feedback.time|date:"Y-m-d" }}</dd>
<dt>Body</dt>
<dd class="pasted">{% decrypt feedback.comments request year 1 %}</dd>
</dl>

View file

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