diff --git a/cms/djangoapps/contentstore/tasks.py b/cms/djangoapps/contentstore/tasks.py index 25ed8596b7..1ad50c68e6 100644 --- a/cms/djangoapps/contentstore/tasks.py +++ b/cms/djangoapps/contentstore/tasks.py @@ -417,7 +417,7 @@ def import_olx(self, user_id, course_key_string, archive_path, archive_name, lan return User.objects.get(pk=user_id) except User.DoesNotExist as exc: with translation_language(language): - self.status.fail(_('Unknown User ID: {0}').format(user_id)) + self.status.fail(_('User permission denied.')) LOGGER.error(f'{log_prefix}: Unknown User: {user_id}') monitor_import_failure(courselike_key, current_step, exception=exc) return @@ -428,7 +428,7 @@ def import_olx(self, user_id, course_key_string, archive_path, archive_name, lan if not has_access: message = f'User permission denied: {user.username}' with translation_language(language): - self.status.fail(_('Permission denied')) + self.status.fail(_('Permission denied. You do not have write access to this course.')) LOGGER.error(f'{log_prefix}: {message}') monitor_import_failure(courselike_key, current_step, message=message) return has_access @@ -452,7 +452,7 @@ def import_olx(self, user_id, course_key_string, archive_path, archive_name, lan if not archive_path_exists: message = f'Uploaded file {archive_path} not found' with translation_language(language): - self.status.fail(_('Tar file not found')) + self.status.fail(_('Uploaded Tar file not found. Try again.')) LOGGER.error(f'{log_prefix}: {message}') monitor_import_failure(courselike_key, current_step, message=message) return archive_path_exists @@ -557,7 +557,7 @@ def import_olx(self, user_id, course_key_string, archive_path, archive_name, lan shutil.rmtree(course_dir) LOGGER.info(f'{log_prefix}: Temp data cleared') - self.status.fail(str(exception)) + self.status.fail(_('An Unknown error occurred during the unpacking step.')) LOGGER.exception(f'{log_prefix}: Unknown error while unpacking', exc_info=True) monitor_import_failure(courselike_key, current_step, exception=exception) return @@ -601,7 +601,8 @@ def import_olx(self, user_id, course_key_string, archive_path, archive_name, lan load_error_modules=False, static_content_store=contentstore(), target_id=courselike_key, - verbose=True + verbose=True, + status=self.status ) new_location = courselike_items[0].location @@ -611,8 +612,9 @@ def import_olx(self, user_id, course_key_string, archive_path, archive_name, lan set_custom_attribute('course_import_completed', True) except Exception as exception: # pylint: disable=broad-except msg = str(exception) - LOGGER.exception(f'{log_prefix}: Unknown error while updating course {msg}') - self.status.fail(msg) + LOGGER.exception(f'{log_prefix}: Unknown error while importing course {msg}') + if self.status.state != UserTaskStatus.FAILED: + self.status.fail(_('Unknown error while importing course.')) monitor_import_failure(courselike_key, current_step, exception=exception) finally: if course_dir.isdir(): diff --git a/cms/djangoapps/contentstore/views/import_export.py b/cms/djangoapps/contentstore/views/import_export.py index 246771219f..f9125d8936 100644 --- a/cms/djangoapps/contentstore/views/import_export.py +++ b/cms/djangoapps/contentstore/views/import_export.py @@ -253,6 +253,7 @@ def import_status_handler(request, course_key_string, filename=None): args = {'course_key_string': course_key_string, 'archive_name': filename} name = CourseImportTask.generate_name(args) task_status = UserTaskStatus.objects.filter(name=name) + message = '' for status_filter in STATUS_FILTERS: task_status = status_filter().filter_queryset(request, task_status, import_status_handler) task_status = task_status.order_by('-created').first() @@ -267,10 +268,13 @@ def import_status_handler(request, course_key_string, filename=None): status = 4 elif task_status.state in (UserTaskStatus.FAILED, UserTaskStatus.CANCELED): status = max(-(task_status.completed_steps + 1), -3) + artifact = UserTaskArtifact.objects.filter(name='Error', status=task_status).order_by('-created').first() + if artifact: + message = artifact.text else: status = min(task_status.completed_steps + 1, 3) - return JsonResponse({"ImportStatus": status}) + return JsonResponse({"ImportStatus": status, "Message": message}) def send_tarball(tarball, size): diff --git a/cms/static/js/features/import/views/import.js b/cms/static/js/features/import/views/import.js index 928071eae5..8bd9f91ed7 100644 --- a/cms/static/js/features/import/views/import.js +++ b/cms/static/js/features/import/views/import.js @@ -240,7 +240,7 @@ define( * * @param {int} [stage=0] Starting stage. */ - pollStatus: function(stage) { + pollStatus: function(stage, message) { if (current.state !== STATE.IN_PROGRESS) { return; } @@ -250,13 +250,13 @@ define( if (current.stage === STAGE.SUCCESS) { success(); } else if (current.stage < STAGE.UPLOADING) { // Failed - error(gettext('Error importing course')); + error(message || gettext('Error importing course')); } else { // In progress updateFeedbackList(); $.getJSON(file.url, function(data) { timeout.id = setTimeout(function() { - this.pollStatus(data.ImportStatus); + this.pollStatus(data.ImportStatus, data.Message); }.bind(this), timeout.delay); }.bind(this)); } @@ -292,7 +292,7 @@ define( if (current.stage !== STAGE.UPLOADING) { current.state = STATE.IN_PROGRESS; - this.pollStatus(current.stage); + this.pollStatus(current.stage, data.Message); } else { // An import in the upload stage cannot be resumed error(gettext('There was an error with the upload')); diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py index 2f6a31b2e9..f4a145e458 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml_importer.py +++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py @@ -30,6 +30,7 @@ import re from abc import abstractmethod import xblock +from django.utils.translation import ugettext as _ from lxml import etree from opaque_keys.edx.keys import UsageKey from opaque_keys.edx.locator import LibraryLocator @@ -233,6 +234,7 @@ class ImportManager: create_if_not_present=False, raise_on_failure=False, static_content_subdir=DEFAULT_STATIC_CONTENT_SUBDIR, python_lib_filename='python_lib.zip', + status=None ): self.store = store self.user_id = user_id @@ -257,6 +259,7 @@ class ImportManager: xblock_select=store.xblock_select, target_course_id=target_id, ) + self.status = status self.logger, self.errors = make_error_tracker() def preflight(self): @@ -362,6 +365,8 @@ class ImportManager: monitor_import_failure(course_id, 'Updating', exception=exc) logging.exception(f'Course import {course_id}: Error while parsing asset xml.') if self.raise_on_failure: # lint-amnesty, pylint: disable=no-else-raise + if self.status: + self.status.fail(_('Error while parsing xml for {}').format(assets_filename)) raise else: return @@ -479,6 +484,12 @@ class ImportManager: log.exception( f'Course import {dest_id}: failed to import module location {child.location}' ) + if self.status: + self.status.fail( + _('Failed to import module: {} at location: {}').format( + child.display_name, child.location + ) + ) raise depth_first(child) @@ -502,6 +513,12 @@ class ImportManager: except Exception: msg = f'Course import {dest_id}: failed to import module location {leftover}' log.error(msg) + if self.status: + self.status.fail( + _('Failed to import module: {} at location: {}').format( + leftover.display_name, leftover.location + ) + ) raise def run_imports(self): @@ -588,6 +605,10 @@ class CourseImportManager(ImportManager): "Skipping import of course with id, %s, " "since it collides with an existing one", dest_id ) + if self.status: + self.status.fail( + _('Aborting import because a course with this id: {} already exists.').format(dest_id) + ) raise return dest_id, runtime @@ -697,6 +718,8 @@ class LibraryImportManager(ImportManager): "Skipping import of Library with id %s, " "since it collides with an existing one", dest_id ) + if self.status: + self.status.fail(_('Aborting import since a library with this id already exists.')) raise return dest_id, runtime