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,10 +1,12 @@
1
1
  from __future__ import absolute_import
2
2
 
3
3
  import time
4
- import re
4
+ from datetime import timedelta
5
+ from unittest.mock import ANY, Mock, patch
6
+ from uuid import uuid4
5
7
 
6
- from aimmo.models import Game
7
- from common.models import Class, Student, Teacher
8
+ import jwt
9
+ from common.mail import campaign_ids
8
10
  from common.tests.utils import email as email_utils
9
11
  from common.tests.utils.classes import create_class_directly
10
12
  from common.tests.utils.organisation import (
@@ -21,284 +23,29 @@ from common.tests.utils.teacher import (
21
23
  signup_teacher_directly,
22
24
  verify_email,
23
25
  )
26
+ from django.conf import settings
24
27
  from django.core import mail
25
28
  from django.test import Client, TestCase
26
29
  from django.urls import reverse
30
+ from django.utils import timezone
31
+ from selenium.webdriver.common.by import By
32
+ from selenium.webdriver.support import expected_conditions as EC
27
33
  from selenium.webdriver.support.wait import WebDriverWait
28
34
 
29
35
  from portal.forms.error_messages import INVALID_LOGIN_MESSAGE
36
+ from portal.tests.test_invite_teacher import WAIT_TIME
30
37
  from .base_test import BaseTest
31
38
  from .pageObjects.portal.home_page import HomePage
32
39
  from .utils.messages import (
33
- is_email_verified_message_showing,
34
- is_teacher_details_updated_message_showing,
35
40
  is_email_updated_message_showing,
41
+ is_email_verified_message_showing,
42
+ is_message_showing,
36
43
  is_password_updated_message_showing,
44
+ is_teacher_details_updated_message_showing,
37
45
  )
38
46
 
39
47
 
40
48
  class TestTeacher(TestCase):
41
- def test_new_student_can_play_games(self):
42
- """
43
- Given a teacher has an kurono game,
44
- When they add a new student to their class,
45
- Then the new student should be able to play that class's games
46
- """
47
- email, password = signup_teacher_directly()
48
- create_organisation_directly(email)
49
- klass, _, access_code = create_class_directly(email)
50
- create_school_student_directly(access_code)
51
-
52
- c = Client()
53
- c.login(username=email, password=password)
54
- c.post(
55
- reverse("teacher_aimmo_dashboard"),
56
- {"game_class": klass.id},
57
- )
58
- c.post(
59
- reverse("view_class", kwargs={"access_code": access_code}),
60
- {"names": "Florian"},
61
- )
62
-
63
- game = Game.objects.get(id=1)
64
- new_student = Student.objects.last()
65
- assert game.can_user_play(new_student.new_user)
66
-
67
- def test_accepted_independent_student_can_play_games(self):
68
- """
69
- Given an independent student requests access to a class,
70
- When the teacher for that class accepts the request,
71
- Then the new student should have access to that class's games
72
- """
73
- email, password = signup_teacher_directly()
74
- create_organisation_directly(email)
75
- klass, _, access_code = create_class_directly(email)
76
- klass.always_accept_requests = True
77
- klass.save()
78
- create_school_student_directly(access_code)
79
- (
80
- indep_username,
81
- indep_password,
82
- indep_student,
83
- ) = create_independent_student_directly()
84
-
85
- c = Client()
86
-
87
- c.login(username=indep_username, password=indep_password)
88
- c.post(
89
- reverse("student_join_organisation"),
90
- {"access_code": access_code, "class_join_request": "Request"},
91
- )
92
- c.logout()
93
-
94
- c.login(username=email, password=password)
95
- c.post(
96
- reverse("teacher_aimmo_dashboard"),
97
- {"game_class": klass.pk},
98
- )
99
- c.post(
100
- reverse("teacher_accept_student_request", kwargs={"pk": indep_student.pk}),
101
- {"name": "Florian"},
102
- )
103
-
104
- game: Game = Game.objects.get(id=1)
105
- new_student = Student.objects.last()
106
- assert game.can_user_play(new_student.new_user)
107
-
108
- def test_moved_class_has_correct_permissions_for_students_and_teachers(self):
109
- """
110
- Given two teachers each with a class and an aimmo game,
111
- When teacher 1 transfers their class to teacher 2,
112
- Then:
113
- - Students in each class still only have access to their class games
114
- - Teacher 2 has access to both games and teacher 1 has access to none
115
- """
116
-
117
- # Create teacher 1 -> class 1 -> student 1
118
- email1, password1 = signup_teacher_directly()
119
- school_name, postcode = create_organisation_directly(email1)
120
- klass1, _, access_code1 = create_class_directly(email1, "Class 1")
121
- create_school_student_directly(access_code1)
122
-
123
- # Create teacher 2 -> class 2 -> student 2
124
- email2, password2 = signup_teacher_directly()
125
- join_teacher_to_organisation(email2, school_name, postcode)
126
- klass2, _, access_code2 = create_class_directly(email2, "Class 2")
127
- create_school_student_directly(access_code2)
128
-
129
- teacher1: Teacher = Teacher.objects.get(new_user__email=email1)
130
- teacher2: Teacher = Teacher.objects.get(new_user__email=email2)
131
-
132
- c = Client()
133
-
134
- # Create game 1 under class 1
135
- c.login(username=email1, password=password1)
136
- c.post(
137
- reverse("teacher_aimmo_dashboard"),
138
- {"game_class": klass1.pk},
139
- )
140
- c.logout()
141
-
142
- # Create game 2 under class 2
143
- c.login(username=email2, password=password2)
144
- c.post(
145
- reverse("teacher_aimmo_dashboard"),
146
- {"game_class": klass2.pk},
147
- )
148
- c.logout()
149
-
150
- game1: Game = Game.objects.get(owner=teacher1.new_user)
151
- game2: Game = Game.objects.get(owner=teacher2.new_user)
152
-
153
- student1: Student = Student.objects.get(class_field=klass1)
154
- student2: Student = Student.objects.get(class_field=klass2)
155
-
156
- # Check student permissions for each game
157
- assert game1.can_user_play(student1.new_user)
158
- assert game2.can_user_play(student2.new_user)
159
- assert not game1.can_user_play(student2.new_user)
160
- assert not game2.can_user_play(student1.new_user)
161
-
162
- # Check teacher permissions for each game
163
- assert game1.can_user_play(teacher1.new_user)
164
- assert game2.can_user_play(teacher2.new_user)
165
- assert not game1.can_user_play(teacher2.new_user)
166
- assert not game2.can_user_play(teacher1.new_user)
167
-
168
- # Transfer class 1 over to teacher 2
169
- c.login(username=email1, password=password1)
170
- response = c.post(
171
- reverse("teacher_edit_class", kwargs={"access_code": access_code1}),
172
- {"new_teacher": teacher2.pk, "class_move_submit": ""},
173
- )
174
- assert response.status_code == 302
175
- c.logout()
176
-
177
- # Refresh model instances
178
- klass1: Class = Class.objects.get(pk=klass1.pk)
179
- klass2: Class = Class.objects.get(pk=klass2.pk)
180
- game1 = Game.objects.get(pk=game1.pk)
181
- game2 = Game.objects.get(pk=game2.pk)
182
-
183
- # Check teacher 2 is the teacher for class 1
184
- assert klass1.teacher == teacher2
185
-
186
- # Check that the students' permissions have not changed
187
- assert game1.can_user_play(student1.new_user)
188
- assert game2.can_user_play(student2.new_user)
189
- assert not game1.can_user_play(student2.new_user)
190
- assert not game2.can_user_play(student1.new_user)
191
-
192
- # Check that teacher 1 cannot access class 1's game 1 anymore
193
- assert not game1.can_user_play(teacher1.new_user)
194
-
195
- # Check that teacher 2 can access game 1
196
- assert game1.can_user_play(teacher2.new_user)
197
-
198
- def test_moved_student_has_access_to_only_new_teacher_games(self):
199
- """
200
- Given a student in a class,
201
- When a teacher transfers them to another class with a new teacher,
202
- Then the student should only have access to the new teacher's games
203
- """
204
-
205
- email1, password1 = signup_teacher_directly()
206
- school_name, postcode = create_organisation_directly(email1)
207
- klass1, _, access_code1 = create_class_directly(email1, "Class 1")
208
- create_school_student_directly(access_code1)
209
-
210
- email2, password2 = signup_teacher_directly()
211
- join_teacher_to_organisation(email2, school_name, postcode)
212
- klass2, _, access_code2 = create_class_directly(email2, "Class 2")
213
- create_school_student_directly(access_code2)
214
-
215
- teacher1 = Teacher.objects.get(new_user__email=email1)
216
- teacher2 = Teacher.objects.get(new_user__email=email2)
217
-
218
- c = Client()
219
- c.login(username=email2, password=password2)
220
- c.post(
221
- reverse("teacher_aimmo_dashboard"),
222
- {"game_class": klass2.pk},
223
- )
224
- c.logout()
225
-
226
- c.login(username=email1, password=password1)
227
- c.post(
228
- reverse("teacher_aimmo_dashboard"),
229
- {"game_class": klass1.pk},
230
- )
231
-
232
- game1 = Game.objects.get(owner=teacher1.new_user)
233
- game2 = Game.objects.get(owner=teacher2.new_user)
234
-
235
- student1 = Student.objects.get(class_field=klass1)
236
- student2 = Student.objects.get(class_field=klass2)
237
-
238
- assert game1.can_user_play(student1.new_user)
239
- assert game2.can_user_play(student2.new_user)
240
-
241
- c.post(
242
- reverse("teacher_move_students", kwargs={"access_code": access_code1}),
243
- {"transfer_students": student1.pk},
244
- )
245
- c.post(
246
- reverse(
247
- "teacher_move_students_to_class", kwargs={"access_code": access_code1}
248
- ),
249
- {
250
- "form-0-name": student1.user.user.first_name,
251
- "form-MAX_NUM_FORMS": 1000,
252
- "form-0-orig_name": student1.user.user.first_name,
253
- "form-TOTAL_FORMS": 1,
254
- "form-MIN_NUM_FORMS": 0,
255
- "submit_disambiguation": "",
256
- "form-INITIAL_FORMS": 1,
257
- "new_class": klass2.pk,
258
- },
259
- )
260
- c.logout()
261
-
262
- game1 = Game.objects.get(owner=teacher1.new_user)
263
- game2 = Game.objects.get(owner=teacher2.new_user)
264
-
265
- assert not game1.can_user_play(student1.new_user)
266
- assert game2.can_user_play(student1.new_user)
267
-
268
- def test_teacher_cannot_create_duplicate_game(self):
269
- """
270
- Given a teacher, a class and a worksheet,
271
- When the teacher creates a game for that class and worksheet, and then tries to
272
- create the exact same game again,
273
- Then the class should only have one game, and an error message should appear.
274
- """
275
-
276
- email, password = signup_teacher_directly()
277
- _, _ = create_organisation_directly(email)
278
- klass, _, _ = create_class_directly(email)
279
-
280
- c = Client()
281
- c.login(username=email, password=password)
282
- game1_response = c.post(
283
- reverse("teacher_aimmo_dashboard"),
284
- {"game_class": klass.pk},
285
- )
286
-
287
- assert game1_response.status_code == 302
288
- assert Game.objects.filter(game_class=klass, is_archived=False).count() == 1
289
- assert klass.active_game != None
290
- messages = list(game1_response.wsgi_request._messages)
291
- assert len([m for m in messages if m.tags == "warning"]) == 0
292
-
293
- game2_response = c.post(
294
- reverse("teacher_aimmo_dashboard"),
295
- {"game_class": klass.pk},
296
- )
297
-
298
- messages = list(game2_response.wsgi_request._messages)
299
- assert len([m for m in messages if m.tags == "warning"]) == 1
300
- assert messages[0].message == "An active game already exists for this class"
301
-
302
49
  def test_signup_short_password_fails(self):
303
50
  c = Client()
304
51
 
@@ -308,6 +55,7 @@ class TestTeacher(TestCase):
308
55
  "teacher_signup-teacher_first_name": "Test Name",
309
56
  "teacher_signup-teacher_last_name": "Test Last Name",
310
57
  "teacher_signup-teacher_email": "test@email.com",
58
+ "teacher_signup-consent_ticked": "on",
311
59
  "teacher_signup-teacher_password": "test",
312
60
  "teacher_signup-teacher_confirm_password": "test",
313
61
  "g-recaptcha-response": "something",
@@ -326,6 +74,7 @@ class TestTeacher(TestCase):
326
74
  "teacher_signup-teacher_first_name": "Test Name",
327
75
  "teacher_signup-teacher_last_name": "Test Last Name",
328
76
  "teacher_signup-teacher_email": "test@email.com",
77
+ "teacher_signup-consent_ticked": "on",
329
78
  "teacher_signup-teacher_password": "Password1",
330
79
  "teacher_signup-teacher_confirm_password": "Password1",
331
80
  "g-recaptcha-response": "something",
@@ -335,6 +84,22 @@ class TestTeacher(TestCase):
335
84
  # Assert response isn't a redirect (submit failure)
336
85
  assert response.status_code == 200
337
86
 
87
+ response = c.post(
88
+ reverse("register"),
89
+ {
90
+ "independent_student_signup-date_of_birth_day": 7,
91
+ "independent_student_signup-date_of_birth_month": 10,
92
+ "independent_student_signup-date_of_birth_year": 1997,
93
+ "independent_student_signup-name": "Test Name",
94
+ "independent_student_signup-email": "test@email.com",
95
+ "independent_student_signup-consent_ticked": "on",
96
+ "independent_student_signup-password": "Password123$",
97
+ "independent_student_signup-confirm_password": "Password123$",
98
+ "g-recaptcha-response": "something",
99
+ },
100
+ )
101
+ assert response.status_code == 200
102
+
338
103
  def test_signup_passwords_do_not_match_fails(self):
339
104
  c = Client()
340
105
 
@@ -344,6 +109,7 @@ class TestTeacher(TestCase):
344
109
  "teacher_signup-teacher_first_name": "Test Name",
345
110
  "teacher_signup-teacher_last_name": "Test Last Name",
346
111
  "teacher_signup-teacher_email": "test@email.com",
112
+ "teacher_signup-consent_ticked": "on",
347
113
  "teacher_signup-teacher_password": "StrongPassword1!",
348
114
  "teacher_signup-teacher_confirm_password": "StrongPassword2!",
349
115
  "g-recaptcha-response": "something",
@@ -353,7 +119,7 @@ class TestTeacher(TestCase):
353
119
  # Assert response isn't a redirect (submit failure)
354
120
  assert response.status_code == 200
355
121
 
356
- def test_signup_email_verification(self):
122
+ def test_signup_fails_without_consent(self):
357
123
  c = Client()
358
124
 
359
125
  response = c.post(
@@ -368,19 +134,52 @@ class TestTeacher(TestCase):
368
134
  },
369
135
  )
