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,19 +1,327 @@
1
+ from datetime import timedelta
2
+ from time import sleep
3
+ from uuid import uuid4
4
+
5
+ import pytest
6
+ from common.models import SchoolTeacherInvitation, Teacher
7
+ from common.tests.utils.classes import create_class_directly
8
+ from common.tests.utils.organisation import create_organisation_directly, join_teacher_to_organisation
9
+ from common.tests.utils.teacher import signup_teacher_directly
10
+ from django.contrib.messages import get_messages
11
+ from django.core import mail
12
+ from django.test import Client, TestCase
1
13
  from django.urls import reverse
2
- from django.test import TestCase, Client
14
+ from django.utils import timezone
15
+ from selenium.webdriver.common.by import By
16
+ from selenium.webdriver.support import expected_conditions as EC
17
+ from selenium.webdriver.support.wait import WebDriverWait
18
+
19
+ from portal.tests.base_test import BaseTest
20
+
21
+ FADE_TIME = 0.9
22
+ WAIT_TIME = 15
3
23
 
4
24
 
5
25
  class TestInviteTeacher(TestCase):
6
26
  def test_invite_teacher_successful(self):
7
- url = reverse("invite_teacher")
27
+ email, password = signup_teacher_directly()
28
+ school = create_organisation_directly(email)
29
+ create_class_directly(email)
30
+ teacher = Teacher.objects.get(new_user__email=email)
31
+
8
32
  client = Client()
9
- data = {"email": "valid_email@example.com"}
10
- response = client.post(url, data)
11
- self.assertEquals(response.status_code, 200)
12
- self.assertTemplateUsed(response, "portal/email_invitation_sent.html")
33
+ client.login(username=email, password=password)
34
+
35
+ invited_teacher_first_name = "Valid"
36
+ invited_teacher_last_name = "Name"
37
+ invited_teacher_email = "valid_email@example.com"
38
+ invited_teacher_password = "$RRFVBGT%^yhnmju7"
39
+
40
+ # Invite another teacher to school and check they got an email
41
+ dashboard_url = reverse("dashboard")
42
+ data = {
43
+ "teacher_first_name": invited_teacher_first_name,
44
+ "teacher_last_name": invited_teacher_last_name,
45
+ "teacher_email": invited_teacher_email,
46
+ "invite_teacher": "",
47
+ }
48
+ assert len(mail.outbox) == 0
49
+ response = client.post(dashboard_url, data)
50
+ assert response.status_code == 200
51
+ messages = list(response.context["messages"])
52
+ assert len(messages) == 1
53
+ assert (
54
+ str(messages[0])
55
+ == f"You have invited {invited_teacher_first_name} {invited_teacher_last_name} to your school."
56
+ )
57
+ assert len(mail.outbox) == 1
58
+ client.logout()
59
+
60
+ # Complete the registration as the invited teacher
61
+ invitation = SchoolTeacherInvitation.objects.get(invited_teacher_email=invited_teacher_email)
62
+ invitation_url = reverse("invited_teacher", kwargs={"token": invitation.token})
63
+ response = client.post(
64
+ invitation_url,
65
+ {
66
+ "teacher_signup-teacher_password": invited_teacher_password,
67
+ "teacher_signup-teacher_confirm_password": invited_teacher_password,
68
+ "teacher_signup-consent_ticked": "on",
69
+ },
70
+ )
71
+
72
+ # Check the message displays correctly after registration
73
+ messages = [m.message for m in get_messages(response.wsgi_request)]
74
+ assert len(messages) == 1
75
+ assert messages[0] == "Your account has been created successfully, please log in."
76
+
77
+ # Check that the teacher account is created successfully and linked to the school
78
+ invited_teacher = Teacher.objects.get(new_user__email=invited_teacher_email)
79
+ assert invited_teacher.new_user.first_name == invited_teacher_first_name
80
+ assert invited_teacher.new_user.last_name == invited_teacher_last_name
81
+ assert invited_teacher.school == school
82
+ assert invited_teacher.invited_by == teacher
83
+
84
+ # Check that the invitation is now inactive
85
+ with pytest.raises(SchoolTeacherInvitation.DoesNotExist):
86
+ SchoolTeacherInvitation.objects.get(invited_teacher_email=invited_teacher_email)
87
+ old_invitation = SchoolTeacherInvitation._base_manager.get(id=invitation.id)
88
+ assert old_invitation.invited_teacher_first_name != invited_teacher_first_name
89
+ assert old_invitation.invited_teacher_last_name != invited_teacher_last_name
90
+ assert old_invitation.invited_teacher_email != invited_teacher_email
91
+ assert not old_invitation.is_active
13
92
 
