Merge branch 'master' of github.com:MITx/mitx
Conflicts: lms/djangoapps/courseware/module_render.py lms/djangoapps/courseware/views.py lms/envs/common.py
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "askbot"]
|
||||
path = askbot
|
||||
url = git@github.com:MITx/askbot-devel.git
|
||||
1
askbot
Submodule
@@ -1,34 +0,0 @@
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.xml import XMLModuleStore
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def import_from_xml(data_dir, course_dirs=None):
|
||||
"""
|
||||
Import the specified xml data_dir into the django defined modulestore,
|
||||
using org and course as the location org and course.
|
||||
"""
|
||||
module_store = XMLModuleStore(
|
||||
data_dir,
|
||||
default_class='xmodule.raw_module.RawDescriptor',
|
||||
eager=True,
|
||||
course_dirs=course_dirs
|
||||
)
|
||||
for module in module_store.modules.itervalues():
|
||||
|
||||
# TODO (cpennington): This forces import to overrite the same items.
|
||||
# This should in the future create new revisions of the items on import
|
||||
try:
|
||||
modulestore().create_item(module.location)
|
||||
except:
|
||||
log.exception('Item already exists at %s' % module.location.url())
|
||||
pass
|
||||
if 'data' in module.definition:
|
||||
modulestore().update_item(module.location, module.definition['data'])
|
||||
if 'children' in module.definition:
|
||||
modulestore().update_children(module.location, module.definition['children'])
|
||||
modulestore().update_metadata(module.location, dict(module.metadata))
|
||||
|
||||
return module_store
|
||||
@@ -1,9 +1,11 @@
|
||||
###
|
||||
### One-off script for importing courseware form XML format
|
||||
### Script for importing courseware from XML format
|
||||
###
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from contentstore import import_from_xml
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
|
||||
unnamed_modules = 0
|
||||
|
||||
@@ -21,4 +23,4 @@ class Command(BaseCommand):
|
||||
course_dirs = args[1:]
|
||||
else:
|
||||
course_dirs = None
|
||||
import_from_xml(data_dir, course_dirs)
|
||||
import_from_xml(modulestore(), data_dir, course_dirs)
|
||||
|
||||
@@ -12,7 +12,7 @@ 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
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
import copy
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ class ContentStoreTestCase(TestCase):
|
||||
return resp
|
||||
|
||||
def _activate_user(self, email):
|
||||
'''look up the user's activation key in the db, then hit the activate view.
|
||||
'''Look up the activation key for the user, then hit the activate view.
|
||||
No error checking'''
|
||||
activation_key = registration(email).activation_key
|
||||
|
||||
@@ -102,7 +102,7 @@ class AuthTestCase(ContentStoreTestCase):
|
||||
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 = (
|
||||
@@ -196,7 +196,7 @@ class EditTestCase(ContentStoreTestCase):
|
||||
xmodule.modulestore.django.modulestore().collection.drop()
|
||||
|
||||
def check_edit_item(self, test_course_name):
|
||||
import_from_xml('common/test/data/', test_course_name)
|
||||
import_from_xml(modulestore(), 'common/test/data/', [test_course_name])
|
||||
|
||||
for descriptor in modulestore().get_items(Location(None, None, None, None, None)):
|
||||
print "Checking ", descriptor.location.url()
|
||||
|
||||
@@ -5,35 +5,53 @@ from django.conf import settings
|
||||
from fs.osfs import OSFS
|
||||
from git import Repo, PushInfo
|
||||
|
||||
from contentstore import import_from_xml
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from collections import namedtuple
|
||||
|
||||
from .exceptions import GithubSyncError
|
||||
from .exceptions import GithubSyncError, InvalidRepo
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
RepoSettings = namedtuple('RepoSettings', 'path branch origin')
|
||||
|
||||
|
||||
def sync_all_with_github():
|
||||
"""
|
||||
Sync all defined repositories from github
|
||||
"""
|
||||
for repo_name in settings.REPOS:
|
||||
sync_with_github(load_repo_settings(repo_name))
|
||||
|
||||
|
||||
def sync_with_github(repo_settings):
|
||||
"""
|
||||
Sync specified repository from github
|
||||
|
||||
repo_settings: A RepoSettings defining which repo to sync
|
||||
"""
|
||||
revision, course = import_from_github(repo_settings)
|
||||
export_to_github(course, "Changes from cms import of revision %s" % revision, "CMS <cms@edx.org>")
|
||||
|
||||
|
||||
def setup_repo(repo_settings):
|
||||
"""
|
||||
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_settings (RepoSettings): The settings for the repo to reset
|
||||
"""
|
||||
course_dir = repo_settings['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)
|
||||
Repo.clone_from(repo_settings.origin, repo_path)
|
||||
|
||||
git_repo = Repo(repo_path)
|
||||
origin = git_repo.remotes.origin
|
||||
origin.fetch()
|
||||
|
||||
# Do a hard reset to the remote branch so that we have a clean import
|
||||
git_repo.git.checkout(repo_settings['branch'])
|
||||
git_repo.git.checkout(repo_settings.branch)
|
||||
|
||||
return git_repo
|
||||
|
||||
@@ -42,21 +60,22 @@ 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)
|
||||
if course_dir not in settings.REPOS:
|
||||
raise InvalidRepo(course_dir)
|
||||
|
||||
return RepoSettings(course_dir, **settings.REPOS[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']
|
||||
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)
|
||||
git_repo.head.reset('origin/%s' % repo_settings.branch, index=True, working_tree=True)
|
||||
|
||||
module_store = import_from_xml(settings.GITHUB_REPO_ROOT, course_dirs=[course_dir])
|
||||
module_store = import_from_xml(modulestore(),
|
||||
settings.GITHUB_REPO_ROOT, course_dirs=[course_dir])
|
||||
return git_repo.head.commit.hexsha, module_store.courses[course_dir]
|
||||
|
||||
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
class GithubSyncError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidRepo(Exception):
|
||||
pass
|
||||
|
||||
0
cms/djangoapps/github_sync/management/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
||||
###
|
||||
### Script for syncing CMS with defined github repos
|
||||
###
|
||||
|
||||
from django.core.management.base import NoArgsCommand
|
||||
from github_sync import sync_all_with_github
|
||||
|
||||
|
||||
class Command(NoArgsCommand):
|
||||
help = \
|
||||
'''Sync the CMS with the defined github repos'''
|
||||
|
||||
def handle_noargs(self, **options):
|
||||
sync_all_with_github()
|
||||
@@ -1,14 +1,17 @@
|
||||
from django.test import TestCase
|
||||
from path import path
|
||||
import shutil
|
||||
import os
|
||||
from github_sync import import_from_github, export_to_github
|
||||
from github_sync import (
|
||||
import_from_github, export_to_github, load_repo_settings,
|
||||
sync_all_with_github, sync_with_github
|
||||
)
|
||||
from git import Repo
|
||||
from django.conf import settings
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore import Location
|
||||
from override_settings import override_settings
|
||||
from github_sync.exceptions import GithubSyncError
|
||||
from mock import patch, Mock
|
||||
|
||||
REPO_DIR = settings.GITHUB_REPO_ROOT / 'local_repo'
|
||||
WORKING_DIR = path(settings.TEST_ROOT)
|
||||
@@ -16,8 +19,7 @@ REMOTE_DIR = WORKING_DIR / 'remote_repo'
|
||||
|
||||
|
||||
@override_settings(REPOS={
|
||||
'local': {
|
||||
'path': 'local_repo',
|
||||
'local_repo': {
|
||||
'origin': REMOTE_DIR,
|
||||
'branch': 'master',
|
||||
}
|
||||
@@ -40,7 +42,7 @@ class GithubSyncTestCase(TestCase):
|
||||
remote.git.commit(m='Initial commit')
|
||||
remote.git.config("receive.denyCurrentBranch", "ignore")
|
||||
|
||||
self.import_revision, self.import_course = import_from_github(settings.REPOS['local'])
|
||||
self.import_revision, self.import_course = import_from_github(load_repo_settings('local_repo'))
|
||||
|
||||
def tearDown(self):
|
||||
self.cleanup()
|
||||
@@ -57,10 +59,23 @@ class GithubSyncTestCase(TestCase):
|
||||
"""
|
||||
self.assertEquals('Toy Course', self.import_course.metadata['display_name'])
|
||||
self.assertIn(
|
||||
Location('i4x://edx/local_repo/chapter/Overview'),
|
||||
Location('i4x://edX/toy/chapter/Overview'),
|
||||
[child.location for child in self.import_course.get_children()])
|
||||
self.assertEquals(1, len(self.import_course.get_children()))
|
||||
|
||||
@patch('github_sync.sync_with_github')
|
||||
def test_sync_all_with_github(self, sync_with_github):
|
||||
sync_all_with_github()
|
||||
sync_with_github.assert_called_with(load_repo_settings('local_repo'))
|
||||
|
||||
def test_sync_with_github(self):
|
||||
with patch('github_sync.import_from_github', Mock(return_value=(Mock(), Mock()))) as import_from_github:
|
||||
with patch('github_sync.export_to_github') as export_to_github:
|
||||
settings = load_repo_settings('local_repo')
|
||||
sync_with_github(settings)
|
||||
import_from_github.assert_called_with(settings)
|
||||
export_to_github.assert_called
|
||||
|
||||
@override_settings(MITX_FEATURES={'GITHUB_PUSH': False})
|
||||
def test_export_no_pash(self):
|
||||
"""
|
||||
|
||||
@@ -1,52 +1,43 @@
|
||||
import json
|
||||
from django.test.client import Client
|
||||
from django.test import TestCase
|
||||
from mock import patch, Mock
|
||||
from mock import patch
|
||||
from override_settings import override_settings
|
||||
from django.conf import settings
|
||||
from github_sync import load_repo_settings
|
||||
|
||||
|
||||
@override_settings(REPOS={'repo': {'path': 'path', 'branch': 'branch'}})
|
||||
@override_settings(REPOS={'repo': {'branch': 'branch', 'origin': 'origin'}})
|
||||
class PostReceiveTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
|
||||
@patch('github_sync.views.export_to_github')
|
||||
@patch('github_sync.views.import_from_github')
|
||||
def test_non_branch(self, import_from_github, export_to_github):
|
||||
@patch('github_sync.views.sync_with_github')
|
||||
def test_non_branch(self, sync_with_github):
|
||||
self.client.post('/github_service_hook', {'payload': json.dumps({
|
||||
'ref': 'refs/tags/foo'})
|
||||
})
|
||||
self.assertFalse(import_from_github.called)
|
||||
self.assertFalse(export_to_github.called)
|
||||
self.assertFalse(sync_with_github.called)
|
||||
|
||||
@patch('github_sync.views.export_to_github')
|
||||
@patch('github_sync.views.import_from_github')
|
||||
def test_non_watched_repo(self, import_from_github, export_to_github):
|
||||
@patch('github_sync.views.sync_with_github')
|
||||
def test_non_watched_repo(self, sync_with_github):
|
||||
self.client.post('/github_service_hook', {'payload': json.dumps({
|
||||
'ref': 'refs/heads/branch',
|
||||
'repository': {'name': 'bad_repo'}})
|
||||
})
|
||||
self.assertFalse(import_from_github.called)
|
||||
self.assertFalse(export_to_github.called)
|
||||
self.assertFalse(sync_with_github.called)
|
||||
|
||||
@patch('github_sync.views.export_to_github')
|
||||
@patch('github_sync.views.import_from_github')
|
||||
def test_non_tracked_branch(self, import_from_github, export_to_github):
|
||||
@patch('github_sync.views.sync_with_github')
|
||||
def test_non_tracked_branch(self, sync_with_github):
|
||||
self.client.post('/github_service_hook', {'payload': json.dumps({
|
||||
'ref': 'refs/heads/non_branch',
|
||||
'repository': {'name': 'repo'}})
|
||||
})
|
||||
self.assertFalse(import_from_github.called)
|
||||
self.assertFalse(export_to_github.called)
|
||||
self.assertFalse(sync_with_github.called)
|
||||
|
||||
@patch('github_sync.views.export_to_github')
|
||||
@patch('github_sync.views.import_from_github', return_value=(Mock(), Mock()))
|
||||
def test_tracked_branch(self, import_from_github, export_to_github):
|
||||
@patch('github_sync.views.sync_with_github')
|
||||
def test_tracked_branch(self, sync_with_github):
|
||||
self.client.post('/github_service_hook', {'payload': json.dumps({
|
||||
'ref': 'refs/heads/branch',
|
||||
'repository': {'name': 'repo'}})
|
||||
})
|
||||
import_from_github.assert_called_with(settings.REPOS['repo'])
|
||||
mock_revision, mock_course = import_from_github.return_value
|
||||
export_to_github.assert_called_with(mock_course, 'path', "Changes from cms import of revision %s" % mock_revision)
|
||||
sync_with_github.assert_called_with(load_repo_settings('repo'))
|
||||
|
||||
@@ -5,7 +5,7 @@ from django.http import HttpResponse
|
||||
from django.conf import settings
|
||||
from django_future.csrf import csrf_exempt
|
||||
|
||||
from . import import_from_github, export_to_github
|
||||
from . import sync_with_github, load_repo_settings
|
||||
|
||||
log = logging.getLogger()
|
||||
|
||||
@@ -40,13 +40,12 @@ def github_post_receive(request):
|
||||
log.info('No repository matching %s found' % repo_name)
|
||||
return HttpResponse('No Repo Found')
|
||||
|
||||
repo = settings.REPOS[repo_name]
|
||||
repo = load_repo_settings(repo_name)
|
||||
|
||||
if repo['branch'] != branch_name:
|
||||
if repo.branch != branch_name:
|
||||
log.info('Ignoring changes to non-tracked branch %s in repo %s' % (branch_name, repo_name))
|
||||
return HttpResponse('Ignoring non-tracked branch')
|
||||
|
||||
revision, course = import_from_github(repo)
|
||||
export_to_github(course, repo['path'], "Changes from cms import of revision %s" % revision)
|
||||
sync_with_github(repo)
|
||||
|
||||
return HttpResponse('Push received')
|
||||
|
||||
48
cms/envs/aws.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
This is the default template for our main set of AWS servers.
|
||||
"""
|
||||
import json
|
||||
|
||||
from .logsettings import get_logger_config
|
||||
from .common import *
|
||||
|
||||
############################### ALWAYS THE SAME ################################
|
||||
DEBUG = False
|
||||
TEMPLATE_DEBUG = False
|
||||
|
||||
EMAIL_BACKEND = 'django_ses.SESBackend'
|
||||
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
|
||||
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto.S3BotoStorage'
|
||||
|
||||
########################### NON-SECURE ENV CONFIG ##############################
|
||||
# Things like server locations, ports, etc.
|
||||
with open(ENV_ROOT / "cms.env.json") as env_file:
|
||||
ENV_TOKENS = json.load(env_file)
|
||||
|
||||
SITE_NAME = ENV_TOKENS['SITE_NAME']
|
||||
|
||||
LOG_DIR = ENV_TOKENS['LOG_DIR']
|
||||
|
||||
CACHES = ENV_TOKENS['CACHES']
|
||||
|
||||
for feature, value in ENV_TOKENS.get('MITX_FEATURES', {}).items():
|
||||
MITX_FEATURES[feature] = value
|
||||
|
||||
LOGGING = get_logger_config(LOG_DIR,
|
||||
logging_env=ENV_TOKENS['LOGGING_ENV'],
|
||||
syslog_addr=(ENV_TOKENS['SYSLOG_SERVER'], 514),
|
||||
debug=False)
|
||||
|
||||
with open(ENV_ROOT / "repos.json") as repos_file:
|
||||
REPOS = json.load(repos_file)
|
||||
|
||||
|
||||
############################## SECURE AUTH ITEMS ###############################
|
||||
# Secret things: passwords, access keys, etc.
|
||||
with open(ENV_ROOT / "cms.auth.json") as auth_file:
|
||||
AUTH_TOKENS = json.load(auth_file)
|
||||
|
||||
AWS_ACCESS_KEY_ID = AUTH_TOKENS["AWS_ACCESS_KEY_ID"]
|
||||
AWS_SECRET_ACCESS_KEY = AUTH_TOKENS["AWS_SECRET_ACCESS_KEY"]
|
||||
DATABASES = AUTH_TOKENS['DATABASES']
|
||||
MODULESTORE = AUTH_TOKENS['MODULESTORE']
|
||||
@@ -243,7 +243,7 @@ with open(module_styles_path, 'w') as module_styles:
|
||||
PIPELINE_CSS = {
|
||||
'base-style': {
|
||||
'source_filenames': ['sass/base-style.scss'],
|
||||
'output_filename': 'css/base-style.css',
|
||||
'output_filename': 'css/cms-base-style.css',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -260,15 +260,15 @@ PIPELINE_JS = {
|
||||
for pth
|
||||
in glob2.glob(PROJECT_ROOT / 'static/coffee/src/**/*.coffee')
|
||||
],
|
||||
'output_filename': 'js/application.js',
|
||||
'output_filename': 'js/cms-application.js',
|
||||
},
|
||||
'module-js': {
|
||||
'source_filenames': module_js_sources,
|
||||
'output_filename': 'js/modules.js',
|
||||
'output_filename': 'js/cms-modules.js',
|
||||
},
|
||||
'spec': {
|
||||
'source_filenames': [pth.replace(PROJECT_ROOT / 'static/', '') for pth in glob2.glob(PROJECT_ROOT / 'static/coffee/spec/**/*.coffee')],
|
||||
'output_filename': 'js/spec.js'
|
||||
'output_filename': 'js/cms-spec.js'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,6 +309,7 @@ INSTALLED_APPS = (
|
||||
|
||||
# For CMS
|
||||
'contentstore',
|
||||
'github_sync',
|
||||
'student', # misleading name due to sharing with lms
|
||||
|
||||
# For asset pipelining
|
||||
|
||||
@@ -32,38 +32,23 @@ DATABASES = {
|
||||
|
||||
REPOS = {
|
||||
'edx4edx': {
|
||||
'path': "edx4edx",
|
||||
'org': 'edx',
|
||||
'course': 'edx4edx',
|
||||
'branch': 'for_cms',
|
||||
'branch': 'master',
|
||||
'origin': 'git@github.com:MITx/edx4edx.git',
|
||||
},
|
||||
'6002x-fall-2012': {
|
||||
'path': '6002x-fall-2012',
|
||||
'org': 'mit.edu',
|
||||
'course': '6.002x',
|
||||
'branch': 'for_cms',
|
||||
'content-mit-6002x': {
|
||||
'branch': 'master',
|
||||
'origin': 'git@github.com:MITx/6002x-fall-2012.git',
|
||||
},
|
||||
'6.00x': {
|
||||
'path': '6.00x',
|
||||
'org': 'mit.edu',
|
||||
'course': '6.00x',
|
||||
'branch': 'for_cms',
|
||||
'branch': 'master',
|
||||
'origin': 'git@github.com:MITx/6.00x.git',
|
||||
},
|
||||
'7.00x': {
|
||||
'path': '7.00x',
|
||||
'org': 'mit.edu',
|
||||
'course': '7.00x',
|
||||
'branch': 'for_cms',
|
||||
'branch': 'master',
|
||||
'origin': 'git@github.com:MITx/7.00x.git',
|
||||
},
|
||||
'3.091x': {
|
||||
'path': '3.091x',
|
||||
'org': 'mit.edu',
|
||||
'course': '3.091x',
|
||||
'branch': 'for_cms',
|
||||
'branch': 'master',
|
||||
'origin': 'git@github.com:MITx/3.091x.git',
|
||||
},
|
||||
}
|
||||
|
||||
95
cms/envs/logsettings.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import os
|
||||
import os.path
|
||||
import platform
|
||||
import sys
|
||||
|
||||
def get_logger_config(log_dir,
|
||||
logging_env="no_env",
|
||||
tracking_filename=None,
|
||||
syslog_addr=None,
|
||||
debug=False):
|
||||
"""Return the appropriate logging config dictionary. You should assign the
|
||||
result of this to the LOGGING var in your settings. The reason it's done
|
||||
this way instead of registering directly is because I didn't want to worry
|
||||
about resetting the logging state if this is called multiple times when
|
||||
settings are extended."""
|
||||
|
||||
# If we're given an explicit place to put tracking logs, we do that (say for
|
||||
# debugging). However, logging is not safe for multiple processes hitting
|
||||
# the same file. So if it's left blank, we dynamically create the filename
|
||||
# based on the PID of this worker process.
|
||||
if tracking_filename:
|
||||
tracking_file_loc = os.path.join(log_dir, tracking_filename)
|
||||
else:
|
||||
pid = os.getpid() # So we can log which process is creating the log
|
||||
tracking_file_loc = os.path.join(log_dir, "tracking_{0}.log".format(pid))
|
||||
|
||||
hostname = platform.node().split(".")[0]
|
||||
syslog_format = ("[%(name)s][env:{logging_env}] %(levelname)s [{hostname} " +
|
||||
" %(process)d] [%(filename)s:%(lineno)d] - %(message)s").format(
|
||||
logging_env=logging_env, hostname=hostname)
|
||||
|
||||
handlers = ['console'] if debug else ['console', 'syslogger', 'newrelic']
|
||||
|
||||
return {
|
||||
'version': 1,
|
||||
'formatters' : {
|
||||
'standard' : {
|
||||
'format' : '%(asctime)s %(levelname)s %(process)d [%(name)s] %(filename)s:%(lineno)d - %(message)s',
|
||||
},
|
||||
'syslog_format' : { 'format' : syslog_format },
|
||||
'raw' : { 'format' : '%(message)s' },
|
||||
},
|
||||
'handlers' : {
|
||||
'console' : {
|
||||
'level' : 'DEBUG' if debug else 'INFO',
|
||||
'class' : 'logging.StreamHandler',
|
||||
'formatter' : 'standard',
|
||||
'stream' : sys.stdout,
|
||||
},
|
||||
'syslogger' : {
|
||||
'level' : 'INFO',
|
||||
'class' : 'logging.handlers.SysLogHandler',
|
||||
'address' : syslog_addr,
|
||||
'formatter' : 'syslog_format',
|
||||
},
|
||||
'tracking' : {
|
||||
'level' : 'DEBUG',
|
||||
'class' : 'logging.handlers.WatchedFileHandler',
|
||||
'filename' : tracking_file_loc,
|
||||
'formatter' : 'raw',
|
||||
},
|
||||
'newrelic' : {
|
||||
'level': 'ERROR',
|
||||
'class': 'newrelic_logging.NewRelicHandler',
|
||||
'formatter': 'raw',
|
||||
}
|
||||
},
|
||||
'loggers' : {
|
||||
'django' : {
|
||||
'handlers' : handlers,
|
||||
'propagate' : True,
|
||||
'level' : 'INFO'
|
||||
},
|
||||
'tracking' : {
|
||||
'handlers' : ['tracking'],
|
||||
'level' : 'DEBUG',
|
||||
'propagate' : False,
|
||||
},
|
||||
'' : {
|
||||
'handlers' : handlers,
|
||||
'level' : 'DEBUG',
|
||||
'propagate' : False
|
||||
},
|
||||
'mitx' : {
|
||||
'handlers' : handlers,
|
||||
'level' : 'DEBUG',
|
||||
'propagate' : False
|
||||
},
|
||||
'keyedcache' : {
|
||||
'handlers' : handlers,
|
||||
'level' : 'DEBUG',
|
||||
'propagate' : False
|
||||
},
|
||||
}
|
||||
}
|
||||
BIN
cms/static/img/content-types/module.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
cms/static/img/menu.png
Normal file
|
After Width: | Height: | Size: 95 B |
BIN
cms/static/img/noise.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
@@ -5,7 +5,7 @@ $body-font-family: "Open Sans", "Lucida Grande", "Lucida Sans Unicode", "Lucida
|
||||
$body-font-size: 14px;
|
||||
$body-line-height: 20px;
|
||||
|
||||
$light-blue: #f0f8fa;
|
||||
$light-blue: #f0f7fd;
|
||||
$dark-blue: #50545c;
|
||||
$bright-blue: #3c8ebf;
|
||||
$orange: #f96e5b;
|
||||
@@ -13,6 +13,14 @@ $yellow: #fff8af;
|
||||
$cream: #F6EFD4;
|
||||
$mit-red: #933;
|
||||
|
||||
@mixin hide-text {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
color: transparent;
|
||||
font: 0/0 a;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
// Base html styles
|
||||
html {
|
||||
height: 100%;
|
||||
@@ -34,14 +42,18 @@ input {
|
||||
|
||||
button, input[type="submit"], .button {
|
||||
background-color: $orange;
|
||||
border: 0;
|
||||
border: 1px solid darken($orange, 15%);
|
||||
@include border-radius(4px);
|
||||
@include box-shadow(inset 0 0 0 1px adjust-hue($orange, 20%), 0 1px 0 #fff);
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
padding: 8px 10px;
|
||||
@include linear-gradient(adjust-hue($orange, 8%), $orange);
|
||||
padding: 6px 20px;
|
||||
text-shadow: 0 1px 0 darken($orange, 10%);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
&:hover {
|
||||
background-color: shade($orange, 10%);
|
||||
&:hover, &:focus {
|
||||
@include box-shadow(inset 0 0 6px 1px adjust-hue($orange, 30%));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,10 +134,10 @@ textarea {
|
||||
}
|
||||
}
|
||||
|
||||
.wip {
|
||||
outline: 1px solid #f00 !important;
|
||||
position: relative;
|
||||
}
|
||||
// .wip {
|
||||
// outline: 1px solid #f00 !important;
|
||||
// position: relative;
|
||||
// }
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
section.cal {
|
||||
@include box-sizing(border-box);
|
||||
@include clearfix;
|
||||
padding: 25px;
|
||||
padding: 20px;
|
||||
|
||||
> header {
|
||||
display: none;
|
||||
@include clearfix;
|
||||
margin-bottom: 10px;
|
||||
opacity: .4;
|
||||
@include transition;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
@@ -70,12 +72,15 @@ section.cal {
|
||||
ol {
|
||||
list-style: none;
|
||||
@include clearfix;
|
||||
border-left: 1px solid lighten($dark-blue, 40%);
|
||||
border-top: 1px solid lighten($dark-blue, 40%);
|
||||
border: 1px solid lighten( $dark-blue , 30% );
|
||||
background: #FFF;
|
||||
width: 100%;
|
||||
@include box-sizing(border-box);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@include box-shadow(0 0 5px lighten($dark-blue, 45%));
|
||||
@include border-radius(3px);
|
||||
overflow: hidden;
|
||||
|
||||
> li {
|
||||
border-right: 1px solid lighten($dark-blue, 40%);
|
||||
@@ -84,6 +89,7 @@ section.cal {
|
||||
float: left;
|
||||
width: flex-grid(3) + ((flex-gutter() * 3) / 4);
|
||||
background-color: $light-blue;
|
||||
@include box-shadow(inset 0 0 0 1px lighten($light-blue, 8%));
|
||||
|
||||
&:hover {
|
||||
li.create-module {
|
||||
@@ -91,6 +97,10 @@ section.cal {
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(4n) {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
header {
|
||||
border-bottom: 1px solid lighten($dark-blue, 40%);
|
||||
@include box-shadow(0 2px 2px $light-blue);
|
||||
@@ -128,6 +138,7 @@ section.cal {
|
||||
color: #888;
|
||||
border-bottom: 0;
|
||||
font-size: 12px;
|
||||
@include box-shadow(none);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,9 +149,11 @@ section.cal {
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
border-bottom: 1px solid darken($light-blue, 8%);
|
||||
position: relative;
|
||||
border-bottom: 1px solid darken($light-blue, 6%);
|
||||
// @include box-shadow(0 1px 0 lighten($light-blue, 4%));
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($yellow, 14%);
|
||||
@@ -314,16 +327,13 @@ section.cal {
|
||||
@include box-sizing(border-box);
|
||||
opacity: .4;
|
||||
@include transition();
|
||||
background: darken($light-blue, 2%);
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
width: flex-grid(5) + flex-gutter();
|
||||
background-color: transparent;
|
||||
|
||||
+ section.main-content {
|
||||
width: flex-grid(7);
|
||||
opacity: .6;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,6 +350,7 @@ section.cal {
|
||||
display: block;
|
||||
|
||||
li {
|
||||
|
||||
ul {
|
||||
display: inline;
|
||||
}
|
||||
@@ -351,6 +362,7 @@ section.cal {
|
||||
li {
|
||||
@include box-sizing(border-box);
|
||||
width: 100%;
|
||||
border-right: 0;
|
||||
|
||||
&.create-module {
|
||||
display: none;
|
||||
|
||||
@@ -53,3 +53,13 @@
|
||||
@extend .content-type;
|
||||
background-image: url('../img/content-types/chapter.png');
|
||||
}
|
||||
|
||||
.module a:first-child {
|
||||
@extend .content-type;
|
||||
background-image: url('/static/img/content-types/module.png');
|
||||
}
|
||||
|
||||
.module a:first-child {
|
||||
@extend .content-type;
|
||||
background-image: url('/static/img/content-types/module.png');
|
||||
}
|
||||
|
||||
80
cms/static/sass/_index.scss
Normal file
@@ -0,0 +1,80 @@
|
||||
body.index {
|
||||
> header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> h1 {
|
||||
font-weight: 300;
|
||||
color: lighten($dark-blue, 40%);
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
max-width: 600px;
|
||||
text-align: center;
|
||||
margin: 80px auto 30px;
|
||||
}
|
||||
|
||||
section.main-container {
|
||||
border-right: 3px;
|
||||
background: #FFF;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
@include box-sizing(border-box);
|
||||
border: 1px solid lighten( $dark-blue , 30% );
|
||||
@include border-radius(3px);
|
||||
overflow: hidden;
|
||||
@include bounce-in-animation(.8s);
|
||||
|
||||
header {
|
||||
border-bottom: 1px solid lighten($dark-blue, 50%);
|
||||
@include linear-gradient(#fff, lighten($dark-blue, 62%));
|
||||
@include clearfix();
|
||||
@include box-shadow( 0 2px 0 $light-blue, inset 0 -1px 0 #fff);
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
|
||||
h1 {
|
||||
font-size: 14px;
|
||||
padding: 8px 20px;
|
||||
float: left;
|
||||
color: $dark-blue;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
float: right;
|
||||
padding: 8px 20px;
|
||||
border-left: 1px solid lighten($dark-blue, 50%);
|
||||
@include box-shadow( inset -1px 0 0 #fff);
|
||||
font-weight: bold;
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
color: $dark-blue;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
border-bottom: 1px solid lighten($dark-blue, 50%);
|
||||
|
||||
a {
|
||||
display: block;
|
||||
padding: 10px 20px;
|
||||
|
||||
&:hover {
|
||||
color: $dark-blue;
|
||||
background: lighten($yellow, 10%);
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
cms/static/sass/_keyframes.scss
Normal file
@@ -0,0 +1,27 @@
|
||||
@mixin bounce-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
@include transform(scale(.3));
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
@include transform(scale(1.05));
|
||||
}
|
||||
|
||||
100% {
|
||||
@include transform(scale(1));
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes bounce-in { @include bounce-in(); }
|
||||
@-webkit-keyframes bounce-in { @include bounce-in(); }
|
||||
@-o-keyframes bounce-in { @include bounce-in(); }
|
||||
@keyframes bounce-in { @include bounce-in();}
|
||||
|
||||
@mixin bounce-in-animation($duration, $timing: ease-in-out) {
|
||||
@include animation-name(bounce-in);
|
||||
@include animation-duration($duration);
|
||||
@include animation-timing-function($timing);
|
||||
@include animation-fill-mode(both);
|
||||
}
|
||||
@@ -2,6 +2,8 @@ body {
|
||||
@include clearfix();
|
||||
height: 100%;
|
||||
font: 14px $body-font-family;
|
||||
background-color: lighten($dark-blue, 62%);
|
||||
background-image: url('/static/img/noise.png');
|
||||
|
||||
> section {
|
||||
display: table;
|
||||
@@ -11,28 +13,53 @@ body {
|
||||
|
||||
> header {
|
||||
background: $dark-blue;
|
||||
@include background-image(url('/static/img/noise.png'), linear-gradient(lighten($dark-blue, 10%), $dark-blue));
|
||||
border-bottom: 1px solid darken($dark-blue, 15%);
|
||||
@include box-shadow(inset 0 -1px 0 lighten($dark-blue, 10%));
|
||||
@include box-sizing(border-box);
|
||||
color: #fff;
|
||||
display: block;
|
||||
float: none;
|
||||
padding: 8px 25px;
|
||||
padding: 0 20px;
|
||||
text-shadow: 0 -1px 0 darken($dark-blue, 15%);
|
||||
width: 100%;
|
||||
@include box-sizing(border-box);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
nav {
|
||||
@include clearfix;
|
||||
|
||||
h2 {
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
> a {
|
||||
@include hide-text;
|
||||
background: url('/static/img/menu.png') 0 center no-repeat;
|
||||
border-right: 1px solid darken($dark-blue, 10%);
|
||||
@include box-shadow(1px 0 0 lighten($dark-blue, 10%));
|
||||
display: block;
|
||||
float: left;
|
||||
margin: 0 15px 0 0;
|
||||
height: 19px;
|
||||
padding: 8px 10px 8px 0;
|
||||
width: 14px;
|
||||
|
||||
&:hover, &:focus {
|
||||
opacity: .7;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
border-right: 1px solid darken($dark-blue, 10%);
|
||||
@include box-shadow(1px 0 0 lighten($dark-blue, 10%));
|
||||
float: left;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
a {
|
||||
color: #fff;
|
||||
padding: 8px 20px;
|
||||
display: block;
|
||||
|
||||
&:hover {
|
||||
color: rgba(#fff, .6);
|
||||
background-color: rgba(darken($dark-blue, 15%), .5);
|
||||
color: $yellow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,21 +75,35 @@ body {
|
||||
ul {
|
||||
float: left;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
@include clearfix;
|
||||
|
||||
&.user-nav {
|
||||
float: right;
|
||||
border-left: 1px solid darken($dark-blue, 10%);
|
||||
}
|
||||
|
||||
li {
|
||||
@include inline-block();
|
||||
border-right: 1px solid darken($dark-blue, 10%);
|
||||
float: left;
|
||||
@include box-shadow(1px 0 0 lighten($dark-blue, 10%));
|
||||
|
||||
a {
|
||||
padding: 8px 10px;
|
||||
padding: 8px 20px;
|
||||
display: block;
|
||||
margin: -8px 0;
|
||||
|
||||
&:hover {
|
||||
background-color: darken($dark-blue, 15%);
|
||||
background-color: rgba(darken($dark-blue, 15%), .5);
|
||||
color: $yellow;
|
||||
}
|
||||
|
||||
&.new-module {
|
||||
&:before {
|
||||
@include inline-block;
|
||||
content: "+";
|
||||
font-weight: bold;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,8 +117,9 @@ body {
|
||||
@include box-sizing(border-box);
|
||||
width: flex-grid(9) + flex-gutter();
|
||||
float: left;
|
||||
@include box-shadow( -2px 0 0 darken($light-blue, 3%));
|
||||
@include box-shadow( -2px 0 0 lighten($dark-blue, 55%));
|
||||
@include transition();
|
||||
background: #FFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
section#unit-wrapper {
|
||||
section.filters {
|
||||
@include clearfix;
|
||||
display: none;
|
||||
opacity: .4;
|
||||
margin-bottom: 10px;
|
||||
@include transition;
|
||||
@@ -52,22 +53,22 @@ section#unit-wrapper {
|
||||
display: table;
|
||||
border: 1px solid lighten($dark-blue, 40%);
|
||||
width: 100%;
|
||||
@include border-radius(3px);
|
||||
@include box-shadow(0 0 4px lighten($dark-blue, 50%));
|
||||
|
||||
section {
|
||||
header {
|
||||
background: #fff;
|
||||
padding: 6px;
|
||||
border-bottom: 1px solid lighten($dark-blue, 60%);
|
||||
border-top: 1px solid lighten($dark-blue, 60%);
|
||||
margin-top: -1px;
|
||||
@include clearfix;
|
||||
|
||||
h2 {
|
||||
color: $bright-blue;
|
||||
float: left;
|
||||
font-size: 12px;
|
||||
// float: left;
|
||||
font-size: 14px;
|
||||
letter-spacing: 1px;
|
||||
line-height: 19px;
|
||||
// line-height: 20px;
|
||||
text-transform: uppercase;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -172,7 +173,6 @@ section#unit-wrapper {
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
border-bottom: 1px solid darken($light-blue, 8%);
|
||||
background: $light-blue;
|
||||
|
||||
&:last-child {
|
||||
@@ -181,6 +181,7 @@ section#unit-wrapper {
|
||||
|
||||
&.new-module a {
|
||||
background-color: darken($light-blue, 2%);
|
||||
border-bottom: 1px solid darken($light-blue, 8%);
|
||||
|
||||
&:hover {
|
||||
background-color: lighten($yellow, 10%);
|
||||
@@ -199,6 +200,7 @@ section#unit-wrapper {
|
||||
li {
|
||||
padding: 6px;
|
||||
border-collapse: collapse;
|
||||
border-bottom: 1px solid darken($light-blue, 8%);
|
||||
position: relative;
|
||||
|
||||
&:last-child {
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
section#unit-wrapper {
|
||||
> header {
|
||||
border-bottom: 2px solid $dark-blue;
|
||||
border-bottom: 1px solid lighten($dark-blue, 50%);
|
||||
@include linear-gradient(#fff, lighten($dark-blue, 62%));
|
||||
@include clearfix();
|
||||
@include box-shadow( 0 2px 0 darken($light-blue, 3%));
|
||||
padding: 6px 20px;
|
||||
@include box-shadow( 0 2px 0 $light-blue, inset 0 -1px 0 #fff);
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
|
||||
section {
|
||||
float: left;
|
||||
padding: 10px 20px;
|
||||
|
||||
h1 {
|
||||
font-size: 16px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
font-size: 18px;
|
||||
@include inline-block();
|
||||
color: $bright-blue;
|
||||
color: $dark-blue;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -22,32 +22,41 @@ section#unit-wrapper {
|
||||
margin: 0;
|
||||
|
||||
a {
|
||||
text-indent: -9999px;
|
||||
@include inline-block();
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
float: right;
|
||||
@include clearfix;
|
||||
color: #666;
|
||||
float: right;
|
||||
padding: 6px 20px;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
@include inline-block;
|
||||
|
||||
&.cancel {
|
||||
margin-right: 20px;
|
||||
font-style: italic;
|
||||
font-size: 12px;
|
||||
}
|
||||
&.cancel {
|
||||
margin-right: 20px;
|
||||
font-style: italic;
|
||||
font-size: 12px;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
&.save-update {
|
||||
@extend .button;
|
||||
margin: -6px -21px -6px 0;
|
||||
}
|
||||
&.save-update {
|
||||
padding: 6px 20px;
|
||||
@include border-radius(3px);
|
||||
border: 1px solid lighten($dark-blue, 40%);
|
||||
@include box-shadow(inset 0 0 0 1px #fff);
|
||||
color: $dark-blue;
|
||||
@include linear-gradient(lighten($dark-blue, 60%), lighten($dark-blue, 55%));
|
||||
|
||||
&:hover, &:focus {
|
||||
@include linear-gradient(lighten($dark-blue, 58%), lighten($dark-blue, 53%));
|
||||
@include box-shadow(inset 0 0 6px 1px #fff);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
@import 'bourbon/bourbon';
|
||||
@import 'vendor/normalize';
|
||||
@import 'keyframes';
|
||||
|
||||
@import 'base', 'layout', 'content-types';
|
||||
@import 'calendar';
|
||||
@import 'section', 'unit';
|
||||
@import 'section', 'unit', 'index';
|
||||
|
||||
@import 'module/module-styles.scss';
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<meta name="path_prefix" content="${MITX_ROOT_URL}">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body class="<%block name='bodyclass'></%block>">
|
||||
|
||||
<%include file="widgets/header.html"/>
|
||||
<%include file="courseware_vendor_js.html"/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<%inherit file="base.html" />
|
||||
<%block name="title">Course Manager</%block>
|
||||
<%include file="widgets/header.html"/>
|
||||
|
||||
<%block name="content">
|
||||
<section class="main-container">
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
<%inherit file="base.html" />
|
||||
<%block name="bodyclass">index</%block>
|
||||
<%block name="title">Courses</%block>
|
||||
|
||||
<%block name="content">
|
||||
<h1>edX Course Management</h1>
|
||||
|
||||
<section class="main-container">
|
||||
<header>
|
||||
<h1>Courses</h1>
|
||||
<a href="#" class="wip">+</a>
|
||||
</header>
|
||||
|
||||
<ol>
|
||||
%for course, url in courses:
|
||||
<li><a href="${url}">${course}</a></li>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<header>
|
||||
<nav>
|
||||
<h2><a href="/">edX CMS: TODO:-course-name-here</a></h2>
|
||||
<a href="/">Home</a>
|
||||
<h2><a href="#">edX CMS: TODO:-course-name-here</a></h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="#" class="new-module wip">New Module</a>
|
||||
<a href="#" class="new-module wip">Module</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="new-module wip">New Unit</a>
|
||||
<a href="#" class="new-module wip">Unit</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ from django_future.csrf import ensure_csrf_cookie
|
||||
from student.models import Registration, UserProfile, PendingNameChange, PendingEmailChange, CourseEnrollment
|
||||
from util.cache import cache_if_anonymous
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from x_module import XModuleDescriptor
|
||||
from lxml import etree
|
||||
from functools import wraps
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -12,8 +13,8 @@ log = logging.getLogger(__name__)
|
||||
def process_includes(fn):
|
||||
"""
|
||||
Wraps a XModuleDescriptor.from_xml method, and modifies xml_data to replace
|
||||
any immediate child <include> items with the contents of the file that they are
|
||||
supposed to include
|
||||
any immediate child <include> items with the contents of the file that they
|
||||
are supposed to include
|
||||
"""
|
||||
@wraps(fn)
|
||||
def from_xml(cls, xml_data, system, org=None, course=None):
|
||||
@@ -21,23 +22,31 @@ def process_includes(fn):
|
||||
next_include = xml_object.find('include')
|
||||
while next_include is not None:
|
||||
file = next_include.get('file')
|
||||
if file is not None:
|
||||
try:
|
||||
ifp = system.resources_fs.open(file)
|
||||
except Exception:
|
||||
log.exception('Error in problem xml include: %s' % (etree.tostring(next_include, pretty_print=True)))
|
||||
log.exception('Cannot find file %s in %s' % (file, dir))
|
||||
raise
|
||||
try:
|
||||
# read in and convert to XML
|
||||
incxml = etree.XML(ifp.read())
|
||||
except Exception:
|
||||
log.exception('Error in problem xml include: %s' % (etree.tostring(next_include, pretty_print=True)))
|
||||
log.exception('Cannot parse XML in %s' % (file))
|
||||
raise
|
||||
parent = next_include.getparent()
|
||||
|
||||
if file is None:
|
||||
continue
|
||||
|
||||
try:
|
||||
ifp = system.resources_fs.open(file)
|
||||
# read in and convert to XML
|
||||
incxml = etree.XML(ifp.read())
|
||||
|
||||
# insert new XML into tree in place of inlcude
|
||||
parent = next_include.getparent()
|
||||
parent.insert(parent.index(next_include), incxml)
|
||||
except Exception:
|
||||
msg = "Error in problem xml include: %s" % (etree.tostring(next_include, pretty_print=True))
|
||||
log.exception(msg)
|
||||
parent = next_include.getparent()
|
||||
|
||||
errorxml = etree.Element('error')
|
||||
messagexml = etree.SubElement(errorxml, 'message')
|
||||
messagexml.text = msg
|
||||
stackxml = etree.SubElement(errorxml, 'stacktrace')
|
||||
stackxml.text = traceback.format_exc()
|
||||
|
||||
# insert error XML in place of include
|
||||
parent.insert(parent.index(next_include), errorxml)
|
||||
parent.remove(next_include)
|
||||
|
||||
next_include = xml_object.find('include')
|
||||
@@ -50,8 +59,8 @@ class SemanticSectionDescriptor(XModuleDescriptor):
|
||||
@process_includes
|
||||
def from_xml(cls, xml_data, system, org=None, course=None):
|
||||
"""
|
||||
Removes sections single child elements in favor of just embedding the child element
|
||||
|
||||
Removes sections with single child elements in favor of just embedding
|
||||
the child element
|
||||
"""
|
||||
xml_object = etree.fromstring(xml_data)
|
||||
|
||||
@@ -76,7 +85,6 @@ class TranslateCustomTagDescriptor(XModuleDescriptor):
|
||||
xml_object = etree.fromstring(xml_data)
|
||||
tag = xml_object.tag
|
||||
xml_object.tag = 'customtag'
|
||||
impl = etree.SubElement(xml_object, 'impl')
|
||||
impl.text = tag
|
||||
xml_object.attrib['impl'] = tag
|
||||
|
||||
return system.process_xml(etree.tostring(xml_object))
|
||||
|
||||
@@ -67,7 +67,8 @@ class ComplexEncoder(json.JSONEncoder):
|
||||
|
||||
class CapaModule(XModule):
|
||||
'''
|
||||
An XModule implementing LonCapa format problems, implemented by way of capa.capa_problem.LoncapaProblem
|
||||
An XModule implementing LonCapa format problems, implemented by way of
|
||||
capa.capa_problem.LoncapaProblem
|
||||
'''
|
||||
icon_class = 'problem'
|
||||
|
||||
@@ -77,8 +78,10 @@ class CapaModule(XModule):
|
||||
js_module_name = "Problem"
|
||||
css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]}
|
||||
|
||||
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
|
||||
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)
|
||||
def __init__(self, system, location, definition, instance_state=None,
|
||||
shared_state=None, **kwargs):
|
||||
XModule.__init__(self, system, location, definition, instance_state,
|
||||
shared_state, **kwargs)
|
||||
|
||||
self.attempts = 0
|
||||
self.max_attempts = None
|
||||
@@ -133,7 +136,8 @@ class CapaModule(XModule):
|
||||
seed = None
|
||||
|
||||
try:
|
||||
self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(), instance_state, seed=seed, system=self.system)
|
||||
self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(),
|
||||
instance_state, seed=seed, system=self.system)
|
||||
except Exception:
|
||||
msg = 'cannot create LoncapaProblem %s' % self.location.url()
|
||||
log.exception(msg)
|
||||
@@ -141,15 +145,20 @@ class CapaModule(XModule):
|
||||
msg = '<p>%s</p>' % msg.replace('<', '<')
|
||||
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '<')
|
||||
# create a dummy problem with error message instead of failing
|
||||
problem_text = '<problem><text><font color="red" size="+2">Problem %s has an error:</font>%s</text></problem>' % (self.location.url(), msg)
|
||||
self.lcp = LoncapaProblem(problem_text, self.location.html_id(), instance_state, seed=seed, system=self.system)
|
||||
problem_text = ('<problem><text><font color="red" size="+2">'
|
||||
'Problem %s has an error:</font>%s</text></problem>' %
|
||||
(self.location.url(), msg))
|
||||
self.lcp = LoncapaProblem(
|
||||
problem_text, self.location.html_id(),
|
||||
instance_state, seed=seed, system=self.system)
|
||||
else:
|
||||
raise
|
||||
|
||||
@property
|
||||
def rerandomize(self):
|
||||
"""
|
||||
Property accessor that returns self.metadata['rerandomize'] in a canonical form
|
||||
Property accessor that returns self.metadata['rerandomize'] in a
|
||||
canonical form
|
||||
"""
|
||||
rerandomize = self.metadata.get('rerandomize', 'always')
|
||||
if rerandomize in ("", "always", "true"):
|
||||
@@ -203,7 +212,10 @@ class CapaModule(XModule):
|
||||
except Exception, err:
|
||||
if self.system.DEBUG:
|
||||
log.exception(err)
|
||||
msg = '[courseware.capa.capa_module] <font size="+1" color="red">Failed to generate HTML for problem %s</font>' % (self.location.url())
|
||||
msg = (
|
||||
'[courseware.capa.capa_module] <font size="+1" color="red">'
|
||||
'Failed to generate HTML for problem %s</font>' %
|
||||
(self.location.url()))
|
||||
msg += '<p>Error:</p><p><pre>%s</pre></p>' % str(err).replace('<', '<')
|
||||
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '<')
|
||||
html = msg
|
||||
@@ -215,8 +227,8 @@ class CapaModule(XModule):
|
||||
'weight': self.weight,
|
||||
}
|
||||
|
||||
# We using strings as truthy values, because the terminology of the check button
|
||||
# is context-specific.
|
||||
# We using strings as truthy values, because the terminology of the
|
||||
# check button is context-specific.
|
||||
check_button = "Grade" if self.max_attempts else "Check"
|
||||
reset_button = True
|
||||
save_button = True
|
||||
@@ -242,7 +254,8 @@ class CapaModule(XModule):
|
||||
if not self.lcp.done:
|
||||
reset_button = False
|
||||
|
||||
# We don't need a "save" button if infinite number of attempts and non-randomized
|
||||
# We don't need a "save" button if infinite number of attempts and
|
||||
# non-randomized
|
||||
if self.max_attempts is None and self.rerandomize != "always":
|
||||
save_button = False
|
||||
|
||||
@@ -517,11 +530,13 @@ class CapaModule(XModule):
|
||||
|
||||
self.lcp.do_reset()
|
||||
if self.rerandomize == "always":
|
||||
# reset random number generator seed (note the self.lcp.get_state() in next line)
|
||||
# reset random number generator seed (note the self.lcp.get_state()
|
||||
# in next line)
|
||||
self.lcp.seed = None
|
||||
|
||||
self.lcp = LoncapaProblem(self.definition['data'],
|
||||
self.location.html_id(), self.lcp.get_state(), system=self.system)
|
||||
self.location.html_id(), self.lcp.get_state(),
|
||||
system=self.system)
|
||||
|
||||
event_info['new_state'] = self.lcp.get_state()
|
||||
self.system.track_function('reset_problem', event_info)
|
||||
@@ -537,6 +552,7 @@ class CapaDescriptor(RawDescriptor):
|
||||
|
||||
module_class = CapaModule
|
||||
|
||||
# VS[compat]
|
||||
# TODO (cpennington): Delete this method once all fall 2012 course are being
|
||||
# edited in the cms
|
||||
@classmethod
|
||||
@@ -545,3 +561,7 @@ class CapaDescriptor(RawDescriptor):
|
||||
'problems/' + path[8:],
|
||||
path[8:],
|
||||
]
|
||||
@classmethod
|
||||
def split_to_file(cls, xml_object):
|
||||
'''Problems always written in their own files'''
|
||||
return True
|
||||
|
||||
@@ -10,6 +10,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
class CourseDescriptor(SequenceDescriptor):
|
||||
module_class = SequenceModule
|
||||
metadata_attributes = SequenceDescriptor.metadata_attributes + ('org', 'course')
|
||||
|
||||
def __init__(self, system, definition=None, **kwargs):
|
||||
super(CourseDescriptor, self).__init__(system, definition, **kwargs)
|
||||
@@ -17,23 +18,40 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
try:
|
||||
self.start = time.strptime(self.metadata["start"], "%Y-%m-%dT%H:%M")
|
||||
except KeyError:
|
||||
self.start = time.gmtime(0) # The epoch
|
||||
log.critical("Course loaded without a start date. " + str(self.id))
|
||||
except ValueError, e:
|
||||
self.start = time.gmtime(0) # The epoch
|
||||
log.critical("Course loaded with a bad start date. " + str(self.id) + " '" + str(e) + "'")
|
||||
self.start = time.gmtime(0) #The epoch
|
||||
log.critical("Course loaded without a start date. %s", self.id)
|
||||
except ValueError as e:
|
||||
self.start = time.gmtime(0) #The epoch
|
||||
log.critical("Course loaded with a bad start date. %s '%s'",
|
||||
self.id, e)
|
||||
|
||||
def has_started(self):
|
||||
return time.gmtime() > self.start
|
||||
|
||||
@classmethod
|
||||
def id_to_location(cls, course_id):
|
||||
@staticmethod
|
||||
def id_to_location(course_id):
|
||||
'''Convert the given course_id (org/course/name) to a location object.
|
||||
Throws ValueError if course_id is of the wrong format.
|
||||
'''
|
||||
org, course, name = course_id.split('/')
|
||||
return Location('i4x', org, course, 'course', name)
|
||||
|
||||
@staticmethod
|
||||
def location_to_id(location):
|
||||
'''Convert a location of a course to a course_id. If location category
|
||||
is not "course", raise a ValueError.
|
||||
|
||||
location: something that can be passed to Location
|
||||
'''
|
||||
loc = Location(location)
|
||||
if loc.category != "course":
|
||||
raise ValueError("{0} is not a course location".format(loc))
|
||||
return "/".join([loc.org, loc.course, loc.name])
|
||||
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return "/".join([self.location.org, self.location.course, self.location.name])
|
||||
return self.location_to_id(self.location)
|
||||
|
||||
@property
|
||||
def start_date_text(self):
|
||||
|
||||
45
common/lib/xmodule/xmodule/errorhandlers.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def in_exception_handler():
|
||||
'''Is there an active exception?'''
|
||||
return sys.exc_info() != (None, None, None)
|
||||
|
||||
def strict_error_handler(msg, exc_info=None):
|
||||
'''
|
||||
Do not let errors pass. If exc_info is not None, ignore msg, and just
|
||||
re-raise. Otherwise, check if we are in an exception-handling context.
|
||||
If so, re-raise. Otherwise, raise Exception(msg).
|
||||
|
||||
Meant for use in validation, where any errors should trap.
|
||||
'''
|
||||
if exc_info is not None:
|
||||
raise exc_info[0], exc_info[1], exc_info[2]
|
||||
|
||||
if in_exception_handler():
|
||||
raise
|
||||
|
||||
raise Exception(msg)
|
||||
|
||||
|
||||
def logging_error_handler(msg, exc_info=None):
|
||||
'''Log all errors, but otherwise let them pass, relying on the caller to
|
||||
workaround.'''
|
||||
if exc_info is not None:
|
||||
log.exception(msg, exc_info=exc_info)
|
||||
return
|
||||
|
||||
if in_exception_handler():
|
||||
log.exception(msg)
|
||||
return
|
||||
|
||||
log.error(msg)
|
||||
|
||||
|
||||
def ignore_errors_handler(msg, exc_info=None):
|
||||
'''Ignore all errors, relying on the caller to workaround.
|
||||
Meant for use in the LMS, where an error in one part of the course
|
||||
shouldn't bring down the whole system'''
|
||||
pass
|
||||
@@ -1,5 +1,6 @@
|
||||
class InvalidDefinitionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NotFoundError(Exception):
|
||||
pass
|
||||
|
||||
@@ -12,8 +12,10 @@ class HtmlModule(XModule):
|
||||
def get_html(self):
|
||||
return self.html
|
||||
|
||||
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
|
||||
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)
|
||||
def __init__(self, system, location, definition,
|
||||
instance_state=None, shared_state=None, **kwargs):
|
||||
XModule.__init__(self, system, location, definition,
|
||||
instance_state, shared_state, **kwargs)
|
||||
self.html = self.definition['data']
|
||||
|
||||
|
||||
@@ -42,3 +44,8 @@ class HtmlDescriptor(RawDescriptor):
|
||||
def file_to_xml(cls, file_object):
|
||||
parser = etree.HTMLParser()
|
||||
return etree.parse(file_object, parser).getroot()
|
||||
|
||||
@classmethod
|
||||
def split_to_file(cls, xml_object):
|
||||
# never include inline html
|
||||
return True
|
||||
|
||||
@@ -52,6 +52,7 @@ function update_schematics() {
|
||||
schematics[i].setAttribute("loaded","true");
|
||||
}
|
||||
}
|
||||
window.update_schematics = update_schematics;
|
||||
|
||||
// add ourselves to the tasks that get performed when window is loaded
|
||||
function add_schematic_handler(other_onload) {
|
||||
|
||||
@@ -2,9 +2,12 @@ from x_module import XModuleDescriptor, DescriptorSystem
|
||||
|
||||
|
||||
class MakoDescriptorSystem(DescriptorSystem):
|
||||
def __init__(self, render_template, *args, **kwargs):
|
||||
def __init__(self, load_item, resources_fs, error_handler,
|
||||
render_template):
|
||||
super(MakoDescriptorSystem, self).__init__(
|
||||
load_item, resources_fs, error_handler)
|
||||
|
||||
self.render_template = render_template
|
||||
super(MakoDescriptorSystem, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class MakoModuleDescriptor(XModuleDescriptor):
|
||||
@@ -19,7 +22,9 @@ class MakoModuleDescriptor(XModuleDescriptor):
|
||||
|
||||
def __init__(self, system, definition=None, **kwargs):
|
||||
if getattr(system, 'render_template', None) is None:
|
||||
raise TypeError('{system} must have a render_template function in order to use a MakoDescriptor'.format(system=system))
|
||||
raise TypeError('{system} must have a render_template function'
|
||||
' in order to use a MakoDescriptor'.format(
|
||||
system=system))
|
||||
super(MakoModuleDescriptor, self).__init__(system, definition, **kwargs)
|
||||
|
||||
def get_context(self):
|
||||
@@ -29,4 +34,5 @@ class MakoModuleDescriptor(XModuleDescriptor):
|
||||
return {'module': self}
|
||||
|
||||
def get_html(self):
|
||||
return self.system.render_template(self.mako_template, self.get_context())
|
||||
return self.system.render_template(
|
||||
self.mako_template, self.get_context())
|
||||
|
||||
@@ -45,13 +45,28 @@ class Location(_LocationBase):
|
||||
"""
|
||||
return re.sub('_+', '_', INVALID_CHARS.sub('_', value))
|
||||
|
||||
def __new__(_cls, loc_or_tag=None, org=None, course=None, category=None, name=None, revision=None):
|
||||
@classmethod
|
||||
def is_valid(cls, value):
|
||||
'''
|
||||
Check if the value is a valid location, in any acceptable format.
|
||||
'''
|
||||
try:
|
||||
Location(value)
|
||||
except InvalidLocationError:
|
||||
return False
|
||||
return True
|
||||
|
||||
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.
|
||||
|
||||
location - Can be any of the following types:
|
||||
string: should be of the form {tag}://{org}/{course}/{category}/{name}[/{revision}]
|
||||
string: should be of the form
|
||||
{tag}://{org}/{course}/{category}/{name}[/{revision}]
|
||||
|
||||
list: should be of the form [tag, org, course, category, name, revision]
|
||||
|
||||
dict: should be of the form {
|
||||
'tag': tag,
|
||||
'org': org,
|
||||
@@ -62,16 +77,19 @@ class Location(_LocationBase):
|
||||
}
|
||||
Location: another Location object
|
||||
|
||||
In both the dict and list forms, the revision is optional, and can be ommitted.
|
||||
In both the dict and list forms, the revision is optional, and can be
|
||||
ommitted.
|
||||
|
||||
Components must be composed of alphanumeric characters, or the characters '_', '-', and '.'
|
||||
Components must be composed of alphanumeric characters, or the
|
||||
characters '_', '-', and '.'
|
||||
|
||||
Components may be set to None, which may be interpreted by some contexts to mean
|
||||
wildcard selection
|
||||
Components may be set to None, which may be interpreted by some contexts
|
||||
to mean wildcard selection
|
||||
"""
|
||||
|
||||
|
||||
if org is None and course is None and category is None and name is None and revision is None:
|
||||
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)
|
||||
@@ -131,9 +149,11 @@ class Location(_LocationBase):
|
||||
|
||||
def html_id(self):
|
||||
"""
|
||||
Return a string with a version of the location that is safe for use in html id attributes
|
||||
Return a string with a version of the location that is safe for use in
|
||||
html id attributes
|
||||
"""
|
||||
return "-".join(str(v) for v in self.list() if v is not None).replace('.', '_')
|
||||
return "-".join(str(v) for v in self.list()
|
||||
if v is not None).replace('.', '_')
|
||||
|
||||
def dict(self):
|
||||
"""
|
||||
@@ -154,7 +174,8 @@ class Location(_LocationBase):
|
||||
|
||||
class ModuleStore(object):
|
||||
"""
|
||||
An abstract interface for a database backend that stores XModuleDescriptor instances
|
||||
An abstract interface for a database backend that stores XModuleDescriptor
|
||||
instances
|
||||
"""
|
||||
def get_item(self, location, depth=0):
|
||||
"""
|
||||
@@ -164,13 +185,16 @@ class ModuleStore(object):
|
||||
|
||||
If any segment of the location is None except revision, raises
|
||||
xmodule.modulestore.exceptions.InsufficientSpecificationError
|
||||
If no object is found at that location, raises xmodule.modulestore.exceptions.ItemNotFoundError
|
||||
|
||||
If no object is found at that location, raises
|
||||
xmodule.modulestore.exceptions.ItemNotFoundError
|
||||
|
||||
location: Something that can be passed to Location
|
||||
|
||||
depth (int): An argument that some module stores may use to prefetch descendents of the queried modules
|
||||
for more efficient results later in the request. The depth is counted in the number of
|
||||
calls to get_children() to cache. None indicates to cache all descendents
|
||||
depth (int): An argument that some module stores may use to prefetch
|
||||
descendents of the queried modules for more efficient results later
|
||||
in the request. The depth is counted in the number of calls to
|
||||
get_children() to cache. None indicates to cache all descendents
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -182,9 +206,10 @@ class ModuleStore(object):
|
||||
|
||||
location: Something that can be passed to Location
|
||||
|
||||
depth: An argument that some module stores may use to prefetch descendents of the queried modules
|
||||
for more efficient results later in the request. The depth is counted in the number of calls
|
||||
to get_children() to cache. None indicates to cache all descendents
|
||||
depth: An argument that some module stores may use to prefetch
|
||||
descendents of the queried modules for more efficient results later
|
||||
in the request. The depth is counted in the number of calls to
|
||||
get_children() to cache. None indicates to cache all descendents
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -229,3 +254,25 @@ class ModuleStore(object):
|
||||
'''
|
||||
raise NotImplementedError
|
||||
|
||||
def path_to_location(self, location, course=None, chapter=None, section=None):
|
||||
'''
|
||||
Try to find a course/chapter/section[/position] path to this location.
|
||||
|
||||
raise ItemNotFoundError if the location doesn't exist.
|
||||
|
||||
If course, chapter, section are not None, restrict search to paths with those
|
||||
components as specified.
|
||||
|
||||
raise NoPathToItem if the location exists, but isn't accessible via
|
||||
a path that matches the course/chapter/section restrictions.
|
||||
|
||||
In general, a location may be accessible via many paths. This method may
|
||||
return any valid path.
|
||||
|
||||
Return a tuple (course, chapter, section, position).
|
||||
|
||||
If the section a sequence, position should be the position of this location
|
||||
in that sequence. Otherwise, position should be None.
|
||||
'''
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -13,3 +13,11 @@ class InsufficientSpecificationError(Exception):
|
||||
|
||||
class InvalidLocationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NoPathToItem(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DuplicateItemError(Exception):
|
||||
pass
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import pymongo
|
||||
|
||||
from bson.objectid import ObjectId
|
||||
from bson.son import SON
|
||||
from fs.osfs import OSFS
|
||||
from itertools import repeat
|
||||
from path import path
|
||||
|
||||
from importlib import import_module
|
||||
from xmodule.errorhandlers import strict_error_handler
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
from xmodule.mako_module import MakoDescriptorSystem
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
|
||||
from . import ModuleStore, Location
|
||||
from .exceptions import ItemNotFoundError, InsufficientSpecificationError
|
||||
from .exceptions import (ItemNotFoundError, InsufficientSpecificationError,
|
||||
NoPathToItem, DuplicateItemError)
|
||||
|
||||
# TODO (cpennington): This code currently operates under the assumption that
|
||||
# there is only one revision for each item. Once we start versioning inside the CMS,
|
||||
@@ -23,15 +26,26 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
|
||||
A system that has a cache of module json that it will use to load modules
|
||||
from, with a backup of calling to the underlying modulestore for more data
|
||||
"""
|
||||
def __init__(self, modulestore, module_data, default_class, resources_fs, render_template):
|
||||
def __init__(self, modulestore, module_data, default_class, resources_fs,
|
||||
error_handler, render_template):
|
||||
"""
|
||||
modulestore: the module store that can be used to retrieve additional modules
|
||||
module_data: a dict mapping Location -> json that was cached from the underlying modulestore
|
||||
default_class: The default_class to use when loading an XModuleDescriptor from the module_data
|
||||
|
||||
module_data: a dict mapping Location -> json that was cached from the
|
||||
underlying modulestore
|
||||
|
||||
default_class: The default_class to use when loading an
|
||||
XModuleDescriptor from the module_data
|
||||
|
||||
resources_fs: a filesystem, as per MakoDescriptorSystem
|
||||
render_template: a function for rendering templates, as per MakoDescriptorSystem
|
||||
|
||||
error_handler:
|
||||
|
||||
render_template: a function for rendering templates, as per
|
||||
MakoDescriptorSystem
|
||||
"""
|
||||
super(CachingDescriptorSystem, self).__init__(render_template, self.load_item, resources_fs)
|
||||
super(CachingDescriptorSystem, self).__init__(
|
||||
self.load_item, resources_fs, error_handler, render_template)
|
||||
self.modulestore = modulestore
|
||||
self.module_data = module_data
|
||||
self.default_class = default_class
|
||||
@@ -83,7 +97,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
|
||||
self.fs_root = path(fs_root)
|
||||
|
||||
def _clean_item_data(self, item):
|
||||
"""
|
||||
@@ -127,19 +141,23 @@ class MongoModuleStore(ModuleStore):
|
||||
"""
|
||||
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']))
|
||||
data_dir = item.get('metadata', {}).get('data_dir', item['location']['course'])
|
||||
resource_fs = OSFS(self.fs_root / data_dir)
|
||||
|
||||
system = CachingDescriptorSystem(
|
||||
self,
|
||||
data_cache,
|
||||
self.default_class,
|
||||
resource_fs,
|
||||
render_to_string
|
||||
strict_error_handler,
|
||||
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
|
||||
Load a list of xmodules from the data in items, with children cached up
|
||||
to specified depth
|
||||
"""
|
||||
data_cache = self._cache_children(items, depth)
|
||||
|
||||
@@ -153,6 +171,14 @@ class MongoModuleStore(ModuleStore):
|
||||
course_filter = Location("i4x", category="course")
|
||||
return self.get_items(course_filter)
|
||||
|
||||
def _find_one(self, location):
|
||||
'''Look for a given location in the collection.
|
||||
If revision isn't specified, returns the latest.'''
|
||||
return self.collection.find_one(
|
||||
location_to_query(location),
|
||||
sort=[('revision', pymongo.ASCENDING)],
|
||||
)
|
||||
|
||||
def get_item(self, location, depth=0):
|
||||
"""
|
||||
Returns an XModuleDescriptor instance for the item at location.
|
||||
@@ -176,10 +202,7 @@ class MongoModuleStore(ModuleStore):
|
||||
if key != 'revision' and val is None:
|
||||
raise InsufficientSpecificationError(location)
|
||||
|
||||
item = self.collection.find_one(
|
||||
location_to_query(location),
|
||||
sort=[('revision', pymongo.ASCENDING)],
|
||||
)
|
||||
item = self._find_one(location)
|
||||
if item is None:
|
||||
raise ItemNotFoundError(location)
|
||||
return self._load_items([item], depth)[0]
|
||||
@@ -192,15 +215,22 @@ class MongoModuleStore(ModuleStore):
|
||||
|
||||
return self._load_items(list(items), depth)
|
||||
|
||||
# TODO (cpennington): This needs to be replaced by clone_item as soon as we allow
|
||||
# creation of items from the cms
|
||||
def create_item(self, location):
|
||||
"""
|
||||
Create an empty item at the specified location with the supplied editor
|
||||
Create an empty item at the specified location.
|
||||
|
||||
If that location already exists, raises a DuplicateItemError
|
||||
|
||||
location: Something that can be passed to Location
|
||||
"""
|
||||
self.collection.insert({
|
||||
'_id': Location(location).dict(),
|
||||
})
|
||||
try:
|
||||
self.collection.insert({
|
||||
'_id': Location(location).dict(),
|
||||
})
|
||||
except pymongo.errors.DuplicateKeyError:
|
||||
raise DuplicateItemError(location)
|
||||
|
||||
def update_item(self, location, data):
|
||||
"""
|
||||
@@ -237,7 +267,7 @@ class MongoModuleStore(ModuleStore):
|
||||
|
||||
def update_metadata(self, location, metadata):
|
||||
"""
|
||||
Set the children for the item specified by the location to
|
||||
Set the metadata for the item specified by the location to
|
||||
metadata
|
||||
|
||||
location: Something that can be passed to Location
|
||||
@@ -250,3 +280,98 @@ class MongoModuleStore(ModuleStore):
|
||||
{'_id': Location(location).dict()},
|
||||
{'$set': {'metadata': metadata}}
|
||||
)
|
||||
|
||||
def get_parent_locations(self, location):
|
||||
'''Find all locations that are the parents of this location.
|
||||
Mostly intended for use in path_to_location, but exposed for testing
|
||||
and possible other usefulness.
|
||||
|
||||
returns an iterable of things that can be passed to Location.
|
||||
'''
|
||||
location = Location(location)
|
||||
items = self.collection.find({'definition.children': str(location)},
|
||||
{'_id': True})
|
||||
return [i['_id'] for i in items]
|
||||
|
||||
def path_to_location(self, location, course_name=None):
|
||||
'''
|
||||
Try to find a course_id/chapter/section[/position] path to this location.
|
||||
The courseware insists that the first level in the course is chapter,
|
||||
but any kind of module can be a "section".
|
||||
|
||||
location: something that can be passed to Location
|
||||
course_name: [optional]. If not None, restrict search to paths
|
||||
in that course.
|
||||
|
||||
raise ItemNotFoundError if the location doesn't exist.
|
||||
|
||||
raise NoPathToItem if the location exists, but isn't accessible via
|
||||
a chapter/section path in the course(s) being searched.
|
||||
|
||||
Return a tuple (course_id, chapter, section, position) suitable for the
|
||||
courseware index view.
|
||||
|
||||
A location may be accessible via many paths. This method may
|
||||
return any valid path.
|
||||
|
||||
If the section is a sequence, position will be the position
|
||||
of this location in that sequence. Otherwise, position will
|
||||
be None. TODO (vshnayder): Not true yet.
|
||||
'''
|
||||
# Check that location is present at all
|
||||
if self._find_one(location) is None:
|
||||
raise ItemNotFoundError(location)
|
||||
|
||||
def flatten(xs):
|
||||
'''Convert lisp-style (a, (b, (c, ()))) lists into a python list.
|
||||
Not a general flatten function. '''
|
||||
p = []
|
||||
while xs != ():
|
||||
p.append(xs[0])
|
||||
xs = xs[1]
|
||||
return p
|
||||
|
||||
def find_path_to_course(location, course_name=None):
|
||||
'''Find a path up the location graph to a node with the
|
||||
specified category. If no path exists, return None. If a
|
||||
path exists, return it as a list with target location
|
||||
first, and the starting location last.
|
||||
'''
|
||||
# Standard DFS
|
||||
|
||||
# To keep track of where we came from, the work queue has
|
||||
# tuples (location, path-so-far). To avoid lots of
|
||||
# copying, the path-so-far is stored as a lisp-style
|
||||
# list--nested hd::tl tuples, and flattened at the end.
|
||||
queue = [(location, ())]
|
||||
while len(queue) > 0:
|
||||
(loc, path) = queue.pop() # Takes from the end
|
||||
loc = Location(loc)
|
||||
# print 'Processing loc={0}, path={1}'.format(loc, path)
|
||||
if loc.category == "course":
|
||||
if course_name is None or course_name == loc.name:
|
||||
# Found it!
|
||||
path = (loc, path)
|
||||
return flatten(path)
|
||||
|
||||
# otherwise, add parent locations at the end
|
||||
newpath = (loc, path)
|
||||
parents = self.get_parent_locations(loc)
|
||||
queue.extend(zip(parents, repeat(newpath)))
|
||||
|
||||
# If we're here, there is no path
|
||||
return None
|
||||
|
||||
path = find_path_to_course(location, course_name)
|
||||
if path is None:
|
||||
raise(NoPathToItem(location))
|
||||
|
||||
n = len(path)
|
||||
course_id = CourseDescriptor.location_to_id(path[0])
|
||||
chapter = path[1].name if n > 1 else None
|
||||
section = path[2].name if n > 2 else None
|
||||
|
||||
# TODO (vshnayder): not handling position at all yet...
|
||||
position = None
|
||||
|
||||
return (course_id, chapter, section, position)
|
||||
|
||||
@@ -13,14 +13,51 @@ def test_string_roundtrip():
|
||||
check_string_roundtrip("tag://org/course/category/name/revision")
|
||||
|
||||
|
||||
input_dict = {
|
||||
'tag': 'tag',
|
||||
'course': 'course',
|
||||
'category': 'category',
|
||||
'name': 'name',
|
||||
'org': 'org'
|
||||
}
|
||||
|
||||
input_list = ['tag', 'org', 'course', 'category', 'name']
|
||||
|
||||
input_str = "tag://org/course/category/name"
|
||||
input_str_rev = "tag://org/course/category/name/revision"
|
||||
|
||||
valid = (input_list, input_dict, input_str, input_str_rev)
|
||||
|
||||
invalid_dict = {
|
||||
'tag': 'tag',
|
||||
'course': 'course',
|
||||
'category': 'category',
|
||||
'name': 'name/more_name',
|
||||
'org': 'org'
|
||||
}
|
||||
|
||||
invalid_dict2 = {
|
||||
'tag': 'tag',
|
||||
'course': 'course',
|
||||
'category': 'category',
|
||||
'name': 'name ', # extra space
|
||||
'org': 'org'
|
||||
}
|
||||
|
||||
invalid = ("foo", ["foo"], ["foo", "bar"],
|
||||
["foo", "bar", "baz", "blat", "foo/bar"],
|
||||
"tag://org/course/category/name with spaces/revision",
|
||||
invalid_dict,
|
||||
invalid_dict2)
|
||||
|
||||
def test_is_valid():
|
||||
for v in valid:
|
||||
assert_equals(Location.is_valid(v), True)
|
||||
|
||||
for v in invalid:
|
||||
assert_equals(Location.is_valid(v), False)
|
||||
|
||||
def test_dict():
|
||||
input_dict = {
|
||||
'tag': 'tag',
|
||||
'course': 'course',
|
||||
'category': 'category',
|
||||
'name': 'name',
|
||||
'org': 'org'
|
||||
}
|
||||
assert_equals("tag://org/course/category/name", Location(input_dict).url())
|
||||
assert_equals(dict(revision=None, **input_dict), Location(input_dict).dict())
|
||||
|
||||
@@ -30,7 +67,6 @@ def test_dict():
|
||||
|
||||
|
||||
def test_list():
|
||||
input_list = ['tag', 'org', 'course', 'category', 'name']
|
||||
assert_equals("tag://org/course/category/name", Location(input_list).url())
|
||||
assert_equals(input_list + [None], Location(input_list).list())
|
||||
|
||||
@@ -65,3 +101,13 @@ def test_equality():
|
||||
Location('tag', 'org', 'course', 'category', 'name1'),
|
||||
Location('tag', 'org', 'course', 'category', 'name')
|
||||
)
|
||||
|
||||
def test_clean():
|
||||
pairs = [ ('',''),
|
||||
(' ', '_'),
|
||||
('abc,', 'abc_'),
|
||||
('ab fg!@//\\aj', 'ab_fg_aj'),
|
||||
(u"ab\xA9", "ab_"), # no unicode allowed for now
|
||||
]
|
||||
for input, output in pairs:
|
||||
assert_equals(Location.clean(input), output)
|
||||
|
||||
136
common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py
Normal file
@@ -0,0 +1,136 @@
|
||||
import pymongo
|
||||
|
||||
from nose.tools import assert_equals, assert_raises, assert_not_equals, with_setup
|
||||
from path import path
|
||||
from pprint import pprint
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem
|
||||
from xmodule.modulestore.mongo import MongoModuleStore
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
|
||||
# from ~/mitx_all/mitx/common/lib/xmodule/xmodule/modulestore/tests/
|
||||
# to ~/mitx_all/mitx/common/test
|
||||
TEST_DIR = path(__file__).abspath().dirname()
|
||||
for i in range(5):
|
||||
TEST_DIR = TEST_DIR.dirname()
|
||||
TEST_DIR = TEST_DIR / 'test'
|
||||
|
||||
DATA_DIR = TEST_DIR / 'data'
|
||||
|
||||
|
||||
HOST = 'localhost'
|
||||
PORT = 27017
|
||||
DB = 'test'
|
||||
COLLECTION = 'modulestore'
|
||||
FS_ROOT = DATA_DIR # TODO (vshnayder): will need a real fs_root for testing load_item
|
||||
DEFAULT_CLASS = 'xmodule.raw_module.RawDescriptor'
|
||||
|
||||
|
||||
class TestMongoModuleStore(object):
|
||||
|
||||
@classmethod
|
||||
def setupClass(cls):
|
||||
cls.connection = pymongo.connection.Connection(HOST, PORT)
|
||||
cls.connection.drop_database(DB)
|
||||
|
||||
# NOTE: Creating a single db for all the tests to save time. This
|
||||
# is ok only as long as none of the tests modify the db.
|
||||
# If (when!) that changes, need to either reload the db, or load
|
||||
# once and copy over to a tmp db for each test.
|
||||
cls.store = cls.initdb()
|
||||
|
||||
@classmethod
|
||||
def teardownClass(cls):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def initdb():
|
||||
# connect to the db
|
||||
store = MongoModuleStore(HOST, DB, COLLECTION, FS_ROOT, default_class=DEFAULT_CLASS)
|
||||
# Explicitly list the courses to load (don't want the big one)
|
||||
courses = ['toy', 'simple']
|
||||
import_from_xml(store, DATA_DIR, courses)
|
||||
return store
|
||||
|
||||
@staticmethod
|
||||
def destroy_db(connection):
|
||||
# Destroy the test db.
|
||||
connection.drop_database(DB)
|
||||
|
||||
def setUp(self):
|
||||
# make a copy for convenience
|
||||
self.connection = TestMongoModuleStore.connection
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_init(self):
|
||||
'''Make sure the db loads, and print all the locations in the db.
|
||||
Call this directly from failing tests to see what's loaded'''
|
||||
ids = list(self.connection[DB][COLLECTION].find({}, {'_id': True}))
|
||||
|
||||
pprint([Location(i['_id']).url() for i in ids])
|
||||
|
||||
def test_get_courses(self):
|
||||
'''Make sure the course objects loaded properly'''
|
||||
courses = self.store.get_courses()
|
||||
assert_equals(len(courses), 2)
|
||||
courses.sort(key=lambda c: c.id)
|
||||
assert_equals(courses[0].id, 'edX/simple/2012_Fall')
|
||||
assert_equals(courses[1].id, 'edX/toy/2012_Fall')
|
||||
|
||||
def test_loads(self):
|
||||
assert_not_equals(
|
||||
self.store.get_item("i4x://edX/toy/course/2012_Fall"),
|
||||
None)
|
||||
|
||||
assert_not_equals(
|
||||
self.store.get_item("i4x://edX/simple/course/2012_Fall"),
|
||||
None)
|
||||
|
||||
assert_not_equals(
|
||||
self.store.get_item("i4x://edX/toy/video/Welcome"),
|
||||
None)
|
||||
|
||||
|
||||
|
||||
def test_find_one(self):
|
||||
assert_not_equals(
|
||||
self.store._find_one(Location("i4x://edX/toy/course/2012_Fall")),
|
||||
None)
|
||||
|
||||
assert_not_equals(
|
||||
self.store._find_one(Location("i4x://edX/simple/course/2012_Fall")),
|
||||
None)
|
||||
|
||||
assert_not_equals(
|
||||
self.store._find_one(Location("i4x://edX/toy/video/Welcome")),
|
||||
None)
|
||||
|
||||
def test_path_to_location(self):
|
||||
'''Make sure that path_to_location works'''
|
||||
should_work = (
|
||||
("i4x://edX/toy/video/Welcome",
|
||||
("edX/toy/2012_Fall", "Overview", "Welcome", None)),
|
||||
("i4x://edX/toy/html/toylab",
|
||||
("edX/toy/2012_Fall", "Overview", "Toy_Videos", None)),
|
||||
)
|
||||
for location, expected in should_work:
|
||||
assert_equals(self.store.path_to_location(location), expected)
|
||||
|
||||
not_found = (
|
||||
"i4x://edX/toy/video/WelcomeX",
|
||||
)
|
||||
for location in not_found:
|
||||
assert_raises(ItemNotFoundError, self.store.path_to_location, location)
|
||||
|
||||
# Since our test files are valid, there shouldn't be any
|
||||
# elements with no path to them. But we can look for them in
|
||||
# another course.
|
||||
no_path = (
|
||||
"i4x://edX/simple/video/Lost_Video",
|
||||
)
|
||||
for location in no_path:
|
||||
assert_raises(NoPathToItem, self.store.path_to_location, location, "toy")
|
||||
|
||||
@@ -3,6 +3,7 @@ from fs.osfs import OSFS
|
||||
from importlib import import_module
|
||||
from lxml import etree
|
||||
from path import path
|
||||
from xmodule.errorhandlers import logging_error_handler
|
||||
from xmodule.x_module import XModuleDescriptor, XMLParsingSystem
|
||||
from xmodule.mako_module import MakoDescriptorSystem
|
||||
from cStringIO import StringIO
|
||||
@@ -12,153 +13,188 @@ import re
|
||||
from . import ModuleStore, Location
|
||||
from .exceptions import ItemNotFoundError
|
||||
|
||||
etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False,
|
||||
remove_comments=True, remove_blank_text=True))
|
||||
etree.set_default_parser(
|
||||
etree.XMLParser(dtd_validation=False, load_dtd=False,
|
||||
remove_comments=True, remove_blank_text=True))
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
|
||||
# TODO (cpennington): Remove this once all fall 2012 courses have been imported into the cms from xml
|
||||
# VS[compat]
|
||||
# TODO (cpennington): Remove this once all fall 2012 courses have been imported
|
||||
# into the cms from xml
|
||||
def clean_out_mako_templating(xml_string):
|
||||
xml_string = xml_string.replace('%include', 'include')
|
||||
xml_string = re.sub("(?m)^\s*%.*$", '', xml_string)
|
||||
return xml_string
|
||||
|
||||
class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
|
||||
def __init__(self, xmlstore, org, course, course_dir, error_handler):
|
||||
"""
|
||||
A class that handles loading from xml. Does some munging to ensure that
|
||||
all elements have unique slugs.
|
||||
|
||||
xmlstore: the XMLModuleStore to store the loaded modules in
|
||||
"""
|
||||
self.unnamed_modules = 0
|
||||
self.used_slugs = set()
|
||||
|
||||
def process_xml(xml):
|
||||
try:
|
||||
# VS[compat]
|
||||
# TODO (cpennington): Remove this once all fall 2012 courses
|
||||
# have been imported into the cms from xml
|
||||
xml = clean_out_mako_templating(xml)
|
||||
xml_data = etree.fromstring(xml)
|
||||
except:
|
||||
log.exception("Unable to parse xml: {xml}".format(xml=xml))
|
||||
raise
|
||||
|
||||
# VS[compat]. Take this out once course conversion is done
|
||||
if xml_data.get('slug') is None and xml_data.get('url_name') is None:
|
||||
if xml_data.get('name'):
|
||||
slug = Location.clean(xml_data.get('name'))
|
||||
elif xml_data.get('display_name'):
|
||||
slug = Location.clean(xml_data.get('display_name'))
|
||||
else:
|
||||
self.unnamed_modules += 1
|
||||
slug = '{tag}_{count}'.format(tag=xml_data.tag,
|
||||
count=self.unnamed_modules)
|
||||
|
||||
while slug in self.used_slugs:
|
||||
self.unnamed_modules += 1
|
||||
slug = '{slug}_{count}'.format(slug=slug,
|
||||
count=self.unnamed_modules)
|
||||
|
||||
self.used_slugs.add(slug)
|
||||
# log.debug('-> slug=%s' % slug)
|
||||
xml_data.set('url_name', slug)
|
||||
|
||||
module = XModuleDescriptor.load_from_xml(
|
||||
etree.tostring(xml_data), self, org,
|
||||
course, xmlstore.default_class)
|
||||
|
||||
#log.debug('==> importing module location %s' % repr(module.location))
|
||||
module.metadata['data_dir'] = course_dir
|
||||
|
||||
xmlstore.modules[module.location] = module
|
||||
|
||||
if xmlstore.eager:
|
||||
module.get_children()
|
||||
return module
|
||||
|
||||
render_template = lambda: ''
|
||||
load_item = xmlstore.get_item
|
||||
resources_fs = OSFS(xmlstore.data_dir / course_dir)
|
||||
|
||||
MakoDescriptorSystem.__init__(self, load_item, resources_fs,
|
||||
error_handler, render_template)
|
||||
XMLParsingSystem.__init__(self, load_item, resources_fs,
|
||||
error_handler, process_xml)
|
||||
|
||||
|
||||
|
||||
class XMLModuleStore(ModuleStore):
|
||||
"""
|
||||
An XML backed ModuleStore
|
||||
"""
|
||||
def __init__(self, data_dir, default_class=None, eager=False, course_dirs=None):
|
||||
def __init__(self, data_dir, default_class=None, eager=False,
|
||||
course_dirs=None,
|
||||
error_handler=logging_error_handler):
|
||||
"""
|
||||
Initialize an XMLModuleStore from data_dir
|
||||
|
||||
data_dir: path to data directory containing the course directories
|
||||
default_class: dot-separated string defining the default descriptor class to use if non is specified in entry_points
|
||||
eager: If true, load the modules children immediately to force the entire course tree to be parsed
|
||||
course_dirs: If specified, the list of course_dirs to load. Otherwise, load
|
||||
all course dirs
|
||||
|
||||
default_class: dot-separated string defining the default descriptor
|
||||
class to use if none is specified in entry_points
|
||||
|
||||
eager: If true, load the modules children immediately to force the
|
||||
entire course tree to be parsed
|
||||
|
||||
course_dirs: If specified, the list of course_dirs to load. Otherwise,
|
||||
load all course dirs
|
||||
|
||||
error_handler: The error handler used here and in the underlying
|
||||
DescriptorSystem. By default, raise exceptions for all errors.
|
||||
See the comments in x_module.py:DescriptorSystem
|
||||
"""
|
||||
|
||||
self.eager = eager
|
||||
self.data_dir = path(data_dir)
|
||||
self.modules = {} # location -> XModuleDescriptor
|
||||
self.courses = {} # course_dir -> XModuleDescriptor for the course
|
||||
self.error_handler = error_handler
|
||||
|
||||
if default_class is None:
|
||||
self.default_class = None
|
||||
else:
|
||||
module_path, _, class_name = default_class.rpartition('.')
|
||||
log.debug('module_path = %s' % module_path)
|
||||
#log.debug('module_path = %s' % module_path)
|
||||
class_ = getattr(import_module(module_path), class_name)
|
||||
self.default_class = class_
|
||||
|
||||
# TODO (cpennington): We need a better way of selecting specific sets of debug messages to enable. These were drowning out important messages
|
||||
# TODO (cpennington): We need a better way of selecting specific sets of
|
||||
# debug messages to enable. These were drowning out important messages
|
||||
#log.debug('XMLModuleStore: eager=%s, data_dir = %s' % (eager, self.data_dir))
|
||||
#log.debug('default_class = %s' % self.default_class)
|
||||
|
||||
for course_dir in os.listdir(self.data_dir):
|
||||
if course_dirs is not None and course_dir not in course_dirs:
|
||||
continue
|
||||
|
||||
if not os.path.exists(self.data_dir / course_dir / "course.xml"):
|
||||
continue
|
||||
# If we are specifically asked for missing courses, that should
|
||||
# be an error. If we are asked for "all" courses, find the ones
|
||||
# that have a course.xml
|
||||
if course_dirs is None:
|
||||
course_dirs = [d for d in os.listdir(self.data_dir) if
|
||||
os.path.exists(self.data_dir / d / "course.xml")]
|
||||
|
||||
for course_dir in course_dirs:
|
||||
try:
|
||||
course_descriptor = self.load_course(course_dir)
|
||||
self.courses[course_dir] = course_descriptor
|
||||
except:
|
||||
log.exception("Failed to load course %s" % course_dir)
|
||||
msg = "Failed to load course '%s'" % course_dir
|
||||
log.exception(msg)
|
||||
error_handler(msg)
|
||||
|
||||
|
||||
def load_course(self, course_dir):
|
||||
"""
|
||||
Load a course into this module store
|
||||
course_path: Course directory name
|
||||
|
||||
returns a CourseDescriptor for the course
|
||||
"""
|
||||
log.debug('========> Starting course import from {0}'.format(course_dir))
|
||||
|
||||
with open(self.data_dir / course_dir / "course.xml") as course_file:
|
||||
|
||||
# TODO (cpennington): Remove this once all fall 2012 courses have been imported
|
||||
# into the cms from xml
|
||||
# VS[compat]
|
||||
# TODO (cpennington): Remove this once all fall 2012 courses have
|
||||
# been imported into the cms from xml
|
||||
course_file = StringIO(clean_out_mako_templating(course_file.read()))
|
||||
|
||||
course_data = etree.parse(course_file).getroot()
|
||||
org = course_data.get('org')
|
||||
|
||||
if org is None:
|
||||
log.error(
|
||||
"No 'org' attribute set for course in {dir}. Using default 'edx'".format(
|
||||
dir=course_dir))
|
||||
log.error("No 'org' attribute set for course in {dir}. "
|
||||
"Using default 'edx'".format(dir=course_dir))
|
||||
org = 'edx'
|
||||
|
||||
course = course_data.get('course')
|
||||
|
||||
if course is None:
|
||||
log.error(
|
||||
"No 'course' attribute set for course in {dir}. Using default '{default}'".format(
|
||||
dir=course_dir,
|
||||
default=course_dir
|
||||
))
|
||||
log.error("No 'course' attribute set for course in {dir}."
|
||||
" Using default '{default}'".format(
|
||||
dir=course_dir,
|
||||
default=course_dir
|
||||
))
|
||||
course = course_dir
|
||||
|
||||
class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
|
||||
def __init__(self, xmlstore):
|
||||
"""
|
||||
xmlstore: the XMLModuleStore to store the loaded modules in
|
||||
"""
|
||||
self.unnamed_modules = 0
|
||||
self.used_slugs = set()
|
||||
system = ImportSystem(self, org, course, course_dir,
|
||||
self.error_handler)
|
||||
|
||||
def process_xml(xml):
|
||||
try:
|
||||
# TODO (cpennington): Remove this once all fall 2012 courses
|
||||
# have been imported into the cms from xml
|
||||
xml = clean_out_mako_templating(xml)
|
||||
xml_data = etree.fromstring(xml)
|
||||
except:
|
||||
log.exception("Unable to parse xml: {xml}".format(xml=xml))
|
||||
raise
|
||||
if xml_data.get('slug') is None:
|
||||
if xml_data.get('name'):
|
||||
slug = Location.clean(xml_data.get('name'))
|
||||
else:
|
||||
self.unnamed_modules += 1
|
||||
slug = '{tag}_{count}'.format(tag=xml_data.tag,
|
||||
count=self.unnamed_modules)
|
||||
|
||||
if slug in self.used_slugs:
|
||||
self.unnamed_modules += 1
|
||||
slug = '{slug}_{count}'.format(slug=slug,
|
||||
count=self.unnamed_modules)
|
||||
|
||||
self.used_slugs.add(slug)
|
||||
# log.debug('-> slug=%s' % slug)
|
||||
xml_data.set('slug', slug)
|
||||
|
||||
module = XModuleDescriptor.load_from_xml(
|
||||
etree.tostring(xml_data), self, org,
|
||||
course, xmlstore.default_class)
|
||||
log.debug('==> importing module location %s' % repr(module.location))
|
||||
module.metadata['data_dir'] = course_dir
|
||||
|
||||
xmlstore.modules[module.location] = module
|
||||
|
||||
if xmlstore.eager:
|
||||
module.get_children()
|
||||
return module
|
||||
|
||||
system_kwargs = dict(
|
||||
render_template=lambda: '',
|
||||
load_item=xmlstore.get_item,
|
||||
resources_fs=OSFS(xmlstore.data_dir / course_dir),
|
||||
process_xml=process_xml
|
||||
)
|
||||
MakoDescriptorSystem.__init__(self, **system_kwargs)
|
||||
XMLParsingSystem.__init__(self, **system_kwargs)
|
||||
|
||||
|
||||
course_descriptor = ImportSystem(self).process_xml(etree.tostring(course_data))
|
||||
log.debug('========> Done with course import')
|
||||
course_descriptor = system.process_xml(etree.tostring(course_data))
|
||||
log.debug('========> Done with course import from {0}'.format(course_dir))
|
||||
return course_descriptor
|
||||
|
||||
def get_item(self, location, depth=0):
|
||||
@@ -169,7 +205,9 @@ class XMLModuleStore(ModuleStore):
|
||||
|
||||
If any segment of the location is None except revision, raises
|
||||
xmodule.modulestore.exceptions.InsufficientSpecificationError
|
||||
If no object is found at that location, raises xmodule.modulestore.exceptions.ItemNotFoundError
|
||||
|
||||
If no object is found at that location, raises
|
||||
xmodule.modulestore.exceptions.ItemNotFoundError
|
||||
|
||||
location: Something that can be passed to Location
|
||||
"""
|
||||
|
||||
40
common/lib/xmodule/xmodule/modulestore/xml_importer.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import logging
|
||||
|
||||
from .xml import XMLModuleStore
|
||||
from .exceptions import DuplicateItemError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def import_from_xml(store, data_dir, course_dirs=None, eager=True,
|
||||
default_class='xmodule.raw_module.RawDescriptor'):
|
||||
"""
|
||||
Import the specified xml data_dir into the "store" modulestore,
|
||||
using org and course as the location org and course.
|
||||
|
||||
course_dirs: If specified, the list of course_dirs to load. Otherwise, load
|
||||
all course dirs
|
||||
|
||||
"""
|
||||
module_store = XMLModuleStore(
|
||||
data_dir,
|
||||
default_class=default_class,
|
||||
eager=eager,
|
||||
course_dirs=course_dirs
|
||||
)
|
||||
for module in module_store.modules.itervalues():
|
||||
|
||||
# TODO (cpennington): This forces import to overrite the same items.
|
||||
# This should in the future create new revisions of the items on import
|
||||
try:
|
||||
store.create_item(module.location)
|
||||
except DuplicateItemError:
|
||||
log.exception('Item already exists at %s' % module.location.url())
|
||||
pass
|
||||
if 'data' in module.definition:
|
||||
store.update_item(module.location, module.definition['data'])
|
||||
if 'children' in module.definition:
|
||||
store.update_children(module.location, module.definition['children'])
|
||||
store.update_metadata(module.location, dict(module.metadata))
|
||||
|
||||
return module_store
|
||||
@@ -6,9 +6,10 @@ import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RawDescriptor(MakoModuleDescriptor, XmlDescriptor):
|
||||
"""
|
||||
Module that provides a raw editing view of it's data and children
|
||||
Module that provides a raw editing view of its data and children
|
||||
"""
|
||||
mako_template = "widgets/raw-edit.html"
|
||||
|
||||
@@ -31,8 +32,11 @@ class RawDescriptor(MakoModuleDescriptor, XmlDescriptor):
|
||||
except etree.XMLSyntaxError as err:
|
||||
lines = self.definition['data'].split('\n')
|
||||
line, offset = err.position
|
||||
log.exception("Unable to create xml for problem {loc}. Context: '{context}'".format(
|
||||
context=lines[line-1][offset - 40:offset + 40],
|
||||
loc=self.location
|
||||
))
|
||||
msg = ("Unable to create xml for problem {loc}. "
|
||||
"Context: '{context}'".format(
|
||||
context=lines[line - 1][offset - 40:offset + 40],
|
||||
loc=self.location))
|
||||
log.exception(msg)
|
||||
self.system.error_handler(msg)
|
||||
# no workaround possible, so just re-raise
|
||||
raise
|
||||
|
||||
@@ -20,12 +20,15 @@ class_priority = ['video', 'problem']
|
||||
class SequenceModule(XModule):
|
||||
''' Layout module which lays out content in a temporal sequence
|
||||
'''
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/sequence/display.coffee')]}
|
||||
js = {'coffee': [resource_string(__name__,
|
||||
'js/src/sequence/display.coffee')]}
|
||||
css = {'scss': [resource_string(__name__, 'css/sequence/display.scss')]}
|
||||
js_module_name = "Sequence"
|
||||
|
||||
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
|
||||
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)
|
||||
def __init__(self, system, location, definition, instance_state=None,
|
||||
shared_state=None, **kwargs):
|
||||
XModule.__init__(self, system, location, definition,
|
||||
instance_state, shared_state, **kwargs)
|
||||
self.position = 1
|
||||
|
||||
if instance_state is not None:
|
||||
@@ -92,7 +95,8 @@ class SequenceModule(XModule):
|
||||
self.rendered = True
|
||||
|
||||
def get_icon_class(self):
|
||||
child_classes = set(child.get_icon_class() for child in self.get_children())
|
||||
child_classes = set(child.get_icon_class()
|
||||
for child in self.get_children())
|
||||
new_class = 'other'
|
||||
for c in class_priority:
|
||||
if c in child_classes:
|
||||
@@ -114,5 +118,20 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
|
||||
def definition_to_xml(self, resource_fs):
|
||||
xml_object = etree.Element('sequential')
|
||||
for child in self.get_children():
|
||||
xml_object.append(etree.fromstring(child.export_to_xml(resource_fs)))
|
||||
xml_object.append(
|
||||
etree.fromstring(child.export_to_xml(resource_fs)))
|
||||
return xml_object
|
||||
|
||||
@classmethod
|
||||
def split_to_file(cls, xml_object):
|
||||
# Note: if we end up needing subclasses, can port this logic there.
|
||||
yes = ('chapter',)
|
||||
no = ('course',)
|
||||
|
||||
if xml_object.tag in yes:
|
||||
return True
|
||||
elif xml_object.tag in no:
|
||||
return False
|
||||
|
||||
# otherwise maybe--delegate to superclass.
|
||||
return XmlDescriptor.split_to_file(xml_object)
|
||||
|
||||
@@ -21,19 +21,23 @@ class CustomTagModule(XModule):
|
||||
|
||||
course.xml::
|
||||
...
|
||||
<customtag page="234"><impl>book</impl></customtag>
|
||||
<customtag page="234" impl="book"/>
|
||||
...
|
||||
|
||||
Renders to::
|
||||
More information given in <a href="/book/234">the text</a>
|
||||
"""
|
||||
|
||||
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
|
||||
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)
|
||||
def __init__(self, system, location, definition,
|
||||
instance_state=None, shared_state=None, **kwargs):
|
||||
XModule.__init__(self, system, location, definition,
|
||||
instance_state, shared_state, **kwargs)
|
||||
|
||||
xmltree = etree.fromstring(self.definition['data'])
|
||||
template_name = xmltree.find('impl').text
|
||||
template_name = xmltree.attrib['impl']
|
||||
params = dict(xmltree.items())
|
||||
with self.system.filestore.open('custom_tags/{name}'.format(name=template_name)) as template:
|
||||
with self.system.filestore.open(
|
||||
'custom_tags/{name}'.format(name=template_name)) as template:
|
||||
self.html = Template(template.read()).render(**params)
|
||||
|
||||
def get_html(self):
|
||||
|
||||
@@ -60,7 +60,7 @@ class VideoModule(XModule):
|
||||
return None
|
||||
|
||||
def get_instance_state(self):
|
||||
log.debug(u"STATE POSITION {0}".format(self.position))
|
||||
#log.debug(u"STATE POSITION {0}".format(self.position))
|
||||
return json.dumps({'position': self.position})
|
||||
|
||||
def video_list(self):
|
||||
|
||||
@@ -3,6 +3,7 @@ import pkg_resources
|
||||
import logging
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
from functools import partial
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
@@ -31,23 +32,28 @@ class Plugin(object):
|
||||
def load_class(cls, identifier, default=None):
|
||||
"""
|
||||
Loads a single class instance specified by identifier. If identifier
|
||||
specifies more than a single class, then logs a warning and returns the first
|
||||
class identified.
|
||||
specifies more than a single class, then logs a warning and returns the
|
||||
first class identified.
|
||||
|
||||
If default is not None, will return default if no entry_point matching identifier
|
||||
is found. Otherwise, will raise a ModuleMissingError
|
||||
If default is not None, will return default if no entry_point matching
|
||||
identifier is found. Otherwise, will raise a ModuleMissingError
|
||||
"""
|
||||
if cls._plugin_cache is None:
|
||||
cls._plugin_cache = {}
|
||||
|
||||
if identifier not in cls._plugin_cache:
|
||||
identifier = identifier.lower()
|
||||
classes = list(pkg_resources.iter_entry_points(cls.entry_point, name=identifier))
|
||||
classes = list(pkg_resources.iter_entry_points(
|
||||
cls.entry_point, name=identifier))
|
||||
|
||||
if len(classes) > 1:
|
||||
log.warning("Found multiple classes for {entry_point} with identifier {id}: {classes}. Returning the first one.".format(
|
||||
log.warning("Found multiple classes for {entry_point} with "
|
||||
"identifier {id}: {classes}. "
|
||||
"Returning the first one.".format(
|
||||
entry_point=cls.entry_point,
|
||||
id=identifier,
|
||||
classes=", ".join(class_.module_name for class_ in classes)))
|
||||
classes=", ".join(
|
||||
class_.module_name for class_ in classes)))
|
||||
|
||||
if len(classes) == 0:
|
||||
if default is not None:
|
||||
@@ -79,9 +85,12 @@ class HTMLSnippet(object):
|
||||
def get_javascript(cls):
|
||||
"""
|
||||
Return a dictionary containing some of the following keys:
|
||||
|
||||
coffee: A list of coffeescript fragments that should be compiled and
|
||||
placed on the page
|
||||
js: A list of javascript fragments that should be included on the page
|
||||
|
||||
js: A list of javascript fragments that should be included on the
|
||||
page
|
||||
|
||||
All of these will be loaded onto the page in the CMS
|
||||
"""
|
||||
@@ -91,12 +100,15 @@ class HTMLSnippet(object):
|
||||
def get_css(cls):
|
||||
"""
|
||||
Return a dictionary containing some of the following keys:
|
||||
css: A list of css fragments that should be applied to the html contents
|
||||
of the snippet
|
||||
sass: A list of sass fragments that should be applied to the html contents
|
||||
of the snippet
|
||||
scss: A list of scss fragments that should be applied to the html contents
|
||||
of the snippet
|
||||
|
||||
css: A list of css fragments that should be applied to the html
|
||||
contents of the snippet
|
||||
|
||||
sass: A list of sass fragments that should be applied to the html
|
||||
contents of the snippet
|
||||
|
||||
scss: A list of scss fragments that should be applied to the html
|
||||
contents of the snippet
|
||||
"""
|
||||
return cls.css
|
||||
|
||||
@@ -104,47 +116,70 @@ class HTMLSnippet(object):
|
||||
"""
|
||||
Return the html used to display this snippet
|
||||
"""
|
||||
raise NotImplementedError("get_html() must be provided by specific modules - not present in {0}"
|
||||
raise NotImplementedError(
|
||||
"get_html() must be provided by specific modules - not present in {0}"
|
||||
.format(self.__class__))
|
||||
|
||||
|
||||
class XModule(HTMLSnippet):
|
||||
''' Implements a generic learning module.
|
||||
|
||||
Subclasses must at a minimum provide a definition for get_html in order to be displayed to users.
|
||||
Subclasses must at a minimum provide a definition for get_html in order
|
||||
to be displayed to users.
|
||||
|
||||
See the HTML module for a simple example.
|
||||
'''
|
||||
|
||||
# The default implementation of get_icon_class returns the icon_class attribute of the class
|
||||
# This attribute can be overridden by subclasses, and the function can also be overridden
|
||||
# if the icon class depends on the data in the module
|
||||
# The default implementation of get_icon_class returns the icon_class
|
||||
# attribute of the class
|
||||
#
|
||||
# This attribute can be overridden by subclasses, and
|
||||
# the function can also be overridden if the icon class depends on the data
|
||||
# in the module
|
||||
icon_class = 'other'
|
||||
|
||||
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
|
||||
def __init__(self, system, location, definition,
|
||||
instance_state=None, shared_state=None, **kwargs):
|
||||
'''
|
||||
Construct a new xmodule
|
||||
|
||||
system: A ModuleSystem allowing access to external resources
|
||||
|
||||
location: Something Location-like that identifies this xmodule
|
||||
definition: A dictionary containing 'data' and 'children'. Both are optional
|
||||
'data': is JSON-like (string, dictionary, list, bool, or None, optionally nested).
|
||||
This defines all of the data necessary for a problem to display that is intrinsic to the problem.
|
||||
It should not include any data that would vary between two courses using the same problem
|
||||
|
||||
definition: A dictionary containing 'data' and 'children'. Both are
|
||||
optional
|
||||
|
||||
'data': is JSON-like (string, dictionary, list, bool, or None,
|
||||
optionally nested).
|
||||
|
||||
This defines all of the data necessary for a problem to display
|
||||
that is intrinsic to the problem. It should not include any
|
||||
data that would vary between two courses using the same problem
|
||||
(due dates, grading policy, randomization, etc.)
|
||||
'children': is a list of Location-like values for child modules that this module depends on
|
||||
instance_state: A string of serialized json that contains the state of this module for
|
||||
current student accessing the system, or None if no state has been saved
|
||||
shared_state: A string of serialized json that contains the state that is shared between
|
||||
this module and any modules of the same type with the same shared_state_key. This
|
||||
state is only shared per-student, not across different students
|
||||
kwargs: Optional arguments. Subclasses should always accept kwargs and pass them
|
||||
to the parent class constructor.
|
||||
|
||||
'children': is a list of Location-like values for child modules that
|
||||
this module depends on
|
||||
|
||||
instance_state: A string of serialized json that contains the state of
|
||||
this module for current student accessing the system, or None if
|
||||
no state has been saved
|
||||
|
||||
shared_state: A string of serialized json that contains the state that
|
||||
is shared between this module and any modules of the same type with
|
||||
the same shared_state_key. This state is only shared per-student,
|
||||
not across different students
|
||||
|
||||
kwargs: Optional arguments. Subclasses should always accept kwargs and
|
||||
pass them to the parent class constructor.
|
||||
|
||||
Current known uses of kwargs:
|
||||
metadata: SCAFFOLDING - This dictionary will be split into several different types of metadata
|
||||
in the future (course policy, modification history, etc).
|
||||
A dictionary containing data that specifies information that is particular
|
||||
to a problem in the context of a course
|
||||
|
||||
metadata: SCAFFOLDING - This dictionary will be split into
|
||||
several different types of metadata in the future (course
|
||||
policy, modification history, etc). A dictionary containing
|
||||
data that specifies information that is particular to a
|
||||
problem in the context of a course
|
||||
'''
|
||||
self.system = system
|
||||
self.location = Location(location)
|
||||
@@ -158,24 +193,23 @@ class XModule(HTMLSnippet):
|
||||
self._loaded_children = None
|
||||
|
||||
def get_name(self):
|
||||
name = self.__xmltree.get('name')
|
||||
if name:
|
||||
return name
|
||||
else:
|
||||
raise "We should iterate through children and find a default name"
|
||||
return self.name
|
||||
|
||||
def get_children(self):
|
||||
'''
|
||||
Return module instances for all the children of this module.
|
||||
'''
|
||||
if self._loaded_children is None:
|
||||
self._loaded_children = [self.system.get_module(child) for child in self.definition.get('children', [])]
|
||||
self._loaded_children = [
|
||||
self.system.get_module(child)
|
||||
for child in self.definition.get('children', [])]
|
||||
|
||||
return self._loaded_children
|
||||
|
||||
def get_display_items(self):
|
||||
'''
|
||||
Returns a list of descendent module instances that will display immediately
|
||||
inside this module
|
||||
Returns a list of descendent module instances that will display
|
||||
immediately inside this module
|
||||
'''
|
||||
items = []
|
||||
for child in self.get_children():
|
||||
@@ -185,8 +219,8 @@ class XModule(HTMLSnippet):
|
||||
|
||||
def displayable_items(self):
|
||||
'''
|
||||
Returns list of displayable modules contained by this module. If this module
|
||||
is visible, should return [self]
|
||||
Returns list of displayable modules contained by this module. If this
|
||||
module is visible, should return [self]
|
||||
'''
|
||||
return [self]
|
||||
|
||||
@@ -217,16 +251,21 @@ class XModule(HTMLSnippet):
|
||||
|
||||
def max_score(self):
|
||||
''' Maximum score. Two notes:
|
||||
* This is generic; in abstract, a problem could be 3/5 points on one randomization, and 5/7 on another
|
||||
* In practice, this is a Very Bad Idea, and (a) will break some code in place (although that code
|
||||
should get fixed), and (b) break some analytics we plan to put in place.
|
||||
|
||||
* This is generic; in abstract, a problem could be 3/5 points on one
|
||||
randomization, and 5/7 on another
|
||||
|
||||
* In practice, this is a Very Bad Idea, and (a) will break some code
|
||||
in place (although that code should get fixed), and (b) break some
|
||||
analytics we plan to put in place.
|
||||
'''
|
||||
return None
|
||||
|
||||
def get_progress(self):
|
||||
''' Return a progress.Progress object that represents how far the student has gone
|
||||
in this module. Must be implemented to get correct progress tracking behavior in
|
||||
nesting modules like sequence and vertical.
|
||||
''' Return a progress.Progress object that represents how far the
|
||||
student has gone in this module. Must be implemented to get correct
|
||||
progress tracking behavior in nesting modules like sequence and
|
||||
vertical.
|
||||
|
||||
If this module has no notion of progress, return None.
|
||||
'''
|
||||
@@ -240,13 +279,14 @@ class XModule(HTMLSnippet):
|
||||
|
||||
class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
"""
|
||||
An XModuleDescriptor is a specification for an element of a course. This could
|
||||
be a problem, an organizational element (a group of content), or a segment of video,
|
||||
for example.
|
||||
An XModuleDescriptor is a specification for an element of a course. This
|
||||
could be a problem, an organizational element (a group of content), or a
|
||||
segment of video, for example.
|
||||
|
||||
XModuleDescriptors are independent and agnostic to the current student state on a
|
||||
problem. They handle the editing interface used by instructors to create a problem,
|
||||
and can generate XModules (which do know about student state).
|
||||
XModuleDescriptors are independent and agnostic to the current student state
|
||||
on a problem. They handle the editing interface used by instructors to
|
||||
create a problem, and can generate XModules (which do know about student
|
||||
state).
|
||||
"""
|
||||
entry_point = "xmodule.v1"
|
||||
module_class = XModule
|
||||
@@ -255,46 +295,58 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
inheritable_metadata = (
|
||||
'graded', 'start', 'due', 'graceperiod', 'showanswer', 'rerandomize',
|
||||
|
||||
# This is used by the XMLModuleStore to provide for locations for static files,
|
||||
# and will need to be removed when that code is removed
|
||||
# TODO: This is used by the XMLModuleStore to provide for locations for
|
||||
# static files, and will need to be removed when that code is removed
|
||||
'data_dir'
|
||||
)
|
||||
|
||||
# A list of descriptor attributes that must be equal for the descriptors to be
|
||||
# equal
|
||||
equality_attributes = ('definition', 'metadata', 'location', 'shared_state_key', '_inherited_metadata')
|
||||
# A list of descriptor attributes that must be equal for the descriptors to
|
||||
# be equal
|
||||
equality_attributes = ('definition', 'metadata', 'location',
|
||||
'shared_state_key', '_inherited_metadata')
|
||||
|
||||
# ============================= STRUCTURAL MANIPULATION ===========================
|
||||
# ============================= STRUCTURAL MANIPULATION ===================
|
||||
def __init__(self,
|
||||
system,
|
||||
definition=None,
|
||||
**kwargs):
|
||||
"""
|
||||
Construct a new XModuleDescriptor. The only required arguments are the
|
||||
system, used for interaction with external resources, and the definition,
|
||||
which specifies all the data needed to edit and display the problem (but none
|
||||
of the associated metadata that handles recordkeeping around the problem).
|
||||
system, used for interaction with external resources, and the
|
||||
definition, which specifies all the data needed to edit and display the
|
||||
problem (but none of the associated metadata that handles recordkeeping
|
||||
around the problem).
|
||||
|
||||
This allows for maximal flexibility to add to the interface while preserving
|
||||
backwards compatibility.
|
||||
This allows for maximal flexibility to add to the interface while
|
||||
preserving backwards compatibility.
|
||||
|
||||
system: An XModuleSystem for interacting with external resources
|
||||
definition: A dict containing `data` and `children` representing the problem definition
|
||||
system: A DescriptorSystem for interacting with external resources
|
||||
|
||||
definition: A dict containing `data` and `children` representing the
|
||||
problem definition
|
||||
|
||||
Current arguments passed in kwargs:
|
||||
location: A xmodule.modulestore.Location object indicating the name and ownership of this problem
|
||||
shared_state_key: The key to use for sharing StudentModules with other
|
||||
modules of this type
|
||||
|
||||
location: A xmodule.modulestore.Location object indicating the name
|
||||
and ownership of this problem
|
||||
|
||||
shared_state_key: The key to use for sharing StudentModules with
|
||||
other modules of this type
|
||||
|
||||
metadata: A dictionary containing the following optional keys:
|
||||
goals: A list of strings of learning goals associated with this module
|
||||
display_name: The name to use for displaying this module to the user
|
||||
goals: A list of strings of learning goals associated with this
|
||||
module
|
||||
display_name: The name to use for displaying this module to the
|
||||
user
|
||||
format: The format of this module ('Homework', 'Lab', etc)
|
||||
graded (bool): Whether this module is should be graded or not
|
||||
start (string): The date for which this module will be available
|
||||
due (string): The due date for this module
|
||||
graceperiod (string): The amount of grace period to allow when enforcing the due date
|
||||
graceperiod (string): The amount of grace period to allow when
|
||||
enforcing the due date
|
||||
showanswer (string): When to show answers for this module
|
||||
rerandomize (string): When to generate a newly randomized instance of the module data
|
||||
rerandomize (string): When to generate a newly randomized
|
||||
instance of the module data
|
||||
"""
|
||||
self.system = system
|
||||
self.metadata = kwargs.get('metadata', {})
|
||||
@@ -321,7 +373,8 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
self.metadata[attr] = metadata[attr]
|
||||
|
||||
def get_children(self):
|
||||
"""Returns a list of XModuleDescriptor instances for the children of this module"""
|
||||
"""Returns a list of XModuleDescriptor instances for the children of
|
||||
this module"""
|
||||
if self._child_instances is None:
|
||||
self._child_instances = []
|
||||
for child_loc in self.definition.get('children', []):
|
||||
@@ -333,8 +386,9 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
|
||||
def xmodule_constructor(self, system):
|
||||
"""
|
||||
Returns a constructor for an XModule. This constructor takes two arguments:
|
||||
instance_state and shared_state, and returns a fully nstantiated XModule
|
||||
Returns a constructor for an XModule. This constructor takes two
|
||||
arguments: instance_state and shared_state, and returns a fully
|
||||
instantiated XModule
|
||||
"""
|
||||
return partial(
|
||||
self.module_class,
|
||||
@@ -344,7 +398,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
metadata=self.metadata
|
||||
)
|
||||
|
||||
# ================================= JSON PARSING ===================================
|
||||
# ================================= JSON PARSING ===========================
|
||||
@staticmethod
|
||||
def load_from_json(json_data, system, default_class=None):
|
||||
"""
|
||||
@@ -366,13 +420,14 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
Creates an instance of this descriptor from the supplied json_data.
|
||||
This may be overridden by subclasses
|
||||
|
||||
json_data: A json object specifying the definition and any optional keyword arguments for
|
||||
the XModuleDescriptor
|
||||
system: An XModuleSystem for interacting with external resources
|
||||
json_data: A json object specifying the definition and any optional
|
||||
keyword arguments for the XModuleDescriptor
|
||||
|
||||
system: A DescriptorSystem for interacting with external resources
|
||||
"""
|
||||
return cls(system=system, **json_data)
|
||||
|
||||
# ================================= XML PARSING ====================================
|
||||
# ================================= XML PARSING ============================
|
||||
@staticmethod
|
||||
def load_from_xml(xml_data,
|
||||
system,
|
||||
@@ -384,16 +439,19 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
on the contents of xml_data.
|
||||
|
||||
xml_data must be a string containing valid xml
|
||||
|
||||
system is an XMLParsingSystem
|
||||
org and course are optional strings that will be used in the generated modules
|
||||
url identifiers
|
||||
|
||||
org and course are optional strings that will be used in the generated
|
||||
modules url identifiers
|
||||
"""
|
||||
class_ = XModuleDescriptor.load_class(
|
||||
etree.fromstring(xml_data).tag,
|
||||
default_class
|
||||
)
|
||||
# leave next line in code, commented out - useful for low-level debugging
|
||||
# log.debug('[XModuleDescriptor.load_from_xml] tag=%s, class_=%s' % (etree.fromstring(xml_data).tag,class_))
|
||||
# leave next line, commented out - useful for low-level debugging
|
||||
# log.debug('[XModuleDescriptor.load_from_xml] tag=%s, class_=%s' % (
|
||||
# etree.fromstring(xml_data).tag,class_))
|
||||
return class_.from_xml(xml_data, system, org, course)
|
||||
|
||||
@classmethod
|
||||
@@ -402,35 +460,42 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
Creates an instance of this descriptor from the supplied xml_data.
|
||||
This may be overridden by subclasses
|
||||
|
||||
xml_data: A string of xml that will be translated into data and children for
|
||||
this module
|
||||
xml_data: A string of xml that will be translated into data and children
|
||||
for this module
|
||||
|
||||
system is an XMLParsingSystem
|
||||
org and course are optional strings that will be used in the generated modules
|
||||
url identifiers
|
||||
|
||||
org and course are optional strings that will be used in the generated
|
||||
module's url identifiers
|
||||
"""
|
||||
raise NotImplementedError('Modules must implement from_xml to be parsable from xml')
|
||||
raise NotImplementedError(
|
||||
'Modules must implement from_xml to be parsable from xml')
|
||||
|
||||
def export_to_xml(self, resource_fs):
|
||||
"""
|
||||
Returns an xml string representing this module, and all modules underneath it.
|
||||
May also write required resources out to resource_fs
|
||||
Returns an xml string representing this module, and all modules
|
||||
underneath it. May also write required resources out to resource_fs
|
||||
|
||||
Assumes that modules have single parantage (that no module appears twice in the same course),
|
||||
and that it is thus safe to nest modules as xml children as appropriate.
|
||||
Assumes that modules have single parentage (that no module appears twice
|
||||
in the same course), and that it is thus safe to nest modules as xml
|
||||
children as appropriate.
|
||||
|
||||
The returned XML should be able to be parsed back into an identical XModuleDescriptor
|
||||
using the from_xml method with the same system, org, and course
|
||||
The returned XML should be able to be parsed back into an identical
|
||||
XModuleDescriptor using the from_xml method with the same system, org,
|
||||
and course
|
||||
"""
|
||||
raise NotImplementedError('Modules must implement export_to_xml to enable xml export')
|
||||
raise NotImplementedError(
|
||||
'Modules must implement export_to_xml to enable xml export')
|
||||
|
||||
# =============================== Testing ===================================
|
||||
# =============================== Testing ==================================
|
||||
def get_sample_state(self):
|
||||
"""
|
||||
Return a list of tuples of instance_state, shared_state. Each tuple defines a sample case for this module
|
||||
Return a list of tuples of instance_state, shared_state. Each tuple
|
||||
defines a sample case for this module
|
||||
"""
|
||||
return [('{}', '{}')]
|
||||
|
||||
# =============================== BUILTIN METHODS ===========================
|
||||
# =============================== BUILTIN METHODS ==========================
|
||||
def __eq__(self, other):
|
||||
eq = (self.__class__ == other.__class__ and
|
||||
all(getattr(self, attr, None) == getattr(other, attr, None)
|
||||
@@ -438,38 +503,72 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
|
||||
if not eq:
|
||||
for attr in self.equality_attributes:
|
||||
print getattr(self, attr, None), getattr(other, attr, None), getattr(self, attr, None) == getattr(other, attr, None)
|
||||
print(getattr(self, attr, None),
|
||||
getattr(other, attr, None),
|
||||
getattr(self, attr, None) == getattr(other, attr, None))
|
||||
|
||||
return eq
|
||||
|
||||
def __repr__(self):
|
||||
return "{class_}({system!r}, {definition!r}, location={location!r}, metadata={metadata!r})".format(
|
||||
return ("{class_}({system!r}, {definition!r}, location={location!r},"
|
||||
" metadata={metadata!r})".format(
|
||||
class_=self.__class__.__name__,
|
||||
system=self.system,
|
||||
definition=self.definition,
|
||||
location=self.location,
|
||||
metadata=self.metadata
|
||||
)
|
||||
))
|
||||
|
||||
|
||||
class DescriptorSystem(object):
|
||||
def __init__(self, load_item, resources_fs, **kwargs):
|
||||
def __init__(self, load_item, resources_fs, error_handler):
|
||||
"""
|
||||
load_item: Takes a Location and returns an XModuleDescriptor
|
||||
|
||||
resources_fs: A Filesystem object that contains all of the
|
||||
resources needed for the course
|
||||
|
||||
error_handler: A hook for handling errors in loading the descriptor.
|
||||
Must be a function of (error_msg, exc_info=None).
|
||||
See errorhandlers.py for some simple ones.
|
||||
|
||||
Patterns for using the error handler:
|
||||
try:
|
||||
x = access_some_resource()
|
||||
check_some_format(x)
|
||||
except SomeProblem:
|
||||
msg = 'Grommet {0} is broken'.format(x)
|
||||
log.exception(msg) # don't rely on handler to log
|
||||
self.system.error_handler(msg)
|
||||
# if we get here, work around if possible
|
||||
raise # if no way to work around
|
||||
OR
|
||||
return 'Oops, couldn't load grommet'
|
||||
|
||||
OR, if not in an exception context:
|
||||
|
||||
if not check_something(thingy):
|
||||
msg = "thingy {0} is broken".format(thingy)
|
||||
log.critical(msg)
|
||||
error_handler(msg)
|
||||
# if we get here, work around
|
||||
pass # e.g. if no workaround needed
|
||||
"""
|
||||
|
||||
self.load_item = load_item
|
||||
self.resources_fs = resources_fs
|
||||
self.error_handler = error_handler
|
||||
|
||||
|
||||
class XMLParsingSystem(DescriptorSystem):
|
||||
def __init__(self, load_item, resources_fs, process_xml, **kwargs):
|
||||
def __init__(self, load_item, resources_fs, error_handler, process_xml):
|
||||
"""
|
||||
process_xml: Takes an xml string, and returns the the XModuleDescriptor created from that xml
|
||||
load_item, resources_fs, error_handler: see DescriptorSystem
|
||||
|
||||
process_xml: Takes an xml string, and returns a XModuleDescriptor
|
||||
created from that xml
|
||||
"""
|
||||
DescriptorSystem.__init__(self, load_item, resources_fs)
|
||||
DescriptorSystem.__init__(self, load_item, resources_fs, error_handler)
|
||||
self.process_xml = process_xml
|
||||
|
||||
|
||||
@@ -487,24 +586,33 @@ class ModuleSystem(object):
|
||||
'''
|
||||
def __init__(self, ajax_url, track_function,
|
||||
get_module, render_template, replace_urls,
|
||||
user=None, filestore=None, debug=False, xqueue_callback_url=None):
|
||||
user=None, filestore=None, debug=False,
|
||||
xqueue_callback_url=None):
|
||||
'''
|
||||
Create a closure around the system environment.
|
||||
|
||||
ajax_url - the url where ajax calls to the encapsulating module go.
|
||||
|
||||
track_function - function of (event_type, event), intended for logging
|
||||
or otherwise tracking the event.
|
||||
TODO: Not used, and has inconsistent args in different
|
||||
files. Update or remove.
|
||||
|
||||
get_module - function that takes (location) and returns a corresponding
|
||||
module instance object.
|
||||
render_template - a function that takes (template_file, context), and returns
|
||||
rendered html.
|
||||
user - The user to base the random number generator seed off of for this request
|
||||
filestore - A filestore ojbect. Defaults to an instance of OSFS based at
|
||||
settings.DATA_DIR.
|
||||
module instance object.
|
||||
|
||||
render_template - a function that takes (template_file, context), and
|
||||
returns rendered html.
|
||||
|
||||
user - The user to base the random number generator seed off of for this
|
||||
request
|
||||
|
||||
filestore - A filestore ojbect. Defaults to an instance of OSFS based
|
||||
at settings.DATA_DIR.
|
||||
|
||||
replace_urls - TEMPORARY - A function like static_replace.replace_urls
|
||||
that capa_module can use to fix up the static urls in ajax results.
|
||||
that capa_module can use to fix up the static urls in
|
||||
ajax results.
|
||||
'''
|
||||
self.ajax_url = ajax_url
|
||||
self.xqueue_callback_url = xqueue_callback_url
|
||||
@@ -529,4 +637,3 @@ class ModuleSystem(object):
|
||||
|
||||
def __str__(self):
|
||||
return str(self.__dict__)
|
||||
|
||||
|
||||
@@ -1,25 +1,33 @@
|
||||
from collections import MutableMapping
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
from xmodule.modulestore import Location
|
||||
from lxml import etree
|
||||
import copy
|
||||
import logging
|
||||
import traceback
|
||||
from collections import namedtuple
|
||||
from fs.errors import ResourceNotFoundError
|
||||
import os
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# TODO (cpennington): This was implemented in an attempt to improve performance,
|
||||
# but the actual improvement wasn't measured (and it was implemented late at night).
|
||||
# We should check if it hurts, and whether there's a better way of doing lazy loading
|
||||
|
||||
|
||||
class LazyLoadingDict(MutableMapping):
|
||||
"""
|
||||
A dictionary object that lazily loads it's contents from a provided
|
||||
function on reads (of members that haven't already been set)
|
||||
A dictionary object that lazily loads its contents from a provided
|
||||
function on reads (of members that haven't already been set).
|
||||
"""
|
||||
|
||||
def __init__(self, loader):
|
||||
'''
|
||||
On the first read from this dictionary, it will call loader() to
|
||||
populate its contents. loader() must return something dict-like. Any
|
||||
elements set before the first read will be preserved.
|
||||
'''
|
||||
self._contents = {}
|
||||
self._loaded = False
|
||||
self._loader = loader
|
||||
@@ -70,10 +78,17 @@ _AttrMapBase = namedtuple('_AttrMap', 'metadata_key to_metadata from_metadata')
|
||||
|
||||
class AttrMap(_AttrMapBase):
|
||||
"""
|
||||
A class that specifies a metadata_key, a function to transform an xml attribute to be placed in that key,
|
||||
and to transform that key value
|
||||
A class that specifies a metadata_key, and two functions:
|
||||
|
||||
to_metadata: convert value from the xml representation into
|
||||
an internal python representation
|
||||
|
||||
from_metadata: convert the internal python representation into
|
||||
the value to store in the xml.
|
||||
"""
|
||||
def __new__(_cls, metadata_key, to_metadata=lambda x: x, from_metadata=lambda x: x):
|
||||
def __new__(_cls, metadata_key,
|
||||
to_metadata=lambda x: x,
|
||||
from_metadata=lambda x: x):
|
||||
return _AttrMapBase.__new__(_cls, metadata_key, to_metadata, from_metadata)
|
||||
|
||||
|
||||
@@ -88,15 +103,35 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
# The attributes will be removed from the definition xml passed
|
||||
# to definition_from_xml, and from the xml returned by definition_to_xml
|
||||
metadata_attributes = ('format', 'graceperiod', 'showanswer', 'rerandomize',
|
||||
'start', 'due', 'graded', 'name', 'slug', 'hide_from_toc')
|
||||
'start', 'due', 'graded', 'display_name', 'url_name', 'hide_from_toc',
|
||||
# VS[compat] Remove once unused.
|
||||
'name', 'slug')
|
||||
|
||||
# A dictionary mapping xml attribute names to functions of the value
|
||||
# that return the metadata key and value
|
||||
|
||||
# A dictionary mapping xml attribute names AttrMaps that describe how
|
||||
# to import and export them
|
||||
xml_attribute_map = {
|
||||
'graded': AttrMap('graded', lambda val: val == 'true', lambda val: str(val).lower()),
|
||||
'name': AttrMap('display_name'),
|
||||
# type conversion: want True/False in python, "true"/"false" in xml
|
||||
'graded': AttrMap('graded',
|
||||
lambda val: val == 'true',
|
||||
lambda val: str(val).lower()),
|
||||
}
|
||||
|
||||
|
||||
# VS[compat]. Backwards compatibility code that can go away after
|
||||
# importing 2012 courses.
|
||||
# A set of metadata key conversions that we want to make
|
||||
metadata_translations = {
|
||||
'slug' : 'url_name',
|
||||
'name' : 'display_name',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _translate(cls, key):
|
||||
'VS[compat]'
|
||||
return cls.metadata_translations.get(key, key)
|
||||
|
||||
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
"""
|
||||
@@ -105,12 +140,14 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
|
||||
xml_object: An etree Element
|
||||
"""
|
||||
raise NotImplementedError("%s does not implement definition_from_xml" % cls.__name__)
|
||||
raise NotImplementedError(
|
||||
"%s does not implement definition_from_xml" % cls.__name__)
|
||||
|
||||
@classmethod
|
||||
def clean_metadata_from_xml(cls, xml_object):
|
||||
"""
|
||||
Remove any attribute named in self.metadata_attributes from the supplied xml_object
|
||||
Remove any attribute named in cls.metadata_attributes from the supplied
|
||||
xml_object
|
||||
"""
|
||||
for attr in cls.metadata_attributes:
|
||||
if xml_object.get(attr) is not None:
|
||||
@@ -134,20 +171,25 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
|
||||
xml_data: A string of xml that will be translated into data and children for
|
||||
this module
|
||||
system: An XModuleSystem for interacting with external resources
|
||||
system: A DescriptorSystem for interacting with external resources
|
||||
org and course are optional strings that will be used in the generated modules
|
||||
url identifiers
|
||||
"""
|
||||
xml_object = etree.fromstring(xml_data)
|
||||
# VS[compat] -- just have the url_name lookup once translation is done
|
||||
slug = xml_object.get('url_name', xml_object.get('slug'))
|
||||
location = Location('i4x', org, course, xml_object.tag, slug)
|
||||
|
||||
def metadata_loader():
|
||||
metadata = {}
|
||||
for attr in cls.metadata_attributes:
|
||||
val = xml_object.get(attr)
|
||||
if val is not None:
|
||||
# VS[compat]. Remove after all key translations done
|
||||
attr = cls._translate(attr)
|
||||
|
||||
attr_map = cls.xml_attribute_map.get(attr, AttrMap(attr))
|
||||
metadata[attr_map.metadata_key] = attr_map.to_metadata(val)
|
||||
|
||||
return metadata
|
||||
|
||||
def definition_loader():
|
||||
@@ -157,6 +199,7 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
else:
|
||||
filepath = cls._format_filepath(xml_object.tag, filename)
|
||||
|
||||
# VS[compat]
|
||||
# TODO (cpennington): If the file doesn't exist at the right path,
|
||||
# give the class a chance to fix it up. The file will be written out again
|
||||
# in the correct format.
|
||||
@@ -169,13 +212,20 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
filepath = candidate
|
||||
break
|
||||
|
||||
log.debug('filepath=%s, resources_fs=%s' % (filepath, system.resources_fs))
|
||||
try:
|
||||
with system.resources_fs.open(filepath) as file:
|
||||
definition_xml = cls.file_to_xml(file)
|
||||
except (ResourceNotFoundError, etree.XMLSyntaxError):
|
||||
log.exception('Unable to load file contents at path %s' % filepath)
|
||||
return {'data': 'Error loading file contents at path %s' % filepath}
|
||||
msg = 'Unable to load file contents at path %s for item %s' % (filepath, location.url())
|
||||
log.exception(msg)
|
||||
system.error_handler(msg)
|
||||
# if error_handler didn't reraise, work around problem.
|
||||
error_elem = etree.Element('error')
|
||||
message_elem = etree.SubElement(error_elem, 'error_message')
|
||||
message_elem.text = msg
|
||||
stack_elem = etree.SubElement(error_elem, 'stack_trace')
|
||||
stack_elem.text = traceback.format_exc()
|
||||
return {'data': etree.tostring(error_elem)}
|
||||
|
||||
cls.clean_metadata_from_xml(definition_xml)
|
||||
return cls.definition_from_xml(definition_xml, system)
|
||||
@@ -183,64 +233,90 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
return cls(
|
||||
system,
|
||||
LazyLoadingDict(definition_loader),
|
||||
location=['i4x',
|
||||
org,
|
||||
course,
|
||||
xml_object.tag,
|
||||
xml_object.get('slug')],
|
||||
location=location,
|
||||
metadata=LazyLoadingDict(metadata_loader),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _format_filepath(cls, category, name):
|
||||
return u'{category}/{name}.{ext}'.format(category=category, name=name, ext=cls.filename_extension)
|
||||
return u'{category}/{name}.{ext}'.format(category=category,
|
||||
name=name,
|
||||
ext=cls.filename_extension)
|
||||
|
||||
@classmethod
|
||||
def split_to_file(cls, xml_object):
|
||||
'''
|
||||
Decide whether to write this object to a separate file or not.
|
||||
|
||||
xml_object: an xml definition of an instance of cls.
|
||||
|
||||
This default implementation will split if this has more than 7
|
||||
descendant tags.
|
||||
|
||||
Can be overridden by subclasses.
|
||||
'''
|
||||
return len(list(xml_object.iter())) > 7
|
||||
|
||||
def export_to_xml(self, resource_fs):
|
||||
"""
|
||||
Returns an xml string representing this module, and all modules underneath it.
|
||||
May also write required resources out to resource_fs
|
||||
Returns an xml string representing this module, and all modules
|
||||
underneath it. May also write required resources out to resource_fs
|
||||
|
||||
Assumes that modules have single parantage (that no module appears twice in the same course),
|
||||
and that it is thus safe to nest modules as xml children as appropriate.
|
||||
Assumes that modules have single parentage (that no module appears twice
|
||||
in the same course), and that it is thus safe to nest modules as xml
|
||||
children as appropriate.
|
||||
|
||||
The returned XML should be able to be parsed back into an identical XModuleDescriptor
|
||||
using the from_xml method with the same system, org, and course
|
||||
The returned XML should be able to be parsed back into an identical
|
||||
XModuleDescriptor using the from_xml method with the same system, org,
|
||||
and course
|
||||
|
||||
resource_fs is a pyfilesystem office (from the fs package)
|
||||
resource_fs is a pyfilesystem object (from the fs package)
|
||||
"""
|
||||
|
||||
# Get the definition
|
||||
xml_object = self.definition_to_xml(resource_fs)
|
||||
self.__class__.clean_metadata_from_xml(xml_object)
|
||||
|
||||
# Put content in a separate file if it's large (has more than 5 descendent tags)
|
||||
if len(list(xml_object.iter())) > 5:
|
||||
# Set the tag first, so it's right if writing to a file
|
||||
xml_object.tag = self.category
|
||||
|
||||
# Write it to a file if necessary
|
||||
if self.split_to_file(xml_object):
|
||||
# Put this object in it's own file
|
||||
filepath = self.__class__._format_filepath(self.category, self.name)
|
||||
resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True)
|
||||
with resource_fs.open(filepath, 'w') as file:
|
||||
file.write(etree.tostring(xml_object, pretty_print=True))
|
||||
|
||||
# ...and remove all of its children here
|
||||
for child in xml_object:
|
||||
xml_object.remove(child)
|
||||
# also need to remove the text of this object.
|
||||
xml_object.text = ''
|
||||
# and the tail for good measure...
|
||||
xml_object.tail = ''
|
||||
|
||||
|
||||
xml_object.set('filename', self.name)
|
||||
|
||||
xml_object.set('slug', self.name)
|
||||
xml_object.tag = self.category
|
||||
|
||||
# Add the metadata
|
||||
xml_object.set('url_name', self.name)
|
||||
for attr in self.metadata_attributes:
|
||||
attr_map = self.xml_attribute_map.get(attr, AttrMap(attr))
|
||||
metadata_key = attr_map.metadata_key
|
||||
|
||||
if metadata_key not in self.metadata or metadata_key in self._inherited_metadata:
|
||||
if (metadata_key not in self.metadata or
|
||||
metadata_key in self._inherited_metadata):
|
||||
continue
|
||||
|
||||
val = attr_map.from_metadata(self.metadata[metadata_key])
|
||||
xml_object.set(attr, val)
|
||||
|
||||
# Now we just have to make it beautiful
|
||||
return etree.tostring(xml_object, pretty_print=True)
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
"""
|
||||
Return a new etree Element object created from this modules definition.
|
||||
"""
|
||||
raise NotImplementedError("%s does not implement definition_to_xml" % self.__class__.__name__)
|
||||
raise NotImplementedError(
|
||||
"%s does not implement definition_to_xml" % self.__class__.__name__)
|
||||
|
||||
|
Before Width: | Height: | Size: 224 B After Width: | Height: | Size: 224 B |
|
Before Width: | Height: | Size: 257 B After Width: | Height: | Size: 257 B |
|
Before Width: | Height: | Size: 212 B After Width: | Height: | Size: 212 B |
@@ -3,6 +3,7 @@
|
||||
<sequential filename="System_Usage_Sequence" slug="System_Usage_Sequence" format="Lecture Sequence" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="System Usage Sequence"/>
|
||||
<vertical slug="Lab0_Using_the_tools" format="Lab" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="Lab0: Using the tools">
|
||||
<html slug="html_19" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never"> See the <a href="/section/labintro"> Lab Introduction </a> or <a href="/static/handouts/schematic_tutorial.pdf">Interactive Lab Usage Handout </a> for information on how to do the lab </html>
|
||||
<html slug="html_5555" filename="html_5555"/>
|
||||
<problem filename="Lab_0_Using_the_Tools" slug="Lab_0_Using_the_Tools" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="false" name="Lab 0: Using the Tools"/>
|
||||
</vertical>
|
||||
<problem filename="Circuit_Sandbox" slug="Circuit_Sandbox" format="Lab" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="false" name="Circuit Sandbox"/>
|
||||
|
||||
@@ -1 +1 @@
|
||||
<course filename="6.002_Spring_2012" slug="6.002_Spring_2012" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="6.002 Spring 2012"/>
|
||||
<course filename="6.002_Spring_2012" slug="6.002_Spring_2012" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="6.002 Spring 2012" start="2015-07-17T12:00" course="full" org="edx"/>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<vertical slug="vertical_1122" graceperiod="0 day 0 hours 5 minutes 0 seconds" showanswer="attempted" rerandomize="per_student" due="April 30, 12:00" graded="true">
|
||||
<html filename="Midterm_Exam_1123" slug="Midterm_Exam_1123" graceperiod="0 day 0 hours 5 minutes 0 seconds" rerandomize="per_student" due="April 30, 12:00" graded="true" name="Midterm Exam"/>
|
||||
</vertical>
|
||||
<vertical filename="vertical_1124" slug="vertical_1124" graceperiod="0 day 0 hours 5 minutes 0 seconds" showanswer="attempted" rerandomize="per_student" due="April 30, 12:00" graded="true"/>
|
||||
<vertical filename="vertical_98" slug="vertical_1124" graceperiod="0 day 0 hours 5 minutes 0 seconds" showanswer="attempted" rerandomize="per_student" due="April 30, 12:00" graded="true"/>
|
||||
</sequential>
|
||||
</chapter>
|
||||
</sequential>
|
||||
|
||||
1
common/test/data/full/html/html_5555.html
Normal file
@@ -0,0 +1 @@
|
||||
<html slug="html_5555" filename="html_5555> See the <a href="/section/labintro"> Lab Introduction </a> or <a href="/static/handouts/schematic_tutorial.pdf">Interactive Lab Usage Handout </a> for information on how to do the lab </html>
|
||||
@@ -2,17 +2,13 @@
|
||||
<vertical filename="vertical_58" slug="vertical_58" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never"/>
|
||||
<vertical slug="vertical_66" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never">
|
||||
<problem filename="S1E3_AC_power" slug="S1E3_AC_power" name="S1E3: AC power"/>
|
||||
<customtag tag="S1E3" slug="discuss_67">
|
||||
<impl>discuss</impl>
|
||||
</customtag>
|
||||
<customtag tag="S1E3" slug="discuss_67" impl="discuss"/>
|
||||
<html slug="html_68"> S1E4 has been removed. </html>
|
||||
</vertical>
|
||||
<vertical filename="vertical_89" slug="vertical_89" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never"/>
|
||||
<vertical slug="vertical_94" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never">
|
||||
<video youtube="0.75:XNh13VZhThQ,1.0:XbDRmF6J0K0,1.25:JDty12WEQWk,1.50:wELKGj-5iyM" slug="What_s_next" name="What's next"/>
|
||||
<html slug="html_95">Minor correction: Six elements (five resistors)</html>
|
||||
<customtag tag="S1" slug="discuss_96">
|
||||
<impl>discuss</impl>
|
||||
</customtag>
|
||||
<customtag tag="S1" slug="discuss_96" impl="discuss"/>
|
||||
</vertical>
|
||||
</sequential>
|
||||
|
||||
@@ -3,5 +3,4 @@
|
||||
<problem filename="Sample_Numeric_Problem" slug="Sample_Numeric_Problem" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="false" name="Sample Numeric Problem"/>
|
||||
<problem filename="Sample_Algebraic_Problem" slug="Sample_Algebraic_Problem" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="false" name="Sample Algebraic Problem"/>
|
||||
</vertical>
|
||||
<vertical filename="vertical_16" slug="vertical_16" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never"/>
|
||||
</sequential>
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
<sequential>
|
||||
<video youtube="1.50:8kARlsUt9lM,1.25:4cLA-IME32w,1.0:pFOrD8k9_p4,0.75:CcgAYu0n0bg" slug="S1V9_Demo_Setup_-_Lumped_Elements" name="S1V9: Demo Setup - Lumped Elements"/>
|
||||
<customtag tag="S1" slug="discuss_59">
|
||||
<impl>discuss</impl>
|
||||
</customtag>
|
||||
<customtag page="29" slug="book_60">
|
||||
<impl>book</impl>
|
||||
</customtag>
|
||||
<customtag lecnum="1" slug="slides_61">
|
||||
<impl>slides</impl>
|
||||
</customtag>
|
||||
<customtag tag="S1" slug="discuss_59" impl="discuss"/>
|
||||
<customtag page="29" slug="book_60" impl="book"/>
|
||||
<customtag lecnum="1" slug="slides_61" impl="slides"/>
|
||||
</sequential>
|
||||
|
||||
@@ -3,13 +3,7 @@
|
||||
<h1> </h1>
|
||||
</html>
|
||||
<video youtube="1.50:vl9xrfxcr38,1.25:qxNX4REGqx4,1.0:BGU1poJDgOY,0.75:8rK9vnpystQ" slug="S1V14_Summary" name="S1V14: Summary"/>
|
||||
<customtag tag="S1" slug="discuss_91">
|
||||
<impl>discuss</impl>
|
||||
</customtag>
|
||||
<customtag page="70" slug="book_92">
|
||||
<impl>book</impl>
|
||||
</customtag>
|
||||
<customtag lecnum="1" slug="slides_93">
|
||||
<impl>slides</impl>
|
||||
</customtag>
|
||||
<customtag tag="S1" slug="discuss_91" impl="discuss"/>
|
||||
<customtag page="70" slug="book_92" impl="book"/>
|
||||
<customtag lecnum="1" slug="slides_93" impl="slides"/>
|
||||
</sequential>
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
<sequential>
|
||||
<video youtube="0.75:3NIegrCmA5k,1.0:eLAyO33baQ8,1.25:m1zWi_sh4Aw,1.50:EG-fRTJln_E" slug="S2V1_Review_KVL_KCL" name="S2V1: Review KVL, KCL"/>
|
||||
<customtag tag="S2" slug="discuss_95">
|
||||
<impl>discuss</impl>
|
||||
</customtag>
|
||||
<customtag page="54" slug="book_96">
|
||||
<impl>book</impl>
|
||||
</customtag>
|
||||
<customtag lecnum="2" slug="slides_97">
|
||||
<impl>slides</impl>
|
||||
</customtag>
|
||||
<customtag tag="S2" slug="discuss_95" impl="discuss"/>
|
||||
<customtag page="54" slug="book_96" impl="book"/>
|
||||
<customtag lecnum="2" slug="slides_97" impl="slides"/>
|
||||
</sequential>
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
<sequential>
|
||||
<video youtube="0.75:S_1NaY5te8Q,1.0:G_2F9wivspM,1.25:b-r7dISY-Uc,1.50:jjxHom0oXWk" slug="S2V2_Demo-_KVL_KCL" name="S2V2: Demo- KVL, KCL"/>
|
||||
<customtag tag="S2" slug="discuss_99">
|
||||
<impl>discuss</impl>
|
||||
</customtag>
|
||||
<customtag page="56" slug="book_100">
|
||||
<impl>book</impl>
|
||||
</customtag>
|
||||
<customtag lecnum="2" slug="slides_101">
|
||||
<impl>slides</impl>
|
||||
</customtag>
|
||||
<customtag tag="S2" slug="discuss_99" impl="discuss"/>
|
||||
<customtag page="56" slug="book_100" impl="book"/>
|
||||
<customtag lecnum="2" slug="slides_101" impl="slides"/>
|
||||
</sequential>
|
||||
|
||||
24
common/test/data/simple/course.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<course name="A Simple Course" org="edX" course="simple" graceperiod="1 day 5 hours 59 minutes 59 seconds" slug="2012_Fall">
|
||||
<chapter name="Overview">
|
||||
<video name="Welcome" youtube="0.75:izygArpw-Qo,1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8"/>
|
||||
<videosequence format="Lecture Sequence" name="A simple sequence">
|
||||
<html id="toylab" filename="toylab"/>
|
||||
<video name="S0V1: Video Resources" youtube="0.75:EuzkdzfR0i8,1.0:1bK-WdDi6Qw,1.25:0v1VzoDVUTM,1.50:Bxk_-ZJb240"/>
|
||||
</videosequence>
|
||||
<section name="Lecture 2">
|
||||
<sequential>
|
||||
<video youtube="1.0:TBvX7HzxexQ"/>
|
||||
<problem name="L1 Problem 1" points="1" type="lecture" showanswer="attempted" filename="L1_Problem_1" rerandomize="never"/>
|
||||
</sequential>
|
||||
</section>
|
||||
</chapter>
|
||||
<chapter name="Chapter 2">
|
||||
<section name="Problem Set 1">
|
||||
<sequential>
|
||||
<problem type="lecture" showanswer="attempted" rerandomize="true" title="A simple coding problem" name="Simple coding problem" filename="ps01-simple"/>
|
||||
</sequential>
|
||||
</section>
|
||||
<video name="Lost Video" youtube="1.0:TBvX7HzxexQ"/>
|
||||
</chapter>
|
||||
|
||||
</course>
|
||||
3
common/test/data/simple/html/toylab.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<b>Lab 2A: Superposition Experiment</b>
|
||||
|
||||
<p>Isn't the toy course great?</p>
|
||||
43
common/test/data/simple/problems/L1_Problem_1.xml
Normal file
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0"?>
|
||||
<problem>
|
||||
<p>
|
||||
<h1>Finger Exercise 1</h1>
|
||||
</p>
|
||||
<p>
|
||||
Here are two definitions: </p>
|
||||
<ol class="enumerate">
|
||||
<li>
|
||||
<p>
|
||||
Declarative knowledge refers to statements of fact. </p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
Imperative knowledge refers to 'how to' methods. </p>
|
||||
</li>
|
||||
</ol>
|
||||
<p>
|
||||
Which of the following choices is correct? </p>
|
||||
<ol class="enumerate">
|
||||
<li>
|
||||
<p>
|
||||
Statement 1 is true, Statement 2 is false </p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
Statement 1 is false, Statement 2 is true </p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
Statement 1 and Statement 2 are both false </p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
Statement 1 and Statement 2 are both true </p>
|
||||
</li>
|
||||
</ol>
|
||||
<p>
|
||||
<symbolicresponse answer="4">
|
||||
<textline size="90" math="1"/>
|
||||
</symbolicresponse>
|
||||
</p>
|
||||
</problem>
|
||||
62
common/test/data/simple/problems/ps01-simple.xml
Normal file
@@ -0,0 +1,62 @@
|
||||
<problem><style media="all" type="text/css"/>
|
||||
<text><h2>Paying Off Credit Card Debt</h2>
|
||||
<p> Each month, a credit
|
||||
card statement will come with the option for you to pay a
|
||||
minimum amount of your charge, usually 2% of the balance due.
|
||||
However, the credit card company earns money by charging
|
||||
interest on the balance that you don't pay. So even if you
|
||||
pay credit card payments on time, interest is still accruing
|
||||
on the outstanding balance.</p>
|
||||
<p >Say you've made a
|
||||
$5,000 purchase on a credit card with 18% annual interest
|
||||
rate and 2% minimum monthly payment rate. After a year, how
|
||||
much is the remaining balance? Use the following
|
||||
equations.</p>
|
||||
<blockquote>
|
||||
<p><strong>Minimum monthly payment</strong>
|
||||
= (Minimum monthly payment rate) x (Balance)<br/>
|
||||
(Minimum monthly payment gets split into interest paid and
|
||||
principal paid)<br/>
|
||||
<strong>Interest Paid</strong> = (Annual interest rate) / (12
|
||||
months) x (Balance)<br/>
|
||||
<strong>Principal paid</strong> = (Minimum monthly payment) -
|
||||
(Interest paid)<br/>
|
||||
<strong>Remaining balance</strong> = Balance - (Principal
|
||||
paid)</p>
|
||||
</blockquote>
|
||||
<p >For month 1, compute the minimum monthly payment by taking 2% of the balance.</p>
|
||||
<blockquote xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:x="urn:schemas-microsoft-com:office:excel">
|
||||
<p><strong>Minimum monthly payment</strong>
|
||||
= .02 x $5000 = $100</p>
|
||||
<p>We can't simply deduct this from the balance because
|
||||
there is compounding interest. Of this $100 monthly
|
||||
payment, compute how much will go to paying off interest
|
||||
and how much will go to paying off the principal. Remember
|
||||
that it's the annual interest rate that is given, so we
|
||||
need to divide it by 12 to get the monthly interest
|
||||
rate.</p>
|
||||
<p><strong>Interest paid</strong> = .18/12 x $5000 =
|
||||
$75<br/>
|
||||
<strong>Principal paid</strong> = $100 - $75 = $25</p>
|
||||
<p>The remaining balance at the end of the first month will
|
||||
be the principal paid this month subtracted from the
|
||||
balance at the start of the month.</p>
|
||||
<p><strong>Remaining balance</strong> = $5000 - $25 =
|
||||
$4975</p>
|
||||
</blockquote>
|
||||
<p xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:x="urn:schemas-microsoft-com:office:excel">For month 2, we
|
||||
repeat the same steps.</p>
|
||||
<blockquote xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:x="urn:schemas-microsoft-com:office:excel">
|
||||
<p><strong>Minimum monthly payment</strong>
|
||||
= .02 x $4975 = $99.50<br/>
|
||||
<strong>Interest Paid</strong> = .18/12 x $4975 =
|
||||
$74.63<br/>
|
||||
<strong>Principal Paid</strong> = $99.50 - $74.63 =
|
||||
$24.87<br/>
|
||||
<strong>Remaining Balance</strong> = $4975 - $24.87 =
|
||||
$4950.13</p>
|
||||
</blockquote>
|
||||
<p xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:x="urn:schemas-microsoft-com:office:excel">After 12 months, the
|
||||
total amount paid is $1167.55, leaving an outstanding balance
|
||||
of $4708.10. Pretty depressing!</p>
|
||||
</text></problem>
|
||||
@@ -1,9 +1,9 @@
|
||||
<course name="Toy Course" graceperiod="1 day 5 hours 59 minutes 59 seconds" showanswer="always" rerandomize="never">
|
||||
<course name="Toy Course" org="edX" course="toy" graceperiod="1 day 5 hours 59 minutes 59 seconds" slug="2012_Fall" start="2015-07-17T12:00">
|
||||
<chapter name="Overview">
|
||||
<video name="Welcome" youtube="0.75:izygArpw-Qo,1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8"/>
|
||||
<videosequence format="Lecture Sequence" name="System Usage Sequence">
|
||||
<html id="Lab2A" filename="Lab2A"/>
|
||||
<video name="S0V1: Video Resources" youtube="0.75:EuzkdzfR0i8,1.0:1bK-WdDi6Qw,1.25:0v1VzoDVUTM,1.50:Bxk_-ZJb240"/>
|
||||
<videosequence format="Lecture Sequence" name="Toy Videos">
|
||||
<html name="toylab" filename="toylab"/>
|
||||
<video name="Video Resources" youtube="1.0:1bK-WdDi6Qw"/>
|
||||
</videosequence>
|
||||
<video name="Welcome" youtube="1.0:p2Q6BrNhdh8"/>
|
||||
</chapter>
|
||||
</course>
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
$("#r1_slider").slider({
|
||||
value: 1, min: 1, max: 10, step: 1, slide: schematic.component_slider,
|
||||
schematic: "ctrls", component: "R1", property: "r", analysis: "dc",
|
||||
})
|
||||
|
||||
$("#r2_slider").slider({
|
||||
value: 1, min: 1, max: 10, step: 1, slide: schematic.component_slider,
|
||||
schematic: "ctrls", component: "R2", property: "r", analysis: "dc",
|
||||
})
|
||||
|
||||
$("#r3_slider").slider({
|
||||
value: 1, min: 1, max: 10, step: 1, slide: schematic.component_slider,
|
||||
schematic: "ctrls", component: "R3", property: "r", analysis: "dc",
|
||||
})
|
||||
|
||||
$("#r4_slider").slider({
|
||||
value: 1, min: 1, max: 10, step: 1, slide: schematic.component_slider,
|
||||
schematic: "ctrls", component: "R4", property: "r", analysis: "dc",
|
||||
})
|
||||
|
||||
$("#slider").slider(); });
|
||||
</script>
|
||||
|
||||
<b>Lab 2A: Superposition Experiment</b>
|
||||
|
||||
<br><br><i>Note: This part of the lab is just to develop your intuition about
|
||||
superposition. There are no responses that need to be checked.</i>
|
||||
|
||||
<br/><br/>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?
|
||||
|
||||
<center>
|
||||
<input width="425" type="hidden" height="150" id="schematic1" parts="" analyses="" class="schematic ctrls" name="test2" value="[["w",[160,64,184,64]],["w",[160,16,184,16]],["w",[64,16,112,16]],["w",[112,64,88,64]],["w",[64,64,88,64]],["g",[88,64,0],{},["0"]],["w",[112,64,160,64]],["w",[16,64,64,64]],["r",[160,16,0],{"name":"R4","r":"1"},["1","0"]],["r",[160,16,1],{"name":"R3","r":"1"},["1","2"]],["i",[112,64,6],{"name":"","value":"6A"},["0","2"]],["r",[64,16,0],{"name":"R2","r":"1"},["2","0"]],["r",[64,16,1],{"name":"R1","r":"1"},["2","3"]],["v",[16,16,0],{"name":"","value":"8V"},["3","0"]],["view",-24,0,2]]"/>
|
||||
Figure 1. Example multi-source circuit
|
||||
</center>
|
||||
|
||||
<br/><br/>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.
|
||||
|
||||
<center>
|
||||
<table><tr><td>
|
||||
<input style="display:inline;" width="425" type="hidden" height="150" id="schematic2" parts="" analyses="" class="schematic ctrls" name="test2" value="[["w",[160,64,184,64]],["w",[160,16,184,16]],["w",[64,16,112,16]],["w",[112,64,88,64]],["w",[64,64,88,64]],["g",[88,64,0],{},["0"]],["w",[112,64,160,64]],["w",[16,64,64,64]],["r",[160,16,0],{"name":"R4","r":"1"},["1","0"]],["r",[160,16,1],{"name":"R3","r":"1"},["1","2"]],["r",[64,16,0],{"name":"R2","r":"1"},["2","0"]],["r",[64,16,1],{"name":"R1","r":"1"},["2","3"]],["v",[16,16,0],{"name":"","value":"8V"},["3","0"]],["view",-24,0,2]]"/>
|
||||
(a) Subcircuit for analyzing contribution of voltage source
|
||||
</td><td>
|
||||
<input width="425" type="hidden" height="150" id="schematic3" parts="" analyses="" class="schematic ctrls" name="test2" value="[["w",[16,16,16,64]],["w",[160,64,184,64]],["w",[160,16,184,16]],["w",[64,16,112,16]],["w",[112,64,88,64]],["w",[64,64,88,64]],["g",[88,64,0],{},["0"]],["w",[112,64,160,64]],["w",[16,64,64,64]],["r",[160,16,0],{"name":"R4","r":"1"},["1","0"]],["r",[160,16,1],{"name":"R3","r":"1"},["1","2"]],["i",[112,64,6],{"name":"","value":"6A"},["0","2"]],["r",[64,16,0],{"name":"R2","r":"1"},["2","0"]],["r",[64,16,1],{"name":"R1","r":"1"},["2","3"]],["view",-24,0,2]]"/>
|
||||
(b) Subcircuit for analyzing contribution of current source
|
||||
</td></tr></table>
|
||||
<br>Figure 2. Decomposition of Figure 1 into subcircuits
|
||||
</center>
|
||||
|
||||
<br/>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:
|
||||
|
||||
<ul style="margin-left:2em;margin-top:1em;margin-right:2em;margin-bottom:1em;">
|
||||
<i>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.)</i>
|
||||
</ul>
|
||||
|
||||
<br>
|
||||
<center>
|
||||
<table><tr valign="top">
|
||||
<td>
|
||||
<table>
|
||||
<tr valign="top">
|
||||
<td>R1</td>
|
||||
<td>
|
||||
<div id="r1_slider" style="width:200px; height:10px; margin-left:15px"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr valign="top">
|
||||
<td>R2</td>
|
||||
<td>
|
||||
<div id="r2_slider" style="width:200px; height:10px; margin-left:15px; margin-top:10px;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr valign="top">
|
||||
<td>R3</td>
|
||||
<td>
|
||||
<div id="r3_slider" style="width:200px; height:10px; margin-left:15px; margin-top:10px;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr valign="top">
|
||||
<td>R4</td>
|
||||
<td>
|
||||
<div id="r4_slider" style="width:200px; height:10px; margin-left:15px; margin-top:10px;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td></tr></table>
|
||||
</center>
|
||||
3
common/test/data/toy/html/toylab.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<b>Lab 2A: Superposition Experiment</b>
|
||||
|
||||
<p>Isn't the toy course great?</p>
|
||||
@@ -13,17 +13,17 @@ You should be familiar with the following. If you're not, go read some docs...
|
||||
- css
|
||||
- git
|
||||
- mako templates -- we use these instead of django templates, because they support embedding real python.
|
||||
|
||||
|
||||
## Other relevant terms
|
||||
|
||||
- CAPA -- lon-capa.org -- content management system that has defined a standard for online learning and assessment materials. Many of our materials follow this standard.
|
||||
- TODO: add more details / link to relevant docs. lon-capa.org is not immediately intuitive.
|
||||
- TODO: add more details / link to relevant docs. lon-capa.org is not immediately intuitive.
|
||||
- lcp = loncapa problem
|
||||
|
||||
|
||||
## Parts of the system
|
||||
|
||||
- LMS -- Learning Management System. The student-facing parts of the system. Handles student accounts, displaying videos, tutorials, exercies, problems, etc.
|
||||
- LMS -- Learning Management System. The student-facing parts of the system. Handles student accounts, displaying videos, tutorials, exercies, problems, etc.
|
||||
|
||||
- CMS -- Course Management System. The instructor-facing parts of the system. Allows instructors to see and modify their course, add lectures, problems, reorder things, etc.
|
||||
|
||||
@@ -42,7 +42,7 @@ You should be familiar with the following. If you're not, go read some docs...
|
||||
|
||||
## High Level Entities in the code
|
||||
|
||||
### Common libraries
|
||||
### Common libraries
|
||||
|
||||
- xmodule: generic learning modules. *x* can be sequence, video, template, html,
|
||||
vertical, capa, etc. These are the things that one puts inside sections
|
||||
@@ -51,7 +51,7 @@ You should be familiar with the following. If you're not, go read some docs...
|
||||
- XModuleDescriptor: This defines the problem and all data and UI needed to edit
|
||||
that problem. It is unaware of any student data, but can be used to retrieve
|
||||
an XModule, which is aware of that student state.
|
||||
|
||||
|
||||
- XModule: The XModule is a problem instance that is particular to a student. It knows
|
||||
how to render itself to html to display the problem, how to score itself,
|
||||
and how to handle ajax calls from the front end.
|
||||
@@ -59,19 +59,25 @@ You should be familiar with the following. If you're not, go read some docs...
|
||||
- Both XModule and XModuleDescriptor take system context parameters. These are named
|
||||
ModuleSystem and DescriptorSystem respectively. These help isolate the XModules
|
||||
from any interactions with external resources that they require.
|
||||
|
||||
|
||||
For instance, the DescriptorSystem has a function to load an XModuleDescriptor
|
||||
from a Location object, and the ModuleSystem knows how to render things,
|
||||
track events, and complain about 404s
|
||||
- TODO: document the system context interface--it's different in `x_module.XModule.__init__` and in `x_module tests.py` (do this in the code, not here)
|
||||
|
||||
- `course.xml` format. We use python setuptools to connect supported tags with the descriptors that handle them. See `common/lib/xmodule/setup.py`. There are checking and validation tools in `common/validate`.
|
||||
|
||||
- the xml import+export functionality is in `xml_module.py:XmlDescriptor`, which is a mixin class that's used by the actual descriptor classes.
|
||||
|
||||
- There is a distinction between descriptor _definitions_ that stay the same for any use of that descriptor (e.g. here is what a particular problem is), and _metadata_ describing how that descriptor is used (e.g. whether to allow checking of answers, due date, etc). When reading in `from_xml`, the code pulls out the metadata attributes into a separate structure, and puts it back on export.
|
||||
|
||||
- in `common/lib/xmodule`
|
||||
|
||||
- capa modules -- defines `LoncapaProblem` and many related things.
|
||||
- capa modules -- defines `LoncapaProblem` and many related things.
|
||||
- in `common/lib/capa`
|
||||
|
||||
### LMS
|
||||
### LMS
|
||||
|
||||
The LMS is a django site, with root in `lms/`. It runs in many different environments--the settings files are in `lms/envs`.
|
||||
The LMS is a django site, with root in `lms/`. It runs in many different environments--the settings files are in `lms/envs`.
|
||||
|
||||
- We use the Django Auth system, including the is_staff and is_superuser flags. User profiles and related code lives in `lms/djangoapps/student/`. There is support for groups of students (e.g. 'want emails about future courses', 'have unenrolled', etc) in `lms/djangoapps/student/models.py`.
|
||||
|
||||
@@ -79,19 +85,19 @@ The LMS is a django site, with root in `lms/`. It runs in many different enviro
|
||||
- `lms/djangoapps/courseware/models.py`
|
||||
|
||||
- Core rendering path:
|
||||
- `lms/urls.py` points to `courseware.views.index`, which gets module info from the course xml file, pulls list of `StudentModule` objects for this user (to avoid multiple db hits).
|
||||
- `lms/urls.py` points to `courseware.views.index`, which gets module info from the course xml file, pulls list of `StudentModule` objects for this user (to avoid multiple db hits).
|
||||
|
||||
- Calls `render_accordion` to render the "accordion"--the display of the course structure.
|
||||
|
||||
- To render the current module, calls `module_render.py:render_x_module()`, which gets the `StudentModule` instance, and passes the `StudentModule` state and other system context to the module constructor the get an instance of the appropriate module class for this user.
|
||||
|
||||
- calls the module's `.get_html()` method. If the module has nested submodules, render_x_module() will be called again for each.
|
||||
|
||||
|
||||
- ajax calls go to `module_render.py:modx_dispatch()`, which passes it to the module's `handle_ajax()` function, and then updates the grade and state if they changed.
|
||||
|
||||
- [This diagram](https://github.com/MITx/mitx/wiki/MITx-Architecture) visually shows how the clients communicate with problems + modules.
|
||||
|
||||
- See `lms/urls.py` for the wirings of urls to views.
|
||||
|
||||
- See `lms/urls.py` for the wirings of urls to views.
|
||||
|
||||
- Tracking: there is support for basic tracking of client-side events in `lms/djangoapps/track`.
|
||||
|
||||
@@ -110,7 +116,7 @@ environments, defined in `cms/envs`.
|
||||
|
||||
- _mako_ -- we use this for templates, and have wrapper called mitxmako that makes mako look like the django templating calls.
|
||||
|
||||
We use a fork of django-pipeline to make sure that the js and css always reflect the latest `*.coffee` and `*.sass` files (We're hoping to get our changes merged in the official version soon). This works differently in development and production. Test uses the production settings.
|
||||
We use a fork of django-pipeline to make sure that the js and css always reflect the latest `*.coffee` and `*.sass` files (We're hoping to get our changes merged in the official version soon). This works differently in development and production. Test uses the production settings.
|
||||
|
||||
In production, the django `collectstatic` command recompiles everything and puts all the generated static files in a static/ dir. A starting point in the code is `django-pipeline/pipeline/packager.py:pack`.
|
||||
|
||||
@@ -127,8 +133,6 @@ See `testing.md`.
|
||||
|
||||
## TODO:
|
||||
|
||||
- update lms/envs/README.txt
|
||||
|
||||
- describe our production environment
|
||||
|
||||
- describe the front-end architecture, tools, etc. Starting point: `lms/static`
|
||||
|
||||
171
lms/djangoapps/courseware/management/commands/clean_xml.py
Normal file
@@ -0,0 +1,171 @@
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from filecmp import dircmp
|
||||
from fs.osfs import OSFS
|
||||
from path import path
|
||||
from lxml import etree
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from xmodule.modulestore.xml import XMLModuleStore
|
||||
|
||||
|
||||
def traverse_tree(course):
|
||||
'''Load every descriptor in course. Return bool success value.'''
|
||||
queue = [course]
|
||||
while len(queue) > 0:
|
||||
node = queue.pop()
|
||||
# print '{0}:'.format(node.location)
|
||||
# if 'data' in node.definition:
|
||||
# print '{0}'.format(node.definition['data'])
|
||||
queue.extend(node.get_children())
|
||||
|
||||
return True
|
||||
|
||||
def make_logging_error_handler():
|
||||
'''Return a tuple (handler, error_list), where
|
||||
the handler appends the message and any exc_info
|
||||
to the error_list on every call.
|
||||
'''
|
||||
errors = []
|
||||
|
||||
def error_handler(msg, exc_info=None):
|
||||
'''Log errors'''
|
||||
if exc_info is None:
|
||||
if sys.exc_info() != (None, None, None):
|
||||
exc_info = sys.exc_info()
|
||||
|
||||
errors.append((msg, exc_info))
|
||||
|
||||
return (error_handler, errors)
|
||||
|
||||
|
||||
def export(course, export_dir):
|
||||
"""Export the specified course to course_dir. Creates dir if it doesn't exist.
|
||||
Overwrites files, does not clean out dir beforehand.
|
||||
"""
|
||||
fs = OSFS(export_dir, create=True)
|
||||
if not fs.isdirempty('.'):
|
||||
print ('WARNING: Directory {dir} not-empty.'
|
||||
' May clobber/confuse things'.format(dir=export_dir))
|
||||
|
||||
try:
|
||||
xml = course.export_to_xml(fs)
|
||||
with fs.open('course.xml', mode='w') as f:
|
||||
f.write(xml)
|
||||
|
||||
return True
|
||||
except:
|
||||
print 'Export failed!'
|
||||
traceback.print_exc()
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def import_with_checks(course_dir, verbose=True):
|
||||
all_ok = True
|
||||
|
||||
print "Attempting to load '{0}'".format(course_dir)
|
||||
|
||||
course_dir = path(course_dir)
|
||||
data_dir = course_dir.dirname()
|
||||
course_dirs = [course_dir.basename()]
|
||||
|
||||
(error_handler, errors) = make_logging_error_handler()
|
||||
# No default class--want to complain if it doesn't find plugins for any
|
||||
# module.
|
||||
modulestore = XMLModuleStore(data_dir,
|
||||
default_class=None,
|
||||
eager=True,
|
||||
course_dirs=course_dirs,
|
||||
error_handler=error_handler)
|
||||
|
||||
def str_of_err(tpl):
|
||||
(msg, exc_info) = tpl
|
||||
if exc_info is None:
|
||||
return msg
|
||||
|
||||
exc_str = '\n'.join(traceback.format_exception(*exc_info))
|
||||
return '{msg}\n{exc}'.format(msg=msg, exc=exc_str)
|
||||
|
||||
courses = modulestore.get_courses()
|
||||
if len(errors) != 0:
|
||||
all_ok = False
|
||||
print '\n'
|
||||
print "=" * 40
|
||||
print 'ERRORs during import:'
|
||||
print '\n'.join(map(str_of_err,errors))
|
||||
print "=" * 40
|
||||
print '\n'
|
||||
|
||||
n = len(courses)
|
||||
if n != 1:
|
||||
print 'ERROR: Expect exactly 1 course. Loaded {n}: {lst}'.format(
|
||||
n=n, lst=courses)
|
||||
return (False, None)
|
||||
|
||||
course = courses[0]
|
||||
|
||||
#print course
|
||||
validators = (
|
||||
traverse_tree,
|
||||
)
|
||||
|
||||
print "=" * 40
|
||||
print "Running validators..."
|
||||
|
||||
for validate in validators:
|
||||
print 'Running {0}'.format(validate.__name__)
|
||||
all_ok = validate(course) and all_ok
|
||||
|
||||
|
||||
if all_ok:
|
||||
print 'Course passes all checks!'
|
||||
else:
|
||||
print "Course fails some checks. See above for errors."
|
||||
return all_ok, course
|
||||
|
||||
|
||||
def check_roundtrip(course_dir):
|
||||
'''Check that import->export leaves the course the same'''
|
||||
|
||||
print "====== Roundtrip import ======="
|
||||
(ok, course) = import_with_checks(course_dir)
|
||||
if not ok:
|
||||
raise Exception("Roundtrip import failed!")
|
||||
|
||||
print "====== Roundtrip export ======="
|
||||
export_dir = course_dir + ".rt"
|
||||
export(course, export_dir)
|
||||
|
||||
# dircmp doesn't do recursive diffs.
|
||||
# diff = dircmp(course_dir, export_dir, ignore=[], hide=[])
|
||||
print "======== Roundtrip diff: ========="
|
||||
os.system("diff -r {0} {1}".format(course_dir, export_dir))
|
||||
print "======== ideally there is no diff above this ======="
|
||||
|
||||
|
||||
def clean_xml(course_dir, export_dir):
|
||||
(ok, course) = import_with_checks(course_dir)
|
||||
if ok:
|
||||
export(course, export_dir)
|
||||
check_roundtrip(export_dir)
|
||||
else:
|
||||
print "Did NOT export"
|
||||
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = """Imports specified course.xml, validate it, then exports in
|
||||
a canonical format.
|
||||
|
||||
Usage: clean_xml PATH-TO-COURSE-DIR PATH-TO-OUTPUT-DIR
|
||||
"""
|
||||
def handle(self, *args, **options):
|
||||
if len(args) != 2:
|
||||
print Command.help
|
||||
return
|
||||
|
||||
clean_xml(args[0], args[1])
|
||||
@@ -119,7 +119,7 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
instance_module is a StudentModule specific to this module for this student,
|
||||
or None if this is an anonymous user
|
||||
shared_module is a StudentModule specific to all modules with the same
|
||||
'shared_state_key' attribute, or None if the module doesn't elect to
|
||||
'shared_state_key' attribute, or None if the module does not elect to
|
||||
share state
|
||||
'''
|
||||
descriptor = modulestore().get_item(location)
|
||||
@@ -131,11 +131,13 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
if course_id:
|
||||
course_id = course_id.group('course_id')
|
||||
|
||||
instance_module = student_module_cache.lookup(descriptor.category, descriptor.location.url())
|
||||
instance_module = student_module_cache.lookup(descriptor.category,
|
||||
descriptor.location.url())
|
||||
|
||||
shared_state_key = getattr(descriptor, 'shared_state_key', None)
|
||||
if shared_state_key is not None:
|
||||
shared_module = student_module_cache.lookup(descriptor.category, shared_state_key)
|
||||
shared_module = student_module_cache.lookup(descriptor.category,
|
||||
shared_state_key)
|
||||
else:
|
||||
shared_module = None
|
||||
|
||||
@@ -150,10 +152,12 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
# TODO (vshnayder): fix hardcoded urls (use reverse)
|
||||
# Setup system context for module instance
|
||||
ajax_url = settings.MITX_ROOT_URL + '/modx/' + descriptor.location.url() + '/'
|
||||
xqueue_callback_url = settings.MITX_ROOT_URL + '/xqueue/' + str(user.id) + '/' + descriptor.location.url() + '/'
|
||||
xqueue_callback_url = (settings.MITX_ROOT_URL + '/xqueue/' +
|
||||
str(user.id) + '/' + descriptor.location.url() + '/')
|
||||
|
||||
def _get_module(location):
|
||||
(module, _, _, _) = get_module(user, request, location, student_module_cache, position)
|
||||
(module, _, _, _) = get_module(user, request, location,
|
||||
student_module_cache, position)
|
||||
return module
|
||||
|
||||
# TODO (cpennington): When modules are shared between courses, the static
|
||||
|
||||
1
lms/djangoapps/courseware/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
208
lms/djangoapps/courseware/tests/tests.py
Normal file
@@ -0,0 +1,208 @@
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
|
||||
from pprint import pprint
|
||||
|
||||
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 xmodule.modulestore.xml_importer import import_from_xml
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
# A bit of a hack--want mongo modulestore for these tests, until
|
||||
# jump_to works with the xmlmodulestore or we have an even better solution
|
||||
# NOTE: this means this test requires mongo to be running.
|
||||
|
||||
def mongo_store_config(data_dir):
|
||||
return {
|
||||
'default': {
|
||||
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
|
||||
'OPTIONS': {
|
||||
'default_class': 'xmodule.raw_module.RawDescriptor',
|
||||
'host': 'localhost',
|
||||
'db': 'xmodule',
|
||||
'collection': 'modulestore',
|
||||
'fs_root': data_dir,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
|
||||
TEST_DATA_MODULESTORE = mongo_store_config(TEST_DATA_DIR)
|
||||
|
||||
REAL_DATA_DIR = settings.GITHUB_REPO_ROOT
|
||||
REAL_DATA_MODULESTORE = mongo_store_config(REAL_DATA_DIR)
|
||||
|
||||
class ActivateLoginTestCase(TestCase):
|
||||
'''Check that we can activate and log in'''
|
||||
|
||||
def setUp(self):
|
||||
email = 'view@test.com'
|
||||
password = 'foo'
|
||||
self.create_account('viewtest', email, password)
|
||||
self.activate_user(email)
|
||||
self.login(email, password)
|
||||
|
||||
# ============ User creation and login ==============
|
||||
|
||||
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'),
|
||||
{'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(email, pw)
|
||||
data = parse_json(resp)
|
||||
self.assertTrue(data['success'])
|
||||
return resp
|
||||
|
||||
def _create_account(self, username, email, pw):
|
||||
'''Try to create an account. No error checking'''
|
||||
resp = self.client.post('/create_account', {
|
||||
'username': username,
|
||||
'email': email,
|
||||
'password': pw,
|
||||
'name': 'Fred Weasley',
|
||||
'terms_of_service': 'true',
|
||||
'honor_code': 'true',
|
||||
})
|
||||
return resp
|
||||
|
||||
def create_account(self, username, email, pw):
|
||||
'''Create the account and check that it worked'''
|
||||
resp = self._create_account(username, email, pw)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
data = parse_json(resp)
|
||||
self.assertEqual(data['success'], True)
|
||||
|
||||
# Check both that the user is created, and inactive
|
||||
self.assertFalse(user(email).is_active)
|
||||
|
||||
return resp
|
||||
|
||||
def _activate_user(self, email):
|
||||
'''Look up the activation key for the user, then hit the activate view.
|
||||
No error checking'''
|
||||
activation_key = registration(email).activation_key
|
||||
|
||||
# and now we try to activate
|
||||
resp = self.client.get(reverse('activate', kwargs={'key': activation_key}))
|
||||
return resp
|
||||
|
||||
def activate_user(self, email):
|
||||
resp = self._activate_user(email)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
# Now make sure that the user is now actually activated
|
||||
self.assertTrue(user(email).is_active)
|
||||
|
||||
def test_activate_login(self):
|
||||
'''The setup function does all the work'''
|
||||
pass
|
||||
|
||||
|
||||
class PageLoader(ActivateLoginTestCase):
|
||||
''' Base class that adds a function to load all pages in a modulestore '''
|
||||
|
||||
def check_pages_load(self, course_name, data_dir, modstore):
|
||||
print "Checking course {0} in {1}".format(course_name, data_dir)
|
||||
import_from_xml(modstore, data_dir, [course_name])
|
||||
|
||||
n = 0
|
||||
num_bad = 0
|
||||
all_ok = True
|
||||
for descriptor in modstore.get_items(
|
||||
Location(None, None, None, None, None)):
|
||||
n += 1
|
||||
print "Checking ", descriptor.location.url()
|
||||
#print descriptor.__class__, descriptor.location
|
||||
resp = self.client.get(reverse('jump_to',
|
||||
kwargs={'location': descriptor.location.url()}))
|
||||
msg = str(resp.status_code)
|
||||
|
||||
if resp.status_code != 200:
|
||||
msg = "ERROR " + msg
|
||||
all_ok = False
|
||||
num_bad += 1
|
||||
print msg
|
||||
self.assertTrue(all_ok) # fail fast
|
||||
|
||||
print "{0}/{1} good".format(n - num_bad, n)
|
||||
self.assertTrue(all_ok)
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
|
||||
class TestCoursesLoadTestCase(PageLoader):
|
||||
'''Check that all pages in test courses load properly'''
|
||||
|
||||
def setUp(self):
|
||||
ActivateLoginTestCase.setUp(self)
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
xmodule.modulestore.django.modulestore().collection.drop()
|
||||
|
||||
def test_toy_course_loads(self):
|
||||
self.check_pages_load('toy', TEST_DATA_DIR, modulestore())
|
||||
|
||||
def test_full_course_loads(self):
|
||||
self.check_pages_load('full', TEST_DATA_DIR, modulestore())
|
||||
|
||||
|
||||
# ========= TODO: check ajax interaction here too?
|
||||
|
||||
|
||||
@override_settings(MODULESTORE=REAL_DATA_MODULESTORE)
|
||||
class RealCoursesLoadTestCase(PageLoader):
|
||||
'''Check that all pages in real courses load properly'''
|
||||
|
||||
def setUp(self):
|
||||
ActivateLoginTestCase.setUp(self)
|
||||
xmodule.modulestore.django._MODULESTORES = {}
|
||||
xmodule.modulestore.django.modulestore().collection.drop()
|
||||
|
||||
# TODO: Disabled test for now.. Fix once things are cleaned up.
|
||||
def Xtest_real_courses_loads(self):
|
||||
'''See if any real courses are available at the REAL_DATA_DIR.
|
||||
If they are, check them.'''
|
||||
|
||||
# TODO: adjust staticfiles_dirs
|
||||
if not os.path.isdir(REAL_DATA_DIR):
|
||||
# No data present. Just pass.
|
||||
return
|
||||
|
||||
courses = [course_dir for course_dir in os.listdir(REAL_DATA_DIR)
|
||||
if os.path.isdir(REAL_DATA_DIR / course_dir)]
|
||||
for course in courses:
|
||||
self.check_pages_load(course, REAL_DATA_DIR, modulestore())
|
||||
|
||||
|
||||
# ========= TODO: check ajax interaction here too?
|
||||
@@ -25,12 +25,16 @@ from models import StudentModuleCache
|
||||
from student.models import UserProfile
|
||||
from multicourse import multicourse_settings
|
||||
from django_comment_client.utils import get_discussion_title
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
|
||||
from util.cache import cache, cache_if_anonymous
|
||||
from student.models import UserTestGroup, CourseEnrollment
|
||||
from courseware import grades
|
||||
from courseware.courses import check_course
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
|
||||
import comment_client
|
||||
|
||||
@@ -206,65 +210,59 @@ def index(request, course_id, chapter=None, section=None,
|
||||
if look_for_module:
|
||||
# TODO (cpennington): Pass the right course in here
|
||||
|
||||
section = get_section(course, chapter, section)
|
||||
student_module_cache = StudentModuleCache(request.user, section)
|
||||
module, _, _, _ = get_module(request.user, request, section.location, student_module_cache)
|
||||
context['content'] = module.get_html()
|
||||
section_descriptor = get_section(course, chapter, section)
|
||||
if section_descriptor is not None:
|
||||
student_module_cache = StudentModuleCache(request.user,
|
||||
section_descriptor)
|
||||
module, _, _, _ = get_module(request.user, request,
|
||||
section_descriptor.location,
|
||||
student_module_cache)
|
||||
context['content'] = module.get_html()
|
||||
else:
|
||||
log.warning("Couldn't find a section descriptor for course_id '{0}',"
|
||||
"chapter '{1}', section '{2}'".format(
|
||||
course_id, chapter, section))
|
||||
|
||||
|
||||
result = render_to_response('courseware.html', context)
|
||||
return result
|
||||
|
||||
|
||||
def jump_to(request, probname=None):
|
||||
@ensure_csrf_cookie
|
||||
def jump_to(request, location):
|
||||
'''
|
||||
Jump to viewing a specific problem. The problem is specified by a
|
||||
problem name - currently the filename (minus .xml) of the problem.
|
||||
Maybe this should change to a more generic tag, eg "name" given as
|
||||
an attribute in <problem>.
|
||||
Show the page that contains a specific location.
|
||||
|
||||
We do the jump by (1) reading course.xml to find the first
|
||||
instance of <problem> with the given filename, then (2) finding
|
||||
the parent element of the problem, then (3) rendering that parent
|
||||
element with a specific computed position value (if it is
|
||||
<sequential>).
|
||||
If the location is invalid, return a 404.
|
||||
|
||||
If the location is valid, but not present in a course, ?
|
||||
|
||||
If the location is valid, but in a course the current user isn't registered for, ?
|
||||
TODO -- let the index view deal with it?
|
||||
'''
|
||||
# get coursename if stored
|
||||
coursename = multicourse_settings.get_coursename_from_request(request)
|
||||
# Complain if the location isn't valid
|
||||
try:
|
||||
location = Location(location)
|
||||
except InvalidLocationError:
|
||||
raise Http404("Invalid location")
|
||||
|
||||
# begin by getting course.xml tree
|
||||
xml = content_parser.course_file(request.user, coursename)
|
||||
# Complain if there's not data for this location
|
||||
try:
|
||||
(course_id, chapter, section, position) = modulestore().path_to_location(location)
|
||||
except ItemNotFoundError:
|
||||
raise Http404("No data at this location: {0}".format(location))
|
||||
except NoPathToItem:
|
||||
raise Http404("This location is not in any class: {0}".format(location))
|
||||
|
||||
# look for problem of given name
|
||||
pxml = xml.xpath('//problem[@filename="%s"]' % probname)
|
||||
if pxml:
|
||||
pxml = pxml[0]
|
||||
|
||||
# get the parent element
|
||||
parent = pxml.getparent()
|
||||
|
||||
# figure out chapter and section names
|
||||
chapter = None
|
||||
section = None
|
||||
branch = parent
|
||||
for k in range(4): # max depth of recursion
|
||||
if branch.tag == 'section':
|
||||
section = branch.get('name')
|
||||
if branch.tag == 'chapter':
|
||||
chapter = branch.get('name')
|
||||
branch = branch.getparent()
|
||||
|
||||
position = None
|
||||
if parent.tag == 'sequential':
|
||||
position = parent.index(pxml) + 1 # position in sequence
|
||||
|
||||
return index(request,
|
||||
course=coursename, chapter=chapter,
|
||||
section=section, position=position)
|
||||
|
||||
return index(request, course_id, chapter, section, position)
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def course_info(request, course_id):
|
||||
'''
|
||||
Display the course's info.html, or 404 if there is no such course.
|
||||
|
||||
Assumes the course_id is in a valid format.
|
||||
'''
|
||||
course = check_course(course_id)
|
||||
|
||||
return render_to_response('info.html', {'course': course})
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
Transitional for moving to new settings scheme.
|
||||
Transitional for moving to new settings scheme.
|
||||
|
||||
To use:
|
||||
django-admin.py runserver --settings=envs.dev --pythonpath=.
|
||||
To use:
|
||||
rake lms
|
||||
or
|
||||
django-admin.py runserver --settings=lms.envs.dev --pythonpath=.
|
||||
|
||||
NOTE: Using manage.py will automatically run mitx/settings.py first, regardless
|
||||
of what you send it for an explicit --settings flag. It still works, but might
|
||||
have odd side effects. Using django-admin.py avoids that problem.
|
||||
have odd side effects. Using django-admin.py avoids that problem.
|
||||
django-admin.py is installed by default when you install Django.
|
||||
|
||||
To use with gunicorn_django in debug mode:
|
||||
|
||||
gunicorn_django envs/dev.py
|
||||
gunicorn_django lms/envs/dev.py
|
||||
|
||||
|
||||
@@ -32,12 +32,12 @@ LOG_DIR = ENV_TOKENS['LOG_DIR']
|
||||
|
||||
CACHES = ENV_TOKENS['CACHES']
|
||||
|
||||
for feature,value in ENV_TOKENS.get('MITX_FEATURES', {}).items():
|
||||
for feature, value in ENV_TOKENS.get('MITX_FEATURES', {}).items():
|
||||
MITX_FEATURES[feature] = value
|
||||
|
||||
WIKI_ENABLED = ENV_TOKENS.get('WIKI_ENABLED',WIKI_ENABLED)
|
||||
WIKI_ENABLED = ENV_TOKENS.get('WIKI_ENABLED', WIKI_ENABLED)
|
||||
|
||||
LOGGING = get_logger_config(LOG_DIR,
|
||||
LOGGING = get_logger_config(LOG_DIR,
|
||||
logging_env=ENV_TOKENS['LOGGING_ENV'],
|
||||
syslog_addr=(ENV_TOKENS['SYSLOG_SERVER'], 514),
|
||||
debug=False)
|
||||
|
||||
@@ -6,16 +6,16 @@ MITX_FEATURES[...]. Modules that extend this one can change the feature
|
||||
configuration in an environment specific config file and re-calculate those
|
||||
values.
|
||||
|
||||
We should make a method that calls all these config methods so that you just
|
||||
We should make a method that calls all these config methods so that you just
|
||||
make one call at the end of your site-specific dev file to reset all the
|
||||
dependent variables (like INSTALLED_APPS) for you.
|
||||
|
||||
Longer TODO:
|
||||
1. Right now our treatment of static content in general and in particular
|
||||
1. Right now our treatment of static content in general and in particular
|
||||
course-specific static content is haphazard.
|
||||
2. We should have a more disciplined approach to feature flagging, even if it
|
||||
just means that we stick them in a dict called MITX_FEATURES.
|
||||
3. We need to handle configuration for multiple courses. This could be as
|
||||
3. We need to handle configuration for multiple courses. This could be as
|
||||
multiple sites, but we do need a way to map their data assets.
|
||||
"""
|
||||
import sys
|
||||
@@ -43,7 +43,7 @@ MITX_FEATURES = {
|
||||
'SAMPLE' : False,
|
||||
'USE_DJANGO_PIPELINE' : True,
|
||||
'DISPLAY_HISTOGRAMS_TO_STAFF' : True,
|
||||
'REROUTE_ACTIVATION_EMAIL' : False, # nonempty string = address for all activation emails
|
||||
'REROUTE_ACTIVATION_EMAIL' : False, # nonempty string = address for all activation emails
|
||||
'DEBUG_LEVEL' : 0, # 0 = lowest level, least verbose, 255 = max level, most verbose
|
||||
|
||||
## DO NOT SET TO True IN THIS FILE
|
||||
@@ -62,7 +62,7 @@ PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /mitx/lms
|
||||
REPO_ROOT = PROJECT_ROOT.dirname()
|
||||
COMMON_ROOT = REPO_ROOT / "common"
|
||||
ENV_ROOT = REPO_ROOT.dirname() # virtualenv dir /mitx is in
|
||||
ASKBOT_ROOT = ENV_ROOT / "askbot-devel"
|
||||
ASKBOT_ROOT = REPO_ROOT / "askbot"
|
||||
COURSES_ROOT = ENV_ROOT / "data"
|
||||
|
||||
# FIXME: To support multiple courses, we should walk the courses dir at startup
|
||||
@@ -86,7 +86,7 @@ MAKO_TEMPLATES['main'] = [PROJECT_ROOT / 'templates',
|
||||
COMMON_ROOT / 'lib' / 'capa' / 'capa' / 'templates',
|
||||
COMMON_ROOT / 'djangoapps' / 'pipeline_mako' / 'templates']
|
||||
|
||||
# This is where Django Template lookup is defined. There are a few of these
|
||||
# This is where Django Template lookup is defined. There are a few of these
|
||||
# still left lying around.
|
||||
TEMPLATE_DIRS = (
|
||||
PROJECT_ROOT / "templates",
|
||||
@@ -104,8 +104,8 @@ TEMPLATE_CONTEXT_PROCESSORS = (
|
||||
)
|
||||
|
||||
|
||||
# FIXME:
|
||||
# We should have separate S3 staged URLs in case we need to make changes to
|
||||
# FIXME:
|
||||
# We should have separate S3 staged URLs in case we need to make changes to
|
||||
# these assets and test them.
|
||||
LIB_URL = '/static/js/'
|
||||
|
||||
@@ -121,7 +121,7 @@ STATIC_GRAB = False
|
||||
DEV_CONTENT = True
|
||||
|
||||
# FIXME: Should we be doing this truncation?
|
||||
TRACK_MAX_EVENT = 10000
|
||||
TRACK_MAX_EVENT = 10000
|
||||
DEBUG_TRACK_LOG = False
|
||||
|
||||
MITX_ROOT_URL = ''
|
||||
@@ -130,7 +130,7 @@ COURSE_NAME = "6.002_Spring_2012"
|
||||
COURSE_NUMBER = "6.002x"
|
||||
COURSE_TITLE = "Circuits and Electronics"
|
||||
|
||||
### Dark code. Should be enabled in local settings for devel.
|
||||
### Dark code. Should be enabled in local settings for devel.
|
||||
|
||||
ENABLE_MULTICOURSE = False # set to False to disable multicourse display (see lib.util.views.mitxhome)
|
||||
QUICKEDIT = False
|
||||
@@ -212,9 +212,9 @@ USE_L10N = True
|
||||
MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
|
||||
|
||||
#################################### AWS #######################################
|
||||
# S3BotoStorage insists on a timeout for uploaded assets. We should make it
|
||||
# S3BotoStorage insists on a timeout for uploaded assets. We should make it
|
||||
# permanent instead, but rather than trying to figure out exactly where that
|
||||
# setting is, I'm just bumping the expiration time to something absurd (100
|
||||
# setting is, I'm just bumping the expiration time to something absurd (100
|
||||
# years). This is only used if DEFAULT_FILE_STORAGE is overriden to use S3
|
||||
# in the global settings.py
|
||||
AWS_QUERYSTRING_EXPIRE = 10 * 365 * 24 * 60 * 60 # 10 years
|
||||
@@ -281,7 +281,7 @@ MIDDLEWARE_CLASSES = (
|
||||
# Instead of AuthenticationMiddleware, we use a cached backed version
|
||||
#'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'cache_toolbox.middleware.CacheBackedAuthenticationMiddleware',
|
||||
|
||||
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'track.middleware.TrackMiddleware',
|
||||
'mitxmako.middleware.MakoMiddleware',
|
||||
@@ -303,15 +303,15 @@ STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage'
|
||||
PIPELINE_CSS = {
|
||||
'application': {
|
||||
'source_filenames': ['sass/application.scss'],
|
||||
'output_filename': 'css/application.css',
|
||||
'output_filename': 'css/lms-application.css',
|
||||
},
|
||||
'course': {
|
||||
'source_filenames': ['sass/course.scss', 'js/vendor/CodeMirror/codemirror.css', 'css/vendor/jquery.treeview.css'],
|
||||
'output_filename': 'css/course.css',
|
||||
'output_filename': 'css/lms-course.css',
|
||||
},
|
||||
'ie-fixes': {
|
||||
'source_filenames': ['sass/ie.scss'],
|
||||
'output_filename': 'css/ie.css',
|
||||
'output_filename': 'css/lms-ie.css',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -412,23 +412,23 @@ PIPELINE_JS = {
|
||||
'js/toggle_login_modal.js',
|
||||
'js/sticky_filter.js',
|
||||
],
|
||||
'output_filename': 'js/application.js'
|
||||
'output_filename': 'js/lms-application.js'
|
||||
},
|
||||
'courseware': {
|
||||
'source_filenames': [pth.replace(PROJECT_ROOT / 'static/', '') for pth in courseware_only_js],
|
||||
'output_filename': 'js/courseware.js'
|
||||
'output_filename': 'js/lms-courseware.js'
|
||||
},
|
||||
'main_vendor': {
|
||||
'source_filenames': main_vendor_js,
|
||||
'output_filename': 'js/main_vendor.js',
|
||||
'output_filename': 'js/lms-main_vendor.js',
|
||||
},
|
||||
'module-js': {
|
||||
'source_filenames': module_js_sources,
|
||||
'output_filename': 'js/modules.js',
|
||||
'output_filename': 'js/lms-modules.js',
|
||||
},
|
||||
'spec': {
|
||||
'source_filenames': [pth.replace(PROJECT_ROOT / 'static/', '') for pth in glob2.glob(PROJECT_ROOT / 'static/coffee/spec/**/*.coffee')],
|
||||
'output_filename': 'js/spec.js'
|
||||
'output_filename': 'js/lms-spec.js'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,17 +12,21 @@ from .logsettings import get_logger_config
|
||||
import os
|
||||
from path import path
|
||||
|
||||
INSTALLED_APPS = [
|
||||
app
|
||||
for app
|
||||
in INSTALLED_APPS
|
||||
if not app.startswith('askbot')
|
||||
]
|
||||
# can't test start dates with this True, but on the other hand,
|
||||
# can test everything else :)
|
||||
MITX_FEATURES['DISABLE_START_DATES'] = True
|
||||
|
||||
# Need wiki for courseware views to work. TODO (vshnayder): shouldn't need it.
|
||||
WIKI_ENABLED = True
|
||||
|
||||
# Makes the tests run much faster...
|
||||
SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead
|
||||
|
||||
# Nose Test Runner
|
||||
INSTALLED_APPS += ['django_nose']
|
||||
INSTALLED_APPS += ('django_nose',)
|
||||
NOSE_ARGS = ['--cover-erase', '--with-xunit', '--with-xcoverage', '--cover-html',
|
||||
'--cover-inclusive', '--cover-html-dir', os.environ.get('NOSE_COVER_HTML_DIR', 'cover_html')]
|
||||
'--cover-inclusive', '--cover-html-dir',
|
||||
os.environ.get('NOSE_COVER_HTML_DIR', 'cover_html')]
|
||||
for app in os.listdir(PROJECT_ROOT / 'djangoapps'):
|
||||
NOSE_ARGS += ['--cover-package', app]
|
||||
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
|
||||
@@ -30,25 +34,23 @@ TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
|
||||
# Local Directories
|
||||
TEST_ROOT = path("test_root")
|
||||
# Want static files in the same dir for running on jenkins.
|
||||
STATIC_ROOT = TEST_ROOT / "staticfiles"
|
||||
STATIC_ROOT = TEST_ROOT / "staticfiles"
|
||||
|
||||
COURSES_ROOT = TEST_ROOT / "data"
|
||||
DATA_DIR = COURSES_ROOT
|
||||
MAKO_TEMPLATES['course'] = [DATA_DIR]
|
||||
MAKO_TEMPLATES['sections'] = [DATA_DIR / 'sections']
|
||||
MAKO_TEMPLATES['custom_tags'] = [DATA_DIR / 'custom_tags']
|
||||
MAKO_TEMPLATES['main'] = [PROJECT_ROOT / 'templates',
|
||||
DATA_DIR / 'info',
|
||||
DATA_DIR / 'problems']
|
||||
|
||||
LOGGING = get_logger_config(TEST_ROOT / "log",
|
||||
LOGGING = get_logger_config(TEST_ROOT / "log",
|
||||
logging_env="dev",
|
||||
tracking_filename="tracking.log",
|
||||
debug=True)
|
||||
|
||||
COMMON_TEST_DATA_ROOT = COMMON_ROOT / "test" / "data"
|
||||
# Where the content data is checked out. This may not exist on jenkins.
|
||||
GITHUB_REPO_ROOT = ENV_ROOT / "data"
|
||||
|
||||
# TODO (cpennington): We need to figure out how envs/test.py can inject things into common.py so that we don't have to repeat this sort of thing
|
||||
|
||||
# TODO (cpennington): We need to figure out how envs/test.py can inject things
|
||||
# into common.py so that we don't have to repeat this sort of thing
|
||||
STATICFILES_DIRS = [
|
||||
COMMON_ROOT / "static",
|
||||
PROJECT_ROOT / "static",
|
||||
@@ -67,7 +69,7 @@ DATABASES = {
|
||||
}
|
||||
|
||||
CACHES = {
|
||||
# This is the cache used for most things. Askbot will not work without a
|
||||
# This is the cache used for most things. Askbot will not work without a
|
||||
# functioning cache -- it relies on caching to load its settings in places.
|
||||
# In staging/prod envs, the sessions also live here.
|
||||
'default': {
|
||||
|
||||
10
lms/envs/with_cms.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
Settings for the LMS that runs alongside the CMS on AWS
|
||||
"""
|
||||
|
||||
from .aws import *
|
||||
|
||||
with open(ENV_ROOT / "cms.auth.json") as auth_file:
|
||||
CMS_AUTH_TOKENS = json.load(auth_file)
|
||||
|
||||
MODULESTORE = CMS_AUTH_TOKENS['MODULESTORE']
|
||||
3
lms/static/.gitignore
vendored
@@ -5,4 +5,5 @@
|
||||
*.orig
|
||||
*.DS_Store
|
||||
application.css
|
||||
ie.css
|
||||
ie.css
|
||||
Gemfile.lock
|
||||
|
||||
97
lms/urls.py
@@ -13,23 +13,23 @@ if settings.DEBUG:
|
||||
urlpatterns = ('',
|
||||
url(r'^$', 'student.views.index', name="root"), # Main marketing page, or redirect to courseware
|
||||
url(r'^dashboard$', 'student.views.dashboard', name="dashboard"),
|
||||
|
||||
|
||||
url(r'^change_email$', 'student.views.change_email_request'),
|
||||
url(r'^email_confirm/(?P<key>[^/]*)$', 'student.views.confirm_email_change'),
|
||||
url(r'^change_name$', 'student.views.change_name_request'),
|
||||
url(r'^accept_name_change$', 'student.views.accept_name_change'),
|
||||
url(r'^reject_name_change$', 'student.views.reject_name_change'),
|
||||
url(r'^pending_name_changes$', 'student.views.pending_name_changes'),
|
||||
|
||||
|
||||
url(r'^event$', 'track.views.user_track'),
|
||||
url(r'^t/(?P<template>[^/]*)$', 'static_template_view.views.index'), # TODO: Is this used anymore? What is STATIC_GRAB?
|
||||
|
||||
url(r'^login$', 'student.views.login_user'),
|
||||
|
||||
url(r'^login$', 'student.views.login_user', name="login"),
|
||||
url(r'^login/(?P<error>[^/]*)$', 'student.views.login_user'),
|
||||
url(r'^logout$', 'student.views.logout_user', name='logout'),
|
||||
url(r'^create_account$', 'student.views.create_account'),
|
||||
url(r'^activate/(?P<key>[^/]*)$', 'student.views.activate_account'),
|
||||
|
||||
url(r'^activate/(?P<key>[^/]*)$', 'student.views.activate_account', name="activate"),
|
||||
|
||||
url(r'^password_reset/$', 'student.views.password_reset', name='password_reset'),
|
||||
## Obsolete Django views for password resets
|
||||
## TODO: Replace with Mako-ized views
|
||||
@@ -44,48 +44,48 @@ urlpatterns = ('',
|
||||
name='auth_password_reset_complete'),
|
||||
url(r'^password_reset_done/$', django.contrib.auth.views.password_reset_done,
|
||||
name='auth_password_reset_done'),
|
||||
|
||||
|
||||
url(r'^heartbeat$', include('heartbeat.urls')),
|
||||
|
||||
|
||||
url(r'^university_profile/(?P<org_id>[^/]+)$', 'courseware.views.university_profile', name="university_profile"),
|
||||
|
||||
|
||||
#Semi-static views (these need to be rendered and have the login bar, but don't change)
|
||||
url(r'^404$', 'static_template_view.views.render',
|
||||
url(r'^404$', 'static_template_view.views.render',
|
||||
{'template': '404.html'}, name="404"),
|
||||
url(r'^about$', 'static_template_view.views.render',
|
||||
url(r'^about$', 'static_template_view.views.render',
|
||||
{'template': 'about.html'}, name="about_edx"),
|
||||
url(r'^jobs$', 'static_template_view.views.render',
|
||||
url(r'^jobs$', 'static_template_view.views.render',
|
||||
{'template': 'jobs.html'}, name="jobs"),
|
||||
url(r'^contact$', 'static_template_view.views.render',
|
||||
url(r'^contact$', 'static_template_view.views.render',
|
||||
{'template': 'contact.html'}, name="contact"),
|
||||
url(r'^press$', 'student.views.press', name="press"),
|
||||
url(r'^faq$', 'static_template_view.views.render',
|
||||
url(r'^faq$', 'static_template_view.views.render',
|
||||
{'template': 'faq.html'}, name="faq_edx"),
|
||||
url(r'^help$', 'static_template_view.views.render',
|
||||
url(r'^help$', 'static_template_view.views.render',
|
||||
{'template': 'help.html'}, name="help_edx"),
|
||||
|
||||
url(r'^tos$', 'static_template_view.views.render',
|
||||
url(r'^tos$', 'static_template_view.views.render',
|
||||
{'template': 'tos.html'}, name="tos"),
|
||||
url(r'^privacy$', 'static_template_view.views.render',
|
||||
url(r'^privacy$', 'static_template_view.views.render',
|
||||
{'template': 'privacy.html'}, name="privacy_edx"),
|
||||
# TODO: (bridger) The copyright has been removed until it is updated for edX
|
||||
# url(r'^copyright$', 'static_template_view.views.render',
|
||||
# url(r'^copyright$', 'static_template_view.views.render',
|
||||
# {'template': 'copyright.html'}, name="copyright"),
|
||||
url(r'^honor$', 'static_template_view.views.render',
|
||||
url(r'^honor$', 'static_template_view.views.render',
|
||||
{'template': 'honor.html'}, name="honor"),
|
||||
|
||||
#Press releases
|
||||
url(r'^press/mit-and-harvard-announce-edx$', 'static_template_view.views.render',
|
||||
|
||||
#Press releases
|
||||
url(r'^press/mit-and-harvard-announce-edx$', 'static_template_view.views.render',
|
||||
{'template': 'press_releases/MIT_and_Harvard_announce_edX.html'}, name="press/mit-and-harvard-announce-edx"),
|
||||
url(r'^press/uc-berkeley-joins-edx$', 'static_template_view.views.render',
|
||||
url(r'^press/uc-berkeley-joins-edx$', 'static_template_view.views.render',
|
||||
{'template': 'press_releases/UC_Berkeley_joins_edX.html'}, name="press/uc-berkeley-joins-edx"),
|
||||
# Should this always update to point to the latest press release?
|
||||
(r'^pressrelease$', 'django.views.generic.simple.redirect_to', {'url': '/press/uc-berkeley-joins-edx'}),
|
||||
|
||||
|
||||
|
||||
(r'^pressrelease$', 'django.views.generic.simple.redirect_to', {'url': '/press/uc-berkeley-joins-edx'}),
|
||||
|
||||
|
||||
|
||||
(r'^favicon\.ico$', 'django.views.generic.simple.redirect_to', {'url': '/static/images/favicon.ico'}),
|
||||
|
||||
|
||||
# TODO: These urls no longer work. They need to be updated before they are re-enabled
|
||||
# url(r'^send_feedback$', 'util.views.send_feedback'),
|
||||
# url(r'^reactivate/(?P<key>[^/]*)$', 'student.views.reactivation_email'),
|
||||
@@ -97,42 +97,43 @@ if settings.PERFSTATS:
|
||||
if settings.COURSEWARE_ENABLED:
|
||||
urlpatterns += (
|
||||
url(r'^masquerade/', include('masquerade.urls')),
|
||||
url(r'^jumpto/(?P<probname>[^/]+)/$', 'courseware.views.jump_to'),
|
||||
url(r'^jump_to/(?P<location>.*)$', 'courseware.views.jump_to', name="jump_to"),
|
||||
|
||||
url(r'^modx/(?P<id>.*?)/(?P<dispatch>[^/]*)$', 'courseware.module_render.modx_dispatch'), #reset_problem'),
|
||||
url(r'^xqueue/(?P<userid>[^/]*)/(?P<id>.*?)/(?P<dispatch>[^/]*)$', 'courseware.module_render.xqueue_callback'),
|
||||
url(r'^change_setting$', 'student.views.change_setting'),
|
||||
|
||||
|
||||
# TODO: These views need to be updated before they work
|
||||
# url(r'^calculate$', 'util.views.calculate'),
|
||||
# url(r'^gradebook$', 'courseware.views.gradebook'),
|
||||
# TODO: We should probably remove the circuit package. I believe it was only used in the old way of saving wiki circuits for the wiki
|
||||
# url(r'^edit_circuit/(?P<circuit>[^/]*)$', 'circuit.views.edit_circuit'),
|
||||
# url(r'^save_circuit/(?P<circuit>[^/]*)$', 'circuit.views.save_circuit'),
|
||||
|
||||
url(r'^courses/?$', 'courseware.views.courses', name="courses"),
|
||||
url(r'^change_enrollment$',
|
||||
|
||||
url(r'^courses/?$', 'courseware.views.courses', name="courses"),
|
||||
url(r'^change_enrollment$',
|
||||
'student.views.change_enrollment_view', name="change_enrollment"),
|
||||
|
||||
|
||||
#About the course
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/about$',
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/about$',
|
||||
'courseware.views.course_about', name="about_course"),
|
||||
|
||||
|
||||
#Inside the course
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/info$',
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/info$',
|
||||
'courseware.views.course_info', name="info"),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book$',
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book$',
|
||||
'staticbook.views.index', name="book"),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book/(?P<page>[^/]*)$',
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book/(?P<page>[^/]*)$',
|
||||
'staticbook.views.index'),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book-shifted/(?P<page>[^/]*)$',
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book-shifted/(?P<page>[^/]*)$',
|
||||
'staticbook.views.index_shifted'),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/?$',
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/?$',
|
||||
'courseware.views.index', name="courseware"),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/(?P<chapter>[^/]*)/(?P<section>[^/]*)/$',
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/(?P<chapter>[^/]*)/(?P<section>[^/]*)/$',
|
||||
'courseware.views.index', name="courseware_section"),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/profile$',
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/profile$',
|
||||
'courseware.views.profile', name="profile"),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/profile/(?P<student_id>[^/]*)/$',
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/profile/(?P<student_id>[^/]*)/$',
|
||||
'courseware.views.profile'),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/news$',
|
||||
'courseware.views.news', name="news"),
|
||||
@@ -141,7 +142,7 @@ if settings.COURSEWARE_ENABLED:
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/discussion/',
|
||||
include('django_comment_client.urls')),
|
||||
)
|
||||
|
||||
|
||||
# Multicourse wiki
|
||||
if settings.WIKI_ENABLED:
|
||||
urlpatterns += (
|
||||
@@ -170,9 +171,9 @@ urlpatterns = patterns(*urlpatterns)
|
||||
|
||||
if settings.DEBUG:
|
||||
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
|
||||
|
||||
#Custom error pages
|
||||
|
||||
|
||||
#Custom error pages
|
||||
handler404 = 'static_template_view.views.render_404'
|
||||
handler500 = 'static_template_view.views.render_500'
|
||||
|
||||
|
||||
13
rakefile
@@ -27,7 +27,7 @@ NORMALIZED_DEPLOY_NAME = DEPLOY_NAME.downcase().gsub(/[_\/]/, '-')
|
||||
INSTALL_DIR_PATH = File.join(DEPLOY_DIR, NORMALIZED_DEPLOY_NAME)
|
||||
PIP_REPO_REQUIREMENTS = "#{INSTALL_DIR_PATH}/repo-requirements.txt"
|
||||
# Set up the clean and clobber tasks
|
||||
CLOBBER.include(BUILD_DIR, REPORT_DIR, 'cover*', '.coverage', 'test_root/*_repo')
|
||||
CLOBBER.include(BUILD_DIR, REPORT_DIR, 'cover*', '.coverage', 'test_root/*_repo', 'test_root/staticfiles')
|
||||
CLEAN.include("#{BUILD_DIR}/*.deb", "#{BUILD_DIR}/util")
|
||||
|
||||
def select_executable(*cmds)
|
||||
@@ -51,6 +51,11 @@ default_options = {
|
||||
task :predjango do
|
||||
sh("find . -type f -name *.pyc -delete")
|
||||
sh('pip install -e common/lib/xmodule')
|
||||
sh('git submodule update --init')
|
||||
end
|
||||
|
||||
task :clean_test_files do
|
||||
sh("git clean -fdx test_root")
|
||||
end
|
||||
|
||||
[:lms, :cms, :common].each do |system|
|
||||
@@ -92,7 +97,7 @@ end
|
||||
|
||||
# Per System tasks
|
||||
desc "Run all django tests on our djangoapps for the #{system}"
|
||||
task "test_#{system}" => ["#{system}:collectstatic:test", "fasttest_#{system}"]
|
||||
task "test_#{system}" => ["clean_test_files", "#{system}:collectstatic:test", "fasttest_#{system}"]
|
||||
|
||||
# Have a way to run the tests without running collectstatic -- useful when debugging without
|
||||
# messing with static files.
|
||||
@@ -150,14 +155,14 @@ end
|
||||
|
||||
task :package do
|
||||
FileUtils.mkdir_p(BUILD_DIR)
|
||||
|
||||
|
||||
Dir.chdir(BUILD_DIR) do
|
||||
afterremove = Tempfile.new('afterremove')
|
||||
afterremove.write <<-AFTERREMOVE.gsub(/^\s*/, '')
|
||||
#! /bin/bash
|
||||
set -e
|
||||
set -x
|
||||
|
||||
|
||||
# to be a little safer this rm is executed
|
||||
# as the makeitso user
|
||||
|
||||
|
||||