feat: implement import page (#587)

This commit is contained in:
Kyrylo Kholodenko
2023-09-25 19:07:08 +03:00
committed by GitHub
parent c47c800cfa
commit 2ea876ae4f
42 changed files with 1515 additions and 21 deletions

1
.env
View File

@@ -35,7 +35,6 @@ ENABLE_NEW_COURSE_OUTLINE_PAGE = false
ENABLE_NEW_VIDEO_UPLOAD_PAGE = false
ENABLE_NEW_GRADING_PAGE = false
ENABLE_NEW_COURSE_TEAM_PAGE = false
ENABLE_NEW_IMPORT_PAGE = false
ENABLE_UNIT_PAGE = false
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN = false
BBB_LEARN_MORE_URL=''

View File

@@ -36,7 +36,6 @@ ENABLE_NEW_COURSE_OUTLINE_PAGE = false
ENABLE_NEW_VIDEO_UPLOAD_PAGE = false
ENABLE_NEW_GRADING_PAGE = false
ENABLE_NEW_COURSE_TEAM_PAGE = false
ENABLE_NEW_IMPORT_PAGE = false
ENABLE_UNIT_PAGE = false
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN = false
BBB_LEARN_MORE_URL=''

View File

@@ -33,7 +33,6 @@ ENABLE_NEW_COURSE_OUTLINE_PAGE = true
ENABLE_NEW_VIDEO_UPLOAD_PAGE = true
ENABLE_NEW_GRADING_PAGE = true
ENABLE_NEW_COURSE_TEAM_PAGE = true
ENABLE_NEW_IMPORT_PAGE = true
ENABLE_UNIT_PAGE = true
ENABLE_VIDEO_UPLOAD_PAGE_LINK_IN_CONTENT_DROPDOWN = true
BBB_LEARN_MORE_URL=''

View File

@@ -16,6 +16,7 @@ import { GradingSettings } from './grading-settings';
import CourseTeam from './course-team/CourseTeam';
import { CourseUpdates } from './course-updates';
import CourseExportPage from './export-page/CourseExportPage';
import CourseImportPage from './import-page/CourseImportPage';
/**
* As of this writing, these routes are mounted at a path prefixed with the following:
@@ -100,10 +101,7 @@ const CourseAuthoringRoutes = ({ courseId }) => {
<AdvancedSettings courseId={courseId} />
</PageRoute>
<PageRoute path={`${path}/import`}>
{process.env.ENABLE_NEW_IMPORT_PAGE === 'true'
&& (
<Placeholder />
)}
<CourseImportPage courseId={courseId} />
</PageRoute>
<PageRoute path={`${path}/export`}>
<CourseExportPage courseId={courseId} />

View File

@@ -836,5 +836,39 @@
"course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.",
"course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?",
"course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.",
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs"
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs",
"course-authoring.import.file-section.title": "Select a .tar.gz file to replace your course content",
"course-authoring.import.file-section.chosen-file": "File chosen: {fileName}",
"course-authoring.import.sidebar.title1": "Why import a course?",
"course-authoring.import.sidebar.description1": "You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.",
"course-authoring.import.sidebar.importedContent": "What content is imported?",
"course-authoring.import.sidebar.importedContentHeading": "The following content is imported.",
"course-authoring.import.sidebar.content1": "Course content and structure",
"course-authoring.import.sidebar.content2": "Course dates",
"course-authoring.import.sidebar.content3": "Grading policy",
"course-authoring.import.sidebar.content4": "Any group configurations",
"course-authoring.import.sidebar.content5": "Settings on the advanced settings page, including MATLAB API keys and LTI passports",
"course-authoring.import.sidebar.notImportedContent": "The following content is not exported.",
"course-authoring.import.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data",
"course-authoring.import.sidebar.content7": "The course team",
"course-authoring.import.sidebar.warningTitle": "Warning: importing while a course is running",
"course-authoring.import.sidebar.warningDescription": "If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students' problem scores.",
"course-authoring.import.sidebar.learnMoreButtonTitle": "Learn more about importing a course",
"course-authoring.import.stepper.title.uploading": "Uploading",
"course-authoring.import.stepper.title.unpacking": "Unpacking",
"course-authoring.import.stepper.title.verifying": "Verifying",
"course-authoring.import.stepper.title.updating": "Updating сourse",
"course-authoring.import.stepper.title.success": "Success",
"course-authoring.import.stepper.description.uploading": "Transferring your file to our servers",
"course-authoring.import.stepper.description.unpacking": "Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)",
"course-authoring.import.stepper.description.verifying": "Reviewing semantics, syntax, and required data",
"course-authoring.import.stepper.description.updating": "Integrating your imported content into this course. This process might take longer with larger courses.",
"course-authoring.import.stepper.description.success": "Your imported content has now been integrated into this course",
"course-authoring.import.stepper.button.outline": "View updated outline",
"course-authoring.import.stepper.error.default": "Error importing course",
"course-authoring.import.heading.title": "Course import",
"course-authoring.import.heading.subtitle": "Tools",
"course-authoring.import.description1": "Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.",
"course-authoring.import.description2": "The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.",
"course-authoring.import.description3": "The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed."
}

View File

@@ -837,5 +837,39 @@
"course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.",
"course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?",
"course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.",
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs"
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs",
"course-authoring.import.file-section.title": "Select a .tar.gz file to replace your course content",
"course-authoring.import.file-section.chosen-file": "File chosen: {fileName}",
"course-authoring.import.sidebar.title1": "Why import a course?",
"course-authoring.import.sidebar.description1": "You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.",
"course-authoring.import.sidebar.importedContent": "What content is imported?",
"course-authoring.import.sidebar.importedContentHeading": "The following content is imported.",
"course-authoring.import.sidebar.content1": "Course content and structure",
"course-authoring.import.sidebar.content2": "Course dates",
"course-authoring.import.sidebar.content3": "Grading policy",
"course-authoring.import.sidebar.content4": "Any group configurations",
"course-authoring.import.sidebar.content5": "Settings on the advanced settings page, including MATLAB API keys and LTI passports",
"course-authoring.import.sidebar.notImportedContent": "The following content is not exported.",
"course-authoring.import.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data",
"course-authoring.import.sidebar.content7": "The course team",
"course-authoring.import.sidebar.warningTitle": "Warning: importing while a course is running",
"course-authoring.import.sidebar.warningDescription": "If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students' problem scores.",
"course-authoring.import.sidebar.learnMoreButtonTitle": "Learn more about importing a course",
"course-authoring.import.stepper.title.uploading": "Uploading",
"course-authoring.import.stepper.title.unpacking": "Unpacking",
"course-authoring.import.stepper.title.verifying": "Verifying",
"course-authoring.import.stepper.title.updating": "Updating сourse",
"course-authoring.import.stepper.title.success": "Success",
"course-authoring.import.stepper.description.uploading": "Transferring your file to our servers",
"course-authoring.import.stepper.description.unpacking": "Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)",
"course-authoring.import.stepper.description.verifying": "Reviewing semantics, syntax, and required data",
"course-authoring.import.stepper.description.updating": "Integrating your imported content into this course. This process might take longer with larger courses.",
"course-authoring.import.stepper.description.success": "Your imported content has now been integrated into this course",
"course-authoring.import.stepper.button.outline": "View updated outline",
"course-authoring.import.stepper.error.default": "Error importing course",
"course-authoring.import.heading.title": "Course import",
"course-authoring.import.heading.subtitle": "Tools",
"course-authoring.import.description1": "Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.",
"course-authoring.import.description2": "The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.",
"course-authoring.import.description3": "The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed."
}

View File

@@ -837,5 +837,39 @@
"course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.",
"course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?",
"course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.",
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs"
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs",
"course-authoring.import.file-section.title": "Select a .tar.gz file to replace your course content",
"course-authoring.import.file-section.chosen-file": "File chosen: {fileName}",
"course-authoring.import.sidebar.title1": "Why import a course?",
"course-authoring.import.sidebar.description1": "You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.",
"course-authoring.import.sidebar.importedContent": "What content is imported?",
"course-authoring.import.sidebar.importedContentHeading": "The following content is imported.",
"course-authoring.import.sidebar.content1": "Course content and structure",
"course-authoring.import.sidebar.content2": "Course dates",
"course-authoring.import.sidebar.content3": "Grading policy",
"course-authoring.import.sidebar.content4": "Any group configurations",
"course-authoring.import.sidebar.content5": "Settings on the advanced settings page, including MATLAB API keys and LTI passports",
"course-authoring.import.sidebar.notImportedContent": "The following content is not exported.",
"course-authoring.import.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data",
"course-authoring.import.sidebar.content7": "The course team",
"course-authoring.import.sidebar.warningTitle": "Warning: importing while a course is running",
"course-authoring.import.sidebar.warningDescription": "If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students' problem scores.",
"course-authoring.import.sidebar.learnMoreButtonTitle": "Learn more about importing a course",
"course-authoring.import.stepper.title.uploading": "Uploading",
"course-authoring.import.stepper.title.unpacking": "Unpacking",
"course-authoring.import.stepper.title.verifying": "Verifying",
"course-authoring.import.stepper.title.updating": "Updating сourse",
"course-authoring.import.stepper.title.success": "Success",
"course-authoring.import.stepper.description.uploading": "Transferring your file to our servers",
"course-authoring.import.stepper.description.unpacking": "Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)",
"course-authoring.import.stepper.description.verifying": "Reviewing semantics, syntax, and required data",
"course-authoring.import.stepper.description.updating": "Integrating your imported content into this course. This process might take longer with larger courses.",
"course-authoring.import.stepper.description.success": "Your imported content has now been integrated into this course",
"course-authoring.import.stepper.button.outline": "View updated outline",
"course-authoring.import.stepper.error.default": "Error importing course",
"course-authoring.import.heading.title": "Course import",
"course-authoring.import.heading.subtitle": "Tools",
"course-authoring.import.description1": "Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.",
"course-authoring.import.description2": "The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.",
"course-authoring.import.description3": "The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed."
}

View File

@@ -837,5 +837,39 @@
"course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.",
"course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?",
"course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.",
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs"
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs",
"course-authoring.import.file-section.title": "Select a .tar.gz file to replace your course content",
"course-authoring.import.file-section.chosen-file": "File chosen: {fileName}",
"course-authoring.import.sidebar.title1": "Why import a course?",
"course-authoring.import.sidebar.description1": "You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.",
"course-authoring.import.sidebar.importedContent": "What content is imported?",
"course-authoring.import.sidebar.importedContentHeading": "The following content is imported.",
"course-authoring.import.sidebar.content1": "Course content and structure",
"course-authoring.import.sidebar.content2": "Course dates",
"course-authoring.import.sidebar.content3": "Grading policy",
"course-authoring.import.sidebar.content4": "Any group configurations",
"course-authoring.import.sidebar.content5": "Settings on the advanced settings page, including MATLAB API keys and LTI passports",
"course-authoring.import.sidebar.notImportedContent": "The following content is not exported.",
"course-authoring.import.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data",
"course-authoring.import.sidebar.content7": "The course team",
"course-authoring.import.sidebar.warningTitle": "Warning: importing while a course is running",
"course-authoring.import.sidebar.warningDescription": "If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students' problem scores.",
"course-authoring.import.sidebar.learnMoreButtonTitle": "Learn more about importing a course",
"course-authoring.import.stepper.title.uploading": "Uploading",
"course-authoring.import.stepper.title.unpacking": "Unpacking",
"course-authoring.import.stepper.title.verifying": "Verifying",
"course-authoring.import.stepper.title.updating": "Updating сourse",
"course-authoring.import.stepper.title.success": "Success",
"course-authoring.import.stepper.description.uploading": "Transferring your file to our servers",
"course-authoring.import.stepper.description.unpacking": "Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)",
"course-authoring.import.stepper.description.verifying": "Reviewing semantics, syntax, and required data",
"course-authoring.import.stepper.description.updating": "Integrating your imported content into this course. This process might take longer with larger courses.",
"course-authoring.import.stepper.description.success": "Your imported content has now been integrated into this course",
"course-authoring.import.stepper.button.outline": "View updated outline",
"course-authoring.import.stepper.error.default": "Error importing course",
"course-authoring.import.heading.title": "Course import",
"course-authoring.import.heading.subtitle": "Tools",
"course-authoring.import.description1": "Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.",
"course-authoring.import.description2": "The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.",
"course-authoring.import.description3": "The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed."
}

View File

@@ -837,5 +837,39 @@
"course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.",
"course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?",
"course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.",
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs"
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs",
"course-authoring.import.file-section.title": "Select a .tar.gz file to replace your course content",
"course-authoring.import.file-section.chosen-file": "File chosen: {fileName}",
"course-authoring.import.sidebar.title1": "Why import a course?",
"course-authoring.import.sidebar.description1": "You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.",
"course-authoring.import.sidebar.importedContent": "What content is imported?",
"course-authoring.import.sidebar.importedContentHeading": "The following content is imported.",
"course-authoring.import.sidebar.content1": "Course content and structure",
"course-authoring.import.sidebar.content2": "Course dates",
"course-authoring.import.sidebar.content3": "Grading policy",
"course-authoring.import.sidebar.content4": "Any group configurations",
"course-authoring.import.sidebar.content5": "Settings on the advanced settings page, including MATLAB API keys and LTI passports",
"course-authoring.import.sidebar.notImportedContent": "The following content is not exported.",
"course-authoring.import.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data",
"course-authoring.import.sidebar.content7": "The course team",
"course-authoring.import.sidebar.warningTitle": "Warning: importing while a course is running",
"course-authoring.import.sidebar.warningDescription": "If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students' problem scores.",
"course-authoring.import.sidebar.learnMoreButtonTitle": "Learn more about importing a course",
"course-authoring.import.stepper.title.uploading": "Uploading",
"course-authoring.import.stepper.title.unpacking": "Unpacking",
"course-authoring.import.stepper.title.verifying": "Verifying",
"course-authoring.import.stepper.title.updating": "Updating сourse",
"course-authoring.import.stepper.title.success": "Success",
"course-authoring.import.stepper.description.uploading": "Transferring your file to our servers",
"course-authoring.import.stepper.description.unpacking": "Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)",
"course-authoring.import.stepper.description.verifying": "Reviewing semantics, syntax, and required data",
"course-authoring.import.stepper.description.updating": "Integrating your imported content into this course. This process might take longer with larger courses.",
"course-authoring.import.stepper.description.success": "Your imported content has now been integrated into this course",
"course-authoring.import.stepper.button.outline": "View updated outline",
"course-authoring.import.stepper.error.default": "Error importing course",
"course-authoring.import.heading.title": "Course import",
"course-authoring.import.heading.subtitle": "Tools",
"course-authoring.import.description1": "Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.",
"course-authoring.import.description2": "The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.",
"course-authoring.import.description3": "The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed."
}

View File

@@ -837,5 +837,39 @@
"course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.",
"course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?",
"course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.",
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs"
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs",
"course-authoring.import.file-section.title": "Select a .tar.gz file to replace your course content",
"course-authoring.import.file-section.chosen-file": "File chosen: {fileName}",
"course-authoring.import.sidebar.title1": "Why import a course?",
"course-authoring.import.sidebar.description1": "You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.",
"course-authoring.import.sidebar.importedContent": "What content is imported?",
"course-authoring.import.sidebar.importedContentHeading": "The following content is imported.",
"course-authoring.import.sidebar.content1": "Course content and structure",
"course-authoring.import.sidebar.content2": "Course dates",
"course-authoring.import.sidebar.content3": "Grading policy",
"course-authoring.import.sidebar.content4": "Any group configurations",
"course-authoring.import.sidebar.content5": "Settings on the advanced settings page, including MATLAB API keys and LTI passports",
"course-authoring.import.sidebar.notImportedContent": "The following content is not exported.",
"course-authoring.import.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data",
"course-authoring.import.sidebar.content7": "The course team",
"course-authoring.import.sidebar.warningTitle": "Warning: importing while a course is running",
"course-authoring.import.sidebar.warningDescription": "If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students' problem scores.",
"course-authoring.import.sidebar.learnMoreButtonTitle": "Learn more about importing a course",
"course-authoring.import.stepper.title.uploading": "Uploading",
"course-authoring.import.stepper.title.unpacking": "Unpacking",
"course-authoring.import.stepper.title.verifying": "Verifying",
"course-authoring.import.stepper.title.updating": "Updating сourse",
"course-authoring.import.stepper.title.success": "Success",
"course-authoring.import.stepper.description.uploading": "Transferring your file to our servers",
"course-authoring.import.stepper.description.unpacking": "Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)",
"course-authoring.import.stepper.description.verifying": "Reviewing semantics, syntax, and required data",
"course-authoring.import.stepper.description.updating": "Integrating your imported content into this course. This process might take longer with larger courses.",
"course-authoring.import.stepper.description.success": "Your imported content has now been integrated into this course",
"course-authoring.import.stepper.button.outline": "View updated outline",
"course-authoring.import.stepper.error.default": "Error importing course",
"course-authoring.import.heading.title": "Course import",
"course-authoring.import.heading.subtitle": "Tools",
"course-authoring.import.description1": "Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.",
"course-authoring.import.description2": "The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.",
"course-authoring.import.description3": "The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed."
}

View File

@@ -837,5 +837,39 @@
"course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.",
"course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?",
"course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.",
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs"
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs",
"course-authoring.import.file-section.title": "Select a .tar.gz file to replace your course content",
"course-authoring.import.file-section.chosen-file": "File chosen: {fileName}",
"course-authoring.import.sidebar.title1": "Why import a course?",
"course-authoring.import.sidebar.description1": "You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.",
"course-authoring.import.sidebar.importedContent": "What content is imported?",
"course-authoring.import.sidebar.importedContentHeading": "The following content is imported.",
"course-authoring.import.sidebar.content1": "Course content and structure",
"course-authoring.import.sidebar.content2": "Course dates",
"course-authoring.import.sidebar.content3": "Grading policy",
"course-authoring.import.sidebar.content4": "Any group configurations",
"course-authoring.import.sidebar.content5": "Settings on the advanced settings page, including MATLAB API keys and LTI passports",
"course-authoring.import.sidebar.notImportedContent": "The following content is not exported.",
"course-authoring.import.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data",
"course-authoring.import.sidebar.content7": "The course team",
"course-authoring.import.sidebar.warningTitle": "Warning: importing while a course is running",
"course-authoring.import.sidebar.warningDescription": "If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students' problem scores.",
"course-authoring.import.sidebar.learnMoreButtonTitle": "Learn more about importing a course",
"course-authoring.import.stepper.title.uploading": "Uploading",
"course-authoring.import.stepper.title.unpacking": "Unpacking",
"course-authoring.import.stepper.title.verifying": "Verifying",
"course-authoring.import.stepper.title.updating": "Updating сourse",
"course-authoring.import.stepper.title.success": "Success",
"course-authoring.import.stepper.description.uploading": "Transferring your file to our servers",
"course-authoring.import.stepper.description.unpacking": "Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)",
"course-authoring.import.stepper.description.verifying": "Reviewing semantics, syntax, and required data",
"course-authoring.import.stepper.description.updating": "Integrating your imported content into this course. This process might take longer with larger courses.",
"course-authoring.import.stepper.description.success": "Your imported content has now been integrated into this course",
"course-authoring.import.stepper.button.outline": "View updated outline",
"course-authoring.import.stepper.error.default": "Error importing course",
"course-authoring.import.heading.title": "Course import",
"course-authoring.import.heading.subtitle": "Tools",
"course-authoring.import.description1": "Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.",
"course-authoring.import.description2": "The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.",
"course-authoring.import.description3": "The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed."
}

View File

@@ -837,5 +837,39 @@
"course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.",
"course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?",
"course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.",
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs"
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs",
"course-authoring.import.file-section.title": "Select a .tar.gz file to replace your course content",
"course-authoring.import.file-section.chosen-file": "File chosen: {fileName}",
"course-authoring.import.sidebar.title1": "Why import a course?",
"course-authoring.import.sidebar.description1": "You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.",
"course-authoring.import.sidebar.importedContent": "What content is imported?",
"course-authoring.import.sidebar.importedContentHeading": "The following content is imported.",
"course-authoring.import.sidebar.content1": "Course content and structure",
"course-authoring.import.sidebar.content2": "Course dates",
"course-authoring.import.sidebar.content3": "Grading policy",
"course-authoring.import.sidebar.content4": "Any group configurations",
"course-authoring.import.sidebar.content5": "Settings on the advanced settings page, including MATLAB API keys and LTI passports",
"course-authoring.import.sidebar.notImportedContent": "The following content is not exported.",
"course-authoring.import.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data",
"course-authoring.import.sidebar.content7": "The course team",
"course-authoring.import.sidebar.warningTitle": "Warning: importing while a course is running",
"course-authoring.import.sidebar.warningDescription": "If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students' problem scores.",
"course-authoring.import.sidebar.learnMoreButtonTitle": "Learn more about importing a course",
"course-authoring.import.stepper.title.uploading": "Uploading",
"course-authoring.import.stepper.title.unpacking": "Unpacking",
"course-authoring.import.stepper.title.verifying": "Verifying",
"course-authoring.import.stepper.title.updating": "Updating сourse",
"course-authoring.import.stepper.title.success": "Success",
"course-authoring.import.stepper.description.uploading": "Transferring your file to our servers",
"course-authoring.import.stepper.description.unpacking": "Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)",
"course-authoring.import.stepper.description.verifying": "Reviewing semantics, syntax, and required data",
"course-authoring.import.stepper.description.updating": "Integrating your imported content into this course. This process might take longer with larger courses.",
"course-authoring.import.stepper.description.success": "Your imported content has now been integrated into this course",
"course-authoring.import.stepper.button.outline": "View updated outline",
"course-authoring.import.stepper.error.default": "Error importing course",
"course-authoring.import.heading.title": "Course import",
"course-authoring.import.heading.subtitle": "Tools",
"course-authoring.import.description1": "Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.",
"course-authoring.import.description2": "The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.",
"course-authoring.import.description3": "The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed."
}

View File

@@ -837,5 +837,39 @@
"course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.",
"course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?",
"course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.",
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs"
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs",
"course-authoring.import.file-section.title": "Select a .tar.gz file to replace your course content",
"course-authoring.import.file-section.chosen-file": "File chosen: {fileName}",
"course-authoring.import.sidebar.title1": "Why import a course?",
"course-authoring.import.sidebar.description1": "You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.",
"course-authoring.import.sidebar.importedContent": "What content is imported?",
"course-authoring.import.sidebar.importedContentHeading": "The following content is imported.",
"course-authoring.import.sidebar.content1": "Course content and structure",
"course-authoring.import.sidebar.content2": "Course dates",
"course-authoring.import.sidebar.content3": "Grading policy",
"course-authoring.import.sidebar.content4": "Any group configurations",
"course-authoring.import.sidebar.content5": "Settings on the advanced settings page, including MATLAB API keys and LTI passports",
"course-authoring.import.sidebar.notImportedContent": "The following content is not exported.",
"course-authoring.import.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data",
"course-authoring.import.sidebar.content7": "The course team",
"course-authoring.import.sidebar.warningTitle": "Warning: importing while a course is running",
"course-authoring.import.sidebar.warningDescription": "If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students' problem scores.",
"course-authoring.import.sidebar.learnMoreButtonTitle": "Learn more about importing a course",
"course-authoring.import.stepper.title.uploading": "Uploading",
"course-authoring.import.stepper.title.unpacking": "Unpacking",
"course-authoring.import.stepper.title.verifying": "Verifying",
"course-authoring.import.stepper.title.updating": "Updating сourse",
"course-authoring.import.stepper.title.success": "Success",
"course-authoring.import.stepper.description.uploading": "Transferring your file to our servers",
"course-authoring.import.stepper.description.unpacking": "Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)",
"course-authoring.import.stepper.description.verifying": "Reviewing semantics, syntax, and required data",
"course-authoring.import.stepper.description.updating": "Integrating your imported content into this course. This process might take longer with larger courses.",
"course-authoring.import.stepper.description.success": "Your imported content has now been integrated into this course",
"course-authoring.import.stepper.button.outline": "View updated outline",
"course-authoring.import.stepper.error.default": "Error importing course",
"course-authoring.import.heading.title": "Course import",
"course-authoring.import.heading.subtitle": "Tools",
"course-authoring.import.description1": "Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.",
"course-authoring.import.description2": "The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.",
"course-authoring.import.description3": "The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed."
}

View File

@@ -837,5 +837,39 @@
"course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.",
"course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?",
"course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.",
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs"
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs",
"course-authoring.import.file-section.title": "Select a .tar.gz file to replace your course content",
"course-authoring.import.file-section.chosen-file": "File chosen: {fileName}",
"course-authoring.import.sidebar.title1": "Why import a course?",
"course-authoring.import.sidebar.description1": "You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.",
"course-authoring.import.sidebar.importedContent": "What content is imported?",
"course-authoring.import.sidebar.importedContentHeading": "The following content is imported.",
"course-authoring.import.sidebar.content1": "Course content and structure",
"course-authoring.import.sidebar.content2": "Course dates",
"course-authoring.import.sidebar.content3": "Grading policy",
"course-authoring.import.sidebar.content4": "Any group configurations",
"course-authoring.import.sidebar.content5": "Settings on the advanced settings page, including MATLAB API keys and LTI passports",
"course-authoring.import.sidebar.notImportedContent": "The following content is not exported.",
"course-authoring.import.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data",
"course-authoring.import.sidebar.content7": "The course team",
"course-authoring.import.sidebar.warningTitle": "Warning: importing while a course is running",
"course-authoring.import.sidebar.warningDescription": "If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students' problem scores.",
"course-authoring.import.sidebar.learnMoreButtonTitle": "Learn more about importing a course",
"course-authoring.import.stepper.title.uploading": "Uploading",
"course-authoring.import.stepper.title.unpacking": "Unpacking",
"course-authoring.import.stepper.title.verifying": "Verifying",
"course-authoring.import.stepper.title.updating": "Updating сourse",
"course-authoring.import.stepper.title.success": "Success",
"course-authoring.import.stepper.description.uploading": "Transferring your file to our servers",
"course-authoring.import.stepper.description.unpacking": "Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)",
"course-authoring.import.stepper.description.verifying": "Reviewing semantics, syntax, and required data",
"course-authoring.import.stepper.description.updating": "Integrating your imported content into this course. This process might take longer with larger courses.",
"course-authoring.import.stepper.description.success": "Your imported content has now been integrated into this course",
"course-authoring.import.stepper.button.outline": "View updated outline",
"course-authoring.import.stepper.error.default": "Error importing course",
"course-authoring.import.heading.title": "Course import",
"course-authoring.import.heading.subtitle": "Tools",
"course-authoring.import.description1": "Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.",
"course-authoring.import.description2": "The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.",
"course-authoring.import.description3": "The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed."
}

View File

@@ -837,5 +837,39 @@
"course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.",
"course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?",
"course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.",
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs"
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs",
"course-authoring.import.file-section.title": "Select a .tar.gz file to replace your course content",
"course-authoring.import.file-section.chosen-file": "File chosen: {fileName}",
"course-authoring.import.sidebar.title1": "Why import a course?",
"course-authoring.import.sidebar.description1": "You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.",
"course-authoring.import.sidebar.importedContent": "What content is imported?",
"course-authoring.import.sidebar.importedContentHeading": "The following content is imported.",
"course-authoring.import.sidebar.content1": "Course content and structure",
"course-authoring.import.sidebar.content2": "Course dates",
"course-authoring.import.sidebar.content3": "Grading policy",
"course-authoring.import.sidebar.content4": "Any group configurations",
"course-authoring.import.sidebar.content5": "Settings on the advanced settings page, including MATLAB API keys and LTI passports",
"course-authoring.import.sidebar.notImportedContent": "The following content is not exported.",
"course-authoring.import.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data",
"course-authoring.import.sidebar.content7": "The course team",
"course-authoring.import.sidebar.warningTitle": "Warning: importing while a course is running",
"course-authoring.import.sidebar.warningDescription": "If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students' problem scores.",
"course-authoring.import.sidebar.learnMoreButtonTitle": "Learn more about importing a course",
"course-authoring.import.stepper.title.uploading": "Uploading",
"course-authoring.import.stepper.title.unpacking": "Unpacking",
"course-authoring.import.stepper.title.verifying": "Verifying",
"course-authoring.import.stepper.title.updating": "Updating сourse",
"course-authoring.import.stepper.title.success": "Success",
"course-authoring.import.stepper.description.uploading": "Transferring your file to our servers",
"course-authoring.import.stepper.description.unpacking": "Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)",
"course-authoring.import.stepper.description.verifying": "Reviewing semantics, syntax, and required data",
"course-authoring.import.stepper.description.updating": "Integrating your imported content into this course. This process might take longer with larger courses.",
"course-authoring.import.stepper.description.success": "Your imported content has now been integrated into this course",
"course-authoring.import.stepper.button.outline": "View updated outline",
"course-authoring.import.stepper.error.default": "Error importing course",
"course-authoring.import.heading.title": "Course import",
"course-authoring.import.heading.subtitle": "Tools",
"course-authoring.import.description1": "Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.",
"course-authoring.import.description2": "The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.",
"course-authoring.import.description3": "The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed."
}

View File

@@ -873,5 +873,39 @@
"course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.",
"course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?",
"course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.",
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs"
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs",
"course-authoring.import.file-section.title": "Select a .tar.gz file to replace your course content",
"course-authoring.import.file-section.chosen-file": "File chosen: {fileName}",
"course-authoring.import.sidebar.title1": "Why import a course?",
"course-authoring.import.sidebar.description1": "You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.",
"course-authoring.import.sidebar.importedContent": "What content is imported?",
"course-authoring.import.sidebar.importedContentHeading": "The following content is imported.",
"course-authoring.import.sidebar.content1": "Course content and structure",
"course-authoring.import.sidebar.content2": "Course dates",
"course-authoring.import.sidebar.content3": "Grading policy",
"course-authoring.import.sidebar.content4": "Any group configurations",
"course-authoring.import.sidebar.content5": "Settings on the advanced settings page, including MATLAB API keys and LTI passports",
"course-authoring.import.sidebar.notImportedContent": "The following content is not exported.",
"course-authoring.import.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data",
"course-authoring.import.sidebar.content7": "The course team",
"course-authoring.import.sidebar.warningTitle": "Warning: importing while a course is running",
"course-authoring.import.sidebar.warningDescription": "If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students' problem scores.",
"course-authoring.import.sidebar.learnMoreButtonTitle": "Learn more about importing a course",
"course-authoring.import.stepper.title.uploading": "Uploading",
"course-authoring.import.stepper.title.unpacking": "Unpacking",
"course-authoring.import.stepper.title.verifying": "Verifying",
"course-authoring.import.stepper.title.updating": "Updating сourse",
"course-authoring.import.stepper.title.success": "Success",
"course-authoring.import.stepper.description.uploading": "Transferring your file to our servers",
"course-authoring.import.stepper.description.unpacking": "Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)",
"course-authoring.import.stepper.description.verifying": "Reviewing semantics, syntax, and required data",
"course-authoring.import.stepper.description.updating": "Integrating your imported content into this course. This process might take longer with larger courses.",
"course-authoring.import.stepper.description.success": "Your imported content has now been integrated into this course",
"course-authoring.import.stepper.button.outline": "View updated outline",
"course-authoring.import.stepper.error.default": "Error importing course",
"course-authoring.import.heading.title": "Course import",
"course-authoring.import.heading.subtitle": "Tools",
"course-authoring.import.description1": "Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.",
"course-authoring.import.description2": "The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.",
"course-authoring.import.description3": "The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed."
}

View File

@@ -837,5 +837,39 @@
"course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.",
"course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?",
"course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.",
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs"
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs",
"course-authoring.import.file-section.title": "Select a .tar.gz file to replace your course content",
"course-authoring.import.file-section.chosen-file": "File chosen: {fileName}",
"course-authoring.import.sidebar.title1": "Why import a course?",
"course-authoring.import.sidebar.description1": "You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.",
"course-authoring.import.sidebar.importedContent": "What content is imported?",
"course-authoring.import.sidebar.importedContentHeading": "The following content is imported.",
"course-authoring.import.sidebar.content1": "Course content and structure",
"course-authoring.import.sidebar.content2": "Course dates",
"course-authoring.import.sidebar.content3": "Grading policy",
"course-authoring.import.sidebar.content4": "Any group configurations",
"course-authoring.import.sidebar.content5": "Settings on the advanced settings page, including MATLAB API keys and LTI passports",
"course-authoring.import.sidebar.notImportedContent": "The following content is not exported.",
"course-authoring.import.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data",
"course-authoring.import.sidebar.content7": "The course team",
"course-authoring.import.sidebar.warningTitle": "Warning: importing while a course is running",
"course-authoring.import.sidebar.warningDescription": "If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students' problem scores.",
"course-authoring.import.sidebar.learnMoreButtonTitle": "Learn more about importing a course",
"course-authoring.import.stepper.title.uploading": "Uploading",
"course-authoring.import.stepper.title.unpacking": "Unpacking",
"course-authoring.import.stepper.title.verifying": "Verifying",
"course-authoring.import.stepper.title.updating": "Updating сourse",
"course-authoring.import.stepper.title.success": "Success",
"course-authoring.import.stepper.description.uploading": "Transferring your file to our servers",
"course-authoring.import.stepper.description.unpacking": "Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)",
"course-authoring.import.stepper.description.verifying": "Reviewing semantics, syntax, and required data",
"course-authoring.import.stepper.description.updating": "Integrating your imported content into this course. This process might take longer with larger courses.",
"course-authoring.import.stepper.description.success": "Your imported content has now been integrated into this course",
"course-authoring.import.stepper.button.outline": "View updated outline",
"course-authoring.import.stepper.error.default": "Error importing course",
"course-authoring.import.heading.title": "Course import",
"course-authoring.import.heading.subtitle": "Tools",
"course-authoring.import.description1": "Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.",
"course-authoring.import.description2": "The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.",
"course-authoring.import.description3": "The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed."
}

View File

@@ -837,5 +837,39 @@
"course-authoring.course-rerun.sidebar.section-2.description": "The new course has the same course outline and content as the original course. All problems, videos, announcements, and other files are duplicated to the new course.",
"course-authoring.course-rerun.sidebar.section-3.title": "What does not transfer from the original course?",
"course-authoring.course-rerun.sidebar.section-3.description": "You are the only member of the new course's staff. No students are enrolled in the course, and there is no student data. There is no content in the discussion topics or wiki.",
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs"
"course-authoring.course-rerun.sidebar.section-4.link": "Learn more about course re-runs",
"course-authoring.import.file-section.title": "Select a .tar.gz file to replace your course content",
"course-authoring.import.file-section.chosen-file": "File chosen: {fileName}",
"course-authoring.import.sidebar.title1": "Why import a course?",
"course-authoring.import.sidebar.description1": "You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.",
"course-authoring.import.sidebar.importedContent": "What content is imported?",
"course-authoring.import.sidebar.importedContentHeading": "The following content is imported.",
"course-authoring.import.sidebar.content1": "Course content and structure",
"course-authoring.import.sidebar.content2": "Course dates",
"course-authoring.import.sidebar.content3": "Grading policy",
"course-authoring.import.sidebar.content4": "Any group configurations",
"course-authoring.import.sidebar.content5": "Settings on the advanced settings page, including MATLAB API keys and LTI passports",
"course-authoring.import.sidebar.notImportedContent": "The following content is not exported.",
"course-authoring.import.sidebar.content6": "Learner-specific content, such as learner grades and discussion forum data",
"course-authoring.import.sidebar.content7": "The course team",
"course-authoring.import.sidebar.warningTitle": "Warning: importing while a course is running",
"course-authoring.import.sidebar.warningDescription": "If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students' problem scores.",
"course-authoring.import.sidebar.learnMoreButtonTitle": "Learn more about importing a course",
"course-authoring.import.stepper.title.uploading": "Uploading",
"course-authoring.import.stepper.title.unpacking": "Unpacking",
"course-authoring.import.stepper.title.verifying": "Verifying",
"course-authoring.import.stepper.title.updating": "Updating сourse",
"course-authoring.import.stepper.title.success": "Success",
"course-authoring.import.stepper.description.uploading": "Transferring your file to our servers",
"course-authoring.import.stepper.description.unpacking": "Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)",
"course-authoring.import.stepper.description.verifying": "Reviewing semantics, syntax, and required data",
"course-authoring.import.stepper.description.updating": "Integrating your imported content into this course. This process might take longer with larger courses.",
"course-authoring.import.stepper.description.success": "Your imported content has now been integrated into this course",
"course-authoring.import.stepper.button.outline": "View updated outline",
"course-authoring.import.stepper.error.default": "Error importing course",
"course-authoring.import.heading.title": "Course import",
"course-authoring.import.heading.subtitle": "Tools",
"course-authoring.import.description1": "Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.",
"course-authoring.import.description2": "The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.",
"course-authoring.import.description3": "The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don't make important changes to your course until the import operation has completed."
}

View File

@@ -0,0 +1,103 @@
/* eslint-disable max-len */
import React, { useEffect } from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Container, Layout,
} from '@edx/paragon';
import Cookies from 'universal-cookie';
import { Helmet } from 'react-helmet';
import SubHeader from '../generic/sub-header/SubHeader';
import InternetConnectionAlert from '../generic/internet-connection-alert';
import { RequestStatus } from '../data/constants';
import { useModel } from '../generic/model-store';
import {
updateFileName, updateImportTriggered, updateSavingStatus, updateSuccessDate,
} from './data/slice';
import ImportStepper from './import-stepper/ImportStepper';
import { getImportTriggered, getLoadingStatus, getSavingStatus } from './data/selectors';
import { LAST_IMPORT_COOKIE_NAME } from './data/constants';
import ImportSidebar from './import-sidebar/ImportSidebar';
import FileSection from './file-section/FileSection';
import messages from './messages';
const CourseImportPage = ({ intl, courseId }) => {
const dispatch = useDispatch();
const cookies = new Cookies();
const courseDetails = useModel('courseDetails', courseId);
const importTriggered = useSelector(getImportTriggered);
const savingStatus = useSelector(getSavingStatus);
const loadingStatus = useSelector(getLoadingStatus);
const anyRequestFailed = savingStatus === RequestStatus.FAILED || loadingStatus === RequestStatus.FAILED;
const anyRequestInProgress = savingStatus === RequestStatus.PENDING || loadingStatus === RequestStatus.IN_PROGRESS;
useEffect(() => {
const cookieData = cookies.get(LAST_IMPORT_COOKIE_NAME);
if (cookieData) {
dispatch(updateSavingStatus(RequestStatus.SUCCESSFUL));
dispatch(updateImportTriggered(true));
dispatch(updateFileName(cookieData.fileName));
dispatch(updateSuccessDate(cookieData.date));
}
}, []);
return (
<>
<Helmet>
<title>
{intl.formatMessage(messages.pageTitle, {
headingTitle: intl.formatMessage(messages.headingTitle),
courseName: courseDetails?.name,
siteName: process.env.SITE_NAME,
})}
</title>
</Helmet>
<Container size="xl" className="m-4 import">
<section className="setting-items mb-4">
<Layout
lg={[{ span: 9 }, { span: 3 }]}
md={[{ span: 9 }, { span: 3 }]}
sm={[{ span: 9 }, { span: 3 }]}
xs={[{ span: 9 }, { span: 3 }]}
xl={[{ span: 9 }, { span: 3 }]}
>
<Layout.Element>
<article>
<SubHeader
title={intl.formatMessage(messages.headingTitle)}
subtitle={intl.formatMessage(messages.headingSubtitle)}
/>
<p>{intl.formatMessage(messages.description1)}</p>
<p>{intl.formatMessage(messages.description2)}</p>
<p>{intl.formatMessage(messages.description3)}</p>
<FileSection courseId={courseId} />
{importTriggered && <ImportStepper courseId={courseId} />}
</article>
</Layout.Element>
<Layout.Element>
<ImportSidebar courseId={courseId} />
</Layout.Element>
</Layout>
</section>
</Container>
<div className="alert-toast">
<InternetConnectionAlert
isFailed={anyRequestFailed}
isQueryPending={anyRequestInProgress}
onInternetConnectionFailed={() => null}
/>
</div>
</>
);
};
CourseImportPage.propTypes = {
intl: intlShape.isRequired,
courseId: PropTypes.string.isRequired,
};
CourseImportPage.defaultProps = {};
export default injectIntl(CourseImportPage);