14
93
  def test_invite_teacher_fail(self):
15
- url = reverse("invite_teacher")
94
+ email, password = signup_teacher_directly()
95
+ school = create_organisation_directly(email)
96
+ create_class_directly(email)
97
+ teacher = Teacher.objects.get(new_user__email=email)
98
+
99
+ client = Client()
100
+ client.login(username=email, password=password)
101
+
102
+ # Try to invite a teacher with an invalid email address
103
+ dashboard_url = reverse("dashboard")
104
+ data = {
105
+ "teacher_first_name": "Valid",
106
+ "teacher_last_name": "Name",
107
+ "teacher_email": "invalid_email",
108
+ "invite_teacher": "",
109
+ }
110
+ response = client.post(dashboard_url, data)
111
+ assert len(response.context["invite_teacher_form"]["teacher_email"].errors) == 1
112
+ assert response.context["invite_teacher_form"]["teacher_email"].errors[0] == "Enter a valid email address."
113
+ client.logout()
114
+
115
+ # Try to access an invitation with an invalid token
116
+ invitation_url = reverse("invited_teacher", kwargs={"token": "1"})
117
+ response = client.get(invitation_url)
118
+ assert response.context["error_message"] == "Uh oh, the Invitation does not exist or it has expired. 😞"
119
+
120
+ # Try to access an expired invitation
121
+ expired_invitation = SchoolTeacherInvitation.objects.create(
122
+ token=uuid4().hex,
123
+ school=school,
124
+ from_teacher=teacher,
125
+ invited_teacher_first_name="Valid",
126
+ invited_teacher_last_name="Name",
127
+ invited_teacher_email="valid@cfl.com",
128
+ expiry=timezone.now() - timedelta(days=1),
129
+ )
130
+ invitation_url = reverse("invited_teacher", kwargs={"token": expired_invitation.token})
131
+ response = client.get(invitation_url)
132
+ assert response.context["error_message"] == "Uh oh, the Invitation does not exist or it has expired. 😞"
133
+
134
+ # Try to access an invitation for an account that already exists
135
+ same_account_invitation = SchoolTeacherInvitation.objects.create(
136
+ token=uuid4().hex,
137
+ school=school,
138
+ from_teacher=teacher,
139
+ invited_teacher_first_name="Valid",
140
+ invited_teacher_last_name="Name",
141
+ invited_teacher_email=email,
142
+ expiry=timezone.now() + timedelta(days=1),
143
+ )
144
+ invitation_url = reverse("invited_teacher", kwargs={"token": same_account_invitation.token})
145
+ response = client.get(invitation_url)
146
+ assert response.context["error_message"] == (
147
+ "It looks like an account is already registered with this email address. You will need to delete the "
148
+ "other account first or change the email associated with it in order to proceed. You will then be able to "
149
+ "access this page."
150
+ )
151
+
152
+ def test_invite_permissions(self):
153
+ # Create an admin and a standard teacher in the same school
154
+ admin_email, admin_password = signup_teacher_directly()
155
+ school = create_organisation_directly(admin_email)
156
+ create_class_directly(admin_email)
157
+
158
+ standard_email, standard_password = signup_teacher_directly()
159
+ join_teacher_to_organisation(standard_email, school.name)
160
+ create_class_directly(standard_email)
161
+
162
+ # Log in as standard teacher, try inviting a teacher, no invitation should be created
163
+ client = Client()
164
+ client.login(username=standard_email, password=standard_password)
165
+
166
+ dashboard_url = reverse("dashboard")
167
+ data = {
168
+ "teacher_first_name": "Valid",
169
+ "teacher_last_name": "Name",
170
+ "teacher_email": "new@teacher.com",
171
+ "invite_teacher": "",
172
+ }
173
+
174
+ response = client.post(dashboard_url, data)
175
+
176
+ assert response.status_code == 200
177
+ messages = list(response.context["messages"])
178
+ assert len(messages) == 0
179
+ assert len(mail.outbox) == 0
180
+ client.logout()
181
+
182
+ assert not SchoolTeacherInvitation.objects.filter(invited_teacher_email="new@teacher.com").exists()
183
+
184
+ # Log in as admin teacher to invite a teacher
185
+ client.login(username=admin_email, password=admin_password)
186
+
187
+ dashboard_url = reverse("dashboard")
188
+ data = {
189
+ "teacher_first_name": "Valid",
190
+ "teacher_last_name": "Name",
191
+ "teacher_email": "new@teacher.com",
192
+ "invite_teacher": "",
193
+ }
194
+
195
+ client.post(dashboard_url, data)
196
+ client.logout()
197
+
198
+ assert SchoolTeacherInvitation.objects.filter(invited_teacher_email="new@teacher.com").exists()
199
+ invite = SchoolTeacherInvitation.objects.get(invited_teacher_email="new@teacher.com")
200
+
201
+ # Log in as standard teacher, try resending and deleting the invitation, both should fail
202
+ client.login(username=standard_email, password=standard_password)
203
+
204
+ response = client.post(reverse("resend_invite_teacher", kwargs={"token": invite.token}))
205
+ message = list(response.wsgi_request._messages)[0].message
206
+ assert message == "You do not have permission to perform this action or the invite does not exist"
207
+
208
+ response = client.post(reverse("delete_teacher_invite", kwargs={"token": invite.token}))
209
+ message = list(response.wsgi_request._messages)[0].message
210
+ assert message == "You do not have permission to perform this action or the invite does not exist"
211
+
212
+ def test_delete_exception(self):
213
+ email, password = signup_teacher_directly()
214
+ create_organisation_directly(email)
215
+ create_class_directly(email)
216
+
16
217
  client = Client()
