Skip to content

Commit e81b473

Browse files
committed
Expose views for concluded nomcoms. Close feedback and nomination. Initial work on factory-boy based testing. Partially addresses ietf-tools#1856
- Legacy-Id: 10520
1 parent 2197259 commit e81b473

14 files changed

Lines changed: 283 additions & 18 deletions

ietf/group/factories.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import factory
2+
3+
from ietf.group.models import Group
4+
5+
class GroupFactory(factory.DjangoModelFactory):
6+
class Meta:
7+
model = Group
8+
9+
name = factory.Faker('sentence',nb_words=6)
10+
acronym = factory.Faker('word')

ietf/ietfauth/utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ def has_role(user, role_names, *args, **kwargs):
6464
"RG Secretary": Q(person=person,name="secr", group__type="rg", group__state__in=["active","proposed"]),
6565
"AG Secretary": Q(person=person,name="secr", group__type="ag", group__state__in=["active"]),
6666
"Team Chair": Q(person=person,name="chair", group__type="team", group__state="active"),
67-
"Nomcom Chair": Q(person=person, name="chair", group__type="nomcom", group__state="active", group__acronym__icontains=kwargs.get('year', '0000')),
68-
"Nomcom Advisor": Q(person=person, name="advisor", group__type="nomcom", group__state="active", group__acronym__icontains=kwargs.get('year', '0000')),
69-
"Nomcom": Q(person=person, group__type="nomcom", group__state="active", group__acronym__icontains=kwargs.get('year', '0000')),
67+
"Nomcom Chair": Q(person=person, name="chair", group__type="nomcom", group__acronym__icontains=kwargs.get('year', '0000')),
68+
"Nomcom Advisor": Q(person=person, name="advisor", group__type="nomcom", group__acronym__icontains=kwargs.get('year', '0000')),
69+
"Nomcom": Q(person=person, group__type="nomcom", group__acronym__icontains=kwargs.get('year', '0000')),
7070
"Liaison Manager": Q(person=person,name="liaiman",group__type="sdo",group__state="active", ),
7171
"Authorized Individual": Q(person=person,name="auth",group__type="sdo",group__state="active", ),
7272
}

