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.
- cfl_common/common/helpers/emails.py +18 -10
- cfl_common/common/mail.py +1 -0
- cfl_common/common/migrations/0052_add_cse_fields.py +68 -0
- cfl_common/common/models.py +148 -36
- {codeforlife_portal-6.44.9.dist-info → codeforlife_portal-6.45.0.dist-info}/METADATA +2 -2
- {codeforlife_portal-6.44.9.dist-info → codeforlife_portal-6.45.0.dist-info}/RECORD +14 -13
- portal/__init__.py +1 -1
- portal/forms/play.py +10 -6
- portal/tests/test_independent_student.py +7 -3
- portal/tests/test_views.py +188 -45
- portal/views/teacher/teach.py +3 -2
- {codeforlife_portal-6.44.9.dist-info → codeforlife_portal-6.45.0.dist-info}/LICENSE.md +0 -0
- {codeforlife_portal-6.44.9.dist-info → codeforlife_portal-6.45.0.dist-info}/WHEEL +0 -0
- {codeforlife_portal-6.44.9.dist-info → codeforlife_portal-6.45.0.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
107
|
+
send_dotdigital_email(
|
|
108
|
+
campaign_ids["verify_new_user"], [user.email], personalization_values={"VERIFICATION_LINK": url}
|
|
109
|
+
)
|
|
102
110
|
|
|
103
|
-
|
|
104
|
-
|
|
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
|
@@ -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
|
+
]
|
cfl_common/common/models.py
CHANGED
|
@@ -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
|
|
20
|
-
# deleted every time the Game gets
|
|
21
|
-
#
|
|
22
|
-
#
|
|
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(
|
|
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
|
|
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,
|
|
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(
|
|
90
|
-
|
|
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",
|
|
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
|
|
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(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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(
|
|
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,
|
|
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
|
|
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 =
|
|
202
|
-
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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(
|
|
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
|
|
321
|
-
student details download clicks, through the CSV and login
|
|
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
|
|
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
|
|
365
|
-
|
|
366
|
-
|
|
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.
|
|
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.
|
|
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=
|
|
9
|
-
cfl_common/common/models.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
646
|
-
codeforlife_portal-6.
|
|
647
|
-
codeforlife_portal-6.
|
|
648
|
-
codeforlife_portal-6.
|
|
649
|
-
codeforlife_portal-6.
|
|
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.
|
|
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(
|
|
285
|
+
raise forms.ValidationError(join_error_text)
|
|
286
|
+
|
|
285
287
|
self.klass = classes[0]
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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()
|
portal/tests/test_views.py
CHANGED
|
@@ -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
|
-
_,
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
329
|
+
url = (
|
|
330
|
+
reverse(
|
|
331
|
+
"student_login", kwargs={"access_code": class_access_code}
|
|
332
|
+
)
|
|
333
|
+
+ "?next=/"
|
|
334
|
+
)
|
|
275
335
|
else:
|
|
276
|
-
url = reverse(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
872
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
self.send_verify_email_reminder(
|
|
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(
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
self.send_verify_email_reminder(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
1042
|
-
|
|
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
|
|
1046
|
-
|
|
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()
|
portal/views/teacher/teach.py
CHANGED
|
@@ -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.
|
|
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.")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|