codeforlife-portal 5.32.2__py2.py3-none-any.whl → 8.9.9__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.
Files changed (393) hide show
  1. cfl_common/common/__init__.py +1 -0
  2. cfl_common/common/app_settings.py +66 -0
  3. cfl_common/common/apps.py +6 -0
  4. cfl_common/common/context_processors.py +9 -0
  5. cfl_common/common/csp_config.py +85 -0
  6. cfl_common/common/helpers/__init__.py +0 -0
  7. cfl_common/common/helpers/data_migration_loader.py +42 -0
  8. cfl_common/common/helpers/emails.py +393 -0
  9. cfl_common/common/helpers/generators.py +52 -0
  10. cfl_common/common/helpers/organisation.py +10 -0
  11. cfl_common/common/mail.py +201 -0
  12. cfl_common/common/migrations/0001_initial.py +240 -0
  13. cfl_common/common/migrations/0002_emailverification.py +55 -0
  14. cfl_common/common/migrations/0003_aimmocharacter.py +31 -0
  15. cfl_common/common/migrations/0004_add_aimmocharacters.py +17 -0
  16. cfl_common/common/migrations/0005_add_worksheets.py +8 -0
  17. cfl_common/common/migrations/0006_update_aimmo_character_image_path.py +17 -0
  18. cfl_common/common/migrations/0007_add_pdf_names_to_first_two_worksheets.py +8 -0
  19. cfl_common/common/migrations/0008_unlock_worksheet_3.py +11 -0
  20. cfl_common/common/migrations/0009_add_blocked_time_to_teacher_and_student.py +24 -0
  21. cfl_common/common/migrations/0010_remove_teacher_title.py +18 -0
  22. cfl_common/common/migrations/0011_student_login_id.py +18 -0
  23. cfl_common/common/migrations/0012_usersession.py +39 -0
  24. cfl_common/common/migrations/0013_class_school.py +42 -0
  25. cfl_common/common/migrations/0014_login_type.py +29 -0
  26. cfl_common/common/migrations/0015_dailyactivity.py +31 -0
  27. cfl_common/common/migrations/0016_joinreleasestudent.py +42 -0
  28. cfl_common/common/migrations/0017_copy_email_to_username.py +18 -0
  29. cfl_common/common/migrations/0018_update_aimmo_character_image_path.py +15 -0
  30. cfl_common/common/migrations/0019_aimmocharacter_alt.py +16 -0
  31. cfl_common/common/migrations/0020_class_is_active_and_null_access_code.py +23 -0
  32. cfl_common/common/migrations/0021_school_is_active.py +28 -0
  33. cfl_common/common/migrations/0022_school_cleanup.py +29 -0
  34. cfl_common/common/migrations/0023_userprofile_aimmo_badges.py +22 -0
  35. cfl_common/common/migrations/0024_teacher_invited_by.py +25 -0
  36. cfl_common/common/migrations/0025_schoolteacherinvitation.py +47 -0
  37. cfl_common/common/migrations/0026_teacher_remove_join_request.py +22 -0
  38. cfl_common/common/migrations/0027_class_created_by.py +25 -0
  39. cfl_common/common/migrations/0028_coding_club_downloads.py +23 -0
  40. cfl_common/common/migrations/0029_dynamicelement.py +22 -0
  41. cfl_common/common/migrations/0030_add_maintenance_banner.py +25 -0
  42. cfl_common/common/migrations/0031_improve_admin_panel.py +56 -0
  43. cfl_common/common/migrations/0032_dailyactivity_level_control_submits.py +18 -0
  44. cfl_common/common/migrations/0033_password_reset_tracking_fields.py +23 -0
  45. cfl_common/common/migrations/0034_dailyactivity_daily_school_student_lockout_reset.py +18 -0
  46. cfl_common/common/migrations/0035_rename_lockout_fields.py +27 -0
  47. cfl_common/common/migrations/0036_rename_awaiting_email_verification_userprofile_is_verified.py +17 -0
  48. cfl_common/common/migrations/0037_migrate_email_verification.py +21 -0
  49. cfl_common/common/migrations/0038_delete_emailverification.py +16 -0
  50. cfl_common/common/migrations/0039_copy_email_to_username.py +18 -0
  51. cfl_common/common/migrations/0040_school_county.py +18 -0
  52. cfl_common/common/migrations/0041_populate_gb_counties.py +27 -0
  53. cfl_common/common/migrations/0042_totalactivity.py +25 -0
  54. cfl_common/common/migrations/0043_add_total_activity.py +30 -0
  55. cfl_common/common/migrations/0044_update_activity_models.py +33 -0
  56. cfl_common/common/migrations/0045_otp.py +23 -0
  57. cfl_common/common/migrations/0046_alter_school_country.py +19 -0
  58. cfl_common/common/migrations/0047_delete_school_postcode.py +16 -0
  59. cfl_common/common/migrations/0048_unique_school_names.py +42 -0
  60. cfl_common/common/migrations/0049_anonymise_orphan_users.py +29 -0
  61. cfl_common/common/migrations/0050_anonymise_orphan_schools.py +30 -0
  62. cfl_common/common/migrations/0051_verify_returning_users.py +26 -0
  63. cfl_common/common/migrations/0052_add_cse_fields.py +68 -0
  64. cfl_common/common/migrations/0053_clean_class_data.py +24 -0
  65. cfl_common/common/migrations/0054_delete_aimmo_models.py +20 -0
  66. cfl_common/common/migrations/0055_alter_schoolteacherinvitation_token.py +18 -0
  67. cfl_common/common/migrations/0056_set_non_school_teachers_as_non_admins.py +25 -0
  68. cfl_common/common/migrations/0057_teacher_teacher__is_admin.py +19 -0
  69. cfl_common/common/migrations/0058_userprofile_google_refresh_token_and_more.py +24 -0
  70. cfl_common/common/migrations/__init__.py +0 -0
  71. cfl_common/common/models.py +557 -0
  72. cfl_common/common/permissions.py +84 -0
  73. cfl_common/common/tests/__init__.py +0 -0
  74. cfl_common/common/tests/test_migration_anonymise_orphan_schools.py +30 -0
  75. cfl_common/common/tests/test_migration_anonymise_orphan_users.py +30 -0
  76. cfl_common/common/tests/test_migration_blocked_time.py +15 -0
  77. cfl_common/common/tests/test_migration_remove_teacher_title.py +13 -0
  78. cfl_common/common/tests/test_migration_unique_school_names.py +33 -0
  79. cfl_common/common/tests/test_migration_verify_returning_users.py +59 -0
  80. cfl_common/common/tests/test_models.py +87 -0
  81. cfl_common/common/tests/utils/__init__.py +0 -0
  82. cfl_common/common/tests/utils/classes.py +38 -0
  83. cfl_common/common/tests/utils/email.py +67 -0
  84. cfl_common/common/tests/utils/organisation.py +41 -0
  85. cfl_common/common/tests/utils/student.py +123 -0
  86. cfl_common/common/tests/utils/teacher.py +73 -0
  87. cfl_common/common/tests/utils/user.py +27 -0
  88. cfl_common/common/utils.py +56 -0
  89. cfl_common/setup.py +61 -0
  90. codeforlife_portal-8.9.9.dist-info/METADATA +226 -0
  91. {codeforlife_portal-5.32.2.dist-info → codeforlife_portal-8.9.9.dist-info}/RECORD +341 -238
  92. {codeforlife_portal-5.32.2.dist-info → codeforlife_portal-8.9.9.dist-info}/WHEEL +1 -1
  93. codeforlife_portal-8.9.9.dist-info/licenses/LICENSE.md +3 -0
  94. {codeforlife_portal-5.32.2.dist-info → codeforlife_portal-8.9.9.dist-info}/top_level.txt +1 -0
  95. deploy/middleware/maintenance.py +25 -0
  96. deploy/middleware/screentime_warning.py +29 -0
  97. deploy/middleware/security.py +5 -6
  98. deploy/middleware/session_timeout.py +31 -0
  99. deploy/middleware/tmp_basic_auth.py +41 -0
  100. example_project/portal_test_settings.py +239 -0
  101. example_project/settings.py +156 -17
  102. example_project/urls.py +5 -6
  103. portal/__init__.py +1 -1
  104. portal/admin.py +142 -29
  105. portal/app_settings.py +10 -7
  106. portal/forms/dotmailer.py +6 -4
  107. portal/forms/invite_teacher.py +19 -10
  108. portal/forms/organisation.py +137 -68
  109. portal/forms/play.py +53 -98
  110. portal/forms/registration.py +70 -164
  111. portal/forms/teach.py +147 -121
  112. portal/handlers.py +1 -2
  113. portal/helpers/decorators.py +30 -10
  114. portal/helpers/password.py +86 -47
  115. portal/helpers/ratelimit.py +32 -15
  116. portal/helpers/regexes.py +5 -0
  117. portal/helpers/request_handlers.py +10 -0
  118. portal/migrations/0044_auto_20150430_0959.py +6 -2
  119. portal/mixins/__init__.py +1 -0
  120. portal/mixins/cron_mixin.py +12 -0
  121. portal/permissions/__init__.py +1 -0
  122. portal/permissions/is_cron_request_from_google.py +14 -0
  123. portal/static/portal/img/10_years_anniversary.png +0 -0
  124. portal/static/portal/img/RR_logo_grass_background.png +0 -0
  125. portal/static/portal/img/coding_club_hero.jpg +0 -0
  126. portal/static/portal/img/coding_club_python_pack.png +0 -0
  127. portal/static/portal/img/facebook.png +0 -0
  128. portal/static/portal/img/gitbook.png +0 -0
  129. portal/static/portal/img/howe_dell_1.png +0 -0
  130. portal/static/portal/img/howe_dell_2.png +0 -0
  131. portal/static/portal/img/howe_dell_3.png +0 -0
  132. portal/static/portal/img/logo_cfl.png +0 -0
  133. portal/static/portal/img/logo_cfl_powered.svg +35 -0
  134. portal/static/portal/img/logo_cfl_reminder_cards.jpg +0 -0
  135. portal/static/portal/img/logo_ocado_group.png +0 -0
  136. portal/static/portal/img/logo_python_den.svg +21 -0
  137. portal/static/portal/img/long_europe_map.png +0 -0
  138. portal/static/portal/img/python_den.png +0 -0
  139. portal/static/portal/img/python_den_banner.svg +26 -0
  140. portal/static/portal/img/rapid_router_landing_hero.png +0 -0
  141. portal/static/portal/img/rr_advanced.png +0 -0
  142. portal/static/portal/img/ten_year_map_pin.svg +1 -0
  143. portal/static/portal/img/thumbnail_educate_rapid_router.png +0 -0
  144. portal/static/portal/img/thumbnail_educate_resources.png +0 -0
  145. portal/static/portal/img/thumbnail_play_rapid_router.png +0 -0
  146. portal/static/portal/img/thumbnail_python_den.png +0 -0
  147. portal/static/portal/img/twitter.png +0 -0
  148. portal/static/portal/js/carouselCards.js +25 -0
  149. portal/static/portal/js/common.js +96 -1
  150. portal/static/portal/js/independentLogin.js +16 -0
  151. portal/static/portal/js/independentRegistration.js +86 -0
  152. portal/static/portal/js/levelControl.js +77 -0
  153. portal/static/portal/js/lib/jquery.min.js +2 -0
  154. portal/static/portal/js/organisation_manage.js +142 -14
  155. portal/static/portal/js/passwordStrength.js +154 -64
  156. portal/static/portal/js/resetPassword.js +23 -0
  157. portal/static/portal/js/riveted.min.js +238 -239
  158. portal/static/portal/js/school.js +13 -0
  159. portal/static/portal/js/studentLogin.js +16 -0
  160. portal/static/portal/js/teacherEditStudent.js +23 -0
  161. portal/static/portal/js/teacherLogin.js +16 -0
  162. portal/static/portal/js/tenYearMap.js +14 -0
  163. portal/static/portal/sass/colorbox.scss +0 -1
  164. portal/static/portal/sass/modules/_colours.scss +1 -0
  165. portal/static/portal/sass/modules/_levels.scss +1 -1
  166. portal/static/portal/sass/modules/_mixins.scss +21 -0
  167. portal/static/portal/sass/partials/_banners.scss +4 -177
  168. portal/static/portal/sass/partials/_buttons.scss +12 -15
  169. portal/static/portal/sass/partials/_carousel.scss +129 -0
  170. portal/static/portal/sass/partials/_footer.scss +21 -22
  171. portal/static/portal/sass/partials/_forms.scss +60 -5
  172. portal/static/portal/sass/partials/_grids.scss +34 -61
  173. portal/static/portal/sass/partials/_header.scss +28 -20
  174. portal/static/portal/sass/partials/_images.scss +292 -39
  175. portal/static/portal/sass/partials/_popup.scss +18 -15
  176. portal/static/portal/sass/partials/_tables.scss +12 -20
  177. portal/static/portal/sass/partials/_text.scss +6 -10
  178. portal/static/portal/sass/styles.scss +0 -1
  179. portal/static/portal/video/code for life .pdf +0 -0
  180. portal/strings/about.py +5 -0
  181. portal/strings/coding_club.py +9 -0
  182. portal/strings/play.py +6 -5
  183. portal/strings/teach.py +1 -1
  184. portal/strings/teacher_resources.py +2 -8
  185. portal/strings/ten_year_map.py +13 -0
  186. portal/templates/403.html +2 -2
  187. portal/templates/404.html +1 -1
  188. portal/templates/500.html +2 -2
  189. portal/templates/{captcha → django_recaptcha}/includes/js_v2_invisible.html +3 -3
  190. portal/templates/{captcha → django_recaptcha}/widget_v2_invisible.html +2 -2
  191. portal/templates/email.html +4 -2
  192. portal/templates/maintenance.html +34 -0
  193. portal/templates/portal/about.html +94 -62
  194. portal/templates/portal/base.html +176 -152
  195. portal/templates/portal/coding_club.html +100 -0
  196. portal/templates/portal/contribute.html +56 -52
  197. portal/templates/portal/email_invitation_sent.html +1 -1
  198. portal/templates/portal/email_style_template.html +374 -0
  199. portal/templates/portal/email_verification_failed.html +1 -1
  200. portal/templates/portal/email_verification_needed.html +9 -9
  201. portal/templates/portal/form_shapes.html +20 -8
  202. portal/templates/portal/getinvolved.html +6 -6
  203. portal/templates/portal/home.html +35 -10
  204. portal/templates/portal/home_learning.html +19 -19
  205. portal/templates/portal/locked_out.html +0 -1
  206. portal/templates/portal/locked_out_school_student.html +16 -0
  207. portal/templates/portal/login/independent_student.html +31 -15
  208. portal/templates/portal/login/student.html +10 -7
  209. portal/templates/portal/login/student_class_code.html +7 -4
  210. portal/templates/portal/login/teacher.html +34 -17
  211. portal/templates/portal/partials/banner.html +18 -4
  212. portal/templates/portal/partials/benefits.html +1 -1
  213. portal/templates/portal/partials/card_list.html +34 -24
  214. portal/templates/portal/partials/character_list.html +5 -5
  215. portal/templates/portal/partials/cookie_list.html +161 -0
  216. portal/templates/portal/partials/delete_popup.html +18 -0
  217. portal/templates/portal/partials/footer.html +57 -26
  218. portal/templates/portal/partials/header.html +118 -117
  219. portal/templates/portal/partials/hero_card.html +4 -3
  220. portal/templates/portal/partials/info_popup.html +3 -3
  221. portal/templates/portal/partials/invite_admin_teacher.html +23 -0
  222. portal/templates/portal/partials/popup.html +7 -2
  223. portal/templates/portal/partials/register_newsletter_tickbox.html +2 -5
  224. portal/templates/portal/partials/screentime_popup.html +14 -0
  225. portal/templates/portal/partials/service_unavailable_popup.html +17 -0
  226. portal/templates/portal/partials/session_popup.html +19 -0
  227. portal/templates/portal/play/student_dashboard.html +42 -29
  228. portal/templates/portal/play/student_edit_account.html +64 -9
  229. portal/templates/portal/play.html +61 -41
  230. portal/templates/portal/privacy_notice.html +697 -0
  231. portal/templates/portal/register.html +122 -92
  232. portal/templates/portal/reset_password.html +20 -40
  233. portal/templates/portal/reset_password_confirm.html +9 -4
  234. portal/templates/portal/reset_password_email_sent.html +15 -13
  235. portal/templates/portal/teach/base_registering.html +1 -1
  236. portal/templates/portal/teach/class.html +4 -6
  237. portal/templates/portal/teach/dashboard.html +212 -117
  238. portal/templates/portal/teach/invited.html +90 -0
  239. portal/templates/portal/teach/onboarding_classes.html +5 -3
  240. portal/templates/portal/teach/onboarding_print.html +1 -1
  241. portal/templates/portal/teach/onboarding_school.html +26 -139
  242. portal/templates/portal/teach/onboarding_students.html +1 -1
  243. portal/templates/portal/teach/teacher_dismiss_students.html +73 -55
  244. portal/templates/portal/teach/teacher_edit_class.html +168 -11
  245. portal/templates/portal/teach/teacher_edit_student.html +12 -5
  246. portal/templates/portal/teach/teacher_move_all_classes.html +25 -38
  247. portal/templates/portal/teach/teacher_move_students_to_class.html +1 -1
  248. portal/templates/portal/teach.html +61 -42
  249. portal/templates/portal/ten_year_map.html +147 -0
  250. portal/templates/portal/terms.html +191 -42
  251. portal/templates/two_factor/core/login.html +71 -59
  252. portal/templates/two_factor/core/setup.html +58 -49
  253. portal/templates/two_factor/profile/disable.html +1 -1
  254. portal/templates/two_factor/profile/profile.html +35 -17
  255. portal/templatetags/app_tags.py +59 -84
  256. portal/templatetags/card_list_tags.py +0 -4
  257. portal/tests/base_test.py +14 -3
  258. portal/tests/conftest.py +0 -15
  259. portal/tests/migrations/test_migration_make_portaladmin_teacher.py +2 -6
  260. portal/tests/migrations/test_migration_preview_users.py +3 -9
  261. portal/tests/migrations/test_migration_remove_guardian.py +1 -3
  262. portal/tests/migrations/test_migration_use_common_models.py +2 -6
  263. portal/tests/migrations/test_migration_verify_portaladmin.py +1 -3
  264. portal/tests/pageObjects/portal/admin/admin_base_page.py +0 -21
  265. portal/tests/pageObjects/portal/base_page.py +16 -26
  266. portal/tests/pageObjects/portal/email_verification_needed_page.py +3 -2
  267. portal/tests/pageObjects/portal/game_page.py +12 -19
  268. portal/tests/pageObjects/portal/home_page.py +13 -15
  269. portal/tests/pageObjects/portal/independent_login_page.py +13 -17
  270. portal/tests/pageObjects/portal/password_reset_form_page.py +20 -4
  271. portal/tests/pageObjects/portal/password_reset_page.py +25 -0
  272. portal/tests/pageObjects/portal/play/account_page.py +18 -27
  273. portal/tests/pageObjects/portal/play/dashboard_page.py +4 -4
  274. portal/tests/pageObjects/portal/play/join_school_or_club_page.py +8 -10
  275. portal/tests/pageObjects/portal/play/play_base_page.py +5 -3
  276. portal/tests/pageObjects/portal/signup_page.py +28 -59
  277. portal/tests/pageObjects/portal/student_login_class_code.py +6 -9
  278. portal/tests/pageObjects/portal/student_login_page.py +6 -8
  279. portal/tests/pageObjects/portal/teach/add_independent_student_to_class_page.py +3 -3
  280. portal/tests/pageObjects/portal/teach/added_independent_student_to_class_page.py +3 -1
  281. portal/tests/pageObjects/portal/teach/class_page.py +36 -13
  282. portal/tests/pageObjects/portal/teach/dashboard_page.py +43 -84
  283. portal/tests/pageObjects/portal/teach/dismiss_students_page.py +7 -5
  284. portal/tests/pageObjects/portal/teach/edit_student_page.py +10 -8
  285. portal/tests/pageObjects/portal/teach/move_class_page.py +5 -10
  286. portal/tests/pageObjects/portal/teach/move_classes_page.py +4 -2
  287. portal/tests/pageObjects/portal/teach/move_students_disambiguate_page.py +4 -2
  288. portal/tests/pageObjects/portal/teach/move_students_page.py +6 -13
  289. portal/tests/pageObjects/portal/teach/onboarding_classes_page.py +5 -3
  290. portal/tests/pageObjects/portal/teach/onboarding_organisation_page.py +11 -49
  291. portal/tests/pageObjects/portal/teach/onboarding_student_list_page.py +7 -12
  292. portal/tests/pageObjects/portal/teach/onboarding_students_page.py +4 -27
  293. portal/tests/pageObjects/portal/teach/teach_base_page.py +6 -4
  294. portal/tests/pageObjects/portal/teacher_login_page.py +10 -16
  295. portal/tests/selenium_test_case.py +3 -43
  296. portal/tests/snapshots/snap_test_partials.py +11 -165
  297. portal/tests/test_2FA.py +15 -33
  298. portal/tests/test_admin.py +15 -97
  299. portal/tests/test_api.py +212 -73
  300. portal/tests/test_captcha_forms.py +2 -2
  301. portal/tests/test_class.py +374 -24
  302. portal/tests/test_emails.py +83 -20
  303. portal/tests/{test_newsletter_footer.py → test_global_forms.py} +5 -5
  304. portal/tests/test_helper_methods.py +30 -0
  305. portal/tests/test_independent_student.py +255 -144
  306. portal/tests/test_invite_teacher.py +318 -10
  307. portal/tests/test_middleware.py +138 -15
  308. portal/tests/test_organisation.py +78 -262
  309. portal/tests/test_partials.py +0 -88
  310. portal/tests/test_ratelimit.py +218 -36
  311. portal/tests/test_school_student.py +35 -40
  312. portal/tests/test_security.py +12 -31
  313. portal/tests/test_teacher.py +425 -325
  314. portal/tests/test_teacher_student.py +103 -91
  315. portal/tests/test_views.py +900 -76
  316. portal/tests/utils/classes.py +2 -2
  317. portal/tests/utils/messages.py +13 -28
  318. portal/urls.py +237 -164
  319. portal/views/admin.py +0 -332
  320. portal/views/api.py +82 -68
  321. portal/views/cron/__init__.py +1 -0
  322. portal/views/cron/user.py +322 -0
  323. portal/views/dotmailer.py +9 -1
  324. portal/views/email.py +33 -77
  325. portal/views/google_analytics.py +28 -0
  326. portal/views/home.py +126 -97
  327. portal/views/legal.py +13 -0
  328. portal/views/login/independent_student.py +5 -5
  329. portal/views/login/student.py +51 -14
  330. portal/views/login/teacher.py +2 -6
  331. portal/views/organisation.py +20 -189
  332. portal/views/registration.py +97 -17
  333. portal/views/student/edit_account_details.py +99 -72
  334. portal/views/student/play.py +81 -62
  335. portal/views/teacher/dashboard.py +421 -149
  336. portal/views/teacher/teach.py +226 -177
  337. portal/views/two_factor/__init__.py +0 -0
  338. portal/views/two_factor/core.py +28 -0
  339. portal/views/two_factor/form.py +7 -0
  340. portal/views/two_factor/profile.py +11 -0
  341. codeforlife_portal-5.32.2.dist-info/LICENSE.md +0 -577
  342. codeforlife_portal-5.32.2.dist-info/METADATA +0 -38
  343. deploy/permissions.py +0 -2
  344. example_project/manage.py +0 -10
  345. portal/autoconfig.py +0 -140
  346. portal/csp_config.py +0 -60
  347. portal/forms/add_game.py +0 -33
  348. portal/helpers/location.py +0 -121
  349. portal/static/portal/img/kurono_hero.jpg +0 -0
  350. portal/static/portal/img/kurono_landing_hero.png +0 -0
  351. portal/static/portal/img/kurono_logo.svg +0 -1
  352. portal/static/portal/img/kurono_logo_grey_background.svg +0 -1
  353. portal/static/portal/img/kurono_logo_mark.svg +0 -1
  354. portal/static/portal/img/kurono_resources_hero.jpg +0 -0
  355. portal/static/portal/img/kurono_story.png +0 -0
  356. portal/static/portal/img/ocado-swirl.svg +0 -22
  357. portal/static/portal/img/thumbnail_educate_kurono.png +0 -0
  358. portal/static/portal/img/thumbnail_educate_resources_and_progress_tracking.png +0 -0
  359. portal/static/portal/img/thumbnail_kurono_resources.png +0 -0
  360. portal/static/portal/img/thumbnail_play_kurono.png +0 -0
  361. portal/static/portal/img/x_close_video.png +0 -0
  362. portal/static/portal/js/aimmoGame.js +0 -106
  363. portal/static/portal/js/deleteWorkspaces.js +0 -14
  364. portal/static/portal/js/fuzzySchoolLookup.js +0 -46
  365. portal/static/portal/js/lib/jquery-3.5.1.min.js +0 -2
  366. portal/static/portal/js/lib/jquery-ui-1.12.1.min.js +0 -13
  367. portal/static/portal/sass/partials/_videos.scss +0 -10
  368. portal/static/portal/video/aimmo_play_now_background_video.mp4 +0 -0
  369. portal/strings/student_aimmo_dashboard.py +0 -6
  370. portal/templates/portal/admin/aggregated_data.html +0 -35
  371. portal/templates/portal/admin/map.html +0 -70
  372. portal/templates/portal/mouseflow.html +0 -9
  373. portal/templates/portal/partials/aimmo_games_table.html +0 -83
  374. portal/templates/portal/partials/register_over_required_age_tickbox.html +0 -9
  375. portal/templates/portal/play/independent_student_dashboard.html +0 -64
  376. portal/templates/portal/play/student_aimmo_dashboard.html +0 -63
  377. portal/templates/portal/privacy_policy.html +0 -483
  378. portal/templates/portal/reset_password_email.html +0 -9
  379. portal/templates/portal/teach/invite.html +0 -25
  380. portal/templates/portal/teach/teacher_aimmo_dashboard.html +0 -95
  381. portal/templates/portal/teach/teacher_resources.html +0 -68
  382. portal/templatetags/character_list_tags.py +0 -16
  383. portal/tests/pageObjects/portal/kurono_teacher_dashboard_page.py +0 -49
  384. portal/tests/pageObjects/portal/student_password_reset_form_page.py +0 -23
  385. portal/tests/pageObjects/portal/teach/onboarding_revoke_request_page.py +0 -20
  386. portal/tests/pageObjects/portal/teacher_password_reset_form_page.py +0 -23
  387. portal/tests/test_aimmo_dashboards.py +0 -172
  388. portal/tests/test_location.py +0 -217
  389. portal/tests/utils/aimmo_games.py +0 -30
  390. portal/views/aimmo/dashboard.py +0 -119
  391. portal/views/privacy_policy.py +0 -9
  392. portal/views/teacher/teacher_resources.py +0 -42
  393. {portal/views/aimmo → cfl_common}/__init__.py +0 -0
