From 3d10cea1372bd3b47beb2c67898c7d326aa94095 Mon Sep 17 00:00:00 2001 From: Zainab Amir Date: Mon, 27 Feb 2023 20:30:41 +0500 Subject: [PATCH] feat: implement fallback recommendations (#758) --- src/config/index.js | 1 + src/recommendations/RecommendationCard.jsx | 2 + src/recommendations/RecommendationsList.jsx | 1 + src/recommendations/RecommendationsPage.jsx | 22 +++-- .../tests/RecommendationsPage.test.jsx | 14 ++- src/recommendations/tests/mockedData.js | 85 ++++++++++++++++++- src/recommendations/track.js | 3 +- 7 files changed, 118 insertions(+), 10 deletions(-) diff --git a/src/config/index.js b/src/config/index.js index 75841cec..200dceea 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -20,6 +20,7 @@ const configuration = { TOS_AND_HONOR_CODE: process.env.TOS_AND_HONOR_CODE || null, TOS_LINK: process.env.TOS_LINK || null, // Miscellaneous + GENERAL_RECOMMENDATIONS: process.env.GENERAL_RECOMMENDATIONS || [], INFO_EMAIL: process.env.INFO_EMAIL || '', }; diff --git a/src/recommendations/RecommendationCard.jsx b/src/recommendations/RecommendationCard.jsx index 0c89ad49..f546e9c2 100644 --- a/src/recommendations/RecommendationCard.jsx +++ b/src/recommendations/RecommendationCard.jsx @@ -29,6 +29,7 @@ const RecommendationCard = (props) => { position + 1, userId, recommendation.marketingUrl, + recommendation.recommendationType || 'algolia', ); }; @@ -71,6 +72,7 @@ RecommendationCard.propTypes = { logoImageUrl: PropTypes.string.isRequired, })), marketingUrl: PropTypes.string.isRequired, + recommendationType: PropTypes.string, }).isRequired, position: PropTypes.number.isRequired, userId: PropTypes.number, diff --git a/src/recommendations/RecommendationsList.jsx b/src/recommendations/RecommendationsList.jsx index 46ab7ed9..ecd20fcc 100644 --- a/src/recommendations/RecommendationsList.jsx +++ b/src/recommendations/RecommendationsList.jsx @@ -43,6 +43,7 @@ RecommendationsList.propTypes = { logoImageUrl: PropTypes.string.isRequired, })), marketingUrl: PropTypes.string.isRequired, + recommendationType: PropTypes.string, })), userId: PropTypes.number, }; diff --git a/src/recommendations/RecommendationsPage.jsx b/src/recommendations/RecommendationsPage.jsx index 5448ec59..a473dc86 100644 --- a/src/recommendations/RecommendationsPage.jsx +++ b/src/recommendations/RecommendationsPage.jsx @@ -29,14 +29,26 @@ const RecommendationsPage = (props) => { if (registrationResponse) { let coursesWithKeys = []; getPersonalizedRecommendations(educationLevel).then((response) => { - if (response.length) { - coursesWithKeys = response.map(course => ({ - ...course, - courseKey: convertCourseRunKeytoCourseKey(course.activeRunKey), - })); + coursesWithKeys = response.map(course => ({ + ...course, + courseKey: convertCourseRunKeytoCourseKey(course.activeRunKey), + })); + + if (coursesWithKeys.length >= RECOMMENDATIONS_COUNT) { setRecommendations(coursesWithKeys.slice(0, RECOMMENDATIONS_COUNT)); + } else { + const courseRecommendations = coursesWithKeys.concat(getConfig().GENERAL_RECOMMENDATIONS); + // Remove duplicate recommendations + const uniqueRecommendations = courseRecommendations.filter( + (recommendation, index, self) => index === self.findIndex((existingRecommendation) => ( + existingRecommendation.courseKey === recommendation.courseKey + )), + ); + setRecommendations(uniqueRecommendations.slice(0, RECOMMENDATIONS_COUNT)); } + setIsLoading(false); + // We only want to track the recommendations returned by Algolia const courseKeys = coursesWithKeys.map(course => course.courseKey); trackRecommendationsViewed(courseKeys, false, userId); }) diff --git a/src/recommendations/tests/RecommendationsPage.test.jsx b/src/recommendations/tests/RecommendationsPage.test.jsx index 92c91c9a..1842a951 100644 --- a/src/recommendations/tests/RecommendationsPage.test.jsx +++ b/src/recommendations/tests/RecommendationsPage.test.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { Provider } from 'react-redux'; -import { getConfig } from '@edx/frontend-platform'; +import { getConfig, mergeConfig } from '@edx/frontend-platform'; import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { mount } from 'enzyme'; import { act } from 'react-dom/test-utils'; @@ -10,7 +10,7 @@ import configureStore from 'redux-mock-store'; import { DEFAULT_REDIRECT_URL } from '../../data/constants'; import * as getPersonalizedRecommendations from '../data/service'; import RecommendationsPage from '../RecommendationsPage'; -import mockedResponse from './mockedData'; +import { mockedGeneralRecommendations, mockedResponse } from './mockedData'; const IntlRecommendationsPage = injectIntl(RecommendationsPage); const mockStore = configureStore(); @@ -100,6 +100,16 @@ describe('RecommendationsPageTests', () => { expect(window.location.href).toEqual(registrationResult.redirectUrl); }); + it('should not redirect if fallback recommendations are enabled', async () => { + mergeConfig({ + GENERAL_RECOMMENDATIONS: mockedGeneralRecommendations, + }); + getPersonalizedRecommendations.default = jest.fn().mockImplementation(() => Promise.resolve([])); + const recommendationsPage = await getRecommendationsPage(); + + expect(recommendationsPage.find('#course-recommendations').exists()).toBeTruthy(); + }); + it('should display all owners for a course', async () => { getPersonalizedRecommendations.default = jest.fn().mockImplementation(() => Promise.resolve(mockedResponse)); const recommendationsPage = await getRecommendationsPage(); diff --git a/src/recommendations/tests/mockedData.js b/src/recommendations/tests/mockedData.js index 0ab9c16d..e3407da4 100644 --- a/src/recommendations/tests/mockedData.js +++ b/src/recommendations/tests/mockedData.js @@ -1,4 +1,4 @@ -const mockedResponse = [ +export const mockedResponse = [ { title: 'How to Learn Online 1', marketingUrl: 'https://test-recommendations.com/course/how-to-learn-online-1', @@ -76,4 +76,85 @@ const mockedResponse = [ }, ]; -export default mockedResponse; +export const mockedGeneralRecommendations = [ + { + courseKey: 'MITx+6.00.1x', + activeRunKey: 'course-v1:MITx+6.00.1x+1T2023', + cardImageUrl: 'https://prod-discovery.edx-cdn.org/media/course/image/956319ec-8665-4039-8bc6-32c9a9aea5e9-885268c71902.jpg', + marketingUrl: 'https://www.edx.org/course/introduction-to-computer-science-and-programming-7', + objectId: 'course-956319ec-8665-4039-8bc6-32c9a9aea5e9', + owners: [ + { + key: 'MITx', + logoImageUrl: 'https://prod-discovery.edx-cdn.org/organization/logos/2a73d2ce-c34a-4e08-8223-83bca9d2f01d-2cc8854c6fee.png', + name: 'Massachusetts Institute of Technology', + }, + ], + title: 'Introduction to Computer Science and Programming Using Python', + recommendationType: 'general', + }, + { + courseKey: 'IBM+PY0101EN', + activeRunKey: 'course-v1:IBM+PY0101EN+2T2021', + cardImageUrl: 'https://prod-discovery.edx-cdn.org/media/course/image/381a0046-5d78-4790-8776-74620d59f48e-e2e7f4677ce2.jpeg', + marketingUrl: 'https://www.edx.org/course/python-basics-for-data-science', + objectID: 'course-381a0046-5d78-4790-8776-74620d59f48e', + owners: [ + { + key: 'IBM', + logoImageUrl: 'https://prod-discovery.edx-cdn.org/organization/logos/87b07564-d569-4cfd-bee6-8b0a407acb73-dc33e4b5f353.png', + name: 'IBM', + }, + ], + title: 'Python Basics for Data Science', + recommendationType: 'general', + }, + { + courseKey: 'HarvardX+CS50P', + activeRunKey: 'course-v1:HarvardX+CS50P+Python', + cardImageUrl: 'https://prod-discovery.edx-cdn.org/media/course/image/2cc794d0-316d-42f7-bbfd-25c34e4cd5df-033e46d516c0.png', + marketingUrl: 'https://www.edx.org/course/cs50s-introduction-to-programming-with-python', + objectID: 'course-2cc794d0-316d-42f7-bbfd-25c34e4cd5df', + owners: [ + { + key: 'HarvardX', + logoImageUrl: 'https://prod-discovery.edx-cdn.org/organization/logos/44022f13-20df-4666-9111-cede3e5dc5b6-2cc39992c67a.png', + name: 'Harvard University', + }, + ], + title: 'CS50\'s Introduction to Programming with Python', + recommendationType: 'general', + }, + { + courseKey: 'UQx+IELTSx', + activeRunKey: 'course-v1:UQx+IELTSx+1T2022', + cardImageUrl: 'https://prod-discovery.edx-cdn.org/media/course/image/d61d7a1f-3333-4169-a786-92e2bf690c6f-fa8a6909baec.jpg', + marketingUrl: 'https://www.edx.org/course/ielts-academic-test-preparation', + objectID: 'course-d61d7a1f-3333-4169-a786-92e2bf690c6f', + owners: [ + { + key: 'UQx', + logoImageUrl: 'https://prod-discovery.edx-cdn.org/organization/logos/8554749f-b920-4d7f-8986-af6bb95290aa-f336c6a2ca11.png', + name: 'The University of Queensland', + }, + ], + title: 'IELTS Academic Test Preparation', + recommendationType: 'general', + }, + { + courseKey: 'HarvardX+CS50x', + activeRunKey: 'course-v1:HarvardX+CS50+X', + cardImageUrl: 'https://prod-discovery.edx-cdn.org/media/course/image/da1b2400-322b-459b-97b0-0c557f05d017-a3d1899c3344.png', + marketingUrl: 'https://www.edx.org/course/introduction-computer-science-harvardx-cs50x', + objectID: 'course-da1b2400-322b-459b-97b0-0c557f05d017', + owners: [ + { + key: 'HarvardX', + logoImageUrl: 'https://prod-discovery.edx-cdn.org/organization/logos/44022f13-20df-4666-9111-cede3e5dc5b6-2cc39992c67a.png', + name: 'Harvard University', + }, + ], + title: 'CS50\'s Introduction to Computer Science', + recommendationType: 'general', + }, +]; diff --git a/src/recommendations/track.js b/src/recommendations/track.js index 48bbf470..3bdcf5c1 100644 --- a/src/recommendations/track.js +++ b/src/recommendations/track.js @@ -16,11 +16,12 @@ export const createLinkTracker = (tracker, href, openInNewTab = false) => (e) => return setTimeout(() => { global.location.href = href; }, LINK_TIMEOUT); }; -export const trackRecommendationsClicked = (courseKey, isControl, position, userId, href) => { +export const trackRecommendationsClicked = (courseKey, isControl, position, userId, href, recommendationType) => { createLinkTracker( sendTrackEvent(eventNames.recommendedCourseClicked, { page: 'authn_recommendations', position, + recommendation_type: recommendationType, course_key: courseKey, is_control: isControl, user_id: userId,