Commit 8ad4d081 by Jonathan Piacenti

Added library import and export via .tar.gz'd XML files.

parent 3359d6e3
......@@ -15,7 +15,7 @@ from django.utils.translation import ugettext_lazy as _
from xmodule.contentstore.django import contentstore
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.xml_exporter import export_course_to_xml
log = logging.getLogger(__name__)
......@@ -133,8 +133,8 @@ def export_to_git(course_id, repo, user='', rdir=None):
root_dir = os.path.dirname(rdirp)
course_dir = os.path.basename(rdirp).rsplit('.git', 1)[0]
try:
export_to_xml(modulestore(), contentstore(), course_id,
root_dir, course_dir)
export_course_to_xml(modulestore(), contentstore(), course_id,
root_dir, course_dir)
except (EnvironmentError, AttributeError):
log.exception('Failed export to xml')
raise GitExportError(GitExportError.XML_EXPORT_FAIL)
......
......@@ -4,7 +4,7 @@ Script for exporting courseware from Mongo to a tar.gz file
import os
from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.xml_exporter import export_course_to_xml
from xmodule.modulestore.django import modulestore
from opaque_keys.edx.keys import CourseKey
from xmodule.contentstore.django import contentstore
......@@ -35,4 +35,4 @@ class Command(BaseCommand):
root_dir = os.path.dirname(output_path)
course_dir = os.path.splitext(os.path.basename(output_path))[0]
export_to_xml(modulestore(), contentstore(), course_key, root_dir, course_dir)
export_course_to_xml(modulestore(), contentstore(), course_key, root_dir, course_dir)
......@@ -2,7 +2,7 @@
Script for exporting all courseware from Mongo to a directory and listing the courses which failed to export
"""
from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.xml_exporter import export_course_to_xml
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore
......@@ -49,7 +49,7 @@ def export_courses_to_output_path(output_path):
print(u"Exporting course id = {0} to {1}".format(course_id, output_path))
try:
course_dir = course_id.to_deprecated_string().replace('/', '...')
export_to_xml(module_store, content_store, course_id, root_dir, course_dir)
export_course_to_xml(module_store, content_store, course_id, root_dir, course_dir)
except Exception as err: # pylint: disable=broad-except
failed_export_courses.append(unicode(course_id))
print(u"=" * 30 + u"> Oops, failed to export {0}".format(course_id))
......
......@@ -5,7 +5,7 @@ Script for importing courseware from XML format
from django.core.management.base import BaseCommand, CommandError, make_option
from django_comment_common.utils import (seed_permissions_roles,
are_permissions_roles_seeded)
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.xml_importer import import_course_from_xml
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore
......@@ -31,20 +31,20 @@ class Command(BaseCommand):
data_dir = args[0]
do_import_static = not (options.get('nostatic', False))
if len(args) > 1:
course_dirs = args[1:]
source_dirs = args[1:]
else:
course_dirs = None
self.stdout.write("Importing. Data_dir={data}, course_dirs={courses}\n".format(
source_dirs = None
self.stdout.write("Importing. Data_dir={data}, source_dirs={courses}\n".format(
data=data_dir,
courses=course_dirs,
dis=do_import_static))
courses=source_dirs,
))
mstore = modulestore()
course_items = import_from_xml(
mstore, ModuleStoreEnum.UserID.mgmt_command, data_dir, course_dirs, load_error_modules=False,
course_items = import_course_from_xml(
mstore, ModuleStoreEnum.UserID.mgmt_command, data_dir, source_dirs, load_error_modules=False,
static_content_store=contentstore(), verbose=True,
do_import_static=do_import_static,
create_course_if_not_present=True,
create_if_not_present=True,
)
for course in course_items:
......
......@@ -11,7 +11,7 @@ from xmodule.modulestore.django import modulestore
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.mongo.base import location_to_query
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.xml_importer import import_course_from_xml
from django.conf import settings
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
......@@ -34,7 +34,7 @@ class ExportAllCourses(ModuleStoreTestCase):
This test validates that redundant Mac metadata files ('._example.txt', '.DS_Store') are
cleaned up on import
"""
import_from_xml(
import_course_from_xml(
self.module_store,
'**replace_user**',
TEST_DATA_DIR,
......
......@@ -16,10 +16,10 @@ class Command(BaseCommand):
data_dir = args[0]
if len(args) > 1:
course_dirs = args[1:]
source_dirs = args[1:]
else:
course_dirs = None
print("Importing. Data_dir={data}, course_dirs={courses}".format(
source_dirs = None
print("Importing. Data_dir={data}, source_dirs={courses}".format(
data=data_dir,
courses=course_dirs))
perform_xlint(data_dir, course_dirs, load_error_modules=False)
courses=source_dirs))
perform_xlint(data_dir, source_dirs, load_error_modules=False)
......@@ -2,7 +2,7 @@
# pylint: disable=no-member
# pylint: disable=protected-access
"""
Tests for import_from_xml using the mongo modulestore.
Tests for import_course_from_xml using the mongo modulestore.
"""
from django.test.client import Client
......@@ -16,7 +16,7 @@ from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore
from xmodule.modulestore.tests.factories import check_exact_number_of_calls, check_number_of_calls
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.xml_importer import import_course_from_xml
from xmodule.exceptions import NotFoundError
from uuid import uuid4
......@@ -39,7 +39,7 @@ class ContentStoreImportTest(ModuleStoreTestCase):
self.client = Client()
self.client.login(username=self.user.username, password=password)
def load_test_import_course(self, target_course_id=None, create_course_if_not_present=True, module_store=None):
def load_test_import_course(self, target_id=None, create_if_not_present=True, module_store=None):
'''
Load the standard course used to test imports
(for do_import_static=False behavior).
......@@ -47,7 +47,7 @@ class ContentStoreImportTest(ModuleStoreTestCase):
content_store = contentstore()
if module_store is None:
module_store = modulestore()
import_from_xml(
import_course_from_xml(
module_store,
self.user.id,
TEST_DATA_DIR,
......@@ -55,8 +55,8 @@ class ContentStoreImportTest(ModuleStoreTestCase):
static_content_store=content_store,
do_import_static=False,
verbose=True,
target_course_id=target_course_id,
create_course_if_not_present=create_course_if_not_present,
target_id=target_id,
create_if_not_present=create_if_not_present,
)
course_id = module_store.make_course_key('edX', 'test_import_course', '2012_Fall')
course = module_store.get_course(course_id)
......@@ -69,12 +69,12 @@ class ContentStoreImportTest(ModuleStoreTestCase):
# edx/course can be imported into a namespace with an org/course
# like edx/course_name
module_store, __, course = self.load_test_import_course()
course_items = import_from_xml(
course_items = import_course_from_xml(
module_store,
self.user.id,
TEST_DATA_DIR,
['test_import_course_2'],
target_course_id=course.id,
target_id=course.id,
verbose=True,
)
self.assertEqual(len(course_items), 1)
......@@ -87,13 +87,13 @@ class ContentStoreImportTest(ModuleStoreTestCase):
with modulestore().default_store(ModuleStoreEnum.Type.split):
module_store = modulestore()
course_id = module_store.make_course_key(u'Юникода', u'unicode_course', u'échantillon')
import_from_xml(
import_course_from_xml(
module_store,
self.user.id,
TEST_DATA_DIR,
['2014_Uni'],
target_course_id=course_id,
create_course_if_not_present=True
target_id=course_id,
create_if_not_present=True
)
course = module_store.get_course(course_id)
......@@ -134,10 +134,10 @@ class ContentStoreImportTest(ModuleStoreTestCase):
content_store = contentstore()
module_store = modulestore()
import_from_xml(
import_course_from_xml(
module_store, self.user.id, TEST_DATA_DIR, ['toy'],
static_content_store=content_store, do_import_static=False,
create_course_if_not_present=True, verbose=True
create_if_not_present=True, verbose=True
)
course = module_store.get_course(module_store.make_course_key('edX', 'toy', '2012_Fall'))
......@@ -149,9 +149,9 @@ class ContentStoreImportTest(ModuleStoreTestCase):
def test_no_static_link_rewrites_on_import(self):
module_store = modulestore()
courses = import_from_xml(
courses = import_course_from_xml(
module_store, self.user.id, TEST_DATA_DIR, ['toy'], do_import_static=False, verbose=True,
create_course_if_not_present=True
create_if_not_present=True
)
course_key = courses[0].id
......@@ -179,63 +179,63 @@ class ContentStoreImportTest(ModuleStoreTestCase):
# NOTE: On Jenkins, with memcache enabled, the number of calls here is only 1.
# Locally, without memcache, the number of calls is actually 2 (once more during the publish step)
with check_number_of_calls(store, '_compute_metadata_inheritance_tree', 2):
self.load_test_import_course(create_course_if_not_present=False, module_store=store)
self.load_test_import_course(create_if_not_present=False, module_store=store)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_reimport(self, default_ms_type):
with modulestore().default_store(default_ms_type):
__, __, course = self.load_test_import_course(create_course_if_not_present=True)
self.load_test_import_course(target_course_id=course.id)
__, __, course = self.load_test_import_course(create_if_not_present=True)
self.load_test_import_course(target_id=course.id)
def test_rewrite_reference_list(self):
# This test fails with split modulestore (the HTML component is not in "different_course_id" namespace).
# More investigation needs to be done.
module_store = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.mongo)
target_course_id = module_store.make_course_key('testX', 'conditional_copy', 'copy_run')
import_from_xml(
target_id = module_store.make_course_key('testX', 'conditional_copy', 'copy_run')
import_course_from_xml(
module_store,
self.user.id,
TEST_DATA_DIR,
['conditional'],
target_course_id=target_course_id
target_id=target_id
)
conditional_module = module_store.get_item(
target_course_id.make_usage_key('conditional', 'condone')
target_id.make_usage_key('conditional', 'condone')
)
self.assertIsNotNone(conditional_module)
different_course_id = module_store.make_course_key('edX', 'different_course', None)
self.assertListEqual(
[
target_course_id.make_usage_key('problem', 'choiceprob'),
target_id.make_usage_key('problem', 'choiceprob'),
different_course_id.make_usage_key('html', 'for_testing_import_rewrites')
],
conditional_module.sources_list
)
self.assertListEqual(
[
target_course_id.make_usage_key('html', 'congrats'),
target_course_id.make_usage_key('html', 'secret_page')
target_id.make_usage_key('html', 'congrats'),
target_id.make_usage_key('html', 'secret_page')
],
conditional_module.show_tag_list
)
def test_rewrite_reference(self):
module_store = modulestore()
target_course_id = module_store.make_course_key('testX', 'peergrading_copy', 'copy_run')
import_from_xml(
target_id = module_store.make_course_key('testX', 'peergrading_copy', 'copy_run')
import_course_from_xml(
module_store,
self.user.id,
TEST_DATA_DIR,
['open_ended'],
target_course_id=target_course_id,
create_course_if_not_present=True
target_id=target_id,
create_if_not_present=True
)
peergrading_module = module_store.get_item(
target_course_id.make_usage_key('peergrading', 'PeerGradingLinked')
target_id.make_usage_key('peergrading', 'PeerGradingLinked')
)
self.assertIsNotNone(peergrading_module)
self.assertEqual(
target_course_id.make_usage_key('combinedopenended', 'SampleQuestion'),
target_id.make_usage_key('combinedopenended', 'SampleQuestion'),
peergrading_module.link_to_location
)
......@@ -263,22 +263,22 @@ class ContentStoreImportTest(ModuleStoreTestCase):
def _verify_split_test_import(self, target_course_name, source_course_name, split_test_name, groups_to_verticals):
module_store = modulestore()
target_course_id = module_store.make_course_key('testX', target_course_name, 'copy_run')
import_from_xml(
target_id = module_store.make_course_key('testX', target_course_name, 'copy_run')
import_course_from_xml(
module_store,
self.user.id,
TEST_DATA_DIR,
[source_course_name],
target_course_id=target_course_id,
create_course_if_not_present=True
target_id=target_id,
create_if_not_present=True
)
split_test_module = module_store.get_item(
target_course_id.make_usage_key('split_test', split_test_name)
target_id.make_usage_key('split_test', split_test_name)
)
self.assertIsNotNone(split_test_module)
remapped_verticals = {
key: target_course_id.make_usage_key('vertical', value) for key, value in groups_to_verticals.iteritems()
key: target_id.make_usage_key('vertical', value) for key, value in groups_to_verticals.iteritems()
}
self.assertEqual(remapped_verticals, split_test_module.group_id_to_child)
from xmodule.modulestore.xml_importer import import_from_xml
"""
Tests Draft import order.
"""
from xmodule.modulestore.xml_importer import import_course_from_xml
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.django import modulestore
......@@ -12,9 +15,12 @@ TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
class DraftReorderTestCase(ModuleStoreTestCase):
def test_order(self):
"""
Verify that drafts are imported in the correct order.
"""
store = modulestore()
course_items = import_from_xml(
store, self.user.id, TEST_DATA_DIR, ['import_draft_order'], create_course_if_not_present=True
course_items = import_course_from_xml(
store, self.user.id, TEST_DATA_DIR, ['import_draft_order'], create_if_not_present=True
)
course_key = course_items[0].id
sequential = store.get_item(course_key.make_usage_key('sequential', '0f4f7649b10141b0bdc9922dcf94515a'))
......
......@@ -7,7 +7,7 @@ from xblock.fields import String
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.xml_importer import import_course_from_xml
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.mongo.draft import as_draft
from django.conf import settings
......@@ -65,8 +65,8 @@ class XBlockImportTest(ModuleStoreTestCase):
# It is necessary to use the "old mongo" modulestore because split doesn't work
# with the "has_draft" logic below.
store = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.mongo) # pylint: disable=protected-access
courses = import_from_xml(
store, self.user.id, TEST_DATA_DIR, [course_dir], create_course_if_not_present=True
courses = import_course_from_xml(
store, self.user.id, TEST_DATA_DIR, [course_dir], create_if_not_present=True
)
xblock_location = courses[0].id.make_usage_key('stubxblock', 'xblock_test')
......
......@@ -19,7 +19,7 @@ from xmodule.modulestore.inheritance import own_metadata
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.django_utils import TEST_DATA_MOCK_MODULESTORE
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.xml_importer import import_course_from_xml
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
......@@ -148,7 +148,7 @@ class CourseTestCase(ModuleStoreTestCase):
Imports the test toy course and populates it with additional test data
"""
content_store = contentstore()
import_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['toy'], static_content_store=content_store)
import_course_from_xml(self.store, self.user.id, TEST_DATA_DIR, ['toy'], static_content_store=content_store)
course_id = SlashSeparatedCourseKey('edX', 'toy', '2012_Fall')
# create an Orphan
......
......@@ -197,7 +197,6 @@ def library_blocks_view(library, user, response_format):
'component_templates': json.dumps(component_templates),
'xblock_info': xblock_info,
'templates': CONTAINER_TEMPATES,
'lib_users_url': reverse_library_url('manage_library_users', unicode(library.location.library_key)),
})
......
......@@ -14,7 +14,7 @@ from xmodule.assetstore import AssetMetadata
from xmodule.contentstore.content import StaticContent
from xmodule.contentstore.django import contentstore
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.xml_importer import import_course_from_xml
from django.test.utils import override_settings
from opaque_keys.edx.locations import SlashSeparatedCourseKey, AssetLocation
import mock
......@@ -65,7 +65,7 @@ class BasicAssetsTestCase(AssetsTestCase):
def test_pdf_asset(self):
module_store = modulestore()
course_items = import_from_xml(
course_items = import_course_from_xml(
module_store,
self.user.id,
TEST_DATA_DIR,
......@@ -349,7 +349,7 @@ class LockAssetTestCase(AssetsTestCase):
# Load the toy course.
module_store = modulestore()
course_items = import_from_xml(
course_items = import_course_from_xml(
module_store,
self.user.id,
TEST_DATA_DIR,
......
......@@ -4,6 +4,7 @@ Unit tests for course import and export
import copy
import json
import logging
import lxml
import os
import shutil
import tarfile
......@@ -13,16 +14,22 @@ from uuid import uuid4
from django.test.utils import override_settings
from django.conf import settings
from xmodule.contentstore.django import contentstore
from xmodule.modulestore.xml_exporter import export_library_to_xml
from xmodule.modulestore.xml_importer import import_library_from_xml
from xmodule.modulestore import LIBRARY_ROOT
from contentstore.utils import reverse_course_url
from xmodule.modulestore.tests.factories import ItemFactory
from xmodule.modulestore.tests.factories import ItemFactory, LibraryFactory
from contentstore.tests.utils import CourseTestCase
from extract_tar import safetar_extractall
from student import auth
from student.roles import CourseInstructorRole, CourseStaffRole
TEST_DATA_CONTENTSTORE = copy.deepcopy(settings.CONTENTSTORE)
TEST_DATA_CONTENTSTORE['DOC_STORE_CONFIG']['db'] = 'test_xcontent_%s' % uuid4().hex
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
log = logging.getLogger(__name__)
......@@ -30,7 +37,7 @@ log = logging.getLogger(__name__)
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE)
class ImportTestCase(CourseTestCase):
"""
Unit tests for importing a course
Unit tests for importing a course or Library
"""
def setUp(self):
super(ImportTestCase, self).setUp()
......@@ -241,6 +248,81 @@ class ImportTestCase(CourseTestCase):
import_status = json.loads(resp_status.content)["ImportStatus"]
self.assertIn(import_status, (0, 3))
def test_library_import(self):
"""
Try importing a known good library archive, and verify that the
contents of the library have completely replaced the old contents.
"""
# Create some blocks to overwrite
library = LibraryFactory.create(modulestore=self.store)
lib_key = library.location.library_key
test_block = ItemFactory.create(
category="vertical",
parent_location=library.location,
user_id=self.user.id,
publish_item=False,
)
test_block2 = ItemFactory.create(
category="vertical",
parent_location=library.location,
user_id=self.user.id,
publish_item=False
)
# Create a library and blocks that should remain unmolested.
unchanged_lib = LibraryFactory.create()
unchanged_key = unchanged_lib.location.library_key
test_block3 = ItemFactory.create(
category="vertical",
parent_location=unchanged_lib.location,
user_id=self.user.id,
publish_item=False
)
test_block4 = ItemFactory.create(
category="vertical",
parent_location=unchanged_lib.location,
user_id=self.user.id,
publish_item=False
)
# Refresh library.
library = self.store.get_library(lib_key)
children = [self.store.get_item(child).url_name for child in library.children]
self.assertEqual(len(children), 2)
self.assertIn(test_block.url_name, children)
self.assertIn(test_block2.url_name, children)
unchanged_lib = self.store.get_library(unchanged_key)
children = [self.store.get_item(child).url_name for child in unchanged_lib.children]
self.assertEqual(len(children), 2)
self.assertIn(test_block3.url_name, children)
self.assertIn(test_block4.url_name, children)
extract_dir = path(tempfile.mkdtemp())
try:
tar = tarfile.open(path(TEST_DATA_DIR) / 'imports' / 'library.HhJfPD.tar.gz')
safetar_extractall(tar, extract_dir)
library_items = import_library_from_xml(
self.store, self.user.id,
settings.GITHUB_REPO_ROOT, [extract_dir / 'library'],
load_error_modules=False,
static_content_store=contentstore(),
target_id=lib_key
)
finally:
shutil.rmtree(extract_dir)
self.assertEqual(lib_key, library_items[0].location.library_key)
library = self.store.get_library(lib_key)
children = [self.store.get_item(child).url_name for child in library.children]
self.assertEqual(len(children), 3)
self.assertNotIn(test_block.url_name, children)
self.assertNotIn(test_block2.url_name, children)
unchanged_lib = self.store.get_library(unchanged_key)
children = [self.store.get_item(child).url_name for child in unchanged_lib.children]
self.assertEqual(len(children), 2)
self.assertIn(test_block3.url_name, children)
self.assertIn(test_block4.url_name, children)
@override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE)
class ExportTestCase(CourseTestCase):
......@@ -315,3 +397,35 @@ class ExportTestCase(CourseTestCase):
self.assertIsNone(resp.get('Content-Disposition'))
self.assertContains(resp, 'Unable to create xml for module')
self.assertContains(resp, expected_text)
def test_library_export(self):
"""
Verify that useable library data can be exported.
"""
youtube_id = "qS4NO9MNC6w"
library = LibraryFactory.create(modulestore=self.store)
video_block = ItemFactory.create(
category="video",
parent_location=library.location,
user_id=self.user.id,
publish_item=False,
youtube_id_1_0=youtube_id
)
name = library.url_name
lib_key = library.location.library_key
root_dir = path(tempfile.mkdtemp())
try:
export_library_to_xml(self.store, contentstore(), lib_key, root_dir, name)
# pylint: disable=no-member
lib_xml = lxml.etree.XML(open(root_dir / name / LIBRARY_ROOT).read())
self.assertEqual(lib_xml.get('org'), lib_key.org)
self.assertEqual(lib_xml.get('library'), lib_key.library)
block = lib_xml.find('video')
self.assertIsNotNone(block)
self.assertEqual(block.get('url_name'), video_block.url_name)
# pylint: disable=no-member
video_xml = lxml.etree.XML(open(root_dir / name / 'video' / video_block.url_name + '.xml').read())
self.assertEqual(video_xml.tag, 'video')
self.assertEqual(video_xml.get('youtube_id_1_0'), youtube_id)
finally:
shutil.rmtree(root_dir / name)
define(['gettext', 'js/views/feedback_prompt'], function(gettext, PromptView) {
'use strict';
return function (hasUnit, editUnitUrl, courseHomeUrl, errMsg) {
return function (hasUnit, editUnitUrl, courselikeHomeUrl, library, errMsg) {
var dialog;
if(hasUnit) {
dialog = new PromptView({
......@@ -24,17 +24,26 @@ define(['gettext', 'js/views/feedback_prompt'], function(gettext, PromptView) {
}
});
} else {
var msg = '<p>' + gettext('There has been a failure to export your course to XML. Unfortunately, we do not have specific enough information to assist you in identifying the failed component. It is recommended that you inspect your courseware to identify any components in error and try again.') + '</p><p>' + gettext('The raw error message is:') + '</p>' + errMsg;
var msg = '<p>';
var action;
if (library) {
msg += gettext('Your library could not be exported to XML. There is not enough information to identify the failed component. Inspect your library to identify any problematic components and try again.');
action = gettext('Take me to the main library page')
} else {
msg += gettext('Your course could not be exported to XML. There is not enough information to identify the failed component. Inspect your course to identify any problematic components and try again.');
action = gettext('Take me to the main course page')
}
msg += '</p><p>' + gettext('The raw error message is:') + '</p>' + errMsg;
dialog = new PromptView({
title: gettext('There has been an error with your export.'),
message: msg,
intent: 'error',
actions: {
primary: {
text: gettext('Yes, take me to the main course page'),
text: action,
click: function(view) {
view.hide();
document.location = courseHomeUrl;
document.location = courselikeHomeUrl;
}
},
secondary: {
......
define([
'js/views/import', 'jquery', 'gettext', 'jquery.fileupload', 'jquery.cookie'
], function(CourseImport, $, gettext) {
], function(Import, $, gettext) {
'use strict';
return function (feedbackUrl) {
return function (feedbackUrl, library) {
var dbError;
if (library) {
dbError = gettext('There was an error while importing the new library to our database.');
} else {
dbError = gettext('There was an error while importing the new course to our database.');
}
var bar = $('.progress-bar'),
fill = $('.progress-fill'),
submitBtn = $('.submit-button'),
......@@ -11,14 +17,14 @@ define([
gettext('There was an error during the upload process.') + '\n',
gettext('There was an error while unpacking the file.') + '\n',
gettext('There was an error while verifying the file you submitted.') + '\n',
gettext('There was an error while importing the new course to our database.') + '\n'
dbError + '\n'
],
// Display the status of last file upload on page load
lastFileUpload = $.cookie('lastfileupload'),
file;
if (lastFileUpload){
CourseImport.getAndStartUploadFeedback(feedbackUrl.replace('fillerName', lastFileUpload), lastFileUpload);
Import.getAndStartUploadFeedback(feedbackUrl.replace('fillerName', lastFileUpload), lastFileUpload);
}
$('#fileupload').fileupload({
......@@ -27,8 +33,8 @@ define([
maxChunkSize: 20 * 1000000, // 20 MB
autoUpload: false,
add: function(e, data) {
CourseImport.clearImportDisplay();
CourseImport.okayToNavigateAway = false;
Import.clearImportDisplay();
Import.okayToNavigateAway = false;
submitBtn.unbind('click');
file = data.files[0];
if (file.name.match(/tar\.gz$/)) {
......@@ -36,7 +42,7 @@ define([
event.preventDefault();
$.cookie('lastfileupload', file.name);
submitBtn.hide();
CourseImport.startUploadFeedback();
Import.startUploadFeedback();
data.submit().complete(function(result, textStatus, xhr) {
window.onbeforeunload = null;
if (xhr.status != 200) {
......@@ -49,7 +55,7 @@ define([
errMsg = serverMsg.hasOwnProperty('ErrMsg') ? serverMsg.ErrMsg : '' ;
if (serverMsg.hasOwnProperty('Stage')) {
stage = Math.abs(serverMsg.Stage);
CourseImport.stageError(stage, defaults[stage] + errMsg);
Import.stageError(stage, defaults[stage] + errMsg);
}
else {
alert(gettext('Your import has failed.') + '\n\n' + errMsg);
......@@ -57,7 +63,7 @@ define([
chooseBtn.html(gettext('Choose new file')).show();
bar.hide();
}
CourseImport.stopGetStatus = true;
Import.stopGetStatus = true;
chooseBtn.html(gettext('Choose new file')).show();
bar.hide();
});
......@@ -83,7 +89,7 @@ define([
bar.hide();
// Start feedback with delay so that current stage of import properly updates in session
setTimeout(
function () { CourseImport.startServerFeedback(feedbackUrl.replace('fillerName', file.name));},
function () { Import.startServerFeedback(feedbackUrl.replace('fillerName', file.name));},
3000
);
} else {
......@@ -94,11 +100,11 @@ define([
done: function(event, data){
bar.hide();
window.onbeforeunload = null;
CourseImport.displayFinishedImport();
Import.displayFinishedImport();
},
start: function(event) {
window.onbeforeunload = function() {
if (!CourseImport.okayToNavigateAway) {
if (!Import.okayToNavigateAway) {
return "${_('Your import is in progress; navigating away will abort it.')}";
}
};
......
......@@ -108,6 +108,9 @@ define(
$(elem).find('p.copy').show();
updateCog($(elem), false);
});
all.find('.fa-check-square-o'). // Replace checkmark with unchecked box
removeClass('fa-check-square-o').
addClass('fa-square-o');
this.stopGetStatus = false;
},
......@@ -119,12 +122,16 @@ define(
this.stopGetStatus = true;
var all = $('ol.status-progress').children();
_.map(all, function (elem){
elem = $(elem);
$(elem).
removeClass("is-not-started").
removeClass("is-started").
addClass("is-complete");
updateCog($(elem), false);
});
all.find('.fa-square-o').
removeClass('fa-square-o').
addClass('fa-check-square-o');
},
/**
......
......@@ -6,18 +6,25 @@
from django.utils.translation import ugettext as _
import json
%>
<%block name="title">${_("Course Export")}</%block>
<%block name="title">
%if library:
${_("Library Export")}
%else:
${_("Course Export")}
%endif
</%block>
<%block name="bodyclass">is-signedin course tools view-export</%block>
<%block name="requirejs">
% if in_err:
var hasUnit = ${json.dumps(bool(unit))},
editUnitUrl = "${edit_unit_url or ""}",
courseHomeUrl = "${course_home_url or ""}",
courselikeHomeUrl = "${courselike_home_url or ""}",
is_library = ${json.dumps(library)}
errMsg = ${json.dumps(raw_err_msg or "")};
require(["js/factories/export"], function(ExportFactory) {
ExportFactory(hasUnit, editUnitUrl, courseHomeUrl, errMsg);
ExportFactory(hasUnit, editUnitUrl, courselikeHomeUrl, is_library, errMsg);
});
%endif
</%block>
......@@ -27,7 +34,12 @@
<header class="mast has-subtitle">
<h1 class="page-header">
<small class="subtitle">${_("Tools")}</small>
<span class="sr">&gt; </span>${_("Course Export")}
<span class="sr">&gt; </span>
%if library:
${_("Library Export")}
%else:
${_("Course Export")}
%endif
</h1>
</header>
</div>
......@@ -37,29 +49,49 @@
<article class="content-primary" role="main">
<div class="introduction">
<h2 class="title">${_("About Exporting Courses")}</h2>
<div class="copy">
## Translators: ".tar.gz" is a file extension, and should not be translated
<p>${_("You can export courses and edit them outside of {studio_name}. The exported file is a .tar.gz file (that is, a .tar file compressed with GNU Zip) that contains the course structure and content. You can also re-import courses that you've exported.").format(
studio_name=settings.STUDIO_SHORT_NAME, em_start='<strong>', em_end="</strong>"
%if library:
<h2 class="title">${_("About Exporting Libraries")}</h2>
<div class="copy">
## Translators: ".tar.gz" is a file extension, and should not be translated
<p>${_("You can export libraries and edit them outside of {studio_name}. The exported file is a .tar.gz file (that is, a .tar file compressed with GNU Zip) that contains the library structure and content. You can also re-import libraries that you've exported.").format(
studio_name=settings.STUDIO_SHORT_NAME,
)}</p>
<p>${_("{em_start}Caution:{em_end} When you export a course, information such as MATLAB API keys, LTI passports, annotation secret token strings, and annotation storage URLs are included in the exported data. If you share your exported files, you may also be sharing sensitive or license-specific information.").format(em_start='<strong>', em_end="</strong>")}</p>
</div>
</div>
%else:
<h2 class="title">${_("About Exporting Courses")}</h2>
<div class="copy">
## Translators: ".tar.gz" is a file extension, and should not be translated
<p>${_("You can export courses and edit them outside of {studio_name}. The exported file is a .tar.gz file (that is, a .tar file compressed with GNU Zip) that contains the course structure and content. You can also re-import courses that you've exported.").format(
studio_name=settings.STUDIO_SHORT_NAME
)}</p>
<p>${_("{em_start}Caution:{em_end} When you export a course, information such as MATLAB API keys, LTI passports, annotation secret token strings, and annotation storage URLs are included in the exported data. If you share your exported files, you may also be sharing sensitive or license-specific information.").format(em_start='<strong>', em_end="</strong>")}</p>
</div>
%endif
</div>
<div class="export-controls">
<h2 class="title">${_("Export My Course Content")}</h2>
<h2 class="title">
%if library:
${_("Export My Library Content")}
%else:
${_("Export My Course Content")}
%endif</h2>
<ul class="list-actions">
<li class="item-action">
<a class="action action-export action-primary" href="${export_url}">
<i class="icon fa fa-arrow-circle-o-down"></i>
<span class="copy">${_("Export Course Content")}</span>
<span class="copy">
%if library:
${_("Export Library Content")}
%else:
${_("Export Course Content")}
%endif</span>
</a>
</li>
</ul>
</div>
%if not library:
<div class="export-contents">
<div class="export-includes">
<h3 class="title-3">${_("Data {em_start}exported with{em_end} your course:").format(em_start='<strong>', em_end="</strong>")}</h3>
......@@ -84,8 +116,27 @@
</ul>
</div>
</div>
%endif
</article>
%if library:
<aside class="content-supplementary" role="complementary">
<div class="bit">
<h3 class="title-3">${_("Why export a library?")}</h3>
<p>${_("You may want to edit the XML in your library directly, outside of {studio_name}. You may want to create a backup copy of your library. Or, you may want to create a copy of your library that you can later import into another library instance and customize.").format(
studio_name=settings.STUDIO_SHORT_NAME,
)}</p>
</div>
<div class="bit">
<h3 class="title-3">${_("Opening the downloaded file")}</h3>
## Translators: ".tar.gz" is a file extension, and should not be translated
<p>${_("Use an archive program to extract the data from the .tar.gz file. Extracted data includes the library.xml file, as well as subfolders that contain library content.")}</p>
</div>
<div class="bit external-help">
<a href="${get_online_help_info(online_help_token())['doc_url']}" target="_blank" class="button external-help-button">${_("Learn more about exporting a library")}</a>
</div>
</aside>
%else:
<aside class="content-supplementary" role="complementary">
<div class="bit">
<h3 class="title-3">${_("Why export a course?")}</h3>
......@@ -109,6 +160,7 @@
<a href="${get_online_help_info(online_help_token())['doc_url']}" target="_blank" class="button external-help-button">${_("Learn more about exporting a course")}</a>
</div>
</aside>
%endif
</section>
</div>
</%block>
......@@ -3,8 +3,19 @@
<%namespace name='static' file='static_content.html'/>
<%!
from django.utils.translation import ugettext as _
import json
try:
library = True
except NameError:
library = False
%>
<%block name="title">${_("Course Import")}</%block>
<%block name="title">
%if library:
${_("Library Import")}
%else:
${_("Course Import")}
%endif
</%block>
<%block name="bodyclass">is-signedin course tools view-import</%block>
<%block name="content">
......@@ -12,7 +23,12 @@
<header class="mast has-subtitle">
<h1 class="page-header">
<small class="subtitle">${_("Tools")}</small>
<span class="sr">&gt; </span>${_("Course Import")}
<span class="sr">&gt; </span>
%if library:
${_("Library Import")}
%else:
${_("Course Import")}
%endif
</h1>
</header>
</div>
......@@ -20,18 +36,30 @@
<div class="wrapper-content wrapper">
<section class="content">
<article class="content-primary" role="main">
<div class="introduction">
<p>${_("Be sure you want to import a course before continuing. Content of the imported course replaces all the content of this course. {em_start}You cannot undo a course import{em_end}. We recommend that you first export the current course, so you have a backup copy of it.").format(em_start='<strong>', em_end="</strong>")}</p>
## Translators: ".tar.gz" is a file extension, and files with that extension are called "gzipped tar files": these terms should not be translated
<p>${_("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.")}</p>
<p>${_("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.")}</p>
## Translators: ".tar.gz" is a file extension, and files with that extension are called "gzipped tar files": these terms should not be translated
%if library:
<p>${_("Be sure you want to import a library before continuing. The contents of the imported library will replace the contents of the existing library. {em_start}You cannot undo a library import{em_end}. Before you proceed, we recommend that you export the current library, so that you have a backup copy of it.").format(em_start='<strong>', em_end="</strong>")}</p>
<p>${_("The library 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 library.xml file. It may also contain other files.")}</p>
<p>${_("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 library until the import operation has completed.")}</p>
%else:
<p>${_("Be sure you want to import a course before continuing. The contents of the imported course will replace the contents of the existing course. {em_start}You cannot undo a course import{em_end}. Before you proceed, we recommend that you export the current course, so that you have a backup copy of it.").format(em_start='<strong>', em_end="</strong>")}</p>
<p>${_("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.")}</p>
<p>${_("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.")}</p>
%endif
</div>
<form id="fileupload" method="post" enctype="multipart/form-data" class="import-form">
## Translators: ".tar.gz" is a file extension, and files with that extension are called "gzipped tar files": these terms should not be translated
<h2 class="title">${_("Select a .tar.gz File to Replace Your Course Content")}</h2>
<h2 class="title">
%if library:
${_("Select a .tar.gz File to Replace Your Library Content")}
%else:
${_("Select a .tar.gz File to Replace Your Course Content")}
%endif
</h2>
<p class="error-block"></p>
......@@ -48,11 +76,23 @@
<input type="file" name="course-data" class="file-input" />
<input type="submit" value="${_('Replace my course with the one above')}" class="submit-button" />
<input type="submit"
%if library:
value="${_('Replace my library with the selected file')}"
%else:
value="${_('Replace my course with the selected file')}"
%endif
class="submit-button" id="replace-courselike-button" />
</div>
<div class="wrapper wrapper-status is-hidden">
<h3 class="title">${_("Course Import Status")}</h3>
<h3 class="title">
%if library:
${_("Library Import Status")}
%else:
${_("Course Import Status")}
%endif
</h3>
<ol class="status-progress list-progress">
<li class="item-progresspoint item-progresspoint-upload is-complete">
......@@ -102,8 +142,20 @@
</span>
<div class="status-detail">
<h3 class="title">${_("Updating Course")}</h3>
<p class="copy">${_("Integrating your imported content into this course. This may take a while with larger courses.")}</p>
<h3 class="title">
%if library:
${_("Updating Library")}
%else:
${_("Updating Course")}
%endif
</h3>
<p class="copy">
%if Library:
${_("Integrating your imported content into this library. This process might take longer with larger libraries.")}
%else:
${_("Integrating your imported content into this course. This process might take longer with larger courses.")}
%endif
</p>
</div>
</li>
<li class="item-progresspoint item-progresspoint-success has-actions is-not-started">
......@@ -113,11 +165,23 @@
<div class="status-detail">
<h3 class="title">${_("Success")}</h3>
<p class="copy">${_("Your imported content has now been integrated into this course")}</p>
<p class="copy">
%if library:
${_("Your imported content has now been integrated into this library")}
%else:
${_("Your imported content has now been integrated into this course")}
%endif
</p>
<ul class="list-actions">
<li class="item-action">
<a href="${successful_import_redirect_url}" class="action action-primary">${_("View Updated Outline")}</a>
<a href="${successful_import_redirect_url}" id="view-updated-button" class="action action-primary">
%if library:
${_("View Updated Library")}
%else:
${_("View Updated Outline")}
%endif
</a>
</li>
</ul>
</div>
......@@ -127,6 +191,19 @@
</form>
</article>
%if library:
<aside class="content-supplementary" role="complimentary">
<div class="bit">
<h3 class="title-3">${_("Why import a library?")}</h3>
<p>${_("You might want to update an existing library to a new version, or replace an existing library entirely. You might also have developed a library outside of Studio.")}</p>
</div>
<div class="bit">
<h3 class="title-3">${_("Note: Library content is not automatically updated in courses")}</h3>
<p>${_("If you change and import a library that is referenced by randomized content blocks in one or more courses, those courses do not automatically use the updated content. You must manually refresh the randomized content blocks to bring them up to date with the latest library content.")}</p>
</div>
</aside>
%else:
<aside class="content-supplementary" role="complementary">
<div class="bit">
<h3 class="title-3">${_("Why import a course?")}</h3>
......@@ -143,12 +220,13 @@
<p>${_("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.")}</p>
</div>
</aside>
%endif
</section>
</div>
</%block>
<%block name="requirejs">
require(["js/factories/import"], function(ImportFactory) {
ImportFactory("${import_status_url}");
ImportFactory("${import_status_url}", ${json.dumps(library)});
});
</%block>
......@@ -132,6 +132,9 @@
<%
library_key = context_library.location.course_key
index_url = reverse('contentstore.views.library_handler', kwargs={'library_key_string': unicode(library_key)})
import_url = reverse('contentstore.views.import_handler', kwargs={'course_key_string': unicode(library_key)})
lib_users_url = reverse('contentstore.views.manage_library_users', kwargs={'library_key_string': unicode(library_key)})
export_url = reverse('contentstore.views.export_handler', kwargs={'course_key_string': unicode(library_key)})
%>
<h2 class="info-course">
<span class="sr">${_("Current Library:")}</span>
......@@ -147,7 +150,6 @@
<li class="nav-item nav-library-settings">
<h3 class="title"><span class="label"><span class="label-prefix sr">${_("Library")} </span>${_("Settings")}</span> <i class="icon fa fa-caret-down ui-toggle-dd"></i></h3>
<div class="wrapper wrapper-nav-sub">
<div class="nav-sub">
<ul>
......@@ -158,6 +160,22 @@
</div>
</div>
</li>
<li class="nav-item nav-course-tools">
<h3 class="title"><span class="label">${_("Tools")}</span> <i class="icon fa fa-caret-down ui-toggle-dd"></i></h3>
<div class="wrapper wrapper-nav-sub">
<div class="nav-sub">
<ul>
<li class="nav-item nav-course-tools-import">
<a href="${import_url}">${_("Import")}</a>
</li>
<li class="nav-item nav-course-tools-export">
<a href="${export_url}">${_("Export")}</a>
</li>
</ul>
</div>
</div>
</li>
</ol>
</nav>
% endif
......
......@@ -95,9 +95,9 @@ urlpatterns += patterns(
url(r'^checklists/{}/(?P<checklist_index>\d+)?$'.format(settings.COURSE_KEY_PATTERN), 'checklists_handler'),
url(r'^orphan/{}$'.format(settings.COURSE_KEY_PATTERN), 'orphan_handler'),
url(r'^assets/{}/{}?$'.format(settings.COURSE_KEY_PATTERN, settings.ASSET_KEY_PATTERN), 'assets_handler'),
url(r'^import/{}$'.format(settings.COURSE_KEY_PATTERN), 'import_handler'),
url(r'^import_status/{}/(?P<filename>.+)$'.format(settings.COURSE_KEY_PATTERN), 'import_status_handler'),
url(r'^export/{}$'.format(settings.COURSE_KEY_PATTERN), 'export_handler'),
url(r'^import/{}$'.format(COURSELIKE_KEY_PATTERN), 'import_handler'),
url(r'^import_status/{}/(?P<filename>.+)$'.format(COURSELIKE_KEY_PATTERN), 'import_status_handler'),
url(r'^export/{}$'.format(COURSELIKE_KEY_PATTERN), 'export_handler'),
url(r'^xblock/outline/{}$'.format(settings.USAGE_KEY_PATTERN), 'xblock_outline_handler'),
url(r'^xblock/container/{}$'.format(settings.USAGE_KEY_PATTERN), 'xblock_container_handler'),
url(r'^xblock/{}/(?P<view_name>[^/]+)$'.format(settings.USAGE_KEY_PATTERN), 'xblock_view_handler'),
......
......@@ -15,7 +15,7 @@ from xmodule.contentstore.django import contentstore
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.xml_importer import import_course_from_xml
from contentserver.middleware import parse_range_header
from student.models import CourseEnrollment
......@@ -49,7 +49,7 @@ class ContentStoreToyCourseTest(ModuleStoreTestCase):
self.course_key = store.make_course_key('edX', 'toy', '2012_Fall')
import_from_xml(
import_course_from_xml(
store, self.user.id, TEST_DATA_DIR, ['toy'],
static_content_store=self.contentstore, verbose=True
)
......
......@@ -2,6 +2,8 @@
"""
LibraryContent: The XBlock used to include blocks from a library in a course.
"""
import json
from lxml import etree
from bson.objectid import ObjectId, InvalidId
from collections import namedtuple
from copy import copy
......@@ -306,6 +308,10 @@ class LibraryContentDescriptor(LibraryContentFields, MakoModuleDescriptor, XmlDe
this block is up to date or not.
"""
lib_tools = self.runtime.service(self, 'library_tools')
if not lib_tools:
# This error is diagnostic. The user won't see it, but it may be helpful
# during debugging.
return Response(_(u"Course does not support Library tools."), status=400)
user_service = self.runtime.service(self, 'user')
user_perms = self.runtime.service(self, 'studio_user_permissions')
if user_service:
......@@ -477,18 +483,27 @@ class LibraryContentDescriptor(LibraryContentFields, MakoModuleDescriptor, XmlDe
@classmethod
def definition_from_xml(cls, xml_object, system):
""" XML support not yet implemented. """
raise NotImplementedError
children = [
# pylint: disable=no-member
system.process_xml(etree.tostring(child)).scope_ids.usage_id
for child in xml_object.getchildren()
]
definition = {
attr_name: json.loads(attr_value)
for attr_name, attr_value in xml_object.attrib
}
return definition, children
def definition_to_xml(self, resource_fs):
""" XML support not yet implemented. """
raise NotImplementedError
@classmethod
def from_xml(cls, xml_data, system, id_generator):
""" XML support not yet implemented. """
raise NotImplementedError
def export_to_xml(self, resource_fs):
""" XML support not yet implemented. """
raise NotImplementedError
""" Exports Library Content Module to XML """
# pylint: disable=no-member
xml_object = etree.Element('library_content')
for child in self.get_children():
self.runtime.add_block_as_child_node(child, xml_object)
# Set node attributes based on our fields.
for field_name, field in self.fields.iteritems():
if field_name in ('children', 'parent', 'content'):
continue
if field.is_set_on(self):
xml_object.set(field_name, unicode(field.read_from(self)))
return xml_object
......@@ -29,7 +29,8 @@ class LibraryRoot(XBlock):
advanced_modules = List(
display_name=_("Advanced Module List"),
help=_("Enter the names of the advanced components to use in your library."),
scope=Scope.settings
scope=Scope.settings,
xml_node=True,
)
has_children = True
has_author_view = True
......@@ -105,12 +106,3 @@ class LibraryRoot(XBlock):
Always returns the raw 'library' field from the key.
"""
return self.scope_ids.usage_id.course_key.library
@classmethod
def parse_xml(cls, xml_data, system, id_generator, **kwargs):
""" XML support not yet implemented. """
raise NotImplementedError
def add_xml_to_node(self, resource_fs):
""" XML support not yet implemented. """
raise NotImplementedError
......@@ -39,6 +39,9 @@ new_contract('AssetKey', AssetKey)
new_contract('AssetMetadata', AssetMetadata)
new_contract('XBlock', XBlock)
LIBRARY_ROOT = 'library.xml'
COURSE_ROOT = 'course.xml'
class ModuleStoreEnum(object):
"""
......
......@@ -14,13 +14,12 @@ import ddt
from nose.plugins.skip import SkipTest
from xmodule.assetstore import AssetMetadata
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.xml_importer import import_course_from_xml
from xmodule.modulestore.xml_exporter import export_course_to_xml
from xmodule.modulestore.tests.test_cross_modulestore_import_export import (
MODULESTORE_SETUPS,
SHORT_NAME_MAP,
TEST_DATA_DIR,
MongoContentstoreBuilder,
)
from xmodule.modulestore.perf_tests.generate_asset_xml import make_asset_xml, validate_xml, ASSET_XSD_FILE
......@@ -111,19 +110,19 @@ class CrossStoreXMLRoundtrip(unittest.TestCase):
dest_course_key = dest_store.make_course_key('a', 'course', 'course')
with CodeBlockTimer("initial_import"):
import_from_xml(
import_course_from_xml(
source_store,
'test_user',
TEST_DATA_ROOT,
course_dirs=TEST_COURSE,
source_dirs=TEST_COURSE,
static_content_store=source_content,
target_course_id=source_course_key,
create_course_if_not_present=True,
target_id=source_course_key,
create_if_not_present=True,
raise_on_failure=True,
)
with CodeBlockTimer("export"):
export_to_xml(
export_course_to_xml(
source_store,
source_content,
source_course_key,
......@@ -132,14 +131,14 @@ class CrossStoreXMLRoundtrip(unittest.TestCase):
)
with CodeBlockTimer("second_import"):
import_from_xml(
import_course_from_xml(
dest_store,
'test_user',
self.export_dir,
course_dirs=['exported_source_course'],
source_dirs=['exported_source_course'],
static_content_store=dest_content,
target_course_id=dest_course_key,
create_course_if_not_present=True,
target_id=dest_course_key,
create_if_not_present=True,
raise_on_failure=True,
)
......@@ -194,14 +193,14 @@ class FindAssetTest(unittest.TestCase):
)
with CodeBlockTimer("initial_import"):
import_from_xml(
import_course_from_xml(
source_store,
'test_user',
TEST_DATA_ROOT,
course_dirs=TEST_COURSE,
source_dirs=TEST_COURSE,
static_content_store=source_content,
target_course_id=source_course_key,
create_course_if_not_present=True,
target_id=source_course_key,
create_if_not_present=True,
raise_on_failure=True,
)
......@@ -259,14 +258,14 @@ class TestModulestoreAssetSize(unittest.TestCase):
with source_ms.build() as (source_content, source_store):
source_course_key = source_store.make_course_key('a', 'course', 'course')
import_from_xml(
import_course_from_xml(
source_store,
'test_user',
TEST_DATA_ROOT,
course_dirs=TEST_COURSE,
source_dirs=TEST_COURSE,
static_content_store=source_content,
target_course_id=source_course_key,
create_course_if_not_present=True,
target_id=source_course_key,
create_if_not_present=True,
raise_on_failure=True,
)
......
......@@ -1601,7 +1601,8 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
super(SplitMongoModuleStore, self).clone_course(source_course_id, dest_course_id, user_id, fields, **kwargs)
return new_course
DEFAULT_ROOT_BLOCK_ID = 'course'
DEFAULT_ROOT_COURSE_BLOCK_ID = 'course'
DEFAULT_ROOT_LIBRARY_BLOCK_ID = 'library'
def create_course(
self, org, course, run, user_id, master_branch=None, fields=None,
......@@ -1687,7 +1688,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
user_id,
BlockKey(
root_category,
root_block_id or SplitMongoModuleStore.DEFAULT_ROOT_BLOCK_ID,
root_block_id or SplitMongoModuleStore.DEFAULT_ROOT_COURSE_BLOCK_ID,
),
block_fields,
definition_id
......
......@@ -487,7 +487,9 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli
with self.bulk_operations(course_key):
# hardcode course root block id
if block_type == 'course':
block_id = self.DEFAULT_ROOT_BLOCK_ID
block_id = self.DEFAULT_ROOT_COURSE_BLOCK_ID
elif block_type == 'library':
block_id = self.DEFAULT_ROOT_LIBRARY_BLOCK_ID
new_usage_key = course_key.make_usage_key(block_type, block_id)
if self.get_branch_setting() == ModuleStoreEnum.Branch.published_only:
......
......@@ -66,7 +66,7 @@ def rewrite_nonportable_content_links(source_course_id, dest_course_id, text):
#
if source_course_id != dest_course_id:
try:
generic_courseware_link_base = u'/courses/{}/'.format(source_course_id.to_deprecated_string())
generic_courseware_link_base = u'/courses/{}/'.format(unicode(source_course_id))
text = re.sub(_prefix_only_url_replace_regex(generic_courseware_link_base), portable_asset_link_subtitution, text)
except Exception as exc: # pylint: disable=broad-except
logging.warning("Error producing regex substitution %r for text = %r.\n\nError msg = %s", source_course_id, text, str(exc))
......
......@@ -29,7 +29,7 @@ class StoreConstructors(object):
draft, split, xml = range(3)
def mixed_store_config(data_dir, mappings, include_xml=False, xml_course_dirs=None, store_order=None):
def mixed_store_config(data_dir, mappings, include_xml=False, xml_source_dirs=None, store_order=None):
"""
Return a `MixedModuleStore` configuration, which provides
access to both Mongo- and XML-backed courses.
......@@ -49,11 +49,11 @@ def mixed_store_config(data_dir, mappings, include_xml=False, xml_course_dirs=No
Keyword Args:
include_xml (boolean): If True, include an XML modulestore in the configuration.
xml_course_dirs (list): The directories containing XML courses to load from disk.
xml_source_dirs (list): The directories containing XML courses to load from disk.
note: For the courses to be loaded into the XML modulestore and accessible do the following:
* include_xml should be True
* xml_course_dirs should be the list of directories (relative to data_dir)
* xml_source_dirs should be the list of directories (relative to data_dir)
containing the courses you want to load
* mappings should be configured, pointing the xml courses to the xml modulestore
......@@ -67,7 +67,7 @@ def mixed_store_config(data_dir, mappings, include_xml=False, xml_course_dirs=No
store_constructors = {
StoreConstructors.split: split_mongo_store_config(data_dir)['default'],
StoreConstructors.draft: draft_mongo_store_config(data_dir)['default'],
StoreConstructors.xml: xml_store_config(data_dir, course_dirs=xml_course_dirs)['default'],
StoreConstructors.xml: xml_store_config(data_dir, source_dirs=xml_source_dirs)['default'],
}
store = {
......@@ -137,11 +137,11 @@ def split_mongo_store_config(data_dir):
return store
def xml_store_config(data_dir, course_dirs=None):
def xml_store_config(data_dir, source_dirs=None):
"""
Defines default module store using XMLModuleStore.
Note: you should pass in a list of course_dirs that you care about,
Note: you should pass in a list of source_dirs that you care about,
otherwise all courses in the data_dir will be processed.
"""
store = {
......@@ -151,7 +151,7 @@ def xml_store_config(data_dir, course_dirs=None):
'OPTIONS': {
'data_dir': data_dir,
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
'course_dirs': course_dirs,
'source_dirs': source_dirs,
}
}
}
......@@ -161,24 +161,24 @@ def xml_store_config(data_dir, course_dirs=None):
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
# This is an XML only modulestore with only the toy course loaded
TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR, course_dirs=['toy'])
TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR, source_dirs=['toy'])
# This modulestore will provide both a mixed mongo editable modulestore, and
# an XML store with just the toy course loaded.
TEST_DATA_MIXED_TOY_MODULESTORE = mixed_store_config(
TEST_DATA_DIR, {'edX/toy/2012_Fall': 'xml', }, include_xml=True, xml_course_dirs=['toy']
TEST_DATA_DIR, {'edX/toy/2012_Fall': 'xml', }, include_xml=True, xml_source_dirs=['toy']
)
# This modulestore will provide both a mixed mongo editable modulestore, and
# an XML store with common/test/data/2014 loaded, which is a course that is closed.
TEST_DATA_MIXED_CLOSED_MODULESTORE = mixed_store_config(
TEST_DATA_DIR, {'edX/detached_pages/2014': 'xml', }, include_xml=True, xml_course_dirs=['2014']
TEST_DATA_DIR, {'edX/detached_pages/2014': 'xml', }, include_xml=True, xml_source_dirs=['2014']
)
# This modulestore will provide both a mixed mongo editable modulestore, and
# an XML store with common/test/data/graded loaded, which is a course that is graded.
TEST_DATA_MIXED_GRADED_MODULESTORE = mixed_store_config(
TEST_DATA_DIR, {'edX/graded/2012_Fall': 'xml', }, include_xml=True, xml_course_dirs=['graded']
TEST_DATA_DIR, {'edX/graded/2012_Fall': 'xml', }, include_xml=True, xml_source_dirs=['graded']
)
# All store requests now go through mixed
......
......@@ -26,8 +26,8 @@ from xmodule.modulestore.mongo.base import ModuleStoreEnum
from xmodule.modulestore.mongo.draft import DraftModuleStore
from xmodule.modulestore.mixed import MixedModuleStore
from xmodule.contentstore.mongo import MongoContentStore
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.xml_importer import import_course_from_xml
from xmodule.modulestore.xml_exporter import export_course_to_xml
from xmodule.modulestore.split_mongo.split_draft import DraftVersioningModuleStore
from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST
from xmodule.modulestore.inheritance import InheritanceMixin
......@@ -381,18 +381,18 @@ class CrossStoreXMLRoundtrip(CourseComparisonTest, PartitionTestCase):
source_course_key = source_store.make_course_key('a', 'course', 'course')
dest_course_key = dest_store.make_course_key('a', 'course', 'course')
import_from_xml(
import_course_from_xml(
source_store,
'test_user',
TEST_DATA_DIR,
course_dirs=[course_data_name],
source_dirs=[course_data_name],
static_content_store=source_content,
target_course_id=source_course_key,
create_course_if_not_present=True,
target_id=source_course_key,
raise_on_failure=True,
create_if_not_present=True,
)
export_to_xml(
export_course_to_xml(
source_store,
source_content,
source_course_key,
......@@ -400,19 +400,19 @@ class CrossStoreXMLRoundtrip(CourseComparisonTest, PartitionTestCase):
'exported_source_course',
)
import_from_xml(
import_course_from_xml(
dest_store,
'test_user',
self.export_dir,
course_dirs=['exported_source_course'],
source_dirs=['exported_source_course'],
static_content_store=dest_content,
target_course_id=dest_course_key,
create_course_if_not_present=True,
target_id=dest_course_key,
raise_on_failure=True,
create_if_not_present=True,
)
# NOT CURRENTLY USED
# export_to_xml(
# export_course_to_xml(
# dest_store,
# dest_content,
# dest_course_key,
......
......@@ -4,8 +4,10 @@ Basic unit tests related to content libraries.
Higher-level tests are in `cms/djangoapps/contentstore`.
"""
from bson.objectid import ObjectId
import ddt
from bson.objectid import ObjectId
from opaque_keys.edx.locator import LibraryLocator
from xmodule.modulestore.exceptions import DuplicateCourseError
......
......@@ -25,7 +25,7 @@ from xmodule.modulestore.inheritance import InheritanceMixin
from xmodule.modulestore.tests.test_cross_modulestore_import_export import MongoContentstoreBuilder
from xmodule.contentstore.content import StaticContent
from opaque_keys.edx.keys import CourseKey
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.xml_importer import import_course_from_xml
from xmodule.modulestore.django import SignalHandler
if not settings.configured:
......@@ -2031,9 +2031,9 @@ class TestMixedModuleStore(CourseComparisonTest):
# Note: The signal is fired once when the course is created and
# a second time after the actual data import.
receiver.reset_mock()
import_from_xml(
import_course_from_xml(
self.store, self.user_id, DATA_DIR, ['toy'], load_error_modules=False,
static_content_store=contentstore,
create_course_if_not_present=True,
create_if_not_present=True,
)
self.assertEqual(receiver.call_count, 2)
......@@ -31,8 +31,8 @@ from xmodule.modulestore.draft import DraftModuleStore
from opaque_keys.edx.locations import SlashSeparatedCourseKey, AssetLocation
from opaque_keys.edx.locator import LibraryLocator, CourseLocator
from opaque_keys.edx.keys import UsageKey
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.xml_importer import import_from_xml, perform_xlint
from xmodule.modulestore.xml_exporter import export_course_to_xml
from xmodule.modulestore.xml_importer import import_course_from_xml, perform_xlint
from xmodule.contentstore.mongo import MongoContentStore
from nose.tools import assert_in
......@@ -127,7 +127,7 @@ class TestMongoModuleStoreBase(unittest.TestCase):
xblock_mixins=(EditInfoMixin,)
)
import_from_xml(
import_course_from_xml(
draft_store,
999,
DATA_DIR,
......@@ -136,7 +136,7 @@ class TestMongoModuleStoreBase(unittest.TestCase):
)
# also test a course with no importing of static content
import_from_xml(
import_course_from_xml(
draft_store,
999,
DATA_DIR,
......@@ -514,7 +514,7 @@ class TestMongoModuleStore(TestMongoModuleStoreBase):
root_dir = path(mkdtemp())
try:
export_to_xml(self.draft_store, self.content_store, course_key, root_dir, 'test_export')
export_course_to_xml(self.draft_store, self.content_store, course_key, root_dir, 'test_export')
assert_true(path(root_dir / 'test_export/static/images/course_image.jpg').isfile())
assert_true(path(root_dir / 'test_export/static/images_course_image.jpg').isfile())
finally:
......@@ -530,7 +530,7 @@ class TestMongoModuleStore(TestMongoModuleStoreBase):
root_dir = path(mkdtemp())
try:
export_to_xml(self.draft_store, self.content_store, course.id, root_dir, 'test_export')
export_course_to_xml(self.draft_store, self.content_store, course.id, root_dir, 'test_export')
assert_true(path(root_dir / 'test_export/static/just_a_test.jpg').isfile())
assert_false(path(root_dir / 'test_export/static/images/course_image.jpg').isfile())
finally:
......@@ -544,7 +544,7 @@ class TestMongoModuleStore(TestMongoModuleStoreBase):
course = self.draft_store.get_course(SlashSeparatedCourseKey('edX', 'simple_with_draft', '2012_Fall'))
root_dir = path(mkdtemp())
try:
export_to_xml(self.draft_store, self.content_store, course.id, root_dir, 'test_export')
export_course_to_xml(self.draft_store, self.content_store, course.id, root_dir, 'test_export')
assert_false(path(root_dir / 'test_export/static/images/course_image.jpg').isfile())
assert_false(path(root_dir / 'test_export/static/images_course_image.jpg').isfile())
finally:
......@@ -670,9 +670,12 @@ class TestMongoModuleStore(TestMongoModuleStoreBase):
root_dir = path(mkdtemp())
# export_to_xml should work.
# export_course_to_xml should work.
try:
export_to_xml(self.draft_store, self.content_store, interface_location.course_key, root_dir, 'test_export')
export_course_to_xml(
self.draft_store, self.content_store, interface_location.course_key,
root_dir, 'test_export'
)
finally:
shutil.rmtree(root_dir)
......
......@@ -8,8 +8,8 @@ from shutil import rmtree
from unittest import TestCase, skip
import ddt
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.xml_importer import import_course_from_xml
from xmodule.modulestore.xml_exporter import export_course_to_xml
from xmodule.modulestore.tests.factories import check_mongo_calls
from xmodule.modulestore.tests.test_cross_modulestore_import_export import (
MixedModulestoreBuilder, VersioningModulestoreBuilder,
......@@ -47,19 +47,19 @@ class CountMongoCallsXMLRoundtrip(TestCase):
# the course id and the wiki_slug in the test XML course. The course must be updated
# with the correct wiki_slug during import.
with check_mongo_calls(import_reads, first_import_writes):
import_from_xml(
import_course_from_xml(
source_store,
'test_user',
TEST_DATA_DIR,
course_dirs=['manual-testing-complete'],
source_dirs=['manual-testing-complete'],
static_content_store=source_content,
target_course_id=source_course_key,
create_course_if_not_present=True,
target_id=source_course_key,
create_if_not_present=True,
raise_on_failure=True,
)
with check_mongo_calls(export_reads):
export_to_xml(
export_course_to_xml(
source_store,
source_content,
source_course_key,
......@@ -68,14 +68,14 @@ class CountMongoCallsXMLRoundtrip(TestCase):
)
with check_mongo_calls(import_reads, second_import_writes):
import_from_xml(
import_course_from_xml(
dest_store,
'test_user',
self.export_dir,
course_dirs=['exported_source_course'],
source_dirs=['exported_source_course'],
static_content_store=dest_content,
target_course_id=dest_course_key,
create_course_if_not_present=True,
target_id=dest_course_key,
create_if_not_present=True,
raise_on_failure=True,
)
......@@ -125,14 +125,14 @@ class CountMongoCallsCourseTraversal(TestCase):
source_course_key = source_store.make_course_key('a', 'course', 'course')
# First, import a course.
import_from_xml(
import_course_from_xml(
source_store,
'test_user',
TEST_DATA_DIR,
course_dirs=['manual-testing-complete'],
source_dirs=['manual-testing-complete'],
static_content_store=source_content,
target_course_id=source_course_key,
create_course_if_not_present=True,
target_id=source_course_key,
create_if_not_present=True,
raise_on_failure=True,
)
......
......@@ -31,7 +31,7 @@ class TestXMLModuleStore(unittest.TestCase):
Test around the XML modulestore
"""
def test_xml_modulestore_type(self):
store = XMLModuleStore(DATA_DIR, course_dirs=[])
store = XMLModuleStore(DATA_DIR, source_dirs=[])
self.assertEqual(store.get_modulestore_type(), ModuleStoreEnum.Type.xml)
def test_unicode_chars_in_xml_content(self):
......@@ -46,7 +46,7 @@ class TestXMLModuleStore(unittest.TestCase):
# Load the course, but don't make error modules. This will succeed,
# but will record the errors.
modulestore = XMLModuleStore(DATA_DIR, course_dirs=['toy'], load_error_modules=False)
modulestore = XMLModuleStore(DATA_DIR, source_dirs=['toy'], load_error_modules=False)
# Look up the errors during load. There should be none.
errors = modulestore.get_course_errors(SlashSeparatedCourseKey("edX", "toy", "2012_Fall"))
......@@ -54,7 +54,7 @@ class TestXMLModuleStore(unittest.TestCase):
@patch("xmodule.modulestore.xml.glob.glob", side_effect=glob_tildes_at_end)
def test_tilde_files_ignored(self, _fake_glob):
modulestore = XMLModuleStore(DATA_DIR, course_dirs=['tilde'], load_error_modules=False)
modulestore = XMLModuleStore(DATA_DIR, source_dirs=['tilde'], load_error_modules=False)
about_location = SlashSeparatedCourseKey('edX', 'tilde', '2012_Fall').make_usage_key(
'about', 'index',
)
......@@ -66,7 +66,7 @@ class TestXMLModuleStore(unittest.TestCase):
"""
Test the get_courses_for_wiki method
"""
store = XMLModuleStore(DATA_DIR, course_dirs=['toy', 'simple'])
store = XMLModuleStore(DATA_DIR, source_dirs=['toy', 'simple'])
for course in store.get_courses():
course_locations = store.get_courses_for_wiki(course.wiki_slug)
self.assertEqual(len(course_locations), 1)
......@@ -92,7 +92,7 @@ class TestXMLModuleStore(unittest.TestCase):
Test the has_course method
"""
check_has_course_method(
XMLModuleStore(DATA_DIR, course_dirs=['toy', 'simple']),
XMLModuleStore(DATA_DIR, source_dirs=['toy', 'simple']),
SlashSeparatedCourseKey('edX', 'toy', '2012_Fall'),
locator_key_fields=SlashSeparatedCourseKey.KEY_FIELDS
)
......@@ -101,7 +101,7 @@ class TestXMLModuleStore(unittest.TestCase):
"""
Test the branch setting context manager
"""
store = XMLModuleStore(DATA_DIR, course_dirs=['toy'])
store = XMLModuleStore(DATA_DIR, source_dirs=['toy'])
course = store.get_courses()[0]
# XML store allows published_only branch setting
......@@ -119,7 +119,7 @@ class TestXMLModuleStore(unittest.TestCase):
"""
Test a course whose structure is not a tree.
"""
store = XMLModuleStore(DATA_DIR, course_dirs=['xml_dag'])
store = XMLModuleStore(DATA_DIR, source_dirs=['xml_dag'])
course_key = store.get_courses()[0].id
mock_logging.warning.assert_called_with(
......
......@@ -23,7 +23,7 @@ class DummySystem(ImportSystem):
@patch('xmodule.modulestore.xml.OSFS', lambda directory: MemoryFS())
def __init__(self, load_error_modules):
xmlstore = XMLModuleStore("data_dir", course_dirs=[], load_error_modules=load_error_modules)
xmlstore = XMLModuleStore("data_dir", source_dirs=[], load_error_modules=load_error_modules)
super(DummySystem, self).__init__(
xmlstore=xmlstore,
......@@ -186,7 +186,7 @@ class ConditionalModuleXmlTest(unittest.TestCase):
"""Get a test course by directory name. If there's more than one, error."""
print "Importing {0}".format(name)
modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
modulestore = XMLModuleStore(DATA_DIR, source_dirs=[name])
courses = modulestore.get_courses()
self.modulestore = modulestore
self.assertEquals(len(courses), 1)
......
......@@ -31,7 +31,7 @@ class DummySystem(ImportSystem):
@patch('xmodule.modulestore.xml.OSFS', lambda dir: MemoryFS())
def __init__(self, load_error_modules):
xmlstore = XMLModuleStore("data_dir", course_dirs=[],
xmlstore = XMLModuleStore("data_dir", source_dirs=[],
load_error_modules=load_error_modules)
course_id = SlashSeparatedCourseKey(ORG, COURSE, 'test_run')
course_dir = "test_dir"
......
......@@ -104,7 +104,7 @@ class RoundTripTestCase(unittest.TestCase):
shutil.copytree(data_dir / course_dir, root_dir / course_dir)
print("Starting import")
initial_import = XMLModuleStore(root_dir, course_dirs=[course_dir], xblock_mixins=(XModuleMixin,))
initial_import = XMLModuleStore(root_dir, source_dirs=[course_dir], xblock_mixins=(XModuleMixin,))
courses = initial_import.get_courses()
self.assertEquals(len(courses), 1)
......@@ -122,7 +122,7 @@ class RoundTripTestCase(unittest.TestCase):
lxml.etree.ElementTree(root).write(course_xml)
print("Starting second import")
second_import = XMLModuleStore(root_dir, course_dirs=[course_dir], xblock_mixins=(XModuleMixin,))
second_import = XMLModuleStore(root_dir, source_dirs=[course_dir], xblock_mixins=(XModuleMixin,))
courses2 = second_import.get_courses()
self.assertEquals(len(courses2), 1)
......
......@@ -85,7 +85,8 @@ class DummyModulestore(object):
raise NotImplementedError("Sub-tests must specify how to generate a module-system")
def setup_modulestore(self, name):
self.modulestore = XMLModuleStore(DATA_DIR, course_dirs=[name])
# pylint: disable=attribute-defined-outside-init
self.modulestore = XMLModuleStore(DATA_DIR, source_dirs=[name])
def get_course(self, _):
"""Get a test course by directory name. If there's more than one, error."""
......
......@@ -383,7 +383,7 @@ class TestXModuleHandler(TestCase):
class TestXmlExport(XBlockWrapperTestMixin, TestCase):
"""
This tests that XModuleDescriptor.export_to_xml and add_xml_to_node produce the same results.
This tests that XModuleDescriptor.export_course_to_xml and add_xml_to_node produce the same results.
"""
def skip_if_invalid(self, descriptor_cls):
if descriptor_cls.add_xml_to_node != XModuleDescriptor.add_xml_to_node:
......
"""
Course Import page.
"""
from .course_page import CoursePage
class ImportPage(CoursePage):
"""
Course Import page.
"""
url_path = "import"
def is_browser_on_page(self):
return self.q(css='body.view-import').present
"""
Course Export page.
"""
from .course_page import CoursePage
from utils import click_css
class ExportPage(CoursePage):
"""
Course Export page.
"""
url_path = "export"
def is_browser_on_page(self):
return self.q(css='body.view-export').present
def click_export_button(self):
"""
Clicks export button.
"""
click_css(self, "a.action-export")
"""
Import/Export pages.
"""
from bok_choy.promise import EmptyPromise
import os
import requests
from .utils import click_css
from .library import LibraryPage
from .course_page import CoursePage
from . import BASE_URL
class ExportMixin(object):
"""
Export page Mixin.
"""
url_path = "export"
def is_browser_on_page(self):
"""
Verify this is the export page
"""
return self.q(css='body.view-export').present
def _get_tarball(self, url):
"""
Download tarball at `url`
"""
kwargs = dict()
session_id = [{i['name']: i['value']} for i in self.browser.get_cookies() if i['name'] == u'sessionid']
if session_id:
kwargs.update({
'cookies': session_id[0]
})
response = requests.get(url, **kwargs)
return response.status_code == 200, response.headers
def download_tarball(self):
"""
Downloads the course or library in tarball form.
"""
tarball_url = self.q(css='a.action-export').attrs('href')[0]
good_status, headers = self._get_tarball(tarball_url)
return good_status, headers['content-type'] == 'application/x-tgz'
def click_export(self):
"""
Click the export button. Should only be used if expected to fail, as
otherwise a browser dialog for saving the file will be presented.
"""
self.q(css='a.action-export').click()
def is_error_modal_showing(self):
"""
Indicates whether or not the error modal is showing.
"""
return self.q(css='.prompt.error').visible
def click_modal_button(self):
"""
Click the button on the modal dialog that appears when there's a problem.
"""
self.q(css='.prompt.error .action-primary').click()
def wait_for_error_modal(self):
"""
If an import or export has an error, an error modal will be shown.
"""
EmptyPromise(self.is_error_modal_showing, 'Error Modal Displayed', timeout=30).fulfill()
class LibraryLoader(object):
"""
URL loading mixing for Library import/export
"""
@property
def url(self):
"""
This pattern isn't followed universally by library URLs,
but is used for import/export.
"""
# pylint: disable=no-member
return "/".join([BASE_URL, self.url_path, unicode(self.locator)])
class ExportCoursePage(ExportMixin, CoursePage):
"""
Export page for Courses
"""
class ExportLibraryPage(ExportMixin, LibraryLoader, LibraryPage):
"""
Export page for Libraries
"""
class ImportMixin(object):
"""
Import page mixin
"""
url_path = "import"
def is_browser_on_page(self):
"""
Verify this is the export page
"""
return self.q(css='.choose-file-button').present
@staticmethod
def file_path(filename):
"""
Construct file path to be uploaded from the data upload folder.
Arguments:
filename (str): asset filename
"""
# Should grab common point between this page module and the data folder.
return os.sep.join(__file__.split(os.sep)[:-4]) + '/data/imports/' + filename
def _wait_for_button(self):
"""
Wait for the upload button to appear.
"""
return EmptyPromise(
lambda: self.q(css='#replace-courselike-button')[0],
"Upload button appears",
timeout=30
).fulfill()
def upload_tarball(self, tarball_filename):
"""
Upload a tarball to be imported.
"""
asset_file_path = self.file_path(tarball_filename)
# Make the upload elements visible to the WebDriver.
self.browser.execute_script('$(".file-name-block").show();$(".file-input").show()')
self.q(css='input[type="file"]')[0].send_keys(asset_file_path)
self._wait_for_button()
click_css(self, '.submit-button', require_notification=False)
def is_upload_finished(self):
"""
Checks if the 'view updated' button is showing.
"""
return self.q(css='#view-updated-button').visible
@staticmethod
def _task_properties(completed):
"""
Outputs the CSS class and promise description for task states based on completion.
"""
if completed:
return 'is-complete', "'{}' is marked complete"
else:
return 'is-not-started', "'{}' is in not-yet-started status"
def wait_for_tasks(self, completed=False, fail_on=None):
"""
Wait for all of the items in the task list to be set to the correct state.
"""
classes = {
'Uploading': 'item-progresspoint-upload',
'Unpacking': 'item-progresspoint-unpack',
'Verifying': 'item-progresspoint-verify',
'Updating': 'item-progresspoint-import',
'Success': 'item-progresspoint-success'
}
if fail_on:
# Makes no sense to include this if the tasks haven't run.
completed = True
state, desc_template = self._task_properties(completed)
for desc, css_class in classes.items():
desc_text = desc_template.format(desc)
# pylint: disable=cell-var-from-loop
EmptyPromise(lambda: self.q(css='.{}.{}'.format(css_class, state)).present, desc_text, timeout=30)
if fail_on == desc:
EmptyPromise(
lambda: self.q(css='.{}.is-complete.has-error'.format(css_class)).present,
"{} checkpoint marked as failed".format(desc),
timeout=30
)
# The rest should never run.
state, desc_template = self._task_properties(False)
def wait_for_upload(self):
"""
Wait for the upload to be confirmed.
"""
EmptyPromise(self.is_upload_finished, 'Upload Finished', timeout=30).fulfill()
def is_filename_error_showing(self):
"""
An should be shown if the user tries to upload the wrong kind of file.
Tell us whether it's currently being shown.
"""
return self.q(css='#fileupload .error-block').visible
def is_task_list_showing(self):
"""
The task list shows a series of steps being performed during import. It is normally
hidden until the upload begins.
Tell us whether it's currently visible.
"""
return self.q(css='.wrapper-status').visible
def wait_for_filename_error(self):
"""
Wait for the upload field to display an error.
"""
EmptyPromise(self.is_filename_error_showing, 'Upload Error Displayed', timeout=30).fulfill()
def finished_target_url(self):
"""
Grab the URL of the 'view updated library/course outline' button.
"""
return self.q(css='.action.action-primary')[0].get_attribute('href')
class ImportCoursePage(ImportMixin, CoursePage):
"""
Import page for Courses
"""
class ImportLibraryPage(ImportMixin, LibraryLoader, LibraryPage):
"""
Import page for Libraries
"""
......@@ -13,11 +13,10 @@ from .utils import confirm_prompt, wait_for_notification
from . import BASE_URL
class LibraryPage(PageObject, PaginatedMixin):
class LibraryPage(PageObject):
"""
Library page in Studio
Base page for Library pages. Defaults URL to the edit page.
"""
def __init__(self, browser, locator):
super(LibraryPage, self).__init__(browser)
self.locator = locator
......@@ -35,6 +34,12 @@ class LibraryPage(PageObject, PaginatedMixin):
"""
return self.q(css='body.view-library').present
class LibraryEditPage(LibraryPage, PaginatedMixin):
"""
Library edit page in Studio
"""
def get_header_title(self):
"""
The text of the main heading (H1) visible on the page.
......
......@@ -8,10 +8,9 @@ from bok_choy.web_app_test import WebAppTest
from ...pages.studio.asset_index import AssetIndexPage
from ...pages.studio.auto_auth import AutoAuthPage
from ...pages.studio.checklists import ChecklistsPage
from ...pages.studio.course_import import ImportPage
from ...pages.studio.course_info import CourseUpdatesPage
from ...pages.studio.edit_tabs import PagesPage
from ...pages.studio.export import ExportPage
from ...pages.studio.import_export import ExportCoursePage, ImportCoursePage
from ...pages.studio.howitworks import HowitworksPage
from ...pages.studio.index import DashboardPage
from ...pages.studio.login import LoginPage
......@@ -82,8 +81,8 @@ class CoursePagesTest(StudioCourseTest):
self.pages = [
clz(self.browser, self.course_info['org'], self.course_info['number'], self.course_info['run'])
for clz in [
AssetIndexPage, ChecklistsPage, ImportPage, CourseUpdatesPage,
PagesPage, ExportPage, CourseTeamPage, CourseOutlinePage, SettingsPage,
AssetIndexPage, ChecklistsPage, CourseUpdatesPage,
PagesPage, ExportCoursePage, ImportCoursePage, CourseTeamPage, CourseOutlinePage, SettingsPage,
AdvancedSettingsPage, GradingPage, TextbooksPage
]
]
......
......@@ -6,7 +6,7 @@ from opaque_keys.edx.locator import LibraryLocator
from unittest import skip
from ...pages.studio.auto_auth import AutoAuthPage
from ...pages.studio.library import LibraryPage
from ...pages.studio.library import LibraryEditPage
from ...pages.studio.index import DashboardPage
......@@ -51,7 +51,7 @@ class CreateLibraryTest(WebAppTest):
self.dashboard_page.submit_new_library_form()
# The next page is the library edit view; make sure it loads:
lib_page = LibraryPage(self.browser, LibraryLocator(org, number))
lib_page = LibraryEditPage(self.browser, LibraryLocator(org, number))
lib_page.wait_for_page()
# Then go back to the home page and make sure the new library is listed there:
......
......@@ -7,7 +7,7 @@ from .base_studio_test import StudioLibraryTest
from ...fixtures.course import XBlockFixtureDesc
from ...pages.studio.auto_auth import AutoAuthPage
from ...pages.studio.utils import add_component
from ...pages.studio.library import LibraryPage
from ...pages.studio.library import LibraryEditPage
from ...pages.studio.users import LibraryUsersPage
......@@ -21,7 +21,7 @@ class LibraryEditPageTest(StudioLibraryTest):
Ensure a library exists and navigate to the library edit page.
"""
super(LibraryEditPageTest, self).setUp()
self.lib_page = LibraryPage(self.browser, self.library_key)
self.lib_page = LibraryEditPage(self.browser, self.library_key)
self.lib_page.visit()
self.lib_page.wait_until_ready()
......@@ -193,7 +193,7 @@ class LibraryNavigationTest(StudioLibraryTest):
Ensure a library exists and navigate to the library edit page.
"""
super(LibraryNavigationTest, self).setUp()
self.lib_page = LibraryPage(self.browser, self.library_key)
self.lib_page = LibraryEditPage(self.browser, self.library_key)
self.lib_page.visit()
self.lib_page.wait_until_ready()
......
......@@ -3,7 +3,7 @@ Acceptance tests for Studio related to edit/save peer grading interface.
"""
from ...fixtures.course import XBlockFixtureDesc
from ...pages.studio.export import ExportPage
from ...pages.studio.import_export import ExportCoursePage
from ...pages.studio.component_editor import ComponentEditorView
from ...pages.studio.overview import CourseOutlinePage
from base_studio_test import StudioCourseTest
......@@ -22,7 +22,10 @@ class ORAComponentTest(StudioCourseTest):
self.course_outline_page = CourseOutlinePage(
self.browser, self.course_info['org'], self.course_info['number'], self.course_info['run']
)
self.export_page = ExportPage(self.browser, self.course_info['org'], self.course_info['number'], self.course_info['run'])
self.export_page = ExportCoursePage(
self.browser,
self.course_info['org'], self.course_info['number'], self.course_info['run']
)
def populate_course_fixture(self, course_fixture):
"""
......
......@@ -52,14 +52,14 @@ def import_with_checks(course_dir):
course_dir = path(course_dir)
data_dir = course_dir.dirname()
course_dirs = [course_dir.basename()]
source_dirs = [course_dir.basename()]
# No default class--want to complain if it doesn't find plugins for any
# module.
modulestore = XMLModuleStore(
data_dir,
default_class=None,
course_dirs=course_dirs
source_dirs=source_dirs
)
def str_of_err(tpl):
......
......@@ -17,7 +17,7 @@ from path import path
from django.core.management.base import BaseCommand, CommandError
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml_exporter import export_to_xml
from xmodule.modulestore.xml_exporter import export_course_to_xml
from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey
......@@ -89,7 +89,7 @@ def export_course_to_directory(course_key, root_dir):
course_dir = replacement_char.join([course.id.org, course.id.course, course.id.run])
course_dir = re.sub(r'[^\w\.\-]', replacement_char, course_dir)
export_to_xml(store, None, course.id, root_dir, course_dir)
export_course_to_xml(store, None, course.id, root_dir, course_dir)
export_dir = path(root_dir) / course_dir
return export_dir
......
......@@ -12,7 +12,6 @@ import factory
from django.conf import settings
from django.core.management import call_command
from django.test.utils import override_settings
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
......@@ -21,7 +20,7 @@ from xmodule.modulestore.tests.django_utils import (
TEST_DATA_MONGO_MODULESTORE, TEST_DATA_SPLIT_MODULESTORE
)
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.xml_importer import import_course_from_xml
DATA_DIR = settings.COMMON_TEST_DATA_ROOT
XML_COURSE_DIRS = ['toy', 'simple', 'open_ended']
......@@ -32,7 +31,7 @@ MAPPINGS = {
}
TEST_DATA_MIXED_XML_MODULESTORE = mixed_store_config(
DATA_DIR, MAPPINGS, include_xml=True, xml_course_dirs=XML_COURSE_DIRS
DATA_DIR, MAPPINGS, include_xml=True, xml_source_dirs=XML_COURSE_DIRS,
)
......@@ -67,8 +66,8 @@ class CommandsTestBase(ModuleStoreTestCase):
courses = store.get_courses()
# NOTE: if xml store owns these, it won't import them into mongo
if self.test_course_key not in [c.id for c in courses]:
import_from_xml(
store, ModuleStoreEnum.UserID.mgmt_command, DATA_DIR, XML_COURSE_DIRS, create_course_if_not_present=True
import_course_from_xml(
store, ModuleStoreEnum.UserID.mgmt_command, DATA_DIR, XML_COURSE_DIRS, create_if_not_present=True
)
return [course.id for course in store.get_courses()]
......
......@@ -15,7 +15,7 @@ from courseware.tests.helpers import get_request_for_user
from student.tests.factories import UserFactory
import xmodule.modulestore.django as store_django
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.xml_importer import import_course_from_xml
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.django_utils import (
TEST_DATA_MOCK_MODULESTORE, TEST_DATA_MIXED_TOY_MODULESTORE
......@@ -160,7 +160,7 @@ class CoursesRenderTest(ModuleStoreTestCase):
super(CoursesRenderTest, self).setUp()
store = store_django.modulestore()
course_items = import_from_xml(store, self.user.id, TEST_DATA_DIR, ['toy'])
course_items = import_course_from_xml(store, self.user.id, TEST_DATA_DIR, ['toy'])
course_key = course_items[0].id
self.course = get_course_by_id(course_key)
self.request = get_request_for_user(UserFactory.create())
......
......@@ -231,7 +231,7 @@ def add_repo(repo, rdir_in, branch=None):
# extract course ID from output of import-command-run and make symlink
# this is needed in order for custom course scripts to work
match = re.search(r'(?ms)===> IMPORTING course (\S+)', ret_import)
match = re.search(r'(?ms)===> IMPORTING courselike (\S+)', ret_import)
if match:
course_id = match.group(1)
try:
......
......@@ -15,7 +15,7 @@ from courseware.tests.factories import StudentModuleFactory, UserFactory
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.django_utils import TEST_DATA_MOCK_MODULESTORE
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.xml_importer import import_course_from_xml
from xmodule.open_ended_grading_classes.openendedchild import OpenEndedChild
from xmodule.tests.test_util_open_ended import (
STATE_INITIAL, STATE_ACCESSING, STATE_POST_ASSESSMENT
......@@ -36,7 +36,7 @@ class OpenEndedPostTest(ModuleStoreTestCase):
super(OpenEndedPostTest, self).setUp()
self.user = UserFactory()
store = modulestore()
course_items = import_from_xml(store, self.user.id, TEST_DATA_DIR, ['open_ended']) # pylint: disable=maybe-no-member
course_items = import_course_from_xml(store, self.user.id, TEST_DATA_DIR, ['open_ended']) # pylint: disable=maybe-no-member
self.course = course_items[0]
self.course_id = self.course.id
......@@ -138,7 +138,7 @@ class OpenEndedStatsTest(ModuleStoreTestCase):
self.user = UserFactory()
store = modulestore()
course_items = import_from_xml(store, self.user.id, TEST_DATA_DIR, ['open_ended']) # pylint: disable=maybe-no-member
course_items = import_course_from_xml(store, self.user.id, TEST_DATA_DIR, ['open_ended']) # pylint: disable=maybe-no-member
self.course = course_items[0]
self.course_id = self.course.id
......
......@@ -8,7 +8,7 @@ from django.conf import settings
from xmodule.html_module import CourseInfoModule
from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.xml_importer import import_course_from_xml
from ..testutils import (
MobileAPITestCase, MobileCourseAccessTestMixin, MobileEnrolledCourseAccessTestMixin, MobileAuthTestMixin
......@@ -126,7 +126,7 @@ class TestHandouts(MobileAPITestCase, MobileAuthTestMixin, MobileEnrolledCourseA
self.store = modulestore()._get_modulestore_by_type(ModuleStoreEnum.Type.mongo)
# use toy course with handouts, and make it mobile_available
course_items = import_from_xml(self.store, self.user.id, settings.COMMON_TEST_DATA_ROOT, ['toy'])
course_items = import_course_from_xml(self.store, self.user.id, settings.COMMON_TEST_DATA_ROOT, ['toy'])
self.course = course_items[0]
self.course.mobile_available = True
self.store.update_item(self.course, self.user.id)
......
......@@ -31,7 +31,7 @@ from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.django_utils import (
TEST_DATA_MOCK_MODULESTORE, TEST_DATA_MIXED_TOY_MODULESTORE
)
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.xml_importer import import_course_from_xml
from xmodule.open_ended_grading_classes import peer_grading_service, controller_query_service
from xmodule.tests import test_util_open_ended
......@@ -453,7 +453,7 @@ class TestPanel(ModuleStoreTestCase):
super(TestPanel, self).setUp()
self.user = factories.UserFactory()
store = modulestore()
course_items = import_from_xml(store, self.user.id, TEST_DATA_DIR, ['open_ended']) # pylint: disable=maybe-no-member
course_items = import_course_from_xml(store, self.user.id, TEST_DATA_DIR, ['open_ended']) # pylint: disable=maybe-no-member
self.course = course_items[0]
self.course_key = self.course.id
......@@ -497,7 +497,7 @@ class TestPeerGradingFound(ModuleStoreTestCase):
super(TestPeerGradingFound, self).setUp()
self.user = factories.UserFactory()
store = modulestore()
course_items = import_from_xml(store, self.user.id, TEST_DATA_DIR, ['open_ended_nopath']) # pylint: disable=maybe-no-member
course_items = import_course_from_xml(store, self.user.id, TEST_DATA_DIR, ['open_ended_nopath']) # pylint: disable=maybe-no-member
self.course = course_items[0]
self.course_key = self.course.id
......@@ -521,7 +521,7 @@ class TestStudentProblemList(ModuleStoreTestCase):
# Load an open ended course with several problems.
self.user = factories.UserFactory()
store = modulestore()
course_items = import_from_xml(store, self.user.id, TEST_DATA_DIR, ['open_ended']) # pylint: disable=maybe-no-member
course_items = import_course_from_xml(store, self.user.id, TEST_DATA_DIR, ['open_ended']) # pylint: disable=maybe-no-member
self.course = course_items[0]
self.course_key = self.course.id
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment