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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
default_app_config = "common.apps.CommonConfig"
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from django.conf import settings
|
|
2
|
+
|
|
3
|
+
# Email address to source notifications from
|
|
4
|
+
EMAIL_ADDRESS = getattr(settings, "EMAIL_ADDRESS", "no-reply@codeforlife.education")
|
|
5
|
+
|
|
6
|
+
# Dotdigital authorization details
|
|
7
|
+
DOTDIGITAL_AUTH = getattr(settings, "DOTDIGITAL_AUTH", "")
|
|
8
|
+
|
|
9
|
+
# Dotmailer URLs for adding users to the newsletter address book
|
|
10
|
+
DOTMAILER_CREATE_CONTACT_URL = getattr(settings, "DOTMAILER_CREATE_CONTACT_URL", "")
|
|
11
|
+
DOTMAILER_TEACHER_ADDRESS_BOOK_URL = getattr(settings, "DOTMAILER_TEACHER_ADDRESS_BOOK_URL", "")
|
|
12
|
+
DOTMAILER_STUDENT_ADDRESS_BOOK_URL = getattr(settings, "DOTMAILER_STUDENT_ADDRESS_BOOK_URL", "")
|
|
13
|
+
DOTMAILER_NO_ACCOUNT_ADDRESS_BOOK_URL = getattr(settings, "DOTMAILER_NO_ACCOUNT_ADDRESS_BOOK_URL", "")
|
|
14
|
+
|
|
15
|
+
# Dotmailer username for API authentication
|
|
16
|
+
DOTMAILER_USER = getattr(settings, "DOTMAILER_USER", "")
|
|
17
|
+
|
|
18
|
+
# Dotmailer password for API authentication
|
|
19
|
+
DOTMAILER_PASSWORD = getattr(settings, "DOTMAILER_PASSWORD", "")
|
|
20
|
+
|
|
21
|
+
# Dotmailer default preferences to what users are signed up to
|
|
22
|
+
DOTMAILER_DEFAULT_PREFERENCES = getattr(settings, "DOTMAILER_DEFAULT_PREFERENCES", [])
|
|
23
|
+
|
|
24
|
+
# Dotmailer URL for getting a user by email
|
|
25
|
+
DOTMAILER_GET_USER_BY_EMAIL_URL = getattr(
|
|
26
|
+
settings,
|
|
27
|
+
"DOTMAILER_GET_USER_BY_EMAIL_URL",
|
|
28
|
+
"",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Dotmailer URL for deleting a contact by id
|
|
32
|
+
DOTMAILER_DELETE_USER_BY_ID_URL = getattr(
|
|
33
|
+
settings,
|
|
34
|
+
"DOTMAILER_DELETE_USER_BY_ID_URL",
|
|
35
|
+
"",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Dotmailer URL for adding consent data to a user
|
|
39
|
+
DOTMAILER_PUT_CONSENT_DATA_URL = getattr(settings, "DOTMAILER_PUT_CONSENT_DATA_URL", "")
|
|
40
|
+
|
|
41
|
+
# Dotmailer URL for sending a triggered campaign to a users
|
|
42
|
+
DOTMAILER_SEND_CAMPAIGN_URL = getattr(settings, "DOTMAILER_SEND_CAMPAIGN_URL", "")
|
|
43
|
+
|
|
44
|
+
# ID of the "Thanks for staying!" campaign in Dotmailer
|
|
45
|
+
DOTMAILER_THANKS_FOR_STAYING_CAMPAIGN_ID = getattr(settings, "DOTMAILER_THANKS_FOR_STAYING_CAMPAIGN_ID", "")
|
|
46
|
+
|
|
47
|
+
# Fernet encryption for OAuth2 sign in
|
|
48
|
+
ENCRYPTION_KEY = getattr(settings, "ENCRYPTION_KEY", "")
|
|
49
|
+
|
|
50
|
+
# The name of the google app engine service the application is running on, local otherwise
|
|
51
|
+
MODULE_NAME = getattr(settings, "MODULE_NAME", "local")
|
|
52
|
+
|
|
53
|
+
# Boolean indicating if OneTrust cookie management is enabled or not
|
|
54
|
+
COOKIE_MANAGEMENT_ENABLED = getattr(settings, "COOKIE_MANAGEMENT_ENABLED", True)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def domain():
|
|
58
|
+
"""Returns the full domain depending on whether it's local, dev, staging or prod."""
|
|
59
|
+
domain = "https://www.codeforlife.education"
|
|
60
|
+
|
|
61
|
+
if MODULE_NAME == "local":
|
|
62
|
+
domain = "localhost:8000"
|
|
63
|
+
elif MODULE_NAME == "staging" or MODULE_NAME == "dev":
|
|
64
|
+
domain = f"https://{MODULE_NAME}-dot-decent-digit-629.appspot.com"
|
|
65
|
+
|
|
66
|
+
return domain
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""CSP Config"""
|
|
2
|
+
|
|
3
|
+
from .app_settings import domain, MODULE_NAME
|
|
4
|
+
|
|
5
|
+
CSP_DEFAULT_SRC = ("self",)
|
|
6
|
+
CSP_CONNECT_SRC = (
|
|
7
|
+
"'self'",
|
|
8
|
+
"https://*.onetrust.com/",
|
|
9
|
+
"https://api.pwnedpasswords.com",
|
|
10
|
+
"https://euc-widget.freshworks.com/",
|
|
11
|
+
"https://codeforlife.freshdesk.com/",
|
|
12
|
+
"https://api.iconify.design/",
|
|
13
|
+
"https://api.simplesvg.com/",
|
|
14
|
+
"https://api.unisvg.com/",
|
|
15
|
+
"https://www.google-analytics.com/",
|
|
16
|
+
"https://pyodide-cdn2.iodide.io/v0.15.0/full/",
|
|
17
|
+
"https://crowdin.com/",
|
|
18
|
+
)
|
|
19
|
+
CSP_FONT_SRC = ("'self'", "https://fonts.gstatic.com/", "https://fonts.googleapis.com/", "https://use.typekit.net/")
|
|
20
|
+
CSP_SCRIPT_SRC = (
|
|
21
|
+
"'self'",
|
|
22
|
+
"'unsafe-inline'",
|
|
23
|
+
"'unsafe-eval'",
|
|
24
|
+
"https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js",
|
|
25
|
+
"https://cdn.crowdin.com/",
|
|
26
|
+
"https://*.onetrust.com/",
|
|
27
|
+
"https://code.jquery.com/",
|
|
28
|
+
"https://euc-widget.freshworks.com/",
|
|
29
|
+
"https://cdn-ukwest.onetrust.com/",
|
|
30
|
+
"https://code.iconify.design/2/2.0.3/iconify.min.js",
|
|
31
|
+
"https://www.googletagmanager.com/",
|
|
32
|
+
"https://www.recaptcha.net/",
|
|
33
|
+
"https://www.google.com/recaptcha/",
|
|
34
|
+
"https://www.gstatic.com/recaptcha/",
|
|
35
|
+
"https://use.typekit.net/mrl4ieu.js",
|
|
36
|
+
"https://pyodide-cdn2.iodide.io/v0.15.0/full/",
|
|
37
|
+
f"{domain()}/static/portal/",
|
|
38
|
+
f"{domain()}/static/common/",
|
|
39
|
+
)
|
|
40
|
+
CSP_STYLE_SRC = (
|
|
41
|
+
"'self'",
|
|
42
|
+
"'unsafe-inline'",
|
|
43
|
+
"https://euc-widget.freshworks.com/",
|
|
44
|
+
"https://cdn-ukwest.onetrust.com/",
|
|
45
|
+
"https://fonts.googleapis.com/",
|
|
46
|
+
"https://code.jquery.com/ui/1.13.1/themes/base/jquery-ui.css",
|
|
47
|
+
"https://cdn.crowdin.com/",
|
|
48
|
+
f"{domain()}/static/portal/",
|
|
49
|
+
)
|
|
50
|
+
CSP_FRAME_SRC = (
|
|
51
|
+
"https://storage.googleapis.com/",
|
|
52
|
+
"https://2662351606-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/",
|
|
53
|
+
"https://files.gitbook.com/v0/b/gitbook-x-prod.appspot.com/",
|
|
54
|
+
"https://www.recaptcha.net/",
|
|
55
|
+
"https://www.google.com/recaptcha/",
|
|
56
|
+
"https://crowdin.com/",
|
|
57
|
+
f"{domain()}/static/common/img/",
|
|
58
|
+
f"{domain()}/static/game/image/",
|
|
59
|
+
)
|
|
60
|
+
CSP_IMG_SRC = (
|
|
61
|
+
"'self'",
|
|
62
|
+
"https://storage.googleapis.com/codeforlife-assets/images/",
|
|
63
|
+
"https://cdn-ukwest.onetrust.com/",
|
|
64
|
+
"https://p.typekit.net/",
|
|
65
|
+
"https://cdn.crowdin.com/",
|
|
66
|
+
"https://crowdin-static.downloads.crowdin.com/",
|
|
67
|
+
"data:",
|
|
68
|
+
f"{domain()}/static/portal/img/",
|
|
69
|
+
f"{domain()}/static/portal/static/portal/img/",
|
|
70
|
+
f"{domain()}/static/portal/img/",
|
|
71
|
+
f"{domain()}/favicon.ico",
|
|
72
|
+
f"{domain()}/img/",
|
|
73
|
+
f"{domain()}/account/two_factor/qrcode/",
|
|
74
|
+
f"{domain()}/static/",
|
|
75
|
+
f"{domain()}/static/game/image/",
|
|
76
|
+
f"{domain()}/static/game/raphael_image/",
|
|
77
|
+
f"{domain()}/static/game/js/blockly/media/",
|
|
78
|
+
f"{domain()}/static/icons/",
|
|
79
|
+
)
|
|
80
|
+
CSP_OBJECT_SRC = (f"{domain()}/static/common/img/", f"{domain()}/static/game/image/")
|
|
81
|
+
CSP_MEDIA_SRC = (
|
|
82
|
+
"https://files.gitbook.com/v0/b/gitbook-x-prod.appspot.com/",
|
|
83
|
+
f"{domain()}/static/game/sound/",
|
|
84
|
+
f"{domain()}/static/game/js/blockly/media/",
|
|
85
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Callable
|
|
3
|
+
|
|
4
|
+
from django.core.management import call_command
|
|
5
|
+
from django.core.serializers import base, python
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def load_data_from_file(file_name) -> Callable:
|
|
9
|
+
"""Returns a migration function that loads a json file produced by `manage.py dumpdata` into the database.
|
|
10
|
+
For use with migrations.RunPython
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
file_name (str): The name of the file containing the data you want to load. Include `.json` at the end.
|
|
14
|
+
The file must be in the fixtures directory.
|
|
15
|
+
"""
|
|
16
|
+
absolute_file_path = Path(__file__).resolve().parent.parent / "fixtures" / file_name
|
|
17
|
+
|
|
18
|
+
def _load_fixture(apps, schema_editor):
|
|
19
|
+
# Save the default _get_model() function
|
|
20
|
+
default_get_model = python._get_model
|
|
21
|
+
|
|
22
|
+
# Define new _get_model() function here, which utilizes the apps argument to
|
|
23
|
+
# get the historical version of a model. This piece of code is directly taken
|
|
24
|
+
# from django.core.serializers.python._get_model, unchanged. However, here it
|
|
25
|
+
# has a different context, specifically, the apps variable.
|
|
26
|
+
def _get_model(model_identifier):
|
|
27
|
+
try:
|
|
28
|
+
return apps.get_model(model_identifier)
|
|
29
|
+
except (LookupError, TypeError):
|
|
30
|
+
raise base.DeserializationError("Invalid model identifier: '%s'" % model_identifier)
|
|
31
|
+
|
|
32
|
+
# Replace the _get_model() function on the module, so loaddata can utilize it.
|
|
33
|
+
python._get_model = _get_model
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
# Call loaddata command
|
|
37
|
+
call_command("loaddata", absolute_file_path, app_label="common")
|
|
38
|
+
finally:
|
|
39
|
+
# Restore default _get_model() function
|
|
40
|
+
python._get_model = default_get_model
|
|
41
|
+
|
|
42
|
+
return _load_fixture
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import json
|
|
3
|
+
from enum import Enum, auto
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
|
|
6
|
+
import jwt
|
|
7
|
+
from common import app_settings
|
|
8
|
+
from common.mail import (
|
|
9
|
+
address_book_ids,
|
|
10
|
+
campaign_ids,
|
|
11
|
+
django_send_email,
|
|
12
|
+
send_dotdigital_email,
|
|
13
|
+
)
|
|
14
|
+
from common.models import Student, Teacher
|
|
15
|
+
from django.conf import settings
|
|
16
|
+
from django.contrib.auth.models import User
|
|
17
|
+
from django.http import HttpResponse
|
|
18
|
+
from django.urls import reverse
|
|
19
|
+
from django.utils import timezone
|
|
20
|
+
from requests import delete, get, post, put
|
|
21
|
+
from requests.exceptions import RequestException
|
|
22
|
+
|
|
23
|
+
NOTIFICATION_EMAIL = (
|
|
24
|
+
"Code For Life Notification <" + app_settings.EMAIL_ADDRESS + ">"
|
|
25
|
+
)
|
|
26
|
+
VERIFICATION_EMAIL = (
|
|
27
|
+
"Code For Life Verification <" + app_settings.EMAIL_ADDRESS + ">"
|
|
28
|
+
)
|
|
29
|
+
PASSWORD_RESET_EMAIL = (
|
|
30
|
+
"Code For Life Password Reset <" + app_settings.EMAIL_ADDRESS + ">"
|
|
31
|
+
)
|
|
32
|
+
INVITE_FROM = "Code For Life Invitation <" + app_settings.EMAIL_ADDRESS + ">"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class DotmailerUserType(Enum):
|
|
36
|
+
TEACHER = auto()
|
|
37
|
+
STUDENT = auto()
|
|
38
|
+
NO_ACCOUNT = auto()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def generate_token(user, new_email="", preverified=False):
|
|
42
|
+
if preverified:
|
|
43
|
+
user.userprofile.is_verified = preverified
|
|
44
|
+
user.userprofile.save()
|
|
45
|
+
|
|
46
|
+
return generate_token_for_email(user.email, new_email)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def generate_token_for_email(email: str, new_email: str = ""):
|
|
50
|
+
return jwt.encode(
|
|
51
|
+
{
|
|
52
|
+
"email": email,
|
|
53
|
+
"new_email": new_email,
|
|
54
|
+
"email_verification_token": uuid4().hex[:30],
|
|
55
|
+
"expires": (
|
|
56
|
+
timezone.now() + datetime.timedelta(hours=1)
|
|
57
|
+
).timestamp(),
|
|
58
|
+
},
|
|
59
|
+
settings.SECRET_KEY,
|
|
60
|
+
algorithm="HS256",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _newsletter_ticked(data):
|
|
65
|
+
return "newsletter_ticked" in data and data["newsletter_ticked"]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def send_email(
|
|
69
|
+
sender,
|
|
70
|
+
recipients,
|
|
71
|
+
subject,
|
|
72
|
+
text_content,
|
|
73
|
+
title,
|
|
74
|
+
replace_url=None,
|
|
75
|
+
plaintext_template="email.txt",
|
|
76
|
+
html_template="email.html",
|
|
77
|
+
):
|
|
78
|
+
django_send_email(
|
|
79
|
+
sender,
|
|
80
|
+
recipients,
|
|
81
|
+
subject,
|
|
82
|
+
text_content,
|
|
83
|
+
title,
|
|
84
|
+
replace_url,
|
|
85
|
+
plaintext_template,
|
|
86
|
+
html_template,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def send_verification_email(
|
|
91
|
+
request, user, data, new_email=None, age=None, school=None
|
|
92
|
+
):
|
|
93
|
+
"""
|
|
94
|
+
Sends emails relating to email address verification.
|
|
95
|
+
|
|
96
|
+
On registration:
|
|
97
|
+
- if the user is under 13, send a verification email addressed to the parent / guardian
|
|
98
|
+
- if the user is over 13, send a regular verification email
|
|
99
|
+
- if the user is a student who just got released, send a verification email explaining the situation
|
|
100
|
+
- if the user is a student who has requested to sign up to the newsletter, handle their Dotmailer subscription
|
|
101
|
+
|
|
102
|
+
On email address update:
|
|
103
|
+
- sends an email to the old address alerting the user that an email change request has occurred
|
|
104
|
+
- sends an email to the new address requesting email address verification
|
|
105
|
+
|
|
106
|
+
:param request: The Django form request
|
|
107
|
+
:param user: The user object concerned with the email verification
|
|
108
|
+
:param data: The data inputted by the user in the Django form.
|
|
109
|
+
:param new_email: New email the user wants to associate to their account - if not provided, it means the user is
|
|
110
|
+
registering a new account
|
|
111
|
+
:param age: The user's age (collected only for the purposes of this function and if the user is an independent
|
|
112
|
+
student)
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
# verifying first email address (registration or unverified login attempt)
|
|
116
|
+
if not new_email:
|
|
117
|
+
verification = generate_token(user)
|
|
118
|
+
|
|
119
|
+
if age is None:
|
|
120
|
+
# if the user is a released student
|
|
121
|
+
if hasattr(user, "new_student") and school is not None:
|
|
122
|
+
url = f"{request.build_absolute_uri(reverse('verify_email', kwargs={'token': verification}))}"
|
|
123
|
+
|
|
124
|
+
send_dotdigital_email(
|
|
125
|
+
campaign_ids["verify_released_student"],
|
|
126
|
+
[user.email],
|
|
127
|
+
personalization_values={
|
|
128
|
+
"VERIFICATION_LINK": url,
|
|
129
|
+
"SCHOOL_NAME": school.name,
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
else:
|
|
133
|
+
url = f"{request.build_absolute_uri(reverse('verify_email', kwargs={'token': verification}))}"
|
|
134
|
+
|
|
135
|
+
send_dotdigital_email(
|
|
136
|
+
campaign_ids["verify_new_user"],
|
|
137
|
+
[user.email],
|
|
138
|
+
personalization_values={"VERIFICATION_LINK": url},
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if _newsletter_ticked(data):
|
|
142
|
+
add_to_dotmailer(
|
|
143
|
+
user.first_name,
|
|
144
|
+
user.last_name,
|
|
145
|
+
user.email,
|
|
146
|
+
address_book_ids["newsletter"],
|
|
147
|
+
DotmailerUserType.TEACHER,
|
|
148
|
+
)
|
|
149
|
+
# if the user is an independent student
|
|
150
|
+
else:
|
|
151
|
+
if age < 13:
|
|
152
|
+
url = f"{request.build_absolute_uri(reverse('verify_email', kwargs={'token': verification}))}"
|
|
153
|
+
send_dotdigital_email(
|
|
154
|
+
campaign_ids["verify_new_user_via_parent"],
|
|
155
|
+
[user.email],
|
|
156
|
+
personalization_values={
|
|
157
|
+
"FIRST_NAME": user.first_name,
|
|
158
|
+
"ACTIVATION_LINK": url,
|
|
159
|
+
},
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
url = f"{request.build_absolute_uri(reverse('verify_email', kwargs={'token': verification}))}"
|
|
163
|
+
send_dotdigital_email(
|
|
164
|
+
campaign_ids["verify_new_user"],
|
|
165
|
+
[user.email],
|
|
166
|
+
personalization_values={"VERIFICATION_LINK": url},
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if _newsletter_ticked(data):
|
|
170
|
+
add_to_dotmailer(
|
|
171
|
+
user.first_name,
|
|
172
|
+
user.last_name,
|
|
173
|
+
user.email,
|
|
174
|
+
address_book_ids["newsletter"],
|
|
175
|
+
DotmailerUserType.STUDENT,
|
|
176
|
+
)
|
|
177
|
+
# verifying change of email address.
|
|
178
|
+
else:
|
|
179
|
+
verification = generate_token(user, new_email)
|
|
180
|
+
url = f"{request.build_absolute_uri(reverse('verify_email', kwargs={'token': verification}))}"
|
|
181
|
+
send_dotdigital_email(
|
|
182
|
+
campaign_ids["email_change_verification"],
|
|
183
|
+
[new_email],
|
|
184
|
+
personalization_values={"VERIFICATION_LINK": url},
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def add_to_dotmailer(
|
|
189
|
+
first_name: str,
|
|
190
|
+
last_name: str,
|
|
191
|
+
email: str,
|
|
192
|
+
address_book_id: int,
|
|
193
|
+
user_type: DotmailerUserType = None,
|
|
194
|
+
):
|
|
195
|
+
try:
|
|
196
|
+
create_contact(first_name, last_name, email)
|
|
197
|
+
add_contact_to_address_book(
|
|
198
|
+
first_name, last_name, email, address_book_id, user_type
|
|
199
|
+
)
|
|
200
|
+
except RequestException:
|
|
201
|
+
return HttpResponse(status=404)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def create_contact(first_name, last_name, email):
|
|
205
|
+
url = app_settings.DOTMAILER_CREATE_CONTACT_URL
|
|
206
|
+
body = {
|
|
207
|
+
"contact": {
|
|
208
|
+
"email": email,
|
|
209
|
+
"optInType": "VerifiedDouble",
|
|
210
|
+
"emailType": "Html",
|
|
211
|
+
"dataFields": [
|
|
212
|
+
{"key": "FIRSTNAME", "value": first_name},
|
|
213
|
+
{"key": "LASTNAME", "value": last_name},
|
|
214
|
+
{"key": "FULLNAME", "value": f"{first_name} {last_name}"},
|
|
215
|
+
],
|
|
216
|
+
},
|
|
217
|
+
"consentFields": [
|
|
218
|
+
{
|
|
219
|
+
"fields": [
|
|
220
|
+
{
|
|
221
|
+
"key": "DATETIMECONSENTED",
|
|
222
|
+
"value": datetime.datetime.now().__str__(),
|
|
223
|
+
}
|
|
224
|
+
]
|
|
225
|
+
}
|
|
226
|
+
],
|
|
227
|
+
"preferences": app_settings.DOTMAILER_DEFAULT_PREFERENCES,
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
post(
|
|
231
|
+
url,
|
|
232
|
+
json=body,
|
|
233
|
+
auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD),
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def add_contact_to_address_book(
|
|
238
|
+
first_name: str,
|
|
239
|
+
last_name: str,
|
|
240
|
+
email: str,
|
|
241
|
+
address_book_id: int,
|
|
242
|
+
user_type: DotmailerUserType = None,
|
|
243
|
+
):
|
|
244
|
+
main_address_book_url = f"https://r1-api.dotmailer.com/v2/address-books/{address_book_id}/contacts"
|
|
245
|
+
|
|
246
|
+
body = {
|
|
247
|
+
"email": email,
|
|
248
|
+
"optInType": "VerifiedDouble",
|
|
249
|
+
"emailType": "Html",
|
|
250
|
+
"dataFields": [
|
|
251
|
+
{"key": "FIRSTNAME", "value": first_name},
|
|
252
|
+
{"key": "LASTNAME", "value": last_name},
|
|
253
|
+
{"key": "FULLNAME", "value": f"{first_name} {last_name}"},
|
|
254
|
+
],
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
post(
|
|
258
|
+
main_address_book_url,
|
|
259
|
+
json=body,
|
|
260
|
+
auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
if user_type is not None:
|
|
264
|
+
specific_address_book_url = (
|
|
265
|
+
app_settings.DOTMAILER_NO_ACCOUNT_ADDRESS_BOOK_URL
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if user_type == DotmailerUserType.TEACHER:
|
|
269
|
+
specific_address_book_url = (
|
|
270
|
+
app_settings.DOTMAILER_TEACHER_ADDRESS_BOOK_URL
|
|
271
|
+
)
|
|
272
|
+
elif user_type == DotmailerUserType.STUDENT:
|
|
273
|
+
specific_address_book_url = (
|
|
274
|
+
app_settings.DOTMAILER_STUDENT_ADDRESS_BOOK_URL
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
post(
|
|
278
|
+
specific_address_book_url,
|
|
279
|
+
json=body,
|
|
280
|
+
auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD),
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def delete_contact(email: str):
|
|
285
|
+
try:
|
|
286
|
+
user = get_dotmailer_user_by_email(email)
|
|
287
|
+
user_id = user.get("id")
|
|
288
|
+
if user_id:
|
|
289
|
+
url = app_settings.DOTMAILER_DELETE_USER_BY_ID_URL.replace(
|
|
290
|
+
"ID", str(user_id)
|
|
291
|
+
)
|
|
292
|
+
delete(
|
|
293
|
+
url,
|
|
294
|
+
auth=(
|
|
295
|
+
app_settings.DOTMAILER_USER,
|
|
296
|
+
app_settings.DOTMAILER_PASSWORD,
|
|
297
|
+
),
|
|
298
|
+
)
|
|
299
|
+
except RequestException:
|
|
300
|
+
return HttpResponse(status=404)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def get_dotmailer_user_by_email(email):
|
|
304
|
+
url = app_settings.DOTMAILER_GET_USER_BY_EMAIL_URL.replace("EMAIL", email)
|
|
305
|
+
|
|
306
|
+
response = get(
|
|
307
|
+
url, auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD)
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
return json.loads(response.content)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def add_consent_record_to_dotmailer_user(user):
|
|
314
|
+
consent_date_time = datetime.datetime.now().__str__()
|
|
315
|
+
|
|
316
|
+
url = app_settings.DOTMAILER_PUT_CONSENT_DATA_URL.replace(
|
|
317
|
+
"USER_ID", str(user["id"])
|
|
318
|
+
)
|
|
319
|
+
body = {
|
|
320
|
+
"contact": {
|
|
321
|
+
"email": user["email"],
|
|
322
|
+
"optInType": user["optInType"],
|
|
323
|
+
"emailType": user["emailType"],
|
|
324
|
+
"dataFields": user["dataFields"],
|
|
325
|
+
},
|
|
326
|
+
"consentFields": [
|
|
327
|
+
{
|
|
328
|
+
"fields": [
|
|
329
|
+
{"key": "DATETIMECONSENTED", "value": consent_date_time}
|
|
330
|
+
]
|
|
331
|
+
}
|
|
332
|
+
],
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
put(
|
|
336
|
+
url,
|
|
337
|
+
json=body,
|
|
338
|
+
auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD),
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def send_dotmailer_consent_confirmation_email_to_user(user):
|
|
343
|
+
url = app_settings.DOTMAILER_SEND_CAMPAIGN_URL
|
|
344
|
+
campaign_id = app_settings.DOTMAILER_THANKS_FOR_STAYING_CAMPAIGN_ID
|
|
345
|
+
body = {"campaignID": campaign_id, "contactIds": [str(user["id"])]}
|
|
346
|
+
|
|
347
|
+
post(
|
|
348
|
+
url,
|
|
349
|
+
json=body,
|
|
350
|
+
auth=(app_settings.DOTMAILER_USER, app_settings.DOTMAILER_PASSWORD),
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def update_indy_email(user, request, data):
|
|
355
|
+
changing_email = False
|
|
356
|
+
new_email = data["email"]
|
|
357
|
+
|
|
358
|
+
if new_email != "" and new_email != user.email:
|
|
359
|
+
changing_email = True
|
|
360
|
+
users_with_email = User.objects.filter(email=new_email)
|
|
361
|
+
|
|
362
|
+
send_dotdigital_email(
|
|
363
|
+
campaign_ids["email_change_notification"],
|
|
364
|
+
[user.email],
|
|
365
|
+
personalization_values={"NEW_EMAIL_ADDRESS": new_email},
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
# email is available
|
|
369
|
+
if not users_with_email.exists():
|
|
370
|
+
# new email to set and verify
|
|
371
|
+
send_verification_email(request, user, data, new_email)
|
|
372
|
+
return changing_email, new_email
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def update_email(user: Teacher or Student, request, data):
|
|
376
|
+
changing_email = False
|
|
377
|
+
new_email = data["email"]
|
|
378
|
+
|
|
379
|
+
if new_email != "" and new_email != user.new_user.email:
|
|
380
|
+
changing_email = True
|
|
381
|
+
users_with_email = User.objects.filter(email=new_email)
|
|
382
|
+
|
|
383
|
+
send_dotdigital_email(
|
|
384
|
+
campaign_ids["email_change_notification"],
|
|
385
|
+
[user.new_user.email],
|
|
386
|
+
personalization_values={"NEW_EMAIL_ADDRESS": new_email},
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# email is available
|
|
390
|
+
if not users_with_email.exists():
|
|
391
|
+
# new email to set and verify
|
|
392
|
+
send_verification_email(request, user.new_user, data, new_email)
|
|
393
|
+
return changing_email, new_email
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import random
|
|
3
|
+
import string
|
|
4
|
+
from builtins import range, str
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
from common.models import Class, Student
|
|
8
|
+
from django.contrib.auth.models import User
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_random_username():
|
|
12
|
+
while True:
|
|
13
|
+
random_username = uuid4().hex[:30] # generate a random username
|
|
14
|
+
if not User.objects.filter(username=random_username).exists():
|
|
15
|
+
return random_username
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def generate_new_student_name(orig_name):
|
|
19
|
+
if not Student.objects.filter(new_user__username=orig_name).exists():
|
|
20
|
+
return orig_name
|
|
21
|
+
|
|
22
|
+
i = 1
|
|
23
|
+
while True:
|
|
24
|
+
new_name = orig_name + str(i)
|
|
25
|
+
if not Student.objects.filter(new_user__username=new_name).exists():
|
|
26
|
+
return new_name
|
|
27
|
+
i += 1
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def generate_access_code():
|
|
31
|
+
while True:
|
|
32
|
+
access_code = "".join(random.choice(string.ascii_uppercase) for _ in range(5))
|
|
33
|
+
|
|
34
|
+
if not Class.objects.filter(access_code=access_code).exists():
|
|
35
|
+
return access_code
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def generate_password(length):
|
|
39
|
+
return "".join(random.choice(string.ascii_lowercase) for _ in range(length))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def generate_login_id():
|
|
43
|
+
"""Returns the uuid string and its hashed.
|
|
44
|
+
The string is used for URL, and the hashed is stored in the DB."""
|
|
45
|
+
login_id = uuid4().hex
|
|
46
|
+
hashed_login_id = get_hashed_login_id(login_id)
|
|
47
|
+
return login_id, hashed_login_id
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_hashed_login_id(login_id):
|
|
51
|
+
"""Returns the hash of a given string used for login url"""
|
|
52
|
+
return hashlib.sha256(login_id.encode()).hexdigest()
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# TODO: Move to Address model once we create it
|
|
2
|
+
def sanitise_uk_postcode(postcode):
|
|
3
|
+
if len(postcode) >= 5: # Valid UK postcodes are at least 5 chars long
|
|
4
|
+
outcode = postcode[:-3] # UK incodes are always 3 characters
|
|
5
|
+
|
|
6
|
+
# Insert a space between outcode and incode if there isn't already one
|
|
7
|
+
if not outcode.endswith(" "):
|
|
8
|
+
postcode = postcode[:-3] + " " + postcode[-3:]
|
|
9
|
+
|
|
10
|
+
return postcode
|