Coverage for gui/tests.py: 100%

947 statements  

« 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 

9 

10from allauth.account.models import EmailAddress 

11 

12from htmlvalidator.client import ValidatingClient 

13 

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 

20 

21from collections import OrderedDict 

22import logging 

23import os 

24 

25logging.disable(logging.CRITICAL) 

26 

27 

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() 

34 

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) 

40 

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() 

46 

47 self.assertEqual(len(mail.outbox), 1) 

48 self.assertEqual(mail.outbox[0].from_email, 'hello2@me.com') 

49 

50 

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)) 

57 

58 email = AdminEmail("my subject", 'the wonderful\nadmin\nmessage') 

59 

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') 

63 

64 

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') 

70 

71 

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 

80 

81 

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) 

86 

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) 

92 

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) 

98 

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)) 

102 

103 self.assertEqual(MembershipPeriod.current_period(), new) 

104 

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)) 

109 

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) 

114 

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)) 

120 

121 

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') 

127 

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") 

134 

135 def test_approved(self): 

136 membership = Membership(period=self.period, user_profile=self.user.profile) 

137 

138 membership.approve() 

139 

140 self.assertEqual(str(membership), 

141 " <user@provider.com>'s membership for the period 2018-2019 ({})".format(membership.status)) 

142 

143 # Check that we do not allow double approval 

144 self.assertRaisesMessage(ValueError, 'The membership has already been approved', membership.approve) 

145 

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())) 

150 

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) 

157 

158 def test_rejected(self): 

159 membership = Membership(period=self.period, user_profile=self.user.profile) 

160 

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='') 

166 

167 membership.reject('No real reason') 

168 

169 self.assertEqual(str(membership), 

170 " <user@provider.com>'s membership for the period 2018-2019 ({})".format(membership.status)) 

171 

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') 

175 

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())) 

180 

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) 

188 

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())) 

195 

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) 

202 

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 

210 

211 membership = Membership(approved_on=timezone.now(), rejected_on=timezone.now()) 

212 with self.assertRaises(ValueError, msg="Unknown state"): 

213 membership.status 

214 

215 

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 

225 

226 def setUp(self): 

227 self.user = self.create_user(username='user') 

228 self.profile = self.user.profile 

229 

230 def test_membership__no_period(self): 

231 self.assertEqual(self.profile.membership, None) 

232 

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) 

237 

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) 

243 

244 def test_last_membership__no_periods(self): 

245 self.assertEqual(self.profile.last_membership, None) 

246 

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)) 

254 

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) 

259 

260 self.assertTrue(self.profile.last_membership, period2) 

261 

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() 

265 

266 ballots = self.profile.active_ballots 

267 

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) 

274 

275 def test_str(self): 

276 self.assertEqual(str(self.profile), "First Last <a@x.org>") 

277 

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) 

282 

283 self.assertEqual(Profile.existing_affiliations(), 

284 ['Canonical', 'Intel', 'Red Hat', 'Samsung', 'Suse']) 

285 

286 def test_send_email(self): 

287 self.profile.send_email("My subject", "My message") 

288 

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) 

293 

294 

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}) 

302 

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}) 

309 

310 

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() 

318 

319 def test_website(self): 

320 website = global_context(None).get("website") 

321 

322 with patch.dict('os.environ', {}, clear=True): 

323 self.assertEqual(website.version, None) 

324 

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) 

330 

331 

332class ProfileFormTests(TestCase): 

333 def test_empty_form(self): 

334 form = ProfileForm({}) 

335 

336 user = MagicMock() 

337 form.save(user) 

338 

339 self.assertFalse(form.is_valid()) 

340 user.save.assert_not_called() 

341 

342 def test_all_but_affiliation(self): 

343 form = ProfileForm({'first_name': 'First', 'last_name': 'Last', 'public_statement': 'My statement'}) 

344 

345 user = MagicMock() 

