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,21 +1,30 @@
1
1
  import csv
2
2
  import io
3
3
  import json
4
- from datetime import timedelta, date
4
+ from datetime import date, datetime, timedelta
5
+ from unittest.mock import ANY, Mock, patch
5
6
 
6
- import PyPDF2
7
7
  import pytest
8
- from aimmo.models import Game
9
- from common.models import Teacher, UserSession, Student, Class, DailyActivity
8
+ from common.mail import campaign_ids
9
+ from common.models import (
10
+ Class,
11
+ DailyActivity,
12
+ School,
13
+ Student,
14
+ Teacher,
15
+ TotalActivity,
16
+ UserProfile,
17
+ UserSession,
18
+ )
10
19
  from common.tests.utils.classes import create_class_directly
11
20
  from common.tests.utils.organisation import (
12
21
  create_organisation_directly,
13
22
  join_teacher_to_organisation,
14
23
  )
15
24
  from common.tests.utils.student import (
25
+ create_independent_student_directly,
16
26
  create_school_student_directly,
17
27
  create_student_with_direct_login,
18
- create_independent_student_directly,
19
28
  )
20
29
  from common.tests.utils.teacher import signup_teacher_directly
21
30
  from django.contrib.auth.models import User
@@ -25,11 +34,21 @@ from django.utils import timezone
25
34
  from game.models import Level
26
35
  from game.tests.utils.attempt import create_attempt
27
36
  from game.tests.utils.level import create_save_level
37
+ from pypdf import PdfReader
38
+ from rest_framework.test import APIClient, APITestCase
28
39
 
29
40
  from deploy import captcha
41
+ from portal.templatetags.app_tags import is_logged_in_as_admin_teacher
42
+ from portal.views.api import anonymise
43
+ from portal.views.cron.user import (
44
+ USER_DELETE_UNVERIFIED_ACCOUNT_DAYS,
45
+ USER_2ND_INACTIVE_REMINDER_DAYS,
46
+ USER_FINAL_INACTIVE_REMINDER_DAYS,
47
+ USER_RETENTION_PERIOD,
48
+ )
30
49
  from portal.views.teacher.teach import (
31
- REMINDER_CARDS_PDF_ROWS,
32
50
  REMINDER_CARDS_PDF_COLUMNS,
51
+ REMINDER_CARDS_PDF_ROWS,
33
52
  REMINDER_CARDS_PDF_WARNING_TEXT,
34
53
  count_student_details_click,
35
54
  )
@@ -39,8 +58,11 @@ class TestTeacherViews(TestCase):
39
58
  @classmethod
40
59
  def setUpTestData(cls):
41
60
  cls.email, cls.password = signup_teacher_directly()
61
+ cls.school = create_organisation_directly(cls.email)
42
62
  _, _, cls.class_access_code = create_class_directly(cls.email)
43
- _, _, cls.student = create_school_student_directly(cls.class_access_code)
63
+ _, cls.password_student, cls.student = create_school_student_directly(
64
+ cls.class_access_code
65
+ )
44
66
 
45
67
  def login(self):
46
68
  c = Client()
@@ -49,7 +71,9 @@ class TestTeacherViews(TestCase):
49
71
 
50
72
  def test_reminder_cards(self):
51
73
  c = self.login()
52
- url = reverse("teacher_print_reminder_cards", args=[self.class_access_code])
74
+ url = reverse(
75
+ "teacher_print_reminder_cards", args=[self.class_access_code]
76
+ )
53
77
 
54
78
  # First test with 2 dummy students
55
79
  NAME1 = "Test name"
@@ -69,10 +93,10 @@ class TestTeacherViews(TestCase):
69
93
 
70
94
  # read PDF, check there's only 1 page and that the correct student details show
71
95
  with io.BytesIO(response.content) as pdf_file:
72
- file_reader = PyPDF2.PdfFileReader(pdf_file)
73
- assert file_reader.getNumPages() == 1
96
+ file_reader = PdfReader(pdf_file)
97
+ assert len(file_reader.pages) == 1
74
98
 
75
- page_text = file_reader.getPage(0).extractText()
99
+ page_text = file_reader.pages[0].extract_text()
76
100
  assert NAME1 in page_text
77
101
  assert NAME2 in page_text
78
102
  assert PASSWORD1 in page_text
@@ -83,7 +107,9 @@ class TestTeacherViews(TestCase):
83
107
  # page number
84
108
  students_per_page = REMINDER_CARDS_PDF_ROWS * REMINDER_CARDS_PDF_COLUMNS
85
109
  for _ in range(len(studentlist), students_per_page + 1):
86
- studentlist.append({"name": NAME1, "password": PASSWORD1, "login_url": URL})
110
+ studentlist.append(
111
+ {"name": NAME1, "password": PASSWORD1, "login_url": URL}
112
+ )
87
113
 
88
114
  assert len(studentlist) == students_per_page + 1
89
115
 
@@ -93,11 +119,11 @@ class TestTeacherViews(TestCase):
93
119
 
94
120
  # Check there are 2 pages and that each page contains the warning text
95
121
  with io.BytesIO(response.content) as pdf_file:
96
- file_reader = PyPDF2.PdfFileReader(pdf_file)
97
- assert file_reader.getNumPages() == 2
122
+ file_reader = PdfReader(pdf_file)
123
+ assert len(file_reader.pages) == 2
98
124
 
99
- page1_text = file_reader.getPage(0).extractText()
100
- page2_text = file_reader.getPage(1).extractText()
125
+ page1_text = file_reader.pages[0].extract_text()
126
+ page2_text = file_reader.pages[1].extract_text()
101
127
  assert REMINDER_CARDS_PDF_WARNING_TEXT in page1_text
102
128
  assert REMINDER_CARDS_PDF_WARNING_TEXT in page2_text
103
129
 
@@ -122,7 +148,9 @@ class TestTeacherViews(TestCase):
122
148
  reader = csv.reader(io.StringIO(content))
123
149
 
124
150
  access_code = self.class_access_code