View File

@@ -0,0 +1,7 @@
@import "./import-sidebar/ImportSidebar";
.import {
.help-sidebar {
margin-top: 7.188rem;
}
}

View File

@@ -0,0 +1,111 @@
import React from 'react';
import { initializeMockApp } from '@edx/frontend-platform';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { render, waitFor } from '@testing-library/react';
import { Helmet } from 'react-helmet';
import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import Cookies from 'universal-cookie';
import initializeStore from '../store';
import messages from './messages';
import CourseImportPage from './CourseImportPage';
import { getImportStatusApiUrl } from './data/api';
import { IMPORT_STAGES } from './data/constants';
import stepperMessages from './import-stepper/messages';
let store;
let axiosMock;
let cookies;
const courseId = '123';
const courseName = 'About Node JS';
jest.mock('../generic/model-store', () => ({
useModel: jest.fn().mockReturnValue({
name: courseName,
}),
}));
jest.mock('universal-cookie', () => {
const Cookie = {
get: jest.fn(),
set: jest.fn(),
};
return jest.fn(() => Cookie);
});
const RootWrapper = () => (
<AppProvider store={store}>
<IntlProvider locale="en" messages={{}}>
<CourseImportPage intl={injectIntl} courseId={courseId} />
</IntlProvider>
</AppProvider>
);
describe('<CourseImportPage />', () => {
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock
.onGet(getImportStatusApiUrl(courseId, 'testFileName.test'))
.reply(200, { importStatus: 1, message: '' });
cookies = new Cookies();
cookies.get.mockReturnValue(null);
});
it('should render page title correctly', async () => {
render(<RootWrapper />);
await waitFor(() => {
const helmet = Helmet.peek();
expect(helmet.title).toEqual(
`${messages.headingTitle.defaultMessage} | ${courseName} | ${process.env.SITE_NAME}`,
);
});
});
it('should render without errors', async () => {
const { getByText } = render(<RootWrapper />);
await waitFor(() => {
expect(getByText(messages.headingSubtitle.defaultMessage)).toBeInTheDocument();
const importPageElement = getByText(messages.headingTitle.defaultMessage, {
selector: 'h2.sub-header-title',
});
expect(importPageElement).toBeInTheDocument();
expect(getByText(messages.description1.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.description2.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.description3.defaultMessage)).toBeInTheDocument();
});
});
it('should fetch status without clicking when cookies has', async () => {
cookies.get.mockReturnValue({ date: 1679787000, completed: false, fileName: 'testFileName.test' });
const { getByText } = render(<RootWrapper />);
expect(getByText(stepperMessages.stepperUnpackingDescription.defaultMessage)).toBeInTheDocument();
});
it('should show error', async () => {
axiosMock
.onGet(getImportStatusApiUrl(courseId, 'testFileName.tar.gz'))
.reply(200, { importStatus: -IMPORT_STAGES.UPDATING, message: '' });
cookies.get.mockReturnValue({ date: 1679787000, completed: false, fileName: 'testFileName.tar.gz' });
const { getByText } = render(<RootWrapper />);
// eslint-disable-next-line no-promise-executor-return
await new Promise((r) => setTimeout(r, 3500));
expect(getByText(stepperMessages.defaultErrorMessage.defaultMessage)).toBeInTheDocument();
});
it('should show success button', async () => {
axiosMock
.onGet(getImportStatusApiUrl(courseId, 'testFileName.tar.gz'))
.reply(200, { importStatus: IMPORT_STAGES.SUCCESS, message: '' });
cookies.get.mockReturnValue({ date: 1679787000, completed: false, fileName: 'testFileName.tar.gz' });
const { getByText } = render(<RootWrapper />);
// eslint-disable-next-line no-promise-executor-return
await new Promise((r) => setTimeout(r, 3500));
expect(getByText(stepperMessages.viewOutlineButton.defaultMessage)).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,32 @@
/* eslint-disable import/prefer-default-export */
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
export const postImportCourseApiUrl = (courseId) => `${getApiBaseUrl()}/import/${courseId}`;
export const getImportStatusApiUrl = (courseId, fileName) => `${getApiBaseUrl()}/import_status/${courseId}/${fileName}`;
/**
* Start import course.
* @param {string} courseId
* @param {Object} fileData
* @param {Object} requestConfig
* @returns {Promise<Object>}
*/
export async function startCourseImporting(courseId, fileData, requestConfig) {
const { data } = await getAuthenticatedHttpClient()
.post(postImportCourseApiUrl(courseId), { 'course-data': fileData }, { headers: { 'content-type': 'multipart/form-data' }, ...requestConfig });
return camelCaseObject(data);
}
/**
* Get import status.
* @param {string} courseId
* @param {string} fileName
* @returns {Promise<Object>}
*/
export async function getImportStatus(courseId, fileName) {
const { data } = await getAuthenticatedHttpClient()
.get(getImportStatusApiUrl(courseId, fileName));
return camelCaseObject(data);
}

View File

@@ -0,0 +1,48 @@
import MockAdapter from 'axios-mock-adapter';
import { initializeMockApp, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { getImportStatus, postImportCourseApiUrl, startCourseImporting } from './api';
let axiosMock;
const courseId = 'course-123';
describe('API Functions', () => {
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
});
afterEach(() => {
jest.clearAllMocks();
});
it('should fetch status on start importing', async () => {
const data = { importStatus: 1 };
axiosMock.onPost(postImportCourseApiUrl(courseId)).reply(200, data);
const result = await startCourseImporting(courseId);
expect(axiosMock.history.post[0].url).toEqual(postImportCourseApiUrl(courseId));
expect(result).toEqual(data);
});
it('should fetch on get import status', async () => {
const data = { importStatus: 2 };
const fileName = 'testFileName.test';
const queryUrl = new URL(`import_status/${courseId}/${fileName}`, getConfig().STUDIO_BASE_URL).href;
axiosMock.onGet(queryUrl).reply(200, data);
const result = await getImportStatus(courseId, fileName);
expect(axiosMock.history.get[0].url).toEqual(queryUrl);
expect(result).toEqual(data);
});
});

View File

@@ -0,0 +1,8 @@
export const LAST_IMPORT_COOKIE_NAME = 'lastimport';
export const IMPORT_STAGES = {
UPLOADING: 0,
UNPACKING: 1,
VERIFYING: 2,
UPDATING: 3,
SUCCESS: 4,
};

View File

@@ -0,0 +1,8 @@
export const getProgress = (state) => state.courseImport.progress;
export const getCurrentStage = (state) => state.courseImport.currentStage;
export const getImportTriggered = (state) => state.courseImport.importTriggered;
export const getFileName = (state) => state.courseImport.fileName;
export const getError = (state) => state.courseImport.error;
export const getLoadingStatus = (state) => state.courseImport.loadingStatus;
export const getSavingStatus = (state) => state.courseImport.savingStatus;
export const getSuccessDate = (state) => state.courseImport.successDate;

View File

@@ -0,0 +1,63 @@
/* eslint-disable no-param-reassign */
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
currentStage: 0,
error: { hasError: false, message: '' },
progress: 0,
importTriggered: false,
fileName: null,
loadingStatus: '',
savingStatus: '',
successDate: null,
};
const slice = createSlice({
name: 'importPage',
initialState,
reducers: {
updateCurrentStage: (state, { payload }) => {
if (payload >= state.currentStage) {
state.currentStage = payload;
}
},
updateError: (state, { payload }) => {
state.error = { ...state.error, ...payload };
},
updateProgress: (state, { payload }) => {
state.progress = payload;
},
updateImportTriggered: (state, { payload }) => {
state.importTriggered = payload;
},
updateFileName: (state, { payload }) => {
state.fileName = payload;
},
reset: () => initialState,
updateLoadingStatus: (state, { payload }) => {
state.loadingStatus = payload;
},
updateSavingStatus: (state, { payload }) => {
state.savingStatus = payload;
},
updateSuccessDate: (state, { payload }) => {
state.successDate = payload;
},
},
});
export const {
updateCurrentStage,
updateError,
updateProgress,
updateImportTriggered,
updateFileName,
reset,
updateLoadingStatus,
updateSavingStatus,
updateSuccessDate,
} = slice.actions;
export const {
reducer,
} = slice;