ietf/nomcom/factories.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import factory
2+
import random
3+
4+
from ietf.nomcom.models import NomCom, Position, Nominee, NomineePosition
5+
from ietf.group.factories import GroupFactory
6+
from ietf.person.factories import PersonFactory
7+
8+
import debug # pyflakes:ignore
9+
10+
cert = '''-----BEGIN CERTIFICATE-----
11+
MIIDHjCCAgagAwIBAgIJAKDCCjbQboJzMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNV
12+
BAMMCE5vbUNvbTE1MB4XDTE0MDQwNDIxMTQxNFoXDTE2MDQwMzIxMTQxNFowEzER
13+
MA8GA1UEAwwITm9tQ29tMTUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
14+
AQC2QXCsAitYSOgPYor77zQnEeHuVqlcuhpH1wpKB+N6WcScA5N3AnX9uZEFOt6M
15+
cJ+MCiHECdqDlH6npQTJlpCpIVgAD4B6xzjRBRww8d3lClA/kKwsKzuX93RS0Uv3
16+
0hAD6q9wjqK/m6vR5Y1SsvJYV0y+Yu5j9xUEsojMH7O3NlXWAYOb6oH+f/X7PX27
17+
IhtiCwfICMmVWh/hKeXuFx6HSOcH3gZ6Tlk1llfDbE/ArpsZ6JmnLn73+64yqIoO
18+
ZOc4JJUPrdsmbNwXoxQSQhrpwjN8NpSkQaJbHGB3G+OWvP4fpqcweFHxlEq1Hhef
19+
uR9E6jc3qwxVQfwjbcq6N/4JAgMBAAGjdTBzMB0GA1UdDgQWBBTJow+TJynRWsTQ
20+
LzoS861FGb/rxDAOBgNVHQ8BAf8EBAMCBLAwDwYDVR0TAQH/BAUwAwEB/zAcBgNV
21+
HREEFTATgRFub21jb20xNUBpZXRmLm9yZzATBgNVHSUEDDAKBggrBgEFBQcDBDAN
22+
BgkqhkiG9w0BAQsFAAOCAQEAJwLapB9u5N3iK6SCTqh+PVkigZeB2YMVBW8WA3Ut
23+
iRPBj+jHWOpF5pzZHTOcNaAxDEG9lyIlcWqc93A24K/Gen11Tx0hO4FAPOG0+PP8
24+
4lx7F6xeeyUNR44pInrB93G2q0jl+3wjZH8uhBKlGji4UTMpDPpEl6uiyQCbkMMm
25+
Vr7HZH5Dv/lsjGHHf8uJO7+mcMh+tqxLn3DzPrm61OfeWdkoVX2pTz0imRQ3Es+8
26+
I7zNMk+fNNaEEyPnEyHfuWq0uD/qKeP27NZIoINy6E3INQ5QaE2uc1nQULg5y7uJ
27+
toX3j+FUe2UiUak3ACXdrOPSsFP0KRrFwuMnuHHXkGj/Uw==
28+
-----END CERTIFICATE-----
29+
'''
30+
31+
key = '''-----BEGIN PRIVATE KEY-----
32+
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC2QXCsAitYSOgP
33+
Yor77zQnEeHuVqlcuhpH1wpKB+N6WcScA5N3AnX9uZEFOt6McJ+MCiHECdqDlH6n
34+
pQTJlpCpIVgAD4B6xzjRBRww8d3lClA/kKwsKzuX93RS0Uv30hAD6q9wjqK/m6vR
35+
5Y1SsvJYV0y+Yu5j9xUEsojMH7O3NlXWAYOb6oH+f/X7PX27IhtiCwfICMmVWh/h
36+
KeXuFx6HSOcH3gZ6Tlk1llfDbE/ArpsZ6JmnLn73+64yqIoOZOc4JJUPrdsmbNwX
37+
oxQSQhrpwjN8NpSkQaJbHGB3G+OWvP4fpqcweFHxlEq1HhefuR9E6jc3qwxVQfwj
38+
bcq6N/4JAgMBAAECggEAb5SS4YwWc193S2v+QQ2KdVz6YEuINq/tRQw/TWGVACQT
39+
PZzm3FaSXDsOsRAAjiSpWTgewgFyWVpBTGu4CZ73g8RZNvhGpWRwwW8KemCpg/8T
40+
cEcnUYdKXdhuzAE9LETb7znwHM4Gj55DzCZopjfOLQ2Ne4XgAy2THaQcIjRKd6Bw
41+
3mteJ2ityDj3iFN7cq9ntDzp+2BqLOi7AZmLntmUZxtkPCT6k5/dcKFYQW9Eb3bt
42+
MON+BIYVzqhAijkP/cAWmbgZAP9EFng5PpE1lc/shl0W8eX4yvjNoMPRq3wphS4j
43+
L16VncUeDep3vR0CECx7gnTfR0uCDEgKow50pzGQAQKBgQDaQWwK/o39zI3lCGzy
44+
oSNJRNQJ/iZBkbbwpCCaka7VnBfd0ZH54VEWL3oMTkkWRSZtjsPAqT+ndwZitm0D
45+
Kww9FUDMP7j/tMOwAUHYfjYFqFTn6ipkBuby9tbZtL7lgJO6Iu2Qk3afqADD0kcP
46+
zRLxcYSLjrmp9NyUlNnpswR4CQKBgQDVxjwG/orCmiuyA1Bu4u1hdUD0w9CKnyjp
47+
VTbkv8lxk5V3pYzms2Awb0X43W2OioYGBk5yw+9GCF//xCrfbGV7BLZnDTGShjkJ
48+
8oTpLPGBsDSfaKVXE3Hko4LVLBMQIm0tDyuPD1Naia7ZknYn906skonEG8WgHUyp
49+
c/BgkvzWAQKBgBdojuL6/FWtO8bFyZGYUMWJ+Uf9FzNPIpTatZh+aYcFj9W9pW9s
50+
iBreCrQJLXOTBRUZC8u9G1Olw2yQ7k45rr1aazG83+WlCJv29o32s2qV7E1XYyaJ
51+
SvniGZcN+K96w91h46Lu/fkPts1J309FinOU3kdtjmI5HfNdp6WWCrOpAoGBAMjc
52+
TEaeIK8cwPWwG4E1A6pQy8mvu2Ckj4I+KSfh9FsdOpGDIdMas8SOqQZet7P5AFjk
53+
0A0RgN8iu2DMZyQq62cdVG2bffqY1zs7fhrBueILOEaXwtMAWEFmSWYW1YqRbleq
54+
K1luIvms6HdSIGcI/gk0XvG+zn/VR9ToNPHo6lwBAoGBAIrYGYPf+cjZ1V/tNqnL
55+
IecEZb4Gkp1hVhOpNT4U+T2LROxrZtFxxsw2vuIRa5a5FtMbDq9Xyhkm0QppliBd
56+
KQ38jTT0EaD2+vstTqL8vxupo25RQWV1XsmLL4pLbKnm2HnnwB3vEtsiokWKW0q0
57+
Tdb0MiLc+r/zvx8oXtgDjDUa
58+
-----END PRIVATE KEY-----
59+
'''
60+
61+
def nomcom_kwargs_for_year(year=None, *args, **kwargs):
62+
if not year:
63+
year = random.randint(1980,2100)
64+
if 'group__state_id' not in kwargs:
65+
kwargs['group__state_id']='active'
66+
if 'group__acronym' not in kwargs:
67+
kwargs['group__acronym'] = 'nomcom%d'%year
68+
if 'group__name' not in kwargs:
69+
kwargs['group__name'] = 'TEST VERSION of IAB/IESG Nominating Committee %d/%d'%(year,year+1)
70+
return kwargs
71+
72+
73+
class NomComFactory(factory.DjangoModelFactory):
74+
class Meta:
75+
model = NomCom
76+
77+
group = factory.SubFactory(GroupFactory,type_id='nomcom')
78+
79+
public_key = factory.django.FileField(data=cert)
80+
81+
@factory.post_generation
82+
def populate_positions(self, create, extracted, **kwargs):
83+
'''
84+
Create a set of nominees and positions unless NomcomFactory is called
85+
with populate_positions=False
86+
'''
87+
if extracted is None:
88+
extracted = True
89+
if create and extracted:
90+
nominees = [Nominee.objects.create(nomcom=self, email=PersonFactory().email_set.first()) for i in range(2)]
91+
positions = [PositionFactory(nomcom=self) for i in range(3)]
92+
93+
def npc(x,y):
94+
return NomineePosition.objects.create(position=x,
95+
nominee=y,
96+
state_id='accepted')
97+
# This gives us positions with 0, 1 and 2 nominees, and
98+
# one person who's been nomminated for more than one position
99+
npc(positions[0],nominees[0])
100+
npc(positions[1],nominees[0])
101+
npc(positions[1],nominees[1])
102+
103+
@factory.post_generation
104+
def populate_personnel(self, create, extracted, **kwargs):
105+
'''
106+
Create a default set of role holders, unless the factory is called
107+
with populate_personnel=False
108+
'''
109+
if extracted is None:
110+
extracted = True
111+
if create and extracted:
112+
#roles= ['chair', 'advisor'] + ['member']*10
113+
roles = ['chair', 'advisor', 'member']
114+
for role in roles:
115+
p = PersonFactory()
116+
self.group.role_set.create(name_id=role,person=p,email=p.email_set.first())
117+
118+
class PositionFactory(factory.DjangoModelFactory):
119+
class Meta:
120+
model = Position
121+
122+
name = factory.Faker('sentence',nb_words=10)
123+
description = factory.Faker('paragraph',nb_sentences=4)
124+
is_open = True
125+

