codeforlife-portal 6.46.1__py2.py3-none-any.whl → 7.1.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/csp_config.py +0 -2
- cfl_common/common/mail.py +31 -6
- cfl_common/common/migrations/0005_add_worksheets.py +1 -5
- cfl_common/common/migrations/0007_add_pdf_names_to_first_two_worksheets.py +1 -5
- cfl_common/common/migrations/0054_delete_aimmo_models.py +20 -0
- cfl_common/common/models.py +0 -25
- {codeforlife_portal-6.46.1.dist-info → codeforlife_portal-7.1.0.dist-info}/METADATA +3 -4
- {codeforlife_portal-6.46.1.dist-info → codeforlife_portal-7.1.0.dist-info}/RECORD +44 -68
- example_project/portal_test_settings.py +0 -1
- example_project/settings.py +0 -1
- example_project/urls.py +0 -2
- portal/__init__.py +1 -1
- portal/static/portal/sass/partials/_banners.scss +0 -177
- portal/static/portal/sass/partials/_buttons.scss +0 -12
- portal/static/portal/sass/partials/_grids.scss +0 -53
- portal/static/portal/sass/partials/_text.scss +1 -10
- portal/static/portal/sass/styles.scss +0 -1
- portal/strings/play.py +1 -2
- portal/strings/teacher_resources.py +0 -10
- portal/templates/portal/about.html +91 -60
- portal/templates/portal/contribute.html +45 -49
- portal/templates/portal/partials/header.html +0 -12
- portal/templates/portal/play/independent_student_dashboard.html +12 -25
- portal/templates/portal/play/student_dashboard.html +16 -34
- portal/templates/portal/play.html +36 -49
- portal/templates/portal/register.html +1 -1
- portal/templates/portal/teach.html +37 -55
- portal/templates/portal/ten_year_map.html +9 -9
- portal/templatetags/app_tags.py +13 -28
- portal/tests/conftest.py +4 -16
- portal/tests/pageObjects/portal/base_page.py +20 -20
- portal/tests/snapshots/snap_test_partials.py +0 -452
- portal/tests/test_class.py +213 -45
- portal/tests/test_independent_student.py +0 -9
- portal/tests/test_partials.py +6 -56
- portal/tests/test_teacher.py +221 -285
- portal/tests/test_views.py +257 -73
- portal/urls.py +38 -20
- portal/views/cron/user.py +158 -15
- portal/views/student/play.py +36 -25
- portal/views/teacher/teach.py +0 -5
- cfl_common/common/tests/test_migration_aimmo_characters.py +0 -29
- portal/forms/add_game.py +0 -29
- portal/static/portal/img/kurono_hero.jpg +0 -0
- portal/static/portal/img/kurono_landing_hero.png +0 -0
- portal/static/portal/img/kurono_logo.svg +0 -1
- portal/static/portal/img/kurono_logo_grey_background.svg +0 -1
- portal/static/portal/img/kurono_logo_mark.svg +0 -1
- portal/static/portal/img/kurono_resources_hero.jpg +0 -0
- portal/static/portal/img/kurono_story.png +0 -0
- portal/static/portal/img/thumbnail_educate_kurono.png +0 -0
- portal/static/portal/img/thumbnail_kurono_resources.png +0 -0
- portal/static/portal/img/thumbnail_play_kurono.png +0 -0
- portal/static/portal/js/aimmoGame.js +0 -106
- portal/static/portal/sass/partials/_videos.scss +0 -10
- portal/static/portal/video/aimmo_play_now_background_video.mp4 +0 -0
- portal/strings/student_aimmo_dashboard.py +0 -6
- portal/templates/portal/partials/aimmo_games_table.html +0 -89
- portal/templates/portal/play/student_aimmo_dashboard.html +0 -46
- portal/templates/portal/teach/teacher_aimmo_dashboard.html +0 -95
- portal/templatetags/character_list_tags.py +0 -16
- portal/tests/pageObjects/portal/kurono_teacher_dashboard_page.py +0 -49
- portal/tests/test_aimmo_dashboards.py +0 -206
- portal/tests/utils/aimmo_games.py +0 -30
- portal/views/aimmo/__init__.py +0 -0
- portal/views/aimmo/dashboard.py +0 -105
- {codeforlife_portal-6.46.1.dist-info → codeforlife_portal-7.1.0.dist-info}/LICENSE.md +0 -0
- {codeforlife_portal-6.46.1.dist-info → codeforlife_portal-7.1.0.dist-info}/WHEEL +0 -0
- {codeforlife_portal-6.46.1.dist-info → codeforlife_portal-7.1.0.dist-info}/top_level.txt +0 -0
portal/urls.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
from aimmo.urls import HOMEPAGE_REGEX
|
|
2
1
|
from common.permissions import teacher_verified
|
|
3
2
|
from django.conf.urls import include, url
|
|
4
3
|
from django.http import HttpResponse
|
|
@@ -25,8 +24,10 @@ from portal.helpers.ratelimit import (
|
|
|
25
24
|
from portal.helpers.regexes import ACCESS_CODE_REGEX, JWT_REGEX
|
|
26
25
|
from portal.views import cron
|
|
27
26
|
from portal.views.about import about, contribute, getinvolved
|
|
28
|
-
from portal.views.admin import
|
|
29
|
-
|
|
27
|
+
from portal.views.admin import (
|
|
28
|
+
AdminChangePasswordDoneView,
|
|
29
|
+
AdminChangePasswordView,
|
|
30
|
+
)
|
|
30
31
|
from portal.views.api import (
|
|
31
32
|
AnonymiseOrphanSchoolsView,
|
|
32
33
|
InactiveUsersView,
|
|
@@ -35,7 +36,10 @@ from portal.views.api import (
|
|
|
35
36
|
number_users_per_country,
|
|
36
37
|
registered_users,
|
|
37
38
|
)
|
|
38
|
-
from portal.views.dotmailer import
|
|
39
|
+
from portal.views.dotmailer import (
|
|
40
|
+
dotmailer_consent_form,
|
|
41
|
+
process_newsletter_form,
|
|
42
|
+
)
|
|
39
43
|
from portal.views.email import verify_email
|
|
40
44
|
from portal.views.home import (
|
|
41
45
|
coding_club,
|
|
@@ -109,7 +113,9 @@ from portal.views.two_factor.profile import CustomDisableView
|
|
|
109
113
|
js_info_dict = {"packages": ("conf.locale",)}
|
|
110
114
|
|
|
111
115
|
two_factor_patterns = [
|
|
112
|
-
url(
|
|
116
|
+
url(
|
|
117
|
+
r"^account/two_factor/setup/$", CustomSetupView.as_view(), name="setup"
|
|
118
|
+
),
|
|
113
119
|
url(r"^account/two_factor/qrcode/$", QRGeneratorView.as_view(), name="qr"),
|
|
114
120
|
url(
|
|
115
121
|
r"^account/two_factor/setup/complete/$",
|
|
@@ -158,26 +164,32 @@ urlpatterns = [
|
|
|
158
164
|
cron.user.AnonymiseUnverifiedAccounts.as_view(),
|
|
159
165
|
name="anonymise-unverified-accounts",
|
|
160
166
|
),
|
|
167
|
+
path(
|
|
168
|
+
"inactive/send-first-reminder/",
|
|
169
|
+
cron.user.FirstInactivityReminderView.as_view(),
|
|
170
|
+
name="first-inactivity-reminder",
|
|
171
|
+
),
|
|
172
|
+
path(
|
|
173
|
+
"inactive/send-second-reminder/",
|
|
174
|
+
cron.user.SecondInactivityReminderView.as_view(),
|
|
175
|
+
name="second-inactivity-reminder",
|
|
176
|
+
),
|
|
177
|
+
path(
|
|
178
|
+
"inactive/send-final-reminder/",
|
|
179
|
+
cron.user.FinalInactivityReminderView.as_view(),
|
|
180
|
+
name="final-inactivity-reminder",
|
|
181
|
+
),
|
|
161
182
|
]
|
|
162
183
|
),
|
|
163
184
|
),
|
|
164
185
|
]
|
|
165
186
|
),
|
|
166
187
|
),
|
|
167
|
-
url(HOMEPAGE_REGEX, include("aimmo.urls")),
|
|
168
|
-
url(
|
|
169
|
-
r"^teach/kurono/dashboard/$",
|
|
170
|
-
TeacherAimmoDashboard.as_view(),
|
|
171
|
-
name="teacher_aimmo_dashboard",
|
|
172
|
-
),
|
|
173
|
-
url(
|
|
174
|
-
r"^play/kurono/dashboard/$",
|
|
175
|
-
StudentAimmoDashboard.as_view(),
|
|
176
|
-
name="student_aimmo_dashboard",
|
|
177
|
-
),
|
|
178
188
|
url(
|
|
179
189
|
r"^favicon\.ico$",
|
|
180
|
-
RedirectView.as_view(
|
|
190
|
+
RedirectView.as_view(
|
|
191
|
+
url="/static/portal/img/favicon.ico", permanent=True
|
|
192
|
+
),
|
|
181
193
|
),
|
|
182
194
|
url(
|
|
183
195
|
r"^administration/password_change/$",
|
|
@@ -189,7 +201,9 @@ urlpatterns = [
|
|
|
189
201
|
AdminChangePasswordDoneView.as_view(),
|
|
190
202
|
name="administration_password_change_done",
|
|
191
203
|
),
|
|
192
|
-
url(
|
|
204
|
+
url(
|
|
205
|
+
r"^users/inactive/", InactiveUsersView.as_view(), name="inactive_users"
|
|
206
|
+
),
|
|
193
207
|
url(
|
|
194
208
|
r"^locked_out/$",
|
|
195
209
|
TemplateView.as_view(template_name="portal/locked_out.html"),
|
|
@@ -267,7 +281,9 @@ urlpatterns = [
|
|
|
267
281
|
url(r"^consent_form/$", dotmailer_consent_form, name="consent_form"),
|
|
268
282
|
url(
|
|
269
283
|
r"^verify_email/$",
|
|
270
|
-
TemplateView.as_view(
|
|
284
|
+
TemplateView.as_view(
|
|
285
|
+
template_name="portal/email_verification_needed.html"
|
|
286
|
+
),
|
|
271
287
|
name="email_verification",
|
|
272
288
|
),
|
|
273
289
|
url(
|
|
@@ -380,7 +396,9 @@ urlpatterns = [
|
|
|
380
396
|
url(r"^contribute", contribute, name="contribute"),
|
|
381
397
|
url(r"^terms", terms, name="terms"),
|
|
382
398
|
url(r"^privacy-notice/$", privacy_notice, name="privacy_notice"),
|
|
383
|
-
url(
|
|
399
|
+
url(
|
|
400
|
+
r"^privacy-policy/$", privacy_notice, name="privacy_policy"
|
|
401
|
+
), # Keeping this to route from old URL
|
|
384
402
|
url(r"^teach/dashboard/$", dashboard_manage, name="dashboard"),
|
|
385
403
|
url(
|
|
386
404
|
r"^teach/dashboard/kick/(?P<pk>[0-9]+)/$",
|
portal/views/cron/user.py
CHANGED
|
@@ -5,24 +5,28 @@ from common.helpers.emails import generate_token_for_email
|
|
|
5
5
|
from common.mail import campaign_ids, send_dotdigital_email
|
|
6
6
|
from common.models import DailyActivity, TotalActivity
|
|
7
7
|
from django.contrib.auth.models import User
|
|
8
|
-
from django.db.models import F
|
|
8
|
+
from django.db.models import F, Q
|
|
9
9
|
from django.db.models.query import QuerySet
|
|
10
10
|
from django.urls import reverse
|
|
11
11
|
from django.utils import timezone
|
|
12
12
|
from rest_framework.response import Response
|
|
13
13
|
from rest_framework.views import APIView
|
|
14
14
|
|
|
15
|
-
from portal.views.api import anonymise
|
|
16
|
-
|
|
17
15
|
from ...mixins import CronMixin
|
|
16
|
+
from ...views.api import anonymise
|
|
18
17
|
|
|
19
|
-
# TODO: move email templates to DotDigital.
|
|
20
18
|
USER_1ST_VERIFY_EMAIL_REMINDER_DAYS = 7
|
|
21
19
|
USER_2ND_VERIFY_EMAIL_REMINDER_DAYS = 14
|
|
22
20
|
USER_DELETE_UNVERIFIED_ACCOUNT_DAYS = 19
|
|
23
21
|
|
|
22
|
+
USER_1ST_INACTIVE_REMINDER_DAYS = 730 # 2 years
|
|
23
|
+
USER_2ND_INACTIVE_REMINDER_DAYS = 973 # roughly 2 years and 8 months
|
|
24
|
+
USER_FINAL_INACTIVE_REMINDER_DAYS = 1065 # 2 years and 11 months
|
|
25
|
+
|
|
24
26
|
|
|
25
|
-
def get_unverified_users(
|
|
27
|
+
def get_unverified_users(
|
|
28
|
+
days: int, same_day: bool
|
|
29
|
+
) -> (QuerySet[User], QuerySet[User]):
|
|
26
30
|
now = timezone.now()
|
|
27
31
|
|
|
28
32
|
# All expired unverified users.
|
|
@@ -31,7 +35,9 @@ def get_unverified_users(days: int, same_day: bool) -> (QuerySet[User], QuerySet
|
|
|
31
35
|
userprofile__is_verified=False,
|
|
32
36
|
)
|
|
33
37
|
if same_day:
|
|
34
|
-
user_queryset = user_queryset.filter(
|
|
38
|
+
user_queryset = user_queryset.filter(
|
|
39
|
+
date_joined__gt=now - timedelta(days=days + 1)
|
|
40
|
+
)
|
|
35
41
|
|
|
36
42
|
teacher_queryset = user_queryset.filter(
|
|
37
43
|
new_teacher__isnull=False,
|
|
@@ -45,10 +51,31 @@ def get_unverified_users(days: int, same_day: bool) -> (QuerySet[User], QuerySet
|
|
|
45
51
|
return teacher_queryset, independent_student_queryset
|
|
46
52
|
|
|
47
53
|
|
|
54
|
+
def get_inactive_users(days: int) -> QuerySet[User]:
|
|
55
|
+
now = timezone.now()
|
|
56
|
+
|
|
57
|
+
# All users who haven't logged in in X days OR who've never logged in and
|
|
58
|
+
# registered over X days ago.
|
|
59
|
+
user_queryset = User.objects.filter(
|
|
60
|
+
Q(
|
|
61
|
+
last_login__isnull=False,
|
|
62
|
+
last_login__lte=now - timedelta(days=days),
|
|
63
|
+
last_login__gt=now - timedelta(days=days + 1),
|
|
64
|
+
)
|
|
65
|
+
| Q(
|
|
66
|
+
last_login__isnull=True,
|
|
67
|
+
date_joined__lte=now - timedelta(days=days),
|
|
68
|
+
date_joined__gt=now - timedelta(days=days + 1),
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
return user_queryset.exclude(email__isnull=True).exclude(email="")
|
|
73
|
+
|
|
74
|
+
|
|
48
75
|
def build_absolute_google_uri(request, location: str) -> str:
|
|
49
76
|
"""
|
|
50
|
-
This is needed specifically for emails sent by cron jobs as the protocol for
|
|
51
|
-
and the service name is wrongly parsed.
|
|
77
|
+
This is needed specifically for emails sent by cron jobs as the protocol for
|
|
78
|
+
cron jobs is HTTP and the service name is wrongly parsed.
|
|
52
79
|
"""
|
|
53
80
|
url = request.build_absolute_uri(location)
|
|
54
81
|
url = url.replace("http", "https")
|
|
@@ -70,7 +97,9 @@ class FirstVerifyEmailReminderView(CronMixin, APIView):
|
|
|
70
97
|
|
|
71
98
|
if user_count > 0:
|
|
72
99
|
sent_email_count = 0
|
|
73
|
-
for email in user_queryset.values_list("email", flat=True).iterator(
|
|
100
|
+
for email in user_queryset.values_list("email", flat=True).iterator(
|
|
101
|
+
chunk_size=500
|
|
102
|
+
):
|
|
74
103
|
email_verification_url = build_absolute_google_uri(
|
|
75
104
|
request,
|
|
76
105
|
reverse(
|
|
@@ -83,7 +112,9 @@ class FirstVerifyEmailReminderView(CronMixin, APIView):
|
|
|
83
112
|
send_dotdigital_email(
|
|
84
113
|
campaign_ids["verify_new_user_first_reminder"],
|
|
85
114
|
[email],
|
|
86
|
-
personalization_values={
|
|
115
|
+
personalization_values={
|
|
116
|
+
"VERIFICATION_LINK": email_verification_url
|
|
117
|
+
},
|
|
87
118
|
)
|
|
88
119
|
|
|
89
120
|
sent_email_count += 1
|
|
@@ -109,7 +140,9 @@ class SecondVerifyEmailReminderView(CronMixin, APIView):
|
|
|
109
140
|
if user_count > 0:
|
|
110
141
|
|
|
111
142
|
sent_email_count = 0
|
|
112
|
-
for email in user_queryset.values_list("email", flat=True).iterator(
|
|
143
|
+
for email in user_queryset.values_list("email", flat=True).iterator(
|
|
144
|
+
chunk_size=500
|
|
145
|
+
):
|
|
113
146
|
email_verification_url = build_absolute_google_uri(
|
|
114
147
|
request,
|
|
115
148
|
reverse(
|
|
@@ -122,7 +155,9 @@ class SecondVerifyEmailReminderView(CronMixin, APIView):
|
|
|
122
155
|
send_dotdigital_email(
|
|
123
156
|
campaign_ids["verify_new_user_second_reminder"],
|
|
124
157
|
[email],
|
|
125
|
-
personalization_values={
|
|
158
|
+
personalization_values={
|
|
159
|
+
"VERIFICATION_LINK": email_verification_url
|
|
160
|
+
},
|
|
126
161
|
)
|
|
127
162
|
|
|
128
163
|
sent_email_count += 1
|
|
@@ -157,14 +192,122 @@ class AnonymiseUnverifiedAccounts(CronMixin, APIView):
|
|
|
157
192
|
user_count -= User.objects.filter(is_active=True).count()
|
|
158
193
|
logging.info(f"{user_count} unverified users anonymised.")
|
|
159
194
|
|
|
160
|
-
activity_today = DailyActivity.objects.get_or_create(
|
|
195
|
+
activity_today = DailyActivity.objects.get_or_create(
|
|
196
|
+
date=datetime.now().date()
|
|
197
|
+
)[0]
|
|
161
198
|
activity_today.anonymised_unverified_teachers = teacher_count
|
|
162
199
|
activity_today.anonymised_unverified_independents = indy_count
|
|
163
200
|
activity_today.save()
|
|
164
201
|
|
|
165
202
|
TotalActivity.objects.update(
|
|
166
|
-
anonymised_unverified_teachers=F("anonymised_unverified_teachers")
|
|
167
|
-
|
|
203
|
+
anonymised_unverified_teachers=F("anonymised_unverified_teachers")
|
|
204
|
+
+ teacher_count,
|
|
205
|
+
anonymised_unverified_independents=F(
|
|
206
|
+
"anonymised_unverified_independents"
|
|
207
|
+
)
|
|
208
|
+
+ indy_count,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
return Response()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class FirstInactivityReminderView(CronMixin, APIView):
|
|
215
|
+
def get(self, request):
|
|
216
|
+
user_queryset = get_inactive_users(USER_1ST_INACTIVE_REMINDER_DAYS)
|
|
217
|
+
user_count = user_queryset.count()
|
|
218
|
+
|
|
219
|
+
logging.info(
|
|
220
|
+
f"{user_count} inactive users after "
|
|
221
|
+
f"{USER_1ST_INACTIVE_REMINDER_DAYS} days."
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
if user_count > 0:
|
|
225
|
+
sent_email_count = 0
|
|
226
|
+
for email in user_queryset.values_list("email", flat=True).iterator(
|
|
227
|
+
chunk_size=500
|
|
228
|
+
):
|
|
229
|
+
try:
|
|
230
|
+
send_dotdigital_email(
|
|
231
|
+
campaign_ids[
|
|
232
|
+
"inactive_users_on_website_first_reminder"
|
|
233
|
+
],
|
|
234
|
+
[email],
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
sent_email_count += 1
|
|
238
|
+
except Exception as ex:
|
|
239
|
+
logging.exception(ex)
|
|
240
|
+
|
|
241
|
+
logging.info(
|
|
242
|
+
f"Reminded {sent_email_count}/{user_count} inactive users."
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
return Response()
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class SecondInactivityReminderView(CronMixin, APIView):
|
|
249
|
+
def get(self, request):
|
|
250
|
+
user_queryset = get_inactive_users(USER_2ND_INACTIVE_REMINDER_DAYS)
|
|
251
|
+
user_count = user_queryset.count()
|
|
252
|
+
|
|
253
|
+
logging.info(
|
|
254
|
+
f"{user_count} inactive users after "
|
|
255
|
+
f"{USER_2ND_INACTIVE_REMINDER_DAYS} days."
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
if user_count > 0:
|
|
259
|
+
sent_email_count = 0
|
|
260
|
+
for email in user_queryset.values_list("email", flat=True).iterator(
|
|
261
|
+
chunk_size=500
|
|
262
|
+
):
|
|
263
|
+
try:
|
|
264
|
+
send_dotdigital_email(
|
|
265
|
+
campaign_ids[
|
|
266
|
+
"inactive_users_on_website_second_reminder"
|
|
267
|
+
],
|
|
268
|
+
[email],
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
sent_email_count += 1
|
|
272
|
+
except Exception as ex:
|
|
273
|
+
logging.exception(ex)
|
|
274
|
+
|
|
275
|
+
logging.info(
|
|
276
|
+
f"Reminded {sent_email_count}/{user_count} inactive users."
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
return Response()
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class FinalInactivityReminderView(CronMixin, APIView):
|
|
283
|
+
def get(self, request):
|
|
284
|
+
user_queryset = get_inactive_users(USER_FINAL_INACTIVE_REMINDER_DAYS)
|
|
285
|
+
user_count = user_queryset.count()
|
|
286
|
+
|
|
287
|
+
logging.info(
|
|
288
|
+
f"{user_count} inactive users after "
|
|
289
|
+
f"{USER_FINAL_INACTIVE_REMINDER_DAYS} days."
|
|
168
290
|
)
|
|
169
291
|
|
|
292
|
+
if user_count > 0:
|
|
293
|
+
sent_email_count = 0
|
|
294
|
+
for email in user_queryset.values_list("email", flat=True).iterator(
|
|
295
|
+
chunk_size=500
|
|
296
|
+
):
|
|
297
|
+
try:
|
|
298
|
+
send_dotdigital_email(
|
|
299
|
+
campaign_ids[
|
|
300
|
+
"inactive_users_on_website_final_reminder"
|
|
301
|
+
],
|
|
302
|
+
[email],
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
sent_email_count += 1
|
|
306
|
+
except Exception as ex:
|
|
307
|
+
logging.exception(ex)
|
|
308
|
+
|
|
309
|
+
logging.info(
|
|
310
|
+
f"Reminded {sent_email_count}/{user_count} inactive users."
|
|
311
|
+
)
|
|
312
|
+
|
|
170
313
|
return Response()
|
portal/views/student/play.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from typing import Any, Dict, List, Optional
|
|
2
2
|
|
|
3
|
-
from common.helpers.emails import NOTIFICATION_EMAIL, send_email
|
|
4
3
|
from common.mail import campaign_ids, send_dotdigital_email
|
|
5
4
|
from common.models import Student
|
|
6
5
|
from common.permissions import (
|
|
@@ -22,7 +21,9 @@ from game.models import Attempt, Level
|
|
|
22
21
|
from portal.forms.play import StudentJoinOrganisationForm
|
|
23
22
|
|
|
24
23
|
|
|
25
|
-
class SchoolStudentDashboard(
|
|
24
|
+
class SchoolStudentDashboard(
|
|
25
|
+
LoginRequiredNoErrorMixin, UserPassesTestMixin, TemplateView
|
|
26
|
+
):
|
|
26
27
|
template_name = "portal/play/student_dashboard.html"
|
|
27
28
|
login_url = reverse_lazy("student_login_access_code")
|
|
28
29
|
|
|
@@ -31,10 +32,9 @@ class SchoolStudentDashboard(LoginRequiredNoErrorMixin, UserPassesTestMixin, Tem
|
|
|
31
32
|
|
|
32
33
|
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
|
33
34
|
"""
|
|
34
|
-
Gathers the context data required by the template. First, the student's
|
|
35
|
-
for the original Rapid Router levels is gathered, second,
|
|
36
|
-
for any levels shared with them by their teacher
|
|
37
|
-
Kurono game information if they have one.
|
|
35
|
+
Gathers the context data required by the template. First, the student's
|
|
36
|
+
scores for the original Rapid Router levels is gathered, second,
|
|
37
|
+
the student's scores for any levels shared with them by their teacher.
|
|
38
38
|
"""
|
|
39
39
|
# Get score data for all original levels
|
|
40
40
|
levels = Level.objects.sorted_levels()
|
|
@@ -42,29 +42,30 @@ class SchoolStudentDashboard(LoginRequiredNoErrorMixin, UserPassesTestMixin, Tem
|
|
|
42
42
|
|
|
43
43
|
context_data = _compute_rapid_router_scores(student, levels)
|
|
44
44
|
|
|
45
|
-
# Find any custom levels created by the teacher and shared with the
|
|
45
|
+
# Find any custom levels created by the teacher and shared with the
|
|
46
|
+
# student
|
|
46
47
|
klass = student.class_field
|
|
47
48
|
teacher = klass.teacher.user
|
|
48
49
|
custom_levels = student.new_user.shared.filter(owner=teacher)
|
|
49
50
|
|
|
50
51
|
if custom_levels:
|
|
51
|
-
custom_levels_data = _compute_rapid_router_scores(
|
|
52
|
+
custom_levels_data = _compute_rapid_router_scores(
|
|
53
|
+
student, custom_levels
|
|
54
|
+
)
|
|
52
55
|
|
|
53
|
-
context_data["total_custom_score"] = custom_levels_data[
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
active_worksheet = aimmo_game.worksheet
|
|
60
|
-
|
|
61
|
-
context_data["worksheet_id"] = active_worksheet.id
|
|
62
|
-
context_data["worksheet_image"] = active_worksheet.image_path
|
|
56
|
+
context_data["total_custom_score"] = custom_levels_data[
|
|
57
|
+
"total_score"
|
|
58
|
+
]
|
|
59
|
+
context_data["total_custom_available_score"] = custom_levels_data[
|
|
60
|
+
"total_available_score"
|
|
61
|
+
]
|
|
63
62
|
|
|
64
63
|
return context_data
|
|
65
64
|
|
|
66
65
|
|
|
67
|
-
class IndependentStudentDashboard(
|
|
66
|
+
class IndependentStudentDashboard(
|
|
67
|
+
LoginRequiredNoErrorMixin, UserPassesTestMixin, TemplateView, FormView
|
|
68
|
+
):
|
|
68
69
|
template_name = "portal/play/independent_student_dashboard.html"
|
|
69
70
|
login_url = reverse_lazy("independent_student_login")
|
|
70
71
|
|
|
@@ -81,7 +82,9 @@ class IndependentStudentDashboard(LoginRequiredNoErrorMixin, UserPassesTestMixin
|
|
|
81
82
|
)
|
|
82
83
|
|
|
83
84
|
|
|
84
|
-
def _compute_rapid_router_scores(
|
|
85
|
+
def _compute_rapid_router_scores(
|
|
86
|
+
student: Student, levels: List[Level] or QuerySet
|
|
87
|
+
) -> Dict[str, int]:
|
|
85
88
|
"""
|
|
86
89
|
Finds Rapid Router progress and score data for a specific student and a specific
|
|
87
90
|
set of levels. This is used to show quick score data to the student on their
|
|
@@ -100,9 +103,9 @@ def _compute_rapid_router_scores(student: Student, levels: List[Level] or QueryS
|
|
|
100
103
|
num_completed = num_top_scores = total_available_score = 0
|
|
101
104
|
total_score = 0.0
|
|
102
105
|
# Get a QuerySet of best attempts for each level
|
|
103
|
-
best_attempts = Attempt.objects.filter(
|
|
104
|
-
|
|
105
|
-
)
|
|
106
|
+
best_attempts = Attempt.objects.filter(
|
|
107
|
+
level__in=levels, student=student, is_best_attempt=True
|
|
108
|
+
).select_related("level")
|
|
106
109
|
|
|
107
110
|
for level in levels:
|
|
108
111
|
total_available_score += _get_max_score_for_level(level)
|
|
@@ -110,7 +113,10 @@ def _compute_rapid_router_scores(student: Student, levels: List[Level] or QueryS
|
|
|
110
113
|
# For each level, compare best attempt's score with level's max score and
|
|
111
114
|
# increment variables as needed
|
|
112
115
|
if best_attempts:
|
|
113
|
-
attempts_dict = {
|
|
116
|
+
attempts_dict = {
|
|
117
|
+
best_attempt.level.id: best_attempt
|
|
118
|
+
for best_attempt in best_attempts
|
|
119
|
+
}
|
|
114
120
|
for level in levels:
|
|
115
121
|
attempt = attempts_dict.get(level.id)
|
|
116
122
|
|
|
@@ -140,7 +146,12 @@ def _get_max_score_for_level(level: Level) -> int:
|
|
|
140
146
|
"""
|
|
141
147
|
return (
|
|
142
148
|
10
|
|
143
|
-
if level.id > 12
|
|
149
|
+
if level.id > 12
|
|
150
|
+
and (
|
|
151
|
+
level.disable_route_score
|
|
152
|
+
or level.disable_algorithm_score
|
|
153
|
+
or not level.episode
|
|
154
|
+
)
|
|
144
155
|
else 20
|
|
145
156
|
)
|
|
146
157
|
|
portal/views/teacher/teach.py
CHANGED
|
@@ -6,7 +6,6 @@ from enum import Enum
|
|
|
6
6
|
from functools import partial, wraps
|
|
7
7
|
from uuid import uuid4
|
|
8
8
|
|
|
9
|
-
from aimmo.models import Game
|
|
10
9
|
from common.helpers.emails import send_verification_email
|
|
11
10
|
from common.helpers.generators import generate_access_code, generate_login_id, generate_password, get_hashed_login_id
|
|
12
11
|
from common.models import Class, DailyActivity, JoinReleaseStudent, Student, Teacher, TotalActivity
|
|
@@ -188,7 +187,6 @@ def teacher_view_class(request, access_code):
|
|
|
188
187
|
@user_passes_test(logged_in_as_teacher, login_url=reverse_lazy("teacher_login"))
|
|
189
188
|
def teacher_delete_class(request, access_code):
|
|
190
189
|
klass = get_object_or_404(Class, access_code=access_code)
|
|
191
|
-
games = Game.objects.filter(game_class=klass)
|
|
192
190
|
|
|
193
191
|
# check user authorised to see class
|
|
194
192
|
check_teacher_authorised(request, klass.teacher)
|
|
@@ -199,9 +197,6 @@ def teacher_delete_class(request, access_code):
|
|
|
199
197
|
)
|
|
200
198
|
return HttpResponseRedirect(reverse_lazy("view_class", kwargs={"access_code": access_code}))
|
|
201
199
|
|
|
202
|
-
for game in games:
|
|
203
|
-
game.is_archived = True
|
|
204
|
-
game.save()
|
|
205
200
|
klass.anonymise()
|
|
206
201
|
|
|
207
202
|
return HttpResponseRedirect(reverse_lazy("dashboard") + "#classes")
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
from django.db.models.query import QuerySet
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
@pytest.mark.django_db
|
|
6
|
-
def test_characters_added(migrator):
|
|
7
|
-
migrator.apply_initial_migration(("common", "0002_emailverification"))
|
|
8
|
-
new_state = migrator.apply_tested_migration(("common", "0004_add_aimmocharacters"))
|
|
9
|
-
|
|
10
|
-
model_names = [model._meta.db_table for model in new_state.apps.get_models()]
|
|
11
|
-
|
|
12
|
-
assert "common_aimmocharacter" in model_names
|
|
13
|
-
|
|
14
|
-
AimmoCharacter = new_state.apps.get_model("common", "aimmocharacter")
|
|
15
|
-
all_characters: QuerySet = AimmoCharacter.objects.all()
|
|
16
|
-
|
|
17
|
-
assert all_characters.count() == 3
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@pytest.mark.django_db
|
|
21
|
-
def test_image_paths_updated(migrator):
|
|
22
|
-
migrator.apply_initial_migration(("common", "0005_add_worksheets"))
|
|
23
|
-
new_state = migrator.apply_tested_migration(("common", "0006_update_aimmo_character_image_path"))
|
|
24
|
-
|
|
25
|
-
AimmoCharacter = new_state.apps.get_model("common", "aimmocharacter")
|
|
26
|
-
all_characters: QuerySet = AimmoCharacter.objects.all()
|
|
27
|
-
|
|
28
|
-
for character in all_characters:
|
|
29
|
-
assert character.image_path.startswith("images/aimmo_characters/")
|
portal/forms/add_game.py
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
from aimmo.models import Game
|
|
2
|
-
from common.models import Class
|
|
3
|
-
from django.core.exceptions import ValidationError
|
|
4
|
-
from django.db.models.query import QuerySet
|
|
5
|
-
from django.forms import ModelChoiceField, ModelForm, Select
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class AddGameForm(ModelForm):
|
|
9
|
-
def __init__(self, classes: QuerySet, *args, **kwargs):
|
|
10
|
-
super(AddGameForm, self).__init__(*args, **kwargs)
|
|
11
|
-
self.fields["game_class"].queryset = classes
|
|
12
|
-
|
|
13
|
-
game_class = ModelChoiceField(queryset=None, widget=Select, label="Class", required=True)
|
|
14
|
-
|
|
15
|
-
class Meta:
|
|
16
|
-
model = Game
|
|
17
|
-
fields = ["game_class"]
|
|
18
|
-
|
|
19
|
-
def clean(self):
|
|
20
|
-
super(AddGameForm, self).clean()
|
|
21
|
-
game_class: Class = self.cleaned_data.get("game_class")
|
|
22
|
-
|
|
23
|
-
if not game_class:
|
|
24
|
-
raise ValidationError("An invalid class was entered")
|
|
25
|
-
|
|
26
|
-
if Game.objects.filter(game_class=game_class, is_archived=False).exists():
|
|
27
|
-
raise ValidationError("An active game already exists for this class")
|
|
28
|
-
|
|
29
|
-
return self.cleaned_data
|
|
Binary file
|
|
Binary file
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 781.61 255.82"><defs><style>.cls-1{fill:url(#linear-gradient);}.cls-2{fill:#fff;}</style><linearGradient id="linear-gradient" x1="390.81" x2="390.81" y2="255.82" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#00cea7"/><stop offset="0.2" stop-color="#00c3ac"/><stop offset="0.68" stop-color="#00afb5"/><stop offset="1" stop-color="#00a8b8"/></linearGradient></defs><title>K_simple_logo_full_colour</title><path class="cls-1" d="M697.33.34A84.18,84.18,0,0,0,630.4,33.48,71.21,71.21,0,0,0,621,21.9C607,8,588,.41,567.25.41S527.48,8,513.54,21.9a72,72,0,0,0-9.45,11.59,84.09,84.09,0,0,0-115.1-18A35.89,35.89,0,0,0,359.73.41c-21.56,0-41.4,8-55.87,22.43-.52.53-1,1.07-1.52,1.61a37.13,37.13,0,0,0-65.33-8,9.77,9.77,0,0,1-16.12-.1A37.16,37.16,0,0,0,188.05.48a36.22,36.22,0,0,0-20.39,7.87A37.45,37.45,0,0,0,144.19,0a37,37,0,0,0-22.25,7.39L74.47,42.77V37.24A37.24,37.24,0,1,0,0,37.24V131.3A37.18,37.18,0,0,0,13.53,160a37,37,0,0,0-8.08,47.6,99.5,99.5,0,0,0,170.54,0A37,37,0,0,0,167.9,160a36.51,36.51,0,0,0,6-6.27A38.17,38.17,0,0,0,177,149c13.77,12.81,32.12,19.86,52,19.86,20.69,0,39.77-7.63,53.71-21.49.37-.37.73-.76,1.09-1.14a37.16,37.16,0,0,0,71.36-14.53V103.94a84.2,84.2,0,0,0,139.53,42.21,36.93,36.93,0,0,0,64.5,6.69,9.78,9.78,0,0,1,16.17,0,36.92,36.92,0,0,0,64.49-6.69A84.25,84.25,0,1,0,697.33.34Z"/><path class="cls-2" d="M284.47,37.56V91.85c0,34-24.37,56.71-55.49,56.71s-55.5-22.69-55.5-56.71V37.56a16.89,16.89,0,0,1,33.78,0V93.05c0,15.69,9.66,24.14,21.72,24.14s21.71-8.45,21.71-24.14V37.56a16.89,16.89,0,0,1,33.78,0Z"/><path class="cls-2" d="M375.42,36.36a15.66,15.66,0,0,1-15.69,15.69c-13.75,0-24.85,9.65-24.85,29.67v49.95a16.9,16.9,0,0,1-33.79,0V81.72c0-37.39,26.07-61,58.64-61A15.66,15.66,0,0,1,375.42,36.36Z"/><path class="cls-2" d="M622.74,77.39v54.29a16.89,16.89,0,0,1-33.77,0V76.19c0-15.69-9.65-24.14-21.72-24.14s-21.71,8.45-21.71,24.14v55.49a16.9,16.9,0,0,1-33.79,0V77.39c0-34,24.37-56.71,55.5-56.71S622.74,43.37,622.74,77.39Z"/><path class="cls-2" d="M761.28,84.62a64,64,0,1,1-63.95-63.95A63.95,63.95,0,0,1,761.28,84.62ZM697.33,52a32.58,32.58,0,1,0,32.58,32.58A32.58,32.58,0,0,0,697.33,52Z"/><path class="cls-2" d="M501.11,84.62a64,64,0,1,1-63.95-63.95A63.95,63.95,0,0,1,501.11,84.62ZM437.16,52a32.58,32.58,0,1,0,32.58,32.58A32.58,32.58,0,0,0,437.16,52Z"/><path class="cls-2" d="M37.24,148.21A16.91,16.91,0,0,1,20.33,131.3V37.24a16.91,16.91,0,1,1,33.81,0V131.3A16.91,16.91,0,0,1,37.24,148.21Z"/><path class="cls-2" d="M144.19,148.21A16.8,16.8,0,0,1,134,144.8L71.27,97.5a16.9,16.9,0,0,1,.08-27l62.74-46.77a16.91,16.91,0,1,1,20.22,27.1l-44.69,33.3,44.76,33.72a16.91,16.91,0,0,1-10.19,30.4Z"/><path class="cls-2" d="M90.72,235.49a78.54,78.54,0,0,1-67.85-38.37A16.76,16.76,0,0,1,51.6,179.85a45.65,45.65,0,0,0,78.24,0,16.76,16.76,0,1,1,28.72,17.27A78.52,78.52,0,0,1,90.72,235.49Z"/></svg>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1330.02 596.66"><defs><style>.a{fill:#f2f2f2;}.b{fill:url(#a);}.c{fill:#fff;}</style><linearGradient id="a" x1="665.01" y1="510.52" x2="665.01" y2="287.55" gradientTransform="matrix(1, 0, 0, -1, 0, 724.17)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#00cea7"/><stop offset="0.2" stop-color="#00c3ac"/><stop offset="0.68" stop-color="#00afb5"/><stop offset="1" stop-color="#00a8b8"/></linearGradient></defs><rect class="a" width="1330.02" height="596.66"/><path class="b" d="M932.17,214a73.34,73.34,0,0,0-58.33,28.88,62.06,62.06,0,0,0-8.2-10.09C853.44,220.62,836.88,214,818.8,214s-34.67,6.61-46.82,18.73a62.32,62.32,0,0,0-8.23,10.1,73.3,73.3,0,0,0-100.32-15.69A31.27,31.27,0,0,0,637.93,214c-18.8,0-36.09,7-48.7,19.55-.45.46-.87.93-1.32,1.4A32.36,32.36,0,0,0,531,228a8.53,8.53,0,0,1-11.86,2.14,8.44,8.44,0,0,1-2.19-2.23,32.39,32.39,0,0,0-28.63-13.83,31.57,31.57,0,0,0-17.77,6.86,32.64,32.64,0,0,0-20.45-7.28,32.29,32.29,0,0,0-19.4,6.44L389.3,250.93v-4.82A32.46,32.46,0,1,0,324.39,245v83a32.39,32.39,0,0,0,11.79,25,32.25,32.25,0,0,0-7,41.49,86.72,86.72,0,0,0,148.64,0,32.25,32.25,0,0,0-7-41.49,31.82,31.82,0,0,0,5.23-5.46,34.75,34.75,0,0,0,2.7-4.12c12,11.16,28,17.31,45.32,17.31,18,0,34.67-6.65,46.82-18.73l1-1a32.38,32.38,0,0,0,62.19-12.66v-24.2A73.39,73.39,0,0,0,755.55,341a32.19,32.19,0,0,0,56.22,5.83,8.52,8.52,0,0,1,14.09,0A32.17,32.17,0,0,0,882.07,341,73.43,73.43,0,1,0,932.17,214Z"/><path class="c" d="M572.33,246.39v47.32c0,29.63-21.24,49.42-48.36,49.42s-48.38-19.77-48.38-49.42V246.39a14.73,14.73,0,0,1,29.45,0v48.36c0,13.68,8.42,21,18.93,21s18.92-7.36,18.92-21V246.39a14.72,14.72,0,0,1,29.44,0Z"/><path class="c" d="M651.6,245.34A13.65,13.65,0,0,1,638,259h-.05c-12,0-21.66,8.41-21.66,25.86v43.53a14.73,14.73,0,0,1-29.45,0V284.88c0-32.59,22.72-53.17,51.11-53.17a13.65,13.65,0,0,1,13.67,13.62Z"/><path class="c" d="M867.16,281.1v47.32a14.72,14.72,0,0,1-29.43,0V280.06c0-13.68-8.41-21-18.93-21s-18.93,7.36-18.93,21v48.36a14.73,14.73,0,0,1-29.45,0V281.1c0-29.63,21.24-49.43,48.38-49.43S867.16,251.45,867.16,281.1Z"/><path class="c" d="M987.91,287.4a55.78,55.78,0,1,1-55.83-55.73h.09a55.74,55.74,0,0,1,55.74,55.73ZM932.17,259a28.4,28.4,0,1,0,28.4,28.4,28.41,28.41,0,0,0-28.4-28.4Z"/><path class="c" d="M761.15,287.4a55.78,55.78,0,1,1-55.83-55.73h.09a55.74,55.74,0,0,1,55.74,55.73ZM705.41,259a28.4,28.4,0,1,0,28.4,28.4h0A28.41,28.41,0,0,0,705.41,259Z"/><path class="c" d="M356.85,342.83a14.74,14.74,0,0,1-14.74-14.74h0v-82a14.74,14.74,0,0,1,29.47-.72q0,.36,0,.72v82A14.74,14.74,0,0,1,356.85,342.83Z"/><path class="c" d="M450.07,342.83a14.7,14.7,0,0,1-8.89-3l-54.67-41.23a14.73,14.73,0,0,1,.07-23.53l54.68-40.77a14.74,14.74,0,0,1,18.14,23.24l-.51.38-39,29,39,29.39a14.74,14.74,0,0,1-8.88,26.49Z"/><path class="c" d="M403.46,418.9a68.44,68.44,0,0,1-59.13-33.44,14.61,14.61,0,0,1,25-15.06,39.79,39.79,0,0,0,68.19,0,14.61,14.61,0,0,1,25.35,14.53c-.1.18-.21.35-.32.53A68.45,68.45,0,0,1,403.46,418.9Z"/></svg>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 230.45 324.92"><defs><style>.cls-1{fill:url(#linear-gradient);}.cls-2{fill:#fff;}</style><linearGradient id="linear-gradient" x1="107.38" y1="84.32" x2="127.22" y2="408.64" gradientTransform="translate(13.19 -90.72) rotate(3.5)" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#00cea7"/><stop offset="0.2" stop-color="#00c3ac"/><stop offset="0.68" stop-color="#00afb5"/><stop offset="1" stop-color="#00a8b8"/></linearGradient></defs><title>K_mark_full_colour</title><path class="cls-1" d="M220.9,195.26A47.28,47.28,0,0,0,211.62,129l-29.32-22.1L211.42,85.2A47.28,47.28,0,0,0,221.06,19,47.55,47.55,0,0,0,183.13,0a46.93,46.93,0,0,0-28.25,9.38L94.58,54.33v-7A47.29,47.29,0,0,0,0,47.29V166.77a47.24,47.24,0,0,0,17.19,36.45A47,47,0,0,0,6.92,263.67a126.37,126.37,0,0,0,216.6,0,47,47,0,0,0-10.27-60.45A47.3,47.3,0,0,0,220.9,195.26Z"/><path class="cls-2" d="M47.29,188.24a21.47,21.47,0,0,1-21.47-21.47V47.29a21.47,21.47,0,0,1,42.94,0V166.77A21.47,21.47,0,0,1,47.29,188.24Z"/><path class="cls-2" d="M183.13,188.24a21.4,21.4,0,0,1-12.92-4.33L90.52,123.84a21.47,21.47,0,0,1,.1-34.35l79.69-59.41A21.48,21.48,0,0,1,196,64.51L139.23,106.8l56.85,42.83a21.47,21.47,0,0,1-13,38.61Z"/><path class="cls-2" d="M115.22,299.1a99.75,99.75,0,0,1-86.17-48.74,21.29,21.29,0,0,1,36.49-21.94,58,58,0,0,0,99.36,0,21.29,21.29,0,0,1,36.49,21.94A99.75,99.75,0,0,1,115.22,299.1Z"/></svg>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|