diff --git a/cms/djangoapps/contentstore/tests/factories.py b/cms/djangoapps/contentstore/tests/factories.py
index cb9f451d38..f9c505d68f 100644
--- a/cms/djangoapps/contentstore/tests/factories.py
+++ b/cms/djangoapps/contentstore/tests/factories.py
@@ -1,117 +1,44 @@
from factory import Factory
-from xmodule.modulestore import Location
-from xmodule.modulestore.django import modulestore
-from time import gmtime
+from datetime import datetime
from uuid import uuid4
-from xmodule.timeparse import stringify_time
+from student.models import (User, UserProfile, Registration,
+ CourseEnrollmentAllowed)
+from django.contrib.auth.models import Group
+class UserProfileFactory(Factory):
+ FACTORY_FOR = UserProfile
-def XMODULE_COURSE_CREATION(class_to_create, **kwargs):
- return XModuleCourseFactory._create(class_to_create, **kwargs)
+ user = None
+ name = 'Robot Studio'
+ courseware = 'course.xml'
-def XMODULE_ITEM_CREATION(class_to_create, **kwargs):
- return XModuleItemFactory._create(class_to_create, **kwargs)
+class RegistrationFactory(Factory):
+ FACTORY_FOR = Registration
-class XModuleCourseFactory(Factory):
- """
- Factory for XModule courses.
- """
+ user = None
+ activation_key = uuid4().hex
- ABSTRACT_FACTORY = True
- _creation_function = (XMODULE_COURSE_CREATION,)
+class UserFactory(Factory):
+ FACTORY_FOR = User
- @classmethod
- def _create(cls, target_class, *args, **kwargs):
+ username = 'robot'
+ email = 'robot@edx.org'
+ password = 'test'
+ first_name = 'Robot'
+ last_name = 'Tester'
+ is_staff = False
+ is_active = True
+ is_superuser = False
+ last_login = datetime.now()
+ date_joined = datetime.now()
- template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
- org = kwargs.get('org')
- number = kwargs.get('number')
- display_name = kwargs.get('display_name')
- location = Location('i4x', org, number,
- 'course', Location.clean(display_name))
+class GroupFactory(Factory):
+ FACTORY_FOR = Group
- store = modulestore('direct')
+ name = 'test_group'
- # Write the data to the mongo datastore
- new_course = store.clone_item(template, location)
+class CourseEnrollmentAllowedFactory(Factory):
+ FACTORY_FOR = CourseEnrollmentAllowed
- # This metadata code was copied from cms/djangoapps/contentstore/views.py
- if display_name is not None:
- new_course.metadata['display_name'] = display_name
-
- new_course.metadata['data_dir'] = uuid4().hex
- new_course.metadata['start'] = stringify_time(gmtime())
- new_course.tabs = [{"type": "courseware"},
- {"type": "course_info", "name": "Course Info"},
- {"type": "discussion", "name": "Discussion"},
- {"type": "wiki", "name": "Wiki"},
- {"type": "progress", "name": "Progress"}]
-
- # Update the data in the mongo datastore
- store.update_metadata(new_course.location.url(), new_course.own_metadata)
-
- return new_course
-
-class Course:
- pass
-
-class CourseFactory(XModuleCourseFactory):
- FACTORY_FOR = Course
-
- template = 'i4x://edx/templates/course/Empty'
- org = 'MITx'
- number = '999'
- display_name = 'Robot Super Course'
-
-class XModuleItemFactory(Factory):
- """
- Factory for XModule items.
- """
-
- ABSTRACT_FACTORY = True
- _creation_function = (XMODULE_ITEM_CREATION,)
-
- @classmethod
- def _create(cls, target_class, *args, **kwargs):
- """
- kwargs must include parent_location, template. Can contain display_name
- target_class is ignored
- """
-
- DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
-
- parent_location = Location(kwargs.get('parent_location'))
- template = Location(kwargs.get('template'))
- display_name = kwargs.get('display_name')
-
- store = modulestore('direct')
-
- # This code was based off that in cms/djangoapps/contentstore/views.py
- parent = store.get_item(parent_location)
- dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
-
- new_item = store.clone_item(template, dest_location)
-
- # TODO: This needs to be deleted when we have proper storage for static content
- new_item.metadata['data_dir'] = parent.metadata['data_dir']
-
- # replace the display name with an optional parameter passed in from the caller
- if display_name is not None:
- new_item.metadata['display_name'] = display_name
-
- store.update_metadata(new_item.location.url(), new_item.own_metadata)
-
- if new_item.location.category not in DETACHED_CATEGORIES:
- store.update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()])
-
- return new_item
-
-class Item:
- pass
-
-class ItemFactory(XModuleItemFactory):
- FACTORY_FOR = Item
-
- parent_location = 'i4x://MITx/999/course/Robot_Super_Course'
- template = 'i4x://edx/templates/chapter/Empty'
- display_name = 'Section One'
\ No newline at end of file
+ email = 'test@edx.org'
+ course_id = 'edX/test/2012_Fall'
diff --git a/cms/djangoapps/contentstore/tests/test_contentstore.py b/cms/djangoapps/contentstore/tests/test_contentstore.py
new file mode 100644
index 0000000000..ce7d9e757c
--- /dev/null
+++ b/cms/djangoapps/contentstore/tests/test_contentstore.py
@@ -0,0 +1,400 @@
+import json
+import shutil
+from django.test.client import Client
+from override_settings import override_settings
+from django.conf import settings
+from django.core.urlresolvers import reverse
+from path import path
+from tempfile import mkdtemp
+import json
+from fs.osfs import OSFS
+import copy
+from mock import Mock
+
+from student.models import Registration
+from django.contrib.auth.models import User
+from cms.djangoapps.contentstore.utils import get_modulestore
+
+from utils import ModuleStoreTestCase, parse_json
+from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
+
+from xmodule.modulestore import Location
+from xmodule.modulestore.store_utilities import clone_course
+from xmodule.modulestore.store_utilities import delete_course
+from xmodule.modulestore.django import modulestore, _MODULESTORES
+from xmodule.contentstore.django import contentstore
+from xmodule.templates import update_templates
+from xmodule.modulestore.xml_exporter import export_to_xml
+from xmodule.modulestore.xml_importer import import_from_xml
+
+from xmodule.capa_module import CapaDescriptor
+from xmodule.course_module import CourseDescriptor
+from xmodule.seq_module import SequenceDescriptor
+
+TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
+TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
+TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data')
+
+@override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
+class ContentStoreToyCourseTest(ModuleStoreTestCase):
+ """
+ Tests that rely on the toy courses.
+ TODO: refactor using CourseFactory so they do not.
+ """
+ def setUp(self):
+ uname = 'testuser'
+ email = 'test+courses@edx.org'
+ password = 'foo'
+
+ # Create the use so we can log them in.
+ self.user = User.objects.create_user(uname, email, password)
+
+ # Note that we do not actually need to do anything
+ # for registration if we directly mark them active.
+ self.user.is_active = True
+ # Staff has access to view all courses
+ self.user.is_staff = True
+ self.user.save()
+
+ self.client = Client()
+ self.client.login(username=uname, password=password)
+
+
+ def check_edit_unit(self, test_course_name):
+ import_from_xml(modulestore(), 'common/test/data/', [test_course_name])
+
+ for descriptor in modulestore().get_items(Location(None, None, 'vertical', None, None)):
+ print "Checking ", descriptor.location.url()
+ print descriptor.__class__, descriptor.location
+ resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
+ self.assertEqual(resp.status_code, 200)
+
+ def test_edit_unit_toy(self):
+ self.check_edit_unit('toy')
+
+ def test_edit_unit_full(self):
+ self.check_edit_unit('full')
+
+ def test_static_tab_reordering(self):
+ import_from_xml(modulestore(), 'common/test/data/', ['full'])
+
+ ms = modulestore('direct')
+ course = ms.get_item(Location(['i4x','edX','full','course','6.002_Spring_2012', None]))
+
+ # reverse the ordering
+ reverse_tabs = []
+ for tab in course.tabs:
+ if tab['type'] == 'static_tab':
+ reverse_tabs.insert(0, 'i4x://edX/full/static_tab/{0}'.format(tab['url_slug']))
+
+ resp = self.client.post(reverse('reorder_static_tabs'), json.dumps({'tabs':reverse_tabs}), "application/json")
+
+ course = ms.get_item(Location(['i4x','edX','full','course','6.002_Spring_2012', None]))
+
+ # compare to make sure that the tabs information is in the expected order after the server call
+ course_tabs = []
+ for tab in course.tabs:
+ if tab['type'] == 'static_tab':
+ course_tabs.append('i4x://edX/full/static_tab/{0}'.format(tab['url_slug']))
+
+ self.assertEqual(reverse_tabs, course_tabs)
+
+ def test_about_overrides(self):
+ '''
+ This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html
+ while there is a base definition in /about/effort.html
+ '''
+ import_from_xml(modulestore(), 'common/test/data/', ['full'])
+ ms = modulestore('direct')
+ effort = ms.get_item(Location(['i4x','edX','full','about','effort', None]))
+ self.assertEqual(effort.definition['data'],'6 hours')
+
+ # this one should be in a non-override folder
+ effort = ms.get_item(Location(['i4x','edX','full','about','end_date', None]))
+ self.assertEqual(effort.definition['data'],'TBD')
+
+ def test_remove_hide_progress_tab(self):
+ import_from_xml(modulestore(), 'common/test/data/', ['full'])
+
+ ms = modulestore('direct')
+ cs = contentstore()
+
+ source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
+ course = ms.get_item(source_location)
+ self.assertNotIn('hide_progress_tab', course.metadata)
+
+ def test_clone_course(self):
+
+ course_data = {
+ 'template': 'i4x://edx/templates/course/Empty',
+ 'org': 'MITx',
+ 'number': '999',
+ 'display_name': 'Robot Super Course',
+ }
+
+ import_from_xml(modulestore(), 'common/test/data/', ['full'])
+
+ resp = self.client.post(reverse('create_new_course'), course_data)
+ self.assertEqual(resp.status_code, 200)
+ data = parse_json(resp)
+ self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course')
+
+ ms = modulestore('direct')
+ cs = contentstore()
+
+ source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
+ dest_location = CourseDescriptor.id_to_location('MITx/999/Robot_Super_Course')
+
+ clone_course(ms, cs, source_location, dest_location)
+
+ # now loop through all the units in the course and verify that the clone can render them, which
+ # means the objects are at least present
+ items = ms.get_items(Location(['i4x','edX', 'full', 'vertical', None]))
+ self.assertGreater(len(items), 0)
+ clone_items = ms.get_items(Location(['i4x', 'MITx','999','vertical', None]))
+ self.assertGreater(len(clone_items), 0)
+ for descriptor in items:
+ new_loc = descriptor.location._replace(org = 'MITx', course='999')
+ print "Checking {0} should now also be at {1}".format(descriptor.location.url(), new_loc.url())
+ resp = self.client.get(reverse('edit_unit', kwargs={'location': new_loc.url()}))
+ self.assertEqual(resp.status_code, 200)
+
+ def test_delete_course(self):
+ import_from_xml(modulestore(), 'common/test/data/', ['full'])
+
+ ms = modulestore('direct')
+ cs = contentstore()
+
+ location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
+
+ delete_course(ms, cs, location)
+
+ items = ms.get_items(Location(['i4x','edX', 'full', 'vertical', None]))
+ self.assertEqual(len(items), 0)
+
+ def verify_content_existence(self, modulestore, root_dir, location, dirname, category_name, filename_suffix=''):
+ fs = OSFS(root_dir / 'test_export')
+ self.assertTrue(fs.exists(dirname))
+
+ query_loc = Location('i4x', location.org, location.course, category_name, None)
+ items = modulestore.get_items(query_loc)
+
+ for item in items:
+ fs = OSFS(root_dir / ('test_export/' + dirname))
+ self.assertTrue(fs.exists(item.location.name + filename_suffix))
+
+ def test_export_course(self):
+ ms = modulestore('direct')
+ cs = contentstore()
+
+ import_from_xml(ms, 'common/test/data/', ['full'])
+ location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
+
+ root_dir = path(mkdtemp())
+
+ print 'Exporting to tempdir = {0}'.format(root_dir)
+
+ # export out to a tempdir
+ export_to_xml(ms, cs, location, root_dir, 'test_export')
+
+ # check for static tabs
+ self.verify_content_existence(ms, root_dir, location, 'tabs', 'static_tab', '.html')
+
+ # check for custom_tags
+ self.verify_content_existence(ms, root_dir, location, 'info', 'course_info', '.html')
+
+ # check for custom_tags
+ self.verify_content_existence(ms, root_dir, location, 'custom_tags', 'custom_tag_template')
+
+
+ # remove old course
+ delete_course(ms, cs, location)
+
+ # reimport
+ import_from_xml(ms, root_dir, ['test_export'])
+
+ items = ms.get_items(Location(['i4x','edX', 'full', 'vertical', None]))
+ self.assertGreater(len(items), 0)
+ for descriptor in items:
+ print "Checking {0}....".format(descriptor.location.url())
+ resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
+ self.assertEqual(resp.status_code, 200)
+
+ shutil.rmtree(root_dir)
+
+ def test_course_handouts_rewrites(self):
+ ms = modulestore('direct')
+ cs = contentstore()
+
+ # import a test course
+ import_from_xml(ms, 'common/test/data/', ['full'])
+
+ handout_location= Location(['i4x', 'edX', 'full', 'course_info', 'handouts'])
+
+ # get module info
+ resp = self.client.get(reverse('module_info', kwargs={'module_location': handout_location}))
+
+ # make sure we got a successful response
+ self.assertEqual(resp.status_code, 200)
+
+ # check that /static/ has been converted to the full path
+ # note, we know the link it should be because that's what in the 'full' course in the test data
+ self.assertContains(resp, '/c4x/edX/full/asset/handouts_schematic_tutorial.pdf')
+
+
+class ContentStoreTest(ModuleStoreTestCase):
+ """
+ Tests for the CMS ContentStore application.
+ """
+ def setUp(self):
+ """
+ These tests need a user in the DB so that the django Test Client
+ can log them in.
+ They inherit from the ModuleStoreTestCase class so that the mongodb collection
+ will be cleared out before each test case execution and deleted
+ afterwards.
+ """
+ uname = 'testuser'
+ email = 'test+courses@edx.org'
+ password = 'foo'
+
+ # Create the use so we can log them in.
+ self.user = User.objects.create_user(uname, email, password)
+
+ # Note that we do not actually need to do anything
+ # for registration if we directly mark them active.
+ self.user.is_active = True
+ # Staff has access to view all courses
+ self.user.is_staff = True
+ self.user.save()
+
+ self.client = Client()
+ self.client.login(username=uname, password=password)
+
+ self.course_data = {
+ 'template': 'i4x://edx/templates/course/Empty',
+ 'org': 'MITx',
+ 'number': '999',
+ 'display_name': 'Robot Super Course',
+ }
+
+ def test_create_course(self):
+ """Test new course creation - happy path"""
+ resp = self.client.post(reverse('create_new_course'), self.course_data)
+ self.assertEqual(resp.status_code, 200)
+ data = parse_json(resp)
+ self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course')
+
+ def test_create_course_duplicate_course(self):
+ """Test new course creation - error path"""
+ resp = self.client.post(reverse('create_new_course'), self.course_data)
+ resp = self.client.post(reverse('create_new_course'), self.course_data)
+ data = parse_json(resp)
+ self.assertEqual(resp.status_code, 200)
+ self.assertEqual(data['ErrMsg'], 'There is already a course defined with this name.')
+
+ def test_create_course_duplicate_number(self):
+ """Test new course creation - error path"""
+ resp = self.client.post(reverse('create_new_course'), self.course_data)
+ self.course_data['display_name'] = 'Robot Super Course Two'
+
+ resp = self.client.post(reverse('create_new_course'), self.course_data)
+ data = parse_json(resp)
+
+ self.assertEqual(resp.status_code, 200)
+ self.assertEqual(data['ErrMsg'],
+ 'There is already a course defined with the same organization and course number.')
+
+ def test_create_course_with_bad_organization(self):
+ """Test new course creation - error path for bad organization name"""
+ self.course_data['org'] = 'University of California, Berkeley'
+ resp = self.client.post(reverse('create_new_course'), self.course_data)
+ data = parse_json(resp)
+
+ self.assertEqual(resp.status_code, 200)
+ self.assertEqual(data['ErrMsg'],
+ "Unable to create course 'Robot Super Course'.\n\nInvalid characters in 'University of California, Berkeley'.")
+
+ def test_course_index_view_with_no_courses(self):
+ """Test viewing the index page with no courses"""
+ # Create a course so there is something to view
+ resp = self.client.get(reverse('index'))
+ self.assertContains(resp,
+ '
My Courses
',
+ status_code=200,
+ html=True)
+
+ def test_course_factory(self):
+ """Test that the course factory works correctly."""
+ course = CourseFactory.create()
+ self.assertIsInstance(course, CourseDescriptor)
+
+ def test_item_factory(self):
+ """Test that the item factory works correctly."""
+ course = CourseFactory.create()
+ item = ItemFactory.create(parent_location=course.location)
+ self.assertIsInstance(item, SequenceDescriptor)
+
+ def test_course_index_view_with_course(self):
+ """Test viewing the index page with an existing course"""
+ CourseFactory.create(display_name='Robot Super Educational Course')
+ resp = self.client.get(reverse('index'))
+ self.assertContains(resp,
+ 'Robot Super Educational Course',
+ status_code=200,
+ html=True)
+
+ def test_course_overview_view_with_course(self):
+ """Test viewing the course overview page with an existing course"""
+ CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
+
+ data = {
+ 'org': 'MITx',
+ 'course': '999',
+ 'name': Location.clean('Robot Super Course'),
+ }
+
+ resp = self.client.get(reverse('course_index', kwargs=data))
+ self.assertContains(resp,
+ 'Robot Super Course',
+ status_code=200,
+ html=True)
+
+ def test_clone_item(self):
+ """Test cloning an item. E.g. creating a new section"""
+ CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
+
+ section_data = {
+ 'parent_location' : 'i4x://MITx/999/course/Robot_Super_Course',
+ 'template' : 'i4x://edx/templates/chapter/Empty',
+ 'display_name': 'Section One',
+ }
+
+ resp = self.client.post(reverse('clone_item'), section_data)
+
+ self.assertEqual(resp.status_code, 200)
+ data = parse_json(resp)
+ self.assertRegexpMatches(data['id'],
+ '^i4x:\/\/MITx\/999\/chapter\/([0-9]|[a-f]){32}$')
+
+ def test_capa_module(self):
+ """Test that a problem treats markdown specially."""
+ CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
+
+ problem_data = {
+ 'parent_location' : 'i4x://MITx/999/course/Robot_Super_Course',
+ 'template' : 'i4x://edx/templates/problem/Empty'
+ }
+
+ resp = self.client.post(reverse('clone_item'), problem_data)
+
+ self.assertEqual(resp.status_code, 200)
+ payload = parse_json(resp)
+ problem_loc = payload['id']
+ problem = get_modulestore(problem_loc).get_item(problem_loc)
+ # should be a CapaDescriptor
+ self.assertIsInstance(problem, CapaDescriptor, "New problem is not a CapaDescriptor")
+ context = problem.get_context()
+ self.assertIn('markdown', context, "markdown is missing from context")
+ self.assertIn('markdown', problem.metadata, "markdown is missing from metadata")
+ self.assertNotIn('markdown', problem.editable_metadata_fields, "Markdown slipped into the editable metadata fields")
diff --git a/cms/djangoapps/contentstore/tests/test_core_caching.py b/cms/djangoapps/contentstore/tests/test_core_caching.py
index 0cb4a4930c..ed41e5cc64 100644
--- a/cms/djangoapps/contentstore/tests/test_core_caching.py
+++ b/cms/djangoapps/contentstore/tests/test_core_caching.py
@@ -1,7 +1,7 @@
-from django.test.testcases import TestCase
from cache_toolbox.core import get_cached_content, set_cached_content, del_cached_content
from xmodule.modulestore import Location
from xmodule.contentstore.content import StaticContent
+from django.test import TestCase
class Content:
def __init__(self, location, content):
@@ -32,7 +32,3 @@ class CachingTestCase(TestCase):
'should not be stored in cache with unicodeLocation')
self.assertEqual(None, get_cached_content(self.nonUnicodeLocation),
'should not be stored in cache with nonUnicodeLocation')
-
-
-
-
diff --git a/cms/djangoapps/contentstore/tests/test_course_settings.py b/cms/djangoapps/contentstore/tests/test_course_settings.py
index 74eff6e9cc..f47733b32c 100644
--- a/cms/djangoapps/contentstore/tests/test_course_settings.py
+++ b/cms/djangoapps/contentstore/tests/test_course_settings.py
@@ -1,21 +1,27 @@
-from django.test.testcases import TestCase
import datetime
import time
+import json
+import calendar
+import copy
+from util import converters
+from util.converters import jsdate_to_time
+
from django.contrib.auth.models import User
-import xmodule
from django.test.client import Client
from django.core.urlresolvers import reverse
-from xmodule.modulestore import Location
-from cms.djangoapps.models.settings.course_details import CourseDetails,\
- CourseSettingsEncoder
-import json
-from util import converters
-import calendar
-from util.converters import jsdate_to_time
from django.utils.timezone import UTC
+
+import xmodule
+from xmodule.modulestore import Location
+from cms.djangoapps.models.settings.course_details import (CourseDetails,
+ CourseSettingsEncoder)
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
from cms.djangoapps.contentstore.utils import get_modulestore
-import copy
+
+from django.test import TestCase
+from utils import ModuleStoreTestCase
+from xmodule.modulestore.tests.factories import CourseFactory
+
# YYYY-MM-DDThh:mm:ss.s+/-HH:MM
class ConvertersTestCase(TestCase):
@@ -36,8 +42,15 @@ class ConvertersTestCase(TestCase):
self.compare_dates(converters.jsdate_to_time("2013-01-01T00:00:00"), converters.jsdate_to_time("2012-12-31T23:59:59"), datetime.timedelta(seconds=1))
-class CourseTestCase(TestCase):
+class CourseTestCase(ModuleStoreTestCase):
def setUp(self):
+ """
+ These tests need a user in the DB so that the django Test Client
+ can log them in.
+ They inherit from the ModuleStoreTestCase class so that the mongodb collection
+ will be cleared out before each test case execution and deleted
+ afterwards.
+ """
uname = 'testuser'
email = 'test+courses@edx.org'
password = 'foo'
@@ -52,36 +65,15 @@ class CourseTestCase(TestCase):
self.user.is_staff = True
self.user.save()
- # Flush and initialize the module store
- # It needs the templates because it creates new records
- # by cloning from the template.
- # Note that if your test module gets in some weird state
- # (though it shouldn't), do this manually
- # from the bash shell to drop it:
- # $ mongo test_xmodule --eval "db.dropDatabase()"
- xmodule.modulestore.django._MODULESTORES = {}
- xmodule.modulestore.django.modulestore().collection.drop()
- xmodule.templates.update_templates()
-
self.client = Client()
self.client.login(username=uname, password=password)
- self.course_data = {
- 'template': 'i4x://edx/templates/course/Empty',
- 'org': 'MITx',
- 'number': '999',
- 'display_name': 'Robot Super Course',
- }
- self.course_location = Location('i4x', 'MITx', '999', 'course', 'Robot_Super_Course')
- self.create_course()
-
- def tearDown(self):
- xmodule.modulestore.django._MODULESTORES = {}
- xmodule.modulestore.django.modulestore().collection.drop()
-
- def create_course(self):
- """Create new course"""
- self.client.post(reverse('create_new_course'), self.course_data)
+ t='i4x://edx/templates/course/Empty'
+ o='MITx'
+ n='999'
+ dn='Robot Super Course'
+ self.course_location = Location('i4x', o, n, 'course', 'Robot_Super_Course')
+ CourseFactory.create(template=t, org=o, number=n, display_name=dn)
class CourseDetailsTestCase(CourseTestCase):
def test_virgin_fetch(self):
@@ -145,7 +137,6 @@ class CourseDetailsViewTest(CourseTestCase):
return datetime.isoformat("T")
else:
return None
-
def test_update_and_fetch(self):
details = CourseDetails.fetch(self.course_location)
@@ -271,5 +262,3 @@ class CourseGradingTest(CourseTestCase):
test_grader.graders[1]['drop_count'] = test_grader.graders[1].get('drop_count') + 1
altered_grader = CourseGradingModel.update_grader_from_json(test_grader.course_location, test_grader.graders[1])
self.assertDictEqual(test_grader.graders[1], altered_grader, "drop_count[1] + 2")
-
-
diff --git a/cms/djangoapps/contentstore/tests/test_utils.py b/cms/djangoapps/contentstore/tests/test_utils.py
index 13f6189cc5..6811d64c12 100644
--- a/cms/djangoapps/contentstore/tests/test_utils.py
+++ b/cms/djangoapps/contentstore/tests/test_utils.py
@@ -1,6 +1,6 @@
-from django.test.testcases import TestCase
from cms.djangoapps.contentstore import utils
import mock
+from django.test import TestCase
class LMSLinksTestCase(TestCase):
def about_page_test(self):
diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py
index 085ecebff1..df2e4dcc79 100644
--- a/cms/djangoapps/contentstore/tests/tests.py
+++ b/cms/djangoapps/contentstore/tests/tests.py
@@ -1,6 +1,5 @@
import json
import shutil
-from django.test import TestCase
from django.test.client import Client
from override_settings import override_settings
from django.conf import settings
@@ -9,40 +8,27 @@ from path import path
from tempfile import mkdtemp
import json
from fs.osfs import OSFS
-
-
-from student.models import Registration
-from django.contrib.auth.models import User
-import xmodule.modulestore.django
-from xmodule.modulestore.xml_importer import import_from_xml
import copy
-from factories import *
+from cms.djangoapps.contentstore.utils import get_modulestore
+
+from xmodule.modulestore import Location
from xmodule.modulestore.store_utilities import clone_course
from xmodule.modulestore.store_utilities import delete_course
-from xmodule.modulestore.django import modulestore
+from xmodule.modulestore.django import modulestore, _MODULESTORES
from xmodule.contentstore.django import contentstore
-from xmodule.course_module import CourseDescriptor
+from xmodule.templates import update_templates
from xmodule.modulestore.xml_exporter import export_to_xml
-from cms.djangoapps.contentstore.utils import get_modulestore
+from xmodule.modulestore.xml_importer import import_from_xml
+
from xmodule.capa_module import CapaDescriptor
+from xmodule.course_module import CourseDescriptor
+from xmodule.seq_module import SequenceDescriptor
-def parse_json(response):
- """Parse response, which is assumed to be json"""
- return json.loads(response.content)
+from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
+from utils import ModuleStoreTestCase, parse_json, user, registration
-
-def user(email):
- """look up a user by email"""
- return User.objects.get(email=email)
-
-
-def registration(email):
- """look up registration object by email"""
- return Registration.objects.get(user__email=email)
-
-
-class ContentStoreTestCase(TestCase):
+class ContentStoreTestCase(ModuleStoreTestCase):
def _login(self, email, pw):
"""Login. View should always return 200. The success/fail is in the
returned json"""
@@ -187,356 +173,3 @@ class AuthTestCase(ContentStoreTestCase):
self.assertEqual(resp.status_code, 302)
# Logged in should work.
-
-TEST_DATA_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
-TEST_DATA_MODULESTORE['default']['OPTIONS']['fs_root'] = path('common/test/data')
-TEST_DATA_MODULESTORE['direct']['OPTIONS']['fs_root'] = path('common/test/data')
-
-@override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
-class ContentStoreTest(TestCase):
-
- def setUp(self):
- uname = 'testuser'
- email = 'test+courses@edx.org'
- password = 'foo'
-
- # Create the use so we can log them in.
- self.user = User.objects.create_user(uname, email, password)
-
- # Note that we do not actually need to do anything
- # for registration if we directly mark them active.
- self.user.is_active = True
- # Staff has access to view all courses
- self.user.is_staff = True
- self.user.save()
-
- # Flush and initialize the module store
- # It needs the templates because it creates new records
- # by cloning from the template.
- # Note that if your test module gets in some weird state
- # (though it shouldn't), do this manually
- # from the bash shell to drop it:
- # $ mongo test_xmodule --eval "db.dropDatabase()"
- xmodule.modulestore.django._MODULESTORES = {}
- xmodule.modulestore.django.modulestore().collection.drop()
- xmodule.templates.update_templates()
-
- self.client = Client()
- self.client.login(username=uname, password=password)
-
- self.course_data = {
- 'template': 'i4x://edx/templates/course/Empty',
- 'org': 'MITx',
- 'number': '999',
- 'display_name': 'Robot Super Course',
- }
-
- def tearDown(self):
- # Make sure you flush out the test modulestore after the end
- # of the last test because otherwise on the next run
- # cms/djangoapps/contentstore/__init__.py
- # update_templates() will try to update the templates
- # via upsert and it sometimes seems to be messing things up.
- xmodule.modulestore.django._MODULESTORES = {}
- xmodule.modulestore.django.modulestore().collection.drop()
-
- def test_create_course(self):
- """Test new course creation - happy path"""
- resp = self.client.post(reverse('create_new_course'), self.course_data)
- self.assertEqual(resp.status_code, 200)
- data = parse_json(resp)
- self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course')
-
- def test_create_course_duplicate_course(self):
- """Test new course creation - error path"""
- resp = self.client.post(reverse('create_new_course'), self.course_data)
- resp = self.client.post(reverse('create_new_course'), self.course_data)
- data = parse_json(resp)
- self.assertEqual(resp.status_code, 200)
- self.assertEqual(data['ErrMsg'], 'There is already a course defined with this name.')
-
- def test_create_course_duplicate_number(self):
- """Test new course creation - error path"""
- resp = self.client.post(reverse('create_new_course'), self.course_data)
- self.course_data['display_name'] = 'Robot Super Course Two'
-
- resp = self.client.post(reverse('create_new_course'), self.course_data)
- data = parse_json(resp)
-
- self.assertEqual(resp.status_code, 200)
- self.assertEqual(data['ErrMsg'],
- 'There is already a course defined with the same organization and course number.')
-
- def test_create_course_with_bad_organization(self):
- """Test new course creation - error path for bad organization name"""
- self.course_data['org'] = 'University of California, Berkeley'
- resp = self.client.post(reverse('create_new_course'), self.course_data)
- data = parse_json(resp)
-
- self.assertEqual(resp.status_code, 200)
- self.assertEqual(data['ErrMsg'],
- "Unable to create course 'Robot Super Course'.\n\nInvalid characters in 'University of California, Berkeley'.")
-
- def test_course_index_view_with_no_courses(self):
- """Test viewing the index page with no courses"""
- # Create a course so there is something to view
- resp = self.client.get(reverse('index'))
- self.assertContains(resp,
- 'My Courses
',
- status_code=200,
- html=True)
-
- def test_course_factory(self):
- course = CourseFactory.create()
- self.assertIsInstance(course, xmodule.course_module.CourseDescriptor)
-
- def test_item_factory(self):
- course = CourseFactory.create()
- item = ItemFactory.create(parent_location=course.location)
- self.assertIsInstance(item, xmodule.seq_module.SequenceDescriptor)
-
- def test_course_index_view_with_course(self):
- """Test viewing the index page with an existing course"""
- CourseFactory.create(display_name='Robot Super Educational Course')
- resp = self.client.get(reverse('index'))
- self.assertContains(resp,
- 'Robot Super Educational Course',
- status_code=200,
- html=True)
-
- def test_course_overview_view_with_course(self):
- """Test viewing the course overview page with an existing course"""
- CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
-
- data = {
- 'org': 'MITx',
- 'course': '999',
- 'name': Location.clean('Robot Super Course'),
- }
-
- resp = self.client.get(reverse('course_index', kwargs=data))
- self.assertContains(resp,
- 'Robot Super Course',
- status_code=200,
- html=True)
-
- def test_clone_item(self):
- """Test cloning an item. E.g. creating a new section"""
- CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
-
- section_data = {
- 'parent_location' : 'i4x://MITx/999/course/Robot_Super_Course',
- 'template' : 'i4x://edx/templates/chapter/Empty',
- 'display_name': 'Section One',
- }
-
- resp = self.client.post(reverse('clone_item'), section_data)
-
- self.assertEqual(resp.status_code, 200)
- data = parse_json(resp)
- self.assertRegexpMatches(data['id'],
- '^i4x:\/\/MITx\/999\/chapter\/([0-9]|[a-f]){32}$')
-
- def check_edit_unit(self, test_course_name):
- import_from_xml(modulestore(), 'common/test/data/', [test_course_name])
-
- for descriptor in modulestore().get_items(Location(None, None, 'vertical', None, None)):
- print "Checking ", descriptor.location.url()
- print descriptor.__class__, descriptor.location
- resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
- self.assertEqual(resp.status_code, 200)
-
- def test_edit_unit_toy(self):
- self.check_edit_unit('toy')
-
- def test_edit_unit_full(self):
- self.check_edit_unit('full')
-
- def test_static_tab_reordering(self):
- import_from_xml(modulestore(), 'common/test/data/', ['full'])
-
- ms = modulestore('direct')
- course = ms.get_item(Location(['i4x','edX','full','course','6.002_Spring_2012', None]))
-
- # reverse the ordering
- reverse_tabs = []
- for tab in course.tabs:
- if tab['type'] == 'static_tab':
- reverse_tabs.insert(0, 'i4x://edX/full/static_tab/{0}'.format(tab['url_slug']))
-
- resp = self.client.post(reverse('reorder_static_tabs'), json.dumps({'tabs':reverse_tabs}), "application/json")
-
- course = ms.get_item(Location(['i4x','edX','full','course','6.002_Spring_2012', None]))
-
- # compare to make sure that the tabs information is in the expected order after the server call
- course_tabs = []
- for tab in course.tabs:
- if tab['type'] == 'static_tab':
- course_tabs.append('i4x://edX/full/static_tab/{0}'.format(tab['url_slug']))
-
- self.assertEqual(reverse_tabs, course_tabs)
-
-
-
-
- def test_about_overrides(self):
- '''
- This test case verifies that a course can use specialized override for about data, e.g. /about/Fall_2012/effort.html
- while there is a base definition in /about/effort.html
- '''
- import_from_xml(modulestore(), 'common/test/data/', ['full'])
- ms = modulestore('direct')
- effort = ms.get_item(Location(['i4x','edX','full','about','effort', None]))
- self.assertEqual(effort.definition['data'],'6 hours')
-
- # this one should be in a non-override folder
- effort = ms.get_item(Location(['i4x','edX','full','about','end_date', None]))
- self.assertEqual(effort.definition['data'],'TBD')
-
- def test_remove_hide_progress_tab(self):
- import_from_xml(modulestore(), 'common/test/data/', ['full'])
-
- ms = modulestore('direct')
- cs = contentstore()
-
- source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
- course = ms.get_item(source_location)
- self.assertNotIn('hide_progress_tab', course.metadata)
-
-
- def test_clone_course(self):
- import_from_xml(modulestore(), 'common/test/data/', ['full'])
-
- resp = self.client.post(reverse('create_new_course'), self.course_data)
- self.assertEqual(resp.status_code, 200)
- data = parse_json(resp)
- self.assertEqual(data['id'], 'i4x://MITx/999/course/Robot_Super_Course')
-
- ms = modulestore('direct')
- cs = contentstore()
-
- source_location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
- dest_location = CourseDescriptor.id_to_location('MITx/999/Robot_Super_Course')
-
- clone_course(ms, cs, source_location, dest_location)
-
- # now loop through all the units in the course and verify that the clone can render them, which
- # means the objects are at least present
- items = ms.get_items(Location(['i4x','edX', 'full', 'vertical', None]))
- self.assertGreater(len(items), 0)
- clone_items = ms.get_items(Location(['i4x', 'MITx','999','vertical', None]))
- self.assertGreater(len(clone_items), 0)
- for descriptor in items:
- new_loc = descriptor.location._replace(org = 'MITx', course='999')
- print "Checking {0} should now also be at {1}".format(descriptor.location.url(), new_loc.url())
- resp = self.client.get(reverse('edit_unit', kwargs={'location': new_loc.url()}))
- self.assertEqual(resp.status_code, 200)
-
- def test_delete_course(self):
- import_from_xml(modulestore(), 'common/test/data/', ['full'])
-
- ms = modulestore('direct')
- cs = contentstore()
-
- location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
-
- delete_course(ms, cs, location)
-
- items = ms.get_items(Location(['i4x','edX', 'full', 'vertical', None]))
- self.assertEqual(len(items), 0)
-
- def verify_content_existence(self, modulestore, root_dir, location, dirname, category_name, filename_suffix=''):
- fs = OSFS(root_dir / 'test_export')
- self.assertTrue(fs.exists(dirname))
-
- query_loc = Location('i4x', location.org, location.course, category_name, None)
- items = modulestore.get_items(query_loc)
-
- for item in items:
- fs = OSFS(root_dir / ('test_export/' + dirname))
- self.assertTrue(fs.exists(item.location.name + filename_suffix))
-
- def test_export_course(self):
- ms = modulestore('direct')
- cs = contentstore()
-
- import_from_xml(ms, 'common/test/data/', ['full'])
- location = CourseDescriptor.id_to_location('edX/full/6.002_Spring_2012')
-
- root_dir = path(mkdtemp())
-
- print 'Exporting to tempdir = {0}'.format(root_dir)
-
- # export out to a tempdir
- export_to_xml(ms, cs, location, root_dir, 'test_export')
-
- # check for static tabs
- self.verify_content_existence(ms, root_dir, location, 'tabs', 'static_tab', '.html')
-
- # check for custom_tags
- self.verify_content_existence(ms, root_dir, location, 'info', 'course_info', '.html')
-
- # check for custom_tags
- self.verify_content_existence(ms, root_dir, location, 'custom_tags', 'custom_tag_template')
-
-
- # remove old course
- delete_course(ms, cs, location)
-
- # reimport
- import_from_xml(ms, root_dir, ['test_export'])
-
- items = ms.get_items(Location(['i4x','edX', 'full', 'vertical', None]))
- self.assertGreater(len(items), 0)
- for descriptor in items:
- print "Checking {0}....".format(descriptor.location.url())
- resp = self.client.get(reverse('edit_unit', kwargs={'location': descriptor.location.url()}))
- self.assertEqual(resp.status_code, 200)
-
- shutil.rmtree(root_dir)
-
-
- def test_course_handouts_rewrites(self):
- ms = modulestore('direct')
- cs = contentstore()
-
- # import a test course
- import_from_xml(ms, 'common/test/data/', ['full'])
-
- handout_location= Location(['i4x', 'edX', 'full', 'course_info', 'handouts'])
-
- # get module info
- resp = self.client.get(reverse('module_info', kwargs={'module_location': handout_location}))
-
- # make sure we got a successful response
- self.assertEqual(resp.status_code, 200)
-
- # check that /static/ has been converted to the full path
- # note, we know the link it should be because that's what in the 'full' course in the test data
- self.assertContains(resp, '/c4x/edX/full/asset/handouts_schematic_tutorial.pdf')
-
- def test_missing_static_content(self):
- resp = self.client.get("/c4x/asd/asd/asd/asd")
- self.assertEqual(resp.status_code, 404)
-
- def test_capa_module(self):
- """Test that a problem treats markdown specially."""
- CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
-
- problem_data = {
- 'parent_location' : 'i4x://MITx/999/course/Robot_Super_Course',
- 'template' : 'i4x://edx/templates/problem/Empty'
- }
-
- resp = self.client.post(reverse('clone_item'), problem_data)
-
- self.assertEqual(resp.status_code, 200)
- payload = parse_json(resp)
- problem_loc = payload['id']
- problem = get_modulestore(problem_loc).get_item(problem_loc)
- # should be a CapaDescriptor
- self.assertIsInstance(problem, CapaDescriptor, "New problem is not a CapaDescriptor")
- context = problem.get_context()
- self.assertIn('markdown', context, "markdown is missing from context")
- self.assertIn('markdown', problem.metadata, "markdown is missing from metadata")
- self.assertNotIn('markdown', problem.editable_metadata_fields, "Markdown slipped into the editable metadata fields")
\ No newline at end of file
diff --git a/cms/djangoapps/contentstore/tests/utils.py b/cms/djangoapps/contentstore/tests/utils.py
new file mode 100644
index 0000000000..7fa5b76685
--- /dev/null
+++ b/cms/djangoapps/contentstore/tests/utils.py
@@ -0,0 +1,63 @@
+import json
+import copy
+from time import time
+from django.test import TestCase
+from override_settings import override_settings
+from django.conf import settings
+
+from student.models import Registration
+from django.contrib.auth.models import User
+
+import xmodule.modulestore.django
+from xmodule.templates import update_templates
+
+class ModuleStoreTestCase(TestCase):
+ """ Subclass for any test case that uses the mongodb
+ module store. This populates a uniquely named modulestore
+ collection with templates before running the TestCase
+ and drops it they are finished. """
+
+ def _pre_setup(self):
+ super(ModuleStoreTestCase, self)._pre_setup()
+
+ # Use the current seconds since epoch to differentiate
+ # the mongo collections on jenkins.
+ sec_since_epoch = '%s' % int(time()*100)
+ self.orig_MODULESTORE = copy.deepcopy(settings.MODULESTORE)
+ self.test_MODULESTORE = self.orig_MODULESTORE
+ self.test_MODULESTORE['default']['OPTIONS']['collection'] = 'modulestore_%s' % sec_since_epoch
+ self.test_MODULESTORE['direct']['OPTIONS']['collection'] = 'modulestore_%s' % sec_since_epoch
+ settings.MODULESTORE = self.test_MODULESTORE
+
+ # Flush and initialize the module store
+ # It needs the templates because it creates new records
+ # by cloning from the template.
+ # Note that if your test module gets in some weird state
+ # (though it shouldn't), do this manually
+ # from the bash shell to drop it:
+ # $ mongo test_xmodule --eval "db.dropDatabase()"
+ xmodule.modulestore.django._MODULESTORES = {}
+ update_templates()
+
+ def _post_teardown(self):
+ # Make sure you flush out the modulestore.
+ # Drop the collection at the end of the test,
+ # otherwise there will be lingering collections leftover
+ # from executing the tests.
+ xmodule.modulestore.django._MODULESTORES = {}
+ xmodule.modulestore.django.modulestore().collection.drop()
+ settings.MODULESTORE = self.orig_MODULESTORE
+
+ super(ModuleStoreTestCase, self)._post_teardown()
+
+def parse_json(response):
+ """Parse response, which is assumed to be json"""
+ return json.loads(response.content)
+
+def user(email):
+ """look up a user by email"""
+ return User.objects.get(email=email)
+
+def registration(email):
+ """look up registration object by email"""
+ return Registration.objects.get(user__email=email)
diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
index 7ebb2648ec..fb63cb34ed 100644
--- a/cms/djangoapps/contentstore/views.py
+++ b/cms/djangoapps/contentstore/views.py
@@ -1240,6 +1240,11 @@ def edge(request):
@login_required
@expect_json
def create_new_course(request):
+ # This logic is repeated in xmodule/modulestore/tests/factories.py
+ # so if you change anything here, you need to also change it there.
+ # TODO: write a test that creates two courses, one with the factory and
+ # the other with this method, then compare them to make sure they are
+ # equivalent.
template = Location(request.POST['template'])
org = request.POST.get('org')
number = request.POST.get('number')
@@ -1289,8 +1294,11 @@ def initialize_course_tabs(course):
# at least a list populated with the minimal times
# @TODO: I don't like the fact that the presentation tier is away of these data related constraints, let's find a better
# place for this. Also rather than using a simple list of dictionaries a nice class model would be helpful here
- course.tabs = [{"type": "courseware"},
- {"type": "course_info", "name": "Course Info"},
+
+ # This logic is repeated in xmodule/modulestore/tests/factories.py
+ # so if you change anything here, you need to also change it there.
+ course.tabs = [{"type": "courseware"},
+ {"type": "course_info", "name": "Course Info"},
{"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"},
{"type": "progress", "name": "Progress"}]
diff --git a/cms/envs/test.py b/cms/envs/test.py
index d9a2597cbb..74c3e349a4 100644
--- a/cms/envs/test.py
+++ b/cms/envs/test.py
@@ -11,7 +11,6 @@ from .common import *
import os
from path import path
-
# Nose Test Runner
INSTALLED_APPS += ('django_nose',)
NOSE_ARGS = ['--with-xunit']
@@ -72,17 +71,6 @@ DATABASES = {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ENV_ROOT / "db" / "cms.db",
},
-
- # The following are for testing purposes...
- 'edX/toy/2012_Fall': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': ENV_ROOT / "db" / "course1.db",
- },
-
- 'edx/full/6.002_Spring_2012': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': ENV_ROOT / "db" / "course2.db",
- }
}
LMS_BASE = "localhost:8000"
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/factories.py b/common/lib/xmodule/xmodule/modulestore/tests/factories.py
new file mode 100644
index 0000000000..b4264b30c9
--- /dev/null
+++ b/common/lib/xmodule/xmodule/modulestore/tests/factories.py
@@ -0,0 +1,118 @@
+from factory import Factory
+from time import gmtime
+from uuid import uuid4
+from xmodule.modulestore import Location
+from xmodule.modulestore.django import modulestore
+from xmodule.timeparse import stringify_time
+
+def XMODULE_COURSE_CREATION(class_to_create, **kwargs):
+ return XModuleCourseFactory._create(class_to_create, **kwargs)
+
+def XMODULE_ITEM_CREATION(class_to_create, **kwargs):
+ return XModuleItemFactory._create(class_to_create, **kwargs)
+
+class XModuleCourseFactory(Factory):
+ """
+ Factory for XModule courses.
+ """
+
+ ABSTRACT_FACTORY = True
+ _creation_function = (XMODULE_COURSE_CREATION,)
+
+ @classmethod
+ def _create(cls, target_class, *args, **kwargs):
+ # This logic was taken from the create_new_course method in
+ # cms/djangoapps/contentstore/views.py
+ template = Location('i4x', 'edx', 'templates', 'course', 'Empty')
+ org = kwargs.get('org')
+ number = kwargs.get('number')
+ display_name = kwargs.get('display_name')
+ location = Location('i4x', org, number,
+ 'course', Location.clean(display_name))
+
+ store = modulestore('direct')
+
+ # Write the data to the mongo datastore
+ new_course = store.clone_item(template, location)
+
+ # This metadata code was copied from cms/djangoapps/contentstore/views.py
+ if display_name is not None:
+ new_course.metadata['display_name'] = display_name
+
+ new_course.metadata['data_dir'] = uuid4().hex
+ new_course.metadata['start'] = stringify_time(gmtime())
+
+ new_course.tabs = [{"type": "courseware"},
+ {"type": "course_info", "name": "Course Info"},
+ {"type": "discussion", "name": "Discussion"},
+ {"type": "wiki", "name": "Wiki"},
+ {"type": "progress", "name": "Progress"}]
+
+ # Update the data in the mongo datastore
+ store.update_metadata(new_course.location.url(), new_course.own_metadata)
+
+ return new_course
+
+class Course:
+ pass
+
+class CourseFactory(XModuleCourseFactory):
+ FACTORY_FOR = Course
+
+ template = 'i4x://edx/templates/course/Empty'
+ org = 'MITx'
+ number = '999'
+ display_name = 'Robot Super Course'
+
+class XModuleItemFactory(Factory):
+ """
+ Factory for XModule items.
+ """
+
+ ABSTRACT_FACTORY = True
+ _creation_function = (XMODULE_ITEM_CREATION,)
+
+ @classmethod
+ def _create(cls, target_class, *args, **kwargs):
+ """
+ kwargs must include parent_location, template. Can contain display_name
+ target_class is ignored
+ """
+
+ DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
+
+ parent_location = Location(kwargs.get('parent_location'))
+ template = Location(kwargs.get('template'))
+ display_name = kwargs.get('display_name')
+
+ store = modulestore('direct')
+
+ # This code was based off that in cms/djangoapps/contentstore/views.py
+ parent = store.get_item(parent_location)
+ dest_location = parent_location._replace(category=template.category, name=uuid4().hex)
+
+ new_item = store.clone_item(template, dest_location)
+
+ # TODO: This needs to be deleted when we have proper storage for static content
+ new_item.metadata['data_dir'] = parent.metadata['data_dir']
+
+ # replace the display name with an optional parameter passed in from the caller
+ if display_name is not None:
+ new_item.metadata['display_name'] = display_name
+
+ store.update_metadata(new_item.location.url(), new_item.own_metadata)
+
+ if new_item.location.category not in DETACHED_CATEGORIES:
+ store.update_children(parent_location, parent.definition.get('children', []) + [new_item.location.url()])
+
+ return new_item
+
+class Item:
+ pass
+
+class ItemFactory(XModuleItemFactory):
+ FACTORY_FOR = Item
+
+ parent_location = 'i4x://MITx/999/course/Robot_Super_Course'
+ template = 'i4x://edx/templates/chapter/Empty'
+ display_name = 'Section One'