From 1db2162b14100f5f11222a1f6b86c1d75aceadd4 Mon Sep 17 00:00:00 2001 From: qasimgulzar Date: Thu, 8 Aug 2024 23:19:33 +0500 Subject: [PATCH 1/5] feat: integrate openedx search api with existing CoursewareSearch.jsx --- public/index.html | 2 ++ .../courseware-search/CoursewareSearch.jsx | 23 ++++++++---- .../courseware-search/map-search-response.js | 18 +++++----- src/course-home/data/api.js | 24 +++++++++++-- src/course-home/data/thunks.js | 35 ++++++++++++++++--- src/generic/model-store/index.js | 1 + src/generic/model-store/slice.js | 5 +++ 7 files changed, 85 insertions(+), 23 deletions(-) diff --git a/public/index.html b/public/index.html index ab09aacd2b..0776d43a76 100644 --- a/public/index.html +++ b/public/index.html @@ -9,6 +9,8 @@ <% if (htmlWebpackPlugin.options.OPTIMIZELY_PROJECT_ID) { %> <% } %> +
diff --git a/src/course-home/courseware-search/CoursewareSearch.jsx b/src/course-home/courseware-search/CoursewareSearch.jsx index 4050aebf7c..9a72b884fd 100644 --- a/src/course-home/courseware-search/CoursewareSearch.jsx +++ b/src/course-home/courseware-search/CoursewareSearch.jsx @@ -1,6 +1,6 @@ import React, { useEffect } from 'react'; import { useParams } from 'react-router'; -import { useDispatch } from 'react-redux'; +import { connect, useDispatch } from 'react-redux'; import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { @@ -16,9 +16,9 @@ import messages from './messages'; import CoursewareSearchForm from './CoursewareSearchForm'; import CoursewareSearchResultsFilterContainer from './CoursewareResultsFilter'; import { updateModel, useModel } from '../../generic/model-store'; -import { searchCourseContent } from '../data/thunks'; +import { searchCourseContent, generateSearchEngineAuthToken } from '../data/thunks'; -const CoursewareSearch = ({ intl, ...sectionProps }) => { +const CoursewareSearch = ({ searchEngineConfig, intl, ...sectionProps }) => { const { courseId } = useParams(); const { query: searchKeyword, setQuery, clearSearchParams } = useCoursewareSearchParams(); const dispatch = useDispatch(); @@ -62,14 +62,20 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => { keyword: value, }); - dispatch(searchCourseContent(courseId, value)); + dispatch(searchCourseContent(courseId, value, searchEngineConfig)); setQuery(value); }; useEffect(() => { - handleSubmit(searchKeyword); + dispatch(generateSearchEngineAuthToken()); }, []); + useEffect(() => { + if (searchEngineConfig) { + handleSubmit(searchKeyword); + } + }, [searchEngineConfig]); + const handleOnChange = (value) => { if (value === searchKeyword) { return; } if (!value) { clearSearch(); } @@ -143,6 +149,11 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => { CoursewareSearch.propTypes = { intl: intlShape.isRequired, + searchEngineConfig: injectIntl.isRequired, }; -export default injectIntl(CoursewareSearch); +function mapStateToProps(state) { + const { searchEngineConfig } = state.models; + return { searchEngineConfig }; +} +export default injectIntl(connect(mapStateToProps)(CoursewareSearch)); diff --git a/src/course-home/courseware-search/map-search-response.js b/src/course-home/courseware-search/map-search-response.js index 49953b3f6b..b6ed4b9baa 100644 --- a/src/course-home/courseware-search/map-search-response.js +++ b/src/course-home/courseware-search/map-search-response.js @@ -39,17 +39,15 @@ export default function mapSearchResponse(response, searchKeywords = '') { const results = rawResults.map(result => { const { score, - data: { - id, - content: { - displayName, - htmlContent, - transcriptEn, - }, - contentType, - location, - url, + id, + content: { + displayName, + htmlContent, + transcriptEn, }, + contentType, + location, + url, } = result; const type = contentType?.toLowerCase() || defaultType; diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js index 22b6738df4..b232f9aa4a 100644 --- a/src/course-home/data/api.js +++ b/src/course-home/data/api.js @@ -1,6 +1,6 @@ import { camelCaseObject, getConfig } from '@edx/frontend-platform'; -import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { logInfo } from '@edx/frontend-platform/logging'; +import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth'; +import { logError, logInfo } from '@edx/frontend-platform/logging'; import { appendBrowserTimezoneToUrl } from '../../utils'; const calculateAssignmentTypeGrades = (points, assignmentWeight, numDroppable) => { @@ -465,3 +465,23 @@ export async function searchCourseContentFromAPI(courseId, searchKeyword, option return camelCaseObject(response); } + +export async function getSearchEngineAuthToken() { + const authenticatedUser = getAuthenticatedUser(); + const url = new URL(`${getConfig().LMS_BASE_URL}/api/search/token/`); + + if (authenticatedUser) { + try { + const { data } = await getAuthenticatedHttpClient().get(url.href, {}); + return data; + } catch (e) { + const { customAttributes: { httpErrorStatus } } = e; + if (httpErrorStatus === 404) { + logInfo(`${e}. This probably happened because the search plugin is not installed on platform.`); + } else { + logError(e); + } + } + } + return null; +} diff --git a/src/course-home/data/thunks.js b/src/course-home/data/thunks.js index ec0567f9cf..90b9c9558b 100644 --- a/src/course-home/data/thunks.js +++ b/src/course-home/data/thunks.js @@ -13,11 +13,13 @@ import { postRequestCert, getLiveTabIframe, getCoursewareSearchEnabledFlag, - searchCourseContentFromAPI, + getSearchEngineAuthToken, } from './api'; import { - addModel, updateModel, + addModel, + updateModel, + setSearchEngineAuthToken, } from '../../generic/model-store'; import { @@ -166,10 +168,13 @@ export async function fetchCoursewareSearchSettings(courseId) { } } -export function searchCourseContent(courseId, searchKeyword) { +export function searchCourseContent(courseId, searchKeyword, config) { return async (dispatch) => { const start = new Date(); + // eslint-disable-next-line no-undef + const searchEngine = new SearchEngine(config.searchEngine, config, 'courseware_course_structure'); + dispatch(addModel({ modelType: 'contentSearchResults', model: { @@ -185,8 +190,21 @@ export function searchCourseContent(courseId, searchKeyword) { let curatedResponse; let errors; try { - ({ data } = await searchCourseContentFromAPI(courseId, searchKeyword)); - curatedResponse = mapSearchResponse(data, searchKeyword); + data = await searchEngine.search(searchKeyword, {}); + curatedResponse = mapSearchResponse({ + ...data, + results: data.results.map(hit => ({ + id: hit.item_id, + location: [hit.usage_key], + url: 'http://localhost:8080', + contentType: hit.content_type, + content: { + displayName: hit?.content?.display_name, + htmlContent: hit?.content?.display_name, + transcriptEn: hit?.content?.display_name, + }, + })), + }, searchKeyword); } catch (e) { // TODO: Remove when publishing to prod. Just temporary for performance debugging. // eslint-disable-next-line no-console @@ -223,3 +241,10 @@ export function searchCourseContent(courseId, searchKeyword) { }); }; } + +export function generateSearchEngineAuthToken() { + return async (dispatch) => { + const response = await getSearchEngineAuthToken(); + dispatch(setSearchEngineAuthToken(camelCaseObject(response))); + }; +} diff --git a/src/generic/model-store/index.js b/src/generic/model-store/index.js index 954f5f3448..af1b745921 100644 --- a/src/generic/model-store/index.js +++ b/src/generic/model-store/index.js @@ -8,6 +8,7 @@ export { updateModelsMap, removeModel, removeModels, + setSearchEngineAuthToken, } from './slice'; export { diff --git a/src/generic/model-store/slice.js b/src/generic/model-store/slice.js index bc6dda3d6a..7f13207b6e 100644 --- a/src/generic/model-store/slice.js +++ b/src/generic/model-store/slice.js @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign */ import { createSlice } from '@reduxjs/toolkit'; +import { camelCaseObject } from '@edx/frontend-platform'; function add(state, modelType, model, idField) { idField = idField ?? 'id'; @@ -63,6 +64,9 @@ const slice = createSlice({ const { modelType, ids } = payload; ids.forEach(id => remove(state, modelType, id)); }, + setSearchEngineAuthToken: (state, { payload }) => { + state.searchEngineConfig = camelCaseObject(payload); + }, }, }); @@ -75,6 +79,7 @@ export const { updateModelsMap, removeModel, removeModels, + setSearchEngineAuthToken, } = slice.actions; export const { reducer } = slice; From b72c4fe052b6cd120903258ee761fe906cfdb9b9 Mon Sep 17 00:00:00 2001 From: qasimgulzar Date: Thu, 8 Aug 2024 23:27:16 +0500 Subject: [PATCH 2/5] fix: format correct url --- src/course-home/data/thunks.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/course-home/data/thunks.js b/src/course-home/data/thunks.js index 90b9c9558b..c5573834a9 100644 --- a/src/course-home/data/thunks.js +++ b/src/course-home/data/thunks.js @@ -1,5 +1,5 @@ import { logError } from '@edx/frontend-platform/logging'; -import { camelCaseObject } from '@edx/frontend-platform'; +import { camelCaseObject, getConfig } from '@edx/frontend-platform'; import { executePostFromPostEvent, getCourseHomeCourseMetadata, @@ -196,7 +196,7 @@ export function searchCourseContent(courseId, searchKeyword, config) { results: data.results.map(hit => ({ id: hit.item_id, location: [hit.usage_key], - url: 'http://localhost:8080', + url: `${getConfig().LMS_BASE_URL}/courses/${courseId}/jump_to/${hit.usage_key}`, contentType: hit.content_type, content: { displayName: hit?.content?.display_name, From 774ce341cbd69e9f754a9c3d3ece6d87b0aed2a2 Mon Sep 17 00:00:00 2001 From: qasimgulzar Date: Tue, 13 Aug 2024 13:17:33 +0500 Subject: [PATCH 3/5] fix: add search_library as external script. --- public/index.html | 2 -- src/course-home/data/thunks.js | 1 + webpack.dev.config.js | 9 ++++++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/public/index.html b/public/index.html index 0776d43a76..ab09aacd2b 100644 --- a/public/index.html +++ b/public/index.html @@ -9,8 +9,6 @@ <% if (htmlWebpackPlugin.options.OPTIMIZELY_PROJECT_ID) { %> <% } %> -
diff --git a/src/course-home/data/thunks.js b/src/course-home/data/thunks.js index c5573834a9..b237739436 100644 --- a/src/course-home/data/thunks.js +++ b/src/course-home/data/thunks.js @@ -1,5 +1,6 @@ import { logError } from '@edx/frontend-platform/logging'; import { camelCaseObject, getConfig } from '@edx/frontend-platform'; +import 'search_library'; // eslint-disable-line import/no-unresolved import { executePostFromPostEvent, getCourseHomeCourseMetadata, diff --git a/webpack.dev.config.js b/webpack.dev.config.js index ddf63def17..919ab4ee12 100644 --- a/webpack.dev.config.js +++ b/webpack.dev.config.js @@ -7,5 +7,12 @@ config.resolve.alias = { ...config.resolve.alias, '@src': path.resolve(__dirname, 'src'), }; - +config.externalsType = 'script'; +config.externals = { + search_library: [ + `${process.env.LMS_BASE_URL}/static/django_search_backend/js/search_library.js`, + 'SearchEngine', + 'SearchEngine', + ], +}; module.exports = config; From 52368c79f031c040af40346a56ccc2453dc2f801 Mon Sep 17 00:00:00 2001 From: qasimgulzar Date: Tue, 13 Aug 2024 20:31:39 +0500 Subject: [PATCH 4/5] fix: added location info to index --- src/course-home/data/thunks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/course-home/data/thunks.js b/src/course-home/data/thunks.js index b237739436..84acf529d2 100644 --- a/src/course-home/data/thunks.js +++ b/src/course-home/data/thunks.js @@ -196,7 +196,7 @@ export function searchCourseContent(courseId, searchKeyword, config) { ...data, results: data.results.map(hit => ({ id: hit.item_id, - location: [hit.usage_key], + location: hit.location || [], url: `${getConfig().LMS_BASE_URL}/courses/${courseId}/jump_to/${hit.usage_key}`, contentType: hit.content_type, content: { From 6845cd1b9e42cdbfefabf4ffd05cabaf54339d61 Mon Sep 17 00:00:00 2001 From: qasimgulzar Date: Wed, 2 Oct 2024 13:55:21 +0500 Subject: [PATCH 5/5] fix: install search client as package. --- package-lock.json | 6 ++++++ package.json | 1 + src/course-home/data/thunks.js | 3 +-- webpack.dev.config.js | 8 +------- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2a16d33469..7c1d34967c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "lodash.camelcase": "4.3.0", "patch-package": "^8.0.0", "postcss-loader": "^8.1.1", + "openedx-search-api": "github:qasimgulzar/openedx-search-api#main", "prop-types": "15.8.1", "query-string": "^7.1.3", "react": "17.0.2", @@ -15438,6 +15439,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openedx-search-api": { + "version": "1.0.0", + "resolved": "git+ssh://git@github.com/qasimgulzar/openedx-search-api.git#1c3ba4308a3d0c77ee5a6511be601e5b26eae166", + "license": "ISC" + }, "node_modules/opener": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", diff --git a/package.json b/package.json index 799d0e2ba5..0b917936d5 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "lodash.camelcase": "4.3.0", "patch-package": "^8.0.0", "postcss-loader": "^8.1.1", + "openedx-search-api": "github:qasimgulzar/openedx-search-api#main", "prop-types": "15.8.1", "query-string": "^7.1.3", "react": "17.0.2", diff --git a/src/course-home/data/thunks.js b/src/course-home/data/thunks.js index 84acf529d2..20d867cb43 100644 --- a/src/course-home/data/thunks.js +++ b/src/course-home/data/thunks.js @@ -1,6 +1,6 @@ import { logError } from '@edx/frontend-platform/logging'; import { camelCaseObject, getConfig } from '@edx/frontend-platform'; -import 'search_library'; // eslint-disable-line import/no-unresolved +import SearchEngine from 'openedx-search-api'; import { executePostFromPostEvent, getCourseHomeCourseMetadata, @@ -173,7 +173,6 @@ export function searchCourseContent(courseId, searchKeyword, config) { return async (dispatch) => { const start = new Date(); - // eslint-disable-next-line no-undef const searchEngine = new SearchEngine(config.searchEngine, config, 'courseware_course_structure'); dispatch(addModel({ diff --git a/webpack.dev.config.js b/webpack.dev.config.js index 919ab4ee12..4c77ed2d79 100644 --- a/webpack.dev.config.js +++ b/webpack.dev.config.js @@ -8,11 +8,5 @@ config.resolve.alias = { '@src': path.resolve(__dirname, 'src'), }; config.externalsType = 'script'; -config.externals = { - search_library: [ - `${process.env.LMS_BASE_URL}/static/django_search_backend/js/search_library.js`, - 'SearchEngine', - 'SearchEngine', - ], -}; + module.exports = config;