Merge branch 'dormsbee/multicourse' of github.com:MITx/mitx into dormsbee/multicourse
This commit is contained in:
@@ -5,12 +5,17 @@ import logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def import_from_xml(org, course, data_dir):
|
||||
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(org, course, data_dir, 'xmodule.raw_module.RawDescriptor', eager=True)
|
||||
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.
|
||||
@@ -26,4 +31,4 @@ def import_from_xml(org, course, data_dir):
|
||||
modulestore().update_children(module.location, module.definition['children'])
|
||||
modulestore().update_metadata(module.location, dict(module.metadata))
|
||||
|
||||
return module_store.course
|
||||
return module_store
|
||||
|
||||
@@ -13,8 +13,12 @@ class Command(BaseCommand):
|
||||
'''Import the specified data directory into the default ModuleStore'''
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if len(args) != 3:
|
||||
raise CommandError("import requires 3 arguments: <org> <course> <data directory>")
|
||||
if len(args) == 0:
|
||||
raise CommandError("import requires at least one argument: <data directory> [<course dir>...]")
|
||||
|
||||
org, course, data_dir = args
|
||||
import_from_xml(org, course, data_dir)
|
||||
data_dir = args[0]
|
||||
if len(args) > 1:
|
||||
course_dirs = args[1:]
|
||||
else:
|
||||
course_dirs = None
|
||||
import_from_xml(data_dir, course_dirs)
|
||||
|
||||
@@ -6,7 +6,7 @@ from django_future.csrf import ensure_csrf_cookie
|
||||
from fs.osfs import OSFS
|
||||
from django.core.urlresolvers import reverse
|
||||
from xmodule.modulestore import Location
|
||||
from github_sync import repo_path_from_location, export_to_github
|
||||
from github_sync import export_to_github
|
||||
|
||||
from mitxmako.shortcuts import render_to_response
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -51,11 +51,12 @@ def save_item(request):
|
||||
modulestore().update_item(item_id, data)
|
||||
|
||||
# Export the course back to github
|
||||
# This uses wildcarding to find the course, which requires handling
|
||||
# multiple courses returned, but there should only ever be one
|
||||
course_location = Location(item_id)._replace(category='course', name=None)
|
||||
courses = modulestore().get_items(course_location)
|
||||
for course in courses:
|
||||
repo_path = repo_path_from_location(course.location)
|
||||
export_to_github(course, repo_path, "CMS Edit")
|
||||
export_to_github(course, "CMS Edit")
|
||||
|
||||
return HttpResponse(json.dumps({}))
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ from .exceptions import GithubSyncError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def import_from_github(repo_settings):
|
||||
"""
|
||||
Imports data into the modulestore based on the XML stored on github
|
||||
@@ -19,10 +20,9 @@ def import_from_github(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
|
||||
org: name of the organization to use in the imported course
|
||||
course: name of the coures to use in the imported course
|
||||
"""
|
||||
repo_path = repo_settings['path']
|
||||
data_dir, course_dir = os.path.split(repo_path)
|
||||
|
||||
if not os.path.isdir(repo_path):
|
||||
Repo.clone_from(repo_settings['origin'], repo_path)
|
||||
@@ -34,18 +34,12 @@ def import_from_github(repo_settings):
|
||||
# Do a hard reset to the remote branch so that we have a clean import
|
||||
git_repo.git.checkout(repo_settings['branch'])
|
||||
git_repo.head.reset('origin/%s' % repo_settings['branch'], index=True, working_tree=True)
|
||||
|
||||
return git_repo.head.commit.hexsha, import_from_xml(repo_settings['org'], repo_settings['course'], repo_path)
|
||||
module_store = import_from_xml(data_dir, course_dirs=[course_dir])
|
||||
return git_repo.head.commit.hexsha, module_store.courses[course_dir]
|
||||
|
||||
|
||||
def repo_path_from_location(location):
|
||||
location = Location(location)
|
||||
for name, repo in settings.REPOS.items():
|
||||
if repo['org'] == location.org and repo['course'] == location.course:
|
||||
return repo['path']
|
||||
|
||||
|
||||
def export_to_github(course, repo_path, commit_message):
|
||||
def export_to_github(course, commit_message):
|
||||
repo_path = settings.DATA_DIR / course.metadata.get('course_dir', course.location.course)
|
||||
fs = OSFS(repo_path)
|
||||
xml = course.export_to_xml(fs)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from django.test import TestCase
|
||||
from path import path
|
||||
import shutil
|
||||
from github_sync import import_from_github, export_to_github, repo_path_from_location
|
||||
from github_sync import import_from_github, export_to_github
|
||||
from git import Repo
|
||||
from django.conf import settings
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -10,6 +10,7 @@ from override_settings import override_settings
|
||||
from github_sync.exceptions import GithubSyncError
|
||||
|
||||
|
||||
@override_settings(DATA_DIR=path('test_root'))
|
||||
class GithubSyncTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
@@ -29,8 +30,6 @@ class GithubSyncTestCase(TestCase):
|
||||
'path': self.repo_dir,
|
||||
'origin': self.remote_dir,
|
||||
'branch': 'master',
|
||||
'org': 'org',
|
||||
'course': 'course'
|
||||
})
|
||||
|
||||
def tearDown(self):
|
||||
@@ -49,7 +48,7 @@ class GithubSyncTestCase(TestCase):
|
||||
"""
|
||||
self.assertEquals('Toy Course', self.import_course.metadata['display_name'])
|
||||
self.assertIn(
|
||||
Location('i4x://org/course/chapter/Overview'),
|
||||
Location('i4x://edx/local_repo/chapter/Overview'),
|
||||
[child.location for child in self.import_course.get_children()])
|
||||
self.assertEquals(1, len(self.import_course.get_children()))
|
||||
|
||||
@@ -58,7 +57,7 @@ class GithubSyncTestCase(TestCase):
|
||||
"""
|
||||
Test that with the GITHUB_PUSH feature disabled, no content is pushed to the remote
|
||||
"""
|
||||
export_to_github(self.import_course, self.repo_dir, 'Test no-push')
|
||||
export_to_github(self.import_course, 'Test no-push')
|
||||
self.assertEquals(1, Repo(self.remote_dir).head.commit.count())
|
||||
|
||||
@override_settings(MITX_FEATURES={'GITHUB_PUSH': True})
|
||||
@@ -67,7 +66,7 @@ class GithubSyncTestCase(TestCase):
|
||||
Test that with GITHUB_PUSH enabled, content is pushed to the remote
|
||||
"""
|
||||
self.import_course.metadata['display_name'] = 'Changed display name'
|
||||
export_to_github(self.import_course, self.repo_dir, 'Test push')
|
||||
export_to_github(self.import_course, 'Test push')
|
||||
self.assertEquals(2, Repo(self.remote_dir).head.commit.count())
|
||||
|
||||
@override_settings(MITX_FEATURES={'GITHUB_PUSH': True})
|
||||
@@ -80,17 +79,6 @@ class GithubSyncTestCase(TestCase):
|
||||
remote = Repo(self.remote_dir)
|
||||
remote.git.commit(allow_empty=True, m="Testing conflict commit")
|
||||
|
||||
self.assertRaises(GithubSyncError, export_to_github, self.import_course, self.repo_dir, 'Test push')
|
||||
self.assertRaises(GithubSyncError, export_to_github, self.import_course, 'Test push')
|
||||
self.assertEquals(2, remote.head.reference.commit.count())
|
||||
self.assertEquals("Testing conflict commit\n", remote.head.reference.commit.message)
|
||||
|
||||
|
||||
@override_settings(REPOS={'namea': {'path': 'patha', 'org': 'orga', 'course': 'coursea'},
|
||||
'nameb': {'path': 'pathb', 'org': 'orgb', 'course': 'courseb'}})
|
||||
class RepoPathLookupTestCase(TestCase):
|
||||
def test_successful_lookup(self):
|
||||
self.assertEquals('patha', repo_path_from_location('i4x://orga/coursea/course/foo'))
|
||||
self.assertEquals('pathb', repo_path_from_location('i4x://orgb/courseb/course/foo'))
|
||||
|
||||
def test_failed_lookup(self):
|
||||
self.assertEquals(None, repo_path_from_location('i4x://c/c/course/foo'))
|
||||
|
||||
@@ -412,210 +412,208 @@ class GraderTest(unittest.TestCase):
|
||||
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
|
||||
self.assertEqual( len(graded['grade_breakdown']), 3 )
|
||||
|
||||
graded = zeroWeightsGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.2525 )
|
||||
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
|
||||
self.assertEqual( len(graded['grade_breakdown']), 3 )
|
||||
graded = zeroWeightsGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.2525 )
|
||||
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
|
||||
self.assertEqual( len(graded['grade_breakdown']), 3 )
|
||||
|
||||
|
||||
graded = allZeroWeightsGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.0 )
|
||||
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
|
||||
self.assertEqual( len(graded['grade_breakdown']), 3 )
|
||||
|
||||
for graded in [ weightedGrader.grade(self.empty_gradesheet),
|
||||
weightedGrader.grade(self.incomplete_gradesheet),
|
||||
zeroWeightsGrader.grade(self.empty_gradesheet),
|
||||
allZeroWeightsGrader.grade(self.empty_gradesheet)]:
|
||||
graded = allZeroWeightsGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.0 )
|
||||
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
|
||||
self.assertEqual( len(graded['grade_breakdown']), 3 )
|
||||
|
||||
for graded in [ weightedGrader.grade(self.empty_gradesheet),
|
||||
weightedGrader.grade(self.incomplete_gradesheet),
|
||||
zeroWeightsGrader.grade(self.empty_gradesheet),
|
||||
allZeroWeightsGrader.grade(self.empty_gradesheet)]:
|
||||
self.assertAlmostEqual( graded['percent'], 0.0 )
|
||||
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
|
||||
self.assertEqual( len(graded['grade_breakdown']), 3 )
|
||||
|
||||
|
||||
graded = emptyGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.0 )
|
||||
self.assertEqual( len(graded['section_breakdown']), 0 )
|
||||
self.assertEqual( len(graded['grade_breakdown']), 0 )
|
||||
|
||||
def test_graderFromConf(self):
|
||||
|
||||
graded = emptyGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.0 )
|
||||
self.assertEqual( len(graded['section_breakdown']), 0 )
|
||||
self.assertEqual( len(graded['grade_breakdown']), 0 )
|
||||
#Confs always produce a graders.WeightedSubsectionsGrader, so we test this by repeating the test
|
||||
#in test_graders.WeightedSubsectionsGrader, but generate the graders with confs.
|
||||
|
||||
weightedGrader = graders.grader_from_conf([
|
||||
{
|
||||
'type' : "Homework",
|
||||
'min_count' : 12,
|
||||
'drop_count' : 2,
|
||||
'short_label' : "HW",
|
||||
'weight' : 0.25,
|
||||
},
|
||||
{
|
||||
'type' : "Lab",
|
||||
'min_count' : 7,
|
||||
'drop_count' : 3,
|
||||
'category' : "Labs",
|
||||
'weight' : 0.25
|
||||
},
|
||||
{
|
||||
'type' : "Midterm",
|
||||
'name' : "Midterm Exam",
|
||||
'short_label' : "Midterm",
|
||||
'weight' : 0.5,
|
||||
},
|
||||
])
|
||||
|
||||
emptyGrader = graders.grader_from_conf([])
|
||||
|
||||
graded = weightedGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.5106547619047619 )
|
||||
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
|
||||
self.assertEqual( len(graded['grade_breakdown']), 3 )
|
||||
|
||||
graded = emptyGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.0 )
|
||||
self.assertEqual( len(graded['section_breakdown']), 0 )
|
||||
self.assertEqual( len(graded['grade_breakdown']), 0 )
|
||||
|
||||
#Test that graders can also be used instead of lists of dictionaries
|
||||
homeworkGrader = graders.AssignmentFormatGrader("Homework", 12, 2)
|
||||
homeworkGrader2 = graders.grader_from_conf(homeworkGrader)
|
||||
|
||||
graded = homeworkGrader2.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.11 )
|
||||
self.assertEqual( len(graded['section_breakdown']), 12 + 1 )
|
||||
|
||||
|
||||
|
||||
def test_graderFromConf(self):
|
||||
|
||||
#Confs always produce a graders.WeightedSubsectionsGrader, so we test this by repeating the test
|
||||
#in test_graders.WeightedSubsectionsGrader, but generate the graders with confs.
|
||||
|
||||
weightedGrader = graders.grader_from_conf([
|
||||
{
|
||||
'type' : "Homework",
|
||||
'min_count' : 12,
|
||||
'drop_count' : 2,
|
||||
'short_label' : "HW",
|
||||
'weight' : 0.25,
|
||||
},
|
||||
{
|
||||
'type' : "Lab",
|
||||
'min_count' : 7,
|
||||
'drop_count' : 3,
|
||||
'category' : "Labs",
|
||||
'weight' : 0.25
|
||||
},
|
||||
{
|
||||
'type' : "Midterm",
|
||||
'name' : "Midterm Exam",
|
||||
'short_label' : "Midterm",
|
||||
'weight' : 0.5,
|
||||
},
|
||||
])
|
||||
|
||||
emptyGrader = graders.grader_from_conf([])
|
||||
|
||||
graded = weightedGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.5106547619047619 )
|
||||
self.assertEqual( len(graded['section_breakdown']), (12 + 1) + (7+1) + 1 )
|
||||
self.assertEqual( len(graded['grade_breakdown']), 3 )
|
||||
|
||||
graded = emptyGrader.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.0 )
|
||||
self.assertEqual( len(graded['section_breakdown']), 0 )
|
||||
self.assertEqual( len(graded['grade_breakdown']), 0 )
|
||||
|
||||
#Test that graders can also be used instead of lists of dictionaries
|
||||
homeworkGrader = graders.AssignmentFormatGrader("Homework", 12, 2)
|
||||
homeworkGrader2 = graders.grader_from_conf(homeworkGrader)
|
||||
|
||||
graded = homeworkGrader2.grade(self.test_gradesheet)
|
||||
self.assertAlmostEqual( graded['percent'], 0.11 )
|
||||
self.assertEqual( len(graded['section_breakdown']), 12 + 1 )
|
||||
|
||||
#TODO: How do we test failure cases? The parser only logs an error when it can't parse something. Maybe it should throw exceptions?
|
||||
#TODO: How do we test failure cases? The parser only logs an error when it can't parse something. Maybe it should throw exceptions?
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Module progress tests
|
||||
|
||||
class ProgressTest(unittest.TestCase):
|
||||
''' Test that basic Progress objects work. A Progress represents a
|
||||
fraction between 0 and 1.
|
||||
'''
|
||||
not_started = Progress(0, 17)
|
||||
part_done = Progress(2, 6)
|
||||
half_done = Progress(3, 6)
|
||||
also_half_done = Progress(1, 2)
|
||||
done = Progress(7, 7)
|
||||
''' Test that basic Progress objects work. A Progress represents a
|
||||
fraction between 0 and 1.
|
||||
'''
|
||||
not_started = Progress(0, 17)
|
||||
part_done = Progress(2, 6)
|
||||
half_done = Progress(3, 6)
|
||||
also_half_done = Progress(1, 2)
|
||||
done = Progress(7, 7)
|
||||
|
||||
def test_create_object(self):
|
||||
# These should work:
|
||||
p = Progress(0, 2)
|
||||
p = Progress(1, 2)
|
||||
p = Progress(2, 2)
|
||||
def test_create_object(self):
|
||||
# These should work:
|
||||
p = Progress(0, 2)
|
||||
p = Progress(1, 2)
|
||||
p = Progress(2, 2)
|
||||
|
||||
p = Progress(2.5, 5.0)
|
||||
p = Progress(3.7, 12.3333)
|
||||
|
||||
# These shouldn't
|
||||
self.assertRaises(ValueError, Progress, 0, 0)
|
||||
self.assertRaises(ValueError, Progress, 2, 0)
|
||||
self.assertRaises(ValueError, Progress, 1, -2)
|
||||
self.assertRaises(ValueError, Progress, 3, 2)
|
||||
self.assertRaises(ValueError, Progress, -2, 5)
|
||||
p = Progress(2.5, 5.0)
|
||||
p = Progress(3.7, 12.3333)
|
||||
|
||||
# These shouldn't
|
||||
self.assertRaises(ValueError, Progress, 0, 0)
|
||||
self.assertRaises(ValueError, Progress, 2, 0)
|
||||
self.assertRaises(ValueError, Progress, 1, -2)
|
||||
self.assertRaises(ValueError, Progress, 3, 2)
|
||||
self.assertRaises(ValueError, Progress, -2, 5)
|
||||
|
||||
self.assertRaises(TypeError, Progress, 0, "all")
|
||||
# check complex numbers just for the heck of it :)
|
||||
self.assertRaises(TypeError, Progress, 2j, 3)
|
||||
self.assertRaises(TypeError, Progress, 0, "all")
|
||||
# check complex numbers just for the heck of it :)
|
||||
self.assertRaises(TypeError, Progress, 2j, 3)
|
||||
|
||||
def test_frac(self):
|
||||
p = Progress(1, 2)
|
||||
(a, b) = p.frac()
|
||||
self.assertEqual(a, 1)
|
||||
self.assertEqual(b, 2)
|
||||
def test_frac(self):
|
||||
p = Progress(1, 2)
|
||||
(a, b) = p.frac()
|
||||
self.assertEqual(a, 1)
|
||||
self.assertEqual(b, 2)
|
||||
|
||||
def test_percent(self):
|
||||
self.assertEqual(self.not_started.percent(), 0)
|
||||
self.assertAlmostEqual(self.part_done.percent(), 33.33333333333333)
|
||||
self.assertEqual(self.half_done.percent(), 50)
|
||||
self.assertEqual(self.done.percent(), 100)
|
||||
def test_percent(self):
|
||||
self.assertEqual(self.not_started.percent(), 0)
|
||||
self.assertAlmostEqual(self.part_done.percent(), 33.33333333333333)
|
||||
self.assertEqual(self.half_done.percent(), 50)
|
||||
self.assertEqual(self.done.percent(), 100)
|
||||
|
||||
self.assertEqual(self.half_done.percent(), self.also_half_done.percent())
|
||||
self.assertEqual(self.half_done.percent(), self.also_half_done.percent())
|
||||
|
||||
def test_started(self):
|
||||
self.assertFalse(self.not_started.started())
|
||||
def test_started(self):
|
||||
self.assertFalse(self.not_started.started())
|
||||
|
||||
self.assertTrue(self.part_done.started())
|
||||
self.assertTrue(self.half_done.started())
|
||||
self.assertTrue(self.done.started())
|
||||
self.assertTrue(self.part_done.started())
|
||||
self.assertTrue(self.half_done.started())
|
||||
self.assertTrue(self.done.started())
|
||||
|
||||
def test_inprogress(self):
|
||||
# only true if working on it
|
||||
self.assertFalse(self.done.inprogress())
|
||||
self.assertFalse(self.not_started.inprogress())
|
||||
def test_inprogress(self):
|
||||
# only true if working on it
|
||||
self.assertFalse(self.done.inprogress())
|
||||
self.assertFalse(self.not_started.inprogress())
|
||||
|
||||
self.assertTrue(self.part_done.inprogress())
|
||||
self.assertTrue(self.half_done.inprogress())
|
||||
self.assertTrue(self.part_done.inprogress())
|
||||
self.assertTrue(self.half_done.inprogress())
|
||||
|
||||
def test_done(self):
|
||||
self.assertTrue(self.done.done())
|
||||
self.assertFalse(self.half_done.done())
|
||||
self.assertFalse(self.not_started.done())
|
||||
|
||||
def test_str(self):
|
||||
self.assertEqual(str(self.not_started), "0/17")
|
||||
self.assertEqual(str(self.part_done), "2/6")
|
||||
self.assertEqual(str(self.done), "7/7")
|
||||
def test_done(self):
|
||||
self.assertTrue(self.done.done())
|
||||
self.assertFalse(self.half_done.done())
|
||||
self.assertFalse(self.not_started.done())
|
||||
|
||||
def test_str(self):
|
||||
self.assertEqual(str(self.not_started), "0/17")
|
||||
self.assertEqual(str(self.part_done), "2/6")
|
||||
self.assertEqual(str(self.done), "7/7")
|
||||
|
||||
def test_ternary_str(self):
|
||||
self.assertEqual(self.not_started.ternary_str(), "none")
|
||||
self.assertEqual(self.half_done.ternary_str(), "in_progress")
|
||||
self.assertEqual(self.done.ternary_str(), "done")
|
||||
def test_ternary_str(self):
|
||||
self.assertEqual(self.not_started.ternary_str(), "none")
|
||||
self.assertEqual(self.half_done.ternary_str(), "in_progress")
|
||||
self.assertEqual(self.done.ternary_str(), "done")
|
||||
|
||||
def test_to_js_status(self):
|
||||
'''Test the Progress.to_js_status_str() method'''
|
||||
|
||||
self.assertEqual(Progress.to_js_status_str(self.not_started), "none")
|
||||
self.assertEqual(Progress.to_js_status_str(self.half_done), "in_progress")
|
||||
self.assertEqual(Progress.to_js_status_str(self.done), "done")
|
||||
self.assertEqual(Progress.to_js_status_str(None), "NA")
|
||||
def test_to_js_status(self):
|
||||
'''Test the Progress.to_js_status_str() method'''
|
||||
|
||||
self.assertEqual(Progress.to_js_status_str(self.not_started), "none")
|
||||
self.assertEqual(Progress.to_js_status_str(self.half_done), "in_progress")
|
||||
self.assertEqual(Progress.to_js_status_str(self.done), "done")
|
||||
self.assertEqual(Progress.to_js_status_str(None), "NA")
|
||||
|
||||
def test_to_js_detail_str(self):
|
||||
'''Test the Progress.to_js_detail_str() method'''
|
||||
f = Progress.to_js_detail_str
|
||||
for p in (self.not_started, self.half_done, self.done):
|
||||
self.assertEqual(f(p), str(p))
|
||||
# But None should be encoded as NA
|
||||
self.assertEqual(f(None), "NA")
|
||||
def test_to_js_detail_str(self):
|
||||
'''Test the Progress.to_js_detail_str() method'''
|
||||
f = Progress.to_js_detail_str
|
||||
for p in (self.not_started, self.half_done, self.done):
|
||||
self.assertEqual(f(p), str(p))
|
||||
# But None should be encoded as NA
|
||||
self.assertEqual(f(None), "NA")
|
||||
|
||||
def test_add(self):
|
||||
'''Test the Progress.add_counts() method'''
|
||||
p = Progress(0, 2)
|
||||
p2 = Progress(1, 3)
|
||||
p3 = Progress(2, 5)
|
||||
pNone = None
|
||||
add = lambda a, b: Progress.add_counts(a, b).frac()
|
||||
def test_add(self):
|
||||
'''Test the Progress.add_counts() method'''
|
||||
p = Progress(0, 2)
|
||||
p2 = Progress(1, 3)
|
||||
p3 = Progress(2, 5)
|
||||
pNone = None
|
||||
add = lambda a, b: Progress.add_counts(a, b).frac()
|
||||
|
||||
self.assertEqual(add(p, p), (0, 4))
|
||||
self.assertEqual(add(p, p2), (1, 5))
|
||||
self.assertEqual(add(p2, p3), (3, 8))
|
||||
|
||||
self.assertEqual(add(p2, pNone), p2.frac())
|
||||
self.assertEqual(add(pNone, p2), p2.frac())
|
||||
self.assertEqual(add(p, p), (0, 4))
|
||||
self.assertEqual(add(p, p2), (1, 5))
|
||||
self.assertEqual(add(p2, p3), (3, 8))
|
||||
|
||||
self.assertEqual(add(p2, pNone), p2.frac())
|
||||
self.assertEqual(add(pNone, p2), p2.frac())
|
||||
|
||||
def test_equality(self):
|
||||
'''Test that comparing Progress objects for equality
|
||||
works correctly.'''
|
||||
p = Progress(1, 2)
|
||||
p2 = Progress(2, 4)
|
||||
p3 = Progress(1, 2)
|
||||
self.assertTrue(p == p3)
|
||||
self.assertFalse(p == p2)
|
||||
def test_equality(self):
|
||||
'''Test that comparing Progress objects for equality
|
||||
works correctly.'''
|
||||
p = Progress(1, 2)
|
||||
p2 = Progress(2, 4)
|
||||
p3 = Progress(1, 2)
|
||||
self.assertTrue(p == p3)
|
||||
self.assertFalse(p == p2)
|
||||
|
||||
# Check != while we're at it
|
||||
self.assertTrue(p != p2)
|
||||
self.assertFalse(p != p3)
|
||||
# Check != while we're at it
|
||||
self.assertTrue(p != p2)
|
||||
self.assertFalse(p != p3)
|
||||
|
||||
|
||||
class ModuleProgressTest(unittest.TestCase):
|
||||
''' Test that get_progress() does the right thing for the different modules
|
||||
'''
|
||||
def test_xmodule_default(self):
|
||||
'''Make sure default get_progress exists, returns None'''
|
||||
''' Test that get_progress() does the right thing for the different modules
|
||||
'''
|
||||
def test_xmodule_default(self):
|
||||
'''Make sure default get_progress exists, returns None'''
|
||||
xm = x_module.XModule(i4xs, 'a://b/c/d/e', {})
|
||||
p = xm.get_progress()
|
||||
self.assertEqual(p, None)
|
||||
|
||||
@@ -15,7 +15,6 @@ from xmodule.raw_module import RawDescriptor
|
||||
from progress import Progress
|
||||
from capa.capa_problem import LoncapaProblem
|
||||
from capa.responsetypes import StudentInputError
|
||||
from static_replace import replace_urls
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
@@ -275,7 +274,7 @@ class CapaModule(XModule):
|
||||
html = '<div id="problem_{id}" class="problem" data-url="{ajax_url}">'.format(
|
||||
id=self.location.html_id(), ajax_url=self.system.ajax_url) + html + "</div>"
|
||||
|
||||
return replace_urls(html, self.metadata['data_dir'])
|
||||
return self.system.replace_urls(html, self.metadata['data_dir'])
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
'''
|
||||
|
||||
@@ -20,19 +20,21 @@ class XMLModuleStore(ModuleStore):
|
||||
"""
|
||||
An XML backed ModuleStore
|
||||
"""
|
||||
def __init__(self, data_dir, default_class=None, eager=False):
|
||||
def __init__(self, data_dir, default_class=None, eager=False, course_dirs=None):
|
||||
"""
|
||||
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
|
||||
"""
|
||||
|
||||
self.eager = eager
|
||||
self.data_dir = path(data_dir)
|
||||
self.modules = {}
|
||||
self.courses = []
|
||||
self.courses = {}
|
||||
|
||||
if default_class is None:
|
||||
self.default_class = None
|
||||
@@ -46,10 +48,14 @@ class XMLModuleStore(ModuleStore):
|
||||
log.debug('default_class = %s' % self.default_class)
|
||||
|
||||
for course_dir in os.listdir(self.data_dir):
|
||||
if not os.path.exists(self.data_dir + "/" + course_dir + "/course.xml"):
|
||||
if course_dirs is not None and course_dir not in course_dirs:
|
||||
continue
|
||||
|
||||
self.courses.append(self.load_course(course_dir))
|
||||
if not os.path.exists(self.data_dir / course_dir / "course.xml"):
|
||||
continue
|
||||
|
||||
course_descriptor = self.load_course(course_dir)
|
||||
self.courses[course_dir] = course_descriptor
|
||||
|
||||
def load_course(self, course_dir):
|
||||
"""
|
||||
@@ -148,7 +154,7 @@ class XMLModuleStore(ModuleStore):
|
||||
"""
|
||||
Returns a list of course descriptors
|
||||
"""
|
||||
return self.courses
|
||||
return self.courses.values()
|
||||
|
||||
def create_item(self, location):
|
||||
raise NotImplementedError("XMLModuleStores are read-only")
|
||||
|
||||
@@ -27,8 +27,8 @@ class I4xSystem(object):
|
||||
and user, or other environment-specific info.
|
||||
'''
|
||||
def __init__(self, ajax_url, track_function,
|
||||
get_module, render_template, user=None,
|
||||
filestore=None):
|
||||
get_module, render_template, replace_urls,
|
||||
user=None, filestore=None):
|
||||
'''
|
||||
Create a closure around the system environment.
|
||||
|
||||
@@ -44,6 +44,8 @@ class I4xSystem(object):
|
||||
user - The user to base the 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.
|
||||
'''
|
||||
self.ajax_url = ajax_url
|
||||
self.track_function = track_function
|
||||
@@ -53,6 +55,7 @@ class I4xSystem(object):
|
||||
self.exception404 = Http404
|
||||
self.DEBUG = settings.DEBUG
|
||||
self.seed = user.id if user is not None else 0
|
||||
self.replace_urls = replace_urls
|
||||
|
||||
def get(self, attr):
|
||||
''' provide uniform access to attributes (like etree).'''
|
||||
@@ -209,6 +212,9 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
(module, _, _, _) = get_module(user, request, location, student_module_cache, position)
|
||||
return module
|
||||
|
||||
# TODO (cpennington): When modules are shared between courses, the static
|
||||
# prefix is going to have to be specific to the module, not the directory
|
||||
# that the xml was loaded from
|
||||
system = I4xSystem(track_function=make_track_function(request),
|
||||
render_template=render_to_string,
|
||||
ajax_url=ajax_url,
|
||||
@@ -216,17 +222,18 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
filestore=descriptor.system.resources_fs,
|
||||
get_module=_get_module,
|
||||
user=user,
|
||||
# TODO (cpennington): This should be removed when all html from
|
||||
# a module is coming through get_html and is therefore covered
|
||||
# by the replace_static_urls code below
|
||||
replace_urls=replace_urls,
|
||||
)
|
||||
# pass position specified in URL to module through I4xSystem
|
||||
system.set('position', position)
|
||||
|
||||
module = descriptor.xmodule_constructor(system)(instance_state, shared_state)
|
||||
|
||||
# TODO (cpennington): When modules are shared between courses, the static
|
||||
# prefix is going to have to be specific to the module, not the directory
|
||||
# that the xml was loaded from
|
||||
prefix = module.metadata['data_dir']
|
||||
module = replace_static_urls(module, prefix)
|
||||
replace_prefix = module.metadata['data_dir']
|
||||
module = replace_static_urls(module, replace_prefix)
|
||||
|
||||
if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF') and user.is_staff:
|
||||
module = add_histogram(module)
|
||||
|
||||
3
rakefile
3
rakefile
@@ -27,7 +27,7 @@ NORMALIZED_DEPLOY_NAME = DEPLOY_NAME.downcase().gsub(/[_\/]/, '-')
|
||||
INSTALL_DIR_PATH = File.join(DEPLOY_DIR, NORMALIZED_DEPLOY_NAME)
|
||||
|
||||
# Set up the clean and clobber tasks
|
||||
CLOBBER.include(BUILD_DIR, REPORT_DIR, 'cover*', '.coverage')
|
||||
CLOBBER.include(BUILD_DIR, REPORT_DIR, 'cover*', '.coverage', 'test_root/*_repo')
|
||||
CLEAN.include("#{BUILD_DIR}/*.deb", "#{BUILD_DIR}/util")
|
||||
|
||||
def select_executable(*cmds)
|
||||
@@ -159,7 +159,6 @@ task :package do
|
||||
FileUtils.chmod(0755, postinstall.path)
|
||||
|
||||
args = ["fakeroot", "fpm", "-s", "dir", "-t", "deb",
|
||||
"--verbose",
|
||||
"--after-install=#{postinstall.path}",
|
||||
"--after-remove=#{afterremove.path}",
|
||||
"--prefix=#{INSTALL_DIR_PATH}",
|
||||
|
||||
@@ -5,20 +5,15 @@ scipy
|
||||
matplotlib
|
||||
markdown
|
||||
pygments
|
||||
django-mako
|
||||
django-ses
|
||||
lxml
|
||||
boto
|
||||
mako
|
||||
python-memcached
|
||||
django-celery
|
||||
path.py
|
||||
django_debug_toolbar
|
||||
-e git+git://github.com/MITx/django-pipeline.git#egg=django-pipeline
|
||||
django-staticfiles>=1.2.1
|
||||
django-masquerade
|
||||
fs
|
||||
django-jasmine
|
||||
beautifulsoup
|
||||
requests
|
||||
sympy
|
||||
@@ -36,10 +31,14 @@ django-override-settings
|
||||
mock>=0.8, <0.9
|
||||
PyYAML
|
||||
South
|
||||
django-celery
|
||||
django-countries
|
||||
django-kombu
|
||||
django-followit
|
||||
django-jasmine
|
||||
django-keyedcache
|
||||
django-mako
|
||||
django-masquerade
|
||||
django-robots
|
||||
django-ses
|
||||
django-storages
|
||||
|
||||
Reference in New Issue
Block a user