Commit ba157352 by Chris Dodge

implement static tabs. rejigger .tar.gz import to do the re-namespacing as a…

implement static tabs. rejigger .tar.gz import to do the re-namespacing as a post-processing step as we need to retain the original url_name during the import. Also, migrate the presentation tier to use module_render.py::get_module() to better unify HTML'ifying stuff
parent 606e2dba
...@@ -1029,18 +1029,8 @@ def import_course(request, org, course, name): ...@@ -1029,18 +1029,8 @@ def import_course(request, org, course, name):
for fname in os.listdir(r): for fname in os.listdir(r):
shutil.move(r/fname, course_dir) shutil.move(r/fname, course_dir)
with open(course_dir / 'course.xml', 'r') as course_file:
course_data = etree.parse(course_file, parser=edx_xml_parser)
course_data_root = course_data.getroot()
course_data_root.set('org', org)
course_data_root.set('course', course)
course_data_root.set('url_name', name)
with open(course_dir / 'course.xml', 'w') as course_file:
course_data.write(course_file)
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_dir], load_error_modules=False, static_content_store=contentstore()) [course_dir], load_error_modules=False, static_content_store=contentstore(), target_location_namespace = Location(location))
# 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)
......
...@@ -35,6 +35,8 @@ setup( ...@@ -35,6 +35,8 @@ setup(
"videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor", "videodev = xmodule.backcompat_module:TranslateCustomTagDescriptor",
"videosequence = xmodule.seq_module:SequenceDescriptor", "videosequence = xmodule.seq_module:SequenceDescriptor",
"discussion = xmodule.discussion_module:DiscussionDescriptor", "discussion = xmodule.discussion_module:DiscussionDescriptor",
"course_info = xmodule.html_module:HtmlDescriptor",
"static_tab = xmodule.html_module:HtmlDescriptor",
] ]
} }
) )
...@@ -4,6 +4,7 @@ import logging ...@@ -4,6 +4,7 @@ import logging
import os import os
import re import re
import sys import sys
import glob
from collections import defaultdict from collections import defaultdict
from cStringIO import StringIO from cStringIO import StringIO
...@@ -333,7 +334,6 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -333,7 +334,6 @@ class XMLModuleStore(ModuleStoreBase):
if not os.path.exists(policy_path): if not os.path.exists(policy_path):
return {} return {}
try: try:
log.debug("Loading policy from {0}".format(policy_path))
with open(policy_path) as f: with open(policy_path) as f:
return json.load(f) return json.load(f)
except (IOError, ValueError) as err: except (IOError, ValueError) as err:
...@@ -388,6 +388,7 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -388,6 +388,7 @@ class XMLModuleStore(ModuleStoreBase):
if url_name: if url_name:
policy_dir = self.data_dir / course_dir / 'policies' / url_name policy_dir = self.data_dir / course_dir / 'policies' / url_name
policy_path = policy_dir / 'policy.json' policy_path = policy_dir / 'policy.json'
policy = self.load_policy(policy_path, tracker) policy = self.load_policy(policy_path, tracker)
# VS[compat]: remove once courses use the policy dirs. # VS[compat]: remove once courses use the policy dirs.
...@@ -405,7 +406,6 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -405,7 +406,6 @@ class XMLModuleStore(ModuleStoreBase):
raise ValueError("Can't load a course without a 'url_name' " raise ValueError("Can't load a course without a 'url_name' "
"(or 'name') set. Set url_name.") "(or 'name') set. Set url_name.")
course_id = CourseDescriptor.make_id(org, course, url_name) course_id = CourseDescriptor.make_id(org, course, url_name)
system = ImportSystem( system = ImportSystem(
self, self,
...@@ -438,11 +438,27 @@ class XMLModuleStore(ModuleStoreBase): ...@@ -438,11 +438,27 @@ class XMLModuleStore(ModuleStoreBase):
filepath = info_path / info_filename + '.html' filepath = info_path / info_filename + '.html'
if os.path.exists(filepath): if os.path.exists(filepath):
with open(filepath) as info_file: with open(filepath) as info_file:
html = info_file.read() html = info_file.read().decode('utf-8')
loc = Location('i4x', course_descriptor.location.org, course_descriptor.location.course, 'course_info', info_filename) loc = Location('i4x', course_descriptor.location.org, course_descriptor.location.course, 'course_info', info_filename)
html_module = HtmlDescriptor(system, definition={'data' : html}, **{'location' : loc}) info_module = HtmlDescriptor(system, definition={'data' : html}, **{'location' : loc})
self.modules[course_id][html_module.location] = html_module self.modules[course_id][info_module.location] = info_module
# now import all static tabs which are expected to be stored in
# in <content_dir>/tabs or <content_dir>/tabs/<url_name>
if url_name:
tabs_path = self.data_dir / course_dir / 'tabs' / url_name
if not os.path.exists(tabs_path):
tabs_path = self.data_dir / course_dir / 'tabs'
for tab_filepath in glob.glob(tabs_path / '*.html'):
with open(tab_filepath) as tab_file:
html = tab_file.read().decode('utf-8')
# tabs are referenced in policy.json through a 'slug' which is just the filename without the .html suffix
slug = os.path.splitext(os.path.basename(tab_filepath))[0]
loc = Location('i4x', course_descriptor.location.org, course_descriptor.location.course, 'static_tab', slug)
tab_module = HtmlDescriptor(system, definition={'data' : html}, **{'location' : loc})
self.modules[course_id][tab_module.location] = tab_module
log.debug('========> Done with course import from {0}'.format(course_dir)) log.debug('========> Done with course import from {0}'.format(course_dir))
return course_descriptor return course_descriptor
......
...@@ -67,7 +67,7 @@ def import_static_content(modules, data_dir, static_content_store): ...@@ -67,7 +67,7 @@ def import_static_content(modules, data_dir, static_content_store):
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): load_error_modules=True, static_content_store=None, target_location_namespace = 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.
...@@ -75,6 +75,11 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -75,6 +75,11 @@ def import_from_xml(store, data_dir, course_dirs=None,
course_dirs: If specified, the list of course_dirs to load. Otherwise, load course_dirs: If specified, the list of course_dirs to load. Otherwise, load
all course dirs all course dirs
target_location_namespace is the namespace [passed as Location] (i.e. {tag},{org},{course}) that all modules in the should be remapped to
after import off disk. We do this remapping as a post-processing step because there's logic in the importing which
expects a 'url_name' as an identifier to where things are on disk e.g. ../policies/<url_name>/policy.json as well as metadata keys in
the policy.json. so we need to keep the original url_name during import
""" """
module_store = XMLModuleStore( module_store = XMLModuleStore(
...@@ -95,6 +100,27 @@ def import_from_xml(store, data_dir, course_dirs=None, ...@@ -95,6 +100,27 @@ def import_from_xml(store, data_dir, course_dirs=None,
for module in module_store.modules[course_id].itervalues(): for module in module_store.modules[course_id].itervalues():
# remap module to the new namespace
if target_location_namespace is not None:
# This looks a bit wonky as we need to also change the 'name' of the imported course to be what
# the caller passed in
module.location = Location(target_location_namespace.tag,
target_location_namespace.org, target_location_namespace.course, module.location.category,
module.location.name if module.location.category != 'course' else target_location_namespace.name)
# then remap children pointers since they too will be re-namespaced
children_locs = module.definition.get('children')
if children_locs is not None:
new_locs = []
for child in children_locs:
child_loc = Location(child)
new_locs.append(Location(target_location_namespace.tag, target_location_namespace.org,
target_location_namespace.course, child_loc.category, child_loc.name).url())
module.definition['children'] = new_locs
if module.category == 'course': if module.category == 'course':
# HACK: for now we don't support progress tabs. There's a special metadata configuration setting for this. # HACK: for now we don't support progress tabs. There's a special metadata configuration setting for this.
module.metadata['hide_progress_tab'] = True module.metadata['hide_progress_tab'] = True
......
...@@ -10,6 +10,7 @@ from django.conf import settings ...@@ -10,6 +10,7 @@ from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.http import Http404 from django.http import Http404
from module_render import get_module
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -20,6 +21,8 @@ from static_replace import replace_urls, try_staticfiles_lookup ...@@ -20,6 +21,8 @@ from static_replace import replace_urls, try_staticfiles_lookup
from courseware.access import has_access from courseware.access import has_access
import branding import branding
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -140,27 +143,9 @@ def get_course_about_section(course, section_key): ...@@ -140,27 +143,9 @@ def get_course_about_section(course, section_key):
raise KeyError("Invalid about key " + str(section_key)) raise KeyError("Invalid about key " + str(section_key))
def get_course_info_section_from_db(course, section_key):
loc = Location(course.location.tag, course.location.org, course.location.course, 'course_info', section_key)
html = ''
try:
item = modulestore().get_item(loc)
# return the raw HTML here which is stored as part of the definition. If we call get_html here, HTMLModule's parent
# descriptors will try to return an 'editing' rendering of the HTML
_html = item.definition['data']
try:
# apply link transforms which are defined in XModule, maybe that should actually be a static method in
# Content.py
html = rewrite_links(_html, XModule.rewrite_content_links)
except:
logging.error('error rewriting links on the following HTML content: {0}'.format(_html))
except Exception, e:
logging.exception("Could not find course_info section {0} at {1}: {2}".format(section_key, loc, str(e)))
return html
def get_course_info_section(course, section_key): def get_course_info_section(request, cache, course, section_key):
""" """
This returns the snippet of html to be rendered on the course info page, This returns the snippet of html to be rendered on the course info page,
given the key for the section. given the key for the section.
...@@ -172,34 +157,18 @@ def get_course_info_section(course, section_key): ...@@ -172,34 +157,18 @@ def get_course_info_section(course, section_key):
- guest_updates - guest_updates
""" """
# Many of these are stored as html files instead of some semantic loc = Location(course.location.tag, course.location.org, course.location.course, 'course_info', section_key)
# markup. This can change without effecting this interface when we find a course_module = get_module(request.user, request, loc, cache, course.id)
# good format for defining so many snippets of text/html.
if not isinstance(modulestore(), XMLModuleStore): logging.debug('course_module = {0}'.format(course_module))
return get_course_info_section_from_db(course, section_key)
if section_key in ['handouts', 'guest_handouts', 'updates', 'guest_updates']: html = ''
try:
fs = course.system.resources_fs
# first look for a run-specific version
dirs = [path("info") / course.url_name, path("info")]
filepath = find_file(fs, dirs, section_key + ".html")
with fs.open(filepath) as htmlFile: if course_module is not None:
# Replace '/static/' urls html = course_module.get_html()
info_html = replace_urls(htmlFile.read().decode('utf-8'), course.metadata['data_dir'])
# Replace '/course/' urls return html
course_root = reverse('course_root', args=[course.id])[:-1] # Remove trailing slash
info_html = replace_urls(info_html, course_root, '/course/')
return info_html
except ResourceNotFoundError:
log.exception("Missing info section {key} in course {url}".format(
key=section_key, url=course.location.url()))
return "! Info section missing !"
raise KeyError("Invalid about key " + str(section_key))
# TODO: Fix this such that these are pulled in as extra course-specific tabs. # TODO: Fix this such that these are pulled in as extra course-specific tabs.
......
...@@ -256,7 +256,7 @@ def _get_module(user, request, location, student_module_cache, course_id, positi ...@@ -256,7 +256,7 @@ def _get_module(user, request, location, student_module_cache, course_id, positi
module.get_html = replace_static_urls( module.get_html = replace_static_urls(
wrap_xmodule(module.get_html, module, 'xmodule_display.html'), wrap_xmodule(module.get_html, module, 'xmodule_display.html'),
module.metadata['data_dir']) module.metadata['data_dir'] if 'data_dir' in module.metadata else '')
# Allow URLs of the form '/course/' refer to the root of multicourse directory # Allow URLs of the form '/course/' refer to the root of multicourse directory
# hierarchy of this course # hierarchy of this course
......
...@@ -17,8 +17,17 @@ from django.core.urlresolvers import reverse ...@@ -17,8 +17,17 @@ from django.core.urlresolvers import reverse
from fs.errors import ResourceNotFoundError from fs.errors import ResourceNotFoundError
from lxml.html import rewrite_links
from module_render import get_module
from courseware.access import has_access from courseware.access import has_access
from static_replace import replace_urls from static_replace import replace_urls
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml import XMLModuleStore
from xmodule.x_module import XModule
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -248,27 +257,16 @@ def get_static_tab_by_slug(course, tab_slug): ...@@ -248,27 +257,16 @@ def get_static_tab_by_slug(course, tab_slug):
return None return None
def get_static_tab_contents(request, cache, course, tab):
def get_static_tab_contents(course, tab): loc = Location(course.location.tag, course.location.org, course.location.course, 'static_tab', tab['url_slug'])
""" course_module = get_module(request.user, request, loc, cache, course.id)
Given a course and a static tab config dict, load the tab contents,
returning None if not found.
Looks in tabs/{course_url_name}/{tab_slug}.html first, then tabs/{tab_slug}.html. logging.debug('course_module = {0}'.format(course_module))
"""
slug = tab['url_slug'] html = ''
paths = ['tabs/{0}/{1}.html'.format(course.url_name, slug),
'tabs/{0}.html'.format(slug)] if course_module is not None:
fs = course.system.resources_fs html = course_module.get_html()
for p in paths:
if fs.exists(p): return html
try:
with fs.open(p) as tabfile:
# TODO: redundant with module_render.py. Want to be helper methods in static_replace or something.
text = tabfile.read().decode('utf-8')
contents = replace_urls(text, course.metadata['data_dir'])
return replace_urls(contents, staticfiles_prefix='/courses/'+course.id, replace_prefix='/course/')
except (ResourceNotFoundError) as err:
log.exception("Couldn't load tab contents from '{0}': {1}".format(p, err))
return None
return None
...@@ -347,8 +347,13 @@ def course_info(request, course_id): ...@@ -347,8 +347,13 @@ def course_info(request, course_id):
course = get_course_with_access(request.user, course_id, 'load') course = get_course_with_access(request.user, course_id, 'load')
staff_access = has_access(request.user, course, 'staff') staff_access = has_access(request.user, course, 'staff')
return render_to_response('courseware/info.html', {'course': course,
'staff_access': staff_access,})
cache = StudentModuleCache.cache_for_descriptor_descendents(
course.id, request.user, course, depth=2)
return render_to_response('courseware/info.html', {'request' : request, 'course_id' : course_id, 'cache' : cache,
'course': course, 'staff_access': staff_access})
@ensure_csrf_cookie @ensure_csrf_cookie
def static_tab(request, course_id, tab_slug): def static_tab(request, course_id, tab_slug):
...@@ -363,7 +368,10 @@ def static_tab(request, course_id, tab_slug): ...@@ -363,7 +368,10 @@ def static_tab(request, course_id, tab_slug):
if tab is None: if tab is None:
raise Http404 raise Http404
contents = tabs.get_static_tab_contents(course, tab) cache = StudentModuleCache.cache_for_descriptor_descendents(
course.id, request.user, course, depth=2)
contents = tabs.get_static_tab_contents(request, cache, course, tab)
if contents is None: if contents is None:
raise Http404 raise Http404
......
...@@ -27,20 +27,20 @@ $(document).ready(function(){ ...@@ -27,20 +27,20 @@ $(document).ready(function(){
% if user.is_authenticated(): % if user.is_authenticated():
<section class="updates"> <section class="updates">
<h1>Course Updates &amp; News</h1> <h1>Course Updates &amp; News</h1>
${get_course_info_section(course, 'updates')} ${get_course_info_section(request, cache, course, 'updates')}
</section> </section>
<section aria-label="Handout Navigation" class="handouts"> <section aria-label="Handout Navigation" class="handouts">
<h1>${course.info_sidebar_name}</h1> <h1>${course.info_sidebar_name}</h1>
${get_course_info_section(course, 'handouts')} ${get_course_info_section(request, cache, course, 'handouts')}
</section> </section>
% else: % else:
<section class="updates"> <section class="updates">
<h1>Course Updates &amp; News</h1> <h1>Course Updates &amp; News</h1>
${get_course_info_section(course, 'guest_updates')} ${get_course_info_section(request, cache, course, 'guest_updates')}
</section> </section>
<section aria-label="Handout Navigation" class="handouts"> <section aria-label="Handout Navigation" class="handouts">
<h1>Course Handouts</h1> <h1>Course Handouts</h1>
${get_course_info_section(course, 'guest_handouts')} ${get_course_info_section(request, cache, course, 'guest_handouts')}
</section> </section>
% endif % endif
</div> </div>
......
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