View File

@@ -0,0 +1,60 @@
/* eslint-disable import/prefer-default-export */
import Cookies from 'universal-cookie';
import moment from 'moment';
import { RequestStatus } from '../../data/constants';
import { setImportCookie } from '../utils';
import { getImportStatus, startCourseImporting } from './api';
import {
reset, updateCurrentStage, updateError, updateFileName,
updateImportTriggered, updateLoadingStatus, updateSavingStatus, updateSuccessDate,
} from './slice';
import { IMPORT_STAGES, LAST_IMPORT_COOKIE_NAME } from './constants';
export function fetchImportStatus(courseId, fileName) {
return async (dispatch) => {
try {
dispatch(updateLoadingStatus(RequestStatus.IN_PROGRESS));
const { importStatus, message } = await getImportStatus(courseId, fileName);
dispatch(updateCurrentStage(Math.abs(importStatus)));
const cookies = new Cookies();
const cookieData = cookies.get(LAST_IMPORT_COOKIE_NAME);
if (importStatus < 0) {
dispatch(updateError({ hasError: true, message }));
} else if (importStatus === IMPORT_STAGES.SUCCESS && !cookieData?.completed) {
dispatch(updateSuccessDate(moment().valueOf()));
}
if (!cookieData?.completed) {
setImportCookie(moment().valueOf(), importStatus === IMPORT_STAGES.SUCCESS, fileName);
}
dispatch(updateLoadingStatus(RequestStatus.SUCCESSFUL));
return true;
} catch (error) {
dispatch(updateLoadingStatus(RequestStatus.FAILED));
return false;
}
};
}
export function handleProcessUpload(courseId, fileData, requestConfig, handleError) {
return async (dispatch) => {
try {
const file = fileData.get('file');
dispatch(reset());
dispatch(updateSavingStatus(RequestStatus.PENDING));
dispatch(updateImportTriggered(true));
dispatch(updateFileName(file.name));
const { importStatus } = await startCourseImporting(courseId, file, requestConfig);
dispatch(updateCurrentStage(importStatus));
setImportCookie(moment().valueOf(), importStatus === IMPORT_STAGES.SUCCESS, file.name);
dispatch(updateSavingStatus(RequestStatus.SUCCESSFUL));
return true;
} catch (error) {
handleError(error);
dispatch(updateSavingStatus(RequestStatus.FAILED));
return false;
}
};
}

View File

@@ -0,0 +1,64 @@
import React from 'react';
import {
injectIntl,
intlShape,
} from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { Card, Dropzone } from '@edx/paragon';
import { IMPORT_STAGES } from '../data/constants';
import {
getCurrentStage, getError, getFileName, getImportTriggered,
} from '../data/selectors';
import { updateProgress } from '../data/slice';
import messages from './messages';
import { handleProcessUpload } from '../data/thunks';
const FileSection = ({ intl, courseId }) => {
const dispatch = useDispatch();
const importTriggered = useSelector(getImportTriggered);
const currentStage = useSelector(getCurrentStage);
const fileName = useSelector(getFileName);
const { hasError } = useSelector(getError);
const isShowedDropzone = !importTriggered || currentStage === IMPORT_STAGES.SUCCESS || hasError;
return (
<Card>
<Card.Header
className="h3 px-3 text-black"
title={intl.formatMessage(messages.headingTitle)}
/>
<Card.Section className="px-3 py-1">
{isShowedDropzone
&& (
<Dropzone
onProcessUpload={
({ fileData, requestConfig, handleError }) => dispatch(handleProcessUpload(
courseId,
fileData,
requestConfig,
handleError,
))
}
onUploadProgress={(percent) => dispatch(updateProgress(percent))}
accept={{ 'application/gzip': ['.tar.gz'] }}
data-testid="dropzone"
/>
)}
</Card.Section>
{fileName && (
<div className="px-3 py-1">
{intl.formatMessage(messages.fileChosen, { fileName })}
</div>
)}
</Card>
);
};
FileSection.propTypes = {
intl: intlShape.isRequired,
courseId: PropTypes.string.isRequired,
};
export default injectIntl(FileSection);

