codeforlife-portal 6.44.10__py2.py3-none-any.whl → 6.45.1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of codeforlife-portal might be problematic. Click here for more details.

@@ -1,6 +1,5 @@
1
1
  import datetime
2
2
  import json
3
- import re
4
3
  from enum import Enum, auto
5
4
  from uuid import uuid4
6
5
 
@@ -66,13 +65,14 @@ def send_email(
66
65
  django_send_email(sender, recipients, subject, text_content, title, replace_url, plaintext_template, html_template)
67
66
 
68
67
 
69
- def send_verification_email(request, user, data, new_email=None, age=None):
68
+ def send_verification_email(request, user, data, new_email=None, age=None, school=None):
70
69
  """
71
70
  Sends emails relating to email address verification.
72
71
 
73
72
  On registration:
74
73
  - if the user is under 13, send a verification email addressed to the parent / guardian
75
74
  - if the user is over 13, send a regular verification email
75
+ - if the user is a student who just got released, send a verification email explaining the situation
76
76
  - if the user is a student who has requested to sign up to the newsletter, handle their Dotmailer subscription
77
77
 
78
78
  On email address update:
@@ -88,20 +88,28 @@ def send_verification_email(request, user, data, new_email=None, age=None):
88
88
  student)
89
89
  """
90
90
 
91
- # verifying first email address (registration)
91
+ # verifying first email address (registration or unverified login attempt)
92
92
  if not new_email:
93
93
  verification = generate_token(user)
94
94
 
95
- # if the user is a teacher
96
95
  if age is None:
97
- url = f"{request.build_absolute_uri(reverse('verify_email', kwargs={'token': verification}))}"
96
+ # if the user is a released student
97
+ if hasattr(user, "new_student") and school is not None:
98
+ url = f"{request.build_absolute_uri(reverse('verify_email', kwargs={'token': verification}))}"
99
+
100
+ send_dotdigital_email(
101
+ campaign_ids["verify_released_student"], [user.email],
102
+ personalization_values={"VERIFICATION_LINK": url, "SCHOOL_NAME": school.name}
103
+ )
104
+ else:
105
+ url = f"{request.build_absolute_uri(reverse('verify_email', kwargs={'token': verification}))}"
98
106
 
99
- send_dotdigital_email(
100
- campaign_ids["verify_new_user"], [user.email], personalization_values={"VERIFICATION_LINK": url}
101
- )
107
+ send_dotdigital_email(
108
+ campaign_ids["verify_new_user"], [user.email], personalization_values={"VERIFICATION_LINK": url}
109
+ )
102
110
 
103
- if _newsletter_ticked(data):
104
- add_to_dotmailer(user.first_name, user.last_name, user.email, DotmailerUserType.TEACHER)
111
+ if _newsletter_ticked(data):
112
+ add_to_dotmailer(user.first_name, user.last_name, user.email, DotmailerUserType.TEACHER)
105
113
  # if the user is an independent student
106
114
  else:
107
115
  if age < 13:
cfl_common/common/mail.py CHANGED
@@ -26,6 +26,7 @@ campaign_ids = {
26
26
  "verify_new_user_first_reminder": 1557170,
27
27
  "verify_new_user_second_reminder": 1557173,
28
28
  "verify_new_user_via_parent": 1551587,
29
+ "verify_released_student": 1580574,
29
30
  }
30
31
 
31
32
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: codeforlife-portal
3
- Version: 6.44.10
3
+ Version: 6.45.1
4
4
  Classifier: Programming Language :: Python
5
5
  Classifier: Programming Language :: Python :: 3.8
6
6
  Classifier: Framework :: Django
@@ -25,7 +25,7 @@ Requires-Dist: django-classy-tags ==2.0.0
25
25
  Requires-Dist: libsass ==0.23.0
26
26
  Requires-Dist: phonenumbers ==8.12.12
27
27
  Requires-Dist: more-itertools ==8.7.0
28
- Requires-Dist: cfl-common ==6.44.10
28
+ Requires-Dist: cfl-common ==6.45.1
29
29
  Requires-Dist: django-ratelimit ==3.0.1
30
30
  Requires-Dist: django-preventconcurrentlogins ==0.8.2
31
31
  Requires-Dist: django-csp ==3.7
@@ -5,13 +5,13 @@ cfl_common/common/app_settings.py,sha256=x2ROLY5Xl5LgqjxyTiChZvQorZYUXpFzEkaLsjh
5
5
  cfl_common/common/apps.py,sha256=49UXZ3bSkFKvIEOL4zM7y1sAhccQJyRtsoOg5XVd_8Y,129
6
6
  cfl_common/common/context_processors.py,sha256=X0iuX5qu9kMWa7q8osE9CJ2LgM7pPOYQFGdjm8X3rk0,236
7
7
  cfl_common/common/csp_config.py,sha256=sZT6s9zMT5FFIqNODsURT0ifxbDgXpDlki8UxaBq2iE,2940
8
- cfl_common/common/mail.py,sha256=XPImcfZKcW5mxov04_0jc3xkx_u1SJE6Hhn8K2kPgoA,6354
8
+ cfl_common/common/mail.py,sha256=Lmn7CepceKw1UqxjxTR_24fo7izecw0ihBJ3W_9tcco,6394
9
9
  cfl_common/common/models.py,sha256=1e_3zHf8h_K812-2cQymRLZAKoA73_5-t4LQGPQlifE,16946
10
10
  cfl_common/common/permissions.py,sha256=gC6RQGZI2QDBbglx-xr_V4Hl2C2nf1V2_uPmEuoEcJo,2416
11
11
  cfl_common/common/utils.py,sha256=Nn2Npao9Uqad5Js_IdHwF-ow6wrPNpBLW4AO1LxoEBc,1727
12
12
  cfl_common/common/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  cfl_common/common/helpers/data_migration_loader.py,sha256=_BhS5lPmhcuVUbryBmJytlWdHyT02KYyxPkHar32mOE,1748
14
- cfl_common/common/helpers/emails.py,sha256=6xY8YxYY3ywcFWguys2G_36HTaBBah4ma1p20s4mpKU,10099
14
+ cfl_common/common/helpers/emails.py,sha256=Gu4YAd977k7v_4qezcYFwWYEBUAV3r8pXZYDVxJL3qw,10700
15
15
  cfl_common/common/helpers/generators.py,sha256=kTL5e91I8wgmjJ-mu4jr9vIacjccUZ5pZSAz5cUNhdM,1505
16
16
  cfl_common/common/helpers/organisation.py,sha256=e-JKumKoXrkMTzZPv0H4ViWL8vtCt7oXJjn_zZ1ec00,427
17
17
  cfl_common/common/migrations/0001_initial.py,sha256=Y2kt2xmdCbrmDXCgqmhXeacicNg26Zj7L7SANSsgAAI,9664
@@ -106,7 +106,7 @@ example_project/portal_test_settings.py,sha256=frp_XMpd-z1g3VFCRxB2w7AaFW2ivRVKn
106
106
  example_project/settings.py,sha256=XRZZvASoIl5a9xe3masTq_CUBleuJq9ByHx8f_e2UFc,5613
107
107
  example_project/urls.py,sha256=OVeRQ-TCpzHISBRuzqD0yd3ewF7H5U3c-f2p2alfUD0,430
108
108
  example_project/wsgi.py,sha256=U1W6WzZxZaIdYZ5tks7w9fqp5WS5qvn2iThsVcskrWw,829
109
- portal/__init__.py,sha256=DAOvnlpkYOeLySc4ozoUqbAuehzkrm5oNyavRwvs7H4,24
109
+ portal/__init__.py,sha256=dPUG1ltD8pZ0AeP68Fyop2d_Gq7K8-Bf9xTLEJzD4uc,23
110
110
  portal/admin.py,sha256=on1-zNRnZvf2cwBN6GVRVYRhkaksrCgfzX8XPWtkvz8,6062
111
111
  portal/app_settings.py,sha256=DhWLQOwM0zVOXE3O5TNKbMM9K6agfLuCsHOdr1J7xEI,651
112
112
  portal/backends.py,sha256=2Dss6_WoQwPuDzJUF1yEaTQTNG4eUrD12ujJQ5cp5Tc,812
@@ -123,7 +123,7 @@ portal/forms/dotmailer.py,sha256=McD9_u8yxUfE7PSVG3MPMilRJtx9GTe9QsNzDDV3uuI,761
123
123
  portal/forms/error_messages.py,sha256=8d3z_3e2L-5zwj5hFhnUByC5k2CEpIVVuJg2nYkCUQ8,148
124
124
  portal/forms/invite_teacher.py,sha256=jkDNcCfkts4_lXRzhcI3xBam21Zn2yX9wMpMVhDtW1w,880
125
125
  portal/forms/organisation.py,sha256=QcQyd7AiqBmvt4y8uQSQylguUbKOKqo2pjqWIkpWjDg,7433
126
- portal/forms/play.py,sha256=IO0gfKfTv7lXEN1K9w0XG8vY-55obBqLpiEBWaUluZ8,11351
126
+ portal/forms/play.py,sha256=z9P5LzyS3jjYcnfco84d2x8ptgLxRmh94Dnj05plmbY,11505
127
127
  portal/forms/registration.py,sha256=gWcY7rllhWO3c9as6QHUDWZx1Jme7DqtGHYaKcvxe-U,5990
128
128
  portal/forms/teach.py,sha256=-3dMQxIQtYq2xg5DgtIJMpN7RajNhTvc56Clr5QjsHo,20440
129
129
  portal/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -444,7 +444,7 @@ portal/templates/portal/dotmailer_consent_form.html,sha256=UDdizPoKYZGybr6z9nzDV
444
444
  portal/templates/portal/email_invitation_sent.html,sha256=hAMzQXE3NFGnOsQlCGuo3Aps-vazSJb5BhAN7bWT48M,641
445
445
  portal/templates/portal/email_style_template.html,sha256=VodOGssxRS9aJTjSaU7iv5qJgYe66C_gPL_98ZX_cIQ,11935
446
446
  portal/templates/portal/email_verification_failed.html,sha256=i93oA6EXxa-Tj6A05Bk6ftR0h0A4M_oTjNqqyCJ4c8g,806
447
- portal/templates/portal/email_verification_needed.html,sha256=a2DseyE_7AwwUIzwKWfbBBTDSSA_pvP3oAnZyL41AEQ,1062
447
+ portal/templates/portal/email_verification_needed.html,sha256=lJvwWjBg6y-9V17gyVCIQj1sXzB6O35porHGhrLT6WM,1053
448
448
  portal/templates/portal/form_shapes.html,sha256=oiaQ4RKZF8STTQsoQqql184UoYVyFQ60Mwve8lJMKu0,1223
449
449
  portal/templates/portal/getinvolved.html,sha256=jC56RTuLjDISr7eJUPA2TErz1TBcLi9G42BY-jo3Xe8,2872
450
450
  portal/templates/portal/home.html,sha256=pHXbzXAfCgaZnGDvofvNcb63dkZ309XGKg35dxZlHWw,6178
@@ -458,7 +458,7 @@ portal/templates/portal/register.html,sha256=9AFNejCCcG2VwNCJ8AU5r3Wk0u_ftK_NbVf
458
458
  portal/templates/portal/reset_password.html,sha256=YzsREz5D2OwhicMLahVOVDXiNDxoHlPqU5iu96i36W0,1373
459
459
  portal/templates/portal/reset_password_confirm.html,sha256=jPHSDatezRXzCG4zH_5BQPWAxLblidqro0hzvsH54ho,3499
460
460
  portal/templates/portal/reset_password_done.html,sha256=rpzN3svZne5H2FS3TJaGnHypRj2KX-SRS6DbFQkgLf0,667
461
- portal/templates/portal/reset_password_email_sent.html,sha256=nqZYSa34zacTmLJOe4zdyLM0u7bO2Wje1zTLTOtHvr8,923
461
+ portal/templates/portal/reset_password_email_sent.html,sha256=kwNvoH_M-Qd5s-g0HiULwfQt_SD12WEMgPRSnLmldTQ,914
462
462
  portal/templates/portal/teach.html,sha256=5Ua6l2H6dBIXjKjMTUGkM92SnBnAXTBRpM6sKlwnrUk,8724
463
463
  portal/templates/portal/terms.html,sha256=SO8ruLF2kdYpOinxlVx1CHfPNgphemX4wvWJ9-W6Nj4,29155
464
464
  portal/templates/portal/login/independent_student.html,sha256=G4u82m0lko4yQvqRDMger5p63A6KtFI_bsEOsQWlyJw,1777
@@ -545,7 +545,7 @@ portal/tests/test_class.py,sha256=V6Fkc6PqdisefKD3xs9PbfE2pKp-9e0gwQVkPUiu6bk,14
545
545
  portal/tests/test_daily_activities.py,sha256=-siDCMGBD1ijjccHVk7eEmrk4bgTsvbh0B6hDoj2fo0,1803
546
546
  portal/tests/test_emails.py,sha256=xNgOt592r2nrQu7VeBdc8kvSnw6Z5fPJqMx2-UMcyyk,9482
547
547
  portal/tests/test_helper_methods.py,sha256=-SQCDZm2XUtyXGEp0CHIb_SSC9CPD-XOSnpnY8QclHk,890
548
- portal/tests/test_independent_student.py,sha256=jyNpHyisCfkD-CDsjIfSxWnWTxDmkReJ-BdXEBdJoMo,27081
548
+ portal/tests/test_independent_student.py,sha256=mRbWZVwbZKPqHajTd9bYrCAn9ZJLpexpk-YSiZuDAbM,27540
549
549
  portal/tests/test_invite_teacher.py,sha256=oeOaoJV1IqJSYPlaPFjnhVXdB2mq8otCTLp_lfjuCfk,12224
550
550
  portal/tests/test_middleware.py,sha256=b6jfNmiRZ2snqLKsyJUG-RivoX5fmrqLlQkG9MeVnqM,8034
551
551
  portal/tests/test_newsletter_footer.py,sha256=MdVUX53mEoDTa4Krq-jg9LFNo-QyghqvTvhHeNXBGnE,838
@@ -556,7 +556,7 @@ portal/tests/test_school_student.py,sha256=bFZwY4twaFHQLp0cltMq8cLNDZGgCHTZBCZHK
556
556
  portal/tests/test_security.py,sha256=FGrlRfnzi-Xx2_bn4fTZlYORKm7w_GhGkD3havvplwc,3239
557
557
  portal/tests/test_teacher.py,sha256=_VmQCWq07uCFbvq6Vd7GN00mE7vY7WNMeQTk6bHxFPI,36898
558
558
  portal/tests/test_teacher_student.py,sha256=NWITbUw1kijqu3c8eRHLHJKaYQMOsOMvl7PAVx5QghI,21567
559
- portal/tests/test_views.py,sha256=IElQJnp9fhlmqR-54nBycul2uUIjsG9dTRnYy5SxXJ4,39278
559
+ portal/tests/test_views.py,sha256=g6WQtexZ-UfwpNxpTmfiB-RTBtLFiYBmjAa5oFCSBDk,42073
560
560
  portal/tests/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
561
561
  portal/tests/migrations/test_migration_make_portaladmin_teacher.py,sha256=ekMRb6cU97oT0k9gCKW7IUB7oPuGmv4uWJCqInQN7x8,2589
562
562
  portal/tests/migrations/test_migration_preview_user_remove.py,sha256=K6D-FZT9YFEA8oMxHz9VTglVV6MZOTRYVlvwWwXc2vU,555
@@ -638,13 +638,13 @@ portal/views/student/edit_account_details.py,sha256=Ba-3D_zzKbX5N01NG5qqBS0ud10B
638
638
  portal/views/student/play.py,sha256=r5TADH_wYn3d1beezfvkYBiendQ9qLys9dUJwHqF_44,8581
639
639
  portal/views/teacher/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
640
640
  portal/views/teacher/dashboard.py,sha256=8WglspwuHF__2LtoX5_XvoW1ulICSupjKv--MtjrvJk,25714
641
- portal/views/teacher/teach.py,sha256=B71jReMJ4BYFmo7NtJVK3-4DeXEwxfu_WA3Ij1RYzdI,34725
641
+ portal/views/teacher/teach.py,sha256=PJAUjLeIBELWc5e2Eek0mr8kYleYWb1MH9FF4O2q7Ok,34779
642
642
  portal/views/two_factor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
643
643
  portal/views/two_factor/core.py,sha256=O_wcBeFqdPYSGNGv-pT_vbs5-Dj1Z-Jfkd6f9-E5yZI,760
644
644
  portal/views/two_factor/form.py,sha256=lnHNKI-BMlpncTuW3zUzjPaJJNuEra2I_nOam0eOKFY,257
645
645
  portal/views/two_factor/profile.py,sha256=tkl_ludo8arMtd5LKNmohM66vpC_YQiP-0nspTSJiJ4,383
646
- codeforlife_portal-6.44.10.dist-info/LICENSE.md,sha256=9AbRlCDqD2D1tPibimysFv3zg3AIc49-eyv9aEsyq9w,115
647
- codeforlife_portal-6.44.10.dist-info/METADATA,sha256=Sfk3uOefLkZtSMygjh3kRTpcYdv5Kh-PgalkF64N6rQ,3476
648
- codeforlife_portal-6.44.10.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
649
- codeforlife_portal-6.44.10.dist-info/top_level.txt,sha256=8e5pdsuIoTqEAMqpelHBjGjLbffcBtgOoggmd2q7nMw,41
650
- codeforlife_portal-6.44.10.dist-info/RECORD,,
646
+ codeforlife_portal-6.45.1.dist-info/LICENSE.md,sha256=9AbRlCDqD2D1tPibimysFv3zg3AIc49-eyv9aEsyq9w,115
647
+ codeforlife_portal-6.45.1.dist-info/METADATA,sha256=2OBfzK553a8yDjRvTDEJnXFO_iKwA-UPK4VINUfmTDc,3474
648
+ codeforlife_portal-6.45.1.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
649
+ codeforlife_portal-6.45.1.dist-info/top_level.txt,sha256=8e5pdsuIoTqEAMqpelHBjGjLbffcBtgOoggmd2q7nMw,41
650
+ codeforlife_portal-6.45.1.dist-info/RECORD,,
portal/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "6.44.10"
1
+ __version__ = "6.45.1"
portal/forms/play.py CHANGED
@@ -277,15 +277,19 @@ class StudentJoinOrganisationForm(forms.Form):
277
277
 
278
278
  def clean(self):
279
279
  access_code = self.cleaned_data.get("access_code", None)
280
+ join_error_text = "The class code you entered either does not exist or is not currently accepting join requests. Please double check that you have entered the correct class code and contact the teacher of the class to ensure their class is currently accepting join requests."
280
281
 
281
282
  if access_code:
282
283
  classes = Class.objects.filter(access_code=access_code)
283
284
  if len(classes) != 1:
284
- raise forms.ValidationError("Cannot find the school or club and/or class")
285
+ raise forms.ValidationError(join_error_text)
286
+
285
287
  self.klass = classes[0]
286
- if not self.klass.always_accept_requests:
287
- if self.klass.accept_requests_until is None:
288
- raise forms.ValidationError("Cannot find the school or club and/or class")
289
- elif (self.klass.accept_requests_until - timezone.now()) < timedelta():
290
- raise forms.ValidationError("Cannot find the school or club and/or class")
288
+
289
+ if not self.klass.always_accept_requests and (
290
+ self.klass.accept_requests_until is None
291
+ or self.klass.accept_requests_until - timezone.now()
292
+ < timedelta()
293
+ ):
294
+ raise forms.ValidationError(join_error_text)
291
295
  return self.cleaned_data
@@ -10,7 +10,7 @@
10
10
  <img class="background" title="Paper Plane" alt="Verification email sent" src="{% static 'portal/img/paper_plane.png' %}">
11
11
  <p class="text-left">Please follow the link within the email to verify your details. This will expire in one hour.</p>
12
12
  <div>
13
- <a target="_blank" href="https://mail.google.com/mail/#search/from%3AOcado.Innovation.Ltd%40r1.dotdigital-email.com+subject%3AEmail+Verification"
13
+ <a target="_blank" href="https://mail.google.com/mail/#search/from%3Ano-reply%40info.codeforlife.education+subject%3AEmail+Verification"
14
14
  class="button button--home button-email">Open in Gmail</a>
15
15
  <a target="_blank" href="https://outlook.live.com/mail/"
16
16
  class="button button--home button-email">Open in Outlook</a>
@@ -10,7 +10,7 @@
10
10
  src="{% static 'portal/img/paper_plane.png' %}">
11
11
  <div>
12
12
  <a target="_blank"
13
- href="https://mail.google.com/mail/#search/from%3AOcado.Innovation.Ltd%40r1.dotdigital-email.com+subject%3APassword+reset+request"
13
+ href="https://mail.google.com/mail/#search/from%3Ano-reply%40info.codeforlife.education+subject%3APassword+reset+request"
14
14
  class="button button--home button-email">Open in Gmail</a>
15
15
  <a target="_blank" href="https://outlook.live.com/mail/" class="button button--home button-email">Open in
16
16
  Outlook</a>
@@ -283,7 +283,7 @@ class TestIndependentStudentFrontend(BaseTest):
283
283
  page = page.go_to_independent_student_login_page()
284
284
  page = page.independent_student_login_failure(username, password)
285
285
 
286
- errors = page.has_login_failed("independent_student_login_form", INVALID_LOGIN_MESSAGE)
286
+ page.has_login_failed("independent_student_login_form", INVALID_LOGIN_MESSAGE)
287
287
  assert page.has_login_failed("independent_student_login_form", INVALID_LOGIN_MESSAGE)
288
288
 
289
289
  verification_url = mock_send_dotdigital_email.call_args.kwargs["personalization_values"]["VERIFICATION_LINK"]
@@ -471,7 +471,9 @@ class TestIndependentStudentFrontend(BaseTest):
471
471
  )
