diff --git a/cms/djangoapps/contentstore/tests/tests.py b/cms/djangoapps/contentstore/tests/tests.py
index ade7c4e75d..6b346046fc 100644
--- a/cms/djangoapps/contentstore/tests/tests.py
+++ b/cms/djangoapps/contentstore/tests/tests.py
@@ -1,13 +1,19 @@
import json
-from django.test.client import Client
from django.test import TestCase
+from django.test.client import Client
from mock import patch, Mock
from override_settings import override_settings
from django.conf import settings
from django.core.urlresolvers import reverse
+from path import path
from student.models import Registration
from django.contrib.auth.models import User
+from xmodule.modulestore.django import modulestore
+import xmodule.modulestore.django
+from xmodule.modulestore import Location
+from contentstore import import_from_xml
+import copy
def parse_json(response):
@@ -25,36 +31,21 @@ def registration(email):
return Registration.objects.get(user__email=email)
-class AuthTestCase(TestCase):
- """Check that various permissions-related things work"""
-
- def setUp(self):
- self.email = 'a@b.com'
- self.pw = 'xyz'
- self.username = 'testuser'
- self.client = Client()
-
- def check_page_get(self, url, expected):
- resp = self.client.get(url)
- self.assertEqual(resp.status_code, expected)
+class ContentStoreTestCase(TestCase):
+ def _login(self, email, pw):
+ '''Login. View should always return 200. The success/fail is in the
+ returned json'''
+ resp = self.client.post(reverse('login_post'),
+ {'email': email, 'password': pw})
+ self.assertEqual(resp.status_code, 200)
return resp
- def test_public_pages_load(self):
- """Make sure pages that don't require login load without error."""
- pages = (
- reverse('login'),
- reverse('signup'),
- )
- for page in pages:
- print "Checking '{0}'".format(page)
- self.check_page_get(page, 200)
-
- def test_create_account_errors(self):
- # No post data -- should fail
- resp = self.client.post('/create_account', {})
- self.assertEqual(resp.status_code, 200)
+ def login(self, email, pw):
+ '''Login, check that it worked.'''
+ resp = self._login(email, pw)
data = parse_json(resp)
- self.assertEqual(data['success'], False)
+ self.assertTrue(data['success'])
+ return resp
def _create_account(self, username, email, pw):
'''Try to create an account. No error checking'''
@@ -78,7 +69,7 @@ class AuthTestCase(TestCase):
self.assertEqual(data['success'], True)
# Check both that the user is created, and inactive
- self.assertFalse(user(self.email).is_active)
+ self.assertFalse(user(email).is_active)
return resp
@@ -95,27 +86,44 @@ class AuthTestCase(TestCase):
resp = self._activate_user(email)
self.assertEqual(resp.status_code, 200)
# Now make sure that the user is now actually activated
- self.assertTrue(user(self.email).is_active)
+ self.assertTrue(user(email).is_active)
+
+
+class AuthTestCase(ContentStoreTestCase):
+ """Check that various permissions-related things work"""
+
+ def setUp(self):
+ self.email = 'a@b.com'
+ self.pw = 'xyz'
+ self.username = 'testuser'
+ self.client = Client()
+
+ def check_page_get(self, url, expected):
+ resp = self.client.get(url)
+ self.assertEqual(resp.status_code, expected)
+ return resp
+
+ def test_public_pages_load(self):
+ """Make sure pages that don't require login load without error."""
+ pages = (
+ reverse('login'),
+ reverse('signup'),
+ )
+ for page in pages:
+ print "Checking '{0}'".format(page)
+ self.check_page_get(page, 200)
+
+ def test_create_account_errors(self):
+ # No post data -- should fail
+ resp = self.client.post('/create_account', {})
+ self.assertEqual(resp.status_code, 200)
+ data = parse_json(resp)
+ self.assertEqual(data['success'], False)
def test_create_account(self):
self.create_account(self.username, self.email, self.pw)
self.activate_user(self.email)
- def _login(self, email, pw):
- '''Login. View should always return 200. The success/fail is in the
- returned json'''
- resp = self.client.post(reverse('login_post'),
- {'email': email, 'password': pw})
- self.assertEqual(resp.status_code, 200)
- return resp
-
- def login(self, email, pw):
- '''Login, check that it worked.'''
- resp = self._login(self.email, self.pw)
- data = parse_json(resp)
- self.assertTrue(data['success'])
- return resp
-
def test_login(self):
self.create_account(self.username, self.email, self.pw)
@@ -170,3 +178,30 @@ class AuthTestCase(TestCase):
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')
+
+@override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
+class EditTestCase(ContentStoreTestCase):
+ """Check that editing functionality works on example courses"""
+
+ def setUp(self):
+ email = 'edit@test.com'
+ password = 'foo'
+ self.create_account('edittest', email, password)
+ self.activate_user(email)
+ self.login(email, password)
+ xmodule.modulestore.django._MODULESTORES = {}
+
+ def check_edit_item(self, test_course_name):
+ import_from_xml('common/test/data/', test_course_name)
+
+ for descriptor in modulestore().get_items(Location(None, None, None, None, None)):
+ print "Checking ", descriptor.location.url()
+ print descriptor.__class__, descriptor.location
+ resp = self.client.get(reverse('edit_item'), {'id': descriptor.location.url()})
+ self.assertEqual(resp.status_code, 200)
+
+ def test_edit_item_toy(self):
+ self.check_edit_item('toy')
diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
index 2bec0272fa..a79aa0e18f 100644
--- a/cms/djangoapps/contentstore/views.py
+++ b/cms/djangoapps/contentstore/views.py
@@ -92,7 +92,6 @@ def edit_item(request):
"""
# TODO (vshnayder): change name from id to location in coffee+html as well.
item_location = request.GET['id']
- print item_location, request.GET
if not has_access(request.user, item_location):
raise Http404 # TODO (vshnayder): better error
diff --git a/cms/djangoapps/github_sync/__init__.py b/cms/djangoapps/github_sync/__init__.py
index 108fd5629f..149b92670a 100644
--- a/cms/djangoapps/github_sync/__init__.py
+++ b/cms/djangoapps/github_sync/__init__.py
@@ -13,16 +13,17 @@ from .exceptions import GithubSyncError
log = logging.getLogger(__name__)
-def import_from_github(repo_settings):
+def setup_repo(repo_settings):
"""
- Imports data into the modulestore based on the XML stored on github
+ Reset the local github repo specified by repo_settings
repo_settings is a dictionary with the following keys:
path: file system path to the local git repo
branch: name of the branch to track on github
+ origin: git url for the repository to track
"""
- repo_path = repo_settings['path']
- data_dir, course_dir = os.path.split(repo_path)
+ course_dir = repo_settings['path']
+ repo_path = settings.GITHUB_REPO_ROOT / course_dir
if not os.path.isdir(repo_path):
Repo.clone_from(repo_settings['origin'], repo_path)
@@ -33,8 +34,29 @@ def import_from_github(repo_settings):
# Do a hard reset to the remote branch so that we have a clean import
git_repo.git.checkout(repo_settings['branch'])
+
+ return git_repo
+
+
+def load_repo_settings(course_dir):
+ """
+ Returns the repo_settings for the course stored in course_dir
+ """
+ for repo_settings in settings.REPOS.values():
+ if repo_settings['path'] == course_dir:
+ return repo_settings
+ raise InvalidRepo(course_dir)
+
+
+def import_from_github(repo_settings):
+ """
+ Imports data into the modulestore based on the XML stored on github
+ """
+ course_dir = repo_settings['path']
+ git_repo = setup_repo(repo_settings)
git_repo.head.reset('origin/%s' % repo_settings['branch'], index=True, working_tree=True)
- module_store = import_from_xml(data_dir, course_dirs=[course_dir])
+
+ module_store = import_from_xml(settings.GITHUB_REPO_ROOT, course_dirs=[course_dir])
return git_repo.head.commit.hexsha, module_store.courses[course_dir]
@@ -44,14 +66,16 @@ def export_to_github(course, commit_message, author_str=None):
and push to github (if MITX_FEATURES['GITHUB_PUSH'] is True).
If author_str is specified, uses it in the commit.
'''
- repo_path = settings.DATA_DIR / course.metadata.get('data_dir', course.location.course)
- fs = OSFS(repo_path)
+ course_dir = course.metadata.get('data_dir', course.location.course)
+ repo_settings = load_repo_settings(course_dir)
+ git_repo = setup_repo(repo_settings)
+
+ fs = OSFS(git_repo.working_dir)
xml = course.export_to_xml(fs)
with fs.open('course.xml', 'w') as course_xml:
course_xml.write(xml)
- git_repo = Repo(repo_path)
if git_repo.is_dirty():
git_repo.git.add(A=True)
if author_str is not None:
diff --git a/cms/djangoapps/github_sync/tests/__init__.py b/cms/djangoapps/github_sync/tests/__init__.py
index 452904ffff..c95d538030 100644
--- a/cms/djangoapps/github_sync/tests/__init__.py
+++ b/cms/djangoapps/github_sync/tests/__init__.py
@@ -10,36 +10,37 @@ from xmodule.modulestore import Location
from override_settings import override_settings
from github_sync.exceptions import GithubSyncError
+REPO_DIR = settings.GITHUB_REPO_ROOT / 'local_repo'
+WORKING_DIR = path(settings.TEST_ROOT)
+REMOTE_DIR = WORKING_DIR / 'remote_repo'
-@override_settings(DATA_DIR=path('test_root'))
+
+@override_settings(REPOS={
+ 'local': {
+ 'path': 'local_repo',
+ 'origin': REMOTE_DIR,
+ 'branch': 'master',
+ }
+})
class GithubSyncTestCase(TestCase):
def cleanup(self):
- shutil.rmtree(self.repo_dir, ignore_errors=True)
- shutil.rmtree(self.remote_dir, ignore_errors=True)
+ shutil.rmtree(REPO_DIR, ignore_errors=True)
+ shutil.rmtree(REMOTE_DIR, ignore_errors=True)
+ modulestore().collection.drop()
def setUp(self):
- self.working_dir = path(settings.TEST_ROOT)
- self.repo_dir = self.working_dir / 'local_repo'
- self.remote_dir = self.working_dir / 'remote_repo'
-
# make sure there's no stale data lying around
self.cleanup()
- shutil.copytree('common/test/data/toy', self.remote_dir)
+ shutil.copytree('common/test/data/toy', REMOTE_DIR)
- remote = Repo.init(self.remote_dir)
+ remote = Repo.init(REMOTE_DIR)
remote.git.add(A=True)
remote.git.commit(m='Initial commit')
remote.git.config("receive.denyCurrentBranch", "ignore")
- modulestore().collection.drop()
-
- self.import_revision, self.import_course = import_from_github({
- 'path': self.repo_dir,
- 'origin': self.remote_dir,
- 'branch': 'master',
- })
+ self.import_revision, self.import_course = import_from_github(settings.REPOS['local'])
def tearDown(self):
self.cleanup()
@@ -48,7 +49,7 @@ class GithubSyncTestCase(TestCase):
"""
Test that importing from github will create a repo if the repo doesn't already exist
"""
- self.assertEquals(1, len(Repo(self.repo_dir).head.reference.log()))
+ self.assertEquals(1, len(Repo(REPO_DIR).head.reference.log()))
def test_import_contents(self):
"""
@@ -66,7 +67,7 @@ class GithubSyncTestCase(TestCase):
Test that with the GITHUB_PUSH feature disabled, no content is pushed to the remote
"""
export_to_github(self.import_course, 'Test no-push')
- self.assertEquals(1, Repo(self.remote_dir).head.commit.count())
+ self.assertEquals(1, Repo(REMOTE_DIR).head.commit.count())
@override_settings(MITX_FEATURES={'GITHUB_PUSH': True})
def test_export_push(self):
@@ -75,7 +76,7 @@ class GithubSyncTestCase(TestCase):
"""
self.import_course.metadata['display_name'] = 'Changed display name'
export_to_github(self.import_course, 'Test push')
- self.assertEquals(2, Repo(self.remote_dir).head.commit.count())
+ self.assertEquals(2, Repo(REMOTE_DIR).head.commit.count())
@override_settings(MITX_FEATURES={'GITHUB_PUSH': True})
def test_export_conflict(self):
@@ -84,7 +85,7 @@ class GithubSyncTestCase(TestCase):
"""
self.import_course.metadata['display_name'] = 'Changed display name'
- remote = Repo(self.remote_dir)
+ remote = Repo(REMOTE_DIR)
remote.git.commit(allow_empty=True, m="Testing conflict commit")
self.assertRaises(GithubSyncError, export_to_github, self.import_course, 'Test push')
diff --git a/cms/envs/common.py b/cms/envs/common.py
index 5fb0c82bb7..7b1f6e3fcd 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -44,10 +44,8 @@ PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /mitx/cms
REPO_ROOT = PROJECT_ROOT.dirname()
COMMON_ROOT = REPO_ROOT / "common"
ENV_ROOT = REPO_ROOT.dirname() # virtualenv dir /mitx is in
-COURSES_ROOT = ENV_ROOT / "data"
-# FIXME: To support multiple courses, we should walk the courses dir at startup
-DATA_DIR = COURSES_ROOT
+GITHUB_REPO_ROOT = ENV_ROOT / "data"
sys.path.append(REPO_ROOT)
sys.path.append(PROJECT_ROOT / 'djangoapps')
diff --git a/cms/envs/dev.py b/cms/envs/dev.py
index 40139a8a22..dd12ce5770 100644
--- a/cms/envs/dev.py
+++ b/cms/envs/dev.py
@@ -18,6 +18,7 @@ MODULESTORE = {
'host': 'localhost',
'db': 'xmodule',
'collection': 'modulestore',
+ 'fs_root': GITHUB_REPO_ROOT,
}
}
}
@@ -31,35 +32,35 @@ DATABASES = {
REPOS = {
'edx4edx': {
- 'path': DATA_DIR / "edx4edx",
+ 'path': "edx4edx",
'org': 'edx',
'course': 'edx4edx',
'branch': 'for_cms',
'origin': 'git@github.com:MITx/edx4edx.git',
},
'6002x-fall-2012': {
- 'path': DATA_DIR / '6002x-fall-2012',
+ 'path': '6002x-fall-2012',
'org': 'mit.edu',
'course': '6.002x',
'branch': 'for_cms',
'origin': 'git@github.com:MITx/6002x-fall-2012.git',
},
'6.00x': {
- 'path': DATA_DIR / '6.00x',
+ 'path': '6.00x',
'org': 'mit.edu',
'course': '6.00x',
'branch': 'for_cms',
'origin': 'git@github.com:MITx/6.00x.git',
},
'7.00x': {
- 'path': DATA_DIR / '7.00x',
+ 'path': '7.00x',
'org': 'mit.edu',
'course': '7.00x',
'branch': 'for_cms',
'origin': 'git@github.com:MITx/7.00x.git',
},
'3.091x': {
- 'path': DATA_DIR / '3.091x',
+ 'path': '3.091x',
'org': 'mit.edu',
'course': '3.091x',
'branch': 'for_cms',
diff --git a/cms/envs/test.py b/cms/envs/test.py
index 2a867af91f..28dce36682 100644
--- a/cms/envs/test.py
+++ b/cms/envs/test.py
@@ -24,6 +24,7 @@ TEST_ROOT = path('test_root')
# Want static files in the same dir for running on jenkins.
STATIC_ROOT = TEST_ROOT / "staticfiles"
+GITHUB_REPO_ROOT = TEST_ROOT / "data"
MODULESTORE = {
'default': {
@@ -33,6 +34,7 @@ MODULESTORE = {
'host': 'localhost',
'db': 'test_xmodule',
'collection': 'modulestore',
+ 'fs_root': GITHUB_REPO_ROOT,
}
}
}
diff --git a/common/lib/xmodule/xmodule/modulestore/__init__.py b/common/lib/xmodule/xmodule/modulestore/__init__.py
index 30010a09a7..0390c314ab 100644
--- a/common/lib/xmodule/xmodule/modulestore/__init__.py
+++ b/common/lib/xmodule/xmodule/modulestore/__init__.py
@@ -45,7 +45,7 @@ class Location(_LocationBase):
"""
return re.sub('_+', '_', INVALID_CHARS.sub('_', value))
- def __new__(_cls, loc_or_tag, org=None, course=None, category=None, name=None, revision=None):
+ def __new__(_cls, loc_or_tag=None, org=None, course=None, category=None, name=None, revision=None):
"""
Create a new location that is a clone of the specifed one.
@@ -70,11 +70,15 @@ class Location(_LocationBase):
wildcard selection
"""
+
if org is None and course is None and category is None and name is None and revision is None:
location = loc_or_tag
else:
location = (loc_or_tag, org, course, category, name, revision)
+ if location is None:
+ return _LocationBase.__new__(_cls, *([None] * 6))
+
def check_dict(dict_):
check_list(dict_.itervalues())
diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py
index 7ebee98c16..63c955f6da 100644
--- a/common/lib/xmodule/xmodule/modulestore/mongo.py
+++ b/common/lib/xmodule/xmodule/modulestore/mongo.py
@@ -6,6 +6,7 @@ from xmodule.mako_module import MakoDescriptorSystem
from mitxmako.shortcuts import render_to_string
from bson.son import SON
from itertools import repeat
+from fs.osfs import OSFS
from . import ModuleStore, Location
from .exceptions import ItemNotFoundError, InsufficientSpecificationError
@@ -61,7 +62,9 @@ class MongoModuleStore(ModuleStore):
"""
A Mongodb backed ModuleStore
"""
- def __init__(self, host, db, collection, port=27017, default_class=None):
+
+ # TODO (cpennington): Enable non-filesystem filestores
+ def __init__(self, host, db, collection, fs_root, port=27017, default_class=None):
self.collection = pymongo.connection.Connection(
host=host,
port=port
@@ -77,6 +80,7 @@ class MongoModuleStore(ModuleStore):
module_path, _, class_name = default_class.rpartition('.')
class_ = getattr(import_module(module_path), class_name)
self.default_class = class_
+ self.fs_root = fs_root
def _clean_item_data(self, item):
"""
@@ -113,13 +117,27 @@ class MongoModuleStore(ModuleStore):
return data
+ def _load_item(self, item, data_cache):
+ """
+ Load an XModuleDescriptor from item, using the children stored in data_cache
+ """
+ resource_fs = OSFS(self.fs_root / item.get('data_dir', item['location']['course']))
+ system = CachingDescriptorSystem(
+ self,
+ data_cache,
+ self.default_class,
+ resource_fs,
+ render_to_string
+ )
+ return system.load_item(item['location'])
+
def _load_items(self, items, depth=0):
"""
Load a list of xmodules from the data in items, with children cached up to specified depth
"""
data_cache = self._cache_children(items, depth)
- system = CachingDescriptorSystem(self, data_cache, self.default_class, None, render_to_string)
- return [system.load_item(item['location']) for item in items]
+
+ return [self._load_item(item, data_cache) for item in items]
def get_item(self, location, depth=0):
"""
diff --git a/common/test/data/toy/course.xml b/common/test/data/toy/course.xml
index ecac9a4776..645fbd7af8 100644
--- a/common/test/data/toy/course.xml
+++ b/common/test/data/toy/course.xml
@@ -1,11 +1,9 @@
-
-
-
-
-
+
+
+
-
+
diff --git a/common/test/data/toy/html/Lab2A.html b/common/test/data/toy/html/Lab2A.html
new file mode 100644
index 0000000000..7fe52cc1be
--- /dev/null
+++ b/common/test/data/toy/html/Lab2A.html
@@ -0,0 +1,105 @@
+
+
+Lab 2A: Superposition Experiment
+
+
Note: This part of the lab is just to develop your intuition about
+superposition. There are no responses that need to be checked.
+
+
Circuits with multiple sources can be hard to analyze as-is. For example, what is the voltage
+between the two terminals on the right of Figure 1?
+
+
+
+Figure 1. Example multi-source circuit
+
+
+
We can use superposition to make the analysis much easier.
+The circuit in Figure 1 can be decomposed into two separate
+subcircuits: one involving only the voltage source and one involving only the
+current source. We'll analyze each circuit separately and combine the
+results using superposition. Recall that to decompose a circuit for
+analysis, we'll pick each source in turn and set all the other sources
+to zero (i.e., voltage sources become short circuits and current
+sources become open circuits). The circuit above has two sources, so
+the decomposition produces two subcircuits, as shown in Figure 2.
+
+
+
+
+(a) Subcircuit for analyzing contribution of voltage source
+
+
+(b) Subcircuit for analyzing contribution of current source
+
+ Figure 2. Decomposition of Figure 1 into subcircuits
+
+
+ Let's use the DC analysis capability of the schematic tool to see superposition
+in action. The sliders below control the resistances of R1, R2, R3 and R4 in all
+the diagrams. As you move the sliders, the schematic tool will adjust the appropriate
+resistance, perform a DC analysis and display the node voltages on the diagrams. Here's
+what you want to observe as you play with the sliders:
+
+
+The voltage for a node in Figure 1 is the sum of the voltages for
+that node in Figures 2(a) and 2(b), just as predicted by
+superposition. (Note that due to round-off in the display of the
+voltages, the sum of the displayed voltages in Figure 2 may only be within
+.01 of the voltages displayed in Figure 1.)
+