View File

@@ -0,0 +1,61 @@
import React from 'react';
import { initializeMockApp } from '@edx/frontend-platform';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { fireEvent, render, waitFor } from '@testing-library/react';
import initializeStore from '../../store';
import messages from './messages';
import FileSection from './FileSection';
let store;
const courseId = '123';
const RootWrapper = () => (
<AppProvider store={store}>
<IntlProvider locale="en" messages={{}}>
<FileSection intl={injectIntl} courseId={courseId} />
</IntlProvider>
</AppProvider>
);
describe('<FileSection />', () => {
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
store = initializeStore();
});
it('should render without errors', async () => {
const { getByText } = render(<RootWrapper />);
await waitFor(() => {
expect(getByText(messages.headingTitle.defaultMessage)).toBeInTheDocument();
});
});
it('should displays Dropzone when import is not triggered or in success stage or has an error', async () => {
const { getByTestId } = render(<RootWrapper />);
await waitFor(() => {
expect(getByTestId('dropzone')).toBeInTheDocument();
});
});
it('should work Dropzone', async () => {
const {
getByText, getByTestId, queryByTestId, container,
} = render(<RootWrapper />);
const dropzoneElement = getByTestId('dropzone');
const file = new File(['file contents'], 'example.tar.gz', { type: 'application/gzip' });
fireEvent.drop(dropzoneElement, { dataTransfer: { files: [file], types: ['Files'] } });
await waitFor(() => {
expect(getByText('File chosen: example.tar.gz')).toBeInTheDocument();
expect(queryByTestId(container, 'dropzone')).not.toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,14 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
headingTitle: {
id: 'course-authoring.import.file-section.title',
defaultMessage: 'Select a .tar.gz file to replace your course content',
},
fileChosen: {
id: 'course-authoring.import.file-section.chosen-file',
defaultMessage: 'File chosen: {fileName}',
},
});
export default messages;

View File

@@ -0,0 +1,57 @@
import React from 'react';
import {
injectIntl,
intlShape,
} from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import { Button } from '@edx/paragon';
import { getConfig } from '@edx/frontend-platform';
import { HelpSidebar } from '../../generic/help-sidebar';
import { useHelpUrls } from '../../help-urls/hooks';
import messages from './messages';
const ImportSidebar = ({ intl, courseId }) => {
const { importCourse: importLearnMoreUrl } = useHelpUrls(['importCourse']);
return (
<HelpSidebar courseId={courseId}>
<h4 className="help-sidebar-about-title">{intl.formatMessage(messages.title1)}</h4>
<p className="help-sidebar-about-descriptions">
{intl.formatMessage(messages.description1, { studioShortName: getConfig().STUDIO_SHORT_NAME })}
</p>
<hr />
<h4 className="help-sidebar-about-title">{intl.formatMessage(messages.importedContent)}</h4>
<p className="help-sidebar-about-descriptions">{intl.formatMessage(messages.importedContentHeading)}</p>
<ul className="import-sidebar-list">
<li className="help-sidebar-about-descriptions">{intl.formatMessage(messages.content1)}</li>
<li className="help-sidebar-about-descriptions">{intl.formatMessage(messages.content2)}</li>
<li className="help-sidebar-about-descriptions">{intl.formatMessage(messages.content3)}</li>
<li className="help-sidebar-about-descriptions">{intl.formatMessage(messages.content4)}</li>
<li className="help-sidebar-about-descriptions">{intl.formatMessage(messages.content5)}</li>
</ul>
<p className="help-sidebar-about-descriptions">{intl.formatMessage(messages.notImportedContent)}</p>
<ul className="import-sidebar-list">
<li className="help-sidebar-about-descriptions">{intl.formatMessage(messages.content6)}</li>
<li className="help-sidebar-about-descriptions">{intl.formatMessage(messages.content7)}</li>
</ul>
<hr />
<h4 className="help-sidebar-about-title">{intl.formatMessage(messages.warningTitle)}</h4>
<p className="help-sidebar-about-descriptions">{intl.formatMessage(messages.warningDescription)}</p>
<hr />
<Button
href={importLearnMoreUrl}
target="_blank"
variant="outline-primary"
>
{intl.formatMessage(messages.learnMoreButtonTitle)}
</Button>
</HelpSidebar>
);
};
ImportSidebar.propTypes = {
intl: intlShape.isRequired,
courseId: PropTypes.string.isRequired,
};
export default injectIntl(ImportSidebar);

View File

@@ -0,0 +1,4 @@
.import-sidebar-list {
list-style: none;
padding-left: 0;
}

View File

@@ -0,0 +1,39 @@
import React from 'react';
import { render } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { initializeMockApp } from '@edx/frontend-platform';
import { AppProvider } from '@edx/frontend-platform/react';
import initializeStore from '../../store';
import messages from './messages';
import ImportSidebar from './ImportSidebar';
const courseId = 'course-123';
let store;
const RootWrapper = () => (
<AppProvider store={store}>
<IntlProvider locale="en" messages={{}}>
<ImportSidebar intl={{ formatMessage: jest.fn() }} courseId={courseId} />
</IntlProvider>
</AppProvider>
);
describe('<ImportSidebar />', () => {
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
store = initializeStore();
});
it('render sidebar correctly', () => {
const { getByText } = render(<RootWrapper />);
expect(getByText(messages.title1.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.importedContentHeading.defaultMessage)).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,66 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
title1: {
id: 'course-authoring.import.sidebar.title1',
defaultMessage: 'Why import a course?',
},
description1: {
id: 'course-authoring.import.sidebar.description1',
defaultMessage: 'You may want to run a new version of an existing course, or replace an existing course altogether. Or, you may have developed a course outside {studioShortName}.',
},
importedContent: {
id: 'course-authoring.import.sidebar.importedContent',
defaultMessage: 'What content is imported?',
},
importedContentHeading: {
id: 'course-authoring.import.sidebar.importedContentHeading',
defaultMessage: 'The following content is imported.',
},
content1: {
id: 'course-authoring.import.sidebar.content1',
defaultMessage: 'Course content and structure',
},
content2: {
id: 'course-authoring.import.sidebar.content2',
defaultMessage: 'Course dates',
},
content3: {
id: 'course-authoring.import.sidebar.content3',
defaultMessage: 'Grading policy',
},
content4: {
id: 'course-authoring.import.sidebar.content4',
defaultMessage: 'Any group configurations',
},
content5: {
id: 'course-authoring.import.sidebar.content5',
defaultMessage: 'Settings on the advanced settings page, including MATLAB API keys and LTI passports',
},
notImportedContent: {
id: 'course-authoring.import.sidebar.notImportedContent',
defaultMessage: 'The following content is not exported.',
},
content6: {
id: 'course-authoring.import.sidebar.content6',
defaultMessage: 'Learner-specific content, such as learner grades and discussion forum data',
},
content7: {
id: 'course-authoring.import.sidebar.content7',
defaultMessage: 'The course team',
},
warningTitle: {
id: 'course-authoring.import.sidebar.warningTitle',
defaultMessage: 'Warning: importing while a course is running',
},
warningDescription: {
id: 'course-authoring.import.sidebar.warningDescription',
defaultMessage: 'If you perform an import while your course is running, and you change the URL names (or url_name nodes) of any problem components, the student data associated with those problem components may be lost. This data includes students\' problem scores.',
},
learnMoreButtonTitle: {
id: 'course-authoring.import.sidebar.learnMoreButtonTitle',
defaultMessage: 'Learn more about importing a course',
},
});
export default messages;

View File

@@ -0,0 +1,103 @@
import React, { useEffect } from 'react';
import {
injectIntl,
intlShape,
} from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { Button } from '@edx/paragon';
import { getConfig } from '@edx/frontend-platform';
import { getFormattedSuccessDate } from '../../export-page/utils';
import { RequestStatus } from '../../data/constants';
import CourseStepper from '../../generic/course-stepper';
import { IMPORT_STAGES } from '../data/constants';
import { fetchImportStatus } from '../data/thunks';
import {
getCurrentStage, getError, getFileName, getLoadingStatus, getProgress, getSavingStatus, getSuccessDate,
} from '../data/selectors';
import messages from './messages';
const ImportStepper = ({ intl, courseId }) => {
const currentStage = useSelector(getCurrentStage);
const fileName = useSelector(getFileName);
const { hasError, message: errorMessage } = useSelector(getError);
const progress = useSelector(getProgress);
const dispatch = useDispatch();
const loadingStatus = useSelector(getLoadingStatus);
const savingStatus = useSelector(getSavingStatus);
const successDate = useSelector(getSuccessDate);
const isStopFetching = currentStage === IMPORT_STAGES.SUCCESS
|| loadingStatus === RequestStatus.FAILED
|| savingStatus === RequestStatus.FAILED
|| hasError;
const formattedErrorMessage = hasError ? errorMessage || intl.formatMessage(messages.defaultErrorMessage) : '';
useEffect(() => {
const id = setInterval(() => {
if (isStopFetching) {
clearInterval(id);
} else if (fileName) {
dispatch(fetchImportStatus(courseId, fileName));
}
}, 3000);
return () => clearInterval(id);
});
let successTitle = intl.formatMessage(messages.stepperSuccessTitle);
const formattedSuccessDate = getFormattedSuccessDate(successDate);
if (currentStage === IMPORT_STAGES.SUCCESS && formattedSuccessDate) {
successTitle += formattedSuccessDate;
}
const handleRedirectCourseOutline = () => window.location.replace(`${getConfig().STUDIO_BASE_URL}/course/${courseId}`);
const steps = [
{
title: intl.formatMessage(messages.stepperUploadingTitle),
description: intl.formatMessage(messages.stepperUploadingDescription),
key: IMPORT_STAGES.UPLOADING,
}, {
title: intl.formatMessage(messages.stepperUnpackingTitle),
description: intl.formatMessage(messages.stepperUnpackingDescription),
key: IMPORT_STAGES.UNPACKING,
}, {
title: intl.formatMessage(messages.stepperVerifyingTitle),
description: intl.formatMessage(messages.stepperVerifyingDescription),
key: IMPORT_STAGES.VERIFYING,
}, {
title: intl.formatMessage(messages.stepperUpdatingTitle),
description: intl.formatMessage(messages.stepperUpdatingDescription),
key: IMPORT_STAGES.UPDATING,
}, {
title: successTitle,
description: intl.formatMessage(messages.stepperSuccessDescription),
key: IMPORT_STAGES.SUCCESS,
},
];
return (
<section>
<h3 className="mt-4">{intl.formatMessage(messages.stepperHeaderTitle)}</h3>
<hr />
<CourseStepper
courseId={courseId}
percent={progress}
steps={steps}
activeKey={currentStage}
hasError={hasError}
errorMessage={formattedErrorMessage}
/>
{currentStage === IMPORT_STAGES.SUCCESS && (
<Button onClick={handleRedirectCourseOutline}>{intl.formatMessage(messages.viewOutlineButton)}</Button>
)}
</section>
);
};
ImportStepper.propTypes = {
intl: intlShape.isRequired,
courseId: PropTypes.string.isRequired,
};
export default injectIntl(ImportStepper);

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { render } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { initializeMockApp } from '@edx/frontend-platform';
import { AppProvider } from '@edx/frontend-platform/react';
import initializeStore from '../../store';
import messages from './messages';
import ImportStepper from './ImportStepper';
const courseId = 'course-123';
let store;
const RootWrapper = () => (
<AppProvider store={store}>
<IntlProvider locale="en" messages={{}}>
<ImportStepper intl={{ formatMessage: jest.fn() }} courseId={courseId} />
</IntlProvider>
</AppProvider>
);
describe('<ImportStepper />', () => {
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
store = initializeStore();
});
it('render stepper correctly', () => {
const { getByText } = render(<RootWrapper />);
expect(getByText(messages.stepperHeaderTitle.defaultMessage)).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,58 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
stepperUploadingTitle: {
id: 'course-authoring.import.stepper.title.uploading',
defaultMessage: 'Uploading',
},
stepperUnpackingTitle: {
id: 'course-authoring.import.stepper.title.unpacking',
defaultMessage: 'Unpacking',
},
stepperVerifyingTitle: {
id: 'course-authoring.import.stepper.title.verifying',
defaultMessage: 'Verifying',
},
stepperUpdatingTitle: {
id: 'course-authoring.import.stepper.title.updating',
defaultMessage: 'Updating сourse',
},
stepperSuccessTitle: {
id: 'course-authoring.import.stepper.title.success',
defaultMessage: 'Success',
},
stepperUploadingDescription: {
id: 'course-authoring.import.stepper.description.uploading',
defaultMessage: 'Transferring your file to our servers',
},
stepperUnpackingDescription: {
id: 'course-authoring.import.stepper.description.unpacking',
defaultMessage: 'Expanding and preparing folder/file structure (You can now leave this page safely, but avoid making drastic changes to content until this import is complete)',
},
stepperVerifyingDescription: {
id: 'course-authoring.import.stepper.description.verifying',
defaultMessage: 'Reviewing semantics, syntax, and required data',
},
stepperUpdatingDescription: {
id: 'course-authoring.import.stepper.description.updating',
defaultMessage: 'Integrating your imported content into this course. This process might take longer with larger courses.',
},
stepperSuccessDescription: {
id: 'course-authoring.import.stepper.description.success',
defaultMessage: 'Your imported content has now been integrated into this course',
},
viewOutlineButton: {
id: 'course-authoring.import.stepper.button.outline',
defaultMessage: 'View updated outline',
},
defaultErrorMessage: {
id: 'course-authoring.import.stepper.error.default',
defaultMessage: 'Error importing course',
},
stepperHeaderTitle: {
id: 'course-authoring.export.stepper.header.title',
defaultMessage: 'Course import status',
},
});
export default messages;

View File

@@ -0,0 +1,30 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
pageTitle: {
id: 'course-authoring.import.page.title',
defaultMessage: '{headingTitle} | {courseName} | {siteName}',
},
headingTitle: {
id: 'course-authoring.import.heading.title',
defaultMessage: 'Course import',
},
headingSubtitle: {
id: 'course-authoring.import.heading.subtitle',
defaultMessage: 'Tools',
},
description1: {
id: 'course-authoring.import.description1',
defaultMessage: 'Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. You cannot undo a course import. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.',
},
description2: {
id: 'course-authoring.import.description2',
defaultMessage: 'The course that you import must be in a .tar.gz file (that is, a .tar file compressed with GNU Zip). This .tar.gz file must contain a course.xml file. It may also contain other files.',
},
description3: {
id: 'course-authoring.import.description3',
defaultMessage: 'The import process has five stages. During the first two stages, you must stay on this page. You can leave this page after the unpacking stage has completed. We recommend, however, that you don\'t make important changes to your course until the import operation has completed.',
},
});
export default messages;