125
- class_url = reverse("student_login", kwargs={"access_code": access_code})
151
+ class_url = reverse(
152
+ "student_login", kwargs={"access_code": access_code}
153
+ )
126
154
  row0 = next(reader)
127
155
  assert row0[0].strip() == access_code
128
156
  assert class_url in row0[1].strip()
@@ -147,9 +175,9 @@ class TestTeacherViews(TestCase):
147
175
 
148
176
  def test_organisation_kick_has_correct_permissions(self):
149
177
  teacher2_email, _ = signup_teacher_directly()
150
- org_name, org_postcode = create_organisation_directly(self.email)
151
- join_teacher_to_organisation(self.email, org_name, org_postcode, is_admin=True)
152
- join_teacher_to_organisation(teacher2_email, org_name, org_postcode)
178
+ school = create_organisation_directly(self.email)
179
+ join_teacher_to_organisation(self.email, school.name, is_admin=True)
180
+ join_teacher_to_organisation(teacher2_email, school.name)
153
181
  teacher2_id = Teacher.objects.get(new_user__email=teacher2_email).id
154
182
 
155
183
  client = self.login()
@@ -161,7 +189,9 @@ class TestTeacherViews(TestCase):
161
189
 
162
190
  def test_daily_activity_student_details(self):
163
191
  c = self.login()
164
- url = reverse("teacher_print_reminder_cards", args=[self.class_access_code])
192
+ url = reverse(
193
+ "teacher_print_reminder_cards", args=[self.class_access_code]
194
+ )
165
195
 
166
196
  data = {
167
197
  "data": json.dumps(
@@ -170,7 +200,7 @@ class TestTeacherViews(TestCase):
170
200
  "name": self.student.new_user.first_name,
171
201
  "password": self.student.new_user.password,
172
202
  "login_url": self.student.login_id,
173
- },
203
+ }
174
204
  ]
175
205
  )
176
206
  }
@@ -207,6 +237,72 @@ class TestTeacherViews(TestCase):
207
237
  with pytest.raises(Exception):
208
238
  count_student_details_click("Wrong download method")
209
239
 
240
+ def test_release_verified_student(self):
241
+ c = Client()
242
+ student_login_url = reverse(
243
+ "student_login", args=[self.class_access_code]
244
+ )
245
+ response = c.post(
246
+ student_login_url,
247
+ {
248
+ "username": self.student.new_user.first_name,
249
+ "password": self.password_student,
250
+ },
251
+ )
252
+ assert response.status_code == 302
253
+
254
+ student = Student.objects.get(pk=self.student.pk)
255
+
256
+ assert student.user.is_verified
257
+
258
+ c.logout()
259
+ c.login(username=self.email, password=self.password)
260
+
261
+ teacher = Teacher.objects.factory(
262
+ "the", "teacher", "theteacher@foo.com", "password"
263
+ )
264
+ level = Level.objects.create()
265
+
266
+ level.owner = student.new_user.userprofile
267
+ level.shared_with.add(teacher.new_user)
268
+ level.save()
269
+
270
+ students_levels = Level.objects.filter(
271
+ owner=student.new_user.userprofile
272
+ ).all()
273
+
274
+ for level in students_levels.all():
275
+ assert level.shared_with.exists()
276
+
277
+ release_url = reverse(
278
+ "teacher_dismiss_students", args=[self.class_access_code]
279
+ )
280
+ response = c.post(
281
+ release_url,
282
+ {
283
+ "form-TOTAL_FORMS": 1,
284
+ "form-INITIAL_FORMS": 1,
285
+ "form-MIN_NUM_FORMS": 0,
286
+ "form-MAX_NUM_FORMS": 1000,
287
+ "form-0-orig_name": self.student.new_user.first_name,
288
+ "form-0-name": self.student.new_user.first_name,
289
+ "form-0-email": "independent@gmail.com",
290
+ "form-0-confirm_email": "independent@gmail.com",
291
+ "submit_dismiss": "",
292
+ },
293
+ )
294
+ assert response.status_code == 302
295
+
296
+ student = Student.objects.get(pk=self.student.pk)
297
+ assert not student.user.is_verified
298
+
299
+ students_levels = Level.objects.filter(
300
+ owner=student.new_user.userprofile
301
+ ).all()
302
+
303
+ for level in students_levels.all():
304
+ assert not level.shared_with.exists()
305
+
210
306
 
211
307
  class TestLoginViews(TestCase):
212
308
  @classmethod
@@ -260,11 +356,15 @@ class TestLoginViews(TestCase):
260
356
 
261
357
  if next_url:
262
358
  url = (
263
- reverse("student_login", kwargs={"access_code": class_access_code})
359
+ reverse(
360
+ "student_login", kwargs={"access_code": class_access_code}
361
+ )
264
362
  + "?next=/"
265
363
  )
266
364
  else:
267
- url = reverse("student_login", kwargs={"access_code": class_access_code})
365
+ url = reverse(
366
+ "student_login", kwargs={"access_code": class_access_code}
367
+ )
268
368
 
269
369
  c = Client()
270
370
  response = c.post(url, {"username": name, "password": password})
@@ -316,13 +416,17 @@ class TestLoginViews(TestCase):
316
416
  c = Client()
317
417
 
318
418
  resp = c.post(
319
- reverse("student_login_access_code"), {"access_code": class_access_code}
419
+ reverse("student_login_access_code"),
420
+ {"access_code": class_access_code},
320
421
  )
321
422
  assert resp.status_code == 302
322
423
  nexturl = resp.url
323
424
  assert nexturl == reverse(
324
425
  "student_login",
325
- kwargs={"access_code": class_access_code, "login_type": "classform"},
426
+ kwargs={
427
+ "access_code": class_access_code,
428
+ "login_type": "classform",
429
+ },
326
430
  )
327
431
  c.post(nexturl, {"username": name, "password": password})
328
432
 
@@ -343,7 +447,9 @@ class TestLoginViews(TestCase):
343
447
  _, _, name, password, class_access_code = self._set_up_test_data()