@@ -1,44 +1,51 @@
1
- from __future__ import division
2
-
3
1
  import csv
4
2
  import json
5
- from datetime import timedelta, datetime
3
+ from datetime import datetime, timedelta
6
4
  from enum import Enum
7
5
  from functools import partial, wraps
8
6
  from uuid import uuid4
9
7
 
10
- from common import email_messages
11
- from common.helpers.emails import INVITE_FROM, send_email, send_verification_email
8
+ import pytz
9
+ from common.helpers.emails import send_verification_email
12
10
  from common.helpers.generators import (
13
11
  generate_access_code,
14
12
  generate_login_id,
15
- generate_new_student_name,
16
13
  generate_password,
17
14
  get_hashed_login_id,
18
15
  )
19
- from common.models import Class, Student, Teacher, DailyActivity
20
- from common.permissions import logged_in_as_teacher
16
+ from common.models import (
17
+ Class,
18
+ DailyActivity,
19
+ JoinReleaseStudent,
20
+ Student,
21
+ Teacher,
22
+ TotalActivity,
23
+ )
24
+ from common.permissions import check_teacher_authorised, logged_in_as_teacher
21
25
  from django.contrib import messages as messages
22
26
  from django.contrib.auth.decorators import login_required, user_passes_test
27
+ from django.contrib.auth.models import User
23
28
  from django.contrib.staticfiles.storage import staticfiles_storage