472
472
 
473
473
  assert self.is_join_class_page(page)
474
- assert page.has_join_request_failed("Cannot find the school or club and/or class")
474
+ assert page.has_join_request_failed(
475
+ "The class code you entered either does not exist or is not currently accepting join requests. Please double check that you have entered the correct class code and contact the teacher of the class to ensure their class is currently accepting join requests."
476
+ )
475
477
 
476
478
  def test_join_class_not_accepting_requests(self):
477
479
  teacher_email, _ = signup_teacher_directly()
@@ -490,7 +492,9 @@ class TestIndependentStudentFrontend(BaseTest):
490
492
  )
491
493
 
492
494
  assert self.is_join_class_page(page)
493
- assert page.has_join_request_failed("Cannot find the school or club and/or class")
495
+ assert page.has_join_request_failed(
496
+ "The class code you entered either does not exist or is not currently accepting join requests. Please double check that you have entered the correct class code and contact the teacher of the class to ensure their class is currently accepting join requests."
497
+ )
494
498
 
495
499
  def test_join_class_revoked(self):
496
500
  teacher_email, _ = signup_teacher_directly()
@@ -53,8 +53,11 @@ class TestTeacherViews(TestCase):
53
53
  @classmethod
54
54
  def setUpTestData(cls):
55
55
  cls.email, cls.password = signup_teacher_directly()