370
136
 
137
+ # Assert response isn't a redirect (submit failure)
138
+ assert response.status_code == 200
139
+
140
+ @patch("common.helpers.emails.send_dotdigital_email")
141
+ def test_signup_email_verification(self, mock_send_dotdigital_email: Mock):
142
+ c = Client()
143
+
144
+ response = c.post(
145
+ reverse("register"),
146
+ {
147
+ "teacher_signup-teacher_first_name": "Test Name",
148
+ "teacher_signup-teacher_last_name": "Test Last Name",
149
+ "teacher_signup-teacher_email": "test@email.com",
150
+ "teacher_signup-consent_ticked": "on",
151
+ "teacher_signup-teacher_password": "czYuH)g0FbD_5E9/",
152
+ "teacher_signup-teacher_confirm_password": "czYuH)g0FbD_5E9/",
153
+ "g-recaptcha-response": "something",
154
+ },
155
+ )
156
+
371
157
  assert response.status_code == 302
372
- assert len(mail.outbox) == 1
158
+ mock_send_dotdigital_email.assert_called_once_with(
159
+ campaign_ids["verify_new_user"], ANY, personalization_values=ANY
160
+ )
373
161
 
374
162
  # Try verification URL with a fake token
375
- bad_url = reverse("verify_email", kwargs={"token": "abcdef"})
163
+ fake_token = jwt.encode(
164
+ {
165
+ "email": "fake_email",
166
+ "new_email": "",
167
+ "email_verification_token": uuid4().hex[:30],
168
+ "expires": (timezone.now() + timedelta(hours=1)).timestamp(),
169
+ },
170
+ settings.SECRET_KEY,
171
+ algorithm="HS256",
172
+ )
173
+ bad_url = reverse("verify_email", kwargs={"token": fake_token})
376
174
  bad_verification_response = c.get(bad_url)