29
+ from django.db.models import F
24
30
  from django.forms.formsets import formset_factory
25
31
  from django.http import Http404, HttpResponse, HttpResponseRedirect
26
32
  from django.shortcuts import get_object_or_404, render
27
33
  from django.urls import reverse, reverse_lazy
28
34
  from django.utils import timezone
29
35
  from django.views.decorators.http import require_POST
30
- from past.utils import old_div
36
+ from game.models import Level
37
+ from game.views.level_selection import get_blockly_episodes, get_python_episodes
31
38
  from reportlab.lib.colors import black, red
32
39
  from reportlab.lib.pagesizes import A4
33
40
  from reportlab.lib.utils import ImageReader
34
41
  from reportlab.pdfgen import canvas
35
42
 
36
- from portal.forms.invite_teacher import InviteTeacherForm
37
43
  from portal.forms.teach import (
38
44
  BaseTeacherDismissStudentsFormSet,
39
45
  BaseTeacherMoveStudentsDisambiguationFormSet,
40
46
  ClassCreationForm,
41
47
  ClassEditForm,
48
+ ClassLevelControlForm,
42
49
  ClassMoveForm,
43
50
  StudentCreationForm,
44
51
  TeacherDismissStudentsForm,
@@ -47,13 +54,13 @@ from portal.forms.teach import (
47
54
  TeacherMoveStudentsDestinationForm,
48
55
  TeacherSetStudentPass,
49
56
  )
57
+ from portal.helpers.ratelimit import clear_ratelimit_cache_for_user
58
+ from portal.views.registration import handle_reset_password_tracking
50
59
 
51
60
  STUDENT_PASSWORD_LENGTH = 6
52
61
  REMINDER_CARDS_PDF_ROWS = 8
53
62
  REMINDER_CARDS_PDF_COLUMNS = 1
54
- REMINDER_CARDS_PDF_WARNING_TEXT = (
55
- "Please ensure students keep login details in a secure place"
56
- )
63
+ REMINDER_CARDS_PDF_WARNING_TEXT = "Please ensure students keep login details in a secure place"
57
64
 
58
65
 
59
66
  @login_required(login_url=reverse_lazy("teacher_login"))
@@ -63,22 +70,18 @@ def teacher_onboarding_create_class(request):
63
70
  Onboarding view for creating a class (and organisation if there isn't one, yet)
64
71
  """
65
72
  teacher = request.user.new_teacher
66
- requests = Student.objects.filter(
67
- pending_class_request__teacher=teacher, new_user__is_active=True
68
- )
73
+ requests = Student.objects.filter(pending_class_request__teacher=teacher, new_user__is_active=True)
69
74
 
70
75
  if not teacher.school:
71
76
  return HttpResponseRedirect(reverse_lazy("onboarding-organisation"))
72
77
 
73
78
  if request.method == "POST":
74
- form = ClassCreationForm(request.POST)
79
+ form = ClassCreationForm(request.POST, teacher=teacher)
75
80
  if form.is_valid():
76
81
  created_class = create_class(form, teacher)
77
82
  messages.success(
78
83
  request,
79
- "The class '{className}' has been created successfully.".format(
80
- className=created_class.name
81
- ),
84
+ "The class '{className}' has been created successfully.".format(className=created_class.name),
82
85
  )
83
86
  return HttpResponseRedirect(
84
87
  reverse_lazy(
@@ -87,7 +90,7 @@ def teacher_onboarding_create_class(request):
87
90
  )
88
91
  )
89
92
  else:
90
- form = ClassCreationForm()
93
+ form = ClassCreationForm(teacher=teacher)
91
94
 
92
95
  classes = Class.objects.filter(teacher=teacher)
93
96
 
@@ -98,13 +101,14 @@ def teacher_onboarding_create_class(request):
98
101
  )
99
102
 
100
103
 
101
- def create_class(form, teacher):
104
+ def create_class(form, class_teacher, class_creator=None):
102
105
  classmate_progress = bool(form.cleaned_data["classmate_progress"])
103
106
  klass = Class.objects.create(
104
107
  name=form.cleaned_data["class_name"],
105
- teacher=teacher,
108
+ teacher=class_teacher,
106
109
  access_code=generate_access_code(),
107
110
  classmates_data_viewable=classmate_progress,
111
+ created_by=class_teacher if class_creator is None else class_creator,
108
112
  )
109
113
  return klass
110
114
 
@@ -124,11 +128,9 @@ def process_edit_class(request, access_code, onboarding_done, next_url):
124
128
  """
125
129
  klass = get_object_or_404(Class, access_code=access_code)
126
130
  teacher = request.user.new_teacher
127
- students = Student.objects.filter(
128
- class_field=klass, new_user__is_active=True
129
- ).order_by("new_user__first_name")
131
+ students = Student.objects.filter(class_field=klass, new_user__is_active=True).order_by("new_user__first_name")
130
132
 
131
- check_user_is_authorised(request, klass)
133
+ check_teacher_authorised(request, klass.teacher)
132
134
 
133
135
  if request.method == "POST":
134
136
  new_students_form = StudentCreationForm(klass, request.POST)
@@ -147,6 +149,8 @@ def process_edit_class(request, access_code, onboarding_done, next_url):
147
149
  login_id=hashed_login_id,
148
150
  )
149
151
 
152
+ TotalActivity.objects.update(student_registrations=F("student_registrations") + 1)
153
+
150
154
  login_url = generate_student_url(request, new_student, login_id)
151
155
  students_info.append(
152
156
  {
@@ -167,7 +171,8 @@ def process_edit_class(request, access_code, onboarding_done, next_url):
167
171
  "query_data": json.dumps(students_info),
168
172
  "class_url": request.build_absolute_uri(
169
173
  reverse(
170
- "student_login", kwargs={"access_code": klass.access_code}
174
+ "student_login",
175
+ kwargs={"access_code": klass.access_code},
171
176
  )
172
177
  ),
173
178
  },
@@ -204,12 +209,6 @@ def teacher_onboarding_edit_class(request, access_code):
204
209
  )
205
210
 
206
211
 
207
- def check_user_is_authorised(request, klass):
208
- # check user authorised to see class
209
- if request.user.new_teacher != klass.teacher:
210
- raise Http404
211
-
212
-
213
212
  @login_required(login_url=reverse_lazy("teacher_login"))
214
213
  @user_passes_test(logged_in_as_teacher, login_url=reverse_lazy("teacher_login"))
215
214
  def teacher_view_class(request, access_code):
@@ -217,7 +216,10 @@ def teacher_view_class(request, access_code):
217
216
  Adding students to a class after the onboarding process has been completed
218
217
  """
219
218
  return process_edit_class(
220
- request, access_code, onboarding_done=True, next_url="portal/teach/class.html"
219
+ request,
220
+ access_code,
221
+ onboarding_done=True,
222
+ next_url="portal/teach/class.html",
221
223
  )
222
224
 
223
225
 
@@ -228,19 +230,16 @@ def teacher_delete_class(request, access_code):
228
230
  klass = get_object_or_404(Class, access_code=access_code)
229
231
 
230
232
  # check user authorised to see class
231
- if request.user.new_teacher != klass.teacher:
232
- raise Http404
233
+ check_teacher_authorised(request, klass.teacher)
233
234
 
234
235
  if Student.objects.filter(class_field=klass, new_user__is_active=True).exists():
235
236
  messages.info(
236
237
  request,
237
238
  "This class still has students, please remove or delete them all before deleting the class.",
238
239
  )
239
- return HttpResponseRedirect(
240
- reverse_lazy("view_class", kwargs={"access_code": access_code})
241
- )
240
+ return HttpResponseRedirect(reverse_lazy("view_class", kwargs={"access_code": access_code}))
242
241
 
243
- klass.delete()
242
+ klass.anonymise()
244
243
 
245
244
  return HttpResponseRedirect(reverse_lazy("dashboard") + "#classes")
246
245
 
@@ -251,14 +250,11 @@ def teacher_delete_students(request, access_code):
251
250
  klass = get_object_or_404(Class, access_code=access_code)
252
251
 
253
252
  # check user is authorised to deal with class
254
- if request.user.new_teacher != klass.teacher:
255
- raise Http404
253
+ check_teacher_authorised(request, klass.teacher)
256
254
 
257
255
  # get student objects for students to be deleted, confirming they are in the class
258
256
  student_ids = json.loads(request.POST.get("transfer_students", "[]"))
259
- students = [
260
- get_object_or_404(Student, id=i, class_field=klass) for i in student_ids
261
- ]
257
+ students = [get_object_or_404(Student, id=i, class_field=klass) for i in student_ids]
262
258
 
263
259
  def __anonymise(user):
264
260
  # Delete all personal data from inactive user and mark as inactive.
@@ -280,35 +276,42 @@ def teacher_delete_students(request, access_code):
280
276
  else: # otherwise, just delete
281
277
  student.new_user.delete()
282
278
 
283
- return HttpResponseRedirect(
284
- reverse_lazy("view_class", kwargs={"access_code": access_code})
285
- )
279
+ return HttpResponseRedirect(reverse_lazy("view_class", kwargs={"access_code": access_code}))
286
280
 
287
281
 
288
282
  @login_required(login_url=reverse_lazy("teacher_login"))
289
283
  @user_passes_test(logged_in_as_teacher, login_url=reverse_lazy("teacher_login"))
290
284
  def teacher_edit_class(request, access_code):
291
285
  """
292
- Editing class details
286
+ Editing additional class details. Provides functionality for:
287
+ - Editing the class name, sharing and joining settings
288
+ - Locking or unlocking specific Rapid Router levels
289
+ - Transferring the class to another teacher
293
290
  """
294
291
  klass = get_object_or_404(Class, access_code=access_code)
295
292
  old_teacher = klass.teacher
296
- other_teachers = Teacher.objects.filter(school=old_teacher.school).exclude(
297
- user=old_teacher.user
298
- )
293
+ other_teachers = Teacher.objects.filter(school=old_teacher.school).exclude(user=old_teacher.user)
299
294
 
300
295
  # check user authorised to see class
301
- if request.user.new_teacher != klass.teacher:
302
- raise Http404
296
+ check_teacher_authorised(request, klass.teacher)
303
297
 
304
298
  external_requests_message = klass.get_requests_message()
305
299
 
300
+ blockly_episodes = get_blockly_episodes(request)
301
+ python_episodes = get_python_episodes(request)
302
+
303
+ locked_levels = klass.locked_levels.all()
304
+ locked_levels_ids = [locked_level.id for locked_level in locked_levels]
305
+
306
+ locked_worksheet_ids = [worksheet.id for worksheet in klass.locked_worksheets.all()]
307
+
306
308
  form = ClassEditForm(
307
309
  initial={
308
310
  "name": klass.name,
309
311
  "classmate_progress": klass.classmates_data_viewable,
310
312
  }
311
313
  )
314
+ level_control_form = ClassLevelControlForm()
312
315
  class_move_form = ClassMoveForm(other_teachers)
313
316
 
314
317
  if request.method == "POST":
@@ -316,6 +319,10 @@ def teacher_edit_class(request, access_code):
316
319
  form = ClassEditForm(request.POST)
317
320
  if form.is_valid():
318
321
  return process_edit_class_form(request, klass, form)
322
+ elif "level_control_submit" in request.POST:
323
+ level_control_form = ClassLevelControlForm(request.POST)
324
+ if level_control_form.is_valid():
325
+ return process_level_control_form(request, klass, blockly_episodes, python_episodes)
319
326
  elif "class_move_submit" in request.POST:
320
327
  class_move_form = ClassMoveForm(other_teachers, request.POST)
321
328
  if class_move_form.is_valid():
@@ -327,6 +334,11 @@ def teacher_edit_class(request, access_code):
327
334
  {
328
335
  "form": form,
329
336
  "class_move_form": class_move_form,
337
+ "level_control_form": level_control_form,
338
+ "blockly_episodes": blockly_episodes,
339
+ "python_episodes": python_episodes,
340
+ "locked_levels": locked_levels_ids,
341
+ "locked_worksheet_ids": locked_worksheet_ids,
330
342
  "class": klass,
331
343
  "external_requests_message": external_requests_message,
332
344
  },
@@ -377,9 +389,65 @@ def process_edit_class_form(request, klass, form):
377
389
 
378
390
  messages.success(request, "The class's settings have been changed successfully.")
379
391
 
380
- return HttpResponseRedirect(
381
- reverse_lazy("view_class", kwargs={"access_code": klass.access_code})
382
- )
392
+ return HttpResponseRedirect(reverse_lazy("view_class", kwargs={"access_code": klass.access_code}))
393
+
394
+
395
+ def process_level_control_form(request, klass: Class, blockly_episodes, python_episodes):
396
+ """
397
+ Find the levels that the user wants to lock and lock them for the specific class.
398
+ :param request: The request sent by the user submitting the form.
399
+ :param klass: The class for which the levels are being locked / unlocked.
400
+ :param blockly_episodes: The set of Blockly Episodes (Rapid Router).
401
+ :param blockly_episodes: The set of Python Episodes (Python Den).
402
+ :return: A redirect to the teacher dashboard with a success message.
403
+ """
404
+ levels_to_lock_ids = []
405
+ locked_worksheet_ids = []
406
+
407
+ mark_levels_to_lock_in_episodes(request, blockly_episodes, levels_to_lock_ids, locked_worksheet_ids)
408
+ mark_levels_to_lock_in_episodes(request, python_episodes, levels_to_lock_ids, locked_worksheet_ids)
409
+
410
+ klass.locked_levels.clear()
411
+ [klass.locked_levels.add(levels_to_lock_id) for levels_to_lock_id in levels_to_lock_ids]
412
+ klass.locked_worksheets.clear()
413
+ for locked_worksheet_id in locked_worksheet_ids:
414
+ klass.locked_worksheets.add(locked_worksheet_id)
415
+
416
+ messages.success(request, "Your level preferences have been saved.")
417
+ activity_today = DailyActivity.objects.get_or_create(date=datetime.now().date())[0]
418
+ activity_today.level_control_submits += 1
419
+ activity_today.save()
420
+
421
+ return HttpResponseRedirect(reverse_lazy("dashboard"))
422
+
423
+
424
+ def mark_levels_to_lock_in_episodes(request, episodes, levels_to_lock_ids, locked_worksheet_ids: list):
425
+ """
426
+ For a given set of Episodes, find which Levels are to be locked. This is done by checking the POST request data.
427
+ If a Level ID is missing from the request.POST, it means it needs to be locked, and if the entire Episode is missing
428
+ from the request.POST, it means all the Levels under that Episode need to be locked.
429
+ :param request: The request sent by the user submitting the form.
430
+ :param episodes: The set of Episodes, in this case either the Blockly Episodes or the Python Episodes.
431
+ :param levels_to_lock_ids: A list of Level IDs marked to be locked.
432
+ """
433
+ for episode in episodes:
434
+ episode_levels = episode["levels"]
435
+ episode_worksheets = episode["worksheets"]
436
+ episode_index = f"episode{episode['id']}"
437
+ if episode_index in request.POST:
438
+ [
439
+ levels_to_lock_ids.append(episode_level["id"])
440
+ for episode_level in episode_levels
441
+ if f'level:{episode_level["id"]}' not in request.POST.getlist(episode_index)
442
+ ]
443
+ for episode_worksheet in episode_worksheets:
444
+ worksheet_id = episode_worksheet["id"]
445
+ if f"worksheet:{worksheet_id}" not in request.POST.getlist(episode_index):
446
+ locked_worksheet_ids.append(worksheet_id)
447
+ else:
448
+ [levels_to_lock_ids.append(episode_level["id"]) for episode_level in episode_levels]
449
+ for episode_worksheet in episode_worksheets:
450
+ locked_worksheet_ids.append(episode_worksheet["id"])
383
451
 
384
452
 
385
453
  def process_move_class_form(request, klass, form):
@@ -403,12 +471,9 @@ def teacher_edit_student(request, pk):
403
471
  Changing a student's details
404
472
  """
405
473
  student = get_object_or_404(Student, id=pk)
474
+ check_teacher_authorised(request, student.class_field.teacher)
406
475
 
407
- check_if_edit_authorised(request, student)
408
-
409
- name_form = TeacherEditStudentForm(
410
- student, initial={"name": student.new_user.first_name}
411
- )
476
+ name_form = TeacherEditStudentForm(student, initial={"name": student.new_user.first_name})
412
477
 
413
478
  password_form = TeacherSetStudentPass()
414
479
  set_password_mode = False
@@ -423,7 +488,8 @@ def teacher_edit_student(request, pk):
423
488
  student.save()
424
489
 
425
490
  messages.success(
426
- request, "The student's details have been changed successfully."
491
+ request,
492
+ "The student's details have been changed successfully.",
427
493
  )
428
494
 
429
495
  return HttpResponseRedirect(
@@ -462,10 +528,7 @@ def process_reset_password_form(request, student, password_form):
462
528
  login_url = request.build_absolute_uri(
463
529
  reverse(
464
530
  "student_direct_login",
465
- kwargs={
466
- "user_id": student.new_user.id,
467
- "login_id": uuidstr,
468
- },
531
+ kwargs={"user_id": student.new_user.id, "login_id": uuidstr},
469
532
  )
470
533
  )
471
534
 
@@ -478,9 +541,12 @@ def process_reset_password_form(request, student, password_form):
478
541
  }
479
542
  ]
