codeforlife-portal 6.44.9__py2.py3-none-any.whl → 6.45.0__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
 
@@ -0,0 +1,68 @@
1
+ # Generated by Django 3.2.25 on 2024-05-22 11:30
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('common', '0051_verify_returning_users'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='schoolteacherinvitation',
15
+ name='_invited_teacher_email',
16
+ field=models.BinaryField(blank=True, null=True),
17
+ ),
18
+ migrations.AddField(
19
+ model_name='schoolteacherinvitation',
20
+ name='_invited_teacher_first_name',
21
+ field=models.BinaryField(blank=True, null=True),
22
+ ),
23
+ migrations.AddField(
24
+ model_name='schoolteacherinvitation',
25
+ name='_invited_teacher_last_name',
26
+ field=models.BinaryField(blank=True, null=True),
27
+ ),
28
+ migrations.AddField(
29
+ model_name='userprofile',
30
+ name='_email',
31
+ field=models.BinaryField(blank=True, null=True),
32
+ ),
33
+ migrations.AddField(
34
+ model_name='userprofile',
35
+ name='_first_name',
36
+ field=models.BinaryField(blank=True, null=True),
37
+ ),
38
+ migrations.AddField(
39
+ model_name='userprofile',
40
+ name='_last_name',
41
+ field=models.BinaryField(blank=True, null=True),
42
+ ),
43
+ migrations.AddField(
44
+ model_name='userprofile',
45
+ name='_username',
46
+ field=models.BinaryField(blank=True, null=True),
47
+ ),
48
+ migrations.AddField(
49
+ model_name='userprofile',
50
+ name='email',
51
+ field=models.CharField(blank=True, max_length=200, null=True),
52
+ ),
53
+ migrations.AddField(
54
+ model_name='userprofile',
55
+ name='first_name',
56
+ field=models.CharField(blank=True, max_length=200, null=True),
57
+ ),
58
+ migrations.AddField(
59
+ model_name='userprofile',
60
+ name='last_name',
61
+ field=models.CharField(blank=True, max_length=200, null=True),
62
+ ),
63
+ migrations.AddField(
64
+ model_name='userprofile',
65
+ name='username',
66
+ field=models.CharField(blank=True, max_length=200, null=True),
67
+ ),
68
+ ]
@@ -16,12 +16,25 @@ class UserProfile(models.Model):
16
16
  developer = models.BooleanField(default=False)
17
17
  is_verified = models.BooleanField(default=False)
18
18
 
19
- # Holds the user's earned kurono badges. This information has to be on the UserProfile as the Avatar objects are
20
- # deleted every time the Game gets deleted.
21
- # This is a string showing which badges in which worksheets have been earned. The format is "X:Y" where X is the
22
- # worksheet ID and Y is the badge ID. This repeats for all badges and each pair is comma-separated.
19
+ # Holds the user's earned kurono badges. This information has to be on the
20
+ # UserProfile as the Avatar objects are deleted every time the Game gets
21
+ # deleted.
22
+ # This is a string showing which badges in which worksheets have been
23
+ # earned. The format is "X:Y" where X is the worksheet ID and Y is the
24
+ # badge ID. This repeats for all badges and each pair is comma-separated.
23
25
  aimmo_badges = models.CharField(max_length=200, null=True, blank=True)
24
26
 
27
+ # TODO: Make not nullable once data has been transferred
28
+ first_name = models.CharField(max_length=200, null=True, blank=True)
29
+ _first_name = models.BinaryField(null=True, blank=True)
30
+ last_name = models.CharField(max_length=200, null=True, blank=True)
31
+ _last_name = models.BinaryField(null=True, blank=True)
32
+ email = models.CharField(max_length=200, null=True, blank=True)
33
+ _email = models.BinaryField(null=True, blank=True)
34
+ # TODO: Make not nullable once data has been transferred
35
+ username = models.CharField(max_length=200, null=True, blank=True)
36
+ _username = models.BinaryField(null=True, blank=True)
37
+
25
38
  def __str__(self):
26
39
  return f"{self.user.first_name} {self.user.last_name}"
27
40
 
@@ -38,7 +51,9 @@ class SchoolModelManager(models.Manager):
38
51
 
39
52
  class School(models.Model):
40
53
  name = models.CharField(max_length=200, unique=True)
41
- country = CountryField(blank_label="(select country)", null=True, blank=True)
54
+ country = CountryField(
55
+ blank_label="(select country)", null=True, blank=True
56
+ )
42
57
  # TODO: Create an Address model to house address details
43
58
  county = models.CharField(max_length=50, blank=True, null=True)
44
59
  creation_time = models.DateTimeField(default=timezone.now, null=True)
@@ -61,7 +76,11 @@ class School(models.Model):
61
76
 
62
77
  def admins(self):
63
78
  teachers = self.teacher_school.all()
64
- return [teacher for teacher in teachers if teacher.is_admin] if teachers else None
79
+ return (
80
+ [teacher for teacher in teachers if teacher.is_admin]
81
+ if teachers
82
+ else None
83
+ )
65
84
 
66
85
  def anonymise(self):
67
86
  self.name = uuid4().hex
@@ -72,7 +91,11 @@ class School(models.Model):
72
91
  class TeacherModelManager(models.Manager):
73
92
  def factory(self, first_name, last_name, email, password):
74
93
  user = User.objects.create_user(
75
- username=email, email=email, password=password, first_name=first_name, last_name=last_name
94
+ username=email,
95
+ email=email,
96
+ password=password,
97
+ first_name=first_name,
98
+ last_name=last_name,
76
99
  )
77
100
 
78
101
  user_profile = UserProfile.objects.create(user=user)
@@ -86,12 +109,28 @@ class TeacherModelManager(models.Manager):
86
109
 
87
110
  class Teacher(models.Model):
88
111
  user = models.OneToOneField(UserProfile, on_delete=models.CASCADE)
89
- new_user = models.OneToOneField(User, related_name="new_teacher", null=True, blank=True, on_delete=models.CASCADE)
90
- school = models.ForeignKey(School, related_name="teacher_school", null=True, blank=True, on_delete=models.SET_NULL)
112
+ new_user = models.OneToOneField(
113
+ User,
114
+ related_name="new_teacher",
115
+ null=True,
116
+ blank=True,
117
+ on_delete=models.CASCADE,
118
+ )
119
+ school = models.ForeignKey(
120
+ School,
121
+ related_name="teacher_school",
122
+ null=True,
123
+ blank=True,
124
+ on_delete=models.SET_NULL,
125
+ )
91
126
  is_admin = models.BooleanField(default=False)
92
127
  blocked_time = models.DateTimeField(null=True, blank=True)
93
128
  invited_by = models.ForeignKey(
94
- "self", related_name="invited_teachers", null=True, blank=True, on_delete=models.SET_NULL
129
+ "self",
130
+ related_name="invited_teachers",
131
+ null=True,
132
+ blank=True,
133
+ on_delete=models.SET_NULL,
95
134
  )
96
135
 
97
136
  objects = TeacherModelManager()
@@ -99,7 +138,10 @@ class Teacher(models.Model):
99
138
  def teaches(self, userprofile):
100
139
  if hasattr(userprofile, "student"):
101
140
  student = userprofile.student
102
- return not student.is_independent() and student.class_field.teacher == self
141
+ return (
142
+ not student.is_independent()
143
+ and student.class_field.teacher == self
144
+ )
103
145
 
104
146
  def has_school(self):
105
147
  return self.school is not (None or "")
@@ -119,11 +161,32 @@ class SchoolTeacherInvitationModelManager(models.Manager):
119
161
 
120
162
  class SchoolTeacherInvitation(models.Model):
121
163
  token = models.CharField(max_length=32)