346 form.save(user) 

347 

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() 

354 

355 

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)) 

361 

362 @patch('gui.models.Membership.objects.create') 

363 def test_empty_form(self, create_mock): 

364 form = MembershipApplicationForm({}) 

365 self.assertFalse(form.save()) 

366 

367 self.assertFalse(form.is_valid()) 

368 create_mock.assert_not_called() 

369 

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()) 

375 

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() 

379 

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()) 

385 

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) 

389 

390 

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 

396 

397 if os.environ.get('VALIDATE_HTML') is not None: 

398 self.client = ValidatingClient() # pragma: no cover 

399 

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', ) 

402 

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) 

405 

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, {}) 

410 

411 def test_unauthenticated_user(self): 

412 response = self.client.get(self.url) 

413 

414 if self.verified_user_needed: 

415 self.assertEqual(response.status_code, 302) 

416 else: 

417 self.assertEqual(response.status_code, 200) 

418 

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">') 

424 

425 self.additional_unauthenticated_checks(response) 

426 

427 def test_logged_in_unverified_user(self): 

428 self.create_user_and_log_in(admin=False, verified=False) 

429 

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) 

434 

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">') 

441 

442 self.additional_logged_in_user_checks(response) 

443 

444 return response 

445 

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) 

450 

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">') 

457 

458 self.additional_logged_in_user_checks(response) 

459 

460 return response 

461 

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) 

468 

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>') 

475 

476 self.additional_admin_checks(response) 

477 

478 return response 

479 

480 # To be overriden 

481 def additional_unauthenticated_checks(self, response): 

482 pass 

483 

484 def additional_logged_in_user_checks(self, response): 

485 pass 

486 

487 def additional_admin_checks(self, response): 

488 pass 

489 

490 

491class ViewIndexTests(TestCase, GuiViewMixin): 

492 def setUp(self): 

493 self.setUpGuiTests(reverse('index')) 

494 

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 

502 

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)) 

512 

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) 

520 

521 def additional_unauthenticated_checks(self, response): 

522 self.assertContains(response, "Log-in or Sign-up") 

523 

524 self.assertNotContains(response, 'href="{}"'.format(reverse('members'))) 

525 

526 def additional_logged_in_user_checks(self, response): 

527 self.assertContains(response, 'role="button">Apply</a>') 

528 

529 self.assertNotContains(response, 'href="{}"'.format(reverse('members'))) 

530 

531 def additional_admin_checks(self, response): 

532 self.additional_logged_in_user_checks(response) 

533 

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) 

539 

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!") 

543 

544 self.assertNotContains(response, 'href="{}"'.format(reverse('members'))) 

545 

546 def test_user_with_active_membership(self): 

547 self.create_valid_member() 

548 

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!") 

552 

553 self.assertContains(response, 'href="{}"'.format(reverse('members'))) 

554 

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") 

561 

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>") 

566 

567 self.assertNotContains(response, 'href="{}"'.format(reverse('members'))) 

568 

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)) 

574 

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})) 

581 

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)) 

588 

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})) 

596 

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()) 

601 

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})) 

608 

609 

610class ViewMembersTests(TestCase, GuiViewMixin): 

611 def setUp(self): 

612 self.setUpGuiTests(reverse('members'), verified_user_needed=True) 

613 

614 self.period = MembershipPeriod.objects.create(short_name="2018-2019", start=timezone.now(), 

615 end=timezone.now() + timedelta(hours=2)) 

616 

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 

621 

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 

625 

626 def test_membership_pending(self): 

627 self._create_membership() 

628 response = self.client.get(self.url) 

629 self.assertEqual(response.status_code, 403) 

630 

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) 

635 

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) 

640 

641 @patch('gui.models.MembershipPeriod.members') 

642 def test_no_members(self, members_mock): 

643 response = self.test_logged_in_verified_user() 

644 

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) 

651 

652 self.assertContains(response, "No members yet.") 