480
543
 
544
+ handle_reset_password_tracking(request, "SCHOOL_STUDENT")
481
545
  student.new_user.set_password(new_password)
482
546
  student.new_user.save()
483
547
  student.login_id = login_id
548
+ clear_ratelimit_cache_for_user(f"{student.new_user.first_name},{student.class_field.access_code}")
549
+ student.blocked_time = datetime.now(tz=pytz.utc) - timedelta(days=1)
484
550
  student.save()
485
551
 
486
552
  return render(
@@ -501,12 +567,6 @@ def process_reset_password_form(request, student, password_form):
501
567
  )
502
568
 
503
569
 
504
- def check_if_edit_authorised(request, student):
505
- # check user is authorised to edit student
506
- if request.user.new_teacher != student.class_field.teacher:
507
- raise Http404
508
-
509
-
510
570
  @login_required(login_url=reverse_lazy("teacher_login"))
511
571
  @user_passes_test(logged_in_as_teacher, login_url=reverse_lazy("teacher_login"))
512
572
  def teacher_dismiss_students(request, access_code):
@@ -515,13 +575,11 @@ def teacher_dismiss_students(request, access_code):
515
575
  """
516
576
  klass = get_object_or_404(Class, access_code=access_code)
517
577
 
518
- check_if_dismiss_authorised(request, klass)
578
+ check_teacher_authorised(request, klass.teacher)
519
579
 
520
580
  # get student objects for students to be dismissed, confirming they are in the class
521
581
  student_ids = json.loads(request.POST.get("transfer_students", "[]"))
522
- students = [
523
- get_object_or_404(Student, id=i, class_field=klass) for i in student_ids
524
- ]
582
+ students = [get_object_or_404(Student, id=i, class_field=klass) for i in student_ids]
525
583
 
526
584
  TeacherDismissStudentsFormSet = formset_factory(
527
585
  wraps(TeacherDismissStudentsForm)(partial(TeacherDismissStudentsForm)),
@@ -553,38 +611,59 @@ def teacher_dismiss_students(request, access_code):
553
611
  )
554
612
 
555
613
 
556
- def check_if_dismiss_authorised(request, klass):
557
- # check user is authorised to deal with class
558
- if request.user.new_teacher != klass.teacher:
559
- raise Http404
560
-
561
-
562
614
  def is_right_dismiss_form(request):
563
615
  return request.method == "POST" and "submit_dismiss" in request.POST
564
616
 
565
617
 
566
618
  def process_dismiss_student_form(request, formset, klass, access_code):
619
+ failed_users = [] # users that failed to be transferred
567
620
  for data in formset.cleaned_data:
621
+ # check if email is already used
622
+ users_with_email = User.objects.filter(email=data["email"])
623
+ # email is already taken, skip this user
624
+ if users_with_email.exists():
625
+ failed_users.append(data["orig_name"])
626
+ continue
627
+
568
628
  student = get_object_or_404(
569
- Student, class_field=klass, new_user__first_name__iexact=data["orig_name"]
629
+ Student,
630
+ class_field=klass,
631
+ new_user__first_name__iexact=data["orig_name"],
570
632
  )
571
633
 
634
+ students_levels = Level.objects.filter(owner=student.new_user.userprofile).all()
635
+ for level in students_levels:
636
+ level.shared_with.set([])
637
+ level.save()
638
+
572
639
  student.class_field = None
573
640
  student.new_user.first_name = data["name"]
574
- student.new_user.username = data["name"]
641
+ student.new_user.username = data["email"]
575
642
  student.new_user.email = data["email"]
643
+ student.user.is_verified = False
576
644
  student.save()
577
645
  student.new_user.save()
578
- student.new_user.userprofile.save()
646
+ student.user.save()
579
647
 
580
- send_verification_email(request, student.new_user)
648
+ # log the data
649
+ joinrelease = JoinReleaseStudent.objects.create(student=student, action_type=JoinReleaseStudent.RELEASE)
650
+ joinrelease.save()
581
651
 
582
- messages.success(
583
- request, "The students have been removed successfully from the class."
584
- )
585
- return HttpResponseRedirect(
586
- reverse_lazy("view_class", kwargs={"access_code": access_code})
587
- )
652
+ send_verification_email(request, student.new_user, data, school=klass.teacher.school)
653
+
654
+ if not failed_users:
655
+ messages.success(
656
+ request,
657
+ "The students have been released successfully from the class.",
658
+ )
659
+ else:
660
+ messages.warning(
661
+ request,
662
+ f"The following students could not be released: {', '.join(failed_users)}. "
663
+ "Please make sure the email has not been registered to another account.",
664
+ )
665
+
666
+ return HttpResponseRedirect(reverse_lazy("view_class", kwargs={"access_code": access_code}))
588
667
 
589
668
 
590
669
  @login_required(login_url=reverse_lazy("teacher_login"))
@@ -596,15 +675,13 @@ def teacher_class_password_reset(request, access_code):
596
675
  klass = get_object_or_404(Class, access_code=access_code)
597
676
 
598
677
  # check user authorised to see class
599
- if request.user.new_teacher != klass.teacher:
600
- raise Http404
678
+ check_teacher_authorised(request, klass.teacher)
601
679
 
602
680
  student_ids = json.loads(request.POST.get("transfer_students", "[]"))
603
- students = [
604
- get_object_or_404(Student, id=i, class_field=klass) for i in student_ids
605
- ]
681
+ students = [get_object_or_404(Student, id=i, class_field=klass) for i in student_ids]
606
682
 
607
683
  students_info = []
684
+ handle_reset_password_tracking(request, "SCHOOL_STUDENT", access_code)
608
685
  for student in students:
609
686
  password = generate_password(STUDENT_PASSWORD_LENGTH)
610
687
 
@@ -623,6 +700,8 @@ def teacher_class_password_reset(request, access_code):
623
700
  student.new_user.set_password(password)
624
701
  student.new_user.save()
625
702
  student.login_id = hashed_login_id
703
+ clear_ratelimit_cache_for_user(f"{student.new_user.first_name},{access_code}")
704
+ student.blocked_time = datetime.now(tz=pytz.utc) - timedelta(days=1)
626
705
  student.save()
627
706
 
628
707
  return render(
@@ -650,8 +729,7 @@ def teacher_move_students(request, access_code):
650
729
  klass = get_object_or_404(Class, access_code=access_code)
651
730
 
652
731
  # check user is authorised to deal with class
653
- if request.user.new_teacher != klass.teacher:
654
- raise Http404
732
+ check_teacher_authorised(request, klass.teacher)
655
733
 
656
734
  transfer_students = request.POST.get("transfer_students", "[]")
657
735
 
@@ -666,7 +744,11 @@ def teacher_move_students(request, access_code):
666
744
  return render(
667
745
  request,
668
746
  "portal/teach/teacher_move_students.html",
669
- {"transfer_students": transfer_students, "old_class": klass, "form": form},
747
+ {
748
+ "transfer_students": transfer_students,
749
+ "old_class": klass,
750
+ "form": form,
751
+ },
670
752
  )
671
753
 
672
754
 
@@ -685,20 +767,15 @@ def teacher_move_students_to_class(request, access_code):
685
767
  transfer_students_ids = json.loads(request.POST.get("transfer_students", "[]"))
686
768
 
687
769
  # get student objects for students to be transferred, confirming they are in the old class still
688
- transfer_students = [
689
- get_object_or_404(Student, id=i, class_field=old_class)
690
- for i in transfer_students_ids
691
- ]
770
+ transfer_students = [get_object_or_404(Student, id=i, class_field=old_class) for i in transfer_students_ids]
692
771
 
693
772
  # get new class' students
694
- new_class_students = Student.objects.filter(
695
- class_field=new_class, new_user__is_active=True
696
- ).order_by("new_user__first_name")
773
+ new_class_students = Student.objects.filter(class_field=new_class, new_user__is_active=True).order_by(
774
+ "new_user__first_name"
775
+ )
697
776
 
698
777
  TeacherMoveStudentDisambiguationFormSet = formset_factory(
699
- wraps(TeacherMoveStudentDisambiguationForm)(
700
- partial(TeacherMoveStudentDisambiguationForm)
701
- ),
778
+ wraps(TeacherMoveStudentDisambiguationForm)(partial(TeacherMoveStudentDisambiguationForm)),
702
779
  extra=0,
703
780
  formset=BaseTeacherMoveStudentsDisambiguationFormSet,
704
781
  )
@@ -717,9 +794,7 @@ def teacher_move_students_to_class(request, access_code):
717
794
  for student in transfer_students
718
795
  ]
719
796
 
720
- formset = TeacherMoveStudentDisambiguationFormSet(
721
- new_class, initial=initial_data
722
- )
797
+ formset = TeacherMoveStudentDisambiguationFormSet(new_class, initial=initial_data)
723
798
 
724
799
  return render(
725
800
  request,
@@ -735,12 +810,11 @@ def teacher_move_students_to_class(request, access_code):
735
810
 
736
811
 
737
812
  def check_if_move_authorised(request, old_class, new_class):
738
- # check user is authorised to deal with class
739
- if request.user.new_teacher != old_class.teacher:
740
- raise Http404
813
+ teacher = request.user.new_teacher
741
814
 
742
- # check teacher authorised to transfer to new class
743
- if request.user.new_teacher.school != new_class.teacher.school:
815
+ # check teacher has permission to edit old_class and that both classes
816
+ # are in the same school
817
+ if (not teacher.is_admin and teacher != old_class.teacher) or teacher.school != new_class.teacher.school:
744
818
  raise Http404
745
819
 
746
820
 
@@ -765,14 +839,14 @@ def process_move_students_form(request, formset, old_class, new_class):
765
839
  student.new_user.save()
766
840
 
767
841
  messages.success(request, "The students have been transferred successfully.")
768
- return HttpResponseRedirect(
769
- reverse_lazy("view_class", kwargs={"access_code": old_class.access_code})
770
- )
842
+ return HttpResponseRedirect(reverse_lazy("view_class", kwargs={"access_code": old_class.access_code}))
771
843
 
772
844
 
773
845
  class DownloadType(Enum):
774
846
  CSV = 1
775
847
  LOGIN_CARDS = 2
848
+ PRIMARY_PACK = 3
849
+ PYTHON_PACK = 4
776
850
 
777
851
 
778
852
  @login_required(login_url=reverse_lazy("teacher_login"))
@@ -785,36 +859,29 @@ def teacher_print_reminder_cards(request, access_code):
785
859
 
786
860
  # Define constants that determine the look of the cards
787
861
  PAGE_WIDTH, PAGE_HEIGHT = A4
788
- PAGE_MARGIN = old_div(PAGE_WIDTH, 16)
789
- INTER_CARD_MARGIN = old_div(PAGE_WIDTH, 64)
790
- CARD_PADDING = old_div(PAGE_WIDTH, 48)
862
+ PAGE_MARGIN = PAGE_WIDTH // 16
863
+ INTER_CARD_MARGIN = PAGE_WIDTH // 64
864
+ CARD_PADDING = PAGE_WIDTH // 48
791
865
 
792
866
  # rows and columns on page
793
867
  NUM_X = REMINDER_CARDS_PDF_COLUMNS
794
868
  NUM_Y = REMINDER_CARDS_PDF_ROWS
795
869
 
796
- CARD_WIDTH = old_div(PAGE_WIDTH - PAGE_MARGIN * 2, NUM_X)
797
- CARD_HEIGHT = old_div(PAGE_HEIGHT - PAGE_MARGIN * 4, NUM_Y)
870
+ CARD_WIDTH = (PAGE_WIDTH - PAGE_MARGIN * 2) // NUM_X
871
+ CARD_HEIGHT = (PAGE_HEIGHT - PAGE_MARGIN * 4) // NUM_Y
798
872
 
799
873
  CARD_INNER_HEIGHT = CARD_HEIGHT - CARD_PADDING * 2
800
874
 
801
- logo_image = ImageReader(
802
- staticfiles_storage.path("portal/img/logo_cfl_reminder_cards.jpg")
803
- )
875
+ logo_image = ImageReader(staticfiles_storage.path("portal/img/logo_cfl_reminder_cards.jpg"))
804
876
 
805
877
  klass = get_object_or_404(Class, access_code=access_code)
806
878
  # Check auth
807
- if klass.teacher.new_user != request.user:
808
- raise Http404
879
+ check_teacher_authorised(request, klass.teacher)
809
880
 
810
881
  # Use data from the query string if given
811
882
  student_data = get_student_data(request)
812
- student_login_link = request.build_absolute_uri(
813
- reverse("student_login_access_code")
814
- )
815
- class_login_link = request.build_absolute_uri(
816
- reverse("student_login", kwargs={"access_code": access_code})
817
- )
883
+ student_login_link = request.build_absolute_uri(reverse("student_login_access_code"))
884
+ class_login_link = request.build_absolute_uri(reverse("student_login", kwargs={"access_code": access_code}))
818
885
 
819
886
  # Now draw everything
820
887
  x = 0
@@ -829,9 +896,7 @@ def teacher_print_reminder_cards(request, access_code):
829
896
  p.drawString(PAGE_MARGIN, PAGE_MARGIN / 2, REMINDER_CARDS_PDF_WARNING_TEXT)
830
897
 
831
898
  left = PAGE_MARGIN + x * CARD_WIDTH + x * INTER_CARD_MARGIN * 2
832
- bottom = (
833
- PAGE_HEIGHT - PAGE_MARGIN - (y + 1) * CARD_HEIGHT - y * INTER_CARD_MARGIN
834
- )
899
+ bottom = PAGE_HEIGHT - PAGE_MARGIN - (y + 1) * CARD_HEIGHT - y * INTER_CARD_MARGIN
835
900
 
836
901
  inner_bottom = bottom + CARD_PADDING
837
902
 
@@ -839,16 +904,19 @@ def teacher_print_reminder_cards(request, access_code):
839
904
  p.setStrokeColor(black)
840
905
  p.rect(left, bottom, CARD_WIDTH, CARD_HEIGHT)
841
906
 
907
+ card_logo_height = CARD_HEIGHT - INTER_CARD_MARGIN * 2
908
+
842
909
  # logo
843
910
  p.drawImage(
844
911
  logo_image,
845
- left,
912
+ left + INTER_CARD_MARGIN,
846
913
  bottom + INTER_CARD_MARGIN,
847
- height=CARD_HEIGHT - INTER_CARD_MARGIN * 2,
914
+ height=card_logo_height,
848
915
  preserveAspectRatio=True,
916
+ anchor="w",
849
917
  )
850
918
 
851
- text_left = left + logo_image.getSize()[0]
919
+ text_left = left + INTER_CARD_MARGIN + (logo_image.getSize()[0] / logo_image.getSize()[1]) * card_logo_height
852
920
 
853
921
  # student details
854
922
  p.setFillColor(black)
@@ -859,11 +927,7 @@ def teacher_print_reminder_cards(request, access_code):
859
927
  f"Class code: {klass.access_code} at {student_login_link}",
860
928
  )
861
929
  p.setFont("Helvetica-BoldOblique", 12)
862
- p.drawString(
863
- text_left,
864
- inner_bottom + CARD_INNER_HEIGHT * 0.6,
865
- "OR",
866
- )
930
+ p.drawString(text_left, inner_bottom + CARD_INNER_HEIGHT * 0.6, "OR")
867
931
  p.setFont("Helvetica", 12)
868
932
  p.drawString(
869
933
  text_left + 22,
@@ -898,12 +962,9 @@ def teacher_download_csv(request, access_code):
898
962
 
899
963
  klass = get_object_or_404(Class, access_code=access_code)
900
964
  # Check auth
901
- if klass.teacher.new_user != request.user:
902
- raise Http404
965
+ check_teacher_authorised(request, klass.teacher)
903
966
 
904
- class_url = request.build_absolute_uri(
905
- reverse("student_login", kwargs={"access_code": access_code})
906
- )
967
+ class_url = request.build_absolute_uri(reverse("student_login", kwargs={"access_code": access_code}))
907
968
 
908
969
  # Use data from the query string if given
909
970
  student_data = get_student_data(request)
@@ -911,9 +972,7 @@ def teacher_download_csv(request, access_code):
911
972
  writer = csv.writer(response)
912
973
  writer.writerow([access_code, class_url])
913
974
  for student in student_data:
914
- writer.writerow(
915
- [student["name"], student["password"], student["login_url"]]
916
- )
975
+ writer.writerow([student["name"], student["password"], student["login_url"]])
917
976
 
918
977
  count_student_details_click(DownloadType.CSV)
919
978
 
@@ -940,6 +999,15 @@ def compute_show_page_end(p, x, y):
940
999
  p.showPage()
941
1000
 
942
1001
 
1002
+ def count_student_pack_downloads_click(student_pack_type):
1003
+ activity_today = DailyActivity.objects.get_or_create(date=datetime.now().date())[0]
1004
+ if DownloadType(student_pack_type) == DownloadType.PRIMARY_PACK:
1005
+ activity_today.primary_coding_club_downloads += 1
1006
+ else:
1007
+ raise Exception("Unknown download type")
1008
+ activity_today.save()
1009
+
1010
+
943
1011
  def count_student_details_click(download_type):
944
1012
  activity_today = DailyActivity.objects.get_or_create(date=datetime.now().date())[0]
945
1013
 
@@ -951,22 +1019,3 @@ def count_student_details_click(download_type):
951
1019
  raise Exception("Unknown download type")
952
1020
 
953
1021
  activity_today.save()
954
-
955
-
956
- def invite_teacher(request):
957
- if request.method == "POST":
958
- invite_teacher_form = InviteTeacherForm(data=request.POST)
959
- if invite_teacher_form.is_valid():
960
- email_address = invite_teacher_form.cleaned_data["email"]
961
- email_message = email_messages.inviteTeacherEmail(request)
962
- send_email(
963
- INVITE_FROM,
964
- [email_address],
965
- email_message["subject"],
966
- email_message["message"],
967
- )
968
- return render(request, "portal/email_invitation_sent.html")
969
-
970
- return render(
971
- request, "portal/teach/invite.html", {"invite_form": InviteTeacherForm()}
972
- )