377
175
 
378
176
  # Assert response isn't a redirect (get failure)
379
177
  assert bad_verification_response.status_code == 200
380
178
 
381
- # Get verification link from email
382
- message = str(mail.outbox[0].body)
383
- verification_url = re.search("http.+/", message).group(0)
179
+ # Get verification link from function call
180
+ verification_url = mock_send_dotdigital_email.call_args.kwargs[
181
+ "personalization_values"
182
+ ]["VERIFICATION_LINK"]
384
183
 
385
184
  # Verify the email properly
386
185
  verification_response = c.get(verification_url)
@@ -397,6 +196,28 @@ class TestTeacher(TestCase):
397
196
 
398
197
  # Class for Selenium tests. We plan to replace these and turn them into Cypress tests
399
198
  class TestTeacherFrontend(BaseTest):
199
+ def test_password_too_common(self):
200
+ self.selenium.get(self.live_server_url)
201
+ page = HomePage(self.selenium).go_to_signup_page()
202
+ page = page.signup(
203
+ "first_name",
204
+ "last_name",
205
+ "e@ma.il",
206
+ "Password123$",
207
+ "Password123$",
208
+ success=False,
209
+ )
210
+ try:
211
+ submit_button = WebDriverWait(self.selenium, 10).until(
212
+ EC.element_to_be_clickable((By.NAME, "teacher_signup"))
213
+ )
214
+ submit_button.click()
215
+ except:
216
+ assert page.was_form_invalid(
217
+ "form-reg-teacher",
218
+ "Password is too common, consider using a different password.",
219
+ )
220
+
400
221
  def test_signup_without_newsletter(self):
401
222
  self.selenium.get(self.live_server_url)
402
223
  page = HomePage(self.selenium)
@@ -432,7 +253,9 @@ class TestTeacherFrontend(BaseTest):
432
253
  page = page.login_failure(
433
254
  "non-existent-email@codeforlife.com", "Incorrect password"
434
255
  )
435
- assert page.has_login_failed("form-login-teacher", INVALID_LOGIN_MESSAGE)
256
+ assert page.has_login_failed(
257
+ "form-login-teacher", INVALID_LOGIN_MESSAGE
258
+ )
436
259
 
437
260
  def test_login_success(self):
438
261
  email, password = signup_teacher_directly()
@@ -445,7 +268,8 @@ class TestTeacherFrontend(BaseTest):
445
268
  page = page.login(email, password)
446
269
  assert self.is_dashboard_page(page)
447
270
 
448
- def test_login_not_verified(self):
271
+ @patch("common.helpers.emails.send_dotdigital_email")
272
+ def test_login_not_verified(self, mock_send_dotdigital_email):
449
273
  email, password = signup_teacher_directly(preverified=False)
450
274
  create_organisation_directly(email)
451
275
  _, _, access_code = create_class_directly(email)
@@ -455,9 +279,15 @@ class TestTeacherFrontend(BaseTest):
455
279
  page = page.go_to_teacher_login_page()
456
280
  page = page.login_failure(email, password)
457
281
 
458
- assert page.has_login_failed("form-login-teacher", INVALID_LOGIN_MESSAGE)
282
+ assert page.has_login_failed(
283
+ "form-login-teacher", INVALID_LOGIN_MESSAGE
284
+ )
285
+
286
+ verification_url = mock_send_dotdigital_email.call_args.kwargs[
287
+ "personalization_values"
288
+ ]["VERIFICATION_LINK"]
459
289
 
460
- verify_email(page)
290
+ verify_email(page, verification_url)
461
291
 
462
292
  assert is_email_verified_message_showing(self.selenium)
463
293
 
@@ -472,26 +302,6 @@ class TestTeacherFrontend(BaseTest):
472
302
  page = page.login_no_school(email, password)
473
303
  assert self.is_onboarding_page(page)
474
304
 
475
- def test_view_resources(self):
476
- email, password = signup_teacher_directly()
477
- create_organisation_directly(email)
478
- _, _, access_code = create_class_directly(email)
479
- create_school_student_directly(access_code)
480
- self.selenium.get(self.live_server_url)
481
- page = HomePage(self.selenium)
482
- page = page.go_to_teacher_login_page()
483
- page = page.login(email, password)
484
-
485
- assert self.is_dashboard_page(page)
486
-
487
- page = page.go_to_rapid_router_resources_page()
488
-
489
- assert self.is_resources_page(page)
490
-
491
- page = page.go_to_kurono_resources_page()
492
-
493
- assert self.is_resources_page(page)
494
-
495
305
  def test_edit_details(self):
496
306
  email, password = signup_teacher_directly()
497
307
  create_organisation_directly(email)
@@ -510,7 +320,7 @@ class TestTeacherFrontend(BaseTest):
510
320
  {
511
321
  "first_name": "Paulina",
512
322
  "last_name": "Koch",
513
- "current_password": "Password2!",
323
+ "current_password": "$RFVBGT%6yhn$RFVBGT%6yhn$RFVBGT%6yhn$RFVBGT%6yhn",
514
324
  }
515
325
  )
516
326
  assert self.is_dashboard_page(page)
@@ -523,10 +333,10 @@ class TestTeacherFrontend(BaseTest):
523
333
  def test_edit_details_non_admin(self):
524
334
  email_1, _ = signup_teacher_directly()
525
335
  email_2, password_2 = signup_teacher_directly()
526
- name, postcode = create_organisation_directly(email_1)
336
+ school = create_organisation_directly(email_1)
527
337
  _, _, access_code_1 = create_class_directly(email_1)
528
338
  create_school_student_directly(access_code_1)
529
- join_teacher_to_organisation(email_2, name, postcode)
339
+ join_teacher_to_organisation(email_2, school.name)
530
340
  _, _, access_code_2 = create_class_directly(email_2)
531
341
  create_school_student_directly(access_code_2)
532
342
 
@@ -542,7 +352,7 @@ class TestTeacherFrontend(BaseTest):
542
352
  {
543
353
  "first_name": "Florian",
544
354
  "last_name": "Aucomte",
545
- "current_password": "Password2!",
355
+ "current_password": password_2,
546
356
  }
547
357
  )
548
358
  assert self.is_dashboard_page(page)
@@ -552,7 +362,8 @@ class TestTeacherFrontend(BaseTest):
552
362
  {"first_name": "Florian", "last_name": "Aucomte"}
553
363
  )
554
364
 
555
- def test_change_email(self):
365
+ @patch("common.helpers.emails.send_dotdigital_email")
366
+ def test_change_email(self, mock_send_dotdigital_email):
556
367
  email, password = signup_teacher_directly()
557
368
  create_organisation_directly(email)
558
369
  _, _, access_code = create_class_directly(email)
@@ -561,32 +372,48 @@ class TestTeacherFrontend(BaseTest):
561
372
  other_email, _ = signup_teacher_directly()
562
373
 
563
374
  page = self.go_to_homepage()
564
- page = page.go_to_teacher_login_page().login(email, password).open_account_tab()
375
+ page = (
376
+ page.go_to_teacher_login_page()
377
+ .login(email, password)
378
+ .open_account_tab()
379
+ )
565
380
 
566
381
  # Try changing email to an existing email, should fail
567
382
  page = page.change_email("Test", "Teacher", other_email, password)
568
383
  assert self.is_email_verification_page(page)
569
384
  assert is_email_updated_message_showing(self.selenium)
570
385
 
571
- subject = str(mail.outbox[0].subject)
572
- assert subject == "Code for Life: Duplicate account error"
573
- mail.outbox = []
386
+ mock_send_dotdigital_email.assert_called_with(
387
+ campaign_ids["email_change_notification"],
388
+ ANY,
389
+ personalization_values=ANY,
390
+ )
574
391
 
575
392
  # Try changing email to an existing indy student's email, should fail
576
393
  indy_email, _, _ = create_independent_student_directly()
577
394
  page = self.go_to_homepage()
578
- page = page.go_to_teacher_login_page().login(email, password).open_account_tab()
395
+ page = (
396
+ page.go_to_teacher_login_page()
397
+ .login(email, password)
398
+ .open_account_tab()
399
+ )
579
400
 
580
401
  page = page.change_email("Test", "Teacher", indy_email, password)
581
402
  assert self.is_email_verification_page(page)
582
403
  assert is_email_updated_message_showing(self.selenium)