ietf/nomcom/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ def save(self, *args, **kwargs):
6666
if created:
6767
initialize_templates_for_group(self)
6868

69+
def year(self):
70+
year = getattr(self,'_cached_year',None)
71+
if year is None:
72+
if self.group and self.group.acronym.startswith('nomcom'):
73+
year = int(self.group.acronym[6:])
74+
self._cached_year = year
75+
return year
76+
6977

7078
def delete_nomcom(sender, **kwargs):
7179
nomcom = kwargs.get('instance', None)

ietf/nomcom/tests.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
from ietf.nomcom.utils import get_nomcom_by_year, get_or_create_nominee
3131
from ietf.nomcom.management.commands.send_reminders import Command, is_time_to_send
3232

33+
from ietf.nomcom.factories import NomComFactory, nomcom_kwargs_for_year
34+
from ietf.person.factories import PersonFactory
35+
3336
client_test_cert_files = None
3437

3538
def get_cert_files():
@@ -920,3 +923,51 @@ def test_remind_questionnaire_view(self):
920923
self.assertEqual(len(outbox), messages_before + 1)
921924
self.assertTrue('nominee1@' in outbox[-1]['To'])
922925

926+
class InactiveNomcomTests(TestCase):
927+
928+
def setUp(self):
929+
self.nc = NomComFactory.create(**nomcom_kwargs_for_year(group__state_id='conclude'))
930+
self.plain_person = PersonFactory.create()
931+
932+
def test_feedback_closed(self):
933+
for view in ['nomcom_public_feedback', 'nomcom_private_feedback']:
934+
url = reverse(view, kwargs={'year': self.nc.year()})
935+
who = self.plain_person if 'public' in view else self.nc.group.role_set.filter(name='member').first().person
936+
login_testing_unauthorized(self, who.user.username, url)
937+
response = self.client.get(url)
938+
self.assertEqual(response.status_code, 200)
939+
q = PyQuery(response.content)
940+
self.assertTrue( '(Concluded)' in q('h1').text())
941+
self.assertTrue( 'closed' in q('#instructions').text())
942+
self.assertTrue( q('#nominees a') )
943+
self.assertFalse( q('#nominees a[href]') )
944+
945+
url += "?nominee=%d&position=%d" % (self.nc.nominee_set.first().id, self.nc.nominee_set.first().nomineeposition_set.first().position.id)
946+
response = self.client.get(url)
947+
self.assertEqual(response.status_code, 200)
948+
q = PyQuery(response.content)
949+
self.assertFalse( q('#feedbackform'))
950+
951+
empty_outbox()
952+
fb_before = self.nc.feedback_set.count()
953+
test_data = {'comments': u'Test feedback view. Comments with accents äöåÄÖÅ éáíóú âêîôû ü àèìòù.',
954+
'nominator_email': self.plain_person.email_set.first().address,
955+
'confirmation': True}
956+
response = self.client.post(url, test_data)
957+
self.assertEqual(response.status_code, 200)
958+
q = PyQuery(response.content)
959+
self.assertTrue( 'closed' in q('#instructions').text())
960+
self.assertEqual( len(outbox), 0 )
961+
self.assertEqual( fb_before, self.nc.feedback_set.count() )
962+
963+
def test_nominations_closed(self):
964+
for view in ['nomcom_public_nominate', 'nomcom_private_nominate']:
965+
url = reverse(view, kwargs={'year': self.nc.year() })
966+
who = self.plain_person if 'public' in view else self.nc.group.role_set.filter(name='member').first().person
967+
login_testing_unauthorized(self, who.user.username, url)
968+
response = self.client.get(url)
969+
self.assertEqual(response.status_code, 200)
970+
q = PyQuery(response.content)
971+
self.assertTrue( '(Concluded)' in q('h1').text())
972+
self.assertTrue( 'closed' in q('.alert-warning').text())
973+

