├── .codecov.yml ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── new_task.md ├── pull_request_template.md └── workflows │ ├── ci.yml │ ├── issues.yml │ ├── publish-python-package.yml │ ├── semantic-pull-request-check.yml │ └── snyk.yaml ├── .gitignore ├── .venv └── .gitkeep ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── MANIFEST.in ├── Pipfile ├── Pipfile.lock ├── README.md ├── cfl_common ├── MANIFEST.in ├── Pipfile ├── Pipfile.lock ├── __init__.py ├── common │ ├── __init__.py │ ├── app_settings.py │ ├── apps.py │ ├── context_processors.py │ ├── csp_config.py │ ├── fixtures │ │ ├── aimmo_characters.json │ │ ├── aimmo_characters2.json │ │ └── aimmo_characters3.json │ ├── helpers │ │ ├── __init__.py │ │ ├── data_migration_loader.py │ │ ├── emails.py │ │ ├── generators.py │ │ └── organisation.py │ ├── mail.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_emailverification.py │ │ ├── 0003_aimmocharacter.py │ │ ├── 0004_add_aimmocharacters.py │ │ ├── 0005_add_worksheets.py │ │ ├── 0006_update_aimmo_character_image_path.py │ │ ├── 0007_add_pdf_names_to_first_two_worksheets.py │ │ ├── 0008_unlock_worksheet_3.py │ │ ├── 0009_add_blocked_time_to_teacher_and_student.py │ │ ├── 0010_remove_teacher_title.py │ │ ├── 0011_student_login_id.py │ │ ├── 0012_usersession.py │ │ ├── 0013_class_school.py │ │ ├── 0014_login_type.py │ │ ├── 0015_dailyactivity.py │ │ ├── 0016_joinreleasestudent.py │ │ ├── 0017_copy_email_to_username.py │ │ ├── 0018_update_aimmo_character_image_path.py │ │ ├── 0019_aimmocharacter_alt.py │ │ ├── 0020_class_is_active_and_null_access_code.py │ │ ├── 0021_school_is_active.py │ │ ├── 0022_school_cleanup.py │ │ ├── 0023_userprofile_aimmo_badges.py │ │ ├── 0024_teacher_invited_by.py │ │ ├── 0025_schoolteacherinvitation.py │ │ ├── 0026_teacher_remove_join_request.py │ │ ├── 0027_class_created_by.py │ │ ├── 0028_coding_club_downloads.py │ │ ├── 0029_dynamicelement.py │ │ ├── 0030_add_maintenance_banner.py │ │ ├── 0031_improve_admin_panel.py │ │ ├── 0032_dailyactivity_level_control_submits.py │ │ ├── 0033_password_reset_tracking_fields.py │ │ ├── 0034_dailyactivity_daily_school_student_lockout_reset.py │ │ ├── 0035_rename_lockout_fields.py │ │ ├── 0036_rename_awaiting_email_verification_userprofile_is_verified.py │ │ ├── 0037_migrate_email_verification.py │ │ ├── 0038_delete_emailverification.py │ │ ├── 0039_copy_email_to_username.py │ │ ├── 0040_school_county.py │ │ ├── 0041_populate_gb_counties.py │ │ ├── 0042_totalactivity.py │ │ ├── 0043_add_total_activity.py │ │ ├── 0044_update_activity_models.py │ │ ├── 0045_otp.py │ │ ├── 0046_alter_school_country.py │ │ ├── 0047_delete_school_postcode.py │ │ ├── 0048_unique_school_names.py │ │ ├── 0049_anonymise_orphan_users.py │ │ ├── 0050_anonymise_orphan_schools.py │ │ ├── 0051_verify_returning_users.py │ │ ├── 0052_add_cse_fields.py │ │ ├── 0053_clean_class_data.py │ │ ├── 0054_delete_aimmo_models.py │ │ ├── 0055_alter_schoolteacherinvitation_token.py │ │ ├── 0056_set_non_school_teachers_as_non_admins.py │ │ ├── 0057_teacher_teacher__is_admin.py │ │ ├── 0058_userprofile_google_refresh_token_and_more.py │ │ └── __init__.py │ ├── models.py │ ├── permissions.py │ ├── static │ │ └── common │ │ │ └── img │ │ │ ├── RR_logo.svg │ │ │ └── brain.svg │ ├── templates │ │ └── common │ │ │ ├── freshdesk_widget.html │ │ │ └── onetrust_cookies_consent_notice.html │ ├── tests │ │ ├── __init__.py │ │ ├── test_migration_anonymise_orphan_schools.py │ │ ├── test_migration_anonymise_orphan_users.py │ │ ├── test_migration_blocked_time.py │ │ ├── test_migration_remove_teacher_title.py │ │ ├── test_migration_unique_school_names.py │ │ ├── test_migration_verify_returning_users.py │ │ ├── test_models.py │ │ └── utils │ │ │ ├── __init__.py │ │ │ ├── classes.py │ │ │ ├── email.py │ │ │ ├── organisation.py │ │ │ ├── student.py │ │ │ ├── teacher.py │ │ │ └── user.py │ └── utils.py ├── pyproject.toml └── setup.py ├── cypress.config.js ├── deploy ├── __init__.py ├── captcha.py ├── middleware │ ├── __init__.py │ ├── admin_access.py │ ├── basicauth.py │ ├── exceptionlogging.py │ ├── maintenance.py │ ├── screentime_warning.py │ ├── security.py │ ├── session_timeout.py │ └── tmp_basic_auth.py ├── static │ ├── apple-touch-icon-120x120.png │ ├── apple-touch-icon-152x152.png │ ├── apple-touch-icon-180x180.png │ ├── apple-touch-icon-76x76.png │ ├── apple-touch-icon.png │ └── robots.txt ├── templates │ └── deploy │ │ └── csrf_failure.html └── views.py ├── example_project ├── __init__.py ├── portal_test_settings.py ├── settings.py ├── urls.py └── wsgi.py ├── manage.py ├── package.json ├── portal ├── __init__.py ├── admin.py ├── app_settings.py ├── backends.py ├── beta.py ├── context_processors.py ├── forms │ ├── __init__.py │ ├── admin.py │ ├── dotmailer.py │ ├── error_messages.py │ ├── invite_teacher.py │ ├── organisation.py │ ├── play.py │ ├── registration.py │ └── teach.py ├── handlers.py ├── helpers │ ├── __init__.py │ ├── captcha.py │ ├── decorators.py │ ├── password.py │ ├── ratelimit.py │ ├── regexes.py │ └── request_handlers.py ├── migrations │ ├── 0001_squashed_0041_new_news.py │ ├── 0042_school_country.py │ ├── 0043_auto_20150430_0952.py │ ├── 0044_auto_20150430_0959.py │ ├── 0045_auto_20150430_1446.py │ ├── 0046_auto_20150723_1101.py │ ├── 0047_remove_userprofile_avatar.py │ ├── 0048_plural_management_frontnews.py │ ├── 0049_refactor_emailverifications.py │ ├── 0050_refactor_emailverifications_2.py │ ├── 0051_add_missing_ev_records.py │ ├── 0052_refactor_emailverifications_3.py │ ├── 0053_refactor_teacher_student_1.py │ ├── 0054_pending_join_request_can_be_blank.py │ ├── 0055_add_preview_user.py │ ├── 0056_remove_preview_user.py │ ├── 0057_delete_frontpagenews.py │ ├── 0058_move_to_common_models.py │ ├── 0059_move_email_verifications_to_common.py │ ├── 0060_delete_guardian.py │ ├── 0061_make_portaladmin_teacher.py │ ├── 0062_verify_portaladmin.py │ └── __init__.py ├── mixins │ ├── __init__.py │ └── cron_mixin.py ├── models.py ├── permissions │ ├── __init__.py │ └── is_cron_request_from_google.py ├── pipeline_compilers │ ├── __init__.py │ └── libsass_compiler.py ├── static │ └── portal │ │ ├── fonts │ │ └── bootstrap │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ └── glyphicons-halflings-regular.woff2 │ │ ├── img │ │ ├── 10_years_anniversary.png │ │ ├── 10x_logo.png │ │ ├── RR_logo_grass_background.png │ │ ├── RR_logo_green.svg │ │ ├── RR_logo_simple.png │ │ ├── about_us_cfl.jpg │ │ ├── about_us_hero.jpg │ │ ├── about_us_ocado.jpg │ │ ├── barefoot_logo.png │ │ ├── bcs_logo.png │ │ ├── clubs.png │ │ ├── coding_club_hero.jpg │ │ ├── coding_club_python_pack.png │ │ ├── colorboxImages │ │ │ ├── border.png │ │ │ ├── controls.png │ │ │ ├── loading.gif │ │ │ ├── loading_background.png │ │ │ └── overlay.png │ │ ├── confirmation_tick.png │ │ ├── cross.png │ │ ├── dee.png │ │ ├── facebook.png │ │ ├── favicon.ico │ │ ├── get_involved_hero.png │ │ ├── gitbook.png │ │ ├── gitbook_space.png │ │ ├── github.png │ │ ├── github_hero.png │ │ ├── gla_logo.png │ │ ├── hamburger.png │ │ ├── help_and_support_hero.jpg │ │ ├── home_educate.png │ │ ├── home_educate_hero.jpg │ │ ├── home_learning_hero.jpg │ │ ├── home_play.png │ │ ├── home_play_hero.jpg │ │ ├── hope_logo.png │ │ ├── howe_dell_1.png │ │ ├── howe_dell_2.png │ │ ├── howe_dell_3.png │ │ ├── icl_logo.png │ │ ├── icon_controller.png │ │ ├── icon_free.png │ │ ├── icon_globe.png │ │ ├── icon_piechart.png │ │ ├── icon_step_by_step.png │ │ ├── icon_tracking.png │ │ ├── icon_uk_flag.png │ │ ├── kirsty.png │ │ ├── logo_cfl.png │ │ ├── logo_cfl_powered.svg │ │ ├── logo_cfl_reminder_cards.jpg │ │ ├── logo_cfl_white_landscape.png │ │ ├── logo_ocado.png │ │ ├── logo_ocado_group.png │ │ ├── logo_ocado_group.svg │ │ ├── logo_ocado_group_white.svg │ │ ├── logo_python_den.svg │ │ ├── long_europe_map.png │ │ ├── mc_saatchi_logo.png │ │ ├── nigel.png │ │ ├── oval_blue.svg │ │ ├── oval_pink.svg │ │ ├── oval_yellow.svg │ │ ├── paper_plane.png │ │ ├── phil.png │ │ ├── polygon_blue.svg │ │ ├── polygon_pink.svg │ │ ├── polygon_yellow.svg │ │ ├── pressure_cooker_logo.png │ │ ├── python_den.png │ │ ├── python_den_banner.svg │ │ ├── rapid_router.png │ │ ├── rapid_router_landing_hero.png │ │ ├── rapidrouter.png │ │ ├── resources_hero.jpg │ │ ├── resources_montage.jpg │ │ ├── reuben.png │ │ ├── rob.png │ │ ├── rr_advanced.png │ │ ├── rr_beginner.png │ │ ├── rr_intermediate.png │ │ ├── sadface.png │ │ ├── sharon_harrison.jpg │ │ ├── sian.png │ │ ├── teaching_resources_hero.jpg │ │ ├── ten_year_map_pin.svg │ │ ├── thumbnail_educate_rapid_router.png │ │ ├── thumbnail_educate_resources.png │ │ ├── thumbnail_intro_c4l.jpg │ │ ├── thumbnail_play_rapid_router.png │ │ ├── thumbnail_python_den.png │ │ ├── twitter.png │ │ ├── universities.png │ │ └── wes.png │ │ ├── js │ │ ├── bootstrap.min.js │ │ ├── carouselCards.js │ │ ├── common.js │ │ ├── independentLogin.js │ │ ├── independentRegistration.js │ │ ├── join_create_game_toggle.js │ │ ├── jquery.placeholder.js │ │ ├── levelControl.js │ │ ├── lib │ │ │ ├── jquery-video-lightning.js │ │ │ ├── jquery.colorbox.js │ │ │ ├── jquery.easy-ticker.js │ │ │ ├── jquery.min.js │ │ │ ├── modernizr-build.js │ │ │ └── papaparse.min.js │ │ ├── organisation_manage.js │ │ ├── passwordStrength.js │ │ ├── play.js │ │ ├── resetPassword.js │ │ ├── riveted.min.js │ │ ├── school.js │ │ ├── sticky_subnav.js │ │ ├── studentLogin.js │ │ ├── teach_browser.js │ │ ├── teach_class.js │ │ ├── teacherEditStudent.js │ │ ├── teacherLogin.js │ │ └── tenYearMap.js │ │ ├── sass │ │ ├── bootstrap.scss │ │ ├── bootstrap │ │ │ ├── _alerts.scss │ │ │ ├── _badges.scss │ │ │ ├── _breadcrumbs.scss │ │ │ ├── _button-groups.scss │ │ │ ├── _buttons.scss │ │ │ ├── _carousel.scss │ │ │ ├── _close.scss │ │ │ ├── _code.scss │ │ │ ├── _component-animations.scss │ │ │ ├── _dropdowns.scss │ │ │ ├── _forms.scss │ │ │ ├── _glyphicons.scss │ │ │ ├── _grid.scss │ │ │ ├── _input-groups.scss │ │ │ ├── _jumbotron.scss │ │ │ ├── _labels.scss │ │ │ ├── _list-group.scss │ │ │ ├── _media.scss │ │ │ ├── _mixins.scss │ │ │ ├── _modals.scss │ │ │ ├── _navbar.scss │ │ │ ├── _navs.scss │ │ │ ├── _normalize.scss │ │ │ ├── _pager.scss │ │ │ ├── _pagination.scss │ │ │ ├── _panels.scss │ │ │ ├── _popovers.scss │ │ │ ├── _print.scss │ │ │ ├── _progress-bars.scss │ │ │ ├── _responsive-embed.scss │ │ │ ├── _responsive-utilities.scss │ │ │ ├── _scaffolding.scss │ │ │ ├── _tables.scss │ │ │ ├── _theme.scss │ │ │ ├── _thumbnails.scss │ │ │ ├── _tooltip.scss │ │ │ ├── _type.scss │ │ │ ├── _utilities.scss │ │ │ ├── _variables.scss │ │ │ ├── _wells.scss │ │ │ └── mixins │ │ │ │ ├── _alerts.scss │ │ │ │ ├── _background-variant.scss │ │ │ │ ├── _border-radius.scss │ │ │ │ ├── _breakpoints.scss │ │ │ │ ├── _buttons.scss │ │ │ │ ├── _center-block.scss │ │ │ │ ├── _clearfix.scss │ │ │ │ ├── _forms.scss │ │ │ │ ├── _gradients.scss │ │ │ │ ├── _grid-framework.scss │ │ │ │ ├── _grid.scss │ │ │ │ ├── _hide-text.scss │ │ │ │ ├── _image.scss │ │ │ │ ├── _labels.scss │ │ │ │ ├── _list-group.scss │ │ │ │ ├── _nav-divider.scss │ │ │ │ ├── _nav-vertical-align.scss │ │ │ │ ├── _opacity.scss │ │ │ │ ├── _pagination.scss │ │ │ │ ├── _panels.scss │ │ │ │ ├── _progress-bar.scss │ │ │ │ ├── _reset-filter.scss │ │ │ │ ├── _reset-text.scss │ │ │ │ ├── _resize.scss │ │ │ │ ├── _responsive-visibility.scss │ │ │ │ ├── _size.scss │ │ │ │ ├── _tab-focus.scss │ │ │ │ ├── _table-row.scss │ │ │ │ ├── _text-emphasis.scss │ │ │ │ ├── _text-overflow.scss │ │ │ │ ├── _text-truncate.scss │ │ │ │ └── _vendor-prefixes.scss │ │ ├── bootstrap_mixins │ │ │ ├── _all.scss │ │ │ ├── _border-radius.scss │ │ │ ├── _box-shadow.scss │ │ │ ├── _hover.scss │ │ │ └── _nav-divider.scss │ │ ├── bootstrap_partials │ │ │ ├── _dropdown.scss │ │ │ ├── _glyphicons.scss │ │ │ ├── _grid.scss │ │ │ └── _variables.scss │ │ ├── bootstrap_utilities │ │ │ ├── _align.scss │ │ │ ├── _background.scss │ │ │ ├── _borders.scss │ │ │ ├── _clearfix.scss │ │ │ ├── _cursor.scss │ │ │ ├── _display.scss │ │ │ ├── _flex.scss │ │ │ ├── _float.scss │ │ │ ├── _position.scss │ │ │ ├── _screenreaders.scss │ │ │ ├── _sizing.scss │ │ │ ├── _spacing.scss │ │ │ ├── _text.scss │ │ │ └── _visibility.scss │ │ ├── colorbox.scss │ │ ├── modules │ │ │ ├── _all.scss │ │ │ ├── _animation.scss │ │ │ ├── _breakpoints.scss │ │ │ ├── _card_constants.scss │ │ │ ├── _colours.scss │ │ │ ├── _homepage_constants.scss │ │ │ ├── _levels.scss │ │ │ ├── _mixins.scss │ │ │ └── _spacing.scss │ │ ├── partials │ │ │ ├── _banners.scss │ │ │ ├── _base.scss │ │ │ ├── _buttons.scss │ │ │ ├── _carousel.scss │ │ │ ├── _footer.scss │ │ │ ├── _forms.scss │ │ │ ├── _grids.scss │ │ │ ├── _header.scss │ │ │ ├── _images.scss │ │ │ ├── _popup.scss │ │ │ ├── _progress-bars.scss │ │ │ ├── _subnavs.scss │ │ │ ├── _tables.scss │ │ │ ├── _text.scss │ │ │ ├── _ui-dialog.scss │ │ │ └── _utils.scss │ │ └── styles.scss │ │ └── video │ │ └── code for life .pdf ├── strings │ ├── __init__.py │ ├── about.py │ ├── coding_club.py │ ├── help_and_support.py │ ├── home_learning.py │ ├── materials.py │ ├── play.py │ ├── teach.py │ ├── teacher_resources.py │ └── ten_year_map.py ├── templates │ ├── 403.html │ ├── 404.html │ ├── 500.html │ ├── django_recaptcha │ │ ├── includes │ │ │ └── js_v2_invisible.html │ │ └── widget_v2_invisible.html │ ├── email.html │ ├── email.txt │ ├── maintenance.html │ ├── portal │ │ ├── about.html │ │ ├── base.html │ │ ├── base_no_userprofile.html │ │ ├── coding_club.html │ │ ├── contribute.html │ │ ├── dotmailer_consent_form.html │ │ ├── email_invitation_sent.html │ │ ├── email_style_template.html │ │ ├── email_verification_failed.html │ │ ├── email_verification_needed.html │ │ ├── form_shapes.html │ │ ├── getinvolved.html │ │ ├── home.html │ │ ├── home_learning.html │ │ ├── locked_out.html │ │ ├── locked_out_school_student.html │ │ ├── login │ │ │ ├── independent_student.html │ │ │ ├── student.html │ │ │ ├── student_class_code.html │ │ │ └── teacher.html │ │ ├── partials │ │ │ ├── banner.html │ │ │ ├── benefits.html │ │ │ ├── card_list.html │ │ │ ├── character_list.html │ │ │ ├── cookie_list.html │ │ │ ├── delete_popup.html │ │ │ ├── footer.html │ │ │ ├── header.html │ │ │ ├── headline.html │ │ │ ├── hero_card.html │ │ │ ├── info_popup.html │ │ │ ├── invite_admin_teacher.html │ │ │ ├── popup.html │ │ │ ├── register_newsletter_tickbox.html │ │ │ ├── screentime_popup.html │ │ │ ├── service_unavailable_popup.html │ │ │ ├── session_popup.html │ │ │ ├── teacher_non_dashboard_subnav.html │ │ │ └── teacher_non_dashboard_subnav_account.html │ │ ├── play.html │ │ ├── play │ │ │ ├── student_dashboard.html │ │ │ ├── student_edit_account.html │ │ │ └── student_join_organisation.html │ │ ├── privacy_notice.html │ │ ├── register.html │ │ ├── reset_password.html │ │ ├── reset_password_confirm.html │ │ ├── reset_password_done.html │ │ ├── reset_password_email_sent.html │ │ ├── tag_manager │ │ │ ├── tag_manager_body.html │ │ │ └── tag_manager_head.html │ │ ├── teach.html │ │ ├── teach │ │ │ ├── base_registering.html │ │ │ ├── class.html │ │ │ ├── dashboard.html │ │ │ ├── invited.html │ │ │ ├── onboarding_classes.html │ │ │ ├── onboarding_print.html │ │ │ ├── onboarding_school.html │ │ │ ├── onboarding_students.html │ │ │ ├── teacher_add_external_student.html │ │ │ ├── teacher_added_external_student.html │ │ │ ├── teacher_dismiss_students.html │ │ │ ├── teacher_edit_class.html │ │ │ ├── teacher_edit_student.html │ │ │ ├── teacher_move_all_classes.html │ │ │ ├── teacher_move_students.html │ │ │ └── teacher_move_students_to_class.html │ │ ├── ten_year_map.html │ │ └── terms.html │ └── two_factor │ │ ├── _base.html │ │ ├── _wizard_actions.html │ │ ├── _wizard_actions_enable_2fa.html │ │ ├── _wizard_actions_submit.html │ │ ├── _wizard_forms.html │ │ ├── _wizard_forms_token.html │ │ ├── backup_token.html │ │ ├── core │ │ ├── backup_tokens.html │ │ ├── login.html │ │ ├── setup.html │ │ └── setup_complete.html │ │ ├── profile │ │ ├── disable.html │ │ └── profile.html │ │ └── setup_wizard_token.html ├── templatetags │ ├── __init__.py │ ├── app_tags.py │ ├── banner_tags.py │ ├── benefits_tags.py │ ├── card_list_tags.py │ ├── future.py │ ├── headline_tags.py │ ├── hero_card_tags.py │ └── table_tags.py ├── tests │ ├── __init__.py │ ├── base_test.py │ ├── conftest.py │ ├── cypress │ │ ├── fixtures │ │ │ └── teachersToBeDeleted.json │ │ ├── integration │ │ │ ├── admin.js │ │ │ ├── admin.spec.js │ │ │ ├── independentStudent.spec.js │ │ │ ├── student.spec.js │ │ │ ├── teacher.spec.js │ │ │ └── user.spec.js │ │ └── support │ │ │ ├── classTester.ts │ │ │ ├── commands.ts │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ ├── multipleLoginSessionsTester.ts │ │ │ └── registrationTester.ts │ ├── data │ │ ├── 10000_gb.json │ │ ├── al109ne.json │ │ ├── al109ne_ab.json │ │ ├── al109ne_gb.json │ │ ├── al109ne_kr.json │ │ ├── sw72az.json │ │ ├── sw72az_gb.json │ │ ├── test_students_names.csv │ │ ├── test_students_names_no_name.csv │ │ └── xxxxx.json │ ├── forms │ │ └── test_admin_password.py │ ├── migrations │ │ ├── __init__.py │ │ ├── test_migration_make_portaladmin_teacher.py │ │ ├── test_migration_preview_user_remove.py │ │ ├── test_migration_preview_users.py │ │ ├── test_migration_remove_front_page_news.py │ │ ├── test_migration_remove_guardian.py │ │ ├── test_migration_use_common_models.py │ │ └── test_migration_verify_portaladmin.py │ ├── pageObjects │ │ ├── __init__.py │ │ └── portal │ │ │ ├── __init__.py │ │ │ ├── admin │ │ │ ├── __init__.py │ │ │ ├── admin_base_page.py │ │ │ ├── admin_data_page.py │ │ │ └── admin_map_page.py │ │ │ ├── base_page.py │ │ │ ├── email_verification_needed_page.py │ │ │ ├── forbidden_page.py │ │ │ ├── game_page.py │ │ │ ├── home_page.py │ │ │ ├── independent_login_page.py │ │ │ ├── password_reset_form_page.py │ │ │ ├── password_reset_page.py │ │ │ ├── play │ │ │ ├── __init__.py │ │ │ ├── account_page.py │ │ │ ├── dashboard_page.py │ │ │ ├── join_school_or_club_page.py │ │ │ └── play_base_page.py │ │ │ ├── play_page.py │ │ │ ├── resources_page.py │ │ │ ├── signup_page.py │ │ │ ├── student_login_class_code.py │ │ │ ├── student_login_page.py │ │ │ ├── teach │ │ │ ├── __init__.py │ │ │ ├── add_independent_student_to_class_page.py │ │ │ ├── added_independent_student_to_class_page.py │ │ │ ├── class_page.py │ │ │ ├── dashboard_page.py │ │ │ ├── dismiss_students_page.py │ │ │ ├── edit_student_page.py │ │ │ ├── move_class_page.py │ │ │ ├── move_classes_page.py │ │ │ ├── move_students_disambiguate_page.py │ │ │ ├── move_students_page.py │ │ │ ├── onboarding_classes_page.py │ │ │ ├── onboarding_organisation_page.py │ │ │ ├── onboarding_student_list_page.py │ │ │ ├── onboarding_students_page.py │ │ │ └── teach_base_page.py │ │ │ └── teacher_login_page.py │ ├── selenium_test_case.py │ ├── snapshots │ │ ├── __init__.py │ │ └── snap_test_partials.py │ ├── test_2FA.py │ ├── test_admin.py │ ├── test_api.py │ ├── test_captcha_forms.py │ ├── test_class.py │ ├── test_emails.py │ ├── test_global_forms.py │ ├── test_helper_methods.py │ ├── test_independent_student.py │ ├── test_invite_teacher.py │ ├── test_middleware.py │ ├── test_organisation.py │ ├── test_partials.py │ ├── test_ratelimit.py │ ├── test_school_student.py │ ├── test_security.py │ ├── test_teacher.py │ ├── test_teacher_student.py │ ├── test_views.py │ └── utils │ │ ├── __init__.py │ │ ├── classes.py │ │ └── messages.py ├── urls.py ├── views │ ├── __init__.py │ ├── about.py │ ├── admin.py │ ├── api.py │ ├── cron │ │ ├── __init__.py │ │ └── user.py │ ├── dotmailer.py │ ├── email.py │ ├── google_analytics.py │ ├── home.py │ ├── legal.py │ ├── login │ │ ├── __init__.py │ │ ├── independent_student.py │ │ ├── student.py │ │ └── teacher.py │ ├── organisation.py │ ├── play_landing_page.py │ ├── registration.py │ ├── student │ │ ├── __init__.py │ │ ├── edit_account_details.py │ │ └── play.py │ ├── teach.py │ ├── teacher │ │ ├── __init__.py │ │ ├── dashboard.py │ │ └── teach.py │ └── two_factor │ │ ├── __init__.py │ │ ├── core.py │ │ ├── form.py │ │ └── profile.py └── wsgi.py ├── pyproject.toml ├── pytest.ini ├── run ├── run.bat ├── run_testserver ├── setup.cfg ├── setup.py ├── tsconfig.json └── yarn.lock /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | precision: 2 3 | round: down 4 | range: "70...100" 5 | status: 6 | patch: 7 | default: 8 | target: 90% 9 | project: 10 | default: 11 | target: 90% 12 | 13 | ignore: 14 | - "portal/tests/*.py" 15 | - "portal/tests/**/*.py" 16 | - "example_project/*.py" 17 | - "example_project/**/*.py" 18 | - "portal_test_settings.py" 19 | - "setup.py" 20 | - "cfl_common/setup.py" 21 | - "cfl_common/common/apps.py" 22 | - "portal/helpers/ratelimit.py" 23 | - "portal/autoconfig.py" 24 | - "cfl_common/common/app_settings.py" 25 | - "cfl_common/common/csp_config.py" 26 | 27 | comment: false 28 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | portal/_version.py export-subst 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behaviour: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behaviour** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. Ubuntu] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Good Places to Start** 29 | 30 | _Link to a specific directory / file / line that would be a good place to 31 | start working on this task. The more specific the better._ 32 | 33 | **Additional context** 34 | Add any other context about the problem here. 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new_task.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New task 3 | about: A template for a new task 4 | 5 | --- 6 | 7 | ## Task Description 8 | 9 | ## Acceptance Criteria 10 | - [ ] ... 11 | - [ ] ... 12 | 13 | ## Good Places to Start 14 | 15 | _Link to a specific directory / file / line that would be a good place to 16 | start working on this task. The more specific the better._ 17 | 18 | ### Analytics Requirements 19 | - [ ] ... 20 | 21 | ### Copywrite Requirements 22 | - [ ] ... 23 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## How Has This Been Tested? 7 | 8 | 9 | 10 | 11 | ## Checklist: 12 | 13 | 14 | - [ ] My change requires a change to the documentation. 15 | - [ ] I have updated the documentation accordingly. 16 | - [ ] I have linked this PR to a ZenHub Issue. 17 | -------------------------------------------------------------------------------- /.github/workflows/issues.yml: -------------------------------------------------------------------------------- 1 | name: Issues 2 | 3 | on: 4 | issues: 5 | issue_comment: 6 | 7 | jobs: 8 | issue: 9 | uses: ocadotechnology/codeforlife-workspace/.github/workflows/issues.yaml@main 10 | secrets: inherit 11 | -------------------------------------------------------------------------------- /.github/workflows/semantic-pull-request-check.yml: -------------------------------------------------------------------------------- 1 | name: "Semantic PR check" 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | main: 12 | name: Validate PR title 13 | runs-on: ubuntu-22.04 14 | steps: 15 | - uses: amannn/action-semantic-pull-request@v5 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/snyk.yaml: -------------------------------------------------------------------------------- 1 | name: Snyk 2 | 3 | on: 4 | release: 5 | types: [published] 6 | pull_request: 7 | workflow_dispatch: 8 | 9 | jobs: 10 | test: 11 | if: github.repository_owner_id == 2088731 12 | runs-on: ubuntu-22.04 13 | steps: 14 | - name: 🐍 Set up Python 3.12 Environment 15 | uses: ocadotechnology/codeforlife-workspace/.github/actions/python/setup-environment@main 16 | with: 17 | python-version: 3.12 18 | 19 | - name: ❌ Delete frontend package manager files 20 | run: rm package.json yarn.lock 21 | 22 | - name: 🐕‍🦺 Run Snyk Tests 23 | uses: ocadotechnology/codeforlife-workspace/.github/actions/snyk/run-tests@main 24 | with: 25 | snyk-token: ${{ secrets.SNYK_TOKEN }} 26 | config-path: "backend/.snyk" 27 | add-test-args: --command=.venv/bin/python 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.pyc 3 | local_settings.py 4 | /static/ 5 | /VIRTUALENV/ 6 | /env/ 7 | *.iml 8 | *~ 9 | 10 | __pycache__ 11 | .settings 12 | .project 13 | .pydevproject 14 | .idea 15 | dbfile 16 | testdbfile 17 | *.orig 18 | .sass-cache 19 | *.DS_Store 20 | *.jshintrc 21 | portal/tests/chromedriver 22 | 23 | db.sqlite3 24 | example_project/static/ 25 | codeforlife_portal.egg-info 26 | *.egg-info/ 27 | build/ 28 | # .vscode/ 29 | dist/ 30 | 31 | # Cypress generated folder 32 | /cypress 33 | 34 | # tunnel software 35 | ngrok 36 | 37 | .venv 38 | .coverage -------------------------------------------------------------------------------- /.venv/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/.venv/.gitkeep -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "args": [ 5 | "runserver", 6 | "localhost:8000" 7 | ], 8 | "django": true, 9 | "justMyCode": false, 10 | "name": "Django Server", 11 | "program": "${workspaceFolder}/example_project/manage.py", 12 | "request": "launch", 13 | "type": "debugpy" 14 | }, 15 | { 16 | "console": "integratedTerminal", 17 | "justMyCode": false, 18 | "name": "Python: Current File", 19 | "program": "${file}", 20 | "request": "launch", 21 | "type": "debugpy" 22 | }, 23 | { 24 | "env": { 25 | "PYTEST_ADDOPTS": "--no-cov" 26 | }, 27 | "justMyCode": false, 28 | "name": "Pytest", 29 | "presentation": { 30 | "hidden": true 31 | }, 32 | "request": "test", 33 | "type": "debugpy" 34 | } 35 | ], 36 | "version": "0.2.0" 37 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Find our contribution agreement [here](https://github.com/ocadotechnology/codeforlife-workspace/blob/main/CONTRIBUTING.md). 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | Find our license [here](https://github.com/ocadotechnology/codeforlife-workspace/blob/main/LICENSE.md). 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | graft deploy/static 3 | graft deploy/templates 4 | graft portal/static 5 | graft portal/templates 6 | prune cfl_common 7 | prune example_project 8 | -------------------------------------------------------------------------------- /cfl_common/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include common/fixtures/*.json 2 | graft common/templates 3 | graft common/static 4 | -------------------------------------------------------------------------------- /cfl_common/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | cryptography = "==44.0.1" 8 | django = "==5.1.13" 9 | django-countries = "==7.6.1" 10 | django-csp = "==3.8" 11 | django-import-export = "==4.2.0" 12 | django-pipeline = "==4.0.0" 13 | django-two-factor-auth = "==1.17.0" 14 | djangorestframework = "==3.16.0" 15 | libsass = "==0.23.0" 16 | more-itertools = "==8.7.0" 17 | pgeocode = "==0.4.0" 18 | pyjwt = "==2.6.0" 19 | 20 | [dev-packages] 21 | 22 | [requires] 23 | python_version = "3.12" 24 | -------------------------------------------------------------------------------- /cfl_common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/cfl_common/__init__.py -------------------------------------------------------------------------------- /cfl_common/common/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = "common.apps.CommonConfig" 2 | -------------------------------------------------------------------------------- /cfl_common/common/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CommonConfig(AppConfig): 5 | name = "common" 6 | verbose_name = "Code for Life Common" 7 | -------------------------------------------------------------------------------- /cfl_common/common/context_processors.py: -------------------------------------------------------------------------------- 1 | from common import app_settings 2 | 3 | 4 | def module_name(request): 5 | return {"module_name": app_settings.MODULE_NAME} 6 | 7 | 8 | def cookie_management_enabled(request): 9 | return {"cookie_management_enabled": app_settings.COOKIE_MANAGEMENT_ENABLED} 10 | -------------------------------------------------------------------------------- /cfl_common/common/helpers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/cfl_common/common/helpers/__init__.py -------------------------------------------------------------------------------- /cfl_common/common/helpers/organisation.py: -------------------------------------------------------------------------------- 1 | # TODO: Move to Address model once we create it 2 | def sanitise_uk_postcode(postcode): 3 | if len(postcode) >= 5: # Valid UK postcodes are at least 5 chars long 4 | outcode = postcode[:-3] # UK incodes are always 3 characters 5 | 6 | # Insert a space between outcode and incode if there isn't already one 7 | if not outcode.endswith(" "): 8 | postcode = postcode[:-3] + " " + postcode[-3:] 9 | 10 | return postcode 11 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0004_add_aimmocharacters.py: -------------------------------------------------------------------------------- 1 | from common.helpers.data_migration_loader import load_data_from_file 2 | from django.db import migrations 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [("common", "0003_aimmocharacter")] 8 | 9 | def dummy_reverse_code(app, schema_editor): 10 | pass 11 | 12 | operations = [ 13 | migrations.RunPython( 14 | load_data_from_file("aimmo_characters.json"), 15 | reverse_code=dummy_reverse_code, 16 | ) 17 | ] 18 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0005_add_worksheets.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | 4 | class Migration(migrations.Migration): 5 | 6 | dependencies = [("common", "0004_add_aimmocharacters")] 7 | 8 | operations = [migrations.RunPython(migrations.RunPython.noop, reverse_code=migrations.RunPython.noop)] 9 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0006_update_aimmo_character_image_path.py: -------------------------------------------------------------------------------- 1 | from common.helpers.data_migration_loader import load_data_from_file 2 | from django.db import migrations 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [("common", "0005_add_worksheets")] 8 | 9 | def dummy_reverse_code(app, schema_editor): 10 | pass 11 | 12 | operations = [ 13 | migrations.RunPython( 14 | load_data_from_file("aimmo_characters2.json"), 15 | reverse_code=dummy_reverse_code, 16 | ) 17 | ] 18 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0007_add_pdf_names_to_first_two_worksheets.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | 4 | class Migration(migrations.Migration): 5 | 6 | dependencies = [("common", "0006_update_aimmo_character_image_path")] 7 | 8 | operations = [migrations.RunPython(migrations.RunPython.noop, reverse_code=migrations.RunPython.noop)] 9 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0008_unlock_worksheet_3.py: -------------------------------------------------------------------------------- 1 | from common.helpers.data_migration_loader import load_data_from_file 2 | from django.db import migrations 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [ 8 | ("common", "0007_add_pdf_names_to_first_two_worksheets"), 9 | ] 10 | 11 | operations = [migrations.RunPython(migrations.RunPython.noop, reverse_code=migrations.RunPython.noop)] 12 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0009_add_blocked_time_to_teacher_and_student.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.20 on 2021-05-05 11:40 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("common", "0008_unlock_worksheet_3"), 10 | ("portal", "0058_move_to_common_models"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="student", 16 | name="blocked_time", 17 | field=models.DateTimeField(null=True), 18 | ), 19 | migrations.AddField( 20 | model_name="teacher", 21 | name="blocked_time", 22 | field=models.DateTimeField(null=True), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0010_remove_teacher_title.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.24 on 2021-08-18 15:51 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("common", "0009_add_blocked_time_to_teacher_and_student"), 10 | ("portal", "0062_verify_portaladmin"), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name="teacher", 16 | name="title", 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0011_student_login_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.24 on 2021-09-24 16:36 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("common", "0010_remove_teacher_title"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="student", 15 | name="login_id", 16 | field=models.CharField(max_length=64, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0014_login_type.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.24 on 2021-11-09 15:39 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("common", "0013_class_school"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="usersession", 16 | name="login_type", 17 | field=models.CharField(max_length=100, null=True), 18 | ), 19 | migrations.AlterField( 20 | model_name="class", 21 | name="creation_time", 22 | field=models.DateTimeField(default=django.utils.timezone.now, null=True), 23 | ), 24 | migrations.AlterField( 25 | model_name="school", 26 | name="creation_time", 27 | field=models.DateTimeField(default=django.utils.timezone.now, null=True), 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0015_dailyactivity.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2.24 on 2021-12-06 15:03 2 | 3 | from django.db import migrations, models 4 | import django.utils.timezone 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("common", "0014_login_type"), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name="DailyActivity", 16 | fields=[ 17 | ( 18 | "id", 19 | models.AutoField( 20 | auto_created=True, 21 | primary_key=True, 22 | serialize=False, 23 | verbose_name="ID", 24 | ), 25 | ), 26 | ("date", models.DateField(default=django.utils.timezone.now)), 27 | ("csv_click_count", models.PositiveIntegerField(default=0)), 28 | ("login_cards_click_count", models.PositiveIntegerField(default=0)), 29 | ], 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0017_copy_email_to_username.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations 2 | 3 | 4 | def copy_email_to_username(apps, schema): 5 | Student = apps.get_model("common", "Student") 6 | independent_students = Student.objects.filter(class_field__isnull=True, new_user__is_active=True) 7 | for student in independent_students: 8 | student.new_user.username = student.new_user.email 9 | student.new_user.save() 10 | 11 | 12 | class Migration(migrations.Migration): 13 | 14 | dependencies = [ 15 | ("common", "0016_joinreleasestudent"), 16 | ] 17 | 18 | operations = [migrations.RunPython(code=copy_email_to_username, reverse_code=migrations.RunPython.noop)] 19 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0018_update_aimmo_character_image_path.py: -------------------------------------------------------------------------------- 1 | from common.helpers.data_migration_loader import load_data_from_file 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [("common", "0017_copy_email_to_username")] 8 | 9 | operations = [ 10 | migrations.AddField( 11 | "AimmoCharacter", 12 | "alt", 13 | models.CharField(max_length=255, null=True), 14 | ), 15 | ] 16 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0019_aimmocharacter_alt.py: -------------------------------------------------------------------------------- 1 | from common.helpers.data_migration_loader import load_data_from_file 2 | from django.db import migrations, models 3 | 4 | 5 | class Migration(migrations.Migration): 6 | 7 | dependencies = [ 8 | ("common", "0018_update_aimmo_character_image_path"), 9 | ] 10 | 11 | operations = [ 12 | migrations.RunPython( 13 | load_data_from_file("aimmo_characters3.json"), 14 | reverse_code=migrations.RunPython.noop, 15 | ) 16 | ] 17 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0020_class_is_active_and_null_access_code.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.12 on 2022-04-07 16:46 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("common", "0019_aimmocharacter_alt"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="class", 15 | name="is_active", 16 | field=models.BooleanField(default=True), 17 | ), 18 | migrations.AlterField( 19 | model_name="class", 20 | name="access_code", 21 | field=models.CharField(max_length=5, null=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0021_school_is_active.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.13 on 2022-04-29 17:00 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("common", "0020_class_is_active_and_null_access_code"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="school", 15 | name="is_active", 16 | field=models.BooleanField(default=True), 17 | ), 18 | migrations.AlterField( 19 | model_name="school", 20 | name="postcode", 21 | field=models.CharField(max_length=10, null=True), 22 | ), 23 | migrations.AlterField( 24 | model_name="school", 25 | name="town", 26 | field=models.CharField(max_length=200, null=True), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0022_school_cleanup.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.13 on 2022-05-03 11:01 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("common", "0021_school_is_active"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="school", 15 | name="latitude", 16 | ), 17 | migrations.RemoveField( 18 | model_name="school", 19 | name="longitude", 20 | ), 21 | migrations.RemoveField( 22 | model_name="school", 23 | name="town", 24 | ), 25 | migrations.RemoveField( 26 | model_name="userprofile", 27 | name="can_view_aggregated_data", 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0023_userprofile_aimmo_badges.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.13 on 2022-05-17 17:05 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("common", "0022_school_cleanup"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterModelOptions( 14 | name="school", 15 | options={}, 16 | ), 17 | migrations.AddField( 18 | model_name="userprofile", 19 | name="aimmo_badges", 20 | field=models.CharField(blank=True, max_length=200, null=True), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0024_teacher_invited_by.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.13 on 2022-06-07 17:20 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("common", "0023_userprofile_aimmo_badges"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="teacher", 16 | name="invited_by", 17 | field=models.ForeignKey( 18 | blank=True, 19 | null=True, 20 | on_delete=django.db.models.deletion.SET_NULL, 21 | related_name="invited_teachers", 22 | to="common.teacher", 23 | ), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0026_teacher_remove_join_request.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.13 on 2022-06-22 15:33 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("common", "0025_schoolteacherinvitation"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="teacher", 15 | name="pending_join_request", 16 | ), 17 | migrations.AlterField( 18 | model_name="teacher", 19 | name="blocked_time", 20 | field=models.DateTimeField(blank=True, null=True), 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0027_class_created_by.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.13 on 2022-07-13 16:04 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("common", "0026_teacher_remove_join_request"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="class", 16 | name="created_by", 17 | field=models.ForeignKey( 18 | blank=True, 19 | null=True, 20 | on_delete=django.db.models.deletion.SET_NULL, 21 | related_name="created_classes", 22 | to="common.teacher", 23 | ), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0028_coding_club_downloads.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.15 on 2022-09-08 15:27 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("common", "0027_class_created_by"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="dailyactivity", 15 | name="primary_coding_club_downloads", 16 | field=models.PositiveIntegerField(default=0), 17 | ), 18 | migrations.AddField( 19 | model_name="dailyactivity", 20 | name="python_coding_club_downloads", 21 | field=models.PositiveIntegerField(default=0), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0029_dynamicelement.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.15 on 2022-10-12 12:20 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("common", "0028_coding_club_downloads"), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="DynamicElement", 15 | fields=[ 16 | ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), 17 | ("name", models.CharField(max_length=64, unique=True, editable=False)), 18 | ("active", models.BooleanField(default=False)), 19 | ("text", models.TextField(blank=True, null=True)), 20 | ], 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0030_add_maintenance_banner.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0 on 2020-10-28 17:36 2 | 3 | from django.db import migrations 4 | 5 | 6 | def add_maintenance_banner(apps, schema_editor): 7 | """ 8 | This creates the maintenance banner dynamic element which allows us to edit and turn on and off the maintenance 9 | banner dynamically from the admin panel. 10 | """ 11 | DynamicElement = apps.get_model("common", "DynamicElement") 12 | DynamicElement.objects.create(name="Maintenance banner") 13 | 14 | 15 | def remove_maintenance_banner(apps, schema_editor): 16 | DynamicElement = apps.get_model("common", "DynamicElement") 17 | maintenance_banner = DynamicElement.objects.get(name="Maintenance banner") 18 | maintenance_banner.delete() 19 | 20 | 21 | class Migration(migrations.Migration): 22 | 23 | dependencies = [("common", "0029_dynamicelement")] 24 | 25 | operations = [migrations.RunPython(add_maintenance_banner, remove_maintenance_banner)] 26 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0032_dailyactivity_level_control_submits.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.16 on 2022-12-09 13:27 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("common", "0031_improve_admin_panel"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="dailyactivity", 15 | name="level_control_submits", 16 | field=models.PositiveBigIntegerField(default=0), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0033_password_reset_tracking_fields.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.16 on 2023-01-17 10:41 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("common", "0032_dailyactivity_level_control_submits"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="dailyactivity", 15 | name="daily_indy_lockout_reset", 16 | field=models.PositiveIntegerField(default=0), 17 | ), 18 | migrations.AddField( 19 | model_name="dailyactivity", 20 | name="daily_teacher_lockout_reset", 21 | field=models.PositiveIntegerField(default=0), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0034_dailyactivity_daily_school_student_lockout_reset.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.16 on 2023-01-24 15:26 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("common", "0033_password_reset_tracking_fields"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="dailyactivity", 15 | name="daily_school_student_lockout_reset", 16 | field=models.PositiveIntegerField(default=0), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0035_rename_lockout_fields.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.16 on 2023-01-27 03:17 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("common", "0034_dailyactivity_daily_school_student_lockout_reset"), 9 | ] 10 | 11 | operations = [ 12 | migrations.RenameField( 13 | model_name="dailyactivity", 14 | old_name="daily_indy_lockout_reset", 15 | new_name="indy_lockout_resets", 16 | ), 17 | migrations.RenameField( 18 | model_name="dailyactivity", 19 | old_name="daily_school_student_lockout_reset", 20 | new_name="school_student_lockout_resets", 21 | ), 22 | migrations.RenameField( 23 | model_name="dailyactivity", 24 | old_name="daily_teacher_lockout_reset", 25 | new_name="teacher_lockout_resets", 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0036_rename_awaiting_email_verification_userprofile_is_verified.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-04-11 14:31 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("common", "0035_rename_lockout_fields"), 9 | ] 10 | 11 | operations = [ 12 | migrations.RenameField( 13 | model_name="userprofile", 14 | old_name="awaiting_email_verification", 15 | new_name="is_verified", 16 | ), 17 | ] 18 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0038_delete_emailverification.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-04-13 18:41 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("common", "0037_migrate_email_verification"), 10 | ] 11 | 12 | operations = [ 13 | migrations.DeleteModel( 14 | name="EmailVerification", 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0039_copy_email_to_username.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.18 on 2023-04-13 18:41 2 | 3 | from django.db import migrations 4 | from django.db.models import F 5 | 6 | 7 | def copy_email_to_username(apps, schema): 8 | User = apps.get_model("auth", "User") 9 | User.objects.exclude(email="").exclude(email__isnull=True).exclude(email=F("username")).update(username=F("email")) 10 | 11 | 12 | class Migration(migrations.Migration): 13 | 14 | dependencies = [ 15 | ("common", "0038_delete_emailverification"), 16 | ] 17 | 18 | operations = [migrations.RunPython(code=copy_email_to_username, reverse_code=migrations.RunPython.noop)] 19 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0040_school_county.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2023-08-04 14:35 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("common", "0039_copy_email_to_username"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="school", 15 | name="county", 16 | field=models.CharField(blank=True, max_length=50, null=True), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0041_populate_gb_counties.py: -------------------------------------------------------------------------------- 1 | import pgeocode 2 | from django.db import migrations 3 | 4 | from ..helpers.organisation import sanitise_uk_postcode 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [ 9 | ("common", "0040_school_county"), 10 | ] 11 | 12 | def forwards(apps, schema_editor): 13 | """Populate the county field for schools in GB""" 14 | School = apps.get_model("common", "School") 15 | gb_schools = School.objects.filter(country="GB") 16 | nomi = pgeocode.Nominatim("GB") 17 | 18 | for school in gb_schools: 19 | if school.postcode.replace(" ", "") == "": 20 | school.county = "nan" 21 | school.save() 22 | else: 23 | county = nomi.query_postal_code(sanitise_uk_postcode(school.postcode)).county_name 24 | school.county = county 25 | school.save() 26 | 27 | operations = [migrations.RunPython(forwards, reverse_code=migrations.RunPython.noop)] 28 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0042_totalactivity.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2023-09-07 01:56 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("common", "0041_populate_gb_counties"), 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="TotalActivity", 15 | fields=[ 16 | ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), 17 | ("teacher_registrations", models.PositiveIntegerField(default=0)), 18 | ("student_registrations", models.PositiveIntegerField(default=0)), 19 | ("independent_registrations", models.PositiveIntegerField(default=0)), 20 | ], 21 | options={ 22 | "verbose_name_plural": "Total activity", 23 | }, 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0045_otp.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2023-09-27 13:36 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("common", "0044_update_activity_models"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name="userprofile", 15 | name="last_otp_for_time", 16 | field=models.DateTimeField(blank=True, null=True), 17 | ), 18 | migrations.AddField( 19 | model_name="userprofile", 20 | name="otp_secret", 21 | field=models.CharField(blank=True, max_length=40, null=True), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0046_alter_school_country.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2023-11-06 16:04 2 | 3 | from django.db import migrations 4 | import django_countries.fields 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("common", "0045_otp"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name="school", 16 | name="country", 17 | field=django_countries.fields.CountryField(blank=True, max_length=2, null=True), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0047_delete_school_postcode.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.20 on 2023-11-06 18:56 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("common", "0046_alter_school_country"), 9 | ] 10 | 11 | operations = [ 12 | migrations.RemoveField( 13 | model_name="school", 14 | name="postcode", 15 | ), 16 | ] 17 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0049_anonymise_orphan_users.py: -------------------------------------------------------------------------------- 1 | from django.apps.registry import Apps 2 | from django.db import migrations 3 | 4 | from portal.views.api import __anonymise_user 5 | 6 | 7 | def anonymise_orphan_users(apps: Apps, *args): 8 | """ 9 | Users should never exist without a user-type linked to them. Anonymise all 10 | instances of User objects without a Teacher or Student instance. 11 | """ 12 | User = apps.get_model("auth", "User") 13 | 14 | active_orphan_users = User.objects.filter( 15 | new_teacher__isnull=True, new_student__isnull=True, is_active=True 16 | ) 17 | 18 | for active_orphan_user in active_orphan_users: 19 | __anonymise_user(active_orphan_user) 20 | 21 | 22 | class Migration(migrations.Migration): 23 | dependencies = [("common", "0048_unique_school_names")] 24 | 25 | operations = [ 26 | migrations.RunPython( 27 | code=anonymise_orphan_users, reverse_code=migrations.RunPython.noop 28 | ), 29 | ] 30 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0050_anonymise_orphan_schools.py: -------------------------------------------------------------------------------- 1 | from uuid import uuid4 2 | 3 | from django.apps.registry import Apps 4 | from django.db import migrations 5 | 6 | 7 | def anonymise_orphan_schools(apps: Apps, *args): 8 | """ 9 | Schools without any teachers or students should be anonymised (inactive). 10 | Mark all active orphan schools as inactive. 11 | """ 12 | School = apps.get_model("common", "School") 13 | 14 | active_orphan_schools = School.objects.filter(teacher_school__isnull=True) 15 | 16 | for active_orphan_school in active_orphan_schools: 17 | active_orphan_school.name = uuid4().hex 18 | active_orphan_school.is_active = False 19 | active_orphan_school.save() 20 | 21 | 22 | class Migration(migrations.Migration): 23 | dependencies = [("common", "0049_anonymise_orphan_users")] 24 | 25 | operations = [ 26 | migrations.RunPython( 27 | code=anonymise_orphan_schools, 28 | reverse_code=migrations.RunPython.noop, 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0051_verify_returning_users.py: -------------------------------------------------------------------------------- 1 | from django.apps.registry import Apps 2 | from django.db import migrations 3 | 4 | 5 | def verify_returning_users(apps: Apps, *args): 6 | """ 7 | Users cannot be unverified after having logged in at least once. Grab all 8 | instances of unverified UserProfile where the User has logged in and mark it 9 | as verified. 10 | """ 11 | UserProfile = apps.get_model("common", "UserProfile") 12 | 13 | UserProfile.objects.filter( 14 | user__last_login__isnull=False, is_verified=False 15 | ).update(is_verified=True) 16 | 17 | 18 | class Migration(migrations.Migration): 19 | dependencies = [("common", "0050_anonymise_orphan_schools")] 20 | 21 | operations = [ 22 | migrations.RunPython( 23 | code=verify_returning_users, 24 | reverse_code=migrations.RunPython.noop, 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0053_clean_class_data.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from django.apps.registry import Apps 4 | from django.db import migrations, models 5 | 6 | def clean_early_class_data(apps: Apps, *args): 7 | Class = apps.get_model("common", "Class") 8 | 9 | Class.objects.filter( 10 | creation_time__date__lt = date(2021, 10, 15) 11 | ).update(creation_time = None) 12 | 13 | class Migration(migrations.Migration): 14 | 15 | dependencies = [ 16 | ("common", "0052_add_cse_fields") 17 | ] 18 | 19 | operations = [ 20 | migrations.RunPython( 21 | code=clean_early_class_data, 22 | reverse_code=migrations.RunPython.noop, 23 | ), 24 | ] -------------------------------------------------------------------------------- /cfl_common/common/migrations/0054_delete_aimmo_models.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.25 on 2024-07-31 00:13 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('common', '0053_clean_class_data'), 10 | ] 11 | 12 | operations = [ 13 | migrations.DeleteModel( 14 | name='AimmoCharacter', 15 | ), 16 | migrations.RemoveField( 17 | model_name='userprofile', 18 | name='aimmo_badges', 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0055_alter_schoolteacherinvitation_token.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.25 on 2024-09-27 16:04 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('common', '0054_delete_aimmo_models'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='schoolteacherinvitation', 15 | name='token', 16 | field=models.CharField(max_length=88), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0056_set_non_school_teachers_as_non_admins.py: -------------------------------------------------------------------------------- 1 | from django.apps.registry import Apps 2 | from django.db import migrations 3 | 4 | 5 | def set_non_school_teachers_as_non_admins(apps: Apps, *args): 6 | Teacher = apps.get_model("common", "Teacher") 7 | 8 | Teacher.objects.filter( 9 | is_admin=True, 10 | school__isnull=True, 11 | ).update(is_admin=False) 12 | 13 | 14 | class Migration(migrations.Migration): 15 | 16 | dependencies = [ 17 | ("common", "0055_alter_schoolteacherinvitation_token"), 18 | ] 19 | 20 | operations = [ 21 | migrations.RunPython( 22 | code=set_non_school_teachers_as_non_admins, 23 | reverse_code=migrations.RunPython.noop, 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0057_teacher_teacher__is_admin.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.17 on 2025-01-13 17:34 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("common", "0056_set_non_school_teachers_as_non_admins"), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddConstraint( 14 | model_name="teacher", 15 | constraint=models.CheckConstraint( 16 | check=models.Q(("is_admin", True), ("school__isnull", True), _negated=True), name="teacher__is_admin" 17 | ), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/0058_userprofile_google_refresh_token_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.10 on 2025-08-12 12:51 2 | 3 | import common.models 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ("common", "0057_teacher_teacher__is_admin"), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name="userprofile", 16 | name="google_refresh_token", 17 | field=common.models.EncryptedCharField(blank=True, max_length=1004, null=True), 18 | ), 19 | migrations.AddField( 20 | model_name="userprofile", 21 | name="google_sub", 22 | field=models.CharField(blank=True, max_length=255, null=True), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /cfl_common/common/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/cfl_common/common/migrations/__init__.py -------------------------------------------------------------------------------- /cfl_common/common/templates/common/freshdesk_widget.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /cfl_common/common/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/cfl_common/common/tests/__init__.py -------------------------------------------------------------------------------- /cfl_common/common/tests/test_migration_blocked_time.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.django_db 5 | def test_blocked_time_added(migrator): 6 | migrator.apply_initial_migration(("common", "0008_unlock_worksheet_3")) 7 | new_state = migrator.apply_tested_migration(("common", "0009_add_blocked_time_to_teacher_and_student")) 8 | 9 | teacher_model = new_state.apps.get_model("common", "Teacher") 10 | 11 | assert teacher_model._meta.get_field("blocked_time").get_internal_type() == "DateTimeField" 12 | 13 | student_model = new_state.apps.get_model("common", "Student") 14 | 15 | assert student_model._meta.get_field("blocked_time").get_internal_type() == "DateTimeField" 16 | -------------------------------------------------------------------------------- /cfl_common/common/tests/test_migration_remove_teacher_title.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from django.db.models.query import QuerySet 3 | 4 | 5 | @pytest.mark.django_db 6 | def test_teacher_title_removed(migrator): 7 | old_state = migrator.apply_initial_migration(("common", "0009_add_blocked_time_to_teacher_and_student")) 8 | Teacher = old_state.apps.get_model("common", "Teacher") 9 | assert hasattr(Teacher, "title") 10 | 11 | new_state = migrator.apply_tested_migration(("common", "0010_remove_teacher_title")) 12 | Teacher = new_state.apps.get_model("common", "Teacher") 13 | assert not hasattr(Teacher, "title") 14 | -------------------------------------------------------------------------------- /cfl_common/common/tests/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/cfl_common/common/tests/utils/__init__.py -------------------------------------------------------------------------------- /cfl_common/common/tests/utils/user.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from django.utils import timezone 3 | 4 | 5 | def get_superuser(): 6 | """Get a superuser for testing, or create one if there isn't one.""" 7 | try: 8 | return User.objects.get(username="superuser") 9 | except User.DoesNotExist: 10 | return User.objects.create_superuser("superuser", "superuser@codeforlife.education", "password") 11 | 12 | 13 | def create_user_directly(active=True, **kwargs): 14 | """Create a user in the database.""" 15 | days_to_subtract = 10 if active else 2000 16 | username = "old_user+{:d}".format(create_user_directly.next_id) 17 | user = User.objects.create_user(username, password="password") 18 | user.last_login = timezone.now() - timezone.timedelta(days=days_to_subtract) 19 | user.date_joined = timezone.now() - timezone.timedelta(days=days_to_subtract - 1) 20 | user.save() 21 | 22 | create_user_directly.next_id += 1 23 | 24 | return user 25 | 26 | 27 | create_user_directly.next_id = 1 28 | -------------------------------------------------------------------------------- /cfl_common/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta:__legacy__" 4 | 5 | [tool.black] 6 | line-length = 120 7 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | projectId: 'hcq23q', 3 | videoUploadOnPasses: false, 4 | e2e: { 5 | setupNodeEvents(on, config) {}, 6 | baseUrl: 'http://localhost:8000', 7 | specPattern: 'portal/tests//**/*.spec.js', 8 | supportFile: 'portal/tests/cypress/support/index.js', 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /deploy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/deploy/__init__.py -------------------------------------------------------------------------------- /deploy/captcha.py: -------------------------------------------------------------------------------- 1 | CAPTCHA_ENABLED = True 2 | -------------------------------------------------------------------------------- /deploy/middleware/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/deploy/middleware/__init__.py -------------------------------------------------------------------------------- /deploy/middleware/exceptionlogging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | 4 | class ExceptionLoggingMiddleware(object): 5 | def __init__(self, get_response): 6 | self.get_response = get_response 7 | 8 | def __call__(self, request): 9 | response = self.get_response(request) 10 | return response 11 | 12 | def process_exception(self, request, exception): 13 | logging.exception( 14 | "Exception occurred while handling %s request to %s", 15 | request.method, 16 | request.path, 17 | ) 18 | 19 | return None 20 | -------------------------------------------------------------------------------- /deploy/middleware/maintenance.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponseRedirect 2 | from django.urls import reverse, reverse_lazy 3 | 4 | 5 | class MaintenanceMiddleware(object): 6 | """ 7 | This middleware allows us to turn on "Maintenance Mode". Toggle `MAINTENANCE_MODE` to True in 8 | `process_view` to redirect all requests in the app to the maintenance holding page. 9 | """ 10 | 11 | def __init__(self, get_response): 12 | self.get_response = get_response 13 | 14 | def __call__(self, request): 15 | response = self.get_response(request) 16 | return response 17 | 18 | def process_view(self, request, callback, callback_args, callback_kwargs): 19 | MAINTENANCE_MODE = False 20 | 21 | if MAINTENANCE_MODE and not request.path.startswith(reverse("maintenance")): 22 | return HttpResponseRedirect(reverse_lazy("maintenance")) 23 | 24 | if not MAINTENANCE_MODE and request.path.startswith(reverse("maintenance")): 25 | return HttpResponseRedirect(reverse_lazy("home")) 26 | -------------------------------------------------------------------------------- /deploy/middleware/security.py: -------------------------------------------------------------------------------- 1 | from django.middleware.security import SecurityMiddleware 2 | 3 | 4 | class CustomSecurityMiddleware(SecurityMiddleware): 5 | """ 6 | Extends Django's Security Middleware. 7 | See https://docs.djangoproject.com/en/4.2/_modules/django/middleware/security/ for 8 | the source code, as well as https://docs.djangoproject.com/en/4.2/ref/middleware/#module-django.middleware.security 9 | for docs on security middleware. 10 | """ 11 | 12 | def process_response(self, request, response): 13 | """ 14 | Extends the original security middleware to ensure the X-XSS-Protection header 15 | is set to 1. 16 | https://docs.djangoproject.com/en/5.1/releases/4.0/#securitymiddleware-no-longer-sets-the-x-xss-protection-header 17 | """ 18 | super().process_response(request, response) 19 | response.headers.setdefault("X-XSS-Protection", "1; mode=block") 20 | 21 | return response 22 | -------------------------------------------------------------------------------- /deploy/static/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/deploy/static/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /deploy/static/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/deploy/static/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /deploy/static/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/deploy/static/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /deploy/static/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/deploy/static/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /deploy/static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/deploy/static/apple-touch-icon.png -------------------------------------------------------------------------------- /deploy/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /deploy/templates/deploy/csrf_failure.html: -------------------------------------------------------------------------------- 1 | {% extends 'portal/base.html' %} 2 | 3 | {% block content %} 4 | 5 |