17
- data = {"email": "invalid_email"}
18
- response = client.post(url, data)
19
- self.assertTemplateNotUsed(response, "portal/email_invitation_sent.html")
218
+ client.login(username=email, password=password)
219
+
220
+ response = client.post(reverse("delete_teacher_invite", kwargs={"token": "2345678"}))
221
+ message = list(response.wsgi_request._messages)[0].message
222
+ assert message == "You do not have permission to perform this action or the invite does not exist"
223
+
224
+ response = client.post(reverse("resend_invite_teacher", kwargs={"token": "2345678"}))
225
+ message = list(response.wsgi_request._messages)[0].message
226
+ assert message == "You do not have permission to perform this action or the invite does not exist"
227
+
228
+
229
+ class TestTeacherInviteActions(BaseTest):
230
+ def test_revoke_and_make_admin_invite(self):
231
+ teacher_email, teacher_password = signup_teacher_directly()
232
+ create_organisation_directly(teacher_email)
233
+ class_name = "Test Class"
234
+ klass, _, _ = create_class_directly(teacher_email, class_name)
235
+
236
+ page = self.go_to_homepage()
237
+ page = page.go_to_teacher_login_page().login(teacher_email, teacher_password)
238
+
239
+ # Generate an invite and make admin
240
+ invite_data = {"teacher_first_name": "Adam", "teacher_last_name": "NotAdam", "teacher_email": "adam@adam.not"}
241
+ for key in invite_data.keys():
242
+ field = page.browser.find_element(By.NAME, key)
243
+ field.send_keys(invite_data[key])
244
+
245
+ # check if invite text for a user has been generated
246
+ page.browser.find_element(By.ID, "invite_teacher_button").click()
247
+ banner = page.browser.find_element(By.ID, "messages")
248
+ assert (
249
+ f"You have invited {invite_data['teacher_first_name']} {invite_data['teacher_last_name']} to your school."
250
+ in banner.text
251
+ )
252
+
253
+ # check if popup message appears and if the invite is changed to admin
254
+ sleep(1) # this HAS to be there because of the animation :/
255
+ page.browser.find_element(By.ID, "make_admin_button_invite").click()
256
+ sleep(1)
257
+ page.browser.find_element(By.ID, "add_admin_button").click()
258
+
259
+ invite = SchoolTeacherInvitation.objects.filter(invited_teacher_first_name="Adam")[0]
260
+ assert invite.invited_teacher_is_admin
261
+ banner = page.browser.find_element(By.ID, "messages")
262
+ assert "Administrator invite status has been given successfully" in banner.text
263
+
264
+ # revoke admin
265
+ page.browser.find_element(By.ID, "make_non_admin_button_invite").click()
266
+
267
+ banner = page.browser.find_element(By.ID, "messages")
268
+ assert "Administrator invite status has been revoked successfully" in banner.text
269
+
270
+ def test_delete_invite(self):
271
+ teacher_email, teacher_password = signup_teacher_directly()
272
+ create_organisation_directly(teacher_email)
273
+ class_name = "Test Class"
274
+ klass, _, _ = create_class_directly(teacher_email, class_name)
275
+
276
+ page = self.go_to_homepage()
277
+ page = page.go_to_teacher_login_page().login(teacher_email, teacher_password)
278
+
279
+ # Generate an invite
280
+ invite_data = {"teacher_first_name": "Adam", "teacher_last_name": "NotAdam", "teacher_email": "adam@adam.not"}
281
+ for key in invite_data.keys():
282
+ field = page.browser.find_element(By.NAME, key)
283
+ field.send_keys(invite_data[key])
284
+ page.browser.find_element(By.NAME, "invite_teacher_button").click()
285
+
286
+ # check object was created
287
+ invite_queryset = SchoolTeacherInvitation.objects.filter(invited_teacher_first_name="Adam")
288
+ assert len(invite_queryset) == 1
289
+ sleep(FADE_TIME)
290
+ # delete
291
+ delete_invite_button = WebDriverWait(self.selenium, WAIT_TIME).until(
292
+ EC.element_to_be_clickable((By.ID, "delete-invite"))
293
+ )
294
+ delete_invite_button.click()
295
+
296
+ empty_invite_queryset = SchoolTeacherInvitation.objects.filter(invited_teacher_first_name="Adam")
297
+ assert len(empty_invite_queryset) == 0
298
+
299
+ def test_resend_invite(self):
300
+ teacher_email, teacher_password = signup_teacher_directly()
301
+ create_organisation_directly(teacher_email)
302
+ class_name = "Test Class"
303
+ klass, _, _ = create_class_directly(teacher_email, class_name)
304
+
305
+ page = self.go_to_homepage()
306
+ page = page.go_to_teacher_login_page().login(teacher_email, teacher_password)
307
+
308
+ # Generate an invite
309
+ invite_data = {"teacher_first_name": "Adam", "teacher_last_name": "NotAdam", "teacher_email": "adam@adam.not"}
310
+ for key in invite_data.keys():
311
+ field = page.browser.find_element(By.NAME, key)
312
+ field.send_keys(invite_data[key])
313
+
314
+ page.browser.find_element(By.ID, "invite_teacher_button").click()
315
+
316
+ banner = page.browser.find_element(By.XPATH, '//*[@id="messages"]/div/div/div/div/div/p')
317
+ assert (
318
+ banner.text
319
+ == f"You have invited {invite_data['teacher_first_name']} {invite_data['teacher_last_name']} to your school."
320
+ )
321
+
322
+ # resend an invite
323
+ page.browser.find_element(By.ID, "resend-invite").click()
324
+
325
+ # check if invite was updated by 30 days (used 29 for rounding errors)
326
+ new_invite_expiry = SchoolTeacherInvitation.objects.filter(invited_teacher_first_name="Adam")[0].expiry
327
+ assert timezone.now() + timedelta(days=29) <= new_invite_expiry
@@ -1,5 +1,7 @@
1
1
  import time