122
- school = models.ForeignKey(School, related_name="teacher_invitations", null=True, on_delete=models.SET_NULL)
123
- from_teacher = models.ForeignKey(Teacher, related_name="school_invitations", null=True, on_delete=models.SET_NULL)
124
- invited_teacher_first_name = models.CharField(max_length=150) # Same as User model
125
- invited_teacher_last_name = models.CharField(max_length=150) # Same as User model
164
+ school = models.ForeignKey(
165
+ School,
166
+ related_name="teacher_invitations",
167
+ null=True,
168
+ on_delete=models.SET_NULL,
169
+ )
170
+ from_teacher = models.ForeignKey(
171
+ Teacher,
172
+ related_name="school_invitations",
173
+ null=True,
174
+ on_delete=models.SET_NULL,
175
+ )
176
+ invited_teacher_first_name = models.CharField(
177
+ max_length=150
178
+ ) # Same as User model
179
+ # TODO: Make not nullable once data has been transferred
180
+ _invited_teacher_first_name = models.BinaryField(null=True, blank=True)
181
+ invited_teacher_last_name = models.CharField(
182
+ max_length=150
183
+ ) # Same as User model
184
+ # TODO: Make not nullable once data has been transferred
185
+ _invited_teacher_last_name = models.BinaryField(null=True, blank=True)
186
+ # TODO: Switch to a CharField to be able to hold hashed value
126
187
  invited_teacher_email = models.EmailField() # Same as User model
188
+ # TODO: Make not nullable once data has been transferred
189
+ _invited_teacher_email = models.BinaryField(null=True, blank=True)
127
190
  invited_teacher_is_admin = models.BooleanField(default=False)
128
191
  expiry = models.DateTimeField()
129
192
  creation_time = models.DateTimeField(default=timezone.now, null=True)
@@ -168,7 +231,9 @@ class ClassModelManager(models.Manager):
168
231
 
169
232
  class Class(models.Model):
170
233
  name = models.CharField(max_length=200)
171
- teacher = models.ForeignKey(Teacher, related_name="class_teacher", on_delete=models.CASCADE)
234
+ teacher = models.ForeignKey(
235
+ Teacher, related_name="class_teacher", on_delete=models.CASCADE
236
+ )
172
237
  access_code = models.CharField(max_length=5, null=True)
173
238
  classmates_data_viewable = models.BooleanField(default=False)
174
239
  always_accept_requests = models.BooleanField(default=False)
@@ -176,7 +241,11 @@ class Class(models.Model):
176
241
  creation_time = models.DateTimeField(default=timezone.now, null=True)
177
242
  is_active = models.BooleanField(default=True)
178
243
  created_by = models.ForeignKey(
179
- Teacher, null=True, blank=True, related_name="created_classes", on_delete=models.SET_NULL
244
+ Teacher,
245
+ null=True,
246
+ blank=True,
247
+ related_name="created_classes",
248
+ on_delete=models.SET_NULL,
180
249
  )
181
250
 
182
251
  objects = ClassModelManager()
@@ -188,7 +257,9 @@ class Class(models.Model):
188
257
  def active_game(self):
189
258
  games = self.game_set.filter(game_class=self, is_archived=False)
190
259
  if len(games) >= 1:
191
- assert len(games) == 1 # there should NOT be more than one active game
260
+ assert (
261
+ len(games) == 1
262
+ ) # there should NOT be more than one active game
192
263
  return games[0]
193
264
  return None
194
265
 
@@ -198,8 +269,13 @@ class Class(models.Model):
198
269
 
199
270
  def get_requests_message(self):
200
271
  if self.always_accept_requests:
201
- external_requests_message = "This class is currently set to always accept requests."
202
- elif self.accept_requests_until is not None and (self.accept_requests_until - timezone.now()) >= timedelta():
272
+ external_requests_message = (
273
+ "This class is currently set to always accept requests."
274
+ )
275
+ elif (
276
+ self.accept_requests_until is not None
277
+ and (self.accept_requests_until - timezone.now()) >= timedelta()
278
+ ):
203
279
  external_requests_message = (
204
280
  "This class is accepting external requests until "
205
281
  + self.accept_requests_until.strftime("%d-%m-%Y %H:%M")
@@ -207,7 +283,9 @@ class Class(models.Model):
207
283
  + timezone.get_current_timezone_name()
208
284
  )
209
285
  else:
