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:
zubiar-arbi
2014-02-26 13:00:53 +05:00
parent 663671ec4c
commit cfa6b14575
16 changed files with 478 additions and 131 deletions

View File

@@ -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")

View File

@@ -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))

View File

@@ -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):
"""

View 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 youd like.</p>
</article>
<article class="response">
<h3>Question #2</h3>
<p>Your answer would be displayed here.</p>
</article>
</section>
</section>

View File

@@ -0,0 +1 @@
<course url_name="2014_T1" org="edX" course="course_info_updates"/>

View File

@@ -0,0 +1 @@
<course display_name="Toy Course"/>

View File

@@ -0,0 +1 @@
<ol></ol>

View File

@@ -0,0 +1 @@
<ol><li><h2>February 13, 2014</h2>Sample update</li></ol>

View File

@@ -0,0 +1 @@
[{"date": "February 13, 2014", "content": "Sample update", "status": "visible", "id": 1}]

View File

@@ -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}}

View File

@@ -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"}}}}

View File

@@ -0,0 +1 @@
{}