codeforlife-portal 7.2.1__py2.py3-none-any.whl → 7.3.1__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of codeforlife-portal might be problematic. Click here for more details.
- {codeforlife_portal-7.2.1.dist-info → codeforlife_portal-7.3.1.dist-info}/METADATA +2 -2
- {codeforlife_portal-7.2.1.dist-info → codeforlife_portal-7.3.1.dist-info}/RECORD +11 -11
- example_project/settings.py +0 -1
- portal/__init__.py +1 -1
- portal/forms/teach.py +206 -56
- portal/templates/portal/play.html +1 -1
- portal/templates/portal/teach/teacher_edit_class.html +57 -1
- portal/views/teacher/teach.py +294 -83
- {codeforlife_portal-7.2.1.dist-info → codeforlife_portal-7.3.1.dist-info}/LICENSE.md +0 -0
- {codeforlife_portal-7.2.1.dist-info → codeforlife_portal-7.3.1.dist-info}/WHEEL +0 -0
- {codeforlife_portal-7.2.1.dist-info → codeforlife_portal-7.3.1.dist-info}/top_level.txt +0 -0
portal/views/teacher/teach.py
CHANGED
|
@@ -7,8 +7,20 @@ from functools import partial, wraps
|
|
|
7
7
|
from uuid import uuid4
|
|
8
8
|
|
|
9
9
|
from common.helpers.emails import send_verification_email
|
|
10
|
-
from common.helpers.generators import
|
|
11
|
-
|
|
10
|
+
from common.helpers.generators import (
|
|
11
|
+
generate_access_code,
|
|
12
|
+
generate_login_id,
|
|
13
|
+
generate_password,
|
|
14
|
+
get_hashed_login_id,
|
|
15
|
+
)
|
|
16
|
+
from common.models import (
|
|
17
|
+
Class,
|
|
18
|
+
DailyActivity,
|
|
19
|
+
JoinReleaseStudent,
|
|
20
|
+
Student,
|
|
21
|
+
Teacher,
|
|
22
|
+
TotalActivity,
|
|
23
|
+
)
|
|
12
24
|
from common.permissions import check_teacher_authorised, logged_in_as_teacher
|
|
13
25
|
from django.contrib import messages as messages
|
|
14
26
|
from django.contrib.auth.decorators import login_required, user_passes_test
|
|
@@ -21,7 +33,7 @@ from django.shortcuts import get_object_or_404, render
|
|
|
21
33
|
from django.urls import reverse, reverse_lazy
|
|
22
34
|
from django.utils import timezone
|
|
23
35
|
from django.views.decorators.http import require_POST
|
|
24
|
-
from game.views.level_selection import get_blockly_episodes
|
|
36
|
+
from game.views.level_selection import get_blockly_episodes, get_python_episodes
|
|
25
37
|
from portal.views.registration import handle_reset_password_tracking
|
|
26
38
|
from reportlab.lib.colors import black, red
|
|
27
39
|
from reportlab.lib.pagesizes import A4
|
|
@@ -47,7 +59,9 @@ from portal.forms.teach import (
|
|
|
47
59
|
STUDENT_PASSWORD_LENGTH = 6
|
|
48
60
|
REMINDER_CARDS_PDF_ROWS = 8
|
|
49
61
|
REMINDER_CARDS_PDF_COLUMNS = 1
|
|
50
|
-
REMINDER_CARDS_PDF_WARNING_TEXT =
|
|
62
|
+
REMINDER_CARDS_PDF_WARNING_TEXT = (
|
|
63
|
+
"Please ensure students keep login details in a secure place"
|
|
64
|
+
)
|
|
51
65
|
|
|
52
66
|
|
|
53
67
|
@login_required(login_url=reverse_lazy("teacher_login"))
|
|
@@ -57,7 +71,9 @@ def teacher_onboarding_create_class(request):
|
|
|
57
71
|
Onboarding view for creating a class (and organisation if there isn't one, yet)
|
|
58
72
|
"""
|
|
59
73
|
teacher = request.user.new_teacher
|
|
60
|
-
requests = Student.objects.filter(
|
|
74
|
+
requests = Student.objects.filter(
|
|
75
|
+
pending_class_request__teacher=teacher, new_user__is_active=True
|
|
76
|
+
)
|
|
61
77
|
|
|
62
78
|
if not teacher.school:
|
|
63
79
|
return HttpResponseRedirect(reverse_lazy("onboarding-organisation"))
|
|
@@ -67,10 +83,16 @@ def teacher_onboarding_create_class(request):
|
|
|
67
83
|
if form.is_valid():
|
|
68
84
|
created_class = create_class(form, teacher)
|
|
69
85
|
messages.success(
|
|
70
|
-
request,
|
|
86
|
+
request,
|
|
87
|
+
"The class '{className}' has been created successfully.".format(
|
|
88
|
+
className=created_class.name
|
|
89
|
+
),
|
|
71
90
|
)
|
|
72
91
|
return HttpResponseRedirect(
|
|
73
|
-
reverse_lazy(
|
|
92
|
+
reverse_lazy(
|
|
93
|
+
"onboarding-class",
|
|
94
|
+
kwargs={"access_code": created_class.access_code},
|
|
95
|
+
)
|
|
74
96
|
)
|
|
75
97
|
else:
|
|
76
98
|
form = ClassCreationForm(teacher=teacher)
|
|
@@ -78,7 +100,9 @@ def teacher_onboarding_create_class(request):
|
|
|
78
100
|
classes = Class.objects.filter(teacher=teacher)
|
|
79
101
|
|
|
80
102
|
return render(
|
|
81
|
-
request,
|
|
103
|
+
request,
|
|
104
|
+
"portal/teach/onboarding_classes.html",
|
|
105
|
+
{"form": form, "classes": classes, "requests": requests},
|
|
82
106
|
)
|
|
83
107
|
|
|
84
108
|
|
|
@@ -96,7 +120,10 @@ def create_class(form, class_teacher, class_creator=None):
|
|
|
96
120
|
|
|
97
121
|
def generate_student_url(request, student, login_id):
|
|
98
122
|
return request.build_absolute_uri(
|
|
99
|
-
reverse(
|
|
123
|
+
reverse(
|
|
124
|
+
"student_direct_login",
|
|
125
|
+
kwargs={"user_id": student.new_user.id, "login_id": login_id},
|
|
126
|
+
)
|
|
100
127
|
)
|
|
101
128
|
|
|
102
129
|
|
|
@@ -106,7 +133,9 @@ def process_edit_class(request, access_code, onboarding_done, next_url):
|
|
|
106
133
|
"""
|
|
107
134
|
klass = get_object_or_404(Class, access_code=access_code)
|
|
108
135
|
teacher = request.user.new_teacher
|
|
109
|
-
students = Student.objects.filter(
|
|
136
|
+
students = Student.objects.filter(
|
|
137
|
+
class_field=klass, new_user__is_active=True
|
|
138
|
+
).order_by("new_user__first_name")
|
|
110
139
|
|
|
111
140
|
check_teacher_authorised(request, klass.teacher)
|
|
112
141
|
|
|
@@ -121,14 +150,24 @@ def process_edit_class(request, access_code, onboarding_done, next_url):
|
|
|
121
150
|
login_id, hashed_login_id = generate_login_id()
|
|
122
151
|
|
|
123
152
|
new_student = Student.objects.schoolFactory(
|
|
124
|
-
klass=klass,
|
|
153
|
+
klass=klass,
|
|
154
|
+
name=name,
|
|
155
|
+
password=password,
|
|
156
|
+
login_id=hashed_login_id,
|
|
125
157
|
)
|
|
126
158
|
|
|
127
|
-
TotalActivity.objects.update(
|
|
159
|
+
TotalActivity.objects.update(
|
|
160
|
+
student_registrations=F("student_registrations") + 1
|
|
161
|
+
)
|
|
128
162
|
|
|
129
163
|
login_url = generate_student_url(request, new_student, login_id)
|
|
130
164
|
students_info.append(
|
|
131
|
-
{
|
|
165
|
+
{
|
|
166
|
+
"id": new_student.new_user.id,
|
|
167
|
+
"name": name,
|
|
168
|
+
"password": password,
|
|
169
|
+
"login_url": login_url,
|
|
170
|
+
}
|
|
132
171
|
)
|
|
133
172
|
|
|
134
173
|
return render(
|
|
@@ -140,7 +179,10 @@ def process_edit_class(request, access_code, onboarding_done, next_url):
|
|
|
140
179
|
"onboarding_done": onboarding_done,
|
|
141
180
|
"query_data": json.dumps(students_info),
|
|
142
181
|
"class_url": request.build_absolute_uri(
|
|
143
|
-
reverse(
|
|
182
|
+
reverse(
|
|
183
|
+
"student_login",
|
|
184
|
+
kwargs={"access_code": klass.access_code},
|
|
185
|
+
)
|
|
144
186
|
),
|
|
145
187
|
},
|
|
146
188
|
)
|
|
@@ -169,7 +211,10 @@ def teacher_onboarding_edit_class(request, access_code):
|
|
|
169
211
|
Adding students to a class during the onboarding process
|
|
170
212
|
"""
|
|
171
213
|
return process_edit_class(
|
|
172
|
-
request,
|
|
214
|
+
request,
|
|
215
|
+
access_code,
|
|
216
|
+
onboarding_done=False,
|
|
217
|
+
next_url="portal/teach/onboarding_students.html",
|
|
173
218
|
)
|
|
174
219
|
|
|
175
220
|
|
|
@@ -179,7 +224,12 @@ def teacher_view_class(request, access_code):
|
|
|
179
224
|
"""
|
|
180
225
|
Adding students to a class after the onboarding process has been completed
|
|
181
226
|
"""
|
|
182
|
-
return process_edit_class(
|
|
227
|
+
return process_edit_class(
|
|
228
|
+
request,
|
|
229
|
+
access_code,
|
|
230
|
+
onboarding_done=True,
|
|
231
|
+
next_url="portal/teach/class.html",
|
|
232
|
+
)
|
|
183
233
|
|
|
184
234
|
|
|
185
235
|
@require_POST
|
|
@@ -191,11 +241,16 @@ def teacher_delete_class(request, access_code):
|
|
|
191
241
|
# check user authorised to see class
|
|
192
242
|
check_teacher_authorised(request, klass.teacher)
|
|
193
243
|
|
|
194
|
-
if Student.objects.filter(
|
|
244
|
+
if Student.objects.filter(
|
|
245
|
+
class_field=klass, new_user__is_active=True
|
|
246
|
+
).exists():
|
|
195
247
|
messages.info(
|
|
196
|
-
request,
|
|
248
|
+
request,
|
|
249
|
+
"This class still has students, please remove or delete them all before deleting the class.",
|
|
250
|
+
)
|
|
251
|
+
return HttpResponseRedirect(
|
|
252
|
+
reverse_lazy("view_class", kwargs={"access_code": access_code})
|
|
197
253
|
)
|
|
198
|
-
return HttpResponseRedirect(reverse_lazy("view_class", kwargs={"access_code": access_code}))
|
|
199
254
|
|
|
200
255
|
klass.anonymise()
|
|
201
256
|
|
|
@@ -212,7 +267,9 @@ def teacher_delete_students(request, access_code):
|
|
|
212
267
|
|
|
213
268
|
# get student objects for students to be deleted, confirming they are in the class
|
|
214
269
|
student_ids = json.loads(request.POST.get("transfer_students", "[]"))
|
|
215
|
-
students = [
|
|
270
|
+
students = [
|
|
271
|
+
get_object_or_404(Student, id=i, class_field=klass) for i in student_ids
|
|
272
|
+
]
|
|
216
273
|
|
|
217
274
|
def __anonymise(user):
|
|
218
275
|
# Delete all personal data from inactive user and mark as inactive.
|
|
@@ -234,7 +291,9 @@ def teacher_delete_students(request, access_code):
|
|
|
234
291
|
else: # otherwise, just delete
|
|
235
292
|
student.new_user.delete()
|
|
236
293
|
|
|
237
|
-
return HttpResponseRedirect(
|
|
294
|
+
return HttpResponseRedirect(
|
|
295
|
+
reverse_lazy("view_class", kwargs={"access_code": access_code})
|
|
296
|
+
)
|
|
238
297
|
|
|
239
298
|
|
|
240
299
|
@login_required(login_url=reverse_lazy("teacher_login"))
|
|
@@ -248,7 +307,9 @@ def teacher_edit_class(request, access_code):
|
|
|
248
307
|
"""
|
|
249
308
|
klass = get_object_or_404(Class, access_code=access_code)
|
|
250
309
|
old_teacher = klass.teacher
|
|
251
|
-
other_teachers = Teacher.objects.filter(school=old_teacher.school).exclude(
|
|
310
|
+
other_teachers = Teacher.objects.filter(school=old_teacher.school).exclude(
|
|
311
|
+
user=old_teacher.user
|
|
312
|
+
)
|
|
252
313
|
|
|
253
314
|
# check user authorised to see class
|
|
254
315
|
check_teacher_authorised(request, klass.teacher)
|
|
@@ -256,11 +317,17 @@ def teacher_edit_class(request, access_code):
|
|
|
256
317
|
external_requests_message = klass.get_requests_message()
|
|
257
318
|
|
|
258
319
|
blockly_episodes = get_blockly_episodes(request)
|
|
320
|
+
python_episodes = get_python_episodes(request)
|
|
259
321
|
|
|
260
322
|
locked_levels = klass.locked_levels.all()
|
|
261
323
|
locked_levels_ids = [locked_level.id for locked_level in locked_levels]
|
|
262
324
|
|
|
263
|
-
form = ClassEditForm(
|
|
325
|
+
form = ClassEditForm(
|
|
326
|
+
initial={
|
|
327
|
+
"name": klass.name,
|
|
328
|
+
"classmate_progress": klass.classmates_data_viewable,
|
|
329
|
+
}
|
|
330
|
+
)
|
|
264
331
|
level_control_form = ClassLevelControlForm()
|
|
265
332
|
class_move_form = ClassMoveForm(other_teachers)
|
|
266
333
|
|
|
@@ -272,7 +339,9 @@ def teacher_edit_class(request, access_code):
|
|
|
272
339
|
elif "level_control_submit" in request.POST:
|
|
273
340
|
level_control_form = ClassLevelControlForm(request.POST)
|
|
274
341
|
if level_control_form.is_valid():
|
|
275
|
-
return process_level_control_form(
|
|
342
|
+
return process_level_control_form(
|
|
343
|
+
request, klass, blockly_episodes, python_episodes
|
|
344
|
+
)
|
|
276
345
|
elif "class_move_submit" in request.POST:
|
|
277
346
|
class_move_form = ClassMoveForm(other_teachers, request.POST)
|
|
278
347
|
if class_move_form.is_valid():
|
|
@@ -286,6 +355,7 @@ def teacher_edit_class(request, access_code):
|
|
|
286
355
|
"class_move_form": class_move_form,
|
|
287
356
|
"level_control_form": level_control_form,
|
|
288
357
|
"blockly_episodes": blockly_episodes,
|
|
358
|
+
"python_episodes": python_episodes,
|
|
289
359
|
"locked_levels": locked_levels_ids,
|
|
290
360
|
"class": klass,
|
|
291
361
|
"external_requests_message": external_requests_message,
|
|
@@ -305,12 +375,17 @@ def process_edit_class_form(request, klass, form):
|
|
|
305
375
|
# Setting to off
|
|
306
376
|
klass.always_accept_requests = False
|
|
307
377
|
klass.accept_requests_until = None
|
|
308
|
-
messages.info(
|
|
378
|
+
messages.info(
|
|
379
|
+
request,
|
|
380
|
+
"Class set successfully to never receive requests from external students.",
|
|
381
|
+
)
|
|
309
382
|
|
|
310
383
|
elif hours < 1000:
|
|
311
384
|
# Setting to number of hours
|
|
312
385
|
klass.always_accept_requests = False
|
|
313
|
-
klass.accept_requests_until = timezone.now() + timedelta(
|
|
386
|
+
klass.accept_requests_until = timezone.now() + timedelta(
|
|
387
|
+
hours=hours
|
|
388
|
+
)
|
|
314
389
|
messages.info(
|
|
315
390
|
request,
|
|
316
391
|
"Class set successfully to receive requests from external students until "
|
|
@@ -324,35 +399,53 @@ def process_edit_class_form(request, klass, form):
|
|
|
324
399
|
klass.always_accept_requests = True
|
|
325
400
|
klass.accept_requests_until = None
|
|
326
401
|
messages.info(
|
|
327
|
-
request,
|
|
402
|
+
request,
|
|
403
|
+
"Class set successfully to always receive requests from external students (not recommended)",
|
|
328
404
|
)
|
|
329
405
|
|
|
330
406
|
klass.name = name
|
|
331
407
|
klass.classmates_data_viewable = classmate_progress
|
|
332
408
|
klass.save()
|
|
333
409
|
|
|
334
|
-
messages.success(
|
|
410
|
+
messages.success(
|
|
411
|
+
request, "The class's settings have been changed successfully."
|
|
412
|
+
)
|
|
335
413
|
|
|
336
|
-
return HttpResponseRedirect(
|
|
414
|
+
return HttpResponseRedirect(
|
|
415
|
+
reverse_lazy("view_class", kwargs={"access_code": klass.access_code})
|
|
416
|
+
)
|
|
337
417
|
|
|
338
418
|
|
|
339
|
-
def process_level_control_form(
|
|
419
|
+
def process_level_control_form(
|
|
420
|
+
request, klass, blockly_episodes, python_episodes
|
|
421
|
+
):
|
|
340
422
|
"""
|
|
341
423
|
Find the levels that the user wants to lock and lock them for the specific class.
|
|
342
424
|
:param request: The request sent by the user submitting the form.
|
|
343
425
|
:param klass: The class for which the levels are being locked / unlocked.
|
|
344
|
-
:param blockly_episodes: The set of Blockly Episodes
|
|
426
|
+
:param blockly_episodes: The set of Blockly Episodes (Rapid Router).
|
|
427
|
+
:param blockly_episodes: The set of Python Episodes (Python Den).
|
|
345
428
|
:return: A redirect to the teacher dashboard with a success message.
|
|
346
429
|
"""
|
|
347
430
|
levels_to_lock_ids = []
|
|
348
431
|
|
|
349
|
-
mark_levels_to_lock_in_episodes(
|
|
432
|
+
mark_levels_to_lock_in_episodes(
|
|
433
|
+
request, blockly_episodes, levels_to_lock_ids
|
|
434
|
+
)
|
|
435
|
+
mark_levels_to_lock_in_episodes(
|
|
436
|
+
request, python_episodes, levels_to_lock_ids
|
|
437
|
+
)
|
|
350
438
|
|
|
351
439
|
klass.locked_levels.clear()
|
|
352
|
-
[
|
|
440
|
+
[
|
|
441
|
+
klass.locked_levels.add(levels_to_lock_id)
|
|
442
|
+
for levels_to_lock_id in levels_to_lock_ids
|
|
443
|
+
]
|
|
353
444
|
|
|
354
445
|
messages.success(request, "Your level preferences have been saved.")
|
|
355
|
-
activity_today = DailyActivity.objects.get_or_create(
|
|
446
|
+
activity_today = DailyActivity.objects.get_or_create(
|
|
447
|
+
date=datetime.now().date()
|
|
448
|
+
)[0]
|
|
356
449
|
activity_today.level_control_submits += 1
|
|
357
450
|
activity_today.save()
|
|
358
451
|
|
|
@@ -375,10 +468,14 @@ def mark_levels_to_lock_in_episodes(request, episodes, levels_to_lock_ids):
|
|
|
375
468
|
[
|
|
376
469
|
levels_to_lock_ids.append(episode_level["id"])
|
|
377
470
|
for episode_level in episode_levels
|
|
378
|
-
if str(episode_level["id"])
|
|
471
|
+
if str(episode_level["id"])
|
|
472
|
+
not in request.POST.getlist(episode_name)
|
|
379
473
|
]
|
|
380
474
|
else:
|
|
381
|
-
[
|
|
475
|
+
[
|
|
476
|
+
levels_to_lock_ids.append(episode_level["id"])
|
|
477
|
+
for episode_level in episode_levels
|
|
478
|
+
]
|
|
382
479
|
|
|
383
480
|
|
|
384
481
|
def process_move_class_form(request, klass, form):
|
|
@@ -388,7 +485,10 @@ def process_move_class_form(request, klass, form):
|
|
|
388
485
|
klass.teacher = new_teacher
|
|
389
486
|
klass.save()
|
|
390
487
|
|
|
391
|
-
messages.success(
|
|
488
|
+
messages.success(
|
|
489
|
+
request,
|
|
490
|
+
"The class has been successfully assigned to a different teacher.",
|
|
491
|
+
)
|
|
392
492
|
return HttpResponseRedirect(reverse_lazy("dashboard"))
|
|
393
493
|
|
|
394
494
|
|
|
@@ -401,7 +501,9 @@ def teacher_edit_student(request, pk):
|
|
|
401
501
|
student = get_object_or_404(Student, id=pk)
|
|
402
502
|
check_teacher_authorised(request, student.class_field.teacher)
|
|
403
503
|
|
|
404
|
-
name_form = TeacherEditStudentForm(
|
|
504
|
+
name_form = TeacherEditStudentForm(
|
|
505
|
+
student, initial={"name": student.new_user.first_name}
|
|
506
|
+
)
|
|
405
507
|
|
|
406
508
|
password_form = TeacherSetStudentPass()
|
|
407
509
|
set_password_mode = False
|
|
@@ -415,16 +517,24 @@ def teacher_edit_student(request, pk):
|
|
|
415
517
|
student.new_user.save()
|
|
416
518
|
student.save()
|
|
417
519
|
|
|
418
|
-
messages.success(
|
|
520
|
+
messages.success(
|
|
521
|
+
request,
|
|
522
|
+
"The student's details have been changed successfully.",
|
|
523
|
+
)
|
|
419
524
|
|
|
420
525
|
return HttpResponseRedirect(
|
|
421
|
-
reverse_lazy(
|
|
526
|
+
reverse_lazy(
|
|
527
|
+
"view_class",
|
|
528
|
+
kwargs={"access_code": student.class_field.access_code},
|
|
529
|
+
)
|
|
422
530
|
)
|
|
423
531
|
|
|
424
532
|
else:
|
|
425
533
|
password_form = TeacherSetStudentPass(request.POST)
|
|
426
534
|
if password_form.is_valid():
|
|
427
|
-
return process_reset_password_form(
|
|
535
|
+
return process_reset_password_form(
|
|
536
|
+
request, student, password_form
|
|
537
|
+
)
|
|
428
538
|
set_password_mode = True
|
|
429
539
|
|
|
430
540
|
return render(
|
|
@@ -448,7 +558,10 @@ def process_reset_password_form(request, student, password_form):
|
|
|
448
558
|
uuidstr = uuid4().hex
|
|
449
559
|
login_id = get_hashed_login_id(uuidstr)
|
|
450
560
|
login_url = request.build_absolute_uri(
|
|
451
|
-
reverse(
|
|
561
|
+
reverse(
|
|
562
|
+
"student_direct_login",
|
|
563
|
+
kwargs={"user_id": student.new_user.id, "login_id": uuidstr},
|
|
564
|
+
)
|
|
452
565
|
)
|
|
453
566
|
|
|
454
567
|
students_info = [
|
|
@@ -464,7 +577,9 @@ def process_reset_password_form(request, student, password_form):
|
|
|
464
577
|
student.new_user.set_password(new_password)
|
|
465
578
|
student.new_user.save()
|
|
466
579
|
student.login_id = login_id
|
|
467
|
-
clear_ratelimit_cache_for_user(
|
|
580
|
+
clear_ratelimit_cache_for_user(
|
|
581
|
+
f"{student.new_user.first_name},{student.class_field.access_code}"
|
|
582
|
+
)
|
|
468
583
|
student.blocked_time = datetime.now(tz=pytz.utc) - timedelta(days=1)
|
|
469
584
|
student.save()
|
|
470
585
|
|
|
@@ -477,7 +592,10 @@ def process_reset_password_form(request, student, password_form):
|
|
|
477
592
|
"onboarding_done": True,
|
|
478
593
|
"query_data": json.dumps(students_info),
|
|
479
594
|
"class_url": request.build_absolute_uri(
|
|
480
|
-
reverse(
|
|
595
|
+
reverse(
|
|
596
|
+
"student_login",
|
|
597
|
+
kwargs={"access_code": student.class_field.access_code},
|
|
598
|
+
)
|
|
481
599
|
),
|
|
482
600
|
},
|
|
483
601
|
)
|
|
@@ -495,7 +613,9 @@ def teacher_dismiss_students(request, access_code):
|
|
|
495
613
|
|
|
496
614
|
# get student objects for students to be dismissed, confirming they are in the class
|
|
497
615
|
student_ids = json.loads(request.POST.get("transfer_students", "[]"))
|
|
498
|
-
students = [
|
|
616
|
+
students = [
|
|
617
|
+
get_object_or_404(Student, id=i, class_field=klass) for i in student_ids
|
|
618
|
+
]
|
|
499
619
|
|
|
500
620
|
TeacherDismissStudentsFormSet = formset_factory(
|
|
501
621
|
wraps(TeacherDismissStudentsForm)(partial(TeacherDismissStudentsForm)),
|
|
@@ -506,11 +626,17 @@ def teacher_dismiss_students(request, access_code):
|
|
|
506
626
|
if is_right_dismiss_form(request):
|
|
507
627
|
formset = TeacherDismissStudentsFormSet(request.POST)
|
|
508
628
|
if formset.is_valid():
|
|
509
|
-
return process_dismiss_student_form(
|
|
629
|
+
return process_dismiss_student_form(
|
|
630
|
+
request, formset, klass, access_code
|
|
631
|
+
)
|
|
510
632
|
|
|
511
633
|
else:
|
|
512
634
|
initial_data = [
|
|
513
|
-
{
|
|
635
|
+
{
|
|
636
|
+
"orig_name": student.new_user.first_name,
|
|
637
|
+
"name": student.new_user.first_name,
|
|
638
|
+
"email": "",
|
|
639
|
+
}
|
|
514
640
|
for student in students
|
|
515
641
|
]
|
|
516
642
|
|
|
@@ -537,7 +663,11 @@ def process_dismiss_student_form(request, formset, klass, access_code):
|
|
|
537
663
|
failed_users.append(data["orig_name"])
|
|
538
664
|
continue
|
|
539
665
|
|
|
540
|
-
student = get_object_or_404(
|
|
666
|
+
student = get_object_or_404(
|
|
667
|
+
Student,
|
|
668
|
+
class_field=klass,
|
|
669
|
+
new_user__first_name__iexact=data["orig_name"],
|
|
670
|
+
)
|
|
541
671
|
|
|
542
672
|
student.class_field = None
|
|
543
673
|
student.new_user.first_name = data["name"]
|
|
@@ -549,13 +679,20 @@ def process_dismiss_student_form(request, formset, klass, access_code):
|
|
|
549
679
|
student.user.save()
|
|
550
680
|
|
|
551
681
|
# log the data
|
|
552
|
-
joinrelease = JoinReleaseStudent.objects.create(
|
|
682
|
+
joinrelease = JoinReleaseStudent.objects.create(
|
|
683
|
+
student=student, action_type=JoinReleaseStudent.RELEASE
|
|
684
|
+
)
|
|
553
685
|
joinrelease.save()
|
|
554
686
|
|
|
555
|
-
send_verification_email(
|
|
687
|
+
send_verification_email(
|
|
688
|
+
request, student.new_user, data, school=klass.teacher.school
|
|
689
|
+
)
|
|
556
690
|
|
|
557
691
|
if not failed_users:
|
|
558
|
-
messages.success(
|
|
692
|
+
messages.success(
|
|
693
|
+
request,
|
|
694
|
+
"The students have been released successfully from the class.",
|
|
695
|
+
)
|
|
559
696
|
else:
|
|
560
697
|
messages.warning(
|
|
561
698
|
request,
|
|
@@ -563,7 +700,9 @@ def process_dismiss_student_form(request, formset, klass, access_code):
|
|
|
563
700
|
"Please make sure the email has not been registered to another account.",
|
|
564
701
|
)
|
|
565
702
|
|
|
566
|
-
return HttpResponseRedirect(
|
|
703
|
+
return HttpResponseRedirect(
|
|
704
|
+
reverse_lazy("view_class", kwargs={"access_code": access_code})
|
|
705
|
+
)
|
|
567
706
|
|
|
568
707
|
|
|
569
708
|
@login_required(login_url=reverse_lazy("teacher_login"))
|
|
@@ -578,7 +717,9 @@ def teacher_class_password_reset(request, access_code):
|
|
|
578
717
|
check_teacher_authorised(request, klass.teacher)
|
|
579
718
|
|
|
580
719
|
student_ids = json.loads(request.POST.get("transfer_students", "[]"))
|
|
581
|
-
students = [
|
|
720
|
+
students = [
|
|
721
|
+
get_object_or_404(Student, id=i, class_field=klass) for i in student_ids
|
|
722
|
+
]
|
|
582
723
|
|
|
583
724
|
students_info = []
|
|
584
725
|
handle_reset_password_tracking(request, "SCHOOL_STUDENT", access_code)
|
|
@@ -600,7 +741,9 @@ def teacher_class_password_reset(request, access_code):
|
|
|
600
741
|
student.new_user.set_password(password)
|
|
601
742
|
student.new_user.save()
|
|
602
743
|
student.login_id = hashed_login_id
|
|
603
|
-
clear_ratelimit_cache_for_user(
|
|
744
|
+
clear_ratelimit_cache_for_user(
|
|
745
|
+
f"{student.new_user.first_name},{access_code}"
|
|
746
|
+
)
|
|
604
747
|
student.blocked_time = datetime.now(tz=pytz.utc) - timedelta(days=1)
|
|
605
748
|
student.save()
|
|
606
749
|
|
|
@@ -614,7 +757,9 @@ def teacher_class_password_reset(request, access_code):
|
|
|
614
757
|
"students_info": students_info,
|
|
615
758
|
"query_data": json.dumps(students_info),
|
|
616
759
|
"class_url": request.build_absolute_uri(
|
|
617
|
-
reverse(
|
|
760
|
+
reverse(
|
|
761
|
+
"student_login", kwargs={"access_code": klass.access_code}
|
|
762
|
+
)
|
|
618
763
|
),
|
|
619
764
|
},
|
|
620
765
|
)
|
|
@@ -644,7 +789,11 @@ def teacher_move_students(request, access_code):
|
|
|
644
789
|
return render(
|
|
645
790
|
request,
|
|
646
791
|
"portal/teach/teacher_move_students.html",
|
|
647
|
-
{
|
|
792
|
+
{
|
|
793
|
+
"transfer_students": transfer_students,
|
|
794
|
+
"old_class": klass,
|
|
795
|
+
"form": form,
|
|
796
|
+
},
|
|
648
797
|
)
|
|
649
798
|
|
|
650
799
|
|
|
@@ -660,34 +809,50 @@ def teacher_move_students_to_class(request, access_code):
|
|
|
660
809
|
|
|
661
810
|
check_if_move_authorised(request, old_class, new_class)
|
|
662
811
|
|
|
663
|
-
transfer_students_ids = json.loads(
|
|
812
|
+
transfer_students_ids = json.loads(
|
|
813
|
+
request.POST.get("transfer_students", "[]")
|
|
814
|
+
)
|
|
664
815
|
|
|
665
816
|
# get student objects for students to be transferred, confirming they are in the old class still
|
|
666
|
-
transfer_students = [
|
|
817
|
+
transfer_students = [
|
|
818
|
+
get_object_or_404(Student, id=i, class_field=old_class)
|
|
819
|
+
for i in transfer_students_ids
|
|
820
|
+
]
|
|
667
821
|
|
|
668
822
|
# get new class' students
|
|
669
|
-
new_class_students = Student.objects.filter(
|
|
670
|
-
|
|
671
|
-
)
|
|
823
|
+
new_class_students = Student.objects.filter(
|
|
824
|
+
class_field=new_class, new_user__is_active=True
|
|
825
|
+
).order_by("new_user__first_name")
|
|
672
826
|
|
|
673
827
|
TeacherMoveStudentDisambiguationFormSet = formset_factory(
|
|
674
|
-
wraps(TeacherMoveStudentDisambiguationForm)(
|
|
828
|
+
wraps(TeacherMoveStudentDisambiguationForm)(
|
|
829
|
+
partial(TeacherMoveStudentDisambiguationForm)
|
|
830
|
+
),
|
|
675
831
|
extra=0,
|
|
676
832
|
formset=BaseTeacherMoveStudentsDisambiguationFormSet,
|
|
677
833
|
)
|
|
678
834
|
|
|
679
835
|
if is_right_move_form(request):
|
|
680
|
-
formset = TeacherMoveStudentDisambiguationFormSet(
|
|
836
|
+
formset = TeacherMoveStudentDisambiguationFormSet(
|
|
837
|
+
new_class, request.POST
|
|
838
|
+
)
|
|
681
839
|
if formset.is_valid():
|
|
682
|
-
return process_move_students_form(
|
|
840
|
+
return process_move_students_form(
|
|
841
|
+
request, formset, old_class, new_class
|
|
842
|
+
)
|
|
683
843
|
else:
|
|
684
844
|
# format the students for the form
|
|
685
845
|
initial_data = [
|
|
686
|
-
{
|
|
846
|
+
{
|
|
847
|
+
"orig_name": student.new_user.first_name,
|
|
848
|
+
"name": student.new_user.first_name,
|
|
849
|
+
}
|
|
687
850
|
for student in transfer_students
|
|
688
851
|
]
|
|
689
852
|
|
|
690
|
-
formset = TeacherMoveStudentDisambiguationFormSet(
|
|
853
|
+
formset = TeacherMoveStudentDisambiguationFormSet(
|
|
854
|
+
new_class, initial=initial_data
|
|
855
|
+
)
|
|
691
856
|
|
|
692
857
|
return render(
|
|
693
858
|
request,
|
|
@@ -722,7 +887,9 @@ def process_move_students_form(request, formset, old_class, new_class):
|
|
|
722
887
|
|
|
723
888
|
for name_update in formset.cleaned_data:
|
|
724
889
|
student = get_object_or_404(
|
|
725
|
-
Student,
|
|
890
|
+
Student,
|
|
891
|
+
class_field=old_class,
|
|
892
|
+
new_user__first_name__iexact=name_update["orig_name"],
|
|
726
893
|
)
|
|
727
894
|
student.class_field = new_class
|
|
728
895
|
student.new_user.first_name = name_update["name"]
|
|
@@ -730,8 +897,14 @@ def process_move_students_form(request, formset, old_class, new_class):
|
|
|
730
897
|
student.save()
|
|
731
898
|
student.new_user.save()
|
|
732
899
|
|
|
733
|
-
messages.success(
|
|
734
|
-
|
|
900
|
+
messages.success(
|
|
901
|
+
request, "The students have been transferred successfully."
|
|
902
|
+
)
|
|
903
|
+
return HttpResponseRedirect(
|
|
904
|
+
reverse_lazy(
|
|
905
|
+
"view_class", kwargs={"access_code": old_class.access_code}
|
|
906
|
+
)
|
|
907
|
+
)
|
|
735
908
|
|
|
736
909
|
|
|
737
910
|
class DownloadType(Enum):
|
|
@@ -764,7 +937,9 @@ def teacher_print_reminder_cards(request, access_code):
|
|
|
764
937
|
|
|
765
938
|
CARD_INNER_HEIGHT = CARD_HEIGHT - CARD_PADDING * 2
|
|
766
939
|
|
|
767
|
-
logo_image = ImageReader(
|
|
940
|
+
logo_image = ImageReader(
|
|
941
|
+
staticfiles_storage.path("portal/img/logo_cfl_reminder_cards.jpg")
|
|
942
|
+
)
|
|
768
943
|
|
|
769
944
|
klass = get_object_or_404(Class, access_code=access_code)
|
|
770
945
|
# Check auth
|
|
@@ -772,8 +947,12 @@ def teacher_print_reminder_cards(request, access_code):
|
|
|
772
947
|
|
|
773
948
|
# Use data from the query string if given
|
|
774
949
|
student_data = get_student_data(request)
|
|
775
|
-
student_login_link = request.build_absolute_uri(
|
|
776
|
-
|
|
950
|
+
student_login_link = request.build_absolute_uri(
|
|
951
|
+
reverse("student_login_access_code")
|
|
952
|
+
)
|
|
953
|
+
class_login_link = request.build_absolute_uri(
|
|
954
|
+
reverse("student_login", kwargs={"access_code": access_code})
|
|
955
|
+
)
|
|
777
956
|
|
|
778
957
|
# Now draw everything
|
|
779
958
|
x = 0
|
|
@@ -785,10 +964,17 @@ def teacher_print_reminder_cards(request, access_code):
|
|
|
785
964
|
if current_student_count % (NUM_X * NUM_Y) == 0:
|
|
786
965
|
p.setFillColor(red)
|
|
787
966
|
p.setFont("Helvetica-Bold", 10)
|
|
788
|
-
p.drawString(
|
|
967
|
+
p.drawString(
|
|
968
|
+
PAGE_MARGIN, PAGE_MARGIN / 2, REMINDER_CARDS_PDF_WARNING_TEXT
|
|
969
|
+
)
|
|
789
970
|
|
|
790
971
|
left = PAGE_MARGIN + x * CARD_WIDTH + x * INTER_CARD_MARGIN * 2
|
|
791
|
-
bottom =
|
|
972
|
+
bottom = (
|
|
973
|
+
PAGE_HEIGHT
|
|
974
|
+
- PAGE_MARGIN
|
|
975
|
+
- (y + 1) * CARD_HEIGHT
|
|
976
|
+
- y * INTER_CARD_MARGIN
|
|
977
|
+
)
|
|
792
978
|
|
|
793
979
|
inner_bottom = bottom + CARD_PADDING
|
|
794
980
|
|
|
@@ -808,7 +994,12 @@ def teacher_print_reminder_cards(request, access_code):
|
|
|
808
994
|
anchor="w",
|
|
809
995
|
)
|
|
810
996
|
|
|
811
|
-
text_left =
|
|
997
|
+
text_left = (
|
|
998
|
+
left
|
|
999
|
+
+ INTER_CARD_MARGIN
|
|
1000
|
+
+ (logo_image.getSize()[0] / logo_image.getSize()[1])
|
|
1001
|
+
* card_logo_height
|
|
1002
|
+
)
|
|
812
1003
|
|
|
813
1004
|
# student details
|
|
814
1005
|
p.setFillColor(black)
|
|
@@ -821,9 +1012,19 @@ def teacher_print_reminder_cards(request, access_code):
|
|
|
821
1012
|
p.setFont("Helvetica-BoldOblique", 12)
|
|
822
1013
|
p.drawString(text_left, inner_bottom + CARD_INNER_HEIGHT * 0.6, "OR")
|
|
823
1014
|
p.setFont("Helvetica", 12)
|
|
824
|
-
p.drawString(
|
|
825
|
-
|
|
826
|
-
|
|
1015
|
+
p.drawString(
|
|
1016
|
+
text_left + 22,
|
|
1017
|
+
inner_bottom + CARD_INNER_HEIGHT * 0.6,
|
|
1018
|
+
f"class link: {class_login_link}",
|
|
1019
|
+
)
|
|
1020
|
+
p.drawString(
|
|
1021
|
+
text_left,
|
|
1022
|
+
inner_bottom + CARD_INNER_HEIGHT * 0.3,
|
|
1023
|
+
f"Name: {student['name']}",
|
|
1024
|
+
)
|
|
1025
|
+
p.drawString(
|
|
1026
|
+
text_left, inner_bottom, f"Password: {student['password']}"
|
|
1027
|
+
)
|
|
827
1028
|
|
|
828
1029
|
x = (x + 1) % NUM_X
|
|
829
1030
|
y = compute_show_page_character(p, x, y, NUM_Y)
|
|
@@ -842,13 +1043,17 @@ def teacher_print_reminder_cards(request, access_code):
|
|
|
842
1043
|
@user_passes_test(logged_in_as_teacher, login_url=reverse_lazy("teacher_login"))
|
|
843
1044
|
def teacher_download_csv(request, access_code):
|
|
844
1045
|
response = HttpResponse(content_type="text/csv")
|
|
845
|
-
response[
|
|
1046
|
+
response[
|
|
1047
|
+
"Content-Disposition"
|
|
1048
|
+
] = 'attachment; filename="student_login_urls.csv"'
|
|
846
1049
|
|
|
847
1050
|
klass = get_object_or_404(Class, access_code=access_code)
|
|
848
1051
|
# Check auth
|
|
849
1052
|
check_teacher_authorised(request, klass.teacher)
|
|
850
1053
|
|
|
851
|
-
class_url = request.build_absolute_uri(
|
|
1054
|
+
class_url = request.build_absolute_uri(
|
|
1055
|
+
reverse("student_login", kwargs={"access_code": access_code})
|
|
1056
|
+
)
|
|
852
1057
|
|
|
853
1058
|
# Use data from the query string if given
|
|
854
1059
|
student_data = get_student_data(request)
|
|
@@ -856,7 +1061,9 @@ def teacher_download_csv(request, access_code):
|
|
|
856
1061
|
writer = csv.writer(response)
|
|
857
1062
|
writer.writerow([access_code, class_url])
|
|
858
1063
|
for student in student_data:
|
|
859
|
-
writer.writerow(
|
|
1064
|
+
writer.writerow(
|
|
1065
|
+
[student["name"], student["password"], student["login_url"]]
|
|
1066
|
+
)
|
|
860
1067
|
|
|
861
1068
|
count_student_details_click(DownloadType.CSV)
|
|
862
1069
|
|
|
@@ -884,7 +1091,9 @@ def compute_show_page_end(p, x, y):
|
|
|
884
1091
|
|
|
885
1092
|
|
|
886
1093
|
def count_student_pack_downloads_click(student_pack_type):
|
|
887
|
-
activity_today = DailyActivity.objects.get_or_create(
|
|
1094
|
+
activity_today = DailyActivity.objects.get_or_create(
|
|
1095
|
+
date=datetime.now().date()
|
|
1096
|
+
)[0]
|
|
888
1097
|
if DownloadType(student_pack_type) == DownloadType.PRIMARY_PACK:
|
|
889
1098
|
activity_today.primary_coding_club_downloads += 1
|
|
890
1099
|
elif DownloadType(student_pack_type) == DownloadType.PYTHON_PACK:
|
|
@@ -895,7 +1104,9 @@ def count_student_pack_downloads_click(student_pack_type):
|
|
|
895
1104
|
|
|
896
1105
|
|
|
897
1106
|
def count_student_details_click(download_type):
|
|
898
|
-
activity_today = DailyActivity.objects.get_or_create(
|
|
1107
|
+
activity_today = DailyActivity.objects.get_or_create(
|
|
1108
|
+
date=datetime.now().date()
|
|
1109
|
+
)[0]
|
|
899
1110
|
|
|
900
1111
|
if download_type == DownloadType.CSV:
|
|
901
1112
|
activity_today.csv_click_count += 1
|