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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (391) hide show
  1. cfl_common/common/__init__.py +1 -0
  2. cfl_common/common/app_settings.py +66 -0
  3. cfl_common/common/apps.py +6 -0
  4. cfl_common/common/context_processors.py +9 -0
  5. cfl_common/common/csp_config.py +85 -0
  6. cfl_common/common/helpers/__init__.py +0 -0
  7. cfl_common/common/helpers/data_migration_loader.py +42 -0
  8. cfl_common/common/helpers/emails.py +393 -0
  9. cfl_common/common/helpers/generators.py +52 -0
  10. cfl_common/common/helpers/organisation.py +10 -0
  11. cfl_common/common/mail.py +201 -0
  12. cfl_common/common/migrations/0001_initial.py +240 -0
  13. cfl_common/common/migrations/0002_emailverification.py +55 -0
  14. cfl_common/common/migrations/0003_aimmocharacter.py +31 -0
  15. cfl_common/common/migrations/0004_add_aimmocharacters.py +17 -0
  16. cfl_common/common/migrations/0005_add_worksheets.py +8 -0
  17. cfl_common/common/migrations/0006_update_aimmo_character_image_path.py +17 -0
  18. cfl_common/common/migrations/0007_add_pdf_names_to_first_two_worksheets.py +8 -0
  19. cfl_common/common/migrations/0008_unlock_worksheet_3.py +11 -0
  20. cfl_common/common/migrations/0009_add_blocked_time_to_teacher_and_student.py +24 -0
  21. cfl_common/common/migrations/0010_remove_teacher_title.py +18 -0
  22. cfl_common/common/migrations/0011_student_login_id.py +18 -0
  23. cfl_common/common/migrations/0012_usersession.py +39 -0
  24. cfl_common/common/migrations/0013_class_school.py +42 -0
  25. cfl_common/common/migrations/0014_login_type.py +29 -0
  26. cfl_common/common/migrations/0015_dailyactivity.py +31 -0
  27. cfl_common/common/migrations/0016_joinreleasestudent.py +42 -0
  28. cfl_common/common/migrations/0017_copy_email_to_username.py +18 -0
  29. cfl_common/common/migrations/0018_update_aimmo_character_image_path.py +15 -0
  30. cfl_common/common/migrations/0019_aimmocharacter_alt.py +16 -0
  31. cfl_common/common/migrations/0020_class_is_active_and_null_access_code.py +23 -0
  32. cfl_common/common/migrations/0021_school_is_active.py +28 -0
  33. cfl_common/common/migrations/0022_school_cleanup.py +29 -0
  34. cfl_common/common/migrations/0023_userprofile_aimmo_badges.py +22 -0
  35. cfl_common/common/migrations/0024_teacher_invited_by.py +25 -0
  36. cfl_common/common/migrations/0025_schoolteacherinvitation.py +47 -0
  37. cfl_common/common/migrations/0026_teacher_remove_join_request.py +22 -0
  38. cfl_common/common/migrations/0027_class_created_by.py +25 -0
  39. cfl_common/common/migrations/0028_coding_club_downloads.py +23 -0
  40. cfl_common/common/migrations/0029_dynamicelement.py +22 -0
  41. cfl_common/common/migrations/0030_add_maintenance_banner.py +25 -0
  42. cfl_common/common/migrations/0031_improve_admin_panel.py +56 -0
  43. cfl_common/common/migrations/0032_dailyactivity_level_control_submits.py +18 -0
  44. cfl_common/common/migrations/0033_password_reset_tracking_fields.py +23 -0
  45. cfl_common/common/migrations/0034_dailyactivity_daily_school_student_lockout_reset.py +18 -0
  46. cfl_common/common/migrations/0035_rename_lockout_fields.py +27 -0
  47. cfl_common/common/migrations/0036_rename_awaiting_email_verification_userprofile_is_verified.py +17 -0
  48. cfl_common/common/migrations/0037_migrate_email_verification.py +21 -0
  49. cfl_common/common/migrations/0038_delete_emailverification.py +16 -0
  50. cfl_common/common/migrations/0039_copy_email_to_username.py +18 -0
  51. cfl_common/common/migrations/0040_school_county.py +18 -0
  52. cfl_common/common/migrations/0041_populate_gb_counties.py +27 -0
  53. cfl_common/common/migrations/0042_totalactivity.py +25 -0
  54. cfl_common/common/migrations/0043_add_total_activity.py +30 -0
  55. cfl_common/common/migrations/0044_update_activity_models.py +33 -0
  56. cfl_common/common/migrations/0045_otp.py +23 -0
  57. cfl_common/common/migrations/0046_alter_school_country.py +19 -0
  58. cfl_common/common/migrations/0047_delete_school_postcode.py +16 -0
  59. cfl_common/common/migrations/0048_unique_school_names.py +42 -0
  60. cfl_common/common/migrations/0049_anonymise_orphan_users.py +29 -0
  61. cfl_common/common/migrations/0050_anonymise_orphan_schools.py +30 -0
  62. cfl_common/common/migrations/0051_verify_returning_users.py +26 -0
  63. cfl_common/common/migrations/0052_add_cse_fields.py +68 -0
  64. cfl_common/common/migrations/0053_clean_class_data.py +24 -0
  65. cfl_common/common/migrations/0054_delete_aimmo_models.py +20 -0
  66. cfl_common/common/migrations/0055_alter_schoolteacherinvitation_token.py +18 -0
  67. cfl_common/common/migrations/0056_set_non_school_teachers_as_non_admins.py +25 -0
  68. cfl_common/common/migrations/0057_teacher_teacher__is_admin.py +19 -0
  69. cfl_common/common/migrations/0058_userprofile_google_refresh_token_and_more.py +24 -0
  70. cfl_common/common/migrations/__init__.py +0 -0
  71. cfl_common/common/models.py +557 -0
  72. cfl_common/common/permissions.py +84 -0
  73. cfl_common/common/tests/__init__.py +0 -0
  74. cfl_common/common/tests/test_migration_anonymise_orphan_schools.py +30 -0
  75. cfl_common/common/tests/test_migration_anonymise_orphan_users.py +30 -0
  76. cfl_common/common/tests/test_migration_blocked_time.py +15 -0
  77. cfl_common/common/tests/test_migration_remove_teacher_title.py +13 -0
  78. cfl_common/common/tests/test_migration_unique_school_names.py +33 -0
  79. cfl_common/common/tests/test_migration_verify_returning_users.py +59 -0
  80. cfl_common/common/tests/test_models.py +87 -0
  81. cfl_common/common/tests/utils/__init__.py +0 -0
  82. cfl_common/common/tests/utils/classes.py +38 -0
  83. cfl_common/common/tests/utils/email.py +67 -0
  84. cfl_common/common/tests/utils/organisation.py +41 -0
  85. cfl_common/common/tests/utils/student.py +123 -0
  86. cfl_common/common/tests/utils/teacher.py +73 -0
  87. cfl_common/common/tests/utils/user.py +27 -0
  88. cfl_common/common/utils.py +56 -0
  89. cfl_common/setup.py +61 -0
  90. codeforlife_portal-8.9.9.dist-info/METADATA +226 -0
  91. {codeforlife_portal-5.33.5.dist-info → codeforlife_portal-8.9.9.dist-info}/RECORD +339 -241
  92. {codeforlife_portal-5.33.5.dist-info → codeforlife_portal-8.9.9.dist-info}/WHEEL +1 -1
  93. codeforlife_portal-8.9.9.dist-info/licenses/LICENSE.md +3 -0
  94. {codeforlife_portal-5.33.5.dist-info → codeforlife_portal-8.9.9.dist-info}/top_level.txt +1 -0
  95. deploy/middleware/maintenance.py +25 -0
  96. deploy/middleware/screentime_warning.py +29 -0
  97. deploy/middleware/security.py +5 -6
  98. deploy/middleware/session_timeout.py +4 -2
  99. deploy/middleware/tmp_basic_auth.py +41 -0
  100. example_project/portal_test_settings.py +239 -0
  101. example_project/settings.py +156 -17
  102. example_project/urls.py +5 -6
  103. portal/__init__.py +1 -1
  104. portal/admin.py +142 -29
  105. portal/app_settings.py +8 -7
  106. portal/forms/dotmailer.py +6 -4
  107. portal/forms/invite_teacher.py +19 -10
  108. portal/forms/organisation.py +137 -68
  109. portal/forms/play.py +53 -98
  110. portal/forms/registration.py +70 -164
  111. portal/forms/teach.py +147 -121
  112. portal/handlers.py +1 -2
  113. portal/helpers/decorators.py +30 -10
  114. portal/helpers/password.py +86 -47
  115. portal/helpers/ratelimit.py +32 -15
  116. portal/helpers/regexes.py +5 -0
  117. portal/helpers/request_handlers.py +10 -0
  118. portal/migrations/0044_auto_20150430_0959.py +6 -2
  119. portal/mixins/__init__.py +1 -0
  120. portal/mixins/cron_mixin.py +12 -0
  121. portal/permissions/__init__.py +1 -0
  122. portal/permissions/is_cron_request_from_google.py +14 -0
  123. portal/static/portal/img/10_years_anniversary.png +0 -0
  124. portal/static/portal/img/RR_logo_grass_background.png +0 -0
  125. portal/static/portal/img/coding_club_hero.jpg +0 -0
  126. portal/static/portal/img/coding_club_python_pack.png +0 -0
  127. portal/static/portal/img/facebook.png +0 -0
  128. portal/static/portal/img/gitbook.png +0 -0
  129. portal/static/portal/img/howe_dell_1.png +0 -0
  130. portal/static/portal/img/howe_dell_2.png +0 -0
  131. portal/static/portal/img/howe_dell_3.png +0 -0
  132. portal/static/portal/img/logo_cfl.png +0 -0
  133. portal/static/portal/img/logo_cfl_powered.svg +35 -0
  134. portal/static/portal/img/logo_cfl_reminder_cards.jpg +0 -0
  135. portal/static/portal/img/logo_ocado_group.png +0 -0
  136. portal/static/portal/img/logo_python_den.svg +21 -0
  137. portal/static/portal/img/long_europe_map.png +0 -0
  138. portal/static/portal/img/python_den.png +0 -0
  139. portal/static/portal/img/python_den_banner.svg +26 -0
  140. portal/static/portal/img/rapid_router_landing_hero.png +0 -0
  141. portal/static/portal/img/rr_advanced.png +0 -0
  142. portal/static/portal/img/ten_year_map_pin.svg +1 -0
  143. portal/static/portal/img/thumbnail_educate_rapid_router.png +0 -0
  144. portal/static/portal/img/thumbnail_educate_resources.png +0 -0
  145. portal/static/portal/img/thumbnail_play_rapid_router.png +0 -0
  146. portal/static/portal/img/thumbnail_python_den.png +0 -0
  147. portal/static/portal/img/twitter.png +0 -0
  148. portal/static/portal/js/carouselCards.js +25 -0
  149. portal/static/portal/js/common.js +96 -1
  150. portal/static/portal/js/independentLogin.js +16 -0
  151. portal/static/portal/js/independentRegistration.js +86 -0
  152. portal/static/portal/js/levelControl.js +77 -0
  153. portal/static/portal/js/lib/jquery.min.js +2 -0
  154. portal/static/portal/js/organisation_manage.js +142 -14
  155. portal/static/portal/js/passwordStrength.js +154 -64
  156. portal/static/portal/js/resetPassword.js +23 -0
  157. portal/static/portal/js/riveted.min.js +238 -239
  158. portal/static/portal/js/school.js +13 -0
  159. portal/static/portal/js/studentLogin.js +16 -0
  160. portal/static/portal/js/teacherEditStudent.js +23 -0
  161. portal/static/portal/js/teacherLogin.js +16 -0
  162. portal/static/portal/js/tenYearMap.js +14 -0
  163. portal/static/portal/sass/colorbox.scss +0 -1
  164. portal/static/portal/sass/modules/_colours.scss +1 -0
  165. portal/static/portal/sass/modules/_levels.scss +1 -1
  166. portal/static/portal/sass/modules/_mixins.scss +21 -0
  167. portal/static/portal/sass/partials/_banners.scss +4 -177
  168. portal/static/portal/sass/partials/_buttons.scss +12 -15
  169. portal/static/portal/sass/partials/_carousel.scss +129 -0
  170. portal/static/portal/sass/partials/_footer.scss +21 -22
  171. portal/static/portal/sass/partials/_forms.scss +60 -5
  172. portal/static/portal/sass/partials/_grids.scss +34 -61
  173. portal/static/portal/sass/partials/_header.scss +28 -20
  174. portal/static/portal/sass/partials/_images.scss +292 -39
  175. portal/static/portal/sass/partials/_popup.scss +18 -15
  176. portal/static/portal/sass/partials/_tables.scss +12 -20
  177. portal/static/portal/sass/partials/_text.scss +6 -10
  178. portal/static/portal/sass/styles.scss +0 -1
  179. portal/static/portal/video/code for life .pdf +0 -0
  180. portal/strings/about.py +5 -0
  181. portal/strings/coding_club.py +9 -0
  182. portal/strings/play.py +6 -5
  183. portal/strings/teach.py +1 -1
  184. portal/strings/teacher_resources.py +2 -8
  185. portal/strings/ten_year_map.py +13 -0
  186. portal/templates/403.html +2 -2
  187. portal/templates/404.html +1 -1
  188. portal/templates/500.html +2 -2
  189. portal/templates/{captcha → django_recaptcha}/includes/js_v2_invisible.html +3 -3
  190. portal/templates/{captcha → django_recaptcha}/widget_v2_invisible.html +2 -2
  191. portal/templates/email.html +4 -2
  192. portal/templates/maintenance.html +34 -0
  193. portal/templates/portal/about.html +94 -62
  194. portal/templates/portal/base.html +176 -152
  195. portal/templates/portal/coding_club.html +100 -0
  196. portal/templates/portal/contribute.html +56 -52
  197. portal/templates/portal/email_invitation_sent.html +1 -1
  198. portal/templates/portal/email_style_template.html +374 -0
  199. portal/templates/portal/email_verification_failed.html +1 -1
  200. portal/templates/portal/email_verification_needed.html +9 -9
  201. portal/templates/portal/form_shapes.html +20 -8
  202. portal/templates/portal/getinvolved.html +6 -6
  203. portal/templates/portal/home.html +35 -10
  204. portal/templates/portal/home_learning.html +19 -19
  205. portal/templates/portal/locked_out.html +0 -1
  206. portal/templates/portal/locked_out_school_student.html +16 -0
  207. portal/templates/portal/login/independent_student.html +31 -15
  208. portal/templates/portal/login/student.html +10 -7
  209. portal/templates/portal/login/student_class_code.html +7 -4
  210. portal/templates/portal/login/teacher.html +34 -17
  211. portal/templates/portal/partials/banner.html +18 -4
  212. portal/templates/portal/partials/benefits.html +1 -1
  213. portal/templates/portal/partials/card_list.html +34 -24
  214. portal/templates/portal/partials/character_list.html +5 -5
  215. portal/templates/portal/partials/cookie_list.html +161 -0
  216. portal/templates/portal/partials/delete_popup.html +18 -0
  217. portal/templates/portal/partials/footer.html +57 -26
  218. portal/templates/portal/partials/header.html +118 -117
  219. portal/templates/portal/partials/hero_card.html +4 -3
  220. portal/templates/portal/partials/info_popup.html +3 -3
  221. portal/templates/portal/partials/invite_admin_teacher.html +23 -0
  222. portal/templates/portal/partials/popup.html +7 -2
  223. portal/templates/portal/partials/register_newsletter_tickbox.html +2 -5
  224. portal/templates/portal/partials/screentime_popup.html +14 -0
  225. portal/templates/portal/partials/service_unavailable_popup.html +17 -0
  226. portal/templates/portal/partials/session_popup.html +19 -0
  227. portal/templates/portal/play/student_dashboard.html +42 -29
  228. portal/templates/portal/play/student_edit_account.html +64 -9
  229. portal/templates/portal/play.html +61 -41
  230. portal/templates/portal/privacy_notice.html +697 -0
  231. portal/templates/portal/register.html +122 -92
  232. portal/templates/portal/reset_password.html +20 -40
  233. portal/templates/portal/reset_password_confirm.html +9 -4
  234. portal/templates/portal/reset_password_email_sent.html +15 -13
  235. portal/templates/portal/teach/base_registering.html +1 -1
  236. portal/templates/portal/teach/class.html +4 -6
  237. portal/templates/portal/teach/dashboard.html +212 -117
  238. portal/templates/portal/teach/invited.html +90 -0
  239. portal/templates/portal/teach/onboarding_classes.html +5 -3
  240. portal/templates/portal/teach/onboarding_print.html +1 -1
  241. portal/templates/portal/teach/onboarding_school.html +26 -139
  242. portal/templates/portal/teach/onboarding_students.html +1 -1
  243. portal/templates/portal/teach/teacher_dismiss_students.html +73 -55
  244. portal/templates/portal/teach/teacher_edit_class.html +168 -11
  245. portal/templates/portal/teach/teacher_edit_student.html +12 -5
  246. portal/templates/portal/teach/teacher_move_all_classes.html +25 -38
  247. portal/templates/portal/teach/teacher_move_students_to_class.html +1 -1
  248. portal/templates/portal/teach.html +61 -42
  249. portal/templates/portal/ten_year_map.html +147 -0
  250. portal/templates/portal/terms.html +191 -42
  251. portal/templates/two_factor/core/login.html +71 -59
  252. portal/templates/two_factor/core/setup.html +58 -49
  253. portal/templates/two_factor/profile/disable.html +1 -1
  254. portal/templates/two_factor/profile/profile.html +35 -17
  255. portal/templatetags/app_tags.py +59 -84
  256. portal/templatetags/card_list_tags.py +0 -4
  257. portal/tests/base_test.py +14 -3
  258. portal/tests/conftest.py +0 -15
  259. portal/tests/migrations/test_migration_make_portaladmin_teacher.py +2 -6
  260. portal/tests/migrations/test_migration_preview_users.py +3 -9
  261. portal/tests/migrations/test_migration_remove_guardian.py +1 -3
  262. portal/tests/migrations/test_migration_use_common_models.py +2 -6
  263. portal/tests/migrations/test_migration_verify_portaladmin.py +1 -3
  264. portal/tests/pageObjects/portal/admin/admin_base_page.py +0 -21
  265. portal/tests/pageObjects/portal/base_page.py +16 -26
  266. portal/tests/pageObjects/portal/email_verification_needed_page.py +3 -2
  267. portal/tests/pageObjects/portal/game_page.py +12 -19
  268. portal/tests/pageObjects/portal/home_page.py +13 -15
  269. portal/tests/pageObjects/portal/independent_login_page.py +13 -17
  270. portal/tests/pageObjects/portal/password_reset_form_page.py +20 -4
  271. portal/tests/pageObjects/portal/password_reset_page.py +25 -0
  272. portal/tests/pageObjects/portal/play/account_page.py +18 -27
  273. portal/tests/pageObjects/portal/play/dashboard_page.py +4 -4
  274. portal/tests/pageObjects/portal/play/join_school_or_club_page.py +8 -10
  275. portal/tests/pageObjects/portal/play/play_base_page.py +5 -3
  276. portal/tests/pageObjects/portal/signup_page.py +28 -59
  277. portal/tests/pageObjects/portal/student_login_class_code.py +6 -9
  278. portal/tests/pageObjects/portal/student_login_page.py +6 -8
  279. portal/tests/pageObjects/portal/teach/add_independent_student_to_class_page.py +3 -3
  280. portal/tests/pageObjects/portal/teach/added_independent_student_to_class_page.py +3 -1
  281. portal/tests/pageObjects/portal/teach/class_page.py +36 -13
  282. portal/tests/pageObjects/portal/teach/dashboard_page.py +43 -84
  283. portal/tests/pageObjects/portal/teach/dismiss_students_page.py +7 -5
  284. portal/tests/pageObjects/portal/teach/edit_student_page.py +10 -8
  285. portal/tests/pageObjects/portal/teach/move_class_page.py +5 -10
  286. portal/tests/pageObjects/portal/teach/move_classes_page.py +4 -2
  287. portal/tests/pageObjects/portal/teach/move_students_disambiguate_page.py +4 -2
  288. portal/tests/pageObjects/portal/teach/move_students_page.py +6 -13
  289. portal/tests/pageObjects/portal/teach/onboarding_classes_page.py +5 -3
  290. portal/tests/pageObjects/portal/teach/onboarding_organisation_page.py +11 -49
  291. portal/tests/pageObjects/portal/teach/onboarding_student_list_page.py +7 -12
  292. portal/tests/pageObjects/portal/teach/onboarding_students_page.py +4 -27
  293. portal/tests/pageObjects/portal/teach/teach_base_page.py +6 -4
  294. portal/tests/pageObjects/portal/teacher_login_page.py +10 -16
  295. portal/tests/selenium_test_case.py +3 -43
  296. portal/tests/snapshots/snap_test_partials.py +11 -165
  297. portal/tests/test_2FA.py +15 -33
  298. portal/tests/test_admin.py +15 -97
  299. portal/tests/test_api.py +212 -91
  300. portal/tests/test_captcha_forms.py +2 -2
  301. portal/tests/test_class.py +374 -24
  302. portal/tests/test_emails.py +83 -20
  303. portal/tests/{test_newsletter_footer.py → test_global_forms.py} +5 -5
  304. portal/tests/test_helper_methods.py +30 -0
  305. portal/tests/test_independent_student.py +255 -144
  306. portal/tests/test_invite_teacher.py +318 -10
  307. portal/tests/test_middleware.py +96 -9
  308. portal/tests/test_organisation.py +78 -262
  309. portal/tests/test_partials.py +0 -88
  310. portal/tests/test_ratelimit.py +218 -36
  311. portal/tests/test_school_student.py +35 -40
  312. portal/tests/test_security.py +12 -31
  313. portal/tests/test_teacher.py +425 -325
  314. portal/tests/test_teacher_student.py +103 -91
  315. portal/tests/test_views.py +900 -76
  316. portal/tests/utils/classes.py +2 -2
  317. portal/tests/utils/messages.py +13 -28
  318. portal/urls.py +235 -166
  319. portal/views/admin.py +0 -332
  320. portal/views/api.py +82 -48
  321. portal/views/cron/__init__.py +1 -0
  322. portal/views/cron/user.py +322 -0
  323. portal/views/dotmailer.py +9 -1
  324. portal/views/email.py +33 -77
  325. portal/views/google_analytics.py +28 -0
  326. portal/views/home.py +126 -97
  327. portal/views/legal.py +13 -0
  328. portal/views/login/independent_student.py +5 -5
  329. portal/views/login/student.py +51 -14
  330. portal/views/login/teacher.py +2 -6
  331. portal/views/organisation.py +20 -189
  332. portal/views/registration.py +97 -17
  333. portal/views/student/edit_account_details.py +99 -72
  334. portal/views/student/play.py +81 -62
  335. portal/views/teacher/dashboard.py +421 -149
  336. portal/views/teacher/teach.py +226 -177
  337. portal/views/two_factor/core.py +22 -19
  338. portal/views/two_factor/profile.py +2 -2
  339. codeforlife_portal-5.33.5.dist-info/LICENSE.md +0 -577
  340. codeforlife_portal-5.33.5.dist-info/METADATA +0 -38
  341. deploy/permissions.py +0 -2
  342. example_project/manage.py +0 -10
  343. portal/autoconfig.py +0 -141
  344. portal/csp_config.py +0 -60
  345. portal/forms/add_game.py +0 -33
  346. portal/helpers/location.py +0 -121
  347. portal/static/portal/img/kurono_hero.jpg +0 -0
  348. portal/static/portal/img/kurono_landing_hero.png +0 -0
  349. portal/static/portal/img/kurono_logo.svg +0 -1
  350. portal/static/portal/img/kurono_logo_grey_background.svg +0 -1
  351. portal/static/portal/img/kurono_logo_mark.svg +0 -1
  352. portal/static/portal/img/kurono_resources_hero.jpg +0 -0
  353. portal/static/portal/img/kurono_story.png +0 -0
  354. portal/static/portal/img/ocado-swirl.svg +0 -22
  355. portal/static/portal/img/thumbnail_educate_kurono.png +0 -0
  356. portal/static/portal/img/thumbnail_educate_resources_and_progress_tracking.png +0 -0
  357. portal/static/portal/img/thumbnail_kurono_resources.png +0 -0
  358. portal/static/portal/img/thumbnail_play_kurono.png +0 -0
  359. portal/static/portal/img/x_close_video.png +0 -0
  360. portal/static/portal/js/aimmoGame.js +0 -106
  361. portal/static/portal/js/deleteWorkspaces.js +0 -14
  362. portal/static/portal/js/fuzzySchoolLookup.js +0 -46
  363. portal/static/portal/js/lib/jquery-3.5.1.min.js +0 -2
  364. portal/static/portal/js/lib/jquery-ui-1.12.1.min.js +0 -13
  365. portal/static/portal/sass/partials/_videos.scss +0 -10
  366. portal/static/portal/video/aimmo_play_now_background_video.mp4 +0 -0
  367. portal/strings/student_aimmo_dashboard.py +0 -6
  368. portal/templates/portal/admin/aggregated_data.html +0 -35
  369. portal/templates/portal/admin/map.html +0 -70
  370. portal/templates/portal/mouseflow.html +0 -9
  371. portal/templates/portal/partials/aimmo_games_table.html +0 -83
  372. portal/templates/portal/partials/register_over_required_age_tickbox.html +0 -9
  373. portal/templates/portal/play/independent_student_dashboard.html +0 -64
  374. portal/templates/portal/play/student_aimmo_dashboard.html +0 -63
  375. portal/templates/portal/privacy_policy.html +0 -483
  376. portal/templates/portal/reset_password_email.html +0 -9
  377. portal/templates/portal/teach/invite.html +0 -25
  378. portal/templates/portal/teach/teacher_aimmo_dashboard.html +0 -95
  379. portal/templates/portal/teach/teacher_resources.html +0 -68
  380. portal/templatetags/character_list_tags.py +0 -16
  381. portal/tests/pageObjects/portal/kurono_teacher_dashboard_page.py +0 -49
  382. portal/tests/pageObjects/portal/student_password_reset_form_page.py +0 -23
  383. portal/tests/pageObjects/portal/teach/onboarding_revoke_request_page.py +0 -20
  384. portal/tests/pageObjects/portal/teacher_password_reset_form_page.py +0 -23
  385. portal/tests/test_aimmo_dashboards.py +0 -172
  386. portal/tests/test_location.py +0 -217
  387. portal/tests/utils/aimmo_games.py +0 -30
  388. portal/views/aimmo/dashboard.py +0 -119
  389. portal/views/privacy_policy.py +0 -9
  390. portal/views/teacher/teacher_resources.py +0 -42
  391. {portal/views/aimmo → cfl_common}/__init__.py +0 -0
