codeforlife-portal 5.33.5__py2.py3-none-any.whl → 8.9.9__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (391) hide show
  1. cfl_common/common/__init__.py +1 -0
  2. cfl_common/common/app_settings.py +66 -0
  3. cfl_common/common/apps.py +6 -0
  4. cfl_common/common/context_processors.py +9 -0
  5. cfl_common/common/csp_config.py +85 -0
  6. cfl_common/common/helpers/__init__.py +0 -0
  7. cfl_common/common/helpers/data_migration_loader.py +42 -0
  8. cfl_common/common/helpers/emails.py +393 -0
  9. cfl_common/common/helpers/generators.py +52 -0
  10. cfl_common/common/helpers/organisation.py +10 -0
  11. cfl_common/common/mail.py +201 -0
  12. cfl_common/common/migrations/0001_initial.py +240 -0
  13. cfl_common/common/migrations/0002_emailverification.py +55 -0
  14. cfl_common/common/migrations/0003_aimmocharacter.py +31 -0
  15. cfl_common/common/migrations/0004_add_aimmocharacters.py +17 -0
  16. cfl_common/common/migrations/0005_add_worksheets.py +8 -0
  17. cfl_common/common/migrations/0006_update_aimmo_character_image_path.py +17 -0
  18. cfl_common/common/migrations/0007_add_pdf_names_to_first_two_worksheets.py +8 -0
  19. cfl_common/common/migrations/0008_unlock_worksheet_3.py +11 -0
  20. cfl_common/common/migrations/0009_add_blocked_time_to_teacher_and_student.py +24 -0
  21. cfl_common/common/migrations/0010_remove_teacher_title.py +18 -0
  22. cfl_common/common/migrations/0011_student_login_id.py +18 -0
  23. cfl_common/common/migrations/0012_usersession.py +39 -0
  24. cfl_common/common/migrations/0013_class_school.py +42 -0
  25. cfl_common/common/migrations/0014_login_type.py +29 -0
  26. cfl_common/common/migrations/0015_dailyactivity.py +31 -0
  27. cfl_common/common/migrations/0016_joinreleasestudent.py +42 -0
  28. cfl_common/common/migrations/0017_copy_email_to_username.py +18 -0
  29. cfl_common/common/migrations/0018_update_aimmo_character_image_path.py +15 -0
  30. cfl_common/common/migrations/0019_aimmocharacter_alt.py +16 -0
  31. cfl_common/common/migrations/0020_class_is_active_and_null_access_code.py +23 -0
  32. cfl_common/common/migrations/0021_school_is_active.py +28 -0
  33. cfl_common/common/migrations/0022_school_cleanup.py +29 -0
  34. cfl_common/common/migrations/0023_userprofile_aimmo_badges.py +22 -0
  35. cfl_common/common/migrations/0024_teacher_invited_by.py +25 -0
  36. cfl_common/common/migrations/0025_schoolteacherinvitation.py +47 -0
  37. cfl_common/common/migrations/0026_teacher_remove_join_request.py +22 -0
  38. cfl_common/common/migrations/0027_class_created_by.py +25 -0
  39. cfl_common/common/migrations/0028_coding_club_downloads.py +23 -0
  40. cfl_common/common/migrations/0029_dynamicelement.py +22 -0
  41. cfl_common/common/migrations/0030_add_maintenance_banner.py +25 -0
  42. cfl_common/common/migrations/0031_improve_admin_panel.py +56 -0
  43. cfl_common/common/migrations/0032_dailyactivity_level_control_submits.py +18 -0
  44. cfl_common/common/migrations/0033_password_reset_tracking_fields.py +23 -0
  45. cfl_common/common/migrations/0034_dailyactivity_daily_school_student_lockout_reset.py +18 -0
  46. cfl_common/common/migrations/0035_rename_lockout_fields.py +27 -0
  47. cfl_common/common/migrations/0036_rename_awaiting_email_verification_userprofile_is_verified.py +17 -0
  48. cfl_common/common/migrations/0037_migrate_email_verification.py +21 -0
  49. cfl_common/common/migrations/0038_delete_emailverification.py +16 -0
  50. cfl_common/common/migrations/0039_copy_email_to_username.py +18 -0
  51. cfl_common/common/migrations/0040_school_county.py +18 -0
  52. cfl_common/common/migrations/0041_populate_gb_counties.py +27 -0
  53. cfl_common/common/migrations/0042_totalactivity.py +25 -0
  54. cfl_common/common/migrations/0043_add_total_activity.py +30 -0
  55. cfl_common/common/migrations/0044_update_activity_models.py +33 -0
  56. cfl_common/common/migrations/0045_otp.py +23 -0
  57. cfl_common/common/migrations/0046_alter_school_country.py +19 -0
  58. cfl_common/common/migrations/0047_delete_school_postcode.py +16 -0
  59. cfl_common/common/migrations/0048_unique_school_names.py +42 -0
  60. cfl_common/common/migrations/0049_anonymise_orphan_users.py +29 -0
  61. cfl_common/common/migrations/0050_anonymise_orphan_schools.py +30 -0
  62. cfl_common/common/migrations/0051_verify_returning_users.py +26 -0
  63. cfl_common/common/migrations/0052_add_cse_fields.py +68 -0
  64. cfl_common/common/migrations/0053_clean_class_data.py +24 -0
  65. cfl_common/common/migrations/0054_delete_aimmo_models.py +20 -0
  66. cfl_common/common/migrations/0055_alter_schoolteacherinvitation_token.py +18 -0
  67. cfl_common/common/migrations/0056_set_non_school_teachers_as_non_admins.py +25 -0
  68. cfl_common/common/migrations/0057_teacher_teacher__is_admin.py +19 -0
  69. cfl_common/common/migrations/0058_userprofile_google_refresh_token_and_more.py +24 -0
  70. cfl_common/common/migrations/__init__.py +0 -0
  71. cfl_common/common/models.py +557 -0
  72. cfl_common/common/permissions.py +84 -0
  73. cfl_common/common/tests/__init__.py +0 -0
  74. cfl_common/common/tests/test_migration_anonymise_orphan_schools.py +30 -0
  75. cfl_common/common/tests/test_migration_anonymise_orphan_users.py +30 -0
  76. cfl_common/common/tests/test_migration_blocked_time.py +15 -0
  77. cfl_common/common/tests/test_migration_remove_teacher_title.py +13 -0
  78. cfl_common/common/tests/test_migration_unique_school_names.py +33 -0
  79. cfl_common/common/tests/test_migration_verify_returning_users.py +59 -0
  80. cfl_common/common/tests/test_models.py +87 -0
  81. cfl_common/common/tests/utils/__init__.py +0 -0
  82. cfl_common/common/tests/utils/classes.py +38 -0
  83. cfl_common/common/tests/utils/email.py +67 -0
  84. cfl_common/common/tests/utils/organisation.py +41 -0
  85. cfl_common/common/tests/utils/student.py +123 -0
  86. cfl_common/common/tests/utils/teacher.py +73 -0
  87. cfl_common/common/tests/utils/user.py +27 -0
  88. cfl_common/common/utils.py +56 -0
  89. cfl_common/setup.py +61 -0
  90. codeforlife_portal-8.9.9.dist-info/METADATA +226 -0
  91. {codeforlife_portal-5.33.5.dist-info → codeforlife_portal-8.9.9.dist-info}/RECORD +339 -241
  92. {codeforlife_portal-5.33.5.dist-info → codeforlife_portal-8.9.9.dist-info}/WHEEL +1 -1
  93. codeforlife_portal-8.9.9.dist-info/licenses/LICENSE.md +3 -0
  94. {codeforlife_portal-5.33.5.dist-info → codeforlife_portal-8.9.9.dist-info}/top_level.txt +1 -0
  95. deploy/middleware/maintenance.py +25 -0
  96. deploy/middleware/screentime_warning.py +29 -0
  97. deploy/middleware/security.py +5 -6
  98. deploy/middleware/session_timeout.py +4 -2
  99. deploy/middleware/tmp_basic_auth.py +41 -0
  100. example_project/portal_test_settings.py +239 -0
  101. example_project/settings.py +156 -17
  102. example_project/urls.py +5 -6
  103. portal/__init__.py +1 -1
  104. portal/admin.py +142 -29
  105. portal/app_settings.py +8 -7
  106. portal/forms/dotmailer.py +6 -4
  107. portal/forms/invite_teacher.py +19 -10
  108. portal/forms/organisation.py +137 -68
  109. portal/forms/play.py +53 -98
  110. portal/forms/registration.py +70 -164
  111. portal/forms/teach.py +147 -121
  112. portal/handlers.py +1 -2
  113. portal/helpers/decorators.py +30 -10
  114. portal/helpers/password.py +86 -47
  115. portal/helpers/ratelimit.py +32 -15
  116. portal/helpers/regexes.py +5 -0
  117. portal/helpers/request_handlers.py +10 -0
  118. portal/migrations/0044_auto_20150430_0959.py +6 -2
  119. portal/mixins/__init__.py +1 -0
  120. portal/mixins/cron_mixin.py +12 -0
  121. portal/permissions/__init__.py +1 -0
  122. portal/permissions/is_cron_request_from_google.py +14 -0
  123. portal/static/portal/img/10_years_anniversary.png +0 -0
  124. portal/static/portal/img/RR_logo_grass_background.png +0 -0
  125. portal/static/portal/img/coding_club_hero.jpg +0 -0
  126. portal/static/portal/img/coding_club_python_pack.png +0 -0
  127. portal/static/portal/img/facebook.png +0 -0
  128. portal/static/portal/img/gitbook.png +0 -0
  129. portal/static/portal/img/howe_dell_1.png +0 -0
  130. portal/static/portal/img/howe_dell_2.png +0 -0
  131. portal/static/portal/img/howe_dell_3.png +0 -0
  132. portal/static/portal/img/logo_cfl.png +0 -0
  133. portal/static/portal/img/logo_cfl_powered.svg +35 -0
  134. portal/static/portal/img/logo_cfl_reminder_cards.jpg +0 -0
  135. portal/static/portal/img/logo_ocado_group.png +0 -0
  136. portal/static/portal/img/logo_python_den.svg +21 -0
  137. portal/static/portal/img/long_europe_map.png +0 -0
  138. portal/static/portal/img/python_den.png +0 -0
  139. portal/static/portal/img/python_den_banner.svg +26 -0
  140. portal/static/portal/img/rapid_router_landing_hero.png +0 -0
  141. portal/static/portal/img/rr_advanced.png +0 -0
  142. portal/static/portal/img/ten_year_map_pin.svg +1 -0
  143. portal/static/portal/img/thumbnail_educate_rapid_router.png +0 -0
  144. portal/static/portal/img/thumbnail_educate_resources.png +0 -0
  145. portal/static/portal/img/thumbnail_play_rapid_router.png +0 -0
  146. portal/static/portal/img/thumbnail_python_den.png +0 -0
  147. portal/static/portal/img/twitter.png +0 -0
  148. portal/static/portal/js/carouselCards.js +25 -0
  149. portal/static/portal/js/common.js +96 -1
  150. portal/static/portal/js/independentLogin.js +16 -0
  151. portal/static/portal/js/independentRegistration.js +86 -0
  152. portal/static/portal/js/levelControl.js +77 -0
  153. portal/static/portal/js/lib/jquery.min.js +2 -0
  154. portal/static/portal/js/organisation_manage.js +142 -14
  155. portal/static/portal/js/passwordStrength.js +154 -64
  156. portal/static/portal/js/resetPassword.js +23 -0
  157. portal/static/portal/js/riveted.min.js +238 -239
  158. portal/static/portal/js/school.js +13 -0
  159. portal/static/portal/js/studentLogin.js +16 -0
  160. portal/static/portal/js/teacherEditStudent.js +23 -0
  161. portal/static/portal/js/teacherLogin.js +16 -0
  162. portal/static/portal/js/tenYearMap.js +14 -0
  163. portal/static/portal/sass/colorbox.scss +0 -1
  164. portal/static/portal/sass/modules/_colours.scss +1 -0
  165. portal/static/portal/sass/modules/_levels.scss +1 -1
  166. portal/static/portal/sass/modules/_mixins.scss +21 -0
  167. portal/static/portal/sass/partials/_banners.scss +4 -177
  168. portal/static/portal/sass/partials/_buttons.scss +12 -15
  169. portal/static/portal/sass/partials/_carousel.scss +129 -0
  170. portal/static/portal/sass/partials/_footer.scss +21 -22
  171. portal/static/portal/sass/partials/_forms.scss +60 -5
  172. portal/static/portal/sass/partials/_grids.scss +34 -61
  173. portal/static/portal/sass/partials/_header.scss +28 -20
  174. portal/static/portal/sass/partials/_images.scss +292 -39
  175. portal/static/portal/sass/partials/_popup.scss +18 -15
  176. portal/static/portal/sass/partials/_tables.scss +12 -20
  177. portal/static/portal/sass/partials/_text.scss +6 -10
  178. portal/static/portal/sass/styles.scss +0 -1
  179. portal/static/portal/video/code for life .pdf +0 -0
  180. portal/strings/about.py +5 -0
  181. portal/strings/coding_club.py +9 -0
  182. portal/strings/play.py +6 -5
  183. portal/strings/teach.py +1 -1
  184. portal/strings/teacher_resources.py +2 -8
  185. portal/strings/ten_year_map.py +13 -0
  186. portal/templates/403.html +2 -2
  187. portal/templates/404.html +1 -1
  188. portal/templates/500.html +2 -2
  189. portal/templates/{captcha → django_recaptcha}/includes/js_v2_invisible.html +3 -3
  190. portal/templates/{captcha → django_recaptcha}/widget_v2_invisible.html +2 -2
  191. portal/templates/email.html +4 -2
  192. portal/templates/maintenance.html +34 -0
  193. portal/templates/portal/about.html +94 -62
  194. portal/templates/portal/base.html +176 -152
  195. portal/templates/portal/coding_club.html +100 -0
  196. portal/templates/portal/contribute.html +56 -52
  197. portal/templates/portal/email_invitation_sent.html +1 -1
  198. portal/templates/portal/email_style_template.html +374 -0
  199. portal/templates/portal/email_verification_failed.html +1 -1
  200. portal/templates/portal/email_verification_needed.html +9 -9
  201. portal/templates/portal/form_shapes.html +20 -8
  202. portal/templates/portal/getinvolved.html +6 -6
  203. portal/templates/portal/home.html +35 -10
  204. portal/templates/portal/home_learning.html +19 -19
  205. portal/templates/portal/locked_out.html +0 -1
  206. portal/templates/portal/locked_out_school_student.html +16 -0
  207. portal/templates/portal/login/independent_student.html +31 -15
  208. portal/templates/portal/login/student.html +10 -7
  209. portal/templates/portal/login/student_class_code.html +7 -4
  210. portal/templates/portal/login/teacher.html +34 -17
  211. portal/templates/portal/partials/banner.html +18 -4
  212. portal/templates/portal/partials/benefits.html +1 -1
  213. portal/templates/portal/partials/card_list.html +34 -24
  214. portal/templates/portal/partials/character_list.html +5 -5
  215. portal/templates/portal/partials/cookie_list.html +161 -0
  216. portal/templates/portal/partials/delete_popup.html +18 -0
  217. portal/templates/portal/partials/footer.html +57 -26
  218. portal/templates/portal/partials/header.html +118 -117
  219. portal/templates/portal/partials/hero_card.html +4 -3
  220. portal/templates/portal/partials/info_popup.html +3 -3
  221. portal/templates/portal/partials/invite_admin_teacher.html +23 -0
  222. portal/templates/portal/partials/popup.html +7 -2
  223. portal/templates/portal/partials/register_newsletter_tickbox.html +2 -5
  224. portal/templates/portal/partials/screentime_popup.html +14 -0
  225. portal/templates/portal/partials/service_unavailable_popup.html +17 -0
  226. portal/templates/portal/partials/session_popup.html +19 -0
  227. portal/templates/portal/play/student_dashboard.html +42 -29
  228. portal/templates/portal/play/student_edit_account.html +64 -9
  229. portal/templates/portal/play.html +61 -41
  230. portal/templates/portal/privacy_notice.html +697 -0
  231. portal/templates/portal/register.html +122 -92
  232. portal/templates/portal/reset_password.html +20 -40
  233. portal/templates/portal/reset_password_confirm.html +9 -4
  234. portal/templates/portal/reset_password_email_sent.html +15 -13
  235. portal/templates/portal/teach/base_registering.html +1 -1
  236. portal/templates/portal/teach/class.html +4 -6
  237. portal/templates/portal/teach/dashboard.html +212 -117
  238. portal/templates/portal/teach/invited.html +90 -0
  239. portal/templates/portal/teach/onboarding_classes.html +5 -3
  240. portal/templates/portal/teach/onboarding_print.html +1 -1
  241. portal/templates/portal/teach/onboarding_school.html +26 -139
  242. portal/templates/portal/teach/onboarding_students.html +1 -1
  243. portal/templates/portal/teach/teacher_dismiss_students.html +73 -55
  244. portal/templates/portal/teach/teacher_edit_class.html +168 -11
  245. portal/templates/portal/teach/teacher_edit_student.html +12 -5
  246. portal/templates/portal/teach/teacher_move_all_classes.html +25 -38
  247. portal/templates/portal/teach/teacher_move_students_to_class.html +1 -1
  248. portal/templates/portal/teach.html +61 -42
  249. portal/templates/portal/ten_year_map.html +147 -0
  250. portal/templates/portal/terms.html +191 -42
  251. portal/templates/two_factor/core/login.html +71 -59
  252. portal/templates/two_factor/core/setup.html +58 -49
  253. portal/templates/two_factor/profile/disable.html +1 -1
  254. portal/templates/two_factor/profile/profile.html +35 -17
  255. portal/templatetags/app_tags.py +59 -84
  256. portal/templatetags/card_list_tags.py +0 -4
  257. portal/tests/base_test.py +14 -3
  258. portal/tests/conftest.py +0 -15
  259. portal/tests/migrations/test_migration_make_portaladmin_teacher.py +2 -6
  260. portal/tests/migrations/test_migration_preview_users.py +3 -9
  261. portal/tests/migrations/test_migration_remove_guardian.py +1 -3
  262. portal/tests/migrations/test_migration_use_common_models.py +2 -6
  263. portal/tests/migrations/test_migration_verify_portaladmin.py +1 -3
  264. portal/tests/pageObjects/portal/admin/admin_base_page.py +0 -21
  265. portal/tests/pageObjects/portal/base_page.py +16 -26
  266. portal/tests/pageObjects/portal/email_verification_needed_page.py +3 -2
  267. portal/tests/pageObjects/portal/game_page.py +12 -19
  268. portal/tests/pageObjects/portal/home_page.py +13 -15
  269. portal/tests/pageObjects/portal/independent_login_page.py +13 -17
  270. portal/tests/pageObjects/portal/password_reset_form_page.py +20 -4
  271. portal/tests/pageObjects/portal/password_reset_page.py +25 -0
  272. portal/tests/pageObjects/portal/play/account_page.py +18 -27
  273. portal/tests/pageObjects/portal/play/dashboard_page.py +4 -4
  274. portal/tests/pageObjects/portal/play/join_school_or_club_page.py +8 -10
  275. portal/tests/pageObjects/portal/play/play_base_page.py +5 -3
  276. portal/tests/pageObjects/portal/signup_page.py +28 -59
  277. portal/tests/pageObjects/portal/student_login_class_code.py +6 -9
  278. portal/tests/pageObjects/portal/student_login_page.py +6 -8
  279. portal/tests/pageObjects/portal/teach/add_independent_student_to_class_page.py +3 -3
  280. portal/tests/pageObjects/portal/teach/added_independent_student_to_class_page.py +3 -1
  281. portal/tests/pageObjects/portal/teach/class_page.py +36 -13
  282. portal/tests/pageObjects/portal/teach/dashboard_page.py +43 -84
  283. portal/tests/pageObjects/portal/teach/dismiss_students_page.py +7 -5
  284. portal/tests/pageObjects/portal/teach/edit_student_page.py +10 -8
  285. portal/tests/pageObjects/portal/teach/move_class_page.py +5 -10
  286. portal/tests/pageObjects/portal/teach/move_classes_page.py +4 -2
  287. portal/tests/pageObjects/portal/teach/move_students_disambiguate_page.py +4 -2
  288. portal/tests/pageObjects/portal/teach/move_students_page.py +6 -13
  289. portal/tests/pageObjects/portal/teach/onboarding_classes_page.py +5 -3
  290. portal/tests/pageObjects/portal/teach/onboarding_organisation_page.py +11 -49
  291. portal/tests/pageObjects/portal/teach/onboarding_student_list_page.py +7 -12
  292. portal/tests/pageObjects/portal/teach/onboarding_students_page.py +4 -27
  293. portal/tests/pageObjects/portal/teach/teach_base_page.py +6 -4
  294. portal/tests/pageObjects/portal/teacher_login_page.py +10 -16
  295. portal/tests/selenium_test_case.py +3 -43
  296. portal/tests/snapshots/snap_test_partials.py +11 -165
  297. portal/tests/test_2FA.py +15 -33
  298. portal/tests/test_admin.py +15 -97
  299. portal/tests/test_api.py +212 -91
  300. portal/tests/test_captcha_forms.py +2 -2
  301. portal/tests/test_class.py +374 -24
  302. portal/tests/test_emails.py +83 -20
  303. portal/tests/{test_newsletter_footer.py → test_global_forms.py} +5 -5
  304. portal/tests/test_helper_methods.py +30 -0
  305. portal/tests/test_independent_student.py +255 -144
  306. portal/tests/test_invite_teacher.py +318 -10
  307. portal/tests/test_middleware.py +96 -9
  308. portal/tests/test_organisation.py +78 -262
  309. portal/tests/test_partials.py +0 -88
  310. portal/tests/test_ratelimit.py +218 -36
  311. portal/tests/test_school_student.py +35 -40
  312. portal/tests/test_security.py +12 -31
  313. portal/tests/test_teacher.py +425 -325
  314. portal/tests/test_teacher_student.py +103 -91
  315. portal/tests/test_views.py +900 -76
  316. portal/tests/utils/classes.py +2 -2
  317. portal/tests/utils/messages.py +13 -28
  318. portal/urls.py +235 -166
  319. portal/views/admin.py +0 -332
  320. portal/views/api.py +82 -48
  321. portal/views/cron/__init__.py +1 -0
  322. portal/views/cron/user.py +322 -0
  323. portal/views/dotmailer.py +9 -1
  324. portal/views/email.py +33 -77
  325. portal/views/google_analytics.py +28 -0
  326. portal/views/home.py +126 -97
  327. portal/views/legal.py +13 -0
  328. portal/views/login/independent_student.py +5 -5
  329. portal/views/login/student.py +51 -14
  330. portal/views/login/teacher.py +2 -6
  331. portal/views/organisation.py +20 -189
  332. portal/views/registration.py +97 -17
  333. portal/views/student/edit_account_details.py +99 -72
  334. portal/views/student/play.py +81 -62
  335. portal/views/teacher/dashboard.py +421 -149
  336. portal/views/teacher/teach.py +226 -177
  337. portal/views/two_factor/core.py +22 -19
  338. portal/views/two_factor/profile.py +2 -2
  339. codeforlife_portal-5.33.5.dist-info/LICENSE.md +0 -577
  340. codeforlife_portal-5.33.5.dist-info/METADATA +0 -38
  341. deploy/permissions.py +0 -2
  342. example_project/manage.py +0 -10
  343. portal/autoconfig.py +0 -141
  344. portal/csp_config.py +0 -60
  345. portal/forms/add_game.py +0 -33
  346. portal/helpers/location.py +0 -121
  347. portal/static/portal/img/kurono_hero.jpg +0 -0
  348. portal/static/portal/img/kurono_landing_hero.png +0 -0
  349. portal/static/portal/img/kurono_logo.svg +0 -1
  350. portal/static/portal/img/kurono_logo_grey_background.svg +0 -1
  351. portal/static/portal/img/kurono_logo_mark.svg +0 -1
  352. portal/static/portal/img/kurono_resources_hero.jpg +0 -0
  353. portal/static/portal/img/kurono_story.png +0 -0
  354. portal/static/portal/img/ocado-swirl.svg +0 -22
  355. portal/static/portal/img/thumbnail_educate_kurono.png +0 -0
  356. portal/static/portal/img/thumbnail_educate_resources_and_progress_tracking.png +0 -0
  357. portal/static/portal/img/thumbnail_kurono_resources.png +0 -0
  358. portal/static/portal/img/thumbnail_play_kurono.png +0 -0
  359. portal/static/portal/img/x_close_video.png +0 -0
  360. portal/static/portal/js/aimmoGame.js +0 -106
  361. portal/static/portal/js/deleteWorkspaces.js +0 -14
  362. portal/static/portal/js/fuzzySchoolLookup.js +0 -46
  363. portal/static/portal/js/lib/jquery-3.5.1.min.js +0 -2
  364. portal/static/portal/js/lib/jquery-ui-1.12.1.min.js +0 -13
  365. portal/static/portal/sass/partials/_videos.scss +0 -10
  366. portal/static/portal/video/aimmo_play_now_background_video.mp4 +0 -0
  367. portal/strings/student_aimmo_dashboard.py +0 -6
  368. portal/templates/portal/admin/aggregated_data.html +0 -35
  369. portal/templates/portal/admin/map.html +0 -70
  370. portal/templates/portal/mouseflow.html +0 -9
  371. portal/templates/portal/partials/aimmo_games_table.html +0 -83
  372. portal/templates/portal/partials/register_over_required_age_tickbox.html +0 -9
  373. portal/templates/portal/play/independent_student_dashboard.html +0 -64
  374. portal/templates/portal/play/student_aimmo_dashboard.html +0 -63
  375. portal/templates/portal/privacy_policy.html +0 -483
  376. portal/templates/portal/reset_password_email.html +0 -9
  377. portal/templates/portal/teach/invite.html +0 -25
  378. portal/templates/portal/teach/teacher_aimmo_dashboard.html +0 -95
  379. portal/templates/portal/teach/teacher_resources.html +0 -68
  380. portal/templatetags/character_list_tags.py +0 -16
  381. portal/tests/pageObjects/portal/kurono_teacher_dashboard_page.py +0 -49
  382. portal/tests/pageObjects/portal/student_password_reset_form_page.py +0 -23
  383. portal/tests/pageObjects/portal/teach/onboarding_revoke_request_page.py +0 -20
  384. portal/tests/pageObjects/portal/teacher_password_reset_form_page.py +0 -23
  385. portal/tests/test_aimmo_dashboards.py +0 -172
  386. portal/tests/test_location.py +0 -217
  387. portal/tests/utils/aimmo_games.py +0 -30
  388. portal/views/aimmo/dashboard.py +0 -119
  389. portal/views/privacy_policy.py +0 -9
  390. portal/views/teacher/teacher_resources.py +0 -42
  391. {portal/views/aimmo → cfl_common}/__init__.py +0 -0
