Coverage for gui/tests.py: 100%
947 statements
« prev ^ index » next coverage.py v7.2.1, created at 2023-03-14 06:24 +0000
« prev ^ index » next coverage.py v7.2.1, created at 2023-03-14 06:24 +0000
1from django.conf import settings
2from django.core import mail
3from django.contrib.auth import get_user_model
4from django.test import TestCase
5from django.utils import timezone
6from django.urls import reverse
7from datetime import timedelta
8from unittest.mock import patch, MagicMock, PropertyMock
10from allauth.account.models import EmailAddress
12from htmlvalidator.client import ValidatingClient
14from .models import MembershipPeriod, Membership, Link, Profile, Ballot, BallotSelectQuestion
15from .models import BallotSelectQuestionPossibleAnswer, BallotSelectedAnswer, BallotRankingQuestion
16from .models import BallotRankingQuestionOption, BallotRankingAnswer, VoteCount, OptionRankingVotes
17from .context_processors import admin_tasks_count, global_context
18from .forms import ProfileForm, MembershipApplicationForm, BallotForm
19from .email import Email, AdminEmail, email_context
21from collections import OrderedDict
22import logging
23import os
25logging.disable(logging.CRITICAL)
28class EmailTests(TestCase):
29 def test_send(self):
30 with self.settings(EMAIL_HOST_USER="hello", EMAIL_HOST='me.com', DEFAULT_FROM_EMAIL=None):
31 email = Email("my subject", 'the wonderful\nmessage',
32 ['one@domain.tld', 'two@domain.tld', 'three@domain.tld'])
33 email.send()
35 self.assertEqual(len(mail.outbox), 1)
36 self.assertEqual(mail.outbox[0].to, email.to)
37 self.assertEqual(mail.outbox[0].from_email, 'hello@me.com')
38 self.assertEqual(mail.outbox[0].subject, '[X.Org membership] ' + email.subject)
39 self.assertEqual(mail.outbox[0].body, email.message)
41 def test_send_with_default_from(self):
42 with self.settings(DEFAULT_FROM_EMAIL='hello2@me.com'):
43 email = Email("my subject", 'the wonderful\nmessage',
44 ['one@domain.tld', 'two@domain.tld', 'three@domain.tld'])
45 email.send()
47 self.assertEqual(len(mail.outbox), 1)
48 self.assertEqual(mail.outbox[0].from_email, 'hello2@me.com')
51class AdminEmailTests(TestCase):
52 def test_init(self):
53 for i in range(6):
54 get_user_model().objects.create_user('user{}'.format(i),
55 'user{}@provider.com'.format(i),
56 'pwd', is_superuser=((i % 2) == 0))
58 email = AdminEmail("my subject", 'the wonderful\nadmin\nmessage')
60 self.assertEqual(email.to, ['user0@provider.com', 'user2@provider.com', 'user4@provider.com'])
61 self.assertEqual(email.subject, 'my subject')
62 self.assertEqual(email.message, 'the wonderful\nadmin\nmessage')
65class EmailContextTests(TestCase):
66 def test_email_context(self):
67 context = email_context(toto="hello")
68 self.assertIn('current_site', context)
69 self.assertEqual(context.get('toto'), 'hello')
72def create_user_and_log_in(client, admin=False, verified=False):
73 user = get_user_model().objects.create_user('user', 'user@provider.com', 'pwd',
74 first_name='First', last_name='Last',
75 is_superuser=admin)
76 EmailAddress.objects.create(user=user, primary=True, verified=verified,
77 email=user.email)
78 client.login(username='user', password='pwd')
79 return user
82class MembershipPeriodTests(TestCase):
83 def test_current_period(self):
84 # Check that by default, we get no periods at all
85 self.assertEqual(MembershipPeriod.current_period(), None)
87 # Check that no future period becomes now
88 MembershipPeriod.objects.create(short_name="future", description="", agreement_url="https://x.org",
89 start=timezone.now() + timedelta(hours=1),
90 end=timezone.now() + timedelta(hours=2))
91 self.assertEqual(MembershipPeriod.current_period(), None)
93 # Check that the ordering is right for dates in the past
94 old = MembershipPeriod.objects.create(short_name="old", description="", agreement_url="https://x.org",
95 start=timezone.now(),
96 end=timezone.now() + timedelta(hours=2))
97 self.assertEqual(MembershipPeriod.current_period(), old)
99 new = MembershipPeriod.objects.create(short_name="new", description="", agreement_url="https://x.org",
100 start=timezone.now(),
101 end=timezone.now() + timedelta(hours=2))
103 self.assertEqual(MembershipPeriod.current_period(), new)
105 @patch('gui.models.Membership.objects.filter')
106 def test_members(self, filter_mocked):
107 period = MembershipPeriod.objects.create(short_name="period name", start=timezone.now(),
108 end=timezone.now() + timedelta(hours=2))
110 members = period.members
111 filter_mocked.assert_called_with(period=period)
112 filter_mocked.return_value.exclude.assert_called_with(approved_on=None, rejected_on=None)
113 self.assertEqual(members, filter_mocked.return_value.exclude.return_value)
115 def test_str(self):
116 start = timezone.now()
117 period = MembershipPeriod.objects.create(short_name="period name", start=start,
118 end=timezone.now() + timedelta(hours=2))
119 self.assertEqual(str(period), "period 'period name' starting on {}".format(start))
122class MembershipTests(TestCase):
123 def setUp(self):
124 self.period = MembershipPeriod.objects.create(short_name='2018-2019', start=timezone.now(),
125 end=timezone.now() + timedelta(hours=2))
126 self.user = get_user_model().objects.create_user('user', 'user@provider.com', 'pwd')
128 def test_empty(self):
129 membership = Membership()
130 self.assertTrue(membership.is_pending)
131 self.assertFalse(membership.is_approved)
132 self.assertFalse(membership.is_rejected)
133 self.assertEqual(membership.status, "Pending approval")
135 def test_approved(self):
136 membership = Membership(period=self.period, user_profile=self.user.profile)
138 membership.approve()
140 self.assertEqual(str(membership),
141 " <user@provider.com>'s membership for the period 2018-2019 ({})".format(membership.status))
143 # Check that we do not allow double approval
144 self.assertRaisesMessage(ValueError, 'The membership has already been approved', membership.approve)
146 self.assertFalse(membership.is_pending)
147 self.assertTrue(membership.is_approved)
148 self.assertFalse(membership.is_rejected)
149 self.assertEqual(membership.status, "Approved on {}".format(membership.approved_on.date()))
151 # Check an email has been sent
152 self.assertEqual(len(mail.outbox), 1)
153 self.assertEqual(mail.outbox[0].to, [self.user.email])
154 self.assertIn("approved", mail.outbox[0].subject)
155 self.assertIn("approved", mail.outbox[0].body)
156 self.assertIn(self.period.short_name, mail.outbox[0].body)
158 def test_rejected(self):
159 membership = Membership(period=self.period, user_profile=self.user.profile)
161 # Check that we do not allow a rejection without a reason
162 self.assertRaisesMessage(ValueError, 'A membership cannot be rejected without a reason',
163 membership.reject, reason=None)
164 self.assertRaisesMessage(ValueError, 'A membership cannot be rejected without a reason',
165 membership.reject, reason='')
167 membership.reject('No real reason')
169 self.assertEqual(str(membership),
170 " <user@provider.com>'s membership for the period 2018-2019 ({})".format(membership.status))
172 # Check that we do not allow double rejection
173 self.assertRaisesMessage(ValueError, 'The membership has already been rejected',
174 membership.reject, reason='No real reason')
176 self.assertFalse(membership.is_pending)
177 self.assertFalse(membership.is_approved)
178 self.assertTrue(membership.is_rejected)
179 self.assertEqual(membership.status, "Rejected on {}".format(membership.rejected_on.date()))
181 # Check an email has been sent
182 self.assertEqual(len(mail.outbox), 1)
183 self.assertEqual(mail.outbox[0].to, [self.user.email])
184 self.assertIn("rejected", mail.outbox[0].subject)
185 self.assertIn("declined", mail.outbox[0].body)
186 self.assertIn(membership.rejection_reason, mail.outbox[0].body)
187 self.assertIn(self.period.short_name, mail.outbox[0].body)
189 def test_approved_and_rejected(self):
190 membership = Membership(approved_on=timezone.now(), rejected_on=timezone.now())
191 self.assertFalse(membership.is_pending)
192 self.assertFalse(membership.is_approved)
193 self.assertTrue(membership.is_rejected)
194 self.assertEqual(membership.status, "Rejected on {}".format(membership.rejected_on.date()))
196 @patch('gui.models.Membership.objects.filter')
197 def test_members(self, filter_mocked):
198 memberships = Membership.pending_memberships()
199 filter_mocked.assert_called_with(approved_on=None, rejected_on=None)
200 filter_mocked.return_value.order_by.assert_called_with('id')
201 self.assertEqual(memberships, filter_mocked.return_value.order_by.return_value)
203 @patch('gui.models.Membership.is_pending', new_callable=PropertyMock)
204 @patch('gui.models.Membership.is_approved', new_callable=PropertyMock)
205 @patch('gui.models.Membership.is_rejected', new_callable=PropertyMock)
206 def test_unknown_state(self, rejected_mock, approved_mock, pending_mock):
207 rejected_mock.return_value = False
208 approved_mock.return_value = False
209 pending_mock.return_value = False
211 membership = Membership(approved_on=timezone.now(), rejected_on=timezone.now())
212 with self.assertRaises(ValueError, msg="Unknown state"):
213 membership.status
216class ProfileTests(TestCase):
217 def create_user(self, username, first_name="First", last_name="Last", email='a@x.org',
218 affiliation='', public_statement=''):
219 user = get_user_model().objects.create(username=username, first_name="First",
220 last_name="Last", email='a@x.org')
221 user.profile.affiliation = affiliation
222 user.public_statement = public_statement
223 user.save()
224 return user
226 def setUp(self):
227 self.user = self.create_user(username='user')
228 self.profile = self.user.profile
230 def test_membership__no_period(self):
231 self.assertEqual(self.profile.membership, None)
233 def test_membership__with_active_period_but_no_membership_application(self):
234 MembershipPeriod.objects.create(start=timezone.now() - timedelta(hours=1),
235 end=timezone.now() + timedelta(hours=2))
236 self.assertEqual(self.profile.membership, None)
238 def test_membership__with_active_period_and_membership_application(self):
239 period = MembershipPeriod.objects.create(start=timezone.now() - timedelta(hours=1),
240 end=timezone.now() + timedelta(hours=2))
241 membership = Membership.objects.create(period=period, user_profile=self.profile)
242 self.assertEqual(self.profile.membership, membership)
244 def test_last_membership__no_periods(self):
245 self.assertEqual(self.profile.last_membership, None)
247 def test_last_membership__with_periods(self):
248 period1 = MembershipPeriod.objects.create(short_name="p1", start=timezone.now(),
249 end=timezone.now() + timedelta(hours=2))
250 period2 = MembershipPeriod.objects.create(short_name="p2", start=timezone.now(),
251 end=timezone.now() + timedelta(hours=2))
252 period3 = MembershipPeriod.objects.create(short_name="p3", start=timezone.now(),
253 end=timezone.now() + timedelta(hours=2))
255 # Create 3 memberships, but the last one pending approval by administrators
256 Membership.objects.create(period=period1, user_profile=self.profile, approved_on=timezone.now())
257 Membership.objects.create(period=period2, user_profile=self.profile, approved_on=timezone.now())
258 Membership.objects.create(period=period3, user_profile=self.profile)
260 self.assertTrue(self.profile.last_membership, period2)
262 @patch('gui.models.Ballot.active_ballots', return_value=[MagicMock(), MagicMock(), MagicMock()])
263 def test_active_ballots(self, ballots_mocked):
264 self.profile.membership = MagicMock()
266 ballots = self.profile.active_ballots
268 self.assertEqual(len(ballots), 3)
269 for i, res in enumerate(ballots):
270 ballot, has_voted = res
271 self.assertEqual(ballot, ballots_mocked.return_value[i])
272 ballot.has_voted.assert_called_with(self.profile.membership)
273 self.assertEqual(has_voted, ballot.has_voted.return_value)
275 def test_str(self):
276 self.assertEqual(str(self.profile), "First Last <a@x.org>")
278 def test_existing_affiliations(self):
279 for i, affiliation in enumerate(['Intel', 'Samsung', 'Suse', 'Red Hat', '', 'Intel',
280 'Canonical']):
281 self.create_user(username="user{}".format(i), affiliation=affiliation)
283 self.assertEqual(Profile.existing_affiliations(),
284 ['Canonical', 'Intel', 'Red Hat', 'Samsung', 'Suse'])
286 def test_send_email(self):
287 self.profile.send_email("My subject", "My message")
289 self.assertEqual(len(mail.outbox), 1)
290 self.assertEqual(mail.outbox[0].to, [self.user.email])
291 self.assertTrue("My subject" in mail.outbox[0].subject)
292 self.assertTrue("My message" in mail.outbox[0].body)
295class CP_AdminTasksCountTests(TestCase):
296 @patch('gui.models.Membership.pending_memberships',
297 return_value=MagicMock(count=MagicMock(return_value=42)))
298 def test_admin_tasks_count__normal_user(self, pending_memberships_mock):
299 request = MagicMock(user=MagicMock(is_superuser=False))
300 self.assertEqual(admin_tasks_count(request),
301 {"admin_tasks_count": 0})
303 @patch('gui.models.Membership.pending_memberships',
304 return_value=MagicMock(count=MagicMock(return_value=42)))
305 def test_admin_tasks_count__superuser(self, pending_memberships_mock):
306 request = MagicMock(user=MagicMock(is_superuser=True))
307 self.assertEqual(admin_tasks_count(request),
308 {"admin_tasks_count": 42})
311class CP_global_context(TestCase):
312 @patch('gui.models.MembershipPeriod.current_period',
313 return_value=MagicMock())
314 def test_current_period(self, current_period_mock):
315 self.assertEqual(global_context(None).get("current_membership_period"),
316 current_period_mock.return_value)
317 current_period_mock.assert_called_with()
319 def test_website(self):
320 website = global_context(None).get("website")
322 with patch.dict('os.environ', {}, clear=True):
323 self.assertEqual(website.version, None)
325 with patch.dict('os.environ', {"WEBSITE_VERSION": "1234"}, clear=True):
326 self.assertEqual(website.version, "1234")
327 self.assertTrue(website.project_url.startswith("https://"))
328 self.assertIn(website.project_url, website.version_url)
329 self.assertIn(website.version, website.version_url)
332class ProfileFormTests(TestCase):
333 def test_empty_form(self):
334 form = ProfileForm({})
336 user = MagicMock()
337 form.save(user)
339 self.assertFalse(form.is_valid())
340 user.save.assert_not_called()
342 def test_all_but_affiliation(self):
343 form = ProfileForm({'first_name': 'First', 'last_name': 'Last', 'public_statement': 'My statement'})
345 user = MagicMock()
346 form.save(user)
348 self.assertTrue(form.is_valid())
349 self.assertEqual(user.first_name, 'First')
350 self.assertEqual(user.last_name, 'Last')
351 self.assertEqual(user.profile.affiliation, '')
352 self.assertEqual(user.profile.public_statement, 'My statement')
353 user.save.assert_called_with()
356class MembershipApplicationFormTests(TestCase):
357 def setUp(self):
358 self.user = get_user_model().objects.create(first_name="First", last_name="Last", email='a@x.org')
359 self.period = MembershipPeriod.objects.create(start=timezone.now(),
360 end=timezone.now() + timedelta(hours=2))
362 @patch('gui.models.Membership.objects.create')
363 def test_empty_form(self, create_mock):
364 form = MembershipApplicationForm({})
365 self.assertFalse(form.save())
367 self.assertFalse(form.is_valid())
368 create_mock.assert_not_called()
370 @patch('gui.models.Membership.objects.create')
371 def test_not_agreeing_to_membership(self, create_mock):
372 form = MembershipApplicationForm({"period_id": self.period.id, "user_id": self.user.id,
373 "public_statement": "My statement", "agree_membership": False})
374 self.assertFalse(form.save())
376 self.assertFalse(form.is_valid())
377 self.assertEqual(get_user_model().objects.get(pk=self.user.id).profile.public_statement, "")
378 create_mock.assert_not_called()
380 @patch('gui.models.Membership.objects.create')
381 def test_all_valid(self, create_mock):
382 form = MembershipApplicationForm({"period_id": self.period.id, "user_id": self.user.id,
383 "public_statement": "My statement", "agree_membership": True})
384 self.assertTrue(form.save())
386 self.assertTrue(form.is_valid(), form.errors)
387 self.assertEqual(get_user_model().objects.get(pk=self.user.id).profile.public_statement, "My statement")
388 create_mock.assert_called_with(period=self.period, user_profile=self.user.profile)
391class GuiViewMixin:
392 def setUpGuiTests(self, url, verified_user_needed=False, superuser_needed=False):
393 self.url = url
394 self.verified_user_needed = verified_user_needed or superuser_needed
395 self.superuser_needed = superuser_needed
397 if os.environ.get('VALIDATE_HTML') is not None:
398 self.client = ValidatingClient() # pragma: no cover
400 # HACK: Massively speed up the login primitive. We don't care about security in tests
401 settings.PASSWORD_HASHERS = ('django.contrib.auth.hashers.MD5PasswordHasher', )
403 def create_user_and_log_in(self, admin=False, verified=False):
404 return create_user_and_log_in(self.client, verified=verified, admin=admin)
406 def do_POST(self, params):
407 response = self.client.post(self.url, params)
408 self.assertEqual(response.status_code, 302)
409 return self.client.get(response.url, {})
411 def test_unauthenticated_user(self):
412 response = self.client.get(self.url)
414 if self.verified_user_needed:
415 self.assertEqual(response.status_code, 302)
416 else:
417 self.assertEqual(response.status_code, 200)
419 # Base.html checks
420 if not self.verified_user_needed:
421 self.assertContains(response, ">Sign In</a>")
422 self.assertContains(response, ">Sign Up</a>")
423 self.assertNotContains(response, '>Admin <span class="badge">')
425 self.additional_unauthenticated_checks(response)
427 def test_logged_in_unverified_user(self):
428 self.create_user_and_log_in(admin=False, verified=False)
430 # make sure no email is getting send, which slows down tests
431 with self.settings(EMAIL_BACKEND='django.core.mail.backends.dummy.EmailBackend'):
432 response = self.client.get(self.url)
433 self.assertEqual(response.status_code, 200)
435 # Base.html checks
436 if not self.verified_user_needed:
437 self.assertContains(response, ">Your Profile</a>")
438 self.assertContains(response, ">Change E-mail</a>")
439 self.assertContains(response, ">Sign Out</a>")
440 self.assertNotContains(response, '>Admin <span class="badge">')
442 self.additional_logged_in_user_checks(response)
444 return response
446 def test_logged_in_verified_user(self):
447 self.create_user_and_log_in(admin=False, verified=True)
448 response = self.client.get(self.url)
449 self.assertEqual(response.status_code, 403 if self.superuser_needed else 200)
451 # Base.html checks
452 if not self.superuser_needed:
453 self.assertContains(response, ">Your Profile</a>")
454 self.assertContains(response, ">Change E-mail</a>")
455 self.assertContains(response, ">Sign Out</a>")
456 self.assertNotContains(response, '>Admin <span class="badge">')
458 self.additional_logged_in_user_checks(response)
460 return response
462 @patch('gui.models.Membership.pending_memberships',
463 return_value=MagicMock(count=MagicMock(return_value=42)))
464 def test_logged_in_admin(self, admin_tasks_count_mocked):
465 self.create_user_and_log_in(admin=True, verified=True)
466 response = self.client.get(self.url)
467 self.assertEqual(response.status_code, 200)
469 # Base.html checks
470 self.assertContains(response, ">Your Profile</a>")
471 self.assertContains(response, ">Change E-mail</a>")
472 self.assertContains(response, ">Sign Out</a>")
473 self.assertContains(response, '>Admin <span class="badge">')
474 self.assertContains(response, '>Approve memberships <span class="badge">42</span>')
476 self.additional_admin_checks(response)
478 return response
480 # To be overriden
481 def additional_unauthenticated_checks(self, response):
482 pass
484 def additional_logged_in_user_checks(self, response):
485 pass
487 def additional_admin_checks(self, response):
488 pass
491class ViewIndexTests(TestCase, GuiViewMixin):
492 def setUp(self):
493 self.setUpGuiTests(reverse('index'))
495 def create_valid_member(self):
496 user = self.create_user_and_log_in(verified=True)
497 period = MembershipPeriod.objects.create(short_name="2018-2019", start=timezone.now(),
498 end=timezone.now() + timedelta(hours=2))
499 membership = Membership.objects.create(period=period, user_profile=user.profile,
500 approved_on=timezone.now())
501 return membership
503 def test_check_links(self):
504 # Create a set of links and their corresponding HTML
505 links = []
506 for i in range(0, 10):
507 short_name = "Link_{}".format(i)
508 url = "http://link-{}.com".format(i)
509 Link.objects.create(short_name=short_name, index_position=10-i, url=url,
510 description="My desc")
511 links.append('<a href="{}" title="My desc">{}</a>'.format(url, short_name))
513 # Now check that everything is alright
514 response = self.client.get(reverse('index'))
515 self.assertEqual(response.status_code, 200)
516 self.assertEqual(list(response.context['links']),
517 list(Link.objects.all().order_by("index_position")))
518 for link in links:
519 self.assertContains(response, link)
521 def additional_unauthenticated_checks(self, response):
522 self.assertContains(response, "Log-in or Sign-up")
524 self.assertNotContains(response, 'href="{}"'.format(reverse('members')))
526 def additional_logged_in_user_checks(self, response):
527 self.assertContains(response, 'role="button">Apply</a>')
529 self.assertNotContains(response, 'href="{}"'.format(reverse('members')))
531 def additional_admin_checks(self, response):
532 self.additional_logged_in_user_checks(response)
534 def test_user_with_pending_membership(self):
535 user = self.create_user_and_log_in(verified=True)
536 period = MembershipPeriod.objects.create(short_name="2018-2019", start=timezone.now(),
537 end=timezone.now() + timedelta(hours=2))
538 Membership.objects.create(period=period, user_profile=user.profile)
540 response = self.client.get(self.url)
541 self.assertEqual(response.status_code, 200)
542 self.assertContains(response, "Thanks for applying, your application is being processed!")
544 self.assertNotContains(response, 'href="{}"'.format(reverse('members')))
546 def test_user_with_active_membership(self):
547 self.create_valid_member()
549 response = self.client.get(self.url)
550 self.assertEqual(response.status_code, 200)
551 self.assertContains(response, "Your application has been approved, thanks for being a member!")
553 self.assertContains(response, 'href="{}"'.format(reverse('members')))
555 def test_user_with_rejected_membership(self):
556 user = self.create_user_and_log_in(verified=True)
557 period = MembershipPeriod.objects.create(short_name="2018-2019", start=timezone.now(),
558 end=timezone.now() + timedelta(hours=2))
559 Membership.objects.create(period=period, user_profile=user.profile,
560 rejected_on=timezone.now(), rejection_reason="You\nare\na\ntroll")
562 response = self.client.get(self.url)
563 self.assertEqual(response.status_code, 200)
564 self.assertContains(response, "<p>Your application has been rejected. Reason:</p>")
565 self.assertContains(response, "<p>You<br>are<br>a<br>troll</p>")
567 self.assertNotContains(response, 'href="{}"'.format(reverse('members')))
569 def test_open_ballot_not_voted_yet(self):
570 self.create_valid_member()
571 ballot = Ballot.objects.create(short_name='2019-2020', description="Ballot description 42",
572 opening_on=timezone.now(),
573 closing_on=timezone.now() + timedelta(hours=1))
575 # Check that the page has a link to the active ballot
576 response = self.client.get(self.url)
577 self.assertContains(response, "Active ballots (1)")
578 self.assertContains(response, ballot.short_name)
579 self.assertContains(response, ballot.description)
580 self.assertContains(response, reverse('ballot-vote', kwargs={"pk": ballot.id}))
582 @patch('gui.models.Ballot.has_voted', return_value=True)
583 def test_open_ballot_has_already_voted(self, has_voted_mocked):
584 self.create_valid_member()
585 ballot = Ballot.objects.create(short_name='2019-2020', description="Ballot description 42",
586 opening_on=timezone.now(),
587 closing_on=timezone.now() + timedelta(hours=1))
589 # Check that the page has a link to the active ballot
590 response = self.client.get(self.url)
591 self.assertContains(response, "Active ballots (1)")
592 self.assertContains(response, ballot.short_name)
593 self.assertContains(response, ballot.description)
594 self.assertContains(response, " (voted)")
595 self.assertNotContains(response, reverse('ballot-vote', kwargs={"pk": ballot.id}))
597 def test_closed_ballot(self):
598 self.create_valid_member()
599 ballot = Ballot.objects.create(short_name='2019-2020', description="Ballot description 42",
600 opening_on=timezone.now(), closing_on=timezone.now())
602 # Check that the page has a link to the active ballot
603 response = self.client.get(self.url)
604 self.assertNotContains(response, "Active ballots")
605 self.assertNotContains(response, ballot.short_name)
606 self.assertNotContains(response, ballot.description)
607 self.assertNotContains(response, reverse('ballot-vote', kwargs={"pk": ballot.id}))
610class ViewMembersTests(TestCase, GuiViewMixin):
611 def setUp(self):
612 self.setUpGuiTests(reverse('members'), verified_user_needed=True)
614 self.period = MembershipPeriod.objects.create(short_name="2018-2019", start=timezone.now(),
615 end=timezone.now() + timedelta(hours=2))
617 def _create_membership(self, admin=False, verified=True):
618 self.user = create_user_and_log_in(self.client, verified=verified, admin=admin)
619 self.membership = Membership.objects.create(period=self.period, user_profile=self.user.profile)
620 return self.membership
622 def create_user_and_log_in(self, admin=False, verified=True):
623 self._create_membership(admin=admin, verified=verified).approve()
624 return self.user
626 def test_membership_pending(self):
627 self._create_membership()
628 response = self.client.get(self.url)
629 self.assertEqual(response.status_code, 403)
631 def test_rejected_member(self):
632 self._create_membership().reject("No reason")
633 response = self.client.get(self.url)
634 self.assertEqual(response.status_code, 403)
636 def test_proper_member(self):
637 self._create_membership().approve()
638 response = self.client.get(self.url)
639 self.assertEqual(response.status_code, 200)
641 @patch('gui.models.MembershipPeriod.members')
642 def test_no_members(self, members_mock):
643 response = self.test_logged_in_verified_user()
645 # Check that the members list has been generated with the right calls
646 members_mock.order_by.assert_called_with("user_profile__user__last_name")
647 members_mock.order_by.return_value.select_related.assert_called_with('user_profile',
648 'user_profile__user')
649 self.assertEqual(response.context['object_list'],
650 members_mock.order_by.return_value.select_related.return_value)
652 self.assertContains(response, "No members yet.")
654 def __setup_db(self):
655 for i in range(0, 3):
656 username = "user_{}".format(i)
657 user = get_user_model().objects.create(first_name=username, last_name="last",
658 username=username)
659 user.is_superuser = (i == 1)
660 user.profile.affiliation = "" if i == 0 else "Intel"
661 user.profile.public_statement = "My public\nstatement"
662 user.save()
663 Membership.objects.create(period=self.period, user_profile=user.profile, approved_on=timezone.now())
665 def test_with_members_as_member(self):
666 self.__setup_db()
668 response = self.test_logged_in_verified_user()
669 self.assertContains(response, "<td>user_0 last</td>")
670 self.assertContains(response, "<td>user_1 last</td>")
671 self.assertContains(response, "<td>user_2 last</td>")
672 self.assertContains(response, "<td>Unknown</td>")
673 self.assertContains(response, "<td>Intel</td>")
674 self.assertContains(response, "<td><p>My public<br>statement</p></td>")
676 # Verify that the administrator-only actions are not visible
677 self.assertNotContains(response, "<th>Action</th>")
678 self.assertNotContains(response, 'value="Make Admin"')
679 self.assertNotContains(response, 'value="Make User"')
680 self.assertNotContains(response, "No actions available")
682 def test_with_members_as_admin(self):
683 self.__setup_db()
685 # Verify that the administrator-only actions are visible
686 response = self.test_logged_in_admin()
687 self.assertContains(response, "<th>Action</th>")
688 self.assertContains(response, 'value="Make Admin"')
689 self.assertContains(response, 'value="Make User"')
690 self.assertContains(response, '<form action="/members/1/make/admin" method="post">')
691 self.assertContains(response, '<form action="/members/2/make/user" method="post">')
692 self.assertContains(response, '<form action="/members/3/make/admin" method="post">')
694 self.assertContains(response, "No actions available")
695 self.assertNotContains(response, '<form action="/members/{}/make/user" method="post">'.format(self.user.id))
698class ViewChangeUserStatus(TestCase):
699 def setUp(self):
700 self.user = get_user_model().objects.create_user('not_admin', 'user@provider.com', 'pwd',
701 is_superuser=False)
703 def test_get_and_admin(self):
704 create_user_and_log_in(self.client, admin=True, verified=True)
705 response = self.client.get(reverse('members-user-to-admin', kwargs={"pk": self.user.id}))
706 self.assertEqual(response.status_code, 405)
708 def test_non_admin(self):
709 create_user_and_log_in(self.client, admin=False, verified=True)
710 response = self.client.post(reverse('members-user-to-admin', kwargs={"pk": self.user.id}))
711 self.assertEqual(response.status_code, 403)
713 def test_admin_post_with_referrer(self):
714 admin = create_user_and_log_in(self.client, admin=True, verified=True)
715 referrer = 'http://my.website.com/foo/bar'
717 self.assertFalse(get_user_model().objects.get(pk=self.user.id).is_superuser)
718 self.assertFalse(get_user_model().objects.get(pk=self.user.id).is_staff)
719 response = self.client.post(reverse('members-user-to-admin', kwargs={"pk": self.user.id}),
720 {}, HTTP_REFERER=referrer)
721 self.assertEqual(response.status_code, 302)
722 self.assertEqual(response.url, referrer)
723 self.assertTrue(get_user_model().objects.get(pk=self.user.id).is_superuser)
724 self.assertTrue(get_user_model().objects.get(pk=self.user.id).is_staff)
726 # Check an email has been sent
727 self.assertEqual(len(mail.outbox), 1)
728 self.assertEqual(mail.outbox[0].to, [self.user.email])
729 self.assertIn("Your status changed from 'user' to 'administrator'", mail.outbox[0].subject)
730 self.assertIn("'user' to 'administrator'", mail.outbox[0].body)
731 self.assertIn(admin.get_full_name(), mail.outbox[0].body)
733 response = self.client.post(reverse('members-admin-to-user', kwargs={"pk": self.user.id}),
734 {}, HTTP_REFERER=referrer)
735 self.assertEqual(response.status_code, 302)
736 self.assertEqual(response.url, referrer)
737 self.assertFalse(get_user_model().objects.get(pk=self.user.id).is_superuser)
739 # Check an email has been sent
740 self.assertEqual(len(mail.outbox), 2)
741 self.assertEqual(mail.outbox[1].to, [self.user.email])
742 self.assertIn("Your status changed from 'administrator' to 'user'", mail.outbox[1].subject)
743 self.assertIn("'administrator' to 'user'", mail.outbox[1].body)
744 self.assertIn(admin.get_full_name(), mail.outbox[1].body)
746 def test_admin_change_its_own_status(self):
747 user = create_user_and_log_in(self.client, admin=True, verified=True)
748 response = self.client.post(reverse('members-admin-to-user', kwargs={"pk": user.id}))
749 self.assertEqual(response.status_code, 302)
750 self.assertTrue(get_user_model().objects.get(pk=user.id).is_superuser)
753class ViewProfileTests(TestCase, GuiViewMixin):
754 def setUp(self):
755 self.setUpGuiTests(reverse('account-profile'), verified_user_needed=True)
757 def test_post_invalid_form(self):
758 self.create_user_and_log_in(verified=True)
759 response = self.do_POST({})
760 self.assertContains(response, "Your profile is invalid")
762 def test_post_valid_form(self):
763 self.create_user_and_log_in(verified=True)
764 response = self.do_POST({"first_name": "First",
765 "last_name": "Last",
766 "public_statement": "Statement"})
767 self.assertContains(response, "Your profile was updated successfully")
769 @patch('gui.models.Profile.existing_affiliations', return_value=['Collabora', 'Intel',
770 'Google', 'Samsung'])
771 def test_existing_affiliations_list(self, existing_affiliations_mock):
772 self.create_user_and_log_in(verified=True)
773 response = self.client.get(self.url)
775 self.assertContains(response, '<datalist id="list__affiliations-list"><option value="Collabora">'
776 '<option value="Intel"><option value="Google"><option value="Samsung"></datalist>')
779class ViewDeleteAccountTests(TestCase, GuiViewMixin):
780 def setUp(self):
781 self.setUpGuiTests(reverse('account-delete'), verified_user_needed=True)
783 def test_do_delete(self):
784 self.create_user_and_log_in(verified=True)
785 response = self.do_POST({})
786 self.assertContains(response, 'Your account has been deleted successfully')
789class ViewMembershipApplicationTests(TestCase, GuiViewMixin):
790 def setUp(self):
791 self.setUpGuiTests(reverse('membership-application'), verified_user_needed=True)
793 def test_no_periods(self):
794 response = self.test_logged_in_admin()
795 self.assertContains(response, "No membership period has been created.")
797 def test_with_period(self):
798 MembershipPeriod.objects.create(short_name="2018-2019", start=timezone.now(),
799 end=timezone.now() + timedelta(hours=2))
801 response = self.test_logged_in_admin()
802 self.assertContains(response, "Membership application - 2018-2019")
804 def test_user_with_current_membership(self):
805 user = self.create_user_and_log_in(verified=True, admin=True)
806 period = MembershipPeriod.objects.create(short_name="2018-2019", start=timezone.now(),
807 end=timezone.now() + timedelta(hours=2))
808 Membership.objects.create(period=period, user_profile=user.profile)
810 response = self.client.get(self.url)
811 self.assertEqual(response.status_code, 200)
813 self.assertContains(response, "You already applied to the period 2018-2019. Current status: Pending approval")
815 def test_post_invalid_form(self):
816 self.create_user_and_log_in(verified=True, admin=True)
817 MembershipPeriod.objects.create(short_name="2018-2019", start=timezone.now(),
818 end=timezone.now() + timedelta(hours=2))
819 response = self.do_POST({})
820 self.assertContains(response, "Your application is invalid")
822 def test_post_invalid_form_no_agreement(self):
823 user = self.create_user_and_log_in(verified=True, admin=True)
824 period = MembershipPeriod.objects.create(short_name="2018-2019", start=timezone.now(),
825 end=timezone.now() + timedelta(hours=2))
827 response = self.do_POST({"period_id": period.id,
828 "user_id": user.id,
829 "public_statement": "My statement",
830 "agree_membership": False})
831 self.assertContains(response, "Your application is invalid")
833 def test_post_valid_form(self):
834 user = self.create_user_and_log_in(verified=True, admin=True)
835 period = MembershipPeriod.objects.create(short_name="2018-2019", start=timezone.now(),
836 end=timezone.now() + timedelta(hours=2))
838 response = self.do_POST({"period_id": period.id,
839 "user_id": user.id,
840 "public_statement": "My statement",
841 "agree_membership": True})
842 self.assertContains(response, "Your application was sent successfully and will be reviewed shortly")
844 # Check that an email has been sent to the admins
845 self.assertEqual(len(mail.outbox), 1)
846 self.assertIn(user.email, mail.outbox[0].to)
847 self.assertIn(reverse('membership-approval'), mail.outbox[0].body)
850class MembershipPeriodCreateViewTests(TestCase, GuiViewMixin):
851 def setUp(self):
852 self.setUpGuiTests(reverse('membership-period-create'), superuser_needed=True)
854 @patch('gui.models.MembershipPeriod.current_period',
855 return_value=MagicMock(short_name="2042-2043", description="Period description",
856 agreement_url="https://x.org/agreement.pdf",
857 start=timezone.now(),
858 end=timezone.now() + timedelta(hours=2)))
859 def test_default_values(self, period_mocked):
860 self.create_user_and_log_in(verified=True, admin=True)
861 response = self.client.get(self.url, {})
862 self.assertContains(response, period_mocked.return_value.short_name)
863 self.assertContains(response, period_mocked.return_value.description)
864 self.assertContains(response, period_mocked.return_value.agreement_url)
865 self.assertContains(response, period_mocked.return_value.start.strftime("%Y-%m-%d %H:%M:%S"))
867 def test_post_invalid_form(self):
868 self.create_user_and_log_in(verified=True, admin=True)
869 response = self.client.post(self.url, {})
870 self.assertEqual(response.request.get('PATH_INFO'), self.url)
871 self.assertContains(response, "has-error")
873 def test_post_valid_form(self):
874 self.create_user_and_log_in(verified=True, admin=True)
875 response = self.client.post(self.url, {"short_name": "My period",
876 'description': "My description",
877 "agreement_url": "https://x.org",
878 "start": '2018-01-01',
879 "end": '2019-01-01'})
880 self.assertEqual(response.status_code, 302)
881 self.assertEqual(response.url, reverse('index'))
884class ViewMembershipApprovalTests(TestCase, GuiViewMixin):
885 def setUp(self):
886 self.setUpGuiTests(reverse('membership-approval'), superuser_needed=True)
888 def test_post_non_integer_ids(self):
889 self.create_user_and_log_in(admin=True, verified=True)
890 response = self.do_POST({'ids': ["0", "1", "2", "a", "d"]})
891 self.assertContains(response, "ERROR: One or more IDs are not integers...")
893 def test_post_invalid_action(self):
894 self.create_user_and_log_in(admin=True, verified=True)
895 response = self.do_POST({'ids': [], "action": "invalid"})
896 self.assertContains(response, "ERROR: Unsupported action...")
898 def test_post_approve_action(self):
899 user = self.create_user_and_log_in(admin=True, verified=True)
900 period1 = MembershipPeriod.objects.create(short_name="2017-2018", start=timezone.now(),
901 end=timezone.now() + timedelta(hours=2))
902 period2 = MembershipPeriod.objects.create(short_name="2018-2019", start=timezone.now(),
903 end=timezone.now() + timedelta(hours=2))
904 membership1 = Membership.objects.create(period=period1, user_profile=user.profile)
905 membership2 = Membership.objects.create(period=period2, user_profile=user.profile)
907 response = self.do_POST({'ids': [membership1.id, membership2.id], "action": "approve"})
908 self.assertContains(response, "You approved 2 membership applications")
910 def test_post_reject_action_no_reasons(self):
911 user = self.create_user_and_log_in(admin=True, verified=True)
912 period1 = MembershipPeriod.objects.create(short_name="2017-2018", start=timezone.now(),
913 end=timezone.now() + timedelta(hours=2))
914 period2 = MembershipPeriod.objects.create(short_name="2018-2019", start=timezone.now(),
915 end=timezone.now() + timedelta(hours=2))
916 membership1 = Membership.objects.create(period=period1, user_profile=user.profile)
917 membership2 = Membership.objects.create(period=period2, user_profile=user.profile)
919 response = self.do_POST({'ids': [membership1.id, membership2.id], "action": "reject"})
920 expected = "Can't refuse First Last <user@provider.com>'s application without a reason"
921 self.assertContains(response, expected)
923 def test_post_reject_action_with_reasons(self):
924 user = self.create_user_and_log_in(admin=True, verified=True)
925 period1 = MembershipPeriod.objects.create(short_name="2017-2018", start=timezone.now(),
926 end=timezone.now() + timedelta(hours=2))
927 period2 = MembershipPeriod.objects.create(short_name="2018-2019", start=timezone.now(),
928 end=timezone.now() + timedelta(hours=2))
929 membership1 = Membership.objects.create(period=period1, user_profile=user.profile)
930 membership2 = Membership.objects.create(period=period2, user_profile=user.profile)
932 response = self.do_POST({'ids': [membership1.id, membership2.id],
933 'reject_reason_{}'.format(membership1.id): "Reason 1",
934 'reject_reason_{}'.format(membership2.id): "Reason 2",
935 "action": "reject"})
936 self.assertContains(response, "You rejected 2 membership applications")
938 def test_with_many_applications(self):
939 # Create a ton of membership to check that the pagination is working
940 period = MembershipPeriod.objects.create(short_name="2018-2019", start=timezone.now(),
941 end=timezone.now() + timedelta(hours=2))
942 for i in range(30):
943 user = get_user_model().objects.create_user('user_{}'.format(i))
944 Membership.objects.create(period=period, user_profile=user.profile)
946 self.create_user_and_log_in(admin=True, verified=True)
948 response = self.client.get(self.url)
949 self.assertNotContains(response, 'name="ids" value="11"')
951 response = self.client.get(self.url + "?page=2")
952 self.assertContains(response, 'name="ids" value="11"')
954 response = self.client.get(self.url + "?page=3")
955 self.assertNotContains(response, 'name="ids" value="11"')
958class ViewAboutTests(TestCase, GuiViewMixin):
959 def setUp(self):
960 self.setUpGuiTests(reverse('about'))
963class BallotTests(TestCase):
964 fixtures = ['ballots.yaml']
966 @patch('gui.models.Ballot.objects.filter')
967 def test_active_ballots(self, filter_mocked):
968 Ballot.active_ballots()
970 args, kwargs = filter_mocked.call_args_list[0]
971 self.assertLess(timezone.now() - kwargs['opening_on__lte'], timedelta(seconds=.1))
972 self.assertLess(timezone.now() - kwargs['closing_on__gt'], timedelta(seconds=.1))
974 def test_potential_voters(self):
975 ballot = Ballot.objects.get(id=1)
976 potential_voters = set()
977 for i in range(1, 5):
978 potential_voters.add(Membership.objects.get(id=i))
979 self.assertEqual(ballot.potential_voters, potential_voters)
981 def test_missing_voters(self):
982 ballot = Ballot.objects.get(id=1)
983 missing_voter = Membership.objects.get(id=4)
984 self.assertEqual(ballot.missing_voters, set([missing_voter]))
986 @patch('gui.models.MembershipPeriod.active_period_at', return_value=None)
987 def test_missing_voters__without_membershipPeriod(self, cur_period_mock):
988 ballot = Ballot.objects.get(id=1)
989 self.assertEqual(ballot.missing_voters, set())
991 def test_time_helpers(self):
992 now = timezone.now()
994 ballot_early = Ballot(opening_on=now+timedelta(hours=.5), closing_on=now+timedelta(hours=1))
995 self.assertFalse(ballot_early.is_active)
996 self.assertFalse(ballot_early.has_started)
997 self.assertFalse(ballot_early.has_closed)
999 ballot_active = Ballot(opening_on=now, closing_on=now+timedelta(hours=1))
1000 self.assertTrue(ballot_active.is_active)
1001 self.assertTrue(ballot_active.has_started)
1002 self.assertFalse(ballot_active.has_closed)
1004 ballot_over = Ballot(opening_on=now, closing_on=now)
1005 self.assertFalse(ballot_over.is_active)
1006 self.assertTrue(ballot_over.has_started)
1007 self.assertTrue(ballot_over.has_closed)
1009 @patch('gui.models.Ballot.voters')
1010 def test_has_voted(self, filter_mocked):
1011 membership = MagicMock()
1012 Ballot().has_voted(membership)
1014 filter_mocked.filter.assert_called_with(pk=membership.pk)
1015 filter_mocked.filter.return_value.exists.assert_called_with()
1017 def test_turnout(self):
1018 ballot = Ballot.objects.get(id=1)
1019 self.assertEqual(ballot.turnout, VoteCount(3, 4))
1021 def test_turnout_with_no_active_period(self):
1022 MembershipPeriod.current_period().delete()
1023 ballot = Ballot.objects.get(id=1)
1024 self.assertEqual(ballot.turnout, VoteCount(0, 0))
1026 def test_turnout_after_new_period_started(self):
1027 MembershipPeriod.objects.create(short_name="new period", start=timezone.now(),
1028 end=timezone.now() + timedelta(hours=1))
1029 ballot = Ballot.objects.get(id=1)
1030 self.assertEqual(ballot.turnout, VoteCount(3, 4))
1032 def test_send_reminder(self):
1033 ballot = Ballot.objects.get(id=1)
1035 ballot.send_reminder()
1037 self.assertEqual(len(mail.outbox), 1)
1038 self.assertEqual(mail.outbox[0].to, ['slacker@example.com'])
1040 self.assertIn("vote", mail.outbox[0].subject)
1041 self.assertIn('2019-2020', mail.outbox[0].body)
1043 self.assertIn("vote", mail.outbox[0].body)
1044 self.assertIn('2019-2020', mail.outbox[0].body)
1045 self.assertIn(reverse('ballot-vote', kwargs={"pk": ballot.id}), mail.outbox[0].body)
1047 def test_str(self):
1048 self.assertEqual(str(Ballot.objects.get(id=1)), "Ballot<2019-2020>")
1051class BallotFormTests(TestCase):
1052 def setUp(self):
1053 self.ballot = Ballot.objects.create(short_name='2019-2020', opening_on=timezone.now(),
1054 closing_on=timezone.now())
1056 # Create select questions
1057 self.select_answers = OrderedDict()
1058 for i in range(3):
1059 question = BallotSelectQuestion.objects.create(ballot=self.ballot,
1060 question='Select question #{}'.format(i))
1061 self.select_answers[question] = []
1062 for a in range(3):
1063 sqpa = BallotSelectQuestionPossibleAnswer.objects.create(question=question,
1064 answer="question {} answer {}".format(i, a))
1065 self.select_answers[question].append(sqpa)
1067 # Create ranking questions
1068 self.ranking_options = OrderedDict()
1069 for i in range(3):
1070 question = BallotRankingQuestion.objects.create(ballot=self.ballot,
1071 question='Ranking question #{}'.format(i))
1072 self.ranking_options[question] = []
1073 for o in range(3):
1074 option = BallotRankingQuestionOption.objects.create(question=question,
1075 option="ranking {} option {}".format(i, o))
1076 self.ranking_options[question].append(option)
1078 def test_ballot_not_specified(self):
1079 form = BallotForm()
1081 # Without a ballot, we should not have any fields
1082 self.assertEqual(sorted(form.fields.keys()), [])
1084 def test_dynamic_form_generation(self):
1085 form = BallotForm(ballot=self.ballot, data={})
1087 # Check that all the expected fields are here
1088 self.assertEqual(sorted(form.fields.keys()),
1089 ['ranking_0', 'ranking_1', 'ranking_2', 'select_0', 'select_1', 'select_2'])
1091 # Check that we created the select fields correctly
1092 for i in range(3):
1093 field_name = 'select_{}'.format(i)
1094 self.assertTrue(form.fields[field_name].required)
1095 self.assertEqual(form.fields[field_name].choices,
1096 [('', '-- Select an answer --'),
1097 (i * 3 + 1, 'question {} answer 0'.format(i)),
1098 (i * 3 + 2, 'question {} answer 1'.format(i)),
1099 (i * 3 + 3, 'question {} answer 2'.format(i))])
1100 self.assertEqual(form.fields[field_name].label, "Select question #{}".format(i))
1102 # Check that we created the ranking fields correctly
1103 for i in range(3):
1104 field_name = 'ranking_{}'.format(i)
1105 for o in range(3):
1106 ranking_field = form.fields[field_name].fields[o]
1108 # Check the field
1109 self.assertEqual(ranking_field.choices,
1110 [(i * 3 + 1, 'ranking {} option 0'.format(i)),
1111 (i * 3 + 2, 'ranking {} option 1'.format(i)),
1112 (i * 3 + 3, 'ranking {} option 2'.format(i)),
1113 (-1, '-- Abstain --')])
1114 self.assertEqual(ranking_field.label, str(o))
1116 # Check the associated widget
1117 widget = form.fields[field_name].widget.widgets[o]
1118 self.assertEqual(widget.get_context("", "", [])['widget'].get('rank'), o + 1)
1119 self.assertEqual(widget.choices,
1120 [('', '-- Select an option --')] + ranking_field.choices)
1121 self.assertEqual(widget.rank, o + 1)
1123 def test_OptionRankingWidget_decompress(self):
1124 form = BallotForm(ballot=self.ballot, data={})
1125 widget = form.fields["ranking_0"].widget
1126 self.assertEqual(widget.decompress(None), [None, None, None])
1127 self.assertEqual(widget.decompress(['toto', 'tata']), ['toto', 'tata'])
1129 def test_empty_form(self):
1130 form = BallotForm(ballot=self.ballot, data={})
1132 # Check that the form is invalid and that the save method fails too
1133 self.assertFalse(form.is_valid())
1134 self.assertFalse(form.save(MagicMock()))
1136 def test_get_objects__non_integer_ids(self):
1137 form = BallotForm(ballot=self.ballot, data={})
1138 form._get_objects("ranking_0", Ballot, ['1', 'toto', '2'])
1139 self.assertEqual(form.errors.get("ranking_0"), ['This field is required.',
1140 'The selected ids are not integer'])
1142 def test_get_objects__duplicated_ids(self):
1143 form = BallotForm(ballot=self.ballot, data={})
1144 form._get_objects("ranking_0", Ballot, ['1', '1', '2'])
1145 self.assertEqual(form.errors.get("ranking_0"), ['This field is required.',
1146 'Options cannot be duplicated'])
1148 def test_get_objects__multiple_abstain_ids_between_options(self):
1149 form = BallotForm(ballot=self.ballot, data={})
1150 form._get_objects("ranking_0", Ballot, ['1', '-1', '-1', 2])
1151 self.assertEqual(form.errors.get("ranking_0"), ['This field is required.',
1152 'Only the lowest-ranking options can be abstained'])
1154 def test_get_objects__non_existing_ids(self):
1155 form = BallotForm(ballot=self.ballot, data={})
1156 form._get_objects("ranking_0", Ballot, ['1', '2'])
1157 self.assertEqual(form.errors.get("ranking_0"), ['This field is required.',
1158 'At least one selected item is invalid'])
1160 def test_valid_form(self):
1161 form = BallotForm(ballot=self.ballot, data={
1162 'select_0': '1', 'select_1': '5', 'select_2': '9',
1163 'ranking_0_0': '1', 'ranking_0_1': '2', 'ranking_0_2': '-1',
1164 'ranking_1_0': '6', 'ranking_1_1': '5', 'ranking_1_2': '4',
1165 'ranking_2_0': '8', 'ranking_2_1': '7', 'ranking_2_2': '9',
1166 })
1168 # Check that the form is valid and that the save method fails too
1169 self.assertEqual(form.errors, {})
1170 self.assertTrue(form.is_valid())
1172 # Try saving the form now
1173 user = get_user_model().objects.create_user('user', 'user@provider.com', 'pwd')
1174 period = MembershipPeriod.objects.create(short_name='2018-2019', start=timezone.now(),
1175 end=timezone.now() + timedelta(hours=2))
1176 membership = Membership.objects.create(period=period, user_profile=user.profile)
1177 self.assertTrue(form.save(membership))
1179 # Check that the membership has been added to the list of voters
1180 self.assertEqual(list(self.ballot.voters.all()), [membership])
1182 # Check that the answers created are the ones we wanted
1183 for i in range(3):
1184 question = list(self.select_answers.keys())[i]
1185 answer = BallotSelectedAnswer.objects.get(member=membership, question=question)
1186 self.assertEqual(answer.answer, self.select_answers[question][i])
1188 # Check that the ranking created is the one we wanted
1189 expected_rank = {
1190 0: [1, 2, 3],
1191 1: [3, 2, 1],
1192 2: [2, 1, 3]
1193 }
1194 for q, question in enumerate(self.ranking_options.keys()):
1195 for o, option in enumerate(self.ranking_options[question]):
1196 # Convert the abstain vote to option 'None'
1197 if q == 0 and o == 2:
1198 option = None
1200 answer = BallotRankingAnswer.objects.get(member=membership, question=question, option=option)
1201 self.assertEqual(answer.rank, expected_rank[q][o])
1204class VoteCountTests(TestCase):
1205 def test_zero_votes(self):
1206 vote = VoteCount(0, 0)
1207 self.assertEqual(vote.percentage, 0)
1208 self.assertEqual(repr(vote), "VoteCount(0, 0)")
1209 self.assertEqual(str(vote), "0.0% (0 / 0)")
1211 def test_count_greater_than_totals(self):
1212 self.assertRaisesMessage(ValueError, 'The count of votes cannot be greater than the total amount of votes',
1213 VoteCount, 2, 1)
1215 def test_normal_case(self):
1216 vote = VoteCount(10, 20)
1217 self.assertEqual(vote.percentage, 50.0)
1218 self.assertEqual(repr(vote), "VoteCount(10, 20)")
1219 self.assertEqual(str(vote), "50.0% (10 / 20)")
1221 def test_equality(self):
1222 self.assertEqual(VoteCount(10, 20), VoteCount(10, 20))
1223 self.assertNotEqual(VoteCount(11, 20), VoteCount(10, 20))
1226class BallotSelectQuestionTests(TestCase):
1227 fixtures = ['ballots.yaml']
1229 def setUp(self):
1230 self.question = BallotSelectQuestion.objects.get(id=1)
1232 def test_tally(self):
1233 answer_yes = BallotSelectQuestionPossibleAnswer.objects.get(id=1)
1234 answer_no = BallotSelectQuestionPossibleAnswer.objects.get(id=2)
1236 results = self.question.tally
1237 self.assertEqual(list(results.keys()), [answer_yes, answer_no])
1238 self.assertEqual(results[answer_yes], VoteCount(2, 3))
1239 self.assertEqual(results[answer_no], VoteCount(1, 3))
1241 def test_tally_on_open_ballot(self):
1242 self.question.ballot.closing_on = timezone.now() + timedelta(hours=1)
1243 with self.assertRaises(ValueError, msg="No tally can be performed until the ballot has closed"):
1244 self.question.tally
1246 def test_str(self):
1247 self.assertEqual(str(self.question),
1248 "Ballot<2019-2020>: Do you accept the new bylaws?")
1251class BallotSelectQuestionPossibleAnswerTests(TestCase):
1252 fixtures = ['ballots.yaml']
1254 def test_str(self):
1255 self.assertEqual(str(BallotSelectQuestionPossibleAnswer.objects.get(id=1)),
1256 "Ballot<2019-2020>: Do you accept the new bylaws?: yes")
1259class BallotSelectedAnswerTests(TestCase):
1260 fixtures = ['ballots.yaml']
1262 def test_str(self):
1263 self.assertEqual(str(BallotSelectedAnswer.objects.get(id=1)),
1264 "Ballot<2019-2020>: Do you accept the new bylaws? - 'first.last@example.com' selected 'no'")
1267class OptionRankingVotesTests(TestCase):
1268 fixtures = ['ballots.yaml']
1270 def test_normal_case(self):
1271 option = BallotRankingQuestionOption.objects.get(id=1)
1272 votes = OptionRankingVotes(option, 4)
1274 self.assertEqual(votes.count_by_rank, [2, 1, 0, 0])
1275 self.assertEqual(votes.score, 11)
1276 self.assertEqual(str(votes),
1277 "Ballot<2019-2020>: Rank the following candidates for the X.Org 2019-2021 mandate: " +
1278 "Candidate 1 - score=11")
1281class BallotRankingQuestionTests(TestCase):
1282 fixtures = ['ballots.yaml']
1284 def setUp(self):
1285 self.question = BallotRankingQuestion.objects.get(id=1)
1287 def test_tally(self):
1288 tally = self.question.tally
1290 # Check that the scores are right
1291 self.assertEqual([t.score for t in tally], [11, 8, 6, 4])
1293 # Check that the candidates are in the right order
1294 candidate1 = BallotRankingQuestionOption.objects.get(id=1)
1295 candidate2 = BallotRankingQuestionOption.objects.get(id=2)
1296 candidate3 = BallotRankingQuestionOption.objects.get(id=3)
1297 candidate4 = BallotRankingQuestionOption.objects.get(id=4)
1298 self.assertEqual([t.ranking_option for t in tally], [candidate1, candidate3, candidate2, candidate4])
1300 def test_tally_on_open_ballot(self):
1301 self.question.ballot.closing_on = timezone.now() + timedelta(hours=1)
1302 with self.assertRaises(ValueError, msg="No tally can be performed until the ballot has closed"):
1303 self.question.tally
1305 def test_str(self):
1306 self.assertEqual(str(self.question),
1307 "Ballot<2019-2020>: Rank the following candidates for the X.Org 2019-2021 mandate")
1310class BallotRankingQuestionOptionTests(TestCase):
1311 fixtures = ['ballots.yaml']
1313 def test_str(self):
1314 self.assertEqual(str(BallotRankingQuestionOption.objects.get(id=1)),
1315 "Ballot<2019-2020>: Rank the following candidates for the X.Org 2019-2021 mandate: " +
1316 "Candidate 1")
1319class BallotRankingAnswerTests(TestCase):
1320 fixtures = ['ballots.yaml']
1322 def test_str(self):
1323 self.assertEqual(str(BallotRankingAnswer.objects.get(id=1)),
1324 "Ballot<2019-2020>: Rank the following candidates for the X.Org 2019-2021 mandate: " +
1325 "'first.last@example.com' - 'Candidate 1' was ranked 1st")
1327 self.assertEqual(str(BallotRankingAnswer.objects.get(id=12)),
1328 "Ballot<2019-2020>: Rank the following candidates for the X.Org 2019-2021 mandate: " +
1329 "'first.last3@example.com' - abstained for 4th rank")
1332class ViewBallotListTests(TestCase, GuiViewMixin):
1333 def setUp(self):
1334 self.setUpGuiTests(reverse('ballot-list'), superuser_needed=True)
1336 def test_with_ballots(self):
1337 for i in range(3):
1338 Ballot.objects.create(short_name="Ballot {}".format(i), opening_on=timezone.now(),
1339 closing_on=timezone.now())
1340 super().test_logged_in_admin()
1343class ViewVoteTests(TestCase, GuiViewMixin):
1344 def setUp(self):
1345 self.ballot = Ballot.objects.create(short_name='2019-2020', opening_on=timezone.now(),
1346 closing_on=timezone.now() + timedelta(hours=1))
1348 self.setUpGuiTests(reverse('ballot-vote', kwargs={"pk": self.ballot.id}), verified_user_needed=True)
1350 def _create_membership(self, admin=False, verified=True):
1351 period = MembershipPeriod.objects.create(short_name="2018-2019", start=timezone.now(),
1352 end=timezone.now() + timedelta(hours=2))
1353 self.user = create_user_and_log_in(self.client, verified=verified, admin=admin)
1354 self.membership = Membership.objects.create(period=period, user_profile=self.user.profile)
1355 return self.membership
1357 def create_user_and_log_in(self, admin=False, verified=True):
1358 self._create_membership(admin=admin, verified=verified).approve()
1359 return self.user
1361 def test_membership_pending(self):
1362 self._create_membership()
1363 response = self.client.get(self.url)
1364 self.assertEqual(response.status_code, 403)
1366 def test_rejected_member(self):
1367 self._create_membership().reject("No reason")
1368 response = self.client.get(self.url)
1369 self.assertEqual(response.status_code, 403)
1371 def test_proper_member(self):
1372 self._create_membership().approve()
1373 response = self.client.get(self.url)
1374 self.assertEqual(response.status_code, 200)
1376 def test_upcoming_ballot(self):
1377 self._create_membership().approve()
1378 self.ballot.opening_on = timezone.now() + timedelta(hours=1)
1379 self.ballot.save()
1381 response = self.client.get(self.url)
1382 self.assertEqual(response.status_code, 404)
1384 def test_expired_ballot(self):
1385 self._create_membership().approve()
1386 self.ballot.closing_on = timezone.now() - timedelta(hours=1)
1387 self.ballot.save()
1389 response = self.client.get(self.url)
1390 self.assertEqual(response.status_code, 404)
1392 def test_post_valid_form(self):
1393 self._create_membership().approve()
1395 response = self.do_POST({})
1396 self.assertContains(response, "Thanks for casting a vote!")
1398 # Try voting again and check it is failing
1399 response = self.client.post(self.url, {})
1400 self.assertEqual(response.status_code, 404)
1403class ViewBallotAdminTests(TestCase, GuiViewMixin):
1404 fixtures = ['ballots.yaml']
1406 def setUp(self):
1407 self.ballot = Ballot.objects.get(pk=1)
1408 self.setUpGuiTests(reverse('ballot-admin', kwargs={"pk": self.ballot.id}), superuser_needed=True)
1410 def test_ballot_still_open(self):
1411 create_user_and_log_in(self.client, verified=True, admin=True)
1412 self.ballot.closing_on = timezone.now() + timedelta(hours=1)
1413 self.ballot.save()
1415 response = self.client.get(self.url)
1416 self.assertNotContains(response, "Select: ")
1417 self.assertNotContains(response, "Ranking: ")
1418 self.assertContains(response, "Send reminder")
1419 self.assertContains(response, "The ballot is not yet closed, and thus results cannot be presented yet.")
1421 def test_ballot_not_yet_open(self):
1422 create_user_and_log_in(self.client, verified=True, admin=True)
1423 self.ballot.opening_on = timezone.now() + timedelta(hours=1)
1424 self.ballot.closing_on = timezone.now() + timedelta(hours=2)
1425 self.ballot.save()
1427 response = self.client.get(self.url)
1428 self.assertNotContains(response, "Select: ")
1429 self.assertNotContains(response, "Ranking: ")
1430 self.assertNotContains(response, "Send reminder")
1431 self.assertContains(response, "The ballot is not yet closed, and thus results cannot be presented yet.")
1433 def test_ballot_is_closed(self):
1434 create_user_and_log_in(self.client, verified=True, admin=True)
1435 self.ballot.closing_on = timezone.now()
1436 self.ballot.save()
1438 response = self.client.get(self.url)
1439 self.assertContains(response, "Select: ")
1440 self.assertContains(response, "Ranking: ")
1441 self.assertNotContains(response, "Send reminder")
1442 self.assertNotContains(response, "The ballot is not yet closed, and thus results cannot be presented yet.")
1445class ViewBallotSendReminderTests(TestCase):
1446 fixtures = ['ballots.yaml']
1448 def setUp(self):
1449 self.url = reverse('ballot-send-reminder', kwargs={"pk": 1})
1451 def test_get_and_admin(self):
1452 create_user_and_log_in(self.client, admin=True, verified=True)
1453 response = self.client.get(self.url)
1454 self.assertEqual(response.status_code, 405)
1456 def test_valid_post(self):
1457 create_user_and_log_in(self.client, admin=True, verified=True)
1459 response = self.client.post(self.url)
1460 self.assertEqual(response.status_code, 302)
1461 self.assertEqual(response.url, "/")
1463 response = self.client.get(response.url, {})
1464 self.assertContains(response, "An email has been sent to all members who have not voted yet")