@@ -1,33 +1,54 @@
1
- from common import email_messages
2
- from common.helpers.emails import NOTIFICATION_EMAIL, send_email, update_email
3
- from common.helpers.generators import generate_access_code, get_random_username
4
- from common.models import Class, Student, Teacher
5
- from common.permissions import logged_in_as_teacher
1
+ from datetime import timedelta
2
+ from uuid import uuid4
3
+
4
+ from common.helpers.emails import (
5
+ DotmailerUserType,
6
+ add_to_dotmailer,
7
+ generate_token,
8
+ update_email,
9
+ )
10
+ from common.helpers.generators import get_random_username
11
+ from common.mail import address_book_ids, campaign_ids, send_dotdigital_email
12
+ from common.models import (
13
+ Class,
14
+ JoinReleaseStudent,
15
+ SchoolTeacherInvitation,
16
+ Student,
17
+ Teacher,
18
+ )
19
+ from common.permissions import check_teacher_authorised, logged_in_as_teacher
6
20
  from common.utils import using_two_factor
7
21
  from django.contrib import messages as messages
8
22
  from django.contrib.auth import logout
9
23
  from django.contrib.auth.decorators import login_required, user_passes_test
24
+ from django.contrib.auth.models import User
10
25
  from django.http import Http404, HttpResponseRedirect
