From e81b47328258a24af82d9173b3676be9a6d745b2 Mon Sep 17 00:00:00 2001
From: Robert Sparks <rjsparks@nostrum.com>
Date: Wed, 25 Nov 2015 22:17:41 +0000
Subject: [PATCH] Expose views for concluded nomcoms. Close feedback and
 nomination. Initial work on factory-boy based testing. Partially addresses
 #1856  - Legacy-Id: 10520

---
 ietf/group/factories.py                       |  10 ++
 ietf/ietfauth/utils.py                        |   6 +-
 ietf/nomcom/factories.py                      | 125 ++++++++++++++++++
 ietf/nomcom/models.py                         |   8 ++
 ietf/nomcom/tests.py                          |  51 +++++++
 ietf/nomcom/utils.py                          |   2 +-
 ietf/nomcom/views.py                          |  21 ++-
 ietf/person/factories.py                      |  56 ++++++++
 ietf/templates/nomcom/feedback.html           |  12 +-
 .../templates/nomcom/nomcom_private_base.html |   2 +-
 ietf/templates/nomcom/nomcom_public_base.html |   2 +-
 ietf/templates/nomcom/private_nominate.html   |   2 +-
 ietf/templates/nomcom/public_nominate.html    |   2 +-
 requirements.txt                              |   2 +
 14 files changed, 283 insertions(+), 18 deletions(-)
 create mode 100644 ietf/group/factories.py
 create mode 100644 ietf/nomcom/factories.py
 create mode 100644 ietf/person/factories.py

diff --git a/ietf/group/factories.py b/ietf/group/factories.py
new file mode 100644
index 000000000..9cef50afd
--- /dev/null
+++ b/ietf/group/factories.py
@@ -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')
diff --git a/ietf/ietfauth/utils.py b/ietf/ietfauth/utils.py
index 2a1db3938..e28587aec 100644
--- a/ietf/ietfauth/utils.py
+++ b/ietf/ietfauth/utils.py
@@ -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", ),
             }
diff --git a/ietf/nomcom/factories.py b/ietf/nomcom/factories.py
new file mode 100644
index 000000000..eed265eff
--- /dev/null
+++ b/ietf/nomcom/factories.py
@@ -0,0 +1,125 @@
+import factory
+import random
+
+from ietf.nomcom.models import NomCom, Position, 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 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 = [Nominee.objects.create(nomcom=self, email=PersonFactory().email_set.first()) for i in range(2)]
+            positions = [PositionFactory(nomcom=self) for i in range(3)]
+
+            def npc(x,y):
+                return NomineePosition.objects.create(position=x,
+                                                      nominee=y,
+                                                      state_id='accepted') 
+            # This gives us positions with 0, 1 and 2 nominees, and
+            # one person who's been nomminated for more than one position
+            npc(positions[0],nominees[0])
+            npc(positions[1],nominees[0])
+            npc(positions[1],nominees[1])
+
+    @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)
+    description = factory.Faker('paragraph',nb_sentences=4)
+    is_open = True
+
diff --git a/ietf/nomcom/models.py b/ietf/nomcom/models.py
index 6b466dd98..36c295086 100644
--- a/ietf/nomcom/models.py
+++ b/ietf/nomcom/models.py
@@ -66,6 +66,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)
diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py
index e81546f04..0520151aa 100644
--- a/ietf/nomcom/tests.py
+++ b/ietf/nomcom/tests.py
@@ -30,6 +30,9 @@ from ietf.nomcom.forms import EditMembersForm, EditMembersFormPreview
 from ietf.nomcom.utils import get_nomcom_by_year, get_or_create_nominee
 from ietf.nomcom.management.commands.send_reminders import Command, is_time_to_send
 
+from ietf.nomcom.factories import NomComFactory, nomcom_kwargs_for_year
+from ietf.person.factories import PersonFactory
+
 client_test_cert_files = None
 
 def get_cert_files():
@@ -920,3 +923,51 @@ class ReminderTest(TestCase):
         self.assertEqual(len(outbox), messages_before + 1)
         self.assertTrue('nominee1@' in outbox[-1]['To'])
 
