From 697c7cd76fa3f005f4f308f9c426cef7df82530b Mon Sep 17 00:00:00 2001 From: Anastasia Beglova Date: Mon, 29 Jul 2024 10:29:47 -0400 Subject: [PATCH 01/11] styling and icon updates (#1316) --- .../components/RootTopicIcon/RootTopicIcon.tsx | 4 ++-- .../ChannelDetails/ChannelDetails.tsx | 18 +++++++++++++++--- .../src/page-components/Header/Header.tsx | 8 ++++++-- .../src/page-components/Header/MenuButton.tsx | 15 ++++++++++++--- .../src/pages/DashboardPage/DashboardPage.tsx | 4 ++-- .../src/components/NavDrawer/NavDrawer.tsx | 15 +++++---------- 6 files changed, 42 insertions(+), 22 deletions(-) diff --git a/frontends/mit-open/src/components/RootTopicIcon/RootTopicIcon.tsx b/frontends/mit-open/src/components/RootTopicIcon/RootTopicIcon.tsx index 942c641905..fbfca85cdd 100644 --- a/frontends/mit-open/src/components/RootTopicIcon/RootTopicIcon.tsx +++ b/frontends/mit-open/src/components/RootTopicIcon/RootTopicIcon.tsx @@ -1,6 +1,6 @@ import { RiPaletteLine, - RiShakeHandsLine, + RiSeedlingLine, RiEarthLine, RiQuillPenLine, RiBriefcase3Line, @@ -27,7 +27,7 @@ export const ICON_MAP = { "Science & Math": RiTestTubeLine, "Social Sciences": RiUserSearchLine, Society: RiEarthLine, - "Education & Teaching": RiShakeHandsLine, + "Education & Teaching": RiSeedlingLine, Engineering: RiRobot2Line, "Innovation & Entrepreneurship": RiTeamLine, } diff --git a/frontends/mit-open/src/page-components/ChannelDetails/ChannelDetails.tsx b/frontends/mit-open/src/page-components/ChannelDetails/ChannelDetails.tsx index 407a888409..24d9a46a0d 100644 --- a/frontends/mit-open/src/page-components/ChannelDetails/ChannelDetails.tsx +++ b/frontends/mit-open/src/page-components/ChannelDetails/ChannelDetails.tsx @@ -33,6 +33,17 @@ const FACETS_BY_CHANNEL_TYPE: Record = { [ChannelTypeEnum.Pathway]: [], } +const ChannelLink = styled.a({ + display: "flex", + alignItems: "center", + span: { + paddingRight: "2px", + }, + svg: { + marginBottom: "2px", + }, +}) + const getFacetManifest = (channelType: ChannelTypeEnum) => { return [ { @@ -91,9 +102,10 @@ const getFacetManifest = (channelType: ChannelTypeEnum) => { labelFunction: (key: string, channelTitle: string) => ( // eslint-disable react/jsx-no-target-blank - - {channelTitle} Website - + + {channelTitle} Website + + ), order: 1, }, diff --git a/frontends/mit-open/src/page-components/Header/Header.tsx b/frontends/mit-open/src/page-components/Header/Header.tsx index 799cd61b45..a57e379df6 100644 --- a/frontends/mit-open/src/page-components/Header/Header.tsx +++ b/frontends/mit-open/src/page-components/Header/Header.tsx @@ -275,10 +275,14 @@ const Header: FunctionComponent = () => { - + - + diff --git a/frontends/mit-open/src/page-components/Header/MenuButton.tsx b/frontends/mit-open/src/page-components/Header/MenuButton.tsx index b0ae726e17..99218234c8 100644 --- a/frontends/mit-open/src/page-components/Header/MenuButton.tsx +++ b/frontends/mit-open/src/page-components/Header/MenuButton.tsx @@ -1,11 +1,15 @@ import { styled } from "ol-components" -import { RiMenuLine } from "@remixicon/react" +import { RiMenuLine, RiCloseLargeLine } from "@remixicon/react" import React from "react" const MenuIcon = styled(RiMenuLine)(({ theme }) => ({ color: theme.custom.colors.darkGray1, })) +const CloseMenuIcon = styled(RiCloseLargeLine)(({ theme }) => ({ + color: theme.custom.colors.darkGray1, +})) + const MenuButtonText = styled.div(({ theme }) => ({ alignSelf: "center", color: theme.custom.colors.darkGray2, @@ -43,12 +47,17 @@ const StyledMenuButton = styled.button(({ theme }) => ({ interface MenuButtonProps { text?: string onClick: React.MouseEventHandler | undefined + drawerOpen: boolean } -const MenuButton: React.FC = ({ text, onClick }) => ( +const MenuButton: React.FC = ({ + text, + onClick, + drawerOpen, +}) => ( - + {drawerOpen ? : } {text ? {text} : ""} diff --git a/frontends/mit-open/src/pages/DashboardPage/DashboardPage.tsx b/frontends/mit-open/src/pages/DashboardPage/DashboardPage.tsx index b578c594f3..a1c77396c2 100644 --- a/frontends/mit-open/src/pages/DashboardPage/DashboardPage.tsx +++ b/frontends/mit-open/src/pages/DashboardPage/DashboardPage.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useState } from "react" import { RiAccountCircleFill, RiDashboardLine, - RiBookMarkedLine, + RiBookmarkLine, RiEditLine, } from "@remixicon/react" import { @@ -340,7 +340,7 @@ const DashboardPage: React.FC = () => { currentValue={tabValue} /> } + icon={} text={TabLabels[TabValues.MY_LISTS]} value={TabValues.MY_LISTS} currentValue={tabValue} diff --git a/frontends/ol-components/src/components/NavDrawer/NavDrawer.tsx b/frontends/ol-components/src/components/NavDrawer/NavDrawer.tsx index f63d4cbf33..aee4fbdfac 100644 --- a/frontends/ol-components/src/components/NavDrawer/NavDrawer.tsx +++ b/frontends/ol-components/src/components/NavDrawer/NavDrawer.tsx @@ -30,13 +30,14 @@ const NavSectionHeader = styled.div(({ theme }) => ({ ...theme.typography.subtitle3, })) -const NavItemsContainer = styled.div({ +const NavItemsContainer = styled.div(({ theme }) => ({ display: "flex", flexDirection: "column", alignItems: "flex-start", alignSelf: "stretch", gap: "12px", -}) + color: theme.custom.colors.silverGrayDark, +})) const NavItemLink = styled.a({ display: "flex", @@ -52,6 +53,7 @@ const NavItemContainer = styled.div(({ theme }) => ({ alignSelf: "stretch", gap: "16px", "&:hover": { + color: theme.custom.colors.darkGray2, ".nav-link-icon": { opacity: "1", }, @@ -60,9 +62,6 @@ const NavItemContainer = styled.div(({ theme }) => ({ textDecorationLine: "underline", textDecorationColor: theme.custom.colors.red, }, - ".nav-link-description": { - color: theme.custom.colors.darkGray1, - }, }, })) @@ -93,7 +92,6 @@ const NavLinkText = styled.div(({ theme }) => ({ const NavLinkDescription = styled.div(({ theme }) => ({ alignSelf: "stretch", - color: theme.custom.colors.darkGray1, ...theme.typography.body3, })) @@ -132,10 +130,7 @@ const NavItem: React.FC = (props) => { {title} {href ? "" : "(Coming Soon)"} {description ? ( - + {description} ) : null} From 603eafe5d919d38d7206df658cebbfe9095c90e9 Mon Sep 17 00:00:00 2001 From: Chris Chudzicki Date: Mon, 29 Jul 2024 10:34:59 -0400 Subject: [PATCH 02/11] Resource availability: backend changes (#1301) --- frontends/api/src/generated/v1/api.ts | 151 ++++++++++++++++-- learning_resources/constants.py | 24 +-- learning_resources/etl/deduplication.py | 8 +- learning_resources/etl/deduplication_test.py | 12 +- learning_resources/etl/loaders.py | 4 +- learning_resources/etl/loaders_test.py | 10 +- learning_resources/etl/micromasters.py | 2 + learning_resources/etl/micromasters_test.py | 2 + learning_resources/etl/mitxonline.py | 12 +- learning_resources/etl/mitxonline_test.py | 17 +- learning_resources/etl/ocw.py | 6 +- learning_resources/etl/openedx.py | 47 +++++- learning_resources/etl/openedx_test.py | 111 ++++++++++++- learning_resources/etl/podcast.py | 4 +- learning_resources/etl/podcast_test.py | 5 +- learning_resources/etl/prolearn.py | 3 +- learning_resources/etl/prolearn_test.py | 2 + learning_resources/etl/utils.py | 4 +- learning_resources/etl/utils_test.py | 22 +-- learning_resources/etl/xpro.py | 3 + learning_resources/etl/xpro_test.py | 4 + learning_resources/etl/youtube.py | 9 +- learning_resources/etl/youtube_test.py | 9 +- learning_resources/factories.py | 11 +- .../0046_learningresource_certification.py | 4 +- .../0059_learningresource_availability.py | 21 +++ .../0060_partially_populate_availability.py | 40 +++++ learning_resources/models.py | 6 + learning_resources/serializers.py | 2 +- learning_resources/serializers_test.py | 1 + openapi/specs/v1.yaml | 97 ++++++++++- test_json/mitxonline_courses.json | 36 +++-- test_json/mitxonline_programs.json | 27 ++-- 33 files changed, 599 insertions(+), 117 deletions(-) create mode 100644 learning_resources/migrations/0059_learningresource_availability.py create mode 100644 learning_resources/migrations/0060_partially_populate_availability.py diff --git a/frontends/api/src/generated/v1/api.ts b/frontends/api/src/generated/v1/api.ts index ffb2c69453..2cc2d9cbdb 100644 --- a/frontends/api/src/generated/v1/api.ts +++ b/frontends/api/src/generated/v1/api.ts @@ -163,6 +163,31 @@ export interface ArticleRequest { */ title: string } +/** + * * `dated` - Dated * `anytime` - Anytime + * @export + * @enum {string} + */ + +export const AvailabilityEnumDescriptions = { + dated: "Dated", + anytime: "Anytime", +} as const + +export const AvailabilityEnum = { + /** + * Dated + */ + Dated: "dated", + /** + * Anytime + */ + Anytime: "anytime", +} as const + +export type AvailabilityEnum = + (typeof AvailabilityEnum)[keyof typeof AvailabilityEnum] + /** * * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @export @@ -733,6 +758,12 @@ export interface CourseResource { * @memberof CourseResource */ next_start_date?: string | null + /** + * + * @type {AvailabilityEnum} + * @memberof CourseResource + */ + availability?: AvailabilityEnum | null } /** @@ -860,7 +891,14 @@ export interface CourseResourceRequest { * @memberof CourseResourceRequest */ next_start_date?: string | null + /** + * + * @type {AvailabilityEnum} + * @memberof CourseResourceRequest + */ + availability?: AvailabilityEnum | null } + /** * * @export @@ -1369,6 +1407,12 @@ export interface LearningPathResource { * @memberof LearningPathResource */ next_start_date?: string | null + /** + * + * @type {AvailabilityEnum} + * @memberof LearningPathResource + */ + availability?: AvailabilityEnum | null } /** @@ -1437,7 +1481,14 @@ export interface LearningPathResourceRequest { * @memberof LearningPathResourceRequest */ next_start_date?: string | null + /** + * + * @type {AvailabilityEnum} + * @memberof LearningPathResourceRequest + */ + availability?: AvailabilityEnum | null } + /** * * @export @@ -2020,12 +2071,6 @@ export interface LearningResourceRun { * @memberof LearningResourceRun */ slug?: string | null - /** - * - * @type {string} - * @memberof LearningResourceRun - */ - availability?: string | null /** * * @type {string} @@ -2174,12 +2219,6 @@ export interface LearningResourceRunRequest { * @memberof LearningResourceRunRequest */ slug?: string | null - /** - * - * @type {string} - * @memberof LearningResourceRunRequest - */ - availability?: string | null /** * * @type {string} @@ -2433,6 +2472,22 @@ export interface MicroUserListRelationship { */ child: number } +/** + * + * @export + * @enum {string} + */ + +export const NullEnumDescriptions = { + null: "", +} as const + +export const NullEnum = { + Null: "null", +} as const + +export type NullEnum = (typeof NullEnum)[keyof typeof NullEnum] + /** * * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education * @export @@ -3207,7 +3262,14 @@ export interface PatchedLearningPathResourceRequest { * @memberof PatchedLearningPathResourceRequest */ next_start_date?: string | null + /** + * + * @type {AvailabilityEnum} + * @memberof PatchedLearningPathResourceRequest + */ + availability?: AvailabilityEnum | null } + /** * Serializer for UserListRelationship model * @export @@ -3870,6 +3932,12 @@ export interface PodcastEpisodeResource { * @memberof PodcastEpisodeResource */ next_start_date?: string | null + /** + * + * @type {AvailabilityEnum} + * @memberof PodcastEpisodeResource + */ + availability?: AvailabilityEnum | null } /** @@ -3938,7 +4006,14 @@ export interface PodcastEpisodeResourceRequest { * @memberof PodcastEpisodeResourceRequest */ next_start_date?: string | null + /** + * + * @type {AvailabilityEnum} + * @memberof PodcastEpisodeResourceRequest + */ + availability?: AvailabilityEnum | null } + /** * * @export @@ -4161,6 +4236,12 @@ export interface PodcastResource { * @memberof PodcastResource */ next_start_date?: string | null + /** + * + * @type {AvailabilityEnum} + * @memberof PodcastResource + */ + availability?: AvailabilityEnum | null } /** @@ -4229,7 +4310,14 @@ export interface PodcastResourceRequest { * @memberof PodcastResourceRequest */ next_start_date?: string | null + /** + * + * @type {AvailabilityEnum} + * @memberof PodcastResourceRequest + */ + availability?: AvailabilityEnum | null } + /** * * @export @@ -4678,6 +4766,12 @@ export interface ProgramResource { * @memberof ProgramResource */ next_start_date?: string | null + /** + * + * @type {AvailabilityEnum} + * @memberof ProgramResource + */ + availability?: AvailabilityEnum | null } /** @@ -4746,7 +4840,14 @@ export interface ProgramResourceRequest { * @memberof ProgramResourceRequest */ next_start_date?: string | null + /** + * + * @type {AvailabilityEnum} + * @memberof ProgramResourceRequest + */ + availability?: AvailabilityEnum | null } + /** * * @export @@ -5406,6 +5507,12 @@ export interface VideoPlaylistResource { * @memberof VideoPlaylistResource */ next_start_date?: string | null + /** + * + * @type {AvailabilityEnum} + * @memberof VideoPlaylistResource + */ + availability?: AvailabilityEnum | null } /** @@ -5474,7 +5581,14 @@ export interface VideoPlaylistResourceRequest { * @memberof VideoPlaylistResourceRequest */ next_start_date?: string | null + /** + * + * @type {AvailabilityEnum} + * @memberof VideoPlaylistResourceRequest + */ + availability?: AvailabilityEnum | null } + /** * * @export @@ -5691,6 +5805,12 @@ export interface VideoResource { * @memberof VideoResource */ next_start_date?: string | null + /** + * + * @type {AvailabilityEnum} + * @memberof VideoResource + */ + availability?: AvailabilityEnum | null } /** @@ -5759,7 +5879,14 @@ export interface VideoResourceRequest { * @memberof VideoResourceRequest */ next_start_date?: string | null + /** + * + * @type {AvailabilityEnum} + * @memberof VideoResourceRequest + */ + availability?: AvailabilityEnum | null } + /** * * @export diff --git a/learning_resources/constants.py b/learning_resources/constants.py index 8f9df5f418..8bd0024ec1 100644 --- a/learning_resources/constants.py +++ b/learning_resources/constants.py @@ -6,20 +6,24 @@ FAVORITES_TITLE = "Favorites" -class AvailabilityType(ExtendedEnum): +class RunAvailability(ExtendedEnum): """ Enum for Course availability options dictated by edX API values. - While these are the options coming in from edX that we store as is, we - display some values differently. Namely "Current" is displayed to the user - as "Available Now" and "Archived" is displayed as "Prior". - As of 06/21/2019, the above mapping occurs in `learning_resources.js:availabilityLabel()`. - All OCW courses should be set to "Current". - """ # noqa: E501 - - current = "Current" # displayed as "Available Now" + """ + + current = "Current" upcoming = "Upcoming" starting_soon = "Starting Soon" - archived = "Archived" # displayed as "Prior" + archived = "Archived" + + +class Availability(ExtendedEnum): + """ + Describes when a resource is available to users. + """ + + dated = "Dated" # available within specific date ranges + anytime = "Anytime" # available any time class LearningResourceType(ExtendedEnum): diff --git a/learning_resources/etl/deduplication.py b/learning_resources/etl/deduplication.py index 1727bfb910..9e69e1e860 100644 --- a/learning_resources/etl/deduplication.py +++ b/learning_resources/etl/deduplication.py @@ -1,6 +1,6 @@ """Functions to combine duplicate courses""" -from learning_resources.constants import AvailabilityType +from learning_resources.constants import RunAvailability def get_most_relevant_run(runs): @@ -15,7 +15,7 @@ def get_most_relevant_run(runs): # if there is a current run in the set pick it most_relevant_run = next( - (run for run in runs if run.availability == AvailabilityType.current.value), + (run for run in runs if run.availability == RunAvailability.current.value), None, ) @@ -28,8 +28,8 @@ def get_most_relevant_run(runs): for run in runs if run.availability in [ - AvailabilityType.upcoming.value, - AvailabilityType.starting_soon.value, + RunAvailability.upcoming.value, + RunAvailability.starting_soon.value, ] ), None, diff --git a/learning_resources/etl/deduplication_test.py b/learning_resources/etl/deduplication_test.py index d0575b11ed..89f8653f5b 100644 --- a/learning_resources/etl/deduplication_test.py +++ b/learning_resources/etl/deduplication_test.py @@ -4,7 +4,7 @@ import pytest -from learning_resources.constants import AvailabilityType +from learning_resources.constants import RunAvailability from learning_resources.etl.deduplication import get_most_relevant_run from learning_resources.factories import LearningResourceRunFactory from learning_resources.models import LearningResourceRun @@ -15,12 +15,12 @@ def test_get_most_relevant_run(): """Verify that most_relevant_run returns the correct run""" most_relevant_run = LearningResourceRunFactory.create( - availability=AvailabilityType.archived.value, + availability=RunAvailability.archived.value, start_date=datetime(2019, 10, 1, tzinfo=UTC), run_id="1", ) LearningResourceRunFactory.create( - availability=AvailabilityType.archived.value, + availability=RunAvailability.archived.value, start_date=datetime(2018, 10, 1, tzinfo=UTC), run_id="2", ) @@ -31,13 +31,13 @@ def test_get_most_relevant_run(): ) most_relevant_run = LearningResourceRunFactory.create( - availability=AvailabilityType.upcoming.value, + availability=RunAvailability.upcoming.value, start_date=datetime(2017, 10, 1, tzinfo=UTC), run_id="3", ) LearningResourceRunFactory.create( - availability=AvailabilityType.upcoming.value, + availability=RunAvailability.upcoming.value, start_date=datetime(2020, 10, 1, tzinfo=UTC), run_id="4", ) @@ -50,7 +50,7 @@ def test_get_most_relevant_run(): ) most_relevant_run = LearningResourceRunFactory.create( - availability=AvailabilityType.current.value, run_id="5" + availability=RunAvailability.current.value, run_id="5" ) assert ( diff --git a/learning_resources/etl/loaders.py b/learning_resources/etl/loaders.py index 021900bb29..52ba23ddf5 100644 --- a/learning_resources/etl/loaders.py +++ b/learning_resources/etl/loaders.py @@ -9,11 +9,11 @@ from django.db import transaction from learning_resources.constants import ( - AvailabilityType, LearningResourceFormat, LearningResourceRelationTypes, LearningResourceType, PlatformType, + RunAvailability, ) from learning_resources.etl.constants import ( READABLE_ID_FIELD, @@ -233,7 +233,7 @@ def load_run( instructors_data = run_data.pop("instructors", []) if ( - run_data.get("availability") == AvailabilityType.archived.value + run_data.get("availability") == RunAvailability.archived.value or learning_resource.certification is False ): # Archived runs or runs of resources w/out certificates should not have prices diff --git a/learning_resources/etl/loaders_test.py b/learning_resources/etl/loaders_test.py index 5bba504525..e908b9f0af 100644 --- a/learning_resources/etl/loaders_test.py +++ b/learning_resources/etl/loaders_test.py @@ -12,12 +12,12 @@ from django.utils import timezone from learning_resources.constants import ( - AvailabilityType, LearningResourceFormat, LearningResourceRelationTypes, LearningResourceType, OfferedBy, PlatformType, + RunAvailability, ) from learning_resources.etl.constants import ( CourseLoaderConfig, @@ -213,10 +213,12 @@ def test_load_program( # noqa: PLR0913 "image": {"url": program.learning_resource.image.url}, "published": is_published, "runs": [run_data], + "availability": program.learning_resource.availability, "courses": [ { "readable_id": course.learning_resource.readable_id, "platform": platform.code, + "availability": course.learning_resource.availability, } for course in courses ], @@ -598,7 +600,7 @@ def test_load_course_dupe_urls(unique_url): @pytest.mark.parametrize("run_exists", [True, False]) @pytest.mark.parametrize( - "availability", [AvailabilityType.archived.value, AvailabilityType.current.value] + "availability", [RunAvailability.archived.value, RunAvailability.current.value] ) @pytest.mark.parametrize("certification", [True, False]) def test_load_run(run_exists, availability, certification): @@ -635,7 +637,7 @@ def test_load_run(run_exists, availability, certification): assert result.prices == ( [] - if (availability == AvailabilityType.archived.value or certification is False) + if (availability == RunAvailability.archived.value or certification is False) else sorted(props["prices"]) ) props.pop("prices") @@ -1397,7 +1399,7 @@ def test_load_prices_by_certificate(certification): run = LearningResourceRunFactory.create( learning_resource=course, published=True, - availability=AvailabilityType.current.value, + availability=RunAvailability.current.value, prices=[Decimal("0.00"), Decimal("20.00")], ) load_next_start_date_and_prices(course) diff --git a/learning_resources/etl/micromasters.py b/learning_resources/etl/micromasters.py index b0234c19eb..c33477c7e7 100644 --- a/learning_resources/etl/micromasters.py +++ b/learning_resources/etl/micromasters.py @@ -6,6 +6,7 @@ from django.conf import settings from learning_resources.constants import ( + Availability, CertificationType, LearningResourceType, OfferedBy, @@ -92,6 +93,7 @@ def transform(programs_data): } ], "topics": program["topics"], + "availability": Availability.dated.name, # only need positioning of courses by course_id for course data "courses": [ { diff --git a/learning_resources/etl/micromasters_test.py b/learning_resources/etl/micromasters_test.py index 8cbb7c6df4..4b2c92fb03 100644 --- a/learning_resources/etl/micromasters_test.py +++ b/learning_resources/etl/micromasters_test.py @@ -4,6 +4,7 @@ import pytest from learning_resources.constants import ( + Availability, CertificationType, LearningResourceType, PlatformType, @@ -127,6 +128,7 @@ def test_micromasters_transform(mock_micromasters_data, missing_url): "etl_source": ETLSource.micromasters.name, "certification": True, "certification_type": CertificationType.micromasters.name, + "availability": Availability.dated.name, "courses": [ { "readable_id": "1", diff --git a/learning_resources/etl/mitxonline.py b/learning_resources/etl/mitxonline.py index f605b71c93..5edbcc3a8e 100644 --- a/learning_resources/etl/mitxonline.py +++ b/learning_resources/etl/mitxonline.py @@ -11,11 +11,11 @@ from django.conf import settings from learning_resources.constants import ( - AvailabilityType, CertificationType, LearningResourceType, OfferedBy, PlatformType, + RunAvailability, ) from learning_resources.etl.constants import ETLSource from learning_resources.etl.utils import ( @@ -211,9 +211,9 @@ def _transform_run(course_run: dict, course: dict) -> dict: {"full_name": instructor["name"]} for instructor in parse_page_attribute(course, "instructors", is_list=True) ], - "availability": AvailabilityType.current.value + "availability": RunAvailability.current.value if parse_page_attribute(course, "page_url") - else AvailabilityType.archived.value, + else RunAvailability.archived.value, } @@ -257,6 +257,7 @@ def _transform_course(course): "image": _transform_image(course), "url": parse_page_attribute(course, "page_url", is_url=True), "description": clean_data(parse_page_attribute(course, "description")), + "availability": course.get("availability"), } @@ -316,6 +317,7 @@ def transform_programs(programs): "description": clean_data(parse_page_attribute(program, "description")), "url": parse_page_attribute(program, "page_url", is_url=True), "image": _transform_image(program), + "availability": program.get("availability"), "published": bool( parse_page_attribute(program, "page_url") ), # a program is only considered published if it has a page url @@ -340,9 +342,9 @@ def transform_programs(programs): parse_page_attribute(program, "description") ), "prices": parse_program_prices(program), - "availability": AvailabilityType.current.value + "availability": RunAvailability.current.value if parse_page_attribute(program, "page_url") - else AvailabilityType.archived.value, + else RunAvailability.archived.value, } ], "courses": transform_courses( diff --git a/learning_resources/etl/mitxonline_test.py b/learning_resources/etl/mitxonline_test.py index 8d6be4e130..900aa57f22 100644 --- a/learning_resources/etl/mitxonline_test.py +++ b/learning_resources/etl/mitxonline_test.py @@ -10,10 +10,10 @@ import pytest from learning_resources.constants import ( - AvailabilityType, CertificationType, LearningResourceType, PlatformType, + RunAvailability, ) from learning_resources.etl.constants import CourseNumberType, ETLSource from learning_resources.etl.mitxonline import ( @@ -147,6 +147,7 @@ def test_mitxonline_transform_programs( program_data.get("page", {}).get("page_url", None) is not None ), "url": parse_page_attribute(program_data, "page_url", is_url=True), + "availability": program_data["availability"], "topics": transform_topics(program_data["topics"], OFFERED_BY["code"]), "runs": [ { @@ -165,9 +166,9 @@ def test_mitxonline_transform_programs( program_data.get("page", {}).get("description", None) ), "url": parse_page_attribute(program_data, "page_url", is_url=True), - "availability": AvailabilityType.current.value + "availability": RunAvailability.current.value if parse_page_attribute(program_data, "page_url") - else AvailabilityType.archived.value, + else RunAvailability.archived.value, } ], "courses": [ @@ -201,6 +202,7 @@ def test_mitxonline_transform_programs( "certification": True, "certification_type": CertificationType.completion.name, "url": parse_page_attribute(course_data, "page_url", is_url=True), + "availability": course_data["availability"], "topics": transform_topics( course_data["topics"], OFFERED_BY["code"] ), @@ -242,9 +244,9 @@ def test_mitxonline_transform_programs( course_run_data, "instructors", is_list=True ) ], - "availability": AvailabilityType.current.value + "availability": RunAvailability.current.value if parse_page_attribute(course_data, "page_url") - else AvailabilityType.archived.value, + else RunAvailability.archived.value, } for course_run_data in course_data["courseruns"] ], @@ -376,9 +378,9 @@ def test_mitxonline_transform_courses(settings, mock_mitxonline_courses_data): course_run_data, "instructors", is_list=True ) ], - "availability": AvailabilityType.current.value + "availability": RunAvailability.current.value if parse_page_attribute(course_data, "page_url") - else AvailabilityType.archived.value, + else RunAvailability.archived.value, } for course_run_data in course_data["courseruns"] ], @@ -393,6 +395,7 @@ def test_mitxonline_transform_courses(settings, mock_mitxonline_courses_data): } ] }, + "availability": course_data["availability"], } for course_data in mock_mitxonline_courses_data["results"] if "PROCTORED EXAM" not in course_data["title"] diff --git a/learning_resources/etl/ocw.py b/learning_resources/etl/ocw.py index 866bd64b73..5ad43b79f3 100644 --- a/learning_resources/etl/ocw.py +++ b/learning_resources/etl/ocw.py @@ -19,10 +19,11 @@ CONTENT_TYPE_PAGE, CONTENT_TYPE_VIDEO, VALID_TEXT_FILE_TYPES, - AvailabilityType, + Availability, LearningResourceType, OfferedBy, PlatformType, + RunAvailability, ) from learning_resources.etl.constants import ETLSource from learning_resources.etl.utils import ( @@ -244,7 +245,7 @@ def transform_run(course_data: dict) -> dict: "description": clean_data(course_data.get("course_description_html")), "year": year, "semester": semester, - "availability": AvailabilityType.current.value, + "availability": RunAvailability.current.value, "image": { "url": urljoin(settings.OCW_BASE_URL, image_src) if image_src else None, "description": course_data.get("course_image_metadata", {}).get( @@ -346,6 +347,7 @@ def transform_course(course_data: dict) -> dict: "runs": [transform_run(course_data)], "resource_type": LearningResourceType.course.name, "unique_field": UNIQUE_FIELD, + "availability": Availability.anytime.name, } diff --git a/learning_resources/etl/openedx.py b/learning_resources/etl/openedx.py index 556b5c4570..c9b8cce55a 100644 --- a/learning_resources/etl/openedx.py +++ b/learning_resources/etl/openedx.py @@ -6,7 +6,7 @@ import logging import re from collections import namedtuple -from datetime import UTC +from datetime import UTC, datetime from pathlib import Path import requests @@ -15,8 +15,10 @@ from toolz import compose from learning_resources.constants import ( + Availability, CertificationType, LearningResourceType, + RunAvailability, ) from learning_resources.etl.constants import COMMON_HEADERS from learning_resources.etl.utils import ( @@ -27,7 +29,7 @@ without_none, ) from learning_resources.utils import get_year_and_semester -from main.utils import clean_data +from main.utils import clean_data, now_in_utc MIT_OWNER_KEYS = ["MITx", "MITx_PRO"] @@ -136,6 +138,41 @@ def _get_course_marketing_url(config, course): return None +def _get_run_published(course_run): + return course_run.get("status", "") == "published" and course_run.get( + "is_enrollable", False + ) + + +def _get_run_availability(course_run): + if course_run.get("availability") == RunAvailability.archived.value: + # Enrollable, archived courses can be started anytime + return Availability.anytime + + start = course_run.get("start") + if ( + course_run.get("pacing_type") == "self_paced" + and start + and datetime.fromisoformat(start) < now_in_utc() + ): + return Availability.anytime + + return Availability.dated + + +def _get_course_availability(course): + published_runs = [ + run for run in course.get("course_runs", []) if _get_run_published(run) + ] + if any(_get_run_availability(run) == Availability.dated for run in published_runs): + return Availability.dated.name + elif published_runs and all( + _get_run_availability(run) == Availability.anytime for run in published_runs + ): + return Availability.anytime.name + return None + + def _is_course_or_run_deleted(title): """ Returns True if '[delete]', 'delete ' (note the ending space character) @@ -223,10 +260,7 @@ def _transform_course_run(config, course_run, course_last_modified, marketing_ur "start_date": course_run.get("start") or course_run.get("enrollment_start"), "end_date": course_run.get("end"), "last_modified": last_modified, - "published": ( - course_run.get("status", "") == "published" - and course_run.get("is_enrollable", False) - ), + "published": _get_run_published(course_run), "enrollment_start": course_run.get("enrollment_start"), "enrollment_end": course_run.get("enrollment_end"), "image": _transform_image(course_run.get("image")), @@ -291,6 +325,7 @@ def _transform_course(config, course): "certification_type": CertificationType.completion.name if has_certification else CertificationType.none.name, + "availability": _get_course_availability(course), } diff --git a/learning_resources/etl/openedx_test.py b/learning_resources/etl/openedx_test.py index 91801278e5..d057c13345 100644 --- a/learning_resources/etl/openedx_test.py +++ b/learning_resources/etl/openedx_test.py @@ -6,7 +6,12 @@ import pytest -from learning_resources.constants import CertificationType, LearningResourceType +from learning_resources.constants import ( + Availability, + CertificationType, + LearningResourceType, + RunAvailability, +) from learning_resources.etl.constants import COMMON_HEADERS, CourseNumberType from learning_resources.etl.openedx import ( OpenEdxConfiguration, @@ -150,7 +155,9 @@ def test_transform_course( # noqa: PLR0913 if is_course_deleted or not has_runs: assert transformed_courses == [] else: - assert transformed_courses[0] == { + transformed_course = transformed_courses[0].copy() + transformed_course.pop("availability") # Tested separately + assert transformed_course == { "title": "The Analytics Edge", "readable_id": "MITx+15.071x", "resource_type": LearningResourceType.course.name, @@ -228,3 +235,103 @@ def test_transform_course( # noqa: PLR0913 assert transformed_courses[1]["runs"][0]["published"] is ( is_run_enrollable and is_run_published ) + + +@pytest.mark.parametrize( + ("run_overrides", "expected_availability"), + [ + ( + { + "availability": RunAvailability.current.value, + "pacing_type": "self_paced", + "start": "2021-01-01T00:00:00Z", # past + }, + Availability.anytime.name, + ), + ( + { + "availability": RunAvailability.current.value, + "pacing_type": "self_paced", + "start": "2221-01-01T00:00:00Z", # future + }, + Availability.dated.name, + ), + ( + { + "availability": RunAvailability.archived.value, + }, + Availability.anytime.name, + ), + ], +) +@pytest.mark.parametrize("status", ["published", "other"]) +@pytest.mark.parametrize("is_enrollable", [True, False]) +def test_transform_course_availability_with_single_run( # noqa: PLR0913 + openedx_extract_transform, + mitx_course_data, + run_overrides, + expected_availability, + status, + is_enrollable, +): + """ + Test transforming openedx courses with a single run into our course-level + availability field. + """ + extracted = mitx_course_data["results"] + run = { + **extracted[0]["course_runs"][0], + **run_overrides, + "is_enrollable": is_enrollable, + "status": status, + } + extracted[0]["course_runs"] = [run] + transformed_courses = openedx_extract_transform.transform([extracted[0]]) + + if status == "published" and is_enrollable: + assert transformed_courses[0]["availability"] == expected_availability + else: + assert transformed_courses[0]["availability"] is None + + +@pytest.mark.parametrize("has_dated", [True, False]) +def test_transform_course_availability_with_multiple_runs( + openedx_extract_transform, mitx_course_data, has_dated +): + """ + Test that if course includes a single run corresponding to availability: "dated", + then the overall course availability is "dated". + """ + extracted = mitx_course_data["results"] + run0 = { # anytime run + **extracted[0]["course_runs"][0], + "availability": RunAvailability.current.value, + "pacing_type": "self_paced", + "start": "2021-01-01T00:00:00Z", # past + "is_enrollable": True, + "status": "published", + } + run1 = { # anytime run + **extracted[0]["course_runs"][0], + "availability": RunAvailability.archived.value, + "is_enrollable": True, + "status": "published", + } + run2 = { # dated run + **extracted[0]["course_runs"][0], + "availability": RunAvailability.current.value, + "pacing_type": "instructor_paced", + "start": "2221-01-01T00:00:00Z", + "is_enrollable": True, + "status": "published", + } + runs = [run0, run1] + if has_dated: + runs.append(run2) + extracted[0]["course_runs"] = runs + transformed_courses = openedx_extract_transform.transform([extracted[0]]) + + if has_dated: + assert transformed_courses[0]["availability"] == Availability.dated.name + else: + assert transformed_courses[0]["availability"] is Availability.anytime.name diff --git a/learning_resources/etl/podcast.py b/learning_resources/etl/podcast.py index bc01071c9a..9d58227fb6 100644 --- a/learning_resources/etl/podcast.py +++ b/learning_resources/etl/podcast.py @@ -10,7 +10,7 @@ from django.conf import settings from requests.exceptions import HTTPError -from learning_resources.constants import LearningResourceType +from learning_resources.constants import Availability, LearningResourceType from learning_resources.etl.constants import ETLSource from learning_resources.etl.utils import iso8601_duration from learning_resources.models import PodcastEpisode @@ -181,6 +181,7 @@ def transform_episode(rss_data, offered_by, topics, parent_image): ), "rss": rss_data.prettify(), }, + "availability": Availability.anytime.name, } @@ -236,6 +237,7 @@ def transform(extracted_podcasts): "google_podcasts_url": google_podcasts_url, "rss_url": config_data["rss_url"], }, + "availability": Availability.anytime.name, } except AttributeError: log.exception("Error parsing podcast data from %s", config_data["rss_url"]) diff --git a/learning_resources/etl/podcast_test.py b/learning_resources/etl/podcast_test.py index 1cdbe6c3e3..c28d1175dd 100644 --- a/learning_resources/etl/podcast_test.py +++ b/learning_resources/etl/podcast_test.py @@ -10,7 +10,7 @@ from django.conf import settings from freezegun import freeze_time -from learning_resources.constants import LearningResourceType, OfferedBy +from learning_resources.constants import Availability, LearningResourceType, OfferedBy from learning_resources.etl.constants import ETLSource from learning_resources.etl.podcast import ( extract, @@ -144,11 +144,13 @@ def test_transform(mock_github_client, title, topics, offered_by): }, "resource_type": LearningResourceType.podcast.name, "topics": expected_topics, + "availability": Availability.anytime.name, "episodes": [ { "readable_id": "tag:soundcloud,2010:tracks/numbers1", "etl_source": ETLSource.podcast.name, "title": "Episode1", + "availability": Availability.anytime.name, "offered_by": expected_offered_by, "description": ( "SMorbi id consequat nisl. Morbi leo elit, vulputate nec" @@ -172,6 +174,7 @@ def test_transform(mock_github_client, title, topics, offered_by): "readable_id": "tag:soundcloud,2010:tracks/numbers2", "etl_source": ETLSource.podcast.name, "title": "Episode2", + "availability": Availability.anytime.name, "offered_by": expected_offered_by, "description": ( "Praesent fermentum suscipit metus nec aliquam. Proin hendrerit" diff --git a/learning_resources/etl/prolearn.py b/learning_resources/etl/prolearn.py index 1851f9f402..5610fbb2e2 100644 --- a/learning_resources/etl/prolearn.py +++ b/learning_resources/etl/prolearn.py @@ -9,7 +9,7 @@ import requests from django.conf import settings -from learning_resources.constants import CertificationType +from learning_resources.constants import Availability, CertificationType from learning_resources.etl.constants import ETLSource from learning_resources.etl.utils import transform_format, transform_topics from learning_resources.models import LearningResourceOfferor, LearningResourcePlatform @@ -350,6 +350,7 @@ def _transform_course( "topics": parse_topic(course, offered_by.code) if offered_by else None, "runs": runs, "unique_field": UNIQUE_FIELD, + "availability": Availability.dated.name, } return None diff --git a/learning_resources/etl/prolearn_test.py b/learning_resources/etl/prolearn_test.py index dfcfcd08e4..241376c23e 100644 --- a/learning_resources/etl/prolearn_test.py +++ b/learning_resources/etl/prolearn_test.py @@ -8,6 +8,7 @@ import pytest from learning_resources.constants import ( + Availability, CertificationType, LearningResourceFormat, OfferedBy, @@ -231,6 +232,7 @@ def test_prolearn_transform_courses(mock_mitpe_courses_data): course["course_application_url"] or urljoin(PROLEARN_BASE_URL, course["url"]) ), + "availability": Availability.dated.name, "runs": [ { "run_id": f"{course['nid']}_{start_val}", diff --git a/learning_resources/etl/utils.py b/learning_resources/etl/utils.py index 21e8d64ca9..b6fb7d1c1b 100644 --- a/learning_resources/etl/utils.py +++ b/learning_resources/etl/utils.py @@ -31,10 +31,10 @@ CONTENT_TYPE_VIDEO, DEPARTMENTS, VALID_TEXT_FILE_TYPES, - AvailabilityType, LearningResourceFormat, LevelType, OfferedBy, + RunAvailability, ) from learning_resources.etl.constants import ( RESOURCE_FORMAT_MAPPING, @@ -703,7 +703,7 @@ def parse_certification(offeror, runs_data): for run in runs_data if run.get("published", True) ] - if (availability and availability != AvailabilityType.archived.value) + if (availability and availability != RunAvailability.archived.value) ] ) diff --git a/learning_resources/etl/utils_test.py b/learning_resources/etl/utils_test.py index f0ba8e9f1e..77ce6cb067 100644 --- a/learning_resources/etl/utils_test.py +++ b/learning_resources/etl/utils_test.py @@ -13,11 +13,11 @@ from learning_resources.constants import ( CONTENT_TYPE_FILE, CONTENT_TYPE_VERTICAL, - AvailabilityType, LearningResourceFormat, LearningResourceType, OfferedBy, PlatformType, + RunAvailability, ) from learning_resources.etl import utils from learning_resources.etl.utils import parse_certification @@ -28,7 +28,6 @@ LearningResourceRunFactory, LearningResourceTopicFactory, ) -from learning_resources.serializers import LearningResourceSerializer pytestmark = pytest.mark.django_db @@ -387,27 +386,27 @@ def test_parse_bad_format(mocker): [ [ # noqa: PT007 OfferedBy.ocw.name, - AvailabilityType.archived.value, + RunAvailability.archived.value, False, ], [ # noqa: PT007 OfferedBy.ocw.name, - AvailabilityType.current.value, + RunAvailability.current.value, False, ], [ # noqa: PT007 OfferedBy.mitx.name, - AvailabilityType.archived.value, + RunAvailability.archived.value, False, ], [ # noqa: PT007 OfferedBy.mitx.name, - AvailabilityType.current.value, + RunAvailability.current.value, True, ], [ # noqa: PT007 OfferedBy.mitx.name, - AvailabilityType.upcoming.value, + RunAvailability.upcoming.value, True, ], ], @@ -426,13 +425,8 @@ def test_parse_certification(offered_by, availability, has_cert): ).learning_resource assert resource.runs.first().availability == availability assert resource.runs.count() == 1 - assert ( - parse_certification( - offered_by_obj.code, - LearningResourceSerializer(instance=resource).data["runs"], - ) - == has_cert - ) + runs = resource.runs.all().values() + assert parse_certification(offered_by_obj.code, runs) == has_cert @pytest.mark.parametrize( diff --git a/learning_resources/etl/xpro.py b/learning_resources/etl/xpro.py index edaa036cdc..1992b17482 100644 --- a/learning_resources/etl/xpro.py +++ b/learning_resources/etl/xpro.py @@ -9,6 +9,7 @@ from django.conf import settings from learning_resources.constants import ( + Availability, CertificationType, LearningResourceType, OfferedBy, @@ -132,6 +133,7 @@ def _transform_learning_resource_course(course): }, "certification": True, "certification_type": CertificationType.professional.name, + "availability": Availability.dated.name, } @@ -193,6 +195,7 @@ def transform_programs(programs): "courses": transform_courses(program["courses"]), "certification": True, "certification_type": CertificationType.professional.name, + "availability": Availability.dated.name, } for program in programs ] diff --git a/learning_resources/etl/xpro_test.py b/learning_resources/etl/xpro_test.py index 9cb55c7d6f..8fd38467f6 100644 --- a/learning_resources/etl/xpro_test.py +++ b/learning_resources/etl/xpro_test.py @@ -8,6 +8,7 @@ import pytest from learning_resources.constants import ( + Availability, CertificationType, LearningResourceType, PlatformType, @@ -101,6 +102,7 @@ def test_xpro_transform_programs(mock_xpro_programs_data): "professional": True, "published": bool(program_data["current_price"]), "url": program_data["url"], + "availability": Availability.dated.name, "topics": transform_topics(program_data["topics"], xpro.OFFERED_BY["code"]), "platform": PlatformType.xpro.name, "resource_type": LearningResourceType.program.name, @@ -140,6 +142,7 @@ def test_xpro_transform_programs(mock_xpro_programs_data): course_run.get("current_price", None) for course_run in course_data["courseruns"] ), + "availability": Availability.dated.name, "topics": transform_topics( course_data["topics"], xpro.OFFERED_BY["code"] ), @@ -210,6 +213,7 @@ def test_xpro_transform_courses(mock_xpro_courses_data): course_run.get("current_price", None) for course_run in course_data["courseruns"] ), + "availability": Availability.dated.name, "topics": transform_topics(course_data["topics"], xpro.OFFERED_BY["code"]), "resource_type": LearningResourceType.course.name, "runs": [ diff --git a/learning_resources/etl/youtube.py b/learning_resources/etl/youtube.py index 869a70946d..c6446a3fd2 100644 --- a/learning_resources/etl/youtube.py +++ b/learning_resources/etl/youtube.py @@ -18,7 +18,12 @@ ) from youtube_transcript_api.formatters import TextFormatter -from learning_resources.constants import LearningResourceType, OfferedBy, PlatformType +from learning_resources.constants import ( + Availability, + LearningResourceType, + OfferedBy, + PlatformType, +) from learning_resources.etl.constants import ETLSource from learning_resources.etl.exceptions import ExtractException from learning_resources.etl.loaders import update_index @@ -424,6 +429,7 @@ def transform_video(video_data: dict, offered_by_code: str) -> dict: "video": { "duration": video_data["contentDetails"]["duration"], }, + "availability": Availability.anytime.name, } @@ -452,6 +458,7 @@ def transform_playlist( transform_video(extracted_video, offered_by_code) for extracted_video in videos ), + "availability": Availability.anytime.name, } diff --git a/learning_resources/etl/youtube_test.py b/learning_resources/etl/youtube_test.py index 7c2d21172a..c68a56ba49 100644 --- a/learning_resources/etl/youtube_test.py +++ b/learning_resources/etl/youtube_test.py @@ -12,7 +12,12 @@ from googleapiclient.errors import HttpError from youtube_transcript_api import NoTranscriptFound -from learning_resources.constants import LearningResourceType, OfferedBy, PlatformType +from learning_resources.constants import ( + Availability, + LearningResourceType, + OfferedBy, + PlatformType, +) from learning_resources.etl import youtube from learning_resources.etl.constants import ETLSource from learning_resources.etl.exceptions import ExtractException @@ -199,6 +204,7 @@ def extracted_and_transformed_values(youtube_api_responses): "offered_by": {"code": offered_by} if offered_by != "csail" else None, + "availability": Availability.anytime.name, "published": True, "videos": [ { @@ -217,6 +223,7 @@ def extracted_and_transformed_values(youtube_api_responses): if offered_by != "csail" else None, "title": video["snippet"]["localized"]["title"], + "availability": Availability.anytime.name, "video": { "duration": video["contentDetails"]["duration"], }, diff --git a/learning_resources/factories.py b/learning_resources/factories.py index b2360d6814..7a3b9cc675 100644 --- a/learning_resources/factories.py +++ b/learning_resources/factories.py @@ -13,6 +13,7 @@ from learning_resources import constants, models from learning_resources.constants import ( DEPARTMENTS, + Availability, LearningResourceFormat, LevelType, PlatformType, @@ -279,6 +280,8 @@ class LearningResourceFactory(DjangoModelFactory): ), ) + availability = FuzzyChoice(Availability.names()) + class Meta: model = models.LearningResource skip_postgeneration_save = True @@ -473,10 +476,10 @@ class LearningResourceRunFactory(DjangoModelFactory): image = factory.SubFactory(LearningResourceImageFactory) availability = FuzzyChoice( ( - constants.AvailabilityType.current.value, - constants.AvailabilityType.upcoming.value, - constants.AvailabilityType.starting_soon.value, - constants.AvailabilityType.archived.value, + constants.RunAvailability.current.value, + constants.RunAvailability.upcoming.value, + constants.RunAvailability.starting_soon.value, + constants.RunAvailability.archived.value, ) ) enrollment_start = factory.Faker("future_datetime", tzinfo=UTC) diff --git a/learning_resources/migrations/0046_learningresource_certification.py b/learning_resources/migrations/0046_learningresource_certification.py index dee37ce56b..64de2d1076 100644 --- a/learning_resources/migrations/0046_learningresource_certification.py +++ b/learning_resources/migrations/0046_learningresource_certification.py @@ -3,9 +3,9 @@ from django.db import migrations, models from learning_resources.constants import ( - AvailabilityType, LearningResourceType, OfferedBy, + RunAvailability, ) @@ -27,7 +27,7 @@ def populate_certification(apps, schema_editor): and lr.offered_by.name == OfferedBy.mitx.value and ( any( - availability != AvailabilityType.archived.value + availability != RunAvailability.archived.value for availability in lr.runs.values_list( "availability", flat=True ) diff --git a/learning_resources/migrations/0059_learningresource_availability.py b/learning_resources/migrations/0059_learningresource_availability.py new file mode 100644 index 0000000000..3c78d16815 --- /dev/null +++ b/learning_resources/migrations/0059_learningresource_availability.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.14 on 2024-07-25 15:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("learning_resources", "0058_add_icon_uuid_and_mappings_to_topics"), + ] + + operations = [ + migrations.AddField( + model_name="learningresource", + name="availability", + field=models.CharField( + choices=[("dated", "Dated"), ("anytime", "Anytime")], + max_length=24, + null=True, + ), + ), + ] diff --git a/learning_resources/migrations/0060_partially_populate_availability.py b/learning_resources/migrations/0060_partially_populate_availability.py new file mode 100644 index 0000000000..38431056b3 --- /dev/null +++ b/learning_resources/migrations/0060_partially_populate_availability.py @@ -0,0 +1,40 @@ +# Generated by Django 4.2.14 on 2024-07-25 17:01 + +from django.db import migrations + +from learning_resources.constants import ( + Availability, + LearningResourceType, + PlatformType, +) + + +def partially_populate_availability(apps, schema_editor): + """ + Set availability for resources that are always available to anytime. + Some resources will need to be updated by ETL pipelines, but this handles + OCW, videos, and podcasts (and their lists). + """ + LearningResource = apps.get_model("learning_resources", "LearningResource") + + for resource in LearningResource.objects.all().filter(published=True): + if resource.resource_type in [ + LearningResourceType.video.name, + LearningResourceType.video_playlist.name, + LearningResourceType.podcast.name, + LearningResourceType.podcast_episode.name, + ] or (resource.platform and resource.platform.code == PlatformType.ocw.name): + resource.availability = Availability.anytime.name + resource.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("learning_resources", "0059_learningresource_availability"), + ] + + operations = [ + migrations.RunPython( + partially_populate_availability, migrations.RunPython.noop + ), + ] diff --git a/learning_resources/models.py b/learning_resources/models.py index c21951e25b..cd580ab0b8 100644 --- a/learning_resources/models.py +++ b/learning_resources/models.py @@ -12,6 +12,7 @@ from learning_resources import constants from learning_resources.constants import ( + Availability, CertificationType, LearningResourceFormat, LearningResourceRelationTypes, @@ -315,6 +316,11 @@ class LearningResource(TimestampedModel): prices = ArrayField( models.DecimalField(decimal_places=2, max_digits=12), default=list ) + availability = models.CharField( # noqa: DJ001 + max_length=24, + null=True, + choices=((member.name, member.value) for member in Availability), + ) @staticmethod def get_prefetches(): diff --git a/learning_resources/serializers.py b/learning_resources/serializers.py index eb59492cf5..639e73d0cf 100644 --- a/learning_resources/serializers.py +++ b/learning_resources/serializers.py @@ -251,7 +251,7 @@ class LearningResourceRunSerializer(serializers.ModelSerializer): class Meta: model = models.LearningResourceRun - exclude = ["learning_resource", *COMMON_IGNORED_FIELDS] + exclude = ["learning_resource", "availability", *COMMON_IGNORED_FIELDS] class ResourceListMixin(serializers.Serializer): diff --git a/learning_resources/serializers_test.py b/learning_resources/serializers_test.py index af0bf11850..324920f908 100644 --- a/learning_resources/serializers_test.py +++ b/learning_resources/serializers_test.py @@ -262,6 +262,7 @@ def test_learning_resource_serializer( # noqa: PLR0913 for lr_format in resource.learning_format ], "next_start_date": resource.next_start_date, + "availability": resource.availability, } diff --git a/openapi/specs/v1.yaml b/openapi/specs/v1.yaml index b0838ebf07..8a53772129 100644 --- a/openapi/specs/v1.yaml +++ b/openapi/specs/v1.yaml @@ -7207,6 +7207,17 @@ components: required: - html - title + AvailabilityEnum: + enum: + - dated + - anytime + type: string + description: |- + * `dated` - Dated + * `anytime` - Anytime + x-enum-descriptions: + - Dated + - Anytime CertificationTypeEnum: enum: - micromasters @@ -7601,6 +7612,11 @@ components: type: string format: date-time nullable: true + availability: + nullable: true + oneOf: + - $ref: '#/components/schemas/AvailabilityEnum' + - $ref: '#/components/schemas/NullEnum' required: - certification - certification_type @@ -7668,6 +7684,11 @@ components: type: string format: date-time nullable: true + availability: + nullable: true + oneOf: + - $ref: '#/components/schemas/AvailabilityEnum' + - $ref: '#/components/schemas/NullEnum' required: - readable_id - title @@ -8011,6 +8032,11 @@ components: type: string format: date-time nullable: true + availability: + nullable: true + oneOf: + - $ref: '#/components/schemas/AvailabilityEnum' + - $ref: '#/components/schemas/NullEnum' required: - certification - certification_type @@ -8075,6 +8101,11 @@ components: type: string format: date-time nullable: true + availability: + nullable: true + oneOf: + - $ref: '#/components/schemas/AvailabilityEnum' + - $ref: '#/components/schemas/NullEnum' required: - title LearningPathResourceResourceTypeEnum: @@ -8514,10 +8545,6 @@ components: type: string nullable: true maxLength: 1024 - availability: - type: string - nullable: true - maxLength: 128 semester: type: string nullable: true @@ -8621,10 +8648,6 @@ components: type: string nullable: true maxLength: 1024 - availability: - type: string - nullable: true - maxLength: 128 semester: type: string nullable: true @@ -8830,6 +8853,9 @@ components: - child - id - parent + NullEnum: + enum: + - null OfferedByEnum: enum: - mitx @@ -9378,6 +9404,11 @@ components: type: string format: date-time nullable: true + availability: + nullable: true + oneOf: + - $ref: '#/components/schemas/AvailabilityEnum' + - $ref: '#/components/schemas/NullEnum' PatchedUserListRelationshipRequest: type: object description: Serializer for UserListRelationship model @@ -9892,6 +9923,11 @@ components: type: string format: date-time nullable: true + availability: + nullable: true + oneOf: + - $ref: '#/components/schemas/AvailabilityEnum' + - $ref: '#/components/schemas/NullEnum' required: - certification - certification_type @@ -9959,6 +9995,11 @@ components: type: string format: date-time nullable: true + availability: + nullable: true + oneOf: + - $ref: '#/components/schemas/AvailabilityEnum' + - $ref: '#/components/schemas/NullEnum' required: - readable_id - title @@ -10142,6 +10183,11 @@ components: type: string format: date-time nullable: true + availability: + nullable: true + oneOf: + - $ref: '#/components/schemas/AvailabilityEnum' + - $ref: '#/components/schemas/NullEnum' required: - certification - certification_type @@ -10209,6 +10255,11 @@ components: type: string format: date-time nullable: true + availability: + nullable: true + oneOf: + - $ref: '#/components/schemas/AvailabilityEnum' + - $ref: '#/components/schemas/NullEnum' required: - readable_id - title @@ -10526,6 +10577,11 @@ components: type: string format: date-time nullable: true + availability: + nullable: true + oneOf: + - $ref: '#/components/schemas/AvailabilityEnum' + - $ref: '#/components/schemas/NullEnum' required: - certification - certification_type @@ -10593,6 +10649,11 @@ components: type: string format: date-time nullable: true + availability: + nullable: true + oneOf: + - $ref: '#/components/schemas/AvailabilityEnum' + - $ref: '#/components/schemas/NullEnum' required: - readable_id - title @@ -11025,6 +11086,11 @@ components: type: string format: date-time nullable: true + availability: + nullable: true + oneOf: + - $ref: '#/components/schemas/AvailabilityEnum' + - $ref: '#/components/schemas/NullEnum' required: - certification - certification_type @@ -11092,6 +11158,11 @@ components: type: string format: date-time nullable: true + availability: + nullable: true + oneOf: + - $ref: '#/components/schemas/AvailabilityEnum' + - $ref: '#/components/schemas/NullEnum' required: - readable_id - title @@ -11265,6 +11336,11 @@ components: type: string format: date-time nullable: true + availability: + nullable: true + oneOf: + - $ref: '#/components/schemas/AvailabilityEnum' + - $ref: '#/components/schemas/NullEnum' required: - certification - certification_type @@ -11332,6 +11408,11 @@ components: type: string format: date-time nullable: true + availability: + nullable: true + oneOf: + - $ref: '#/components/schemas/AvailabilityEnum' + - $ref: '#/components/schemas/NullEnum' required: - readable_id - title diff --git a/test_json/mitxonline_courses.json b/test_json/mitxonline_courses.json index ddcb4df526..51ed56c859 100644 --- a/test_json/mitxonline_courses.json +++ b/test_json/mitxonline_courses.json @@ -62,7 +62,8 @@ "products": [], "approved_flexible_price_exists": false } - ] + ], + "availability": "anytime" }, { "id": 47, @@ -114,7 +115,8 @@ ], "approved_flexible_price_exists": false } - ] + ], + "availability": "dated" }, { "id": 50, @@ -166,7 +168,8 @@ ], "approved_flexible_price_exists": false } - ] + ], + "availability": "dated" }, { "id": 52, @@ -218,7 +221,8 @@ ], "approved_flexible_price_exists": false } - ] + ], + "availability": "dated" }, { "id": 49, @@ -270,7 +274,8 @@ ], "approved_flexible_price_exists": false } - ] + ], + "availability": "dated" }, { "id": 45, @@ -322,7 +327,8 @@ ], "approved_flexible_price_exists": false } - ] + ], + "availability": "anytime" }, { "id": 44, @@ -374,7 +380,8 @@ ], "approved_flexible_price_exists": false } - ] + ], + "availability": "anytime" }, { "id": 51, @@ -426,7 +433,8 @@ ], "approved_flexible_price_exists": false } - ] + ], + "availability": "anytime" }, { "id": 46, @@ -478,7 +486,8 @@ ], "approved_flexible_price_exists": false } - ] + ], + "availability": "dated" }, { "id": 48, @@ -530,7 +539,8 @@ ], "approved_flexible_price_exists": false } - ] + ], + "availability": "dated" }, { "id": 54, @@ -611,7 +621,8 @@ ], "approved_flexible_price_exists": false } - ] + ], + "availability": "anytime" }, { "id": 96, @@ -663,7 +674,8 @@ ], "approved_flexible_price_exists": false } - ] + ], + "availability": "dated" } ] } diff --git a/test_json/mitxonline_programs.json b/test_json/mitxonline_programs.json index cda5bcb858..bd38a94270 100644 --- a/test_json/mitxonline_programs.json +++ b/test_json/mitxonline_programs.json @@ -115,7 +115,8 @@ "program_type": "Series", "departments": ["Mathematics"], "live": true, - "topics": [] + "topics": [], + "availability": "scheduled" }, { "title": "Analysis of Transport Phenomena", @@ -277,7 +278,8 @@ "program_type": "Series", "departments": ["Chemical Engineering"], "live": true, - "topics": [] + "topics": [], + "availability": "anytime" }, { "title": "Data, Economics, and Design of Policy", @@ -439,7 +441,8 @@ { "name": "Social Sciences" } - ] + ], + "availability": null }, { "title": "Data, Economics, and Design of Policy: International Development", @@ -589,7 +592,8 @@ { "name": "Social Sciences" } - ] + ], + "availability": "scheduled" }, { "title": "Data, Economics, and Design of Policy: Public Policy", @@ -717,7 +721,8 @@ "program_type": "MicroMasters®", "departments": ["Economics"], "live": true, - "topics": [] + "topics": [], + "availability": null }, { "title": "Differential Calculus", @@ -807,7 +812,8 @@ "program_type": "Series", "departments": ["Mathematics"], "live": true, - "topics": [] + "topics": [], + "availability": "scheduled" }, { "title": "Introductory Electricity and Magnetics", @@ -897,7 +903,8 @@ "program_type": "Series", "departments": ["Materials Science and Engineering"], "live": true, - "topics": [] + "topics": [], + "availability": "anytime" }, { "title": "xMinor in Materials for Electronic, Optical, and Magnetic Devices", @@ -987,7 +994,8 @@ "program_type": "Series", "departments": ["Materials Science and Engineering"], "live": true, - "topics": [] + "topics": [], + "availability": "scheduled" }, { "title": "xSeries in Introduction to Mechanics", @@ -1089,7 +1097,8 @@ "program_type": "Series", "departments": [], "live": true, - "topics": [] + "topics": [], + "availability": "anytime" } ] } From 6350be58795818f94775c5ab26d5cfa725a38fcb Mon Sep 17 00:00:00 2001 From: Anastasia Beglova Date: Mon, 29 Jul 2024 10:49:21 -0400 Subject: [PATCH 03/11] tab widths (#1309) --- .../SearchDisplay/ResourceCategoryTabs.tsx | 19 +++++++++++++------ .../SearchDisplay/SearchDisplay.tsx | 4 ++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/frontends/mit-open/src/page-components/SearchDisplay/ResourceCategoryTabs.tsx b/frontends/mit-open/src/page-components/SearchDisplay/ResourceCategoryTabs.tsx index 95e74ff481..ea88f888b2 100644 --- a/frontends/mit-open/src/page-components/SearchDisplay/ResourceCategoryTabs.tsx +++ b/frontends/mit-open/src/page-components/SearchDisplay/ResourceCategoryTabs.tsx @@ -8,21 +8,27 @@ import { } from "ol-components" import { ResourceCategoryEnum, LearningResourcesSearchResponse } from "api" -const TabsList = styled(TabButtonList)({ +const TabsList = styled(TabButtonList)(({ theme }) => ({ ".MuiTabScrollButton-root.Mui-disabled": { display: "none", }, -}) + [theme.breakpoints.down("md")]: { + "div div button": { + minWidth: "0 !important", + }, + }, +})) + +const CountSpan = styled.span(({ theme }) => ({ + ...theme.typography.body3, +})) -const CountSpan = styled.span` - min-width: 35px; - text-align: left; -` type TabConfig = { label: string name: string defaultTab?: boolean resource_category: ResourceCategoryEnum | null + minWidth: number } type Aggregations = LearningResourcesSearchResponse["metadata"]["aggregations"] @@ -110,6 +116,7 @@ const ResourceCategoryTabList: React.FC = ({ } return ( Date: Mon, 29 Jul 2024 17:01:20 +0200 Subject: [PATCH 04/11] Modal dialog component and styles * Dialog header styles, message prop, add to SubscriptionToggle * Button and close styles * Rename BasicDialog to Dialog * Style lint * Pass MUI props through * Update tests * Copy update * Dialog story state wrapper * Copy update * Changes requested - cleanup and aria label * Remove follow confirmation dialog --- .../ArticleUpsertForm/ArticleUpsertForm.tsx | 6 +- .../Dialogs/AddToListDialog.tsx | 27 ++--- .../ManageListDialogs/ManageListDialogs.tsx | 10 +- .../SearchSubscriptionToggle.test.tsx | 4 + .../SearchSubscriptionToggle.tsx | 30 +++-- .../ArticleEditPage.test.tsx | 11 +- .../src/pages/ChannelPage/ChannelPage.tsx | 8 +- .../ChannelPage/DefaultChannelTemplate.tsx | 1 + .../pages/ChannelPage/UnitChannelTemplate.tsx | 1 + .../BasicDialog/BasicDialog.stories.tsx | 34 ------ .../src/components/Dialog/Dialog.stories.tsx | 67 +++++++++++ .../BasicDialog.tsx => Dialog/Dialog.tsx} | 105 ++++++++++++------ frontends/ol-components/src/index.ts | 20 ++-- 13 files changed, 196 insertions(+), 128 deletions(-) delete mode 100644 frontends/ol-components/src/components/BasicDialog/BasicDialog.stories.tsx create mode 100644 frontends/ol-components/src/components/Dialog/Dialog.stories.tsx rename frontends/ol-components/src/components/{BasicDialog/BasicDialog.tsx => Dialog/Dialog.tsx} (53%) diff --git a/frontends/mit-open/src/page-components/ArticleUpsertForm/ArticleUpsertForm.tsx b/frontends/mit-open/src/page-components/ArticleUpsertForm/ArticleUpsertForm.tsx index 34a254b18c..71b6b19bab 100644 --- a/frontends/mit-open/src/page-components/ArticleUpsertForm/ArticleUpsertForm.tsx +++ b/frontends/mit-open/src/page-components/ArticleUpsertForm/ArticleUpsertForm.tsx @@ -12,7 +12,7 @@ import { FormHelperText, Grid, TextField, - BasicDialog, + Dialog, styled, } from "ol-components" import * as Yup from "yup" @@ -151,7 +151,7 @@ const ArticleUpsertForm = ({ - Are you sure you want to delete {article.data?.title}? - + ) } diff --git a/frontends/mit-open/src/page-components/Dialogs/AddToListDialog.tsx b/frontends/mit-open/src/page-components/Dialogs/AddToListDialog.tsx index b087365b2f..0e856cc49d 100644 --- a/frontends/mit-open/src/page-components/Dialogs/AddToListDialog.tsx +++ b/frontends/mit-open/src/page-components/Dialogs/AddToListDialog.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react" import { - BasicDialog, + Dialog, Chip, MuiCheckbox, List, @@ -8,6 +8,7 @@ import { ListItemButton, ListItemText, LoadingSpinner, + Typography, styled, } from "ol-components" @@ -168,21 +169,6 @@ const useToggleItemInUserList = (resource?: LearningResource) => { return { handleToggle, isChecked, isAdding, isRemoving } } -const StyledBasicDialog = styled(BasicDialog)` - .MuiDialog-paper { - width: 325px; - } - - .MuiDialogContent-root { - padding: 0; - } -` - -const Description = styled.div({ - marginLeft: 20, - marginRight: 20, -}) - const ResourceTitle = styled.span({ fontStyle: "italic", }) @@ -231,16 +217,17 @@ const AddToListDialogInner: React.FC = ({ dialogTitle = "Add to User List" } return ( - {isReady ? ( <> - + Adding {resource?.title} - + {listType === ListType.LearningPath ? ( = ({ ) : ( )} - + ) } diff --git a/frontends/mit-open/src/page-components/ManageListDialogs/ManageListDialogs.tsx b/frontends/mit-open/src/page-components/ManageListDialogs/ManageListDialogs.tsx index 1fb46e05e7..a52b10cba7 100644 --- a/frontends/mit-open/src/page-components/ManageListDialogs/ManageListDialogs.tsx +++ b/frontends/mit-open/src/page-components/ManageListDialogs/ManageListDialogs.tsx @@ -7,7 +7,7 @@ import { Autocomplete, BooleanRadioChoiceField, FormDialog, - BasicDialog, + Dialog, styled, RadioChoiceField, MenuItem, @@ -351,14 +351,14 @@ const DeleteLearningPathDialog = NiceModal.create( hideModal() }, [destroyList, hideModal, resource]) return ( - Are you sure you want to delete this list? - + ) }, ) @@ -380,14 +380,14 @@ const DeleteUserListDialog = NiceModal.create( hideModal() }, [destroyList, hideModal, userList]) return ( - Are you sure you want to delete this list? - + ) }, ) diff --git a/frontends/mit-open/src/page-components/SearchSubscriptionToggle/SearchSubscriptionToggle.test.tsx b/frontends/mit-open/src/page-components/SearchSubscriptionToggle/SearchSubscriptionToggle.test.tsx index f4519e2db2..c3116e8a82 100644 --- a/frontends/mit-open/src/page-components/SearchSubscriptionToggle/SearchSubscriptionToggle.test.tsx +++ b/frontends/mit-open/src/page-components/SearchSubscriptionToggle/SearchSubscriptionToggle.test.tsx @@ -43,6 +43,7 @@ test("Shows subscription popover if user is NOT authenticated", async () => { setMockResponse.get(urls.userMe.get(), {}, { code: 403 }) renderWithProviders( , @@ -68,6 +69,7 @@ test.each(Object.values(SourceTypeEnum))( }) renderWithProviders( , @@ -76,6 +78,7 @@ test.each(Object.values(SourceTypeEnum))( name: "Follow", }) await user.click(subscribeButton) + expect(makeRequest).toHaveBeenCalledWith("post", subscribeUrl, { source_type: sourceType, offered_by: ["ocw"], @@ -97,6 +100,7 @@ test.each(Object.values(SourceTypeEnum))( renderWithProviders( , diff --git a/frontends/mit-open/src/page-components/SearchSubscriptionToggle/SearchSubscriptionToggle.tsx b/frontends/mit-open/src/page-components/SearchSubscriptionToggle/SearchSubscriptionToggle.tsx index 192449bc09..b80f87e884 100644 --- a/frontends/mit-open/src/page-components/SearchSubscriptionToggle/SearchSubscriptionToggle.tsx +++ b/frontends/mit-open/src/page-components/SearchSubscriptionToggle/SearchSubscriptionToggle.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from "react" +import React, { useState, useMemo } from "react" import { getSearchParamMap } from "@/common/utils" import { useSearchSubscriptionCreate, @@ -10,7 +10,6 @@ import type { SimpleMenuItem } from "ol-components" import { RiArrowDownSLine, RiMailLine } from "@remixicon/react" import { useUserMe } from "api/hooks/user" import { SourceTypeEnum } from "api" - import { SignupPopover } from "../SignupPopover/SignupPopover" const StyledButton = styled(Button)({ @@ -18,6 +17,7 @@ const StyledButton = styled(Button)({ }) type SearchSubscriptionToggleProps = { + itemName: string searchParams: URLSearchParams sourceType: SourceTypeEnum } @@ -26,7 +26,8 @@ const SearchSubscriptionToggle: React.FC = ({ searchParams, sourceType, }) => { - const [buttonEl, setButtonEl] = React.useState(null) + const [buttonEl, setButtonEl] = useState(null) + const subscribeParams: Record = useMemo(() => { return { source_type: sourceType, ...getSearchParamMap(searchParams) } }, [searchParams, sourceType]) @@ -41,6 +42,7 @@ const SearchSubscriptionToggle: React.FC = ({ const unsubscribe = subscriptionDelete.mutate const subscriptionId = subscriptionList.data?.[0]?.id const isSubscribed = !!subscriptionId + const unsubscribeItems: SimpleMenuItem[] = useMemo(() => { if (!subscriptionId) return [] return [ @@ -52,8 +54,19 @@ const SearchSubscriptionToggle: React.FC = ({ ] }, [unsubscribe, subscriptionId]) + const onFollowClick = async (event: React.MouseEvent) => { + if (user?.is_authenticated) { + await subscriptionCreate.mutateAsync({ + PercolateQuerySubscriptionRequestRequest: subscribeParams, + }) + } else { + setButtonEl(event.currentTarget) + } + } + if (user?.is_authenticated && subscriptionList.isLoading) return null if (!user) return null + if (isSubscribed) { return ( = ({ /> ) } + return ( <> } - onClick={(e) => { - if (user?.is_authenticated) { - subscriptionCreate.mutateAsync({ - PercolateQuerySubscriptionRequestRequest: subscribeParams, - }) - } else { - setButtonEl(e.currentTarget) - } - }} + onClick={onFollowClick} > Follow diff --git a/frontends/mit-open/src/pages/ArticleUpsertPages/ArticleEditPage.test.tsx b/frontends/mit-open/src/pages/ArticleUpsertPages/ArticleEditPage.test.tsx index f122151b12..4f1d916d26 100644 --- a/frontends/mit-open/src/pages/ArticleUpsertPages/ArticleEditPage.test.tsx +++ b/frontends/mit-open/src/pages/ArticleUpsertPages/ArticleEditPage.test.tsx @@ -4,7 +4,6 @@ import { screen, user, waitFor, - within, } from "../../test-utils" import type { Article } from "api" import { articles as factory } from "api/test-utils/factories" @@ -100,16 +99,18 @@ describe("ArticleEditPage", () => { async ({ confirmed }) => { const article = factory.article() setup({ article }) - const deleteButton = await screen.findByRole("button", { name: "Delete" }) + const deleteButton = await screen.findByRole("button", { name: "Delete" }) await user.click(deleteButton) - const dialog = await screen.findByRole("dialog", { + + await screen.findByRole("heading", { name: "Are you sure?", }) - const cancelButton = within(dialog).getByRole("button", { + + const cancelButton = await screen.findByRole("button", { name: "Cancel", }) - const confirmButton = within(dialog).getByRole("button", { + const confirmButton = await screen.findByRole("button", { name: "Yes, delete", }) makeRequest.mockClear() diff --git a/frontends/mit-open/src/pages/ChannelPage/ChannelPage.tsx b/frontends/mit-open/src/pages/ChannelPage/ChannelPage.tsx index 6ebb7ec94c..d791861e10 100644 --- a/frontends/mit-open/src/pages/ChannelPage/ChannelPage.tsx +++ b/frontends/mit-open/src/pages/ChannelPage/ChannelPage.tsx @@ -3,10 +3,10 @@ import { useParams } from "react-router" import { ChannelPageTemplate } from "./ChannelPageTemplate" import { useChannelDetail } from "api/hooks/channels" import FieldSearch from "./ChannelSearch" -import { - type Facets, - type FacetKey, - type BooleanFacets, +import type { + Facets, + FacetKey, + BooleanFacets, } from "@mitodl/course-search-utils" import { ChannelTypeEnum } from "api/v0" diff --git a/frontends/mit-open/src/pages/ChannelPage/DefaultChannelTemplate.tsx b/frontends/mit-open/src/pages/ChannelPage/DefaultChannelTemplate.tsx index a5614219dc..ef46a8120c 100644 --- a/frontends/mit-open/src/pages/ChannelPage/DefaultChannelTemplate.tsx +++ b/frontends/mit-open/src/pages/ChannelPage/DefaultChannelTemplate.tsx @@ -121,6 +121,7 @@ const DefaultChannelTemplate: React.FC = ({ {channel.data?.search_filter ? ( diff --git a/frontends/mit-open/src/pages/ChannelPage/UnitChannelTemplate.tsx b/frontends/mit-open/src/pages/ChannelPage/UnitChannelTemplate.tsx index 71d8bca8fc..10fce81257 100644 --- a/frontends/mit-open/src/pages/ChannelPage/UnitChannelTemplate.tsx +++ b/frontends/mit-open/src/pages/ChannelPage/UnitChannelTemplate.tsx @@ -159,6 +159,7 @@ const UnitChannelTemplate: React.FC = ({ {channel.data?.search_filter ? ( diff --git a/frontends/ol-components/src/components/BasicDialog/BasicDialog.stories.tsx b/frontends/ol-components/src/components/BasicDialog/BasicDialog.stories.tsx deleted file mode 100644 index 7dc773f4d7..0000000000 --- a/frontends/ol-components/src/components/BasicDialog/BasicDialog.stories.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from "react" -import type { Meta, StoryObj } from "@storybook/react" -import { BasicDialog } from "./BasicDialog" -import Typography from "@mui/material/Typography" - -const meta: Meta = { - title: "smoot-design/BasicDialog", - render: (props) => ( - - Dialog Content - - ), - argTypes: { - onClose: { - action: "closed", - }, - onConfirm: { - action: "confirmed", - }, - }, -} - -export default meta - -type Story = StoryObj - -const args = { - title: "Dialog Title", - open: true, -} - -export const Simple: Story = { - args, -} diff --git a/frontends/ol-components/src/components/Dialog/Dialog.stories.tsx b/frontends/ol-components/src/components/Dialog/Dialog.stories.tsx new file mode 100644 index 0000000000..e0ef3e5973 --- /dev/null +++ b/frontends/ol-components/src/components/Dialog/Dialog.stories.tsx @@ -0,0 +1,67 @@ +import React, { useState, useEffect } from "react" +import type { Meta, StoryObj } from "@storybook/react" +import { Dialog, DialogProps } from "./Dialog" +import Typography from "@mui/material/Typography" + +const StateWrapper = (props: DialogProps) => { + const [open, setOpen] = useState(props.open) + + useEffect(() => { + setOpen(props.open) + }, [props.open]) + + const close = () => { + console.log("close?", open) + setOpen(false) + props.onClose() + } + return ( + + {props.children} + + ) +} + +const meta: Meta = { + title: "smoot-design/Dialog", + component: StateWrapper, + argTypes: { + open: { + control: { type: "boolean" }, + }, + onClose: { + action: "closed", + }, + onConfirm: { + action: "confirmed", + }, + }, +} + +export default meta + +type Story = StoryObj + +export const Content: Story = { + args: { + title: "Dialog Title", + open: true, + }, + render: (props) => ( + + Dialog Content + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. + + + ), +} + +export const Message: Story = { + args: { + title: "Dialog Title", + message: "Dialog message. Would you like to proceed?", + open: true, + }, + render: (props) => , +} diff --git a/frontends/ol-components/src/components/BasicDialog/BasicDialog.tsx b/frontends/ol-components/src/components/Dialog/Dialog.tsx similarity index 53% rename from frontends/ol-components/src/components/BasicDialog/BasicDialog.tsx rename to frontends/ol-components/src/components/Dialog/Dialog.tsx index 5aba878a3c..f7c749f18f 100644 --- a/frontends/ol-components/src/components/BasicDialog/BasicDialog.tsx +++ b/frontends/ol-components/src/components/Dialog/Dialog.tsx @@ -1,46 +1,62 @@ import React, { useCallback, useState } from "react" -import Dialog from "@mui/material/Dialog" -import type { DialogProps } from "@mui/material/Dialog" -import DialogContent from "@mui/material/DialogContent" -import DialogTitle from "@mui/material/DialogTitle" +import styled from "@emotion/styled" +import { theme } from "../ThemeProvider/ThemeProvider" +import { default as MuiDialog } from "@mui/material/Dialog" +import type { DialogProps as MuiDialogProps } from "@mui/material/Dialog" import { Button, ActionButton } from "../Button/Button" import DialogActions from "@mui/material/DialogActions" import { RiCloseLine } from "@remixicon/react" +import Typography from "@mui/material/Typography" -const topRightStyle: React.CSSProperties = { - position: "absolute", - top: 0, - right: 0, -} +const Close = styled.div` + position: absolute; + top: 11px; + right: 20px; +` + +const Header = styled.div` + border-bottom: 1px solid ${theme.custom.colors.lightGray2}; + background-color: ${theme.custom.colors.lightGray1}; + padding: 20px 58px 20px 28px; +` + +const Content = styled.div` + margin: 28px 28px 40px; +` + +const Actions = styled(DialogActions)` + margin: 0 28px 28px; + padding: 0; + gap: 4px; + + button { + margin-left: 0; + } +` -type BasicDialogProps = { +type DialogProps = { className?: string open: boolean onClose: () => void - /** - * MUI Dialog's [TransitionProps](https://mui.com/material-ui/api/dialog/#props) - */ - TransitionProps?: DialogProps["TransitionProps"] onConfirm?: () => void | Promise - title: string + title?: string + message?: string children?: React.ReactNode - /** - * The text to display on the cancel button. Defaults to "Cancel". - */ cancelText?: string - /** - * The text to display on the confirm button. Defaults to "Confirm". - */ confirmText?: string /** * Defaults to `true`. If `true`, dialog grows to `maxWidth`. See * [Dialog Props](https://mui.com/material-ui/api/dialog/#props). */ fullWidth?: boolean + showFooter?: boolean + /** - * Whether to show the footer buttons. Defaults to `true`. + * MUI Dialog's [TransitionProps](https://mui.com/material-ui/api/dialog/#props) */ - showFooter?: boolean + TransitionProps?: MuiDialogProps["TransitionProps"] + + disableEnforceFocus?: MuiDialogProps["disableEnforceFocus"] } /** @@ -50,9 +66,10 @@ type BasicDialogProps = { * particularly good for forms, where a
element should wrap the inputs * and footer buttons. */ -const BasicDialog: React.FC = ({ +const Dialog: React.FC = ({ title, children, + message, open, onClose, onConfirm, @@ -61,8 +78,11 @@ const BasicDialog: React.FC = ({ fullWidth, className, showFooter = true, + TransitionProps, + disableEnforceFocus, }) => { const [confirming, setConfirming] = useState(false) + const handleConfirm = useCallback(async () => { try { setConfirming(true) @@ -74,22 +94,37 @@ const BasicDialog: React.FC = ({ setConfirming(false) } }, [onClose, onConfirm]) + return ( - - {title} -
- + + -
- {children} + + {title && ( +
+ {title} +
+ )} + + {message && {message}} + {children} + {showFooter && ( - + @@ -100,11 +135,11 @@ const BasicDialog: React.FC = ({ > {confirmText} - + )} -
+ ) } -export { BasicDialog } -export type { BasicDialogProps } +export { Dialog } +export type { DialogProps } diff --git a/frontends/ol-components/src/index.ts b/frontends/ol-components/src/index.ts index 1c53a602d5..da0757a32f 100644 --- a/frontends/ol-components/src/index.ts +++ b/frontends/ol-components/src/index.ts @@ -62,8 +62,8 @@ export type { ClickAwayListenerProps } from "@mui/material/ClickAwayListener" export { default as Container } from "@mui/material/Container" export type { ContainerProps } from "@mui/material/Container" -export { default as Dialog } from "@mui/material/Dialog" -export type { DialogProps } from "@mui/material/Dialog" +export { default as MuiDialog } from "@mui/material/Dialog" +export type { DialogProps as MuiDialogProps } from "@mui/material/Dialog" export { default as DialogActions } from "@mui/material/DialogActions" export type { DialogActionsProps } from "@mui/material/DialogActions" export { default as DialogContent } from "@mui/material/DialogContent" @@ -161,7 +161,6 @@ export { default as FormGroup } from "@mui/material/FormGroup" export { default as Slider } from "@mui/material/Slider" export * from "./components/Alert/Alert" -export * from "./components/BasicDialog/BasicDialog" export * from "./components/BannerPage/BannerPage" export * from "./components/Breadcrumbs/Breadcrumbs" export * from "./components/Card/Card" @@ -169,6 +168,9 @@ export * from "./components/Carousel/Carousel" export * from "./components/Checkbox/Checkbox" export * from "./components/Checkbox/CheckboxChoiceField" export * from "./components/Chips/ChipLink" +export * from "./components/ChoiceBox/ChoiceBox" +export * from "./components/ChoiceBox/ChoiceBoxField" +export * from "./components/Dialog/Dialog" export * from "./components/EmbedlyCard/EmbedlyCard" export * from "./components/FormDialog/FormDialog" export * from "./components/LearningResourceCard/LearningResourceCard" @@ -178,19 +180,17 @@ export * from "./components/LearningResourceCard/LearningResourceListCardCondens export * from "./components/LearningResourceExpanded/LearningResourceExpanded" export * from "./components/LoadingSpinner/LoadingSpinner" export * from "./components/Logo/Logo" -export * from "./components/RoutedDrawer/RoutedDrawer" export * from "./components/NavDrawer/NavDrawer" +export * from "./components/PlainList/PlainList" +export * from "./components/Popover/Popover" +export * from "./components/RoutedDrawer/RoutedDrawer" +export * from "./components/ShareTooltip/ShareTooltip" export * from "./components/SimpleMenu/SimpleMenu" export * from "./components/SortableList/SortableList" -export * from "./components/ShareTooltip/ShareTooltip" -export * from "./components/PlainList/PlainList" -export * from "./components/TruncateText/TruncateText" export * from "./components/ThemeProvider/ThemeProvider" +export * from "./components/TruncateText/TruncateText" export * from "./components/Radio/Radio" export * from "./components/RadioChoiceField/RadioChoiceField" -export * from "./components/ChoiceBox/ChoiceBox" -export * from "./components/ChoiceBox/ChoiceBoxField" -export * from "./components/Popover/Popover" export * from "./constants/imgConfigs" From 22bdadf350e791c114dd31fa66470b6819493f73 Mon Sep 17 00:00:00 2001 From: Anastasia Beglova Date: Mon, 29 Jul 2024 16:27:31 -0400 Subject: [PATCH 05/11] add default yearly_decay_percent (#1330) --- .../src/page-components/SearchDisplay/SearchDisplay.tsx | 2 +- learning_resources_search/api.py | 2 +- learning_resources_search/serializers.py | 2 +- openapi/specs/v1.yaml | 5 +++++ 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/frontends/mit-open/src/page-components/SearchDisplay/SearchDisplay.tsx b/frontends/mit-open/src/page-components/SearchDisplay/SearchDisplay.tsx index 341bb61b0f..62a7231e7a 100644 --- a/frontends/mit-open/src/page-components/SearchDisplay/SearchDisplay.tsx +++ b/frontends/mit-open/src/page-components/SearchDisplay/SearchDisplay.tsx @@ -628,7 +628,7 @@ const SearchDisplay: React.FC = ({ stalenessSliderSetting={ searchParams.get("yearly_decay_percent") ? Number(searchParams.get("yearly_decay_percent")) - : 0 + : 2.5 } setSearchParams={setSearchParams} /> diff --git a/learning_resources_search/api.py b/learning_resources_search/api.py index f2810a3ab4..08f2e3c045 100644 --- a/learning_resources_search/api.py +++ b/learning_resources_search/api.py @@ -486,7 +486,7 @@ def adjust_original_query_for_percolate(query): Remove keys that are irrelevent when storing original queries for percolate uniqueness such as "limit" and "offset" """ - for key in ["limit", "offset", "sortby"]: + for key in ["limit", "offset", "sortby", "yearly_decay_percent"]: query.pop(key, None) return order_params(query) diff --git a/learning_resources_search/serializers.py b/learning_resources_search/serializers.py index 34884e3897..7b89849270 100644 --- a/learning_resources_search/serializers.py +++ b/learning_resources_search/serializers.py @@ -308,7 +308,7 @@ class LearningResourcesSearchRequestSerializer(SearchRequestSerializer): min_value=0, required=False, allow_null=True, - default=None, + default=2.5, help_text=( "Relevance score penalty percent per year for for resources without " "upcoming runs. Only affects results if there is a search term." diff --git a/openapi/specs/v1.yaml b/openapi/specs/v1.yaml index 8a53772129..adeeaba92e 100644 --- a/openapi/specs/v1.yaml +++ b/openapi/specs/v1.yaml @@ -2542,6 +2542,7 @@ paths: maximum: 10 minimum: 0 nullable: true + default: 2.5 description: Relevance score penalty percent per year for for resources without upcoming runs. Only affects results if there is a search term. tags: @@ -2971,6 +2972,7 @@ paths: maximum: 10 minimum: 0 nullable: true + default: 2.5 description: Relevance score penalty percent per year for for resources without upcoming runs. Only affects results if there is a search term. tags: @@ -3439,6 +3441,7 @@ paths: maximum: 10 minimum: 0 nullable: true + default: 2.5 description: Relevance score penalty percent per year for for resources without upcoming runs. Only affects results if there is a search term. tags: @@ -3884,6 +3887,7 @@ paths: maximum: 10 minimum: 0 nullable: true + default: 2.5 description: Relevance score penalty percent per year for for resources without upcoming runs. Only affects results if there is a search term. tags: @@ -9541,6 +9545,7 @@ components: maximum: 10 minimum: 0 nullable: true + default: 2.5 description: Relevance score penalty percent per year for for resources without upcoming runs. Only affects results if there is a search term. certification: From ed4ce1e9d0e05bca0d18a40a31c765b86b7f46d5 Mon Sep 17 00:00:00 2001 From: Matt Bertrand Date: Tue, 30 Jul 2024 10:59:14 -0400 Subject: [PATCH 06/11] Assign mitxonline certificate type from api values (#1335) --- learning_resources/etl/mitxonline.py | 34 +++++++++++++++++++---- learning_resources/etl/mitxonline_test.py | 34 +++++++++++++++-------- test_json/mitxonline_courses.json | 12 ++++++++ test_json/mitxonline_programs.json | 27 ++++++++++++------ 4 files changed, 81 insertions(+), 26 deletions(-) diff --git a/learning_resources/etl/mitxonline.py b/learning_resources/etl/mitxonline.py index 5edbcc3a8e..65ce28c65a 100644 --- a/learning_resources/etl/mitxonline.py +++ b/learning_resources/etl/mitxonline.py @@ -59,6 +59,28 @@ def _parse_datetime(value): return parse(value).replace(tzinfo=UTC) if value else None +def parse_certificate_type(certification_type: str) -> str: + """ + Parse the certification type + + Args: + certification_type(str): the certification type + + Returns: + str: the parsed certification type + """ + cert_map = { + "micromasters credential": CertificationType.micromasters.name, + "certificate of completion": CertificationType.completion.name, + } + + certification_code = cert_map.get(certification_type.lower()) + if not certification_code: + log.error("Unknown MITx Online certification type: %s", certification_type) + return CertificationType.completion.name + return certification_code + + def parse_page_attribute( mitx_json, attribute, @@ -251,7 +273,9 @@ def _transform_course(course): ), # a course is only published if it has a live url and published runs "professional": False, "certification": has_certification, - "certification_type": CertificationType.completion.name + "certification_type": parse_certificate_type( + course.get("certificate_type", CertificationType.none.name) + ) if has_certification else CertificationType.none.name, "image": _transform_image(course), @@ -309,10 +333,10 @@ def transform_programs(programs): "departments": parse_departments(program.get("departments", [])), "platform": PlatformType.mitxonline.name, "professional": False, - "certification": bool(parse_page_attribute(program, "page_url")), - "certification_type": CertificationType.completion.name - if bool(parse_page_attribute(program, "page_url")) - else CertificationType.none.name, + "certification": program.get("certificate_type") is not None, + "certification_type": parse_certificate_type( + program.get("certificate_type", CertificationType.none.name) + ), "topics": transform_topics(program.get("topics", []), OFFERED_BY["code"]), "description": clean_data(parse_page_attribute(program, "description")), "url": parse_page_attribute(program, "page_url", is_url=True), diff --git a/learning_resources/etl/mitxonline_test.py b/learning_resources/etl/mitxonline_test.py index 900aa57f22..fab3246f45 100644 --- a/learning_resources/etl/mitxonline_test.py +++ b/learning_resources/etl/mitxonline_test.py @@ -23,6 +23,7 @@ _transform_run, extract_courses, extract_programs, + parse_certificate_type, parse_page_attribute, parse_program_prices, transform_courses, @@ -136,9 +137,9 @@ def test_mitxonline_transform_programs( "certification": bool( program_data.get("page", {}).get("page_url", None) is not None ), - "certification_type": CertificationType.completion.name - if program_data.get("page", {}).get("page_url", None) is not None - else CertificationType.none.name, + "certification_type": parse_certificate_type( + program_data["certificate_type"] + ), "image": _transform_image(program_data), "description": clean_data( program_data.get("page", {}).get("description", None) @@ -318,15 +319,9 @@ def test_mitxonline_transform_courses(settings, mock_mitxonline_courses_data): for course_run in course_data["courseruns"] ], ), - "certification_type": CertificationType.completion.name - if parse_certification( - OFFERED_BY["code"], - [ - _transform_run(course_run, course_data) - for course_run in course_data["courseruns"] - ], - ) - else CertificationType.none.name, + "certification_type": parse_certificate_type( + course_data["certificate_type"] + ), "topics": transform_topics(course_data["topics"], OFFERED_BY["code"]), "url": ( urljoin( @@ -488,3 +483,18 @@ def test_parse_prices(current_price, page_price, expected): assert parse_program_prices(program_data) == sorted( [float(price) for price in expected] ) + + +@pytest.mark.parametrize( + ("cert_type", "expected_cert_type", "error"), + [ + ("Certificate of Completion", CertificationType.completion.name, False), + ("MicroMasters Credential", CertificationType.micromasters.name, False), + ("Pro Cert", CertificationType.completion.name, True), + ], +) +def test_parse_certificate_type(mocker, cert_type, expected_cert_type, error): + """Test that the certificate type is correctly parsed""" + mock_log = mocker.patch("learning_resources.etl.mitxonline.log.error") + assert parse_certificate_type(cert_type) == expected_cert_type + assert mock_log.call_count == (1 if error else 0) diff --git a/test_json/mitxonline_courses.json b/test_json/mitxonline_courses.json index 51ed56c859..3764758fdb 100644 --- a/test_json/mitxonline_courses.json +++ b/test_json/mitxonline_courses.json @@ -9,6 +9,7 @@ "readable_id": "course-v1:MITxT+14.100PEx", "next_run_id": null, "departments": [], + "certificate_type": "Certificate of Completion", "page": { "feature_image_src": "/media/original_images/14.100x_xGKvQiK.jpg?v=8fe621cc0d80ebe4e57fd1b240e9f95d8c2b31db", "page_url": "/courses/course-v1:MITxT+14.100PEx/", @@ -75,6 +76,7 @@ "name": "Chemical Engineering" } ], + "certificate_type": "Certificate of Completion", "page": { "feature_image_src": "/media/original_images/4-drkpurple.jpg?v=b6024e4bdf229b618e36f2e942f2ae6f173dd9d3", "page_url": "/courses/course-v1:MITxT+10.50.CH04x/", @@ -128,6 +130,7 @@ "name": "Chemical Engineering" } ], + "certificate_type": "Certificate of Completion", "page": { "feature_image_src": "/media/original_images/7-drkBlue_5uVzT1f.jpg?v=0f9790b2ce8c75e91f5fbf01c5373d36fb75a81e", "page_url": "/courses/course-v1:MITxT+10.50.CH07x/", @@ -181,6 +184,7 @@ "name": "Chemical Engineering" } ], + "certificate_type": "Certificate of Completion", "page": { "feature_image_src": "/media/original_images/9-red.jpg?v=71fc9e04b14d210d7f5062dee34dc4e32db6967a", "page_url": "/courses/course-v1:MITxT+10.50.CH09x/", @@ -234,6 +238,7 @@ "name": "Chemical Engineering" } ], + "certificate_type": "Certificate of Completion", "page": { "feature_image_src": "/media/original_images/6-ltBlue.jpg?v=af26cc60badb0a7b289813f9ccbf4c8f77ad8490", "page_url": "/courses/course-v1:MITxT+10.50.CH06x/", @@ -287,6 +292,7 @@ "name": "Chemical Engineering" } ], + "certificate_type": "Certificate of Completion", "page": { "feature_image_src": "/media/original_images/drkGreen_375.png?v=67357416dbff6268f5463acc4ea8847b8a08428a", "page_url": "/courses/course-v1:MITxT+10.50.CH02x/", @@ -340,6 +346,7 @@ "name": "Chemical Engineering" } ], + "certificate_type": "Certificate of Completion", "page": { "feature_image_src": "/media/original_images/1-ltGreen.jpg?v=f40e3581924ca77414ae3977c8cdb1a94765e46e", "page_url": "/courses/course-v1:MITxT+10.50.CH01x/", @@ -393,6 +400,7 @@ "name": "Chemical Engineering" } ], + "certificate_type": "Certificate of Completion", "page": { "feature_image_src": "/media/original_images/8-orange.jpg?v=0be48100571b661b7c8a5b8de061f25232d91301", "page_url": "/courses/course-v1:MITxT+10.50.CH08x/", @@ -446,6 +454,7 @@ "name": "Chemical Engineering" } ], + "certificate_type": "Certificate of Completion", "page": { "feature_image_src": "/media/original_images/3-purple.jpg?v=82c25a37e88b44529a7f4439348f07f099308d1a", "page_url": "/courses/course-v1:MITxT+10.50.CH03x/", @@ -499,6 +508,7 @@ "name": "Chemical Engineering" } ], + "certificate_type": "Certificate of Completion", "page": { "feature_image_src": "/media/original_images/5-brown_2.2.jpg?v=ff1b83927266cef6dcfe8c0c30c0adf18205847b", "page_url": "/courses/course-v1:MITxT+10.50.CH05x/", @@ -552,6 +562,7 @@ "name": "Economics" } ], + "certificate_type": "Certificate of Completion", "page": { "feature_image_src": "/media/original_images/14.01x_Course_Image_MITxO.png?v=f6aacc1df41d983b5cda78201a7fc86751229a62", "page_url": "/courses/course-v1:MITxT+14.01x/", @@ -634,6 +645,7 @@ "name": "Management" } ], + "certificate_type": "Certificate of Completion", "page": { "feature_image_src": "/media/original_images/15-356-2x_Image.jpg?v=3a1d8836abb252670315f0cf70875d8a2feda19a", "page_url": "/courses/course-v1:MITxT+15.356.2x/", diff --git a/test_json/mitxonline_programs.json b/test_json/mitxonline_programs.json index bd38a94270..11b45d4b5c 100644 --- a/test_json/mitxonline_programs.json +++ b/test_json/mitxonline_programs.json @@ -116,7 +116,8 @@ "departments": ["Mathematics"], "live": true, "topics": [], - "availability": "scheduled" + "availability": "scheduled", + "certificate_type": "MicroMasters Credential" }, { "title": "Analysis of Transport Phenomena", @@ -279,7 +280,8 @@ "departments": ["Chemical Engineering"], "live": true, "topics": [], - "availability": "anytime" + "availability": "anytime", + "certificate_type": "Certificate of Completion" }, { "title": "Data, Economics, and Design of Policy", @@ -442,7 +444,8 @@ "name": "Social Sciences" } ], - "availability": null + "availability": null, + "certificate_type": "Certificate of Completion" }, { "title": "Data, Economics, and Design of Policy: International Development", @@ -593,7 +596,8 @@ "name": "Social Sciences" } ], - "availability": "scheduled" + "availability": "scheduled", + "certificate_type": "Certificate of Completion" }, { "title": "Data, Economics, and Design of Policy: Public Policy", @@ -722,7 +726,8 @@ "departments": ["Economics"], "live": true, "topics": [], - "availability": null + "availability": null, + "certificate_type": "Certificate of Completion" }, { "title": "Differential Calculus", @@ -813,7 +818,8 @@ "departments": ["Mathematics"], "live": true, "topics": [], - "availability": "scheduled" + "availability": "scheduled", + "certificate_type": "Certificate of Completion" }, { "title": "Introductory Electricity and Magnetics", @@ -904,7 +910,8 @@ "departments": ["Materials Science and Engineering"], "live": true, "topics": [], - "availability": "anytime" + "availability": "anytime", + "certificate_type": "Certificate of Completion" }, { "title": "xMinor in Materials for Electronic, Optical, and Magnetic Devices", @@ -995,7 +1002,8 @@ "departments": ["Materials Science and Engineering"], "live": true, "topics": [], - "availability": "scheduled" + "availability": "scheduled", + "certificate_type": "Certificate of Completion" }, { "title": "xSeries in Introduction to Mechanics", @@ -1098,7 +1106,8 @@ "departments": [], "live": true, "topics": [], - "availability": "anytime" + "availability": "anytime", + "certificate_type": "MicroMasters Credential" } ] } From 00bf0f38b7b9fc6c8eb06f1dbf38b2e5b4f21057 Mon Sep 17 00:00:00 2001 From: Matt Bertrand Date: Tue, 30 Jul 2024 11:57:38 -0400 Subject: [PATCH 07/11] Avoid course overwrites in program ETL pipelines (#1332) --- learning_resources/etl/constants.py | 4 ++-- learning_resources/etl/loaders.py | 20 +++++++++++++++- learning_resources/etl/loaders_test.py | 30 ++++++++++++++++++++++++ learning_resources/etl/pipelines.py | 14 +++++++---- learning_resources/etl/pipelines_test.py | 14 +++++++---- 5 files changed, 71 insertions(+), 11 deletions(-) diff --git a/learning_resources/etl/constants.py b/learning_resources/etl/constants.py index f3dffd1c8d..9759842f20 100644 --- a/learning_resources/etl/constants.py +++ b/learning_resources/etl/constants.py @@ -26,8 +26,8 @@ CourseLoaderConfig = namedtuple( # noqa: PYI024 "CourseLoaderConfig", - ["prune", "offered_by", "runs"], - defaults=[True, OfferedByLoaderConfig(), LearningResourceRunLoaderConfig()], + ["prune", "offered_by", "runs", "fetch_only"], + defaults=[True, OfferedByLoaderConfig(), LearningResourceRunLoaderConfig(), False], ) ProgramLoaderConfig = namedtuple( # noqa: PYI024 diff --git a/learning_resources/etl/loaders.py b/learning_resources/etl/loaders.py index 52ba23ddf5..0da8afd06f 100644 --- a/learning_resources/etl/loaders.py +++ b/learning_resources/etl/loaders.py @@ -259,7 +259,7 @@ def load_run( return learning_resource_run -def load_course( +def load_course( # noqa: C901 resource_data: dict, blocklist: list[str], duplicates: list[dict], @@ -335,6 +335,20 @@ def load_course( unique_field_value, ) resource_id = deduplicated_course_id or readable_id + + if config.fetch_only: + # Do not upsert the course, it should already exist. + # Just find it and return it. + resource = LearningResource.objects.filter( + readable_id=resource_id, + platform=platform, + resource_type=LearningResourceType.course.name, + published=True, + ).first() + if not resource: + log.warning("No published resource found for %s", resource_id) + return resource + if unique_field_name != READABLE_ID_FIELD: # Some dupes may result, so we need to unpublish resources # with matching unique values and different readable_ids @@ -367,6 +381,10 @@ def load_course( if config.prune: # mark runs no longer included here as unpublished + # This generally should not be done when loading courses + # from a program (config.prune=False). + # The course ETL should be the ultimate source of truth for + # courses and their runs. for run in learning_resource.runs.exclude( run_id__in=run_ids_to_update_or_create ).filter(published=True): diff --git a/learning_resources/etl/loaders_test.py b/learning_resources/etl/loaders_test.py index e908b9f0af..79554c01ea 100644 --- a/learning_resources/etl/loaders_test.py +++ b/learning_resources/etl/loaders_test.py @@ -598,6 +598,36 @@ def test_load_course_dupe_urls(unique_url): assert course.published is (unique_url is False) +@pytest.mark.parametrize("course_exists", [True, False]) +def test_load_course_fetch_only(mocker, course_exists): + """When fetch_only is True, course should just be fetched from db""" + mock_next_runs_prices = mocker.patch( + "learning_resources.etl.loaders.load_next_start_date_and_prices" + ) + mock_warn = mocker.patch("learning_resources.etl.loaders.log.warning") + platform = LearningResourcePlatformFactory.create(code=PlatformType.mitpe.name) + if course_exists: + resource = LearningResourceFactory.create(is_course=True, platform=platform) + else: + resource = LearningResourceFactory.build(is_course=True, platform=platform) + + props = { + "readable_id": resource.readable_id, + "platform": platform.code, + "offered_by": {"code": OfferedBy.ocw.name}, + } + result = load_course(props, [], [], config=CourseLoaderConfig(fetch_only=True)) + if course_exists: + assert result == resource + mock_warn.assert_not_called() + else: + assert result is None + mock_warn.assert_called_once_with( + "No published resource found for %s", resource.readable_id + ) + mock_next_runs_prices.assert_not_called() + + @pytest.mark.parametrize("run_exists", [True, False]) @pytest.mark.parametrize( "availability", [RunAvailability.archived.value, RunAvailability.current.value] diff --git a/learning_resources/etl/pipelines.py b/learning_resources/etl/pipelines.py index 7ccf48d062..c694f8a9ba 100644 --- a/learning_resources/etl/pipelines.py +++ b/learning_resources/etl/pipelines.py @@ -35,7 +35,9 @@ micromasters_etl = compose( load_programs( ETLSource.micromasters.name, - config=ProgramLoaderConfig(prune=True, courses=CourseLoaderConfig()), + config=ProgramLoaderConfig( + prune=True, courses=CourseLoaderConfig(fetch_only=True) + ), ), micromasters.transform, micromasters.extract, @@ -53,7 +55,9 @@ mitxonline_programs_etl = compose( load_programs( ETLSource.mitxonline.name, - config=ProgramLoaderConfig(courses=CourseLoaderConfig(prune=True), prune=True), + config=ProgramLoaderConfig( + courses=CourseLoaderConfig(fetch_only=True), prune=True + ), ), mitxonline.transform_programs, mitxonline.extract_programs, @@ -74,7 +78,7 @@ prolearn_programs_etl = compose( load_programs( ETLSource.prolearn.name, - config=ProgramLoaderConfig(courses=CourseLoaderConfig(prune=True)), + config=ProgramLoaderConfig(courses=CourseLoaderConfig(fetch_only=True)), ), prolearn.transform_programs, prolearn.extract_programs, @@ -91,7 +95,9 @@ xpro_programs_etl = compose( load_programs( ETLSource.xpro.name, - config=ProgramLoaderConfig(courses=CourseLoaderConfig(prune=True)), + config=ProgramLoaderConfig( + courses=CourseLoaderConfig(fetch_only=True), prune=True + ), ), xpro.transform_programs, xpro.extract_programs, diff --git a/learning_resources/etl/pipelines_test.py b/learning_resources/etl/pipelines_test.py index af94bda19d..6872e5feb9 100644 --- a/learning_resources/etl/pipelines_test.py +++ b/learning_resources/etl/pipelines_test.py @@ -79,7 +79,9 @@ def test_mitxonline_programs_etl(): mock_load_programs.assert_called_once_with( ETLSource.mitxonline.name, mock_transform.return_value, - config=ProgramLoaderConfig(courses=CourseLoaderConfig(prune=True), prune=True), + config=ProgramLoaderConfig( + courses=CourseLoaderConfig(fetch_only=True), prune=True + ), ) assert result == mock_load_programs.return_value @@ -146,7 +148,9 @@ def test_xpro_programs_etl(): mock_load_programs.assert_called_once_with( ETLSource.xpro.name, mock_transform.return_value, - config=ProgramLoaderConfig(courses=CourseLoaderConfig(prune=True)), + config=ProgramLoaderConfig( + courses=CourseLoaderConfig(fetch_only=True), prune=True + ), ) assert result == mock_load_programs.return_value @@ -296,7 +300,9 @@ def test_micromasters_etl(): mock_load_programs.assert_called_once_with( ETLSource.micromasters.name, mock_transform.return_value, - config=ProgramLoaderConfig(prune=True, courses=CourseLoaderConfig()), + config=ProgramLoaderConfig( + prune=True, courses=CourseLoaderConfig(fetch_only=True) + ), ) assert result == mock_load_programs.return_value @@ -317,7 +323,7 @@ def test_prolearn_programs_etl(): mock_load_programs.assert_called_once_with( ETLSource.prolearn.name, mock_transform.return_value, - config=ProgramLoaderConfig(courses=CourseLoaderConfig(prune=True)), + config=ProgramLoaderConfig(courses=CourseLoaderConfig(fetch_only=True)), ) assert result == mock_load_programs.return_value From fafe8427b9bd1fce1aa5fbbb5643df59fe6aa97c Mon Sep 17 00:00:00 2001 From: Jon Kafton <939376+jonkafton@users.noreply.github.com> Date: Tue, 30 Jul 2024 18:31:57 +0200 Subject: [PATCH 08/11] Updated designs for the unit page (#1325) * Design updates * Refactor out UnitCard. Apply styles for desktop * Mobile breakpoint for logo container * Migration for updated copy * Remove log line, fix migration * Apply loading styles --- .../migrations/0005_unit_page_copy_updates.py | 35 +++ .../src/pages/UnitsListingPage/UnitCard.tsx | 215 ++++++++++++++++++ .../UnitsListingPage/UnitsListingPage.tsx | 179 ++------------- 3 files changed, 264 insertions(+), 165 deletions(-) create mode 100644 data_fixtures/migrations/0005_unit_page_copy_updates.py create mode 100644 frontends/mit-open/src/pages/UnitsListingPage/UnitCard.tsx diff --git a/data_fixtures/migrations/0005_unit_page_copy_updates.py b/data_fixtures/migrations/0005_unit_page_copy_updates.py new file mode 100644 index 0000000000..c792dc4d46 --- /dev/null +++ b/data_fixtures/migrations/0005_unit_page_copy_updates.py @@ -0,0 +1,35 @@ +from django.db import migrations + +fixtures = [ + { + "name": "mitpe", + "channel_configuration": { + "heading": ( + "Offering lifelong learning opportunities that prepare engineering, " + "science, and technology professionals to address complex industry " + "challenges." + ), + }, + }, +] + + +def update_copy(apps, schema_editor): + Channel = apps.get_model("channels", "Channel") + for fixture in fixtures: + channel_configuration_updates = fixture["channel_configuration"] + channel = Channel.objects.get(name=fixture["name"]) + if Channel.objects.filter(name=fixture["name"]).exists(): + for key, val in channel_configuration_updates.items(): + channel.configuration[key] = val + channel.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("data_fixtures", "0004_upsert_initial_topic_data"), + ] + + operations = [ + migrations.RunPython(update_copy, migrations.RunPython.noop), + ] diff --git a/frontends/mit-open/src/pages/UnitsListingPage/UnitCard.tsx b/frontends/mit-open/src/pages/UnitsListingPage/UnitCard.tsx new file mode 100644 index 0000000000..b0f01991af --- /dev/null +++ b/frontends/mit-open/src/pages/UnitsListingPage/UnitCard.tsx @@ -0,0 +1,215 @@ +import React from "react" +import { LearningResourceOfferorDetail, OfferedByEnum } from "api" +import { Card, Skeleton, Typography, styled, theme } from "ol-components" +import { useChannelDetail } from "api/hooks/channels" + +const CardStyled = styled(Card)({ + height: "100%", +}) + +const UnitCardContainer = styled.div({ + display: "flex", + flexDirection: "column", + alignItems: "center", + height: "100%", + backgroundColor: "rgba(243, 244, 248, 0.50)", + [theme.breakpoints.down("md")]: { + backgroundColor: theme.custom.colors.white, + }, +}) + +const UnitCardContent = styled.div({ + display: "flex", + flexDirection: "column", + flexGrow: 1, + width: "100%", +}) + +const LogoContainer = styled.div({ + padding: "40px 32px", + backgroundColor: theme.custom.colors.white, + [theme.breakpoints.down("md")]: { + padding: "34px 0 14px", + ".MuiSkeleton-root": { + margin: "0 auto", + }, + }, +}) + +const UnitLogo = styled.img({ + height: "50px", + display: "block", + [theme.breakpoints.down("md")]: { + height: "40px", + margin: "0 auto", + }, +}) + +const CardBottom = styled.div({ + padding: "24px", + borderTop: `1px solid ${theme.custom.colors.lightGray2}`, + display: "flex", + flexGrow: 1, + flexDirection: "column", + gap: "24px", + [theme.breakpoints.down("md")]: { + padding: "16px", + gap: "10px", + borderTop: "none", + }, +}) + +const ValuePropContainer = styled.div({ + display: "flex", + flexDirection: "column", + alignItems: "flex-start", + justifyContent: "flex-start", + flexGrow: 1, + paddingBottom: "16px", +}) + +const LoadingContent = styled.div({ + padding: "24px", +}) + +const HeadingText = styled(Typography)(({ theme }) => ({ + alignSelf: "stretch", + color: theme.custom.colors.darkGray2, + ...theme.typography.body2, + [theme.breakpoints.down("md")]: { + display: "none", + }, +})) + +const SubHeadingText = styled(HeadingText)(({ theme }) => ({ + alignSelf: "stretch", + color: theme.custom.colors.darkGray2, + ...theme.typography.body2, + display: "none", + [theme.breakpoints.down("md")]: { + display: "block", + }, +})) + +const CountsTextContainer = styled.div({ + display: "flex", + gap: "10px", + [theme.breakpoints.down("md")]: { + justifyContent: "flex-end", + }, +}) + +const CountsText = styled(Typography)(({ theme }) => ({ + color: theme.custom.colors.darkGray2, + ...theme.typography.body2, + [theme.breakpoints.down("md")]: { + ...theme.typography.body3, + color: theme.custom.colors.silverGrayDark, + }, +})) + +const unitLogos = { + [OfferedByEnum.Mitx]: "/static/images/unit_logos/mitx.svg", + [OfferedByEnum.Ocw]: "/static/images/unit_logos/ocw.svg", + [OfferedByEnum.Bootcamps]: "/static/images/unit_logos/bootcamps.svg", + [OfferedByEnum.Xpro]: "/static/images/unit_logos/xpro.svg", + [OfferedByEnum.Mitpe]: "/static/images/unit_logos/mitpe.svg", + [OfferedByEnum.See]: "/static/images/unit_logos/see.svg", +} + +interface UnitCardsProps { + units: LearningResourceOfferorDetail[] | undefined + courseCounts: Record + programCounts: Record +} + +interface UnitCardProps { + unit: LearningResourceOfferorDetail + logo: string + courseCount: number + programCount: number +} + +const UnitCard: React.FC = (props) => { + const { unit, logo, courseCount, programCount } = props + const channelDetailQuery = useChannelDetail("unit", unit.code) + const channelDetail = channelDetailQuery.data + const unitUrl = channelDetail?.channel_url + + return channelDetailQuery.isLoading ? ( + + ) : ( + + + + + + + + + + + {channelDetail?.configuration?.heading} + + + {channelDetail?.configuration?.sub_heading} + + + + + {courseCount > 0 ? `Courses: ${courseCount}` : ""} + + + {programCount > 0 ? `Programs: ${programCount}` : ""} + + + + + + + + ) +} + +export const UnitCardLoading = () => { + return ( + + + + + + + + + + + + + + + ) +} + +export const UnitCards: React.FC = (props) => { + const { units, courseCounts, programCounts } = props + return ( + <> + {units?.map((unit) => { + const courseCount = courseCounts[unit.code] || 0 + const programCount = programCounts[unit.code] || 0 + const logo = + unitLogos[unit.code as OfferedByEnum] || + `/static/images/unit_logos/${unit.code}.svg` + return unit.value_prop ? ( + + ) : null + })} + + ) +} diff --git a/frontends/mit-open/src/pages/UnitsListingPage/UnitsListingPage.tsx b/frontends/mit-open/src/pages/UnitsListingPage/UnitsListingPage.tsx index ff376a8258..c349ab8adf 100644 --- a/frontends/mit-open/src/pages/UnitsListingPage/UnitsListingPage.tsx +++ b/frontends/mit-open/src/pages/UnitsListingPage/UnitsListingPage.tsx @@ -1,26 +1,24 @@ +import React from "react" import { useLearningResourcesSearch, useOfferorsList, } from "api/hooks/learningResources" import { Banner, - Card, Container, - Skeleton, Typography, styled, Breadcrumbs, + theme, } from "ol-components" import { RiBookOpenLine, RiSuitcaseLine } from "@remixicon/react" -import React from "react" import { LearningResourceOfferorDetail, LearningResourcesSearchResponse, - OfferedByEnum, } from "api" import { MetaTags } from "ol-utilities" -import { useChannelDetail } from "api/hooks/channels" import { HOME } from "@/common/urls" +import { UnitCards, UnitCardLoading } from "./UnitCard" const UNITS_BANNER_IMAGE = "/static/images/background_steps.jpeg" const DESKTOP_WIDTH = "1056px" @@ -61,7 +59,7 @@ const PageContent = styled.div(({ theme }) => ({ flexDirection: "column", alignItems: "center", padding: "40px 10px 80px 10px", - gap: "80px", + gap: "48px", [theme.breakpoints.down("md")]: { padding: "40px 0px 30px 0px", gap: "40px", @@ -84,6 +82,15 @@ const PageHeaderContainerInner = styled.div({ display: "flex", flexDirection: "column", maxWidth: "1000px", + border: `1px solid ${theme.custom.colors.lightGray2}`, + backgroundColor: theme.custom.colors.white, + borderRadius: "8px", + padding: "32px", + [theme.breakpoints.down("md")]: { + backgroundColor: "transparent", + border: "none", + padding: "0", + }, }) const PageHeaderText = styled(Typography)(({ theme }) => ({ @@ -91,10 +98,6 @@ const PageHeaderText = styled(Typography)(({ theme }) => ({ ...theme.typography.subtitle1, })) -const CardStyled = styled(Card)({ - height: "100%", -}) - const UnitContainer = styled.div(({ theme }) => ({ display: "flex", flexDirection: "column", @@ -146,77 +149,12 @@ const GridContainer = styled.div(({ theme }) => ({ display: "grid", gap: "25px", gridTemplateColumns: "repeat(2, 1fr)", + width: "100%", [theme.breakpoints.down("md")]: { gridTemplateColumns: "1fr", }, })) -const UnitCardContainer = styled.div({ - display: "flex", - flexDirection: "column", - alignItems: "center", - padding: "16px", - height: "100%", -}) - -const UnitCardContent = styled.div({ - display: "flex", - flexDirection: "column", - flexGrow: 1, -}) - -const LogoContainer = styled.div({ - display: "flex", - justifyContent: "center", - alignItems: "center", - height: "128px", -}) - -const UnitLogo = styled.img({ - display: "flex", - flexDirection: "column", - justifyContent: "center", - alignItems: "center", - height: "50px", - maxWidth: "100%", -}) - -const ValuePropContainer = styled.div({ - display: "flex", - flexDirection: "column", - alignItems: "flex-start", - justifyContent: "flex-start", - flexGrow: 1, - paddingBottom: "16px", -}) - -const ValuePropText = styled(Typography)(({ theme }) => ({ - alignSelf: "stretch", - color: theme.custom.colors.darkGray2, - ...theme.typography.body2, -})) - -const CountsTextContainer = styled.div({ - display: "flex", - justifyContent: "flex-end", - gap: "10px", -}) - -const CountsText = styled(Typography)(({ theme }) => ({ - textAlign: "center", - color: theme.custom.colors.silverGrayDark, - ...theme.typography.body3, -})) - -const unitLogos = { - [OfferedByEnum.Mitx]: "/static/images/unit_logos/mitx.svg", - [OfferedByEnum.Ocw]: "/static/images/unit_logos/ocw.svg", - [OfferedByEnum.Bootcamps]: "/static/images/unit_logos/bootcamps.svg", - [OfferedByEnum.Xpro]: "/static/images/unit_logos/xpro.svg", - [OfferedByEnum.Mitpe]: "/static/images/unit_logos/mitpe.svg", - [OfferedByEnum.See]: "/static/images/unit_logos/see.svg", -} - interface UnitSectionProps { id: string icon: React.ReactNode @@ -267,95 +205,6 @@ const UnitSection: React.FC = (props) => { ) } -interface UnitCardsProps { - units: LearningResourceOfferorDetail[] | undefined - courseCounts: Record - programCounts: Record -} - -const UnitCards: React.FC = (props) => { - const { units, courseCounts, programCounts } = props - return ( - <> - {units?.map((unit) => { - const courseCount = courseCounts[unit.code] || 0 - const programCount = programCounts[unit.code] || 0 - const logo = - unitLogos[unit.code as OfferedByEnum] || - `/static/images/unit_logos/${unit.code}.svg` - return unit.value_prop ? ( - - ) : null - })} - - ) -} - -interface UnitCardProps { - unit: LearningResourceOfferorDetail - logo: string - courseCount: number - programCount: number -} - -const UnitCard: React.FC = (props) => { - const { unit, logo, courseCount, programCount } = props - const channelDetailQuery = useChannelDetail("unit", unit.code) - const channelDetail = channelDetailQuery.data - const unitUrl = channelDetail?.channel_url - return channelDetailQuery.isLoading ? ( - - ) : ( - - - - - - - - - {unit.value_prop} - - - - {courseCount > 0 ? `Courses: ${courseCount}` : ""} - - - {programCount > 0 ? `Programs: ${programCount}` : ""} - - - - - - - ) -} - -const UnitCardLoading = () => { - return ( - - - - - - - - - - - - - - - ) -} - const UnitsListingPage: React.FC = () => { const unitsQuery = useOfferorsList() const units = unitsQuery.data?.results From c3c345defe8856499baec2ebab8c7adfcb00495f Mon Sep 17 00:00:00 2001 From: Anastasia Beglova Date: Tue, 30 Jul 2024 14:44:38 -0400 Subject: [PATCH 09/11] dev mode (#1333) --- frontends/api/src/generated/v1/api.ts | 96 +++++++++++++++++++ learning_resources_search/api.py | 11 ++- learning_resources_search/api_test.py | 20 ++++ learning_resources_search/constants.py | 4 +- learning_resources_search/serializers.py | 6 ++ learning_resources_search/serializers_test.py | 2 + learning_resources_search/views.py | 27 ++++-- openapi/specs/v1.yaml | 40 ++++++++ 8 files changed, 190 insertions(+), 16 deletions(-) diff --git a/frontends/api/src/generated/v1/api.ts b/frontends/api/src/generated/v1/api.ts index 2cc2d9cbdb..ce5480002b 100644 --- a/frontends/api/src/generated/v1/api.ts +++ b/frontends/api/src/generated/v1/api.ts @@ -3401,6 +3401,12 @@ export interface PercolateQuerySubscriptionRequestRequest { * @memberof PercolateQuerySubscriptionRequestRequest */ topic?: Array + /** + * If true return raw open search results with score explanations + * @type {boolean} + * @memberof PercolateQuerySubscriptionRequestRequest + */ + dev_mode?: boolean | null /** * The id value for the learning resource * @type {Array} @@ -6612,6 +6618,7 @@ export const ContentFileSearchApiAxiosParamCreator = function ( * @summary Search * @param {Array} [aggregations] Show resource counts by category * @param {Array} [content_feature_type] The feature type of the content file. Possible options are at api/v1/course_features/ + * @param {boolean | null} [dev_mode] If true return raw open search results with score explanations * @param {Array} [id] The id value for the content file * @param {number} [limit] Number of results to return per page * @param {Array} [offered_by] The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education @@ -6628,6 +6635,7 @@ export const ContentFileSearchApiAxiosParamCreator = function ( contentFileSearchRetrieve: async ( aggregations?: Array, content_feature_type?: Array, + dev_mode?: boolean | null, id?: Array, limit?: number, offered_by?: Array, @@ -6664,6 +6672,10 @@ export const ContentFileSearchApiAxiosParamCreator = function ( localVarQueryParameter["content_feature_type"] = content_feature_type } + if (dev_mode !== undefined) { + localVarQueryParameter["dev_mode"] = dev_mode + } + if (id) { localVarQueryParameter["id"] = id } @@ -6734,6 +6746,7 @@ export const ContentFileSearchApiFp = function (configuration?: Configuration) { * @summary Search * @param {Array} [aggregations] Show resource counts by category * @param {Array} [content_feature_type] The feature type of the content file. Possible options are at api/v1/course_features/ + * @param {boolean | null} [dev_mode] If true return raw open search results with score explanations * @param {Array} [id] The id value for the content file * @param {number} [limit] Number of results to return per page * @param {Array} [offered_by] The organization that offers the learning resource * `mitx` - MITx * `ocw` - MIT OpenCourseWare * `bootcamps` - Bootcamps * `xpro` - MIT xPRO * `mitpe` - MIT Professional Education * `see` - MIT Sloan Executive Education @@ -6750,6 +6763,7 @@ export const ContentFileSearchApiFp = function (configuration?: Configuration) { async contentFileSearchRetrieve( aggregations?: Array, content_feature_type?: Array, + dev_mode?: boolean | null, id?: Array, limit?: number, offered_by?: Array, @@ -6771,6 +6785,7 @@ export const ContentFileSearchApiFp = function (configuration?: Configuration) { await localVarAxiosParamCreator.contentFileSearchRetrieve( aggregations, content_feature_type, + dev_mode, id, limit, offered_by, @@ -6825,6 +6840,7 @@ export const ContentFileSearchApiFactory = function ( .contentFileSearchRetrieve( requestParameters.aggregations, requestParameters.content_feature_type, + requestParameters.dev_mode, requestParameters.id, requestParameters.limit, requestParameters.offered_by, @@ -6862,6 +6878,13 @@ export interface ContentFileSearchApiContentFileSearchRetrieveRequest { */ readonly content_feature_type?: Array + /** + * If true return raw open search results with score explanations + * @type {boolean} + * @memberof ContentFileSearchApiContentFileSearchRetrieve + */ + readonly dev_mode?: boolean | null + /** * The id value for the content file * @type {Array} @@ -6956,6 +6979,7 @@ export class ContentFileSearchApi extends BaseAPI { .contentFileSearchRetrieve( requestParameters.aggregations, requestParameters.content_feature_type, + requestParameters.dev_mode, requestParameters.id, requestParameters.limit, requestParameters.offered_by, @@ -11642,6 +11666,7 @@ export const LearningResourcesSearchApiAxiosParamCreator = function ( * @param {Array} [certification_type] The type of certificate * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] The course feature. Possible options are at api/v1/course_features/ * @param {Array} [department] The department that offers the learning resource * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `RES` - Supplemental Resources * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies + * @param {boolean | null} [dev_mode] If true return raw open search results with score explanations * @param {boolean | null} [free] * @param {Array} [id] The id value for the learning resource * @param {Array} [learning_format] The format(s) in which the learning resource is offered * `online` - Online * `hybrid` - Hybrid * `in_person` - In person @@ -11666,6 +11691,7 @@ export const LearningResourcesSearchApiAxiosParamCreator = function ( certification_type?: Array, course_feature?: Array, department?: Array, + dev_mode?: boolean | null, free?: boolean | null, id?: Array, learning_format?: Array, @@ -11719,6 +11745,10 @@ export const LearningResourcesSearchApiAxiosParamCreator = function ( localVarQueryParameter["department"] = department } + if (dev_mode !== undefined) { + localVarQueryParameter["dev_mode"] = dev_mode + } + if (free !== undefined) { localVarQueryParameter["free"] = free } @@ -11814,6 +11844,7 @@ export const LearningResourcesSearchApiFp = function ( * @param {Array} [certification_type] The type of certificate * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] The course feature. Possible options are at api/v1/course_features/ * @param {Array} [department] The department that offers the learning resource * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `RES` - Supplemental Resources * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies + * @param {boolean | null} [dev_mode] If true return raw open search results with score explanations * @param {boolean | null} [free] * @param {Array} [id] The id value for the learning resource * @param {Array} [learning_format] The format(s) in which the learning resource is offered * `online` - Online * `hybrid` - Hybrid * `in_person` - In person @@ -11838,6 +11869,7 @@ export const LearningResourcesSearchApiFp = function ( certification_type?: Array, course_feature?: Array, department?: Array, + dev_mode?: boolean | null, free?: boolean | null, id?: Array, learning_format?: Array, @@ -11867,6 +11899,7 @@ export const LearningResourcesSearchApiFp = function ( certification_type, course_feature, department, + dev_mode, free, id, learning_format, @@ -11929,6 +11962,7 @@ export const LearningResourcesSearchApiFactory = function ( requestParameters.certification_type, requestParameters.course_feature, requestParameters.department, + requestParameters.dev_mode, requestParameters.free, requestParameters.id, requestParameters.learning_format, @@ -11992,6 +12026,13 @@ export interface LearningResourcesSearchApiLearningResourcesSearchRetrieveReques */ readonly department?: Array + /** + * If true return raw open search results with score explanations + * @type {boolean} + * @memberof LearningResourcesSearchApiLearningResourcesSearchRetrieve + */ + readonly dev_mode?: boolean | null + /** * * @type {boolean} @@ -12124,6 +12165,7 @@ export class LearningResourcesSearchApi extends BaseAPI { requestParameters.certification_type, requestParameters.course_feature, requestParameters.department, + requestParameters.dev_mode, requestParameters.free, requestParameters.id, requestParameters.learning_format, @@ -12344,6 +12386,7 @@ export const LearningResourcesUserSubscriptionApiAxiosParamCreator = function ( * @param {Array} [certification_type] The type of certificate * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] The course feature. Possible options are at api/v1/course_features/ * @param {Array} [department] The department that offers the learning resource * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `RES` - Supplemental Resources * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies + * @param {boolean | null} [dev_mode] If true return raw open search results with score explanations * @param {boolean | null} [free] * @param {Array} [id] The id value for the learning resource * @param {Array} [learning_format] The format(s) in which the learning resource is offered * `online` - Online * `hybrid` - Hybrid * `in_person` - In person @@ -12369,6 +12412,7 @@ export const LearningResourcesUserSubscriptionApiAxiosParamCreator = function ( certification_type?: Array, course_feature?: Array, department?: Array, + dev_mode?: boolean | null, free?: boolean | null, id?: Array, learning_format?: Array, @@ -12423,6 +12467,10 @@ export const LearningResourcesUserSubscriptionApiAxiosParamCreator = function ( localVarQueryParameter["department"] = department } + if (dev_mode !== undefined) { + localVarQueryParameter["dev_mode"] = dev_mode + } + if (free !== undefined) { localVarQueryParameter["free"] = free } @@ -12509,6 +12557,7 @@ export const LearningResourcesUserSubscriptionApiAxiosParamCreator = function ( * @param {Array} [certification_type] The type of certificate * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] The course feature. Possible options are at api/v1/course_features/ * @param {Array} [department] The department that offers the learning resource * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `RES` - Supplemental Resources * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies + * @param {boolean | null} [dev_mode] If true return raw open search results with score explanations * @param {boolean | null} [free] * @param {Array} [id] The id value for the learning resource * @param {Array} [learning_format] The format(s) in which the learning resource is offered * `online` - Online * `hybrid` - Hybrid * `in_person` - In person @@ -12533,6 +12582,7 @@ export const LearningResourcesUserSubscriptionApiAxiosParamCreator = function ( certification_type?: Array, course_feature?: Array, department?: Array, + dev_mode?: boolean | null, free?: boolean | null, id?: Array, learning_format?: Array, @@ -12586,6 +12636,10 @@ export const LearningResourcesUserSubscriptionApiAxiosParamCreator = function ( localVarQueryParameter["department"] = department } + if (dev_mode !== undefined) { + localVarQueryParameter["dev_mode"] = dev_mode + } + if (free !== undefined) { localVarQueryParameter["free"] = free } @@ -12668,6 +12722,7 @@ export const LearningResourcesUserSubscriptionApiAxiosParamCreator = function ( * @param {Array} [certification_type] The type of certificate * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] The course feature. Possible options are at api/v1/course_features/ * @param {Array} [department] The department that offers the learning resource * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `RES` - Supplemental Resources * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies + * @param {boolean | null} [dev_mode] If true return raw open search results with score explanations * @param {boolean | null} [free] * @param {Array} [id] The id value for the learning resource * @param {Array} [learning_format] The format(s) in which the learning resource is offered * `online` - Online * `hybrid` - Hybrid * `in_person` - In person @@ -12694,6 +12749,7 @@ export const LearningResourcesUserSubscriptionApiAxiosParamCreator = function ( certification_type?: Array, course_feature?: Array, department?: Array, + dev_mode?: boolean | null, free?: boolean | null, id?: Array, learning_format?: Array, @@ -12749,6 +12805,10 @@ export const LearningResourcesUserSubscriptionApiAxiosParamCreator = function ( localVarQueryParameter["department"] = department } + if (dev_mode !== undefined) { + localVarQueryParameter["dev_mode"] = dev_mode + } + if (free !== undefined) { localVarQueryParameter["free"] = free } @@ -12906,6 +12966,7 @@ export const LearningResourcesUserSubscriptionApiFp = function ( * @param {Array} [certification_type] The type of certificate * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] The course feature. Possible options are at api/v1/course_features/ * @param {Array} [department] The department that offers the learning resource * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `RES` - Supplemental Resources * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies + * @param {boolean | null} [dev_mode] If true return raw open search results with score explanations * @param {boolean | null} [free] * @param {Array} [id] The id value for the learning resource * @param {Array} [learning_format] The format(s) in which the learning resource is offered * `online` - Online * `hybrid` - Hybrid * `in_person` - In person @@ -12931,6 +12992,7 @@ export const LearningResourcesUserSubscriptionApiFp = function ( certification_type?: Array, course_feature?: Array, department?: Array, + dev_mode?: boolean | null, free?: boolean | null, id?: Array, learning_format?: Array, @@ -12961,6 +13023,7 @@ export const LearningResourcesUserSubscriptionApiFp = function ( certification_type, course_feature, department, + dev_mode, free, id, learning_format, @@ -13000,6 +13063,7 @@ export const LearningResourcesUserSubscriptionApiFp = function ( * @param {Array} [certification_type] The type of certificate * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] The course feature. Possible options are at api/v1/course_features/ * @param {Array} [department] The department that offers the learning resource * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `RES` - Supplemental Resources * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies + * @param {boolean | null} [dev_mode] If true return raw open search results with score explanations * @param {boolean | null} [free] * @param {Array} [id] The id value for the learning resource * @param {Array} [learning_format] The format(s) in which the learning resource is offered * `online` - Online * `hybrid` - Hybrid * `in_person` - In person @@ -13024,6 +13088,7 @@ export const LearningResourcesUserSubscriptionApiFp = function ( certification_type?: Array, course_feature?: Array, department?: Array, + dev_mode?: boolean | null, free?: boolean | null, id?: Array, learning_format?: Array, @@ -13053,6 +13118,7 @@ export const LearningResourcesUserSubscriptionApiFp = function ( certification_type, course_feature, department, + dev_mode, free, id, learning_format, @@ -13091,6 +13157,7 @@ export const LearningResourcesUserSubscriptionApiFp = function ( * @param {Array} [certification_type] The type of certificate * `micromasters` - Micromasters Credential * `professional` - Professional Certificate * `completion` - Certificate of Completion * `none` - No Certificate * @param {Array} [course_feature] The course feature. Possible options are at api/v1/course_features/ * @param {Array} [department] The department that offers the learning resource * `1` - Civil and Environmental Engineering * `2` - Mechanical Engineering * `3` - Materials Science and Engineering * `4` - Architecture * `5` - Chemistry * `6` - Electrical Engineering and Computer Science * `7` - Biology * `8` - Physics * `9` - Brain and Cognitive Sciences * `10` - Chemical Engineering * `11` - Urban Studies and Planning * `12` - Earth, Atmospheric, and Planetary Sciences * `14` - Economics * `15` - Management * `16` - Aeronautics and Astronautics * `17` - Political Science * `18` - Mathematics * `20` - Biological Engineering * `21A` - Anthropology * `21G` - Global Languages * `21H` - History * `21L` - Literature * `21M` - Music and Theater Arts * `22` - Nuclear Science and Engineering * `24` - Linguistics and Philosophy * `CC` - Concourse * `CMS-W` - Comparative Media Studies/Writing * `EC` - Edgerton Center * `ES` - Experimental Study Group * `ESD` - Engineering Systems Division * `HST` - Medical Engineering and Science * `IDS` - Data, Systems, and Society * `MAS` - Media Arts and Sciences * `PE` - Athletics, Physical Education and Recreation * `RES` - Supplemental Resources * `STS` - Science, Technology, and Society * `WGS` - Women\'s and Gender Studies + * @param {boolean | null} [dev_mode] If true return raw open search results with score explanations * @param {boolean | null} [free] * @param {Array} [id] The id value for the learning resource * @param {Array} [learning_format] The format(s) in which the learning resource is offered * `online` - Online * `hybrid` - Hybrid * `in_person` - In person @@ -13117,6 +13184,7 @@ export const LearningResourcesUserSubscriptionApiFp = function ( certification_type?: Array, course_feature?: Array, department?: Array, + dev_mode?: boolean | null, free?: boolean | null, id?: Array, learning_format?: Array, @@ -13145,6 +13213,7 @@ export const LearningResourcesUserSubscriptionApiFp = function ( certification_type, course_feature, department, + dev_mode, free, id, learning_format, @@ -13240,6 +13309,7 @@ export const LearningResourcesUserSubscriptionApiFactory = function ( requestParameters.certification_type, requestParameters.course_feature, requestParameters.department, + requestParameters.dev_mode, requestParameters.free, requestParameters.id, requestParameters.learning_format, @@ -13278,6 +13348,7 @@ export const LearningResourcesUserSubscriptionApiFactory = function ( requestParameters.certification_type, requestParameters.course_feature, requestParameters.department, + requestParameters.dev_mode, requestParameters.free, requestParameters.id, requestParameters.learning_format, @@ -13315,6 +13386,7 @@ export const LearningResourcesUserSubscriptionApiFactory = function ( requestParameters.certification_type, requestParameters.course_feature, requestParameters.department, + requestParameters.dev_mode, requestParameters.free, requestParameters.id, requestParameters.learning_format, @@ -13398,6 +13470,13 @@ export interface LearningResourcesUserSubscriptionApiLearningResourcesUserSubscr */ readonly department?: Array + /** + * If true return raw open search results with score explanations + * @type {boolean} + * @memberof LearningResourcesUserSubscriptionApiLearningResourcesUserSubscriptionCheckList + */ + readonly dev_mode?: boolean | null + /** * * @type {boolean} @@ -13552,6 +13631,13 @@ export interface LearningResourcesUserSubscriptionApiLearningResourcesUserSubscr */ readonly department?: Array + /** + * If true return raw open search results with score explanations + * @type {boolean} + * @memberof LearningResourcesUserSubscriptionApiLearningResourcesUserSubscriptionList + */ + readonly dev_mode?: boolean | null + /** * * @type {boolean} @@ -13699,6 +13785,13 @@ export interface LearningResourcesUserSubscriptionApiLearningResourcesUserSubscr */ readonly department?: Array + /** + * If true return raw open search results with score explanations + * @type {boolean} + * @memberof LearningResourcesUserSubscriptionApiLearningResourcesUserSubscriptionSubscribeCreate + */ + readonly dev_mode?: boolean | null + /** * * @type {boolean} @@ -13859,6 +13952,7 @@ export class LearningResourcesUserSubscriptionApi extends BaseAPI { requestParameters.certification_type, requestParameters.course_feature, requestParameters.department, + requestParameters.dev_mode, requestParameters.free, requestParameters.id, requestParameters.learning_format, @@ -13899,6 +13993,7 @@ export class LearningResourcesUserSubscriptionApi extends BaseAPI { requestParameters.certification_type, requestParameters.course_feature, requestParameters.department, + requestParameters.dev_mode, requestParameters.free, requestParameters.id, requestParameters.learning_format, @@ -13938,6 +14033,7 @@ export class LearningResourcesUserSubscriptionApi extends BaseAPI { requestParameters.certification_type, requestParameters.course_feature, requestParameters.department, + requestParameters.dev_mode, requestParameters.free, requestParameters.id, requestParameters.learning_format, diff --git a/learning_resources_search/api.py b/learning_resources_search/api.py index 08f2e3c045..1b076722f9 100644 --- a/learning_resources_search/api.py +++ b/learning_resources_search/api.py @@ -21,11 +21,11 @@ DEPARTMENT_QUERY_FIELDS, LEARNING_RESOURCE, LEARNING_RESOURCE_QUERY_FIELDS, - LEARNING_RESOURCE_SEARCH_FILTERS, LEARNING_RESOURCE_TYPES, RESOURCEFILE_QUERY_FIELDS, RUN_INSTRUCTORS_QUERY_FIELDS, RUNS_QUERY_FIELDS, + SEARCH_FILTERS, SOURCE_EXCLUDED_FIELDS, TOPICS_QUERY_FIELDS, ) @@ -340,7 +340,7 @@ def generate_filter_clauses(search_params): """ all_filter_clauses = {} - for filter_name, filter_config in LEARNING_RESOURCE_SEARCH_FILTERS.items(): + for filter_name, filter_config in SEARCH_FILTERS.items(): if search_params.get(filter_name): clauses_for_filter = [ generate_filter_clause( @@ -447,7 +447,7 @@ def generate_aggregation_clauses(search_params, filter_clauses): for aggregation in search_params.get("aggregations"): # Each aggregation clause contains a filter which includes all the filters # except it's own - path = LEARNING_RESOURCE_SEARCH_FILTERS[aggregation].path + path = SEARCH_FILTERS[aggregation].path unfiltered_aggs = generate_aggregation_clause(aggregation, path) other_filters = [ filter_clauses[key] for key in filter_clauses if key != aggregation @@ -486,7 +486,7 @@ def adjust_original_query_for_percolate(query): Remove keys that are irrelevent when storing original queries for percolate uniqueness such as "limit" and "offset" """ - for key in ["limit", "offset", "sortby", "yearly_decay_percent"]: + for key in ["limit", "offset", "sortby", "yearly_decay_percent", "dev_mode"]: query.pop(key, None) return order_params(query) @@ -622,6 +622,9 @@ def construct_search(search_params): ) search = search.extra(aggs=aggregation_clauses) + if search_params.get("dev_mode"): + search = search.extra(explain=True) + return search diff --git a/learning_resources_search/api_test.py b/learning_resources_search/api_test.py index 58d811d819..591c45cbde 100644 --- a/learning_resources_search/api_test.py +++ b/learning_resources_search/api_test.py @@ -2214,3 +2214,23 @@ def test_default_sort(sortby, q, result): } assert construct_search(search_params).to_dict().get("sort") == result + + +@pytest.mark.parametrize( + "dev_mode", + [True, False], +) +def test_dev_mode(dev_mode): + search_params = { + "aggregations": [], + "q": "text", + "limit": 1, + "offset": 1, + "dev_mode": dev_mode, + "endpoint": LEARNING_RESOURCE, + } + + if dev_mode: + assert construct_search(search_params).to_dict().get("explain") + else: + assert construct_search(search_params).to_dict().get("explain") is None diff --git a/learning_resources_search/constants.py b/learning_resources_search/constants.py index 60d203bda2..5e1788c6df 100644 --- a/learning_resources_search/constants.py +++ b/learning_resources_search/constants.py @@ -57,7 +57,7 @@ class FilterConfig: case_sensitive: bool = False -LEARNING_RESOURCE_SEARCH_FILTERS = { +SEARCH_FILTERS = { "resource_type": FilterConfig("resource_type"), "certification": FilterConfig("certification"), "certification_type": FilterConfig("certification_type.code"), @@ -67,7 +67,7 @@ class FilterConfig: "course_feature": FilterConfig("course_feature"), "content_feature_type": FilterConfig("content_feature_type"), "run_id": FilterConfig("run_id", case_sensitive=True), - "resource_id": FilterConfig("resource_id"), + "resource_id": FilterConfig("resource_id", case_sensitive=True), "topic": FilterConfig("topics.name"), "level": FilterConfig("runs.level.code"), "department": FilterConfig("departments.department_id"), diff --git a/learning_resources_search/serializers.py b/learning_resources_search/serializers.py index 7b89849270..23f5b1f5d4 100644 --- a/learning_resources_search/serializers.py +++ b/learning_resources_search/serializers.py @@ -259,6 +259,12 @@ class SearchRequestSerializer(serializers.Serializer): child=serializers.CharField(), help_text="The topic name. To see a list of options go to api/v1/topics/", ) + dev_mode = serializers.BooleanField( + required=False, + allow_null=True, + default=False, + help_text="If true return raw open search results with score explanations", + ) def validate(self, attrs): unknown = set(self.initial_data) - set(self.fields) diff --git a/learning_resources_search/serializers_test.py b/learning_resources_search/serializers_test.py index 7d32686772..c1ad5eaaa2 100644 --- a/learning_resources_search/serializers_test.py +++ b/learning_resources_search/serializers_test.py @@ -831,6 +831,7 @@ def test_learning_resources_search_request_serializer(): "course_feature": ["Lecture Videos"], "aggregations": ["resource_type", "platform", "level", "resource_category"], "yearly_decay_percent": 0.25, + "dev_mode": False, } serialized = LearningResourcesSearchRequestSerializer(data=data) @@ -867,6 +868,7 @@ def test_content_file_search_request_serializer(): "resource_id": [1, 2, 3], "offered_by": ["xpro", "ocw"], "platform": ["xpro", "edx", "ocw"], + "dev_mode": False, } serialized = ContentFileSearchRequestSerializer(data=data) diff --git a/learning_resources_search/views.py b/learning_resources_search/views.py index a7711e1c27..be9aa8219c 100644 --- a/learning_resources_search/views.py +++ b/learning_resources_search/views.py @@ -71,11 +71,15 @@ def get(self, request): response = execute_learn_search( request_data.data | {"endpoint": LEARNING_RESOURCE} ) - return Response( - LearningResourcesSearchResponseSerializer( - response, context={"request": request} - ).data - ) + + if request_data.data.get("dev_mode"): + return Response(response) + else: + return Response( + LearningResourcesSearchResponseSerializer( + response, context={"request": request} + ).data + ) else: errors = {} for key, errors_obj in request_data.errors.items(): @@ -237,11 +241,14 @@ def get(self, request): response = execute_learn_search( request_data.data | {"endpoint": CONTENT_FILE_TYPE} ) - return Response( - ContentFileSearchResponseSerializer( - response, context={"request": request} - ).data - ) + if request_data.data.get("dev_mode"): + return Response(response) + else: + return Response( + LearningResourcesSearchResponseSerializer( + response, context={"request": request} + ).data + ) else: errors = {} for key, errors_obj in request_data.errors.items(): diff --git a/openapi/specs/v1.yaml b/openapi/specs/v1.yaml index adeeaba92e..9b94023551 100644 --- a/openapi/specs/v1.yaml +++ b/openapi/specs/v1.yaml @@ -156,6 +156,13 @@ paths: minLength: 1 description: The feature type of the content file. Possible options are at api/v1/course_features/ + - in: query + name: dev_mode + schema: + type: boolean + nullable: true + default: false + description: If true return raw open search results with score explanations - in: query name: id schema: @@ -2298,6 +2305,13 @@ paths: \ Society\n* `MAS` - Media Arts and Sciences\n* `PE` - Athletics, Physical\ \ Education and Recreation\n* `RES` - Supplemental Resources\n* `STS` -\ \ Science, Technology, and Society\n* `WGS` - Women's and Gender Studies" + - in: query + name: dev_mode + schema: + type: boolean + nullable: true + default: false + description: If true return raw open search results with score explanations - in: query name: free schema: @@ -2728,6 +2742,13 @@ paths: \ Society\n* `MAS` - Media Arts and Sciences\n* `PE` - Athletics, Physical\ \ Education and Recreation\n* `RES` - Supplemental Resources\n* `STS` -\ \ Science, Technology, and Society\n* `WGS` - Women's and Gender Studies" + - in: query + name: dev_mode + schema: + type: boolean + nullable: true + default: false + description: If true return raw open search results with score explanations - in: query name: free schema: @@ -3183,6 +3204,13 @@ paths: \ Society\n* `MAS` - Media Arts and Sciences\n* `PE` - Athletics, Physical\ \ Education and Recreation\n* `RES` - Supplemental Resources\n* `STS` -\ \ Science, Technology, and Society\n* `WGS` - Women's and Gender Studies" + - in: query + name: dev_mode + schema: + type: boolean + nullable: true + default: false + description: If true return raw open search results with score explanations - in: query name: free schema: @@ -3629,6 +3657,13 @@ paths: \ Society\n* `MAS` - Media Arts and Sciences\n* `PE` - Athletics, Physical\ \ Education and Recreation\n* `RES` - Supplemental Resources\n* `STS` -\ \ Science, Technology, and Society\n* `WGS` - Women's and Gender Studies" + - in: query + name: dev_mode + schema: + type: boolean + nullable: true + default: false + description: If true return raw open search results with score explanations - in: query name: free schema: @@ -9500,6 +9535,11 @@ components: type: string minLength: 1 description: The topic name. To see a list of options go to api/v1/topics/ + dev_mode: + type: boolean + nullable: true + default: false + description: If true return raw open search results with score explanations id: type: array items: From b621075c30cd2e9ffe69dc2b6e1df9b29877a0a9 Mon Sep 17 00:00:00 2001 From: Anastasia Beglova Date: Tue, 30 Jul 2024 15:47:18 -0400 Subject: [PATCH 10/11] fix bug (#1340) --- learning_resources_search/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/learning_resources_search/views.py b/learning_resources_search/views.py index be9aa8219c..508c783b7d 100644 --- a/learning_resources_search/views.py +++ b/learning_resources_search/views.py @@ -245,7 +245,7 @@ def get(self, request): return Response(response) else: return Response( - LearningResourcesSearchResponseSerializer( + ContentFileSearchResponseSerializer( response, context={"request": request} ).data ) From 43875953391500be76922aeaefe643b9ac0e89fc Mon Sep 17 00:00:00 2001 From: Doof Date: Tue, 30 Jul 2024 19:48:32 +0000 Subject: [PATCH 11/11] Release 0.14.4 --- RELEASE.rst | 14 ++++++++++++++ main/settings.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/RELEASE.rst b/RELEASE.rst index 3fa0f31b6f..7c5fc3efad 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -1,6 +1,20 @@ Release Notes ============= +Version 0.14.4 +-------------- + +- fix bug (#1340) +- dev mode (#1333) +- Updated designs for the unit page (#1325) +- Avoid course overwrites in program ETL pipelines (#1332) +- Assign mitxonline certificate type from api values (#1335) +- add default yearly_decay_percent (#1330) +- Modal dialog component and styles +- tab widths (#1309) +- Resource availability: backend changes (#1301) +- styling and icon updates (#1316) + Version 0.14.3 (Released July 29, 2024) -------------- diff --git a/main/settings.py b/main/settings.py index fbbecff6b2..fa97bae638 100644 --- a/main/settings.py +++ b/main/settings.py @@ -33,7 +33,7 @@ from main.settings_pluggy import * # noqa: F403 from openapi.settings_spectacular import open_spectacular_settings -VERSION = "0.14.3" +VERSION = "0.14.4" log = logging.getLogger()