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 , the youtube attribute specifies what video this is, and is logically part of the content, not the policy, so it should stay in video/{url_name}.xml.
+
+Example policy file:
+{
+ "course/2012": {
+ "graceperiod": "1 day",
+ "start": "2012-10-15T12:00",
+ "display_name": "Introduction to Computer Science I",
+ "xqa_key": "z1y4vdYcy0izkoPeihtPClDxmbY1ogDK"
+ },
+ "chapter/Week_0": {
+ "display_name": "Week 0"
+ },
+ "sequential/Pre-Course_Survey": {
+ "display_name": "Pre-Course Survey",
+ "format": "Survey"
+ }
+}
+
+NOTE: json is picky about commas. If you have trailing commas before closing braces, it will complain and refuse to parse the file. This is irritating.
+
+
+Valid tag categories:
+
+abtest
+chapter
+course
+customtag
+html
+error -- don't put these in by hand :)
+problem
+problemset
+sequential
+vertical
+video
+videosequence
+
+Obsolete tags:
+Use customtag instead:
+ videodev
+ book
+ slides
+ image
+ discuss
+
+Ex: instead of , use
+
+Use something semantic instead, as makes sense: sequential, vertical, videosequence if it's actually a sequence. If the section would only contain a single element, just include that element directly.
+ section
+
+In general, prefer the most "semantic" name for containers: e.g. use problemset rather than vertical for a problem set. That way, if we decide to display problem sets differently, we don't have to change the xml.
+
+How customtags work:
+ When we see , we will:
+
+ - look for a file called custom_tags/special in your course dir.
+ - render it as a mako template, passing parameters {'animal':'unicorn', 'hat':'blue'}, generating html.
+
+
+METADATA
+
+Metadata that we generally understand:
+Only on course tag in courses/url_name.xml
+ ispublic
+ xqa_key -- set only on course, inherited to everything else
+
+Everything:
+ display_name
+ format (maybe only content containers, e.g. "Lecture sequence", "problem set", "lab", etc. )
+ start -- modules will not show up to non-course-staff users before the start date (in production)
+ hide_from_toc -- if this is true, don't show in table of contents for the course. Useful on chapters, and chapter subsections that are linked to from somewhere else.
+
+Used for problems
+graceperiod
+showanswer
+rerandomize
+graded
+due
+
+
+These are _inherited_ : if specified on the course, will apply to everything in the course, except for things that explicitly specify them, and their children.
+ 'graded', 'start', 'due', 'graceperiod', 'showanswer', 'rerandomize',
+ # TODO (ichuang): used for Fall 2012 xqa server access
+ 'xqa_key',
+
+Example sketch:
+
+ -- start tue
+ --- start tue
+
+ -- start wed
+ -- start thu
+ -- start wed
+
+
+
+
+STATIC LINKS:
+
+if your content links (e.g. in an html file) to "static/blah/ponies.jpg", we will look for this in YOUR_COURSE_DIR/blah/ponies.jpg. Note that this is not looking in a static/ subfolder in your course dir. This may (should?) change at some point.
diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py
index e588f807da..eaf70d7814 100644
--- a/lms/djangoapps/courseware/access.py
+++ b/lms/djangoapps/courseware/access.py
@@ -65,7 +65,7 @@ def has_access(user, obj, action):
# Passing an unknown object here is a coding error, so rather than
# returning a default, complain.
- raise TypeError("Unknown object type in has_access(): '{}'"
+ raise TypeError("Unknown object type in has_access(): '{0}'"
.format(type(obj)))
@@ -255,7 +255,7 @@ def _dispatch(table, action, user, obj):
action)
return result
- raise ValueError("Unknown action for object type '{}': '{}'".format(
+ raise ValueError("Unknown action for object type '{0}': '{1}'".format(
type(obj), action))
def _course_staff_group_name(location):
diff --git a/lms/djangoapps/courseware/management/commands/metadata_to_json.py b/lms/djangoapps/courseware/management/commands/metadata_to_json.py
index 0f48e93319..dcbcdc0df3 100644
--- a/lms/djangoapps/courseware/management/commands/metadata_to_json.py
+++ b/lms/djangoapps/courseware/management/commands/metadata_to_json.py
@@ -41,7 +41,7 @@ def import_course(course_dir, verbose=True):
course = courses[0]
errors = modulestore.get_item_errors(course.location)
if len(errors) != 0:
- sys.stderr.write('ERRORs during import: {}\n'.format('\n'.join(map(str_of_err, errors))))
+ sys.stderr.write('ERRORs during import: {0}\n'.format('\n'.join(map(str_of_err, errors))))
return course
diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py
index f58170552c..558f6deeb2 100644
--- a/lms/djangoapps/courseware/module_render.py
+++ b/lms/djangoapps/courseware/module_render.py
@@ -75,6 +75,10 @@ def toc_for_course(user, request, course, active_chapter, active_section, course
chapters = list()
for chapter in course.get_display_items():
+ hide_from_toc = chapter.metadata.get('hide_from_toc','false').lower() == 'true'
+ if hide_from_toc:
+ continue
+
sections = list()
for section in chapter.get_display_items():
@@ -323,7 +327,7 @@ def xqueue_callback(request, course_id, userid, id, dispatch):
user, modulestore().get_item(id), depth=0, select_for_update=True)
instance = get_module(user, request, id, student_module_cache)
if instance is None:
- log.debug("No module {} for user {}--access denied?".format(id, user))
+ log.debug("No module {0} for user {1}--access denied?".format(id, user))
raise Http404
instance_module = get_instance_module(user, instance, student_module_cache)
@@ -386,7 +390,7 @@ def modx_dispatch(request, dispatch=None, id=None, course_id=None):
if instance is None:
# Either permissions just changed, or someone is trying to be clever
# and load something they shouldn't have access to.
- log.debug("No module {} for user {}--access denied?".format(id, user))
+ log.debug("No module {0} for user {1}--access denied?".format(id, user))
raise Http404
instance_module = get_instance_module(request.user, instance, student_module_cache)
diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py
index f3b978adac..da518f45b7 100644
--- a/lms/djangoapps/courseware/tests/tests.py
+++ b/lms/djangoapps/courseware/tests/tests.py
@@ -56,6 +56,7 @@ def mongo_store_config(data_dir):
'db': 'xmodule',
'collection': 'modulestore',
'fs_root': data_dir,
+ 'render_template': 'mitxmako.shortcuts.render_to_string',
}
}
}
@@ -176,7 +177,7 @@ class PageLoader(ActivateLoginTestCase):
def try_enroll(self, course):
"""Try to enroll. Return bool success instead of asserting it."""
data = self._enroll(course)
- print 'Enrollment in {} result: {}'.format(course.location.url(), data)
+ print 'Enrollment in {0} result: {1}'.format(course.location.url(), data)
return data['success']
def enroll(self, course):
@@ -308,7 +309,7 @@ class TestViewAuth(PageLoader):
# shouldn't be able to get to the instructor pages
for url in instructor_urls(self.toy) + instructor_urls(self.full):
- print 'checking for 404 on {}'.format(url)
+ print 'checking for 404 on {0}'.format(url)
self.check_for_get_code(404, url)
# Make the instructor staff in the toy course
@@ -321,11 +322,11 @@ class TestViewAuth(PageLoader):
# Now should be able to get to the toy course, but not the full course
for url in instructor_urls(self.toy):
- print 'checking for 200 on {}'.format(url)
+ print 'checking for 200 on {0}'.format(url)
self.check_for_get_code(200, url)
for url in instructor_urls(self.full):
- print 'checking for 404 on {}'.format(url)
+ print 'checking for 404 on {0}'.format(url)
self.check_for_get_code(404, url)
@@ -336,7 +337,7 @@ class TestViewAuth(PageLoader):
# and now should be able to load both
for url in instructor_urls(self.toy) + instructor_urls(self.full):
- print 'checking for 200 on {}'.format(url)
+ print 'checking for 200 on {0}'.format(url)
self.check_for_get_code(200, url)
@@ -387,7 +388,11 @@ class TestViewAuth(PageLoader):
list of urls that students should be able to see only
after launch, but staff should see before
"""
- urls = reverse_urls(['info', 'book', 'courseware', 'profile'], course)
+ urls = reverse_urls(['info', 'courseware', 'profile'], course)
+ urls.extend([
+ reverse('book', kwargs={'course_id': course.id, 'book_index': book.title})
+ for book in course.textbooks
+ ])
return urls
def light_student_urls(course):
@@ -412,22 +417,22 @@ class TestViewAuth(PageLoader):
def check_non_staff(course):
"""Check that access is right for non-staff in course"""
- print '=== Checking non-staff access for {}'.format(course.id)
+ print '=== Checking non-staff access for {0}'.format(course.id)
for url in instructor_urls(course) + dark_student_urls(course):
- print 'checking for 404 on {}'.format(url)
+ print 'checking for 404 on {0}'.format(url)
self.check_for_get_code(404, url)
for url in light_student_urls(course):
- print 'checking for 200 on {}'.format(url)
+ print 'checking for 200 on {0}'.format(url)
self.check_for_get_code(200, url)
def check_staff(course):
"""Check that access is right for staff in course"""
- print '=== Checking staff access for {}'.format(course.id)
+ print '=== Checking staff access for {0}'.format(course.id)
for url in (instructor_urls(course) +
dark_student_urls(course) +
light_student_urls(course)):
- print 'checking for 200 on {}'.format(url)
+ print 'checking for 200 on {0}'.format(url)
self.check_for_get_code(200, url)
# First, try with an enrolled student
diff --git a/lms/envs/aws.py b/lms/envs/aws.py
index c704fd164e..d2d71830b0 100644
--- a/lms/envs/aws.py
+++ b/lms/envs/aws.py
@@ -56,3 +56,7 @@ AWS_SECRET_ACCESS_KEY = AUTH_TOKENS["AWS_SECRET_ACCESS_KEY"]
DATABASES = AUTH_TOKENS['DATABASES']
XQUEUE_INTERFACE = AUTH_TOKENS['XQUEUE_INTERFACE']
+
+if 'COURSE_ID' in ENV_TOKENS:
+ ASKBOT_URL = "courses/{0}/discussions/".format(ENV_TOKENS['COURSE_ID'])
+
diff --git a/lms/envs/dev_mongo.py b/lms/envs/dev_mongo.py
index 1c4980a538..6af0a429bb 100644
--- a/lms/envs/dev_mongo.py
+++ b/lms/envs/dev_mongo.py
@@ -14,6 +14,7 @@ MODULESTORE = {
'db': 'xmodule',
'collection': 'modulestore',
'fs_root': GITHUB_REPO_ROOT,
+ 'render_template': 'mitxmako.shortcuts.render_to_string',
}
}
}
diff --git a/lms/templates/courseware.html b/lms/templates/courseware.html
index a14f35d154..92c6c9707c 100644
--- a/lms/templates/courseware.html
+++ b/lms/templates/courseware.html
@@ -8,6 +8,7 @@
%block>
<%block name="js_extra">
+
## codemirror
diff --git a/proxy/nginx.conf b/proxy/nginx.conf
index 470c3933ac..2b48e17d03 100644
--- a/proxy/nginx.conf
+++ b/proxy/nginx.conf
@@ -61,6 +61,11 @@ http {
location /courses/MITx/6.002x/2012_Fall/ {
proxy_pass http://course_mitx_6002_2012_fall;
}
+
+ location ~ /courses/([^/]*)/([^/]*)/([^/]*)/(course_wiki|wiki) {
+ proxy_pass http://portal;
+ }
+
}
}
diff --git a/rakefile b/rakefile
index c62c87701e..9eaa4534f2 100644
--- a/rakefile
+++ b/rakefile
@@ -25,7 +25,6 @@ PACKAGE_REPO = "packages@gp.mitx.mit.edu:/opt/pkgrepo.incoming"
NORMALIZED_DEPLOY_NAME = DEPLOY_NAME.downcase().gsub(/[_\/]/, '-')
INSTALL_DIR_PATH = File.join(DEPLOY_DIR, NORMALIZED_DEPLOY_NAME)
-PIP_REPO_REQUIREMENTS = "#{INSTALL_DIR_PATH}/repo-requirements.txt"
# Set up the clean and clobber tasks
CLOBBER.include(BUILD_DIR, REPORT_DIR, 'cover*', '.coverage', 'test_root/*_repo', 'test_root/staticfiles')
CLEAN.include("#{BUILD_DIR}/*.deb", "#{BUILD_DIR}/util")
@@ -193,36 +192,7 @@ task :package do
afterremove.close()
FileUtils.chmod(0755, afterremove.path)
- postinstall = Tempfile.new('postinstall')
- postinstall.write <<-POSTINSTALL.gsub(/^\s*/, '')
- #! /bin/sh
- set -e
- set -x
-
-
- service gunicorn stop || echo "Unable to stop gunicorn. Continuing"
- rm -f #{LINK_PATH}
- ln -s #{INSTALL_DIR_PATH} #{LINK_PATH}
- chown makeitso:makeitso #{LINK_PATH}
-
- # install python modules that are in the package
- if [ -r #{PIP_REPO_REQUIREMENTS} ]; then
- cd #{INSTALL_DIR_PATH}
- pip install -r #{PIP_REPO_REQUIREMENTS}
- fi
-
- chown -R makeitso:makeitso #{INSTALL_DIR_PATH}
-
- # Delete mako temp files
- rm -rf /tmp/tmp*mako
-
- service gunicorn start || echo "Unable to start gunicorn. Continuing"
- POSTINSTALL
- postinstall.close()
- FileUtils.chmod(0755, postinstall.path)
-
args = ["fakeroot", "fpm", "-s", "dir", "-t", "deb",
- "--after-install=#{postinstall.path}",
"--after-remove=#{afterremove.path}",
"--prefix=#{INSTALL_DIR_PATH}",
"--exclude=**/build/**",
diff --git a/repo-requirements.txt b/repo-requirements.txt
index 74bf02b5bd..dced5b960b 100644
--- a/repo-requirements.txt
+++ b/repo-requirements.txt
@@ -1,3 +1,2 @@
-e common/lib/capa
--e common/lib/mitxmako
-e common/lib/xmodule