583
404
 
584
- subject = str(mail.outbox[0].subject)
585
- assert subject == "Code for Life: Duplicate account error"
586
- mail.outbox = []
405
+ mock_send_dotdigital_email.assert_called_with(
406
+ campaign_ids["email_change_notification"],
407
+ ANY,
408
+ personalization_values=ANY,
409
+ )
587
410
 
588
411
  page = self.go_to_homepage()
589
- page = page.go_to_teacher_login_page().login(email, password).open_account_tab()
412
+ page = (
413
+ page.go_to_teacher_login_page()
414
+ .login(email, password)
415
+ .open_account_tab()
416
+ )
590
417
 
591
418
  # Try changing email to a new one, should succeed
592
419
  new_email = "another-email@codeforlife.com"
@@ -596,13 +423,27 @@ class TestTeacherFrontend(BaseTest):
596
423
 
597
424
  # Check user can still log in with old account before verifying new email
598
425
  self.selenium.get(self.live_server_url)
599
- page = HomePage(self.selenium).go_to_teacher_login_page().login(email, password)
426
+ page = (
427
+ HomePage(self.selenium)
428
+ .go_to_teacher_login_page()
429
+ .login(email, password)
430
+ )
600
431
  assert self.is_dashboard_page(page)
601
432
 
602
433
  page = page.logout()
603
434
 
604
- page = email_utils.follow_change_email_link_to_dashboard(page, mail.outbox[0])
605
- mail.outbox = []
435
+ mock_send_dotdigital_email.assert_called_with(
436
+ campaign_ids["email_change_verification"],
437
+ ANY,
438
+ personalization_values=ANY,
439
+ )
440
+ verification_url = mock_send_dotdigital_email.call_args.kwargs[
441
+ "personalization_values"
442
+ ]["VERIFICATION_LINK"]
443
+
444
+ page = email_utils.follow_change_email_link_to_dashboard(
445
+ page, verification_url
446
+ )
606
447
 
607
448
  page = page.login(new_email, password).open_account_tab()
608
449
 
@@ -633,7 +474,8 @@ class TestTeacherFrontend(BaseTest):
633
474
 
634
475
  assert self.is_dashboard_page(page)
635
476
 
636
- def test_reset_password(self):
477
+ @patch("portal.forms.registration.send_dotdigital_email")
478
+ def test_reset_password(self, mock_send_dotdigital_email: Mock):
637
479
  email, _ = signup_teacher_directly()
638
480
  create_organisation_directly(email)
639
481
  _, _, access_code = create_class_directly(email)
@@ -643,9 +485,17 @@ class TestTeacherFrontend(BaseTest):
643
485
 
644
486
  page.reset_email_submit(email)
645
487
 
646
- self.wait_for_email()
488
+ mock_send_dotdigital_email.assert_called_with(
489
+ campaign_ids["reset_password"], ANY, personalization_values=ANY
490
+ )
491
+
492
+ reset_password_url = mock_send_dotdigital_email.call_args.kwargs[
493
+ "personalization_values"
494
+ ]["RESET_PASSWORD_LINK"]
647
495
 
648
- page = email_utils.follow_reset_email_link(self.selenium, mail.outbox[0])
496
+ page = email_utils.follow_reset_email_link(
497
+ self.selenium, reset_password_url
498
+ )
649
499
 
650
500
  new_password = "AnotherPassword12!"
651
501
 
@@ -659,32 +509,280 @@ class TestTeacherFrontend(BaseTest):
659
509
  )
660
510
  assert self.is_dashboard_page(page)
661
511
 
662
- def test_reset_password_fail(self):
512
+ @patch("portal.forms.registration.send_dotdigital_email")
513
+ def test_reset_with_same_password(self, mock_send_dotdigital_email: Mock):
514
+ email, password = signup_teacher_directly()
515
+ create_organisation_directly(email)
516
+ _, _, access_code = create_class_directly(email)
517
+ create_school_student_directly(access_code)
518
+
519
+ page = self.get_to_forgotten_password_page()
520
+
521
+ page.reset_email_submit(email)
522
+
523
+ mock_send_dotdigital_email.assert_called_with(
524
+ campaign_ids["reset_password"], ANY, personalization_values=ANY
525
+ )
526
+
527
+ reset_password_url = mock_send_dotdigital_email.call_args.kwargs[
528
+ "personalization_values"
529
+ ]["RESET_PASSWORD_LINK"]
530
+
531
+ page = email_utils.follow_reset_email_link(
532
+ self.selenium, reset_password_url
533
+ )
534
+
535
+ page.reset_password_fail(password)
536
+
537
+ message = page.browser.find_element(By.CLASS_NAME, "errorlist")
538
+ assert (
539
+ "Please choose a password that you haven't used before"
540
+ in message.text
541
+ )
542
+
543
+ @patch("portal.forms.registration.send_dotdigital_email")
544
+ def test_reset_password_fail(self, mock_send_dotdigital_email: Mock):
663
545
  page = self.get_to_forgotten_password_page()
664
546
  fake_email = "fake_email@fakeemail.com"