56
+ cls.school = create_organisation_directly(cls.email)
56
57
  _, _, cls.class_access_code = create_class_directly(cls.email)
57
- _, _, cls.student = create_school_student_directly(cls.class_access_code)
58
+ _, cls.password_student, cls.student = create_school_student_directly(
59
+ cls.class_access_code
60
+ )
58
61
 
59
62
  def login(self):
60
63
  c = Client()
@@ -63,7 +66,9 @@ class TestTeacherViews(TestCase):
63
66
 
64
67
  def test_reminder_cards(self):
65
68
  c = self.login()
66
- url = reverse("teacher_print_reminder_cards", args=[self.class_access_code])
69
+ url = reverse(
70
+ "teacher_print_reminder_cards", args=[self.class_access_code]
71
+ )
67
72
 
68
73
  # First test with 2 dummy students
69
74
  NAME1 = "Test name"
@@ -97,7 +102,9 @@ class TestTeacherViews(TestCase):
97
102
  # page number
98
103
  students_per_page = REMINDER_CARDS_PDF_ROWS * REMINDER_CARDS_PDF_COLUMNS
99
104
  for _ in range(len(studentlist), students_per_page + 1):
100
- studentlist.append({"name": NAME1, "password": PASSWORD1, "login_url": URL})
105
+ studentlist.append(
106
+ {"name": NAME1, "password": PASSWORD1, "login_url": URL}
107
+ )
101
108
 