11
26
  from django.shortcuts import get_object_or_404, render
12
- from django.urls import reverse_lazy
27
+ from django.urls import reverse, reverse_lazy
28
+ from django.utils import timezone
13
29
  from django.views.decorators.http import require_POST
30
+ from game.level_management import levels_shared_with, unshare_level
31
+ from game.models import Level
32
+ from game.views.level_selection import is_admin_teacher
33
+ from two_factor.utils import devices_for_user
34
+
35
+ from portal.forms.invite_teacher import InviteTeacherForm
14
36
  from portal.forms.organisation import OrganisationForm
37
+ from portal.forms.registration import DeleteAccountForm
15
38
  from portal.forms.teach import (
16
39
  ClassCreationForm,
40
+ InvitedTeacherForm,
17
41
  TeacherAddExternalStudentForm,
18
42
  TeacherEditAccountForm,
19
43
  )
20
44
  from portal.helpers.decorators import ratelimit
21
- from portal.helpers.location import lookup_coord
22
45
  from portal.helpers.password import check_update_password
23
46
  from portal.helpers.ratelimit import (
24
- RATELIMIT_GROUP,
47
+ RATELIMIT_LOGIN_GROUP,
48
+ RATELIMIT_LOGIN_RATE,
25
49
  RATELIMIT_METHOD,
26
- RATELIMIT_RATE,
27
50
  clear_ratelimit_cache_for_user,
28
51
  )