210
- external_requests_message = "This class is not currently accepting external requests."
286
+ external_requests_message = (
287
+ "This class is not currently accepting external requests."
288
+ )
211
289
 
212
290
  return external_requests_message
213
291
 
@@ -229,7 +307,9 @@ class UserSession(models.Model):
229
307
  login_time = models.DateTimeField(default=timezone.now)
230
308
  school = models.ForeignKey(School, null=True, on_delete=models.SET_NULL)
231
309
  class_field = models.ForeignKey(Class, null=True, on_delete=models.SET_NULL)
232
- login_type = models.CharField(max_length=100, null=True) # for student login
310
+ login_type = models.CharField(
311
+ max_length=100, null=True
312
+ ) # for student login
233
313
 
234
314
  def __str__(self):
235
315
  return f"{self.user} login: {self.login_time} type: {self.login_type}"
@@ -243,13 +323,24 @@ class StudentModelManager(models.Manager):
243
323
  return random_username
244
324
 
245
325
  def schoolFactory(self, klass, name, password, login_id=None):
246
- user = User.objects.create_user(username=self.get_random_username(), password=password, first_name=name)
326
+ user = User.objects.create_user(
327
+ username=self.get_random_username(),
328
+ password=password,
329
+ first_name=name,
330
+ )
247
331
  user_profile = UserProfile.objects.create(user=user)
248
332
 
249
- return Student.objects.create(class_field=klass, user=user_profile, new_user=user, login_id=login_id)
333
+ return Student.objects.create(
334
+ class_field=klass,
335
+ user=user_profile,
336
+ new_user=user,
337
+ login_id=login_id,
338
+ )
250
339
 
251
340
  def independentStudentFactory(self, name, email, password):
252
- user = User.objects.create_user(username=email, email=email, password=password, first_name=name)
341
+ user = User.objects.create_user(
342
+ username=email, email=email, password=password, first_name=name
343
+ )
253
344
 
254
345
  user_profile = UserProfile.objects.create(user=user)
255
346
 
@@ -257,13 +348,29 @@ class StudentModelManager(models.Manager):
257
348
 
258
349
 
259
350
  class Student(models.Model):
260
- class_field = models.ForeignKey(Class, related_name="students", null=True, blank=True, on_delete=models.CASCADE)
351
+ class_field = models.ForeignKey(
352
+ Class,
353
+ related_name="students",
354
+ null=True,
355
+ blank=True,
356
+ on_delete=models.CASCADE,
357
+ )
261
358
  # hashed uuid used for the unique direct login url
262
359
  login_id = models.CharField(max_length=64, null=True)
263
360
  user = models.OneToOneField(UserProfile, on_delete=models.CASCADE)
264
- new_user = models.OneToOneField(User, related_name="new_student", null=True, blank=True, on_delete=models.CASCADE)
361
+ new_user = models.OneToOneField(
362
+ User,
363
+ related_name="new_student",
364
+ null=True,
365
+ blank=True,
366
+ on_delete=models.CASCADE,
367
+ )
265
368
  pending_class_request = models.ForeignKey(
266
- Class, related_name="class_request", null=True, blank=True, on_delete=models.SET_NULL
369
+ Class,
370
+ related_name="class_request",
371
+ null=True,
372
+ blank=True,
373
+ on_delete=models.SET_NULL,
267
374
  )
268
375
  blocked_time = models.DateTimeField(null=True, blank=True)
269
376
 
@@ -309,7 +416,9 @@ class JoinReleaseStudent(models.Model):
309
416
  JOIN = "join"
310
417
  RELEASE = "release"
311
418
 
312
- student = models.ForeignKey(Student, related_name="student", on_delete=models.CASCADE)
419
+ student = models.ForeignKey(
420
+ Student, related_name="student", on_delete=models.CASCADE
421
+ )
313
422
  # either "release" or "join"
314
423
  action_type = models.CharField(max_length=64)
315
424
  action_time = models.DateTimeField(default=timezone.now)
@@ -317,8 +426,9 @@ class JoinReleaseStudent(models.Model):
317
426
 
