diff --git a/frontends/api/src/generated/v0/api.ts b/frontends/api/src/generated/v0/api.ts index bcf1d4f3b7..c1bfc0a52f 100644 --- a/frontends/api/src/generated/v0/api.ts +++ b/frontends/api/src/generated/v0/api.ts @@ -1019,6 +1019,12 @@ export interface CourseResource { * @memberof CourseResource */ pace: Array + /** + * + * @type {LearningResourceRelationshipChildField} + * @memberof CourseResource + */ + children: LearningResourceRelationshipChildField | null /** * * @type {CourseResourceResourceTypeEnum} @@ -2052,6 +2058,12 @@ export interface LearningPathResource { * @memberof LearningPathResource */ pace: Array + /** + * + * @type {LearningResourceRelationshipChildField} + * @memberof LearningPathResource + */ + children: LearningResourceRelationshipChildField | null /** * * @type {LearningPathResourceResourceTypeEnum} @@ -2567,6 +2579,44 @@ export interface LearningResourcePrice { */ currency: string } +/** + * Serializer field for the LearningResourceRelationship model that uses the LearningResourceSerializer to serialize the child resources + * @export + * @interface LearningResourceRelationshipChildField + */ +export interface LearningResourceRelationshipChildField { + /** + * + * @type {number} + * @memberof LearningResourceRelationshipChildField + */ + child: number + /** + * + * @type {number} + * @memberof LearningResourceRelationshipChildField + */ + position?: number + /** + * + * @type {RelationTypeEnum} + * @memberof LearningResourceRelationshipChildField + */ + relation_type?: RelationTypeEnum + /** + * + * @type {string} + * @memberof LearningResourceRelationshipChildField + */ + title: string + /** + * + * @type {string} + * @memberof LearningResourceRelationshipChildField + */ + readable_id: string +} + /** * Serializer for the LearningResourceRun model * @export @@ -3758,6 +3808,12 @@ export interface PodcastEpisodeResource { * @memberof PodcastEpisodeResource */ pace: Array + /** + * + * @type {LearningResourceRelationshipChildField} + * @memberof PodcastEpisodeResource + */ + children: LearningResourceRelationshipChildField | null /** * * @type {PodcastEpisodeResourceResourceTypeEnum} @@ -4059,6 +4115,12 @@ export interface PodcastResource { * @memberof PodcastResource */ pace: Array + /** + * + * @type {LearningResourceRelationshipChildField} + * @memberof PodcastResource + */ + children: LearningResourceRelationshipChildField | null /** * * @type {PodcastResourceResourceTypeEnum} @@ -4749,6 +4811,12 @@ export interface ProgramResource { * @memberof ProgramResource */ pace: Array + /** + * + * @type {LearningResourceRelationshipChildField} + * @memberof ProgramResource + */ + children: LearningResourceRelationshipChildField | null /** * * @type {ProgramResourceResourceTypeEnum} @@ -4918,6 +4986,41 @@ export const ProgramResourceResourceTypeEnum = { export type ProgramResourceResourceTypeEnum = (typeof ProgramResourceResourceTypeEnum)[keyof typeof ProgramResourceResourceTypeEnum] +/** + * * `PROGRAM_COURSES` - Program Courses * `LEARNING_PATH_ITEMS` - Learning Path Items * `PODCAST_EPISODES` - Podcast Episodes * `PLAYLIST_VIDEOS` - Playlist Videos + * @export + * @enum {string} + */ + +export const RelationTypeEnumDescriptions = { + PROGRAM_COURSES: "Program Courses", + LEARNING_PATH_ITEMS: "Learning Path Items", + PODCAST_EPISODES: "Podcast Episodes", + PLAYLIST_VIDEOS: "Playlist Videos", +} as const + +export const RelationTypeEnum = { + /** + * Program Courses + */ + ProgramCourses: "PROGRAM_COURSES", + /** + * Learning Path Items + */ + LearningPathItems: "LEARNING_PATH_ITEMS", + /** + * Podcast Episodes + */ + PodcastEpisodes: "PODCAST_EPISODES", + /** + * Playlist Videos + */ + PlaylistVideos: "PLAYLIST_VIDEOS", +} as const + +export type RelationTypeEnum = + (typeof RelationTypeEnum)[keyof typeof RelationTypeEnum] + /** * * `news` - news * `events` - events * @export @@ -5688,6 +5791,12 @@ export interface VideoPlaylistResource { * @memberof VideoPlaylistResource */ pace: Array + /** + * + * @type {LearningResourceRelationshipChildField} + * @memberof VideoPlaylistResource + */ + children: LearningResourceRelationshipChildField | null /** * * @type {VideoPlaylistResourceResourceTypeEnum} @@ -5989,6 +6098,12 @@ export interface VideoResource { * @memberof VideoResource */ pace: Array + /** + * + * @type {LearningResourceRelationshipChildField} + * @memberof VideoResource + */ + children: LearningResourceRelationshipChildField | null /** * * @type {VideoResourceResourceTypeEnum} diff --git a/frontends/api/src/generated/v1/api.ts b/frontends/api/src/generated/v1/api.ts index 81e4a7dde6..8a95c9813b 100644 --- a/frontends/api/src/generated/v1/api.ts +++ b/frontends/api/src/generated/v1/api.ts @@ -745,6 +745,12 @@ export interface CourseResource { * @memberof CourseResource */ pace: Array + /** + * + * @type {LearningResourceRelationshipChildField} + * @memberof CourseResource + */ + children: LearningResourceRelationshipChildField | null /** * * @type {CourseResourceResourceTypeEnum} @@ -1623,6 +1629,12 @@ export interface LearningPathResource { * @memberof LearningPathResource */ pace: Array + /** + * + * @type {LearningResourceRelationshipChildField} + * @memberof LearningPathResource + */ + children: LearningResourceRelationshipChildField | null /** * * @type {LearningPathResourceResourceTypeEnum} @@ -2632,6 +2644,44 @@ export interface LearningResourceRelationship { child: number } +/** + * Serializer field for the LearningResourceRelationship model that uses the LearningResourceSerializer to serialize the child resources + * @export + * @interface LearningResourceRelationshipChildField + */ +export interface LearningResourceRelationshipChildField { + /** + * + * @type {number} + * @memberof LearningResourceRelationshipChildField + */ + child: number + /** + * + * @type {number} + * @memberof LearningResourceRelationshipChildField + */ + position?: number + /** + * + * @type {RelationTypeEnum} + * @memberof LearningResourceRelationshipChildField + */ + relation_type?: RelationTypeEnum + /** + * + * @type {string} + * @memberof LearningResourceRelationshipChildField + */ + title: string + /** + * + * @type {string} + * @memberof LearningResourceRelationshipChildField + */ + readable_id: string +} + /** * @type LearningResourceRequest * @export @@ -4877,6 +4927,12 @@ export interface PodcastEpisodeResource { * @memberof PodcastEpisodeResource */ pace: Array + /** + * + * @type {LearningResourceRelationshipChildField} + * @memberof PodcastEpisodeResource + */ + children: LearningResourceRelationshipChildField | null /** * * @type {PodcastEpisodeResourceResourceTypeEnum} @@ -5355,6 +5411,12 @@ export interface PodcastResource { * @memberof PodcastResource */ pace: Array + /** + * + * @type {LearningResourceRelationshipChildField} + * @memberof PodcastResource + */ + children: LearningResourceRelationshipChildField | null /** * * @type {PodcastResourceResourceTypeEnum} @@ -6065,6 +6127,12 @@ export interface ProgramResource { * @memberof ProgramResource */ pace: Array + /** + * + * @type {LearningResourceRelationshipChildField} + * @memberof ProgramResource + */ + children: LearningResourceRelationshipChildField | null /** * * @type {ProgramResourceResourceTypeEnum} @@ -7022,6 +7090,12 @@ export interface VideoPlaylistResource { * @memberof VideoPlaylistResource */ pace: Array + /** + * + * @type {LearningResourceRelationshipChildField} + * @memberof VideoPlaylistResource + */ + children: LearningResourceRelationshipChildField | null /** * * @type {VideoPlaylistResourceResourceTypeEnum} @@ -7488,6 +7562,12 @@ export interface VideoResource { * @memberof VideoResource */ pace: Array + /** + * + * @type {LearningResourceRelationshipChildField} + * @memberof VideoResource + */ + children: LearningResourceRelationshipChildField | null /** * * @type {VideoResourceResourceTypeEnum} diff --git a/frontends/main/src/page-components/LearningResourceDrawer/LearningResourceDrawer.test.tsx b/frontends/main/src/page-components/LearningResourceDrawer/LearningResourceDrawer.test.tsx index 1d8f5d88c1..ed3bd49039 100644 --- a/frontends/main/src/page-components/LearningResourceDrawer/LearningResourceDrawer.test.tsx +++ b/frontends/main/src/page-components/LearningResourceDrawer/LearningResourceDrawer.test.tsx @@ -263,8 +263,8 @@ describe("LearningResourceDrawer", () => { mockedUseFeatureFlagEnabled.mockReturnValue(true) const { resource } = setupApis({ resource: { - // Chat is only enabled for courses; NOT enabled here - resource_type: ResourceTypeEnum.Program, + // Chat is only enabled for courses and programs; NOT enabled here + resource_type: ResourceTypeEnum.Podcast, }, }) const { location } = renderWithProviders(, { diff --git a/frontends/main/src/page-components/LearningResourceExpanded/AiChatSyllabusSlideDown.tsx b/frontends/main/src/page-components/LearningResourceExpanded/AiChatSyllabusSlideDown.tsx index 0817f4df0d..02c3d7d275 100644 --- a/frontends/main/src/page-components/LearningResourceExpanded/AiChatSyllabusSlideDown.tsx +++ b/frontends/main/src/page-components/LearningResourceExpanded/AiChatSyllabusSlideDown.tsx @@ -111,7 +111,12 @@ const STARTERS: AiChatProps["conversationStarters"] = [ { content: "What are the prerequisites for this course?" }, { content: "How will this course be graded?" }, ] - +type ChatParams = { + collection_name: string + message: string + course_id: string + related_courses?: string[] +} export const AiChatSyllabusOpener = ({ open, className, @@ -195,11 +200,19 @@ const AiChatSyllabusSlideDown = ({ }, credentials: "include", }, - transformBody: (messages) => ({ - collection_name: "content_files", - message: messages[messages.length - 1].content, - course_id: resource.readable_id, - }), + transformBody: (messages) => { + const params: ChatParams = { + collection_name: "content_files", + message: messages[messages.length - 1].content, + course_id: resource.readable_id, + } + if (Array.isArray(resource.children)) { + params.related_courses = resource.children.map( + (child: { readable_id: string }) => child.readable_id, + ) + } + return params + }, }} /> diff --git a/frontends/main/src/page-components/LearningResourceExpanded/LearningResourceExpanded.test.tsx b/frontends/main/src/page-components/LearningResourceExpanded/LearningResourceExpanded.test.tsx index 72ddd6af2c..f0b02be60b 100644 --- a/frontends/main/src/page-components/LearningResourceExpanded/LearningResourceExpanded.test.tsx +++ b/frontends/main/src/page-components/LearningResourceExpanded/LearningResourceExpanded.test.tsx @@ -372,7 +372,10 @@ describe.each([true, false])( name: "Ask TIM about this course", }) const shouldBeVisible = - enabled && resourceType === ResourceTypeEnum.Course + enabled && + (resourceType === ResourceTypeEnum.Course || + resourceType === ResourceTypeEnum.Program) + expect(!!chatButton).toBe(shouldBeVisible) }, ) diff --git a/frontends/main/src/page-components/LearningResourceExpanded/LearningResourceExpanded.tsx b/frontends/main/src/page-components/LearningResourceExpanded/LearningResourceExpanded.tsx index 3abf192fb2..dc9757da6c 100644 --- a/frontends/main/src/page-components/LearningResourceExpanded/LearningResourceExpanded.tsx +++ b/frontends/main/src/page-components/LearningResourceExpanded/LearningResourceExpanded.tsx @@ -145,7 +145,8 @@ const LearningResourceExpanded: React.FC = ({ const chatEnabled = useFeatureFlagEnabled(FeatureFlags.LrDrawerChatbot) && - resource?.resource_type === ResourceTypeEnum.Course + (resource?.resource_type === ResourceTypeEnum.Course || + resource?.resource_type === ResourceTypeEnum.Program) useEffect(() => { // If URL indicates syllabus open, but it's not enabled, update URL diff --git a/learning_resources/serializers.py b/learning_resources/serializers.py index 7ba0c7a821..7ef54f805e 100644 --- a/learning_resources/serializers.py +++ b/learning_resources/serializers.py @@ -475,6 +475,7 @@ class LearningResourceMetadataDisplaySerializer(serializers.Serializer): languages = serializers.SerializerMethodField( help_text="Languages", allow_null=True ) + levels = serializers.SerializerMethodField(help_text="Levels", allow_null=True) departments = serializers.SerializerMethodField(help_text="Departments") platform = serializers.SerializerMethodField(help_text="Platform", allow_null=True) @@ -856,6 +857,26 @@ def render_chunks(self): ] +class LearningResourceRelationshipChildField(serializers.ModelSerializer): + """ + Serializer field for the LearningResourceRelationship model that uses + the LearningResourceSerializer to serialize the child resources + """ + + readable_id = serializers.ReadOnlyField(source="child.readable_id") + title = serializers.ReadOnlyField(source="child.title") + + class Meta: + model = models.LearningResourceRelationship + fields = ( + "child", + "position", + "relation_type", + "title", + "readable_id", + ) + + class LearningResourceBaseSerializer(serializers.ModelSerializer, WriteableTopicsMixin): """Serializer for LearningResource, minus program""" @@ -889,6 +910,13 @@ class LearningResourceBaseSerializer(serializers.ModelSerializer, WriteableTopic resource_category = serializers.SerializerMethodField() format = serializers.ListField(child=FormatSerializer(), read_only=True) pace = serializers.ListField(child=PaceSerializer(), read_only=True) + children = serializers.SerializerMethodField(allow_null=True) + + @extend_schema_field(LearningResourceRelationshipChildField(allow_null=True)) + def get_children(self, instance): + return LearningResourceRelationshipChildField( + instance.children.all(), many=True, read_only=True + ).data def get_resource_category(self, instance) -> str: """Return the resource category of the resource""" @@ -940,6 +968,7 @@ class Meta: "resource_prices", "resource_category", "certification", + "children", "certification_type", "professional", "views", @@ -969,21 +998,6 @@ class CourseResourceSerializer(LearningResourceBaseSerializer): course = CourseSerializer(read_only=True) -class LearningResourceRelationshipChildField(serializers.ModelSerializer): - """ - Serializer field for the LearningResourceRelationship model that uses - the LearningResourceSerializer to serialize the child resources - """ - - def to_representation(self, instance): - """Serializes child as a LearningResource""" # noqa: D401 - return LearningResourceSerializer(instance=instance.child).data - - class Meta: - model = models.LearningResourceRelationship - exclude = ("parent", *COMMON_IGNORED_FIELDS) - - class LearningPathResourceSerializer(LearningResourceBaseSerializer): """CRUD serializer for LearningPath resources""" diff --git a/learning_resources/serializers_test.py b/learning_resources/serializers_test.py index 965ff8f3e4..06c306add1 100644 --- a/learning_resources/serializers_test.py +++ b/learning_resources/serializers_test.py @@ -218,6 +218,9 @@ def test_learning_resource_serializer( # noqa: PLR0913 "languages": resource.languages, "last_modified": drf_datetime(resource.last_modified), "learning_path_parents": [], + "children": serializers.LearningResourceRelationshipChildField( + resource.children.all(), many=True + ).data, "offered_by": serializers.LearningResourceOfferorSerializer( instance=resource.offered_by ).data, diff --git a/learning_resources/views_test.py b/learning_resources/views_test.py index a619011d22..ff9fe84431 100644 --- a/learning_resources/views_test.py +++ b/learning_resources/views_test.py @@ -169,7 +169,7 @@ def test_program_endpoint(client, url, params): def test_program_detail_endpoint(client, django_assert_num_queries, url): """Test program endpoint""" program = ProgramFactory.create() - with django_assert_num_queries(17): + with django_assert_num_queries(18 + program.learning_resource.children.count()): resp = client.get(reverse(url, args=[program.learning_resource.id])) assert resp.data.get("title") == program.learning_resource.title assert resp.data.get("resource_type") == LearningResourceType.program.name diff --git a/openapi/specs/v0.yaml b/openapi/specs/v0.yaml index 8e549aa2f4..dab79b1502 100644 --- a/openapi/specs/v0.yaml +++ b/openapi/specs/v0.yaml @@ -2067,6 +2067,11 @@ components: - code - name readOnly: true + children: + allOf: + - $ref: '#/components/schemas/LearningResourceRelationshipChildField' + nullable: true + readOnly: true resource_type: allOf: - $ref: '#/components/schemas/CourseResourceResourceTypeEnum' @@ -2166,6 +2171,7 @@ components: required: - certification - certification_type + - children - course - course_feature - delivery @@ -2708,6 +2714,11 @@ components: - code - name readOnly: true + children: + allOf: + - $ref: '#/components/schemas/LearningResourceRelationshipChildField' + nullable: true + readOnly: true resource_type: allOf: - $ref: '#/components/schemas/LearningPathResourceResourceTypeEnum' @@ -2806,6 +2817,7 @@ components: required: - certification - certification_type + - children - course_feature - delivery - departments @@ -3097,6 +3109,30 @@ components: required: - amount - currency + LearningResourceRelationshipChildField: + type: object + description: |- + Serializer field for the LearningResourceRelationship model that uses + the LearningResourceSerializer to serialize the child resources + properties: + child: + type: integer + position: + type: integer + maximum: 2147483647 + minimum: 0 + relation_type: + $ref: '#/components/schemas/RelationTypeEnum' + title: + type: string + readOnly: true + readable_id: + type: string + readOnly: true + required: + - child + - readable_id + - title LearningResourceRun: type: object description: Serializer for the LearningResourceRun model @@ -4001,6 +4037,11 @@ components: - code - name readOnly: true + children: + allOf: + - $ref: '#/components/schemas/LearningResourceRelationshipChildField' + nullable: true + readOnly: true resource_type: allOf: - $ref: '#/components/schemas/PodcastEpisodeResourceResourceTypeEnum' @@ -4100,6 +4141,7 @@ components: required: - certification - certification_type + - children - course_feature - delivery - departments @@ -4273,6 +4315,11 @@ components: - code - name readOnly: true + children: + allOf: + - $ref: '#/components/schemas/LearningResourceRelationshipChildField' + nullable: true + readOnly: true resource_type: allOf: - $ref: '#/components/schemas/PodcastResourceResourceTypeEnum' @@ -4372,6 +4419,7 @@ components: required: - certification - certification_type + - children - course_feature - delivery - departments @@ -4814,6 +4862,11 @@ components: - code - name readOnly: true + children: + allOf: + - $ref: '#/components/schemas/LearningResourceRelationshipChildField' + nullable: true + readOnly: true resource_type: allOf: - $ref: '#/components/schemas/ProgramResourceResourceTypeEnum' @@ -4913,6 +4966,7 @@ components: required: - certification - certification_type + - children - course_feature - delivery - departments @@ -4940,6 +4994,23 @@ components: type: string enum: - program + RelationTypeEnum: + enum: + - PROGRAM_COURSES + - LEARNING_PATH_ITEMS + - PODCAST_EPISODES + - PLAYLIST_VIDEOS + type: string + description: |- + * `PROGRAM_COURSES` - Program Courses + * `LEARNING_PATH_ITEMS` - Learning Path Items + * `PODCAST_EPISODES` - Podcast Episodes + * `PLAYLIST_VIDEOS` - Playlist Videos + x-enum-descriptions: + - Program Courses + - Learning Path Items + - Podcast Episodes + - Playlist Videos ResourceTypeEnum: enum: - news @@ -5497,6 +5568,11 @@ components: - code - name readOnly: true + children: + allOf: + - $ref: '#/components/schemas/LearningResourceRelationshipChildField' + nullable: true + readOnly: true resource_type: allOf: - $ref: '#/components/schemas/VideoPlaylistResourceResourceTypeEnum' @@ -5596,6 +5672,7 @@ components: required: - certification - certification_type + - children - course_feature - delivery - departments @@ -5769,6 +5846,11 @@ components: - code - name readOnly: true + children: + allOf: + - $ref: '#/components/schemas/LearningResourceRelationshipChildField' + nullable: true + readOnly: true resource_type: allOf: - $ref: '#/components/schemas/VideoResourceResourceTypeEnum' @@ -5874,6 +5956,7 @@ components: required: - certification - certification_type + - children - course_feature - delivery - departments diff --git a/openapi/specs/v1.yaml b/openapi/specs/v1.yaml index fa250ae005..1cd9bee89c 100644 --- a/openapi/specs/v1.yaml +++ b/openapi/specs/v1.yaml @@ -9334,6 +9334,11 @@ components: - code - name readOnly: true + children: + allOf: + - $ref: '#/components/schemas/LearningResourceRelationshipChildField' + nullable: true + readOnly: true resource_type: allOf: - $ref: '#/components/schemas/CourseResourceResourceTypeEnum' @@ -9433,6 +9438,7 @@ components: required: - certification - certification_type + - children - course - course_feature - delivery @@ -9892,6 +9898,11 @@ components: - code - name readOnly: true + children: + allOf: + - $ref: '#/components/schemas/LearningResourceRelationshipChildField' + nullable: true + readOnly: true resource_type: allOf: - $ref: '#/components/schemas/LearningPathResourceResourceTypeEnum' @@ -9990,6 +10001,7 @@ components: required: - certification - certification_type + - children - course_feature - delivery - departments @@ -10664,6 +10676,30 @@ components: - id - parent - resource + LearningResourceRelationshipChildField: + type: object + description: |- + Serializer field for the LearningResourceRelationship model that uses + the LearningResourceSerializer to serialize the child resources + properties: + child: + type: integer + position: + type: integer + maximum: 2147483647 + minimum: 0 + relation_type: + $ref: '#/components/schemas/RelationTypeEnum' + title: + type: string + readOnly: true + readable_id: + type: string + readOnly: true + required: + - child + - readable_id + - title LearningResourceRequest: oneOf: - $ref: '#/components/schemas/ProgramResourceRequest' @@ -12410,6 +12446,11 @@ components: - code - name readOnly: true + children: + allOf: + - $ref: '#/components/schemas/LearningResourceRelationshipChildField' + nullable: true + readOnly: true resource_type: allOf: - $ref: '#/components/schemas/PodcastEpisodeResourceResourceTypeEnum' @@ -12509,6 +12550,7 @@ components: required: - certification - certification_type + - children - course_feature - delivery - departments @@ -12808,6 +12850,11 @@ components: - code - name readOnly: true + children: + allOf: + - $ref: '#/components/schemas/LearningResourceRelationshipChildField' + nullable: true + readOnly: true resource_type: allOf: - $ref: '#/components/schemas/PodcastResourceResourceTypeEnum' @@ -12907,6 +12954,7 @@ components: required: - certification - certification_type + - children - course_feature - delivery - departments @@ -13344,6 +13392,11 @@ components: - code - name readOnly: true + children: + allOf: + - $ref: '#/components/schemas/LearningResourceRelationshipChildField' + nullable: true + readOnly: true resource_type: allOf: - $ref: '#/components/schemas/ProgramResourceResourceTypeEnum' @@ -13443,6 +13496,7 @@ components: required: - certification - certification_type + - children - course_feature - delivery - departments @@ -14014,6 +14068,11 @@ components: - code - name readOnly: true + children: + allOf: + - $ref: '#/components/schemas/LearningResourceRelationshipChildField' + nullable: true + readOnly: true resource_type: allOf: - $ref: '#/components/schemas/VideoPlaylistResourceResourceTypeEnum' @@ -14113,6 +14172,7 @@ components: required: - certification - certification_type + - children - course_feature - delivery - departments @@ -14398,6 +14458,11 @@ components: - code - name readOnly: true + children: + allOf: + - $ref: '#/components/schemas/LearningResourceRelationshipChildField' + nullable: true + readOnly: true resource_type: allOf: - $ref: '#/components/schemas/VideoResourceResourceTypeEnum' @@ -14503,6 +14568,7 @@ components: required: - certification - certification_type + - children - course_feature - delivery - departments