29
- from two_factor.utils import devices_for_user
30
-
31
52
  from .teach import create_class
32
53
 
33
54
 
@@ -40,7 +61,7 @@ def _get_update_account_rate(group, request):
40
61
  do not want to ratelimit those.
41
62
  :return: the rate used in the decorator below.
42
63
  """
43
- return RATELIMIT_RATE if "update_account" in request.POST else None
64
+ return RATELIMIT_LOGIN_RATE if "update_account" in request.POST else None
44
65
 
45
66
 
46
67
  def _get_update_account_ratelimit_key(group, request):
@@ -54,7 +75,7 @@ def _get_update_account_ratelimit_key(group, request):
54
75
  @login_required(login_url=reverse_lazy("teacher_login"))
55
76
  @user_passes_test(logged_in_as_teacher, login_url=reverse_lazy("teacher_login"))
56
77
  @ratelimit(
57
- group=RATELIMIT_GROUP,
78
+ group=RATELIMIT_LOGIN_GROUP,
58
79
  key=_get_update_account_ratelimit_key,
59
80
  method=RATELIMIT_METHOD,
60
81
  rate=_get_update_account_rate,
@@ -64,26 +85,39 @@ def dashboard_teacher_view(request, is_admin):
64
85
  teacher = request.user.new_teacher
65
86
  school = teacher.school
66
87
 
67
- coworkers = Teacher.objects.filter(school=school).order_by(
68
- "new_user__last_name", "new_user__first_name"
69
- )
88
+ coworkers = None
89
+ sent_invites = []
90
+ update_school_form = None
70
91
 
71
- join_requests = Teacher.objects.filter(pending_join_request=school).order_by(
72
- "new_user__last_name", "new_user__first_name"
73
- )
74
- requests = Student.objects.filter(pending_class_request__teacher=teacher)
92
+ if school:
93
+ coworkers = Teacher.objects.filter(school=school).order_by(
94
+ "new_user__last_name", "new_user__first_name"
95
+ )
75
96
 
76
- update_school_form = OrganisationForm(user=request.user, current_school=school)
77
- update_school_form.fields["name"].initial = school.name
78
- update_school_form.fields["postcode"].initial = school.postcode
79
- update_school_form.fields["country"].initial = school.country
97
+ sent_invites = (
98
+ SchoolTeacherInvitation.objects.filter(school=school)
99
+ if teacher.is_admin
100
+ else []
101
+ )
102
+
103
+ update_school_form = OrganisationForm(
104
+ user=request.user, current_school=school
105
+ )
106
+ update_school_form.fields["name"].initial = school.name
107
+ update_school_form.fields["country"].initial = school.country
108
+ update_school_form.fields["county"].initial = school.county
80
109
 
81
- create_class_form = ClassCreationForm()
110
+ invite_teacher_form = InviteTeacherForm()
111
+
112
+ create_class_form = ClassCreationForm(teacher=teacher)
82
113
 
83
114
  update_account_form = TeacherEditAccountForm(request.user)
84
115
  update_account_form.fields["first_name"].initial = request.user.first_name
85
116
  update_account_form.fields["last_name"].initial = request.user.last_name
86
117
 
118
+ delete_account_form = DeleteAccountForm(request.user)
119
+ delete_account_confirm = False
120
+
87
121
  anchor = ""
88
122
 
89
123
  backup_tokens = check_backup_tokens(request)
@@ -100,9 +134,17 @@ def dashboard_teacher_view(request, is_admin):
100
134
 
101
135
  elif "create_class" in request.POST:
102
136
  anchor = "new-class"
103
- create_class_form = ClassCreationForm(request.POST)
137
+ create_class_form = ClassCreationForm(request.POST, teacher=teacher)
104
138
  if create_class_form.is_valid():
105
- created_class = create_class(create_class_form, teacher)
139
+ class_teacher = teacher
140
+ # If the logged in teacher is an admin, then get the class teacher from the selected dropdown
141
+ if teacher.is_admin:
142
+ class_teacher = get_object_or_404(
143
+ Teacher, id=create_class_form.cleaned_data["teacher"]
144
+ )
145
+ created_class = create_class(
146
+ create_class_form, class_teacher, class_creator=teacher
147
+ )
106
148
  messages.success(
107
149
  request,
108
150
  "The class '{className}' has been created successfully.".format(
@@ -111,16 +153,78 @@ def dashboard_teacher_view(request, is_admin):
111
153
  )
112
154
  return HttpResponseRedirect(
113
155
  reverse_lazy(
114
- "view_class", kwargs={"access_code": created_class.access_code}
156
+ "view_class",
157
+ kwargs={"access_code": created_class.access_code},
115
158
  )
116
159
  )
117
160
 
118
161
  elif request.POST.get("show_onboarding_complete") == "1":
119
162
  show_onboarding_complete = True
120
163
 
164
+ elif "invite_teacher" in request.POST and is_admin:
165
+ invite_teacher_form = InviteTeacherForm(request.POST)
166
+ if invite_teacher_form.is_valid():
167
+ data = invite_teacher_form.cleaned_data
168
+ invited_teacher_first_name = data["teacher_first_name"]
169
+ invited_teacher_last_name = data["teacher_last_name"]
170
+ invited_teacher_email = data["teacher_email"]
171
+ invited_teacher_is_admin = data["make_admin_ticked"]
172
+
173
+ token = uuid4().hex
174
+ SchoolTeacherInvitation.objects.create(
175
+ token=token,
176
+ school=school,
177
+ from_teacher=teacher,
178
+ invited_teacher_first_name=invited_teacher_first_name,
179
+ invited_teacher_last_name=invited_teacher_last_name,
180
+ invited_teacher_email=invited_teacher_email,
181
+ invited_teacher_is_admin=invited_teacher_is_admin,
182
+ expiry=timezone.now() + timedelta(days=30),
183
+ )
184
+
185
+ account_exists = User.objects.filter(
186
+ email=invited_teacher_email
187
+ ).exists()
188
+
189
+ registration_link = f"{request.build_absolute_uri(reverse('invited_teacher', kwargs={'token': token}))} "
190
+
191
+ campaign_id = (
192
+ campaign_ids["invite_teacher_with_account"]
193
+ if account_exists
194
+ else campaign_ids["invite_teacher_without_account"]
195
+ )
196
+
197
+ send_dotdigital_email(
198
+ campaign_id,
199
+ [invited_teacher_email],
200
+ personalization_values={
201
+ "SCHOOL_NAME": school.name,
202
+ "REGISTRATION_LINK": registration_link,
203
+ },
204
+ )
205
+
206
+ messages.success(
207
+ request,
208
+ f"You have invited {invited_teacher_first_name} {invited_teacher_last_name} to your school.",
209
+ )
210
+
211
+ # Clear form
212
+ invite_teacher_form = InviteTeacherForm()
213
+
214
+ elif "delete_account" in request.POST:
215
+ delete_account_form = DeleteAccountForm(request.user, request.POST)
216
+ if not delete_account_form.is_valid():
217
+ messages.warning(
218
+ request,
219
+ "Your account was not deleted due to incorrect password.",
220
+ )
221
+ else:
222
+ delete_account_confirm = True
121
223
  else:
122
224
  anchor = "account"
123
- update_account_form = TeacherEditAccountForm(request.user, request.POST)
225
+ update_account_form = TeacherEditAccountForm(
226
+ request.user, request.POST
227
+ )
124
228
  (
125
229
  changing_email,
126
230
  new_email,
@@ -137,18 +241,41 @@ def dashboard_teacher_view(request, is_admin):
137
241
  return render(
138
242
  request,
139
243
  "portal/email_verification_needed.html",
140
- {"is_teacher": True},
244
+ {"usertype": "TEACHER"},
141
245
  )
142
246
 
143
247
  if changing_password:
144
248
  logout(request)
145
249
  messages.success(
146
- request,
147
- "Please login using your new password.",
250
+ request, "Please login using your new password."
148
251
  )
149
252
  return HttpResponseRedirect(reverse_lazy("teacher_login"))
150
253
 
151
- classes = Class.objects.filter(teacher=teacher)
254
+ if teacher.is_admin:
255
+ # Making sure the current teacher classes come up first
256
+ classes = school.classes()
257
+ [
258
+ classes.insert(0, classes.pop(i))
259
+ for i in range(len(classes))
260
+ if classes[i].teacher.id == teacher.id
261
+ ]
262
+
263
+ requests = list(
264
+ Student.objects.filter(
265
+ pending_class_request__teacher__school=school
266
+ )
267
+ )
268
+ [
269
+ requests.insert(0, requests.pop(i))
270
+ for i in range(len(requests))
271
+ if requests[i].pending_class_request.teacher.id == teacher.id
272
+ ]
273
+
274
+ else:
275
+ classes = Class.objects.filter(teacher=teacher)
276
+ requests = Student.objects.filter(
277
+ pending_class_request__teacher=teacher
278
+ )
152
279
 
153
280
  return render(
154
281
  request,
@@ -158,14 +285,17 @@ def dashboard_teacher_view(request, is_admin):
158
285
  "classes": classes,
159
286
  "is_admin": is_admin,
160
287
  "coworkers": coworkers,
161
- "join_requests": join_requests,
162
288
  "requests": requests,
289
+ "invite_teacher_form": invite_teacher_form,
163
290
  "update_school_form": update_school_form,
164
291
  "create_class_form": create_class_form,
165
292
  "update_account_form": update_account_form,
293
+ "delete_account_form": delete_account_form,
294
+ "delete_account_confirm": delete_account_confirm,
166
295
  "anchor": anchor,
167
296
  "backup_tokens": backup_tokens,
168
297
  "show_onboarding_complete": show_onboarding_complete,
298
+ "sent_invites": sent_invites,
169
299
  },
170
300
  )
171
301
 
@@ -179,7 +309,9 @@ def check_backup_tokens(request):
179
309
  # For teachers using 2FA, find out how many backup tokens they have
180
310
  if using_two_factor(request.user):
181
311
  try:
182
- backup_tokens = request.user.staticdevice_set.all()[0].token_set.count()
312
+ backup_tokens = request.user.staticdevice_set.all()[
313
+ 0
314
+ ].token_set.count()
183
315
  except Exception:
184
316
  backup_tokens = 0
185
317
 
@@ -193,17 +325,11 @@ def process_update_school_form(request, school, old_anchor):
193
325
  if update_school_form.is_valid():
194
326
  data = update_school_form.cleaned_data
195
327
  name = data.get("name", "")
196
- postcode = data.get("postcode", "")
197
- country = data.get("country", "")
328
+ country = data.get("country")
329
+ county = school.county
198
330
 
199
331
  school.name = name
200
- school.postcode = postcode
201
332
  school.country = country
202
-
203
- error, country, town, lat, lng = lookup_coord(postcode, country)
204
- school.town = town
205
- school.latitude = lat
206
- school.longitude = lng
207
333
  school.save()
208
334
 
209
335
  anchor = "#"
@@ -258,72 +384,12 @@ def process_update_account_form(request, teacher, old_anchor):
258
384
  def dashboard_manage(request):
259
385
  teacher = request.user.new_teacher
260
386
 
261
- if teacher.school:
387
+ if teacher.school or request.GET.get("account") == "true":
262
388
  return dashboard_teacher_view(request, teacher.is_admin)
263
389
  else:
264
390
  return HttpResponseRedirect(reverse_lazy("onboarding-organisation"))
265
391
 
266
392
 
267
- @require_POST
268
- @login_required(login_url=reverse_lazy("teacher_login"))
269
- @user_passes_test(logged_in_as_teacher, login_url=reverse_lazy("teacher_login"))
270
- def organisation_allow_join(request, pk):
271
- teacher = get_object_or_404(Teacher, id=pk)
272
- user = request.user.new_teacher
273
-
274
- # check user has authority to accept teacher
275
- if teacher.pending_join_request != user.school or not user.is_admin:
276
- raise Http404
277
-
278
- teacher.school = teacher.pending_join_request
279
- teacher.pending_join_request = None
280
- teacher.is_admin = False
281
- teacher.save()
282
-
283
- messages.success(request, "The teacher has been added to your school or club.")
284
-
285
- emailMessage = email_messages.joinRequestAcceptedEmail(request, teacher.school.name)
286
- send_email(
287
- NOTIFICATION_EMAIL,
288
- [teacher.new_user.email],
289
- emailMessage["subject"],
290
- emailMessage["message"],
291
- )
292
-
293
- return HttpResponseRedirect(reverse_lazy("dashboard"))
294
-
295
-
296
- @require_POST
297
- @login_required(login_url=reverse_lazy("teacher_login"))
298
- @user_passes_test(logged_in_as_teacher, login_url=reverse_lazy("teacher_login"))
299
- def organisation_deny_join(request, pk):
300
- teacher = get_object_or_404(Teacher, id=pk)
301
- user = request.user.new_teacher
302
-
303
- # check user has authority to accept teacher
304
- if teacher.pending_join_request != user.school or not user.is_admin:
305
- raise Http404
306
-
307
- teacher.pending_join_request = None
308
- teacher.save()
309
-
310
- messages.success(
311
- request, "The request to join your school or club has been successfully denied."
312
- )
313
-
314
- emailMessage = email_messages.joinRequestDeniedEmail(
315
- request, request.user.new_teacher.school.name
316
- )
317
- send_email(
318
- NOTIFICATION_EMAIL,
319
- [teacher.new_user.email],
320
- emailMessage["subject"],
321
- emailMessage["message"],
322
- )
323
-
324
- return HttpResponseRedirect(reverse_lazy("dashboard"))
325
-
326
-
327
393
  def check_teacher_is_authorised(teacher, user):
328
394
  if teacher == user or (teacher.school != user.school or not user.is_admin):
329
395
  raise Http404
@@ -338,6 +404,10 @@ def organisation_kick(request, pk):
338
404
 
339
405
  check_teacher_is_authorised(teacher, user)
340
406
 
407
+ success_message = (
408
+ "The teacher has been successfully removed from your school or club."
409
+ )
410
+
341
411
  classes = Class.objects.filter(teacher=teacher)
342
412
  for klass in classes:
343
413
  teacher_id = request.POST.get(klass.access_code, None)
@@ -346,8 +416,14 @@ def organisation_kick(request, pk):
346
416
  klass.teacher = new_teacher
347
417
  klass.save()
348
418
 
419
+ success_message = success_message.replace(
420
+ ".", " and their classes were successfully transferred."
421
+ )
422
+
349
423
  classes = Class.objects.filter(teacher=teacher)
350
- teachers = Teacher.objects.filter(school=teacher.school).exclude(id=teacher.id)
424
+ teachers = Teacher.objects.filter(school=teacher.school).exclude(
425
+ id=teacher.id
426
+ )
351
427
 
352
428
  if classes.exists():
353
429
  messages.info(
@@ -362,30 +438,60 @@ def organisation_kick(request, pk):
362
438
  "original_teacher": teacher,
363
439
  "classes": classes,
364
440
  "teachers": teachers,
365
- "submit_button_text": "Remove teacher",
441
+ "submit_button_text": "Move classes and remove teacher",
366
442
  },
367
443
  )
368
444
 
369
445
  teacher.school = None
370
446
  teacher.save()
371
447
 
372
- messages.success(
373
- request,
374
- "The teacher has been successfully removed from your school or club.",
375
- )
376
-
377
- emailMessage = email_messages.kickedEmail(request, user.school.name)
448
+ messages.success(request, success_message)
378
449
 
379
- send_email(
380
- NOTIFICATION_EMAIL,
450
+ send_dotdigital_email(
451
+ campaign_ids["teacher_released"],
381
452
  [teacher.new_user.email],
382
- emailMessage["subject"],
383
- emailMessage["message"],
453
+ personalization_values={"SCHOOL_CLUB_NAME": user.school.name},
384
454
  )
385
455
 
386
456
  return HttpResponseRedirect(reverse_lazy("dashboard"))
387
457
 
388
458
 
459
+ @require_POST
460
+ @login_required(login_url=reverse_lazy("teacher_login"))
461
+ @user_passes_test(logged_in_as_teacher, login_url=reverse_lazy("teacher_login"))
462
+ def invite_toggle_admin(request, invite_id):
463
+ invite = SchoolTeacherInvitation.objects.filter(id=invite_id)[0]
464
+ invite.invited_teacher_is_admin = not invite.invited_teacher_is_admin
465
+ invite.save()
466
+
467
+ if invite.invited_teacher_is_admin:
468
+ messages.success(
469
+ request, "Administrator invite status has been given successfully"
470
+ )
471
+ send_dotdigital_email(
472
+ campaign_ids["admin_given"],
473
+ [invite.invited_teacher_email],
474
+ personalization_values={
475
+ "SCHOOL_CLUB_NAME": invite.school,
476
+ "MANAGEMENT_LINK": request.build_absolute_uri(
477
+ reverse("dashboard")
478
+ ),
479
+ },
480
+ )
481
+
482
+ else:
483
+ messages.success(
484
+ request, "Administrator invite status has been revoked successfully"
485
+ )
486
+ send_dotdigital_email(
487
+ campaign_ids["admin_revoked"],
488
+ [invite.invited_teacher_email],
489
+ personalization_values={"SCHOOL_CLUB_NAME": invite.school},
490
+ )
491
+
492
+ return HttpResponseRedirect(reverse_lazy("dashboard"))
493
+
494
+
389
495
  @require_POST
390
496
  @login_required(login_url=reverse_lazy("teacher_login"))
391
497
  @user_passes_test(logged_in_as_teacher, login_url=reverse_lazy("teacher_login"))
@@ -399,18 +505,35 @@ def organisation_toggle_admin(request, pk):
399
505
  teacher.save()
400
506
 
401
507
  if teacher.is_admin:
402
- messages.success(request, "Administrator status has been given successfully.")
403
- emailMessage = email_messages.adminGivenEmail(request, teacher.school.name)
508
+ messages.success(
509
+ request, "Administrator status has been given successfully."
510
+ )
511
+ send_dotdigital_email(
512
+ campaign_ids["admin_given"],
513
+ [teacher.new_user.email],
514
+ personalization_values={
515
+ "SCHOOL_CLUB_NAME": teacher.school.name,
516
+ "MANAGEMENT_LINK": request.build_absolute_uri(
517
+ reverse("dashboard")
518
+ ),
519
+ },
520
+ )
404
521
  else:
405
- messages.success(request, "Administrator status has been revoked successfully.")
406
- emailMessage = email_messages.adminRevokedEmail(request, teacher.school.name)
407
-
408
- send_email(
409
- NOTIFICATION_EMAIL,
410
- [teacher.new_user.email],
411
- emailMessage["subject"],
412
- emailMessage["message"],
413
- )
522
+ # Remove access to all levels that are from other teachers' students
523
+ [
524
+ unshare_level(level, teacher.new_user)
525
+ for level in levels_shared_with(teacher.new_user)
526
+ if hasattr(level.owner, "student")
527
+ and not teacher.teaches(level.owner)
528
+ ]
529
+ messages.success(
530
+ request, "Administrator status has been revoked successfully."
531
+ )
532
+ send_dotdigital_email(
533
+ campaign_ids["admin_revoked"],
534
+ [teacher.new_user.email],
535
+ personalization_values={"SCHOOL_CLUB_NAME": teacher.school.name},
536
+ )
414
537
 
415
538
  return HttpResponseRedirect(reverse_lazy("dashboard"))
416
539
 
@@ -439,9 +562,8 @@ def teacher_disable_2FA(request, pk):
439
562
  @user_passes_test(logged_in_as_teacher, login_url=reverse_lazy("teacher_login"))
440
563
  def teacher_accept_student_request(request, pk):
441
564
  student = get_object_or_404(Student, id=pk)
442
- teacher = request.user.new_teacher
443
565
 
444
- check_student_can_be_accepted(request, student)
566
+ check_student_request_can_be_handled(request, student)
445
567
 
446
568
  students = Student.objects.filter(
447
569
  class_field=student.pending_class_request
@@ -454,16 +576,34 @@ def teacher_accept_student_request(request, pk):
454
576
  if form.is_valid():
455
577
  data = form.cleaned_data
456
578
  student.class_field = student.pending_class_request
579
+ teacher = student.pending_class_request.teacher
457
580
  student.pending_class_request = None
458
581
  student.new_user.username = get_random_username()
459
582
  student.new_user.first_name = data["name"]
460
583
  student.new_user.last_name = ""
461
584
  student.new_user.email = ""
462
585
 
586
+ students_levels = Level.objects.filter(owner=student.new_user.userprofile).all()
587
+ school_admins = teacher.school.admins()
588
+ for level in students_levels:
589
+ level.shared_with.add(*[school_admin.new_user.id for school_admin in school_admins])
590
+
591
+ if not teacher.is_admin:
592
+ level.shared_with.add(teacher.new_user)
593
+
594
+ level.needs_approval = True
595
+ level.save()
596
+
463
597
  student.save()
464
598
  student.new_user.save()
465
599
  student.new_user.userprofile.save()
466
600
 
601
+ # log the data
602
+ joinrelease = JoinReleaseStudent.objects.create(
603
+ student=student, action_type=JoinReleaseStudent.JOIN
604
+ )
605
+ joinrelease.save()
606
+
467
607
  return render(
468
608
  request,
469
609
  "portal/teach/teacher_added_external_student.html",
@@ -471,7 +611,8 @@ def teacher_accept_student_request(request, pk):
471
611
  )
472
612
  else:
473
613
  form = TeacherAddExternalStudentForm(
474
- student.pending_class_request, initial={"name": student.new_user.first_name}
614
+ student.pending_class_request,
615
+ initial={"name": student.new_user.first_name},
475
616
  )
476
617
 
477
618
  return render(
@@ -486,7 +627,7 @@ def teacher_accept_student_request(request, pk):
486
627
  )
487
628
 
488
629
 
489
- def check_student_can_be_accepted(request, student):
630
+ def check_student_request_can_be_handled(request, student):
490
631
  """
