codeforlife-portal 5.32.2__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.32.2.dist-info → codeforlife_portal-8.9.9.dist-info}/RECORD +341 -238
- {codeforlife_portal-5.32.2.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.32.2.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 +31 -0
- 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 +10 -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 -73
- 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 +138 -15
- 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 +237 -164
- portal/views/admin.py +0 -332
- portal/views/api.py +82 -68
- 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/__init__.py +0 -0
- portal/views/two_factor/core.py +28 -0
- portal/views/two_factor/form.py +7 -0
- portal/views/two_factor/profile.py +11 -0
- codeforlife_portal-5.32.2.dist-info/LICENSE.md +0 -577
- codeforlife_portal-5.32.2.dist-info/METADATA +0 -38
- deploy/permissions.py +0 -2
- example_project/manage.py +0 -10
- portal/autoconfig.py +0 -140
- 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
portal/tests/test_views.py
CHANGED
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
import csv
|
|
2
2
|
import io
|
|
3
3
|
import json
|
|
4
|
-
from datetime import
|
|
4
|
+
from datetime import date, datetime, timedelta
|
|
5
|
+
from unittest.mock import ANY, Mock, patch
|
|
5
6
|
|
|
6
|
-
import PyPDF2
|
|
7
7
|
import pytest
|
|
8
|
-
from
|
|
9
|
-
from common.models import
|
|
8
|
+
from common.mail import campaign_ids
|
|
9
|
+
from common.models import (
|
|
10
|
+
Class,
|
|
11
|
+
DailyActivity,
|
|
12
|
+
School,
|
|
13
|
+
Student,
|
|
14
|
+
Teacher,
|
|
15
|
+
TotalActivity,
|
|
16
|
+
UserProfile,
|
|
17
|
+
UserSession,
|
|
18
|
+
)
|
|
10
19
|
from common.tests.utils.classes import create_class_directly
|
|
11
20
|
from common.tests.utils.organisation import (
|
|
12
21
|
create_organisation_directly,
|
|
13
22
|
join_teacher_to_organisation,
|
|
14
23
|
)
|
|
15
24
|
from common.tests.utils.student import (
|
|
25
|
+
create_independent_student_directly,
|
|
16
26
|
create_school_student_directly,
|
|
17
27
|
create_student_with_direct_login,
|
|
18
|
-
create_independent_student_directly,
|
|
19
28
|
)
|
|
20
29
|
from common.tests.utils.teacher import signup_teacher_directly
|
|
21
30
|
from django.contrib.auth.models import User
|
|
@@ -25,11 +34,21 @@ from django.utils import timezone
|
|
|
25
34
|
from game.models import Level
|
|
26
35
|
from game.tests.utils.attempt import create_attempt
|
|
27
36
|
from game.tests.utils.level import create_save_level
|
|
37
|
+
from pypdf import PdfReader
|
|
38
|
+
from rest_framework.test import APIClient, APITestCase
|
|
28
39
|
|
|
29
40
|
from deploy import captcha
|
|
41
|
+
from portal.templatetags.app_tags import is_logged_in_as_admin_teacher
|
|
42
|
+
from portal.views.api import anonymise
|
|
43
|
+
from portal.views.cron.user import (
|
|
44
|
+
USER_DELETE_UNVERIFIED_ACCOUNT_DAYS,
|
|
45
|
+
USER_2ND_INACTIVE_REMINDER_DAYS,
|
|
46
|
+
USER_FINAL_INACTIVE_REMINDER_DAYS,
|
|
47
|
+
USER_RETENTION_PERIOD,
|
|
48
|
+
)
|
|
30
49
|
from portal.views.teacher.teach import (
|
|
31
|
-
REMINDER_CARDS_PDF_ROWS,
|
|
32
50
|
REMINDER_CARDS_PDF_COLUMNS,
|
|
51
|
+
REMINDER_CARDS_PDF_ROWS,
|
|
33
52
|
REMINDER_CARDS_PDF_WARNING_TEXT,
|
|
34
53
|
count_student_details_click,
|
|
35
54
|
)
|
|
@@ -39,8 +58,11 @@ class TestTeacherViews(TestCase):
|
|
|
39
58
|
@classmethod
|
|
40
59
|
def setUpTestData(cls):
|
|
41
60
|
cls.email, cls.password = signup_teacher_directly()
|
|
61
|
+
cls.school = create_organisation_directly(cls.email)
|
|
42
62
|
_, _, cls.class_access_code = create_class_directly(cls.email)
|
|
43
|
-
_,
|
|
63
|
+
_, cls.password_student, cls.student = create_school_student_directly(
|
|
64
|
+
cls.class_access_code
|
|
65
|
+
)
|
|
44
66
|
|
|
45
67
|
def login(self):
|
|
46
68
|
c = Client()
|
|
@@ -49,7 +71,9 @@ class TestTeacherViews(TestCase):
|
|
|
49
71
|
|
|
50
72
|
def test_reminder_cards(self):
|
|
51
73
|
c = self.login()
|
|
52
|
-
url = reverse(
|
|
74
|
+
url = reverse(
|
|
75
|
+
"teacher_print_reminder_cards", args=[self.class_access_code]
|
|
76
|
+
)
|
|
53
77
|
|
|
54
78
|
# First test with 2 dummy students
|
|
55
79
|
NAME1 = "Test name"
|
|
@@ -69,10 +93,10 @@ class TestTeacherViews(TestCase):
|
|
|
69
93
|
|
|
70
94
|
# read PDF, check there's only 1 page and that the correct student details show
|
|
71
95
|
with io.BytesIO(response.content) as pdf_file:
|
|
72
|
-
file_reader =
|
|
73
|
-
assert file_reader.
|
|
96
|
+
file_reader = PdfReader(pdf_file)
|
|
97
|
+
assert len(file_reader.pages) == 1
|
|
74
98
|
|
|
75
|
-
page_text = file_reader.
|
|
99
|
+
page_text = file_reader.pages[0].extract_text()
|
|
76
100
|
assert NAME1 in page_text
|
|
77
101
|
assert NAME2 in page_text
|
|
78
102
|
assert PASSWORD1 in page_text
|
|
@@ -83,7 +107,9 @@ class TestTeacherViews(TestCase):
|
|
|
83
107
|
# page number
|
|
84
108
|
students_per_page = REMINDER_CARDS_PDF_ROWS * REMINDER_CARDS_PDF_COLUMNS
|
|
85
109
|
for _ in range(len(studentlist), students_per_page + 1):
|
|
86
|
-
studentlist.append(
|
|
110
|
+
studentlist.append(
|
|
111
|
+
{"name": NAME1, "password": PASSWORD1, "login_url": URL}
|
|
112
|
+
)
|
|
87
113
|
|
|
88
114
|
assert len(studentlist) == students_per_page + 1
|
|
89
115
|
|
|
@@ -93,11 +119,11 @@ class TestTeacherViews(TestCase):
|
|
|
93
119
|
|
|
94
120
|
# Check there are 2 pages and that each page contains the warning text
|
|
95
121
|
with io.BytesIO(response.content) as pdf_file:
|
|
96
|
-
file_reader =
|
|
97
|
-
assert file_reader.
|
|
122
|
+
file_reader = PdfReader(pdf_file)
|
|
123
|
+
assert len(file_reader.pages) == 2
|
|
98
124
|
|
|
99
|
-
page1_text = file_reader.
|
|
100
|
-
page2_text = file_reader.
|
|
125
|
+
page1_text = file_reader.pages[0].extract_text()
|
|
126
|
+
page2_text = file_reader.pages[1].extract_text()
|
|
101
127
|
assert REMINDER_CARDS_PDF_WARNING_TEXT in page1_text
|
|
102
128
|
assert REMINDER_CARDS_PDF_WARNING_TEXT in page2_text
|
|
103
129
|
|
|
@@ -122,7 +148,9 @@ class TestTeacherViews(TestCase):
|
|
|
122
148
|
reader = csv.reader(io.StringIO(content))
|
|
123
149
|
|
|
124
150
|
access_code = self.class_access_code
|
|
125
|
-
class_url = reverse(
|
|
151
|
+
class_url = reverse(
|
|
152
|
+
"student_login", kwargs={"access_code": access_code}
|
|
153
|
+
)
|
|
126
154
|
row0 = next(reader)
|
|
127
155
|
assert row0[0].strip() == access_code
|
|
128
156
|
assert class_url in row0[1].strip()
|
|
@@ -147,9 +175,9 @@ class TestTeacherViews(TestCase):
|
|
|
147
175
|
|
|
148
176
|
def test_organisation_kick_has_correct_permissions(self):
|
|
149
177
|
teacher2_email, _ = signup_teacher_directly()
|
|
150
|
-
|
|
151
|
-
join_teacher_to_organisation(self.email,
|
|
152
|
-
join_teacher_to_organisation(teacher2_email,
|
|
178
|
+
school = create_organisation_directly(self.email)
|
|
179
|
+
join_teacher_to_organisation(self.email, school.name, is_admin=True)
|
|
180
|
+
join_teacher_to_organisation(teacher2_email, school.name)
|
|
153
181
|
teacher2_id = Teacher.objects.get(new_user__email=teacher2_email).id
|
|
154
182
|
|
|
155
183
|
client = self.login()
|
|
@@ -161,7 +189,9 @@ class TestTeacherViews(TestCase):
|
|
|
161
189
|
|
|
162
190
|
def test_daily_activity_student_details(self):
|
|
163
191
|
c = self.login()
|
|
164
|
-
url = reverse(
|
|
192
|
+
url = reverse(
|
|
193
|
+
"teacher_print_reminder_cards", args=[self.class_access_code]
|
|
194
|
+
)
|
|
165
195
|
|
|
166
196
|
data = {
|
|
167
197
|
"data": json.dumps(
|
|
@@ -170,7 +200,7 @@ class TestTeacherViews(TestCase):
|
|
|
170
200
|
"name": self.student.new_user.first_name,
|
|
171
201
|
"password": self.student.new_user.password,
|
|
172
202
|
"login_url": self.student.login_id,
|
|
173
|
-
}
|
|
203
|
+
}
|
|
174
204
|
]
|
|
175
205
|
)
|
|
176
206
|
}
|
|
@@ -207,6 +237,72 @@ class TestTeacherViews(TestCase):
|
|
|
207
237
|
with pytest.raises(Exception):
|
|
208
238
|
count_student_details_click("Wrong download method")
|
|
209
239
|
|
|
240
|
+
def test_release_verified_student(self):
|
|
241
|
+
c = Client()
|
|
242
|
+
student_login_url = reverse(
|
|
243
|
+
"student_login", args=[self.class_access_code]
|
|
244
|
+
)
|
|
245
|
+
response = c.post(
|
|
246
|
+
student_login_url,
|
|
247
|
+
{
|
|
248
|
+
"username": self.student.new_user.first_name,
|
|
249
|
+
"password": self.password_student,
|
|
250
|
+
},
|
|
251
|
+
)
|
|
252
|
+
assert response.status_code == 302
|
|
253
|
+
|
|
254
|
+
student = Student.objects.get(pk=self.student.pk)
|
|
255
|
+
|
|
256
|
+
assert student.user.is_verified
|
|
257
|
+
|
|
258
|
+
c.logout()
|
|
259
|
+
c.login(username=self.email, password=self.password)
|
|
260
|
+
|
|
261
|
+
teacher = Teacher.objects.factory(
|
|
262
|
+
"the", "teacher", "theteacher@foo.com", "password"
|
|
263
|
+
)
|
|
264
|
+
level = Level.objects.create()
|
|
265
|
+
|
|
266
|
+
level.owner = student.new_user.userprofile
|
|
267
|
+
level.shared_with.add(teacher.new_user)
|
|
268
|
+
level.save()
|
|
269
|
+
|
|
270
|
+
students_levels = Level.objects.filter(
|
|
271
|
+
owner=student.new_user.userprofile
|
|
272
|
+
).all()
|
|
273
|
+
|
|
274
|
+
for level in students_levels.all():
|
|
275
|
+
assert level.shared_with.exists()
|
|
276
|
+
|
|
277
|
+
release_url = reverse(
|
|
278
|
+
"teacher_dismiss_students", args=[self.class_access_code]
|
|
279
|
+
)
|
|
280
|
+
response = c.post(
|
|
281
|
+
release_url,
|
|
282
|
+
{
|
|
283
|
+
"form-TOTAL_FORMS": 1,
|
|
284
|
+
"form-INITIAL_FORMS": 1,
|
|
285
|
+
"form-MIN_NUM_FORMS": 0,
|
|
286
|
+
"form-MAX_NUM_FORMS": 1000,
|
|
287
|
+
"form-0-orig_name": self.student.new_user.first_name,
|
|
288
|
+
"form-0-name": self.student.new_user.first_name,
|
|
289
|
+
"form-0-email": "independent@gmail.com",
|
|
290
|
+
"form-0-confirm_email": "independent@gmail.com",
|
|
291
|
+
"submit_dismiss": "",
|
|
292
|
+
},
|
|
293
|
+
)
|
|
294
|
+
assert response.status_code == 302
|
|
295
|
+
|
|
296
|
+
student = Student.objects.get(pk=self.student.pk)
|
|
297
|
+
assert not student.user.is_verified
|
|
298
|
+
|
|
299
|
+
students_levels = Level.objects.filter(
|
|
300
|
+
owner=student.new_user.userprofile
|
|
301
|
+
).all()
|
|
302
|
+
|
|
303
|
+
for level in students_levels.all():
|
|
304
|
+
assert not level.shared_with.exists()
|
|
305
|
+
|
|
210
306
|
|
|
211
307
|
class TestLoginViews(TestCase):
|
|
212
308
|
@classmethod
|
|
@@ -260,11 +356,15 @@ class TestLoginViews(TestCase):
|
|
|
260
356
|
|
|
261
357
|
if next_url:
|
|
262
358
|
url = (
|
|
263
|
-
reverse(
|
|
359
|
+
reverse(
|
|
360
|
+
"student_login", kwargs={"access_code": class_access_code}
|
|
361
|
+
)
|
|
264
362
|
+ "?next=/"
|
|
265
363
|
)
|
|
266
364
|
else:
|
|
267
|
-
url = reverse(
|
|
365
|
+
url = reverse(
|
|
366
|
+
"student_login", kwargs={"access_code": class_access_code}
|
|
367
|
+
)
|
|
268
368
|
|
|
269
369
|
c = Client()
|
|
270
370
|
response = c.post(url, {"username": name, "password": password})
|
|
@@ -316,13 +416,17 @@ class TestLoginViews(TestCase):
|
|
|
316
416
|
c = Client()
|
|
317
417
|
|
|
318
418
|
resp = c.post(
|
|
319
|
-
reverse("student_login_access_code"),
|
|
419
|
+
reverse("student_login_access_code"),
|
|
420
|
+
{"access_code": class_access_code},
|
|
320
421
|
)
|
|
321
422
|
assert resp.status_code == 302
|
|
322
423
|
nexturl = resp.url
|
|
323
424
|
assert nexturl == reverse(
|
|
324
425
|
"student_login",
|
|
325
|
-
kwargs={
|
|
426
|
+
kwargs={
|
|
427
|
+
"access_code": class_access_code,
|
|
428
|
+
"login_type": "classform",
|
|
429
|
+
},
|
|
326
430
|
)
|
|
327
431
|
c.post(nexturl, {"username": name, "password": password})
|
|
328
432
|
|
|
@@ -343,7 +447,9 @@ class TestLoginViews(TestCase):
|
|
|
343
447
|
_, _, name, password, class_access_code = self._set_up_test_data()
|
|
344
448
|
|
|
345
449
|
c = Client()
|
|
346
|
-
url = reverse(
|
|
450
|
+
url = reverse(
|
|
451
|
+
"student_login", kwargs={"access_code": class_access_code}
|
|
452
|
+
)
|
|
347
453
|
c.post(url, {"username": name, "password": password})
|
|
348
454
|
|
|
349
455
|
# check if there's a UserSession data within the last 10 secs
|
|
@@ -364,8 +470,10 @@ class TestLoginViews(TestCase):
|
|
|
364
470
|
randomname = "randomname"
|
|
365
471
|
|
|
366
472
|
c = Client()
|
|
367
|
-
url = reverse(
|
|
368
|
-
|
|
473
|
+
url = reverse(
|
|
474
|
+
"student_login", kwargs={"access_code": class_access_code}
|
|
475
|
+
)
|
|
476
|
+
c.post(url, {"username": randomname, "password": "xx"})
|
|
369
477
|
|
|
370
478
|
# check if there's a UserSession data within the last 10 secs
|
|
371
479
|
now = timezone.now()
|
|
@@ -390,7 +498,9 @@ class TestLoginViews(TestCase):
|
|
|
390
498
|
|
|
391
499
|
def test_student_direct_login(self):
|
|
392
500
|
_, _, _, _, class_access_code = self._set_up_test_data()
|
|
393
|
-
student, login_id, _, _ = create_student_with_direct_login(
|
|
501
|
+
student, login_id, _, _ = create_student_with_direct_login(
|
|
502
|
+
class_access_code
|
|
503
|
+
)
|
|
394
504
|
|
|
395
505
|
c = Client()
|
|
396
506
|
assert c.login(user_id=student.new_user.id, login_id=login_id) == True
|
|
@@ -433,7 +543,7 @@ class TestLoginViews(TestCase):
|
|
|
433
543
|
|
|
434
544
|
|
|
435
545
|
class TestViews(TestCase):
|
|
436
|
-
def
|
|
546
|
+
def test_home_learning(self):
|
|
437
547
|
c = Client()
|
|
438
548
|
home_url = reverse("home")
|
|
439
549
|
response = c.get(home_url)
|
|
@@ -445,11 +555,11 @@ class TestViews(TestCase):
|
|
|
445
555
|
|
|
446
556
|
expected_html = '<a href="/home-learning">Home learning</a>'
|
|
447
557
|
|
|
448
|
-
|
|
558
|
+
assert expected_html in html
|
|
449
559
|
|
|
450
560
|
response = c.get(page_url)
|
|
451
561
|
|
|
452
|
-
|
|
562
|
+
assert response.status_code == 200
|
|
453
563
|
|
|
454
564
|
def test_contributor(self):
|
|
455
565
|
c = Client()
|
|
@@ -461,56 +571,79 @@ class TestViews(TestCase):
|
|
|
461
571
|
response = c.get(page_url)
|
|
462
572
|
assert response.status_code == 200
|
|
463
573
|
|
|
574
|
+
def test_ten_year_map(self):
|
|
575
|
+
c = Client()
|
|
576
|
+
page_url = reverse("celebrate")
|
|
577
|
+
response = c.get(page_url)
|
|
578
|
+
assert response.status_code == 200
|
|
579
|
+
|
|
464
580
|
def test_student_dashboard_view(self):
|
|
465
581
|
teacher_email, teacher_password = signup_teacher_directly()
|
|
466
582
|
create_organisation_directly(teacher_email)
|
|
467
583
|
klass, _, class_access_code = create_class_directly(teacher_email)
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
584
|
+
(
|
|
585
|
+
student_name,
|
|
586
|
+
student_password,
|
|
587
|
+
student,
|
|
588
|
+
) = create_school_student_directly(class_access_code)
|
|
471
589
|
|
|
472
590
|
# Expected context data when a student hasn't played anything yet
|
|
473
591
|
EXPECTED_DATA_FIRST_LOGIN = {
|
|
474
|
-
"
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
592
|
+
"rapid_router": {
|
|
593
|
+
"num_completed": 0,
|
|
594
|
+
"num_top_scores": 0,
|
|
595
|
+
"total_score": 0,
|
|
596
|
+
"total_available_score": 1450,
|
|
597
|
+
},
|
|
598
|
+
"python_den": {
|
|
599
|
+
"num_completed": 0,
|
|
600
|
+
"num_top_scores": 0,
|
|
601
|
+
"total_score": 0,
|
|
602
|
+
"total_available_score": 830,
|
|
603
|
+
},
|
|
478
604
|
}
|
|
479
605
|
|
|
480
606
|
# Expected context data when a student has attempted some RR levels
|
|
481
607
|
EXPECTED_DATA_WITH_ATTEMPTS = {
|
|
482
|
-
"
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
608
|
+
"rapid_router": {
|
|
609
|
+
"num_completed": 2,
|
|
610
|
+
"num_top_scores": 1,
|
|
611
|
+
"total_score": 39,
|
|
612
|
+
"total_available_score": 1450,
|
|
613
|
+
},
|
|
614
|
+
"python_den": {
|
|
615
|
+
"num_completed": 2,
|
|
616
|
+
"num_top_scores": 2,
|
|
617
|
+
"total_score": 30,
|
|
618
|
+
"total_available_score": 830,
|
|
619
|
+
},
|
|
486
620
|
}
|
|
487
621
|
|
|
488
|
-
# Expected context data when a student has also attempted some custom RR
|
|
622
|
+
# Expected context data when a student has also attempted some custom RR
|
|
623
|
+
# levels
|
|
489
624
|
EXPECTED_DATA_WITH_CUSTOM_ATTEMPTS = {
|
|
490
|
-
"
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
"total_custom_score": 10,
|
|
505
|
-
"total_custom_available_score": 20,
|
|
506
|
-
"worksheet_id": 3,
|
|
507
|
-
"worksheet_image": "images/worksheets/ancient.jpg",
|
|
625
|
+
"rapid_router": {
|
|
626
|
+
"num_completed": 2,
|
|
627
|
+
"num_top_scores": 1,
|
|
628
|
+
"total_score": 39,
|
|
629
|
+
"total_available_score": 1450,
|
|
630
|
+
"total_custom_score": 10,
|
|
631
|
+
"total_custom_available_score": 20,
|
|
632
|
+
},
|
|
633
|
+
"python_den": {
|
|
634
|
+
"num_completed": 2,
|
|
635
|
+
"num_top_scores": 2,
|
|
636
|
+
"total_score": 30,
|
|
637
|
+
"total_available_score": 830,
|
|
638
|
+
},
|
|
508
639
|
}
|
|
509
640
|
|
|
510
641
|
c = Client()
|
|
511
642
|
|
|
512
643
|
# Login and check initial data
|
|
513
|
-
url = reverse(
|
|
644
|
+
url = reverse(
|
|
645
|
+
"student_login", kwargs={"access_code": class_access_code}
|
|
646
|
+
)
|
|
514
647
|
c.post(url, {"username": student_name, "password": student_password})
|
|
515
648
|
|
|
516
649
|
student_dashboard_url = reverse("student_details")
|
|
@@ -519,26 +652,33 @@ class TestViews(TestCase):
|
|
|
519
652
|
assert response.status_code == 200
|
|
520
653
|
assert response.context_data == EXPECTED_DATA_FIRST_LOGIN
|
|
521
654
|
|
|
522
|
-
# Attempt the first two levels, one perfect attempt, one not
|
|
655
|
+
# Attempt the first two RR levels, one perfect attempt, one not
|
|
523
656
|
level1 = Level.objects.get(name="1")
|
|
524
657
|
level2 = Level.objects.get(name="2")
|
|
525
658
|
|
|
526
659
|
create_attempt(student, level1, 20)
|
|
527
660
|
create_attempt(student, level2, 19)
|
|
528
661
|
|
|
662
|
+
# Attempt the first and fourth Python Den levels, both perfect
|
|
663
|
+
level1001 = Level.objects.get(name="1001")
|
|
664
|
+
level1004 = Level.objects.get(name="1004")
|
|
665
|
+
|
|
666
|
+
create_attempt(student, level1001, 20)
|
|
667
|
+
create_attempt(student, level1004, 10)
|
|
668
|
+
|
|
529
669
|
response = c.get(student_dashboard_url)
|
|
530
670
|
|
|
531
671
|
assert response.status_code == 200
|
|
532
672
|
assert response.context_data == EXPECTED_DATA_WITH_ATTEMPTS
|
|
533
673
|
|
|
534
|
-
# Teacher creates 3 custom levels, only shares the first 2 with the
|
|
535
|
-
# Check that the total available score only includes the
|
|
536
|
-
# student. Student attempts one level only.
|
|
674
|
+
# Teacher creates 3 custom levels, only shares the first 2 with the
|
|
675
|
+
# student. Check that the total available score only includes the
|
|
676
|
+
# levels shared with the student. Student attempts one level only.
|
|
537
677
|
custom_level1_id = create_save_level(student.class_field.teacher)
|
|
538
678
|
custom_level2_id = create_save_level(student.class_field.teacher)
|
|
539
679
|
create_save_level(student.class_field.teacher)
|
|
540
|
-
custom_level1 = Level.objects.get(id=custom_level1_id)
|
|
541
|
-
custom_level2 = Level.objects.get(id=custom_level2_id)
|
|
680
|
+
custom_level1 = Level.objects.get(id=custom_level1_id.id)
|
|
681
|
+
custom_level2 = Level.objects.get(id=custom_level2_id.id)
|
|
542
682
|
|
|
543
683
|
student.new_user.shared.add(custom_level1, custom_level2)
|
|
544
684
|
student.new_user.save()
|
|
@@ -550,11 +690,695 @@ class TestViews(TestCase):
|
|
|
550
690
|
assert response.status_code == 200
|
|
551
691
|
assert response.context_data == EXPECTED_DATA_WITH_CUSTOM_ATTEMPTS
|
|
552
692
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
693
|
+
@patch("portal.views.registration.send_dotdigital_email")
|
|
694
|
+
def test_delete_account(self, mock_send_dotdigital_email: Mock):
|
|
695
|
+
email, password = signup_teacher_directly()
|
|
696
|
+
u = User.objects.get(email=email)
|
|
697
|
+
usrid = u.id
|
|
556
698
|
|
|
557
|
-
|
|
699
|
+
c = Client()
|
|
700
|
+
url = reverse("teacher_login")
|
|
701
|
+
c.post(
|
|
702
|
+
url,
|
|
703
|
+
{
|
|
704
|
+
"auth-username": email,
|
|
705
|
+
"auth-password": password,
|
|
706
|
+
"teacher_login_view-current_step": "auth",
|
|
707
|
+
},
|
|
708
|
+
)
|
|
558
709
|
|
|
559
|
-
|
|
560
|
-
|
|
710
|
+
# fail to delete with incorrect password
|
|
711
|
+
url = reverse("delete_account")
|
|
712
|
+
response = c.post(url, {"password": "wrongPassword"})
|
|
713
|
+
|
|
714
|
+
assert response.status_code == 302
|
|
715
|
+
mock_send_dotdigital_email.assert_not_called()
|
|
716
|
+
|
|
717
|
+
# user has not been anonymised
|
|
718
|
+
u = User.objects.get(email=email)
|
|
719
|
+
assert u.id == usrid
|
|
720
|
+
|
|
721
|
+
# try again with the correct password
|
|
722
|
+
url = reverse("delete_account")
|
|
723
|
+
response = c.post(
|
|
724
|
+
url, {"password": password, "unsubscribe_newsletter": "on"}
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
assert response.status_code == 302
|
|
728
|
+
mock_send_dotdigital_email.assert_called_once()
|
|
729
|
+
assert response.url == reverse("home")
|
|
730
|
+
|
|
731
|
+
# user has been anonymised
|
|
732
|
+
u = User.objects.get(id=usrid)
|
|
733
|
+
assert u.first_name == "Deleted"
|
|
734
|
+
assert not u.is_active
|
|
735
|
+
|
|
736
|
+
assert c.login(username=email, password=password) == False
|
|
737
|
+
|
|
738
|
+
@patch("portal.views.registration.send_dotdigital_email")
|
|
739
|
+
def test_delete_account_admin(self, mock_send_dotdigital_email: Mock):
|
|
740
|
+
"""test the passing of admin role after deletion of an admin account"""
|
|
741
|
+
|
|
742
|
+
email1, password1 = signup_teacher_directly()
|
|
743
|
+
email2, password2 = signup_teacher_directly()
|
|
744
|
+
email3, password3 = signup_teacher_directly()
|
|
745
|
+
email4, password4 = signup_teacher_directly()
|
|
746
|
+
|
|
747
|
+
user1 = User.objects.get(email=email1)
|
|
748
|
+
user1.last_name = "Amir"
|
|
749
|
+
user1.save()
|
|
750
|
+
usrid1 = user1.id
|
|
751
|
+
|
|
752
|
+
user2 = User.objects.get(email=email2)
|
|
753
|
+
user2.last_name = "Bee"
|
|
754
|
+
user2.save()
|
|
755
|
+
usrid2 = user2.id
|
|
756
|
+
|
|
757
|
+
user3 = User.objects.get(email=email3)
|
|
758
|
+
user3.last_name = "Jung"
|
|
759
|
+
user3.save()
|
|
760
|
+
usrid3 = user3.id
|
|
761
|
+
|
|
762
|
+
user4 = User.objects.get(email=email4)
|
|
763
|
+
user4.last_name = "Kook"
|
|
764
|
+
user4.save()
|
|
765
|
+
usrid4 = user4.id
|
|
766
|
+
|
|
767
|
+
school = create_organisation_directly(email1)
|
|
768
|
+
klass, class_name, access_code_1 = create_class_directly(email1)
|
|
769
|
+
class_id = klass.id
|
|
770
|
+
_, _, student = create_school_student_directly(access_code_1)
|
|
771
|
+
student_user_id = student.new_user.id
|
|
772
|
+
|
|
773
|
+
join_teacher_to_organisation(email2, school.name)
|
|
774
|
+
_, _, access_code_2 = create_class_directly(email2)
|
|
775
|
+
create_school_student_directly(access_code_2)
|
|
776
|
+
|
|
777
|
+
join_teacher_to_organisation(email3, school.name)
|
|
778
|
+
join_teacher_to_organisation(email4, school.name)
|
|
779
|
+
|
|
780
|
+
c = Client()
|
|
781
|
+
url = reverse("teacher_login")
|
|
782
|
+
c.post(
|
|
783
|
+
url,
|
|
784
|
+
{
|
|
785
|
+
"auth-username": email1,
|
|
786
|
+
"auth-password": password1,
|
|
787
|
+
"teacher_login_view-current_step": "auth",
|
|
788
|
+
},
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
# delete teacher1 account
|
|
792
|
+
url = reverse("delete_account")
|
|
793
|
+
c.post(url, {"password": password1})
|
|
794
|
+
mock_send_dotdigital_email.assert_called_once()
|
|
795
|
+
|
|
796
|
+
# user has been anonymised
|
|
797
|
+
u = User.objects.get(id=usrid1)
|
|
798
|
+
assert not u.is_active
|
|
799
|
+
|
|
800
|
+
# check that the class and student have been anonymised
|
|
801
|
+
assert not Class._base_manager.get(pk=class_id).is_active
|
|
802
|
+
student_user1 = User.objects.get(id=student_user_id)
|
|
803
|
+
assert not student_user1.is_active
|
|
804
|
+
|
|
805
|
+
school_id = school.id
|
|
806
|
+
school_name = school.name
|
|
807
|
+
teachers = Teacher.objects.filter(school=school).order_by(
|
|
808
|
+
"new_user__last_name", "new_user__first_name"
|
|
809
|
+
)
|
|
810
|
+
assert len(teachers) == 3
|
|
811
|
+
|
|
812
|
+
# one of the remaining teachers should be admin (the second in our case, as it's alphabetical)
|
|
813
|
+
u = User.objects.get(id=usrid2)
|
|
814
|
+
assert u.new_teacher.is_admin
|
|
815
|
+
u = User.objects.get(id=usrid3)
|
|
816
|
+
assert not u.new_teacher.is_admin
|
|
817
|
+
u = User.objects.get(id=usrid4)
|
|
818
|
+
assert not u.new_teacher.is_admin
|
|
819
|
+
|
|
820
|
+
# make teacher 3 admin
|
|
821
|
+
user3.new_teacher.is_admin = True
|
|
822
|
+
user3.new_teacher.save()
|
|
823
|
+
|
|
824
|
+
url = reverse("teacher_login")
|
|
825
|
+
c.post(
|
|
826
|
+
url,
|
|
827
|
+
{
|
|
828
|
+
"auth-username": email3,
|
|
829
|
+
"auth-password": password3,
|
|
830
|
+
"teacher_login_view-current_step": "auth",
|
|
831
|
+
},
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
# now delete teacher3 account
|
|
835
|
+
url = reverse("delete_account")
|
|
836
|
+
c.post(url, {"password": password3})
|
|
837
|
+
self.assertEqual(mock_send_dotdigital_email.call_count, 2)
|
|
838
|
+
|
|
839
|
+
# 2 teachers left
|
|
840
|
+
teachers = Teacher.objects.filter(school=school).order_by(
|
|
841
|
+
"new_user__last_name", "new_user__first_name"
|
|
842
|
+
)
|
|
843
|
+
assert len(teachers) == 2
|
|
844
|
+
|
|
845
|
+
# teacher2 should still be admin, teacher4 is not passed admin role because there is teacher2
|
|
846
|
+
u = User.objects.get(id=usrid2)
|
|
847
|
+
assert u.new_teacher.is_admin
|
|
848
|
+
u = User.objects.get(id=usrid4)
|
|
849
|
+
assert not u.new_teacher.is_admin
|
|
850
|
+
|
|
851
|
+
# delete teacher4
|
|
852
|
+
anonymise(user4)
|
|
853
|
+
|
|
854
|
+
teachers = Teacher.objects.filter(school=school).order_by(
|
|
855
|
+
"new_user__last_name", "new_user__first_name"
|
|
856
|
+
)
|
|
857
|
+
assert len(teachers) == 1
|
|
858
|
+
u = User.objects.get(id=usrid2)
|
|
859
|
+
assert u.new_teacher.is_admin
|
|
860
|
+
|
|
861
|
+
# delete teacher2 (the last one left)
|
|
862
|
+
url = reverse("teacher_login")
|
|
863
|
+
c.post(
|
|
864
|
+
url,
|
|
865
|
+
{
|
|
866
|
+
"auth-username": email2,
|
|
867
|
+
"auth-password": password2,
|
|
868
|
+
"teacher_login_view-current_step": "auth",
|
|
869
|
+
},
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
url = reverse("delete_account")
|
|
873
|
+
c.post(url, {"password": password2})
|
|
874
|
+
self.assertEqual(mock_send_dotdigital_email.call_count, 3)
|
|
875
|
+
|
|
876
|
+
# school should be anonymised
|
|
877
|
+
school = School._base_manager.get(id=school_id)
|
|
878
|
+
assert school.name != school_name
|
|
879
|
+
assert not school.is_active
|
|
880
|
+
|
|
881
|
+
with pytest.raises(School.DoesNotExist):
|
|
882
|
+
School.objects.get(id=school_id)
|
|
883
|
+
|
|
884
|
+
def test_legal_pages_load(self):
|
|
885
|
+
c = Client()
|
|
886
|
+
|
|
887
|
+
assert c.get(reverse("privacy_notice")).status_code == 200
|
|
888
|
+
assert c.get(reverse("terms")).status_code == 200
|
|
889
|
+
|
|
890
|
+
def test_logged_in_as_admin_check(self):
|
|
891
|
+
email1, password1 = signup_teacher_directly()
|
|
892
|
+
email2, password2 = signup_teacher_directly()
|
|
893
|
+
school = create_organisation_directly(email1)
|
|
894
|
+
join_teacher_to_organisation(email2, school.name)
|
|
895
|
+
|
|
896
|
+
teacher1 = Teacher.objects.get(new_user__username=email1)
|
|
897
|
+
teacher2 = Teacher.objects.get(new_user__username=email2)
|
|
898
|
+
|
|
899
|
+
c = Client()
|
|
900
|
+
|
|
901
|
+
c.login(username=email1, password=password1)
|
|
902
|
+
|
|
903
|
+
assert is_logged_in_as_admin_teacher(teacher1.new_user)
|
|
904
|
+
|
|
905
|
+
c.logout()
|
|
906
|
+
|
|
907
|
+
c.login(username=email2, password=password2)
|
|
908
|
+
|
|
909
|
+
assert not is_logged_in_as_admin_teacher(teacher2.new_user)
|
|
910
|
+
|
|
911
|
+
c.logout()
|
|
912
|
+
|
|
913
|
+
@patch("common.helpers.emails.send_dotdigital_email")
|
|
914
|
+
def test_registrations_increment_data(
|
|
915
|
+
self, mock_send_dotdigital_email: Mock
|
|
916
|
+
):
|
|
917
|
+
c = Client()
|
|
918
|
+
|
|
919
|
+
total_activity = TotalActivity.objects.get(id=1)
|
|
920
|
+
teacher_registration_count = total_activity.teacher_registrations
|
|
921
|
+
student_registration_count = total_activity.student_registrations
|
|
922
|
+
independent_registration_count = (
|
|
923
|
+
total_activity.independent_registrations
|
|
924
|
+
)
|
|
925
|
+
|
|
926
|
+
response = c.post(
|
|
927
|
+
reverse("register"),
|
|
928
|
+
{
|
|
929
|
+
"teacher_signup-teacher_first_name": "Test Name",
|
|
930
|
+
"teacher_signup-teacher_last_name": "Test Last Name",
|
|
931
|
+
"teacher_signup-teacher_email": "test@email.com",
|
|
932
|
+
"teacher_signup-consent_ticked": "on",
|
|
933
|
+
"teacher_signup-teacher_password": "$RFVBGT%6yhn$RFVBGT%6yhn$RFVBGT%6yhn$RFVBGT%6yhn",
|
|
934
|
+
"teacher_signup-teacher_confirm_password": "$RFVBGT%6yhn$RFVBGT%6yhn$RFVBGT%6yhn$RFVBGT%6yhn",
|
|
935
|
+
"g-recaptcha-response": "something",
|
|
936
|
+
},
|
|
937
|
+
)
|
|
938
|
+
|
|
939
|
+
assert response.status_code == 302
|
|
940
|
+
mock_send_dotdigital_email.assert_called_once()
|
|
941
|
+
|
|
942
|
+
total_activity = TotalActivity.objects.get(id=1)
|
|
943
|
+
|
|
944
|
+
assert (
|
|
945
|
+
total_activity.teacher_registrations
|
|
946
|
+
== teacher_registration_count + 1
|
|
947
|
+
)
|
|
948
|
+
|
|
949
|
+
response = c.post(
|
|
950
|
+
reverse("register"),
|
|
951
|
+
{
|
|
952
|
+
"independent_student_signup-date_of_birth_day": 7,
|
|
953
|
+
"independent_student_signup-date_of_birth_month": 10,
|
|
954
|
+
"independent_student_signup-date_of_birth_year": 1997,
|
|
955
|
+
"independent_student_signup-name": "Test Name",
|
|
956
|
+
"independent_student_signup-email": "test@indy-email.com",
|
|
957
|
+
"independent_student_signup-consent_ticked": "on",
|
|
958
|
+
"independent_student_signup-password": "$RFVBGT%6yhn$RFVBGT%6yhn$RFVBGT%6yhn$RFVBGT%6yhn",
|
|
959
|
+
"independent_student_signup-confirm_password": "$RFVBGT%6yhn$RFVBGT%6yhn$RFVBGT%6yhn$RFVBGT%6yhn",
|
|
960
|
+
"g-recaptcha-response": "something",
|
|
961
|
+
},
|
|
962
|
+
)
|
|
963
|
+
|
|
964
|
+
assert response.status_code == 302
|
|
965
|
+
mock_send_dotdigital_email.assert_called()
|
|
966
|
+
|
|
967
|
+
total_activity = TotalActivity.objects.get(id=1)
|
|
968
|
+
|
|
969
|
+
assert (
|
|
970
|
+
total_activity.independent_registrations
|
|
971
|
+
== independent_registration_count + 1
|
|
972
|
+
)
|
|
973
|
+
|
|
974
|
+
teacher_email, teacher_password = signup_teacher_directly()
|
|
975
|
+
create_organisation_directly(teacher_email)
|
|
976
|
+
_, _, access_code = create_class_directly(teacher_email)
|
|
977
|
+
|
|
978
|
+
c.login(username=teacher_email, password=teacher_password)
|
|
979
|
+
c.post(
|
|
980
|
+
reverse("view_class", kwargs={"access_code": access_code}),
|
|
981
|
+
{"names": "Student 1, Student 2, Student 3"},
|
|
982
|
+
)
|
|
983
|
+
|
|
984
|
+
assert response.status_code == 302
|
|
985
|
+
|
|
986
|
+
total_activity = TotalActivity.objects.get(id=1)
|
|
987
|
+
|
|
988
|
+
assert (
|
|
989
|
+
total_activity.student_registrations
|
|
990
|
+
== student_registration_count + 3
|
|
991
|
+
)
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
# CRON view tests
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
class CronTestClient(APIClient):
|
|
998
|
+
def __init__(self, *args, **kwargs):
|
|
999
|
+
super().__init__(*args, **kwargs, HTTP_X_APPENGINE_CRON="true")
|
|
1000
|
+
|
|
1001
|
+
def generic(
|
|
1002
|
+
self,
|
|
1003
|
+
method,
|
|
1004
|
+
path,
|
|
1005
|
+
data="",
|
|
1006
|
+
content_type="application/octet-stream",
|
|
1007
|
+
secure=False,
|
|
1008
|
+
**extra,
|
|
1009
|
+
):
|
|
1010
|
+
wsgi_response = super().generic(
|
|
1011
|
+
method, path, data, content_type, secure, **extra
|
|
1012
|
+
)
|
|
1013
|
+
assert (
|
|
1014
|
+
200 <= wsgi_response.status_code < 300
|
|
1015
|
+
), f"Response has error status code: {wsgi_response.status_code}"
|
|
1016
|
+
|
|
1017
|
+
return wsgi_response
|
|
1018
|
+
|
|
1019
|
+
|
|
1020
|
+
class CronTestCase(APITestCase):
|
|
1021
|
+
client_class = CronTestClient
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
class TestUser(CronTestCase):
|
|
1025
|
+
# TODO: use fixtures
|
|
1026
|
+
def setUp(self):
|
|
1027
|
+
teacher_email, _ = signup_teacher_directly(preverified=False)
|
|
1028
|
+
create_organisation_directly(teacher_email)
|
|
1029
|
+
_, _, access_code = create_class_directly(teacher_email)
|
|
1030
|
+
_, _, student = create_school_student_directly(access_code)
|
|
1031
|
+
indy_email, _, _ = create_independent_student_directly()
|
|
1032
|
+
|
|
1033
|
+
self.teacher_user = User.objects.get(email=teacher_email)
|
|
1034
|
+
self.teacher_user_profile = UserProfile.objects.get(
|
|
1035
|
+
user=self.teacher_user
|
|
1036
|
+
)
|
|
1037
|
+
|
|
1038
|
+
self.indy_user = User.objects.get(email=indy_email)
|
|
1039
|
+
self.indy_user_profile = UserProfile.objects.get(user=self.indy_user)
|
|
1040
|
+
|
|
1041
|
+
self.student_user: User = student.new_user
|
|
1042
|
+
|
|
1043
|
+
@patch("portal.views.cron.user.send_dotdigital_email")
|
|
1044
|
+
def send_verify_email_reminder(
|
|
1045
|
+
self,
|
|
1046
|
+
days: int,
|
|
1047
|
+
is_verified: bool,
|
|
1048
|
+
view_name: str,
|
|
1049
|
+
assert_called: bool,
|
|
1050
|
+
mock_send_dotdigital_email: Mock,
|
|
1051
|
+
):
|
|
1052
|
+
self.teacher_user.date_joined = timezone.now() - timedelta(
|
|
1053
|
+
days=days, hours=12
|
|
1054
|
+
)
|
|
1055
|
+
self.teacher_user.save()
|
|
1056
|
+
self.student_user.date_joined = timezone.now() - timedelta(
|
|
1057
|
+
days=days, hours=12
|
|
1058
|
+
)
|
|
1059
|
+
self.student_user.save()
|
|
1060
|
+
self.indy_user.date_joined = timezone.now() - timedelta(
|
|
1061
|
+
days=days, hours=12
|
|
1062
|
+
)
|
|
1063
|
+
self.indy_user.save()
|
|
1064
|
+
|
|
1065
|
+
self.teacher_user_profile.is_verified = is_verified
|
|
1066
|
+
self.teacher_user_profile.save()
|
|
1067
|
+
self.indy_user_profile.is_verified = is_verified
|
|
1068
|
+
self.indy_user_profile.save()
|
|
1069
|
+
|
|
1070
|
+
self.client.get(reverse(view_name))
|
|
1071
|
+
|
|
1072
|
+
if assert_called:
|
|
1073
|
+
mock_send_dotdigital_email.assert_any_call(
|
|
1074
|
+
ANY, [self.teacher_user.email], personalization_values=ANY
|
|
1075
|
+
)
|
|
1076
|
+
|
|
1077
|
+
mock_send_dotdigital_email.assert_any_call(
|
|
1078
|
+
ANY, [self.indy_user.email], personalization_values=ANY
|
|
1079
|
+
)
|
|
1080
|
+
|
|
1081
|
+
# Check only two emails are sent - the student should never be included.
|
|
1082
|
+
assert mock_send_dotdigital_email.call_count == 2
|
|
1083
|
+
else:
|
|
1084
|
+
mock_send_dotdigital_email.assert_not_called()
|
|
1085
|
+
|
|
1086
|
+
mock_send_dotdigital_email.reset_mock()
|
|
1087
|
+
|
|
1088
|
+
def test_first_verify_email_reminder_view(self):
|
|
1089
|
+
self.send_verify_email_reminder(
|
|
1090
|
+
6, False, "first-verify-email-reminder", False
|
|
1091
|
+
)
|
|
1092
|
+
self.send_verify_email_reminder(
|
|
1093
|
+
7, False, "first-verify-email-reminder", True
|
|
1094
|
+
)
|
|
1095
|
+
self.send_verify_email_reminder(
|
|
1096
|
+
7, True, "first-verify-email-reminder", False
|
|
1097
|
+
)
|
|
1098
|
+
self.send_verify_email_reminder(
|
|
1099
|
+
8, False, "first-verify-email-reminder", False
|
|
1100
|
+
)
|
|
1101
|
+
|
|
1102
|
+
def test_second_verify_email_reminder_view(self):
|
|
1103
|
+
self.send_verify_email_reminder(
|
|
1104
|
+
13, False, "second-verify-email-reminder", False
|
|
1105
|
+
)
|
|
1106
|
+
self.send_verify_email_reminder(
|
|
1107
|
+
14, False, "second-verify-email-reminder", True
|
|
1108
|
+
)
|
|
1109
|
+
self.send_verify_email_reminder(
|
|
1110
|
+
14, True, "second-verify-email-reminder", False
|
|
1111
|
+
)
|
|
1112
|
+
self.send_verify_email_reminder(
|
|
1113
|
+
15, False, "second-verify-email-reminder", False
|
|
1114
|
+
)
|
|
1115
|
+
|
|
1116
|
+
def test_anonymise_unverified_accounts_view(self):
|
|
1117
|
+
now = timezone.now()
|
|
1118
|
+
|
|
1119
|
+
for user in [self.teacher_user, self.indy_user, self.student_user]:
|
|
1120
|
+
user.date_joined = now - timedelta(
|
|
1121
|
+
days=USER_DELETE_UNVERIFIED_ACCOUNT_DAYS + 1
|
|
1122
|
+
)
|
|
1123
|
+
user.save()
|
|
1124
|
+
|
|
1125
|
+
for user_profile in [self.teacher_user_profile, self.indy_user_profile]:
|
|
1126
|
+
user_profile.is_verified = True
|
|
1127
|
+
user_profile.save()
|
|
1128
|
+
|
|
1129
|
+
def anonymise_unverified_users(
|
|
1130
|
+
days: int,
|
|
1131
|
+
is_verified: bool,
|
|
1132
|
+
assert_active: bool,
|
|
1133
|
+
):
|
|
1134
|
+
date_joined = now - timedelta(days=days, hours=12)
|
|
1135
|
+
|
|
1136
|
+
# Create teacher.
|
|
1137
|
+
teacher_user = User.objects.create(
|
|
1138
|
+
first_name="Unverified",
|
|
1139
|
+
last_name="Teacher",
|
|
1140
|
+
username="unverified.teacher@codeforlife.com",
|
|
1141
|
+
email="unverified.teacher@codeforlife.com",
|
|
1142
|
+
date_joined=date_joined,
|
|
1143
|
+
)
|
|
1144
|
+
teacher_user_profile = UserProfile.objects.create(
|
|
1145
|
+
user=teacher_user,
|
|
1146
|
+
is_verified=is_verified,
|
|
1147
|
+
)
|
|
1148
|
+
Teacher.objects.create(
|
|
1149
|
+
user=teacher_user_profile,
|
|
1150
|
+
new_user=teacher_user,
|
|
1151
|
+
school=self.teacher_user.new_teacher.school,
|
|
1152
|
+
)
|
|
1153
|
+
|
|
1154
|
+
# Create dependent student.
|
|
1155
|
+
student_user = User.objects.create(
|
|
1156
|
+
first_name="Unverified",
|
|
1157
|
+
last_name="DependentStudent",
|
|
1158
|
+
username="UnverifiedDependentStudent",
|
|
1159
|
+
date_joined=date_joined,
|
|
1160
|
+
)
|
|
1161
|
+
student_user_profile = UserProfile.objects.create(
|
|
1162
|
+
user=student_user,
|
|
1163
|
+
)
|
|
1164
|
+
Student.objects.create(
|
|
1165
|
+
user=student_user_profile,
|
|
1166
|
+
new_user=student_user,
|
|
1167
|
+
class_field=self.student_user.new_student.class_field,
|
|
1168
|
+
)
|
|
1169
|
+
|
|
1170
|
+
# Create independent student.
|
|
1171
|
+
indy_user = User.objects.create(
|
|
1172
|
+
first_name="Unverified",
|
|
1173
|
+
last_name="IndependentStudent",
|
|
1174
|
+
username="unverified.independentstudent@codeforlife.com",
|
|
1175
|
+
email="unverified.independentstudent@codeforlife.com",
|
|
1176
|
+
date_joined=date_joined,
|
|
1177
|
+
)
|
|
1178
|
+
indy_user_profile = UserProfile.objects.create(
|
|
1179
|
+
user=indy_user,
|
|
1180
|
+
is_verified=is_verified,
|
|
1181
|
+
)
|
|
1182
|
+
Student.objects.create(
|
|
1183
|
+
user=indy_user_profile,
|
|
1184
|
+
new_user=indy_user,
|
|
1185
|
+
)
|
|
1186
|
+
|
|
1187
|
+
activity_today = DailyActivity.objects.get_or_create(
|
|
1188
|
+
date=datetime.now().date()
|
|
1189
|
+
)[0]
|
|
1190
|
+
daily_teacher_count = activity_today.anonymised_unverified_teachers
|
|
1191
|
+
daily_indy_count = activity_today.anonymised_unverified_independents
|
|
1192
|
+
|
|
1193
|
+
total_activity = TotalActivity.objects.get(id=1)
|
|
1194
|
+
total_teacher_count = total_activity.anonymised_unverified_teachers
|
|
1195
|
+
total_indy_count = total_activity.anonymised_unverified_independents
|
|
1196
|
+
|
|
1197
|
+
self.client.get(reverse("anonymise-unverified-accounts"))
|
|
1198
|
+
|
|
1199
|
+
# Assert the verified users exist
|
|
1200
|
+
assert User.objects.get(id=self.teacher_user.id).is_active
|
|
1201
|
+
assert User.objects.get(id=self.student_user.id).is_active
|
|
1202
|
+
assert User.objects.get(id=self.indy_user.id).is_active
|
|
1203
|
+
|
|
1204
|
+
teacher_user_active = User.objects.get(id=teacher_user.id).is_active
|
|
1205
|
+
indy_user_active = User.objects.get(id=indy_user.id).is_active
|
|
1206
|
+
student_user_active = User.objects.get(id=student_user.id).is_active
|
|
1207
|
+
|
|
1208
|
+
assert teacher_user_active == assert_active
|
|
1209
|
+
assert indy_user_active == assert_active
|
|
1210
|
+
assert student_user_active
|
|
1211
|
+
|
|
1212
|
+
activity_today = DailyActivity.objects.get_or_create(
|
|
1213
|
+
date=datetime.now().date()
|
|
1214
|
+
)[0]
|
|
1215
|
+
total_activity = TotalActivity.objects.get(id=1)
|
|
1216
|
+
|
|
1217
|
+
if not teacher_user_active:
|
|
1218
|
+
assert (
|
|
1219
|
+
activity_today.anonymised_unverified_teachers
|
|
1220
|
+
== daily_teacher_count + 1
|
|
1221
|
+
)
|
|
1222
|
+
assert (
|
|
1223
|
+
total_activity.anonymised_unverified_teachers
|
|
1224
|
+
== total_teacher_count + 1
|
|
1225
|
+
)
|
|
1226
|
+
|
|
1227
|
+
if not indy_user_active:
|
|
1228
|
+
assert (
|
|
1229
|
+
activity_today.anonymised_unverified_independents
|
|
1230
|
+
== daily_indy_count + 1
|
|
1231
|
+
)
|
|
1232
|
+
assert (
|
|
1233
|
+
total_activity.anonymised_unverified_independents
|
|
1234
|
+
== total_indy_count + 1
|
|
1235
|
+
)
|
|
1236
|
+
|
|
1237
|
+
teacher_user.delete()
|
|
1238
|
+
indy_user.delete()
|
|
1239
|
+
student_user.delete()
|
|
1240
|
+
|
|
1241
|
+
anonymise_unverified_users(
|
|
1242
|
+
days=USER_DELETE_UNVERIFIED_ACCOUNT_DAYS - 1,
|
|
1243
|
+
is_verified=False,
|
|
1244
|
+
assert_active=True,
|
|
1245
|
+
)
|
|
1246
|
+
anonymise_unverified_users(
|
|
1247
|
+
days=USER_DELETE_UNVERIFIED_ACCOUNT_DAYS,
|
|
1248
|
+
is_verified=False,
|
|
1249
|
+
assert_active=False,
|
|
1250
|
+
)
|
|
1251
|
+
anonymise_unverified_users(
|
|
1252
|
+
days=USER_DELETE_UNVERIFIED_ACCOUNT_DAYS,
|
|
1253
|
+
is_verified=True,
|
|
1254
|
+
assert_active=True,
|
|
1255
|
+
)
|
|
1256
|
+
anonymise_unverified_users(
|
|
1257
|
+
days=USER_DELETE_UNVERIFIED_ACCOUNT_DAYS + 1,
|
|
1258
|
+
is_verified=False,
|
|
1259
|
+
assert_active=False,
|
|
1260
|
+
)
|
|
1261
|
+
|
|
1262
|
+
@patch("portal.views.cron.user.send_dotdigital_email")
|
|
1263
|
+
def send_inactivity_reminder(
|
|
1264
|
+
self,
|
|
1265
|
+
days: int,
|
|
1266
|
+
view_name: str,
|
|
1267
|
+
assert_called: bool,
|
|
1268
|
+
campaign_name: str,
|
|
1269
|
+
mock_send_dotdigital_email: Mock,
|
|
1270
|
+
personalization_values=None,
|
|
1271
|
+
):
|
|
1272
|
+
self.teacher_user.date_joined = timezone.now() - timedelta(
|
|
1273
|
+
days=days, hours=12
|
|
1274
|
+
)
|
|
1275
|
+
self.teacher_user.save()
|
|
1276
|
+
self.student_user.date_joined = timezone.now() - timedelta(
|
|
1277
|
+
days=days, hours=12
|
|
1278
|
+
)
|
|
1279
|
+
self.student_user.save()
|
|
1280
|
+
self.indy_user.last_login = timezone.now() - timedelta(
|
|
1281
|
+
days=days, hours=12
|
|
1282
|
+
)
|
|
1283
|
+
self.indy_user.save()
|
|
1284
|
+
|
|
1285
|
+
self.client.get(reverse(view_name))
|
|
1286
|
+
|
|
1287
|
+
if assert_called:
|
|
1288
|
+
if personalization_values is not None:
|
|
1289
|
+
mock_send_dotdigital_email.assert_any_call(
|
|
1290
|
+
campaign_ids[campaign_name],
|
|
1291
|
+
[self.teacher_user.email],
|
|
1292
|
+
personalization_values=personalization_values,
|
|
1293
|
+
)
|
|
1294
|
+
|
|
1295
|
+
mock_send_dotdigital_email.assert_any_call(
|
|
1296
|
+
campaign_ids[campaign_name],
|
|
1297
|
+
[self.indy_user.email],
|
|
1298
|
+
personalization_values=personalization_values,
|
|
1299
|
+
)
|
|
1300
|
+
else:
|
|
1301
|
+
mock_send_dotdigital_email.assert_any_call(
|
|
1302
|
+
campaign_ids[campaign_name],
|
|
1303
|
+
[self.teacher_user.email],
|
|
1304
|
+
)
|
|
1305
|
+
|
|
1306
|
+
mock_send_dotdigital_email.assert_any_call(
|
|
1307
|
+
campaign_ids[campaign_name],
|
|
1308
|
+
[self.indy_user.email],
|
|
1309
|
+
)
|
|
1310
|
+
|
|
1311
|
+
# Check only two emails are sent - the student should never be included.
|
|
1312
|
+
assert mock_send_dotdigital_email.call_count == 2
|
|
1313
|
+
else:
|
|
1314
|
+
mock_send_dotdigital_email.assert_not_called()
|
|
1315
|
+
|
|
1316
|
+
mock_send_dotdigital_email.reset_mock()
|
|
1317
|
+
|
|
1318
|
+
def test_first_inactivity_reminder_view(self):
|
|
1319
|
+
self.send_inactivity_reminder(
|
|
1320
|
+
729,
|
|
1321
|
+
"first-inactivity-reminder",
|
|
1322
|
+
False,
|
|
1323
|
+
"inactive_users_on_website_first_reminder",
|
|
1324
|
+
)
|
|
1325
|
+
self.send_inactivity_reminder(
|
|
1326
|
+
730,
|
|
1327
|
+
"first-inactivity-reminder",
|
|
1328
|
+
True,
|
|
1329
|
+
"inactive_users_on_website_first_reminder",
|
|
1330
|
+
)
|
|
1331
|
+
self.send_inactivity_reminder(
|
|
1332
|
+
731,
|
|
1333
|
+
"first-inactivity-reminder",
|
|
1334
|
+
False,
|
|
1335
|
+
"inactive_users_on_website_first_reminder",
|
|
1336
|
+
)
|
|
1337
|
+
|
|
1338
|
+
def test_second_inactivity_reminder_view(self):
|
|
1339
|
+
self.send_inactivity_reminder(
|
|
1340
|
+
972,
|
|
1341
|
+
"second-inactivity-reminder",
|
|
1342
|
+
False,
|
|
1343
|
+
"inactive_users_on_website_second_reminder",
|
|
1344
|
+
)
|
|
1345
|
+
self.send_inactivity_reminder(
|
|
1346
|
+
973,
|
|
1347
|
+
"second-inactivity-reminder",
|
|
1348
|
+
True,
|
|
1349
|
+
"inactive_users_on_website_second_reminder",
|
|
1350
|
+
personalization_values={
|
|
1351
|
+
"DAYS_LEFT": USER_RETENTION_PERIOD
|
|
1352
|
+
- USER_2ND_INACTIVE_REMINDER_DAYS
|
|
1353
|
+
},
|
|
1354
|
+
)
|
|
1355
|
+
self.send_inactivity_reminder(
|
|
1356
|
+
974,
|
|
1357
|
+
"second-inactivity-reminder",
|
|
1358
|
+
False,
|
|
1359
|
+
"inactive_users_on_website_second_reminder",
|
|
1360
|
+
)
|
|
1361
|
+
|
|
1362
|
+
def test_final_inactivity_reminder_view(self):
|
|
1363
|
+
self.send_inactivity_reminder(
|
|
1364
|
+
1064,
|
|
1365
|
+
"final-inactivity-reminder",
|
|
1366
|
+
False,
|
|
1367
|
+
"inactive_users_on_website_final_reminder",
|
|
1368
|
+
)
|
|
1369
|
+
self.send_inactivity_reminder(
|
|
1370
|
+
1065,
|
|
1371
|
+
"final-inactivity-reminder",
|
|
1372
|
+
True,
|
|
1373
|
+
"inactive_users_on_website_final_reminder",
|
|
1374
|
+
personalization_values={
|
|
1375
|
+
"DAYS_LEFT": USER_RETENTION_PERIOD
|
|
1376
|
+
- USER_FINAL_INACTIVE_REMINDER_DAYS
|
|
1377
|
+
},
|
|
1378
|
+
)
|
|
1379
|
+
self.send_inactivity_reminder(
|
|
1380
|
+
1066,
|
|
1381
|
+
"final-inactivity-reminder",
|
|
1382
|
+
False,
|
|
1383
|
+
"inactive_users_on_website_final_reminder",
|
|
1384
|
+
)
|