Commit ba1554b1 by Calen Pennington

Fixing github_sync to work with multiple course data directories

parent 016ac5d9
...@@ -5,12 +5,17 @@ import logging ...@@ -5,12 +5,17 @@ import logging
log = logging.getLogger(__name__) 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, Import the specified xml data_dir into the django defined modulestore,
using org and course as the location org and course. 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(): for module in module_store.modules.itervalues():
# TODO (cpennington): This forces import to overrite the same items. # TODO (cpennington): This forces import to overrite the same items.
...@@ -26,4 +31,4 @@ def import_from_xml(org, course, data_dir): ...@@ -26,4 +31,4 @@ def import_from_xml(org, course, data_dir):
modulestore().update_children(module.location, module.definition['children']) modulestore().update_children(module.location, module.definition['children'])
modulestore().update_metadata(module.location, dict(module.metadata)) modulestore().update_metadata(module.location, dict(module.metadata))
return module_store.course return module_store
...@@ -13,8 +13,12 @@ class Command(BaseCommand): ...@@ -13,8 +13,12 @@ class Command(BaseCommand):
'''Import the specified data directory into the default ModuleStore''' '''Import the specified data directory into the default ModuleStore'''
def handle(self, *args, **options): def handle(self, *args, **options):
if len(args) != 3: if len(args) == 0:
raise CommandError("import requires 3 arguments: <org> <course> <data directory>") raise CommandError("import requires at least one argument: <data directory> [<course dir>...]")
org, course, data_dir = args data_dir = args[0]
import_from_xml(org, course, data_dir) 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 ...@@ -6,7 +6,7 @@ from django_future.csrf import ensure_csrf_cookie
from fs.osfs import OSFS from fs.osfs import OSFS
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from xmodule.modulestore import Location 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 mitxmako.shortcuts import render_to_response
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -51,11 +51,12 @@ def save_item(request): ...@@ -51,11 +51,12 @@ def save_item(request):
modulestore().update_item(item_id, data) modulestore().update_item(item_id, data)
# Export the course back to github # 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) course_location = Location(item_id)._replace(category='course', name=None)
courses = modulestore().get_items(course_location) courses = modulestore().get_items(course_location)
for course in courses: for course in courses:
repo_path = repo_path_from_location(course.location) export_to_github(course, "CMS Edit")
export_to_github(course, repo_path, "CMS Edit")
return HttpResponse(json.dumps({})) return HttpResponse(json.dumps({}))
......
...@@ -12,6 +12,7 @@ from .exceptions import GithubSyncError ...@@ -12,6 +12,7 @@ from .exceptions import GithubSyncError
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
def import_from_github(repo_settings): def import_from_github(repo_settings):
""" """
Imports data into the modulestore based on the XML stored on github Imports data into the modulestore based on the XML stored on github
...@@ -19,10 +20,9 @@ def import_from_github(repo_settings): ...@@ -19,10 +20,9 @@ def import_from_github(repo_settings):
repo_settings is a dictionary with the following keys: repo_settings is a dictionary with the following keys:
path: file system path to the local git repo path: file system path to the local git repo
branch: name of the branch to track on github 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'] repo_path = repo_settings['path']
data_dir, course_dir = os.path.split(repo_path)
if not os.path.isdir(repo_path): if not os.path.isdir(repo_path):
Repo.clone_from(repo_settings['origin'], repo_path) Repo.clone_from(repo_settings['origin'], repo_path)
...@@ -34,18 +34,12 @@ def import_from_github(repo_settings): ...@@ -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 # 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'])
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(data_dir, course_dirs=[course_dir])
return git_repo.head.commit.hexsha, import_from_xml(repo_settings['org'], repo_settings['course'], repo_path) 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) fs = OSFS(repo_path)
xml = course.export_to_xml(fs) xml = course.export_to_xml(fs)
......
from django.test import TestCase from django.test import TestCase
from path import path from path import path
import shutil 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 git import Repo
from django.conf import settings from django.conf import settings
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -10,6 +10,7 @@ from override_settings import override_settings ...@@ -10,6 +10,7 @@ from override_settings import override_settings
from github_sync.exceptions import GithubSyncError from github_sync.exceptions import GithubSyncError
@override_settings(DATA_DIR=path('test_root'))
class GithubSyncTestCase(TestCase): class GithubSyncTestCase(TestCase):
def setUp(self): def setUp(self):
...@@ -29,8 +30,6 @@ class GithubSyncTestCase(TestCase): ...@@ -29,8 +30,6 @@ class GithubSyncTestCase(TestCase):
'path': self.repo_dir, 'path': self.repo_dir,
'origin': self.remote_dir, 'origin': self.remote_dir,
'branch': 'master', 'branch': 'master',
'org': 'org',
'course': 'course'
}) })
def tearDown(self): def tearDown(self):
...@@ -49,7 +48,7 @@ class GithubSyncTestCase(TestCase): ...@@ -49,7 +48,7 @@ class GithubSyncTestCase(TestCase):
""" """
self.assertEquals('Toy Course', self.import_course.metadata['display_name']) self.assertEquals('Toy Course', self.import_course.metadata['display_name'])
self.assertIn( 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()]) [child.location for child in self.import_course.get_children()])
self.assertEquals(1, len(self.import_course.get_children())) self.assertEquals(1, len(self.import_course.get_children()))
...@@ -58,7 +57,7 @@ class GithubSyncTestCase(TestCase): ...@@ -58,7 +57,7 @@ class GithubSyncTestCase(TestCase):
""" """
Test that with the GITHUB_PUSH feature disabled, no content is pushed to the remote 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()) self.assertEquals(1, Repo(self.remote_dir).head.commit.count())
@override_settings(MITX_FEATURES={'GITHUB_PUSH': True}) @override_settings(MITX_FEATURES={'GITHUB_PUSH': True})
...@@ -67,7 +66,7 @@ class GithubSyncTestCase(TestCase): ...@@ -67,7 +66,7 @@ class GithubSyncTestCase(TestCase):
Test that with GITHUB_PUSH enabled, content is pushed to the remote Test that with GITHUB_PUSH enabled, content is pushed to the remote
""" """
self.import_course.metadata['display_name'] = 'Changed display name' 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()) self.assertEquals(2, Repo(self.remote_dir).head.commit.count())
@override_settings(MITX_FEATURES={'GITHUB_PUSH': True}) @override_settings(MITX_FEATURES={'GITHUB_PUSH': True})
...@@ -80,17 +79,6 @@ class GithubSyncTestCase(TestCase): ...@@ -80,17 +79,6 @@ class GithubSyncTestCase(TestCase):
remote = Repo(self.remote_dir) remote = Repo(self.remote_dir)
remote.git.commit(allow_empty=True, m="Testing conflict commit") 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(2, remote.head.reference.commit.count())
self.assertEquals("Testing conflict commit\n", remote.head.reference.commit.message) 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'))
...@@ -15,7 +15,6 @@ from xmodule.raw_module import RawDescriptor ...@@ -15,7 +15,6 @@ from xmodule.raw_module import RawDescriptor
from progress import Progress from progress import Progress
from capa.capa_problem import LoncapaProblem from capa.capa_problem import LoncapaProblem
from capa.responsetypes import StudentInputError from capa.responsetypes import StudentInputError
from static_replace import replace_urls
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
...@@ -275,7 +274,7 @@ class CapaModule(XModule): ...@@ -275,7 +274,7 @@ class CapaModule(XModule):
html = '<div id="problem_{id}" class="problem" data-url="{ajax_url}">'.format( 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>" 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): def handle_ajax(self, dispatch, get):
''' '''
......
...@@ -20,19 +20,21 @@ class XMLModuleStore(ModuleStore): ...@@ -20,19 +20,21 @@ class XMLModuleStore(ModuleStore):
""" """
An XML backed 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 Initialize an XMLModuleStore from data_dir
data_dir: path to data directory containing the course directories 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 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 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.eager = eager
self.data_dir = path(data_dir) self.data_dir = path(data_dir)
self.modules = {} self.modules = {}
self.courses = [] self.courses = {}
if default_class is None: if default_class is None:
self.default_class = None self.default_class = None
...@@ -46,10 +48,14 @@ class XMLModuleStore(ModuleStore): ...@@ -46,10 +48,14 @@ class XMLModuleStore(ModuleStore):
log.debug('default_class = %s' % self.default_class) log.debug('default_class = %s' % self.default_class)
for course_dir in os.listdir(self.data_dir): 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 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): def load_course(self, course_dir):
""" """
...@@ -148,7 +154,7 @@ class XMLModuleStore(ModuleStore): ...@@ -148,7 +154,7 @@ class XMLModuleStore(ModuleStore):
""" """
Returns a list of course descriptors Returns a list of course descriptors
""" """
return self.courses return self.courses.values()
def create_item(self, location): def create_item(self, location):
raise NotImplementedError("XMLModuleStores are read-only") raise NotImplementedError("XMLModuleStores are read-only")
......
...@@ -27,8 +27,8 @@ class I4xSystem(object): ...@@ -27,8 +27,8 @@ class I4xSystem(object):
and user, or other environment-specific info. and user, or other environment-specific info.
''' '''
def __init__(self, ajax_url, track_function, def __init__(self, ajax_url, track_function,
get_module, render_template, user=None, get_module, render_template, replace_urls,
filestore=None): user=None, filestore=None):
''' '''
Create a closure around the system environment. Create a closure around the system environment.
...@@ -44,6 +44,8 @@ class I4xSystem(object): ...@@ -44,6 +44,8 @@ class I4xSystem(object):
user - The user to base the seed off of for this request user - The user to base the seed off of for this request
filestore - A filestore ojbect. Defaults to an instance of OSFS based at filestore - A filestore ojbect. Defaults to an instance of OSFS based at
settings.DATA_DIR. 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.ajax_url = ajax_url
self.track_function = track_function self.track_function = track_function
...@@ -53,6 +55,7 @@ class I4xSystem(object): ...@@ -53,6 +55,7 @@ class I4xSystem(object):
self.exception404 = Http404 self.exception404 = Http404
self.DEBUG = settings.DEBUG self.DEBUG = settings.DEBUG
self.seed = user.id if user is not None else 0 self.seed = user.id if user is not None else 0
self.replace_urls = replace_urls
def get(self, attr): def get(self, attr):
''' provide uniform access to attributes (like etree).''' ''' provide uniform access to attributes (like etree).'''
...@@ -209,6 +212,9 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -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) (module, _, _, _) = get_module(user, request, location, student_module_cache, position)
return module 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), system = I4xSystem(track_function=make_track_function(request),
render_template=render_to_string, render_template=render_to_string,
ajax_url=ajax_url, ajax_url=ajax_url,
...@@ -216,17 +222,18 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -216,17 +222,18 @@ def get_module(user, request, location, student_module_cache, position=None):
filestore=descriptor.system.resources_fs, filestore=descriptor.system.resources_fs,
get_module=_get_module, get_module=_get_module,
user=user, 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 # pass position specified in URL to module through I4xSystem
system.set('position', position) system.set('position', position)
module = descriptor.xmodule_constructor(system)(instance_state, shared_state) module = descriptor.xmodule_constructor(system)(instance_state, shared_state)
# TODO (cpennington): When modules are shared between courses, the static replace_prefix = module.metadata['data_dir']
# prefix is going to have to be specific to the module, not the directory module = replace_static_urls(module, replace_prefix)
# that the xml was loaded from
prefix = module.metadata['data_dir']
module = replace_static_urls(module, prefix)
if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF') and user.is_staff: if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF') and user.is_staff:
module = add_histogram(module) module = add_histogram(module)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment