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
@@ -1,35 +1,41 @@
1
1
  from __future__ import absolute_import
2
2
 
3
- import time
3
+ import datetime
4
+ from unittest.mock import ANY, Mock, patch
4
5
 
6
+ from common.mail import campaign_ids
7
+ from common.models import JoinReleaseStudent
5
8
  from common.tests.utils import email as email_utils
6
9
  from common.tests.utils.classes import create_class_directly
7
10
  from common.tests.utils.organisation import (
8
11
  create_organisation_directly,
12
+ join_teacher_to_organisation,
9
13
  )
10
14
  from common.tests.utils.student import (
11
15
  create_independent_student,
12
16
  create_independent_student_directly,
13
17
  create_school_student_directly,
14
- signup_duplicate_independent_student_fail,
18
+ generate_independent_student_details,
15
19
  verify_email,
16
20
  )
17
21
  from common.tests.utils.teacher import signup_teacher_directly
22
+ from django.contrib.auth.models import User
18
23
  from django.core import mail
19
24
  from django.test import Client, TestCase
20
25
  from django.urls import reverse
26
+ from selenium.webdriver.common.by import By
21
27
  from selenium.webdriver.support.wait import WebDriverWait
22
28
 
23
29
  from portal.forms.error_messages import INVALID_LOGIN_MESSAGE
24
30
  from .base_test import BaseTest
25
31
  from .pageObjects.portal.home_page import HomePage
26
32
  from .utils.messages import (
33
+ is_email_updated_message_showing,
27
34
  is_email_verified_message_showing,
28
35
  is_indep_student_join_request_received_message_showing,
29
36
  is_indep_student_join_request_revoked_message_showing,
30
- is_student_details_updated_message_showing,
31
- is_email_updated_message_showing,
32
37
  is_password_updated_message_showing,
38
+ is_student_details_updated_message_showing,
33
39
  )
34
40
 
35
41
 
