diff --git a/cms/envs/dev.py b/cms/envs/dev.py index c5e1cf5689..13436ac5a5 100644 --- a/cms/envs/dev.py +++ b/cms/envs/dev.py @@ -24,6 +24,7 @@ MODULESTORE = { 'db': 'xmodule', 'collection': 'modulestore', 'fs_root': GITHUB_REPO_ROOT, + 'render_template': 'mitxmako.shortcuts.render_to_string', } } } diff --git a/cms/envs/test.py b/cms/envs/test.py index 3823cd9dd9..7dcd32caab 100644 --- a/cms/envs/test.py +++ b/cms/envs/test.py @@ -47,6 +47,7 @@ MODULESTORE = { 'db': 'test_xmodule', 'collection': 'modulestore', 'fs_root': GITHUB_REPO_ROOT, + 'render_template': 'mitxmako.shortcuts.render_to_string', } } } diff --git a/common/lib/mitxmako/README b/common/djangoapps/mitxmako/README similarity index 100% rename from common/lib/mitxmako/README rename to common/djangoapps/mitxmako/README diff --git a/common/lib/mitxmako/mitxmako/__init__.py b/common/djangoapps/mitxmako/__init__.py similarity index 100% rename from common/lib/mitxmako/mitxmako/__init__.py rename to common/djangoapps/mitxmako/__init__.py diff --git a/common/lib/mitxmako/mitxmako/makoloader.py b/common/djangoapps/mitxmako/makoloader.py similarity index 100% rename from common/lib/mitxmako/mitxmako/makoloader.py rename to common/djangoapps/mitxmako/makoloader.py diff --git a/common/lib/mitxmako/mitxmako/middleware.py b/common/djangoapps/mitxmako/middleware.py similarity index 100% rename from common/lib/mitxmako/mitxmako/middleware.py rename to common/djangoapps/mitxmako/middleware.py diff --git a/common/lib/mitxmako/mitxmako/shortcuts.py b/common/djangoapps/mitxmako/shortcuts.py similarity index 100% rename from common/lib/mitxmako/mitxmako/shortcuts.py rename to common/djangoapps/mitxmako/shortcuts.py diff --git a/common/lib/mitxmako/mitxmako/template.py b/common/djangoapps/mitxmako/template.py similarity index 100% rename from common/lib/mitxmako/mitxmako/template.py rename to common/djangoapps/mitxmako/template.py diff --git a/common/lib/mitxmako/mitxmako/templatetag_helpers.py b/common/djangoapps/mitxmako/templatetag_helpers.py similarity index 100% rename from common/lib/mitxmako/mitxmako/templatetag_helpers.py rename to common/djangoapps/mitxmako/templatetag_helpers.py diff --git a/common/djangoapps/static_replace.py b/common/djangoapps/static_replace.py index ce3dc55031..58e2c8da15 100644 --- a/common/djangoapps/static_replace.py +++ b/common/djangoapps/static_replace.py @@ -15,7 +15,7 @@ def try_staticfiles_lookup(path): try: url = staticfiles_storage.url(path) except Exception as err: - log.warning("staticfiles_storage couldn't find path {}: {}".format( + log.warning("staticfiles_storage couldn't find path {0}: {1}".format( path, str(err))) # Just return the original path; don't kill everything. url = path diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 6d1cbb5afb..4a8de5f5ed 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -279,7 +279,8 @@ def replicate_enrollment_save(sender, **kwargs): enrollment_obj = kwargs['instance'] log.debug("Replicating user because of new enrollment") - replicate_user(enrollment_obj.user, enrollment_obj.course_id) + for course_db_name in db_names_to_replicate_to(enrollment_obj.user.id): + replicate_user(enrollment_obj.user, course_db_name) log.debug("Replicating enrollment because of new enrollment") replicate_model(CourseEnrollment.save, enrollment_obj, enrollment_obj.user_id) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index d327b80c14..3a2e40f896 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -873,6 +873,7 @@ def sympy_check2(): msg = 'No answer entered!' if self.xml.get('empty_answer_err') else '' return CorrectMap(idset[0], 'incorrect', msg=msg) + # NOTE: correct = 'unknown' could be dangerous. Inputtypes such as textline are not expecting 'unknown's correct = ['unknown'] * len(idset) messages = [''] * len(idset) @@ -898,6 +899,7 @@ def sympy_check2(): if type(self.code) == str: try: exec self.code in self.context['global_context'], self.context + correct = self.context['correct'] except Exception as err: print "oops in customresponse (code) error %s" % err print "context = ", self.context @@ -1271,7 +1273,7 @@ main() def setup_response(self): xml = self.xml - self.url = xml.get('url') or "http://eecs1.mit.edu:8889/pyloncapa" # FIXME - hardcoded URL + self.url = xml.get('url') or "http://qisx.mit.edu:8889/pyloncapa" # FIXME - hardcoded URL # answer = xml.xpath('//*[@id=$id]//answer',id=xml.get('id'))[0] # FIXME - catch errors answer = xml.find('answer') diff --git a/common/lib/capa/capa/templates/textinput.html b/common/lib/capa/capa/templates/textinput.html index 7f2f563b81..08aa8379a7 100644 --- a/common/lib/capa/capa/templates/textinput.html +++ b/common/lib/capa/capa/templates/textinput.html @@ -38,5 +38,7 @@ % if msg: ${msg|n} % endif +% if state in ['unsubmitted', 'correct', 'incorrect', 'incomplete'] or hidden: +% endif diff --git a/common/lib/mitxmako/setup.py b/common/lib/mitxmako/setup.py deleted file mode 100644 index 535d86f90e..0000000000 --- a/common/lib/mitxmako/setup.py +++ /dev/null @@ -1,8 +0,0 @@ -from setuptools import setup, find_packages - -setup( - name="mitxmako", - version="0.1", - packages=find_packages(exclude=["tests"]), - install_requires=['distribute'], -) diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py index 8a0a6bb139..cc5dfd183a 100644 --- a/common/lib/xmodule/setup.py +++ b/common/lib/xmodule/setup.py @@ -10,7 +10,6 @@ setup( }, requires=[ 'capa', - 'mitxmako' ], # See http://guide.python-distribute.org/creation.html#entry-points diff --git a/common/lib/xmodule/xmodule/backcompat_module.py b/common/lib/xmodule/xmodule/backcompat_module.py index ed2bdb837a..1e63bfadf8 100644 --- a/common/lib/xmodule/xmodule/backcompat_module.py +++ b/common/lib/xmodule/xmodule/backcompat_module.py @@ -68,7 +68,7 @@ class SemanticSectionDescriptor(XModuleDescriptor): the child element """ xml_object = etree.fromstring(xml_data) - system.error_tracker("WARNING: the <{}> tag is deprecated. Please do not use in new content." + system.error_tracker("WARNING: the <{0}> tag is deprecated. Please do not use in new content." .format(xml_object.tag)) if len(xml_object) == 1: diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index b90a94279c..e6da87b5c6 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -108,11 +108,9 @@ class CapaModule(XModule): self.grace_period = None self.close_date = self.display_due_date - self.max_attempts = only_one(dom2.xpath('/problem/@attempts')) - if len(self.max_attempts) > 0: + self.max_attempts = self.metadata.get('attempts', None) + if self.max_attempts is not None: self.max_attempts = int(self.max_attempts) - else: - self.max_attempts = None self.show_answer = self.metadata.get('showanswer', 'closed') @@ -244,7 +242,14 @@ class CapaModule(XModule): # We using strings as truthy values, because the terminology of the # check button is context-specific. - check_button = "Grade" if self.max_attempts else "Check" + + # Put a "Check" button if unlimited attempts or still some left + if self.max_attempts is None or self.attempts < self.max_attempts-1: + check_button = "Check" + else: + # Will be final check so let user know that + check_button = "Final Check" + reset_button = True save_button = True diff --git a/common/lib/xmodule/xmodule/css/capa/display.scss b/common/lib/xmodule/xmodule/css/capa/display.scss index 6a9b57cfef..666ca57800 100644 --- a/common/lib/xmodule/xmodule/css/capa/display.scss +++ b/common/lib/xmodule/xmodule/css/capa/display.scss @@ -1,298 +1,295 @@ h2 { -margin-top: 0; -margin-bottom: 15px; -width: flex-grid(2, 9); -padding-right: flex-gutter(9); -border-right: 1px dashed #ddd; -@include box-sizing(border-box); -display: table-cell; -vertical-align: top; + margin-top: 0; + margin-bottom: 15px; + width: flex-grid(2, 9); + padding-right: flex-gutter(9); + border-right: 1px dashed #ddd; + @include box-sizing(border-box); + display: table-cell; + vertical-align: top; -&.problem-header { - section.staff { - margin-top: 30px; - font-size: 80%; + &.problem-header { + section.staff { + margin-top: 30px; + font-size: 80%; + } } -} -@media screen and (max-width:1120px) { - display: block; - width: auto; - border-right: 0; -} + @media screen and (max-width:1120px) { + display: block; + width: auto; + border-right: 0; + } -@media print { - display: block; - width: auto; - border-right: 0; -} + @media print { + display: block; + width: auto; + border-right: 0; + } } section.problem { -display: table-cell; -width: flex-grid(7, 9); -padding-left: flex-gutter(9); + display: table-cell; + width: flex-grid(7, 9); + padding-left: flex-gutter(9); -@media screen and (max-width:1120px) { - display: block; - width: auto; - padding: 0; -} - -@media print { - display: block; - width: auto; - padding: 0; - - canvas, img { - page-break-inside: avoid; - } -} - - - -div { - p.status { - text-indent: -9999px; - margin: -1px 0 0 10px; - } - - &.unanswered { - p.status { - @include inline-block(); - background: url('../images/unanswered-icon.png') center center no-repeat; - height: 14px; - width: 14px; - } - } - - &.processing { - p.status { - @include inline-block(); - background: url('../images/spinner.gif') center center no-repeat; - height: 20px; - width: 20px; - text-indent: -9999px; - } - } - - &.correct, &.ui-icon-check { - p.status { - @include inline-block(); - background: url('../images/correct-icon.png') center center no-repeat; - height: 20px; - width: 25px; - } - - input { - border-color: green; - } - } - - &.processing { - p.status { - @include inline-block(); - background: url('../images/spinner.gif') center center no-repeat; - height: 20px; - width: 20px; - } - - input { - border-color: #aaa; - } - } - - &.incorrect, &.ui-icon-close { - p.status { - @include inline-block(); - background: url('../images/incorrect-icon.png') center center no-repeat; - height: 20px; - width: 20px; - text-indent: -9999px; - } - - input { - border-color: red; - } - } - - > span { + @media screen and (max-width:1120px) { display: block; - margin-bottom: lh(.5); + width: auto; + padding: 0; } - p.answer { - @include inline-block(); - margin-bottom: 0; - margin-left: 10px; - - &:before { - content: "Answer: "; - font-weight: bold; - display: inline; + @media print { + display: block; + width: auto; + padding: 0; + canvas, img { + page-break-inside: avoid; } - &:empty { + } + + div { + p { + &.answer { + margin-top: -2px; + } + &.status { + text-indent: -9999px; + margin: 8px 0 0 10px; + } + } + + &.unanswered { + p.status { + @include inline-block(); + background: url('../images/unanswered-icon.png') center center no-repeat; + height: 14px; + width: 14px; + } + } + + &.processing { + p.status { + @include inline-block(); + background: url('../images/spinner.gif') center center no-repeat; + height: 20px; + width: 20px; + text-indent: -9999px; + } + } + + &.correct, &.ui-icon-check { + p.status { + @include inline-block(); + background: url('../images/correct-icon.png') center center no-repeat; + height: 20px; + width: 25px; + } + + input { + border-color: green; + } + } + + &.processing { + p.status { + @include inline-block(); + background: url('../images/spinner.gif') center center no-repeat; + height: 20px; + width: 20px; + } + + input { + border-color: #aaa; + } + } + + &.incorrect, &.ui-icon-close { + p.status { + @include inline-block(); + background: url('../images/incorrect-icon.png') center center no-repeat; + height: 20px; + width: 20px; + text-indent: -9999px; + } + + input { + border-color: red; + } + } + + > span { + display: block; + margin-bottom: lh(.5); + } + + p.answer { + @include inline-block(); + margin-bottom: 0; + margin-left: 10px; + &:before { - display: none; + content: "Answer: "; + font-weight: bold; + display: inline; + + } + &:empty { + &:before { + display: none; + } + } + } + + div.equation { + clear: both; + padding: 6px; + background: #eee; + + span { + margin-bottom: 0; + } + } + + span { + &.unanswered, &.ui-icon-bullet { + @include inline-block(); + background: url('../images/unanswered-icon.png') center center no-repeat; + height: 14px; + position: relative; + top: 4px; + width: 14px; + } + + &.processing, &.ui-icon-processing { + @include inline-block(); + background: url('../images/spinner.gif') center center no-repeat; + height: 20px; + position: relative; + top: 6px; + width: 25px; + } + + &.correct, &.ui-icon-check { + @include inline-block(); + background: url('../images/correct-icon.png') center center no-repeat; + height: 20px; + position: relative; + top: 6px; + width: 25px; + } + + &.incorrect, &.ui-icon-close { + @include inline-block(); + background: url('../images/incorrect-icon.png') center center no-repeat; + height: 20px; + width: 20px; + position: relative; + top: 6px; } } } - div.equation { - clear: both; - padding: 6px; - background: #eee; + ul { + list-style: disc outside none; + margin-bottom: lh(); + margin-left: .75em; + margin-left: .75rem; + } - span { + ol { + list-style: decimal outside none; + margin-bottom: lh(); + margin-left: .75em; + margin-left: .75rem; + } + + dl { + line-height: 1.4em; + } + + dl dt { + font-weight: bold; + } + + dl dd { + margin-bottom: 0; + } + + dd { + margin-left: .5em; + margin-left: .5rem; + } + + li { + line-height: 1.4em; + margin-bottom: lh(.5); + + &:last-child { margin-bottom: 0; } } - span { - &.unanswered, &.ui-icon-bullet { - @include inline-block(); - background: url('../images/unanswered-icon.png') center center no-repeat; - height: 14px; - position: relative; - top: 4px; - width: 14px; + p { + margin-bottom: lh(); + } + + table { + margin-bottom: lh(); + border-collapse: collapse; + table-layout: auto; + + th { + font-weight: bold; + text-align: left; } - &.processing, &.ui-icon-processing { - @include inline-block(); - background: url('../images/spinner.gif') center center no-repeat; - height: 20px; - position: relative; - top: 6px; - width: 25px; + caption, th, td { + padding: .25em .75em .25em 0; + padding: .25rem .75rem .25rem 0; } - &.correct, &.ui-icon-check { - @include inline-block(); - background: url('../images/correct-icon.png') center center no-repeat; - height: 20px; - position: relative; - top: 6px; - width: 25px; + caption { + background: #f1f1f1; + margin-bottom: .75em; + margin-bottom: .75rem; + padding: .75em 0; + padding: .75rem 0; } - &.incorrect, &.ui-icon-close { - @include inline-block(); - background: url('../images/incorrect-icon.png') center center no-repeat; - height: 20px; - width: 20px; - position: relative; - top: 6px; + tr, td, th { + vertical-align: middle; } - } -} -ul { - list-style: disc outside none; - margin-bottom: lh(); - margin-left: .75em; - margin-left: .75rem; -} - -ol { - list-style: decimal outside none; - margin-bottom: lh(); - margin-left: .75em; - margin-left: .75rem; -} - -dl { - line-height: 1.4em; -} - -dl dt { - font-weight: bold; -} - -dl dd { - margin-bottom: 0; -} - -dd { - margin-left: .5em; - margin-left: .5rem; -} - -li { - line-height: 1.4em; - margin-bottom: lh(.5); - - &:last-child { - margin-bottom: 0; - } -} - -p { - margin-bottom: lh(); -} - -table { - margin-bottom: lh(); - width: 100%; - // border: 1px solid #ddd; - border-collapse: collapse; - - th { - // border-bottom: 2px solid #ccc; - font-weight: bold; - text-align: left; } - td { - // border: 1px solid #ddd; + hr { + background: #ddd; + border: none; + clear: both; + color: #ddd; + float: none; + height: 1px; + margin: 0 0 .75rem; + width: 100%; } - caption, th, td { - padding: .25em .75em .25em 0; - padding: .25rem .75rem .25rem 0; + .hidden { + display: none; + visibility: hidden; } - caption { - background: #f1f1f1; - margin-bottom: .75em; - margin-bottom: .75rem; - padding: .75em 0; - padding: .75rem 0; + #{$all-text-inputs} { + display: inline; + width: auto; } - tr, td, th { - vertical-align: middle; + // this supports a deprecated element and should be removed once the center tag is removed + center { + display: block; + margin: lh() 0; + border: 1px solid #ccc; + padding: lh(); } - -} - -hr { - background: #ddd; - border: none; - clear: both; - color: #ddd; - float: none; - height: 1px; - margin: 0 0 .75rem; - width: 100%; -} - -.hidden { - display: none; - visibility: hidden; -} - -#{$all-text-inputs} { - display: inline; - width: auto; -} - -// this supports a deprecated element and should be removed once the center tag is removed -center { - display: block; - margin: lh() 0; - border: 1px solid #ccc; - padding: lh(); -} } diff --git a/common/lib/xmodule/xmodule/css/video/display.scss b/common/lib/xmodule/xmodule/css/video/display.scss index 9e32de941a..0b4cf883bf 100644 --- a/common/lib/xmodule/xmodule/css/video/display.scss +++ b/common/lib/xmodule/xmodule/css/video/display.scss @@ -14,7 +14,7 @@ div.video { section.video-player { height: 0; - // overflow: hidden; + overflow: hidden; padding-bottom: 56.25%; position: relative; @@ -171,7 +171,7 @@ div.video { position: relative; @include transition(); -webkit-font-smoothing: antialiased; - width: 110px; + width: 116px; h3 { color: #999; @@ -209,7 +209,7 @@ div.video { display: none; opacity: 0; position: absolute; - width: 125px; + width: 133px; z-index: 10; li { @@ -444,13 +444,13 @@ div.video { height: 100%; left: 0; margin: 0; - max-height: 100%; overflow: hidden; padding: 0; position: fixed; top: 0; width: 100%; z-index: 999; + vertical-align: middle; &.closed { ol.subtitles { @@ -459,30 +459,17 @@ div.video { } } - a.exit { - color: #aaa; - display: none; - font-style: 12px; - left: 20px; - letter-spacing: 1px; - position: absolute; - text-transform: uppercase; - top: 20px; - - &::after { - content: "✖"; - @include inline-block(); - padding-left: 6px; - } - - &:hover { - color: $mit-red; - } - } - div.tc-wrapper { + @include clearfix; + display: table; + width: 100%; + height: 100%; + article.video-wrapper { width: 100%; + display: table-cell; + vertical-align: middle; + float: none; } object, iframe { diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py index 5e7c1f7e3f..c0b82fef90 100644 --- a/common/lib/xmodule/xmodule/html_module.py +++ b/common/lib/xmodule/xmodule/html_module.py @@ -39,6 +39,8 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): def backcompat_paths(cls, path): if path.endswith('.html.xml'): path = path[:-9] + '.html' # backcompat--look for html instead of xml + if path.endswith('.html.html'): + path = path[:-5] # some people like to include .html in filenames.. candidates = [] while os.sep in path: candidates.append(path) @@ -73,7 +75,9 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): cls.clean_metadata_from_xml(definition_xml) return {'data': stringify_children(definition_xml)} else: - filepath = cls._format_filepath(xml_object.tag, filename) + # html is special. cls.filename_extension is 'xml', but if 'filename' is in the definition, + # that means to load from .html + filepath = "{category}/{name}.html".format(category='html', name=filename) # VS[compat] # TODO (cpennington): If the file doesn't exist at the right path, diff --git a/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee b/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee index 0ef091d6b3..4b265d20c8 100644 --- a/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee +++ b/common/lib/xmodule/xmodule/js/src/video/display/video_player.coffee @@ -130,13 +130,11 @@ class @VideoPlayer extends Subview toggleFullScreen: (event) => event.preventDefault() if @el.hasClass('fullscreen') - @$('.exit').remove() @$('.add-fullscreen').attr('title', 'Fill browser') @el.removeClass('fullscreen') else - @el.append('Exit').addClass('fullscreen') + @el.addClass('fullscreen') @$('.add-fullscreen').attr('title', 'Exit fill browser') - @$('.exit').click @toggleFullScreen @caption.resize() # Delegates diff --git a/common/lib/xmodule/xmodule/modulestore/django.py b/common/lib/xmodule/xmodule/modulestore/django.py index 546aaf30c8..6a7315a074 100644 --- a/common/lib/xmodule/xmodule/modulestore/django.py +++ b/common/lib/xmodule/xmodule/modulestore/django.py @@ -12,15 +12,34 @@ from django.conf import settings _MODULESTORES = {} +FUNCTION_KEYS = ['render_template'] + + +def load_function(path): + """ + Load a function by name. + + path is a string of the form "path.to.module.function" + returns the imported python object `function` from `path.to.module` + """ + module_path, _, name = path.rpartition('.') + return getattr(import_module(module_path), name) + def modulestore(name='default'): global _MODULESTORES if name not in _MODULESTORES: - class_path = settings.MODULESTORE[name]['ENGINE'] - module_path, _, class_name = class_path.rpartition('.') - class_ = getattr(import_module(module_path), class_name) + class_ = load_function(settings.MODULESTORE[name]['ENGINE']) + + options = {} + options.update(settings.MODULESTORE[name]['OPTIONS']) + for key in FUNCTION_KEYS: + if key in options: + options[key] = load_function(options[key]) + _MODULESTORES[name] = class_( - **settings.MODULESTORE[name]['OPTIONS']) + **options + ) return _MODULESTORES[name] diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index b6101a6929..caada39f3e 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -9,7 +9,6 @@ from importlib import import_module from xmodule.errortracker import null_error_tracker from xmodule.x_module import XModuleDescriptor from xmodule.mako_module import MakoDescriptorSystem -from mitxmako.shortcuts import render_to_string from . import ModuleStoreBase, Location from .exceptions import (ItemNotFoundError, @@ -82,7 +81,8 @@ class MongoModuleStore(ModuleStoreBase): """ # TODO (cpennington): Enable non-filesystem filestores - def __init__(self, host, db, collection, fs_root, port=27017, default_class=None, + def __init__(self, host, db, collection, fs_root, render_template, + port=27017, default_class=None, error_tracker=null_error_tracker): ModuleStoreBase.__init__(self) @@ -108,6 +108,7 @@ class MongoModuleStore(ModuleStoreBase): self.default_class = None self.fs_root = path(fs_root) self.error_tracker = error_tracker + self.render_template = render_template def _clean_item_data(self, item): """ @@ -160,7 +161,7 @@ class MongoModuleStore(ModuleStoreBase): self.default_class, resource_fs, self.error_tracker, - render_to_string, + self.render_template, ) return system.load_item(item['location']) diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py index cb94444b7a..746240e763 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py @@ -26,6 +26,7 @@ DB = 'test' COLLECTION = 'modulestore' FS_ROOT = DATA_DIR # TODO (vshnayder): will need a real fs_root for testing load_item DEFAULT_CLASS = 'xmodule.raw_module.RawDescriptor' +RENDER_TEMPLATE = lambda t_n, d, ctx=None, nsp='main': '' class TestMongoModuleStore(object): @@ -48,7 +49,7 @@ class TestMongoModuleStore(object): @staticmethod def initdb(): # connect to the db - store = MongoModuleStore(HOST, DB, COLLECTION, FS_ROOT, default_class=DEFAULT_CLASS) + store = MongoModuleStore(HOST, DB, COLLECTION, FS_ROOT, RENDER_TEMPLATE, default_class=DEFAULT_CLASS) # Explicitly list the courses to load (don't want the big one) courses = ['toy', 'simple'] import_from_xml(store, DATA_DIR, courses) diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py index 971124e413..a77266113f 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml.py +++ b/common/lib/xmodule/xmodule/modulestore/xml.py @@ -187,11 +187,11 @@ class XMLModuleStore(ModuleStoreBase): if not os.path.exists(policy_path): return {} try: - log.debug("Loading policy from {}".format(policy_path)) + log.debug("Loading policy from {0}".format(policy_path)) with open(policy_path) as f: return json.load(f) except (IOError, ValueError) as err: - msg = "Error loading course policy from {}".format(policy_path) + msg = "Error loading course policy from {0}".format(policy_path) tracker(msg) log.warning(msg + " " + str(err)) return {} @@ -238,7 +238,7 @@ class XMLModuleStore(ModuleStoreBase): url_name = course_data.get('url_name') if url_name: - policy_path = self.data_dir / course_dir / 'policies' / '{}.json'.format(url_name) + policy_path = self.data_dir / course_dir / 'policies' / '{0}.json'.format(url_name) policy = self.load_policy(policy_path, tracker) else: policy = {} diff --git a/common/lib/xmodule/xmodule/tests/test_import.py b/common/lib/xmodule/xmodule/tests/test_import.py index dfa75f9137..2061b63fb6 100644 --- a/common/lib/xmodule/xmodule/tests/test_import.py +++ b/common/lib/xmodule/xmodule/tests/test_import.py @@ -201,7 +201,7 @@ class ImportTestCase(unittest.TestCase): def check_for_key(key, node): "recursive check for presence of key" - print "Checking {}".format(node.location.url()) + print "Checking {0}".format(node.location.url()) self.assertTrue(key in node.metadata) for c in node.get_children(): check_for_key(key, c) diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index d4d61f4aa1..c581911c03 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -629,7 +629,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet): try: return parse_time(self.metadata[key]) except ValueError as e: - msg = "Descriptor {} loaded with a bad metadata key '{}': '{}'".format( + msg = "Descriptor {0} loaded with a bad metadata key '{1}': '{2}'".format( self.location.url(), self.metadata[key], e) log.warning(msg) return None diff --git a/common/xml_cleanup.py b/common/xml_cleanup.py index 8e794b97c2..15890fb99e 100755 --- a/common/xml_cleanup.py +++ b/common/xml_cleanup.py @@ -47,12 +47,12 @@ def cleanup(filepath, remove_meta): 'ispublic', 'xqa_key') try: - print "Cleaning {}".format(filepath) + print "Cleaning {0}".format(filepath) with open(filepath) as f: parser = etree.XMLParser(remove_comments=False) xml = etree.parse(filepath, parser=parser) except: - print "Error parsing file {}".format(filepath) + print "Error parsing file {0}".format(filepath) return for node in xml.iter(tag=etree.Element): @@ -67,12 +67,12 @@ def cleanup(filepath, remove_meta): del attrs['name'] if 'url_name' in attrs and 'slug' in attrs: - print "WARNING: {} has both slug and url_name" + print "WARNING: {0} has both slug and url_name".format(node) if ('url_name' in attrs and 'filename' in attrs and len(attrs)==2 and attrs['url_name'] == attrs['filename']): # This is a pointer tag in disguise. Get rid of the filename. - print 'turning {}.{} into a pointer tag'.format(node.tag, attrs['url_name']) + print 'turning {0}.{1} into a pointer tag'.format(node.tag, attrs['url_name']) del attrs['filename'] if remove_meta: diff --git a/doc/xml-format.md b/doc/xml-format.md new file mode 100644 index 0000000000..2a9e379ccc --- /dev/null +++ b/doc/xml-format.md @@ -0,0 +1,147 @@ +This doc is a rough spec of our xml format + +Every content element (within a course) should have a unique id. This id is formed as {category}/{url_name}. Categories are the different tag types ('chapter', 'problem', 'html', 'sequential', etc). Url_name is a string containing a-z, A-Z, dot (.) and _. This is what appears in urls that point to this object. + +File layout: + +- Xml files have content +- "policy", which is also called metadata in various places, should live in a policy file. + +- each module (except customtag and course, which are special, see below) should live in a file, located at {category}/{url_name].xml +To include this module in another one (e.g. to put a problem in a vertical), put in a "pointer tag": <{category} url_name="{url_name}"/>. When we read that, we'll load the actual contents. + +Customtag is already a pointer, you can just use it in place: + +Course tags: + - the top level course pointer tag lives in course.xml + - have 2 extra required attributes: "org" and "course" -- organization name, and course name. Note that the course name is referring to the platonic ideal of this course, not to any particular run of this course. The url_name should be particular run of this course. E.g. + +If course.xml contains: + + +we would load the actual course definition from course/2012.xml + +To support multiple different runs of the course, you could have a different course.xml, containing + + + +which would load the Harvard-internal version from course/2012H.xml + +If there is only one run of the course for now, just have a single course.xml with the right url_name. + +If there is more than one run of the course, the different course root pointer files should live in +roots/url_name.xml, and course.xml should be a symbolic link to the one you want to run in your dev instance. + +If you want to run both versions, you need to checkout the repo twice, and have course.xml point to different root/{url_name}.xml files. + +Policies: + - the policy for a course url_name lives in policies/{url_name}.json + +The format is called "json", and is best shown by example (though also feel free to google :) + +the file is a dictionary (mapping from keys to values, syntax "{ key : value, key2 : value2, etc}" + +Keys are in the form "{category}/{url_name}", which should uniquely id a content element. +Values are dictionaries of the form {"metadata-key" : "metadata-value"}. + +metadata can also live in the xml files, but anything defined in the policy file overrides anything in the xml. This is primarily for backwards compatibility, and you should probably not use both. If you do leave some metadata tags in the xml, please be consistent (e.g. if display_names stay in xml, they should all stay in xml). + - note, some xml attributes are not metadata. e.g. in