665
547
  page.reset_email_submit(fake_email)
666
548
 
667
- time.sleep(5)
549
+ mock_send_dotdigital_email.assert_not_called()
550
+
551
+ def test_admin_sees_all_school_classes(self):
552
+ email, password = signup_teacher_directly()
553
+ school = create_organisation_directly(email)
554
+ klass, _, access_code = create_class_directly(email, "class123")
668
555
 
669
- assert len(mail.outbox) == 0
556
+ # create non_admin account to join the school
557
+ # check if they cannot see classes
558
+ standard_email, standard_password = signup_teacher_directly()
559
+ join_teacher_to_organisation(standard_email, school.name)
560
+
561
+ page = (
562
+ self.go_to_homepage()
563
+ .go_to_teacher_login_page()
564
+ .login(standard_email, standard_password)
565
+ .open_classes_tab()
566
+ )
567
+
568
+ assert page.element_does_not_exist_by_id(f"class-code-{access_code}")
569
+
570
+ self.go_to_homepage().teacher_logout()
571
+ # then make an admin account and check
572
+ # if the teacher can see the classes
573
+
574
+ admin_email, admin_password = signup_teacher_directly()
575
+ join_teacher_to_organisation(admin_email, school.name, is_admin=True)
576
+
577
+ page = (
578
+ self.go_to_homepage()
579
+ .go_to_teacher_login_page()
580
+ .login(admin_email, admin_password)
581
+ .open_classes_tab()
582
+ )
583
+ class_code_field = page.browser.find_element(
584
+ By.ID, f"class-code-{access_code}"
585
+ )
586
+ assert class_code_field.text == access_code
587
+
588
+ def test_admin_student_edit(self):
589
+ email, password = signup_teacher_directly()
590
+ school = create_organisation_directly(email)
591
+
592
+ klass, _, access_code = create_class_directly(email, "class123")
593
+ (
594
+ student_name,
595
+ student_password,
596
+ student_student,
597
+ ) = create_school_student_directly(access_code)
598
+
599
+ joining_email, joining_password = signup_teacher_directly()
600
+ join_teacher_to_organisation(joining_email, school.name, is_admin=True)
601
+
602
+ page = (
603
+ self.go_to_homepage()
604
+ .go_to_teacher_login_page()
605
+ .login(joining_email, joining_password)
606
+ .open_classes_tab()
607
+ )
608
+
609
+ class_button = WebDriverWait(self.selenium, WAIT_TIME).until(
610
+ EC.element_to_be_clickable((By.ID, "class_button"))
611
+ )
612
+ class_button.click()
613
+
614
+ edit_student_button = WebDriverWait(self.selenium, WAIT_TIME).until(
615
+ EC.element_to_be_clickable((By.ID, "edit_student_button"))
616
+ )
617
+ edit_student_button.click()
618
+
619
+ title = page.browser.find_element(By.ID, "student_details")
620
+ assert (
621
+ title.text
622
+ == f"Edit student details for {student_name} from class {klass} ({access_code})"
623
+ )
624
+
625
+ def test_make_admin_popup(self):
626
+ email, password = signup_teacher_directly()
627
+ school = create_organisation_directly(email)
628
+ page = (
629
+ self.go_to_homepage()
630
+ .go_to_teacher_login_page()
631
+ .login(email, password)
632
+ )
633
+ joining_email, _ = signup_teacher_directly()
634
+
635
+ invite_data = {
636
+ "teacher_first_name": "Real",
637
+ "teacher_last_name": "Name",
638
+ "teacher_email": "ren@me.me",
639
+ }
640
+
641
+ for key in invite_data.keys():
642
+ field = page.browser.find_element(By.NAME, key)
643
+ field.send_keys(invite_data[key])
644
+
645
+ page.browser.find_element(By.NAME, "invite_teacher_button").click()
646
+ # Once invite sent test the make admin button
647
+ page.browser.find_element(By.ID, "make_admin_button_invite").click()
648
+ time.sleep(1)
649
+ page.browser.find_element(By.ID, "cancel_admin_popup_button").click()
650
+ time.sleep(1)
651
+ page.browser.find_element(By.ID, "delete-invite").click()
652
+
653
+ # Delete the invite and check if the form invite with
654
+ # admin checked also makes a popup
655
+
656
+ for key in invite_data.keys():
657
+ field = page.browser.find_element(By.NAME, key)
658
+ field.send_keys(invite_data[key])
659
+ checkbox = page.browser.find_element(By.NAME, "make_admin_ticked")
660
+ checkbox.click()
661
+
662
+ page.browser.find_element(By.ID, "invite_teacher_button").click()
663
+ time.sleep(1)
664
+ page.browser.find_element(By.ID, "cancel_admin_popup_button").click()
665
+
666
+ # Non admin teacher joined - make admin should also make a popup
667
+ join_teacher_to_organisation(joining_email, school.name)
668
+
669
+ # refresh the page and scroll to the buttons
670
+ time.sleep(1)
671
+ page.browser.find_element(By.CSS_SELECTOR, ".logo").click()
672
+ page.browser.find_element(By.ID, "make_admin_button").click()
673
+
674
+ assert page.element_exists((By.CLASS_NAME, "popup-box__msg"))
675
+
676
+ def test_delete_account(self):
677
+ FADE_TIME = 0.9 # often fails if lower
670
678
 
