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 @@
1
+ default_app_config = "common.apps.CommonConfig"
@@ -0,0 +1,66 @@
1
+ from django.conf import settings
2
+
3
+ # Email address to source notifications from
4
+ EMAIL_ADDRESS = getattr(settings, "EMAIL_ADDRESS", "no-reply@codeforlife.education")
5
+
6
+ # Dotdigital authorization details
7
+ DOTDIGITAL_AUTH = getattr(settings, "DOTDIGITAL_AUTH", "")
8
+
9
+ # Dotmailer URLs for adding users to the newsletter address book
10
+ DOTMAILER_CREATE_CONTACT_URL = getattr(settings, "DOTMAILER_CREATE_CONTACT_URL", "")
11
+ DOTMAILER_TEACHER_ADDRESS_BOOK_URL = getattr(settings, "DOTMAILER_TEACHER_ADDRESS_BOOK_URL", "")
12
+ DOTMAILER_STUDENT_ADDRESS_BOOK_URL = getattr(settings, "DOTMAILER_STUDENT_ADDRESS_BOOK_URL", "")
13
+ DOTMAILER_NO_ACCOUNT_ADDRESS_BOOK_URL = getattr(settings, "DOTMAILER_NO_ACCOUNT_ADDRESS_BOOK_URL", "")
14
+
15
+ # Dotmailer username for API authentication
16
+ DOTMAILER_USER = getattr(settings, "DOTMAILER_USER", "")
17
+
18
+ # Dotmailer password for API authentication
19
+ DOTMAILER_PASSWORD = getattr(settings, "DOTMAILER_PASSWORD", "")
20
+
21
+ # Dotmailer default preferences to what users are signed up to
22
+ DOTMAILER_DEFAULT_PREFERENCES = getattr(settings, "DOTMAILER_DEFAULT_PREFERENCES", [])
23
+
24
+ # Dotmailer URL for getting a user by email
25
+ DOTMAILER_GET_USER_BY_EMAIL_URL = getattr(
26
+ settings,
27
+ "DOTMAILER_GET_USER_BY_EMAIL_URL",
28
+ "",
29
+ )
30
+
31
+ # Dotmailer URL for deleting a contact by id
32
+ DOTMAILER_DELETE_USER_BY_ID_URL = getattr(
33
+ settings,
34
+ "DOTMAILER_DELETE_USER_BY_ID_URL",
35
+ "",
36
+ )
37
+
38
+ # Dotmailer URL for adding consent data to a user
39
+ DOTMAILER_PUT_CONSENT_DATA_URL = getattr(settings, "DOTMAILER_PUT_CONSENT_DATA_URL", "")
40
+
41
+ # Dotmailer URL for sending a triggered campaign to a users
42
+ DOTMAILER_SEND_CAMPAIGN_URL = getattr(settings, "DOTMAILER_SEND_CAMPAIGN_URL", "")
43
+
44
+ # ID of the "Thanks for staying!" campaign in Dotmailer
45
+ DOTMAILER_THANKS_FOR_STAYING_CAMPAIGN_ID = getattr(settings, "DOTMAILER_THANKS_FOR_STAYING_CAMPAIGN_ID", "")
46
+
47
+ # Fernet encryption for OAuth2 sign in
48
+ ENCRYPTION_KEY = getattr(settings, "ENCRYPTION_KEY", "")
49
+
50
+ # The name of the google app engine service the application is running on, local otherwise
51
+ MODULE_NAME = getattr(settings, "MODULE_NAME", "local")
52
+
53
+ # Boolean indicating if OneTrust cookie management is enabled or not
54
+ COOKIE_MANAGEMENT_ENABLED = getattr(settings, "COOKIE_MANAGEMENT_ENABLED", True)
55
+
56
+
57
+ def domain():
58
+ """Returns the full domain depending on whether it's local, dev, staging or prod."""
59
+ domain = "https://www.codeforlife.education"
60
+
61
+ if MODULE_NAME == "local":
62
+ domain = "localhost:8000"
63
+ elif MODULE_NAME == "staging" or MODULE_NAME == "dev":
64
+ domain = f"https://{MODULE_NAME}-dot-decent-digit-629.appspot.com"
65
+
66
+ return domain
@@ -0,0 +1,6 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class CommonConfig(AppConfig):
5
+ name = "common"
6
+ verbose_name = "Code for Life Common"
@@ -0,0 +1,9 @@
1
+ from common import app_settings
2
+
3
+
4
+ def module_name(request):
5
+ return {"module_name": app_settings.MODULE_NAME}
6
+
7
+
8
+ def cookie_management_enabled(request):
9
+ return {"cookie_management_enabled": app_settings.COOKIE_MANAGEMENT_ENABLED}
@@ -0,0 +1,85 @@
1
+ """CSP Config"""
2
+
3
+ from .app_settings import domain, MODULE_NAME
4
+
5
+ CSP_DEFAULT_SRC = ("self",)
6
+ CSP_CONNECT_SRC = (
7
+ "'self'",
8
+ "https://*.onetrust.com/",
9
+ "https://api.pwnedpasswords.com",
10
+ "https://euc-widget.freshworks.com/",
11
+ "https://codeforlife.freshdesk.com/",
12
+ "https://api.iconify.design/",
13
+ "https://api.simplesvg.com/",
14
+ "https://api.unisvg.com/",
15
+ "https://www.google-analytics.com/",
16
+ "https://pyodide-cdn2.iodide.io/v0.15.0/full/",
17
+ "https://crowdin.com/",
18
+ )
19
+ CSP_FONT_SRC = ("'self'", "https://fonts.gstatic.com/", "https://fonts.googleapis.com/", "https://use.typekit.net/")
20
+ CSP_SCRIPT_SRC = (
21
+ "'self'",
22
+ "'unsafe-inline'",
23
+ "'unsafe-eval'",
24
+ "https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js",
25
+ "https://cdn.crowdin.com/",
26
+ "https://*.onetrust.com/",
27
+ "https://code.jquery.com/",
28
+ "https://euc-widget.freshworks.com/",
29
+ "https://cdn-ukwest.onetrust.com/",
30
+ "https://code.iconify.design/2/2.0.3/iconify.min.js",
31
+ "https://www.googletagmanager.com/",
32
+ "https://www.recaptcha.net/",
33
+ "https://www.google.com/recaptcha/",
34
+ "https://www.gstatic.com/recaptcha/",
35
+ "https://use.typekit.net/mrl4ieu.js",
36
+ "https://pyodide-cdn2.iodide.io/v0.15.0/full/",
37
+ f"{domain()}/static/portal/",
38
+ f"{domain()}/static/common/",
39
+ )
40
+ CSP_STYLE_SRC = (
41
+ "'self'",
42
+ "'unsafe-inline'",
43
+ "https://euc-widget.freshworks.com/",
44
+ "https://cdn-ukwest.onetrust.com/",
45
+ "https://fonts.googleapis.com/",
46
+ "https://code.jquery.com/ui/1.13.1/themes/base/jquery-ui.css",
47
+ "https://cdn.crowdin.com/",
48
+ f"{domain()}/static/portal/",
49
+ )
50
+ CSP_FRAME_SRC = (
51
+ "https://storage.googleapis.com/",
52
+ "https://2662351606-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/",
53
+ "https://files.gitbook.com/v0/b/gitbook-x-prod.appspot.com/",
54
+ "https://www.recaptcha.net/",
55
+ "https://www.google.com/recaptcha/",
56
+ "https://crowdin.com/",
57
+ f"{domain()}/static/common/img/",
58
+ f"{domain()}/static/game/image/",
59
+ )
60
+ CSP_IMG_SRC = (
61
+ "'self'",
62
+ "https://storage.googleapis.com/codeforlife-assets/images/",
63
+ "https://cdn-ukwest.onetrust.com/",
64
+ "https://p.typekit.net/",
65
+ "https://cdn.crowdin.com/",
66
+ "https://crowdin-static.downloads.crowdin.com/",
67
+ "data:",
68
+ f"{domain()}/static/portal/img/",
69
+ f"{domain()}/static/portal/static/portal/img/",
70
+ f"{domain()}/static/portal/img/",
71
+ f"{domain()}/favicon.ico",
72
+ f"{domain()}/img/",
73
+ f"{domain()}/account/two_factor/qrcode/",
74
+ f"{domain()}/static/",
75
+ f"{domain()}/static/game/image/",
76
+ f"{domain()}/static/game/raphael_image/",
77
+ f"{domain()}/static/game/js/blockly/media/",
78
+ f"{domain()}/static/icons/",
79
+ )
80
+ CSP_OBJECT_SRC = (f"{domain()}/static/common/img/", f"{domain()}/static/game/image/")
81
+ CSP_MEDIA_SRC = (
82
+ "https://files.gitbook.com/v0/b/gitbook-x-prod.appspot.com/",
83
+ f"{domain()}/static/game/sound/",
84
+ f"{domain()}/static/game/js/blockly/media/",
85
+ )
File without changes
@@ -0,0 +1,42 @@
1
+ from pathlib import Path
2
+ from typing import Callable
3
+
4
+ from django.core.management import call_command
5
+ from django.core.serializers import base, python
6
+
7
+
8
+ def load_data_from_file(file_name) -> Callable:
9
+ """Returns a migration function that loads a json file produced by `manage.py dumpdata` into the database.
10
+ For use with migrations.RunPython
11
+
12
+ Args:
13
+ file_name (str): The name of the file containing the data you want to load. Include `.json` at the end.
14
+ The file must be in the fixtures directory.
15
+ """
16
+ absolute_file_path = Path(__file__).resolve().parent.parent / "fixtures" / file_name
17
+
18
+ def _load_fixture(apps, schema_editor):
19
+ # Save the default _get_model() function
20
+ default_get_model = python._get_model
21
+
22
+ # Define new _get_model() function here, which utilizes the apps argument to
23
+ # get the historical version of a model. This piece of code is directly taken
24
+ # from django.core.serializers.python._get_model, unchanged. However, here it
25
+ # has a different context, specifically, the apps variable.
26
+ def _get_model(model_identifier):
27
+ try:
28
+ return apps.get_model(model_identifier)
29
+ except (LookupError, TypeError):
30
+ raise base.DeserializationError("Invalid model identifier: '%s'" % model_identifier)
31
+
32
+ # Replace the _get_model() function on the module, so loaddata can utilize it.
33
+ python._get_model = _get_model
34
+
35
+ try:
36
+ # Call loaddata command
37
+ call_command("loaddata", absolute_file_path, app_label="common")
38
+ finally:
39
+ # Restore default _get_model() function
40
+ python._get_model = default_get_model
41
+
42
+ return _load_fixture
@@ -0,0 +1,393 @@
1
+ import datetime
2
+ import json
3
+ from enum import Enum, auto
4
+ from uuid import uuid4
5
+
6
+ import jwt
7
+ from common import app_settings
8
+ from common.mail import (
9
+ address_book_ids,
10
+ campaign_ids,
11
+ django_send_email,
12
+ send_dotdigital_email,
13
+ )
14
+ from common.models import Student, Teacher
15
+ from django.conf import settings
16
+ from django.contrib.auth.models import User
17
+ from django.http import HttpResponse
18
+ from django.urls import reverse
19
+ from django.utils import timezone
20
+ from requests import delete, get, post, put
21
+ from requests.exceptions import RequestException
22
+
23
+ NOTIFICATION_EMAIL = (
24
+ "Code For Life Notification <" + app_settings.EMAIL_ADDRESS + ">"
25
+ )
26
+ VERIFICATION_EMAIL = (
27
+ "Code For Life Verification <" + app_settings.EMAIL_ADDRESS + ">"
28
+ )
29
+ PASSWORD_RESET_EMAIL = (
30
+ "Code For Life Password Reset <" + app_settings.EMAIL_ADDRESS + ">"
31
+ )
32
+ INVITE_FROM = "Code For Life Invitation <" + app_settings.EMAIL_ADDRESS + ">"
33
+
34
+
35
+ class DotmailerUserType(Enum):
36
+ TEACHER = auto()
37
+ STUDENT = auto()
38
+ NO_ACCOUNT = auto()
39
+
40
+
41
+ def generate_token(user, new_email="", preverified=False):
42
+ if preverified:
43
+ user.userprofile.is_verified = preverified
44
+ user.userprofile.save()
45
+
46
+ return generate_token_for_email(user.email, new_email)
47
+
48
+
49
+ def generate_token_for_email(email: str, new_email: str = ""):
50
+ return jwt.encode(
51
+ {
52
+ "email": email,
53
+ "new_email": new_email,
54
+ "email_verification_token": uuid4().hex[:30],
55
+ "expires": (
56
+ timezone.now() + datetime.timedelta(hours=1)
57
+ ).timestamp(),
58
+ },
59
+ settings.SECRET_KEY,
60
+ algorithm="HS256",
61
+ )
62
+
63
+
64
+ def _newsletter_ticked(data):
65
+ return "newsletter_ticked" in data and data["newsletter_ticked"]
66
+
67
+
68
+ def send_email(
69
+ sender,
70
+ recipients,
71
+ subject,
72
+ text_content,
73
+ title,
74
+ replace_url=None,
75
+ plaintext_template="email.txt",
76
+ html_template="email.html",
77
+ ):
78
+ django_send_email(
79
+ sender,
80
+ recipients,
81
+ subject,
82
+ text_content,
83
+ title,
84
+ replace_url,
85
+ plaintext_template,
86
+ html_template,
87
+ )
88
+
89
+
90
+ def send_verification_email(
91
+ request, user, data, new_email=None, age=None, school=None
92
+ ):
93
+ """
94
+ Sends emails relating to email address verification.
95
+
96
+ On registration:
97
+ - if the user is under 13, send a verification email addressed to the parent / guardian
98
+ - if the user is over 13, send a regular verification email
99
+ - if the user is a student who just got released, send a verification email explaining the situation
100
+ - if the user is a student who has requested to sign up to the newsletter, handle their Dotmailer subscription
101
+
102
+ On email address update:
103
+ - sends an email to the old address alerting the user that an email change request has occurred
104
+ - sends an email to the new address requesting email address verification
105
+
106
+ :param request: The Django form request
107
+ :param user: The user object concerned with the email verification
108
+ :param data: The data inputted by the user in the Django form.
109
+ :param new_email: New email the user wants to associate to their account - if not provided, it means the user is
110
+ registering a new account
111
+ :param age: The user's age (collected only for the purposes of this function and if the user is an independent
112
+ student)
113
+ """
114
+
115
+ # verifying first email address (registration or unverified login attempt)
116
+ if not new_email:
117
+ verification = generate_token(user)
118
+
119
+ if age is None:
120
+ # if the user is a released student
121
+ if hasattr(user, "new_student") and school is not None:
122
+ url = f"{request.build_absolute_uri(reverse('verify_email', kwargs={'token': verification}))}"
123
+
124
+ send_dotdigital_email(
125
+ campaign_ids["verify_released_student"],
126
+ [user.email],
127
+ personalization_values={
128
+ "VERIFICATION_LINK": url,
129
+ "SCHOOL_NAME": school.name,
130
+ },
131
+ )
132
+ else:
133
+ url = f"{request.build_absolute_uri(reverse('verify_email', kwargs={'token': verification}))}"
134
+
135
+ send_dotdigital_email(
136
+ campaign_ids["verify_new_user"],
137
+ [user.email],
138
+ personalization_values={"VERIFICATION_LINK": url},
139
+ )
140
+
141
+ if _newsletter_ticked(data):
142
+ add_to_dotmailer(
143
+ user.first_name,
144
+ user.last_name,
145
+ user.email,
146
+ address_book_ids["newsletter"],
147
+ DotmailerUserType.TEACHER,
148
+ )
149
+ # if the user is an independent student
150
+ else:
151
+ if age < 13:
152
+ url = f"{request.build_absolute_uri(reverse('verify_email', kwargs={'token': verification}))}"
153
+ send_dotdigital_email(
154
+ campaign_ids["verify_new_user_via_parent"],
155
+ [user.email],
156
+ personalization_values={
157
+ "FIRST_NAME": user.first_name,
158
+ "ACTIVATION_LINK": url,
159
+ },
160
+ )
161
+ else:
162
+ url = f"{request.build_absolute_uri(reverse('verify_email', kwargs={'token': verification}))}"
163
+ send_dotdigital_email(
164
+ campaign_ids["verify_new_user"],
165
+ [user.email],
166
+ personalization_values={"VERIFICATION_LINK": url},
167
+ )
168
+
169
+ if _newsletter_ticked(data):
170
+ add_to_dotmailer(
171
+ user.first_name,
172
+ user.last_name,
173
+ user.email,
174
+ address_book_ids["newsletter"],
175
+ DotmailerUserType.STUDENT,
176
+ )
177
+ # verifying change of email address.
178
+ else:
179
+ verification = generate_token(user, new_email)
180
+ url = f"{request.build_absolute_uri(reverse('verify_email', kwargs={'token': verification}))}"
181
+ send_dotdigital_email(
182
+ campaign_ids["email_change_verification"],
183
+ [new_email],
184
+ personalization_values={"VERIFICATION_LINK": url},
185
+ )
186
+
187
+
188
+ def add_to_dotmailer(
189
+ first_name: str,
190
+ last_name: str,
191
+ email: str,
192
+ address_book_id: int,
193
+ user_type: DotmailerUserType = None,
194
+ ):
195
+ try:
196
+ create_contact(first_name, last_name, email)
197
+ add_contact_to_address_book(
198
+ first_name, last_name, email, address_book_id, user_type
199
+ )
200
+ except RequestException:
201
+ return HttpResponse(status=404)
202
+
203
+
204
+ def create_contact(first_name, last_name, email):
205
+ url = app_settings.DOTMAILER_CREATE_CONTACT_URL
206
+ body = {
207
+ "contact": {
208
+ "email": email,
209
+ "optInType": "VerifiedDouble",
210
+ "emailType": "Html",
211
+ "dataFields": [
212
+ {"key": "FIRSTNAME", "value": first_name},
213
+ {"key": "LASTNAME", "value": last_name},
214
+ {"key": "FULLNAME", "value": f"{first_name} {last_name}"},
215
+ ],
216
+ },
217
+ "consentFields": [
218
+ {
219
+ "fields": [
220
+ {
221
+ "key": "DATETIMECONSENTED",
222
+ "value": datetime.datetime.now().__str__(),
223
+ }
224
+ ]
225
+ }
226
+ ],
227
+ "preferences": app_settings.DOTMAILER_DEFAULT_PREFERENCES,
228
+ }
229
+
230
+ post(
231
+ url,
232
+ json=body,
233
+ auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD),
234
+ )
235
+
236
+
237
+ def add_contact_to_address_book(
238
+ first_name: str,
239
+ last_name: str,
240
+ email: str,
241
+ address_book_id: int,
242
+ user_type: DotmailerUserType = None,
243
+ ):
244
+ main_address_book_url = f"https://r1-api.dotmailer.com/v2/address-books/{address_book_id}/contacts"
245
+
246
+ body = {
247
+ "email": email,
248
+ "optInType": "VerifiedDouble",
249
+ "emailType": "Html",
250
+ "dataFields": [
251
+ {"key": "FIRSTNAME", "value": first_name},
252
+ {"key": "LASTNAME", "value": last_name},
253
+ {"key": "FULLNAME", "value": f"{first_name} {last_name}"},
254
+ ],
255
+ }
256
+
257
+ post(
258
+ main_address_book_url,
259
+ json=body,
260
+ auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD),
261
+ )
262
+
263
+ if user_type is not None:
264
+ specific_address_book_url = (
265
+ app_settings.DOTMAILER_NO_ACCOUNT_ADDRESS_BOOK_URL
266
+ )
267
+
268
+ if user_type == DotmailerUserType.TEACHER:
269
+ specific_address_book_url = (
270
+ app_settings.DOTMAILER_TEACHER_ADDRESS_BOOK_URL
271
+ )
272
+ elif user_type == DotmailerUserType.STUDENT:
273
+ specific_address_book_url = (
274
+ app_settings.DOTMAILER_STUDENT_ADDRESS_BOOK_URL
275
+ )
276
+
277
+ post(
278
+ specific_address_book_url,
279
+ json=body,
280
+ auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD),
281
+ )
282
+
283
+
284
+ def delete_contact(email: str):
285
+ try:
286
+ user = get_dotmailer_user_by_email(email)
287
+ user_id = user.get("id")
288
+ if user_id:
289
+ url = app_settings.DOTMAILER_DELETE_USER_BY_ID_URL.replace(
290
+ "ID", str(user_id)
291
+ )
292
+ delete(
293
+ url,
294
+ auth=(
295
+ app_settings.DOTMAILER_USER,
296
+ app_settings.DOTMAILER_PASSWORD,
297
+ ),
298
+ )
299
+ except RequestException:
300
+ return HttpResponse(status=404)
301
+
302
+
303
+ def get_dotmailer_user_by_email(email):
304
+ url = app_settings.DOTMAILER_GET_USER_BY_EMAIL_URL.replace("EMAIL", email)
305
+
306
+ response = get(
307
+ url, auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD)
308
+ )
309
+
310
+ return json.loads(response.content)
311
+
312
+
313
+ def add_consent_record_to_dotmailer_user(user):
314
+ consent_date_time = datetime.datetime.now().__str__()
315
+
316
+ url = app_settings.DOTMAILER_PUT_CONSENT_DATA_URL.replace(
317
+ "USER_ID", str(user["id"])
318
+ )
319
+ body = {
320
+ "contact": {
321
+ "email": user["email"],
322
+ "optInType": user["optInType"],
323
+ "emailType": user["emailType"],
324
+ "dataFields": user["dataFields"],
325
+ },
326
+ "consentFields": [
327
+ {
328
+ "fields": [
329
+ {"key": "DATETIMECONSENTED", "value": consent_date_time}
330
+ ]
331
+ }
332
+ ],
333
+ }
334
+
335
+ put(
336
+ url,
337
+ json=body,
338
+ auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD),
339
+ )
340
+
341
+
342
+ def send_dotmailer_consent_confirmation_email_to_user(user):
343
+ url = app_settings.DOTMAILER_SEND_CAMPAIGN_URL
344
+ campaign_id = app_settings.DOTMAILER_THANKS_FOR_STAYING_CAMPAIGN_ID
345
+ body = {"campaignID": campaign_id, "contactIds": [str(user["id"])]}
346
+
347
+ post(
348
+ url,
349
+ json=body,
350
+ auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD),
351
+ )
352
+
353
+
354
+ def update_indy_email(user, request, data):
355
+ changing_email = False
356
+ new_email = data["email"]
357
+
358
+ if new_email != "" and new_email != user.email:
359
+ changing_email = True
360
+ users_with_email = User.objects.filter(email=new_email)
361
+
362
+ send_dotdigital_email(
363
+ campaign_ids["email_change_notification"],
364
+ [user.email],
365
+ personalization_values={"NEW_EMAIL_ADDRESS": new_email},
366
+ )
367
+
368
+ # email is available
369
+ if not users_with_email.exists():
370
+ # new email to set and verify
371
+ send_verification_email(request, user, data, new_email)
372
+ return changing_email, new_email
373
+
374
+
375
+ def update_email(user: Teacher or Student, request, data):
376
+ changing_email = False
377
+ new_email = data["email"]
378
+
379
+ if new_email != "" and new_email != user.new_user.email:
380
+ changing_email = True
381
+ users_with_email = User.objects.filter(email=new_email)
382
+
383
+ send_dotdigital_email(
384
+ campaign_ids["email_change_notification"],
385
+ [user.new_user.email],
386
+ personalization_values={"NEW_EMAIL_ADDRESS": new_email},
387
+ )
388
+
389
+ # email is available
390
+ if not users_with_email.exists():
391
+ # new email to set and verify
392
+ send_verification_email(request, user.new_user, data, new_email)
393
+ return changing_email, new_email
@@ -0,0 +1,52 @@
1
+ import hashlib
2
+ import random
3
+ import string
4
+ from builtins import range, str
5
+ from uuid import uuid4
6
+
7
+ from common.models import Class, Student
8
+ from django.contrib.auth.models import User
9
+
10
+
11
+ def get_random_username():
12
+ while True:
13
+ random_username = uuid4().hex[:30] # generate a random username
14
+ if not User.objects.filter(username=random_username).exists():
15
+ return random_username
16
+
17
+
18
+ def generate_new_student_name(orig_name):
19
+ if not Student.objects.filter(new_user__username=orig_name).exists():
20
+ return orig_name
21
+
22
+ i = 1
23
+ while True:
24
+ new_name = orig_name + str(i)
25
+ if not Student.objects.filter(new_user__username=new_name).exists():
26
+ return new_name
27
+ i += 1
28
+
29
+
30
+ def generate_access_code():
31
+ while True:
32
+ access_code = "".join(random.choice(string.ascii_uppercase) for _ in range(5))
33
+
34
+ if not Class.objects.filter(access_code=access_code).exists():
35
+ return access_code
36
+
37
+
38
+ def generate_password(length):
39
+ return "".join(random.choice(string.ascii_lowercase) for _ in range(length))
40
+
41
+
42
+ def generate_login_id():
43
+ """Returns the uuid string and its hashed.
44
+ The string is used for URL, and the hashed is stored in the DB."""
45
+ login_id = uuid4().hex
46
+ hashed_login_id = get_hashed_login_id(login_id)
47
+ return login_id, hashed_login_id
48
+
49
+
50
+ def get_hashed_login_id(login_id):
51
+ """Returns the hash of a given string used for login url"""
52
+ return hashlib.sha256(login_id.encode()).hexdigest()
@@ -0,0 +1,10 @@
1
+ # TODO: Move to Address model once we create it
2
+ def sanitise_uk_postcode(postcode):
3
+ if len(postcode) >= 5: # Valid UK postcodes are at least 5 chars long
4
+ outcode = postcode[:-3] # UK incodes are always 3 characters
5
+
6
+ # Insert a space between outcode and incode if there isn't already one
7
+ if not outcode.endswith(" "):
8
+ postcode = postcode[:-3] + " " + postcode[-3:]
9
+
10
+ return postcode