Something went wrong...


6 | 7 |

Sorry, something went wrong when loading the page. Try again.

8 | 9 | 17 | 18 | {% endblock content %} -------------------------------------------------------------------------------- /deploy/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | 4 | def csrf_failure(request, reason=""): 5 | return render(request, "deploy/csrf_failure.html") 6 | -------------------------------------------------------------------------------- /example_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/example_project/__init__.py -------------------------------------------------------------------------------- /example_project/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import include, path, re_path 3 | from game import python_den_urls 4 | from game import urls as game_urls 5 | 6 | from portal import urls as portal_urls 7 | 8 | admin.autodiscover() 9 | 10 | urlpatterns = [ 11 | re_path(r"^", include(portal_urls)), 12 | path("administration/", admin.site.urls), 13 | re_path(r"^rapidrouter/", include(game_urls)), 14 | re_path(r"^pythonden/", include(python_den_urls)), 15 | ] 16 | -------------------------------------------------------------------------------- /example_project/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for example_project project. 3 | 4 | This module contains the WSGI application used by Django's development server 5 | and any production WSGI deployments. It should expose a module-level variable 6 | named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover 7 | this application via the ``WSGI_APPLICATION`` setting. 8 | 9 | Usually you will have the standard Django WSGI application here, but it also 10 | might make sense to replace the whole Django WSGI application with a custom one 11 | that later delegates to the Django one. For example, you could introduce WSGI 12 | middleware here, or combine a Django application with an application of another 13 | framework. 14 | 15 | """ 16 | import os 17 | 18 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 19 | 20 | from django.core.wsgi import get_wsgi_application 21 | 22 | application = get_wsgi_application() 23 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "cypress": "^13.0.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /portal/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "8.9.6" 2 | -------------------------------------------------------------------------------- /portal/app_settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | 3 | CONTACT_FORM_EMAILS = getattr(settings, "PORTAL_CONTACT_FORM_EMAIL", ("codeforlife@ocado.com",)) 4 | 5 | # Private key for Recaptcha 6 | RECAPTCHA_PRIVATE_KEY = getattr(settings, "RECAPTCHA_PRIVATE_KEY", None) 7 | 8 | # Public key for Recaptcha 9 | RECAPTCHA_PUBLIC_KEY = getattr(settings, "RECAPTCHA_PUBLIC_KEY", None) 10 | 11 | DEBUG = getattr(settings, "DEBUG", False) 12 | 13 | # The permission function for checking if the request is coming from a cron job 14 | IS_CLOUD_SCHEDULER_FUNCTION = getattr(settings, "IS_CLOUD_SCHEDULER_FUNCTION", lambda _: False) 15 | 16 | # Half an hour 17 | SESSION_EXPIRY_TIME = 60 * 30 18 | 19 | # One hour 20 | SCREENTIME_WARNING_EXPIRY_TIME = 60 * 60 21 | 22 | TMP_AUTH_TOKEN = getattr(settings, "TMP_AUTH_TOKEN", "token") 23 | -------------------------------------------------------------------------------- /portal/backends.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import User 2 | from common.models import Student 3 | 4 | from common.helpers.generators import get_hashed_login_id 5 | 6 | 7 | class StudentLoginBackend: 8 | def get_user(self, user_id): 9 | try: 10 | return User.objects.get(id=user_id) 11 | except User.DoesNotExist: 12 | return None 13 | 14 | def authenticate(self, request, user_id=None, login_id=None): 15 | """Check the credentials and return a user.""" 16 | # Get the student by the user id 17 | user = self.get_user(user_id) 18 | if user: 19 | student = Student.objects.get(new_user=user) 20 | # Check the url against the student's stored hash then return the user. 21 | if student.login_id and get_hashed_login_id(login_id) == student.login_id: 22 | return user 23 | return None 24 | -------------------------------------------------------------------------------- /portal/beta.py: -------------------------------------------------------------------------------- 1 | def has_beta_access(request): 2 | return is_developer(request) 3 | 4 | 5 | def is_on_beta_host(request): 6 | return request.get_host().startswith("beta") 7 | 8 | 9 | def is_developer(request): 10 | return (not request.user.is_anonymous) and request.user.userprofile.developer 11 | -------------------------------------------------------------------------------- /portal/context_processors.py: -------------------------------------------------------------------------------- 1 | from portal.forms.dotmailer import NewsletterForm 2 | 3 | 4 | def process_newsletter_form(request): 5 | return {"news_form": NewsletterForm()} 6 | -------------------------------------------------------------------------------- /portal/forms/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/forms/__init__.py -------------------------------------------------------------------------------- /portal/forms/dotmailer.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | 4 | class NewsletterForm(forms.Form): 5 | email = forms.EmailField( 6 | label="Sign up to receive updates about Code for Life games and teaching resources.", 7 | label_suffix="", 8 | widget=forms.EmailInput( 9 | attrs={ 10 | "placeholder": "Your email address", 11 | "id": "newsletter_email_field", 12 | } 13 | ), 14 | help_text="Enter email address above", 15 | ) 16 | 17 | age_verification = forms.BooleanField( 18 | widget=forms.CheckboxInput(), initial=False, required=True 19 | ) 20 | 21 | 22 | class ConsentForm(forms.Form): 23 | email = forms.EmailField( 24 | label="Email", 25 | label_suffix="", 26 | widget=forms.EmailInput( 27 | attrs={"placeholder": "your.name@yourdomain.com"} 28 | ), 29 | ) 30 | 31 | consent_ticked = forms.BooleanField( 32 | widget=forms.CheckboxInput(), initial=False, required=True 33 | ) 34 | -------------------------------------------------------------------------------- /portal/forms/error_messages.py: -------------------------------------------------------------------------------- 1 | INVALID_LOGIN_MESSAGE = "Something is wrong! Please check that you typed your details correctly and that you have verified your account via email." 2 | -------------------------------------------------------------------------------- /portal/forms/invite_teacher.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | 4 | class InviteTeacherForm(forms.Form): 5 | 6 | teacher_first_name = forms.CharField( 7 | help_text="Enter first name of teacher", 8 | max_length=100, 9 | widget=forms.TextInput(attrs={"autocomplete": "off", "placeholder": "First name of teacher"}), 10 | ) 11 | teacher_last_name = forms.CharField( 12 | help_text="Enter last name of teacher", 13 | max_length=100, 14 | widget=forms.TextInput(attrs={"autocomplete": "off", "placeholder": "Last name of teacher"}), 15 | ) 16 | teacher_email = forms.EmailField( 17 | help_text="Enter email address", 18 | widget=forms.EmailInput(attrs={"autocomplete": "off", "placeholder": "Email address"}), 19 | ) 20 | 21 | make_admin_ticked = forms.BooleanField( 22 | label="Make an administrator of the school", 23 | widget=forms.CheckboxInput(), 24 | initial=False, 25 | required=False, 26 | ) 27 | -------------------------------------------------------------------------------- /portal/handlers.py: -------------------------------------------------------------------------------- 1 | from common.utils import two_factor_cache_key 2 | from django.core.cache import cache 3 | from django.db.models.signals import post_save, pre_delete 4 | from django.dispatch import receiver 5 | from django_otp.models import Device 6 | 7 | 8 | @receiver([post_save, pre_delete]) 9 | def clear_two_factor_cache(sender, **kwargs): 10 | if issubclass(sender, Device): 11 | user = kwargs["instance"].user 12 | cache.delete(two_factor_cache_key(user)) 13 | -------------------------------------------------------------------------------- /portal/helpers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/helpers/__init__.py -------------------------------------------------------------------------------- /portal/helpers/captcha.py: -------------------------------------------------------------------------------- 1 | from builtins import map 2 | 3 | 4 | def is_captcha_in_form(form): 5 | return "captcha" in form.fields 6 | 7 | 8 | def remove_captcha_from_forms(*args): 9 | list(map(remove_captcha_from_form, args)) 10 | 11 | 12 | def remove_captcha_from_form(form): 13 | form.fields.pop("captcha", None) 14 | -------------------------------------------------------------------------------- /portal/helpers/regexes.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | ACCESS_CODE_REGEX = "[a-zA-Z]{5}|[a-zA-Z]{2}[0-9]{3}" 4 | ACCESS_CODE_PATTERN = re.compile(rf"^{ACCESS_CODE_REGEX}$") 5 | EMAIL_REGEX = """(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])""" 6 | EMAIL_REGEX_PATTERN = re.compile(EMAIL_REGEX) 7 | ACCESS_CODE_FROM_URL_REGEX = "/login/student/(\w+)" 8 | ACCESS_CODE_FROM_URL_PATTERN = re.compile(ACCESS_CODE_FROM_URL_REGEX) 9 | JWT_REGEX = "([a-zA-Z0-9_=]+)\.([a-zA-Z0-9_=]+)\.([a-zA-Z0-9_\-\+\/=]*)" 10 | -------------------------------------------------------------------------------- /portal/helpers/request_handlers.py: -------------------------------------------------------------------------------- 1 | from portal.helpers.regexes import ACCESS_CODE_FROM_URL_PATTERN 2 | 3 | 4 | def get_access_code_from_request(request): 5 | try: 6 | access_code = ACCESS_CODE_FROM_URL_PATTERN.search(request.get_full_path()).group(1) 7 | except AttributeError: 8 | access_code = "" 9 | print(f"Access code not found in {request.get_full_path()}") 10 | return access_code 11 | -------------------------------------------------------------------------------- /portal/migrations/0042_school_country.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.db import models, migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [("portal", "0001_squashed_0041_new_news")] 9 | 10 | operations = [ 11 | migrations.AddField( 12 | model_name="school", 13 | name="country", 14 | field=models.CharField(max_length=200, null=True, blank=True), 15 | preserve_default=True, 16 | ) 17 | ] 18 | -------------------------------------------------------------------------------- /portal/migrations/0043_auto_20150430_0952.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.db import models, migrations 4 | import django_countries.fields 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [("portal", "0042_school_country")] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="school", 14 | name="country", 15 | field=django_countries.fields.CountryField( 16 | max_length=2, null=True, blank=True 17 | ), 18 | ) 19 | ] 20 | -------------------------------------------------------------------------------- /portal/migrations/0045_auto_20150430_1446.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.db import models, migrations 4 | import django_countries.fields 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [("portal", "0044_auto_20150430_0959")] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="school", 14 | name="country", 15 | field=django_countries.fields.CountryField(max_length=2), 16 | ) 17 | ] 18 | -------------------------------------------------------------------------------- /portal/migrations/0046_auto_20150723_1101.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.db import models, migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [("portal", "0045_auto_20150430_1446")] 9 | 10 | operations = [ 11 | migrations.AlterModelOptions( 12 | name="school", 13 | options={ 14 | "permissions": ( 15 | ("view_aggregated_data", "Can see available aggregated data"), 16 | ("view_map_data", "Can see schools' location displayed on map"), 17 | ) 18 | }, 19 | ) 20 | ] 21 | -------------------------------------------------------------------------------- /portal/migrations/0047_remove_userprofile_avatar.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.db import models, migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [("portal", "0046_auto_20150723_1101")] 9 | 10 | operations = [migrations.RemoveField(model_name="userprofile", name="avatar")] 11 | -------------------------------------------------------------------------------- /portal/migrations/0048_plural_management_frontnews.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.db import models, migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [("portal", "0047_remove_userprofile_avatar")] 9 | 10 | operations = [ 11 | migrations.AlterModelOptions( 12 | name="frontpagenews", options={"verbose_name_plural": "front page news"} 13 | ) 14 | ] 15 | -------------------------------------------------------------------------------- /portal/migrations/0050_refactor_emailverifications_2.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.db import models, migrations 4 | from django.conf import settings 5 | 6 | 7 | class Migration(migrations.Migration): 8 | dependencies = [("portal", "0049_refactor_emailverifications")] 9 | 10 | operations = [ 11 | migrations.RemoveField(model_name="emailverification", name="user"), 12 | migrations.RemoveField(model_name="emailverification", name="used"), 13 | migrations.AddField( 14 | model_name="emailverification", 15 | name="user", 16 | field=models.ForeignKey( 17 | to=settings.AUTH_USER_MODEL, 18 | related_name="email_verifications", 19 | null=True, 20 | blank=True, 21 | on_delete=models.CASCADE, 22 | ), 23 | ), 24 | migrations.RunSQL( 25 | ("UPDATE portal_emailverification" " SET user_id = new_user_id;") 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /portal/migrations/0052_refactor_emailverifications_3.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [("portal", "0051_add_missing_ev_records")] 8 | 9 | operations = [ 10 | migrations.RemoveField(model_name="emailverification", name="new_user") 11 | ] 12 | -------------------------------------------------------------------------------- /portal/migrations/0054_pending_join_request_can_be_blank.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.db import models, migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [("portal", "0053_refactor_teacher_student_1")] 9 | 10 | operations = [ 11 | migrations.AlterField( 12 | model_name="teacher", 13 | name="pending_join_request", 14 | field=models.ForeignKey( 15 | related_name="join_request", 16 | blank=True, 17 | to="portal.School", 18 | null=True, 19 | on_delete=models.SET_NULL, 20 | ), 21 | ) 22 | ] 23 | -------------------------------------------------------------------------------- /portal/migrations/0055_add_preview_user.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.db import models, migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [("portal", "0054_pending_join_request_can_be_blank")] 9 | 10 | operations = [ 11 | migrations.AddField( 12 | model_name="school", 13 | name="eligible_for_testing", 14 | field=models.BooleanField(default=False), 15 | ), 16 | migrations.AddField( 17 | model_name="userprofile", 18 | name="preview_user", 19 | field=models.BooleanField(default=False), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /portal/migrations/0056_remove_preview_user.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.10.8 on 2019-08-27 08:37 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [("portal", "0055_add_preview_user")] 10 | 11 | operations = [ 12 | migrations.RemoveField(model_name="userprofile", name="preview_user"), 13 | migrations.RemoveField(model_name="school", name="eligible_for_testing"), 14 | ] 15 | -------------------------------------------------------------------------------- /portal/migrations/0057_delete_frontpagenews.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 1.11.24 on 2020-03-02 13:10 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [("portal", "0056_remove_preview_user")] 10 | 11 | operations = [migrations.DeleteModel(name="FrontPageNews")] 12 | -------------------------------------------------------------------------------- /portal/migrations/0060_delete_guardian.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.0 on 2020-10-28 17:36 2 | 3 | from django.db import migrations 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ("portal", "0059_move_email_verifications_to_common"), 10 | ] 11 | 12 | operations = [ 13 | migrations.RemoveField( 14 | model_name="guardian", 15 | name="children", 16 | ), 17 | migrations.RemoveField( 18 | model_name="guardian", 19 | name="new_user", 20 | ), 21 | migrations.RemoveField( 22 | model_name="guardian", 23 | name="user", 24 | ), 25 | migrations.DeleteModel( 26 | name="Guardian", 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /portal/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /portal/mixins/__init__.py: -------------------------------------------------------------------------------- 1 | from .cron_mixin import CronMixin 2 | -------------------------------------------------------------------------------- /portal/mixins/cron_mixin.py: -------------------------------------------------------------------------------- 1 | from rest_framework.request import Request 2 | from rest_framework.response import Response 3 | 4 | from ..permissions import IsCronRequestFromGoogle 5 | 6 | 7 | class CronMixin: 8 | http_method_names = ["get"] 9 | permission_classes = [IsCronRequestFromGoogle] 10 | 11 | def get(self, request: Request) -> Response: 12 | raise NotImplementedError() 13 | -------------------------------------------------------------------------------- /portal/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/models.py -------------------------------------------------------------------------------- /portal/permissions/__init__.py: -------------------------------------------------------------------------------- 1 | from .is_cron_request_from_google import IsCronRequestFromGoogle 2 | -------------------------------------------------------------------------------- /portal/permissions/is_cron_request_from_google.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from rest_framework.permissions import BasePermission 3 | from rest_framework.request import Request 4 | from rest_framework.views import View 5 | 6 | 7 | class IsCronRequestFromGoogle(BasePermission): 8 | """ 9 | Validate that requests to your cron URLs are coming from App Engine and not from another source. 10 | https://cloud.google.com/appengine/docs/flexible/scheduling-jobs-with-cron-yaml#securing_urls_for_cron 11 | """ 12 | 13 | def has_permission(self, request: Request, view: View): 14 | return settings.DEBUG or request.META.get("HTTP_X_APPENGINE_CRON") == "true" 15 | -------------------------------------------------------------------------------- /portal/pipeline_compilers/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from .libsass_compiler import LibSassCompiler 3 | -------------------------------------------------------------------------------- /portal/pipeline_compilers/libsass_compiler.py: -------------------------------------------------------------------------------- 1 | """Libsass compiler for django-pipeline. 2 | Speedups development and/or production when compiling sass assets. No need of 3 | ruby sass anymore. 4 | """ 5 | 6 | import sass 7 | import codecs 8 | from pipeline.compilers import CompilerBase 9 | from django.conf import settings 10 | 11 | 12 | class LibSassCompiler(CompilerBase): 13 | output_extension = "css" 14 | 15 | def match_file(self, filename): 16 | return filename.endswith((".scss", ".sass")) 17 | 18 | def compile_file(self, infile, outfile, outdated=False, force=False): 19 | myfile = codecs.open(outfile, "w", "utf-8") 20 | 21 | if settings.DEBUG: 22 | myfile.write(sass.compile(filename=infile)) 23 | else: 24 | myfile.write(sass.compile(filename=infile, output_style="compressed")) 25 | return myfile.close() 26 | -------------------------------------------------------------------------------- /portal/static/portal/fonts/bootstrap/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/fonts/bootstrap/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /portal/static/portal/fonts/bootstrap/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/fonts/bootstrap/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /portal/static/portal/fonts/bootstrap/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/fonts/bootstrap/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /portal/static/portal/fonts/bootstrap/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/fonts/bootstrap/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /portal/static/portal/img/10_years_anniversary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/10_years_anniversary.png -------------------------------------------------------------------------------- /portal/static/portal/img/10x_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/10x_logo.png -------------------------------------------------------------------------------- /portal/static/portal/img/RR_logo_grass_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/RR_logo_grass_background.png -------------------------------------------------------------------------------- /portal/static/portal/img/RR_logo_simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/RR_logo_simple.png -------------------------------------------------------------------------------- /portal/static/portal/img/about_us_cfl.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/about_us_cfl.jpg -------------------------------------------------------------------------------- /portal/static/portal/img/about_us_hero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/about_us_hero.jpg -------------------------------------------------------------------------------- /portal/static/portal/img/about_us_ocado.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/about_us_ocado.jpg -------------------------------------------------------------------------------- /portal/static/portal/img/barefoot_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/barefoot_logo.png -------------------------------------------------------------------------------- /portal/static/portal/img/bcs_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/bcs_logo.png -------------------------------------------------------------------------------- /portal/static/portal/img/clubs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/clubs.png -------------------------------------------------------------------------------- /portal/static/portal/img/coding_club_hero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/coding_club_hero.jpg -------------------------------------------------------------------------------- /portal/static/portal/img/coding_club_python_pack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/coding_club_python_pack.png -------------------------------------------------------------------------------- /portal/static/portal/img/colorboxImages/border.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/colorboxImages/border.png -------------------------------------------------------------------------------- /portal/static/portal/img/colorboxImages/controls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/colorboxImages/controls.png -------------------------------------------------------------------------------- /portal/static/portal/img/colorboxImages/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/colorboxImages/loading.gif -------------------------------------------------------------------------------- /portal/static/portal/img/colorboxImages/loading_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/colorboxImages/loading_background.png -------------------------------------------------------------------------------- /portal/static/portal/img/colorboxImages/overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/colorboxImages/overlay.png -------------------------------------------------------------------------------- /portal/static/portal/img/confirmation_tick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/confirmation_tick.png -------------------------------------------------------------------------------- /portal/static/portal/img/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/cross.png -------------------------------------------------------------------------------- /portal/static/portal/img/dee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/dee.png -------------------------------------------------------------------------------- /portal/static/portal/img/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/facebook.png -------------------------------------------------------------------------------- /portal/static/portal/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/favicon.ico -------------------------------------------------------------------------------- /portal/static/portal/img/get_involved_hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/get_involved_hero.png -------------------------------------------------------------------------------- /portal/static/portal/img/gitbook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/gitbook.png -------------------------------------------------------------------------------- /portal/static/portal/img/gitbook_space.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/gitbook_space.png -------------------------------------------------------------------------------- /portal/static/portal/img/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/github.png -------------------------------------------------------------------------------- /portal/static/portal/img/github_hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/github_hero.png -------------------------------------------------------------------------------- /portal/static/portal/img/gla_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/gla_logo.png -------------------------------------------------------------------------------- /portal/static/portal/img/hamburger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/hamburger.png -------------------------------------------------------------------------------- /portal/static/portal/img/help_and_support_hero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/help_and_support_hero.jpg -------------------------------------------------------------------------------- /portal/static/portal/img/home_educate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/home_educate.png -------------------------------------------------------------------------------- /portal/static/portal/img/home_educate_hero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/home_educate_hero.jpg -------------------------------------------------------------------------------- /portal/static/portal/img/home_learning_hero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/home_learning_hero.jpg -------------------------------------------------------------------------------- /portal/static/portal/img/home_play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/home_play.png -------------------------------------------------------------------------------- /portal/static/portal/img/home_play_hero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/home_play_hero.jpg -------------------------------------------------------------------------------- /portal/static/portal/img/hope_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/hope_logo.png -------------------------------------------------------------------------------- /portal/static/portal/img/howe_dell_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/howe_dell_1.png -------------------------------------------------------------------------------- /portal/static/portal/img/howe_dell_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/howe_dell_2.png -------------------------------------------------------------------------------- /portal/static/portal/img/howe_dell_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/howe_dell_3.png -------------------------------------------------------------------------------- /portal/static/portal/img/icl_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/icl_logo.png -------------------------------------------------------------------------------- /portal/static/portal/img/icon_controller.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/icon_controller.png -------------------------------------------------------------------------------- /portal/static/portal/img/icon_free.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/icon_free.png -------------------------------------------------------------------------------- /portal/static/portal/img/icon_globe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/icon_globe.png -------------------------------------------------------------------------------- /portal/static/portal/img/icon_piechart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/icon_piechart.png -------------------------------------------------------------------------------- /portal/static/portal/img/icon_step_by_step.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/icon_step_by_step.png -------------------------------------------------------------------------------- /portal/static/portal/img/icon_tracking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/icon_tracking.png -------------------------------------------------------------------------------- /portal/static/portal/img/icon_uk_flag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/icon_uk_flag.png -------------------------------------------------------------------------------- /portal/static/portal/img/kirsty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/kirsty.png -------------------------------------------------------------------------------- /portal/static/portal/img/logo_cfl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/logo_cfl.png -------------------------------------------------------------------------------- /portal/static/portal/img/logo_cfl_reminder_cards.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/logo_cfl_reminder_cards.jpg -------------------------------------------------------------------------------- /portal/static/portal/img/logo_cfl_white_landscape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/logo_cfl_white_landscape.png -------------------------------------------------------------------------------- /portal/static/portal/img/logo_ocado.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/logo_ocado.png -------------------------------------------------------------------------------- /portal/static/portal/img/logo_ocado_group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/logo_ocado_group.png -------------------------------------------------------------------------------- /portal/static/portal/img/long_europe_map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/long_europe_map.png -------------------------------------------------------------------------------- /portal/static/portal/img/mc_saatchi_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/mc_saatchi_logo.png -------------------------------------------------------------------------------- /portal/static/portal/img/nigel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/nigel.png -------------------------------------------------------------------------------- /portal/static/portal/img/oval_blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Oval 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /portal/static/portal/img/oval_pink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Oval 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /portal/static/portal/img/oval_yellow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Oval 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /portal/static/portal/img/paper_plane.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/paper_plane.png -------------------------------------------------------------------------------- /portal/static/portal/img/phil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/phil.png -------------------------------------------------------------------------------- /portal/static/portal/img/polygon_blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Polygon 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /portal/static/portal/img/polygon_pink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Polygon 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /portal/static/portal/img/polygon_yellow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Polygon 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /portal/static/portal/img/pressure_cooker_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/pressure_cooker_logo.png -------------------------------------------------------------------------------- /portal/static/portal/img/python_den.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/python_den.png -------------------------------------------------------------------------------- /portal/static/portal/img/rapid_router.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/rapid_router.png -------------------------------------------------------------------------------- /portal/static/portal/img/rapid_router_landing_hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/rapid_router_landing_hero.png -------------------------------------------------------------------------------- /portal/static/portal/img/rapidrouter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/rapidrouter.png -------------------------------------------------------------------------------- /portal/static/portal/img/resources_hero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/resources_hero.jpg -------------------------------------------------------------------------------- /portal/static/portal/img/resources_montage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/resources_montage.jpg -------------------------------------------------------------------------------- /portal/static/portal/img/reuben.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/reuben.png -------------------------------------------------------------------------------- /portal/static/portal/img/rob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/rob.png -------------------------------------------------------------------------------- /portal/static/portal/img/rr_advanced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/rr_advanced.png -------------------------------------------------------------------------------- /portal/static/portal/img/rr_beginner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/rr_beginner.png -------------------------------------------------------------------------------- /portal/static/portal/img/rr_intermediate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/rr_intermediate.png -------------------------------------------------------------------------------- /portal/static/portal/img/sadface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/sadface.png -------------------------------------------------------------------------------- /portal/static/portal/img/sharon_harrison.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/sharon_harrison.jpg -------------------------------------------------------------------------------- /portal/static/portal/img/sian.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/sian.png -------------------------------------------------------------------------------- /portal/static/portal/img/teaching_resources_hero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/teaching_resources_hero.jpg -------------------------------------------------------------------------------- /portal/static/portal/img/ten_year_map_pin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /portal/static/portal/img/thumbnail_educate_rapid_router.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/thumbnail_educate_rapid_router.png -------------------------------------------------------------------------------- /portal/static/portal/img/thumbnail_educate_resources.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/thumbnail_educate_resources.png -------------------------------------------------------------------------------- /portal/static/portal/img/thumbnail_intro_c4l.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/thumbnail_intro_c4l.jpg -------------------------------------------------------------------------------- /portal/static/portal/img/thumbnail_play_rapid_router.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/thumbnail_play_rapid_router.png -------------------------------------------------------------------------------- /portal/static/portal/img/thumbnail_python_den.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/thumbnail_python_den.png -------------------------------------------------------------------------------- /portal/static/portal/img/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/twitter.png -------------------------------------------------------------------------------- /portal/static/portal/img/universities.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/universities.png -------------------------------------------------------------------------------- /portal/static/portal/img/wes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/img/wes.png -------------------------------------------------------------------------------- /portal/static/portal/js/carouselCards.js: -------------------------------------------------------------------------------- 1 | function setUpCarouselCards(cardsLength) { 2 | $(document).ready(function () { 3 | $("#carouselCards").on("slide.bs.carousel", function (event) { 4 | let cardIndex = $(event.relatedTarget).index(); 5 | let indexLimit = cardsLength - 3; 6 | 7 | // don't slide if the card is out of range 8 | if (cardIndex > indexLimit) { 9 | return false; 10 | } 11 | 12 | // add disabled classes if it's the first or last card 13 | if (cardIndex === 0) { 14 | $(".carousel-nav > .prev").addClass("disabled"); 15 | } else { 16 | $(".carousel-nav > .prev").removeClass("disabled"); 17 | } 18 | if (cardIndex >= indexLimit) { 19 | $(".carousel-nav > .next").addClass("disabled"); 20 | } else { 21 | $(".carousel-nav > .next").removeClass("disabled"); 22 | } 23 | }); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /portal/static/portal/js/independentLogin.js: -------------------------------------------------------------------------------- 1 | $(document).ready(() => { 2 | $('#independent_student_login_form').on('click', '#password-field-icon', () => { 3 | let inputType; 4 | let dataIcon = $('#password-field-icon').attr('data-icon'); 5 | if (dataIcon === 'material-symbols:visibility') { 6 | inputType = 'password'; 7 | dataIcon = 'material-symbols:visibility-off'; 8 | } else { 9 | inputType = 'text'; 10 | dataIcon = 'material-symbols:visibility'; 11 | } 12 | 13 | $('#id_password').attr('type', inputType); 14 | $('#password-field-icon').attr('data-icon', dataIcon); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /portal/static/portal/js/join_create_game_toggle.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | var formGameClassId = $("#id_game_class"); 3 | var createGameForm = $("#create-game-form"); 4 | 5 | $("#add-class-dropdown-menu > li > a") 6 | .filter(":not(.disabled)") 7 | .each(function () { 8 | let classId = $(this).data("classId"); 9 | $(this).click(function () { 10 | formGameClassId.val(classId); 11 | createGameForm.submit(); 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /portal/static/portal/js/resetPassword.js: -------------------------------------------------------------------------------- 1 | $(document).ready(() => { 2 | $('#reset-password-form').on( 3 | 'click', 4 | '#password-field-icon, #confirm-password-field-icon', 5 | () => { 6 | let inputType; 7 | let dataIcon = $('#password-field-icon').attr('data-icon'); 8 | if (dataIcon === 'material-symbols:visibility') { 9 | inputType = 'password'; 10 | dataIcon = 'material-symbols:visibility-off'; 11 | } else { 12 | inputType = 'text'; 13 | dataIcon = 'material-symbols:visibility'; 14 | } 15 | 16 | $('#id_new_password1').attr('type', inputType); 17 | $('#id_new_password2').attr('type', inputType); 18 | 19 | $('#password-field-icon').attr('data-icon', dataIcon); 20 | $('#confirm-password-field-icon').attr('data-icon', dataIcon); 21 | } 22 | ); 23 | }); 24 | -------------------------------------------------------------------------------- /portal/static/portal/js/school.js: -------------------------------------------------------------------------------- 1 | $(document).ready(() => { 2 | if ($("#id_country").val() !== 'GB') { 3 | $('#form-row-county').hide(); 4 | } 5 | 6 | $('#id_country').on('change', (event) => { 7 | if (event.target.value === 'GB') { 8 | $('#form-row-county').show(); 9 | } else { 10 | $('#form-row-county').hide(); 11 | } 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /portal/static/portal/js/sticky_subnav.js: -------------------------------------------------------------------------------- 1 | function toggleStickySubnav(scrollToTop) { 2 | $(window).on("scroll", function () { 3 | var currentScroll = $(window).scrollTop(); 4 | if (currentScroll >= scrollToTop) { 5 | if (!$(".sticky-subnav").hasClass("sub-nav--fixed")) { 6 | $(".sticky-subnav").addClass("sub-nav--fixed"); 7 | } 8 | if (!$("#sticky-warning").hasClass("sub-nav--warning--fixed")) { 9 | $("#sticky-warning").addClass("sub-nav--warning--fixed"); 10 | } 11 | $("#top").addClass("sub-nav--filler"); 12 | } else { 13 | $("#sticky-warning").removeClass("sub-nav--warning--fixed"); 14 | $(".sticky-subnav").removeClass("sub-nav--fixed"); 15 | $("#top").removeClass("sub-nav--filler"); 16 | $(".menu").removeClass("hide"); 17 | } 18 | }); 19 | } 20 | 21 | $(function () { 22 | $("a.x-icon").on("click", function () { 23 | $(this).parents(".sub-nav").remove(); 24 | return false; 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /portal/static/portal/js/studentLogin.js: -------------------------------------------------------------------------------- 1 | $(document).ready(() => { 2 | $('#form-login-school').on('click', '#password-field-icon', () => { 3 | let inputType; 4 | let dataIcon = $('#password-field-icon').attr('data-icon'); 5 | if (dataIcon === 'material-symbols:visibility') { 6 | inputType = 'password'; 7 | dataIcon = 'material-symbols:visibility-off'; 8 | } else { 9 | inputType = 'text'; 10 | dataIcon = 'material-symbols:visibility'; 11 | } 12 | 13 | $('#id_password').attr('type', inputType); 14 | $('#password-field-icon').attr('data-icon', dataIcon); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /portal/static/portal/js/teacherEditStudent.js: -------------------------------------------------------------------------------- 1 | $(document).ready(() => { 2 | $('#edit-student-password-form').on( 3 | 'click', 4 | '#password-field-icon, #confirm-password-field-icon', 5 | () => { 6 | let inputType; 7 | let dataIcon = $('#password-field-icon').attr('data-icon'); 8 | if (dataIcon === 'material-symbols:visibility') { 9 | inputType = 'password'; 10 | dataIcon = 'material-symbols:visibility-off'; 11 | } else { 12 | inputType = 'text'; 13 | dataIcon = 'material-symbols:visibility'; 14 | } 15 | 16 | $('#id_password').attr('type', inputType); 17 | $('#id_confirm_password').attr('type', inputType); 18 | 19 | $('#password-field-icon').attr('data-icon', dataIcon); 20 | $('#confirm-password-field-icon').attr('data-icon', dataIcon); 21 | } 22 | ); 23 | }); -------------------------------------------------------------------------------- /portal/static/portal/js/teacherLogin.js: -------------------------------------------------------------------------------- 1 | $(document).ready(() => { 2 | $('#form-login-teacher').on('click', '#password-field-icon', () => { 3 | let inputType; 4 | let dataIcon = $('#password-field-icon').attr('data-icon'); 5 | if (dataIcon === 'material-symbols:visibility') { 6 | inputType = 'password'; 7 | dataIcon = 'material-symbols:visibility-off'; 8 | } else { 9 | inputType = 'text'; 10 | dataIcon = 'material-symbols:visibility'; 11 | } 12 | 13 | $('#id_auth-password').attr('type', inputType); 14 | $('#password-field-icon').attr('data-icon', dataIcon); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /portal/static/portal/js/tenYearMap.js: -------------------------------------------------------------------------------- 1 | let currentActivePin; 2 | 3 | function setActivePin(city) { 4 | if (currentActivePin) { 5 | currentActivePin.setAttribute("fill", "#FFC709"); 6 | } 7 | 8 | currentActivePin = document.getElementById(city + "-pin").getElementsByTagName("svg")[0] 9 | currentActivePin.setAttribute("fill", "#EE0857"); 10 | } 11 | 12 | $(document).ready(function () { 13 | setActivePin("hatfield"); 14 | }); 15 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/_breadcrumbs.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Breadcrumbs 3 | // -------------------------------------------------- 4 | 5 | 6 | .breadcrumb { 7 | padding: $breadcrumb-padding-vertical $breadcrumb-padding-horizontal; 8 | margin-bottom: $line-height-computed; 9 | list-style: none; 10 | background-color: $breadcrumb-bg; 11 | border-radius: $border-radius-base; 12 | 13 | > li { 14 | display: inline-block; 15 | 16 | + li:before { 17 | padding: 0 5px; 18 | color: $breadcrumb-color; 19 | // [converter] Workaround for https://github.com/sass/libsass/issues/1115 20 | $nbsp: "\00a0"; 21 | content: "#{$breadcrumb-separator}#{$nbsp}"; // Unicode space added since inline-block means non-collapsing white-space 22 | } 23 | } 24 | 25 | > .active { 26 | color: $breadcrumb-active-color; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/_close.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Close icons 3 | // -------------------------------------------------- 4 | 5 | 6 | .close { 7 | float: right; 8 | font-size: ($font-size-base * 1.5); 9 | font-weight: $close-font-weight; 10 | line-height: 1; 11 | color: $close-color; 12 | text-shadow: $close-text-shadow; 13 | @include opacity(.2); 14 | 15 | &:hover, 16 | &:focus { 17 | color: $close-color; 18 | text-decoration: none; 19 | cursor: pointer; 20 | @include opacity(.5); 21 | } 22 | 23 | // [converter] extracted button& to button.close 24 | } 25 | 26 | // Additional properties for button version 27 | // iOS requires the button element instead of an anchor tag. 28 | // If you want the anchor version, it requires `href="#"`. 29 | // See https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile 30 | button.close { 31 | padding: 0; 32 | cursor: pointer; 33 | background: transparent; 34 | border: 0; 35 | -webkit-appearance: none; 36 | appearance: none; 37 | } 38 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/_component-animations.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Component animations 3 | // -------------------------------------------------- 4 | 5 | // Heads up! 6 | // 7 | // We don't use the `.opacity()` mixin here since it causes a bug with text 8 | // fields in IE7-8. Source: https://github.com/twbs/bootstrap/pull/3552. 9 | 10 | .fade { 11 | opacity: 0; 12 | @include transition(opacity .15s linear); 13 | 14 | &.in { 15 | opacity: 1; 16 | } 17 | } 18 | 19 | .collapse { 20 | display: none; 21 | 22 | &.in { display: block; } 23 | // [converter] extracted tr&.in to tr.collapse.in 24 | // [converter] extracted tbody&.in to tbody.collapse.in 25 | } 26 | 27 | tr.collapse.in { display: table-row; } 28 | 29 | tbody.collapse.in { display: table-row-group; } 30 | 31 | .collapsing { 32 | position: relative; 33 | height: 0; 34 | overflow: hidden; 35 | @include transition-property(height, visibility); 36 | @include transition-duration(.35s); 37 | @include transition-timing-function(ease); 38 | } 39 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/_responsive-embed.scss: -------------------------------------------------------------------------------- 1 | // Embeds responsive 2 | // 3 | // Credit: Nicolas Gallagher and SUIT CSS. 4 | 5 | .embed-responsive { 6 | position: relative; 7 | display: block; 8 | height: 0; 9 | padding: 0; 10 | overflow: hidden; 11 | 12 | .embed-responsive-item, 13 | iframe, 14 | embed, 15 | object, 16 | video { 17 | position: absolute; 18 | top: 0; 19 | bottom: 0; 20 | left: 0; 21 | width: 100%; 22 | height: 100%; 23 | border: 0; 24 | } 25 | } 26 | 27 | // Modifier class for 16:9 aspect ratio 28 | .embed-responsive-16by9 { 29 | padding-bottom: 56.25%; 30 | } 31 | 32 | // Modifier class for 4:3 aspect ratio 33 | .embed-responsive-4by3 { 34 | padding-bottom: 75%; 35 | } 36 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/_utilities.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Utility classes 3 | // -------------------------------------------------- 4 | 5 | 6 | // Floats 7 | // ------------------------- 8 | 9 | .clearfix { 10 | @include clearfix; 11 | } 12 | .center-block { 13 | @include center-block; 14 | } 15 | .pull-right { 16 | float: right !important; 17 | } 18 | .pull-left { 19 | float: left !important; 20 | } 21 | 22 | 23 | // Toggling content 24 | // ------------------------- 25 | 26 | // Note: Deprecated .hide in favor of .hidden or .sr-only (as appropriate) in v3.0.1 27 | .hide { 28 | display: none !important; 29 | } 30 | .show { 31 | display: block !important; 32 | } 33 | .invisible { 34 | visibility: hidden; 35 | } 36 | .text-hide { 37 | @include text-hide; 38 | } 39 | 40 | 41 | // Hide from screenreaders and browsers 42 | // 43 | // Credit: HTML5 Boilerplate 44 | 45 | .hidden { 46 | display: none !important; 47 | } 48 | 49 | 50 | // For Affix plugin 51 | // ------------------------- 52 | 53 | .affix { 54 | position: fixed; 55 | } 56 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/_wells.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Wells 3 | // -------------------------------------------------- 4 | 5 | 6 | // Base class 7 | .well { 8 | min-height: 20px; 9 | padding: 19px; 10 | margin-bottom: 20px; 11 | background-color: $well-bg; 12 | border: 1px solid $well-border; 13 | border-radius: $border-radius-base; 14 | @include box-shadow(inset 0 1px 1px rgba(0, 0, 0, .05)); 15 | blockquote { 16 | border-color: #ddd; 17 | border-color: rgba(0, 0, 0, .15); 18 | } 19 | } 20 | 21 | // Sizes 22 | .well-lg { 23 | padding: 24px; 24 | border-radius: $border-radius-large; 25 | } 26 | .well-sm { 27 | padding: 9px; 28 | border-radius: $border-radius-small; 29 | } 30 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/mixins/_alerts.scss: -------------------------------------------------------------------------------- 1 | // Alerts 2 | 3 | @mixin alert-variant($background, $border, $text-color) { 4 | color: $text-color; 5 | background-color: $background; 6 | border-color: $border; 7 | 8 | hr { 9 | border-top-color: darken($border, 5%); 10 | } 11 | 12 | .alert-link { 13 | color: darken($text-color, 10%); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/mixins/_background-variant.scss: -------------------------------------------------------------------------------- 1 | // Contextual backgrounds 2 | 3 | // [converter] $parent hack 4 | @mixin bg-variant($parent, $color) { 5 | #{$parent} { 6 | background-color: $color; 7 | } 8 | a#{$parent}:hover, 9 | a#{$parent}:focus { 10 | background-color: darken($color, 10%); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/mixins/_border-radius.scss: -------------------------------------------------------------------------------- 1 | // Single side border-radius 2 | 3 | @mixin border-top-radius($radius) { 4 | border-top-left-radius: $radius; 5 | border-top-right-radius: $radius; 6 | } 7 | @mixin border-right-radius($radius) { 8 | border-top-right-radius: $radius; 9 | border-bottom-right-radius: $radius; 10 | } 11 | @mixin border-bottom-radius($radius) { 12 | border-bottom-right-radius: $radius; 13 | border-bottom-left-radius: $radius; 14 | } 15 | @mixin border-left-radius($radius) { 16 | border-top-left-radius: $radius; 17 | border-bottom-left-radius: $radius; 18 | } 19 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/mixins/_center-block.scss: -------------------------------------------------------------------------------- 1 | // Center-align a block level element 2 | 3 | @mixin center-block() { 4 | display: block; 5 | margin-right: auto; 6 | margin-left: auto; 7 | } 8 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/mixins/_clearfix.scss: -------------------------------------------------------------------------------- 1 | // Clearfix 2 | // 3 | // For modern browsers 4 | // 1. The space content is one way to avoid an Opera bug when the 5 | // contenteditable attribute is included anywhere else in the document. 6 | // Otherwise it causes space to appear at the top and bottom of elements 7 | // that are clearfixed. 8 | // 2. The use of `table` rather than `block` is only necessary if using 9 | // `:before` to contain the top-margins of child elements. 10 | // 11 | // Source: http://nicolasgallagher.com/micro-clearfix-hack/ 12 | 13 | @mixin clearfix() { 14 | &:before, 15 | &:after { 16 | display: table; // 2 17 | content: " "; // 1 18 | } 19 | &:after { 20 | clear: both; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/mixins/_hide-text.scss: -------------------------------------------------------------------------------- 1 | // CSS image replacement 2 | // 3 | // Heads up! v3 launched with only `.hide-text()`, but per our pattern for 4 | // mixins being reused as classes with the same name, this doesn't hold up. As 5 | // of v3.0.1 we have added `.text-hide()` and deprecated `.hide-text()`. 6 | // 7 | // Source: https://github.com/h5bp/html5-boilerplate/commit/aa0396eae757 8 | 9 | // Deprecated as of v3.0.1 (has been removed in v4) 10 | @mixin hide-text() { 11 | font: 0/0 a; 12 | color: transparent; 13 | text-shadow: none; 14 | background-color: transparent; 15 | border: 0; 16 | } 17 | 18 | // New mixin to use as of v3.0.1 19 | @mixin text-hide() { 20 | @include hide-text; 21 | } 22 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/mixins/_labels.scss: -------------------------------------------------------------------------------- 1 | // Labels 2 | 3 | @mixin label-variant($color) { 4 | background-color: $color; 5 | 6 | &[href] { 7 | &:hover, 8 | &:focus { 9 | background-color: darken($color, 10%); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/mixins/_list-group.scss: -------------------------------------------------------------------------------- 1 | // List Groups 2 | 3 | @mixin list-group-item-variant($state, $background, $color) { 4 | .list-group-item-#{$state} { 5 | color: $color; 6 | background-color: $background; 7 | 8 | // [converter] extracted a&, button& to a.list-group-item-#{$state}, button.list-group-item-#{$state} 9 | } 10 | 11 | a.list-group-item-#{$state}, 12 | button.list-group-item-#{$state} { 13 | color: $color; 14 | 15 | .list-group-item-heading { 16 | color: inherit; 17 | } 18 | 19 | &:hover, 20 | &:focus { 21 | color: $color; 22 | background-color: darken($background, 5%); 23 | } 24 | &.active, 25 | &.active:hover, 26 | &.active:focus { 27 | color: #fff; 28 | background-color: $color; 29 | border-color: $color; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/mixins/_nav-divider.scss: -------------------------------------------------------------------------------- 1 | // Horizontal dividers 2 | // 3 | // Dividers (basically an hr) within dropdowns and nav lists 4 | 5 | @mixin nav-divider($color: #e5e5e5) { 6 | height: 1px; 7 | margin: (($line-height-computed / 2) - 1) 0; 8 | overflow: hidden; 9 | background-color: $color; 10 | } 11 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/mixins/_nav-vertical-align.scss: -------------------------------------------------------------------------------- 1 | // Navbar vertical align 2 | // 3 | // Vertically center elements in the navbar. 4 | // Example: an element has a height of 30px, so write out `.navbar-vertical-align(30px);` to calculate the appropriate top margin. 5 | 6 | @mixin navbar-vertical-align($element-height) { 7 | margin-top: (($navbar-height - $element-height) / 2); 8 | margin-bottom: (($navbar-height - $element-height) / 2); 9 | } 10 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/mixins/_opacity.scss: -------------------------------------------------------------------------------- 1 | // Opacity 2 | 3 | @mixin opacity($opacity) { 4 | $opacity-ie: ($opacity * 100); // IE8 filter 5 | filter: alpha(opacity=$opacity-ie); 6 | opacity: $opacity; 7 | } 8 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/mixins/_pagination.scss: -------------------------------------------------------------------------------- 1 | // Pagination 2 | 3 | @mixin pagination-size($padding-vertical, $padding-horizontal, $font-size, $line-height, $border-radius) { 4 | > li { 5 | > a, 6 | > span { 7 | padding: $padding-vertical $padding-horizontal; 8 | font-size: $font-size; 9 | line-height: $line-height; 10 | } 11 | &:first-child { 12 | > a, 13 | > span { 14 | @include border-left-radius($border-radius); 15 | } 16 | } 17 | &:last-child { 18 | > a, 19 | > span { 20 | @include border-right-radius($border-radius); 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/mixins/_panels.scss: -------------------------------------------------------------------------------- 1 | // Panels 2 | 3 | @mixin panel-variant($border, $heading-text-color, $heading-bg-color, $heading-border) { 4 | border-color: $border; 5 | 6 | & > .panel-heading { 7 | color: $heading-text-color; 8 | background-color: $heading-bg-color; 9 | border-color: $heading-border; 10 | 11 | + .panel-collapse > .panel-body { 12 | border-top-color: $border; 13 | } 14 | .badge { 15 | color: $heading-bg-color; 16 | background-color: $heading-text-color; 17 | } 18 | } 19 | & > .panel-footer { 20 | + .panel-collapse > .panel-body { 21 | border-bottom-color: $border; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/mixins/_progress-bar.scss: -------------------------------------------------------------------------------- 1 | // Progress bars 2 | 3 | @mixin progress-bar-variant($color) { 4 | background-color: $color; 5 | 6 | // Deprecated parent class requirement as of v3.2.0 7 | .progress-striped & { 8 | @include gradient-striped; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/mixins/_reset-filter.scss: -------------------------------------------------------------------------------- 1 | // Reset filters for IE 2 | // 3 | // When you need to remove a gradient background, do not forget to use this to reset 4 | // the IE filter for IE9 and below. 5 | 6 | @mixin reset-filter() { 7 | filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); 8 | } 9 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/mixins/_reset-text.scss: -------------------------------------------------------------------------------- 1 | @mixin reset-text() { 2 | font-family: $font-family-base; 3 | // We deliberately do NOT reset font-size. 4 | font-style: normal; 5 | font-weight: 400; 6 | line-height: $line-height-base; 7 | line-break: auto; 8 | text-align: left; // Fallback for where `start` is not supported 9 | text-align: start; 10 | text-decoration: none; 11 | text-shadow: none; 12 | text-transform: none; 13 | letter-spacing: normal; 14 | word-break: normal; 15 | word-spacing: normal; 16 | word-wrap: normal; 17 | white-space: normal; 18 | } 19 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/mixins/_resize.scss: -------------------------------------------------------------------------------- 1 | // Resize anything 2 | 3 | @mixin resizable($direction) { 4 | overflow: auto; // Per CSS3 UI, `resize` only applies when `overflow` isn't `visible` 5 | resize: $direction; // Options: horizontal, vertical, both 6 | } 7 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/mixins/_responsive-visibility.scss: -------------------------------------------------------------------------------- 1 | // [converter] $parent hack 2 | @mixin responsive-visibility($parent) { 3 | #{$parent} { 4 | display: block !important; 5 | } 6 | table#{$parent} { display: table !important; } 7 | tr#{$parent} { display: table-row !important; } 8 | th#{$parent}, 9 | td#{$parent} { display: table-cell !important; } 10 | } 11 | 12 | // [converter] $parent hack 13 | @mixin responsive-invisibility($parent) { 14 | #{$parent} { 15 | display: none !important; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/mixins/_size.scss: -------------------------------------------------------------------------------- 1 | // Sizing shortcuts 2 | 3 | @mixin size($width, $height) { 4 | width: $width; 5 | height: $height; 6 | } 7 | 8 | @mixin square($size) { 9 | @include size($size, $size); 10 | } 11 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/mixins/_tab-focus.scss: -------------------------------------------------------------------------------- 1 | // WebKit-style focus 2 | 3 | @mixin tab-focus() { 4 | // WebKit-specific. Other browsers will keep their default outline style. 5 | // (Initially tried to also force default via `outline: initial`, 6 | // but that seems to erroneously remove the outline in Firefox altogether.) 7 | outline: 5px auto -webkit-focus-ring-color; 8 | outline-offset: -2px; 9 | } 10 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/mixins/_table-row.scss: -------------------------------------------------------------------------------- 1 | // Tables 2 | 3 | @mixin table-row-variant($state, $background) { 4 | // Exact selectors below required to override `.table-striped` and prevent 5 | // inheritance to nested tables. 6 | .table > thead > tr, 7 | .table > tbody > tr, 8 | .table > tfoot > tr { 9 | > td.#{$state}, 10 | > th.#{$state}, 11 | &.#{$state} > td, 12 | &.#{$state} > th { 13 | background-color: $background; 14 | } 15 | } 16 | 17 | // Hover states for `.table-hover` 18 | // Note: this is not available for cells or rows within `thead` or `tfoot`. 19 | .table-hover > tbody > tr { 20 | > td.#{$state}:hover, 21 | > th.#{$state}:hover, 22 | &.#{$state}:hover > td, 23 | &:hover > .#{$state}, 24 | &.#{$state}:hover > th { 25 | background-color: darken($background, 5%); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/mixins/_text-emphasis.scss: -------------------------------------------------------------------------------- 1 | // Typography 2 | 3 | // [converter] $parent hack 4 | @mixin text-emphasis-variant($parent, $color) { 5 | #{$parent} { 6 | color: $color; 7 | } 8 | a#{$parent}:hover, 9 | a#{$parent}:focus { 10 | color: darken($color, 10%); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/mixins/_text-overflow.scss: -------------------------------------------------------------------------------- 1 | // Text overflow 2 | // Requires inline-block or block for proper styling 3 | 4 | @mixin text-overflow() { 5 | overflow: hidden; 6 | text-overflow: ellipsis; 7 | white-space: nowrap; 8 | } 9 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap/mixins/_text-truncate.scss: -------------------------------------------------------------------------------- 1 | // Text truncate 2 | // Requires inline-block or block for proper styling 3 | 4 | @mixin text-truncate() { 5 | overflow: hidden; 6 | text-overflow: ellipsis; 7 | white-space: nowrap; 8 | } 9 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap_mixins/_all.scss: -------------------------------------------------------------------------------- 1 | @import "border-radius"; 2 | @import "box-shadow"; 3 | @import "hover"; 4 | @import "nav-divider"; 5 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap_mixins/_border-radius.scss: -------------------------------------------------------------------------------- 1 | // Single side border-radius 2 | 3 | @mixin border-radius($radius: $border-radius) { 4 | @if $enable-rounded { 5 | border-radius: $radius; 6 | } 7 | } 8 | 9 | @mixin border-top-radius($radius) { 10 | @if $enable-rounded { 11 | border-top-left-radius: $radius; 12 | border-top-right-radius: $radius; 13 | } 14 | } 15 | 16 | @mixin border-right-radius($radius) { 17 | @if $enable-rounded { 18 | border-top-right-radius: $radius; 19 | border-bottom-right-radius: $radius; 20 | } 21 | } 22 | 23 | @mixin border-bottom-radius($radius) { 24 | @if $enable-rounded { 25 | border-bottom-right-radius: $radius; 26 | border-bottom-left-radius: $radius; 27 | } 28 | } 29 | 30 | @mixin border-left-radius($radius) { 31 | @if $enable-rounded { 32 | border-top-left-radius: $radius; 33 | border-bottom-left-radius: $radius; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap_mixins/_box-shadow.scss: -------------------------------------------------------------------------------- 1 | @mixin box-shadow($shadow...) { 2 | @if $enable-shadows { 3 | box-shadow: $shadow; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap_mixins/_nav-divider.scss: -------------------------------------------------------------------------------- 1 | // Horizontal dividers 2 | // 3 | // Dividers (basically an hr) within dropdowns and nav lists 4 | 5 | @mixin nav-divider($color: #e5e5e5) { 6 | height: 0; 7 | margin: ($spacer / 2) 0; 8 | overflow: hidden; 9 | border-top: 1px solid $color; 10 | } 11 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap_utilities/_align.scss: -------------------------------------------------------------------------------- 1 | .align-baseline { vertical-align: baseline !important; } // Browser default 2 | .align-top { vertical-align: top !important; } 3 | .align-middle { vertical-align: middle !important; } 4 | .align-bottom { vertical-align: bottom !important; } 5 | .align-text-bottom { vertical-align: text-bottom !important; } 6 | .align-text-top { vertical-align: text-top !important; } 7 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap_utilities/_background.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Contextual backgrounds 3 | // 4 | 5 | .bg-faded { 6 | background-color: darken($body-bg, 3%); 7 | } 8 | 9 | @include bg-variant('.bg-primary', $brand-primary); 10 | 11 | @include bg-variant('.bg-success', $brand-success); 12 | 13 | @include bg-variant('.bg-info', $brand-info); 14 | 15 | @include bg-variant('.bg-warning', $brand-warning); 16 | 17 | @include bg-variant('.bg-danger', $brand-danger); 18 | 19 | @include bg-variant('.bg-inverse', $brand-inverse); 20 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap_utilities/_borders.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Border 3 | // 4 | 5 | .border-0 { border: 0 !important; } 6 | .border-top-0 { border-top: 0 !important; } 7 | .border-right-0 { border-right: 0 !important; } 8 | .border-bottom-0 { border-bottom: 0 !important; } 9 | .border-left-0 { border-left: 0 !important; } 10 | 11 | // 12 | // Border-radius 13 | // 14 | 15 | .rounded { 16 | @include border-radius($border-radius); 17 | } 18 | .rounded-top { 19 | @include border-top-radius($border-radius); 20 | } 21 | .rounded-right { 22 | @include border-right-radius($border-radius); 23 | } 24 | .rounded-bottom { 25 | @include border-bottom-radius($border-radius); 26 | } 27 | .rounded-left { 28 | @include border-left-radius($border-radius); 29 | } 30 | 31 | .rounded-circle { 32 | border-radius: 50%; 33 | } 34 | 35 | .rounded-0 { 36 | border-radius: 0; 37 | } 38 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap_utilities/_clearfix.scss: -------------------------------------------------------------------------------- 1 | .clearfix { 2 | @include clearfix(); 3 | } 4 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap_utilities/_cursor.scss: -------------------------------------------------------------------------------- 1 | .cursor-pointer { 2 | cursor: pointer !important; 3 | } 4 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap_utilities/_float.scss: -------------------------------------------------------------------------------- 1 | @each $breakpoint in map-keys($grid-breakpoints) { 2 | @include media-breakpoint-up($breakpoint) { 3 | $infix: breakpoint-infix($breakpoint, $grid-breakpoints); 4 | 5 | .float#{$infix}-left { @include float-left; } 6 | .float#{$infix}-right { @include float-right; } 7 | .float#{$infix}-none { @include float-none; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap_utilities/_position.scss: -------------------------------------------------------------------------------- 1 | // Positioning 2 | 3 | .fixed-top { 4 | position: fixed; 5 | top: 0; 6 | right: 0; 7 | left: 0; 8 | z-index: $zindex-fixed; 9 | } 10 | 11 | .fixed-bottom { 12 | position: fixed; 13 | right: 0; 14 | bottom: 0; 15 | left: 0; 16 | z-index: $zindex-fixed; 17 | } 18 | 19 | .sticky-top { 20 | position: sticky; 21 | top: 0; 22 | z-index: $zindex-sticky; 23 | } 24 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap_utilities/_screenreaders.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Screenreaders 3 | // 4 | 5 | .sr-only { 6 | @include sr-only(); 7 | } 8 | 9 | .sr-only-focusable { 10 | @include sr-only-focusable(); 11 | } 12 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap_utilities/_sizing.scss: -------------------------------------------------------------------------------- 1 | // Width and height 2 | 3 | @each $prop, $abbrev in (width: w, height: h) { 4 | @each $size, $length in $sizes { 5 | .#{$abbrev}-#{$size} { #{$prop}: $length !important; } 6 | } 7 | } 8 | 9 | .mw-100 { max-width: 100% !important; } 10 | .mh-100 { max-height: 100% !important; } 11 | -------------------------------------------------------------------------------- /portal/static/portal/sass/bootstrap_utilities/_visibility.scss: -------------------------------------------------------------------------------- 1 | // 2 | // Visibility utilities 3 | // 4 | 5 | .visible { 6 | @include invisible(visible); 7 | } 8 | 9 | .invisible { 10 | @include invisible(hidden); 11 | } 12 | -------------------------------------------------------------------------------- /portal/static/portal/sass/modules/_all.scss: -------------------------------------------------------------------------------- 1 | @import 'animation'; 2 | @import 'breakpoints'; 3 | @import 'card_constants'; 4 | @import 'colours'; 5 | @import 'levels'; 6 | @import 'mixins'; 7 | @import 'spacing'; 8 | @import 'homepage_constants'; 9 | -------------------------------------------------------------------------------- /portal/static/portal/sass/modules/_animation.scss: -------------------------------------------------------------------------------- 1 | // Animation timing 2 | $fade-time: 0.15s; 3 | -------------------------------------------------------------------------------- /portal/static/portal/sass/modules/_breakpoints.scss: -------------------------------------------------------------------------------- 1 | // Desktop sizes 2 | $desktop-extra-large-min-width: 1570px; 3 | $desktop-large-max-width: $desktop-extra-large-min-width - 1; 4 | $desktop-large-min-width: 1400px; 5 | $desktop-small-max-width: $desktop-large-min-width - 1; 6 | $desktop-small-min-width: 1200px; 7 | 8 | // Tablet sizes 9 | $tablet-large-max-width: $desktop-small-min-width - 1; 10 | $tablet-large-min-width: 992px; 11 | $tablet-small-max-width: $tablet-large-min-width - 1; 12 | $tablet-small-min-width: 768px; 13 | 14 | // Mobile sizes 15 | $mobile-max-width: $tablet-small-min-width - 1; 16 | -------------------------------------------------------------------------------- /portal/static/portal/sass/modules/_card_constants.scss: -------------------------------------------------------------------------------- 1 | $card-minimum-width: 280px; 2 | $character-card-minimum-width: 240px; 3 | $character-card-image-max-height: 425px; 4 | -------------------------------------------------------------------------------- /portal/static/portal/sass/modules/_homepage_constants.scss: -------------------------------------------------------------------------------- 1 | $menu-height: 100px; 2 | $menu-height-xs: 80px; 3 | -------------------------------------------------------------------------------- /portal/static/portal/sass/modules/_levels.scss: -------------------------------------------------------------------------------- 1 | $hover-content-level: 100; 2 | $nav-bar-level: 1001; 3 | $popup-overlay-level: 1100; 4 | $behind-content-level: -100; 5 | -------------------------------------------------------------------------------- /portal/static/portal/sass/modules/_spacing.scss: -------------------------------------------------------------------------------- 1 | // Spacing unit 2 | $spacing: 5px; 3 | -------------------------------------------------------------------------------- /portal/static/portal/sass/partials/_base.scss: -------------------------------------------------------------------------------- 1 | @import '../modules/all'; 2 | -------------------------------------------------------------------------------- /portal/static/portal/sass/partials/_progress-bars.scss: -------------------------------------------------------------------------------- 1 | .progress { 2 | height: 10px; 3 | background-color: $color-primary-50; 4 | border-radius: 0; 5 | } 6 | 7 | .progress-bar { 8 | background-color: $color-primary; 9 | } 10 | -------------------------------------------------------------------------------- /portal/static/portal/sass/styles.scss: -------------------------------------------------------------------------------- 1 | @import "partials/base"; 2 | 3 | @import "partials/banners"; 4 | @import "partials/buttons"; 5 | @import "partials/carousel"; 6 | @import "partials/footer"; 7 | @import "partials/forms"; 8 | @import "partials/grids"; 9 | @import "partials/header"; 10 | @import "partials/images"; 11 | @import "partials/popup"; 12 | @import "partials/progress-bars"; 13 | @import "partials/subnavs"; 14 | @import "partials/tables"; 15 | @import "partials/text"; 16 | @import "partials/ui-dialog"; 17 | @import "partials/utils"; 18 | 19 | .content-footer-wrapper { 20 | display: flex; 21 | flex-direction: column; 22 | min-height: 100vh; 23 | justify-content: space-between; 24 | 25 | @media (min-width: $desktop-small-min-width) { 26 | padding-top: $menu-height; 27 | } 28 | 29 | @media (min-width: $tablet-large-min-width) and (max-width: $tablet-large-max-width) { 30 | padding-top: $menu-height-xs; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /portal/static/portal/video/code for life .pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/static/portal/video/code for life .pdf -------------------------------------------------------------------------------- /portal/strings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/strings/__init__.py -------------------------------------------------------------------------------- /portal/strings/about.py: -------------------------------------------------------------------------------- 1 | ABOUT_BANNER = { 2 | "title": "About Code for Life", 3 | "subtitle": "Code For Life gives everyone the ability to shape technology’s future", 4 | "text": "", 5 | "image_class": "banner--picture--about", 6 | } 7 | 8 | GETINVOLVED_BANNER = { 9 | "title": "Get involved", 10 | "subtitle": "How you can get involved with the creation of Code for Life products and resources", 11 | "image_class": "banner--picture--getinvolved", 12 | "button": { 13 | "external": True, 14 | "text": "Opportunities with Code for Life", 15 | "link": "https://2662351606-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M_6UDUiGoJDhr_OXetO%2Fuploads%2FPavvCifcuUeZZZLkV01C%2FOpportunities%20with%20Code%20for%20Life.pdf?alt=media", 16 | }, 17 | } 18 | 19 | CONTRIBUTE_BANNER = { 20 | "title": "Contribute to Code for Life", 21 | "subtitle": "How you can get involved with Code for Life products and resources and help us help others", 22 | "image_class": "banner--picture--contribute", 23 | } 24 | -------------------------------------------------------------------------------- /portal/strings/coding_club.py: -------------------------------------------------------------------------------- 1 | CODING_CLUB_BANNER = { 2 | "title": "Coding clubs", 3 | "subtitle": "A FREE set of slides and guides to run your own coding clubs", 4 | "text": "", 5 | "button": "", 6 | "image_class": "banner--picture--coding-club", 7 | "alt": "Boy using a laptop", 8 | "image_description": "Credit: Jeswin Thomas, Unsplash", 9 | } 10 | -------------------------------------------------------------------------------- /portal/strings/help_and_support.py: -------------------------------------------------------------------------------- 1 | HELP_BANNER = { 2 | "title": "Help and support", 3 | "subtitle": "", 4 | "text": "If you’ve run into problems, please look at our FAQs " 5 | "below for advice to commonly asked questions. If you still can’t find the " 6 | "answers, please contact us. One of the Code for Life team " 7 | "will get back to you and resolve your issues.", 8 | "image_class": "banner--picture--help", 9 | } 10 | -------------------------------------------------------------------------------- /portal/strings/home_learning.py: -------------------------------------------------------------------------------- 1 | HOME_LEARNING_BANNER = { 2 | "title": "Home learning", 3 | "subtitle": "Whether you're a parent, a caregiver, or a curious student — our Rapid Router game is easy to use " 4 | "and free – forever.", 5 | "text": "", 6 | "image_class": "banner--picture--homelearning", 7 | } 8 | -------------------------------------------------------------------------------- /portal/strings/materials.py: -------------------------------------------------------------------------------- 1 | MATERIALS_BANNER = { 2 | "title": "Rapid Router Teaching Packs", 3 | "subtitle": "A set of teaching materials to give you the confidence " 4 | "and resources to use Code for Life’s games with your students.", 5 | "text": "", 6 | "image_class": "banner--picture--tools", 7 | } 8 | -------------------------------------------------------------------------------- /portal/strings/play.py: -------------------------------------------------------------------------------- 1 | PLAY_BANNER = { 2 | "title": "Play", 3 | "subtitle": "Anyone can learn how to code. We will help you learn how. It’s " "fun, free and easy.", 4 | "text": "", 5 | "button": {"text": "Try Rapid Router", "link": "levels"}, 6 | "button2": {"text": "Try Python Den", "link": "python_levels"}, 7 | "image_class": "banner--picture--play", 8 | "alt": "Boy playing on ipad", 9 | "image_description": "Credit: Annie Spratt, Unsplash", 10 | } 11 | 12 | PLAY_HEADLINE = { 13 | "title": "Anyone can code, you can too!", 14 | "description": "Whether you’re a parent, teacher or a student, our games support " 15 | "and guide you, making learning to code great fun. Get started with Rapid Router " 16 | "designed for students new to coding. Rapid Router is where you will build up your " 17 | "ability.", 18 | } 19 | -------------------------------------------------------------------------------- /portal/strings/teach.py: -------------------------------------------------------------------------------- 1 | TEACH_BANNER = { 2 | "title": "Educate", 3 | "subtitle": "Get ready to teach the next generation of computer scientists", 4 | "text": "", 5 | "image_class": "banner--picture--educate", 6 | "alt": "Female teacher helping young female student on a laptop", 7 | } 8 | -------------------------------------------------------------------------------- /portal/strings/teacher_resources.py: -------------------------------------------------------------------------------- 1 | RAPID_ROUTER_RESOURCES_BANNER = { 2 | "title": "Rapid Router Resources", 3 | "subtitle": "We’ve created a comprehensive set of teaching materials to help you " 4 | "teach students Computing.", 5 | "text": "", 6 | "image_class": "banner--picture--play", 7 | "image_description": "Credit: Annie Spratt, Unsplash", 8 | "alt": "Boy playing on ipad", 9 | } 10 | -------------------------------------------------------------------------------- /portal/strings/ten_year_map.py: -------------------------------------------------------------------------------- 1 | TEN_YEAR_MAP_BANNER = { 2 | "title": "Celebrate", 3 | "subtitle": "Join us in celebrating our 10 year anniversary", 4 | "text": "", 5 | "image_class": "banner--picture--educate", 6 | "alt": "Female teacher helping young female student on a laptop", 7 | } 8 | 9 | TEN_YEAR_MAP_HEADLINE = { 10 | "title": "Code for Life is turning 10!", 11 | "description": "For our 10th anniversary, we're going global! Click on the pins on the map " 12 | "to check out what events are being run by our amazing volunteers from around the world.", 13 | } 14 | -------------------------------------------------------------------------------- /portal/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends 'portal/base.html' %} 2 | {% load static %} 3 | 4 | {% block content %} 5 | 6 |
7 |
8 |
9 |

Uh oh!

10 |
Sorry, Nigel can't find the page you were looking for.
11 |

This might be because you have entered a web address incorrectly or the page has moved.

12 |
13 | Nigel 14 |
15 | 16 |
17 | 18 | {% endblock content %} 19 | -------------------------------------------------------------------------------- /portal/templates/django_recaptcha/widget_v2_invisible.html: -------------------------------------------------------------------------------- 1 | 2 | {% include "django_recaptcha/includes/js_v2_invisible.html" %} 3 |
6 |
7 | -------------------------------------------------------------------------------- /portal/templates/email.html: -------------------------------------------------------------------------------- 1 | {% extends 'portal/email_style_template.html' %} 2 | {% load static %} 3 | {% block email_content %} 4 |

{{ title }}

5 | {{ content|urlize|linebreaks }} 6 | {% endblock email_content %} 7 | -------------------------------------------------------------------------------- /portal/templates/email.txt: -------------------------------------------------------------------------------- 1 | {{ content|safe }} 2 | -------------------------------------------------------------------------------- /portal/templates/portal/base_no_userprofile.html: -------------------------------------------------------------------------------- 1 | {% extends 'portal/base.html' %} 2 | 3 | {% block check_user_status %} 4 | {# remove script that checks user_status as admin accounts do not have userProfile #} 5 | {% endblock check_user_status %} 6 | 7 | {% block topBar %} 8 | {# remove topBar that checks user_status as admin accounts do not have userProfile #} 9 | {% endblock topBar %} 10 | 11 | {% block subNav %} 12 | {# remove subNav that checks user_status as admin accounts do not have userProfile #} 13 | {% endblock subNav %} 14 | -------------------------------------------------------------------------------- /portal/templates/portal/dotmailer_consent_form.html: -------------------------------------------------------------------------------- 1 | {% extends 'portal/base.html' %} 2 | 3 | {% block content %} 4 | 5 |
6 |
7 |

Your communication preferences

8 |
9 | {% csrf_token %} 10 | {{ form.non_field_errors }} 11 | 12 | 13 | {{ form.email }} 14 | {{ form.email.errors }} 15 | 16 |
17 |
18 | {{ form.consent_ticked }} 19 |
20 |

I confirm that I am happy to continue receiving email communication from Code for Life.

21 |
22 | 23 | 24 |
25 |
26 |
27 | 28 | {% endblock content %} 29 | -------------------------------------------------------------------------------- /portal/templates/portal/email_invitation_sent.html: -------------------------------------------------------------------------------- 1 | {% extends 'portal/base.html' %} 2 | {% load static %} 3 | {% load app_tags %} 4 | 5 | {% block content %} 6 | 7 |
8 |
9 |

Invitation sent

10 | Verification email sent 11 |

Your invitation is on its way. You will receive a notification on your dashboard once a teacher has 12 | requested to join your school or club.

13 | OK 14 |
15 |
16 | 17 | {% endblock content %} 18 | -------------------------------------------------------------------------------- /portal/templates/portal/email_verification_failed.html: -------------------------------------------------------------------------------- 1 | {% extends 'portal/form_shapes.html' %} 2 | {% load static %} 3 | {% load app_tags %} 4 | 5 | {% block form_content %} 6 |

Your email address verification failed

7 | Verification failure 8 |

You used an invalid link, either you mistyped the URL or that link is expired.

9 |

When you next attempt to log in, you will be sent a new verification email.

10 |
11 | {% if usertype == "TEACHER" %} 12 | Back to homepage 13 | {% else %} 14 | Back to homepage 15 | {% endif %} 16 |
17 | {% endblock form_content %} 18 | -------------------------------------------------------------------------------- /portal/templates/portal/locked_out.html: -------------------------------------------------------------------------------- 1 | {% extends 'portal/base.html' %} 2 | {% load static %} 3 | 4 | {% block content %} 5 |
6 |
7 |
8 |

Temporary lock out!

9 |
Your account has been temporarily blocked as there were too many unsuccessful requests.
10 |

If you wish to proceed, please 11 | {% if is_teacher %} 12 | reset your password. 13 | {% else %} 14 | reset your password. 15 | {% endif %} 16 | Alternatively, you will need to wait 24 hours for your account to be unlocked again.

17 |
18 | Phil 19 |
20 |
21 | {% endblock content %} 22 | -------------------------------------------------------------------------------- /portal/templates/portal/locked_out_school_student.html: -------------------------------------------------------------------------------- 1 | {% extends 'portal/base.html' %} 2 | {% load static %} 3 | 4 | {% block content %} 5 |
6 |
7 |
8 |

Temporary lock out!

9 |
Your account has been temporarily blocked as there were too many unsuccessful requests.
10 |

If you wish to proceed, please ask your teacher to reset your password. 11 | Alternatively, you will need to wait 24 hours for your account to be unlocked again.

12 |
13 | Phil 14 |
15 |
16 | {% endblock content %} 17 | -------------------------------------------------------------------------------- /portal/templates/portal/partials/character_list.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% load app_tags %} 3 | 4 |
5 | {% for character in characters %} 6 |
7 | {{ character.alt }} 8 |
{{ character.name }}
9 |

{{ character.description }}

10 |
11 | {% endfor %} 12 |
13 | -------------------------------------------------------------------------------- /portal/templates/portal/partials/headline.html: -------------------------------------------------------------------------------- 1 |
2 |

{{ title }}

3 |
4 |

{{ description }}

5 | -------------------------------------------------------------------------------- /portal/templates/portal/partials/hero_card.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% load app_tags %} 3 | 4 |
5 |
Activated
6 | 7 |
8 |

{{ title }}

9 |

{{ description }}

10 | 18 |
19 |
20 | -------------------------------------------------------------------------------- /portal/templates/portal/partials/info_popup.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {# An info popup with a close button in the top-right corner. #} 3 | 14 | -------------------------------------------------------------------------------- /portal/templates/portal/partials/popup.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {# A confirmation popup with Cancel and Confirm buttons. #} 3 | 18 | -------------------------------------------------------------------------------- /portal/templates/portal/partials/register_newsletter_tickbox.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{ newsletter_ticked }} 4 |
5 |

Sign up to receive updates about Code for Life games and teaching resources.

6 |
7 | -------------------------------------------------------------------------------- /portal/templates/portal/partials/screentime_popup.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {# A screen-time warning popup with a Continue button. #} 3 | 15 | -------------------------------------------------------------------------------- /portal/templates/portal/partials/service_unavailable_popup.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {# A screen-time warning popup with a Continue button. #} 3 | 18 | -------------------------------------------------------------------------------- /portal/templates/portal/partials/teacher_non_dashboard_subnav.html: -------------------------------------------------------------------------------- 1 | {% load app_tags %} 2 | 3 | 6 | 13 | -------------------------------------------------------------------------------- /portal/templates/portal/partials/teacher_non_dashboard_subnav_account.html: -------------------------------------------------------------------------------- 1 | {% load app_tags %} 2 | 3 | 6 | 13 | -------------------------------------------------------------------------------- /portal/templates/portal/reset_password_done.html: -------------------------------------------------------------------------------- 1 | {% extends 'portal/form_shapes.html' %} 2 | {% load static %} 3 | 4 | {% block form_content %} 5 |
6 |

Your password has been reset

7 | Confirmation tick 8 |

Please log in.

9 |
10 | {% if usertype == "TEACHER" %} 11 | OK 12 | {% else %} 13 | OK 14 | {% endif %} 15 |
16 | {% endblock form_content %} 17 | -------------------------------------------------------------------------------- /portal/templates/portal/reset_password_email_sent.html: -------------------------------------------------------------------------------- 1 | {% extends 'portal/form_shapes.html' %} 2 | {% load static %} 3 | 4 | {% block form_content %} 5 |
6 |

Thank you

7 |
If you have entered a valid email address, you will receive a link to 8 | reset your password. Make sure to check your spam.
9 | Verification email sent 11 |
12 | Open in Gmail 15 | Open in 16 | Outlook 17 |
18 |

< back to homepage

19 | {% endblock form_content %} -------------------------------------------------------------------------------- /portal/templates/portal/tag_manager/tag_manager_body.html: -------------------------------------------------------------------------------- 1 | {% if module_name == "default" %} 2 | 3 | 11 | {% endif %} 12 | {% if module_name == "staging" %} 13 | 14 | 22 | {% endif %} 23 | -------------------------------------------------------------------------------- /portal/templates/portal/teach/base_registering.html: -------------------------------------------------------------------------------- 1 | {% extends 'portal/base.html' %} 2 | {% load static %} 3 | {% load app_tags %} 4 | 5 | {% block subNav %} 6 | {% if not onboarding_done %} 7 | 17 | {% endif %} 18 | {% endblock subNav %} 19 | 20 | {% block content %} 21 | {% endblock content %} 22 | -------------------------------------------------------------------------------- /portal/templates/two_factor/_base.html: -------------------------------------------------------------------------------- 1 | {# Overriden the original "two_factor" template to style #} 2 | 3 | {% extends 'portal/base.html' %} 4 | {% block subNav %} 5 | {% include "portal/partials/teacher_non_dashboard_subnav_account.html" %} 6 | {% endblock subNav %} 7 | {% load future %} 8 | 9 | {% block contentDiv %} 10 |
11 |
12 | {% block content %} 13 | {% endblock content %} 14 |
15 |
16 | {% endblock contentDiv %} 17 | -------------------------------------------------------------------------------- /portal/templates/two_factor/_wizard_actions.html: -------------------------------------------------------------------------------- 1 | {# Overriding the original "two_factor" template #} 2 | {% load i18n %} 3 |
4 | {% trans "Cancel" %} 5 | 6 |
7 | -------------------------------------------------------------------------------- /portal/templates/two_factor/_wizard_actions_enable_2fa.html: -------------------------------------------------------------------------------- 1 | {# Overriding the original "two_factor" template #} 2 | {% load i18n %} 3 |
4 | {% trans "Cancel" %} 5 | 6 |
7 | -------------------------------------------------------------------------------- /portal/templates/two_factor/_wizard_actions_submit.html: -------------------------------------------------------------------------------- 1 | {# Overriding the original "two_factor" template #} 2 | {% load i18n %} 3 |
4 | {% trans "Cancel" %} 5 | 6 |
7 | -------------------------------------------------------------------------------- /portal/templates/two_factor/_wizard_forms.html: -------------------------------------------------------------------------------- 1 | {# Overriding the original "two_factor" template #} 2 |
3 |
4 | 5 | {{ wizard.management_form }} 6 | {{ wizard.form }} 7 | Enter your code from your app 8 |
9 |
10 | -------------------------------------------------------------------------------- /portal/templates/two_factor/_wizard_forms_token.html: -------------------------------------------------------------------------------- 1 | {# Overriding the original "two_factor" template #} 2 |
3 |
4 | 5 | {{ wizard.management_form }} 6 | {{ wizard.form }} 7 | Enter one of your tokens 8 |
9 |
10 | -------------------------------------------------------------------------------- /portal/templates/two_factor/backup_token.html: -------------------------------------------------------------------------------- 1 | {# Overriding the original "two_factor" template #} 2 |
3 |
4 | 5 | {{ wizard.management_form }} 6 | {{ wizard.form }} 7 | Enter one of your tokens 8 |
9 |
10 | -------------------------------------------------------------------------------- /portal/templates/two_factor/core/setup_complete.html: -------------------------------------------------------------------------------- 1 | {# Overriden the original "two_factor" template to direct us somewhere more sensible #} 2 | 3 | {% extends "two_factor/_base_focus.html" %} 4 | {% load i18n future %} 5 | 6 | {% block content %} 7 |

{% block title %}{% trans "Two-factor authentication set up complete" %}{% endblock %}

8 | 9 |

{% blocktrans %}You have successfully set up 2FA. 🎉{% endblocktrans %}

10 |

{% blocktrans %}You will now need to use your code generator the next time you log in.{% endblocktrans %}

11 | 12 |

{% trans "OK" %}

13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /portal/templates/two_factor/setup_wizard_token.html: -------------------------------------------------------------------------------- 1 | {# Overriding the original "two_factor" template #} 2 |
3 |
4 | 5 | {{ wizard.management_form }} 6 | {{ wizard.form }} 7 | Enter token generated by app 8 |
9 |
10 | -------------------------------------------------------------------------------- /portal/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/templatetags/__init__.py -------------------------------------------------------------------------------- /portal/templatetags/banner_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.inclusion_tag("portal/partials/banner.html", takes_context=True) 7 | def banner(context, banner_name): 8 | """ 9 | Registers the inclusion tag for the banner partial. 10 | Takes in the name of the banner. 11 | The template currently expects the following context elements: 12 | - title: the heading of the banner 13 | - subtitle (optional): a smaller heading below the title 14 | - text (optional): a description paragraph below the subtitle 15 | - button (optional): a dictionary containing the text and link of a button below 16 | the text elements 17 | - image_class: the CSS class of the image to be shown in the hexagon 18 | - image_description (optional): an image description that appears when hovering over the image 19 | """ 20 | return context[banner_name] 21 | -------------------------------------------------------------------------------- /portal/templatetags/card_list_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.inclusion_tag("portal/partials/card_list.html", takes_context=True) 7 | def card_list(context): 8 | """ 9 | Registers the inclusion tag for the card list partial. 10 | The template currently expects a list of elements which each contain the following: 11 | - image: the path to the card's image (top-half) 12 | - title: the heading of the card 13 | - description (optional): the text paragraph of the card 14 | """ 15 | return context["CARD_LIST"] 16 | -------------------------------------------------------------------------------- /portal/templatetags/future.py: -------------------------------------------------------------------------------- 1 | """ 2 | A stub for the future tag library to address the registered tag issue 3 | that arises with Django 1.10 4 | """ 5 | from django.template import Library 6 | 7 | register = Library() 8 | -------------------------------------------------------------------------------- /portal/templatetags/headline_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.inclusion_tag("portal/partials/headline.html", takes_context=True) 7 | def headline(context, headline_name): 8 | """ 9 | Registers the inclusion tag for the headline partial. 10 | Takes in the name of the headline. 11 | The template currently expects the following context elements: 12 | - title 13 | - description 14 | """ 15 | return context[headline_name] 16 | -------------------------------------------------------------------------------- /portal/templatetags/hero_card_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | @register.inclusion_tag("portal/partials/hero_card.html", takes_context=True) 7 | def hero_card(context, hero_card_name): 8 | """ 9 | Registers the inclusion tag for the hero card partial. 10 | The template currently expects the following context elements: 11 | - image: the path to the image at the top of the hero card 12 | - title: the heading of the hero card 13 | - description: the text paragraph of the hero card 14 | - button1: dictionary which contains: 15 | - text: the text on the card's first button 16 | - url: the url the first button redirects to 17 | - url_args: the args needed for the first button's url 18 | - button2: dictionary which contains: 19 | - text: the text on the card's second button 20 | - url: the url the second button redirects to 21 | - url_args: the args needed for the second button's url 22 | """ 23 | return context[hero_card_name] 24 | -------------------------------------------------------------------------------- /portal/templatetags/table_tags.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.template.defaultfilters import floatformat 3 | 4 | register = template.Library() 5 | 6 | 7 | @register.filter(name="tableformat") 8 | def tableformat(entry): 9 | 10 | if entry is None: 11 | return "-" 12 | elif is_numerical(entry): 13 | return floatformat(entry, -2) 14 | else: 15 | return entry 16 | 17 | 18 | def is_numerical(str): 19 | try: 20 | float(str) 21 | return True 22 | except (ValueError, TypeError): 23 | return False 24 | -------------------------------------------------------------------------------- /portal/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/tests/__init__.py -------------------------------------------------------------------------------- /portal/tests/cypress/integration/admin.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // import { adminPasswordPolicyTest } from './admin' 4 | 5 | // describe('Admin', () => { 6 | // it('has a strong password policy', () => { 7 | // adminPasswordPolicyTest() 8 | // }) 9 | // }) 10 | -------------------------------------------------------------------------------- /portal/tests/cypress/integration/student.spec.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { testMultipleLoginSessions } from "../support/multipleLoginSessionsTester.ts"; 4 | 5 | describe("Student", () => { 6 | it("cannot login from multiple sessions", () => { 7 | testMultipleLoginSessions(cy.loginAsStudent, "/play/"); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /portal/tests/cypress/integration/user.spec.js: -------------------------------------------------------------------------------- 1 | /// -------------------------------------------------------------------------------- /portal/tests/cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands.ts' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /portal/tests/cypress/support/registrationTester.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | export function testRegistration(registerFunction, registrationDetails, successful, errorMessage) { 4 | // log in, check the user is logged in and get the session cookie. 5 | registerFunction(...registrationDetails); 6 | 7 | if (!successful) { 8 | cy.get('.errorlist').should('be.visible') 9 | cy.get('.errorlist').should('have.text', errorMessage) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /portal/tests/data/al109ne_ab.json: -------------------------------------------------------------------------------- 1 | { 2 | "results" : [], 3 | "status" : "ZERO_RESULTS" 4 | } -------------------------------------------------------------------------------- /portal/tests/data/test_students_names.csv: -------------------------------------------------------------------------------- 1 | Name 2 | Student 1 3 | Student 2 4 | -------------------------------------------------------------------------------- /portal/tests/data/test_students_names_no_name.csv: -------------------------------------------------------------------------------- 1 | Something 2 | Student 1 3 | Student 2 4 | -------------------------------------------------------------------------------- /portal/tests/data/xxxxx.json: -------------------------------------------------------------------------------- 1 | { 2 | "results" : [], 3 | "status" : "ZERO_RESULTS" 4 | } 5 | -------------------------------------------------------------------------------- /portal/tests/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/tests/migrations/__init__.py -------------------------------------------------------------------------------- /portal/tests/migrations/test_migration_preview_user_remove.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from common.utils import field_exists 3 | 4 | 5 | @pytest.mark.django_db 6 | def test_preview_user_field_removed(migrator): 7 | migrator.apply_initial_migration(("portal", "0055_add_preview_user")) 8 | new_state = migrator.apply_tested_migration(("portal", "0056_remove_preview_user")) 9 | 10 | userprofile_model = new_state.apps.get_model("portal", "UserProfile") 11 | assert not field_exists(userprofile_model, "preview_user") 12 | 13 | school_model = new_state.apps.get_model("portal", "School") 14 | assert not field_exists(school_model, "eligible_for_testing") 15 | -------------------------------------------------------------------------------- /portal/tests/migrations/test_migration_preview_users.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.django_db 5 | def test_preview_user_field_added(migrator): 6 | migrator.apply_initial_migration(("portal", "0054_pending_join_request_can_be_blank")) 7 | new_state = migrator.apply_tested_migration(("portal", "0055_add_preview_user")) 8 | 9 | userprofile_model = new_state.apps.get_model("portal", "UserProfile") 10 | assert userprofile_model._meta.get_field("preview_user").get_internal_type(), "BooleanField" 11 | 12 | school_model = new_state.apps.get_model("portal", "School") 13 | assert school_model._meta.get_field("eligible_for_testing").get_internal_type(), "BooleanField" 14 | -------------------------------------------------------------------------------- /portal/tests/migrations/test_migration_remove_front_page_news.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.django_db 5 | def test_front_page_news_model_removed(migrator): 6 | migrator.apply_initial_migration(("portal", "0056_remove_preview_user")) 7 | new_state = migrator.apply_tested_migration(("portal", "0057_delete_frontpagenews")) 8 | 9 | model_names = [model._meta.db_table for model in new_state.apps.get_models()] 10 | assert "portal_frontpagenews" not in model_names 11 | -------------------------------------------------------------------------------- /portal/tests/migrations/test_migration_remove_guardian.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.django_db 5 | def test_guardian_model_removed(migrator): 6 | migrator.apply_initial_migration(("portal", "0059_move_email_verifications_to_common")) 7 | new_state = migrator.apply_tested_migration(("portal", "0060_delete_guardian")) 8 | 9 | model_names = [model._meta.db_table for model in new_state.apps.get_models()] 10 | assert "portal_guardian" not in model_names 11 | -------------------------------------------------------------------------------- /portal/tests/pageObjects/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/tests/pageObjects/__init__.py -------------------------------------------------------------------------------- /portal/tests/pageObjects/portal/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/tests/pageObjects/portal/__init__.py -------------------------------------------------------------------------------- /portal/tests/pageObjects/portal/admin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/tests/pageObjects/portal/admin/__init__.py -------------------------------------------------------------------------------- /portal/tests/pageObjects/portal/admin/admin_base_page.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | 3 | from portal.tests.pageObjects.portal.base_page import BasePage 4 | from portal.tests.pageObjects.portal.forbidden_page import ForbiddenPage 5 | 6 | 7 | class AdminBasePage(BasePage): 8 | def __init__(self, browser, live_server_url): 9 | super(AdminBasePage, self).__init__(browser) 10 | self.live_server_url = live_server_url 11 | -------------------------------------------------------------------------------- /portal/tests/pageObjects/portal/admin/admin_data_page.py: -------------------------------------------------------------------------------- 1 | from portal.tests.pageObjects.portal.admin.admin_base_page import AdminBasePage 2 | 3 | 4 | class AdminDataPage(AdminBasePage): 5 | def __init__(self, browser, live_server_url): 6 | super(AdminDataPage, self).__init__(browser, live_server_url) 7 | 8 | assert self.on_correct_page("admin_data") 9 | -------------------------------------------------------------------------------- /portal/tests/pageObjects/portal/admin/admin_map_page.py: -------------------------------------------------------------------------------- 1 | from portal.tests.pageObjects.portal.admin.admin_base_page import AdminBasePage 2 | 3 | 4 | class AdminMapPage(AdminBasePage): 5 | def __init__(self, browser, live_server_url): 6 | super(AdminMapPage, self).__init__(browser, live_server_url) 7 | 8 | assert self.on_correct_page("admin_map") 9 | -------------------------------------------------------------------------------- /portal/tests/pageObjects/portal/email_verification_needed_page.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from selenium.webdriver.common.by import By 4 | 5 | from . import home_page 6 | from .base_page import BasePage 7 | 8 | 9 | class EmailVerificationNeededPage(BasePage): 10 | def __init__(self, browser): 11 | super(EmailVerificationNeededPage, self).__init__(browser) 12 | assert self.on_correct_page("emailVerificationNeeded_page") 13 | 14 | def return_to_home_page(self): 15 | self.browser.find_element(By.ID, "home_button").click() 16 | return home_page.HomePage(self.browser) 17 | -------------------------------------------------------------------------------- /portal/tests/pageObjects/portal/forbidden_page.py: -------------------------------------------------------------------------------- 1 | from portal.tests.pageObjects.portal.base_page import BasePage 2 | 3 | 4 | class ForbiddenPage(BasePage): 5 | def __init__(self, browser): 6 | super(ForbiddenPage, self).__init__(browser) 7 | assert self.on_correct_page("403_forbidden") 8 | -------------------------------------------------------------------------------- /portal/tests/pageObjects/portal/password_reset_page.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from selenium.webdriver.common.by import By 4 | 5 | from .base_page import BasePage 6 | from .home_page import HomePage 7 | 8 | 9 | class PasswordResetPage(BasePage): 10 | def __init__(self, browser): 11 | super(PasswordResetPage, self).__init__(browser) 12 | 13 | assert self.on_correct_page("reset_password_page") 14 | 15 | def cancel(self): 16 | self.browser.find_element(By.ID, "cancel_button").click() 17 | return HomePage(self.browser) 18 | 19 | def reset_email_submit(self, email): 20 | self.browser.find_element(By.ID, "id_email").send_keys(email) 21 | 22 | self.wait_for_element_by_id("reset_button") 23 | 24 | self.browser.find_element(By.ID, "reset_button").click() 25 | return self 26 | -------------------------------------------------------------------------------- /portal/tests/pageObjects/portal/play/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/tests/pageObjects/portal/play/__init__.py -------------------------------------------------------------------------------- /portal/tests/pageObjects/portal/play/dashboard_page.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from selenium.webdriver.common.by import By 4 | 5 | from portal.tests.pageObjects.portal.play.join_school_or_club_page import JoinSchoolOrClubPage 6 | from .play_base_page import PlayBasePage 7 | 8 | 9 | class PlayDashboardPage(PlayBasePage): 10 | def __init__(self, browser): 11 | super(PlayDashboardPage, self).__init__(browser) 12 | 13 | assert self.on_correct_page("play_dashboard_page") 14 | 15 | def go_to_join_a_school_or_club_page(self): 16 | self.browser.find_element(By.ID, "student_join_school_link").click() 17 | 18 | return JoinSchoolOrClubPage(self.browser) 19 | -------------------------------------------------------------------------------- /portal/tests/pageObjects/portal/play/play_base_page.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import time 4 | 5 | from selenium.webdriver.common.by import By 6 | 7 | from portal.tests.pageObjects.portal.base_page import BasePage 8 | 9 | 10 | class PlayBasePage(BasePage): 11 | def __init__(self, browser): 12 | super(PlayBasePage, self).__init__(browser) 13 | 14 | def logout(self): 15 | self.open_user_options_box() 16 | self.browser.find_element(By.ID, "logout_button").click() 17 | 18 | from portal.tests.pageObjects.portal.home_page import HomePage 19 | 20 | return HomePage(self.browser) 21 | 22 | def go_to_account_page(self): 23 | self.open_user_options_box() 24 | self.browser.find_element(By.ID, "student_edit_account_button").click() 25 | 26 | from .account_page import PlayAccountPage 27 | 28 | return PlayAccountPage(self.browser) 29 | 30 | def open_user_options_box(self): 31 | self.browser.find_element(By.ID, "logout_menu").click() 32 | time.sleep(1) 33 | -------------------------------------------------------------------------------- /portal/tests/pageObjects/portal/play_page.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from .base_page import BasePage 4 | 5 | 6 | class PlayPage(BasePage): 7 | def __init__(self, browser): 8 | super(PlayPage, self).__init__(browser) 9 | 10 | assert self.on_correct_page("play_page") 11 | -------------------------------------------------------------------------------- /portal/tests/pageObjects/portal/resources_page.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from .base_page import BasePage 4 | 5 | 6 | class ResourcesPage(BasePage): 7 | def __init__(self, browser): 8 | super(ResourcesPage, self).__init__(browser) 9 | 10 | assert self.on_correct_page("resources_page") 11 | -------------------------------------------------------------------------------- /portal/tests/pageObjects/portal/teach/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/tests/pageObjects/portal/teach/__init__.py -------------------------------------------------------------------------------- /portal/tests/pageObjects/portal/teach/added_independent_student_to_class_page.py: -------------------------------------------------------------------------------- 1 | from selenium.webdriver.common.by import By 2 | 3 | from portal.tests.pageObjects.portal.teach.class_page import TeachClassPage 4 | from portal.tests.pageObjects.portal.teach.teach_base_page import TeachBasePage 5 | 6 | 7 | class AddedIndependentStudentToClassPage(TeachBasePage): 8 | def __init__(self, browser): 9 | super(AddedIndependentStudentToClassPage, self).__init__(browser) 10 | 11 | assert self.on_correct_page("added_independent_student_to_class") 12 | 13 | def return_to_class(self): 14 | self.browser.find_element(By.ID, "return_button").click() 15 | 16 | return TeachClassPage(self.browser) 17 | -------------------------------------------------------------------------------- /portal/tests/pageObjects/portal/teach/dismiss_students_page.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from selenium.webdriver.common.by import By 4 | 5 | from . import class_page 6 | from .teach_base_page import TeachBasePage 7 | 8 | 9 | class TeachDismissStudentsPage(TeachBasePage): 10 | def __init__(self, browser): 11 | super(TeachDismissStudentsPage, self).__init__(browser) 12 | 13 | assert self.on_correct_page("dismiss_students_page") 14 | 15 | def enter_email(self, email, id=0): 16 | self.browser.find_element(By.ID, f"id_form-{id}-email").send_keys(email) 17 | self.browser.find_element(By.ID, f"id_form-{id}-confirm_email").send_keys(email) 18 | return self 19 | 20 | def cancel(self): 21 | self.browser.find_element(By.ID, "cancel_button").click() 22 | return class_page.TeachClassPage(self.browser) 23 | 24 | def dismiss(self): 25 | self.browser.find_element(By.ID, "dismiss_button").click() 26 | return class_page.TeachClassPage(self.browser) 27 | -------------------------------------------------------------------------------- /portal/tests/pageObjects/portal/teach/move_classes_page.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from selenium.webdriver.common.by import By 4 | 5 | from .teach_base_page import TeachBasePage 6 | 7 | 8 | class TeachMoveClassesPage(TeachBasePage): 9 | def __init__(self, browser): 10 | super(TeachMoveClassesPage, self).__init__(browser) 11 | 12 | assert self.on_correct_page("move_all_classes_page") 13 | 14 | def move_and_kick(self): 15 | self.browser.find_element(By.ID, "move_classes_button").click() 16 | import portal.tests.pageObjects.portal.teach.dashboard_page as dashboard_page 17 | 18 | return dashboard_page.TeachDashboardPage(self.browser) 19 | 20 | def move_and_leave(self): 21 | self.browser.find_element(By.ID, "move_classes_button").click() 22 | import portal.tests.pageObjects.portal.teach.onboarding_organisation_page as onboarding_organisation_page 23 | 24 | return onboarding_organisation_page.OnboardingOrganisationPage(self.browser) 25 | -------------------------------------------------------------------------------- /portal/tests/pageObjects/portal/teach/move_students_disambiguate_page.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from selenium.webdriver.common.by import By 4 | 5 | from . import class_page 6 | from .teach_base_page import TeachBasePage 7 | 8 | 9 | class TeachMoveStudentsDisambiguatePage(TeachBasePage): 10 | def __init__(self, browser): 11 | super(TeachMoveStudentsDisambiguatePage, self).__init__(browser) 12 | 13 | assert self.on_correct_page("move_students_disambiguate_page") 14 | 15 | def cancel(self): 16 | self.browser.find_element(By.ID, "cancel_button").click() 17 | return class_page.TeachClassPage(self.browser) 18 | 19 | def move(self): 20 | self.browser.find_element(By.ID, "move_button").click() 21 | return class_page.TeachClassPage(self.browser) 22 | -------------------------------------------------------------------------------- /portal/tests/selenium_test_case.py: -------------------------------------------------------------------------------- 1 | """ 2 | Patch SeleniumTestCase from django-selenium-clean package. 3 | 4 | Instead of inheriting from StaticLiveServerTestCase, we inherit from LiveServerTestCase. 5 | This solves a bug introduced when upgrading to Django 1.11, 6 | see more information here: https://github.com/jazzband/django-pipeline/issues/593 7 | """ 8 | 9 | from django.contrib.staticfiles.testing import LiveServerTestCase 10 | from django_selenium_clean import SeleniumTestCase 11 | 12 | SeleniumTestCase.__bases__ = (LiveServerTestCase,) 13 | -------------------------------------------------------------------------------- /portal/tests/snapshots/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/tests/snapshots/__init__.py -------------------------------------------------------------------------------- /portal/tests/test_global_forms.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from django.test import TestCase, Client 3 | 4 | 5 | class TestGlobalForms(TestCase): 6 | def test_newsletter_signup_successful(self): 7 | url = reverse("process_newsletter_form") 8 | client = Client() 9 | data = {"email": "valid_email@example.com", "age_verification": "on"} 10 | response = client.post(url, data) 11 | messages = list(response.wsgi_request._messages) 12 | assert len([m for m in messages if m.tags == "success"]) == 1 13 | 14 | def test_newsletter_signup_fail(self): 15 | url = reverse("process_newsletter_form") 16 | client = Client() 17 | data = {"email": "invalid_email", "age_verification": "on"} 18 | response = client.post(url, data) 19 | messages = list(response.wsgi_request._messages) 20 | assert len([m for m in messages if "error" in m.tags]) == 1 21 | -------------------------------------------------------------------------------- /portal/tests/test_helper_methods.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from unittest.mock import patch, Mock 3 | 4 | from portal.helpers.password import is_password_pwned 5 | 6 | 7 | class TestClass: 8 | def test_is_password_pwned(self): 9 | weak_password = "Password123$" 10 | strong_password = "£EDCVFR$%TGBnhy667ujm" 11 | assert is_password_pwned(weak_password) 12 | assert not is_password_pwned(strong_password) 13 | 14 | @patch("requests.get") 15 | def test_is_password_pwned__status_code_not_200(self, mock_get): 16 | # Arrange 17 | password = "password123" 18 | sha1_hash = hashlib.sha1(password.encode()).hexdigest() 19 | 20 | mock_response = Mock() 21 | mock_response.status_code = 500 22 | 23 | mock_get.return_value = mock_response 24 | 25 | # Act 26 | result = is_password_pwned(password) 27 | 28 | # Assert 29 | mock_get.assert_called_once_with(f"https://api.pwnedpasswords.com/range/{sha1_hash[:5]}") 30 | assert not result 31 | -------------------------------------------------------------------------------- /portal/tests/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/tests/utils/__init__.py -------------------------------------------------------------------------------- /portal/tests/utils/classes.py: -------------------------------------------------------------------------------- 1 | from common.tests.utils.classes import generate_details 2 | 3 | 4 | def create_class(page, teacher_id=None): 5 | name, _ = generate_details() 6 | 7 | page = page.create_class(name, "False", teacher_id=teacher_id) 8 | 9 | return page, name 10 | -------------------------------------------------------------------------------- /portal/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/views/__init__.py -------------------------------------------------------------------------------- /portal/views/about.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | from portal.strings.about import ABOUT_BANNER, GETINVOLVED_BANNER, CONTRIBUTE_BANNER 4 | 5 | 6 | def about(request): 7 | return render(request, "portal/about.html", {"BANNER": ABOUT_BANNER}) 8 | 9 | 10 | def getinvolved(request): 11 | return render(request, "portal/getinvolved.html", {"BANNER": GETINVOLVED_BANNER}) 12 | 13 | 14 | def contribute(request): 15 | return render(request, "portal/contribute.html", {"BANNER": CONTRIBUTE_BANNER}) 16 | -------------------------------------------------------------------------------- /portal/views/cron/__init__.py: -------------------------------------------------------------------------------- 1 | from .user import * 2 | -------------------------------------------------------------------------------- /portal/views/google_analytics.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import requests 4 | from django.http import HttpRequest, JsonResponse 5 | from django.views.decorators.csrf import csrf_exempt 6 | 7 | 8 | @csrf_exempt 9 | def collect(request: HttpRequest): 10 | body: dict = json.loads(request.body) 11 | measurement_id = body["measurement_id"] 12 | api_secret = body["api_secret"] 13 | debug = body.get("debug", False) 14 | 15 | response = requests.post( 16 | url=( 17 | "https://www.google-analytics.com/debug/mp/collect" 18 | if debug 19 | else "https://www.google-analytics.com/mp/collect" 20 | ) 21 | + f"?measurement_id=${measurement_id}&api_secret=${api_secret}", 22 | json=body["payload"], 23 | ) 24 | 25 | return JsonResponse( 26 | data=response.json() if debug else {}, 27 | status=response.status_code, 28 | ) 29 | -------------------------------------------------------------------------------- /portal/views/legal.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | 4 | def privacy_notice(request): 5 | return render( 6 | request, 7 | "portal/privacy_notice.html", 8 | {"last_updated": "19th March 2025", "last_updated_children": "19th March 2025"}, 9 | ) 10 | 11 | 12 | def terms(request): 13 | return render(request, "portal/terms.html", {"last_updated": "11th July 2022"}) 14 | -------------------------------------------------------------------------------- /portal/views/login/__init__.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | 3 | import pytz 4 | from common.models import Teacher, Student 5 | from django.shortcuts import redirect 6 | from django.urls import reverse_lazy 7 | 8 | 9 | def old_login_form_redirect(request): 10 | return redirect(reverse_lazy("home")) 11 | 12 | 13 | def has_user_lockout_expired(user: Teacher or Student) -> bool: 14 | return datetime.now(tz=pytz.utc) - user.blocked_time > timedelta(hours=24) 15 | -------------------------------------------------------------------------------- /portal/views/play_landing_page.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | from portal.strings.play import ( 4 | PLAY_BANNER, 5 | PLAY_HEADLINE, 6 | ) 7 | 8 | 9 | def play_landing_page(request): 10 | return render( 11 | request, 12 | "portal/play.html", 13 | { 14 | "BANNER": PLAY_BANNER, 15 | "HEADLINE": PLAY_HEADLINE, 16 | }, 17 | ) 18 | -------------------------------------------------------------------------------- /portal/views/student/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/views/student/__init__.py -------------------------------------------------------------------------------- /portal/views/teach.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | from portal.strings.teach import TEACH_BANNER 4 | 5 | 6 | def teach(request): 7 | return render( 8 | request, 9 | "portal/teach.html", 10 | {"BANNER": TEACH_BANNER}, 11 | ) 12 | -------------------------------------------------------------------------------- /portal/views/teacher/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/views/teacher/__init__.py -------------------------------------------------------------------------------- /portal/views/two_factor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ocadotechnology/codeforlife-portal/00b3e4236e6c6964979e5a60c5bc954680537819/portal/views/two_factor/__init__.py -------------------------------------------------------------------------------- /portal/views/two_factor/form.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | 4 | # This is the form that asks if the user wants to disable 2FA after clicking disable 5 | # 2FA, setting it to always checked and hidden in CSS 6 | class DisableForm(forms.Form): 7 | understand = forms.BooleanField(label="", initial=True) 8 | -------------------------------------------------------------------------------- /portal/views/two_factor/profile.py: -------------------------------------------------------------------------------- 1 | from django.utils.decorators import method_decorator 2 | from django.views.decorators.cache import never_cache 3 | from two_factor.views.profile import DisableView 4 | 5 | from .form import DisableForm 6 | 7 | 8 | # This is not changed but imports the from form.py so it overwrites the disable 2FA form 9 | @method_decorator(never_cache, name='dispatch') 10 | class CustomDisableView(DisableView): 11 | form_class = DisableForm 12 | -------------------------------------------------------------------------------- /portal/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for codeforlife-portal project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "portal.settings") 13 | 14 | from django.core.wsgi import get_wsgi_application 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # https://github.com/python-semantic-release/python-semantic-release/blob/v7.33.0/docs/configuration.rst#distributions 2 | [tool.semantic_release] 3 | version_variables = ["portal/__init__.py:__version__"] 4 | 5 | [build-system] 6 | requires = ["setuptools", "wheel"] 7 | build-backend = "setuptools.build_meta:__legacy__" 8 | 9 | [tool.black] 10 | line-length = 120 11 | 12 | [tool.pytest.ini_options] 13 | DJANGO_SETTINGS_MODULE = "example_project.portal_test_settings" 14 | 15 | [tool.isort] 16 | profile = "black" 17 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | DJANGO_SETTINGS_MODULE = example_project.portal_test_settings 3 | addopts = -p no:warnings 4 | -------------------------------------------------------------------------------- /run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | cd "${BASH_SOURCE%/*}" 4 | 5 | ./manage.py migrate --noinput 6 | ./manage.py collectstatic --noinput --clear 7 | ./manage.py runserver "$@" 8 | -------------------------------------------------------------------------------- /run.bat: -------------------------------------------------------------------------------- 1 | pip install -e . 2 | python ./manage.py migrate --noinput 3 | python ./manage.py collectstatic --noinput 4 | python ./manage.py runserver localhost:8000 -------------------------------------------------------------------------------- /run_testserver: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | cd "${BASH_SOURCE%/*}" 4 | 5 | ./manage.py collectstatic --noinput --clear 6 | ./manage.py testserver portal/tests/cypress/fixtures/teachersToBeDeleted.json --settings="example_project.portal_test_settings" 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [coverage:run] 5 | source = portal 6 | 7 | [pep8] 8 | max-line-length = 160 9 | 10 | [semantic_release] 11 | version_variable = portal/__init__.py:__version__ -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "downlevelIteration": true, 6 | "target": "es5", 7 | "sourceMap": true, 8 | "allowJs": true, 9 | "jsx": "preserve", 10 | "lib": [ 11 | "es2017", 12 | "dom", 13 | "WebWorker.ImportScripts" 14 | ], 15 | "outDir": "./dist/", 16 | "baseUrl": "./src", 17 | "typeRoots": [ 18 | "node_modules/@types" 19 | ], 20 | "noUnusedLocals": true, 21 | "noImplicitReturns": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "types": [ 24 | "cypress" 25 | ] 26 | }, 27 | "include": [ 28 | "src/**/*", 29 | "portal/tests/cypress/**/*" 30 | ], 31 | "exclude": [ 32 | ".git", 33 | "node_modules/**/*" 34 | ] 35 | } --------------------------------------------------------------------------------