653 

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()) 

664 

665 def test_with_members_as_member(self): 

666 self.__setup_db() 

667 

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>") 

675 

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") 

681 

682 def test_with_members_as_admin(self): 

683 self.__setup_db() 

684 

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">') 

693 

694 self.assertContains(response, "No actions available") 

695 self.assertNotContains(response, '<form action="/members/{}/make/user" method="post">'.format(self.user.id)) 

696 

697 

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) 

702 

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) 

707 

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) 

712 

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' 

716 

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) 

725 

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) 

732 

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) 

738 

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) 

745 

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) 

751 

752 

753class ViewProfileTests(TestCase, GuiViewMixin): 

754 def setUp(self): 

755 self.setUpGuiTests(reverse('account-profile'), verified_user_needed=True) 

756 

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") 

761 

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") 

768 

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) 

774 

775 self.assertContains(response, '<datalist id="list__affiliations-list"><option value="Collabora">' 

776 '<option value="Intel"><option value="Google"><option value="Samsung"></datalist>') 

777 

778 

779class ViewDeleteAccountTests(TestCase, GuiViewMixin): 

780 def setUp(self): 

781 self.setUpGuiTests(reverse('account-delete'), verified_user_needed=True) 

782 

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') 

787 

788 

789class ViewMembershipApplicationTests(TestCase, GuiViewMixin): 

790 def setUp(self): 

791 self.setUpGuiTests(reverse('membership-application'), verified_user_needed=True) 

792 

793 def test_no_periods(self): 

794 response = self.test_logged_in_admin() 

795 self.assertContains(response, "No membership period has been created.") 

796 

797 def test_with_period(self): 

798 MembershipPeriod.objects.create(short_name="2018-2019", start=timezone.now(), 

799 end=timezone.now() + timedelta(hours=2)) 

800 

801 response = self.test_logged_in_admin() 

802 self.assertContains(response, "Membership application - 2018-2019") 

803 

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) 

809 

810 response = self.client.get(self.url) 

811 self.assertEqual(response.status_code, 200) 

812 

813 self.assertContains(response, "You already applied to the period 2018-2019. Current status: Pending approval") 

814 

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") 

821 

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)) 

826 

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") 

832 

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)) 

837 

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") 

843 

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) 

848 

849 

850class MembershipPeriodCreateViewTests(TestCase, GuiViewMixin): 

851 def setUp(self): 

852 self.setUpGuiTests(reverse('membership-period-create'), superuser_needed=True) 

853 

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")) 

866 

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") 

872 

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')) 

882 

883 

884class ViewMembershipApprovalTests(TestCase, GuiViewMixin): 

885 def setUp(self): 

886 self.setUpGuiTests(reverse('membership-approval'), superuser_needed=True) 

887 

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...") 

892 

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...") 

897 

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) 

906 

907 response = self.do_POST({'ids': [membership1.id, membership2.id], "action": "approve"}) 

908 self.assertContains(response, "You approved 2 membership applications") 

909 

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) 

918 

919 response = self.do_POST({'ids': [membership1.id, membership2.id], "action": "reject"}) 

920 expected = "Can&#x27;t refuse First Last &lt;user@provider.com&gt;&#x27;s application without a reason" 

921 self.assertContains(response, expected) 

922 

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) 

931 

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") 

937 

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) 

945 

946 self.create_user_and_log_in(admin=True, verified=True) 

947 

948 response = self.client.get(self.url) 

949 self.assertNotContains(response, 'name="ids" value="11"') 

950 

951 response = self.client.get(self.url + "?page=2") 

952 self.assertContains(response, 'name="ids" value="11"') 

953 

954 response = self.client.get(self.url + "?page=3") 

955 self.assertNotContains(response, 'name="ids" value="11"') 

956 

957 

958class ViewAboutTests(TestCase, GuiViewMixin): 

959 def setUp(self): 