ietf/nomcom/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def get_nomcom_by_year(year):
5353
from ietf.nomcom.models import NomCom
5454
return get_object_or_404(NomCom,
5555
group__acronym__icontains=year,
56-
group__state__slug='active')
56+
)
5757

5858

5959
def get_year_by_nomcom(nomcom):

ietf/nomcom/views.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,14 @@ def nominate(request, year, public):
322322
'year': year,
323323
'selected': 'nominate'}, RequestContext(request))
324324

325+
if nomcom.group.state_id == 'conclude':
326+
message = ('warning', "Nominations to this Nomcom are closed.")
327+
return render_to_response(template,
328+
{'message': message,
329+
'nomcom': nomcom,
330+
'year': year,
331+
'selected': 'nominate'}, RequestContext(request))
332+
325333
message = None
326334
if request.method == 'POST':
327335
form = NominateForm(data=request.POST, nomcom=nomcom, user=request.user, public=public)
@@ -355,11 +363,12 @@ def feedback(request, year, public):
355363
has_publickey = nomcom.public_key and True or False
356364
nominee = None
357365
position = None
358-
selected_nominee = request.GET.get('nominee')
359-
selected_position = request.GET.get('position')
360-
if selected_nominee and selected_position:
361-
nominee = get_object_or_404(Nominee, id=selected_nominee)
362-
position = get_object_or_404(Position, id=selected_position)
366+
if nomcom.group.state_id != 'conclude':
367+
selected_nominee = request.GET.get('nominee')
368+
selected_position = request.GET.get('position')
369+
if selected_nominee and selected_position:
370+
nominee = get_object_or_404(Nominee, id=selected_nominee)
371+
position = get_object_or_404(Position, id=selected_position)
363372

364373
positions = Position.objects.get_by_nomcom(nomcom=nomcom).opened()
365374

@@ -379,7 +388,7 @@ def feedback(request, year, public):
379388
})
380389

381390
message = None
382-
if request.method == 'POST':
391+
if nominee and position and request.method == 'POST':
383392
form = FeedbackForm(data=request.POST,
384393
nomcom=nomcom, user=request.user,
385394
public=public, position=position, nominee=nominee)