102
109
  assert len(studentlist) == students_per_page + 1
103
110
 
@@ -136,7 +143,9 @@ class TestTeacherViews(TestCase):
136
143
  reader = csv.reader(io.StringIO(content))
137
144
 
138
145
  access_code = self.class_access_code
139
- class_url = reverse("student_login", kwargs={"access_code": access_code})
146
+ class_url = reverse(
147
+ "student_login", kwargs={"access_code": access_code}
148
+ )
140
149
  row0 = next(reader)
141
150
  assert row0[0].strip() == access_code
142
151
  assert class_url in row0[1].strip()
@@ -175,7 +184,9 @@ class TestTeacherViews(TestCase):
175
184
 
176
185
  def test_daily_activity_student_details(self):
177
186
  c = self.login()
178
- url = reverse("teacher_print_reminder_cards", args=[self.class_access_code])
187
+ url = reverse(
188
+ "teacher_print_reminder_cards", args=[self.class_access_code]
189
+ )
179
190
 
180
191
  data = {
181
192
  "data": json.dumps(
@@ -221,6 +232,48 @@ class TestTeacherViews(TestCase):
221
232
  with pytest.raises(Exception):
222
233
  count_student_details_click("Wrong download method")
223
234
 
235
+ def test_release_verified_student(self):
236
+ c = Client()
237
+ student_login_url = reverse(
238
+ "student_login", args=[self.class_access_code]
239
+ )
240
+ response = c.post(
241
+ student_login_url,
242
+ {
243
+ "username": self.student.new_user.first_name,
244
+ "password": self.password_student,
245
+ },
246
+ )
247
+ assert response.status_code == 302
248
+
249
+ student = Student.objects.get(pk=self.student.pk)
250
+ assert student.user.is_verified
251
+
252
+ c.logout()
253
+ c.login(username=self.email, password=self.password)
254
+
255
+ release_url = reverse(
256
+ "teacher_dismiss_students", args=[self.class_access_code]
257
+ )
258
+ response = c.post(
259
+ release_url,
260
+ {
261
+ "form-TOTAL_FORMS": 1,
262
+ "form-INITIAL_FORMS": 1,
263
+ "form-MIN_NUM_FORMS": 0,
264
+ "form-MAX_NUM_FORMS": 1000,
265
+ "form-0-orig_name": self.student.new_user.first_name,
266
+ "form-0-name": self.student.new_user.first_name,
267
+ "form-0-email": "independent@gmail.com",
268
+ "form-0-confirm_email": "independent@gmail.com",
269
+ "submit_dismiss": "",
270
+ },
271
+ )
272
+ assert response.status_code == 302
273
+
274
+ student = Student.objects.get(pk=self.student.pk)
275
+ assert not student.user.is_verified
276
+
224
277
 
225
278
  class TestLoginViews(TestCase):
226
279
  @classmethod
@@ -238,7 +291,9 @@ class TestLoginViews(TestCase):
238
291
  teacher_email, teacher_password = signup_teacher_directly()
239
292
  create_organisation_directly(teacher_email)
240
293
  _, _, class_access_code = create_class_directly(teacher_email)
241
- student_name, student_password, _ = create_school_student_directly(class_access_code)
294
+ student_name, student_password, _ = create_school_student_directly(
295
+ class_access_code
296
+ )
242
297
 
243
298
  return (
244
299
  teacher_email,
@@ -271,9 +326,16 @@ class TestLoginViews(TestCase):
271
326
  _, _, name, password, class_access_code = self._set_up_test_data()
272
327
 
273
328
  if next_url:
274
- url = reverse("student_login", kwargs={"access_code": class_access_code}) + "?next=/"
329
+ url = (
330
+ reverse(
331
+ "student_login", kwargs={"access_code": class_access_code}
332
+ )
333
+ + "?next=/"
334
+ )
275
335
  else:
276
- url = reverse("student_login", kwargs={"access_code": class_access_code})
336
+ url = reverse(
337
+ "student_login", kwargs={"access_code": class_access_code}
338
+ )
277
339
 
278
340
  c = Client()
279
341
  response = c.post(url, {"username": name, "password": password})
@@ -312,7 +374,9 @@ class TestLoginViews(TestCase):
312
374
 
313
375
  def _get_user_class(self, name, class_access_code):
314
376
  klass = Class.objects.get(access_code=class_access_code)
315
- students = Student.objects.filter(new_user__first_name__iexact=name, class_field=klass)
377
+ students = Student.objects.filter(
378
+ new_user__first_name__iexact=name, class_field=klass
379
+ )
316
380
  assert len(students) == 1
317
381
  user = students[0].new_user
318
382
  return user, klass
@@ -354,7 +418,9 @@ class TestLoginViews(TestCase):
354
418
  _, _, name, password, class_access_code = self._set_up_test_data()
355
419
 
356
420
  c = Client()
357
- url = reverse("student_login", kwargs={"access_code": class_access_code})
421
+ url = reverse(
422
+ "student_login", kwargs={"access_code": class_access_code}
423
+ )
358
424
  c.post(url, {"username": name, "password": password})
359
425
 
360
426
  # check if there's a UserSession data within the last 10 secs
@@ -375,7 +441,9 @@ class TestLoginViews(TestCase):
375
441
  randomname = "randomname"
376
442
 
377
443
  c = Client()
378
- url = reverse("student_login", kwargs={"access_code": class_access_code})
444
+ url = reverse(
445
+ "student_login", kwargs={"access_code": class_access_code}
446
+ )
379
447
  c.post(url, {"username": randomname, "password": "xx"})
380
448
 
381
449
  # check if there's a UserSession data within the last 10 secs
@@ -401,7 +469,9 @@ class TestLoginViews(TestCase):
401
469
 
402
470
  def test_student_direct_login(self):
403
471
  _, _, _, _, class_access_code = self._set_up_test_data()
404
- student, login_id, _, _ = create_student_with_direct_login(class_access_code)
472
+ student, login_id, _, _ = create_student_with_direct_login(
473
+ class_access_code
474
+ )
405
475
 
406
476
  c = Client()
407
477
  assert c.login(user_id=student.new_user.id, login_id=login_id) == True
@@ -523,7 +593,9 @@ class TestViews(TestCase):
523
593
  c = Client()
524
594
 
525
595
  # Login and check initial data
526
- url = reverse("student_login", kwargs={"access_code": class_access_code})
596
+ url = reverse(
597
+ "student_login", kwargs={"access_code": class_access_code}
598
+ )
527
599
  c.post(url, {"username": student_name, "password": student_password})
528
600
 
529
601
  student_dashboard_url = reverse("student_details")
@@ -602,7 +674,9 @@ class TestViews(TestCase):
602
674
 
603
675
  # try again with the correct password
604
676
  url = reverse("delete_account")
605
- response = c.post(url, {"password": password, "unsubscribe_newsletter": "on"})
677
+ response = c.post(
678
+ url, {"password": password, "unsubscribe_newsletter": "on"}
679
+ )
606
680
 
607
681
  assert response.status_code == 302
608
682
  mock_send_dotdigital_email.assert_called_once()
@@ -684,7 +758,9 @@ class TestViews(TestCase):
684
758
 
685
759
  school_id = school.id
686
760
  school_name = school.name
687
- teachers = Teacher.objects.filter(school=school).order_by("new_user__last_name", "new_user__first_name")
761
+ teachers = Teacher.objects.filter(school=school).order_by(
762
+ "new_user__last_name", "new_user__first_name"
763
+ )
688
764
  assert len(teachers) == 3
689
765
 
690
766
  # one of the remaining teachers should be admin (the second in our case, as it's alphabetical)
@@ -715,7 +791,9 @@ class TestViews(TestCase):
715
791
  self.assertEqual(mock_send_dotdigital_email.call_count, 2)
716
792
 
717
793
  # 2 teachers left
718
- teachers = Teacher.objects.filter(school=school).order_by("new_user__last_name", "new_user__first_name")
794
+ teachers = Teacher.objects.filter(school=school).order_by(
795
+ "new_user__last_name", "new_user__first_name"
796
+ )
719
797
  assert len(teachers) == 2
720
798
 
721
799
  # teacher2 should still be admin, teacher4 is not passed admin role because there is teacher2
@@ -727,7 +805,9 @@ class TestViews(TestCase):
727
805
  # delete teacher4
728
806
  anonymise(user4)
729
807
 
730
- teachers = Teacher.objects.filter(school=school).order_by("new_user__last_name", "new_user__first_name")
808
+ teachers = Teacher.objects.filter(school=school).order_by(
809
+ "new_user__last_name", "new_user__first_name"
810
+ )
731
811
  assert len(teachers) == 1
732
812
  u = User.objects.get(id=usrid2)
733
813
  assert u.new_teacher.is_admin
@@ -785,13 +865,17 @@ class TestViews(TestCase):
785
865
  c.logout()
786
866
 
787
867
  @patch("common.helpers.emails.send_dotdigital_email")
788
- def test_registrations_increment_data(self, mock_send_dotdigital_email: Mock):
868
+ def test_registrations_increment_data(
869
+ self, mock_send_dotdigital_email: Mock
870
+ ):
789
871
  c = Client()
790
872
 
791
873
  total_activity = TotalActivity.objects.get(id=1)
792
874
  teacher_registration_count = total_activity.teacher_registrations
793
875
  student_registration_count = total_activity.student_registrations
794
- independent_registration_count = total_activity.independent_registrations
876
+ independent_registration_count = (
877
+ total_activity.independent_registrations
878
+ )
795
879
 
796
880
  response = c.post(
797
881
  reverse("register"),
@@ -811,7 +895,10 @@ class TestViews(TestCase):
811
895
 
812
896
  total_activity = TotalActivity.objects.get(id=1)
813
897
 
814
- assert total_activity.teacher_registrations == teacher_registration_count + 1
898
+ assert (
899
+ total_activity.teacher_registrations
900
+ == teacher_registration_count + 1
901
+ )
815
902
 
816
903
  response = c.post(
817
904
  reverse("register"),
@@ -833,7 +920,10 @@ class TestViews(TestCase):
833
920
 
834
921
  total_activity = TotalActivity.objects.get(id=1)
835
922
 
836
- assert total_activity.independent_registrations == independent_registration_count + 1
923
+ assert (
924
+ total_activity.independent_registrations
925
+ == independent_registration_count + 1
926
+ )
837
927
 
838
928
  teacher_email, teacher_password = signup_teacher_directly()
839
929
  create_organisation_directly(teacher_email)
@@ -849,7 +939,10 @@ class TestViews(TestCase):
849
939
 
850
940
  total_activity = TotalActivity.objects.get(id=1)
851
941
 
852
- assert total_activity.student_registrations == student_registration_count + 3
942
+ assert (
943
+ total_activity.student_registrations
944
+ == student_registration_count + 3
945
+ )
853
946
 
854
947
 
855
948
  # CRON view tests
@@ -868,8 +961,12 @@ class CronTestClient(APIClient):
868
961
  secure=False,
869
962
  **extra,
870
963
  ):
871
- wsgi_response = super().generic(method, path, data, content_type, secure, **extra)
872
- assert 200 <= wsgi_response.status_code < 300, f"Response has error status code: {wsgi_response.status_code}"
964
+ wsgi_response = super().generic(
965
+ method, path, data, content_type, secure, **extra
966
+ )
967
+ assert (
968
+ 200 <= wsgi_response.status_code < 300
969
+ ), f"Response has error status code: {wsgi_response.status_code}"
873
970
 
874
971
  return wsgi_response
875
972
 
@@ -888,7 +985,9 @@ class TestUser(CronTestCase):
888
985
  indy_email, _, _ = create_independent_student_directly()
889
986
 
890
987
  self.teacher_user = User.objects.get(email=teacher_email)
891
- self.teacher_user_profile = UserProfile.objects.get(user=self.teacher_user)
988
+ self.teacher_user_profile = UserProfile.objects.get(
989
+ user=self.teacher_user
990
+ )
892
991
 
893
992
  self.indy_user = User.objects.get(email=indy_email)
894
993
  self.indy_user_profile = UserProfile.objects.get(user=self.indy_user)
@@ -904,11 +1003,17 @@ class TestUser(CronTestCase):
904
1003
  assert_called: bool,
905
1004
  mock_send_dotdigital_email: Mock,
906
1005
  ):
907
- self.teacher_user.date_joined = timezone.now() - timedelta(days=days, hours=12)
1006
+ self.teacher_user.date_joined = timezone.now() - timedelta(
1007
+ days=days, hours=12
1008
+ )
908
1009
  self.teacher_user.save()
909
- self.student_user.date_joined = timezone.now() - timedelta(days=days, hours=12)
1010
+ self.student_user.date_joined = timezone.now() - timedelta(
1011
+ days=days, hours=12
1012
+ )
910
1013
  self.student_user.save()
911
- self.indy_user.date_joined = timezone.now() - timedelta(days=days, hours=12)
1014
+ self.indy_user.date_joined = timezone.now() - timedelta(
1015
+ days=days, hours=12
1016
+ )
912
1017
  self.indy_user.save()
913
1018
 
914
1019
  self.teacher_user_profile.is_verified = is_verified
@@ -919,9 +1024,13 @@ class TestUser(CronTestCase):
919
1024
  self.client.get(reverse(view_name))
920
1025
 
921
1026
  if assert_called:
922
- mock_send_dotdigital_email.assert_any_call(ANY, [self.teacher_user.email], personalization_values=ANY)
1027
+ mock_send_dotdigital_email.assert_any_call(
1028
+ ANY, [self.teacher_user.email], personalization_values=ANY
1029
+ )
923
1030
 
924
- mock_send_dotdigital_email.assert_any_call(ANY, [self.indy_user.email], personalization_values=ANY)
1031
+ mock_send_dotdigital_email.assert_any_call(
1032
+ ANY, [self.indy_user.email], personalization_values=ANY
1033
+ )
925
1034
 
926
1035
  # Check only two emails are sent - the student should never be included.
927
1036
  assert mock_send_dotdigital_email.call_count == 2
@@ -931,22 +1040,40 @@ class TestUser(CronTestCase):
931
1040
  mock_send_dotdigital_email.reset_mock()
932
1041
 
933
1042
  def test_first_verify_email_reminder_view(self):
934
- self.send_verify_email_reminder(6, False, "first-verify-email-reminder", False)
935
- self.send_verify_email_reminder(7, False, "first-verify-email-reminder", True)
936
- self.send_verify_email_reminder(7, True, "first-verify-email-reminder", False)
937
- self.send_verify_email_reminder(8, False, "first-verify-email-reminder", False)
1043
+ self.send_verify_email_reminder(
1044
+ 6, False, "first-verify-email-reminder", False
1045
+ )
1046
+ self.send_verify_email_reminder(
1047
+ 7, False, "first-verify-email-reminder", True
1048
+ )
1049
+ self.send_verify_email_reminder(
1050
+ 7, True, "first-verify-email-reminder", False
1051
+ )
1052
+ self.send_verify_email_reminder(
1053
+ 8, False, "first-verify-email-reminder", False
1054
+ )
938
1055
 
939
1056
  def test_second_verify_email_reminder_view(self):
940
- self.send_verify_email_reminder(13, False, "second-verify-email-reminder", False)
941
- self.send_verify_email_reminder(14, False, "second-verify-email-reminder", True)
942
- self.send_verify_email_reminder(14, True, "second-verify-email-reminder", False)
943
- self.send_verify_email_reminder(15, False, "second-verify-email-reminder", False)
1057
+ self.send_verify_email_reminder(
1058
+ 13, False, "second-verify-email-reminder", False
1059
+ )
1060
+ self.send_verify_email_reminder(
1061
+ 14, False, "second-verify-email-reminder", True
1062
+ )
1063
+ self.send_verify_email_reminder(
1064
+ 14, True, "second-verify-email-reminder", False
1065
+ )
1066
+ self.send_verify_email_reminder(
1067
+ 15, False, "second-verify-email-reminder", False
1068
+ )
944
1069
 
945
1070
  def test_anonymise_unverified_accounts_view(self):
946
1071
  now = timezone.now()
947
1072
 
948
1073
  for user in [self.teacher_user, self.indy_user, self.student_user]:
949
- user.date_joined = now - timedelta(days=USER_DELETE_UNVERIFIED_ACCOUNT_DAYS + 1)
1074
+ user.date_joined = now - timedelta(
1075
+ days=USER_DELETE_UNVERIFIED_ACCOUNT_DAYS + 1
1076
+ )
950
1077
  user.save()
951
1078
 
952
1079
  for user_profile in [self.teacher_user_profile, self.indy_user_profile]:
@@ -1011,7 +1138,9 @@ class TestUser(CronTestCase):
1011
1138
  new_user=indy_user,
1012
1139
  )
1013
1140
 
1014
- activity_today = DailyActivity.objects.get_or_create(date=datetime.now().date())[0]
1141
+ activity_today = DailyActivity.objects.get_or_create(
1142
+ date=datetime.now().date()
1143
+ )[0]
1015
1144
  daily_teacher_count = activity_today.anonymised_unverified_teachers
1016
1145
  daily_indy_count = activity_today.anonymised_unverified_independents
1017
1146
 
@@ -1034,16 +1163,30 @@ class TestUser(CronTestCase):
1034
1163
  assert indy_user_active == assert_active
1035
1164
  assert student_user_active
1036
1165
 
1037
- activity_today = DailyActivity.objects.get_or_create(date=datetime.now().date())[0]
1166
+ activity_today = DailyActivity.objects.get_or_create(
1167
+ date=datetime.now().date()
1168
+ )[0]
1038
1169
  total_activity = TotalActivity.objects.get(id=1)
1039
1170
 
1040
1171
  if not teacher_user_active:
1041
- assert activity_today.anonymised_unverified_teachers == daily_teacher_count + 1
1042
- assert total_activity.anonymised_unverified_teachers == total_teacher_count + 1
1172
+ assert (
1173
+ activity_today.anonymised_unverified_teachers
1174
+ == daily_teacher_count + 1
1175
+ )
1176
+ assert (
1177
+ total_activity.anonymised_unverified_teachers
1178
+ == total_teacher_count + 1
1179
+ )
1043
1180
 
1044
1181
  if not indy_user_active:
1045
- assert activity_today.anonymised_unverified_independents == daily_indy_count + 1
1046
- assert total_activity.anonymised_unverified_independents == total_indy_count + 1
1182
+ assert (
1183
+ activity_today.anonymised_unverified_independents
1184
+ == daily_indy_count + 1
1185
+ )
1186
+ assert (
1187
+ total_activity.anonymised_unverified_independents
1188
+ == total_indy_count + 1
1189
+ )
1047
1190
 
1048
1191
  teacher_user.delete()
1049
1192
  indy_user.delete()
@@ -552,15 +552,16 @@ def process_dismiss_student_form(request, formset, klass, access_code):
552
552
  student.new_user.first_name = data["name"]
553
553
  student.new_user.username = data["email"]
554
554
  student.new_user.email = data["email"]
555
+ student.user.is_verified = False
555
556
  student.save()
556
557
  student.new_user.save()
557
- student.new_user.userprofile.save()
558
+ student.user.save()
558
559
 
559
560
  # log the data
560
561
  joinrelease = JoinReleaseStudent.objects.create(student=student, action_type=JoinReleaseStudent.RELEASE)
561
562
  joinrelease.save()
562
563
 
563
- send_verification_email(request, student.new_user, data)
564
+ send_verification_email(request, student.new_user, data, school=klass.teacher.school)
564
565
 
565
566
  if not failed_users:
566
567
  messages.success(request, "The students have been released successfully from the class.")