17
src/import-page/utils.js Normal file
View File

@@ -0,0 +1,17 @@
import Cookies from 'universal-cookie';
import { LAST_IMPORT_COOKIE_NAME } from './data/constants';
/**
* Sets an import-related cookie with the provided information.
*
* @param {Date} date - Date of import.
* @param {boolean} completed - Indicates if import was completed successfully.
* @param {string} fileName - File name.
* @returns {void}
*/
// eslint-disable-next-line import/prefer-default-export
export const setImportCookie = (date, completed, fileName) => {
const cookies = new Cookies();
cookies.set(LAST_IMPORT_COOKIE_NAME, { date, completed, fileName }, { path: window.location.pathname });
};

View File

@@ -0,0 +1,29 @@
import Cookies from 'universal-cookie';
import { LAST_IMPORT_COOKIE_NAME } from './data/constants';
import { setImportCookie } from './utils';
global.window = Object.create(window);
Object.defineProperty(window, 'location', {
value: {
pathname: '/some-path',
},
});
describe('setImportCookie', () => {
it('should set the import cookie with the provided data', () => {
const cookiesSetMock = jest.spyOn(Cookies.prototype, 'set');
const date = '2023-07-24';
const completed = true;
const fileName = 'testFileName.test';
setImportCookie(date, completed, fileName);
expect(cookiesSetMock).toHaveBeenCalledWith(
LAST_IMPORT_COOKIE_NAME,
{ date, completed, fileName },
{ path: '/some-path' },
);
cookiesSetMock.mockRestore();
});
});

View File

@@ -19,3 +19,4 @@
@import "course-team/CourseTeam";
@import "course-updates/CourseUpdates";
@import "export-page/CourseExportPage";
@import "import-page/CourseImportPage";

View File

@@ -17,6 +17,7 @@ import { reducer as processingNotificationReducer } from './generic/processing-n
import { reducer as helpUrlsReducer } from './help-urls/data/slice';
import { reducer as courseExportReducer } from './export-page/data/slice';
import { reducer as genericReducer } from './generic/data/slice';
import { reducer as courseImportReducer } from './import-page/data/slice';
export default function initializeStore(preloadedState = undefined) {
return configureStore({
@@ -38,6 +39,7 @@ export default function initializeStore(preloadedState = undefined) {
helpUrls: helpUrlsReducer,
courseExport: courseExportReducer,
generic: genericReducer,
courseImport: courseImportReducer,
},
preloadedState,
});