// @ts-check import { camelCaseObject, getConfig } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; export const getCourseOutlineIndexApiUrl = (courseId) => `${getApiBaseUrl()}/api/contentstore/v1/course_index/${courseId}`; export const getCourseBestPracticesApiUrl = ({ courseId, excludeGraded, all, }) => `${getApiBaseUrl()}/api/courses/v1/quality/${courseId}/?exclude_graded=${excludeGraded}&all=${all}`; export const getCourseLaunchApiUrl = ({ courseId, gradedOnly, validateOras, all, }) => `${getApiBaseUrl()}/api/courses/v1/validation/${courseId}/?graded_only=${gradedOnly}&validate_oras=${validateOras}&all=${all}`; export const getCourseBlockApiUrl = (courseId) => { const formattedCourseId = courseId.split('course-v1:')[1]; return `${getApiBaseUrl()}/xblock/block-v1:${formattedCourseId}+type@course+block@course`; }; export const getCourseReindexApiUrl = (reindexLink) => `${getApiBaseUrl()}${reindexLink}`; export const getXBlockBaseApiUrl = () => `${getApiBaseUrl()}/xblock/`; export const getCourseItemApiUrl = (itemId) => `${getXBlockBaseApiUrl()}${itemId}`; export const getXBlockApiUrl = (blockId) => `${getXBlockBaseApiUrl()}outline/${blockId}`; export const exportTags = (courseId) => `${getApiBaseUrl()}/api/content_tagging/v1/object_tags/${courseId}/export/`; /** * @typedef {Object} courseOutline * @property {string} courseReleaseDate * @property {Object} courseStructure * @property {Object} deprecatedBlocksInfo * @property {string} discussionsIncontextLearnmoreUrl * @property {Object} initialState * @property {Object} initialUserClipboard * @property {string} languageCode * @property {string} lmsLink * @property {string} mfeProctoredExamSettingsUrl * @property {string} notificationDismissUrl * @property {string[]} proctoringErrors * @property {string} reindexLink * @property {null} rerunNotificationId */ /** * Get course outline index. * @param {string} courseId * @returns {Promise} */ export async function getCourseOutlineIndex(courseId) { const { data } = await getAuthenticatedHttpClient() .get(getCourseOutlineIndexApiUrl(courseId)); return camelCaseObject(data); } /** * * @param courseId * @returns {Promise} */ export async function createDiscussionsTopics(courseId) { const { data } = await getAuthenticatedHttpClient() .post(`${getApiBaseUrl()}/api/discussions/v0/course/${courseId}/sync_discussion_topics`); return camelCaseObject(data); } /** * Get course best practices. * @param {{courseId: string, excludeGraded: boolean, all: boolean}} options * @returns {Promise<{isSelfPaced: boolean, sections: any, subsection: any, units: any, videos: any }>} */ export async function getCourseBestPractices({ courseId, excludeGraded, all, }) { const { data } = await getAuthenticatedHttpClient() .get(getCourseBestPracticesApiUrl({ courseId, excludeGraded, all })); return camelCaseObject(data); } /** @typedef {object} courseLaunchData * @property {boolean} isSelfPaced * @property {object} dates * @property {object} assignments * @property {object} grades * @property {number} grades.sum_of_weights * @property {object} certificates * @property {object} updates * @property {object} proctoring */ /** * Get course launch. * @param {{courseId: string, gradedOnly: boolean, validateOras: boolean, all: boolean}} options * @returns {Promise} */ export async function getCourseLaunch({ courseId, gradedOnly, validateOras, all, }) { const { data } = await getAuthenticatedHttpClient() .get(getCourseLaunchApiUrl({ courseId, gradedOnly, validateOras, all, })); return camelCaseObject(data); } /** * Enable course highlights emails * @param {string} courseId * @returns {Promise} */ export async function enableCourseHighlightsEmails(courseId) { const { data } = await getAuthenticatedHttpClient() .post(getCourseBlockApiUrl(courseId), { publish: 'republish', metadata: { highlights_enabled_for_messaging: true, }, }); return data; } /** * Restart reindex course * @param {string} reindexLink * @returns {Promise} */ export async function restartIndexingOnCourse(reindexLink) { const { data } = await getAuthenticatedHttpClient() .get(getCourseReindexApiUrl(reindexLink)); return camelCaseObject(data); } /** * @typedef {Object} section * @property {string} id * @property {string} displayName * @property {string} category * @property {boolean} hasChildren * @property {string} editedOn * @property {boolean} published * @property {string} publishedOn * @property {string} studioUrl * @property {boolean} releasedToStudents * @property {string} releaseDate * @property {string} visibilityState * @property {boolean} hasExplicitStaffLock * @property {string} start * @property {boolean} graded * @property {string} dueDate * @property {null} due * @property {null} relativeWeeksDue * @property {null} format * @property {string[]} courseGraders * @property {boolean} hasChanges * @property {object} actions * @property {null} explanatoryMessage * @property {object[]} userPartitions * @property {string} showCorrectness * @property {string[]} highlights * @property {boolean} highlightsEnabled * @property {boolean} highlightsPreviewOnly * @property {string} highlightsDocUrl * @property {object} childInfo * @property {boolean} ancestorHasStaffLock * @property {boolean} staffOnlyMessage * @property {boolean} hasPartitionGroupComponents * @property {object} userPartitionInfo * @property {boolean} enableCopyPasteUnits */ /** * Get course section * @param {string} itemId * @returns {Promise
} */ export async function getCourseItem(itemId) { const { data } = await getAuthenticatedHttpClient() .get(getXBlockApiUrl(itemId)); return camelCaseObject(data); } /** * Update course section highlights * @param {string} sectionId * @param {Array} highlights * @returns {Promise} */ export async function updateCourseSectionHighlights(sectionId, highlights) { const { data } = await getAuthenticatedHttpClient() .post(getCourseItemApiUrl(sectionId), { publish: 'republish', metadata: { highlights, }, }); return data; } /** * Publish course section * @param {string} sectionId * @returns {Promise} */ export async function publishCourseSection(sectionId) { const { data } = await getAuthenticatedHttpClient() .post(getCourseItemApiUrl(sectionId), { publish: 'make_public', }); return data; } /** * Configure course section * @param {string} sectionId * @param {boolean} isVisibleToStaffOnly * @param {string} startDatetime * @returns {Promise} */ export async function configureCourseSection(sectionId, isVisibleToStaffOnly, startDatetime) { const { data } = await getAuthenticatedHttpClient() .post(getCourseItemApiUrl(sectionId), { publish: 'republish', metadata: { // The backend expects metadata.visible_to_staff_only to either true or null visible_to_staff_only: isVisibleToStaffOnly ? true : null, start: startDatetime, }, }); return data; } /** * Configure course section * @param {string} itemId * @param {string} isVisibleToStaffOnly * @param {string} releaseDate * @param {string} graderType * @param {string} dueDate * @param {boolean} isProctoredExam, * @param {boolean} isOnboardingExam, * @param {boolean} isPracticeExam, * @param {string} examReviewRules, * @param {boolean} isTimeLimited * @param {number} defaultTimeLimitMin * @param {string} hideAfterDue * @param {string} showCorrectness * @param {boolean} isPrereq, * @param {string} prereqUsageKey, * @param {number} prereqMinScore, * @param {number} prereqMinCompletion, * @returns {Promise} */ export async function configureCourseSubsection( itemId, isVisibleToStaffOnly, releaseDate, graderType, dueDate, isTimeLimited, isProctoredExam, isOnboardingExam, isPracticeExam, examReviewRules, defaultTimeLimitMin, hideAfterDue, showCorrectness, isPrereq, prereqUsageKey, prereqMinScore, prereqMinCompletion, ) { const { data } = await getAuthenticatedHttpClient() .post(getCourseItemApiUrl(itemId), { publish: 'republish', graderType, isPrereq, prereqUsageKey, prereqMinScore, prereqMinCompletion, metadata: { // The backend expects metadata.visible_to_staff_only to either true or null visible_to_staff_only: isVisibleToStaffOnly ? true : null, due: dueDate, hide_after_due: hideAfterDue, show_correctness: showCorrectness, is_practice_exam: isPracticeExam, is_time_limited: isTimeLimited, is_proctored_enabled: isProctoredExam || isPracticeExam || isOnboardingExam, exam_review_rules: examReviewRules, default_time_limit_minutes: defaultTimeLimitMin, is_onboarding_exam: isOnboardingExam, start: releaseDate, }, }); return data; } /** * Configure course unit * @param {string} unitId * @param {boolean} isVisibleToStaffOnly * @param {object} groupAccess * @returns {Promise} */ export async function configureCourseUnit(unitId, isVisibleToStaffOnly, groupAccess, discussionEnabled) { const { data } = await getAuthenticatedHttpClient() .post(getCourseItemApiUrl(unitId), { publish: 'republish', metadata: { // The backend expects metadata.visible_to_staff_only to either true or null visible_to_staff_only: isVisibleToStaffOnly ? true : null, group_access: groupAccess, discussion_enabled: discussionEnabled, }, }); return data; } /** * Edit course section * @param {string} itemId * @param {string} displayName * @returns {Promise} */ export async function editItemDisplayName(itemId, displayName) { const { data } = await getAuthenticatedHttpClient() .post(getCourseItemApiUrl(itemId), { metadata: { display_name: displayName, }, }); return data; } /** * Delete course section * @param {string} itemId * @returns {Promise} */ export async function deleteCourseItem(itemId) { const { data } = await getAuthenticatedHttpClient() .delete(getCourseItemApiUrl(itemId)); return data; } /** * Duplicate course section * @param {string} itemId * @param {string} parentId * @returns {Promise} */ export async function duplicateCourseItem(itemId, parentId) { const { data } = await getAuthenticatedHttpClient() .post(getXBlockBaseApiUrl(), { duplicate_source_locator: itemId, parent_locator: parentId, }); return data; } /** * Add new course item like section, subsection or unit. * @param {string} parentLocator * @param {string} category * @param {string} displayName * @returns {Promise} */ export async function addNewCourseItem(parentLocator, category, displayName) { const { data } = await getAuthenticatedHttpClient() .post(getXBlockBaseApiUrl(), { parent_locator: parentLocator, category, display_name: displayName, }); return data; } /** * Set order for the list of the sections * @param {string} courseId * @param {Array} children list of sections id's * @returns {Promise} */ export async function setSectionOrderList(courseId, children) { const { data } = await getAuthenticatedHttpClient() .put(getCourseBlockApiUrl(courseId), { children, }); return data; } /** * Set order for the list of the subsections * @param {string} itemId Subsection or unit ID * @param {Array} children list of sections id's * @returns {Promise} */ export async function setCourseItemOrderList(itemId, children) { const { data } = await getAuthenticatedHttpClient() .put(getCourseItemApiUrl(itemId), { children, }); return data; } /** * Set video sharing setting * @param {string} courseId * @param {string} videoSharingOption * @returns {Promise} */ export async function setVideoSharingOption(courseId, videoSharingOption) { const { data } = await getAuthenticatedHttpClient() .post(getCourseBlockApiUrl(courseId), { metadata: { video_sharing_options: videoSharingOption, }, }); return data; } /** * Paste block to clipboard * @param {string} parentLocator * @returns {Promise} */ export async function pasteBlock(parentLocator) { const { data } = await getAuthenticatedHttpClient() .post(getXBlockBaseApiUrl(), { parent_locator: parentLocator, staged_content: 'clipboard', }); return camelCaseObject(data); } /** * Dismiss notification * @param {string} url * @returns void */ export async function dismissNotification(url) { await getAuthenticatedHttpClient() .delete(url); } /** * Downloads the file of the exported tags * @param {string} courseId The ID of the content * @returns void */ export async function getTagsExportFile(courseId, courseName) { // Gets exported tags and builds the blob to download CSV file. // This can be done with this code: // `window.location.href = exportTags(contentId);` // but it is done in this way so we know when the operation ends to close the toast. const response = await getAuthenticatedHttpClient().get(exportTags(courseId), { responseType: 'blob', }); /* istanbul ignore next */ if (response.status !== 200) { throw response.statusText; } const blob = new Blob([response.data], { type: 'text/csv' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${courseName}.csv`; a.click(); window.URL.revokeObjectURL(url); }