318
427
  class DailyActivity(models.Model):
319
428
  """
320
- A model to record sets of daily activity. Currently used to record the amount of
321
- student details download clicks, through the CSV and login cards methods, per day.
429
+ A model to record sets of daily activity. Currently used to record the
430
+ amount of student details download clicks, through the CSV and login
431
+ cards methods, per day.
322
432
  """
323
433
 
324
434
  date = models.DateField(default=timezone.now)
@@ -342,8 +452,8 @@ class DailyActivity(models.Model):
342
452
 
343
453
  class TotalActivity(models.Model):
344
454
  """
345
- A model to record total activity. Meant to only have one entry which records all total activity.
346
- An example of this is total ever registrations.
455
+ A model to record total activity. Meant to only have one entry which
456
+ records all total activity. An example of this is total ever registrations.
347
457
  """
348
458
 
349
459
  teacher_registrations = models.PositiveIntegerField(default=0)
@@ -361,9 +471,11 @@ class TotalActivity(models.Model):
361
471
 
362
472
  class DynamicElement(models.Model):
363
473
  """
364
- This model is meant to allow us to quickly update some elements dynamically on the website without having to
365
- redeploy everytime. For example, if a maintenance banner needs to be added, we check the box in the Django admin
366
- panel, edit the text and it'll show immediately on the website.
474
+ This model is meant to allow us to quickly update some elements
475
+ dynamically on the website without having to redeploy everytime. For
476
+ example, if a maintenance banner needs to be added, we check the box in
477
+ the Django admin panel, edit the text and it'll show immediately on the
478
+ website.
367
479
  """
368
480
 
369
481
  name = models.CharField(max_length=64, unique=True, editable=False)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: codeforlife-portal
3
- Version: 6.44.9
3
+ Version: 6.45.0
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.9
28
+ Requires-Dist: cfl-common ==6.45.0
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
9
- cfl_common/common/models.py,sha256=EunFsc7sOWfWiFf4IQwuy56gu8pu3YpPoOgVtsMhbRM,14958
8
+ cfl_common/common/mail.py,sha256=Lmn7CepceKw1UqxjxTR_24fo7izecw0ihBJ3W_9tcco,6394
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
@@ -65,6 +65,7 @@ cfl_common/common/migrations/0048_unique_school_names.py,sha256=pu5xiuesvFNGngD-
65
65
  cfl_common/common/migrations/0049_anonymise_orphan_users.py,sha256=tw9xMrDMRPDCO8HWjBVlnQF8r1YVCKZnVr2wZ3He6og,847
66
66
  cfl_common/common/migrations/0050_anonymise_orphan_schools.py,sha256=_KCkSkoObTpLplX6gXvlV3JXpddn7neyJEa8YKFWeW0,869
67
67
  cfl_common/common/migrations/0051_verify_returning_users.py,sha256=WMpoTA24WgimLEVmKXuPqZ3aZdClRhY5vuGtYseeJp0,758
68
+ cfl_common/common/migrations/0052_add_cse_fields.py,sha256=NhUkkcu2EBzJFhewCTccQ63AoANkGq1CXbFWIGJG9jk,2232
68
69
  cfl_common/common/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
69
70
  cfl_common/common/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
70
71
  cfl_common/common/tests/test_migration_aimmo_characters.py,sha256=pdCCsns90Qz05QqmaBUYK18jKe9aP-symtZjkKG4rag,1079
@@ -105,7 +106,7 @@ example_project/portal_test_settings.py,sha256=frp_XMpd-z1g3VFCRxB2w7AaFW2ivRVKn
105
106
  example_project/settings.py,sha256=XRZZvASoIl5a9xe3masTq_CUBleuJq9ByHx8f_e2UFc,5613
106
107
  example_project/urls.py,sha256=OVeRQ-TCpzHISBRuzqD0yd3ewF7H5U3c-f2p2alfUD0,430
107
108
  example_project/wsgi.py,sha256=U1W6WzZxZaIdYZ5tks7w9fqp5WS5qvn2iThsVcskrWw,829