344
448
 
345
449
  c = Client()
346
- url = reverse("student_login", kwargs={"access_code": class_access_code})
450
+ url = reverse(
451
+ "student_login", kwargs={"access_code": class_access_code}
452
+ )
347
453
  c.post(url, {"username": name, "password": password})
348
454
 
349
455
  # check if there's a UserSession data within the last 10 secs
@@ -364,8 +470,10 @@ class TestLoginViews(TestCase):
364
470
  randomname = "randomname"
365
471
 
366
472
  c = Client()
367
- url = reverse("student_login", kwargs={"access_code": class_access_code})
368
- resp = c.post(url, {"username": randomname, "password": "xx"})
473
+ url = reverse(
474
+ "student_login", kwargs={"access_code": class_access_code}
475
+ )
476
+ c.post(url, {"username": randomname, "password": "xx"})
369
477
 
370
478
  # check if there's a UserSession data within the last 10 secs
371
479
  now = timezone.now()
@@ -390,7 +498,9 @@ class TestLoginViews(TestCase):
390
498
 
391
499
  def test_student_direct_login(self):
392
500
  _, _, _, _, class_access_code = self._set_up_test_data()
393
- student, login_id, _, _ = create_student_with_direct_login(class_access_code)
501
+ student, login_id, _, _ = create_student_with_direct_login(
502
+ class_access_code
503
+ )
394
504
 
395
505
  c = Client()
396
506
  assert c.login(user_id=student.new_user.id, login_id=login_id) == True
@@ -433,7 +543,7 @@ class TestLoginViews(TestCase):
433
543
 
434
544
 
435
545
  class TestViews(TestCase):
436
- def test_covid_response_page(self):
546
+ def test_home_learning(self):
437
547
  c = Client()
438
548
  home_url = reverse("home")
439
549
  response = c.get(home_url)
@@ -445,11 +555,11 @@ class TestViews(TestCase):
445
555
 
446
556
  expected_html = '<a href="/home-learning">Home learning</a>'
447
557
 
448
- self.assertIn(expected_html, html)
558
+ assert expected_html in html
449
559
 
450
560
  response = c.get(page_url)
451
561
 
452
- self.assertEquals(200, response.status_code)
562
+ assert response.status_code == 200
453
563
 
454
564
  def test_contributor(self):
455
565
  c = Client()
@@ -461,56 +571,79 @@ class TestViews(TestCase):
461
571
  response = c.get(page_url)
462
572
  assert response.status_code == 200
463
573
 
574
+ def test_ten_year_map(self):
575
+ c = Client()
576
+ page_url = reverse("celebrate")
577
+ response = c.get(page_url)
578
+ assert response.status_code == 200
579
+
464
580
  def test_student_dashboard_view(self):
465
581
  teacher_email, teacher_password = signup_teacher_directly()
466
582
  create_organisation_directly(teacher_email)
467
583
  klass, _, class_access_code = create_class_directly(teacher_email)
468
- student_name, student_password, student = create_school_student_directly(
469
- class_access_code
470
- )
584
+ (
585
+ student_name,
586
+ student_password,
587
+ student,
588
+ ) = create_school_student_directly(class_access_code)
471
589
 
472
590
  # Expected context data when a student hasn't played anything yet
473
591
  EXPECTED_DATA_FIRST_LOGIN = {
474
- "num_completed": 0,
475
- "num_top_scores": 0,
476
- "total_score": 0,
477
- "total_available_score": 2070,
592
+ "rapid_router": {
593
+ "num_completed": 0,
594
+ "num_top_scores": 0,
595
+ "total_score": 0,
596
+ "total_available_score": 1450,
597
+ },
598
+ "python_den": {
599
+ "num_completed": 0,
600
+ "num_top_scores": 0,
601
+ "total_score": 0,
602
+ "total_available_score": 830,
603
+ },
478
604
  }
479
605
 
480
606
  # Expected context data when a student has attempted some RR levels
481
607
  EXPECTED_DATA_WITH_ATTEMPTS = {
482
- "num_completed": 2,
483
- "num_top_scores": 1,
484
- "total_score": 39,
485
- "total_available_score": 2070,
608
+ "rapid_router": {
609
+ "num_completed": 2,
610
+ "num_top_scores": 1,
611
+ "total_score": 39,
612
+ "total_available_score": 1450,
613
+ },
614
+ "python_den": {
615
+ "num_completed": 2,
616
+ "num_top_scores": 2,
617
+ "total_score": 30,
618
+ "total_available_score": 830,
619
+ },
486
620
  }
487
621
 
488
- # Expected context data when a student has also attempted some custom RR levels
622
+ # Expected context data when a student has also attempted some custom RR
623
+ # levels
489
624
  EXPECTED_DATA_WITH_CUSTOM_ATTEMPTS = {
490
- "num_completed": 2,
491
- "num_top_scores": 1,
492
- "total_score": 39,
493
- "total_available_score": 2070,
494
- "total_custom_score": 10,
495
- "total_custom_available_score": 20,
496
- }
497
-
498
- # Expected context data when a student also has access to a Kurono game
499
- EXPECTED_DATA_WITH_KURONO_GAME = {
500
- "num_completed": 2,
501
- "num_top_scores": 1,
502
- "total_score": 39,
503
- "total_available_score": 2070,
504
- "total_custom_score": 10,
505
- "total_custom_available_score": 20,
506
- "worksheet_id": 3,
507
- "worksheet_image": "images/worksheets/ancient.jpg",
625
+ "rapid_router": {
626
+ "num_completed": 2,
627
+ "num_top_scores": 1,
628
+ "total_score": 39,
629
+ "total_available_score": 1450,
630
+ "total_custom_score": 10,
631
+ "total_custom_available_score": 20,
632
+ },
633
+ "python_den": {
634
+ "num_completed": 2,
635
+ "num_top_scores": 2,
636
+ "total_score": 30,
637
+ "total_available_score": 830,
638
+ },
508
639
  }