2
+ from typing import Tuple
2
3
  from unittest import mock
4
+
3
5
  from _pytest.monkeypatch import MonkeyPatch
4
6
  from common.tests.utils.classes import create_class_directly
5
7
  from common.tests.utils.organisation import create_organisation_directly
@@ -9,6 +11,7 @@ from django.contrib import auth
9
11
  from django.contrib.auth.models import User
10
12
  from django.http import HttpResponseRedirect
11
13
  from django.test import Client, TestCase
14
+ from django.urls import reverse
12
15
 
13
16
  MOCKED_SESSION_EXPIRY_TIME = 5
14
17
 
@@ -29,9 +32,11 @@ class TestAdminAccessMiddleware(TestCase):
29
32
  self.email, self.password = self._setup_user()
30
33
 
31
34
  self.monkeypatch = MonkeyPatch()
32
- self.monkeypatch.setattr("deploy.middleware.admin_access.MODULE_NAME", "test")
35
+ self.monkeypatch.setattr(
36
+ "deploy.middleware.admin_access.MODULE_NAME", "test"
37
+ )
33
38
 
34
- def _setup_user(self) -> (str, str):
39
+ def _setup_user(self) -> Tuple[str, str]:
35
40
  email, password = signup_teacher_directly()
36
41
  create_organisation_directly(email)
37
42
  _, _, access_code = create_class_directly(email)
@@ -97,7 +102,9 @@ class TestAdminAccessMiddleware(TestCase):
97
102
  return_value=True,
98
103
  autospec=True,
99
104
  )
100
- def test_superuser_with_2FA_can_access_admin_site(self, mock_using_two_factor):
105
+ def test_superuser_with_2FA_can_access_admin_site(
106
+ self, mock_using_two_factor
107
+ ):
101
108
  self._make_user_superuser()
102
109
 
103
110
  self.client.login(username=self.email, password=self.password)
@@ -120,10 +127,10 @@ class TestSecurityMiddleware(TestCase):
120
127
  response = client.get("/")
