Commit 1789814c by Chris Dodge

wip

parent 63085ec6
...@@ -1580,7 +1580,8 @@ def import_course(request, org, course, name): ...@@ -1580,7 +1580,8 @@ def import_course(request, org, course, name):
shutil.move(r / fname, course_dir) shutil.move(r / fname, course_dir)
module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT, module_store, course_items = import_from_xml(modulestore('direct'), settings.GITHUB_REPO_ROOT,
[course_subdir], load_error_modules=False, static_content_store=contentstore(), target_location_namespace=Location(location)) [course_subdir], load_error_modules=False, static_content_store=contentstore(),
target_location_namespace=Location(location), draft_store=modulestore())
# we can blow this away when we're done importing. # we can blow this away when we're done importing.
shutil.rmtree(course_dir) shutil.rmtree(course_dir)
......
...@@ -118,8 +118,8 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): ...@@ -118,8 +118,8 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
with system.resources_fs.open(filepath) as file: with system.resources_fs.open(filepath) as file:
html = file.read().decode('utf-8') html = file.read().decode('utf-8')
# Log a warning if we can't parse the file, but don't error # Log a warning if we can't parse the file, but don't error
if not check_html(html): if not check_html(html) and len(html) > 0:
msg = "Couldn't parse html in {0}.".format(filepath) msg = "Couldn't parse html in {0}, content = {1}".format(filepath, html)
log.warning(msg) log.warning(msg)
system.error_tracker("Warning: " + msg) system.error_tracker("Warning: " + msg)
...@@ -156,7 +156,8 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor): ...@@ -156,7 +156,8 @@ class HtmlDescriptor(HtmlFields, XmlDescriptor, EditingDescriptor):
resource_fs.makedir(os.path.dirname(filepath), recursive=True, allow_recreate=True) resource_fs.makedir(os.path.dirname(filepath), recursive=True, allow_recreate=True)
with resource_fs.open(filepath, 'w') as file: with resource_fs.open(filepath, 'w') as file:
file.write(self.data.encode('utf-8')) html_data = self.data.encode('utf-8')
file.write(html_data)
# write out the relative name # write out the relative name
relname = path(pathname).basename() relname = path(pathname).basename()
......
...@@ -106,7 +106,7 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -106,7 +106,7 @@ class DraftModuleStore(ModuleStoreBase):
""" """
return wrap_draft(super(DraftModuleStore, self).clone_item(source, as_draft(location))) return wrap_draft(super(DraftModuleStore, self).clone_item(source, as_draft(location)))
def update_item(self, location, data): def update_item(self, location, data, allow_not_found=False):
""" """
Set the data in the item specified by the location to Set the data in the item specified by the location to
data data
...@@ -115,9 +115,13 @@ class DraftModuleStore(ModuleStoreBase): ...@@ -115,9 +115,13 @@ class DraftModuleStore(ModuleStoreBase):
data: A nested dictionary of problem data data: A nested dictionary of problem data
""" """
draft_loc = as_draft(location) draft_loc = as_draft(location)
draft_item = self.get_item(location) try:
if not getattr(draft_item, 'is_draft', False): draft_item = self.get_item(location)
self.clone_item(location, draft_loc) if not getattr(draft_item, 'is_draft', False):
self.clone_item(location, draft_loc)
except ItemNotFoundError, e:
if not allow_not_found:
raise e
return super(DraftModuleStore, self).update_item(draft_loc, data) return super(DraftModuleStore, self).update_item(draft_loc, data)
......
...@@ -8,55 +8,59 @@ from json import dumps ...@@ -8,55 +8,59 @@ from json import dumps
def export_to_xml(modulestore, contentstore, course_location, root_dir, course_dir, draft_modulestore = None): def export_to_xml(modulestore, contentstore, course_location, root_dir, course_dir, draft_modulestore = None):
course = modulestore.get_item(course_location) course = modulestore.get_item(course_location)
fs = OSFS(root_dir) fs = OSFS(root_dir)
export_fs = fs.makeopendir(course_dir) export_fs = fs.makeopendir(course_dir)
xml = course.export_to_xml(export_fs) xml = course.export_to_xml(export_fs)
with export_fs.open('course.xml', 'w') as course_xml: with export_fs.open('course.xml', 'w') as course_xml:
course_xml.write(xml) course_xml.write(xml)
# export the static assets # export the static assets
contentstore.export_all_for_course(course_location, root_dir + '/' + course_dir + '/static/') contentstore.export_all_for_course(course_location, root_dir + '/' + course_dir + '/static/')
# export the static tabs # export the static tabs
export_extra_content(export_fs, modulestore, course_location, 'static_tab', 'tabs', '.html') export_extra_content(export_fs, modulestore, course_location, 'static_tab', 'tabs', '.html')
# export the custom tags # export the custom tags
export_extra_content(export_fs, modulestore, course_location, 'custom_tag_template', 'custom_tags') export_extra_content(export_fs, modulestore, course_location, 'custom_tag_template', 'custom_tags')
# export the course updates # export the course updates
export_extra_content(export_fs, modulestore, course_location, 'course_info', 'info', '.html') export_extra_content(export_fs, modulestore, course_location, 'course_info', 'info', '.html')
# export the grading policy # export the grading policy
policies_dir = export_fs.makeopendir('policies') policies_dir = export_fs.makeopendir('policies')
course_run_policy_dir = policies_dir.makeopendir(course.location.name) course_run_policy_dir = policies_dir.makeopendir(course.location.name)
with course_run_policy_dir.open('grading_policy.json', 'w') as grading_policy: with course_run_policy_dir.open('grading_policy.json', 'w') as grading_policy:
grading_policy.write(dumps(course.grading_policy)) grading_policy.write(dumps(course.grading_policy))
# export all of the course metadata in policy.json # export all of the course metadata in policy.json
with course_run_policy_dir.open('policy.json', 'w') as course_policy: with course_run_policy_dir.open('policy.json', 'w') as course_policy:
policy = {'course/' + course.location.name: own_metadata(course)} policy = {'course/' + course.location.name: own_metadata(course)}
course_policy.write(dumps(policy)) course_policy.write(dumps(policy))
# export everything from the draft store, unfortunately this will create lots of duplicates # export everything from the draft store, unfortunately this will create lots of duplicates
'''
if draft_modulestore is not None: if draft_modulestore is not None:
draft_course = draft_modulestore.get_item(course_location) draft_course = draft_modulestore.get_item(course_location)
draft_course_dir = export_fs.makeopendir('drafts')
xml = draft_course.export_to_xml(draft_course_dir) xml = draft_course.export_to_xml(draft_course_dir)
with draft_course_dir.open('course.xml', 'w') as course_xml: with draft_course_dir.open('course.xml', 'w') as course_xml:
course_xml.write(xml) course_xml.write(xml)
''' '''
draft_items = modulestore.get_items([None, None, None, 'vertical', None, 'draft'])
logging.debug('draft_items = {0}'.format(draft_items)) # export draft content
if len(draft_items) > 0: # NOTE: this code assumes that verticals are the top most draftable container
# should we change the application, then this assumption will no longer
# be valid
draft_items = draft_modulestore.get_items([None, course_location.org, course_location.course,
'vertical', None, 'draft'])
if len(draft_items)>0:
draft_course_dir = export_fs.makeopendir('drafts')
for draft_item in draft_items: for draft_item in draft_items:
draft_item.export_to_xml(draft_items_dir) draft_item.export_to_xml(draft_course_dir)
#with draft_items_dir.open(draft_item.location.name + '.xml', 'w'):
'''
def export_extra_content(export_fs, modulestore, course_location, category_type, dirname, file_suffix=''): def export_extra_content(export_fs, modulestore, course_location, category_type, dirname, file_suffix=''):
......
...@@ -6,11 +6,13 @@ from path import path ...@@ -6,11 +6,13 @@ from path import path
from xblock.core import Scope from xblock.core import Scope
from .xml import XMLModuleStore from .xml import XMLModuleStore, ImportSystem, ParentTracker
from .exceptions import DuplicateItemError from .exceptions import DuplicateItemError
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.contentstore.content import StaticContent, XASSET_SRCREF_PREFIX from xmodule.contentstore.content import StaticContent, XASSET_SRCREF_PREFIX
from .inheritance import own_metadata from .inheritance import own_metadata
from xmodule.errortracker import make_error_tracker
from collections import defaultdict
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -175,7 +177,8 @@ def import_course_from_xml(modulestore, static_content_store, course_data_path, ...@@ -175,7 +177,8 @@ def import_course_from_xml(modulestore, static_content_store, course_data_path,
def import_from_xml(store, data_dir, course_dirs=None, def import_from_xml(store, data_dir, course_dirs=None,
default_class='xmodule.raw_module.RawDescriptor', default_class='xmodule.raw_module.RawDescriptor',
load_error_modules=True, static_content_store=None, target_location_namespace=None, verbose=False): load_error_modules=True, static_content_store=None, target_location_namespace=None,
verbose=False, draft_store=None):
""" """
Import the specified xml data_dir into the "store" modulestore, Import the specified xml data_dir into the "store" modulestore,
using org and course as the location org and course. using org and course as the location org and course.
...@@ -190,7 +193,7 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -190,7 +193,7 @@ def import_from_xml(store, data_dir, course_dirs=None,
""" """
module_store = XMLModuleStore( xml_module_store = XMLModuleStore(
data_dir, data_dir,
default_class=default_class, default_class=default_class,
course_dirs=course_dirs, course_dirs=course_dirs,
...@@ -201,7 +204,7 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -201,7 +204,7 @@ def import_from_xml(store, data_dir, course_dirs=None,
# to enumerate the entire collection of course modules. It will be left as a TBD to implement that # to enumerate the entire collection of course modules. It will be left as a TBD to implement that
# method on XmlModuleStore. # method on XmlModuleStore.
course_items = [] course_items = []
for course_id in module_store.modules.keys(): for course_id in xml_module_store.modules.keys():
if target_location_namespace is not None: if target_location_namespace is not None:
pseudo_course_id = '/'.join([target_location_namespace.org, target_location_namespace.course]) pseudo_course_id = '/'.join([target_location_namespace.org, target_location_namespace.course])
...@@ -222,7 +225,7 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -222,7 +225,7 @@ def import_from_xml(store, data_dir, course_dirs=None,
# Quick scan to get course module as we need some info from there. Also we need to make sure that the # Quick scan to get course module as we need some info from there. Also we need to make sure that the
# course module is committed first into the store # course module is committed first into the store
for module in module_store.modules[course_id].itervalues(): for module in xml_module_store.modules[course_id].itervalues():
if module.category == 'course': if module.category == 'course':
course_data_path = path(data_dir) / module.data_dir course_data_path = path(data_dir) / module.data_dir
course_location = module.location course_location = module.location
...@@ -239,11 +242,7 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -239,11 +242,7 @@ def import_from_xml(store, data_dir, course_dirs=None,
{"type": "discussion", "name": "Discussion"}, {"type": "discussion", "name": "Discussion"},
{"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge {"type": "wiki", "name": "Wiki"}] # note, add 'progress' when we can support it on Edge
import_module(module, store, course_data_path, static_content_store)
if hasattr(module, 'data'):
store.update_item(module.location, module.data)
store.update_children(module.location, module.children)
store.update_metadata(module.location, dict(own_metadata(module)))
# a bit of a hack, but typically the "course image" which is shown on marketing pages is hard coded to /images/course_image.jpg # a bit of a hack, but typically the "course image" which is shown on marketing pages is hard coded to /images/course_image.jpg
# so let's make sure we import in case there are no other references to it in the modules # so let's make sure we import in case there are no other references to it in the modules
...@@ -257,11 +256,11 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -257,11 +256,11 @@ def import_from_xml(store, data_dir, course_dirs=None,
_namespace_rename = target_location_namespace if target_location_namespace is not None else course_location _namespace_rename = target_location_namespace if target_location_namespace is not None else course_location
# first pass to find everything in /static/ # first pass to find everything in /static/
import_static_content(module_store.modules[course_id], course_location, course_data_path, static_content_store, import_static_content(xml_module_store.modules[course_id], course_location, course_data_path, static_content_store,
_namespace_rename, subpath='static', verbose=verbose) _namespace_rename, subpath='static', verbose=verbose)
# finally loop through all the modules # finally loop through all the modules
for module in module_store.modules[course_id].itervalues(): for module in xml_module_store.modules[course_id].itervalues():
if module.category == 'course': if module.category == 'course':
# we've already saved the course module up at the top of the loop # we've already saved the course module up at the top of the loop
...@@ -275,49 +274,8 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -275,49 +274,8 @@ def import_from_xml(store, data_dir, course_dirs=None,
if verbose: if verbose:
log.debug('importing module location {0}'.format(module.location)) log.debug('importing module location {0}'.format(module.location))
content = {} import_module(module, store, course_data_path, static_content_store)
for field in module.fields:
if field.scope != Scope.content:
continue
try:
content[field.name] = module._model_data[field.name]
except KeyError:
# Ignore any missing keys in _model_data
pass
if 'data' in content:
module_data = content['data']
# cdodge: now go through any link references to '/static/' and make sure we've imported
# it as a StaticContent asset
try:
remap_dict = {}
# use the rewrite_links as a utility means to enumerate through all links
# in the module data. We use that to load that reference into our asset store
# IMPORTANT: There appears to be a bug in lxml.rewrite_link which makes us not be able to
# do the rewrites natively in that code.
# For example, what I'm seeing is <img src='foo.jpg' /> -> <img src='bar.jpg'>
# Note the dropped element closing tag. This causes the LMS to fail when rendering modules - that's
# no good, so we have to do this kludge
if isinstance(module_data, str) or isinstance(module_data, unicode): # some module 'data' fields are non strings which blows up the link traversal code
lxml_rewrite_links(module_data, lambda link: verify_content_links(module, course_data_path,
static_content_store, link, remap_dict))
for key in remap_dict.keys():
module_data = module_data.replace(key, remap_dict[key])
except Exception, e:
logging.exception("failed to rewrite links on {0}. Continuing...".format(module.location))
store.update_item(module.location, content)
if hasattr(module, 'children') and module.children != []:
store.update_children(module.location, module.children)
# NOTE: It's important to use own_metadata here to avoid writing
# inherited metadata everywhere.
store.update_metadata(module.location, dict(own_metadata(module)))
finally: finally:
# turn back on all write signalling # turn back on all write signalling
if pseudo_course_id in store.ignore_write_events_on_courses: if pseudo_course_id in store.ignore_write_events_on_courses:
...@@ -326,13 +284,110 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -326,13 +284,110 @@ def import_from_xml(store, data_dir, course_dirs=None,
target_location_namespace is not None else course_location) target_location_namespace is not None else course_location)
# now import any 'draft' items # now import any 'draft' items
import_course_draft(store, course_data_path, target_location_namespace) if draft_store is not None:
import_course_draft(xml_module_store, draft_store, course_data_path, static_content_store, target_location_namespace)
return module_store, course_items return xml_module_store, course_items
def import_module(module, store, course_data_path, static_content_store, allow_not_found=False):
content = {}
for field in module.fields:
if field.scope != Scope.content:
continue
try:
content[field.name] = module._model_data[field.name]
except KeyError:
# Ignore any missing keys in _model_data
pass
def import_course_draft(store, course_data_path, target_location_namespace): if 'data' in content:
pass module_data = content['data']
# cdodge: now go through any link references to '/static/' and make sure we've imported
# it as a StaticContent asset
try:
remap_dict = {}
# use the rewrite_links as a utility means to enumerate through all links
# in the module data. We use that to load that reference into our asset store
# IMPORTANT: There appears to be a bug in lxml.rewrite_link which makes us not be able to
# do the rewrites natively in that code.
# For example, what I'm seeing is <img src='foo.jpg' /> -> <img src='bar.jpg'>
# Note the dropped element closing tag. This causes the LMS to fail when rendering modules - that's
# no good, so we have to do this kludge
if isinstance(module_data, str) or isinstance(module_data, unicode): # some module 'data' fields are non strings which blows up the link traversal code
lxml_rewrite_links(module_data, lambda link: verify_content_links(module, course_data_path,
static_content_store, link, remap_dict))
for key in remap_dict.keys():
module_data = module_data.replace(key, remap_dict[key])
except Exception, e:
logging.exception("failed to rewrite links on {0}. Continuing...".format(module.location))
if allow_not_found:
store.update_item(module.location, content, allow_not_found=allow_not_found)
else:
store.update_item(module.location, content)
if hasattr(module, 'children') and module.children != []:
store.update_children(module.location, module.children)
# NOTE: It's important to use own_metadata here to avoid writing
# inherited metadata everywhere.
store.update_metadata(module.location, dict(own_metadata(module)))
def import_course_draft(xml_module_store, store, course_data_path, static_content_store, target_location_namespace):
'''
This will import all the content inside of the 'drafts' folder, if it exists
NOTE: This is not a full course import, basically in our current application only verticals (and downwards)
can be in draft. Therefore, we need to use slightly different call points into the import process_xml
as we can't simply call XMLModuleStore() constructor (like we do for importing public content)
'''
draft_dir = course_data_path + "/drafts"
if not os.path.exists(draft_dir):
return
# create a new 'System' object which will manage the importing
errorlog = make_error_tracker()
system = ImportSystem(
xml_module_store,
target_location_namespace.course_id,
draft_dir,
{},
errorlog.tracker,
ParentTracker(),
None,
)
# now walk the /vertical directory where each file in there will be a draft copy of the Vertical
for dirname, dirnames, filenames in os.walk(draft_dir + "/vertical"):
for filename in filenames:
module_path = os.path.join(dirname, filename)
with open(module_path) as f:
try:
xml = f.read().decode('utf-8')
descriptor = system.process_xml(xml)
def _import_module(module):
module.location = module.location._replace(revision='draft')
import_module(module, store, course_data_path, static_content_store, allow_not_found=True)
for child in module.get_children():
_import_module(child)
# HACK: since we are doing partial imports of drafts
# the vertical doesn't have the 'url-name' set in the attributes (they are normally in the parent
# object, aka sequential), so we have to replace the location.name with the XML filename
# that is part of the pack
fn, fileExtension = os.path.splitext(filename)
descriptor.location = descriptor.location._replace(name=fn)
_import_module(descriptor)
except Exception, e:
logging.exception('There was an error. {0}'.format(unicode(e)))
pass
def remap_namespace(module, target_location_namespace): def remap_namespace(module, target_location_namespace):
......
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