From cec3b0011b88154d5d821fcbc04b7836e55e5c50 Mon Sep 17 00:00:00 2001 From: Marcos Date: Tue, 31 Oct 2023 17:37:49 -0300 Subject: [PATCH 1/6] feat: Endpoint usage implementation for Courseware Search Co-authored-by: Simon Chen --- .../CoursewareResultsFilter.jsx | 63 ++++++++------ .../courseware-search/CoursewareSearch.jsx | 81 +++++++++++++++-- .../CoursewareSearchEmpty.jsx | 15 ++++ .../CoursewareSearchForm.jsx | 6 +- ...rsewareSearchResult.PropTypeDefinition.jsx | 12 --- .../CoursewareSearchResults.jsx | 46 +++++----- .../courseware-search/courseware-search.scss | 12 +++ .../courseware-search/map-search-response.js | 87 +++++++++++++++++++ src/course-home/courseware-search/messages.js | 32 +++++++ src/course-home/data/api.js | 11 +++ src/course-home/data/thunks.js | 48 +++++++++- 11 files changed, 344 insertions(+), 69 deletions(-) create mode 100644 src/course-home/courseware-search/CoursewareSearchEmpty.jsx delete mode 100644 src/course-home/courseware-search/CoursewareSearchResult.PropTypeDefinition.jsx create mode 100644 src/course-home/courseware-search/map-search-response.js diff --git a/src/course-home/courseware-search/CoursewareResultsFilter.jsx b/src/course-home/courseware-search/CoursewareResultsFilter.jsx index 9232041479..a98de01ca2 100644 --- a/src/course-home/courseware-search/CoursewareResultsFilter.jsx +++ b/src/course-home/courseware-search/CoursewareResultsFilter.jsx @@ -1,36 +1,54 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Tabs, Tab } from '@edx/paragon'; -import CoursewareSearchResultPropType from './CoursewareSearchResult.PropTypeDefinition'; +import { useParams } from 'react-router'; import CoursewareSearchResults from './CoursewareSearchResults'; +import messages from './messages'; +import { useModel } from '../../generic/model-store'; -export const filteredResultsBySelection = ({ filterKey = 'all', results = [] }) => { - if (['document', 'video', 'text'].includes(filterKey)) { - return results.filter(result => result?.type?.toLowerCase() === filterKey); - } +const noFilterKey = 'none'; +const noFilterLabel = 'All content'; - return results || []; -}; +export const filteredResultsBySelection = ({ key = noFilterKey, results = [] }) => ( + key === noFilterKey ? results : results.filter(({ type }) => type === key) +); + +export const CoursewareSearchResultsFilter = ({ intl }) => { + const { courseId } = useParams(); + const lastSearch = useModel('contentSearchResults', courseId); + + if (!lastSearch || !lastSearch.results.length) { return null; } -const tabConfiguration = [ - { eventKey: 'all', title: 'All content' }, - { eventKey: 'document', title: 'Course outline' }, - { eventKey: 'text', title: 'Text' }, - { eventKey: 'video', title: 'Video' }, -]; + const { total, results } = lastSearch; -export const CoursewareSearchResultsFilter = ({ intl, results }) => { - if (!results || !results.length) { return null; } + const filters = [ + { + key: noFilterKey, + label: noFilterLabel, + count: total, + }, + ...lastSearch.filters, + ]; + + const getFilterTitle = (key, fallback) => { + const msg = messages[`filter:${key}`]; + if (!msg) { return fallback; } + return intl.formatMessage(msg); + }; return ( - - {tabConfiguration.map((tab) => ( - + + {filters.map(({ key, label }) => ( + ))} @@ -40,11 +58,6 @@ export const CoursewareSearchResultsFilter = ({ intl, results }) => { CoursewareSearchResultsFilter.propTypes = { intl: intlShape.isRequired, - results: PropTypes.arrayOf(CoursewareSearchResultPropType), -}; - -CoursewareSearchResultsFilter.defaultProps = { - results: [], }; export default injectIntl(CoursewareSearchResultsFilter); diff --git a/src/course-home/courseware-search/CoursewareSearch.jsx b/src/course-home/courseware-search/CoursewareSearch.jsx index cfdc32e4b5..1b719d7656 100644 --- a/src/course-home/courseware-search/CoursewareSearch.jsx +++ b/src/course-home/courseware-search/CoursewareSearch.jsx @@ -1,7 +1,9 @@ import React, { useState } from 'react'; +import { useParams } from 'react-router'; import { useDispatch } from 'react-redux'; +import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { Button, Icon } from '@edx/paragon'; +import { Button, Icon, Spinner } from '@edx/paragon'; import { Close, } from '@edx/paragon/icons'; @@ -11,24 +13,65 @@ import messages from './messages'; import CoursewareSearchForm from './CoursewareSearchForm'; import CoursewareSearchResultsFilterContainer from './CoursewareResultsFilter'; -import mockedData from './test-data/mockedResults'; +import { updateModel, useModel } from '../../generic/model-store'; +import { searchCourseContent } from '../data/thunks'; const CoursewareSearch = ({ intl, ...sectionProps }) => { - const [results, setResults] = useState(); + const { courseId } = useParams(); const dispatch = useDispatch(); + const { org } = useModel('courseHomeMeta', courseId); + const { + loading, + searchKeyword: lastSearchKeyword, + total, + } = useModel('contentSearchResults', courseId); + const [searchKeyword, setSearchKeyword] = useState(lastSearchKeyword); useLockScroll(); const info = useElementBoundingBox('courseTabsNavigation'); const top = info ? `${Math.floor(info.top)}px` : 0; - const handleSubmit = (search) => { - if (!search) { - setResults(undefined); + const clearSearch = () => { + dispatch(updateModel({ + modelType: 'contentSearchResults', + model: { + id: courseId, + searchKeyword: '', + results: [], + loading: false, + }, + })); + }; + + const handleSubmit = () => { + if (!searchKeyword) { + clearSearch(); return; } - setResults(search.toLowerCase() !== 'lorem ipsum' ? mockedData : []); + const eventProperties = { + org_key: org, + courserun_key: courseId, + }; + + sendTrackingLogEvent('edx.course.home.courseware_search.submit', { + ...eventProperties, + event_type: 'searchKeyword', + keyword: searchKeyword, + }); + + dispatch(searchCourseContent(courseId, searchKeyword)); + }; + + const handleOnChange = (value) => { + if (value === searchKeyword) { return; } + + setSearchKeyword(value); + + if (!value) { + clearSearch(); + } }; return ( @@ -47,16 +90,36 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {

{intl.formatMessage(messages.searchModuleTitle)}

- + {loading ? ( +
+ +
+ ) : null} + {!loading && lastSearchKeyword ? ( + <> +
{total > 0 + ? ( + intl.formatMessage( + total === 1 + ? messages.searchResultsSingular + : messages.searchResultsPlural, + { total, keyword: lastSearchKeyword }, + ) + ) : intl.formatMessage(messages.searchResultsNone)} +
+ + + ) : null}
); }; - CoursewareSearch.propTypes = { intl: intlShape.isRequired, }; diff --git a/src/course-home/courseware-search/CoursewareSearchEmpty.jsx b/src/course-home/courseware-search/CoursewareSearchEmpty.jsx new file mode 100644 index 0000000000..9eef71cabd --- /dev/null +++ b/src/course-home/courseware-search/CoursewareSearchEmpty.jsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import messages from './messages'; + +const CoursewareSearchEmpty = ({ intl }) => ( +
+

{intl.formatMessage(messages.searchResultsNone)}

+
+); + +CoursewareSearchEmpty.propTypes = { + intl: intlShape.isRequired, +}; + +export default injectIntl(CoursewareSearchEmpty); diff --git a/src/course-home/courseware-search/CoursewareSearchForm.jsx b/src/course-home/courseware-search/CoursewareSearchForm.jsx index eac3b35db0..25bef3e938 100644 --- a/src/course-home/courseware-search/CoursewareSearchForm.jsx +++ b/src/course-home/courseware-search/CoursewareSearchForm.jsx @@ -3,11 +3,13 @@ import { SearchField } from '@edx/paragon'; import PropTypes from 'prop-types'; const CoursewareSearchForm = ({ + value, onSubmit, onChange, placeholder, }) => (
- +
@@ -23,12 +25,14 @@ const CoursewareSearchForm = ({ ); CoursewareSearchForm.propTypes = { + value: PropTypes.string, onSubmit: PropTypes.func, onChange: PropTypes.func, placeholder: PropTypes.string, }; CoursewareSearchForm.defaultProps = { + value: undefined, onSubmit: undefined, onChange: undefined, placeholder: undefined, diff --git a/src/course-home/courseware-search/CoursewareSearchResult.PropTypeDefinition.jsx b/src/course-home/courseware-search/CoursewareSearchResult.PropTypeDefinition.jsx deleted file mode 100644 index ae55aaef25..0000000000 --- a/src/course-home/courseware-search/CoursewareSearchResult.PropTypeDefinition.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import PropTypes from 'prop-types'; - -export default { - results: PropTypes.arrayOf(PropTypes.objectOf({ - title: PropTypes.string.isRequired, - href: PropTypes.string.isRequired, - type: PropTypes.string, - breadcrumbs: PropTypes.arrayOf(PropTypes.string), - contentMatches: PropTypes.number, - isExternal: PropTypes.bool, - })), -}; diff --git a/src/course-home/courseware-search/CoursewareSearchResults.jsx b/src/course-home/courseware-search/CoursewareSearchResults.jsx index 537fe25e52..2950d05d1a 100644 --- a/src/course-home/courseware-search/CoursewareSearchResults.jsx +++ b/src/course-home/courseware-search/CoursewareSearchResults.jsx @@ -1,11 +1,11 @@ import React from 'react'; -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Folder, TextFields, VideoCamera, Article, } from '@edx/paragon/icons'; +import { getConfig } from '@edx/frontend-platform'; import { Icon } from '@edx/paragon'; import PropTypes from 'prop-types'; -import messages from './messages'; +import CoursewareSearchEmpty from './CoursewareSearchEmpty'; const iconTypeMapping = { document: Folder, @@ -14,28 +14,32 @@ const iconTypeMapping = { }; const defaultIcon = Article; -const CoursewareSearchResults = ({ intl, results }) => { +const CoursewareSearchResults = ({ results }) => { if (!results?.length) { - return ( -
-

{intl.formatMessage(messages.searchResultsNone)}

-
- ); + return ; } + const baseUrl = `${getConfig().LMS_BASE_URL}`; + return (
{results.map(({ - title, href, type, breadcrumbs, contentMatches, isExternal, + title, + type, + location, + url, + contentHits, }) => { const key = type.toLowerCase(); const icon = iconTypeMapping[key] || defaultIcon; + const isExternal = !url.startsWith('/'); + const linkProps = isExternal ? { - href, + href: url, target: '_blank', rel: 'nofollow', - } : { href }; + } : { href: `${baseUrl}${url}` }; return ( @@ -43,11 +47,11 @@ const CoursewareSearchResults = ({ intl, results }) => {
{title} - {contentMatches ? ({contentMatches}) : null } + {contentHits ? ({contentHits}) : null }
- {breadcrumbs?.length ? ( + {location?.length ? (
    - {breadcrumbs.map(bc => (
  • {bc}
  • ))} + {location.map(bc => (
  • {bc}
  • ))}
) : null}
@@ -59,14 +63,14 @@ const CoursewareSearchResults = ({ intl, results }) => { }; CoursewareSearchResults.propTypes = { - intl: intlShape.isRequired, results: PropTypes.arrayOf(PropTypes.objectOf({ - title: PropTypes.string.isRequired, - href: PropTypes.string.isRequired, + id: PropTypes.string, + title: PropTypes.string, type: PropTypes.string, - breadcrumbs: PropTypes.arrayOf(PropTypes.string), - contentMatches: PropTypes.number, - isExternal: PropTypes.bool, + location: PropTypes.arrayOf(PropTypes.string), + url: PropTypes.string, + contentHits: PropTypes.number, + score: PropTypes.number, })), }; @@ -74,4 +78,4 @@ CoursewareSearchResults.defaultProps = { results: [], }; -export default injectIntl(CoursewareSearchResults); +export default CoursewareSearchResults; diff --git a/src/course-home/courseware-search/courseware-search.scss b/src/course-home/courseware-search/courseware-search.scss index 6e10a9e23d..054bbd2daf 100644 --- a/src/course-home/courseware-search/courseware-search.scss +++ b/src/course-home/courseware-search/courseware-search.scss @@ -32,6 +32,18 @@ margin-bottom: 2rem; } } + + &__results-summary { + font-size: .9rem; + color: $gray-400; + padding: 1rem 0 .5rem; + } + + &__spinner { + display: grid; + place-items: center; + min-height: 20vh; + } } .courseware-search-results { diff --git a/src/course-home/courseware-search/map-search-response.js b/src/course-home/courseware-search/map-search-response.js new file mode 100644 index 0000000000..59908cac09 --- /dev/null +++ b/src/course-home/courseware-search/map-search-response.js @@ -0,0 +1,87 @@ +const defaultType = 'text'; + +// Parses the search results in a convenient way. +export default function mapSearchResponse(response, searchKeyword) { + const keywords = searchKeyword.split(' '); + + const { + took: ms, + total, + max_score: maxScore, + results: rawResults, + } = response; + + const results = rawResults.map(result => { + const { + score, + data: { + id, + content: { + displayName, + htmlContent, + transcriptEn, + }, + contentType, + location, + url, + }, + } = result; + + const type = contentType?.toLowerCase() || defaultType; + let contentHits = 0; + + const content = htmlContent || transcriptEn || ''; + keywords.forEach(word => { contentHits += content.split(word).length - 1; }); + + const title = displayName || contentType; + + return { + id, + title, + type, + location, + url, + contentHits, + score, + }; + }); + + const filters = rawResults.reduce((list, result) => { + const label = result?.data?.contentType; + + if (!label) { return list; } + + const key = label.toLowerCase(); + + const index = list.findIndex(i => i.key === key); + + if (index === -1) { + return [ + ...list, + { + key, + label, + count: 1, + }, + ]; + } + + const newItem = { ...list[index] }; + newItem.count++; + + const newList = list.slice(0); + newList[index] = newItem; + + return newList; + }, []); + + filters.sort((a, b) => (a.key > b.key ? 1 : -1)); + + return { + results, + filters, + total, + maxScore, + ms, + }; +} diff --git a/src/course-home/courseware-search/messages.js b/src/course-home/courseware-search/messages.js index ca16d50ba0..45722d7b11 100644 --- a/src/course-home/courseware-search/messages.js +++ b/src/course-home/courseware-search/messages.js @@ -21,11 +21,43 @@ const messages = defineMessages({ defaultMessage: 'Search', description: 'Placeholder text for the Courseware Search input control', }, + loading: { + id: 'learn.coursewareSerch.loading', + defaultMessage: 'Searching...', + description: 'Screen reader text to use on the spinner while the search is performing.', + }, searchResultsNone: { id: 'learn.coursewareSerch.searchResultsNone', defaultMessage: 'No results found.', description: 'Text to show when the Courseware Search found no results matching the criteria.', }, + searchResultsSingular: { + id: 'learn.coursewareSerch.searchResultsSingular', + defaultMessage: '1 match found for "{keyword}":', + description: 'Text to show when the Courseware Search found only one result matching the criteria.', + }, + searchResultsPlural: { + id: 'learn.coursewareSerch.searchResultsPlural', + defaultMessage: '{total} matches found for "{keyword}":', + description: 'Text to show when the Courseware Search found multiple results matching the criteria.', + }, + + // These are translations for labeling the filters + 'filter:none': { + id: 'learn.coursewareSerch.filter:none', + defaultMessage: 'All content', + description: 'Label for the search results filter that shows all content (no filter).', + }, + 'filter:text': { + id: 'learn.coursewareSerch.filter:text', + defaultMessage: 'Text', + description: 'Label for the search results filter that shows results with text content.', + }, + 'filter:video': { + id: 'learn.coursewareSerch.filter:video', + defaultMessage: 'Video', + description: 'Label for the search results filter that shows results with video content.', + }, }); export default messages; diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js index ce159dcbd1..963bdefc76 100644 --- a/src/course-home/data/api.js +++ b/src/course-home/data/api.js @@ -451,3 +451,14 @@ export async function getCoursewareSearchEnabledFlag(courseId) { const { data } = await getAuthenticatedHttpClient().get(url.href); return { enabled: data.enabled || false }; } + +export async function searchCourseContentFromAPI(courseId, searchKeyword, options = {}) { + const defaults = { page: 0, limit: 20 }; + const { page, limit } = { ...defaults, ...options }; + + const url = new URL(`${getConfig().LMS_BASE_URL}/search/${courseId}`); + const formData = `search_string=${searchKeyword}&page_size=${limit}&page_index=${page}`; + const response = await getAuthenticatedHttpClient().post(url.href, formData); + + return camelCaseObject(response); +} diff --git a/src/course-home/data/thunks.js b/src/course-home/data/thunks.js index 47339137fd..d640660994 100644 --- a/src/course-home/data/thunks.js +++ b/src/course-home/data/thunks.js @@ -13,10 +13,11 @@ import { postRequestCert, getLiveTabIframe, getCoursewareSearchEnabledFlag, + searchCourseContentFromAPI, } from './api'; import { - addModel, + addModel, updateModel, } from '../../generic/model-store'; import { @@ -27,6 +28,8 @@ import { setCallToActionToast, } from './slice'; +import mapSearchResponse from '../courseware-search/map-search-response'; + const eventTypes = { POST_EVENT: 'post_event', }; @@ -149,3 +152,46 @@ export async function fetchCoursewareSearchSettings(courseId) { return { enabled: false }; } } + +export function searchCourseContent(courseId, searchKeyword) { + return async (dispatch) => { + const start = new Date(); + dispatch(addModel({ + modelType: 'contentSearchResults', + model: { + id: courseId, + searchKeyword, + results: [], + loading: true, + }, + })); + searchCourseContentFromAPI(courseId, searchKeyword).then(response => { + const { data } = response; + dispatch(updateModel({ + modelType: 'contentSearchResults', + model: { + id: courseId, + searchKeyword, + ...mapSearchResponse(data, searchKeyword), + loading: false, + }, + })); + const end = new Date(); + const clientMs = (end - start); + const { + took, total, maxScore, accessDeniedCount, + } = data; + + // TODO: Remove when publishing to prod. Just temporary for performance debugging. + // eslint-disable-next-line no-console + console.table({ + 'Search Keyword': searchKeyword, + 'Client time (ms)': clientMs, + 'Server time (ms)': took, + 'Total matches': total, + 'Max score': maxScore, + 'Access denied count': accessDeniedCount, + }); + }); + }; +} From f76971e2e156db182fea7c1a04330193191cd2d5 Mon Sep 17 00:00:00 2001 From: Marcos Date: Thu, 2 Nov 2023 17:06:14 -0300 Subject: [PATCH 2/6] chore: Added tests and error case --- package-lock.json | 44 ++ package.json | 1 + .../CoursewareResultsFilter.jsx | 4 +- .../courseware-search/CoursewareSearch.jsx | 24 +- .../CoursewareSearchResults.jsx | 10 +- .../CoursewareSearchResults.test.jsx | 6 +- .../map-search-response.test.js.snap | 292 ++++++++++ .../courseware-search/map-search-response.js | 42 +- .../map-search-response.test.js | 52 ++ src/course-home/courseware-search/messages.js | 5 + .../test-data/mocked-response.json | 548 ++++++++++++++++++ .../test-data/mockedResults.js | 66 --- src/course-home/data/thunks.js | 63 +- 13 files changed, 1052 insertions(+), 105 deletions(-) create mode 100644 src/course-home/courseware-search/__snapshots__/map-search-response.test.js.snap create mode 100644 src/course-home/courseware-search/map-search-response.test.js create mode 100644 src/course-home/courseware-search/test-data/mocked-response.json delete mode 100644 src/course-home/courseware-search/test-data/mockedResults.js diff --git a/package-lock.json b/package-lock.json index b6aa73e038..f4741861bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "classnames": "2.3.2", "core-js": "3.22.2", "history": "5.3.0", + "joi": "^17.11.0", "js-cookie": "3.0.5", "lodash.camelcase": "4.3.0", "prop-types": "15.8.1", @@ -4362,6 +4363,19 @@ "postcss": "^8.0.0" } }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", @@ -5878,6 +5892,24 @@ "react": ">=16.8.0" } }, + "node_modules/@sideway/address": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", + "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -16670,6 +16702,18 @@ "jiti": "bin/jiti.js" } }, + "node_modules/joi": { + "version": "17.11.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.11.0.tgz", + "integrity": "sha512-NgB+lZLNoqISVy1rZocE9PZI36bL/77ie924Ri43yEvi9GUUMPeyVIr8KdFTMUlby1p0PBYMk9spIxEUQYqrJQ==", + "dependencies": { + "@hapi/hoek": "^9.0.0", + "@hapi/topo": "^5.0.0", + "@sideway/address": "^4.1.3", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, "node_modules/joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", diff --git a/package.json b/package.json index fd90719173..77a658a715 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "classnames": "2.3.2", "core-js": "3.22.2", "history": "5.3.0", + "joi": "^17.11.0", "js-cookie": "3.0.5", "lodash.camelcase": "4.3.0", "prop-types": "15.8.1", diff --git a/src/course-home/courseware-search/CoursewareResultsFilter.jsx b/src/course-home/courseware-search/CoursewareResultsFilter.jsx index a98de01ca2..8e1d303ccc 100644 --- a/src/course-home/courseware-search/CoursewareResultsFilter.jsx +++ b/src/course-home/courseware-search/CoursewareResultsFilter.jsx @@ -22,6 +22,8 @@ export const CoursewareSearchResultsFilter = ({ intl }) => { const { total, results } = lastSearch; + console.log({ results }); + const filters = [ { key: noFilterKey, @@ -45,7 +47,7 @@ export const CoursewareSearchResultsFilter = ({ intl }) => { defaultActiveKey={noFilterKey} > {filters.map(({ key, label }) => ( - + { const { loading, searchKeyword: lastSearchKeyword, + errors, total, } = useModel('contentSearchResults', courseId); const [searchKeyword, setSearchKeyword] = useState(lastSearchKeyword); @@ -39,6 +42,7 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => { id: courseId, searchKeyword: '', results: [], + errors: undefined, loading: false, }, })); @@ -74,6 +78,15 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => { } }; + let status = 'idle'; + if (loading) { + status = 'loading'; + } else if (errors) { + status = 'error'; + } else if (lastSearchKeyword) { + status = 'results'; + } + return (
@@ -95,12 +108,17 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => { onChange={handleOnChange} placeholder={intl.formatMessage(messages.searchBarPlaceholderText)} /> - {loading ? ( + {status === 'loading' ? (
) : null} - {!loading && lastSearchKeyword ? ( + {status === 'error' && ( + + {intl.formatMessage(messages.searchResultsError)} + + )} + {status === 'results' ? ( <>
{total > 0 ? ( diff --git a/src/course-home/courseware-search/CoursewareSearchResults.jsx b/src/course-home/courseware-search/CoursewareSearchResults.jsx index 2950d05d1a..126e9cf3dc 100644 --- a/src/course-home/courseware-search/CoursewareSearchResults.jsx +++ b/src/course-home/courseware-search/CoursewareSearchResults.jsx @@ -24,6 +24,7 @@ const CoursewareSearchResults = ({ results }) => { return (
{results.map(({ + id, title, type, location, @@ -42,7 +43,7 @@ const CoursewareSearchResults = ({ results }) => { } : { href: `${baseUrl}${url}` }; return ( - +
@@ -51,7 +52,12 @@ const CoursewareSearchResults = ({ results }) => {
{location?.length ? (
    - {location.map(bc => (
  • {bc}
  • ))} + { + // This ignore is necessary because the breadcrumb texts might have duplicates. + // The breadcrumbs are not expected to change. + // eslint-disable-next-line react/no-array-index-key + location.map((bc, i) => (
  • {bc}
  • )) +}
) : null}
diff --git a/src/course-home/courseware-search/CoursewareSearchResults.test.jsx b/src/course-home/courseware-search/CoursewareSearchResults.test.jsx index 406b695b5a..a49ef62a5a 100644 --- a/src/course-home/courseware-search/CoursewareSearchResults.test.jsx +++ b/src/course-home/courseware-search/CoursewareSearchResults.test.jsx @@ -6,7 +6,7 @@ import { } from '../../setupTest'; import CoursewareSearchResults from './CoursewareSearchResults'; import messages from './messages'; -import mockedData from './test-data/mockedResults'; +// import mockedData from './test-data/mockedResults'; // TODO: Update this test. jest.mock('react-redux'); @@ -28,11 +28,11 @@ describe('CoursewareSearchResults', () => { }); }); - describe('when list of results is provided', () => { + /* describe('when list of results is provided', () => { beforeEach(() => { renderComponent({ results: mockedData }); }); it('should match the snapshot', () => { expect(screen.getByTestId('search-results')).toMatchSnapshot(); }); - }); + }); */ }); diff --git a/src/course-home/courseware-search/__snapshots__/map-search-response.test.js.snap b/src/course-home/courseware-search/__snapshots__/map-search-response.test.js.snap new file mode 100644 index 0000000000..9fb892cab0 --- /dev/null +++ b/src/course-home/courseware-search/__snapshots__/map-search-response.test.js.snap @@ -0,0 +1,292 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`mapSearchResponse when the response is correct should match snapshot 1`] = ` +Object { + "filters": Array [ + Object { + "count": 7, + "key": "capa", + "label": "CAPA", + }, + Object { + "count": 2, + "key": "sequence", + "label": "Sequence", + }, + Object { + "count": 9, + "key": "text", + "label": "Text", + }, + Object { + "count": 2, + "key": "video", + "label": "Video", + }, + ], + "maxScore": 3.4545178, + "ms": 5, + "results": Array [ + Object { + "contentHits": 0, + "id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction", + "location": Array [ + "Introduction", + "Demo Course Overview", + ], + "score": 3.4545178, + "title": "Demo Course Overview", + "type": "sequence", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction", + }, + Object { + "contentHits": 0, + "id": "block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9", + "location": Array [ + "About Exams and Certificates", + "edX Exams", + "Passing a Course", + ], + "score": 3.4545178, + "title": "Passing a Course", + "type": "text", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9", + }, + Object { + "contentHits": 0, + "id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff", + "location": Array [ + "About Exams and Certificates", + "edX Exams", + "Passing a Course", + ], + "score": 3.4545178, + "title": "Passing a Course", + "type": "sequence", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff", + }, + Object { + "contentHits": 0, + "id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02", + "location": Array [ + "Example Week 1: Getting Started", + "Homework - Question Styles", + "Text input", + ], + "score": 1.5874016, + "title": "Text Input", + "type": "capa", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02", + }, + Object { + "contentHits": 0, + "id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c", + "location": Array [ + "Example Week 1: Getting Started", + "Homework - Question Styles", + "Pointing on a Picture", + ], + "score": 1.5499392, + "title": "Pointing on a Picture", + "type": "capa", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c", + }, + Object { + "contentHits": 0, + "id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4", + "location": Array [ + "About Exams and Certificates", + "edX Exams", + "Getting Answers", + ], + "score": 1.5003732, + "title": "Getting Answers", + "type": "capa", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4", + }, + Object { + "contentHits": 0, + "id": "block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd", + "location": Array [ + "Introduction", + "Demo Course Overview", + "Introduction: Video and Sequences", + ], + "score": 1.4792063, + "title": "Welcome!", + "type": "video", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd", + }, + Object { + "contentHits": 0, + "id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4", + "location": Array [ + "Example Week 1: Getting Started", + "Homework - Question Styles", + "Multiple Choice Questions", + ], + "score": 1.4341705, + "title": "Multiple Choice Questions", + "type": "capa", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4", + }, + Object { + "contentHits": 0, + "id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974", + "location": Array [ + "Example Week 1: Getting Started", + "Homework - Question Styles", + "Numerical Input", + ], + "score": 1.2987298, + "title": "Numerical Input", + "type": "capa", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974", + }, + Object { + "contentHits": 0, + "id": "block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6", + "location": Array [ + "Example Week 1: Getting Started", + "Lesson 1 - Getting Started", + "Video Presentation Styles", + ], + "score": 1.1870136, + "title": "Connecting a Circuit and a Circuit Diagram", + "type": "video", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6", + }, + Object { + "contentHits": 0, + "id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader", + "location": Array [ + "Example Week 2: Get Interactive", + "Homework - Labs and Demos", + "Code Grader", + ], + "score": 1.0107487, + "title": "CAPA", + "type": "capa", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader", + }, + Object { + "contentHits": 0, + "id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618", + "location": Array [ + "Example Week 1: Getting Started", + "Lesson 1 - Getting Started", + "Interactive Questions", + ], + "score": 0.96387196, + "title": "Interactive Questions", + "type": "capa", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618", + }, + Object { + "contentHits": 0, + "id": "block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4", + "location": Array [ + "Introduction", + "Demo Course Overview", + "Introduction: Video and Sequences", + ], + "score": 0.8844358, + "title": "Blank HTML Page", + "type": "text", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4", + }, + Object { + "contentHits": 0, + "id": "block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7", + "location": Array [ + "Example Week 3: Be Social", + "Lesson 3 - Be Social", + "Discussion Forums", + ], + "score": 0.8803684, + "title": "Discussion Forums", + "type": "text", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7", + }, + Object { + "contentHits": 0, + "id": "block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c", + "location": Array [ + "About Exams and Certificates", + "edX Exams", + "Overall Grade Performance", + ], + "score": 0.87981963, + "title": "Overall Grade", + "type": "text", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c", + }, + Object { + "contentHits": 0, + "id": "block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339", + "location": Array [ + "Example Week 3: Be Social", + "Lesson 3 - Be Social", + "Homework - Find Your Study Buddy", + ], + "score": 0.84284115, + "title": "Blank HTML Page", + "type": "text", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339", + }, + Object { + "contentHits": 0, + "id": "block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5", + "location": Array [ + "Example Week 3: Be Social", + "Homework - Find Your Study Buddy", + "Homework - Find Your Study Buddy", + ], + "score": 0.84284115, + "title": "Find Your Study Buddy", + "type": "text", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5", + }, + Object { + "contentHits": 0, + "id": "block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0", + "location": Array [ + "Example Week 3: Be Social", + "Lesson 3 - Be Social", + "Be Social", + ], + "score": 0.84210813, + "title": "Be Social", + "type": "text", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0", + }, + Object { + "contentHits": 0, + "id": "block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530", + "location": Array [ + "About Exams and Certificates", + "edX Exams", + "EdX Exams", + ], + "score": 0.8306555, + "title": "EdX Exams", + "type": "text", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530", + }, + Object { + "contentHits": 0, + "id": "block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf", + "location": Array [ + "Example Week 1: Getting Started", + "Lesson 1 - Getting Started", + "When Are Your Exams? ", + ], + "score": 0.82610154, + "title": "When Are Your Exams? ", + "type": "text", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf", + }, + ], + "total": 29, +} +`; diff --git a/src/course-home/courseware-search/map-search-response.js b/src/course-home/courseware-search/map-search-response.js index 59908cac09..fa51f4dcf0 100644 --- a/src/course-home/courseware-search/map-search-response.js +++ b/src/course-home/courseware-search/map-search-response.js @@ -1,15 +1,40 @@ +const Joi = require('joi'); + +const endpointSchema = Joi.object({ + took: Joi.number(), + total: Joi.number(), + maxScore: Joi.number(), + results: Joi.array().items(Joi.object({ + id: Joi.string(), + contentType: Joi.string(), + location: Joi.array().items(Joi.string()), + url: Joi.string(), + content: Joi.object({ + displayName: Joi.string(), + htmlContent: Joi.string(), + transcriptEn: Joi.string(), + }), + }).unknown(true)).strict(), +}).unknown(true).strict(); + const defaultType = 'text'; // Parses the search results in a convenient way. -export default function mapSearchResponse(response, searchKeyword) { - const keywords = searchKeyword.split(' '); +export default function mapSearchResponse(response, searchKeywords = '') { + const { error, value: data } = endpointSchema.validate(response); + + if (error) { + throw new Error('Error in server response:', error); + } + + const keywords = searchKeywords ? searchKeywords.toLowerCase().split(' ') : []; const { took: ms, total, - max_score: maxScore, + maxScore, results: rawResults, - } = response; + } = data; const results = rawResults.map(result => { const { @@ -28,10 +53,15 @@ export default function mapSearchResponse(response, searchKeyword) { } = result; const type = contentType?.toLowerCase() || defaultType; - let contentHits = 0; const content = htmlContent || transcriptEn || ''; - keywords.forEach(word => { contentHits += content.split(word).length - 1; }); + const searchContent = content.toLowerCase(); + let contentHits = 0; + if (keywords.length) { + keywords.forEach(word => { + contentHits += searchContent ? searchContent.toLowerCase().split(word).length - 1 : 0; + }); + } const title = displayName || contentType; diff --git a/src/course-home/courseware-search/map-search-response.test.js b/src/course-home/courseware-search/map-search-response.test.js new file mode 100644 index 0000000000..ee55a45b84 --- /dev/null +++ b/src/course-home/courseware-search/map-search-response.test.js @@ -0,0 +1,52 @@ +import { camelCaseObject } from '@edx/frontend-platform'; +import mapSearchResponse from './map-search-response'; +import mockedResponse from './test-data/mocked-response.json'; + +describe('mapSearchResponse', () => { + describe('when the response is correct', () => { + let response; + + beforeEach(() => { + response = mapSearchResponse(camelCaseObject(mockedResponse)); + }); + + it('should match snapshot', () => { + expect(response).toMatchSnapshot(); + }); + + it('should match expected filters', () => { + const expectedFilters = [ + { key: 'capa', label: 'CAPA', count: 7 }, + { key: 'sequence', label: 'Sequence', count: 2 }, + { key: 'text', label: 'Text', count: 9 }, + { key: 'video', label: 'Video', count: 2 }, + ]; + expect(response.filters).toEqual(expectedFilters); + }); + }); + + describe('when the a keyword is provided', () => { + const searchText = 'Course'; + + it('should not count matches title', () => { + const response = mapSearchResponse(camelCaseObject(mockedResponse), searchText); + expect(response.results[0].contentHits).toBe(0); + }); + + it('should count matches on content', () => { + const response = mapSearchResponse(camelCaseObject(mockedResponse), searchText); + expect(response.results[1].contentHits).toBe(1); + }); + + it('should ignore capitalization', () => { + const response = mapSearchResponse(camelCaseObject(mockedResponse), searchText.toUpperCase()); + expect(response.results[1].contentHits).toBe(1); + }); + }); + + describe('when the response has a wrong format', () => { + it('should throw an error', () => { + expect(() => mapSearchResponse({ foo: 'bar' })).toThrow(); + }); + }); +}); diff --git a/src/course-home/courseware-search/messages.js b/src/course-home/courseware-search/messages.js index 45722d7b11..b18c11aa90 100644 --- a/src/course-home/courseware-search/messages.js +++ b/src/course-home/courseware-search/messages.js @@ -41,6 +41,11 @@ const messages = defineMessages({ defaultMessage: '{total} matches found for "{keyword}":', description: 'Text to show when the Courseware Search found multiple results matching the criteria.', }, + searchResultsError: { + id: 'learn.coursewareSerch.searchResultsError', + defaultMessage: 'There was an error on the search process. Please try again in a few minutes. If the problem persists, please contact the support team.', + description: 'Error message to show to the users when there\'s an error with the endpoint or the returned payload format.', + }, // These are translations for labeling the filters 'filter:none': { diff --git a/src/course-home/courseware-search/test-data/mocked-response.json b/src/course-home/courseware-search/test-data/mocked-response.json new file mode 100644 index 0000000000..209ce0433d --- /dev/null +++ b/src/course-home/courseware-search/test-data/mocked-response.json @@ -0,0 +1,548 @@ +{ + "took": 5, + "total": 29, + "max_score": 3.4545178, + "results": [ + { + "_index": "courseware_content", + "_type": "_doc", + "_id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction", + "data": { + "course": "course-v1:edX+DemoX+Demo_Course", + "org": "edX", + "content": { + "display_name": "Demo Course Overview" + }, + "content_type": "Sequence", + "id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction", + "start_date": "1970-01-01T05:00:00+00:00", + "content_groups": null, + "course_name": "Demonstration Course", + "location": [ + "Introduction", + "Demo Course Overview" + ], + "excerpt": "Demo Course Overview", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction" + }, + "score": 3.4545178 + }, + { + "_index": "courseware_content", + "_type": "_doc", + "_id": "block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9", + "data": { + "course": "course-v1:edX+DemoX+Demo_Course", + "org": "edX", + "content": { + "display_name": "Passing a Course", + "html_content": "Passing a COurse After the last assignment in a class has been due, you will see the entry in your student profile change to show progress toward generating your certificate. After the certificate generation process has completed, you will be able to download it from your profile page. " + }, + "content_type": "Text", + "id": "block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9", + "start_date": "2013-02-05T00:00:00+00:00", + "content_groups": null, + "course_name": "Demonstration Course", + "location": [ + "About Exams and Certificates", + "edX Exams", + "Passing a Course" + ], + "excerpt": "Passing a CoursePassing a COurse After the last assignment in a class has been due,", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9" + }, + "score": 3.4545178 + }, + { + "_index": "courseware_content", + "_type": "_doc", + "_id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff", + "data": { + "course": "course-v1:edX+DemoX+Demo_Course", + "org": "edX", + "content": { + "display_name": "Passing a Course" + }, + "content_type": "Sequence", + "id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff", + "start_date": "2013-02-05T00:00:00+00:00", + "content_groups": null, + "course_name": "Demonstration Course", + "location": [ + "About Exams and Certificates", + "edX Exams", + "Passing a Course" + ], + "excerpt": "Passing a Course", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff" + }, + "score": 3.4545178 + }, + { + "_index": "courseware_content", + "_type": "_doc", + "_id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02", + "data": { + "course": "course-v1:edX+DemoX+Demo_Course", + "org": "edX", + "content": { + "display_name": "Text Input", + "capa_content": " Here's a very simple example of a text input question. Depending on the course you may have to observe special text requirements for dates, case sensitivity, etc. Which country contains Paris as its capital? " + }, + "content_type": "CAPA", + "problem_types": [ + "stringresponse" + ], + "id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02", + "start_date": "2013-02-05T00:00:00+00:00", + "content_groups": null, + "course_name": "Demonstration Course", + "location": [ + "Example Week 1: Getting Started", + "Homework - Question Styles", + "Text input" + ], + "excerpt": "the course you may have to observe special text requirements for", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02" + }, + "score": 1.5874016 + }, + { + "_index": "courseware_content", + "_type": "_doc", + "_id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c", + "data": { + "course": "course-v1:edX+DemoX+Demo_Course", + "org": "edX", + "content": { + "display_name": "Pointing on a Picture", + "capa_content": " Some course questions may show you an image and ask that you click on it to answer a question. Try this example. (If you are correct you will see our famous green check mark.) Which animal is a kitten? " + }, + "content_type": "CAPA", + "problem_types": [ + "imageresponse" + ], + "id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c", + "start_date": "2013-02-05T00:00:00+00:00", + "content_groups": null, + "course_name": "Demonstration Course", + "location": [ + "Example Week 1: Getting Started", + "Homework - Question Styles", + "Pointing on a Picture" + ], + "excerpt": " Some course questions may show you an image and ask that you click on", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c" + }, + "score": 1.5499392 + }, + { + "_index": "courseware_content", + "_type": "_doc", + "_id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4", + "data": { + "course": "course-v1:edX+DemoX+Demo_Course", + "org": "edX", + "content": { + "display_name": "Getting Answers", + "capa_content": " In some courses a \"show answer\" button might appear below a question. When you click on this button, you can see the correct answer (with an explanation) that would receive full credit. How much does it cost to take an edX course? Enter the number of dollars. " + }, + "content_type": "CAPA", + "problem_types": [ + "numericalresponse" + ], + "id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4", + "start_date": "2013-02-05T00:00:00+00:00", + "content_groups": null, + "course_name": "Demonstration Course", + "location": [ + "About Exams and Certificates", + "edX Exams", + "Getting Answers" + ], + "excerpt": " In some courses a \"show answer\" button might appear below a question.", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4" + }, + "score": 1.5003732 + }, + { + "_index": "courseware_content", + "_type": "_doc", + "_id": "block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd", + "data": { + "course": "course-v1:edX+DemoX+Demo_Course", + "org": "edX", + "content": { + "display_name": "Welcome!", + "transcript_en": " ERIC: Hi, and welcome to the edX demonstration course. I'm Eric, and I'm here to help you get a better understanding of how fun and easy it is to take an edX course. So, let's get started. Let me show you how all the parts work together. If at any time you want to skip this video and get a firsthand experience of the demonstration course, all you have to do is click week one to the left. Don't worry, I won't be offended. Let's first look along the top of the page. This area's called the navigation bar. Click on Courseware to interact with your course. Course Info contains course announcements and updates from the course staff. If your course has digital textbooks, this is where you'll find them. Discussion is where you can communicate with the fellow students on topics and projects, and even occasionally with the course staff. When available, the course Wiki acts as a knowledge base for your course. It's a helpful resource. Clicking on Progress will reveal how well you're doing in your studies and exams. When you take the demo course, we'll provide you with a simple progress report matching your results. Let's look at the left column now. The left side of the Courseware screen contains a course navigation bar starting from the top down. Many courses start with an overview of edX and an introduction to the course. Below the overview are segments of the course, which are released as the course progresses. Typically, an edX course is delivered in week by week segments, and have lessons and homeworks you need to complete. Many courses are 10 to 12 weeks long. We made this demonstration course three weeks for simplicity. Let's now look at the learning sequence. Each item in the left column reveals a corresponding learning sequence. Work your way from left to right. Learning sequences can contain lectures, exercises, and interactive lessons that you can complete on your own schedule. Next, let's discover what makes edX fun and unique, its interactivity. edX prides itself on its interactive lessons, which can include demonstrations, visualizations, and virtual environments. You can try out some in the demo course. Interactive lessons are often graded and contribute to your final grade. While the edX platform also supports more traditional question formats like multiple choice, our classes also test your understanding by allowing you to use labs and simulators, and even asking you to write an essay. Example of these graded interactions are in the demo course. You can see how the questions the course uses for gauging your learning process can even be auto graded, or detailed feedback given in real time. So while an edX course might be rigorous, the tools and visualizations are really fun and truly interactive. Finally, there are many ways successful students like to you interact to get the most out of a course. Beyond the discussion forums, you can meet and engage with fellow classmates through a local meet up-- which we highly recommend-- a Google Hangout, or even invite students to join you via Twitter, Facebook, or other social networks. It's a proven fact that if you engage with others while taking a course, you're more likely to succeed. Now that you've seen how easy it is to take an edX course, experience this demonstration course. Firsthand all you have to do is click on week one to the left and you can get started. " + }, + "content_type": "Video", + "id": "block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd", + "start_date": "1970-01-01T05:00:00+00:00", + "content_groups": null, + "course_name": "Demonstration Course", + "location": [ + "Introduction", + "Demo Course Overview", + "Introduction: Video and Sequences" + ], + "excerpt": " ERIC: Hi, and welcome to the edX demonstration course. I'm Eric, and", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd" + }, + "score": 1.4792063 + }, + { + "_index": "courseware_content", + "_type": "_doc", + "_id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4", + "data": { + "course": "course-v1:edX+DemoX+Demo_Course", + "org": "edX", + "content": { + "display_name": "Multiple Choice Questions", + "capa_content": " Many edX courses have homework or exercises you need to complete. Notice the clock image to the left? That means this homework or exercise needs to be completed for you to pass the course. (This can be a bit confusing; the exercise may or may not have a due date prior to the end of the course.) We\u2019ve provided eight (8) examples of how a professor might ask you questions. While the multiple choice question types below are somewhat standard, explore the other question types in the sequence above, like the formula builder- try them all out. As you go through the question types, notice how edX gives you immediate feedback on your responses - it really helps in the learning process. What color is the open ocean on a sunny day? 'yellow','blue','green' Which piece of furniture is built for sitting? a table a desk a chair a bookshelf Which of the following are musical instruments? a piano a tree a guitar a window " + }, + "content_type": "CAPA", + "problem_types": [ + "multiplechoiceresponse", + "choiceresponse", + "optionresponse" + ], + "id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4", + "start_date": "2013-02-05T00:00:00+00:00", + "content_groups": null, + "course_name": "Demonstration Course", + "location": [ + "Example Week 1: Getting Started", + "Homework - Question Styles", + "Multiple Choice Questions" + ], + "excerpt": " Many edX courses have homework or exercises you need to complete.", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4" + }, + "score": 1.4341705 + }, + { + "_index": "courseware_content", + "_type": "_doc", + "_id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974", + "data": { + "course": "course-v1:edX+DemoX+Demo_Course", + "org": "edX", + "content": { + "display_name": "Numerical Input", + "capa_content": " Some course questions ask that you insert numbers into web-text fields, and your answers can be judged exactly - or approximately - according to the question. Note that the edX system uses a period to indicate decimals, so fifteen and three quarters is written \"15.75\", not \"15,75\". Enter the numerical value of Pi: Enter the approximate value of 502*9: Enter the number of fingernails on a healthy human hand. For the purposes of this question, please consider the thumb as a finger: " + }, + "content_type": "CAPA", + "problem_types": [ + "numericalresponse" + ], + "id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974", + "start_date": "2013-02-05T00:00:00+00:00", + "content_groups": null, + "course_name": "Demonstration Course", + "location": [ + "Example Week 1: Getting Started", + "Homework - Question Styles", + "Numerical Input" + ], + "excerpt": " Some course questions ask that you insert numbers into web-text", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974" + }, + "score": 1.2987298 + }, + { + "_index": "courseware_content", + "_type": "_doc", + "_id": "block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6", + "data": { + "course": "course-v1:edX+DemoX+Demo_Course", + "org": "edX", + "content": { + "display_name": "Connecting a Circuit and a Circuit Diagram", + "transcript_en": "SPEAKER 1: What we see here-- a mess. OK? What we have is a voltmeter and an amp meter. The amp meter measures current, the voltmeter measures voltage of course. And we're measuring the voltage across the light bulb-- the lamp. And we're measuring the current into the lamp. PETER: So we've got a voltmeter measuring across the-- volts meter-- across there. And then we've got an amp reader measuring into. SPEAKER 1: Right. Exactly. Now, we're going to connect that to a battery. The three cell battery that you've seen before. And we're going to see, of course, that the light bulb lit up. And the current we measure is 122 milliamperes going into the light bulb. 122 milliamperes. And the voltage across is from the plus to minus, 4.31 volts. OK? Now, we can do another experiment. Notice how the light bulb is lit up and how much it's lit, approximately. Now, I'm going to reverse the battery so that we connect the battery in the opposite polarity. OK. Go ahead, Peter. PETER: You connect it, I will draw. SPEAKER 1: OK, I'm just doing it. PETER: So I'll swap these. SPEAKER 1: Yes, sir. OK. And what we observe is basically the amp meter is measuring 122 milliamperes in the negative direction. So that means it's measuring the current into the light bulb-- because I've not changed the orientation with respect to the light bulb-- of minus 122 milliamperes. And the voltage across the light bulb, from here to here, is minus 4.29 volts. PETER: Sorry, that's a minus? SPEAKER 1: Yes. PETER: So if we look at the power in the first case, 122 milliamps times 4.31 volts, we get 526 milliwatts. SPEAKER 1: Yep. PETER: If we measure the power in this case over here, minus 122 milliamps times minus 4.29 volts, we get approximately the same thing. So I'm going to round it off to, let's say best guess, 524, maybe 23 or something. No less. SPEAKER 1: OK. PETER: So this is equal to that within measurement error. SPEAKER 1: And of course, you see the power is the power going into the light bulb and coming out as light and heat. OK? We have arranged our measurements by having these associated reference directions, so that this is plus and that's minus, and that the current always goes into the terminal that we label with plus. That always means that the power we measure by multiplying these two numbers is the power going into this device. PETER: So this light bulb is dissipating 524 milliwatts. If we were to do the same calculation for the battery, so current would be going to the positive terminal, we would-- SPEAKER 1: Well, you have to measure it then from there to there. PETER: Yeah. Plus, minus. That's what we're doing. So this would be 4.29 volts. The current would still be minus 122 milliamps. The current's moving in a loop, so here is the same as here, but the signs are swapped. That same calculation would give us minus 524 milliwatts. And that's because the battery is outputting power, whereas the light bulb is dissipating power. SPEAKER 1: Think about it as, if we're measuring the power entering the battery, it's minus 524 milliwatts. OK? That's the way to think about it. This always gives you the power entering that element." + }, + "content_type": "Video", + "id": "block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6", + "start_date": "2013-02-05T05:00:00+00:00", + "content_groups": null, + "course_name": "Demonstration Course", + "location": [ + "Example Week 1: Getting Started", + "Lesson 1 - Getting Started", + "Video Presentation Styles" + ], + "excerpt": "measures voltage of course. And we're measuring the voltage across the", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6" + }, + "score": 1.1870136 + }, + { + "_index": "courseware_content", + "_type": "_doc", + "_id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader", + "data": { + "course": "course-v1:edX+DemoX+Demo_Course", + "org": "edX", + "content": { + "display_name": "", + "capa_content": " We are searching for the smallest monthly payment such that we can pay off the entire balance of a loan within a year. The following values might be useful when writing your solution Monthly interest rate = (Annual interest rate) / 12 Monthly payment lower bound = Balance / 12 Monthly payment upper bound = (Balance x (1 + Monthly interest rate)12) / 12 The following variables contain values as described below: balance - the outstanding balance on the credit card annualInterestRate - annual interest rate as a decimal Write a program that uses these bounds and bisection search (for more info check out the Wikipedia page on bisection search) to find the smallest monthly payment to the cent such that we can pay off the debt within a year. Note that if you do not use bisection search, your code will not run - your code only has 30 seconds to run on our servers. If you get a message that states \"Your submission could not be graded. Please recheck your submission and try again. If the problem persists, please notify the course staff.\", check to be sure your code doesn't take too long to run. The code you paste into the following box should not specify the values for the variables balance or annualInterestRate - our test code will define those values before testing your submission. monthlyInterestRate = annualInterestRate/12 lowerBound = balance/12 upperBound = (balance * (1+annualInterestRate/12)**12)/12 originalBalance = balance # Keep testing new payment values # until the balance is +/- $0.02 while abs(balance) > .02: # Reset the value of balance to its original value balance = originalBalance # Calculate a new monthly payment value from the bounds payment = (upperBound - lowerBound)/2 + lowerBound # Test if this payment value is sufficient to pay off the # entire balance in 12 months for month in range(12): balance -= payment balance *= 1+monthlyInterestRate # Reset bounds based on the final value of balance if balance > 0: # If the balance is too big, need higher payment # so we increase the lower bound lowerBound = payment else: # If the balance is too small, we need a lower # payment, so we decrease the upper bound upperBound = payment # When the while loop terminates, we know we have # our answer! print(\"Lowest Payment:\", round(payment, 2)) {\"grader\": \"ps02/bisect/grade_bisect.py\"} Note: Depending on where, and how frequently, you round during this function, your answers may be off a few cents in either direction. Try rounding as few times as possible in order to increase the accuracy of your result. Hints Test Cases to test your code with. Be sure to test these on your own machine - and that you get the same output! - before running your code on this webpage! Note: The automated tests are lenient - if your answers are off by a few cents in either direction, your code is OK. Test Cases: Test Case 1: balance = 320000 annualInterestRate = 0.2 Result Your Code Should Generate: ------------------- Lowest Payment: 29157.09 Test Case 2: balance = 999999 annualInterestRate = 0.18 Result Your Code Should Generate: ------------------- Lowest Payment: 90325.07 The autograder says, \"Your submission could not be graded.\" Help! If the autograder gives you the following message: Your submission could not be graded. Please recheck your submission and try again. If the problem persists, please notify the course staff. Don't panic! There are a few things that might be wrong with your code that you should check out. The number one reason this message appears is because your code timed out. You only get 30 seconds of computation time on our servers. If your code times out, you probably have an infinite loop. What to do? The number 1 thing to do is that you need to run this code in your own local environment. Your code should print one line at the end of the loop. If your code never prints anything out - you have an infinite loop! To debug your infinite loop - check your loop conditional. When will it stop? Try inserting print statements inside your loop that prints out information (like variables) - are you incrementing or decrementing your loop counter correctly? Search the forum for people with similar issues. If your search turns up nothing, make a new post and paste in your loop conditional for others to help you out with. Please don't email the course staff unless your code legitimately works and prints out the correct answers in your local environment. In that case, please email your code file, a screenshot of the code printing out the correct answers in your local environment, and a screenshot of the exact same code not working on the tutor. The course staff is otherwise unable to help debug your problem set via email - we can only address platform issues. " + }, + "content_type": "CAPA", + "problem_types": [ + "coderesponse" + ], + "id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader", + "start_date": "2013-02-05T00:00:00+00:00", + "content_groups": null, + "course_name": "Demonstration Course", + "location": [ + "Example Week 2: Get Interactive", + "Homework - Labs and Demos", + "Code Grader" + ], + "excerpt": "notify the course staff.\", check to be sure your code doesn't take too", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader" + }, + "score": 1.0107487 + }, + { + "_index": "courseware_content", + "_type": "_doc", + "_id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618", + "data": { + "course": "course-v1:edX+DemoX+Demo_Course", + "org": "edX", + "content": { + "display_name": "Interactive Questions", + "capa_content": " Most courses have interactive questions that test your knowledge like the one below. They can be part of a learning sequence or an exam. Notice the visual feedback. Go ahead, try it out! Questions which are part of assignments or exams may have due dates - the last possible time you can submit an assignment for grading. Once this time has passed, you will not be able to get credit for any incomplete problems in the assignment. If an assignment has a due date, you can see the due date in the sidebar. (This demo course does not have any assignments with due dates.) If no due date is displayed, the assignment can be turned in at any time. All assignment due dates are displayed in the time zone that you select in your account settings. If you do not specify a time zone, assignment due dates display in your browser's time zone. What kinds of late policies does edX allow? late penalties instructor forgiveness late time budget none of the above " + }, + "content_type": "CAPA", + "problem_types": [ + "choiceresponse" + ], + "id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618", + "start_date": "2013-02-05T05:00:00+00:00", + "content_groups": null, + "course_name": "Demonstration Course", + "location": [ + "Example Week 1: Getting Started", + "Lesson 1 - Getting Started", + "Interactive Questions" + ], + "excerpt": " Most courses have interactive questions that test your knowledge like", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618" + }, + "score": 0.96387196 + }, + { + "_index": "courseware_content", + "_type": "_doc", + "_id": "block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4", + "data": { + "course": "course-v1:edX+DemoX+Demo_Course", + "org": "edX", + "content": { + "display_name": "Blank HTML Page", + "html_content": "Welcome to the Open edX Demo Course Introduction. This is where you can explore how to take an edX course (like this one). Most courses have an \"intro\" video that shows you how it all works. You can watch the introduction video (below) or scroll though the course studies and assignments using the toolbar (above). Just for fun, we'll keep track of your work in this demo course, and show you your progress in the toolbar just like in a real course. Watch the overview video (below), then click on \"Example Week One\" in the left hand navigation to get started. " + }, + "content_type": "Text", + "id": "block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4", + "start_date": "1970-01-01T05:00:00+00:00", + "content_groups": null, + "course_name": "Demonstration Course", + "location": [ + "Introduction", + "Demo Course Overview", + "Introduction: Video and Sequences" + ], + "excerpt": "Welcome to the Open edX Demo Course Introduction. This is where you", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4" + }, + "score": 0.8844358 + }, + { + "_index": "courseware_content", + "_type": "_doc", + "_id": "block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7", + "data": { + "course": "course-v1:edX+DemoX+Demo_Course", + "org": "edX", + "content": { + "display_name": "Discussion Forums", + "html_content": "Discussion FORUMS The discussion forum for each course is found at the top of the course page. You might come across a subset of the discussion forum inside the course (see below), where you can talk with fellow students about the course in context. Go ahead and be social! Make your first post in this demo course. Keep an eye out for posts with a green check mark. The green check means the post has been recognized by a staff member or forum moderator as a great post. You can also actively upvote a post. Others can search on user \u201cupvoted\u201d posts. They tend to be very helpful. " + }, + "content_type": "Text", + "id": "block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7", + "start_date": "1978-02-05T00:00:00+00:00", + "content_groups": null, + "course_name": "Demonstration Course", + "location": [ + "Example Week 3: Be Social", + "Lesson 3 - Be Social", + "Discussion Forums" + ], + "excerpt": "Discussion FORUMS The discussion forum for each course is found at the", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7" + }, + "score": 0.8803684 + }, + { + "_index": "courseware_content", + "_type": "_doc", + "_id": "block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c", + "data": { + "course": "course-v1:edX+DemoX+Demo_Course", + "org": "edX", + "content": { + "display_name": "Overall Grade", + "html_content": " OVERALL GRADE PERFORMANCE The progress tab (selectable near the top of each page in your course) shows your performance. Click on it now, and you will see how you're doing in this demo course. The bar chart shows the overall percentage that you have earned on each assignment in the course, and how each of those assignments combine into your overall grade. Further down the page is a detailed breakdown of your score on every graded question in the class. You might notice that some of your assignments on the bar chart show an 'x'. The 'x's indicate the assignments that the edX system will NOT be counting toward your final grade, according to the course grading. The 'x's go to the assignments that you scored the lowest on. Each course has its own percentage cutoff for a Certificate of Mastery. You can see where those cutoffs are by looking at the vertical description. In this demo, a \"pass\" is considered 60%. When you \"pass\" a live edX course, you will receive a certificate after the class has closed. Sorry - the demo course does not grant certificates! " + }, + "content_type": "Text", + "id": "block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c", + "start_date": "2013-02-05T00:00:00+00:00", + "content_groups": null, + "course_name": "Demonstration Course", + "location": [ + "About Exams and Certificates", + "edX Exams", + "Overall Grade Performance" + ], + "excerpt": "of each page in your course) shows your performance. Click on it now,", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c" + }, + "score": 0.87981963 + }, + { + "_index": "courseware_content", + "_type": "_doc", + "_id": "block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339", + "data": { + "course": "course-v1:edX+DemoX+Demo_Course", + "org": "edX", + "content": { + "display_name": "Blank HTML Page", + "html_content": "Find Your Study Buddy Working with other students offline can help you get the most out of an online course and even increase the likelihood you will successfully complete the course. So, your homework is to find a study buddy. The course specific discussion forums are a great place to find neighbors or even new friends to invite to a Meetup you are looking to organize or even a virtual Google Hangout. " + }, + "content_type": "Text", + "id": "block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339", + "start_date": "1978-02-05T00:00:00+00:00", + "content_groups": null, + "course_name": "Demonstration Course", + "location": [ + "Example Week 3: Be Social", + "Lesson 3 - Be Social", + "Homework - Find Your Study Buddy" + ], + "excerpt": "get the most out of an online course and even increase the likelihood", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339" + }, + "score": 0.84284115 + }, + { + "_index": "courseware_content", + "_type": "_doc", + "_id": "block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5", + "data": { + "course": "course-v1:edX+DemoX+Demo_Course", + "org": "edX", + "content": { + "display_name": "Find Your Study Buddy", + "html_content": "Find Your Study Buddy Working with other students offline can help you get the most out of an online course and even increase the likelihood you will successfully complete the course. So, your homework is to find a study buddy. The course specific discussion forums are a great place to find neighbors or even new friends to invite to a Meetup you are looking to organize or even a virtual Google Hangout. " + }, + "content_type": "Text", + "id": "block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5", + "start_date": "2013-02-05T05:00:00+00:00", + "content_groups": null, + "course_name": "Demonstration Course", + "location": [ + "Example Week 3: Be Social", + "Homework - Find Your Study Buddy", + "Homework - Find Your Study Buddy" + ], + "excerpt": "get the most out of an online course and even increase the likelihood", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5" + }, + "score": 0.84284115 + }, + { + "_index": "courseware_content", + "_type": "_doc", + "_id": "block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0", + "data": { + "course": "course-v1:edX+DemoX+Demo_Course", + "org": "edX", + "content": { + "display_name": "Be Social", + "html_content": "Be SOCIAL A big part of learning online includes \u201cbeing social.\u201d We encourage all students to communicate within the course discussion forums \u2013 a great place to connect with other students and to get support from the course staff. Some students and professors also engage through other social mediums like Meetup or Facebook. Recent research has found that if you take a class with a friend, or engage socially with other learners while taking a course, there is a higher likelihood that you will complete a course. If you haven\u2019t already, consider finding a study buddy! Check out more information about the discussion forum by navigating to the next item in this learning sequence. In the discussion forums, remember to be polite and respectful. Simply put, treat others the way you want to be treated. " + }, + "content_type": "Text", + "id": "block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0", + "start_date": "1978-02-05T00:00:00+00:00", + "content_groups": null, + "course_name": "Demonstration Course", + "location": [ + "Example Week 3: Be Social", + "Lesson 3 - Be Social", + "Be Social" + ], + "excerpt": "encourage all students to communicate within the course discussion", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0" + }, + "score": 0.84210813 + }, + { + "_index": "courseware_content", + "_type": "_doc", + "_id": "block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530", + "data": { + "course": "course-v1:edX+DemoX+Demo_Course", + "org": "edX", + "content": { + "display_name": "EdX Exams", + "html_content": " EDX EXAMS Not all edX courses have exams; many do, but not all. When choosing a course, it's a good idea to check the exam and study requirements, as well as any prerequisites. Of course - you can \"audit\" any edX course, which means you can study alongside other students using the same content, tools and materials, but you're not focused on grades and might skip the exams and assignments. Follow this learning sequence via the links above to understand more about how we grade your work and track your progress. " + }, + "content_type": "Text", + "id": "block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530", + "start_date": "2013-02-05T00:00:00+00:00", + "content_groups": null, + "course_name": "Demonstration Course", + "location": [ + "About Exams and Certificates", + "edX Exams", + "EdX Exams" + ], + "excerpt": " EDX EXAMS Not all edX courses have exams; many do, but not all. When", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530" + }, + "score": 0.8306555 + }, + { + "_index": "courseware_content", + "_type": "_doc", + "_id": "block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf", + "data": { + "course": "course-v1:edX+DemoX+Demo_Course", + "org": "edX", + "content": { + "display_name": "When Are Your Exams? ", + "html_content": "WHEN ARE YOUR Exams? Every course treats the timing on its exams differently, and you should be really careful to pay attention to any announcements about exam timing that your course makes. " + }, + "content_type": "Text", + "id": "block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf", + "start_date": "2013-02-05T05:00:00+00:00", + "content_groups": null, + "course_name": "Demonstration Course", + "location": [ + "Example Week 1: Getting Started", + "Lesson 1 - Getting Started", + "When Are Your Exams? " + ], + "excerpt": "WHEN ARE YOUR Exams? Every course treats the timing on its exams", + "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf" + }, + "score": 0.82610154 + } + ], + "access_denied_count": 0 +} \ No newline at end of file diff --git a/src/course-home/courseware-search/test-data/mockedResults.js b/src/course-home/courseware-search/test-data/mockedResults.js deleted file mode 100644 index 66cfbbd6d3..0000000000 --- a/src/course-home/courseware-search/test-data/mockedResults.js +++ /dev/null @@ -1,66 +0,0 @@ -// Test data for testing the CoursewareSearchResults UI component. - -const mockedResults = [{ - type: 'document', - title: 'A Comprehensive Introduction to Supply Chain Analytics', - href: 'https://www.edx.org/', - isExternal: true, -}, { - type: 'document', - title: 'Basics of Data Collection for Supply Chain Analytics: Exploring Methods and Techniques for Optimal Data Gathering', - breadcrumbs: ['A Comprehensive Introduction to Supply Chain Analytics'], - href: 'https://www.edx.org/', - isExternal: true, -}, { - type: 'document', - title: 'Zero-Waste Strategies in Supply Chain Management', - breadcrumbs: [ - 'A Comprehensive Introduction to Supply Chain Analytics', - 'Basics of Data Collection for Supply Chain Analytics: Exploring Methods and Techniques for Optimal Data Gathering', - ], - href: '/', -}, { - type: 'text', - title: 'Addressing Overproduction and Excess Inventory in Supply Chains', - breadcrumbs: [ - 'A Comprehensive Introduction to Supply Chain Analytics', - 'Basics of Data Collection for Supply Chain Analytics: Exploring Methods and Techniques for Optimal Data Gathering', - 'Zero-Waste Strategies in Supply Chain Management', - ], - href: '/', -}, { - type: 'text', - title: 'Balancing Supply and Demand', - breadcrumbs: [ - 'Strategic Sourcing and Its Impact on Supply-Demand Balance', - 'Dealing with Over-supply and Under-supply Situations', - 'Scenario Planning for Uncertain Supply-Demand Conditions', - ], - contentMatches: 9, - href: '/', -}, { - type: 'text', - title: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin ornare porttitor purus, eget vehicula lorem ullamcorper in. In pellentesque vehicula diam, eget efficitur nisl aliquet id. Donec tincidunt dictum odio quis placerat.', - breadcrumbs: ['Section name', 'Subsection name', 'Unit name'], - contentMatches: 6, - href: '/', -}, { - type: 'video', - title: 'TextSupply chain toolbox', - breadcrumbs: ['Section name', 'Subsection name', 'Unit name'], - href: '/', -}, { - type: 'video', - title: 'Utilizing Demand-Driven Strategies', - breadcrumbs: ['Section name', 'Subsection name', 'Unit name'], - contentMatches: 20, - href: '/', -}, { - type: 'video', - title: 'Video', - breadcrumbs: ['Section name', 'Subsection name', 'Unit name'], - contentMatches: 1, - href: '/', -}]; - -export default mockedResults; diff --git a/src/course-home/data/thunks.js b/src/course-home/data/thunks.js index d640660994..6e0d972530 100644 --- a/src/course-home/data/thunks.js +++ b/src/course-home/data/thunks.js @@ -156,42 +156,57 @@ export async function fetchCoursewareSearchSettings(courseId) { export function searchCourseContent(courseId, searchKeyword) { return async (dispatch) => { const start = new Date(); + dispatch(addModel({ modelType: 'contentSearchResults', model: { id: courseId, searchKeyword, results: [], + errors: undefined, loading: true, }, })); - searchCourseContentFromAPI(courseId, searchKeyword).then(response => { - const { data } = response; - dispatch(updateModel({ - modelType: 'contentSearchResults', - model: { - id: courseId, - searchKeyword, - ...mapSearchResponse(data, searchKeyword), - loading: false, - }, - })); - const end = new Date(); - const clientMs = (end - start); - const { - took, total, maxScore, accessDeniedCount, - } = data; + let data; + let curatedResponse; + let errors; + try { + ({ data } = await searchCourseContentFromAPI(courseId, searchKeyword)); + curatedResponse = mapSearchResponse(data, searchKeyword); + } catch (e) { // TODO: Remove when publishing to prod. Just temporary for performance debugging. // eslint-disable-next-line no-console - console.table({ - 'Search Keyword': searchKeyword, - 'Client time (ms)': clientMs, - 'Server time (ms)': took, - 'Total matches': total, - 'Max score': maxScore, - 'Access denied count': accessDeniedCount, - }); + console.error('Error on Courseware Search: ', e.message); + errors = e.message; + } + + dispatch(updateModel({ + modelType: 'contentSearchResults', + model: { + ...curatedResponse, + id: courseId, + searchKeyword, + errors, + loading: false, + }, + })); + + const end = new Date(); + const clientMs = (end - start); + const { + took, total, maxScore, accessDeniedCount, + } = data; + + // TODO: Remove when publishing to prod. Just temporary for performance debugging. + // eslint-disable-next-line no-console + console.table({ + 'Search Keyword': searchKeyword, + 'Client time (ms)': clientMs, + 'Server time (ms)': took, + 'Total matches': total, + 'Max score': maxScore, + 'Access denied count': accessDeniedCount, }); }; } From 71e5e50a138d7749e87eb277970ac5125986714e Mon Sep 17 00:00:00 2001 From: German Date: Wed, 8 Nov 2023 11:38:01 -0300 Subject: [PATCH 3/6] fix: remove console log --- src/course-home/courseware-search/CoursewareResultsFilter.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/course-home/courseware-search/CoursewareResultsFilter.jsx b/src/course-home/courseware-search/CoursewareResultsFilter.jsx index 8e1d303ccc..c64810d150 100644 --- a/src/course-home/courseware-search/CoursewareResultsFilter.jsx +++ b/src/course-home/courseware-search/CoursewareResultsFilter.jsx @@ -22,8 +22,6 @@ export const CoursewareSearchResultsFilter = ({ intl }) => { const { total, results } = lastSearch; - console.log({ results }); - const filters = [ { key: noFilterKey, From a4b24bb0a8a16dc37cf4057af24770be8382e949 Mon Sep 17 00:00:00 2001 From: German Date: Wed, 8 Nov 2023 18:54:44 -0300 Subject: [PATCH 4/6] fix: update tests --- .../CoursewareResultsFilter.jsx | 3 +- .../CoursewareResultsFilter.test.jsx | 94 ++-- .../courseware-search/CoursewareSearch.jsx | 5 +- .../CoursewareSearch.test.jsx | 73 ++- .../CoursewareSearchResults.jsx | 3 +- .../CoursewareSearchResults.test.jsx.snap | 513 ------------------ src/course-tabs/CourseTabsNavigation.jsx | 4 +- 7 files changed, 117 insertions(+), 578 deletions(-) delete mode 100644 src/course-home/courseware-search/__snapshots__/CoursewareSearchResults.test.jsx.snap diff --git a/src/course-home/courseware-search/CoursewareResultsFilter.jsx b/src/course-home/courseware-search/CoursewareResultsFilter.jsx index c64810d150..edf74a1a6e 100644 --- a/src/course-home/courseware-search/CoursewareResultsFilter.jsx +++ b/src/course-home/courseware-search/CoursewareResultsFilter.jsx @@ -18,7 +18,7 @@ export const CoursewareSearchResultsFilter = ({ intl }) => { const { courseId } = useParams(); const lastSearch = useModel('contentSearchResults', courseId); - if (!lastSearch || !lastSearch.results.length) { return null; } + if (!lastSearch || !lastSearch?.results?.length) { return null; } const { total, results } = lastSearch; @@ -47,7 +47,6 @@ export const CoursewareSearchResultsFilter = ({ intl }) => { {filters.map(({ key, label }) => ( diff --git a/src/course-home/courseware-search/CoursewareResultsFilter.test.jsx b/src/course-home/courseware-search/CoursewareResultsFilter.test.jsx index 8052ab6cba..0ca7faea62 100644 --- a/src/course-home/courseware-search/CoursewareResultsFilter.test.jsx +++ b/src/course-home/courseware-search/CoursewareResultsFilter.test.jsx @@ -1,4 +1,7 @@ import React from 'react'; +import { AppProvider } from '@edx/frontend-platform/react'; +import { Route, Routes } from 'react-router-dom'; +import { history } from '@edx/frontend-platform'; import { initializeMockApp, render, @@ -6,16 +9,56 @@ import { waitFor, } from '../../setupTest'; import { CoursewareSearchResultsFilter, filteredResultsBySelection } from './CoursewareResultsFilter'; +import initializeStore from '../../store'; +import { useModel } from '../../generic/model-store'; + +jest.mock('../../generic/model-store', () => ({ + useModel: jest.fn(), +})); const mockResults = [ - { type: 'video', title: 'video_title' }, - { type: 'video', title: 'video_title2' }, - { type: 'document', title: 'document_title' }, - { type: 'text', title: 'text_title1' }, - { type: 'text', title: 'text_title2' }, - { type: 'text', title: 'text_title3' }, + { + id: 'video-1', type: 'video', title: 'video_title', score: 3, contentHits: 1, url: '/video-1', location: ['path1', 'path2'], + }, + { + id: 'video-2', type: 'video', title: 'video_title2', score: 2, contentHits: 1, url: '/video-2', location: ['path1', 'path2'], + }, + { + id: 'document-1', type: 'document', title: 'document_title', score: 3, contentHits: 1, url: '/document-1', location: ['path1', 'path2'], + }, + { + id: 'text-1', type: 'text', title: 'text_title1', score: 3, contentHits: 1, url: '/text-1', location: ['path1', 'path2'], + }, + { + id: 'text-2', type: 'text', title: 'text_title2', score: 2, contentHits: 1, url: '/text-2', location: ['path1', 'path2'], + }, + { + id: 'text-3', type: 'text', title: 'text_title3', score: 1, contentHits: 1, url: '/text-3', location: ['path1', 'path2'], + }, ]; +const decodedCourseId = 'course-v1:edX+DemoX+Demo_Course'; +const decodedSequenceId = 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction'; +const decodedUnitId = 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc'; +const pathname = `/course/${decodedCourseId}/${decodedSequenceId}/${decodedUnitId}`; + +const intl = { + formatMessage: (message) => message?.defaultMessage || '', +}; + +function renderComponent(props = {}) { + const store = initializeStore(); + history.push(pathname); + const { container } = render( + + + } /> + + , + ); + return container; +} + describe('CoursewareSearchResultsFilter', () => { beforeAll(initializeMockApp); @@ -32,53 +75,42 @@ describe('CoursewareSearchResultsFilter', () => { expect(results.length).toEqual(mockResults.length); }); - it('returns all values when the key value "all" is provided', () => { - const results = filteredResultsBySelection({ filterKey: 'all', results: mockResults }); - - expect(results.length).toEqual(mockResults.length); - }); - it('returns only "video"-typed elements when the key value "video" is given', () => { - const results = filteredResultsBySelection({ filterKey: 'video', results: mockResults }); + const results = filteredResultsBySelection({ key: 'video', results: mockResults }); expect(results.length).toEqual(2); }); it('returns only "course_outline"-typed elements when the key value "document" is given', () => { - const results = filteredResultsBySelection({ filterKey: 'document', results: mockResults }); + const results = filteredResultsBySelection({ key: 'document', results: mockResults }); expect(results.length).toEqual(1); }); it('returns only "text"-typed elements when the key value "text" is given', () => { - const results = filteredResultsBySelection({ filterKey: 'text', results: mockResults }); + const results = filteredResultsBySelection({ key: 'text', results: mockResults }); expect(results.length).toEqual(3); }); }); describe('', () => { - it('should render', async () => { - await render(); + beforeEach(() => { + jest.clearAllMocks(); + }); - await waitFor(() => { - expect(screen.queryByTestId('courseware-search-results-tabs')).toBeInTheDocument(); - expect(screen.queryByText(/All content/)).toBeInTheDocument(); - expect(screen.queryByText(/Course outline/)).toBeInTheDocument(); - expect(screen.queryByText(/Text/)).toBeInTheDocument(); - expect(screen.queryByText(/Video/)).toBeInTheDocument(); + it('should render', async () => { + useModel.mockReturnValue({ + total: 6, + results: mockResults, + filters: [], }); - }); - it('should not render if no results are provided', async () => { - await render(); + await renderComponent(); await waitFor(() => { - expect(screen.queryByTestId('courseware-search-results-tabs')).not.toBeInTheDocument(); - expect(screen.queryByText(/All content/)).not.toBeInTheDocument(); - expect(screen.queryByText(/Course outline/)).not.toBeInTheDocument(); - expect(screen.queryByText(/Text/)).not.toBeInTheDocument(); - expect(screen.queryByText(/Video/)).not.toBeInTheDocument(); + expect(screen.queryByTestId('courseware-search-results-tabs')).toBeInTheDocument(); + expect(screen.queryByText(/All content/)).toBeInTheDocument(); }); }); }); diff --git a/src/course-home/courseware-search/CoursewareSearch.jsx b/src/course-home/courseware-search/CoursewareSearch.jsx index b9611a4903..1aa7f9a213 100644 --- a/src/course-home/courseware-search/CoursewareSearch.jsx +++ b/src/course-home/courseware-search/CoursewareSearch.jsx @@ -18,7 +18,7 @@ import CoursewareSearchResultsFilterContainer from './CoursewareResultsFilter'; import { updateModel, useModel } from '../../generic/model-store'; import { searchCourseContent } from '../data/thunks'; -const CoursewareSearch = ({ intl, ...sectionProps }) => { +const CoursewareSearch = ({ intl }) => { const { courseId } = useParams(); const dispatch = useDispatch(); const { org } = useModel('courseHomeMeta', courseId); @@ -88,7 +88,7 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => { } return ( -
+
); }; + CoursewareSearch.propTypes = { intl: intlShape.isRequired, }; diff --git a/src/course-home/courseware-search/CoursewareSearch.test.jsx b/src/course-home/courseware-search/CoursewareSearch.test.jsx index 276ff1a194..6c8ab76d02 100644 --- a/src/course-home/courseware-search/CoursewareSearch.test.jsx +++ b/src/course-home/courseware-search/CoursewareSearch.test.jsx @@ -1,36 +1,61 @@ import React from 'react'; +import { history } from '@edx/frontend-platform'; +import { AppProvider } from '@edx/frontend-platform/react'; +import { Route, Routes } from 'react-router-dom'; import { - fireEvent, initializeMockApp, render, screen, } from '../../setupTest'; import { CoursewareSearch } from './index'; -import { setShowSearch } from '../data/slice'; import { useElementBoundingBox, useLockScroll } from './hooks'; +import initializeStore from '../../store'; +import { useModel } from '../../generic/model-store'; -const mockDispatch = jest.fn(); - -jest.mock('./hooks'); -jest.mock('../data/slice'); -jest.mock('react-redux', () => ({ - ...jest.requireActual('react-redux'), - useDispatch: () => mockDispatch, +jest.mock('../../generic/model-store', () => ({ + useModel: jest.fn(), })); +const decodedCourseId = 'course-v1:edX+DemoX+Demo_Course'; +const decodedSequenceId = 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction'; +const decodedUnitId = 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc'; +const pathname = `/course/${decodedCourseId}/${decodedSequenceId}/${decodedUnitId}`; + const tabsTopPosition = 128; +const defaultProps = { + org: 'edX', + loading: false, + searchKeyword: '', + errors: undefined, + total: 0, +}; + +const intl = { + formatMessage: (message) => message?.defaultMessage || '', +}; + function renderComponent(props = {}) { - const { container } = render(); + const store = initializeStore(); + history.push(pathname); + const { container } = render( + + + } /> + + , + ); return container; } +const mockModels = ((props = defaultProps) => { + useModel.mockReturnValue(props); +}); + describe('CoursewareSearch', () => { - beforeAll(async () => { - initializeMockApp(); - }); + beforeAll(initializeMockApp); - afterEach(() => { + beforeEach(() => { jest.clearAllMocks(); }); @@ -39,33 +64,28 @@ describe('CoursewareSearch', () => { useElementBoundingBox.mockImplementation(() => ({ top: tabsTopPosition })); }); - beforeEach(() => { + it('Should use useElementBoundingBox() and useLockScroll() hooks', () => { + mockModels(); renderComponent(); - }); - it('Should use useElementBoundingBox() and useLockScroll() hooks', () => { expect(useElementBoundingBox).toBeCalledTimes(1); expect(useLockScroll).toBeCalledTimes(1); }); it('Should have a "--modal-top-position" CSS variable matching the CourseTabsNavigation top position', () => { + mockModels(); + renderComponent(); + const section = screen.getByTestId('courseware-search-section'); expect(section.style.getPropertyValue('--modal-top-position')).toBe(`${tabsTopPosition}px`); }); - - it('Should dispatch setShowSearch(true) when clicking the close button', () => { - const button = screen.getByTestId('courseware-search-close-button'); - fireEvent.click(button); - - expect(mockDispatch).toHaveBeenCalledTimes(1); - expect(setShowSearch).toHaveBeenCalledTimes(1); - expect(setShowSearch).toHaveBeenCalledWith(false); - }); }); describe('when CourseTabsNavigation is not present', () => { it('Should use "--modal-top-position: 0" if nce element is not present', () => { useElementBoundingBox.mockImplementation(() => undefined); + + mockModels(); renderComponent(); const section = screen.getByTestId('courseware-search-section'); @@ -75,6 +95,7 @@ describe('CoursewareSearch', () => { describe('when passing extra props', () => { it('Should pass on extra props to section element', () => { + mockModels(); renderComponent({ foo: 'bar' }); const section = screen.getByTestId('courseware-search-section'); diff --git a/src/course-home/courseware-search/CoursewareSearchResults.jsx b/src/course-home/courseware-search/CoursewareSearchResults.jsx index 126e9cf3dc..a7be9c7735 100644 --- a/src/course-home/courseware-search/CoursewareSearchResults.jsx +++ b/src/course-home/courseware-search/CoursewareSearchResults.jsx @@ -12,6 +12,7 @@ const iconTypeMapping = { text: TextFields, video: VideoCamera, }; + const defaultIcon = Article; const CoursewareSearchResults = ({ results }) => { @@ -57,7 +58,7 @@ const CoursewareSearchResults = ({ results }) => { // The breadcrumbs are not expected to change. // eslint-disable-next-line react/no-array-index-key location.map((bc, i) => (
  • {bc}
  • )) -} + } ) : null}
    diff --git a/src/course-home/courseware-search/__snapshots__/CoursewareSearchResults.test.jsx.snap b/src/course-home/courseware-search/__snapshots__/CoursewareSearchResults.test.jsx.snap deleted file mode 100644 index 25f3541274..0000000000 --- a/src/course-home/courseware-search/__snapshots__/CoursewareSearchResults.test.jsx.snap +++ /dev/null @@ -1,513 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CoursewareSearchResults when list of results is provided should match the snapshot 1`] = ` -
    - -
    - - - -
    -
    -
    - - A Comprehensive Introduction to Supply Chain Analytics - -
    -
    -
    - -
    - - - -
    -
    -
    - - Basics of Data Collection for Supply Chain Analytics: Exploring Methods and Techniques for Optimal Data Gathering - -
    -
      -
    • -
      - A Comprehensive Introduction to Supply Chain Analytics -
      -
    • -
    -
    -
    - -
    - - - -
    -
    -
    - - Zero-Waste Strategies in Supply Chain Management - -
    -
      -
    • -
      - A Comprehensive Introduction to Supply Chain Analytics -
      -
    • -
    • -
      - Basics of Data Collection for Supply Chain Analytics: Exploring Methods and Techniques for Optimal Data Gathering -
      -
    • -
    -
    -
    - -
    - - - -
    -
    -
    - - Addressing Overproduction and Excess Inventory in Supply Chains - -
    -
      -
    • -
      - A Comprehensive Introduction to Supply Chain Analytics -
      -
    • -
    • -
      - Basics of Data Collection for Supply Chain Analytics: Exploring Methods and Techniques for Optimal Data Gathering -
      -
    • -
    • -
      - Zero-Waste Strategies in Supply Chain Management -
      -
    • -
    -
    -
    - -
    - - - -
    -
    -
    - - Balancing Supply and Demand - - - 9 - -
    -
      -
    • -
      - Strategic Sourcing and Its Impact on Supply-Demand Balance -
      -
    • -
    • -
      - Dealing with Over-supply and Under-supply Situations -
      -
    • -
    • -
      - Scenario Planning for Uncertain Supply-Demand Conditions -
      -
    • -
    -
    -
    - -
    - - - -
    -
    -
    - - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin ornare porttitor purus, eget vehicula lorem ullamcorper in. In pellentesque vehicula diam, eget efficitur nisl aliquet id. Donec tincidunt dictum odio quis placerat. - - - 6 - -
    -
      -
    • -
      - Section name -
      -
    • -
    • -
      - Subsection name -
      -
    • -
    • -
      - Unit name -
      -
    • -
    -
    -
    - -
    - - - -
    -
    -
    - - TextSupply chain toolbox - -
    -
      -
    • -
      - Section name -
      -
    • -
    • -
      - Subsection name -
      -
    • -
    • -
      - Unit name -
      -
    • -
    -
    -
    - -
    - - - -
    -
    -
    - - Utilizing Demand-Driven Strategies - - - 20 - -
    -
      -
    • -
      - Section name -
      -
    • -
    • -
      - Subsection name -
      -
    • -
    • -
      - Unit name -
      -
    • -
    -
    -
    - -
    - - - -
    -
    -
    - - Video - - - 1 - -
    -
      -
    • -
      - Section name -
      -
    • -
    • -
      - Subsection name -
      -
    • -
    • -
      - Unit name -
      -
    • -
    -
    -
    -
    -`; diff --git a/src/course-tabs/CourseTabsNavigation.jsx b/src/course-tabs/CourseTabsNavigation.jsx index 87281a45be..014f1269a5 100644 --- a/src/course-tabs/CourseTabsNavigation.jsx +++ b/src/course-tabs/CourseTabsNavigation.jsx @@ -34,9 +34,7 @@ const CourseTabsNavigation = ({ ))}
    - {show ? ( - - ) : null} + {show && }
    ); }; From e7fd09cff6f3cfae793ae5ef43b12355541fd178 Mon Sep 17 00:00:00 2001 From: German Date: Thu, 9 Nov 2023 11:46:56 -0300 Subject: [PATCH 5/6] fix: update tests --- src/course-home/courseware-search/CoursewareSearch.jsx | 4 ++-- src/course-home/courseware-search/CoursewareSearch.test.jsx | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/course-home/courseware-search/CoursewareSearch.jsx b/src/course-home/courseware-search/CoursewareSearch.jsx index 1aa7f9a213..cd6b49a3c8 100644 --- a/src/course-home/courseware-search/CoursewareSearch.jsx +++ b/src/course-home/courseware-search/CoursewareSearch.jsx @@ -18,7 +18,7 @@ import CoursewareSearchResultsFilterContainer from './CoursewareResultsFilter'; import { updateModel, useModel } from '../../generic/model-store'; import { searchCourseContent } from '../data/thunks'; -const CoursewareSearch = ({ intl }) => { +const CoursewareSearch = ({ intl, ...sectionProps }) => { const { courseId } = useParams(); const dispatch = useDispatch(); const { org } = useModel('courseHomeMeta', courseId); @@ -88,7 +88,7 @@ const CoursewareSearch = ({ intl }) => { } return ( -
    +