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,15 @@
1
+ import pytest
2
+
3
+
4
+ @pytest.mark.django_db
5
+ def test_blocked_time_added(migrator):
6
+ migrator.apply_initial_migration(("common", "0008_unlock_worksheet_3"))
7
+ new_state = migrator.apply_tested_migration(("common", "0009_add_blocked_time_to_teacher_and_student"))
8
+
9
+ teacher_model = new_state.apps.get_model("common", "Teacher")
10
+
11
+ assert teacher_model._meta.get_field("blocked_time").get_internal_type() == "DateTimeField"
12
+
13
+ student_model = new_state.apps.get_model("common", "Student")
14
+
15
+ assert student_model._meta.get_field("blocked_time").get_internal_type() == "DateTimeField"
@@ -0,0 +1,13 @@
1
+ import pytest
2
+ from django.db.models.query import QuerySet
3
+
4
+
5
+ @pytest.mark.django_db
6
+ def test_teacher_title_removed(migrator):
7
+ old_state = migrator.apply_initial_migration(("common", "0009_add_blocked_time_to_teacher_and_student"))
8
+ Teacher = old_state.apps.get_model("common", "Teacher")
9
+ assert hasattr(Teacher, "title")
10
+
11
+ new_state = migrator.apply_tested_migration(("common", "0010_remove_teacher_title"))
12
+ Teacher = new_state.apps.get_model("common", "Teacher")
13
+ assert not hasattr(Teacher, "title")
@@ -0,0 +1,33 @@
1
+ import pytest
2
+ from django_test_migrations.migrator import Migrator
3
+
4
+
5
+ @pytest.mark.django_db
6
+ def test_migration_unique_school_names(migrator: Migrator):
7
+ state = migrator.apply_initial_migration(
8
+ ("common", "0047_delete_school_postcode")
9
+ )
10
+ School = state.apps.get_model("common", "School")
11
+
12
+ school_name = "ExampleSchool"
13
+ School.objects.bulk_create(
14
+ [
15
+ School(name=school_name),
16
+ School(name=school_name),
17
+ School(name=f"{school_name} 1"),
18
+ ]
19
+ )
20
+ school_ids = list(
21
+ School.objects.order_by("-id")[:3].values_list("id", flat=True)
22
+ )
23
+ school_ids.reverse()
24
+
25
+ migrator.apply_tested_migration(("common", "0048_unique_school_names"))
26
+ School = state.apps.get_model("common", "School")
27
+
28
+ def assert_school_name(index: int, name: str):
29
+ assert School.objects.get(id=school_ids[index]).name == name
30
+
31
+ assert_school_name(0, school_name)
32
+ assert_school_name(1, f"{school_name} 2")
33
+ assert_school_name(2, f"{school_name} 1")
@@ -0,0 +1,59 @@
1
+ from datetime import datetime, timezone
2
+
3
+ import pytest
4
+ from django_test_migrations.migrator import Migrator
5
+
6
+ from portal.views.api import __anonymise_user
7
+
8
+
9
+ @pytest.mark.django_db
10
+ def test_migration_verify_returning_users(migrator: Migrator):
11
+ state = migrator.apply_initial_migration(
12
+ ("common", "0050_anonymise_orphan_schools")
13
+ )
14
+ User = state.apps.get_model("auth", "User")
15
+ UserProfile = state.apps.get_model("common", "UserProfile")
16
+
17
+ returning_user = User.objects.create_user(
18
+ "ReturningUser",
19
+ password="password",
20
+ last_login=datetime.now(tz=timezone.utc),
21
+ )
22
+ returning_userprofile = UserProfile.objects.create(user=returning_user)
23
+
24
+ non_returning_user = User.objects.create_user(
25
+ "NonReturningUser", password="password"
26
+ )
27
+ non_returning_userprofile = UserProfile.objects.create(
28
+ user=non_returning_user
29
+ )
30
+
31
+ anonymised_returning_user = User.objects.create_user(
32
+ "AnonReturningUser",
33
+ password="password",
34
+ last_login=datetime.now(tz=timezone.utc),
35
+ )
36
+ anonymised_returning_userprofile = UserProfile.objects.create(
37
+ user=anonymised_returning_user
38
+ )
39
+ __anonymise_user(anonymised_returning_user)
40
+
41
+ anonymised_non_returning_user = User.objects.create_user(
42
+ "AnonNonReturningUser", password="password"
43
+ )
44
+ anonymised_non_returning_userprofile = UserProfile.objects.create(
45
+ user=anonymised_non_returning_user
46
+ )
47
+ __anonymise_user(anonymised_non_returning_user)
48
+
49
+ migrator.apply_tested_migration(("common", "0051_verify_returning_users"))
50
+
51
+ def assert_userprofile_is_verified(pk: int, verified: bool):
52
+ assert UserProfile.objects.get(pk=pk).is_verified == verified
53
+
54
+ assert_userprofile_is_verified(returning_userprofile.pk, True)
55
+ assert_userprofile_is_verified(non_returning_userprofile.pk, False)
56
+ assert_userprofile_is_verified(anonymised_returning_userprofile.pk, True)
57
+ assert_userprofile_is_verified(
58
+ anonymised_non_returning_userprofile.pk, False
59
+ )
@@ -0,0 +1,87 @@
1
+ from common.models import DailyActivity, Student, Teacher
2
+ from django.test import TestCase
3
+ from django.utils import timezone
4
+
5
+ from ..helpers.organisation import sanitise_uk_postcode
6
+ from .utils.classes import create_class_directly
7
+ from .utils.organisation import create_organisation_directly, join_teacher_to_organisation
8
+ from .utils.student import create_independent_student_directly
9
+ from .utils.teacher import signup_teacher_directly
10
+
11
+
12
+ class TestModels(TestCase):
13
+ def test_indep_student_pending_class_request_on_delete(self):
14
+ """
15
+ Given a class and an independent student,
16
+ When the student makes a request to join the class and the class is deleted,
17
+ Then the student's pending class request field is set to null.
18
+ """
19
+ teacher_email, _ = signup_teacher_directly()
20
+ create_organisation_directly(teacher_email)
21
+ class_name = "Test Class"
22
+ klass, _, _ = create_class_directly(teacher_email, class_name)
23
+
24
+ username, _, indep_student = create_independent_student_directly()
25
+
26
+ assert indep_student.is_independent()
27
+ assert indep_student.pending_class_request is None
28
+
29
+ indep_student.pending_class_request = klass
30
+ indep_student.save()
31
+
32
+ klass.anonymise()
33
+
34
+ indep_student = Student.objects.get(new_user__username=username)
35
+
36
+ assert indep_student.pending_class_request is None
37
+
38
+ def test_creation_time(self):
39
+ teacher_email, _ = signup_teacher_directly()
40
+
41
+ sometime = timezone.now() # mark time before the school creation
42
+ school = create_organisation_directly(teacher_email)
43
+
44
+ # check the creation time
45
+ assert school.creation_time > sometime
46
+
47
+ sometime = timezone.now() # mark time before the class creation
48
+ klass, name, access_code = create_class_directly(teacher_email)
49
+ # check the creation time
50
+ assert klass.creation_time > sometime
51
+
52
+ def test_school_admins(self):
53
+ """
54
+ Test that only the admins of a school are returned by the school.admins() function.
55
+ """
56
+ email1, password1 = signup_teacher_directly()
57
+ email2, password2 = signup_teacher_directly()
58
+ email3, password3 = signup_teacher_directly()
59
+ school = create_organisation_directly(email1)
60
+ join_teacher_to_organisation(email2, school.name)
61
+ join_teacher_to_organisation(email3, school.name, is_admin=True)
62
+
63
+ teacher1 = Teacher.objects.get(new_user__username=email1)
64
+ teacher2 = Teacher.objects.get(new_user__username=email2)
65
+ teacher3 = Teacher.objects.get(new_user__username=email3)
66
+
67
+ assert len(school.admins()) == 2
68
+ assert teacher1 in school.admins()
69
+ assert teacher2 not in school.admins()
70
+ assert teacher3 in school.admins()
71
+
72
+ def test_sanitise_uk_postcode(self):
73
+ postcode_with_space = "AL10 9NE"
74
+ postcode_without_space = "AL109UL"
75
+ invalid_postcode = "123"
76
+
77
+ assert sanitise_uk_postcode(postcode_with_space) == "AL10 9NE" # Check it stays the same
78
+ assert sanitise_uk_postcode(postcode_without_space) == "AL10 9UL" # Check a space is added
79
+ assert sanitise_uk_postcode(invalid_postcode) == "123" # Check nothing happens
80
+
81
+ def test_daily_activity_serializer(self):
82
+ daily_activity = DailyActivity()
83
+
84
+ assert (
85
+ str(daily_activity)
86
+ == f"Activity on {daily_activity.date}: CSV clicks: 0, login cards clicks: 0, primary pack downloads: 0, python pack downloads: 0, level control submits: 0, teacher lockout resets: 0, indy lockout resets: 0, school student lockout resets: 0, unverified teachers anonymised: 0, unverified independents anonymised: 0"
87
+ )
File without changes
@@ -0,0 +1,38 @@
1
+ from typing import Tuple
2
+
3
+ from common.helpers.generators import generate_access_code
4
+ from common.models import Class, Teacher
5
+
6
+
7
+ def generate_details():
8
+ name = "Class %d" % generate_details.next_id
9
+ access_code = generate_access_code()
10
+
11
+ generate_details.next_id += 1
12
+
13
+ return name, access_code
14
+
15
+
16
+ generate_details.next_id = 1
17
+
18
+
19
+ def create_class_directly(teacher_email: str, class_name: str = None) -> Tuple[Class, str, str]:
20
+ """Generate a class with the details given.
21
+
22
+ Args:
23
+ teacher_email (str): The email of the teacher that will own the class.
24
+ class_name (str, optional): The name of the class. Defaults to auto-generated name.
25
+
26
+ Returns:
27
+ (class: Class, name: str, access_code: str): A tuple with the class model instance, name and access code.
28
+ """
29
+ name, access_code = generate_details()
30
+
31
+ if class_name is not None:
32
+ name = class_name
33
+
34
+ teacher = Teacher.objects.get(new_user__email=teacher_email)
35
+
36
+ klass = Class.objects.create(name=name, access_code=access_code, teacher=teacher)
37
+
38
+ return klass, name, access_code
@@ -0,0 +1,67 @@
1
+ from builtins import str
2
+
3
+
4
+ def follow_verify_email_link_to_onboarding(page, url):
5
+ page.browser.get(url)
6
+
7
+ return go_to_teacher_login_page(page.browser)
8
+
9
+
10
+ def follow_verify_email_link_to_login(page, url, user_type):
11
+ page.browser.get(url)
12
+
13
+ if user_type == "teacher":
14
+ return go_to_teacher_login_page(page.browser)
15
+ elif user_type == "independent":
16
+ return go_to_independent_student_login_page(page.browser)
17
+
18
+
19
+ def follow_duplicate_account_link_to_login(page, url, user_type):
20
+ page.browser.get(url)
21
+
22
+ if user_type == "teacher":
23
+ return go_to_teacher_login_page(page.browser)
24
+ elif user_type == "independent":
25
+ return go_to_independent_student_login_page(page.browser)
26
+
27
+
28
+ def follow_reset_email_link(browser, link):
29
+ browser.get(link)
30
+
31
+ from portal.tests.pageObjects.portal.password_reset_form_page import (
32
+ PasswordResetPage,
33
+ )
34
+
35
+ return PasswordResetPage(browser)
36
+
37
+
38
+ def follow_change_email_link_to_dashboard(page, url):
39
+ page.browser.get(url)
40
+
41
+ return go_to_teacher_login_page(page.browser)
42
+
43
+
44
+ def follow_change_email_link_to_independent_dashboard(page, url):
45
+ page.browser.get(url)
46
+
47
+ return go_to_independent_student_login_page(page.browser)
48
+
49
+
50
+ def go_to_teacher_login_page(browser):
51
+ from portal.tests.pageObjects.portal.teacher_login_page import TeacherLoginPage
52
+
53
+ return TeacherLoginPage(browser)
54
+
55
+
56
+ def go_to_teacher_dashboard_page(browser):
57
+ from portal.tests.pageObjects.portal.teach.dashboard_page import TeachDashboardPage
58
+
59
+ return TeachDashboardPage(browser)
60
+
61
+
62
+ def go_to_independent_student_login_page(browser):
63
+ from portal.tests.pageObjects.portal.independent_login_page import (
64
+ IndependentStudentLoginPage,
65
+ )
66
+
67
+ return IndependentStudentLoginPage(browser)
@@ -0,0 +1,41 @@
1
+ from common.models import School, Teacher
2
+
3
+
4
+ def generate_details(**kwargs):
5
+ name = kwargs.get("name", "School %d" % generate_details.next_id)
6
+
7
+ generate_details.next_id += 1
8
+
9
+ return name
10
+
11
+
12
+ generate_details.next_id = 1
13
+
14
+
15
+ def create_organisation_directly(teacher_email, **kwargs):
16
+ name = generate_details(**kwargs)
17
+
18
+ school = School.objects.create(name=name, country="GB")
19
+
20
+ teacher = Teacher.objects.get(new_user__email=teacher_email)
21
+ teacher.school = school
22
+ teacher.is_admin = True
23
+ teacher.save()
24
+
25
+ return school
26
+
27
+
28
+ def join_teacher_to_organisation(teacher_email, org_name, is_admin=False):
29
+ teacher = Teacher.objects.get(new_user__email=teacher_email)
30
+ school = School.objects.get(name=org_name)
31
+
32
+ teacher.school = school
33
+ teacher.is_admin = is_admin
34
+ teacher.save()
35
+
36
+
37
+ def create_organisation(page, password):
38
+ name = generate_details()
39
+ page = page.create_organisation(name, password)
40
+
41
+ return page, name
@@ -0,0 +1,123 @@
1
+ from builtins import range
2
+ from typing import Tuple
3
+ from unittest.mock import patch
4
+
5
+ from common.helpers.emails import generate_token
6
+ from common.helpers.generators import generate_login_id
7
+ from common.models import Class, Student
8
+
9
+ from . import email
10
+
11
+
12
+ def generate_school_details():
13
+ name = "Student %d" % generate_school_details.next_id
14
+ password = "Password2"
15
+
16
+ generate_school_details.next_id += 1
17
+
18
+ return name, password
19
+
20
+
21
+ generate_school_details.next_id = 1
22
+
23
+
24
+ def create_school_student_directly(access_code) -> Tuple[str, str, Student]:
25
+ """Creates a new student in the class with the specified access code.
26
+
27
+ Args:
28
+ access_code (str): The access code of the class the created student will be in
29
+
30
+ Returns:
31
+ Tuple[str, str, Student]: (name, password, student)
32
+ """
33
+ name, password = generate_school_details()
34
+
35
+ klass = Class.objects.get(access_code=access_code)
36
+
37
+ student = Student.objects.schoolFactory(klass, name, password)
38
+ return name, password, student
39
+
40
+
41
+ def create_student_with_direct_login(access_code) -> Tuple[Student, str]:
42
+ name, password = generate_school_details()
43
+ klass = Class.objects.get(access_code=access_code)
44
+
45
+ # use random string for direct login)
46
+ login_id, hashed_login_id = generate_login_id()
47
+ student = Student.objects.schoolFactory(klass, name, password, hashed_login_id)
48
+
49
+ return student, login_id, name, password
50
+
51
+
52
+ def create_independent_student_directly(preverified=True):
53
+ """
54
+ Creates a Student object and makes it independent by generating random details.
55
+ Also verifies the student's email if preverified is True.
56
+ :param preverified: whether or not the independent student's email should be
57
+ verified.
58
+ :return: the student's username, password and the student object itself.
59
+ """
60
+ name, username, email, password = generate_independent_student_details()
61
+
62
+ student = Student.objects.independentStudentFactory(name, email, password)
63
+
64
+ # verify student
65
+ generate_token(student.new_user, preverified=preverified)
66
+
67
+ return username, password, student
68
+
69
+
70
+ def create_school_student(page):
71
+ name, _ = generate_school_details()
72
+
73
+ page = page.type_student_name(name).create_students()
74
+
75
+ return page, name
76
+
77
+
78
+ def create_many_school_students(page, number_of_students):
79
+ names = ["" for i in range(number_of_students)]
80
+
81
+ for i in range(number_of_students):
82
+ names[i], _ = generate_school_details()
83
+ page = page.type_student_name(names[i])
84
+
85
+ page = page.create_students()
86
+
87
+ return page, names
88
+
89
+
90
+ def generate_independent_student_details():
91
+ name = "Independent Student %d" % generate_independent_student_details.next_id
92
+ email_address = "student%d@codeforlife.com" % generate_independent_student_details.next_id
93
+ username = email_address
94
+ password = "$RFVBGT%^YHNmju7$RFVBGT%^YHNmju7$RFVBGT%^YHNmju7"
95
+
96
+ generate_independent_student_details.next_id += 1
97
+
98
+ return name, username, email_address, password
99
+
100
+
101
+ generate_independent_student_details.next_id = 1
102
+
103
+
104
+ @patch("common.helpers.emails.send_dotdigital_email")
105
+ def create_independent_student(page, mock_send_dotdigital_email):
106
+ page = page.go_to_signup_page()
107
+
108
+ name, username, email_address, password = generate_independent_student_details()
109
+ page = page.independent_student_signup(name, email_address, password=password, confirm_password=password)
110
+
111
+ page = page.return_to_home_page()
112
+
113
+ verification_url = mock_send_dotdigital_email.call_args.kwargs["personalization_values"]["VERIFICATION_LINK"]
114
+
115
+ page = email.follow_verify_email_link_to_login(page, verification_url, "independent")
116
+
117
+ return page, name, username, email_address, password
118
+
119
+
120
+ def verify_email(page, verification_url):
121
+ page = email.follow_verify_email_link_to_login(page, verification_url, "independent")
122
+
123
+ return page
@@ -0,0 +1,73 @@
1
+ import random
2
+ import sys
3
+ from unittest.mock import patch
4
+
5
+ from common.helpers.emails import generate_token
6
+ from common.models import Teacher
7
+
8
+ from . import email
9
+
10
+
11
+ def generate_details(**kwargs):
12
+ random_int = random.randint(1, sys.maxsize)
13
+ first_name = kwargs.get("first_name", "Test")
14
+ last_name = kwargs.get("last_name", f"Teacher {random_int}")
15
+ email_address = kwargs.get("email_address", f"testteacher{random_int}@codeforlife.com")
16
+ password = kwargs.get("password", "$RFVBGT%6yhn$RFVBGT%6yhn$RFVBGT%6yhn$RFVBGT%6yhn")
17
+
18
+ return first_name, last_name, email_address, password
19
+
20
+
21
+ def signup_teacher_directly(preverified=True, **kwargs):
22
+ """
23
+ Creates a Teacher object by using the details passed in as kwargs, or by
24
+ generating random details. Also verifies the teacher's email if preverified
25
+ is True.
26
+ :param preverified: whether or not the teacher's email should be verified.
27
+ :return: the teacher's email and password.
28
+ """
29
+ first_name, last_name, email_address, password = generate_details(**kwargs)
30
+ teacher = Teacher.objects.factory(first_name, last_name, email_address, password)
31
+ generate_token(teacher.new_user, preverified=preverified)
32
+ teacher.user.save()
33
+ return email_address, password
34
+
35
+
36
+ @patch("portal.views.home.send_dotdigital_email")
37
+ def signup_duplicate_teacher_fail(page, duplicate_email, mock_send_dotdigital_email):
38
+ page = page.go_to_signup_page()
39
+
40
+ first_name, last_name, email_address, password = generate_details()
41
+ page = page.signup(first_name, last_name, duplicate_email, password, password)
42
+
43
+ page = page.return_to_home_page()
44
+
45
+ login_link = mock_send_dotdigital_email.call_args.kwargs["personalization_values"]["LOGIN_URL"]
46
+
47
+ page = email.follow_duplicate_account_link_to_login(page, login_link, "teacher")
48
+
49
+ return page, email_address, password
50
+
51
+
52
+ @patch("common.helpers.emails.send_dotdigital_email")
53
+ def signup_teacher(page, mock_send_dotdigital_email, newsletter=False):
54
+ page = page.go_to_signup_page()
55
+
56
+ first_name, last_name, email_address, password = generate_details()
57
+ page = page.signup(
58
+ first_name, last_name, email_address, password=password, confirm_password=password, newsletter=newsletter
59
+ )
60
+
61
+ page = page.return_to_home_page()
62
+
63
+ verification_url = mock_send_dotdigital_email.call_args.kwargs["personalization_values"]["VERIFICATION_LINK"]
64
+
65
+ page = email.follow_verify_email_link_to_onboarding(page, verification_url)
66
+
67
+ return page, email_address, password
68
+
69
+
70
+ def verify_email(page, verification_url):
71
+ page = email.follow_verify_email_link_to_login(page, verification_url, "teacher")
72
+
73
+ return page
@@ -0,0 +1,27 @@
1
+ from django.contrib.auth.models import User
2
+ from django.utils import timezone
3
+
4
+
5
+ def get_superuser():
6
+ """Get a superuser for testing, or create one if there isn't one."""
7
+ try:
8
+ return User.objects.get(username="superuser")
9
+ except User.DoesNotExist:
10
+ return User.objects.create_superuser("superuser", "superuser@codeforlife.education", "password")
11
+
12
+
13
+ def create_user_directly(active=True, **kwargs):
14
+ """Create a user in the database."""
15
+ days_to_subtract = 10 if active else 2000
16
+ username = "old_user+{:d}".format(create_user_directly.next_id)
17
+ user = User.objects.create_user(username, password="password")
18
+ user.last_login = timezone.now() - timezone.timedelta(days=days_to_subtract)
19
+ user.date_joined = timezone.now() - timezone.timedelta(days=days_to_subtract - 1)
20
+ user.save()
21
+
22
+ create_user_directly.next_id += 1
23
+
24
+ return user
25
+
26
+
27
+ create_user_directly.next_id = 1
@@ -0,0 +1,56 @@
1
+ from django.contrib.auth.mixins import LoginRequiredMixin
2
+ from django.contrib.auth.views import redirect_to_login
3
+ from django.core.cache import cache
4
+ from django.core.exceptions import FieldDoesNotExist
5
+ from two_factor.utils import default_device
6
+
7
+
8
+ def two_factor_cache_key(user):
9
+ """Cache key for using_two_factor."""
10
+ return "using-two-factor-%s" % user.pk
11
+
12
+
13
+ def _using_two_factor(user):
14
+ """Returns whether the user is using 2fa or not."""
15
+ return default_device(user)
16
+
17
+
18
+ def using_two_factor(user):
19
+ """Returns whether the user is using 2fa or not (Cached)."""
20
+ if hasattr(user, "using_two_factor_cache"):
21
+ # First try local memory, as we call this a lot in one request
22
+ return user.using_two_factor_cache
23
+ cache_key = two_factor_cache_key(user)
24
+ val = cache.get(cache_key)
25
+ if val is not None:
26
+ # If local memory failed, but we got it from memcache, set local memory
27
+ user.using_two_factor_cache = val
28
+ return val
29
+ val = bool(_using_two_factor(user))
30
+
31
+ # We didn't find it in the cache, so set it there and local memory
32
+ cache.set(cache_key, val, None) # Cache forever
33
+ user.using_two_factor_cache = val
34
+ return val
35
+
36
+
37
+ def field_exists(model, field):
38
+ try:
39
+ field = model._meta.get_field(field)
40
+ except FieldDoesNotExist:
41
+ return False
42
+ return True
43
+
44
+
45
+ class LoginRequiredNoErrorMixin(LoginRequiredMixin):
46
+ """
47
+ Overwrites Django's 2.2 LoginRequiredMixin so as to not raise an error and
48
+ redirect instead.
49
+ """
50
+
51
+ def handle_no_permission(self):
52
+ return redirect_to_login(
53
+ self.request.get_full_path(),
54
+ self.get_login_url(),
55
+ self.get_redirect_field_name(),
56
+ )