960 self.setUpGuiTests(reverse('about')) 

961 

962 

963class BallotTests(TestCase): 

964 fixtures = ['ballots.yaml'] 

965 

966 @patch('gui.models.Ballot.objects.filter') 

967 def test_active_ballots(self, filter_mocked): 

968 Ballot.active_ballots() 

969 

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)) 

973 

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) 

980 

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])) 

985 

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()) 

990 

991 def test_time_helpers(self): 

992 now = timezone.now() 

993 

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) 

998 

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) 

1003 

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) 

1008 

1009 @patch('gui.models.Ballot.voters') 

1010 def test_has_voted(self, filter_mocked): 

1011 membership = MagicMock() 

1012 Ballot().has_voted(membership) 

1013 

1014 filter_mocked.filter.assert_called_with(pk=membership.pk) 

1015 filter_mocked.filter.return_value.exists.assert_called_with() 

1016 

1017 def test_turnout(self): 

1018 ballot = Ballot.objects.get(id=1) 

1019 self.assertEqual(ballot.turnout, VoteCount(3, 4)) 

1020 

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)) 

1025 

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)) 

1031 

1032 def test_send_reminder(self): 

1033 ballot = Ballot.objects.get(id=1) 

1034 

1035 ballot.send_reminder() 

1036 

1037 self.assertEqual(len(mail.outbox), 1) 

1038 self.assertEqual(mail.outbox[0].to, ['slacker@example.com']) 

1039 

1040 self.assertIn("vote", mail.outbox[0].subject) 

1041 self.assertIn('2019-2020', mail.outbox[0].body) 

1042 

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) 

1046 

1047 def test_str(self): 

1048 self.assertEqual(str(Ballot.objects.get(id=1)), "Ballot<2019-2020>") 

1049 

1050 

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()) 

1055 

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) 

1066 

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) 

1077 

1078 def test_ballot_not_specified(self): 

1079 form = BallotForm() 

1080 

1081 # Without a ballot, we should not have any fields 

1082 self.assertEqual(sorted(form.fields.keys()), []) 

1083 

1084 def test_dynamic_form_generation(self): 

1085 form = BallotForm(ballot=self.ballot, data={}) 

1086 

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']) 

1090 

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)) 

1101 

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] 

1107 

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)) 

1115 

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) 

1122 

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']) 

1128 

1129 def test_empty_form(self): 

1130 form = BallotForm(ballot=self.ballot, data={}) 

1131 

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())) 

1135 

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']) 

1141 

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']) 

1147 

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']) 

1153 

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']) 

1159 

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 }) 

1167 

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()) 

1171 

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)) 

1178 

1179 # Check that the membership has been added to the list of voters 

1180 self.assertEqual(list(self.ballot.voters.all()), [membership]) 

1181 

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]) 

1187 

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 

1199 

1200 answer = BallotRankingAnswer.objects.get(member=membership, question=question, option=option) 

1201 self.assertEqual(answer.rank, expected_rank[q][o]) 

1202 

1203 

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)") 

1210 

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) 

1214 

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)") 

1220 

1221 def test_equality(self): 

1222 self.assertEqual(VoteCount(10, 20), VoteCount(10, 20)) 

1223 self.assertNotEqual(VoteCount(11, 20), VoteCount(10, 20)) 

1224 

1225 

1226class BallotSelectQuestionTests(TestCase): 

1227 fixtures = ['ballots.yaml'] 

1228 

1229 def setUp(self): 

1230 self.question = BallotSelectQuestion.objects.get(id=1) 

1231 

1232 def test_tally(self): 

1233 answer_yes = BallotSelectQuestionPossibleAnswer.objects.get(id=1) 

1234 answer_no = BallotSelectQuestionPossibleAnswer.objects.get(id=2) 

1235 

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)) 

1240 

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 

1245 

1246 def test_str(self): 