121
128
 
122
129
  assert response.status_code == 200
123
- assert response._headers["cache-control"][1] == "private"
124
- assert response._headers["x-content-type-options"][1] == "nosniff"
125
- assert response._headers["x-frame-options"][1] == "SAMEORIGIN"
126
- assert response._headers["x-xss-protection"][1] == "0"
130
+ assert response.headers["cache-control"] == "private"
131
+ assert response.headers["x-content-type-options"] == "nosniff"
132
+ assert response.headers["x-frame-options"] == "DENY"
133
+ assert response.headers["x-xss-protection"] == "1; mode=block"
127
134
 
128
135
 
129
136
  class TestSessionTimeoutMiddleware(TestCase):
@@ -141,7 +148,7 @@ class TestSessionTimeoutMiddleware(TestCase):
141
148
  "deploy.middleware.session_timeout.SESSION_EXPIRY_TIME", 5
142
149
  )
143
150
 
144
- def _setup_user(self) -> (str, str):
151
+ def _setup_user(self) -> Tuple[str, str]:
145
152
  email, password = signup_teacher_directly()
146
153
  create_organisation_directly(email)
147
154
  _, _, access_code = create_class_directly(email)
@@ -159,7 +166,87 @@ class TestSessionTimeoutMiddleware(TestCase):
159
166
 
160
167
  time.sleep(MOCKED_SESSION_EXPIRY_TIME)
161
168
 
162
- self.client.get("/")
169
+ response = self.client.get("/")
163
170
  user = auth.get_user(self.client)
164
171
 
165
172
  assert not user.is_authenticated
173
+
174
+ messages = list(response.context["messages"])
175
+ assert len(messages) > 0
176
+ assert str(messages[0]) == "You have been logged out due to inactivity."
177
+
178
+ def test_session_reset(self):
179
+ self.client.login(username=self.email, password=self.password)
180
+
181
+ self.client.get("/")
182
+ user = auth.get_user(self.client)
183
+
184
+ assert user.is_authenticated
185
+
186
+ time.sleep(MOCKED_SESSION_EXPIRY_TIME * 0.66)
187
+
188
+ url = reverse("reset_session_time")
189
+ self.client.get(url)
190
+
191
+ time.sleep(MOCKED_SESSION_EXPIRY_TIME * 0.66)
192
+
193
+ self.client.get("/")
194
+ user = auth.get_user(self.client)
195
+
196
+ assert user.is_authenticated
197
+
198
+
199
+ class TestScreentimeWarningMiddleware(TestCase):
200
+ """
201
+ This tests the ScreentimeWarningMiddleware class and popup timeout is set properly.
202
+ """
203
+
204
+ def setUp(self) -> None:
205
+ self.client = Client()
206
+ self.email, self.password = self._setup_user()
207
+
208
+ def _setup_user(self) -> Tuple[str, str]:
209
+ email, password = signup_teacher_directly()
210
+ create_organisation_directly(email)
211
+ _, _, access_code = create_class_directly(email)
212
+ create_school_student_directly(access_code)
213
+
214
+ return email, password
215
+
216
+ def test_screentime_warning_timeout(self):
217
+ # Timeout should not be there if the user is not logged in
218
+ session = self.client.session
219
+ assert "screentime_warning_timeout" not in session
220
+
221
+ # Log in as a teacher
222
+ self.client.login(username=self.email, password=self.password)
223
+
224
+ # Check the screentime_warning_timeout decreases after consecutive requests
225
+ self.client.get("/")
226
+ session = self.client.session
227
+ assert "screentime_warning_timeout" in session
228
+ previous_screentime_warning_timeout = session[
229
+ "screentime_warning_timeout"
230
+ ]
231
+
232
+ self.client.get("/")
233
+ session = self.client.session
234
+ assert "screentime_warning_timeout" in session
235
+ new_screentime_warning_timeout = session["screentime_warning_timeout"]
236
+
237
+ assert (
238
+ new_screentime_warning_timeout < previous_screentime_warning_timeout
239
+ )
240
+
241
+ # Check the reset_screentime_warning API resets the timeout
242
+ url = reverse("reset_screentime_warning")
243
+ self.client.get(url)
244
+ self.client.get("/")
245
+ session = self.client.session
246
+ assert "screentime_warning_timeout" in session
247
+ renewed_screentime_warning_timeout = session[
248
+ "screentime_warning_timeout"
249
+ ]
250
+ assert (
251
+ renewed_screentime_warning_timeout > new_screentime_warning_timeout
252
+ )