509
640
 
510
641
  c = Client()
511
642
 
512
643
  # Login and check initial data
513
- url = reverse("student_login", kwargs={"access_code": class_access_code})
644
+ url = reverse(
645
+ "student_login", kwargs={"access_code": class_access_code}
646
+ )
514
647
  c.post(url, {"username": student_name, "password": student_password})
515
648
 
516
649
  student_dashboard_url = reverse("student_details")
@@ -519,26 +652,33 @@ class TestViews(TestCase):
519
652
  assert response.status_code == 200
520
653
  assert response.context_data == EXPECTED_DATA_FIRST_LOGIN
521
654
 
522
- # Attempt the first two levels, one perfect attempt, one not
655
+ # Attempt the first two RR levels, one perfect attempt, one not
523
656
  level1 = Level.objects.get(name="1")
524
657
  level2 = Level.objects.get(name="2")
525
658
 
526
659
  create_attempt(student, level1, 20)
527
660
  create_attempt(student, level2, 19)
528
661
 
662
+ # Attempt the first and fourth Python Den levels, both perfect
663
+ level1001 = Level.objects.get(name="1001")
664
+ level1004 = Level.objects.get(name="1004")
665
+
666
+ create_attempt(student, level1001, 20)
667
+ create_attempt(student, level1004, 10)
668
+
529
669
  response = c.get(student_dashboard_url)
530
670
 
531
671
  assert response.status_code == 200
532
672
  assert response.context_data == EXPECTED_DATA_WITH_ATTEMPTS
533
673
 
534
- # Teacher creates 3 custom levels, only shares the first 2 with the student.
535
- # Check that the total available score only includes the levels shared with the
536
- # student. Student attempts one level only.
674
+ # Teacher creates 3 custom levels, only shares the first 2 with the
675
+ # student. Check that the total available score only includes the
676
+ # levels shared with the student. Student attempts one level only.
537
677
  custom_level1_id = create_save_level(student.class_field.teacher)
538
678
  custom_level2_id = create_save_level(student.class_field.teacher)
539
679
  create_save_level(student.class_field.teacher)
540
- custom_level1 = Level.objects.get(id=custom_level1_id)
541
- custom_level2 = Level.objects.get(id=custom_level2_id)
680
+ custom_level1 = Level.objects.get(id=custom_level1_id.id)
681
+ custom_level2 = Level.objects.get(id=custom_level2_id.id)
542
682
 
543
683
  student.new_user.shared.add(custom_level1, custom_level2)
544
684
  student.new_user.save()
@@ -550,11 +690,695 @@ class TestViews(TestCase):
550
690
  assert response.status_code == 200
551
691
  assert response.context_data == EXPECTED_DATA_WITH_CUSTOM_ATTEMPTS
552
692
 
553
- # Link Kurono game to student's class
554
- game = Game(game_class=klass, worksheet_id=3)
555
- game.save()
693
+ @patch("portal.views.registration.send_dotdigital_email")
694
+ def test_delete_account(self, mock_send_dotdigital_email: Mock):
695
+ email, password = signup_teacher_directly()
696
+ u = User.objects.get(email=email)
697
+ usrid = u.id
556
698
 
557
- response = c.get(student_dashboard_url)
699
+ c = Client()
700
+ url = reverse("teacher_login")
701
+ c.post(
702
+ url,
703
+ {
704
+ "auth-username": email,
705
+ "auth-password": password,
706
+ "teacher_login_view-current_step": "auth",
707
+ },
708
+ )
558
709
 