1247 self.assertEqual(str(self.question), 

1248 "Ballot<2019-2020>: Do you accept the new bylaws?") 

1249 

1250 

1251class BallotSelectQuestionPossibleAnswerTests(TestCase): 

1252 fixtures = ['ballots.yaml'] 

1253 

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") 

1257 

1258 

1259class BallotSelectedAnswerTests(TestCase): 

1260 fixtures = ['ballots.yaml'] 

1261 

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'") 

1265 

1266 

1267class OptionRankingVotesTests(TestCase): 

1268 fixtures = ['ballots.yaml'] 

1269 

1270 def test_normal_case(self): 

1271 option = BallotRankingQuestionOption.objects.get(id=1) 

1272 votes = OptionRankingVotes(option, 4) 

1273 

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") 

1279 

1280 

1281class BallotRankingQuestionTests(TestCase): 

1282 fixtures = ['ballots.yaml'] 

1283 

1284 def setUp(self): 

1285 self.question = BallotRankingQuestion.objects.get(id=1) 

1286 

1287 def test_tally(self): 

1288 tally = self.question.tally 

1289 

1290 # Check that the scores are right 

1291 self.assertEqual([t.score for t in tally], [11, 8, 6, 4]) 

1292 

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]) 

1299 

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 

1304 

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") 

1308 

1309 

1310class BallotRankingQuestionOptionTests(TestCase): 

1311 fixtures = ['ballots.yaml'] 

1312 

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") 

1317 

1318 

1319class BallotRankingAnswerTests(TestCase): 

1320 fixtures = ['ballots.yaml'] 

1321 

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") 

1326 

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") 

1330 

1331 

1332class ViewBallotListTests(TestCase, GuiViewMixin): 

1333 def setUp(self): 

1334 self.setUpGuiTests(reverse('ballot-list'), superuser_needed=True) 

1335 

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() 

1341 

1342 

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)) 

1347 

1348 self.setUpGuiTests(reverse('ballot-vote', kwargs={"pk": self.ballot.id}), verified_user_needed=True) 

1349 

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 

1356 

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 

1360 

1361 def test_membership_pending(self): 

1362 self._create_membership() 

1363 response = self.client.get(self.url) 

1364 self.assertEqual(response.status_code, 403) 

1365 

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) 

1370 

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) 

1375 

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() 

1380 

1381 response = self.client.get(self.url) 

1382 self.assertEqual(response.status_code, 404) 

1383 

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() 

1388 

1389 response = self.client.get(self.url) 

1390 self.assertEqual(response.status_code, 404) 

1391 

1392 def test_post_valid_form(self): 

1393 self._create_membership().approve() 

1394 

1395 response = self.do_POST({}) 

1396 self.assertContains(response, "Thanks for casting a vote!") 

1397 

1398 # Try voting again and check it is failing 

1399 response = self.client.post(self.url, {}) 

1400 self.assertEqual(response.status_code, 404) 

1401 

1402 

1403class ViewBallotAdminTests(TestCase, GuiViewMixin): 

1404 fixtures = ['ballots.yaml'] 

1405 

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) 

1409 

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() 

1414 

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.") 

1420 

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() 

1426 

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.") 

1432 

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() 

1437 

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.") 

1443 

1444 

1445class ViewBallotSendReminderTests(TestCase): 

1446 fixtures = ['ballots.yaml'] 

1447 

1448 def setUp(self): 

1449 self.url = reverse('ballot-send-reminder', kwargs={"pk": 1}) 

1450 

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) 

1455 

1456 def test_valid_post(self): 

1457 create_user_and_log_in(self.client, admin=True, verified=True) 

1458 

1459 response = self.client.post(self.url) 

1460 self.assertEqual(response.status_code, 302) 

1461 self.assertEqual(response.url, "/") 

1462 

1463 response = self.client.get(response.url, {}) 

1464 self.assertContains(response, "An email has been sent to all members who have not voted yet")