update course info module to save content in new 'items' field + update import/export to handle content field other than 'data' or 'metadata'
STUD-154
This commit is contained in:
@@ -7,7 +7,7 @@ from lxml import etree
|
||||
from path import path
|
||||
|
||||
from pkg_resources import resource_string
|
||||
from xblock.fields import Scope, String, Boolean
|
||||
from xblock.fields import Scope, String, Boolean, List
|
||||
from xmodule.editing_module import EditingDescriptor
|
||||
from xmodule.html_checker import check_html
|
||||
from xmodule.stringify import stringify_children
|
||||
@@ -293,6 +293,11 @@ class CourseInfoFields(object):
|
||||
"""
|
||||
Field overrides
|
||||
"""
|
||||
items = List(
|
||||
help="List of course update items",
|
||||
default=[],
|
||||
scope=Scope.content
|
||||
)
|
||||
data = String(
|
||||
help="Html contents to display for this module",
|
||||
default="<ol></ol>",
|
||||
@@ -305,7 +310,9 @@ class CourseInfoModule(CourseInfoFields, HtmlModule):
|
||||
"""
|
||||
Just to support xblock field overrides
|
||||
"""
|
||||
pass
|
||||
# statuses
|
||||
STATUS_VISIBLE = 'visible'
|
||||
STATUS_DELETED = 'deleted'
|
||||
|
||||
|
||||
@XBlock.tag("detached")
|
||||
|
||||
@@ -19,6 +19,7 @@ from xmodule.errortracker import make_error_tracker, exc_info_to_str
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.mako_module import MakoDescriptorSystem
|
||||
from xmodule.x_module import XMLParsingSystem, policy_key
|
||||
from xmodule.modulestore.xml_exporter import DEFAULT_CONTENT_FIELDS
|
||||
|
||||
from xblock.fields import ScopeIds
|
||||
from xblock.field_data import DictFieldData
|
||||
@@ -582,9 +583,35 @@ class XMLModuleStore(ModuleStoreReadBase):
|
||||
if os.path.isdir(base_dir / url_name):
|
||||
self._load_extra_content(system, course_descriptor, category, base_dir / url_name, course_dir)
|
||||
|
||||
def _load_extra_content(self, system, course_descriptor, category, path, course_dir):
|
||||
def _import_field_content(self, course_descriptor, category, file_path):
|
||||
"""
|
||||
Import field data content for field other than 'data' or 'metadata' form json file and
|
||||
return field data content as dictionary
|
||||
"""
|
||||
slug, location, data_content = None, None, None
|
||||
try:
|
||||
# try to read json file
|
||||
# file_path format: {dirname}.{field_name}.json
|
||||
dirname, field, file_suffix = file_path.split('/')[-1].split('.')
|
||||
if file_suffix == 'json' and field not in DEFAULT_CONTENT_FIELDS:
|
||||
slug = os.path.splitext(os.path.basename(dirname))[0]
|
||||
location = course_descriptor.scope_ids.usage_id.replace(category=category, name=slug)
|
||||
with open(file_path) as field_content_file:
|
||||
field_data = json.load(field_content_file)
|
||||
data_content = {field: field_data}
|
||||
except (IOError, ValueError):
|
||||
# ignore this exception
|
||||
# only new exported courses which use content fields other than 'metadata' and 'data'
|
||||
# will have this file '{dirname}.{field_name}.json'
|
||||
data_content = None
|
||||
|
||||
for filepath in glob.glob(path / '*'):
|
||||
return slug, location, data_content
|
||||
|
||||
def _load_extra_content(self, system, course_descriptor, category, content_path, course_dir):
|
||||
"""
|
||||
Import fields data content from files
|
||||
"""
|
||||
for filepath in glob.glob(content_path / '*'):
|
||||
if not os.path.isfile(filepath):
|
||||
continue
|
||||
|
||||
@@ -593,28 +620,55 @@ class XMLModuleStore(ModuleStoreReadBase):
|
||||
|
||||
with open(filepath) as f:
|
||||
try:
|
||||
html = f.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(filepath))[0]
|
||||
loc = course_descriptor.scope_ids.usage_id.replace(category=category, name=slug)
|
||||
module = system.construct_xblock(
|
||||
category,
|
||||
# We're loading a descriptor, so student_id is meaningless
|
||||
# We also don't have separate notions of definition and usage ids yet,
|
||||
# so we use the location for both
|
||||
ScopeIds(None, category, loc, loc),
|
||||
DictFieldData({'data': html, 'location': loc, 'category': category}),
|
||||
)
|
||||
# VS[compat]:
|
||||
# Hack because we need to pull in the 'display_name' for static tabs (because we need to edit them)
|
||||
# from the course policy
|
||||
if category == "static_tab":
|
||||
for tab in course_descriptor.tabs or []:
|
||||
if tab.get('url_slug') == slug:
|
||||
module.display_name = tab['name']
|
||||
module.data_dir = course_dir
|
||||
module.save()
|
||||
self.modules[course_descriptor.id][module.scope_ids.usage_id] = module
|
||||
if filepath.find('.json') != -1:
|
||||
# json file with json data content
|
||||
slug, loc, data_content = self._import_field_content(course_descriptor, category, filepath)
|
||||
if data_content is None:
|
||||
continue
|
||||
else:
|
||||
try:
|
||||
# get and update data field in xblock runtime
|
||||
module = system.load_item(loc)
|
||||
for key, value in data_content.iteritems():
|
||||
setattr(module, key, value)
|
||||
module.save()
|
||||
except ItemNotFoundError:
|
||||
module = None
|
||||
data_content['location'] = loc
|
||||
data_content['category'] = category
|
||||
else:
|
||||
slug = os.path.splitext(os.path.basename(filepath))[0]
|
||||
loc = course_descriptor.scope_ids.usage_id.replace(category=category, name=slug)
|
||||
# html file with html data content
|
||||
html = f.read().decode('utf-8')
|
||||
try:
|
||||
module = system.load_item(loc)
|
||||
module.data = html
|
||||
module.save()
|
||||
except ItemNotFoundError:
|
||||
module = None
|
||||
data_content = {'data': html, 'location': loc, 'category': category}
|
||||
|
||||
if module is None:
|
||||
module = system.construct_xblock(
|
||||
category,
|
||||
# We're loading a descriptor, so student_id is meaningless
|
||||
# We also don't have separate notions of definition and usage ids yet,
|
||||
# so we use the location for both
|
||||
ScopeIds(None, category, loc, loc),
|
||||
DictFieldData(data_content),
|
||||
)
|
||||
# VS[compat]:
|
||||
# Hack because we need to pull in the 'display_name' for static tabs (because we need to edit them)
|
||||
# from the course policy
|
||||
if category == "static_tab":
|
||||
for tab in course_descriptor.tabs or []:
|
||||
if tab.get('url_slug') == slug:
|
||||
module.display_name = tab['name']
|
||||
module.data_dir = course_dir
|
||||
module.save()
|
||||
|
||||
self.modules[course_descriptor.id][module.scope_ids.usage_id] = module
|
||||
except Exception, e:
|
||||
logging.exception("Failed to load %s. Skipping... \
|
||||
Exception: %s", filepath, unicode(e))
|
||||
|
||||
@@ -4,6 +4,7 @@ Methods for exporting course data to XML
|
||||
|
||||
import logging
|
||||
import lxml.etree
|
||||
from xblock.fields import Scope
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.inheritance import own_metadata
|
||||
from fs.osfs import OSFS
|
||||
@@ -19,6 +20,9 @@ PUBLISHED_DIR = "published"
|
||||
EXPORT_VERSION_FILE = "format.json"
|
||||
EXPORT_VERSION_KEY = "export_format"
|
||||
|
||||
DEFAULT_CONTENT_FIELDS = ['metadata', 'data']
|
||||
|
||||
|
||||
class EdxJSONEncoder(json.JSONEncoder):
|
||||
"""
|
||||
Custom JSONEncoder that handles `Location` and `datetime.datetime` objects.
|
||||
@@ -120,6 +124,20 @@ def export_to_xml(modulestore, contentstore, course_location, root_dir, course_d
|
||||
draft_vertical.add_xml_to_node(node)
|
||||
|
||||
|
||||
def _export_field_content(xblock_item, item_dir):
|
||||
"""
|
||||
Export all fields related to 'xblock_item' other than 'metadata' and 'data' to json file in provided directory
|
||||
"""
|
||||
module_data = xblock_item.get_explicitly_set_fields_by_scope(Scope.content)
|
||||
if isinstance(module_data, dict):
|
||||
for field_name in module_data:
|
||||
if field_name not in DEFAULT_CONTENT_FIELDS:
|
||||
# filename format: {dirname}.{field_name}.json
|
||||
with item_dir.open('{0}.{1}.{2}'.format(xblock_item.location.name, field_name, 'json'),
|
||||
'w') as field_content_file:
|
||||
field_content_file.write(dumps(module_data.get(field_name, {}), cls=EdxJSONEncoder))
|
||||
|
||||
|
||||
def export_extra_content(export_fs, modulestore, course_id, course_location, category_type, dirname, file_suffix=''):
|
||||
query_loc = Location('i4x', course_location.org, course_location.course, category_type, None)
|
||||
items = modulestore.get_items(query_loc, course_id)
|
||||
@@ -130,6 +148,9 @@ def export_extra_content(export_fs, modulestore, course_id, course_location, cat
|
||||
with item_dir.open(item.location.name + file_suffix, 'w') as item_file:
|
||||
item_file.write(item.data.encode('utf8'))
|
||||
|
||||
# export content fields other then metadata and data in json format in current directory
|
||||
_export_field_content(item, item_dir)
|
||||
|
||||
|
||||
def convert_between_versions(source_dir, target_dir):
|
||||
"""
|
||||
|
||||
47
common/test/data/course_info_updates/about/overview.html
Normal file
47
common/test/data/course_info_updates/about/overview.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<section class="about">
|
||||
<h2>About This Course</h2>
|
||||
<p>Include your long course description here. The long course description should contain 150-400 words.</p>
|
||||
|
||||
<p>This is paragraph 2 of the long course description. Add more paragraphs as needed. Make sure to enclose them in paragraph tags.</p>
|
||||
</section>
|
||||
|
||||
<section class="prerequisites">
|
||||
<h2>Prerequisites</h2>
|
||||
<p>Add information about course prerequisites here.</p>
|
||||
</section>
|
||||
|
||||
<section class="course-staff">
|
||||
<h2>Course Staff</h2>
|
||||
<article class="teacher">
|
||||
<div class="teacher-image">
|
||||
<img src="/static/images/pl-faculty.png" align="left" style="margin:0 20 px 0" alt="Course Staff Image #1">
|
||||
</div>
|
||||
|
||||
<h3>Staff Member #1</h3>
|
||||
<p>Biography of instructor/staff member #1</p>
|
||||
</article>
|
||||
|
||||
<article class="teacher">
|
||||
<div class="teacher-image">
|
||||
<img src="/static/images/pl-faculty.png" align="left" style="margin:0 20 px 0" alt="Course Staff Image #2">
|
||||
</div>
|
||||
|
||||
<h3>Staff Member #2</h3>
|
||||
<p>Biography of instructor/staff member #2</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="faq">
|
||||
<section class="responses">
|
||||
<h2>Frequently Asked Questions</h2>
|
||||
<article class="response">
|
||||
<h3>Do I need to buy a textbook?</h3>
|
||||
<p>No, a free online version of Chemistry: Principles, Patterns, and Applications, First Edition by Bruce Averill and Patricia Eldredge will be available, though you can purchase a printed version (published by FlatWorld Knowledge) if you’d like.</p>
|
||||
</article>
|
||||
|
||||
<article class="response">
|
||||
<h3>Question #2</h3>
|
||||
<p>Your answer would be displayed here.</p>
|
||||
</article>
|
||||
</section>
|
||||
</section>
|
||||
1
common/test/data/course_info_updates/course.xml
Normal file
1
common/test/data/course_info_updates/course.xml
Normal file
@@ -0,0 +1 @@
|
||||
<course url_name="2014_T1" org="edX" course="course_info_updates"/>
|
||||
1
common/test/data/course_info_updates/course/2014_T1.xml
Normal file
1
common/test/data/course_info_updates/course/2014_T1.xml
Normal file
@@ -0,0 +1 @@
|
||||
<course display_name="Toy Course"/>
|
||||
1
common/test/data/course_info_updates/info/handouts.html
Normal file
1
common/test/data/course_info_updates/info/handouts.html
Normal file
@@ -0,0 +1 @@
|
||||
<ol></ol>
|
||||
1
common/test/data/course_info_updates/info/updates.html
Normal file
1
common/test/data/course_info_updates/info/updates.html
Normal file
@@ -0,0 +1 @@
|
||||
<ol><li><h2>February 13, 2014</h2>Sample update</li></ol>
|
||||
@@ -0,0 +1 @@
|
||||
[{"date": "February 13, 2014", "content": "Sample update", "status": "visible", "id": 1}]
|
||||
@@ -0,0 +1 @@
|
||||
{"GRADER": [{"short_label": "HW", "min_count": 12, "type": "Homework", "drop_count": 2, "weight": 0.15}, {"min_count": 12, "type": "Lab", "drop_count": 2, "weight": 0.15}, {"short_label": "Midterm", "min_count": 1, "type": "Midterm Exam", "drop_count": 0, "weight": 0.3}, {"short_label": "Final", "min_count": 1, "type": "Final Exam", "drop_count": 0, "weight": 0.4}], "GRADE_CUTOFFS": {"Pass": 0.5}}
|
||||
@@ -0,0 +1 @@
|
||||
{"course/2014_T1": {"tabs": [{"type": "courseware", "name": "Courseware"}, {"type": "course_info", "name": "Course Info"}, {"type": "discussion", "name": "Discussion"}, {"type": "wiki", "name": "Wiki"}, {"type": "progress", "name": "Progress"}], "display_name": "Toy Course", "discussion_topics": {"General": {"id": "i4x-edX-course_info_updates-course-2014_T1"}}}}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
Reference in New Issue
Block a user