559
- assert response.status_code == 200
560
- assert response.context_data == EXPECTED_DATA_WITH_KURONO_GAME
710
+ # fail to delete with incorrect password
711
+ url = reverse("delete_account")
712
+ response = c.post(url, {"password": "wrongPassword"})
713
+
714
+ assert response.status_code == 302
715
+ mock_send_dotdigital_email.assert_not_called()
716
+
717
+ # user has not been anonymised
718
+ u = User.objects.get(email=email)
719
+ assert u.id == usrid
720
+
721
+ # try again with the correct password
722
+ url = reverse("delete_account")
723
+ response = c.post(
724
+ url, {"password": password, "unsubscribe_newsletter": "on"}
725
+ )
726
+
727
+ assert response.status_code == 302
728
+ mock_send_dotdigital_email.assert_called_once()
729
+ assert response.url == reverse("home")
730
+
731
+ # user has been anonymised
732
+ u = User.objects.get(id=usrid)
733
+ assert u.first_name == "Deleted"
734
+ assert not u.is_active
735
+
736
+ assert c.login(username=email, password=password) == False
737
+
738
+ @patch("portal.views.registration.send_dotdigital_email")
739
+ def test_delete_account_admin(self, mock_send_dotdigital_email: Mock):
740
+ """test the passing of admin role after deletion of an admin account"""
741
+
742
+ email1, password1 = signup_teacher_directly()
743
+ email2, password2 = signup_teacher_directly()
744
+ email3, password3 = signup_teacher_directly()
745
+ email4, password4 = signup_teacher_directly()
746
+
747
+ user1 = User.objects.get(email=email1)
748
+ user1.last_name = "Amir"
749
+ user1.save()
750
+ usrid1 = user1.id
751
+
752
+ user2 = User.objects.get(email=email2)
753
+ user2.last_name = "Bee"
754
+ user2.save()
755
+ usrid2 = user2.id
756
+
757
+ user3 = User.objects.get(email=email3)
758
+ user3.last_name = "Jung"
759
+ user3.save()
760
+ usrid3 = user3.id
761
+
762
+ user4 = User.objects.get(email=email4)
763
+ user4.last_name = "Kook"
764
+ user4.save()
765
+ usrid4 = user4.id
766
+
767
+ school = create_organisation_directly(email1)
768
+ klass, class_name, access_code_1 = create_class_directly(email1)
769
+ class_id = klass.id
770
+ _, _, student = create_school_student_directly(access_code_1)
771
+ student_user_id = student.new_user.id
772
+
773
+ join_teacher_to_organisation(email2, school.name)
774
+ _, _, access_code_2 = create_class_directly(email2)
775
+ create_school_student_directly(access_code_2)
776
+
777
+ join_teacher_to_organisation(email3, school.name)
778
+ join_teacher_to_organisation(email4, school.name)
779
+
780
+ c = Client()
781
+ url = reverse("teacher_login")
782
+ c.post(
783
+ url,
784
+ {
785
+ "auth-username": email1,
786
+ "auth-password": password1,
787
+ "teacher_login_view-current_step": "auth",
788
+ },
789
+ )
790
+
791
+ # delete teacher1 account
792
+ url = reverse("delete_account")
793
+ c.post(url, {"password": password1})
794
+ mock_send_dotdigital_email.assert_called_once()
795
+
796
+ # user has been anonymised
797
+ u = User.objects.get(id=usrid1)
798
+ assert not u.is_active
799
+
800
+ # check that the class and student have been anonymised
801
+ assert not Class._base_manager.get(pk=class_id).is_active
802
+ student_user1 = User.objects.get(id=student_user_id)
803
+ assert not student_user1.is_active
804
+
805
+ school_id = school.id
806
+ school_name = school.name
807
+ teachers = Teacher.objects.filter(school=school).order_by(
808
+ "new_user__last_name", "new_user__first_name"
809
+ )
810
+ assert len(teachers) == 3
811
+
812
+ # one of the remaining teachers should be admin (the second in our case, as it's alphabetical)
813
+ u = User.objects.get(id=usrid2)
814
+ assert u.new_teacher.is_admin
815
+ u = User.objects.get(id=usrid3)
816
+ assert not u.new_teacher.is_admin
817
+ u = User.objects.get(id=usrid4)
818
+ assert not u.new_teacher.is_admin
819
+
820
+ # make teacher 3 admin
821
+ user3.new_teacher.is_admin = True
822
+ user3.new_teacher.save()
823
+
824
+ url = reverse("teacher_login")
825
+ c.post(
826
+ url,
827
+ {
828
+ "auth-username": email3,
829
+ "auth-password": password3,
830
+ "teacher_login_view-current_step": "auth",
831
+ },
832
+ )
833
+
834
+ # now delete teacher3 account
835
+ url = reverse("delete_account")
836
+ c.post(url, {"password": password3})
837
+ self.assertEqual(mock_send_dotdigital_email.call_count, 2)
838
+
839
+ # 2 teachers left
840
+ teachers = Teacher.objects.filter(school=school).order_by(
841
+ "new_user__last_name", "new_user__first_name"
842
+ )
843
+ assert len(teachers) == 2
844
+
845
+ # teacher2 should still be admin, teacher4 is not passed admin role because there is teacher2
846
+ u = User.objects.get(id=usrid2)
847
+ assert u.new_teacher.is_admin
848
+ u = User.objects.get(id=usrid4)
849
+ assert not u.new_teacher.is_admin
850
+
851
+ # delete teacher4
852
+ anonymise(user4)
853
+
854
+ teachers = Teacher.objects.filter(school=school).order_by(
855
+ "new_user__last_name", "new_user__first_name"
856
+ )
857
+ assert len(teachers) == 1
858
+ u = User.objects.get(id=usrid2)
859
+ assert u.new_teacher.is_admin
860
+
861
+ # delete teacher2 (the last one left)
862
+ url = reverse("teacher_login")
863
+ c.post(
864
+ url,
865
+ {
866
+ "auth-username": email2,
867
+ "auth-password": password2,
868
+ "teacher_login_view-current_step": "auth",
869
+ },
870
+ )
871
+
872
+ url = reverse("delete_account")
873
+ c.post(url, {"password": password2})
874
+ self.assertEqual(mock_send_dotdigital_email.call_count, 3)
875
+
876
+ # school should be anonymised
877
+ school = School._base_manager.get(id=school_id)
878
+ assert school.name != school_name
879
+ assert not school.is_active
880
+
881
+ with pytest.raises(School.DoesNotExist):
882
+ School.objects.get(id=school_id)
883
+
884
+ def test_legal_pages_load(self):
885
+ c = Client()
886
+
887
+ assert c.get(reverse("privacy_notice")).status_code == 200
888
+ assert c.get(reverse("terms")).status_code == 200
889
+
890
+ def test_logged_in_as_admin_check(self):
891
+ email1, password1 = signup_teacher_directly()
892
+ email2, password2 = signup_teacher_directly()
893
+ school = create_organisation_directly(email1)
894
+ join_teacher_to_organisation(email2, school.name)
895
+
896
+ teacher1 = Teacher.objects.get(new_user__username=email1)
897
+ teacher2 = Teacher.objects.get(new_user__username=email2)
898
+
899
+ c = Client()
900
+
901
+ c.login(username=email1, password=password1)
902
+
903
+ assert is_logged_in_as_admin_teacher(teacher1.new_user)
904
+
905
+ c.logout()
906
+
907
+ c.login(username=email2, password=password2)
908
+
909
+ assert not is_logged_in_as_admin_teacher(teacher2.new_user)
910
+
911
+ c.logout()
912
+
913
+ @patch("common.helpers.emails.send_dotdigital_email")
914
+ def test_registrations_increment_data(
915
+ self, mock_send_dotdigital_email: Mock
916
+ ):
917
+ c = Client()
918
+
919
+ total_activity = TotalActivity.objects.get(id=1)
920
+ teacher_registration_count = total_activity.teacher_registrations
921
+ student_registration_count = total_activity.student_registrations
922
+ independent_registration_count = (
923
+ total_activity.independent_registrations
924
+ )
925
+
926
+ response = c.post(
927
+ reverse("register"),
928
+ {
929
+ "teacher_signup-teacher_first_name": "Test Name",
930
+ "teacher_signup-teacher_last_name": "Test Last Name",
931
+ "teacher_signup-teacher_email": "test@email.com",
932
+ "teacher_signup-consent_ticked": "on",
933
+ "teacher_signup-teacher_password": "$RFVBGT%6yhn$RFVBGT%6yhn$RFVBGT%6yhn$RFVBGT%6yhn",
934
+ "teacher_signup-teacher_confirm_password": "$RFVBGT%6yhn$RFVBGT%6yhn$RFVBGT%6yhn$RFVBGT%6yhn",
935
+ "g-recaptcha-response": "something",
936
+ },
937
+ )
938
+
939
+ assert response.status_code == 302
940
+ mock_send_dotdigital_email.assert_called_once()
941
+
942
+ total_activity = TotalActivity.objects.get(id=1)
943
+
944
+ assert (
945
+ total_activity.teacher_registrations
946
+ == teacher_registration_count + 1
947
+ )
948
+
949
+ response = c.post(
950
+ reverse("register"),
951
+ {
952
+ "independent_student_signup-date_of_birth_day": 7,
953
+ "independent_student_signup-date_of_birth_month": 10,
954
+ "independent_student_signup-date_of_birth_year": 1997,
955
+ "independent_student_signup-name": "Test Name",
956
+ "independent_student_signup-email": "test@indy-email.com",
957
+ "independent_student_signup-consent_ticked": "on",
958
+ "independent_student_signup-password": "$RFVBGT%6yhn$RFVBGT%6yhn$RFVBGT%6yhn$RFVBGT%6yhn",
959
+ "independent_student_signup-confirm_password": "$RFVBGT%6yhn$RFVBGT%6yhn$RFVBGT%6yhn$RFVBGT%6yhn",
960
+ "g-recaptcha-response": "something",
961
+ },
962
+ )
963
+
964
+ assert response.status_code == 302
965
+ mock_send_dotdigital_email.assert_called()
966
+
967
+ total_activity = TotalActivity.objects.get(id=1)
968
+
969
+ assert (
970
+ total_activity.independent_registrations
971
+ == independent_registration_count + 1
972
+ )
973
+
974
+ teacher_email, teacher_password = signup_teacher_directly()
975
+ create_organisation_directly(teacher_email)
976
+ _, _, access_code = create_class_directly(teacher_email)
977
+
978
+ c.login(username=teacher_email, password=teacher_password)
979
+ c.post(
980
+ reverse("view_class", kwargs={"access_code": access_code}),
981
+ {"names": "Student 1, Student 2, Student 3"},
982
+ )
983
+
984
+ assert response.status_code == 302
985
+
986
+ total_activity = TotalActivity.objects.get(id=1)
987
+
988
+ assert (
989
+ total_activity.student_registrations
990
+ == student_registration_count + 3
991
+ )
992
+
993
+
994
+ # CRON view tests
995
+
996
+
997
+ class CronTestClient(APIClient):
998
+ def __init__(self, *args, **kwargs):
999
+ super().__init__(*args, **kwargs, HTTP_X_APPENGINE_CRON="true")
1000
+
1001
+ def generic(
1002
+ self,
1003
+ method,
1004
+ path,
1005
+ data="",
1006
+ content_type="application/octet-stream",
1007
+ secure=False,
1008
+ **extra,
1009
+ ):
1010
+ wsgi_response = super().generic(
1011
+ method, path, data, content_type, secure, **extra
1012
+ )
1013
+ assert (
1014
+ 200 <= wsgi_response.status_code < 300
1015
+ ), f"Response has error status code: {wsgi_response.status_code}"
1016
+
1017
+ return wsgi_response
1018
+
1019
+
1020
+ class CronTestCase(APITestCase):
1021
+ client_class = CronTestClient
1022
+
1023
+
1024
+ class TestUser(CronTestCase):
1025
+ # TODO: use fixtures
1026
+ def setUp(self):
1027
+ teacher_email, _ = signup_teacher_directly(preverified=False)
1028
+ create_organisation_directly(teacher_email)
1029
+ _, _, access_code = create_class_directly(teacher_email)
1030
+ _, _, student = create_school_student_directly(access_code)
1031
+ indy_email, _, _ = create_independent_student_directly()
1032
+
1033
+ self.teacher_user = User.objects.get(email=teacher_email)
1034
+ self.teacher_user_profile = UserProfile.objects.get(
1035
+ user=self.teacher_user
1036
+ )
1037
+
1038
+ self.indy_user = User.objects.get(email=indy_email)
1039
+ self.indy_user_profile = UserProfile.objects.get(user=self.indy_user)
1040
+
1041
+ self.student_user: User = student.new_user
1042
+
1043
+ @patch("portal.views.cron.user.send_dotdigital_email")
1044
+ def send_verify_email_reminder(
1045
+ self,
1046
+ days: int,
1047
+ is_verified: bool,
1048
+ view_name: str,
1049
+ assert_called: bool,
1050
+ mock_send_dotdigital_email: Mock,
1051
+ ):
1052
+ self.teacher_user.date_joined = timezone.now() - timedelta(
1053
+ days=days, hours=12
1054
+ )
1055
+ self.teacher_user.save()
1056
+ self.student_user.date_joined = timezone.now() - timedelta(
1057
+ days=days, hours=12
1058
+ )
1059
+ self.student_user.save()
1060
+ self.indy_user.date_joined = timezone.now() - timedelta(
1061
+ days=days, hours=12
1062
+ )
1063
+ self.indy_user.save()
1064
+
1065
+ self.teacher_user_profile.is_verified = is_verified
1066
+ self.teacher_user_profile.save()
1067
+ self.indy_user_profile.is_verified = is_verified
1068
+ self.indy_user_profile.save()
1069
+
1070
+ self.client.get(reverse(view_name))
1071
+
1072
+ if assert_called:
1073
+ mock_send_dotdigital_email.assert_any_call(
1074
+ ANY, [self.teacher_user.email], personalization_values=ANY
1075
+ )
1076
+
1077
+ mock_send_dotdigital_email.assert_any_call(
1078
+ ANY, [self.indy_user.email], personalization_values=ANY
1079
+ )
1080
+
1081
+ # Check only two emails are sent - the student should never be included.
1082
+ assert mock_send_dotdigital_email.call_count == 2
1083
+ else:
1084
+ mock_send_dotdigital_email.assert_not_called()
1085
+
1086
+ mock_send_dotdigital_email.reset_mock()
1087
+
1088
+ def test_first_verify_email_reminder_view(self):
1089
+ self.send_verify_email_reminder(
1090
+ 6, False, "first-verify-email-reminder", False
1091
+ )
1092
+ self.send_verify_email_reminder(
1093
+ 7, False, "first-verify-email-reminder", True
1094
+ )
1095
+ self.send_verify_email_reminder(
1096
+ 7, True, "first-verify-email-reminder", False
1097
+ )
1098
+ self.send_verify_email_reminder(
1099
+ 8, False, "first-verify-email-reminder", False
1100
+ )
1101
+
1102
+ def test_second_verify_email_reminder_view(self):
1103
+ self.send_verify_email_reminder(
1104
+ 13, False, "second-verify-email-reminder", False
1105
+ )
1106
+ self.send_verify_email_reminder(
1107
+ 14, False, "second-verify-email-reminder", True
1108
+ )
1109
+ self.send_verify_email_reminder(
1110
+ 14, True, "second-verify-email-reminder", False
1111
+ )
1112
+ self.send_verify_email_reminder(
1113
+ 15, False, "second-verify-email-reminder", False
1114
+ )
1115
+
1116
+ def test_anonymise_unverified_accounts_view(self):
1117
+ now = timezone.now()
1118
+
1119
+ for user in [self.teacher_user, self.indy_user, self.student_user]:
1120
+ user.date_joined = now - timedelta(
1121
+ days=USER_DELETE_UNVERIFIED_ACCOUNT_DAYS + 1
1122
+ )
1123
+ user.save()
1124
+
1125
+ for user_profile in [self.teacher_user_profile, self.indy_user_profile]:
1126
+ user_profile.is_verified = True
1127
+ user_profile.save()
1128
+
1129
+ def anonymise_unverified_users(
1130
+ days: int,
1131
+ is_verified: bool,
1132
+ assert_active: bool,
1133
+ ):
1134
+ date_joined = now - timedelta(days=days, hours=12)
1135
+
1136
+ # Create teacher.
1137
+ teacher_user = User.objects.create(
1138
+ first_name="Unverified",
1139
+ last_name="Teacher",
1140
+ username="unverified.teacher@codeforlife.com",
1141
+ email="unverified.teacher@codeforlife.com",
1142
+ date_joined=date_joined,
1143
+ )
1144
+ teacher_user_profile = UserProfile.objects.create(
1145
+ user=teacher_user,
1146
+ is_verified=is_verified,
1147
+ )
1148
+ Teacher.objects.create(
1149
+ user=teacher_user_profile,
1150
+ new_user=teacher_user,
1151
+ school=self.teacher_user.new_teacher.school,
1152
+ )
1153
+
1154
+ # Create dependent student.
1155
+ student_user = User.objects.create(
1156
+ first_name="Unverified",
1157
+ last_name="DependentStudent",
1158
+ username="UnverifiedDependentStudent",
1159
+ date_joined=date_joined,
1160
+ )
1161
+ student_user_profile = UserProfile.objects.create(
1162
+ user=student_user,
1163
+ )
1164
+ Student.objects.create(
1165
+ user=student_user_profile,
1166
+ new_user=student_user,
1167
+ class_field=self.student_user.new_student.class_field,
1168
+ )
1169
+
1170
+ # Create independent student.
1171
+ indy_user = User.objects.create(
1172
+ first_name="Unverified",
1173
+ last_name="IndependentStudent",
1174
+ username="unverified.independentstudent@codeforlife.com",
1175
+ email="unverified.independentstudent@codeforlife.com",
1176
+ date_joined=date_joined,
1177
+ )
1178
+ indy_user_profile = UserProfile.objects.create(
1179
+ user=indy_user,
1180
+ is_verified=is_verified,
1181
+ )
1182
+ Student.objects.create(
1183
+ user=indy_user_profile,
1184
+ new_user=indy_user,
1185
+ )
1186
+
1187
+ activity_today = DailyActivity.objects.get_or_create(
1188
+ date=datetime.now().date()
1189
+ )[0]
1190
+ daily_teacher_count = activity_today.anonymised_unverified_teachers
1191
+ daily_indy_count = activity_today.anonymised_unverified_independents
1192
+
1193
+ total_activity = TotalActivity.objects.get(id=1)
1194
+ total_teacher_count = total_activity.anonymised_unverified_teachers
1195
+ total_indy_count = total_activity.anonymised_unverified_independents
1196
+
1197
+ self.client.get(reverse("anonymise-unverified-accounts"))
1198
+
1199
+ # Assert the verified users exist
1200
+ assert User.objects.get(id=self.teacher_user.id).is_active
1201
+ assert User.objects.get(id=self.student_user.id).is_active
1202
+ assert User.objects.get(id=self.indy_user.id).is_active
1203
+
1204
+ teacher_user_active = User.objects.get(id=teacher_user.id).is_active
1205
+ indy_user_active = User.objects.get(id=indy_user.id).is_active
1206
+ student_user_active = User.objects.get(id=student_user.id).is_active
1207
+
1208
+ assert teacher_user_active == assert_active
1209
+ assert indy_user_active == assert_active
1210
+ assert student_user_active
1211
+
1212
+ activity_today = DailyActivity.objects.get_or_create(
1213
+ date=datetime.now().date()
1214
+ )[0]
1215
+ total_activity = TotalActivity.objects.get(id=1)
1216
+
1217
+ if not teacher_user_active:
1218
+ assert (
1219
+ activity_today.anonymised_unverified_teachers
1220
+ == daily_teacher_count + 1
1221
+ )
1222
+ assert (
1223
+ total_activity.anonymised_unverified_teachers
1224
+ == total_teacher_count + 1
1225
+ )
1226
+
1227
+ if not indy_user_active:
1228
+ assert (
1229
+ activity_today.anonymised_unverified_independents
1230
+ == daily_indy_count + 1
1231
+ )
1232
+ assert (
1233
+ total_activity.anonymised_unverified_independents
1234
+ == total_indy_count + 1
1235
+ )
1236
+
1237
+ teacher_user.delete()
1238
+ indy_user.delete()
1239
+ student_user.delete()
1240
+
1241
+ anonymise_unverified_users(
1242
+ days=USER_DELETE_UNVERIFIED_ACCOUNT_DAYS - 1,
1243
+ is_verified=False,
1244
+ assert_active=True,
1245
+ )
1246
+ anonymise_unverified_users(
1247
+ days=USER_DELETE_UNVERIFIED_ACCOUNT_DAYS,
1248
+ is_verified=False,
1249
+ assert_active=False,
1250
+ )
1251
+ anonymise_unverified_users(
1252
+ days=USER_DELETE_UNVERIFIED_ACCOUNT_DAYS,
1253
+ is_verified=True,
1254
+ assert_active=True,
1255
+ )
1256
+ anonymise_unverified_users(
1257
+ days=USER_DELETE_UNVERIFIED_ACCOUNT_DAYS + 1,
1258
+ is_verified=False,
1259
+ assert_active=False,
1260
+ )
1261
+
1262
+ @patch("portal.views.cron.user.send_dotdigital_email")
1263
+ def send_inactivity_reminder(
1264
+ self,
1265
+ days: int,
1266
+ view_name: str,
1267
+ assert_called: bool,
1268
+ campaign_name: str,
1269
+ mock_send_dotdigital_email: Mock,
1270
+ personalization_values=None,
1271
+ ):
1272
+ self.teacher_user.date_joined = timezone.now() - timedelta(
1273
+ days=days, hours=12
1274
+ )
1275
+ self.teacher_user.save()
1276
+ self.student_user.date_joined = timezone.now() - timedelta(
1277
+ days=days, hours=12
1278
+ )
1279
+ self.student_user.save()
1280
+ self.indy_user.last_login = timezone.now() - timedelta(
1281
+ days=days, hours=12
1282
+ )
1283
+ self.indy_user.save()
1284
+
1285
+ self.client.get(reverse(view_name))
1286
+
1287
+ if assert_called:
1288
+ if personalization_values is not None:
1289
+ mock_send_dotdigital_email.assert_any_call(
1290
+ campaign_ids[campaign_name],
1291
+ [self.teacher_user.email],
1292
+ personalization_values=personalization_values,
1293
+ )
1294
+
1295
+ mock_send_dotdigital_email.assert_any_call(
1296
+ campaign_ids[campaign_name],
1297
+ [self.indy_user.email],
1298
+ personalization_values=personalization_values,
1299
+ )
1300
+ else:
1301
+ mock_send_dotdigital_email.assert_any_call(
1302
+ campaign_ids[campaign_name],
1303
+ [self.teacher_user.email],
1304
+ )
1305
+
1306
+ mock_send_dotdigital_email.assert_any_call(
1307
+ campaign_ids[campaign_name],
1308
+ [self.indy_user.email],
1309
+ )
1310
+
1311
+ # Check only two emails are sent - the student should never be included.
1312
+ assert mock_send_dotdigital_email.call_count == 2
1313
+ else:
1314
+ mock_send_dotdigital_email.assert_not_called()
1315
+
1316
+ mock_send_dotdigital_email.reset_mock()
1317
+
1318
+ def test_first_inactivity_reminder_view(self):
1319
+ self.send_inactivity_reminder(
1320
+ 729,
1321
+ "first-inactivity-reminder",
1322
+ False,
1323
+ "inactive_users_on_website_first_reminder",
1324
+ )
1325
+ self.send_inactivity_reminder(
1326
+ 730,
1327
+ "first-inactivity-reminder",
1328
+ True,
1329
+ "inactive_users_on_website_first_reminder",
1330
+ )
1331
+ self.send_inactivity_reminder(
1332
+ 731,
1333
+ "first-inactivity-reminder",
1334
+ False,
1335
+ "inactive_users_on_website_first_reminder",
1336
+ )
1337
+
1338
+ def test_second_inactivity_reminder_view(self):
1339
+ self.send_inactivity_reminder(
1340
+ 972,
1341
+ "second-inactivity-reminder",
1342
+ False,
1343
+ "inactive_users_on_website_second_reminder",
1344
+ )
1345
+ self.send_inactivity_reminder(
1346
+ 973,
1347
+ "second-inactivity-reminder",
1348
+ True,
1349
+ "inactive_users_on_website_second_reminder",
1350
+ personalization_values={
1351
+ "DAYS_LEFT": USER_RETENTION_PERIOD
1352
+ - USER_2ND_INACTIVE_REMINDER_DAYS
1353
+ },
1354
+ )
1355
+ self.send_inactivity_reminder(
1356
+ 974,
1357
+ "second-inactivity-reminder",
1358
+ False,
1359
+ "inactive_users_on_website_second_reminder",
1360
+ )
1361
+
1362
+ def test_final_inactivity_reminder_view(self):
1363
+ self.send_inactivity_reminder(
1364
+ 1064,
1365
+ "final-inactivity-reminder",
1366
+ False,
1367
+ "inactive_users_on_website_final_reminder",
1368
+ )
1369
+ self.send_inactivity_reminder(
1370
+ 1065,
1371
+ "final-inactivity-reminder",
1372
+ True,
1373
+ "inactive_users_on_website_final_reminder",
1374
+ personalization_values={
1375
+ "DAYS_LEFT": USER_RETENTION_PERIOD
1376
+ - USER_FINAL_INACTIVE_REMINDER_DAYS
1377
+ },
1378
+ )
1379
+ self.send_inactivity_reminder(
1380
+ 1066,
1381
+ "final-inactivity-reminder",
1382
+ False,
1383
+ "inactive_users_on_website_final_reminder",
1384
+ )