@@ -0,0 +1,557 @@
1
+ import re
2
+ import typing as t
3
+ from datetime import timedelta
4
+ from uuid import uuid4
5
+
6
+ from cryptography.fernet import Fernet
7
+ from django.conf import settings
8
+ from django.contrib.auth.models import User
9
+ from django.db import models
10
+ from django.utils import timezone
11
+ from django_countries.fields import CountryField
12
+
13
+ if t.TYPE_CHECKING:
14
+ from django.db.models import ManyToManyField
15
+ from game.models import Worksheet
16
+
17
+
18
+ class EncryptedCharField(models.CharField):
19
+ """
20
+ A custom CharField that encrypts data before saving and decrypts it when
21
+ retrieved.
22
+ """
23
+
24
+ _fernet = Fernet(settings.ENCRYPTION_KEY)
25
+ _prefix = "ENC:"
26
+
27
+ # pylint: disable-next=unused-argument
28
+ def from_db_value(self, value: t.Optional[str], expression, connection):
29
+ """
30
+ Converts a value as returned by the database to a Python object. It is
31
+ the reverse of get_prep_value().
32
+
33
+ https://docs.djangoproject.com/en/5.1/howto/custom-model-fields/#converting-values-to-python-objects
34
+ """
35
+ if isinstance(value, str):
36
+ return self.decrypt_value(value)
37
+ return value
38
+
39
+ def to_python(self, value: t.Optional[str]):
40
+ """
41
+ Converts the value into the correct Python object. It acts as the
42
+ reverse of value_to_string(), and is also called in clean().
43
+
44
+ https://docs.djangoproject.com/en/5.1/howto/custom-model-fields/#converting-values-to-python-objects
45
+ """
46
+ if isinstance(value, str):
47
+ return self.decrypt_value(value)
48
+ return value
49
+
50
+ def get_prep_value(self, value: t.Optional[str]):
51
+ """
52
+ 'value' is the current value of the model's attribute, and the method
53
+ should return data in a format that has been prepared for use as a
54
+ parameter in a query.
55
+
56
+ https://docs.djangoproject.com/en/5.1/howto/custom-model-fields/#converting-python-objects-to-query-values
57
+ """
58
+ if isinstance(value, str):
59
+ return self.encrypt_value(value)
60
+ return value
61
+
62
+ def encrypt_value(self, value: str):
63
+ """Encrypt the value if it's not encrypted."""
64
+ if not value.startswith(self._prefix):
65
+ return self._prefix + self._fernet.encrypt(value.encode()).decode()
66
+ return value
67
+
68
+ def decrypt_value(self, value: str):
69
+ """Decrpyt the value if it's encrypted.."""
70
+ if value.startswith(self._prefix):
71
+ value = value[len(self._prefix) :]
72
+ return self._fernet.decrypt(value).decode()
73
+ return value
74
+
75
+
76
+ class UserProfile(models.Model):
77
+ user = models.OneToOneField(User, on_delete=models.CASCADE)
78
+
79
+ otp_secret = models.CharField(max_length=40, null=True, blank=True)
80
+ last_otp_for_time = models.DateTimeField(null=True, blank=True)
81
+ developer = models.BooleanField(default=False)
82
+ is_verified = models.BooleanField(default=False)
83
+
84
+ # TODO: Make not nullable once data has been transferred
85
+ first_name = models.CharField(max_length=200, null=True, blank=True)
86
+ _first_name = models.BinaryField(null=True, blank=True)
87
+ last_name = models.CharField(max_length=200, null=True, blank=True)
88
+ _last_name = models.BinaryField(null=True, blank=True)
89
+ email = models.CharField(max_length=200, null=True, blank=True)
90
+ _email = models.BinaryField(null=True, blank=True)
91
+ # TODO: Make not nullable once data has been transferred
92
+ username = models.CharField(max_length=200, null=True, blank=True)
93
+ _username = models.BinaryField(null=True, blank=True)
94
+
95
+ # Google.
96
+ google_refresh_token = EncryptedCharField(
97
+ max_length=1000 + len(EncryptedCharField._prefix), null=True, blank=True
98
+ )
99
+ google_sub = models.CharField(max_length=255, null=True, blank=True)
100
+
101
+ def __str__(self):
102
+ return f"{self.user.first_name} {self.user.last_name}"
103
+
104
+ def joined_recently(self):
105
+ now = timezone.now()
106
+ return now - timedelta(days=7) <= self.user.date_joined
107
+
108
+
109
+ class SchoolModelManager(models.Manager):
110
+ def get_original_queryset(self):
111
+ return super().get_queryset()
112
+
113
+ # Filter out inactive schools by default
114
+ def get_queryset(self):
115
+ return super().get_queryset().filter(is_active=True)
116
+
117
+
118
+ class School(models.Model):
119
+ name = models.CharField(max_length=200, unique=True)
120
+ country = CountryField(
121
+ blank_label="(select country)", null=True, blank=True
122
+ )
123
+ # TODO: Create an Address model to house address details
124
+ county = models.CharField(max_length=50, blank=True, null=True)
125
+ creation_time = models.DateTimeField(default=timezone.now, null=True)
126
+ is_active = models.BooleanField(default=True)
127
+
128
+ objects = SchoolModelManager()
129
+
130
+ def __str__(self):
131
+ return self.name
132
+
133
+ def classes(self):
134
+ teachers = self.teacher_school.all()
135
+ if teachers:
136
+ classes = []
137
+ for teacher in teachers:
138
+ if teacher.class_teacher.all():
139
+ classes.extend(list(teacher.class_teacher.all()))
140
+ return classes
141
+ return None
142
+
143
+ def admins(self):
144
+ teachers = self.teacher_school.all()
145
+ return (
146
+ [teacher for teacher in teachers if teacher.is_admin]
147
+ if teachers
148
+ else None
149
+ )
150
+
151
+ def anonymise(self):
152
+ self.name = uuid4().hex
153
+ self.is_active = False
154
+ self.save()
155
+
156
+
157
+ class TeacherModelManager(models.Manager):
158
+ def factory(self, first_name, last_name, email, password):
159
+ user = User.objects.create_user(
160
+ username=email,
161
+ email=email,
162
+ password=password,
163
+ first_name=first_name,
164
+ last_name=last_name,
165
+ )
166
+
167
+ user_profile = UserProfile.objects.create(user=user)
168
+
169
+ return Teacher.objects.create(user=user_profile, new_user=user)
170
+
171
+ def get_original_queryset(self):
172
+ return super().get_queryset()
173
+
174
+ # Filter out non active teachers by default
175
+ def get_queryset(self):
176
+ return super().get_queryset().filter(new_user__is_active=True)
177
+
178
+
179
+ class Teacher(models.Model):
180
+ user = models.OneToOneField(UserProfile, on_delete=models.CASCADE)
181
+ new_user = models.OneToOneField(
182
+ User,
183
+ related_name="new_teacher",
184
+ null=True,
185
+ blank=True,
186
+ on_delete=models.CASCADE,
187
+ )
188
+ school = models.ForeignKey(
189
+ School,
190
+ related_name="teacher_school",
191
+ null=True,
192
+ blank=True,
193
+ on_delete=models.SET_NULL,
194
+ )
195
+ is_admin = models.BooleanField(default=False)
196
+ blocked_time = models.DateTimeField(null=True, blank=True)
197
+ invited_by = models.ForeignKey(
198
+ "self",
199
+ related_name="invited_teachers",
200
+ null=True,
201
+ blank=True,
202
+ on_delete=models.SET_NULL,
203
+ )
204
+
205
+ objects = TeacherModelManager()
206
+
207
+ class Meta:
208
+ constraints = [
209
+ models.CheckConstraint(
210
+ check=~models.Q(
211
+ school__isnull=True,
212
+ is_admin=True,
213
+ ),
214
+ name="teacher__is_admin",
215
+ )
216
+ ]
217
+
218
+ def teaches(self, userprofile):
219
+ if hasattr(userprofile, "student"):
220
+ student = userprofile.student
221
+ return (
222
+ not student.is_independent()
223
+ and student.class_field.teacher == self
224
+ )
225
+
226
+ def has_school(self):
227
+ return self.school is not (None or "")
228
+
229
+ def has_class(self):
230
+ return self.class_teacher.exists()
231
+
232
+ def __str__(self):
233
+ return f"{self.new_user.first_name} {self.new_user.last_name}"
234
+
235
+
236
+ class SchoolTeacherInvitationModelManager(models.Manager):
237
+ def get_original_queryset(self):
238
+ return super().get_queryset()
239
+
240
+ # Filter out inactive invitations by default
241
+ def get_queryset(self):
242
+ return super().get_queryset().filter(is_active=True)
243
+
244
+
245
+ class SchoolTeacherInvitation(models.Model):
246
+ token = models.CharField(max_length=88)
247
+ school = models.ForeignKey(
248
+ School,
249
+ related_name="teacher_invitations",
250
+ null=True,
251
+ on_delete=models.SET_NULL,
252
+ )
253
+ from_teacher = models.ForeignKey(
254
+ Teacher,
255
+ related_name="school_invitations",
256
+ null=True,
257
+ on_delete=models.SET_NULL,
258
+ )
259
+ invited_teacher_first_name = models.CharField(
260
+ max_length=150
261
+ ) # Same as User model
262
+ # TODO: Make not nullable once data has been transferred
263
+ _invited_teacher_first_name = models.BinaryField(null=True, blank=True)
264
+ invited_teacher_last_name = models.CharField(
265
+ max_length=150
266
+ ) # Same as User model
267
+ # TODO: Make not nullable once data has been transferred
268
+ _invited_teacher_last_name = models.BinaryField(null=True, blank=True)
269
+ # TODO: Switch to a CharField to be able to hold hashed value
270
+ invited_teacher_email = models.EmailField() # Same as User model
271
+ # TODO: Make not nullable once data has been transferred
272
+ _invited_teacher_email = models.BinaryField(null=True, blank=True)
273
+ invited_teacher_is_admin = models.BooleanField(default=False)
274
+ expiry = models.DateTimeField()
275
+ creation_time = models.DateTimeField(default=timezone.now, null=True)
276
+ is_active = models.BooleanField(default=True)
277
+
278
+ objects = SchoolTeacherInvitationModelManager()
279
+
280
+ @property
281
+ def is_expired(self):
282
+ return self.expiry < timezone.now()
283
+
284
+ def __str__(self):
285
+ return f"School teacher invitation for {self.invited_teacher_email} to {self.school.name}"
286
+
287
+ def anonymise(self):
288
+ self.invited_teacher_first_name = uuid4().hex
289
+ self.invited_teacher_last_name = uuid4().hex
290
+ self.invited_teacher_email = uuid4().hex
291
+ self.is_active = False
292
+ self.save()
293
+
294
+
295
+ class ClassModelManager(models.Manager):
296
+ def all_members(self, user):
297
+ members = []
298
+ if hasattr(user, "teacher"):
299
+ members.append(user.teacher)
300
+ if user.teacher.has_school():
301
+ classes = user.teacher.class_teacher.all()
302
+ for c in classes:
303
+ members.extend(c.students.all())
304
+ else:
305
+ c = user.student.class_field
306
+ members.append(c.teacher)
307
+ members.extend(c.students.all())
308
+ return members
309
+
310
+ def get_original_queryset(self):
311
+ return super().get_queryset()
312
+
313
+ # Filter out non active classes by default
314
+ def get_queryset(self):
315
+ return super().get_queryset().filter(is_active=True)
316
+
317
+
318
+ class Class(models.Model):
319
+ locked_worksheets: "ManyToManyField[Worksheet]"
320
+
321
+ name = models.CharField(max_length=200)
322
+ teacher = models.ForeignKey(
323
+ Teacher, related_name="class_teacher", on_delete=models.CASCADE
324
+ )
325
+ access_code = models.CharField(max_length=5, null=True)
326
+ classmates_data_viewable = models.BooleanField(default=False)
327
+ always_accept_requests = models.BooleanField(default=False)
328
+ accept_requests_until = models.DateTimeField(null=True)
329
+ creation_time = models.DateTimeField(default=timezone.now, null=True)
330
+ is_active = models.BooleanField(default=True)
331
+ created_by = models.ForeignKey(
332
+ Teacher,
333
+ null=True,
334
+ blank=True,
335
+ related_name="created_classes",
336
+ on_delete=models.SET_NULL,
337
+ )
338
+
339
+ objects = ClassModelManager()
340
+
341
+ def __str__(self):
342
+ return self.name
343
+
344
+ @property
345
+ def active_game(self):
346
+ games = self.game_set.filter(game_class=self, is_archived=False)
347
+ if len(games) >= 1:
348
+ assert (
349
+ len(games) == 1
350
+ ) # there should NOT be more than one active game
351
+ return games[0]
352
+ return None
353
+
354
+ def has_students(self):
355
+ students = self.students.all()
356
+ return students.count() != 0
357
+
358
+ def get_requests_message(self):
359
+ if self.always_accept_requests:
360
+ external_requests_message = (
361
+ "This class is currently set to always accept requests."
362
+ )
363
+ elif (
364
+ self.accept_requests_until is not None
365
+ and (self.accept_requests_until - timezone.now()) >= timedelta()
366
+ ):
367
+ external_requests_message = (
368
+ "This class is accepting external requests until "
369
+ + self.accept_requests_until.strftime("%d-%m-%Y %H:%M")
370
+ + " "
371
+ + timezone.get_current_timezone_name()
372
+ )
373
+ else:
374
+ external_requests_message = (
375
+ "This class is not currently accepting external requests."
376
+ )
377
+
378
+ return external_requests_message
379
+
380
+ def anonymise(self):
381
+ self.name = uuid4().hex
382
+ self.access_code = ""
383
+ self.is_active = False
384
+ self.save()
385
+
386
+ # Remove independent students' requests to join this class
387
+ self.class_request.clear()
388
+
389
+ class Meta(object):
390
+ verbose_name_plural = "classes"
391
+
392
+
393
+ class UserSession(models.Model):
394
+ user = models.ForeignKey(User, on_delete=models.CASCADE)
395
+ login_time = models.DateTimeField(default=timezone.now)
396
+ school = models.ForeignKey(School, null=True, on_delete=models.SET_NULL)
397
+ class_field = models.ForeignKey(Class, null=True, on_delete=models.SET_NULL)
398
+ login_type = models.CharField(
399
+ max_length=100, null=True
400
+ ) # for student login
401
+
402
+ def __str__(self):
403
+ return f"{self.user} login: {self.login_time} type: {self.login_type}"
404
+
405
+
406
+ class StudentModelManager(models.Manager):
407
+ def get_random_username(self):
408
+ while True:
409
+ random_username = uuid4().hex[:30] # generate a random username
410
+ if not User.objects.filter(username=random_username).exists():
411
+ return random_username
412
+
413
+ def schoolFactory(self, klass, name, password, login_id=None):
414
+ user = User.objects.create_user(
415
+ username=self.get_random_username(),
416
+ password=password,
417
+ first_name=name,
418
+ )
419
+ user_profile = UserProfile.objects.create(user=user)
420
+
421
+ return Student.objects.create(
422
+ class_field=klass,
423
+ user=user_profile,
424
+ new_user=user,
425
+ login_id=login_id,
426
+ )
427
+
428
+ def independentStudentFactory(self, name, email, password):
429
+ user = User.objects.create_user(
430
+ username=email, email=email, password=password, first_name=name
431
+ )
432
+
433
+ user_profile = UserProfile.objects.create(user=user)
434
+
435
+ return Student.objects.create(user=user_profile, new_user=user)
436
+
437
+
438
+ class Student(models.Model):
439
+ class_field = models.ForeignKey(
440
+ Class,
441
+ related_name="students",
442
+ null=True,
443
+ blank=True,
444
+ on_delete=models.CASCADE,
445
+ )
446
+ # hashed uuid used for the unique direct login url
447
+ login_id = models.CharField(max_length=64, null=True)
448
+ user = models.OneToOneField(UserProfile, on_delete=models.CASCADE)
449
+ new_user = models.OneToOneField(
450
+ User,
451
+ related_name="new_student",
452
+ null=True,
453
+ blank=True,
454
+ on_delete=models.CASCADE,
455
+ )
456
+ pending_class_request = models.ForeignKey(
457
+ Class,
458
+ related_name="class_request",
459
+ null=True,
460
+ blank=True,
461
+ on_delete=models.SET_NULL,
462
+ )
463
+ blocked_time = models.DateTimeField(null=True, blank=True)
464
+
465
+ objects = StudentModelManager()
466
+
467
+ def is_independent(self):
468
+ return not self.class_field
469
+
470
+ def __str__(self):
471
+ return f"{self.new_user.first_name} {self.new_user.last_name}"
472
+
473
+
474
+ def stripStudentName(name):
475
+ return re.sub("[ \t]+", " ", name.strip())
476
+
477
+
478
+ # -----------------------------------------------------------------------
479
+ # Below are models used for data tracking and maintenance
480
+ # -----------------------------------------------------------------------
481
+ class JoinReleaseStudent(models.Model):
482
+ """
483
+ To keep track when a student is released to be independent student or
484
+ joins a class to be a school student.
485
+ """
486
+
487
+ JOIN = "join"
488
+ RELEASE = "release"
489
+
490
+ student = models.ForeignKey(
491
+ Student, related_name="student", on_delete=models.CASCADE
492
+ )
493
+ # either "release" or "join"
494
+ action_type = models.CharField(max_length=64)
495
+ action_time = models.DateTimeField(default=timezone.now)
496
+
497
+
498
+ class DailyActivity(models.Model):
499
+ """
500
+ A model to record sets of daily activity. Currently used to record the
501
+ amount of student details download clicks, through the CSV and login
502
+ cards methods, per day.
503
+ """
504
+
505
+ date = models.DateField(default=timezone.now)
506
+ csv_click_count = models.PositiveIntegerField(default=0)
507
+ login_cards_click_count = models.PositiveIntegerField(default=0)
508
+ primary_coding_club_downloads = models.PositiveIntegerField(default=0)
509
+ python_coding_club_downloads = models.PositiveIntegerField(default=0)
510
+ level_control_submits = models.PositiveBigIntegerField(default=0)
511
+ teacher_lockout_resets = models.PositiveIntegerField(default=0)
512
+ indy_lockout_resets = models.PositiveIntegerField(default=0)
513
+ school_student_lockout_resets = models.PositiveIntegerField(default=0)
514
+ anonymised_unverified_teachers = models.PositiveIntegerField(default=0)
515
+ anonymised_unverified_independents = models.PositiveIntegerField(default=0)
516
+
517
+ class Meta:
518
+ verbose_name_plural = "Daily activities"
519
+
520
+ def __str__(self):
521
+ return f"Activity on {self.date}: CSV clicks: {self.csv_click_count}, login cards clicks: {self.login_cards_click_count}, primary pack downloads: {self.primary_coding_club_downloads}, python pack downloads: {self.python_coding_club_downloads}, level control submits: {self.level_control_submits}, teacher lockout resets: {self.teacher_lockout_resets}, indy lockout resets: {self.indy_lockout_resets}, school student lockout resets: {self.school_student_lockout_resets}, unverified teachers anonymised: {self.anonymised_unverified_teachers}, unverified independents anonymised: {self.anonymised_unverified_independents}"
522
+
523
+
524
+ class TotalActivity(models.Model):
525
+ """
526
+ A model to record total activity. Meant to only have one entry which
527
+ records all total activity. An example of this is total ever registrations.
528
+ """
529
+
530
+ teacher_registrations = models.PositiveIntegerField(default=0)
531
+ student_registrations = models.PositiveIntegerField(default=0)
532
+ independent_registrations = models.PositiveIntegerField(default=0)
533
+ anonymised_unverified_teachers = models.PositiveIntegerField(default=0)
534
+ anonymised_unverified_independents = models.PositiveIntegerField(default=0)
535
+
536
+ class Meta:
537
+ verbose_name_plural = "Total activity"
538
+
539
+ def __str__(self):
540
+ return "Total activity"
541
+
542
+
543
+ class DynamicElement(models.Model):
544
+ """
545
+ This model is meant to allow us to quickly update some elements
546
+ dynamically on the website without having to redeploy everytime. For
547
+ example, if a maintenance banner needs to be added, we check the box in
548
+ the Django admin panel, edit the text and it'll show immediately on the
549
+ website.
550
+ """
551
+
552
+ name = models.CharField(max_length=64, unique=True, editable=False)
553
+ active = models.BooleanField(default=False)
554
+ text = models.TextField(null=True, blank=True)
555
+
556
+ def __str__(self) -> str:
557
+ return self.name
@@ -0,0 +1,84 @@
1
+ from functools import wraps
2
+
3
+ from common.utils import using_two_factor
4
+ from django.http import Http404
5
+ from django.http import HttpResponseRedirect
6
+ from django.urls import reverse_lazy
7
+ from rest_framework import permissions
8
+
9
+
10
+ def has_completed_auth_setup(user):
11
+ return (not using_two_factor(user)) or (user.userprofile.is_verified and using_two_factor(user))
12
+
13
+
14
+ class LoggedInAsTeacher(permissions.BasePermission):
15
+ def has_permission(self, request, view):
16
+ return logged_in_as_teacher(request.user)
17
+
18
+
19
+ def logged_in_as_teacher(user):
20
+ try:
21
+ return user.userprofile.teacher and has_completed_auth_setup(user)
22
+ except AttributeError:
23
+ return False
24
+
25
+
26
+ def logged_in_as_student(u):
27
+ try:
28
+ if u.userprofile.student:
29
+ return True
30
+ except AttributeError:
31
+ return False
32
+
33
+
34
+ def logged_in_as_independent_student(u):
35
+ return logged_in_as_student(u) and u.userprofile.student.is_independent()
36
+
37
+
38
+ def logged_in_as_school_student(u):
39
+ return logged_in_as_student(u) and not u.userprofile.student.is_independent()
40
+
41
+
42
+ def check_teacher_authorised(request, class_teacher):
43
+ current_teacher_owns_the_class = class_teacher == request.user.new_teacher
44
+ is_current_teacher_school_admin = (
45
+ class_teacher.school == request.user.new_teacher.school and request.user.new_teacher.is_admin
46
+ )
47
+
48
+ if not (current_teacher_owns_the_class or is_current_teacher_school_admin):
49
+ raise Http404
50
+
51
+
52
+ def not_logged_in(u):
53
+ try:
54
+ if u.userprofile:
55
+ return False
56
+ except AttributeError:
57
+ return True
58
+
59
+
60
+ def not_fully_logged_in(u):
61
+ return not_logged_in(u) or (not logged_in_as_student(u) and not logged_in_as_teacher(u))
62
+
63
+
64
+ def teacher_verified(view_func):
65
+ @wraps(view_func)
66
+ def wrapped(request, *args, **kwargs):
67
+ u = request.user
68
+ try:
69
+ if not u.userprofile.teacher or not has_completed_auth_setup(u):
70
+ return HttpResponseRedirect(reverse_lazy("teach"))
71
+ except AttributeError:
72
+ return HttpResponseRedirect(reverse_lazy("teach"))
73
+ return view_func(request, *args, **kwargs)
74
+
75
+ return wrapped
76
+
77
+
78
+ class CanDeleteGame(permissions.BasePermission):
79
+ def has_permission(self, request, view):
80
+ u = request.user
81
+ try:
82
+ return u.userprofile.teacher and has_completed_auth_setup(u)
83
+ except AttributeError:
84
+ return False
File without changes
@@ -0,0 +1,30 @@
1
+ import pytest
2
+ from django_test_migrations.migrator import Migrator
3
+
4
+
5
+ @pytest.mark.django_db
6
+ def test_migration_anonymise_orphan_schools(migrator: Migrator):
7
+ state = migrator.apply_initial_migration(
8
+ ("common", "0049_anonymise_orphan_users")
9
+ )
10
+ User = state.apps.get_model("auth", "User")
11
+ UserProfile = state.apps.get_model("common", "UserProfile")
12
+ Teacher = state.apps.get_model("common", "Teacher")
13
+ School = state.apps.get_model("common", "School")
14
+
15
+ orphan_school = School.objects.create(name="OrphanSchool")
16
+ teacher_school = School.objects.create(name="TeacherSchool")
17
+
18
+ teacher_user = User.objects.create_user("TeacherUser", password="password")
19
+ teacher_userprofile = UserProfile.objects.create(user=teacher_user)
20
+ Teacher.objects.create(
21
+ user=teacher_userprofile, new_user=teacher_user, school=teacher_school
22
+ )
23
+
24
+ migrator.apply_tested_migration(("common", "0050_anonymise_orphan_schools"))
25
+
26
+ def assert_school_anonymised(pk: int, anonymised: bool):
27
+ assert School.objects.get(pk=pk).is_active != anonymised
28
+
29
+ assert_school_anonymised(orphan_school.pk, True)
30
+ assert_school_anonymised(teacher_school.pk, False)
@@ -0,0 +1,30 @@
1
+ import pytest
2
+ from django_test_migrations.migrator import Migrator
3
+
4
+
5
+ @pytest.mark.django_db
6
+ def test_migration_anonymise_orphan_users(migrator: Migrator):
7
+ state = migrator.apply_initial_migration(
8
+ ("common", "0048_unique_school_names")
9
+ )
10
+ User = state.apps.get_model("auth", "User")
11
+ UserProfile = state.apps.get_model("common", "UserProfile")
12
+ Teacher = state.apps.get_model("common", "Teacher")
13
+ Student = state.apps.get_model("common", "Student")
14
+
15
+ orphan_user = User.objects.create_user("OrphanUser", password="password")
16
+ teacher_user = User.objects.create_user("TeacherUser", password="password")
17
+ student_user = User.objects.create_user("StudentUser", password="password")
18
+ teacher_userprofile = UserProfile.objects.create(user=teacher_user)
19
+ student_userprofile = UserProfile.objects.create(user=student_user)
20
+ Teacher.objects.create(user=teacher_userprofile, new_user=teacher_user)
21
+ Student.objects.create(user=student_userprofile, new_user=student_user)
22
+
23
+ migrator.apply_tested_migration(("common", "0049_anonymise_orphan_users"))
24
+
25
+ def assert_user_anonymised(pk: int, anonymised: bool):
26
+ assert User.objects.get(pk=pk).is_active != anonymised
27
+
28
+ assert_user_anonymised(orphan_user.pk, True)
29
+ assert_user_anonymised(teacher_user.pk, False)
30
+ assert_user_anonymised(student_user.pk, False)