codeforlife-portal 5.33.5__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 (391) 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.33.5.dist-info → codeforlife_portal-8.9.9.dist-info}/RECORD +339 -241
  92. {codeforlife_portal-5.33.5.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.33.5.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 +4 -2
  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 +8 -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 -91
  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 +96 -9
  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 +235 -166
  319. portal/views/admin.py +0 -332
  320. portal/views/api.py +82 -48
  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/core.py +22 -19
  338. portal/views/two_factor/profile.py +2 -2
  339. codeforlife_portal-5.33.5.dist-info/LICENSE.md +0 -577
  340. codeforlife_portal-5.33.5.dist-info/METADATA +0 -38
  341. deploy/permissions.py +0 -2
  342. example_project/manage.py +0 -10
  343. portal/autoconfig.py +0 -141
  344. portal/csp_config.py +0 -60
  345. portal/forms/add_game.py +0 -33
  346. portal/helpers/location.py +0 -121
  347. portal/static/portal/img/kurono_hero.jpg +0 -0
  348. portal/static/portal/img/kurono_landing_hero.png +0 -0
  349. portal/static/portal/img/kurono_logo.svg +0 -1
  350. portal/static/portal/img/kurono_logo_grey_background.svg +0 -1
  351. portal/static/portal/img/kurono_logo_mark.svg +0 -1
  352. portal/static/portal/img/kurono_resources_hero.jpg +0 -0
  353. portal/static/portal/img/kurono_story.png +0 -0
  354. portal/static/portal/img/ocado-swirl.svg +0 -22
  355. portal/static/portal/img/thumbnail_educate_kurono.png +0 -0
  356. portal/static/portal/img/thumbnail_educate_resources_and_progress_tracking.png +0 -0
  357. portal/static/portal/img/thumbnail_kurono_resources.png +0 -0
  358. portal/static/portal/img/thumbnail_play_kurono.png +0 -0
  359. portal/static/portal/img/x_close_video.png +0 -0
  360. portal/static/portal/js/aimmoGame.js +0 -106
  361. portal/static/portal/js/deleteWorkspaces.js +0 -14
  362. portal/static/portal/js/fuzzySchoolLookup.js +0 -46
  363. portal/static/portal/js/lib/jquery-3.5.1.min.js +0 -2
  364. portal/static/portal/js/lib/jquery-ui-1.12.1.min.js +0 -13
  365. portal/static/portal/sass/partials/_videos.scss +0 -10
  366. portal/static/portal/video/aimmo_play_now_background_video.mp4 +0 -0
  367. portal/strings/student_aimmo_dashboard.py +0 -6
  368. portal/templates/portal/admin/aggregated_data.html +0 -35
  369. portal/templates/portal/admin/map.html +0 -70
  370. portal/templates/portal/mouseflow.html +0 -9
  371. portal/templates/portal/partials/aimmo_games_table.html +0 -83
  372. portal/templates/portal/partials/register_over_required_age_tickbox.html +0 -9
  373. portal/templates/portal/play/independent_student_dashboard.html +0 -64
  374. portal/templates/portal/play/student_aimmo_dashboard.html +0 -63
  375. portal/templates/portal/privacy_policy.html +0 -483
  376. portal/templates/portal/reset_password_email.html +0 -9
  377. portal/templates/portal/teach/invite.html +0 -25
  378. portal/templates/portal/teach/teacher_aimmo_dashboard.html +0 -95
  379. portal/templates/portal/teach/teacher_resources.html +0 -68
  380. portal/templatetags/character_list_tags.py +0 -16
  381. portal/tests/pageObjects/portal/kurono_teacher_dashboard_page.py +0 -49
  382. portal/tests/pageObjects/portal/student_password_reset_form_page.py +0 -23
  383. portal/tests/pageObjects/portal/teach/onboarding_revoke_request_page.py +0 -20
  384. portal/tests/pageObjects/portal/teacher_password_reset_form_page.py +0 -23
  385. portal/tests/test_aimmo_dashboards.py +0 -172
  386. portal/tests/test_location.py +0 -217
  387. portal/tests/utils/aimmo_games.py +0 -30
  388. portal/views/aimmo/dashboard.py +0 -119
  389. portal/views/privacy_policy.py +0 -9
  390. portal/views/teacher/teacher_resources.py +0 -42
  391. {portal/views/aimmo → cfl_common}/__init__.py +0 -0
@@ -0,0 +1,322 @@
1
+ import logging
2
+ from datetime import datetime, timedelta
3
+
4
+ from common.helpers.emails import generate_token_for_email
5
+ from common.mail import campaign_ids, send_dotdigital_email
6
+ from common.models import DailyActivity, TotalActivity
7
+ from django.contrib.auth.models import User
8
+ from django.db.models import F, Q
9
+ from django.db.models.query import QuerySet
10
+ from django.urls import reverse
11
+ from django.utils import timezone
12
+ from rest_framework.response import Response
13
+ from rest_framework.views import APIView
14
+
15
+ from ...mixins import CronMixin
16
+ from ...views.api import anonymise
17
+
18
+ USER_1ST_VERIFY_EMAIL_REMINDER_DAYS = 7
19
+ USER_2ND_VERIFY_EMAIL_REMINDER_DAYS = 14
20
+ USER_DELETE_UNVERIFIED_ACCOUNT_DAYS = 19
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
+ USER_RETENTION_PERIOD = 1096 # 3 years
26
+
27
+
28
+ def get_unverified_users(
29
+ days: int, same_day: bool
30
+ ) -> (QuerySet[User], QuerySet[User]):
31
+ now = timezone.now()
32
+
33
+ # All expired unverified users.
34
+ user_queryset = User.objects.filter(
35
+ date_joined__lte=now - timedelta(days=days),
36
+ userprofile__is_verified=False,
37
+ )
38
+ if same_day:
39
+ user_queryset = user_queryset.filter(
40
+ date_joined__gt=now - timedelta(days=days + 1)
41
+ )
42
+
43
+ teacher_queryset = user_queryset.filter(
44
+ new_teacher__isnull=False,
45
+ new_student__isnull=True,
46
+ )
47
+ independent_student_queryset = user_queryset.filter(
48
+ new_teacher__isnull=True,
49
+ new_student__class_field__isnull=True,
50
+ )
51
+
52
+ return teacher_queryset, independent_student_queryset
53
+
54
+
55
+ def get_inactive_users(days: int) -> QuerySet[User]:
56
+ now = timezone.now()
57
+
58
+ # All users who haven't logged in in X days OR who've never logged in and
59
+ # registered over X days ago.
60
+ user_queryset = User.objects.filter(
61
+ Q(
62
+ last_login__isnull=False,
63
+ last_login__lte=now - timedelta(days=days),
64
+ last_login__gt=now - timedelta(days=days + 1),
65
+ )
66
+ | Q(
67
+ last_login__isnull=True,
68
+ date_joined__lte=now - timedelta(days=days),
69
+ date_joined__gt=now - timedelta(days=days + 1),
70
+ )
71
+ )
72
+
73
+ return user_queryset.exclude(email__isnull=True).exclude(email="")
74
+
75
+
76
+ def build_absolute_google_uri(request, location: str) -> str:
77
+ """
78
+ This is needed specifically for emails sent by cron jobs as the protocol for
79
+ cron jobs is HTTP and the service name is wrongly parsed.
80
+ """
81
+ url = request.build_absolute_uri(location)
82
+ url = url.replace("http", "https")
83
+ url = url.replace(".decent", "-dot-decent")
84
+
85
+ return url
86
+
87
+
88
+ class FirstVerifyEmailReminderView(CronMixin, APIView):
89
+ def get(self, request):
90
+ teacher_queryset, independent_student_queryset = get_unverified_users(
91
+ USER_1ST_VERIFY_EMAIL_REMINDER_DAYS,
92
+ same_day=True,
93
+ )
94
+ user_queryset = teacher_queryset.union(independent_student_queryset)
95
+ user_count = user_queryset.count()
96
+
97
+ logging.info(f"{user_count} emails unverified.")
98
+
99
+ if user_count > 0:
100
+ sent_email_count = 0
101
+ for email in user_queryset.values_list("email", flat=True).iterator(
102
+ chunk_size=500
103
+ ):
104
+ email_verification_url = build_absolute_google_uri(
105
+ request,
106
+ reverse(
107
+ "verify_email",
108
+ kwargs={"token": generate_token_for_email(email)},
109
+ ),
110
+ )
111
+
112
+ try:
113
+ send_dotdigital_email(
114
+ campaign_ids["verify_new_user_first_reminder"],
115
+ [email],
116
+ personalization_values={
117
+ "VERIFICATION_LINK": email_verification_url
118
+ },
119
+ )
120
+
121
+ sent_email_count += 1
122
+ except Exception as ex:
123
+ logging.exception(ex)
124
+
125
+ logging.info(f"Sent {sent_email_count}/{user_count} emails.")
126
+
127
+ return Response()
128
+
129
+
130
+ class SecondVerifyEmailReminderView(CronMixin, APIView):
131
+ def get(self, request):
132
+ teacher_queryset, independent_student_queryset = get_unverified_users(
133
+ USER_2ND_VERIFY_EMAIL_REMINDER_DAYS,
134
+ same_day=True,
135
+ )
136
+ user_queryset = teacher_queryset.union(independent_student_queryset)
137
+ user_count = user_queryset.count()
138
+
139
+ logging.info(f"{user_count} emails unverified.")
140
+
141
+ if user_count > 0:
142
+
143
+ sent_email_count = 0
144
+ for email in user_queryset.values_list("email", flat=True).iterator(
145
+ chunk_size=500
146
+ ):
147
+ email_verification_url = build_absolute_google_uri(
148
+ request,
149
+ reverse(
150
+ "verify_email",
151
+ kwargs={"token": generate_token_for_email(email)},
152
+ ),
153
+ )
154
+
155
+ try:
156
+ send_dotdigital_email(
157
+ campaign_ids["verify_new_user_second_reminder"],
158
+ [email],
159
+ personalization_values={
160
+ "VERIFICATION_LINK": email_verification_url
161
+ },
162
+ )
163
+
164
+ sent_email_count += 1
165
+ except Exception as ex:
166
+ logging.exception(ex)
167
+
168
+ logging.info(f"Sent {sent_email_count}/{user_count} emails.")
169
+
170
+ return Response()
171
+
172
+
173
+ class AnonymiseUnverifiedAccounts(CronMixin, APIView):
174
+ def get(self, request):
175
+ user_count = User.objects.filter(is_active=True).count()
176
+
177
+ teacher_queryset, independent_student_queryset = get_unverified_users(
178
+ USER_DELETE_UNVERIFIED_ACCOUNT_DAYS,
179
+ same_day=False,
180
+ )
181
+ teacher_count = teacher_queryset.count()
182
+ indy_count = independent_student_queryset.count()
183
+
184
+ user_queryset = teacher_queryset.union(independent_student_queryset)
185
+
186
+ for user in user_queryset.iterator(chunk_size=100):
187
+ try:
188
+ anonymise(user)
189
+ except Exception as ex:
190
+ logging.error(f"Failed to anonymise user with id: {user.id}")
191
+ logging.exception(ex)
192
+
193
+ user_count -= User.objects.filter(is_active=True).count()
194
+ logging.info(f"{user_count} unverified users anonymised.")
195
+
196
+ activity_today = DailyActivity.objects.get_or_create(
197
+ date=datetime.now().date()
198
+ )[0]
199
+ activity_today.anonymised_unverified_teachers = teacher_count
200
+ activity_today.anonymised_unverified_independents = indy_count
201
+ activity_today.save()
202
+
203
+ TotalActivity.objects.update(
204
+ anonymised_unverified_teachers=F("anonymised_unverified_teachers")
205
+ + teacher_count,
206
+ anonymised_unverified_independents=F(
207
+ "anonymised_unverified_independents"
208
+ )
209
+ + indy_count,
210
+ )
211
+
212
+ return Response()
213
+
214
+
215
+ class FirstInactivityReminderView(CronMixin, APIView):
216
+ def get(self, request):
217
+ user_queryset = get_inactive_users(USER_1ST_INACTIVE_REMINDER_DAYS)
218
+ user_count = user_queryset.count()
219
+
220
+ logging.info(
221
+ f"{user_count} inactive users after "
222
+ f"{USER_1ST_INACTIVE_REMINDER_DAYS} days."
223
+ )
224
+
225
+ if user_count > 0:
226
+ sent_email_count = 0
227
+ for email in user_queryset.values_list("email", flat=True).iterator(
228
+ chunk_size=500
229
+ ):
230
+ try:
231
+ send_dotdigital_email(
232
+ campaign_ids[
233
+ "inactive_users_on_website_first_reminder"
234
+ ],
235
+ [email],
236
+ )
237
+
238
+ sent_email_count += 1
239
+ except Exception as ex:
240
+ logging.exception(ex)
241
+
242
+ logging.info(
243
+ f"Reminded {sent_email_count}/{user_count} inactive users."
244
+ )
245
+
246
+ return Response()
247
+
248
+
249
+ class SecondInactivityReminderView(CronMixin, APIView):
250
+ def get(self, request):
251
+ user_queryset = get_inactive_users(USER_2ND_INACTIVE_REMINDER_DAYS)
252
+ user_count = user_queryset.count()
253
+
254
+ logging.info(
255
+ f"{user_count} inactive users after "
256
+ f"{USER_2ND_INACTIVE_REMINDER_DAYS} days."
257
+ )
258
+
259
+ if user_count > 0:
260
+ sent_email_count = 0
261
+ for email in user_queryset.values_list("email", flat=True).iterator(
262
+ chunk_size=500
263
+ ):
264
+ try:
265
+ send_dotdigital_email(
266
+ campaign_ids[
267
+ "inactive_users_on_website_second_reminder"
268
+ ],
269
+ [email],
270
+ personalization_values={
271
+ "DAYS_LEFT": USER_RETENTION_PERIOD
272
+ - USER_2ND_INACTIVE_REMINDER_DAYS
273
+ },
274
+ )
275
+
276
+ sent_email_count += 1
277
+ except Exception as ex:
278
+ logging.exception(ex)
279
+
280
+ logging.info(
281
+ f"Reminded {sent_email_count}/{user_count} inactive users."
282
+ )
283
+
284
+ return Response()
285
+
286
+
287
+ class FinalInactivityReminderView(CronMixin, APIView):
288
+ def get(self, request):
289
+ user_queryset = get_inactive_users(USER_FINAL_INACTIVE_REMINDER_DAYS)
290
+ user_count = user_queryset.count()
291
+
292
+ logging.info(
293
+ f"{user_count} inactive users after "
294
+ f"{USER_FINAL_INACTIVE_REMINDER_DAYS} days."
295
+ )
296
+
297
+ if user_count > 0:
298
+ sent_email_count = 0
299
+ for email in user_queryset.values_list("email", flat=True).iterator(
300
+ chunk_size=500
301
+ ):
302
+ try:
303
+ send_dotdigital_email(
304
+ campaign_ids[
305
+ "inactive_users_on_website_final_reminder"
306
+ ],
307
+ [email],
308
+ personalization_values={
309
+ "DAYS_LEFT": USER_RETENTION_PERIOD
310
+ - USER_FINAL_INACTIVE_REMINDER_DAYS
311
+ },
312
+ )
313
+
314
+ sent_email_count += 1
315
+ except Exception as ex:
316
+ logging.exception(ex)
317
+
318
+ logging.info(
319
+ f"Reminded {sent_email_count}/{user_count} inactive users."
320
+ )
321
+
322
+ return Response()
portal/views/dotmailer.py CHANGED
@@ -3,7 +3,9 @@ from common.helpers.emails import (
3
3
  get_dotmailer_user_by_email,
4
4
  send_dotmailer_consent_confirmation_email_to_user,
5
5
  add_consent_record_to_dotmailer_user,
6
+ DotmailerUserType,
6
7
  )
8
+ from common.mail import address_book_ids
7
9
  from django.contrib import messages as messages
8
10
  from django.http import HttpResponseRedirect, HttpResponse
9
11
  from django.shortcuts import render
@@ -19,7 +21,13 @@ def process_newsletter_form(request):
19
21
  newsletter_form = NewsletterForm(data=request.POST)
20
22
  if newsletter_form.is_valid():
21
23
  user_email = newsletter_form.cleaned_data["email"]
22
- add_to_dotmailer("", "", user_email)
24
+ add_to_dotmailer(
25
+ "",
26
+ "",
27
+ user_email,
28
+ address_book_ids["newsletter"],
29
+ DotmailerUserType.NO_ACCOUNT,
30
+ )
23
31
  messages.success(request, "Thank you for signing up! 🎉")
24
32
  return HttpResponseRedirect(reverse_lazy("home"))
25
33
  messages.error(
portal/views/email.py CHANGED
@@ -1,58 +1,60 @@
1
1
  from datetime import timedelta
2
2
 
3
- from common.helpers.emails import NOTIFICATION_EMAIL, send_email
4
- from common.models import EmailVerification, School, Student, Teacher
5
- from common.permissions import logged_in_as_independent_student, logged_in_as_teacher
3
+ import jwt
4
+ from common.models import Teacher
5
+ from common.permissions import logged_in_as_independent_student
6
+ from django.conf import settings
6
7
  from django.contrib import messages as messages
7
8
  from django.contrib.auth.models import User
8
- from django.db.models import Count
9
- from django.http import HttpResponse, HttpResponseRedirect
9
+ from django.http import HttpResponseRedirect
10
10
  from django.shortcuts import render
11
11
  from django.urls import reverse_lazy
12
12
  from django.utils import timezone
13
- from django_countries import countries
14
-
15
- from portal.app_settings import CONTACT_FORM_EMAILS
16
13
 
17
14
 
18
15
  def verify_email(request, token):
19
- verifications = EmailVerification.objects.filter(token=token)
16
+ decoded_jwt = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
17
+
18
+ user_found = User.objects.filter(email=decoded_jwt["email"]).first()
19
+ usertype = (
20
+ "TEACHER"
21
+ if Teacher.objects.filter(new_user=user_found).exists()
22
+ else "INDEP_STUDENT"
23
+ )
24
+ is_expired = decoded_jwt["expires"] < timezone.now().timestamp()
25
+
26
+ is_changing_email = decoded_jwt["new_email"] != ""
27
+ updated_email = (
28
+ decoded_jwt["new_email"] if is_changing_email else decoded_jwt["email"]
29
+ )
20
30
 
21
- if not verifications.exists():
31
+ if not user_found or is_expired:
22
32
  return render(
23
33
  request,
24
34
  "portal/email_verification_failed.html",
25
- {"usertype": "INDEP_STUDENT"},
35
+ {"usertype": usertype},
26
36
  )
27
37
 
28
- verification = verifications[0]
29
- user = verification.user
30
-
31
- usertype = (
32
- "TEACHER" if Teacher.objects.filter(new_user=user).exists() else "INDEP_STUDENT"
33
- )
34
-
35
- if has_verification_failed(verifications):
38
+ is_user_verified = user_found.userprofile.is_verified
39
+ if is_user_verified and not is_changing_email:
36
40
  return render(
37
- request, "portal/email_verification_failed.html", {"usertype": usertype}
41
+ request,
42
+ "portal/email_verification_failed.html",
43
+ {"usertype": usertype},
38
44
  )
39
45
 
40
- verification.verified = True
41
- verification.save()
42
-
43
- if verification.email: # verifying change of email address
44
- user.email = verification.email
45
- if logged_in_as_teacher(user):
46
- user.username = verification.email
47
- user.save()
48
-
49
- user.email_verifications.exclude(email=user.email).delete()
46
+ if not is_user_verified or is_changing_email:
47
+ user_found.email = updated_email
48
+ user_found.username = updated_email
49
+ user_found.userprofile.is_verified = True
50
+ user_found.userprofile.save()
51
+ user_found.save()
50
52
 
51
53
  messages.success(
52
54
  request, "Your email address was successfully verified, please log in."
53
55
  )
54
56
 
55
- if logged_in_as_independent_student(user):
57
+ if logged_in_as_independent_student(user_found):
56
58
  login_url = "independent_student_login"
57
59
  else:
58
60
  login_url = "teacher_login"
@@ -66,49 +68,3 @@ def has_verification_failed(verifications):
66
68
  or verifications[0].verified
67
69
  or (verifications[0].expiry - timezone.now()) < timedelta()
68
70
  )
69
-
70
-
71
- def send_new_users_report(request):
72
- new_users_count = User.objects.filter(
73
- date_joined__gte=timezone.now() - timedelta(days=7)
74
- ).count()
75
- users_count = User.objects.count()
76
- active_users = User.objects.filter(
77
- last_login__gte=timezone.now() - timedelta(days=7)
78
- ).count()
79
- school_count = School.objects.count()
80
- teacher_count = Teacher.objects.count()
81
- student_count = Student.objects.count()
82
- schools_countries = (
83
- School.objects.values("country")
84
- .annotate(nb_countries=Count("id"))
85
- .order_by("-nb_countries")
86
- )
87
- nb_countries = schools_countries.count()
88
- countries_count = "\n".join(
89
- "{}: {}".format(dict(countries)[k["country"]], k["nb_countries"])
90
- for k in schools_countries[:3]
91
- )
92
- send_email(
93
- NOTIFICATION_EMAIL,
94
- CONTACT_FORM_EMAILS,
95
- "new users",
96
- "There are {new_users} new users this week!\n"
97
- "The total number of registered users is now: {users}\n"
98
- "Current number of schools: {schools}\n"
99
- "Current number of teachers: {teachers}\n"
100
- "Current number of students: {students}\n"
101
- "Number of users that last logged in during the last week: {active_users}\n"
102
- "Number of countries with registered schools: {countries}\n"
103
- "Top 3 - schools per country:\n{countries_counter}".format(
104
- new_users=new_users_count,
105
- users=users_count,
106
- schools=school_count,
107
- teachers=teacher_count,
108
- students=student_count,
109
- countries=nb_countries,
110
- active_users=active_users,
111
- countries_counter=countries_count,
112
- ),
113
- )
114
- return HttpResponse("success")
@@ -0,0 +1,28 @@
1
+ import json
2
+
3
+ import requests
4
+ from django.http import HttpRequest, JsonResponse
5
+ from django.views.decorators.csrf import csrf_exempt
6
+
7
+
8
+ @csrf_exempt
9
+ def collect(request: HttpRequest):
10
+ body: dict = json.loads(request.body)
11
+ measurement_id = body["measurement_id"]
12
+ api_secret = body["api_secret"]
13
+ debug = body.get("debug", False)
14
+
15
+ response = requests.post(
16
+ url=(
17
+ "https://www.google-analytics.com/debug/mp/collect"
18
+ if debug
19
+ else "https://www.google-analytics.com/mp/collect"
20
+ )
21
+ + f"?measurement_id=${measurement_id}&api_secret=${api_secret}",
22
+ json=body["payload"],
23
+ )
24
+
25
+ return JsonResponse(
26
+ data=response.json() if debug else {},
27
+ status=response.status_code,
28
+ )