491
632
  Check student is awaiting decision on request
492
633
  """
@@ -494,8 +635,7 @@ def check_student_can_be_accepted(request, student):
494
635
  raise Http404
495
636
 
496
637
  # check user (teacher) has authority to accept student
497
- if request.user.new_teacher != student.pending_class_request.teacher:
498
- raise Http404
638
+ check_teacher_authorised(request, student.pending_class_request.teacher)
499
639
 
500
640
 
501
641
  @require_POST
@@ -504,24 +644,15 @@ def check_student_can_be_accepted(request, student):
504
644
  def teacher_reject_student_request(request, pk):
505
645
  student = get_object_or_404(Student, id=pk)
506
646
 
507
- # check student is awaiting decision on request
508
- if not student.pending_class_request:
509
- raise Http404
510
-
511
- # check user (teacher) has authority to reject student
512
- if request.user.new_teacher != student.pending_class_request.teacher:
513
- raise Http404
647
+ check_student_request_can_be_handled(request, student)
514
648
 
515
- emailMessage = email_messages.studentJoinRequestRejectedEmail(
516
- request,
517
- student.pending_class_request.teacher.school.name,
518
- student.pending_class_request.access_code,
519
- )
520
- send_email(
521
- NOTIFICATION_EMAIL,
649
+ send_dotdigital_email(
650
+ campaign_ids["student_join_request_rejected"],
522
651
  [student.new_user.email],
523
- emailMessage["subject"],
524
- emailMessage["message"],
652
+ personalization_values={
653
+ "SCHOOL_CLUB_NAME": student.pending_class_request.teacher.school.name,
654
+ "ACCESS_CODE": student.pending_class_request.access_code,
655
+ },
525
656
  )
526
657
 
527
658
  student.pending_class_request = None
@@ -533,3 +664,144 @@ def teacher_reject_student_request(request, pk):
533
664
  )
534
665
 
535
666
  return HttpResponseRedirect(reverse_lazy("dashboard"))
667
+
668
+
669
+ @login_required(login_url=reverse_lazy("teacher_login"))
670
+ def delete_teacher_invite(request, token):
671
+ try:
672
+ invite = SchoolTeacherInvitation.objects.get(token=token)
673
+ except SchoolTeacherInvitation.DoesNotExist:
674
+ invite = None
675
+ teacher = request.user.new_teacher
676
+
677
+ # auth the user before deletion
678
+ if invite is None or teacher.school != invite.school or not is_admin_teacher(request.user):
679
+ messages.error(
680
+ request,
681
+ "You do not have permission to perform this action or the invite does not exist",
682
+ )
683
+ else:
684
+ invite_teacher_first_name = invite.invited_teacher_first_name
685
+ invite.anonymise()
686
+ messages.success(
687
+ request,
688
+ f"Invite for {invite_teacher_first_name} successfully deleted",
689
+ )
690
+ return HttpResponseRedirect(reverse_lazy("dashboard"))
691
+
692
+
693
+ @login_required(login_url=reverse_lazy("teacher_login"))
694
+ def resend_invite_teacher(request, token):
695
+ try:
696
+ invite = SchoolTeacherInvitation.objects.get(token=token)
697
+ except SchoolTeacherInvitation.DoesNotExist:
698
+ invite = None
699
+ teacher = request.user.new_teacher
700
+
701
+ # auth the user before deletion
702
+ if invite is None or teacher.school != invite.school or not is_admin_teacher(request.user):
703
+ messages.error(
704
+ request,
705
+ "You do not have permission to perform this action or the invite does not exist",
706
+ )
707
+ else:
708
+ invite.expiry = timezone.now() + timedelta(days=30)
709
+ invite.save()
710
+ teacher = Teacher.objects.filter(id=invite.from_teacher.id)
711
+
712
+ messages.success(request, "Teacher re-invited!")
713
+
714
+ registration_link = f"{request.build_absolute_uri(reverse('invited_teacher', kwargs={'token': token}))} "
715
+
716
+ campaign_id = (
717
+ campaign_ids["invite_teacher_with_account"]
718
+ if teacher.exists()
719
+ else campaign_ids["invite_teacher_without_account"]
720
+ )
721
+
722
+ send_dotdigital_email(
723
+ campaign_id,
724
+ [invite.invited_teacher_email],
725
+ personalization_values={
726
+ "SCHOOL_NAME": invite.school,
727
+ "REGISTRATION_LINK": registration_link,
728
+ },
729
+ )
730
+
731
+ return HttpResponseRedirect(reverse_lazy("dashboard"))
732
+
733
+
734
+ def invited_teacher(request, token):
735
+ error_message = process_teacher_invitation(request, token)
736
+
737
+ if request.method == "POST":
738
+ invited_teacher_form = InvitedTeacherForm(request.POST)
739
+ if invited_teacher_form.is_valid():
740
+ messages.success(
741
+ request,
742
+ "Your account has been created successfully, please log in.",
743
+ )
744
+ return HttpResponseRedirect(reverse_lazy("teacher_login"))
745
+ else:
746
+ invited_teacher_form = InvitedTeacherForm()
747
+
748
+ return render(
749
+ request,
750
+ "portal/teach/invited.html",
751
+ {
752
+ "invited_teacher_form": invited_teacher_form,
753
+ "error_message": error_message,
754
+ },
755
+ )
756
+
757
+
758
+ def process_teacher_invitation(request, token):
759
+ try:
760
+ invitation = SchoolTeacherInvitation.objects.get(
761
+ token=token, expiry__gt=timezone.now()
762
+ )
763
+ except SchoolTeacherInvitation.DoesNotExist:
764
+ return "Uh oh, the Invitation does not exist or it has expired. 😞"
765
+
766
+ if User.objects.filter(email=invitation.invited_teacher_email).exists():
767
+ return (
768
+ "It looks like an account is already registered with this email address. You will need to delete the "
769
+ "other account first or change the email associated with it in order to proceed. You will then be able to "
770
+ "access this page."
771
+ )
772
+ else:
773
+ if request.method == "POST":
774
+ invited_teacher_form = InvitedTeacherForm(request.POST)
775
+ if invited_teacher_form.is_valid():
776
+ data = invited_teacher_form.cleaned_data
777
+ invited_teacher_password = data["teacher_password"]
778
+ newsletter_ticked = data["newsletter_ticked"]
779
+
780
+ # Create the teacher
781
+ invited_teacher = Teacher.objects.factory(
782
+ first_name=invitation.invited_teacher_first_name,
783
+ last_name=invitation.invited_teacher_last_name,
784
+ email=invitation.invited_teacher_email,
785
+ password=invited_teacher_password,
786
+ )
787
+ invited_teacher.is_admin = invitation.invited_teacher_is_admin
788
+ invited_teacher.school = invitation.school
789
+ invited_teacher.invited_by = invitation.from_teacher
790
+ invited_teacher.save()
791
+
792
+ # Verify their email
793
+ generate_token(invited_teacher.new_user, preverified=True)
794
+
795
+ # Add to Dotmailer if they ticked the box
796
+ if newsletter_ticked:
797
+ user = invited_teacher.user.user
798
+ add_to_dotmailer(
799
+ user.first_name,
800
+ user.last_name,
801
+ user.email,
802
+ address_book_ids["newsletter"],
803
+ DotmailerUserType.TEACHER,
804
+ )
805
+
806
+ # Anonymise the invitation
807
+ invitation.anonymise()