ietf/person/factories.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import factory
2+
import faker
3+
4+
from unidecode import unidecode
5+
6+
from django.contrib.auth.models import User
7+
from ietf.person.models import Person, Alias, Email
8+
9+
fake = faker.Factory.create()
10+
11+
class UserFactory(factory.DjangoModelFactory):
12+
class Meta:
13+
model = User
14+
django_get_or_create = ('username',)
15+
16+
first_name = factory.Faker('first_name')
17+
last_name = factory.Faker('last_name')
18+
email = factory.LazyAttribute(lambda u: '%s.%s@%s'%(u.first_name,u.last_name,fake.domain_name()))
19+
username = factory.LazyAttribute(lambda u: u.email)
20+
21+
@factory.post_generation
22+
def set_password(self, create, extracted, **kwargs):
23+
self.set_password( '%s+password' % self.username )
24+
25+
class PersonFactory(factory.DjangoModelFactory):
26+
class Meta:
27+
model = Person
28+
29+
user = factory.SubFactory(UserFactory)
30+
name = factory.LazyAttribute(lambda p: '%s %s'%(p.user.first_name,p.user.last_name))
31+
ascii = factory.LazyAttribute(lambda p: unidecode(p.name))
32+
33+
@factory.post_generation
34+
def default_aliases(self, create, extracted, **kwargs):
35+
make_alias = getattr(AliasFactory, 'create' if create else 'build')
36+
make_alias(person=self,name=self.name)
37+
make_alias(person=self,name=self.ascii)
38+
39+
@factory.post_generation
40+
def default_emails(self, create, extracted, **kwargs):
41+
make_email = getattr(EmailFactory, 'create' if create else 'build')
42+
make_email(person=self,address=self.user.email)
43+
44+
class AliasFactory(factory.DjangoModelFactory):
45+
class Meta:
46+
model = Alias
47+
django_get_or_create = ('name',)
48+
49+
name = factory.Faker('name')
50+
51+
class EmailFactory(factory.DjangoModelFactory):
52+
class Meta:
53+
model = Email
54+
django_get_or_create = ('address',)
55+
56+
address = '%s.%s@%s' % (factory.Faker('first_name'),factory.Faker('last_name'),factory.Faker('domain_name'))

ietf/templates/nomcom/feedback.html

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@
99

1010
{% block nomcom_content %}
1111
{% origin %}
12-
<p class="alert alert-info">
13-
Select a nominee from the list of nominees to the right to obtain a new feedback form.
12+
<p id="instructions" class="alert alert-info">
13+
{% if nomcom.group.state_id == 'conclude' %}
14+
Feedback to this nomcom is closed.
15+
{% else %}
16+
Select a nominee from the list of nominees to the right to obtain a new feedback form.
17+
{% endif %}
1418
</p>
1519

1620
{% if message %}
@@ -21,15 +25,15 @@
2125

2226
{% if nomcom|has_publickey %}
2327
<div class="row">
24-
<div class="col-sm-4 col-sm-push-8">
28+
<div id="nominees" class="col-sm-4 col-sm-push-8">
2529
<h3>Nominees</h3>
2630

2731
{% for p in positions %}
2832
{% if p.nomineeposition_set.accepted.not_duplicated %}
2933
<h4>{{ p.name }}</h4>
3034
<div class="btn-group-vertical form-group">
3135
{% for np in p.nomineeposition_set.accepted.not_duplicated %}
32-
<a class="btn btn-default btn-xs" href="?nominee={{np.nominee.id}}&position={{ np.position.id}}">
36+
<a class="btn btn-default btn-xs" {% if nomcom.group.state_id != 'conclude' %}href="?nominee={{np.nominee.id}}&position={{ np.position.id}}"{% endif %}>
3337
{{ np.nominee }}
3438
{% add_num_nominations user np.position np.nominee %}
3539
</a>

ietf/templates/nomcom/nomcom_private_base.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
{% block content %}
1010
{% origin %}
1111

12-
<h1>NomCom {{ year }} <small>Private area {% if is_chair_task %}- Chair/Advisors only{% endif %}</small></h1>
12+
<h1>NomCom {{ year }} {% if nomcom.group.state_id == 'conclude' %}(Concluded){% endif %} <small>Private area {% if is_chair_task %}- Chair/Advisors only{% endif %}</small></h1>
1313

1414
<ul class="nav nav-tabs" role="tablist">
1515
<li {% if selected == "index" %}class="active"{% endif %}><a href="{% url "nomcom_private_index" year %}">Nominees</a></li>

0 commit comments

Comments
 (0)