@@ -40,10 +46,12 @@ class TestIndependentStudent(TestCase):
40
46
  response = c.post(
41
47
  reverse("register"),
42
48
  {
49
+ "independent_student_signup-date_of_birth_day": 7,
50
+ "independent_student_signup-date_of_birth_month": 10,
51
+ "independent_student_signup-date_of_birth_year": 1997,
43
52
  "independent_student_signup-name": "Test Name",
44
- "independent_student_signup-username": "TestUsername",
45
53
  "independent_student_signup-email": "test@email.com",
46
- "independent_student_signup-is_over_required_age": "on",
54
+ "independent_student_signup-consent_ticked": "on",
47
55
  "independent_student_signup-password": "pass",
48
56
  "independent_student_signup-confirm_password": "pass",
49
57
  "g-recaptcha-response": "something",
@@ -59,10 +67,12 @@ class TestIndependentStudent(TestCase):
59
67
  response = c.post(
60
68
  reverse("register"),
61
69
  {
70
+ "independent_student_signup-date_of_birth_day": 7,
71
+ "independent_student_signup-date_of_birth_month": 10,
72
+ "independent_student_signup-date_of_birth_year": 1997,
62
73
  "independent_student_signup-name": "Test Name",
63
- "independent_student_signup-username": "TestUsername",
64
74
  "independent_student_signup-email": "test@email.com",
65
- "independent_student_signup-is_over_required_age": "on",
75
+ "independent_student_signup-consent_ticked": "on",
66
76
  "independent_student_signup-password": "Password1",
67
77
  "independent_student_signup-confirm_password": "Password1",
68
78
  "g-recaptcha-response": "something",
@@ -72,16 +82,34 @@ class TestIndependentStudent(TestCase):
72
82
  # Assert response isn't a redirect (submit failure)
73
83
  assert response.status_code == 200
74
84
 
85
+ response = c.post(
86
+ reverse("register"),
87
+ {
88
+ "independent_student_signup-date_of_birth_day": 7,
89
+ "independent_student_signup-date_of_birth_month": 10,
90
+ "independent_student_signup-date_of_birth_year": 1997,
91
+ "independent_student_signup-name": "Test Name",
92
+ "independent_student_signup-email": "test@email.com",
93
+ "independent_student_signup-consent_ticked": "on",
94
+ "independent_student_signup-password": "Password123$",
95
+ "independent_student_signup-confirm_password": "Password123$",
96
+ "g-recaptcha-response": "something",
97
+ },
98
+ )
99
+ assert response.status_code == 200
100
+
75
101
  def test_signup_passwords_do_not_match_fails(self):
76
102
  c = Client()
77
103
 
78
104
  response = c.post(
79
105
  reverse("register"),
80
106
  {
107
+ "independent_student_signup-date_of_birth_day": 7,
108
+ "independent_student_signup-date_of_birth_month": 10,
109
+ "independent_student_signup-date_of_birth_year": 1997,
81
110
  "independent_student_signup-name": "Test Name",
82
- "independent_student_signup-username": "TestUsername",
83
111
  "independent_student_signup-email": "test@email.com",
84
- "independent_student_signup-is_over_required_age": "on",
112
+ "independent_student_signup-consent_ticked": "on",
85
113
  "independent_student_signup-password": "Password1!",
86
114
  "independent_student_signup-confirm_password": "Password2!",
87
115
  "g-recaptcha-response": "something",
@@ -97,12 +125,14 @@ class TestIndependentStudent(TestCase):
97
125
  response = c.post(
98
126
  reverse("register"),
99
127
  {
128
+ "independent_student_signup-date_of_birth_day": 7,
129
+ "independent_student_signup-date_of_birth_month": 10,
130
+ "independent_student_signup-date_of_birth_year": 1997,
100
131
  "independent_student_signup-name": "///",
101
- "independent_student_signup-username": "TestUsername",
102
132
  "independent_student_signup-email": "test@email.com",
103
- "independent_student_signup-is_over_required_age": "on",
104
- "independent_student_signup-password": "Password1!",
105
- "independent_student_signup-confirm_password": "Password1!",
133
+ "independent_student_signup-consent_ticked": "on",
134
+ "independent_student_signup-password": "$RRFVBGT%6yhnmju7",
135
+ "independent_student_signup-confirm_password": "$RRFVBGT%6yhnmju7",
106
136
  "g-recaptcha-response": "something",
107
137
  },
108
138
  )
@@ -110,95 +140,132 @@ class TestIndependentStudent(TestCase):
110
140
  # Assert response isn't a redirect (submit failure)
111
141
  assert response.status_code == 200
112
142
 
113
- def test_signup_invalid_username_fails(self):
143
+ @patch("common.helpers.emails.send_dotdigital_email")
144
+ def test_signup_under_13_sends_parent_email(self, mock_send_dotdigital_email: Mock):
114
145
  c = Client()
115
146
 
116
147
  response = c.post(
117
148
  reverse("register"),
118
149
  {
119
- "independent_student_signup-name": "Test Name",
120
- "independent_student_signup-username": "///",
150
+ "independent_student_signup-date_of_birth_day": datetime.date.today().day,
151
+ "independent_student_signup-date_of_birth_month": datetime.date.today().month,
152
+ "independent_student_signup-date_of_birth_year": datetime.date.today().year,
153
+ "independent_student_signup-name": "Young person",
121
154
  "independent_student_signup-email": "test@email.com",
122
- "independent_student_signup-is_over_required_age": "on",
123
- "independent_student_signup-password": "Password1!",
124
- "independent_student_signup-confirm_password": "Password1!",
155
+ "independent_student_signup-consent_ticked": "on",
156
+ "independent_student_signup-password": "$RRFVBGT%6yhnmju7",
157
+ "independent_student_signup-confirm_password": "$RRFVBGT%6yhnmju7",
125
158
  "g-recaptcha-response": "something",
126
159
  },
127
160
  )
128
161
 
129
- # Assert response isn't a redirect (submit failure)
130
- assert response.status_code == 200
162
+ assert response.status_code == 302
163
+ mock_send_dotdigital_email.assert_called_once_with(
164
+ campaign_ids["verify_new_user_via_parent"], ANY, personalization_values=ANY
165
+ )
131
166
 
132
167
 
133
168
  # Class for Selenium tests. We plan to replace these and turn them into Cypress tests
134
169
  class TestIndependentStudentFrontend(BaseTest):
135
- def test_signup_without_newsletter(self):
170
+ @patch("portal.views.registration.send_dotdigital_email")
171
+ def test_delete_indy_account(self, mock_send_dotdigital_email: Mock):
136
172
  page = self.go_to_homepage()
137
- page, _, _, _, _ = create_independent_student(page)
138
- assert is_email_verified_message_showing(self.selenium)
173
+ page, _, _, email, password = create_independent_student(page)
174
+ page = page.independent_student_login(email, password)
175
+ page = page.go_to_account_page()
176
+
177
+ # save the user to check if it was anonymised
178
+ user = User.objects.get(email=email)
179
+ user_id = user.id
139
180
 
140
- def test_signup_with_newsletter(self):
181
+ # first check if a wrong password triggers the error
182
+ unsubscribe_newsletter_checkbox = page.browser.find_element(By.NAME, "unsubscribe_newsletter")
183
+ unsubscribe_newsletter_checkbox.click()
184
+
185
+ delete_account_form = page.browser.find_element(By.NAME, "delete_password")
186
+ delete_account_form.send_keys("123") # wrong password
187
+
188
+ delete_account_button = page.browser.find_element(By.ID, "delete_account_button")
189
+ delete_account_button.click()
190
+ assert (
191
+ page.browser.find_element(By.CSS_SELECTOR, "#form-delete-indy-account > ul > li").text
192
+ == "Incorrect password"
193
+ )
194
+
195
+ # now delete the account
196
+ unsubscribe_newsletter_checkbox = page.browser.find_element(By.NAME, "unsubscribe_newsletter")
197
+ unsubscribe_newsletter_checkbox.click()
198
+
199
+ delete_account_form = page.browser.find_element(By.NAME, "delete_password")
200
+ delete_account_form.send_keys(password)
201
+
202
+ delete_account_button = page.browser.find_element(By.ID, "delete_account_button")
203
+ delete_account_button.click()
204
+
205
+ delete_button_confirm = page.browser.find_element(By.ID, "delete_button")
206
+ delete_button_confirm.click()
207
+
208
+ # check if can still login to the account
209
+ page = self.go_to_homepage()
210
+ assert page.go_to_independent_student_login_page().independent_student_login_failure(email, password)
211
+
212
+ # now check if anonymised
213
+ assert not User.objects.get(id=user_id).is_active
214
+
215
+ # check if email has been sent
216
+ mock_send_dotdigital_email.assert_called_once_with(campaign_ids["delete_account"], ANY)
217
+
218
+ def test_signup_without_newsletter(self):
141
219
  page = self.go_to_homepage()
142
- page, _, _, _, _ = create_independent_student(page, newsletter=True)
220
+ page, _, _, _, _ = create_independent_student(page)
143
221
  assert is_email_verified_message_showing(self.selenium)
144
222
 
145
- def test_signup_duplicate_email_failure(self):
223
+ @patch("portal.views.home.send_dotdigital_email")
224
+ def test_signup_duplicate_email_failure(self, mock_send_dotdigital_email):
146
225
  page = self.go_to_homepage()
147
226
  page, _, _, email, _ = create_independent_student(page)
148
227
  assert is_email_verified_message_showing(self.selenium)
149
228
 
150
- page = self.go_to_homepage()
151
- page, _, _, _, _ = signup_duplicate_independent_student_fail(
152
- page, duplicate_email=email
229
+ page = self.go_to_homepage().go_to_signup_page()
230
+
231
+ name, username, email_address, password = generate_independent_student_details()
232
+ page = page.independent_student_signup(name, email, password=password, confirm_password=password)
233
+ page = page.return_to_home_page()
234
+
235
+ mock_send_dotdigital_email.assert_called_once_with(
236
+ campaign_ids["user_already_registered"], ANY, personalization_values=ANY
153
237
  )
154
238
 
155
- assert len(mail.outbox) == 1
156
- assert mail.outbox[0].subject == "Code for Life: Duplicate account error"
239
+ login_link = mock_send_dotdigital_email.call_args.kwargs["personalization_values"]["LOGIN_URL"]
240
+
241
+ page = email_utils.follow_duplicate_account_link_to_login(page, login_link, "independent")
157
242
 
158
243
  assert self.is_login_page(page)
159
244
 
160
- def test_signup_duplicate_email_with_teacher(self):
245
+ @patch("portal.views.home.send_dotdigital_email")
246
+ def test_signup_duplicate_email_with_teacher(self, mock_send_dotdigital_email: Mock):
161
247
  teacher_email, _ = signup_teacher_directly()
162
248
 
163
249
  page = self.go_to_homepage()
164
250
  page = page.go_to_signup_page()
165
251
 
252
+ strong_password = "£EDCVFR$5tgbnhy6"
166
253
  page = page.independent_student_signup(
167
- "indy",
168
- teacher_email,
169
- teacher_email,
170
- password="Password1!",
171
- confirm_password="Password1!",
254
+ "indy", teacher_email, password=strong_password, confirm_password=strong_password
172
255
  )
173
256
 
174
- page = page.return_to_home_page()
175
-
176
- assert len(mail.outbox) == 1
177
- assert mail.outbox[0].subject == "Code for Life: Duplicate account error"
257
+ page.return_to_home_page()
178
258
 
179
- def test_signup_duplicate_username_failure(self):
180
- username, _, _ = create_independent_student_directly()
181
-
182
- page = self.go_to_homepage()
183
- page, _, _, _, _ = signup_duplicate_independent_student_fail(
184
- page, duplicate_username=username
259
+ mock_send_dotdigital_email.assert_called_once_with(
260
+ campaign_ids["user_already_registered"], ANY, personalization_values=ANY
185
261
  )
186
262
 
187
- assert len(mail.outbox) == 1
188
- assert mail.outbox[0].subject == "Code for Life: Username already taken"
189
-
190
- assert self.is_login_page(page)
191
-
192
263
  def test_login_failure(self):
193
264
  page = self.go_to_homepage()
194
265
  page = page.go_to_independent_student_login_page()
195
- page = page.independent_student_login_failure(
196
- "Non existent username", "Incorrect password"
197
- )
266
+ page = page.independent_student_login_failure("non-existent-email@codeforlife.com", "Incorrect password")
198
267
 
199
- assert page.has_login_failed(
200
- "independent_student_login_form", INVALID_LOGIN_MESSAGE
201
- )
268
+ assert page.has_login_failed("independent_student_login_form", INVALID_LOGIN_MESSAGE)
202
269
 
203
270
  def test_login_success(self):
204
271
  page = self.go_to_homepage()
@@ -206,18 +273,20 @@ class TestIndependentStudentFrontend(BaseTest):
206
273
  page = page.independent_student_login(username, password)
207
274
  assert self.is_dashboard(page)
208
275
 
209
- def test_login_not_verified(self):
276
+ @patch("common.helpers.emails.send_dotdigital_email")
277
+ def test_login_not_verified(self, mock_send_dotdigital_email):
210
278
  username, password, _ = create_independent_student_directly(preverified=False)
211
279
  self.selenium.get(self.live_server_url)
212
280
  page = HomePage(self.selenium)
213
281
  page = page.go_to_independent_student_login_page()
214
282
  page = page.independent_student_login_failure(username, password)
215
283
 
216
- assert page.has_login_failed(
217
- "independent_student_login_form", INVALID_LOGIN_MESSAGE
218
- )
284
+ page.has_login_failed("independent_student_login_form", INVALID_LOGIN_MESSAGE)
285
+ assert page.has_login_failed("independent_student_login_form", INVALID_LOGIN_MESSAGE)
286
+
287
+ verification_url = mock_send_dotdigital_email.call_args.kwargs["personalization_values"]["VERIFICATION_LINK"]
219
288
 
220
- verify_email(page)
289
+ verify_email(page, verification_url)
221
290
 
222
291
  assert is_email_verified_message_showing(self.selenium)
223
292
 
@@ -225,17 +294,22 @@ class TestIndependentStudentFrontend(BaseTest):
225
294
 
226
295
  assert self.is_dashboard(page)
227
296
 
228
- def test_reset_password(self):
297
+ @patch("portal.forms.registration.send_dotdigital_email")
298
+ def test_reset_password(self, mock_send_dotdigital_email: Mock):
229
299
  page = self.go_to_homepage()
230
300
 
231
301
  page, name, username, _, _ = create_independent_student(page)
232
302
  page = self.get_to_forgotten_password_page()
233
303
 
234
- page.reset_username_submit(username)
304
+ page.reset_email_submit(username)
235
305
 
236
- self.wait_for_email()
306
+ mock_send_dotdigital_email.assert_called_with(campaign_ids["reset_password"], ANY, personalization_values=ANY)
237
307
 
238
- page = email_utils.follow_reset_email_link(self.selenium, mail.outbox[0])
308
+ reset_password_url = mock_send_dotdigital_email.call_args.kwargs["personalization_values"][
309
+ "RESET_PASSWORD_LINK"
310
+ ]
311
+
312
+ page = email_utils.follow_reset_email_link(self.selenium, reset_password_url)
239
313
 
240
314
  new_password = "AnotherPassword12"
241
315
 
@@ -253,26 +327,20 @@ class TestIndependentStudentFrontend(BaseTest):
253
327
  page = page.go_to_account_page()
254
328
  assert page.check_account_details({"name": name})
255
329
 
256
- def test_reset_password_fail(self):
330
+ @patch("portal.forms.registration.send_dotdigital_email")
331
+ def test_reset_password_fail(self, mock_send_dotdigital_email: Mock):
257
332
  page = self.get_to_forgotten_password_page()
333
+ fake_email = "fake_email@fakeemail.com"
334
+ page.reset_email_submit(fake_email)
258
335
 
259
- fake_username = "fake_username"
260
- page.reset_username_submit(fake_username)
261
-
262
- time.sleep(5)
263
-
264
- assert len(mail.outbox) == 0
336
+ mock_send_dotdigital_email.assert_not_called()
265
337
 
266
338
  def test_update_name_success(self):
267
339
  homepage = self.go_to_homepage()
268
340
 
269
- play_page, name, student_username, _, password = create_independent_student(
270
- homepage
271
- )
341
+ play_page, name, student_username, _, password = create_independent_student(homepage)
272
342
 
273
- page = play_page.independent_student_login(
274
- student_username, password
275
- ).go_to_account_page()
343
+ page = play_page.independent_student_login(student_username, password).go_to_account_page()
276
344
 
277
345
  assert page.check_account_details({"name": name})
278
346
 
@@ -284,9 +352,7 @@ class TestIndependentStudentFrontend(BaseTest):
284
352
  def test_update_name_failure(self):
285
353
  homepage = self.go_to_homepage()
286
354
 
287
- play_page, _, student_username, _, password = create_independent_student(
288
- homepage
289
- )
355
+ play_page, _, student_username, _, password = create_independent_student(homepage)
290
356
 
291
357
  page = (
292
358
  play_page.independent_student_login(student_username, password)
@@ -296,19 +362,17 @@ class TestIndependentStudentFrontend(BaseTest):
296
362
 
297
363
  assert self.is_account_page(page)
298
364
  assert page.was_form_invalid(
299
- "student_account_form",
300
- "Names may only contain letters, numbers, dashes, underscores, and spaces.",
365
+ "student_account_form", "Names may only contain letters, numbers, dashes, underscores, and spaces."
301
366
  )
302
367
 
303
- def test_change_email(self):
368
+ @patch("common.helpers.emails.send_dotdigital_email")
369
+ def test_change_email(self, mock_send_dotdigital_email):
304
370
  homepage = self.go_to_homepage()
305
371
 
306
- _, _, student_username, _, password = create_independent_student(homepage)
372
+ _, _, _, student_email, password = create_independent_student(homepage)
307
373
  play_page, _, _, other_email, _ = create_independent_student(homepage)
308
374
 
309
- page = play_page.independent_student_login(
310
- student_username, password
311
- ).go_to_account_page()
375
+ page = play_page.independent_student_login(student_email, password).go_to_account_page()
312
376
 
313
377
  # Try changing email to an existing email, should fail
314
378
  page = page.change_email(other_email, password)
@@ -316,16 +380,16 @@ class TestIndependentStudentFrontend(BaseTest):
316
380
  assert is_student_details_updated_message_showing(self.selenium)
317
381
  assert is_email_updated_message_showing(self.selenium)
318
382
 
319
- subject = str(mail.outbox[0].subject)
320
- assert subject == "Code for Life: Duplicate account error"
321
- mail.outbox = []
383
+ mock_send_dotdigital_email.assert_called_with(
384
+ campaign_ids["email_change_notification"], ANY, personalization_values=ANY
385
+ )
322
386
 
323
387
  # Try changing email to an existing teacher's email
324
388
  teacher_email, _ = signup_teacher_directly()
325
389
 
326
390
  page = (
327
391
  homepage.go_to_independent_student_login_page()
328
- .independent_student_login(student_username, password)
392
+ .independent_student_login(student_email, password)
329
393
  .go_to_account_page()
330
394
  )
331
395
 
@@ -334,14 +398,14 @@ class TestIndependentStudentFrontend(BaseTest):
334
398
  assert is_student_details_updated_message_showing(self.selenium)
335
399
  assert is_email_updated_message_showing(self.selenium)
336
400
 
337
- subject = str(mail.outbox[0].subject)
338
- assert subject == "Code for Life: Duplicate account error"
339
- mail.outbox = []
401
+ mock_send_dotdigital_email.assert_called_with(
402
+ campaign_ids["email_change_notification"], ANY, personalization_values=ANY
403
+ )
340
404
 
341
405
  page = (
342
406
  self.go_to_homepage()
343
407
  .go_to_independent_student_login_page()
344
- .independent_student_login(student_username, password)
408
+ .independent_student_login(student_email, password)
345
409
  .go_to_account_page()
346
410
  )
347
411
 
@@ -358,31 +422,29 @@ class TestIndependentStudentFrontend(BaseTest):
358
422
  page = (
359
423
  self.go_to_homepage()
360
424
  .go_to_independent_student_login_page()
361
- .independent_student_login(student_username, password)
425
+ .independent_student_login(student_email, password)
362
426
  )
363
427
  assert self.is_dashboard(page)
364
428
 
365
429
  page = page.logout()
366
430
 
367
- page = email_utils.follow_change_email_link_to_independent_dashboard(
368
- page, mail.outbox[0]
431
+ mock_send_dotdigital_email.assert_called_with(
432
+ campaign_ids["email_change_verification"], ANY, personalization_values=ANY
369
433
  )
370
- mail.outbox = []
434
+ verification_url = mock_send_dotdigital_email.call_args.kwargs["personalization_values"]["VERIFICATION_LINK"]
371
435
 
372
- page = page.independent_student_login(student_username, password)
436
+ page = email_utils.follow_change_email_link_to_independent_dashboard(page, verification_url)
437
+
438
+ page = page.independent_student_login(new_email, password)
373
439
 
374
440
  assert self.is_dashboard(page)
375
441
 
376
442
  def test_change_password(self):
377
443
  homepage = self.go_to_homepage()
378
444
 
379
- play_page, _, student_username, _, password = create_independent_student(
380
- homepage
381
- )
445
+ play_page, _, student_username, _, password = create_independent_student(homepage)
382
446
 
383
- page = play_page.independent_student_login(
384
- student_username, password
385
- ).go_to_account_page()
447
+ page = play_page.independent_student_login(student_username, password).go_to_account_page()
386
448
 
387
449
  new_password = "AnotherPassword12"
388
450
  page = page.update_password_success(new_password, password, is_independent=True)
@@ -398,9 +460,7 @@ class TestIndependentStudentFrontend(BaseTest):
398
460
  def test_join_class_nonexistent_class(self):
399
461
  homepage = self.go_to_homepage()
400
462
 
401
- play_page, _, student_username, _, password = create_independent_student(
402
- homepage
403
- )
463
+ play_page, _, student_username, _, password = create_independent_student(homepage)
404
464
 
405
465
  page = (
406
466
  play_page.independent_student_login(student_username, password)
@@ -410,7 +470,7 @@ class TestIndependentStudentFrontend(BaseTest):
410
470
 
411
471
  assert self.is_join_class_page(page)
412
472
  assert page.has_join_request_failed(
413
- "Cannot find the school or club and/or class"
473
+ "The class code you entered either does not exist or is not currently accepting join requests. Please double check that you have entered the correct class code and contact the teacher of the class to ensure their class is currently accepting join requests."
414
474
  )
415
475
 
416
476
  def test_join_class_not_accepting_requests(self):
@@ -421,9 +481,7 @@ class TestIndependentStudentFrontend(BaseTest):
421
481
 
422
482
  homepage = self.go_to_homepage()
423
483
 
424
- play_page, _, student_username, _, password = create_independent_student(
425
- homepage
426
- )
484
+ play_page, _, student_username, _, password = create_independent_student(homepage)
427
485
 
428
486
  page = (
429
487
  play_page.independent_student_login(student_username, password)
@@ -433,7 +491,7 @@ class TestIndependentStudentFrontend(BaseTest):
433
491
 
434
492
  assert self.is_join_class_page(page)
435
493
  assert page.has_join_request_failed(
436
- "Cannot find the school or club and/or class"
494
+ "The class code you entered either does not exist or is not currently accepting join requests. Please double check that you have entered the correct class code and contact the teacher of the class to ensure their class is currently accepting join requests."
437
495
  )
438
496
 
439
497
  def test_join_class_revoked(self):
@@ -446,9 +504,7 @@ class TestIndependentStudentFrontend(BaseTest):
446
504
 
447
505
  homepage = self.go_to_homepage()
448
506
 
449
- play_page, _, student_username, _, password = create_independent_student(
450
- homepage
451
- )
507
+ play_page, _, student_username, _, password = create_independent_student(homepage)
452
508
 
453
509
  page = (
454
510
  play_page.independent_student_login(student_username, password)
@@ -471,17 +527,12 @@ class TestIndependentStudentFrontend(BaseTest):
471
527
  klass.save()
472
528
 
473
529
  homepage = self.go_to_homepage()
530
+ page = homepage.go_to_independent_student_login_page()
474
531
 
475
- (
476
- play_page,
477
- student_name,
478
- student_username,
479
- _,
480
- password,
481
- ) = create_independent_student(homepage)
532
+ username, password, student = create_independent_student_directly()
482
533
 
483
534
  page = (
484
- play_page.independent_student_login(student_username, password)
535
+ page.independent_student_login(username, password)
485
536
  .go_to_join_a_school_or_club_page()
486
537
  .join_a_school_or_club(access_code)
487
538
  )
@@ -495,11 +546,16 @@ class TestIndependentStudentFrontend(BaseTest):
495
546
  .login(teacher_email, teacher_password)
496
547
  .open_classes_tab()
497
548
  .accept_independent_join_request()
498
- .save(student_name)
549
+ .save(username)
499
550
  .return_to_class()
500
551
  )
501
552
 
502
- assert page.student_exists(student_name)
553
+ assert page.student_exists(username)
554
+
555
+ # check whether a record is created correctly
556
+ logs = JoinReleaseStudent.objects.filter(student=student)
557
+ assert len(logs) == 1
558
+ assert logs[0].action_type == JoinReleaseStudent.JOIN
503
559
 
504
560
  def test_join_class_denied(self):
505
561
  teacher_email, teacher_password = signup_teacher_directly()
@@ -511,9 +567,7 @@ class TestIndependentStudentFrontend(BaseTest):
511
567
 
512
568
  homepage = self.go_to_homepage()
513
569
 
514
- play_page, _, student_username, _, password = create_independent_student(
515
- homepage
516
- )
570
+ play_page, _, student_username, _, password = create_independent_student(homepage)
517
571
 
518
572
  page = (
519
573
  play_page.independent_student_login(student_username, password)
@@ -534,20 +588,77 @@ class TestIndependentStudentFrontend(BaseTest):
534
588
 
535
589
  assert dashboard_page.has_no_independent_join_requests()
536
590
 
537
- def test_cannot_see_aimmo(self):
591
+ def test_join_class_denied_and_accepted_by_admin(self):
592
+ # Create 2 teachers in the same school, one admin, one standard
593
+ admin_email, admin_password1 = signup_teacher_directly()
594
+ standard_email, _ = signup_teacher_directly()
595
+ school = create_organisation_directly(admin_email)
596
+ join_teacher_to_organisation(standard_email, school.name, is_admin=False)
597
+
598
+ # Create class for standard teacher which always accepts external requests
599
+ klass, class_name, access_code = create_class_directly(standard_email)
600
+ klass.always_accept_requests = True
601
+ klass.save()
602
+
603
+ # Create two independent students
604
+ username1, password1, student1 = create_independent_student_directly()
605
+ username2, password2, student2 = create_independent_student_directly()
606
+
607
+ # Login as both students and request to join the same class
608
+ homepage = self.go_to_homepage()
609
+ page = homepage.go_to_independent_student_login_page()
610
+
611
+ page = (
612
+ page.independent_student_login(username1, password1)
613
+ .go_to_join_a_school_or_club_page()
614
+ .join_a_school_or_club(access_code)
615
+ )
616
+
617
+ page.logout()
618
+
538
619
  page = self.go_to_homepage()
539
- page, _, username, _, password = create_independent_student(page)
540
- page = page.independent_student_login(username, password)
620
+ page = page.go_to_independent_student_login_page()
621
+
622
+ page = (
623
+ page.independent_student_login(username2, password2)
624
+ .go_to_join_a_school_or_club_page()
625
+ .join_a_school_or_club(access_code)
626
+ )
541
627
 
542
- assert page.element_does_not_exist_by_link_text("Kurono")
628
+ page.logout()
543
629
 
544
- def get_to_forgotten_password_page(self):
545
- self.selenium.get(self.live_server_url)
630
+ # Login as school admin, accept the first request
631
+ page = self.go_to_homepage()
546
632
  page = (
547
- HomePage(self.selenium)
548
- .go_to_independent_student_login_page()
549
- .go_to_indep_forgotten_password_page()
633
+ page.go_to_teacher_login_page()
634
+ .login(admin_email, admin_password1)
635
+ .open_classes_tab()
636
+ .accept_independent_join_request()
637
+ .save(username1)
638
+ .return_to_class()
550
639
  )
640
+
641
+ assert page.student_exists(username1)
642
+
643
+ # check whether a record is created correctly
644
+ logs = JoinReleaseStudent.objects.filter(student=student1)
645
+ assert len(logs) == 1
646
+ assert logs[0].action_type == JoinReleaseStudent.JOIN
647
+
648
+ # Deny the second request
649
+ page = page.go_to_dashboard().open_classes_tab().deny_independent_join_request()
650
+
651
+ assert page.has_no_independent_join_requests()
652
+
653
+ # check a record hasn't been created and the student no longer has a join request
654
+ logs = JoinReleaseStudent.objects.filter(student=student2)
655
+ assert len(logs) == 0
656
+ assert student2.pending_class_request is None
657
+ assert student2.is_independent()
658
+
659
+ def get_to_forgotten_password_page(self):
660
+ self.selenium.get(self.live_server_url)
661
+ page = HomePage(self.selenium).go_to_independent_student_login_page().go_to_indep_forgotten_password_page()
551
662
  return page
552
663
 
553
664
  def wait_for_email(self):