codeforlife-portal 5.33.5__py2.py3-none-any.whl → 8.9.9__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cfl_common/common/__init__.py +1 -0
- cfl_common/common/app_settings.py +66 -0
- cfl_common/common/apps.py +6 -0
- cfl_common/common/context_processors.py +9 -0
- cfl_common/common/csp_config.py +85 -0
- cfl_common/common/helpers/__init__.py +0 -0
- cfl_common/common/helpers/data_migration_loader.py +42 -0
- cfl_common/common/helpers/emails.py +393 -0
- cfl_common/common/helpers/generators.py +52 -0
- cfl_common/common/helpers/organisation.py +10 -0
- cfl_common/common/mail.py +201 -0
- cfl_common/common/migrations/0001_initial.py +240 -0
- cfl_common/common/migrations/0002_emailverification.py +55 -0
- cfl_common/common/migrations/0003_aimmocharacter.py +31 -0
- cfl_common/common/migrations/0004_add_aimmocharacters.py +17 -0
- cfl_common/common/migrations/0005_add_worksheets.py +8 -0
- cfl_common/common/migrations/0006_update_aimmo_character_image_path.py +17 -0
- cfl_common/common/migrations/0007_add_pdf_names_to_first_two_worksheets.py +8 -0
- cfl_common/common/migrations/0008_unlock_worksheet_3.py +11 -0
- cfl_common/common/migrations/0009_add_blocked_time_to_teacher_and_student.py +24 -0
- cfl_common/common/migrations/0010_remove_teacher_title.py +18 -0
- cfl_common/common/migrations/0011_student_login_id.py +18 -0
- cfl_common/common/migrations/0012_usersession.py +39 -0
- cfl_common/common/migrations/0013_class_school.py +42 -0
- cfl_common/common/migrations/0014_login_type.py +29 -0
- cfl_common/common/migrations/0015_dailyactivity.py +31 -0
- cfl_common/common/migrations/0016_joinreleasestudent.py +42 -0
- cfl_common/common/migrations/0017_copy_email_to_username.py +18 -0
- cfl_common/common/migrations/0018_update_aimmo_character_image_path.py +15 -0
- cfl_common/common/migrations/0019_aimmocharacter_alt.py +16 -0
- cfl_common/common/migrations/0020_class_is_active_and_null_access_code.py +23 -0
- cfl_common/common/migrations/0021_school_is_active.py +28 -0
- cfl_common/common/migrations/0022_school_cleanup.py +29 -0
- cfl_common/common/migrations/0023_userprofile_aimmo_badges.py +22 -0
- cfl_common/common/migrations/0024_teacher_invited_by.py +25 -0
- cfl_common/common/migrations/0025_schoolteacherinvitation.py +47 -0
- cfl_common/common/migrations/0026_teacher_remove_join_request.py +22 -0
- cfl_common/common/migrations/0027_class_created_by.py +25 -0
- cfl_common/common/migrations/0028_coding_club_downloads.py +23 -0
- cfl_common/common/migrations/0029_dynamicelement.py +22 -0
- cfl_common/common/migrations/0030_add_maintenance_banner.py +25 -0
- cfl_common/common/migrations/0031_improve_admin_panel.py +56 -0
- cfl_common/common/migrations/0032_dailyactivity_level_control_submits.py +18 -0
- cfl_common/common/migrations/0033_password_reset_tracking_fields.py +23 -0
- cfl_common/common/migrations/0034_dailyactivity_daily_school_student_lockout_reset.py +18 -0
- cfl_common/common/migrations/0035_rename_lockout_fields.py +27 -0
- cfl_common/common/migrations/0036_rename_awaiting_email_verification_userprofile_is_verified.py +17 -0
- cfl_common/common/migrations/0037_migrate_email_verification.py +21 -0
- cfl_common/common/migrations/0038_delete_emailverification.py +16 -0
- cfl_common/common/migrations/0039_copy_email_to_username.py +18 -0
- cfl_common/common/migrations/0040_school_county.py +18 -0
- cfl_common/common/migrations/0041_populate_gb_counties.py +27 -0
- cfl_common/common/migrations/0042_totalactivity.py +25 -0
- cfl_common/common/migrations/0043_add_total_activity.py +30 -0
- cfl_common/common/migrations/0044_update_activity_models.py +33 -0
- cfl_common/common/migrations/0045_otp.py +23 -0
- cfl_common/common/migrations/0046_alter_school_country.py +19 -0
- cfl_common/common/migrations/0047_delete_school_postcode.py +16 -0
- cfl_common/common/migrations/0048_unique_school_names.py +42 -0
- cfl_common/common/migrations/0049_anonymise_orphan_users.py +29 -0
- cfl_common/common/migrations/0050_anonymise_orphan_schools.py +30 -0
- cfl_common/common/migrations/0051_verify_returning_users.py +26 -0
- cfl_common/common/migrations/0052_add_cse_fields.py +68 -0
- cfl_common/common/migrations/0053_clean_class_data.py +24 -0
- cfl_common/common/migrations/0054_delete_aimmo_models.py +20 -0
- cfl_common/common/migrations/0055_alter_schoolteacherinvitation_token.py +18 -0
- cfl_common/common/migrations/0056_set_non_school_teachers_as_non_admins.py +25 -0
- cfl_common/common/migrations/0057_teacher_teacher__is_admin.py +19 -0
- cfl_common/common/migrations/0058_userprofile_google_refresh_token_and_more.py +24 -0
- cfl_common/common/migrations/__init__.py +0 -0
- cfl_common/common/models.py +557 -0
- cfl_common/common/permissions.py +84 -0
- cfl_common/common/tests/__init__.py +0 -0
- cfl_common/common/tests/test_migration_anonymise_orphan_schools.py +30 -0
- cfl_common/common/tests/test_migration_anonymise_orphan_users.py +30 -0
- cfl_common/common/tests/test_migration_blocked_time.py +15 -0
- cfl_common/common/tests/test_migration_remove_teacher_title.py +13 -0
- cfl_common/common/tests/test_migration_unique_school_names.py +33 -0
- cfl_common/common/tests/test_migration_verify_returning_users.py +59 -0
- cfl_common/common/tests/test_models.py +87 -0
- cfl_common/common/tests/utils/__init__.py +0 -0
- cfl_common/common/tests/utils/classes.py +38 -0
- cfl_common/common/tests/utils/email.py +67 -0
- cfl_common/common/tests/utils/organisation.py +41 -0
- cfl_common/common/tests/utils/student.py +123 -0
- cfl_common/common/tests/utils/teacher.py +73 -0
- cfl_common/common/tests/utils/user.py +27 -0
- cfl_common/common/utils.py +56 -0
- cfl_common/setup.py +61 -0
- codeforlife_portal-8.9.9.dist-info/METADATA +226 -0
- {codeforlife_portal-5.33.5.dist-info → codeforlife_portal-8.9.9.dist-info}/RECORD +339 -241
- {codeforlife_portal-5.33.5.dist-info → codeforlife_portal-8.9.9.dist-info}/WHEEL +1 -1
- codeforlife_portal-8.9.9.dist-info/licenses/LICENSE.md +3 -0
- {codeforlife_portal-5.33.5.dist-info → codeforlife_portal-8.9.9.dist-info}/top_level.txt +1 -0
- deploy/middleware/maintenance.py +25 -0
- deploy/middleware/screentime_warning.py +29 -0
- deploy/middleware/security.py +5 -6
- deploy/middleware/session_timeout.py +4 -2
- deploy/middleware/tmp_basic_auth.py +41 -0
- example_project/portal_test_settings.py +239 -0
- example_project/settings.py +156 -17
- example_project/urls.py +5 -6
- portal/__init__.py +1 -1
- portal/admin.py +142 -29
- portal/app_settings.py +8 -7
- portal/forms/dotmailer.py +6 -4
- portal/forms/invite_teacher.py +19 -10
- portal/forms/organisation.py +137 -68
- portal/forms/play.py +53 -98
- portal/forms/registration.py +70 -164
- portal/forms/teach.py +147 -121
- portal/handlers.py +1 -2
- portal/helpers/decorators.py +30 -10
- portal/helpers/password.py +86 -47
- portal/helpers/ratelimit.py +32 -15
- portal/helpers/regexes.py +5 -0
- portal/helpers/request_handlers.py +10 -0
- portal/migrations/0044_auto_20150430_0959.py +6 -2
- portal/mixins/__init__.py +1 -0
- portal/mixins/cron_mixin.py +12 -0
- portal/permissions/__init__.py +1 -0
- portal/permissions/is_cron_request_from_google.py +14 -0
- portal/static/portal/img/10_years_anniversary.png +0 -0
- portal/static/portal/img/RR_logo_grass_background.png +0 -0
- portal/static/portal/img/coding_club_hero.jpg +0 -0
- portal/static/portal/img/coding_club_python_pack.png +0 -0
- portal/static/portal/img/facebook.png +0 -0
- portal/static/portal/img/gitbook.png +0 -0
- portal/static/portal/img/howe_dell_1.png +0 -0
- portal/static/portal/img/howe_dell_2.png +0 -0
- portal/static/portal/img/howe_dell_3.png +0 -0
- portal/static/portal/img/logo_cfl.png +0 -0
- portal/static/portal/img/logo_cfl_powered.svg +35 -0
- portal/static/portal/img/logo_cfl_reminder_cards.jpg +0 -0
- portal/static/portal/img/logo_ocado_group.png +0 -0
- portal/static/portal/img/logo_python_den.svg +21 -0
- portal/static/portal/img/long_europe_map.png +0 -0
- portal/static/portal/img/python_den.png +0 -0
- portal/static/portal/img/python_den_banner.svg +26 -0
- portal/static/portal/img/rapid_router_landing_hero.png +0 -0
- portal/static/portal/img/rr_advanced.png +0 -0
- portal/static/portal/img/ten_year_map_pin.svg +1 -0
- portal/static/portal/img/thumbnail_educate_rapid_router.png +0 -0
- portal/static/portal/img/thumbnail_educate_resources.png +0 -0
- portal/static/portal/img/thumbnail_play_rapid_router.png +0 -0
- portal/static/portal/img/thumbnail_python_den.png +0 -0
- portal/static/portal/img/twitter.png +0 -0
- portal/static/portal/js/carouselCards.js +25 -0
- portal/static/portal/js/common.js +96 -1
- portal/static/portal/js/independentLogin.js +16 -0
- portal/static/portal/js/independentRegistration.js +86 -0
- portal/static/portal/js/levelControl.js +77 -0
- portal/static/portal/js/lib/jquery.min.js +2 -0
- portal/static/portal/js/organisation_manage.js +142 -14
- portal/static/portal/js/passwordStrength.js +154 -64
- portal/static/portal/js/resetPassword.js +23 -0
- portal/static/portal/js/riveted.min.js +238 -239
- portal/static/portal/js/school.js +13 -0
- portal/static/portal/js/studentLogin.js +16 -0
- portal/static/portal/js/teacherEditStudent.js +23 -0
- portal/static/portal/js/teacherLogin.js +16 -0
- portal/static/portal/js/tenYearMap.js +14 -0
- portal/static/portal/sass/colorbox.scss +0 -1
- portal/static/portal/sass/modules/_colours.scss +1 -0
- portal/static/portal/sass/modules/_levels.scss +1 -1
- portal/static/portal/sass/modules/_mixins.scss +21 -0
- portal/static/portal/sass/partials/_banners.scss +4 -177
- portal/static/portal/sass/partials/_buttons.scss +12 -15
- portal/static/portal/sass/partials/_carousel.scss +129 -0
- portal/static/portal/sass/partials/_footer.scss +21 -22
- portal/static/portal/sass/partials/_forms.scss +60 -5
- portal/static/portal/sass/partials/_grids.scss +34 -61
- portal/static/portal/sass/partials/_header.scss +28 -20
- portal/static/portal/sass/partials/_images.scss +292 -39
- portal/static/portal/sass/partials/_popup.scss +18 -15
- portal/static/portal/sass/partials/_tables.scss +12 -20
- portal/static/portal/sass/partials/_text.scss +6 -10
- portal/static/portal/sass/styles.scss +0 -1
- portal/static/portal/video/code for life .pdf +0 -0
- portal/strings/about.py +5 -0
- portal/strings/coding_club.py +9 -0
- portal/strings/play.py +6 -5
- portal/strings/teach.py +1 -1
- portal/strings/teacher_resources.py +2 -8
- portal/strings/ten_year_map.py +13 -0
- portal/templates/403.html +2 -2
- portal/templates/404.html +1 -1
- portal/templates/500.html +2 -2
- portal/templates/{captcha → django_recaptcha}/includes/js_v2_invisible.html +3 -3
- portal/templates/{captcha → django_recaptcha}/widget_v2_invisible.html +2 -2
- portal/templates/email.html +4 -2
- portal/templates/maintenance.html +34 -0
- portal/templates/portal/about.html +94 -62
- portal/templates/portal/base.html +176 -152
- portal/templates/portal/coding_club.html +100 -0
- portal/templates/portal/contribute.html +56 -52
- portal/templates/portal/email_invitation_sent.html +1 -1
- portal/templates/portal/email_style_template.html +374 -0
- portal/templates/portal/email_verification_failed.html +1 -1
- portal/templates/portal/email_verification_needed.html +9 -9
- portal/templates/portal/form_shapes.html +20 -8
- portal/templates/portal/getinvolved.html +6 -6
- portal/templates/portal/home.html +35 -10
- portal/templates/portal/home_learning.html +19 -19
- portal/templates/portal/locked_out.html +0 -1
- portal/templates/portal/locked_out_school_student.html +16 -0
- portal/templates/portal/login/independent_student.html +31 -15
- portal/templates/portal/login/student.html +10 -7
- portal/templates/portal/login/student_class_code.html +7 -4
- portal/templates/portal/login/teacher.html +34 -17
- portal/templates/portal/partials/banner.html +18 -4
- portal/templates/portal/partials/benefits.html +1 -1
- portal/templates/portal/partials/card_list.html +34 -24
- portal/templates/portal/partials/character_list.html +5 -5
- portal/templates/portal/partials/cookie_list.html +161 -0
- portal/templates/portal/partials/delete_popup.html +18 -0
- portal/templates/portal/partials/footer.html +57 -26
- portal/templates/portal/partials/header.html +118 -117
- portal/templates/portal/partials/hero_card.html +4 -3
- portal/templates/portal/partials/info_popup.html +3 -3
- portal/templates/portal/partials/invite_admin_teacher.html +23 -0
- portal/templates/portal/partials/popup.html +7 -2
- portal/templates/portal/partials/register_newsletter_tickbox.html +2 -5
- portal/templates/portal/partials/screentime_popup.html +14 -0
- portal/templates/portal/partials/service_unavailable_popup.html +17 -0
- portal/templates/portal/partials/session_popup.html +19 -0
- portal/templates/portal/play/student_dashboard.html +42 -29
- portal/templates/portal/play/student_edit_account.html +64 -9
- portal/templates/portal/play.html +61 -41
- portal/templates/portal/privacy_notice.html +697 -0
- portal/templates/portal/register.html +122 -92
- portal/templates/portal/reset_password.html +20 -40
- portal/templates/portal/reset_password_confirm.html +9 -4
- portal/templates/portal/reset_password_email_sent.html +15 -13
- portal/templates/portal/teach/base_registering.html +1 -1
- portal/templates/portal/teach/class.html +4 -6
- portal/templates/portal/teach/dashboard.html +212 -117
- portal/templates/portal/teach/invited.html +90 -0
- portal/templates/portal/teach/onboarding_classes.html +5 -3
- portal/templates/portal/teach/onboarding_print.html +1 -1
- portal/templates/portal/teach/onboarding_school.html +26 -139
- portal/templates/portal/teach/onboarding_students.html +1 -1
- portal/templates/portal/teach/teacher_dismiss_students.html +73 -55
- portal/templates/portal/teach/teacher_edit_class.html +168 -11
- portal/templates/portal/teach/teacher_edit_student.html +12 -5
- portal/templates/portal/teach/teacher_move_all_classes.html +25 -38
- portal/templates/portal/teach/teacher_move_students_to_class.html +1 -1
- portal/templates/portal/teach.html +61 -42
- portal/templates/portal/ten_year_map.html +147 -0
- portal/templates/portal/terms.html +191 -42
- portal/templates/two_factor/core/login.html +71 -59
- portal/templates/two_factor/core/setup.html +58 -49
- portal/templates/two_factor/profile/disable.html +1 -1
- portal/templates/two_factor/profile/profile.html +35 -17
- portal/templatetags/app_tags.py +59 -84
- portal/templatetags/card_list_tags.py +0 -4
- portal/tests/base_test.py +14 -3
- portal/tests/conftest.py +0 -15
- portal/tests/migrations/test_migration_make_portaladmin_teacher.py +2 -6
- portal/tests/migrations/test_migration_preview_users.py +3 -9
- portal/tests/migrations/test_migration_remove_guardian.py +1 -3
- portal/tests/migrations/test_migration_use_common_models.py +2 -6
- portal/tests/migrations/test_migration_verify_portaladmin.py +1 -3
- portal/tests/pageObjects/portal/admin/admin_base_page.py +0 -21
- portal/tests/pageObjects/portal/base_page.py +16 -26
- portal/tests/pageObjects/portal/email_verification_needed_page.py +3 -2
- portal/tests/pageObjects/portal/game_page.py +12 -19
- portal/tests/pageObjects/portal/home_page.py +13 -15
- portal/tests/pageObjects/portal/independent_login_page.py +13 -17
- portal/tests/pageObjects/portal/password_reset_form_page.py +20 -4
- portal/tests/pageObjects/portal/password_reset_page.py +25 -0
- portal/tests/pageObjects/portal/play/account_page.py +18 -27
- portal/tests/pageObjects/portal/play/dashboard_page.py +4 -4
- portal/tests/pageObjects/portal/play/join_school_or_club_page.py +8 -10
- portal/tests/pageObjects/portal/play/play_base_page.py +5 -3
- portal/tests/pageObjects/portal/signup_page.py +28 -59
- portal/tests/pageObjects/portal/student_login_class_code.py +6 -9
- portal/tests/pageObjects/portal/student_login_page.py +6 -8
- portal/tests/pageObjects/portal/teach/add_independent_student_to_class_page.py +3 -3
- portal/tests/pageObjects/portal/teach/added_independent_student_to_class_page.py +3 -1
- portal/tests/pageObjects/portal/teach/class_page.py +36 -13
- portal/tests/pageObjects/portal/teach/dashboard_page.py +43 -84
- portal/tests/pageObjects/portal/teach/dismiss_students_page.py +7 -5
- portal/tests/pageObjects/portal/teach/edit_student_page.py +10 -8
- portal/tests/pageObjects/portal/teach/move_class_page.py +5 -10
- portal/tests/pageObjects/portal/teach/move_classes_page.py +4 -2
- portal/tests/pageObjects/portal/teach/move_students_disambiguate_page.py +4 -2
- portal/tests/pageObjects/portal/teach/move_students_page.py +6 -13
- portal/tests/pageObjects/portal/teach/onboarding_classes_page.py +5 -3
- portal/tests/pageObjects/portal/teach/onboarding_organisation_page.py +11 -49
- portal/tests/pageObjects/portal/teach/onboarding_student_list_page.py +7 -12
- portal/tests/pageObjects/portal/teach/onboarding_students_page.py +4 -27
- portal/tests/pageObjects/portal/teach/teach_base_page.py +6 -4
- portal/tests/pageObjects/portal/teacher_login_page.py +10 -16
- portal/tests/selenium_test_case.py +3 -43
- portal/tests/snapshots/snap_test_partials.py +11 -165
- portal/tests/test_2FA.py +15 -33
- portal/tests/test_admin.py +15 -97
- portal/tests/test_api.py +212 -91
- portal/tests/test_captcha_forms.py +2 -2
- portal/tests/test_class.py +374 -24
- portal/tests/test_emails.py +83 -20
- portal/tests/{test_newsletter_footer.py → test_global_forms.py} +5 -5
- portal/tests/test_helper_methods.py +30 -0
- portal/tests/test_independent_student.py +255 -144
- portal/tests/test_invite_teacher.py +318 -10
- portal/tests/test_middleware.py +96 -9
- portal/tests/test_organisation.py +78 -262
- portal/tests/test_partials.py +0 -88
- portal/tests/test_ratelimit.py +218 -36
- portal/tests/test_school_student.py +35 -40
- portal/tests/test_security.py +12 -31
- portal/tests/test_teacher.py +425 -325
- portal/tests/test_teacher_student.py +103 -91
- portal/tests/test_views.py +900 -76
- portal/tests/utils/classes.py +2 -2
- portal/tests/utils/messages.py +13 -28
- portal/urls.py +235 -166
- portal/views/admin.py +0 -332
- portal/views/api.py +82 -48
- portal/views/cron/__init__.py +1 -0
- portal/views/cron/user.py +322 -0
- portal/views/dotmailer.py +9 -1
- portal/views/email.py +33 -77
- portal/views/google_analytics.py +28 -0
- portal/views/home.py +126 -97
- portal/views/legal.py +13 -0
- portal/views/login/independent_student.py +5 -5
- portal/views/login/student.py +51 -14
- portal/views/login/teacher.py +2 -6
- portal/views/organisation.py +20 -189
- portal/views/registration.py +97 -17
- portal/views/student/edit_account_details.py +99 -72
- portal/views/student/play.py +81 -62
- portal/views/teacher/dashboard.py +421 -149
- portal/views/teacher/teach.py +226 -177
- portal/views/two_factor/core.py +22 -19
- portal/views/two_factor/profile.py +2 -2
- codeforlife_portal-5.33.5.dist-info/LICENSE.md +0 -577
- codeforlife_portal-5.33.5.dist-info/METADATA +0 -38
- deploy/permissions.py +0 -2
- example_project/manage.py +0 -10
- portal/autoconfig.py +0 -141
- portal/csp_config.py +0 -60
- portal/forms/add_game.py +0 -33
- portal/helpers/location.py +0 -121
- portal/static/portal/img/kurono_hero.jpg +0 -0
- portal/static/portal/img/kurono_landing_hero.png +0 -0
- portal/static/portal/img/kurono_logo.svg +0 -1
- portal/static/portal/img/kurono_logo_grey_background.svg +0 -1
- portal/static/portal/img/kurono_logo_mark.svg +0 -1
- portal/static/portal/img/kurono_resources_hero.jpg +0 -0
- portal/static/portal/img/kurono_story.png +0 -0
- portal/static/portal/img/ocado-swirl.svg +0 -22
- portal/static/portal/img/thumbnail_educate_kurono.png +0 -0
- portal/static/portal/img/thumbnail_educate_resources_and_progress_tracking.png +0 -0
- portal/static/portal/img/thumbnail_kurono_resources.png +0 -0
- portal/static/portal/img/thumbnail_play_kurono.png +0 -0
- portal/static/portal/img/x_close_video.png +0 -0
- portal/static/portal/js/aimmoGame.js +0 -106
- portal/static/portal/js/deleteWorkspaces.js +0 -14
- portal/static/portal/js/fuzzySchoolLookup.js +0 -46
- portal/static/portal/js/lib/jquery-3.5.1.min.js +0 -2
- portal/static/portal/js/lib/jquery-ui-1.12.1.min.js +0 -13
- portal/static/portal/sass/partials/_videos.scss +0 -10
- portal/static/portal/video/aimmo_play_now_background_video.mp4 +0 -0
- portal/strings/student_aimmo_dashboard.py +0 -6
- portal/templates/portal/admin/aggregated_data.html +0 -35
- portal/templates/portal/admin/map.html +0 -70
- portal/templates/portal/mouseflow.html +0 -9
- portal/templates/portal/partials/aimmo_games_table.html +0 -83
- portal/templates/portal/partials/register_over_required_age_tickbox.html +0 -9
- portal/templates/portal/play/independent_student_dashboard.html +0 -64
- portal/templates/portal/play/student_aimmo_dashboard.html +0 -63
- portal/templates/portal/privacy_policy.html +0 -483
- portal/templates/portal/reset_password_email.html +0 -9
- portal/templates/portal/teach/invite.html +0 -25
- portal/templates/portal/teach/teacher_aimmo_dashboard.html +0 -95
- portal/templates/portal/teach/teacher_resources.html +0 -68
- portal/templatetags/character_list_tags.py +0 -16
- portal/tests/pageObjects/portal/kurono_teacher_dashboard_page.py +0 -49
- portal/tests/pageObjects/portal/student_password_reset_form_page.py +0 -23
- portal/tests/pageObjects/portal/teach/onboarding_revoke_request_page.py +0 -20
- portal/tests/pageObjects/portal/teacher_password_reset_form_page.py +0 -23
- portal/tests/test_aimmo_dashboards.py +0 -172
- portal/tests/test_location.py +0 -217
- portal/tests/utils/aimmo_games.py +0 -30
- portal/views/aimmo/dashboard.py +0 -119
- portal/views/privacy_policy.py +0 -9
- portal/views/teacher/teacher_resources.py +0 -42
- {portal/views/aimmo → cfl_common}/__init__.py +0 -0
portal/views/teacher/teach.py
CHANGED
|
@@ -1,44 +1,51 @@
|
|
|
1
|
-
from __future__ import division
|
|
2
|
-
|
|
3
1
|
import csv
|
|
4
2
|
import json
|
|
5
|
-
from datetime import
|
|
3
|
+
from datetime import datetime, timedelta
|
|
6
4
|
from enum import Enum
|
|
7
5
|
from functools import partial, wraps
|
|
8
6
|
from uuid import uuid4
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
from common.helpers.emails import
|
|
8
|
+
import pytz
|
|
9
|
+
from common.helpers.emails import send_verification_email
|
|
12
10
|
from common.helpers.generators import (
|
|
13
11
|
generate_access_code,
|
|
14
12
|
generate_login_id,
|
|
15
|
-
generate_new_student_name,
|
|
16
13
|
generate_password,
|
|
17
14
|
get_hashed_login_id,
|
|
18
15
|
)
|
|
19
|
-
from common.models import
|
|
20
|
-
|
|
16
|
+
from common.models import (
|
|
17
|
+
Class,
|
|
18
|
+
DailyActivity,
|
|
19
|
+
JoinReleaseStudent,
|
|
20
|
+
Student,
|
|
21
|
+
Teacher,
|
|
22
|
+
TotalActivity,
|
|
23
|
+
)
|
|
24
|
+
from common.permissions import check_teacher_authorised, logged_in_as_teacher
|
|
21
25
|
from django.contrib import messages as messages
|
|
22
26
|
from django.contrib.auth.decorators import login_required, user_passes_test
|
|
27
|
+
from django.contrib.auth.models import User
|
|
23
28
|
from django.contrib.staticfiles.storage import staticfiles_storage
|
|
29
|
+
from django.db.models import F
|
|
24
30
|
from django.forms.formsets import formset_factory
|
|
25
31
|
from django.http import Http404, HttpResponse, HttpResponseRedirect
|
|
26
32
|
from django.shortcuts import get_object_or_404, render
|
|
27
33
|
from django.urls import reverse, reverse_lazy
|
|
28
34
|
from django.utils import timezone
|
|
29
35
|
from django.views.decorators.http import require_POST
|
|
30
|
-
from
|
|
36
|
+
from game.models import Level
|
|
37
|
+
from game.views.level_selection import get_blockly_episodes, get_python_episodes
|
|
31
38
|
from reportlab.lib.colors import black, red
|
|
32
39
|
from reportlab.lib.pagesizes import A4
|
|
33
40
|
from reportlab.lib.utils import ImageReader
|
|
34
41
|
from reportlab.pdfgen import canvas
|
|
35
42
|
|
|
36
|
-
from portal.forms.invite_teacher import InviteTeacherForm
|
|
37
43
|
from portal.forms.teach import (
|
|
38
44
|
BaseTeacherDismissStudentsFormSet,
|
|
39
45
|
BaseTeacherMoveStudentsDisambiguationFormSet,
|
|
40
46
|
ClassCreationForm,
|
|
41
47
|
ClassEditForm,
|
|
48
|
+
ClassLevelControlForm,
|
|
42
49
|
ClassMoveForm,
|
|
43
50
|
StudentCreationForm,
|
|
44
51
|
TeacherDismissStudentsForm,
|
|
@@ -47,13 +54,13 @@ from portal.forms.teach import (
|
|
|
47
54
|
TeacherMoveStudentsDestinationForm,
|
|
48
55
|
TeacherSetStudentPass,
|
|
49
56
|
)
|
|
57
|
+
from portal.helpers.ratelimit import clear_ratelimit_cache_for_user
|
|
58
|
+
from portal.views.registration import handle_reset_password_tracking
|
|
50
59
|
|
|
51
60
|
STUDENT_PASSWORD_LENGTH = 6
|
|
52
61
|
REMINDER_CARDS_PDF_ROWS = 8
|
|
53
62
|
REMINDER_CARDS_PDF_COLUMNS = 1
|
|
54
|
-
REMINDER_CARDS_PDF_WARNING_TEXT =
|
|
55
|
-
"Please ensure students keep login details in a secure place"
|
|
56
|
-
)
|
|
63
|
+
REMINDER_CARDS_PDF_WARNING_TEXT = "Please ensure students keep login details in a secure place"
|
|
57
64
|
|
|
58
65
|
|
|
59
66
|
@login_required(login_url=reverse_lazy("teacher_login"))
|
|
@@ -63,22 +70,18 @@ def teacher_onboarding_create_class(request):
|
|
|
63
70
|
Onboarding view for creating a class (and organisation if there isn't one, yet)
|
|
64
71
|
"""
|
|
65
72
|
teacher = request.user.new_teacher
|
|
66
|
-
requests = Student.objects.filter(
|
|
67
|
-
pending_class_request__teacher=teacher, new_user__is_active=True
|
|
68
|
-
)
|
|
73
|
+
requests = Student.objects.filter(pending_class_request__teacher=teacher, new_user__is_active=True)
|
|
69
74
|
|
|
70
75
|
if not teacher.school:
|
|
71
76
|
return HttpResponseRedirect(reverse_lazy("onboarding-organisation"))
|
|
72
77
|
|
|
73
78
|
if request.method == "POST":
|
|
74
|
-
form = ClassCreationForm(request.POST)
|
|
79
|
+
form = ClassCreationForm(request.POST, teacher=teacher)
|
|
75
80
|
if form.is_valid():
|
|
76
81
|
created_class = create_class(form, teacher)
|
|
77
82
|
messages.success(
|
|
78
83
|
request,
|
|
79
|
-
"The class '{className}' has been created successfully.".format(
|
|
80
|
-
className=created_class.name
|
|
81
|
-
),
|
|
84
|
+
"The class '{className}' has been created successfully.".format(className=created_class.name),
|
|
82
85
|
)
|
|
83
86
|
return HttpResponseRedirect(
|
|
84
87
|
reverse_lazy(
|
|
@@ -87,7 +90,7 @@ def teacher_onboarding_create_class(request):
|
|
|
87
90
|
)
|
|
88
91
|
)
|
|
89
92
|
else:
|
|
90
|
-
form = ClassCreationForm()
|
|
93
|
+
form = ClassCreationForm(teacher=teacher)
|
|
91
94
|
|
|
92
95
|
classes = Class.objects.filter(teacher=teacher)
|
|
93
96
|
|
|
@@ -98,13 +101,14 @@ def teacher_onboarding_create_class(request):
|
|
|
98
101
|
)
|
|
99
102
|
|
|
100
103
|
|
|
101
|
-
def create_class(form,
|
|
104
|
+
def create_class(form, class_teacher, class_creator=None):
|
|
102
105
|
classmate_progress = bool(form.cleaned_data["classmate_progress"])
|
|
103
106
|
klass = Class.objects.create(
|
|
104
107
|
name=form.cleaned_data["class_name"],
|
|
105
|
-
teacher=
|
|
108
|
+
teacher=class_teacher,
|
|
106
109
|
access_code=generate_access_code(),
|
|
107
110
|
classmates_data_viewable=classmate_progress,
|
|
111
|
+
created_by=class_teacher if class_creator is None else class_creator,
|
|
108
112
|
)
|
|
109
113
|
return klass
|
|
110
114
|
|
|
@@ -124,11 +128,9 @@ def process_edit_class(request, access_code, onboarding_done, next_url):
|
|
|
124
128
|
"""
|
|
125
129
|
klass = get_object_or_404(Class, access_code=access_code)
|
|
126
130
|
teacher = request.user.new_teacher
|
|
127
|
-
students = Student.objects.filter(
|
|
128
|
-
class_field=klass, new_user__is_active=True
|
|
129
|
-
).order_by("new_user__first_name")
|
|
131
|
+
students = Student.objects.filter(class_field=klass, new_user__is_active=True).order_by("new_user__first_name")
|
|
130
132
|
|
|
131
|
-
|
|
133
|
+
check_teacher_authorised(request, klass.teacher)
|
|
132
134
|
|
|
133
135
|
if request.method == "POST":
|
|
134
136
|
new_students_form = StudentCreationForm(klass, request.POST)
|
|
@@ -147,6 +149,8 @@ def process_edit_class(request, access_code, onboarding_done, next_url):
|
|
|
147
149
|
login_id=hashed_login_id,
|
|
148
150
|
)
|
|
149
151
|
|
|
152
|
+
TotalActivity.objects.update(student_registrations=F("student_registrations") + 1)
|
|
153
|
+
|
|
150
154
|
login_url = generate_student_url(request, new_student, login_id)
|
|
151
155
|
students_info.append(
|
|
152
156
|
{
|
|
@@ -167,7 +171,8 @@ def process_edit_class(request, access_code, onboarding_done, next_url):
|
|
|
167
171
|
"query_data": json.dumps(students_info),
|
|
168
172
|
"class_url": request.build_absolute_uri(
|
|
169
173
|
reverse(
|
|
170
|
-
"student_login",
|
|
174
|
+
"student_login",
|
|
175
|
+
kwargs={"access_code": klass.access_code},
|
|
171
176
|
)
|
|
172
177
|
),
|
|
173
178
|
},
|
|
@@ -204,12 +209,6 @@ def teacher_onboarding_edit_class(request, access_code):
|
|
|
204
209
|
)
|
|
205
210
|
|
|
206
211
|
|
|
207
|
-
def check_user_is_authorised(request, klass):
|
|
208
|
-
# check user authorised to see class
|
|
209
|
-
if request.user.new_teacher != klass.teacher:
|
|
210
|
-
raise Http404
|
|
211
|
-
|
|
212
|
-
|
|
213
212
|
@login_required(login_url=reverse_lazy("teacher_login"))
|
|
214
213
|
@user_passes_test(logged_in_as_teacher, login_url=reverse_lazy("teacher_login"))
|
|
215
214
|
def teacher_view_class(request, access_code):
|
|
@@ -217,7 +216,10 @@ def teacher_view_class(request, access_code):
|
|
|
217
216
|
Adding students to a class after the onboarding process has been completed
|
|
218
217
|
"""
|
|
219
218
|
return process_edit_class(
|
|
220
|
-
request,
|
|
219
|
+
request,
|
|
220
|
+
access_code,
|
|
221
|
+
onboarding_done=True,
|
|
222
|
+
next_url="portal/teach/class.html",
|
|
221
223
|
)
|
|
222
224
|
|
|
223
225
|
|
|
@@ -228,19 +230,16 @@ def teacher_delete_class(request, access_code):
|
|
|
228
230
|
klass = get_object_or_404(Class, access_code=access_code)
|
|
229
231
|
|
|
230
232
|
# check user authorised to see class
|
|
231
|
-
|
|
232
|
-
raise Http404
|
|
233
|
+
check_teacher_authorised(request, klass.teacher)
|
|
233
234
|
|
|
234
235
|
if Student.objects.filter(class_field=klass, new_user__is_active=True).exists():
|
|
235
236
|
messages.info(
|
|
236
237
|
request,
|
|
237
238
|
"This class still has students, please remove or delete them all before deleting the class.",
|
|
238
239
|
)
|
|
239
|
-
return HttpResponseRedirect(
|
|
240
|
-
reverse_lazy("view_class", kwargs={"access_code": access_code})
|
|
241
|
-
)
|
|
240
|
+
return HttpResponseRedirect(reverse_lazy("view_class", kwargs={"access_code": access_code}))
|
|
242
241
|
|
|
243
|
-
klass.
|
|
242
|
+
klass.anonymise()
|
|
244
243
|
|
|
245
244
|
return HttpResponseRedirect(reverse_lazy("dashboard") + "#classes")
|
|
246
245
|
|
|
@@ -251,14 +250,11 @@ def teacher_delete_students(request, access_code):
|
|
|
251
250
|
klass = get_object_or_404(Class, access_code=access_code)
|
|
252
251
|
|
|
253
252
|
# check user is authorised to deal with class
|
|
254
|
-
|
|
255
|
-
raise Http404
|
|
253
|
+
check_teacher_authorised(request, klass.teacher)
|
|
256
254
|
|
|
257
255
|
# get student objects for students to be deleted, confirming they are in the class
|
|
258
256
|
student_ids = json.loads(request.POST.get("transfer_students", "[]"))
|
|
259
|
-
students = [
|
|
260
|
-
get_object_or_404(Student, id=i, class_field=klass) for i in student_ids
|
|
261
|
-
]
|
|
257
|
+
students = [get_object_or_404(Student, id=i, class_field=klass) for i in student_ids]
|
|
262
258
|
|
|
263
259
|
def __anonymise(user):
|
|
264
260
|
# Delete all personal data from inactive user and mark as inactive.
|
|
@@ -280,35 +276,42 @@ def teacher_delete_students(request, access_code):
|
|
|
280
276
|
else: # otherwise, just delete
|
|
281
277
|
student.new_user.delete()
|
|
282
278
|
|
|
283
|
-
return HttpResponseRedirect(
|
|
284
|
-
reverse_lazy("view_class", kwargs={"access_code": access_code})
|
|
285
|
-
)
|
|
279
|
+
return HttpResponseRedirect(reverse_lazy("view_class", kwargs={"access_code": access_code}))
|
|
286
280
|
|
|
287
281
|
|
|
288
282
|
@login_required(login_url=reverse_lazy("teacher_login"))
|
|
289
283
|
@user_passes_test(logged_in_as_teacher, login_url=reverse_lazy("teacher_login"))
|
|
290
284
|
def teacher_edit_class(request, access_code):
|
|
291
285
|
"""
|
|
292
|
-
Editing class details
|
|
286
|
+
Editing additional class details. Provides functionality for:
|
|
287
|
+
- Editing the class name, sharing and joining settings
|
|
288
|
+
- Locking or unlocking specific Rapid Router levels
|
|
289
|
+
- Transferring the class to another teacher
|
|
293
290
|
"""
|
|
294
291
|
klass = get_object_or_404(Class, access_code=access_code)
|
|
295
292
|
old_teacher = klass.teacher
|
|
296
|
-
other_teachers = Teacher.objects.filter(school=old_teacher.school).exclude(
|
|
297
|
-
user=old_teacher.user
|
|
298
|
-
)
|
|
293
|
+
other_teachers = Teacher.objects.filter(school=old_teacher.school).exclude(user=old_teacher.user)
|
|
299
294
|
|
|
300
295
|
# check user authorised to see class
|
|
301
|
-
|
|
302
|
-
raise Http404
|
|
296
|
+
check_teacher_authorised(request, klass.teacher)
|
|
303
297
|
|
|
304
298
|
external_requests_message = klass.get_requests_message()
|
|
305
299
|
|
|
300
|
+
blockly_episodes = get_blockly_episodes(request)
|
|
301
|
+
python_episodes = get_python_episodes(request)
|
|
302
|
+
|
|
303
|
+
locked_levels = klass.locked_levels.all()
|
|
304
|
+
locked_levels_ids = [locked_level.id for locked_level in locked_levels]
|
|
305
|
+
|
|
306
|
+
locked_worksheet_ids = [worksheet.id for worksheet in klass.locked_worksheets.all()]
|
|
307
|
+
|
|
306
308
|
form = ClassEditForm(
|
|
307
309
|
initial={
|
|
308
310
|
"name": klass.name,
|
|
309
311
|
"classmate_progress": klass.classmates_data_viewable,
|
|
310
312
|
}
|
|
311
313
|
)
|
|
314
|
+
level_control_form = ClassLevelControlForm()
|
|
312
315
|
class_move_form = ClassMoveForm(other_teachers)
|
|
313
316
|
|
|
314
317
|
if request.method == "POST":
|
|
@@ -316,6 +319,10 @@ def teacher_edit_class(request, access_code):
|
|
|
316
319
|
form = ClassEditForm(request.POST)
|
|
317
320
|
if form.is_valid():
|
|
318
321
|
return process_edit_class_form(request, klass, form)
|
|
322
|
+
elif "level_control_submit" in request.POST:
|
|
323
|
+
level_control_form = ClassLevelControlForm(request.POST)
|
|
324
|
+
if level_control_form.is_valid():
|
|
325
|
+
return process_level_control_form(request, klass, blockly_episodes, python_episodes)
|
|
319
326
|
elif "class_move_submit" in request.POST:
|
|
320
327
|
class_move_form = ClassMoveForm(other_teachers, request.POST)
|
|
321
328
|
if class_move_form.is_valid():
|
|
@@ -327,6 +334,11 @@ def teacher_edit_class(request, access_code):
|
|
|
327
334
|
{
|
|
328
335
|
"form": form,
|
|
329
336
|
"class_move_form": class_move_form,
|
|
337
|
+
"level_control_form": level_control_form,
|
|
338
|
+
"blockly_episodes": blockly_episodes,
|
|
339
|
+
"python_episodes": python_episodes,
|
|
340
|
+
"locked_levels": locked_levels_ids,
|
|
341
|
+
"locked_worksheet_ids": locked_worksheet_ids,
|
|
330
342
|
"class": klass,
|
|
331
343
|
"external_requests_message": external_requests_message,
|
|
332
344
|
},
|
|
@@ -377,9 +389,65 @@ def process_edit_class_form(request, klass, form):
|
|
|
377
389
|
|
|
378
390
|
messages.success(request, "The class's settings have been changed successfully.")
|
|
379
391
|
|
|
380
|
-
return HttpResponseRedirect(
|
|
381
|
-
|
|
382
|
-
|
|
392
|
+
return HttpResponseRedirect(reverse_lazy("view_class", kwargs={"access_code": klass.access_code}))
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def process_level_control_form(request, klass: Class, blockly_episodes, python_episodes):
|
|
396
|
+
"""
|
|
397
|
+
Find the levels that the user wants to lock and lock them for the specific class.
|
|
398
|
+
:param request: The request sent by the user submitting the form.
|
|
399
|
+
:param klass: The class for which the levels are being locked / unlocked.
|
|
400
|
+
:param blockly_episodes: The set of Blockly Episodes (Rapid Router).
|
|
401
|
+
:param blockly_episodes: The set of Python Episodes (Python Den).
|
|
402
|
+
:return: A redirect to the teacher dashboard with a success message.
|
|
403
|
+
"""
|
|
404
|
+
levels_to_lock_ids = []
|
|
405
|
+
locked_worksheet_ids = []
|
|
406
|
+
|
|
407
|
+
mark_levels_to_lock_in_episodes(request, blockly_episodes, levels_to_lock_ids, locked_worksheet_ids)
|
|
408
|
+
mark_levels_to_lock_in_episodes(request, python_episodes, levels_to_lock_ids, locked_worksheet_ids)
|
|
409
|
+
|
|
410
|
+
klass.locked_levels.clear()
|
|
411
|
+
[klass.locked_levels.add(levels_to_lock_id) for levels_to_lock_id in levels_to_lock_ids]
|
|
412
|
+
klass.locked_worksheets.clear()
|
|
413
|
+
for locked_worksheet_id in locked_worksheet_ids:
|
|
414
|
+
klass.locked_worksheets.add(locked_worksheet_id)
|
|
415
|
+
|
|
416
|
+
messages.success(request, "Your level preferences have been saved.")
|
|
417
|
+
activity_today = DailyActivity.objects.get_or_create(date=datetime.now().date())[0]
|
|
418
|
+
activity_today.level_control_submits += 1
|
|
419
|
+
activity_today.save()
|
|
420
|
+
|
|
421
|
+
return HttpResponseRedirect(reverse_lazy("dashboard"))
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def mark_levels_to_lock_in_episodes(request, episodes, levels_to_lock_ids, locked_worksheet_ids: list):
|
|
425
|
+
"""
|
|
426
|
+
For a given set of Episodes, find which Levels are to be locked. This is done by checking the POST request data.
|
|
427
|
+
If a Level ID is missing from the request.POST, it means it needs to be locked, and if the entire Episode is missing
|
|
428
|
+
from the request.POST, it means all the Levels under that Episode need to be locked.
|
|
429
|
+
:param request: The request sent by the user submitting the form.
|
|
430
|
+
:param episodes: The set of Episodes, in this case either the Blockly Episodes or the Python Episodes.
|
|
431
|
+
:param levels_to_lock_ids: A list of Level IDs marked to be locked.
|
|
432
|
+
"""
|
|
433
|
+
for episode in episodes:
|
|
434
|
+
episode_levels = episode["levels"]
|
|
435
|
+
episode_worksheets = episode["worksheets"]
|
|
436
|
+
episode_index = f"episode{episode['id']}"
|
|
437
|
+
if episode_index in request.POST:
|
|
438
|
+
[
|
|
439
|
+
levels_to_lock_ids.append(episode_level["id"])
|
|
440
|
+
for episode_level in episode_levels
|
|
441
|
+
if f'level:{episode_level["id"]}' not in request.POST.getlist(episode_index)
|
|
442
|
+
]
|
|
443
|
+
for episode_worksheet in episode_worksheets:
|
|
444
|
+
worksheet_id = episode_worksheet["id"]
|
|
445
|
+
if f"worksheet:{worksheet_id}" not in request.POST.getlist(episode_index):
|
|
446
|
+
locked_worksheet_ids.append(worksheet_id)
|
|
447
|
+
else:
|
|
448
|
+
[levels_to_lock_ids.append(episode_level["id"]) for episode_level in episode_levels]
|
|
449
|
+
for episode_worksheet in episode_worksheets:
|
|
450
|
+
locked_worksheet_ids.append(episode_worksheet["id"])
|
|
383
451
|
|
|
384
452
|
|
|
385
453
|
def process_move_class_form(request, klass, form):
|
|
@@ -403,12 +471,9 @@ def teacher_edit_student(request, pk):
|
|
|
403
471
|
Changing a student's details
|
|
404
472
|
"""
|
|
405
473
|
student = get_object_or_404(Student, id=pk)
|
|
474
|
+
check_teacher_authorised(request, student.class_field.teacher)
|
|
406
475
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
name_form = TeacherEditStudentForm(
|
|
410
|
-
student, initial={"name": student.new_user.first_name}
|
|
411
|
-
)
|
|
476
|
+
name_form = TeacherEditStudentForm(student, initial={"name": student.new_user.first_name})
|
|
412
477
|
|
|
413
478
|
password_form = TeacherSetStudentPass()
|
|
414
479
|
set_password_mode = False
|
|
@@ -423,7 +488,8 @@ def teacher_edit_student(request, pk):
|
|
|
423
488
|
student.save()
|
|
424
489
|
|
|
425
490
|
messages.success(
|
|
426
|
-
request,
|
|
491
|
+
request,
|
|
492
|
+
"The student's details have been changed successfully.",
|
|
427
493
|
)
|
|
428
494
|
|
|
429
495
|
return HttpResponseRedirect(
|
|
@@ -462,10 +528,7 @@ def process_reset_password_form(request, student, password_form):
|
|
|
462
528
|
login_url = request.build_absolute_uri(
|
|
463
529
|
reverse(
|
|
464
530
|
"student_direct_login",
|
|
465
|
-
kwargs={
|
|
466
|
-
"user_id": student.new_user.id,
|
|
467
|
-
"login_id": uuidstr,
|
|
468
|
-
},
|
|
531
|
+
kwargs={"user_id": student.new_user.id, "login_id": uuidstr},
|
|
469
532
|
)
|
|
470
533
|
)
|
|
471
534
|
|
|
@@ -478,9 +541,12 @@ def process_reset_password_form(request, student, password_form):
|
|
|
478
541
|
}
|
|
479
542
|
]
|
|
480
543
|
|
|
544
|
+
handle_reset_password_tracking(request, "SCHOOL_STUDENT")
|
|
481
545
|
student.new_user.set_password(new_password)
|
|
482
546
|
student.new_user.save()
|
|
483
547
|
student.login_id = login_id
|
|
548
|
+
clear_ratelimit_cache_for_user(f"{student.new_user.first_name},{student.class_field.access_code}")
|
|
549
|
+
student.blocked_time = datetime.now(tz=pytz.utc) - timedelta(days=1)
|
|
484
550
|
student.save()
|
|
485
551
|
|
|
486
552
|
return render(
|
|
@@ -501,12 +567,6 @@ def process_reset_password_form(request, student, password_form):
|
|
|
501
567
|
)
|
|
502
568
|
|
|
503
569
|
|
|
504
|
-
def check_if_edit_authorised(request, student):
|
|
505
|
-
# check user is authorised to edit student
|
|
506
|
-
if request.user.new_teacher != student.class_field.teacher:
|
|
507
|
-
raise Http404
|
|
508
|
-
|
|
509
|
-
|
|
510
570
|
@login_required(login_url=reverse_lazy("teacher_login"))
|
|
511
571
|
@user_passes_test(logged_in_as_teacher, login_url=reverse_lazy("teacher_login"))
|
|
512
572
|
def teacher_dismiss_students(request, access_code):
|
|
@@ -515,13 +575,11 @@ def teacher_dismiss_students(request, access_code):
|
|
|
515
575
|
"""
|
|
516
576
|
klass = get_object_or_404(Class, access_code=access_code)
|
|
517
577
|
|
|
518
|
-
|
|
578
|
+
check_teacher_authorised(request, klass.teacher)
|
|
519
579
|
|
|
520
580
|
# get student objects for students to be dismissed, confirming they are in the class
|
|
521
581
|
student_ids = json.loads(request.POST.get("transfer_students", "[]"))
|
|
522
|
-
students = [
|
|
523
|
-
get_object_or_404(Student, id=i, class_field=klass) for i in student_ids
|
|
524
|
-
]
|
|
582
|
+
students = [get_object_or_404(Student, id=i, class_field=klass) for i in student_ids]
|
|
525
583
|
|
|
526
584
|
TeacherDismissStudentsFormSet = formset_factory(
|
|
527
585
|
wraps(TeacherDismissStudentsForm)(partial(TeacherDismissStudentsForm)),
|
|
@@ -553,38 +611,59 @@ def teacher_dismiss_students(request, access_code):
|
|
|
553
611
|
)
|
|
554
612
|
|
|
555
613
|
|
|
556
|
-
def check_if_dismiss_authorised(request, klass):
|
|
557
|
-
# check user is authorised to deal with class
|
|
558
|
-
if request.user.new_teacher != klass.teacher:
|
|
559
|
-
raise Http404
|
|
560
|
-
|
|
561
|
-
|
|
562
614
|
def is_right_dismiss_form(request):
|
|
563
615
|
return request.method == "POST" and "submit_dismiss" in request.POST
|
|
564
616
|
|
|
565
617
|
|
|
566
618
|
def process_dismiss_student_form(request, formset, klass, access_code):
|
|
619
|
+
failed_users = [] # users that failed to be transferred
|
|
567
620
|
for data in formset.cleaned_data:
|
|
621
|
+
# check if email is already used
|
|
622
|
+
users_with_email = User.objects.filter(email=data["email"])
|
|
623
|
+
# email is already taken, skip this user
|
|
624
|
+
if users_with_email.exists():
|
|
625
|
+
failed_users.append(data["orig_name"])
|
|
626
|
+
continue
|
|
627
|
+
|
|
568
628
|
student = get_object_or_404(
|
|
569
|
-
Student,
|
|
629
|
+
Student,
|
|
630
|
+
class_field=klass,
|
|
631
|
+
new_user__first_name__iexact=data["orig_name"],
|
|
570
632
|
)
|
|
571
633
|
|
|
634
|
+
students_levels = Level.objects.filter(owner=student.new_user.userprofile).all()
|
|
635
|
+
for level in students_levels:
|
|
636
|
+
level.shared_with.set([])
|
|
637
|
+
level.save()
|
|
638
|
+
|
|
572
639
|
student.class_field = None
|
|
573
640
|
student.new_user.first_name = data["name"]
|
|
574
|
-
student.new_user.username = data["
|
|
641
|
+
student.new_user.username = data["email"]
|
|
575
642
|
student.new_user.email = data["email"]
|
|
643
|
+
student.user.is_verified = False
|
|
576
644
|
student.save()
|
|
577
645
|
student.new_user.save()
|
|
578
|
-
student.
|
|
646
|
+
student.user.save()
|
|
579
647
|
|
|
580
|
-
|
|
648
|
+
# log the data
|
|
649
|
+
joinrelease = JoinReleaseStudent.objects.create(student=student, action_type=JoinReleaseStudent.RELEASE)
|
|
650
|
+
joinrelease.save()
|
|
581
651
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
652
|
+
send_verification_email(request, student.new_user, data, school=klass.teacher.school)
|
|
653
|
+
|
|
654
|
+
if not failed_users:
|
|
655
|
+
messages.success(
|
|
656
|
+
request,
|
|
657
|
+
"The students have been released successfully from the class.",
|
|
658
|
+
)
|
|
659
|
+
else:
|
|
660
|
+
messages.warning(
|
|
661
|
+
request,
|
|
662
|
+
f"The following students could not be released: {', '.join(failed_users)}. "
|
|
663
|
+
"Please make sure the email has not been registered to another account.",
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
return HttpResponseRedirect(reverse_lazy("view_class", kwargs={"access_code": access_code}))
|
|
588
667
|
|
|
589
668
|
|
|
590
669
|
@login_required(login_url=reverse_lazy("teacher_login"))
|
|
@@ -596,15 +675,13 @@ def teacher_class_password_reset(request, access_code):
|
|
|
596
675
|
klass = get_object_or_404(Class, access_code=access_code)
|
|
597
676
|
|
|
598
677
|
# check user authorised to see class
|
|
599
|
-
|
|
600
|
-
raise Http404
|
|
678
|
+
check_teacher_authorised(request, klass.teacher)
|
|
601
679
|
|
|
602
680
|
student_ids = json.loads(request.POST.get("transfer_students", "[]"))
|
|
603
|
-
students = [
|
|
604
|
-
get_object_or_404(Student, id=i, class_field=klass) for i in student_ids
|
|
605
|
-
]
|
|
681
|
+
students = [get_object_or_404(Student, id=i, class_field=klass) for i in student_ids]
|
|
606
682
|
|
|
607
683
|
students_info = []
|
|
684
|
+
handle_reset_password_tracking(request, "SCHOOL_STUDENT", access_code)
|
|
608
685
|
for student in students:
|
|
609
686
|
password = generate_password(STUDENT_PASSWORD_LENGTH)
|
|
610
687
|
|
|
@@ -623,6 +700,8 @@ def teacher_class_password_reset(request, access_code):
|
|
|
623
700
|
student.new_user.set_password(password)
|
|
624
701
|
student.new_user.save()
|
|
625
702
|
student.login_id = hashed_login_id
|
|
703
|
+
clear_ratelimit_cache_for_user(f"{student.new_user.first_name},{access_code}")
|
|
704
|
+
student.blocked_time = datetime.now(tz=pytz.utc) - timedelta(days=1)
|
|
626
705
|
student.save()
|
|
627
706
|
|
|
628
707
|
return render(
|
|
@@ -650,8 +729,7 @@ def teacher_move_students(request, access_code):
|
|
|
650
729
|
klass = get_object_or_404(Class, access_code=access_code)
|
|
651
730
|
|
|
652
731
|
# check user is authorised to deal with class
|
|
653
|
-
|
|
654
|
-
raise Http404
|
|
732
|
+
check_teacher_authorised(request, klass.teacher)
|
|
655
733
|
|
|
656
734
|
transfer_students = request.POST.get("transfer_students", "[]")
|
|
657
735
|
|
|
@@ -666,7 +744,11 @@ def teacher_move_students(request, access_code):
|
|
|
666
744
|
return render(
|
|
667
745
|
request,
|
|
668
746
|
"portal/teach/teacher_move_students.html",
|
|
669
|
-
{
|
|
747
|
+
{
|
|
748
|
+
"transfer_students": transfer_students,
|
|
749
|
+
"old_class": klass,
|
|
750
|
+
"form": form,
|
|
751
|
+
},
|
|
670
752
|
)
|
|
671
753
|
|
|
672
754
|
|
|
@@ -685,20 +767,15 @@ def teacher_move_students_to_class(request, access_code):
|
|
|
685
767
|
transfer_students_ids = json.loads(request.POST.get("transfer_students", "[]"))
|
|
686
768
|
|
|
687
769
|
# get student objects for students to be transferred, confirming they are in the old class still
|
|
688
|
-
transfer_students = [
|
|
689
|
-
get_object_or_404(Student, id=i, class_field=old_class)
|
|
690
|
-
for i in transfer_students_ids
|
|
691
|
-
]
|
|
770
|
+
transfer_students = [get_object_or_404(Student, id=i, class_field=old_class) for i in transfer_students_ids]
|
|
692
771
|
|
|
693
772
|
# get new class' students
|
|
694
|
-
new_class_students = Student.objects.filter(
|
|
695
|
-
|
|
696
|
-
)
|
|
773
|
+
new_class_students = Student.objects.filter(class_field=new_class, new_user__is_active=True).order_by(
|
|
774
|
+
"new_user__first_name"
|
|
775
|
+
)
|
|
697
776
|
|
|
698
777
|
TeacherMoveStudentDisambiguationFormSet = formset_factory(
|
|
699
|
-
wraps(TeacherMoveStudentDisambiguationForm)(
|
|
700
|
-
partial(TeacherMoveStudentDisambiguationForm)
|
|
701
|
-
),
|
|
778
|
+
wraps(TeacherMoveStudentDisambiguationForm)(partial(TeacherMoveStudentDisambiguationForm)),
|
|
702
779
|
extra=0,
|
|
703
780
|
formset=BaseTeacherMoveStudentsDisambiguationFormSet,
|
|
704
781
|
)
|
|
@@ -717,9 +794,7 @@ def teacher_move_students_to_class(request, access_code):
|
|
|
717
794
|
for student in transfer_students
|
|
718
795
|
]
|
|
719
796
|
|
|
720
|
-
formset = TeacherMoveStudentDisambiguationFormSet(
|
|
721
|
-
new_class, initial=initial_data
|
|
722
|
-
)
|
|
797
|
+
formset = TeacherMoveStudentDisambiguationFormSet(new_class, initial=initial_data)
|
|
723
798
|
|
|
724
799
|
return render(
|
|
725
800
|
request,
|
|
@@ -735,12 +810,11 @@ def teacher_move_students_to_class(request, access_code):
|
|
|
735
810
|
|
|
736
811
|
|
|
737
812
|
def check_if_move_authorised(request, old_class, new_class):
|
|
738
|
-
|
|
739
|
-
if request.user.new_teacher != old_class.teacher:
|
|
740
|
-
raise Http404
|
|
813
|
+
teacher = request.user.new_teacher
|
|
741
814
|
|
|
742
|
-
# check teacher
|
|
743
|
-
|
|
815
|
+
# check teacher has permission to edit old_class and that both classes
|
|
816
|
+
# are in the same school
|
|
817
|
+
if (not teacher.is_admin and teacher != old_class.teacher) or teacher.school != new_class.teacher.school:
|
|
744
818
|
raise Http404
|
|
745
819
|
|
|
746
820
|
|
|
@@ -765,14 +839,14 @@ def process_move_students_form(request, formset, old_class, new_class):
|
|
|
765
839
|
student.new_user.save()
|
|
766
840
|
|
|
767
841
|
messages.success(request, "The students have been transferred successfully.")
|
|
768
|
-
return HttpResponseRedirect(
|
|
769
|
-
reverse_lazy("view_class", kwargs={"access_code": old_class.access_code})
|
|
770
|
-
)
|
|
842
|
+
return HttpResponseRedirect(reverse_lazy("view_class", kwargs={"access_code": old_class.access_code}))
|
|
771
843
|
|
|
772
844
|
|
|
773
845
|
class DownloadType(Enum):
|
|
774
846
|
CSV = 1
|
|
775
847
|
LOGIN_CARDS = 2
|
|
848
|
+
PRIMARY_PACK = 3
|
|
849
|
+
PYTHON_PACK = 4
|
|
776
850
|
|
|
777
851
|
|
|
778
852
|
@login_required(login_url=reverse_lazy("teacher_login"))
|
|
@@ -785,36 +859,29 @@ def teacher_print_reminder_cards(request, access_code):
|
|
|
785
859
|
|
|
786
860
|
# Define constants that determine the look of the cards
|
|
787
861
|
PAGE_WIDTH, PAGE_HEIGHT = A4
|
|
788
|
-
PAGE_MARGIN =
|
|
789
|
-
INTER_CARD_MARGIN =
|
|
790
|
-
CARD_PADDING =
|
|
862
|
+
PAGE_MARGIN = PAGE_WIDTH // 16
|
|
863
|
+
INTER_CARD_MARGIN = PAGE_WIDTH // 64
|
|
864
|
+
CARD_PADDING = PAGE_WIDTH // 48
|
|
791
865
|
|
|
792
866
|
# rows and columns on page
|
|
793
867
|
NUM_X = REMINDER_CARDS_PDF_COLUMNS
|
|
794
868
|
NUM_Y = REMINDER_CARDS_PDF_ROWS
|
|
795
869
|
|
|
796
|
-
CARD_WIDTH =
|
|
797
|
-
CARD_HEIGHT =
|
|
870
|
+
CARD_WIDTH = (PAGE_WIDTH - PAGE_MARGIN * 2) // NUM_X
|
|
871
|
+
CARD_HEIGHT = (PAGE_HEIGHT - PAGE_MARGIN * 4) // NUM_Y
|
|
798
872
|
|
|
799
873
|
CARD_INNER_HEIGHT = CARD_HEIGHT - CARD_PADDING * 2
|
|
800
874
|
|
|
801
|
-
logo_image = ImageReader(
|
|
802
|
-
staticfiles_storage.path("portal/img/logo_cfl_reminder_cards.jpg")
|
|
803
|
-
)
|
|
875
|
+
logo_image = ImageReader(staticfiles_storage.path("portal/img/logo_cfl_reminder_cards.jpg"))
|
|
804
876
|
|
|
805
877
|
klass = get_object_or_404(Class, access_code=access_code)
|
|
806
878
|
# Check auth
|
|
807
|
-
|
|
808
|
-
raise Http404
|
|
879
|
+
check_teacher_authorised(request, klass.teacher)
|
|
809
880
|
|
|
810
881
|
# Use data from the query string if given
|
|
811
882
|
student_data = get_student_data(request)
|
|
812
|
-
student_login_link = request.build_absolute_uri(
|
|
813
|
-
|
|
814
|
-
)
|
|
815
|
-
class_login_link = request.build_absolute_uri(
|
|
816
|
-
reverse("student_login", kwargs={"access_code": access_code})
|
|
817
|
-
)
|
|
883
|
+
student_login_link = request.build_absolute_uri(reverse("student_login_access_code"))
|
|
884
|
+
class_login_link = request.build_absolute_uri(reverse("student_login", kwargs={"access_code": access_code}))
|
|
818
885
|
|
|
819
886
|
# Now draw everything
|
|
820
887
|
x = 0
|
|
@@ -829,9 +896,7 @@ def teacher_print_reminder_cards(request, access_code):
|
|
|
829
896
|
p.drawString(PAGE_MARGIN, PAGE_MARGIN / 2, REMINDER_CARDS_PDF_WARNING_TEXT)
|
|
830
897
|
|
|
831
898
|
left = PAGE_MARGIN + x * CARD_WIDTH + x * INTER_CARD_MARGIN * 2
|
|
832
|
-
bottom = (
|
|
833
|
-
PAGE_HEIGHT - PAGE_MARGIN - (y + 1) * CARD_HEIGHT - y * INTER_CARD_MARGIN
|
|
834
|
-
)
|
|
899
|
+
bottom = PAGE_HEIGHT - PAGE_MARGIN - (y + 1) * CARD_HEIGHT - y * INTER_CARD_MARGIN
|
|
835
900
|
|
|
836
901
|
inner_bottom = bottom + CARD_PADDING
|
|
837
902
|
|
|
@@ -839,16 +904,19 @@ def teacher_print_reminder_cards(request, access_code):
|
|
|
839
904
|
p.setStrokeColor(black)
|
|
840
905
|
p.rect(left, bottom, CARD_WIDTH, CARD_HEIGHT)
|
|
841
906
|
|
|
907
|
+
card_logo_height = CARD_HEIGHT - INTER_CARD_MARGIN * 2
|
|
908
|
+
|
|
842
909
|
# logo
|
|
843
910
|
p.drawImage(
|
|
844
911
|
logo_image,
|
|
845
|
-
left,
|
|
912
|
+
left + INTER_CARD_MARGIN,
|
|
846
913
|
bottom + INTER_CARD_MARGIN,
|
|
847
|
-
height=
|
|
914
|
+
height=card_logo_height,
|
|
848
915
|
preserveAspectRatio=True,
|
|
916
|
+
anchor="w",
|
|
849
917
|
)
|
|
850
918
|
|
|
851
|
-
text_left = left + logo_image.getSize()[0]
|
|
919
|
+
text_left = left + INTER_CARD_MARGIN + (logo_image.getSize()[0] / logo_image.getSize()[1]) * card_logo_height
|
|
852
920
|
|
|
853
921
|
# student details
|
|
854
922
|
p.setFillColor(black)
|
|
@@ -859,11 +927,7 @@ def teacher_print_reminder_cards(request, access_code):
|
|
|
859
927
|
f"Class code: {klass.access_code} at {student_login_link}",
|
|
860
928
|
)
|
|
861
929
|
p.setFont("Helvetica-BoldOblique", 12)
|
|
862
|
-
p.drawString(
|
|
863
|
-
text_left,
|
|
864
|
-
inner_bottom + CARD_INNER_HEIGHT * 0.6,
|
|
865
|
-
"OR",
|
|
866
|
-
)
|
|
930
|
+
p.drawString(text_left, inner_bottom + CARD_INNER_HEIGHT * 0.6, "OR")
|
|
867
931
|
p.setFont("Helvetica", 12)
|
|
868
932
|
p.drawString(
|
|
869
933
|
text_left + 22,
|
|
@@ -898,12 +962,9 @@ def teacher_download_csv(request, access_code):
|
|
|
898
962
|
|
|
899
963
|
klass = get_object_or_404(Class, access_code=access_code)
|
|
900
964
|
# Check auth
|
|
901
|
-
|
|
902
|
-
raise Http404
|
|
965
|
+
check_teacher_authorised(request, klass.teacher)
|
|
903
966
|
|
|
904
|
-
class_url = request.build_absolute_uri(
|
|
905
|
-
reverse("student_login", kwargs={"access_code": access_code})
|
|
906
|
-
)
|
|
967
|
+
class_url = request.build_absolute_uri(reverse("student_login", kwargs={"access_code": access_code}))
|
|
907
968
|
|
|
908
969
|
# Use data from the query string if given
|
|
909
970
|
student_data = get_student_data(request)
|
|
@@ -911,9 +972,7 @@ def teacher_download_csv(request, access_code):
|
|
|
911
972
|
writer = csv.writer(response)
|
|
912
973
|
writer.writerow([access_code, class_url])
|
|
913
974
|
for student in student_data:
|
|
914
|
-
writer.writerow(
|
|
915
|
-
[student["name"], student["password"], student["login_url"]]
|
|
916
|
-
)
|
|
975
|
+
writer.writerow([student["name"], student["password"], student["login_url"]])
|
|
917
976
|
|
|
918
977
|
count_student_details_click(DownloadType.CSV)
|
|
919
978
|
|
|
@@ -940,6 +999,15 @@ def compute_show_page_end(p, x, y):
|
|
|
940
999
|
p.showPage()
|
|
941
1000
|
|
|
942
1001
|
|
|
1002
|
+
def count_student_pack_downloads_click(student_pack_type):
|
|
1003
|
+
activity_today = DailyActivity.objects.get_or_create(date=datetime.now().date())[0]
|
|
1004
|
+
if DownloadType(student_pack_type) == DownloadType.PRIMARY_PACK:
|
|
1005
|
+
activity_today.primary_coding_club_downloads += 1
|
|
1006
|
+
else:
|
|
1007
|
+
raise Exception("Unknown download type")
|
|
1008
|
+
activity_today.save()
|
|
1009
|
+
|
|
1010
|
+
|
|
943
1011
|
def count_student_details_click(download_type):
|
|
944
1012
|
activity_today = DailyActivity.objects.get_or_create(date=datetime.now().date())[0]
|
|
945
1013
|
|
|
@@ -951,22 +1019,3 @@ def count_student_details_click(download_type):
|
|
|
951
1019
|
raise Exception("Unknown download type")
|
|
952
1020
|
|
|
953
1021
|
activity_today.save()
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
def invite_teacher(request):
|
|
957
|
-
if request.method == "POST":
|
|
958
|
-
invite_teacher_form = InviteTeacherForm(data=request.POST)
|
|
959
|
-
if invite_teacher_form.is_valid():
|
|
960
|
-
email_address = invite_teacher_form.cleaned_data["email"]
|
|
961
|
-
email_message = email_messages.inviteTeacherEmail(request)
|
|
962
|
-
send_email(
|
|
963
|
-
INVITE_FROM,
|
|
964
|
-
[email_address],
|
|
965
|
-
email_message["subject"],
|
|
966
|
-
email_message["message"],
|
|
967
|
-
)
|
|
968
|
-
return render(request, "portal/email_invitation_sent.html")
|
|
969
|
-
|
|
970
|
-
return render(
|
|
971
|
-
request, "portal/teach/invite.html", {"invite_form": InviteTeacherForm()}
|
|
972
|
-
)
|