+class InactiveNomcomTests(TestCase):
+
+    def setUp(self):
+        self.nc = NomComFactory.create(**nomcom_kwargs_for_year(group__state_id='conclude'))
+        self.plain_person = PersonFactory.create()
+
+    def test_feedback_closed(self):
+        for view in ['nomcom_public_feedback', 'nomcom_private_feedback']:
+            url = reverse(view, kwargs={'year': self.nc.year()})
+            who = self.plain_person if 'public' in view else self.nc.group.role_set.filter(name='member').first().person
+            login_testing_unauthorized(self, who.user.username, url)
+            response = self.client.get(url)
+            self.assertEqual(response.status_code, 200)
+            q = PyQuery(response.content)
+            self.assertTrue( '(Concluded)' in q('h1').text())
+            self.assertTrue( 'closed' in q('#instructions').text())
+            self.assertTrue( q('#nominees a') )
+            self.assertFalse( q('#nominees a[href]') )
+    
+            url += "?nominee=%d&position=%d" % (self.nc.nominee_set.first().id, self.nc.nominee_set.first().nomineeposition_set.first().position.id)
+            response = self.client.get(url)
+            self.assertEqual(response.status_code, 200)
+            q = PyQuery(response.content)
+            self.assertFalse( q('#feedbackform'))        
+            
+            empty_outbox()
+            fb_before = self.nc.feedback_set.count()
+            test_data = {'comments': u'Test feedback view. Comments with accents äöåÄÖÅ éáíóú âêîôû ü àèìòù.',
+                         'nominator_email': self.plain_person.email_set.first().address,
+                         'confirmation': True}
+            response = self.client.post(url, test_data)
+            self.assertEqual(response.status_code, 200)
+            q = PyQuery(response.content)
+            self.assertTrue( 'closed' in q('#instructions').text())
+            self.assertEqual( len(outbox), 0 )
+            self.assertEqual( fb_before, self.nc.feedback_set.count() )
+
+    def test_nominations_closed(self):
+        for view in ['nomcom_public_nominate', 'nomcom_private_nominate']:
+            url = reverse(view, kwargs={'year': self.nc.year() })
+            who = self.plain_person if 'public' in view else self.nc.group.role_set.filter(name='member').first().person
+            login_testing_unauthorized(self, who.user.username, url)
+            response = self.client.get(url)
+            self.assertEqual(response.status_code, 200)
+            q = PyQuery(response.content)
+            self.assertTrue( '(Concluded)' in q('h1').text())
+            self.assertTrue( 'closed' in q('.alert-warning').text())
+            
diff --git a/ietf/nomcom/utils.py b/ietf/nomcom/utils.py
index 084cf7db7..55cdfa4fb 100644
--- a/ietf/nomcom/utils.py
+++ b/ietf/nomcom/utils.py
@@ -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):
diff --git a/ietf/nomcom/views.py b/ietf/nomcom/views.py
index f6554cb8c..bafa77d55 100644
--- a/ietf/nomcom/views.py
+++ b/ietf/nomcom/views.py
@@ -322,6 +322,14 @@ def nominate(request, year, public):
                                'year': year,
                                'selected': 'nominate'}, RequestContext(request))
 
+    if nomcom.group.state_id == 'conclude':
+        message = ('warning', "Nominations to this Nomcom are closed.")
+        return render_to_response(template,
+                              {'message': message,
+                               'nomcom': nomcom,
+                               'year': year,
+                               'selected': 'nominate'}, RequestContext(request))
+
     message = None
     if request.method == 'POST':
         form = NominateForm(data=request.POST, nomcom=nomcom, user=request.user, public=public)
@@ -355,11 +363,12 @@ def feedback(request, year, public):
     has_publickey = nomcom.public_key and True or False
     nominee = None
     position = None
-    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)
+    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)
 
     positions = Position.objects.get_by_nomcom(nomcom=nomcom).opened()
 
@@ -379,7 +388,7 @@ def feedback(request, year, public):
             })
 
     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)
diff --git a/ietf/person/factories.py b/ietf/person/factories.py
new file mode 100644
index 000000000..79db6fcff
--- /dev/null
+++ b/ietf/person/factories.py
@@ -0,0 +1,56 @@
+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' % (factory.Faker('first_name'),factory.Faker('last_name'),factory.Faker('domain_name'))
diff --git a/ietf/templates/nomcom/feedback.html b/ietf/templates/nomcom/feedback.html
index 7334e9362..1a5e70310 100644
--- a/ietf/templates/nomcom/feedback.html
+++ b/ietf/templates/nomcom/feedback.html
@@ -9,8 +9,12 @@
 
 {% block nomcom_content %}
   {% origin %}
-  <p class="alert alert-info">
-    Select a nominee from the list of nominees to the right to obtain a new feedback form.
+  <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 %}
   </p>
 
   {% if message %}
@@ -21,7 +25,7 @@
 
   {% 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 %}
@@ -29,7 +33,7 @@
             <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}}">
+                <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 }}
 	          {% add_num_nominations user np.position np.nominee %}
                 </a>
diff --git a/ietf/templates/nomcom/nomcom_private_base.html b/ietf/templates/nomcom/nomcom_private_base.html
index cb9e89384..d597ce0f1 100644
--- a/ietf/templates/nomcom/nomcom_private_base.html
+++ b/ietf/templates/nomcom/nomcom_private_base.html
@@ -9,7 +9,7 @@
 {% block content %}
   {% origin %}
 
-  <h1>NomCom {{ year }} <small>Private area {% if is_chair_task %}- Chair/Advisors only{% endif %}</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>
diff --git a/ietf/templates/nomcom/nomcom_public_base.html b/ietf/templates/nomcom/nomcom_public_base.html
index c22fdc6c7..60bd918ec 100644
--- a/ietf/templates/nomcom/nomcom_public_base.html
+++ b/ietf/templates/nomcom/nomcom_public_base.html
@@ -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>
diff --git a/ietf/templates/nomcom/private_nominate.html b/ietf/templates/nomcom/private_nominate.html
index 6530e2d41..978946f57 100644
--- a/ietf/templates/nomcom/private_nominate.html
+++ b/ietf/templates/nomcom/private_nominate.html
@@ -17,7 +17,7 @@
     <p class="alert alert-{{ message.0 }}">{{ message.1 }}</p>
   {% endif %}
 
-  {% if nomcom|has_publickey %}
+  {% if form %}
 
     <form id="nominate-form" method="post">
       {% csrf_token %}
diff --git a/ietf/templates/nomcom/public_nominate.html b/ietf/templates/nomcom/public_nominate.html
index c1384ca76..2e9fa842b 100644
--- a/ietf/templates/nomcom/public_nominate.html
+++ b/ietf/templates/nomcom/public_nominate.html
@@ -15,7 +15,7 @@
 
   {% bootstrap_messages %}
 
-  {% if nomcom|has_publickey %}
+  {% if form %}
     <form id="nominate-form" method="post">
       {% csrf_token %}
       {% bootstrap_form form %}
diff --git a/requirements.txt b/requirements.txt
index af9ca0734..57f04febc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -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