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.
- cfl_common/common/__init__.py +1 -0
- cfl_common/common/app_settings.py +66 -0
- cfl_common/common/apps.py +6 -0
- cfl_common/common/context_processors.py +9 -0
- cfl_common/common/csp_config.py +85 -0
- cfl_common/common/helpers/__init__.py +0 -0
- cfl_common/common/helpers/data_migration_loader.py +42 -0
- cfl_common/common/helpers/emails.py +393 -0
- cfl_common/common/helpers/generators.py +52 -0
- cfl_common/common/helpers/organisation.py +10 -0
- cfl_common/common/mail.py +201 -0
- cfl_common/common/migrations/0001_initial.py +240 -0
- cfl_common/common/migrations/0002_emailverification.py +55 -0
- cfl_common/common/migrations/0003_aimmocharacter.py +31 -0
- cfl_common/common/migrations/0004_add_aimmocharacters.py +17 -0
- cfl_common/common/migrations/0005_add_worksheets.py +8 -0
- cfl_common/common/migrations/0006_update_aimmo_character_image_path.py +17 -0
- cfl_common/common/migrations/0007_add_pdf_names_to_first_two_worksheets.py +8 -0
- cfl_common/common/migrations/0008_unlock_worksheet_3.py +11 -0
- cfl_common/common/migrations/0009_add_blocked_time_to_teacher_and_student.py +24 -0
- cfl_common/common/migrations/0010_remove_teacher_title.py +18 -0
- cfl_common/common/migrations/0011_student_login_id.py +18 -0
- cfl_common/common/migrations/0012_usersession.py +39 -0
- cfl_common/common/migrations/0013_class_school.py +42 -0
- cfl_common/common/migrations/0014_login_type.py +29 -0
- cfl_common/common/migrations/0015_dailyactivity.py +31 -0
- cfl_common/common/migrations/0016_joinreleasestudent.py +42 -0
- cfl_common/common/migrations/0017_copy_email_to_username.py +18 -0
- cfl_common/common/migrations/0018_update_aimmo_character_image_path.py +15 -0
- cfl_common/common/migrations/0019_aimmocharacter_alt.py +16 -0
- cfl_common/common/migrations/0020_class_is_active_and_null_access_code.py +23 -0
- cfl_common/common/migrations/0021_school_is_active.py +28 -0
- cfl_common/common/migrations/0022_school_cleanup.py +29 -0
- cfl_common/common/migrations/0023_userprofile_aimmo_badges.py +22 -0
- cfl_common/common/migrations/0024_teacher_invited_by.py +25 -0
- cfl_common/common/migrations/0025_schoolteacherinvitation.py +47 -0
- cfl_common/common/migrations/0026_teacher_remove_join_request.py +22 -0
- cfl_common/common/migrations/0027_class_created_by.py +25 -0
- cfl_common/common/migrations/0028_coding_club_downloads.py +23 -0
- cfl_common/common/migrations/0029_dynamicelement.py +22 -0
- cfl_common/common/migrations/0030_add_maintenance_banner.py +25 -0
- cfl_common/common/migrations/0031_improve_admin_panel.py +56 -0
- cfl_common/common/migrations/0032_dailyactivity_level_control_submits.py +18 -0
- cfl_common/common/migrations/0033_password_reset_tracking_fields.py +23 -0
- cfl_common/common/migrations/0034_dailyactivity_daily_school_student_lockout_reset.py +18 -0
- cfl_common/common/migrations/0035_rename_lockout_fields.py +27 -0
- cfl_common/common/migrations/0036_rename_awaiting_email_verification_userprofile_is_verified.py +17 -0
- cfl_common/common/migrations/0037_migrate_email_verification.py +21 -0
- cfl_common/common/migrations/0038_delete_emailverification.py +16 -0
- cfl_common/common/migrations/0039_copy_email_to_username.py +18 -0
- cfl_common/common/migrations/0040_school_county.py +18 -0
- cfl_common/common/migrations/0041_populate_gb_counties.py +27 -0
- cfl_common/common/migrations/0042_totalactivity.py +25 -0
- cfl_common/common/migrations/0043_add_total_activity.py +30 -0
- cfl_common/common/migrations/0044_update_activity_models.py +33 -0
- cfl_common/common/migrations/0045_otp.py +23 -0
- cfl_common/common/migrations/0046_alter_school_country.py +19 -0
- cfl_common/common/migrations/0047_delete_school_postcode.py +16 -0
- cfl_common/common/migrations/0048_unique_school_names.py +42 -0
- cfl_common/common/migrations/0049_anonymise_orphan_users.py +29 -0
- cfl_common/common/migrations/0050_anonymise_orphan_schools.py +30 -0
- cfl_common/common/migrations/0051_verify_returning_users.py +26 -0
- cfl_common/common/migrations/0052_add_cse_fields.py +68 -0
- cfl_common/common/migrations/0053_clean_class_data.py +24 -0
- cfl_common/common/migrations/0054_delete_aimmo_models.py +20 -0
- cfl_common/common/migrations/0055_alter_schoolteacherinvitation_token.py +18 -0
- cfl_common/common/migrations/0056_set_non_school_teachers_as_non_admins.py +25 -0
- cfl_common/common/migrations/0057_teacher_teacher__is_admin.py +19 -0
- cfl_common/common/migrations/0058_userprofile_google_refresh_token_and_more.py +24 -0
- cfl_common/common/migrations/__init__.py +0 -0
- cfl_common/common/models.py +557 -0
- cfl_common/common/permissions.py +84 -0
- cfl_common/common/tests/__init__.py +0 -0
- cfl_common/common/tests/test_migration_anonymise_orphan_schools.py +30 -0
- cfl_common/common/tests/test_migration_anonymise_orphan_users.py +30 -0
- cfl_common/common/tests/test_migration_blocked_time.py +15 -0
- cfl_common/common/tests/test_migration_remove_teacher_title.py +13 -0
- cfl_common/common/tests/test_migration_unique_school_names.py +33 -0
- cfl_common/common/tests/test_migration_verify_returning_users.py +59 -0
- cfl_common/common/tests/test_models.py +87 -0
- cfl_common/common/tests/utils/__init__.py +0 -0
- cfl_common/common/tests/utils/classes.py +38 -0
- cfl_common/common/tests/utils/email.py +67 -0
- cfl_common/common/tests/utils/organisation.py +41 -0
- cfl_common/common/tests/utils/student.py +123 -0
- cfl_common/common/tests/utils/teacher.py +73 -0
- cfl_common/common/tests/utils/user.py +27 -0
- cfl_common/common/utils.py +56 -0
- cfl_common/setup.py +61 -0
- codeforlife_portal-8.9.9.dist-info/METADATA +226 -0
- {codeforlife_portal-5.33.5.dist-info → codeforlife_portal-8.9.9.dist-info}/RECORD +339 -241
- {codeforlife_portal-5.33.5.dist-info → codeforlife_portal-8.9.9.dist-info}/WHEEL +1 -1
- codeforlife_portal-8.9.9.dist-info/licenses/LICENSE.md +3 -0
- {codeforlife_portal-5.33.5.dist-info → codeforlife_portal-8.9.9.dist-info}/top_level.txt +1 -0
- deploy/middleware/maintenance.py +25 -0
- deploy/middleware/screentime_warning.py +29 -0
- deploy/middleware/security.py +5 -6
- deploy/middleware/session_timeout.py +4 -2
- deploy/middleware/tmp_basic_auth.py +41 -0
- example_project/portal_test_settings.py +239 -0
- example_project/settings.py +156 -17
- example_project/urls.py +5 -6
- portal/__init__.py +1 -1
- portal/admin.py +142 -29
- portal/app_settings.py +8 -7
- portal/forms/dotmailer.py +6 -4
- portal/forms/invite_teacher.py +19 -10
- portal/forms/organisation.py +137 -68
- portal/forms/play.py +53 -98
- portal/forms/registration.py +70 -164
- portal/forms/teach.py +147 -121
- portal/handlers.py +1 -2
- portal/helpers/decorators.py +30 -10
- portal/helpers/password.py +86 -47
- portal/helpers/ratelimit.py +32 -15
- portal/helpers/regexes.py +5 -0
- portal/helpers/request_handlers.py +10 -0
- portal/migrations/0044_auto_20150430_0959.py +6 -2
- portal/mixins/__init__.py +1 -0
- portal/mixins/cron_mixin.py +12 -0
- portal/permissions/__init__.py +1 -0
- portal/permissions/is_cron_request_from_google.py +14 -0
- portal/static/portal/img/10_years_anniversary.png +0 -0
- portal/static/portal/img/RR_logo_grass_background.png +0 -0
- portal/static/portal/img/coding_club_hero.jpg +0 -0
- portal/static/portal/img/coding_club_python_pack.png +0 -0
- portal/static/portal/img/facebook.png +0 -0
- portal/static/portal/img/gitbook.png +0 -0
- portal/static/portal/img/howe_dell_1.png +0 -0
- portal/static/portal/img/howe_dell_2.png +0 -0
- portal/static/portal/img/howe_dell_3.png +0 -0
- portal/static/portal/img/logo_cfl.png +0 -0
- portal/static/portal/img/logo_cfl_powered.svg +35 -0
- portal/static/portal/img/logo_cfl_reminder_cards.jpg +0 -0
- portal/static/portal/img/logo_ocado_group.png +0 -0
- portal/static/portal/img/logo_python_den.svg +21 -0
- portal/static/portal/img/long_europe_map.png +0 -0
- portal/static/portal/img/python_den.png +0 -0
- portal/static/portal/img/python_den_banner.svg +26 -0
- portal/static/portal/img/rapid_router_landing_hero.png +0 -0
- portal/static/portal/img/rr_advanced.png +0 -0
- portal/static/portal/img/ten_year_map_pin.svg +1 -0
- portal/static/portal/img/thumbnail_educate_rapid_router.png +0 -0
- portal/static/portal/img/thumbnail_educate_resources.png +0 -0
- portal/static/portal/img/thumbnail_play_rapid_router.png +0 -0
- portal/static/portal/img/thumbnail_python_den.png +0 -0
- portal/static/portal/img/twitter.png +0 -0
- portal/static/portal/js/carouselCards.js +25 -0
- portal/static/portal/js/common.js +96 -1
- portal/static/portal/js/independentLogin.js +16 -0
- portal/static/portal/js/independentRegistration.js +86 -0
- portal/static/portal/js/levelControl.js +77 -0
- portal/static/portal/js/lib/jquery.min.js +2 -0
- portal/static/portal/js/organisation_manage.js +142 -14
- portal/static/portal/js/passwordStrength.js +154 -64
- portal/static/portal/js/resetPassword.js +23 -0
- portal/static/portal/js/riveted.min.js +238 -239
- portal/static/portal/js/school.js +13 -0
- portal/static/portal/js/studentLogin.js +16 -0
- portal/static/portal/js/teacherEditStudent.js +23 -0
- portal/static/portal/js/teacherLogin.js +16 -0
- portal/static/portal/js/tenYearMap.js +14 -0
- portal/static/portal/sass/colorbox.scss +0 -1
- portal/static/portal/sass/modules/_colours.scss +1 -0
- portal/static/portal/sass/modules/_levels.scss +1 -1
- portal/static/portal/sass/modules/_mixins.scss +21 -0
- portal/static/portal/sass/partials/_banners.scss +4 -177
- portal/static/portal/sass/partials/_buttons.scss +12 -15
- portal/static/portal/sass/partials/_carousel.scss +129 -0
- portal/static/portal/sass/partials/_footer.scss +21 -22
- portal/static/portal/sass/partials/_forms.scss +60 -5
- portal/static/portal/sass/partials/_grids.scss +34 -61
- portal/static/portal/sass/partials/_header.scss +28 -20
- portal/static/portal/sass/partials/_images.scss +292 -39
- portal/static/portal/sass/partials/_popup.scss +18 -15
- portal/static/portal/sass/partials/_tables.scss +12 -20
- portal/static/portal/sass/partials/_text.scss +6 -10
- portal/static/portal/sass/styles.scss +0 -1
- portal/static/portal/video/code for life .pdf +0 -0
- portal/strings/about.py +5 -0
- portal/strings/coding_club.py +9 -0
- portal/strings/play.py +6 -5
- portal/strings/teach.py +1 -1
- portal/strings/teacher_resources.py +2 -8
- portal/strings/ten_year_map.py +13 -0
- portal/templates/403.html +2 -2
- portal/templates/404.html +1 -1
- portal/templates/500.html +2 -2
- portal/templates/{captcha → django_recaptcha}/includes/js_v2_invisible.html +3 -3
- portal/templates/{captcha → django_recaptcha}/widget_v2_invisible.html +2 -2
- portal/templates/email.html +4 -2
- portal/templates/maintenance.html +34 -0
- portal/templates/portal/about.html +94 -62
- portal/templates/portal/base.html +176 -152
- portal/templates/portal/coding_club.html +100 -0
- portal/templates/portal/contribute.html +56 -52
- portal/templates/portal/email_invitation_sent.html +1 -1
- portal/templates/portal/email_style_template.html +374 -0
- portal/templates/portal/email_verification_failed.html +1 -1
- portal/templates/portal/email_verification_needed.html +9 -9
- portal/templates/portal/form_shapes.html +20 -8
- portal/templates/portal/getinvolved.html +6 -6
- portal/templates/portal/home.html +35 -10
- portal/templates/portal/home_learning.html +19 -19
- portal/templates/portal/locked_out.html +0 -1
- portal/templates/portal/locked_out_school_student.html +16 -0
- portal/templates/portal/login/independent_student.html +31 -15
- portal/templates/portal/login/student.html +10 -7
- portal/templates/portal/login/student_class_code.html +7 -4
- portal/templates/portal/login/teacher.html +34 -17
- portal/templates/portal/partials/banner.html +18 -4
- portal/templates/portal/partials/benefits.html +1 -1
- portal/templates/portal/partials/card_list.html +34 -24
- portal/templates/portal/partials/character_list.html +5 -5
- portal/templates/portal/partials/cookie_list.html +161 -0
- portal/templates/portal/partials/delete_popup.html +18 -0
- portal/templates/portal/partials/footer.html +57 -26
- portal/templates/portal/partials/header.html +118 -117
- portal/templates/portal/partials/hero_card.html +4 -3
- portal/templates/portal/partials/info_popup.html +3 -3
- portal/templates/portal/partials/invite_admin_teacher.html +23 -0
- portal/templates/portal/partials/popup.html +7 -2
- portal/templates/portal/partials/register_newsletter_tickbox.html +2 -5
- portal/templates/portal/partials/screentime_popup.html +14 -0
- portal/templates/portal/partials/service_unavailable_popup.html +17 -0
- portal/templates/portal/partials/session_popup.html +19 -0
- portal/templates/portal/play/student_dashboard.html +42 -29
- portal/templates/portal/play/student_edit_account.html +64 -9
- portal/templates/portal/play.html +61 -41
- portal/templates/portal/privacy_notice.html +697 -0
- portal/templates/portal/register.html +122 -92
- portal/templates/portal/reset_password.html +20 -40
- portal/templates/portal/reset_password_confirm.html +9 -4
- portal/templates/portal/reset_password_email_sent.html +15 -13
- portal/templates/portal/teach/base_registering.html +1 -1
- portal/templates/portal/teach/class.html +4 -6
- portal/templates/portal/teach/dashboard.html +212 -117
- portal/templates/portal/teach/invited.html +90 -0
- portal/templates/portal/teach/onboarding_classes.html +5 -3
- portal/templates/portal/teach/onboarding_print.html +1 -1
- portal/templates/portal/teach/onboarding_school.html +26 -139
- portal/templates/portal/teach/onboarding_students.html +1 -1
- portal/templates/portal/teach/teacher_dismiss_students.html +73 -55
- portal/templates/portal/teach/teacher_edit_class.html +168 -11
- portal/templates/portal/teach/teacher_edit_student.html +12 -5
- portal/templates/portal/teach/teacher_move_all_classes.html +25 -38
- portal/templates/portal/teach/teacher_move_students_to_class.html +1 -1
- portal/templates/portal/teach.html +61 -42
- portal/templates/portal/ten_year_map.html +147 -0
- portal/templates/portal/terms.html +191 -42
- portal/templates/two_factor/core/login.html +71 -59
- portal/templates/two_factor/core/setup.html +58 -49
- portal/templates/two_factor/profile/disable.html +1 -1
- portal/templates/two_factor/profile/profile.html +35 -17
- portal/templatetags/app_tags.py +59 -84
- portal/templatetags/card_list_tags.py +0 -4
- portal/tests/base_test.py +14 -3
- portal/tests/conftest.py +0 -15
- portal/tests/migrations/test_migration_make_portaladmin_teacher.py +2 -6
- portal/tests/migrations/test_migration_preview_users.py +3 -9
- portal/tests/migrations/test_migration_remove_guardian.py +1 -3
- portal/tests/migrations/test_migration_use_common_models.py +2 -6
- portal/tests/migrations/test_migration_verify_portaladmin.py +1 -3
- portal/tests/pageObjects/portal/admin/admin_base_page.py +0 -21
- portal/tests/pageObjects/portal/base_page.py +16 -26
- portal/tests/pageObjects/portal/email_verification_needed_page.py +3 -2
- portal/tests/pageObjects/portal/game_page.py +12 -19
- portal/tests/pageObjects/portal/home_page.py +13 -15
- portal/tests/pageObjects/portal/independent_login_page.py +13 -17
- portal/tests/pageObjects/portal/password_reset_form_page.py +20 -4
- portal/tests/pageObjects/portal/password_reset_page.py +25 -0
- portal/tests/pageObjects/portal/play/account_page.py +18 -27
- portal/tests/pageObjects/portal/play/dashboard_page.py +4 -4
- portal/tests/pageObjects/portal/play/join_school_or_club_page.py +8 -10
- portal/tests/pageObjects/portal/play/play_base_page.py +5 -3
- portal/tests/pageObjects/portal/signup_page.py +28 -59
- portal/tests/pageObjects/portal/student_login_class_code.py +6 -9
- portal/tests/pageObjects/portal/student_login_page.py +6 -8
- portal/tests/pageObjects/portal/teach/add_independent_student_to_class_page.py +3 -3
- portal/tests/pageObjects/portal/teach/added_independent_student_to_class_page.py +3 -1
- portal/tests/pageObjects/portal/teach/class_page.py +36 -13
- portal/tests/pageObjects/portal/teach/dashboard_page.py +43 -84
- portal/tests/pageObjects/portal/teach/dismiss_students_page.py +7 -5
- portal/tests/pageObjects/portal/teach/edit_student_page.py +10 -8
- portal/tests/pageObjects/portal/teach/move_class_page.py +5 -10
- portal/tests/pageObjects/portal/teach/move_classes_page.py +4 -2
- portal/tests/pageObjects/portal/teach/move_students_disambiguate_page.py +4 -2
- portal/tests/pageObjects/portal/teach/move_students_page.py +6 -13
- portal/tests/pageObjects/portal/teach/onboarding_classes_page.py +5 -3
- portal/tests/pageObjects/portal/teach/onboarding_organisation_page.py +11 -49
- portal/tests/pageObjects/portal/teach/onboarding_student_list_page.py +7 -12
- portal/tests/pageObjects/portal/teach/onboarding_students_page.py +4 -27
- portal/tests/pageObjects/portal/teach/teach_base_page.py +6 -4
- portal/tests/pageObjects/portal/teacher_login_page.py +10 -16
- portal/tests/selenium_test_case.py +3 -43
- portal/tests/snapshots/snap_test_partials.py +11 -165
- portal/tests/test_2FA.py +15 -33
- portal/tests/test_admin.py +15 -97
- portal/tests/test_api.py +212 -91
- portal/tests/test_captcha_forms.py +2 -2
- portal/tests/test_class.py +374 -24
- portal/tests/test_emails.py +83 -20
- portal/tests/{test_newsletter_footer.py → test_global_forms.py} +5 -5
- portal/tests/test_helper_methods.py +30 -0
- portal/tests/test_independent_student.py +255 -144
- portal/tests/test_invite_teacher.py +318 -10
- portal/tests/test_middleware.py +96 -9
- portal/tests/test_organisation.py +78 -262
- portal/tests/test_partials.py +0 -88
- portal/tests/test_ratelimit.py +218 -36
- portal/tests/test_school_student.py +35 -40
- portal/tests/test_security.py +12 -31
- portal/tests/test_teacher.py +425 -325
- portal/tests/test_teacher_student.py +103 -91
- portal/tests/test_views.py +900 -76
- portal/tests/utils/classes.py +2 -2
- portal/tests/utils/messages.py +13 -28
- portal/urls.py +235 -166
- portal/views/admin.py +0 -332
- portal/views/api.py +82 -48
- portal/views/cron/__init__.py +1 -0
- portal/views/cron/user.py +322 -0
- portal/views/dotmailer.py +9 -1
- portal/views/email.py +33 -77
- portal/views/google_analytics.py +28 -0
- portal/views/home.py +126 -97
- portal/views/legal.py +13 -0
- portal/views/login/independent_student.py +5 -5
- portal/views/login/student.py +51 -14
- portal/views/login/teacher.py +2 -6
- portal/views/organisation.py +20 -189
- portal/views/registration.py +97 -17
- portal/views/student/edit_account_details.py +99 -72
- portal/views/student/play.py +81 -62
- portal/views/teacher/dashboard.py +421 -149
- portal/views/teacher/teach.py +226 -177
- portal/views/two_factor/core.py +22 -19
- portal/views/two_factor/profile.py +2 -2
- codeforlife_portal-5.33.5.dist-info/LICENSE.md +0 -577
- codeforlife_portal-5.33.5.dist-info/METADATA +0 -38
- deploy/permissions.py +0 -2
- example_project/manage.py +0 -10
- portal/autoconfig.py +0 -141
- portal/csp_config.py +0 -60
- portal/forms/add_game.py +0 -33
- portal/helpers/location.py +0 -121
- portal/static/portal/img/kurono_hero.jpg +0 -0
- portal/static/portal/img/kurono_landing_hero.png +0 -0
- portal/static/portal/img/kurono_logo.svg +0 -1
- portal/static/portal/img/kurono_logo_grey_background.svg +0 -1
- portal/static/portal/img/kurono_logo_mark.svg +0 -1
- portal/static/portal/img/kurono_resources_hero.jpg +0 -0
- portal/static/portal/img/kurono_story.png +0 -0
- portal/static/portal/img/ocado-swirl.svg +0 -22
- portal/static/portal/img/thumbnail_educate_kurono.png +0 -0
- portal/static/portal/img/thumbnail_educate_resources_and_progress_tracking.png +0 -0
- portal/static/portal/img/thumbnail_kurono_resources.png +0 -0
- portal/static/portal/img/thumbnail_play_kurono.png +0 -0
- portal/static/portal/img/x_close_video.png +0 -0
- portal/static/portal/js/aimmoGame.js +0 -106
- portal/static/portal/js/deleteWorkspaces.js +0 -14
- portal/static/portal/js/fuzzySchoolLookup.js +0 -46
- portal/static/portal/js/lib/jquery-3.5.1.min.js +0 -2
- portal/static/portal/js/lib/jquery-ui-1.12.1.min.js +0 -13
- portal/static/portal/sass/partials/_videos.scss +0 -10
- portal/static/portal/video/aimmo_play_now_background_video.mp4 +0 -0
- portal/strings/student_aimmo_dashboard.py +0 -6
- portal/templates/portal/admin/aggregated_data.html +0 -35
- portal/templates/portal/admin/map.html +0 -70
- portal/templates/portal/mouseflow.html +0 -9
- portal/templates/portal/partials/aimmo_games_table.html +0 -83
- portal/templates/portal/partials/register_over_required_age_tickbox.html +0 -9
- portal/templates/portal/play/independent_student_dashboard.html +0 -64
- portal/templates/portal/play/student_aimmo_dashboard.html +0 -63
- portal/templates/portal/privacy_policy.html +0 -483
- portal/templates/portal/reset_password_email.html +0 -9
- portal/templates/portal/teach/invite.html +0 -25
- portal/templates/portal/teach/teacher_aimmo_dashboard.html +0 -95
- portal/templates/portal/teach/teacher_resources.html +0 -68
- portal/templatetags/character_list_tags.py +0 -16
- portal/tests/pageObjects/portal/kurono_teacher_dashboard_page.py +0 -49
- portal/tests/pageObjects/portal/student_password_reset_form_page.py +0 -23
- portal/tests/pageObjects/portal/teach/onboarding_revoke_request_page.py +0 -20
- portal/tests/pageObjects/portal/teacher_password_reset_form_page.py +0 -23
- portal/tests/test_aimmo_dashboards.py +0 -172
- portal/tests/test_location.py +0 -217
- portal/tests/utils/aimmo_games.py +0 -30
- portal/views/aimmo/dashboard.py +0 -119
- portal/views/privacy_policy.py +0 -9
- portal/views/teacher/teacher_resources.py +0 -42
- {portal/views/aimmo → cfl_common}/__init__.py +0 -0
|
@@ -1,33 +1,54 @@
|
|
|
1
|
-
from
|
|
2
|
-
from
|
|
3
|
-
|
|
4
|
-
from common.
|
|
5
|
-
|
|
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
|
-
|
|
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
|
|
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=
|
|
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 =
|
|
68
|
-
|
|
69
|
-
|
|
88
|
+
coworkers = None
|
|
89
|
+
sent_invites = []
|
|
90
|
+
update_school_form = None
|
|
70
91
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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(
|
|
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
|
-
{"
|
|
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
|
-
|
|
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()[
|
|
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
|
-
|
|
197
|
-
|
|
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(
|
|
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": "
|
|
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
|
-
|
|
380
|
-
|
|
450
|
+
send_dotdigital_email(
|
|
451
|
+
campaign_ids["teacher_released"],
|
|
381
452
|
[teacher.new_user.email],
|
|
382
|
-
|
|
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(
|
|
403
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
516
|
-
|
|
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
|
-
|
|
524
|
-
|
|
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()
|