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.

Files changed (69) hide show
  1. cfl_common/common/csp_config.py +0 -2
  2. cfl_common/common/mail.py +31 -6
  3. cfl_common/common/migrations/0005_add_worksheets.py +1 -5
  4. cfl_common/common/migrations/0007_add_pdf_names_to_first_two_worksheets.py +1 -5
  5. cfl_common/common/migrations/0054_delete_aimmo_models.py +20 -0
  6. cfl_common/common/models.py +0 -25
  7. {codeforlife_portal-6.46.1.dist-info → codeforlife_portal-7.1.0.dist-info}/METADATA +3 -4
  8. {codeforlife_portal-6.46.1.dist-info → codeforlife_portal-7.1.0.dist-info}/RECORD +44 -68
  9. example_project/portal_test_settings.py +0 -1
  10. example_project/settings.py +0 -1
  11. example_project/urls.py +0 -2
  12. portal/__init__.py +1 -1
  13. portal/static/portal/sass/partials/_banners.scss +0 -177
  14. portal/static/portal/sass/partials/_buttons.scss +0 -12
  15. portal/static/portal/sass/partials/_grids.scss +0 -53
  16. portal/static/portal/sass/partials/_text.scss +1 -10
  17. portal/static/portal/sass/styles.scss +0 -1
  18. portal/strings/play.py +1 -2
  19. portal/strings/teacher_resources.py +0 -10
  20. portal/templates/portal/about.html +91 -60
  21. portal/templates/portal/contribute.html +45 -49
  22. portal/templates/portal/partials/header.html +0 -12
  23. portal/templates/portal/play/independent_student_dashboard.html +12 -25
  24. portal/templates/portal/play/student_dashboard.html +16 -34
  25. portal/templates/portal/play.html +36 -49
  26. portal/templates/portal/register.html +1 -1
  27. portal/templates/portal/teach.html +37 -55
  28. portal/templates/portal/ten_year_map.html +9 -9
  29. portal/templatetags/app_tags.py +13 -28
  30. portal/tests/conftest.py +4 -16
  31. portal/tests/pageObjects/portal/base_page.py +20 -20
  32. portal/tests/snapshots/snap_test_partials.py +0 -452
  33. portal/tests/test_class.py +213 -45
  34. portal/tests/test_independent_student.py +0 -9
  35. portal/tests/test_partials.py +6 -56
  36. portal/tests/test_teacher.py +221 -285
  37. portal/tests/test_views.py +257 -73
  38. portal/urls.py +38 -20
  39. portal/views/cron/user.py +158 -15
  40. portal/views/student/play.py +36 -25
  41. portal/views/teacher/teach.py +0 -5
  42. cfl_common/common/tests/test_migration_aimmo_characters.py +0 -29
  43. portal/forms/add_game.py +0 -29
  44. portal/static/portal/img/kurono_hero.jpg +0 -0
  45. portal/static/portal/img/kurono_landing_hero.png +0 -0
  46. portal/static/portal/img/kurono_logo.svg +0 -1
  47. portal/static/portal/img/kurono_logo_grey_background.svg +0 -1
  48. portal/static/portal/img/kurono_logo_mark.svg +0 -1
  49. portal/static/portal/img/kurono_resources_hero.jpg +0 -0
  50. portal/static/portal/img/kurono_story.png +0 -0
  51. portal/static/portal/img/thumbnail_educate_kurono.png +0 -0
  52. portal/static/portal/img/thumbnail_kurono_resources.png +0 -0
  53. portal/static/portal/img/thumbnail_play_kurono.png +0 -0
  54. portal/static/portal/js/aimmoGame.js +0 -106
  55. portal/static/portal/sass/partials/_videos.scss +0 -10
  56. portal/static/portal/video/aimmo_play_now_background_video.mp4 +0 -0
  57. portal/strings/student_aimmo_dashboard.py +0 -6
  58. portal/templates/portal/partials/aimmo_games_table.html +0 -89
  59. portal/templates/portal/play/student_aimmo_dashboard.html +0 -46
  60. portal/templates/portal/teach/teacher_aimmo_dashboard.html +0 -95
  61. portal/templatetags/character_list_tags.py +0 -16
  62. portal/tests/pageObjects/portal/kurono_teacher_dashboard_page.py +0 -49
  63. portal/tests/test_aimmo_dashboards.py +0 -206
  64. portal/tests/utils/aimmo_games.py +0 -30
  65. portal/views/aimmo/__init__.py +0 -0
  66. portal/views/aimmo/dashboard.py +0 -105
  67. {codeforlife_portal-6.46.1.dist-info → codeforlife_portal-7.1.0.dist-info}/LICENSE.md +0 -0
  68. {codeforlife_portal-6.46.1.dist-info → codeforlife_portal-7.1.0.dist-info}/WHEEL +0 -0
  69. {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 AdminChangePasswordDoneView, AdminChangePasswordView
29
- from portal.views.aimmo.dashboard import StudentAimmoDashboard, TeacherAimmoDashboard
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 dotmailer_consent_form, process_newsletter_form
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(r"^account/two_factor/setup/$", CustomSetupView.as_view(), name="setup"),
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(url="/static/portal/img/favicon.ico", permanent=True),
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(r"^users/inactive/", InactiveUsersView.as_view(), name="inactive_users"),
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(template_name="portal/email_verification_needed.html"),
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(r"^privacy-policy/$", privacy_notice, name="privacy_policy"), # Keeping this to route from old 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(days: int, same_day: bool) -> (QuerySet[User], QuerySet[User]):
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(date_joined__gt=now - timedelta(days=days + 1))
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 cron jobs is HTTP
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(chunk_size=500):
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={"VERIFICATION_LINK": email_verification_url},
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(chunk_size=500):
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={"VERIFICATION_LINK": email_verification_url},
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(date=datetime.now().date())[0]
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") + teacher_count,
167
- anonymised_unverified_independents=F("anonymised_unverified_independents") + indy_count,
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()
@@ -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(LoginRequiredNoErrorMixin, UserPassesTestMixin, TemplateView):
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 scores
35
- for the original Rapid Router levels is gathered, second, the student's scores
36
- for any levels shared with them by their teacher, and third, the student's
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 student
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(student, custom_levels)
52
+ custom_levels_data = _compute_rapid_router_scores(
53
+ student, custom_levels
54
+ )
52
55
 
53
- context_data["total_custom_score"] = custom_levels_data["total_score"]
54
- context_data["total_custom_available_score"] = custom_levels_data["total_available_score"]
55
-
56
- # Get Kurono game info if the class has a game linked to it
57
- aimmo_game = klass.active_game
58
- if aimmo_game:
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(LoginRequiredNoErrorMixin, UserPassesTestMixin, TemplateView, FormView):
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(student: Student, levels: List[Level] or QuerySet) -> Dict[str, int]:
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(level__in=levels, student=student, is_best_attempt=True).select_related(
104
- "level"
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 = {best_attempt.level.id: best_attempt for best_attempt in best_attempts}
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 and (level.disable_route_score or level.disable_algorithm_score or not level.episode)
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
 
@@ -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
@@ -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