108
- portal/__init__.py,sha256=JY9WjqnqvCSgGAQFOkgL-a5XXdNSPHLA2nGQUzKtRMo,23
109
+ portal/__init__.py,sha256=oVokL7bm_I6q-ZEmSiMqfc1pQbhzWmTkE47idKwv0l4,23
109
110
  portal/admin.py,sha256=on1-zNRnZvf2cwBN6GVRVYRhkaksrCgfzX8XPWtkvz8,6062
110
111
  portal/app_settings.py,sha256=DhWLQOwM0zVOXE3O5TNKbMM9K6agfLuCsHOdr1J7xEI,651
111
112
  portal/backends.py,sha256=2Dss6_WoQwPuDzJUF1yEaTQTNG4eUrD12ujJQ5cp5Tc,812
@@ -122,7 +123,7 @@ portal/forms/dotmailer.py,sha256=McD9_u8yxUfE7PSVG3MPMilRJtx9GTe9QsNzDDV3uuI,761
122
123
  portal/forms/error_messages.py,sha256=8d3z_3e2L-5zwj5hFhnUByC5k2CEpIVVuJg2nYkCUQ8,148
123
124
  portal/forms/invite_teacher.py,sha256=jkDNcCfkts4_lXRzhcI3xBam21Zn2yX9wMpMVhDtW1w,880
124
125
  portal/forms/organisation.py,sha256=QcQyd7AiqBmvt4y8uQSQylguUbKOKqo2pjqWIkpWjDg,7433
125
- portal/forms/play.py,sha256=IO0gfKfTv7lXEN1K9w0XG8vY-55obBqLpiEBWaUluZ8,11351
126
+ portal/forms/play.py,sha256=z9P5LzyS3jjYcnfco84d2x8ptgLxRmh94Dnj05plmbY,11505
126
127
  portal/forms/registration.py,sha256=gWcY7rllhWO3c9as6QHUDWZx1Jme7DqtGHYaKcvxe-U,5990
127
128
  portal/forms/teach.py,sha256=-3dMQxIQtYq2xg5DgtIJMpN7RajNhTvc56Clr5QjsHo,20440
128
129
  portal/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -544,7 +545,7 @@ portal/tests/test_class.py,sha256=V6Fkc6PqdisefKD3xs9PbfE2pKp-9e0gwQVkPUiu6bk,14
544
545
  portal/tests/test_daily_activities.py,sha256=-siDCMGBD1ijjccHVk7eEmrk4bgTsvbh0B6hDoj2fo0,1803
545
546
  portal/tests/test_emails.py,sha256=xNgOt592r2nrQu7VeBdc8kvSnw6Z5fPJqMx2-UMcyyk,9482
546
547
  portal/tests/test_helper_methods.py,sha256=-SQCDZm2XUtyXGEp0CHIb_SSC9CPD-XOSnpnY8QclHk,890
547
- portal/tests/test_independent_student.py,sha256=jyNpHyisCfkD-CDsjIfSxWnWTxDmkReJ-BdXEBdJoMo,27081
548
+ portal/tests/test_independent_student.py,sha256=mRbWZVwbZKPqHajTd9bYrCAn9ZJLpexpk-YSiZuDAbM,27540
548
549
  portal/tests/test_invite_teacher.py,sha256=oeOaoJV1IqJSYPlaPFjnhVXdB2mq8otCTLp_lfjuCfk,12224
549
550
  portal/tests/test_middleware.py,sha256=b6jfNmiRZ2snqLKsyJUG-RivoX5fmrqLlQkG9MeVnqM,8034
550
551
  portal/tests/test_newsletter_footer.py,sha256=MdVUX53mEoDTa4Krq-jg9LFNo-QyghqvTvhHeNXBGnE,838
@@ -555,7 +556,7 @@ portal/tests/test_school_student.py,sha256=bFZwY4twaFHQLp0cltMq8cLNDZGgCHTZBCZHK
555
556
  portal/tests/test_security.py,sha256=FGrlRfnzi-Xx2_bn4fTZlYORKm7w_GhGkD3havvplwc,3239