671
- def test_onboarding_complete(self):
672
679
  email, password = signup_teacher_directly()
673
680
  create_organisation_directly(email)
674
- create_class_directly(email)
675
681
 
676
- student_name = "Test Student"
682
+ self.selenium.get(self.live_server_url)
683
+ page = (
684
+ HomePage(self.selenium)
685
+ .go_to_teacher_login_page()
686
+ .login(email, password)
687
+ .open_account_tab()
688
+ )
689
+
690
+ # test incorrect password
691
+ page.browser.find_element(By.ID, "id_delete_password").send_keys(
692
+ "IncorrectPassword"
693
+ )
694
+ page.browser.find_element(By.ID, "delete_account_button").click()
695
+ is_message_showing(page.browser, "Your account was not deleted")
696
+
697
+ # test cancel (no class)
698
+ time.sleep(FADE_TIME)
699
+ page.browser.find_element(By.ID, "id_delete_password").clear()
700
+ page.browser.find_element(By.ID, "id_delete_password").send_keys(
701
+ password
702
+ )
703
+ page.browser.find_element(By.ID, "delete_account_button").click()
704
+
705
+ time.sleep(FADE_TIME)
706
+ assert page.browser.find_element(
707
+ By.ID, "popup-delete-review"
708
+ ).is_displayed()
709
+ page.browser.find_element(By.ID, "cancel_popup_button").click()
710
+ time.sleep(FADE_TIME)
711
+
712
+ # test close button in the corner
713
+ page.browser.find_element(By.ID, "id_delete_password").clear()
714
+ page.browser.find_element(By.ID, "id_delete_password").send_keys(
715
+ password
716
+ )
717
+ page.browser.find_element(By.ID, "delete_account_button").click()
718
+
719
+ time.sleep(FADE_TIME)
720
+ page.browser.find_element(By.ID, "close_popup_button").click()
721
+ time.sleep(FADE_TIME)
722
+
723
+ # create class
724
+ _, _, access_code = create_class_directly(email)
725
+ create_school_student_directly(access_code)
726
+
727
+ # delete then review classes
728
+ page.browser.find_element(By.ID, "id_delete_password").send_keys(
729
+ password
730
+ )
731
+ page.browser.find_element(By.ID, "delete_account_button").click()
732
+
733
+ time.sleep(FADE_TIME)
734
+ assert page.browser.find_element(
735
+ By.ID, "popup-delete-review"
736
+ ).is_displayed()
737
+ page.browser.find_element(By.ID, "review_button").click()
738
+ time.sleep(FADE_TIME)
739
+
740
+ assert page.has_classes()
741
+ page = page.open_account_tab()
742
+
743
+ # test actual deletion
744
+ page.browser.find_element(By.ID, "id_delete_password").send_keys(
745
+ password
746
+ )
747
+ page.browser.find_element(By.ID, "delete_account_button").click()
748
+
749
+ time.sleep(FADE_TIME)
750
+ page.browser.find_element(By.ID, "delete_button").click()
751
+
752
+ # back to homepage
753
+ assert page.browser.find_element(By.CLASS_NAME, "banner--homepage")
754
+
755
+ # user should not be able to login now
756
+ page = (
757
+ HomePage(self.selenium)
758
+ .go_to_teacher_login_page()
759
+ .login_failure(email, password)
760
+ )
761
+
762
+ assert page.has_login_failed(
763
+ "form-login-teacher", INVALID_LOGIN_MESSAGE
764
+ )
765
+
766
+ def test_onboarding_complete(self):
767
+ email, password = signup_teacher_directly()
677
768
 
678
769
  self.selenium.get(self.live_server_url)
679
770
  page = (
680
771
  HomePage(self.selenium)
681
772
  .go_to_teacher_login_page()
682
- .login_no_students(email, password)
683
- .type_student_name(student_name)
773
+ .login_no_school(email, password)
774
+ )
775
+
776
+ page = page.create_organisation("Test school")
777
+ page = page.create_class("Test class", True)
778
+ page = (
779
+ page.type_student_name("Test Student")
684
780
  .create_students()
685
781
  .complete_setup()
686
782
  )
687
783
 
784
+ time.sleep(1)
785
+
688
786
  assert page.has_onboarding_complete_popup()
689
787
 
690
788
  def get_to_forgotten_password_page(self):
@@ -697,7 +795,9 @@ class TestTeacherFrontend(BaseTest):
697
795
  return page
698
796
 
699
797
  def wait_for_email(self):
700
- WebDriverWait(self.selenium, 2).until(lambda driver: len(mail.outbox) == 1)
798
+ WebDriverWait(self.selenium, 2).until(
799
+ lambda driver: len(mail.outbox) == 1
800
+ )
701
801
 
702
802
  def is_dashboard_page(self, page):
703
803
  return page.__class__.__name__ == "TeachDashboardPage"