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
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import typing as t
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
|
|
6
|
+
from cryptography.fernet import Fernet
|
|
7
|
+
from django.conf import settings
|
|
8
|
+
from django.contrib.auth.models import User
|
|
9
|
+
from django.db import models
|
|
10
|
+
from django.utils import timezone
|
|
11
|
+
from django_countries.fields import CountryField
|
|
12
|
+
|
|
13
|
+
if t.TYPE_CHECKING:
|
|
14
|
+
from django.db.models import ManyToManyField
|
|
15
|
+
from game.models import Worksheet
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class EncryptedCharField(models.CharField):
|
|
19
|
+
"""
|
|
20
|
+
A custom CharField that encrypts data before saving and decrypts it when
|
|
21
|
+
retrieved.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
_fernet = Fernet(settings.ENCRYPTION_KEY)
|
|
25
|
+
_prefix = "ENC:"
|
|
26
|
+
|
|
27
|
+
# pylint: disable-next=unused-argument
|
|
28
|
+
def from_db_value(self, value: t.Optional[str], expression, connection):
|
|
29
|
+
"""
|
|
30
|
+
Converts a value as returned by the database to a Python object. It is
|
|
31
|
+
the reverse of get_prep_value().
|
|
32
|
+
|
|
33
|
+
https://docs.djangoproject.com/en/5.1/howto/custom-model-fields/#converting-values-to-python-objects
|
|
34
|
+
"""
|
|
35
|
+
if isinstance(value, str):
|
|
36
|
+
return self.decrypt_value(value)
|
|
37
|
+
return value
|
|
38
|
+
|
|
39
|
+
def to_python(self, value: t.Optional[str]):
|
|
40
|
+
"""
|
|
41
|
+
Converts the value into the correct Python object. It acts as the
|
|
42
|
+
reverse of value_to_string(), and is also called in clean().
|
|
43
|
+
|
|
44
|
+
https://docs.djangoproject.com/en/5.1/howto/custom-model-fields/#converting-values-to-python-objects
|
|
45
|
+
"""
|
|
46
|
+
if isinstance(value, str):
|
|
47
|
+
return self.decrypt_value(value)
|
|
48
|
+
return value
|
|
49
|
+
|
|
50
|
+
def get_prep_value(self, value: t.Optional[str]):
|
|
51
|
+
"""
|
|
52
|
+
'value' is the current value of the model's attribute, and the method
|
|
53
|
+
should return data in a format that has been prepared for use as a
|
|
54
|
+
parameter in a query.
|
|
55
|
+
|
|
56
|
+
https://docs.djangoproject.com/en/5.1/howto/custom-model-fields/#converting-python-objects-to-query-values
|
|
57
|
+
"""
|
|
58
|
+
if isinstance(value, str):
|
|
59
|
+
return self.encrypt_value(value)
|
|
60
|
+
return value
|
|
61
|
+
|
|
62
|
+
def encrypt_value(self, value: str):
|
|
63
|
+
"""Encrypt the value if it's not encrypted."""
|
|
64
|
+
if not value.startswith(self._prefix):
|
|
65
|
+
return self._prefix + self._fernet.encrypt(value.encode()).decode()
|
|
66
|
+
return value
|
|
67
|
+
|
|
68
|
+
def decrypt_value(self, value: str):
|
|
69
|
+
"""Decrpyt the value if it's encrypted.."""
|
|
70
|
+
if value.startswith(self._prefix):
|
|
71
|
+
value = value[len(self._prefix) :]
|
|
72
|
+
return self._fernet.decrypt(value).decode()
|
|
73
|
+
return value
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class UserProfile(models.Model):
|
|
77
|
+
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
|
78
|
+
|
|
79
|
+
otp_secret = models.CharField(max_length=40, null=True, blank=True)
|
|
80
|
+
last_otp_for_time = models.DateTimeField(null=True, blank=True)
|
|
81
|
+
developer = models.BooleanField(default=False)
|
|
82
|
+
is_verified = models.BooleanField(default=False)
|
|
83
|
+
|
|
84
|
+
# TODO: Make not nullable once data has been transferred
|
|
85
|
+
first_name = models.CharField(max_length=200, null=True, blank=True)
|
|
86
|
+
_first_name = models.BinaryField(null=True, blank=True)
|
|
87
|
+
last_name = models.CharField(max_length=200, null=True, blank=True)
|
|
88
|
+
_last_name = models.BinaryField(null=True, blank=True)
|
|
89
|
+
email = models.CharField(max_length=200, null=True, blank=True)
|
|
90
|
+
_email = models.BinaryField(null=True, blank=True)
|
|
91
|
+
# TODO: Make not nullable once data has been transferred
|
|
92
|
+
username = models.CharField(max_length=200, null=True, blank=True)
|
|
93
|
+
_username = models.BinaryField(null=True, blank=True)
|
|
94
|
+
|
|
95
|
+
# Google.
|
|
96
|
+
google_refresh_token = EncryptedCharField(
|
|
97
|
+
max_length=1000 + len(EncryptedCharField._prefix), null=True, blank=True
|
|
98
|
+
)
|
|
99
|
+
google_sub = models.CharField(max_length=255, null=True, blank=True)
|
|
100
|
+
|
|
101
|
+
def __str__(self):
|
|
102
|
+
return f"{self.user.first_name} {self.user.last_name}"
|
|
103
|
+
|
|
104
|
+
def joined_recently(self):
|
|
105
|
+
now = timezone.now()
|
|
106
|
+
return now - timedelta(days=7) <= self.user.date_joined
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class SchoolModelManager(models.Manager):
|
|
110
|
+
def get_original_queryset(self):
|
|
111
|
+
return super().get_queryset()
|
|
112
|
+
|
|
113
|
+
# Filter out inactive schools by default
|
|
114
|
+
def get_queryset(self):
|
|
115
|
+
return super().get_queryset().filter(is_active=True)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class School(models.Model):
|
|
119
|
+
name = models.CharField(max_length=200, unique=True)
|
|
120
|
+
country = CountryField(
|
|
121
|
+
blank_label="(select country)", null=True, blank=True
|
|
122
|
+
)
|
|
123
|
+
# TODO: Create an Address model to house address details
|
|
124
|
+
county = models.CharField(max_length=50, blank=True, null=True)
|
|
125
|
+
creation_time = models.DateTimeField(default=timezone.now, null=True)
|
|
126
|
+
is_active = models.BooleanField(default=True)
|
|
127
|
+
|
|
128
|
+
objects = SchoolModelManager()
|
|
129
|
+
|
|
130
|
+
def __str__(self):
|
|
131
|
+
return self.name
|
|
132
|
+
|
|
133
|
+
def classes(self):
|
|
134
|
+
teachers = self.teacher_school.all()
|
|
135
|
+
if teachers:
|
|
136
|
+
classes = []
|
|
137
|
+
for teacher in teachers:
|
|
138
|
+
if teacher.class_teacher.all():
|
|
139
|
+
classes.extend(list(teacher.class_teacher.all()))
|
|
140
|
+
return classes
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
def admins(self):
|
|
144
|
+
teachers = self.teacher_school.all()
|
|
145
|
+
return (
|
|
146
|
+
[teacher for teacher in teachers if teacher.is_admin]
|
|
147
|
+
if teachers
|
|
148
|
+
else None
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def anonymise(self):
|
|
152
|
+
self.name = uuid4().hex
|
|
153
|
+
self.is_active = False
|
|
154
|
+
self.save()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class TeacherModelManager(models.Manager):
|
|
158
|
+
def factory(self, first_name, last_name, email, password):
|
|
159
|
+
user = User.objects.create_user(
|
|
160
|
+
username=email,
|
|
161
|
+
email=email,
|
|
162
|
+
password=password,
|
|
163
|
+
first_name=first_name,
|
|
164
|
+
last_name=last_name,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
user_profile = UserProfile.objects.create(user=user)
|
|
168
|
+
|
|
169
|
+
return Teacher.objects.create(user=user_profile, new_user=user)
|
|
170
|
+
|
|
171
|
+
def get_original_queryset(self):
|
|
172
|
+
return super().get_queryset()
|
|
173
|
+
|
|
174
|
+
# Filter out non active teachers by default
|
|
175
|
+
def get_queryset(self):
|
|
176
|
+
return super().get_queryset().filter(new_user__is_active=True)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class Teacher(models.Model):
|
|
180
|
+
user = models.OneToOneField(UserProfile, on_delete=models.CASCADE)
|
|
181
|
+
new_user = models.OneToOneField(
|
|
182
|
+
User,
|
|
183
|
+
related_name="new_teacher",
|
|
184
|
+
null=True,
|
|
185
|
+
blank=True,
|
|
186
|
+
on_delete=models.CASCADE,
|
|
187
|
+
)
|
|
188
|
+
school = models.ForeignKey(
|
|
189
|
+
School,
|
|
190
|
+
related_name="teacher_school",
|
|
191
|
+
null=True,
|
|
192
|
+
blank=True,
|
|
193
|
+
on_delete=models.SET_NULL,
|
|
194
|
+
)
|
|
195
|
+
is_admin = models.BooleanField(default=False)
|
|
196
|
+
blocked_time = models.DateTimeField(null=True, blank=True)
|
|
197
|
+
invited_by = models.ForeignKey(
|
|
198
|
+
"self",
|
|
199
|
+
related_name="invited_teachers",
|
|
200
|
+
null=True,
|
|
201
|
+
blank=True,
|
|
202
|
+
on_delete=models.SET_NULL,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
objects = TeacherModelManager()
|
|
206
|
+
|
|
207
|
+
class Meta:
|
|
208
|
+
constraints = [
|
|
209
|
+
models.CheckConstraint(
|
|
210
|
+
check=~models.Q(
|
|
211
|
+
school__isnull=True,
|
|
212
|
+
is_admin=True,
|
|
213
|
+
),
|
|
214
|
+
name="teacher__is_admin",
|
|
215
|
+
)
|
|
216
|
+
]
|
|
217
|
+
|
|
218
|
+
def teaches(self, userprofile):
|
|
219
|
+
if hasattr(userprofile, "student"):
|
|
220
|
+
student = userprofile.student
|
|
221
|
+
return (
|
|
222
|
+
not student.is_independent()
|
|
223
|
+
and student.class_field.teacher == self
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def has_school(self):
|
|
227
|
+
return self.school is not (None or "")
|
|
228
|
+
|
|
229
|
+
def has_class(self):
|
|
230
|
+
return self.class_teacher.exists()
|
|
231
|
+
|
|
232
|
+
def __str__(self):
|
|
233
|
+
return f"{self.new_user.first_name} {self.new_user.last_name}"
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class SchoolTeacherInvitationModelManager(models.Manager):
|
|
237
|
+
def get_original_queryset(self):
|
|
238
|
+
return super().get_queryset()
|
|
239
|
+
|
|
240
|
+
# Filter out inactive invitations by default
|
|
241
|
+
def get_queryset(self):
|
|
242
|
+
return super().get_queryset().filter(is_active=True)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class SchoolTeacherInvitation(models.Model):
|
|
246
|
+
token = models.CharField(max_length=88)
|
|
247
|
+
school = models.ForeignKey(
|
|
248
|
+
School,
|
|
249
|
+
related_name="teacher_invitations",
|
|
250
|
+
null=True,
|
|
251
|
+
on_delete=models.SET_NULL,
|
|
252
|
+
)
|
|
253
|
+
from_teacher = models.ForeignKey(
|
|
254
|
+
Teacher,
|
|
255
|
+
related_name="school_invitations",
|
|
256
|
+
null=True,
|
|
257
|
+
on_delete=models.SET_NULL,
|
|
258
|
+
)
|
|
259
|
+
invited_teacher_first_name = models.CharField(
|
|
260
|
+
max_length=150
|
|
261
|
+
) # Same as User model
|
|
262
|
+
# TODO: Make not nullable once data has been transferred
|
|
263
|
+
_invited_teacher_first_name = models.BinaryField(null=True, blank=True)
|
|
264
|
+
invited_teacher_last_name = models.CharField(
|
|
265
|
+
max_length=150
|
|
266
|
+
) # Same as User model
|
|
267
|
+
# TODO: Make not nullable once data has been transferred
|
|
268
|
+
_invited_teacher_last_name = models.BinaryField(null=True, blank=True)
|
|
269
|
+
# TODO: Switch to a CharField to be able to hold hashed value
|
|
270
|
+
invited_teacher_email = models.EmailField() # Same as User model
|
|
271
|
+
# TODO: Make not nullable once data has been transferred
|
|
272
|
+
_invited_teacher_email = models.BinaryField(null=True, blank=True)
|
|
273
|
+
invited_teacher_is_admin = models.BooleanField(default=False)
|
|
274
|
+
expiry = models.DateTimeField()
|
|
275
|
+
creation_time = models.DateTimeField(default=timezone.now, null=True)
|
|
276
|
+
is_active = models.BooleanField(default=True)
|
|
277
|
+
|
|
278
|
+
objects = SchoolTeacherInvitationModelManager()
|
|
279
|
+
|
|
280
|
+
@property
|
|
281
|
+
def is_expired(self):
|
|
282
|
+
return self.expiry < timezone.now()
|
|
283
|
+
|
|
284
|
+
def __str__(self):
|
|
285
|
+
return f"School teacher invitation for {self.invited_teacher_email} to {self.school.name}"
|
|
286
|
+
|
|
287
|
+
def anonymise(self):
|
|
288
|
+
self.invited_teacher_first_name = uuid4().hex
|
|
289
|
+
self.invited_teacher_last_name = uuid4().hex
|
|
290
|
+
self.invited_teacher_email = uuid4().hex
|
|
291
|
+
self.is_active = False
|
|
292
|
+
self.save()
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class ClassModelManager(models.Manager):
|
|
296
|
+
def all_members(self, user):
|
|
297
|
+
members = []
|
|
298
|
+
if hasattr(user, "teacher"):
|
|
299
|
+
members.append(user.teacher)
|
|
300
|
+
if user.teacher.has_school():
|
|
301
|
+
classes = user.teacher.class_teacher.all()
|
|
302
|
+
for c in classes:
|
|
303
|
+
members.extend(c.students.all())
|
|
304
|
+
else:
|
|
305
|
+
c = user.student.class_field
|
|
306
|
+
members.append(c.teacher)
|
|
307
|
+
members.extend(c.students.all())
|
|
308
|
+
return members
|
|
309
|
+
|
|
310
|
+
def get_original_queryset(self):
|
|
311
|
+
return super().get_queryset()
|
|
312
|
+
|
|
313
|
+
# Filter out non active classes by default
|
|
314
|
+
def get_queryset(self):
|
|
315
|
+
return super().get_queryset().filter(is_active=True)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
class Class(models.Model):
|
|
319
|
+
locked_worksheets: "ManyToManyField[Worksheet]"
|
|
320
|
+
|
|
321
|
+
name = models.CharField(max_length=200)
|
|
322
|
+
teacher = models.ForeignKey(
|
|
323
|
+
Teacher, related_name="class_teacher", on_delete=models.CASCADE
|
|
324
|
+
)
|
|
325
|
+
access_code = models.CharField(max_length=5, null=True)
|
|
326
|
+
classmates_data_viewable = models.BooleanField(default=False)
|
|
327
|
+
always_accept_requests = models.BooleanField(default=False)
|
|
328
|
+
accept_requests_until = models.DateTimeField(null=True)
|
|
329
|
+
creation_time = models.DateTimeField(default=timezone.now, null=True)
|
|
330
|
+
is_active = models.BooleanField(default=True)
|
|
331
|
+
created_by = models.ForeignKey(
|
|
332
|
+
Teacher,
|
|
333
|
+
null=True,
|
|
334
|
+
blank=True,
|
|
335
|
+
related_name="created_classes",
|
|
336
|
+
on_delete=models.SET_NULL,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
objects = ClassModelManager()
|
|
340
|
+
|
|
341
|
+
def __str__(self):
|
|
342
|
+
return self.name
|
|
343
|
+
|
|
344
|
+
@property
|
|
345
|
+
def active_game(self):
|
|
346
|
+
games = self.game_set.filter(game_class=self, is_archived=False)
|
|
347
|
+
if len(games) >= 1:
|
|
348
|
+
assert (
|
|
349
|
+
len(games) == 1
|
|
350
|
+
) # there should NOT be more than one active game
|
|
351
|
+
return games[0]
|
|
352
|
+
return None
|
|
353
|
+
|
|
354
|
+
def has_students(self):
|
|
355
|
+
students = self.students.all()
|
|
356
|
+
return students.count() != 0
|
|
357
|
+
|
|
358
|
+
def get_requests_message(self):
|
|
359
|
+
if self.always_accept_requests:
|
|
360
|
+
external_requests_message = (
|
|
361
|
+
"This class is currently set to always accept requests."
|
|
362
|
+
)
|
|
363
|
+
elif (
|
|
364
|
+
self.accept_requests_until is not None
|
|
365
|
+
and (self.accept_requests_until - timezone.now()) >= timedelta()
|
|
366
|
+
):
|
|
367
|
+
external_requests_message = (
|
|
368
|
+
"This class is accepting external requests until "
|
|
369
|
+
+ self.accept_requests_until.strftime("%d-%m-%Y %H:%M")
|
|
370
|
+
+ " "
|
|
371
|
+
+ timezone.get_current_timezone_name()
|
|
372
|
+
)
|
|
373
|
+
else:
|
|
374
|
+
external_requests_message = (
|
|
375
|
+
"This class is not currently accepting external requests."
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
return external_requests_message
|
|
379
|
+
|
|
380
|
+
def anonymise(self):
|
|
381
|
+
self.name = uuid4().hex
|
|
382
|
+
self.access_code = ""
|
|
383
|
+
self.is_active = False
|
|
384
|
+
self.save()
|
|
385
|
+
|
|
386
|
+
# Remove independent students' requests to join this class
|
|
387
|
+
self.class_request.clear()
|
|
388
|
+
|
|
389
|
+
class Meta(object):
|
|
390
|
+
verbose_name_plural = "classes"
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class UserSession(models.Model):
|
|
394
|
+
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
|
395
|
+
login_time = models.DateTimeField(default=timezone.now)
|
|
396
|
+
school = models.ForeignKey(School, null=True, on_delete=models.SET_NULL)
|
|
397
|
+
class_field = models.ForeignKey(Class, null=True, on_delete=models.SET_NULL)
|
|
398
|
+
login_type = models.CharField(
|
|
399
|
+
max_length=100, null=True
|
|
400
|
+
) # for student login
|
|
401
|
+
|
|
402
|
+
def __str__(self):
|
|
403
|
+
return f"{self.user} login: {self.login_time} type: {self.login_type}"
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
class StudentModelManager(models.Manager):
|
|
407
|
+
def get_random_username(self):
|
|
408
|
+
while True:
|
|
409
|
+
random_username = uuid4().hex[:30] # generate a random username
|
|
410
|
+
if not User.objects.filter(username=random_username).exists():
|
|
411
|
+
return random_username
|
|
412
|
+
|
|
413
|
+
def schoolFactory(self, klass, name, password, login_id=None):
|
|
414
|
+
user = User.objects.create_user(
|
|
415
|
+
username=self.get_random_username(),
|
|
416
|
+
password=password,
|
|
417
|
+
first_name=name,
|
|
418
|
+
)
|
|
419
|
+
user_profile = UserProfile.objects.create(user=user)
|
|
420
|
+
|
|
421
|
+
return Student.objects.create(
|
|
422
|
+
class_field=klass,
|
|
423
|
+
user=user_profile,
|
|
424
|
+
new_user=user,
|
|
425
|
+
login_id=login_id,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
def independentStudentFactory(self, name, email, password):
|
|
429
|
+
user = User.objects.create_user(
|
|
430
|
+
username=email, email=email, password=password, first_name=name
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
user_profile = UserProfile.objects.create(user=user)
|
|
434
|
+
|
|
435
|
+
return Student.objects.create(user=user_profile, new_user=user)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
class Student(models.Model):
|
|
439
|
+
class_field = models.ForeignKey(
|
|
440
|
+
Class,
|
|
441
|
+
related_name="students",
|
|
442
|
+
null=True,
|
|
443
|
+
blank=True,
|
|
444
|
+
on_delete=models.CASCADE,
|
|
445
|
+
)
|
|
446
|
+
# hashed uuid used for the unique direct login url
|
|
447
|
+
login_id = models.CharField(max_length=64, null=True)
|
|
448
|
+
user = models.OneToOneField(UserProfile, on_delete=models.CASCADE)
|
|
449
|
+
new_user = models.OneToOneField(
|
|
450
|
+
User,
|
|
451
|
+
related_name="new_student",
|
|
452
|
+
null=True,
|
|
453
|
+
blank=True,
|
|
454
|
+
on_delete=models.CASCADE,
|
|
455
|
+
)
|
|
456
|
+
pending_class_request = models.ForeignKey(
|
|
457
|
+
Class,
|
|
458
|
+
related_name="class_request",
|
|
459
|
+
null=True,
|
|
460
|
+
blank=True,
|
|
461
|
+
on_delete=models.SET_NULL,
|
|
462
|
+
)
|
|
463
|
+
blocked_time = models.DateTimeField(null=True, blank=True)
|
|
464
|
+
|
|
465
|
+
objects = StudentModelManager()
|
|
466
|
+
|
|
467
|
+
def is_independent(self):
|
|
468
|
+
return not self.class_field
|
|
469
|
+
|
|
470
|
+
def __str__(self):
|
|
471
|
+
return f"{self.new_user.first_name} {self.new_user.last_name}"
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def stripStudentName(name):
|
|
475
|
+
return re.sub("[ \t]+", " ", name.strip())
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
# -----------------------------------------------------------------------
|
|
479
|
+
# Below are models used for data tracking and maintenance
|
|
480
|
+
# -----------------------------------------------------------------------
|
|
481
|
+
class JoinReleaseStudent(models.Model):
|
|
482
|
+
"""
|
|
483
|
+
To keep track when a student is released to be independent student or
|
|
484
|
+
joins a class to be a school student.
|
|
485
|
+
"""
|
|
486
|
+
|
|
487
|
+
JOIN = "join"
|
|
488
|
+
RELEASE = "release"
|
|
489
|
+
|
|
490
|
+
student = models.ForeignKey(
|
|
491
|
+
Student, related_name="student", on_delete=models.CASCADE
|
|
492
|
+
)
|
|
493
|
+
# either "release" or "join"
|
|
494
|
+
action_type = models.CharField(max_length=64)
|
|
495
|
+
action_time = models.DateTimeField(default=timezone.now)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
class DailyActivity(models.Model):
|
|
499
|
+
"""
|
|
500
|
+
A model to record sets of daily activity. Currently used to record the
|
|
501
|
+
amount of student details download clicks, through the CSV and login
|
|
502
|
+
cards methods, per day.
|
|
503
|
+
"""
|
|
504
|
+
|
|
505
|
+
date = models.DateField(default=timezone.now)
|
|
506
|
+
csv_click_count = models.PositiveIntegerField(default=0)
|
|
507
|
+
login_cards_click_count = models.PositiveIntegerField(default=0)
|
|
508
|
+
primary_coding_club_downloads = models.PositiveIntegerField(default=0)
|
|
509
|
+
python_coding_club_downloads = models.PositiveIntegerField(default=0)
|
|
510
|
+
level_control_submits = models.PositiveBigIntegerField(default=0)
|
|
511
|
+
teacher_lockout_resets = models.PositiveIntegerField(default=0)
|
|
512
|
+
indy_lockout_resets = models.PositiveIntegerField(default=0)
|
|
513
|
+
school_student_lockout_resets = models.PositiveIntegerField(default=0)
|
|
514
|
+
anonymised_unverified_teachers = models.PositiveIntegerField(default=0)
|
|
515
|
+
anonymised_unverified_independents = models.PositiveIntegerField(default=0)
|
|
516
|
+
|
|
517
|
+
class Meta:
|
|
518
|
+
verbose_name_plural = "Daily activities"
|
|
519
|
+
|
|
520
|
+
def __str__(self):
|
|
521
|
+
return f"Activity on {self.date}: CSV clicks: {self.csv_click_count}, login cards clicks: {self.login_cards_click_count}, primary pack downloads: {self.primary_coding_club_downloads}, python pack downloads: {self.python_coding_club_downloads}, level control submits: {self.level_control_submits}, teacher lockout resets: {self.teacher_lockout_resets}, indy lockout resets: {self.indy_lockout_resets}, school student lockout resets: {self.school_student_lockout_resets}, unverified teachers anonymised: {self.anonymised_unverified_teachers}, unverified independents anonymised: {self.anonymised_unverified_independents}"
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
class TotalActivity(models.Model):
|
|
525
|
+
"""
|
|
526
|
+
A model to record total activity. Meant to only have one entry which
|
|
527
|
+
records all total activity. An example of this is total ever registrations.
|
|
528
|
+
"""
|
|
529
|
+
|
|
530
|
+
teacher_registrations = models.PositiveIntegerField(default=0)
|
|
531
|
+
student_registrations = models.PositiveIntegerField(default=0)
|
|
532
|
+
independent_registrations = models.PositiveIntegerField(default=0)
|
|
533
|
+
anonymised_unverified_teachers = models.PositiveIntegerField(default=0)
|
|
534
|
+
anonymised_unverified_independents = models.PositiveIntegerField(default=0)
|
|
535
|
+
|
|
536
|
+
class Meta:
|
|
537
|
+
verbose_name_plural = "Total activity"
|
|
538
|
+
|
|
539
|
+
def __str__(self):
|
|
540
|
+
return "Total activity"
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
class DynamicElement(models.Model):
|
|
544
|
+
"""
|
|
545
|
+
This model is meant to allow us to quickly update some elements
|
|
546
|
+
dynamically on the website without having to redeploy everytime. For
|
|
547
|
+
example, if a maintenance banner needs to be added, we check the box in
|
|
548
|
+
the Django admin panel, edit the text and it'll show immediately on the
|
|
549
|
+
website.
|
|
550
|
+
"""
|
|
551
|
+
|
|
552
|
+
name = models.CharField(max_length=64, unique=True, editable=False)
|
|
553
|
+
active = models.BooleanField(default=False)
|
|
554
|
+
text = models.TextField(null=True, blank=True)
|
|
555
|
+
|
|
556
|
+
def __str__(self) -> str:
|
|
557
|
+
return self.name
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from functools import wraps
|
|
2
|
+
|
|
3
|
+
from common.utils import using_two_factor
|
|
4
|
+
from django.http import Http404
|
|
5
|
+
from django.http import HttpResponseRedirect
|
|
6
|
+
from django.urls import reverse_lazy
|
|
7
|
+
from rest_framework import permissions
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def has_completed_auth_setup(user):
|
|
11
|
+
return (not using_two_factor(user)) or (user.userprofile.is_verified and using_two_factor(user))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LoggedInAsTeacher(permissions.BasePermission):
|
|
15
|
+
def has_permission(self, request, view):
|
|
16
|
+
return logged_in_as_teacher(request.user)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def logged_in_as_teacher(user):
|
|
20
|
+
try:
|
|
21
|
+
return user.userprofile.teacher and has_completed_auth_setup(user)
|
|
22
|
+
except AttributeError:
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def logged_in_as_student(u):
|
|
27
|
+
try:
|
|
28
|
+
if u.userprofile.student:
|
|
29
|
+
return True
|
|
30
|
+
except AttributeError:
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def logged_in_as_independent_student(u):
|
|
35
|
+
return logged_in_as_student(u) and u.userprofile.student.is_independent()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def logged_in_as_school_student(u):
|
|
39
|
+
return logged_in_as_student(u) and not u.userprofile.student.is_independent()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def check_teacher_authorised(request, class_teacher):
|
|
43
|
+
current_teacher_owns_the_class = class_teacher == request.user.new_teacher
|
|
44
|
+
is_current_teacher_school_admin = (
|
|
45
|
+
class_teacher.school == request.user.new_teacher.school and request.user.new_teacher.is_admin
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if not (current_teacher_owns_the_class or is_current_teacher_school_admin):
|
|
49
|
+
raise Http404
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def not_logged_in(u):
|
|
53
|
+
try:
|
|
54
|
+
if u.userprofile:
|
|
55
|
+
return False
|
|
56
|
+
except AttributeError:
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def not_fully_logged_in(u):
|
|
61
|
+
return not_logged_in(u) or (not logged_in_as_student(u) and not logged_in_as_teacher(u))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def teacher_verified(view_func):
|
|
65
|
+
@wraps(view_func)
|
|
66
|
+
def wrapped(request, *args, **kwargs):
|
|
67
|
+
u = request.user
|
|
68
|
+
try:
|
|
69
|
+
if not u.userprofile.teacher or not has_completed_auth_setup(u):
|
|
70
|
+
return HttpResponseRedirect(reverse_lazy("teach"))
|
|
71
|
+
except AttributeError:
|
|
72
|
+
return HttpResponseRedirect(reverse_lazy("teach"))
|
|
73
|
+
return view_func(request, *args, **kwargs)
|
|
74
|
+
|
|
75
|
+
return wrapped
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class CanDeleteGame(permissions.BasePermission):
|
|
79
|
+
def has_permission(self, request, view):
|
|
80
|
+
u = request.user
|
|
81
|
+
try:
|
|
82
|
+
return u.userprofile.teacher and has_completed_auth_setup(u)
|
|
83
|
+
except AttributeError:
|
|
84
|
+
return False
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from django_test_migrations.migrator import Migrator
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@pytest.mark.django_db
|
|
6
|
+
def test_migration_anonymise_orphan_schools(migrator: Migrator):
|
|
7
|
+
state = migrator.apply_initial_migration(
|
|
8
|
+
("common", "0049_anonymise_orphan_users")
|
|
9
|
+
)
|
|
10
|
+
User = state.apps.get_model("auth", "User")
|
|
11
|
+
UserProfile = state.apps.get_model("common", "UserProfile")
|
|
12
|
+
Teacher = state.apps.get_model("common", "Teacher")
|
|
13
|
+
School = state.apps.get_model("common", "School")
|
|
14
|
+
|
|
15
|
+
orphan_school = School.objects.create(name="OrphanSchool")
|
|
16
|
+
teacher_school = School.objects.create(name="TeacherSchool")
|
|
17
|
+
|
|
18
|
+
teacher_user = User.objects.create_user("TeacherUser", password="password")
|
|
19
|
+
teacher_userprofile = UserProfile.objects.create(user=teacher_user)
|
|
20
|
+
Teacher.objects.create(
|
|
21
|
+
user=teacher_userprofile, new_user=teacher_user, school=teacher_school
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
migrator.apply_tested_migration(("common", "0050_anonymise_orphan_schools"))
|
|
25
|
+
|
|
26
|
+
def assert_school_anonymised(pk: int, anonymised: bool):
|
|
27
|
+
assert School.objects.get(pk=pk).is_active != anonymised
|
|
28
|
+
|
|
29
|
+
assert_school_anonymised(orphan_school.pk, True)
|
|
30
|
+
assert_school_anonymised(teacher_school.pk, False)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from django_test_migrations.migrator import Migrator
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@pytest.mark.django_db
|
|
6
|
+
def test_migration_anonymise_orphan_users(migrator: Migrator):
|
|
7
|
+
state = migrator.apply_initial_migration(
|
|
8
|
+
("common", "0048_unique_school_names")
|
|
9
|
+
)
|
|
10
|
+
User = state.apps.get_model("auth", "User")
|
|
11
|
+
UserProfile = state.apps.get_model("common", "UserProfile")
|
|
12
|
+
Teacher = state.apps.get_model("common", "Teacher")
|
|
13
|
+
Student = state.apps.get_model("common", "Student")
|
|
14
|
+
|
|
15
|
+
orphan_user = User.objects.create_user("OrphanUser", password="password")
|
|
16
|
+
teacher_user = User.objects.create_user("TeacherUser", password="password")
|
|
17
|
+
student_user = User.objects.create_user("StudentUser", password="password")
|
|
18
|
+
teacher_userprofile = UserProfile.objects.create(user=teacher_user)
|
|
19
|
+
student_userprofile = UserProfile.objects.create(user=student_user)
|
|
20
|
+
Teacher.objects.create(user=teacher_userprofile, new_user=teacher_user)
|
|
21
|
+
Student.objects.create(user=student_userprofile, new_user=student_user)
|
|
22
|
+
|
|
23
|
+
migrator.apply_tested_migration(("common", "0049_anonymise_orphan_users"))
|
|
24
|
+
|
|
25
|
+
def assert_user_anonymised(pk: int, anonymised: bool):
|
|
26
|
+
assert User.objects.get(pk=pk).is_active != anonymised
|
|
27
|
+
|
|
28
|
+
assert_user_anonymised(orphan_user.pk, True)
|
|
29
|
+
assert_user_anonymised(teacher_user.pk, False)
|
|
30
|
+
assert_user_anonymised(student_user.pk, False)
|