556
557
  portal/tests/test_teacher.py,sha256=_VmQCWq07uCFbvq6Vd7GN00mE7vY7WNMeQTk6bHxFPI,36898
557
558
  portal/tests/test_teacher_student.py,sha256=NWITbUw1kijqu3c8eRHLHJKaYQMOsOMvl7PAVx5QghI,21567
558
- portal/tests/test_views.py,sha256=IElQJnp9fhlmqR-54nBycul2uUIjsG9dTRnYy5SxXJ4,39278
559
+ portal/tests/test_views.py,sha256=g6WQtexZ-UfwpNxpTmfiB-RTBtLFiYBmjAa5oFCSBDk,42073
559
560
  portal/tests/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
560
561
  portal/tests/migrations/test_migration_make_portaladmin_teacher.py,sha256=ekMRb6cU97oT0k9gCKW7IUB7oPuGmv4uWJCqInQN7x8,2589
561
562
  portal/tests/migrations/test_migration_preview_user_remove.py,sha256=K6D-FZT9YFEA8oMxHz9VTglVV6MZOTRYVlvwWwXc2vU,555
@@ -637,13 +638,13 @@ portal/views/student/edit_account_details.py,sha256=Ba-3D_zzKbX5N01NG5qqBS0ud10B
637
638
  portal/views/student/play.py,sha256=r5TADH_wYn3d1beezfvkYBiendQ9qLys9dUJwHqF_44,8581
638
639
  portal/views/teacher/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
639
640
  portal/views/teacher/dashboard.py,sha256=8WglspwuHF__2LtoX5_XvoW1ulICSupjKv--MtjrvJk,25714
640
- portal/views/teacher/teach.py,sha256=B71jReMJ4BYFmo7NtJVK3-4DeXEwxfu_WA3Ij1RYzdI,34725
641
+ portal/views/teacher/teach.py,sha256=PJAUjLeIBELWc5e2Eek0mr8kYleYWb1MH9FF4O2q7Ok,34779
641
642
  portal/views/two_factor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
642
643
  portal/views/two_factor/core.py,sha256=O_wcBeFqdPYSGNGv-pT_vbs5-Dj1Z-Jfkd6f9-E5yZI,760
643
644
  portal/views/two_factor/form.py,sha256=lnHNKI-BMlpncTuW3zUzjPaJJNuEra2I_nOam0eOKFY,257
644
645
  portal/views/two_factor/profile.py,sha256=tkl_ludo8arMtd5LKNmohM66vpC_YQiP-0nspTSJiJ4,383
645
- codeforlife_portal-6.44.9.dist-info/LICENSE.md,sha256=9AbRlCDqD2D1tPibimysFv3zg3AIc49-eyv9aEsyq9w,115
646
- codeforlife_portal-6.44.9.dist-info/METADATA,sha256=g8gbiNBuQ78vTv6D3utFHHXsuEGTOIOvckD-SNGb3ag,3474
647
- codeforlife_portal-6.44.9.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
648
- codeforlife_portal-6.44.9.dist-info/top_level.txt,sha256=8e5pdsuIoTqEAMqpelHBjGjLbffcBtgOoggmd2q7nMw,41
649
- codeforlife_portal-6.44.9.dist-info/RECORD,,
646
+ codeforlife_portal-6.45.0.dist-info/LICENSE.md,sha256=9AbRlCDqD2D1tPibimysFv3zg3AIc49-eyv9aEsyq9w,115
647
+ codeforlife_portal-6.45.0.dist-info/METADATA,sha256=wLw9Jy35iptYZbEN8byRqGpjW7IWIUl9iDNyDJTyBjs,3474
648
+ codeforlife_portal-6.45.0.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110
649
+ codeforlife_portal-6.45.0.dist-info/top_level.txt,sha256=8e5pdsuIoTqEAMqpelHBjGjLbffcBtgOoggmd2q7nMw,41
650
+ codeforlife_portal-6.45.0.dist-info/RECORD,,
portal/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "6.44.9"
1
+ __version__ = "6.45.0"
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
@@ -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.")