diff --git a/README b/README
index 6860772c35..a76ea66690 100644
--- a/README
+++ b/README
@@ -1,2 +1 @@
-This branch (re-)adds dynamic math and symbolicresponse.
-Test cases included.
+see doc/ for documentation.
diff --git a/cms/djangoapps/contentstore/management/commands/import.py b/cms/djangoapps/contentstore/management/commands/import.py
index d6064e1e3d..12ea2e0399 100644
--- a/cms/djangoapps/contentstore/management/commands/import.py
+++ b/cms/djangoapps/contentstore/management/commands/import.py
@@ -10,6 +10,7 @@ import os.path
from StringIO import StringIO
from mako.template import Template
from mako.lookup import TemplateLookup
+from collections import defaultdict
from django.core.management.base import BaseCommand
from keystore.django import keystore
@@ -42,9 +43,7 @@ class Command(BaseCommand):
# Simple lists
'chapter': 'Week',
'course': 'Course',
- 'sequential': 'LectureSequence',
- 'vertical': 'ProblemSet',
- 'section': {
+ 'section': defaultdict(lambda: 'Section', {
'Lab': 'Lab',
'Lecture Sequence': 'LectureSequence',
'Homework': 'Homework',
@@ -52,8 +51,13 @@ class Command(BaseCommand):
'Video': 'VideoSegment',
'Midterm': 'Exam',
'Final': 'Exam',
- None: 'Section',
- },
+ 'Problems': 'ProblemSet',
+ }),
+ 'videosequence': 'VideoSequence',
+ 'problemset': 'ProblemSet',
+ 'vertical': 'Section',
+ 'sequential': 'Section',
+ 'tab': 'Section',
# True types
'video': 'VideoSegment',
'html': 'HTML',
@@ -78,6 +82,8 @@ class Command(BaseCommand):
e.set('url', 'i4x://mit.edu/6002xs12/{category}/{name}'.format(
category=category,
name=name))
+ else:
+ print "Skipping element with tag", e.tag
def handle_skip(e):
@@ -150,6 +156,9 @@ class Command(BaseCommand):
'sequential': handle_list,
'vertical': handle_list,
'section': handle_list,
+ 'videosequence': handle_list,
+ 'problemset': handle_list,
+ 'tab': handle_list,
# True types
'video': handle_video,
'html': handle_html,
diff --git a/cms/djangoapps/contentstore/views.py b/cms/djangoapps/contentstore/views.py
index a87520ab13..8bd55bf60f 100644
--- a/cms/djangoapps/contentstore/views.py
+++ b/cms/djangoapps/contentstore/views.py
@@ -1,8 +1,11 @@
from mitxmako.shortcuts import render_to_response
from keystore.django import keystore
-from django.contrib.auth.decorators import login_required
+from django_future.csrf import ensure_csrf_cookie
+from django.http import HttpResponse
+import json
+@ensure_csrf_cookie
def index(request):
# TODO (cpennington): These need to be read in from the active user
org = 'mit.edu'
@@ -11,3 +14,20 @@ def index(request):
course = keystore().get_item(['i4x', org, course, 'Course', name])
weeks = course.get_children()
return render_to_response('index.html', {'weeks': weeks})
+
+
+def edit_item(request):
+ item_id = request.GET['id']
+ item = keystore().get_item(item_id)
+ return render_to_response('unit.html', {
+ 'contents': item.get_html(),
+ 'type': item.type,
+ 'name': item.name,
+ })
+
+
+def save_item(request):
+ item_id = request.POST['id']
+ data = json.loads(request.POST['data'])
+ keystore().update_item(item_id, data)
+ return HttpResponse(json.dumps({}))
diff --git a/cms/envs/common.py b/cms/envs/common.py
index 20d49b7ac5..fc721ca820 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -21,6 +21,9 @@ Longer TODO:
import sys
import tempfile
+import os.path
+import os
+import errno
from path import path
############################ FEATURE CONFIGURATION #############################
@@ -154,7 +157,38 @@ PIPELINE_CSS = {
PIPELINE_ALWAYS_RECOMPILE = ['sass/base-style.scss']
+from x_module import XModuleDescriptor
+js_file_dir = PROJECT_ROOT / "static" / "coffee" / "module"
+try:
+ os.makedirs(js_file_dir)
+except OSError as exc:
+ if exc.errno == errno.EEXIST:
+ pass
+ else:
+ raise
+
+module_js_sources = []
+for xmodule in XModuleDescriptor.load_classes():
+ js = xmodule.get_javascript()
+ for filetype in ('coffee', 'js'):
+ for idx, fragment in enumerate(js.get(filetype, [])):
+ path = os.path.join(js_file_dir, "{name}.{idx}.{type}".format(
+ name=xmodule.__name__,
+ idx=idx,
+ type=filetype))
+ with open(path, 'w') as js_file:
+ js_file.write(fragment)
+ module_js_sources.append(path.replace(PROJECT_ROOT / "static/", ""))
+
PIPELINE_JS = {
+ 'main': {
+ 'source_filenames': ['coffee/main.coffee', 'coffee/unit.coffee'],
+ 'output_filename': 'js/main.js',
+ },
+ 'module-js': {
+ 'source_filenames': module_js_sources,
+ 'output_filename': 'js/modules.js',
+ }
}
PIPELINE_COMPILERS = [
diff --git a/cms/static/coffee/.gitignore b/cms/static/coffee/.gitignore
new file mode 100644
index 0000000000..bb90193362
--- /dev/null
+++ b/cms/static/coffee/.gitignore
@@ -0,0 +1,2 @@
+*.js
+module
diff --git a/cms/static/coffee/main.coffee b/cms/static/coffee/main.coffee
new file mode 100644
index 0000000000..66f375a7cc
--- /dev/null
+++ b/cms/static/coffee/main.coffee
@@ -0,0 +1,83 @@
+class @CMS
+ @bind = =>
+ $('a.module-edit').click ->
+ CMS.edit_item($(this).attr('id'))
+ return false
+
+ @edit_item = (id) =>
+ $.get('/edit_item', {id: id}, (data) =>
+ $('#module-html').empty().append(data)
+ CMS.bind()
+ $('section.edit-pane').show()
+ $('body').addClass('content')
+ new Unit('unit-wrapper', id)
+ )
+
+$ ->
+ $.ajaxSetup
+ headers : { 'X-CSRFToken': $.cookie 'csrftoken' }
+ $('section.main-content').children().hide()
+ $('.editable').inlineEdit()
+ $('.editable-textarea').inlineEdit({control: 'textarea'})
+
+ heighest = 0
+ $('.cal ol > li').each ->
+ heighest = if $(this).height() > heighest then $(this).height() else heighest
+
+ $('.cal ol > li').css('height',heighest + 'px')
+
+ $('.add-new-section').click -> return false
+
+ $('.new-week .close').click ->
+ $(this).parents('.new-week').hide()
+ $('p.add-new-week').show()
+ return false
+
+ $('.save-update').click ->
+ $(this).parent().parent().hide()
+ return false
+
+ # $('html').keypress ->
+ # $('.wip').css('visibility', 'visible')
+
+ setHeight = ->
+ windowHeight = $(this).height()
+ contentHeight = windowHeight - 29
+
+ $('section.main-content > section').css('min-height', contentHeight)
+ $('body.content .cal').css('height', contentHeight)
+
+ $('.edit-week').click ->
+ $('body').addClass('content')
+ $('body.content .cal').css('height', contentHeight)
+ $('section.edit-pane').show()
+ return false
+
+ $('a.week-edit').click ->
+ $('body').addClass('content')
+ $('body.content .cal').css('height', contentHeight)
+ $('section.edit-pane').show()
+ return false
+
+ $('a.sequence-edit').click ->
+ $('body').addClass('content')
+ $('body.content .cal').css('height', contentHeight)
+ $('section.edit-pane').show()
+ return false
+
+ $('a.module-edit').click ->
+ $('body.content .cal').css('height', contentHeight)
+
+ $(document).ready(setHeight)
+ $(window).bind('resize', setHeight)
+
+ $('.video-new a').click ->
+ $('section.edit-pane').show()
+ return false
+
+ $('.problem-new a').click ->
+ $('section.edit-pane').show()
+ return false
+
+ CMS.bind()
+
diff --git a/cms/static/coffee/unit.coffee b/cms/static/coffee/unit.coffee
new file mode 100644
index 0000000000..b81bc0df08
--- /dev/null
+++ b/cms/static/coffee/unit.coffee
@@ -0,0 +1,15 @@
+class @Unit
+ constructor: (@element_id, @module_id) ->
+ @module = new window[$("##{@element_id}").attr('class')] 'module-html'
+
+ $("##{@element_id} .save-update").click (event) =>
+ event.preventDefault()
+ $.post("save_item", {
+ id: @module_id
+ data: JSON.stringify(@module.save())
+ })
+
+ $("##{@element_id} .cancel").click (event) =>
+ event.preventDefault()
+ CMS.edit_item(@module_id)
+
diff --git a/cms/static/js/jquery.cookie.js b/cms/static/js/jquery.cookie.js
new file mode 100644
index 0000000000..6d5974a2c5
--- /dev/null
+++ b/cms/static/js/jquery.cookie.js
@@ -0,0 +1,47 @@
+/*!
+ * jQuery Cookie Plugin
+ * https://github.com/carhartl/jquery-cookie
+ *
+ * Copyright 2011, Klaus Hartl
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://www.opensource.org/licenses/mit-license.php
+ * http://www.opensource.org/licenses/GPL-2.0
+ */
+(function($) {
+ $.cookie = function(key, value, options) {
+
+ // key and at least value given, set cookie...
+ if (arguments.length > 1 && (!/Object/.test(Object.prototype.toString.call(value)) || value === null || value === undefined)) {
+ options = $.extend({}, options);
+
+ if (value === null || value === undefined) {
+ options.expires = -1;
+ }
+
+ if (typeof options.expires === 'number') {
+ var days = options.expires, t = options.expires = new Date();
+ t.setDate(t.getDate() + days);
+ }
+
+ value = String(value);
+
+ return (document.cookie = [
+ encodeURIComponent(key), '=', options.raw ? value : encodeURIComponent(value),
+ options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
+ options.path ? '; path=' + options.path : '',
+ options.domain ? '; domain=' + options.domain : '',
+ options.secure ? '; secure' : ''
+ ].join(''));
+ }
+
+ // key and possibly options given, get cookie...
+ options = value || {};
+ var decode = options.raw ? function(s) { return s; } : decodeURIComponent;
+
+ var pairs = document.cookie.split('; ');
+ for (var i = 0, pair; pair = pairs[i] && pairs[i].split('='); i++) {
+ if (decode(pair[0]) === key) return decode(pair[1] || ''); // IE saves cookies with empty string as "c; ", e.g. without "=" as opposed to EOMB, thus pair[1] may be undefined
+ }
+ return null;
+ };
+})(jQuery);
diff --git a/cms/static/js/jquery.min.js b/cms/static/js/jquery.min.js
new file mode 100644
index 0000000000..16ad06c5ac
--- /dev/null
+++ b/cms/static/js/jquery.min.js
@@ -0,0 +1,4 @@
+/*! jQuery v1.7.2 jquery.com | jquery.org/license */
+(function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cu(a){if(!cj[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){ck||(ck=c.createElement("iframe"),ck.frameBorder=ck.width=ck.height=0),b.appendChild(ck);if(!cl||!ck.createElement)cl=(ck.contentWindow||ck.contentDocument).document,cl.write((f.support.boxModel?"":"")+"
"),cl.close();d=cl.createElement(a),cl.body.appendChild(d),e=f.css(d,"display"),b.removeChild(ck)}cj[a]=e}return cj[a]}function ct(a,b){var c={};f.each(cp.concat.apply([],cp.slice(0,b)),function(){c[this]=a});return c}function cs(){cq=b}function cr(){setTimeout(cs,0);return cq=f.now()}function ci(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ch(){try{return new a.XMLHttpRequest}catch(b){}}function cb(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g0){if(c!=="border")for(;e=0===c})}function S(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function K(){return!0}function J(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?+d:j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=/-([a-z]|[0-9])/ig,w=/^-ms-/,x=function(a,b){return(b+"").toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=m.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7.2",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.add(a);return this},eq:function(a){a=+a;return a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;A.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").off("ready")}},bindReady:function(){if(!A){A=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a!=null&&a==a.window},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||D.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw new Error(a)},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){if(typeof c!="string"||!c)return null;var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,"ms-").replace(v,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;ca ",d=p.getElementsByTagName("*"),e=p.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=p.getElementsByTagName("input")[0],b={leadingWhitespace:p.firstChild.nodeType===3,tbody:!p.getElementsByTagName("tbody").length,htmlSerialize:!!p.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:p.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,pixelMargin:!0},f.boxModel=b.boxModel=c.compatMode==="CSS1Compat",i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete p.test}catch(r){b.deleteExpando=!1}!p.addEventListener&&p.attachEvent&&p.fireEvent&&(p.attachEvent("onclick",function(){b.noCloneEvent=!1}),p.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),i.setAttribute("name","t"),p.appendChild(i),j=c.createDocumentFragment(),j.appendChild(p.lastChild),b.checkClone=j.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,j.removeChild(i),j.appendChild(p);if(p.attachEvent)for(n in{submit:1,change:1,focusin:1})m="on"+n,o=m in p,o||(p.setAttribute(m,"return;"),o=typeof p[m]=="function"),b[n+"Bubbles"]=o;j.removeChild(p),j=g=h=p=i=null,f(function(){var d,e,g,h,i,j,l,m,n,q,r,s,t,u=c.getElementsByTagName("body")[0];!u||(m=1,t="padding:0;margin:0;border:",r="position:absolute;top:0;left:0;width:1px;height:1px;",s=t+"0;visibility:hidden;",n="style='"+r+t+"5px solid #000;",q=""+"",d=c.createElement("div"),d.style.cssText=s+"width:0;height:0;position:static;top:0;margin-top:"+m+"px",u.insertBefore(d,u.firstChild),p=c.createElement("div"),d.appendChild(p),p.innerHTML="",k=p.getElementsByTagName("td"),o=k[0].offsetHeight===0,k[0].style.display="",k[1].style.display="none",b.reliableHiddenOffsets=o&&k[0].offsetHeight===0,a.getComputedStyle&&(p.innerHTML="",l=c.createElement("div"),l.style.width="0",l.style.marginRight="0",p.style.width="2px",p.appendChild(l),b.reliableMarginRight=(parseInt((a.getComputedStyle(l,null)||{marginRight:0}).marginRight,10)||0)===0),typeof p.style.zoom!="undefined"&&(p.innerHTML="",p.style.width=p.style.padding="1px",p.style.border=0,p.style.overflow="hidden",p.style.display="inline",p.style.zoom=1,b.inlineBlockNeedsLayout=p.offsetWidth===3,p.style.display="block",p.style.overflow="visible",p.innerHTML="
",b.shrinkWrapBlocks=p.offsetWidth!==3),p.style.cssText=r+s,p.innerHTML=q,e=p.firstChild,g=e.firstChild,i=e.nextSibling.firstChild.firstChild,j={doesNotAddBorder:g.offsetTop!==5,doesAddBorderForTableAndCells:i.offsetTop===5},g.style.position="fixed",g.style.top="20px",j.fixedPosition=g.offsetTop===20||g.offsetTop===15,g.style.position=g.style.top="",e.style.overflow="hidden",e.style.position="relative",j.subtractsBorderForOverflowNotVisible=g.offsetTop===-5,j.doesNotIncludeMarginInBodyOffset=u.offsetTop!==m,a.getComputedStyle&&(p.style.marginTop="1%",b.pixelMargin=(a.getComputedStyle(p,null)||{marginTop:0}).marginTop!=="1%"),typeof d.style.zoom!="undefined"&&(d.style.zoom=1),u.removeChild(d),l=p=d=null,f.extend(b,j))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e1,null,!1)},removeData:function(a){return this.each(function(){f.removeData(this,a)})}}),f.extend({_mark:function(a,b){a&&(b=(b||"fx")+"mark",f._data(a,b,(f._data(a,b)||0)+1))},_unmark:function(a,b,c){a!==!0&&(c=b,b=a,a=!1);if(b){c=c||"fx";var d=c+"mark",e=a?0:(f._data(b,d)||1)-1;e?f._data(b,d,e):(f.removeData(b,d,!0),n(b,c,"mark"))}},queue:function(a,b,c){var d;if(a){b=(b||"fx")+"queue",d=f._data(a,b),c&&(!d||f.isArray(c)?d=f._data(a,b,f.makeArray(c)):d.push(c));return d||[]}},dequeue:function(a,b){b=b||"fx";var c=f.queue(a,b),d=c.shift(),e={};d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),f._data(a,b+".run",e),d.call(a,function(){f.dequeue(a,b)},e)),c.length||(f.removeData(a,b+"queue "+b+".run",!0),n(a,b,"queue"))}}),f.fn.extend({queue:function(a,c){var d=2;typeof a!="string"&&(c=a,a="fx",d--);if(arguments.length1)},removeAttr:function(a){return this.each(function(){f.removeAttr(this,a)})},prop:function(a,b){return f.access(this,f.prop,a,b,arguments.length>1)},removeProp:function(a){a=f.propFix[a]||a;return this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,g,h,i;if(f.isFunction(a))return this.each(function(b){f(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(p);for(c=0,d=this.length;c-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.type]||f.valHooks[this.nodeName.toLowerCase()];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.type]||f.valHooks[g.nodeName.toLowerCase()];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h,i=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;i=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/(?:^|\s)hover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function(
+a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")};f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler,g=p.selector),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;le&&j.push({elem:this,matches:d.slice(e)});for(k=0;k0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));o.match.globalPOS=p;var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c ",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML=" ",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="
";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h0)for(h=g;h=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/ ]","i"),bd=/checked\s*(?:[^=]|=\s*.checked.)/i,be=/\/(java|ecma)script/i,bf=/^\s*",""],legend:[1,""," "],thead:[1,""],tr:[2,""],td:[3,""],col:[2,""],area:[1,""," "],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div","
"]),f.fn.extend({text:function(a){return f.access(this,function(a){return a===b?f.text(this):this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a))},null,a,arguments.length)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f
+.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){return f.access(this,function(a){var c=this[0]||{},d=0,e=this.length;if(a===b)return c.nodeType===1?c.innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1>$2>");try{for(;d1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||f.isXMLDoc(a)||!bc.test("<"+a.nodeName+">")?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g,h,i,j=[];b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);for(var k=0,l;(l=a[k])!=null;k++){typeof l=="number"&&(l+="");if(!l)continue;if(typeof l=="string")if(!_.test(l))l=b.createTextNode(l);else{l=l.replace(Y,"<$1>$2>");var m=(Z.exec(l)||["",""])[1].toLowerCase(),n=bg[m]||bg._default,o=n[0],p=b.createElement("div"),q=bh.childNodes,r;b===c?bh.appendChild(p):U(b).appendChild(p),p.innerHTML=n[1]+l+n[2];while(o--)p=p.lastChild;if(!f.support.tbody){var s=$.test(l),t=m==="table"&&!s?p.firstChild&&p.firstChild.childNodes:n[1]===""&&!s?p.childNodes:[];for(i=t.length-1;i>=0;--i)f.nodeName(t[i],"tbody")&&!t[i].childNodes.length&&t[i].parentNode.removeChild(t[i])}!f.support.leadingWhitespace&&X.test(l)&&p.insertBefore(b.createTextNode(X.exec(l)[0]),p.firstChild),l=p.childNodes,p&&(p.parentNode.removeChild(p),q.length>0&&(r=q[q.length-1],r&&r.parentNode&&r.parentNode.removeChild(r)))}var u;if(!f.support.appendChecked)if(l[0]&&typeof (u=l.length)=="number")for(i=0;i1)},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=by(a,"opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d,h==="string"&&(g=bu.exec(d))&&(d=+(g[1]+1)*+g[2]+parseFloat(f.css(a,c)),h="number");if(d==null||h==="number"&&isNaN(d))return;h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(by)return by(a,c)},swap:function(a,b,c){var d={},e,f;for(f in b)d[f]=a.style[f],a.style[f]=b[f];e=c.call(a);for(f in b)a.style[f]=d[f];return e}}),f.curCSS=f.css,c.defaultView&&c.defaultView.getComputedStyle&&(bz=function(a,b){var c,d,e,g,h=a.style;b=b.replace(br,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b))),!f.support.pixelMargin&&e&&bv.test(b)&&bt.test(c)&&(g=h.width,h.width=c,c=e.width,h.width=g);return c}),c.documentElement.currentStyle&&(bA=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f==null&&g&&(e=g[b])&&(f=e),bt.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),by=bz||bA,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth!==0?bB(a,b,d):f.swap(a,bw,function(){return bB(a,b,d)})},set:function(a,b){return bs.test(b)?b+"px":b}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bq.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bp,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bp.test(g)?g.replace(bp,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){return f.swap(a,{display:"inline-block"},function(){return b?by(a,"margin-right"):a.style.marginRight})}})}),f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)}),f.each({margin:"",padding:"",border:"Width"},function(a,b){f.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bx[d]+b]=e[d]||e[d-2]||e[0];return f}}});var bC=/%20/g,bD=/\[\]$/,bE=/\r?\n/g,bF=/#.*$/,bG=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bH=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bI=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bJ=/^(?:GET|HEAD)$/,bK=/^\/\//,bL=/\?/,bM=/
+
+ % if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
+ <%static:js group='main'/>
+ % else:
+ % endif
+
+ <%static:js group='module-js'/>
+
diff --git a/cms/templates/index.html b/cms/templates/index.html
index 11c226ae3d..6bc04dc8a2 100644
--- a/cms/templates/index.html
+++ b/cms/templates/index.html
@@ -7,13 +7,9 @@
<%include file="widgets/navigation.html"/>
- <%include file="widgets/week-edit.html"/>
- <%include file="widgets/week-new.html"/>
- <%include file="widgets/sequnce-edit.html"/>
- <%include file="widgets/video-edit.html"/>
- <%include file="widgets/video-new.html"/>
- <%include file="widgets/problem-edit.html"/>
- <%include file="widgets/problem-new.html"/>
+
diff --git a/cms/templates/unit.html b/cms/templates/unit.html
new file mode 100644
index 0000000000..8cc75cd3bf
--- /dev/null
+++ b/cms/templates/unit.html
@@ -0,0 +1,17 @@
+
diff --git a/cms/templates/widgets/captions.html b/cms/templates/widgets/captions.html
deleted file mode 100644
index 088beb7a33..0000000000
--- a/cms/templates/widgets/captions.html
+++ /dev/null
@@ -1,242 +0,0 @@
-
- English (main)
- French
- English v2
- +
-
-
-
diff --git a/cms/templates/widgets/header.html b/cms/templates/widgets/header.html
index 49965e4026..4577ac64d8 100644
--- a/cms/templates/widgets/header.html
+++ b/cms/templates/widgets/header.html
@@ -5,9 +5,6 @@
New Section
-
- New Module
-
New Unit
diff --git a/cms/templates/widgets/html-edit.html b/cms/templates/widgets/html-edit.html
new file mode 100644
index 0000000000..7eec86215a
--- /dev/null
+++ b/cms/templates/widgets/html-edit.html
@@ -0,0 +1,45 @@
+
+
+
+
+ ${module.definition['data']['text']}
+
+
+
+ <%include file="notes.html"/>
+
diff --git a/cms/templates/widgets/module-dropdown.html b/cms/templates/widgets/module-dropdown.html
index 7c6e1e068c..6edb142e40 100644
--- a/cms/templates/widgets/module-dropdown.html
+++ b/cms/templates/widgets/module-dropdown.html
@@ -1,4 +1,4 @@
-
+
+ Add new module
diff --git a/cms/templates/widgets/navigation.html b/cms/templates/widgets/navigation.html
index 2d5af9ead1..cbdc7660ce 100644
--- a/cms/templates/widgets/navigation.html
+++ b/cms/templates/widgets/navigation.html
@@ -1,5 +1,5 @@
-
+
Filter content:
@@ -38,7 +38,7 @@
% for week in weeks:
-
+
% if week.goals:
% for goal in week.goals:
@@ -53,7 +53,7 @@
% for module in week.get_children():
- ${module.name}
+ ${module.name}
handle
% endfor
diff --git a/cms/templates/widgets/notes.html b/cms/templates/widgets/notes.html
new file mode 100644
index 0000000000..920e88cbcd
--- /dev/null
+++ b/cms/templates/widgets/notes.html
@@ -0,0 +1,21 @@
+
+ Notes
+
+
+
+ Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.
+ Anant Agarwal
+
+
+ Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.
+ Anant Agarwal
+
+
+
+
+
+
diff --git a/cms/templates/widgets/problem-edit.html b/cms/templates/widgets/problem-edit.html
index 49f67e3e26..a2da078927 100644
--- a/cms/templates/widgets/problem-edit.html
+++ b/cms/templates/widgets/problem-edit.html
@@ -1,73 +1,48 @@
-
-
-
-
- New Problem
-
-
-
Last modified:
-
mm/dd/yy
-
-
-
-
-
-
-
-
-
-
- Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.
-
- Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.
-
+
+
+
+ Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.
+
+ Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.
+
+
+
+
+
+ <%include file="notes.html"/>
diff --git a/cms/templates/widgets/raw-videos.html b/cms/templates/widgets/raw-videos.html
deleted file mode 100644
index f466fd59bc..0000000000
--- a/cms/templates/widgets/raw-videos.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
- Video-file-name
-
diff --git a/cms/templates/widgets/save-captions.html b/cms/templates/widgets/save-captions.html
deleted file mode 100644
index 87342f0cd0..0000000000
--- a/cms/templates/widgets/save-captions.html
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/cms/templates/widgets/sequence-edit.html b/cms/templates/widgets/sequence-edit.html
new file mode 100644
index 0000000000..abeec9209d
--- /dev/null
+++ b/cms/templates/widgets/sequence-edit.html
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+ <%include file="new-module.html"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/cms/templates/widgets/sequnce-edit.html b/cms/templates/widgets/sequnce-edit.html
deleted file mode 100644
index b69b523bc4..0000000000
--- a/cms/templates/widgets/sequnce-edit.html
+++ /dev/null
@@ -1,187 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/cms/templates/widgets/speed-tooltip.html b/cms/templates/widgets/speed-tooltip.html
deleted file mode 100644
index 2a82e237e7..0000000000
--- a/cms/templates/widgets/speed-tooltip.html
+++ /dev/null
@@ -1,7 +0,0 @@
-
diff --git a/cms/templates/widgets/video-box-unused.html b/cms/templates/widgets/video-box-unused.html
deleted file mode 100644
index 3d643ff3c9..0000000000
--- a/cms/templates/widgets/video-box-unused.html
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-
-
-
- <%include file="captions.html"/>
- <%include file="save-captions.html"/>
-
-
-
diff --git a/cms/templates/widgets/video-box.html b/cms/templates/widgets/video-box.html
deleted file mode 100644
index 1f17e33511..0000000000
--- a/cms/templates/widgets/video-box.html
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
-
- <%include file="captions.html"/>
-
-
diff --git a/cms/urls.py b/cms/urls.py
index d2e6415827..d7314aafae 100644
--- a/cms/urls.py
+++ b/cms/urls.py
@@ -6,4 +6,6 @@ from django.conf.urls.defaults import patterns, url
urlpatterns = patterns('',
url(r'^$', 'contentstore.views.index', name='index'),
+ url(r'^edit_item$', 'contentstore.views.edit_item', name='edit_item'),
+ url(r'^save_item$', 'contentstore.views.save_item', name='save_item'),
)
diff --git a/common/lib/mitxmako/shortcuts.py b/common/lib/mitxmako/shortcuts.py
index 9f6044b81e..72a9b81e3c 100644
--- a/common/lib/mitxmako/shortcuts.py
+++ b/common/lib/mitxmako/shortcuts.py
@@ -12,13 +12,16 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import logging
+
+log = logging.getLogger("mitx.common.lib.mitxmako")
+
from django.template import Context
from django.http import HttpResponse
from . import middleware
from django.conf import settings
-
def render_to_string(template_name, dictionary, context=None, namespace='main'):
context_instance = Context(dictionary)
# add dictionary to context_instance
@@ -27,8 +30,11 @@ def render_to_string(template_name, dictionary, context=None, namespace='main'):
context_dictionary = {}
context_instance['settings'] = settings
context_instance['MITX_ROOT_URL'] = settings.MITX_ROOT_URL
- for d in middleware.requestcontext:
- context_dictionary.update(d)
+
+ # In various testing contexts, there might not be a current request context.
+ if middleware.requestcontext is not None:
+ for d in middleware.requestcontext:
+ context_dictionary.update(d)
for d in context_instance:
context_dictionary.update(d)
if context:
diff --git a/common/lib/xmodule/capa_module.py b/common/lib/xmodule/capa_module.py
index c12446e744..7698c0937e 100644
--- a/common/lib/xmodule/capa_module.py
+++ b/common/lib/xmodule/capa_module.py
@@ -10,7 +10,8 @@ import StringIO
from datetime import timedelta
from lxml import etree
-from x_module import XModule, XModuleDescriptor
+from x_module import XModule
+from mako_module import MakoModuleDescriptor
from progress import Progress
from capa.capa_problem import LoncapaProblem
from capa.responsetypes import StudentInputError
@@ -63,8 +64,14 @@ class ComplexEncoder(json.JSONEncoder):
return json.JSONEncoder.default(self, obj)
-class ModuleDescriptor(XModuleDescriptor):
- pass
+class CapaModuleDescriptor(MakoModuleDescriptor):
+ """
+ Module implementing problems in the LON-CAPA format,
+ as implemented by capa.capa_problem
+ """
+
+ mako_template = 'widgets/problem-edit.html'
+
class Module(XModule):
diff --git a/common/lib/xmodule/html_module.py b/common/lib/xmodule/html_module.py
index 8d9d0b17d6..977b5ef606 100644
--- a/common/lib/xmodule/html_module.py
+++ b/common/lib/xmodule/html_module.py
@@ -1,14 +1,24 @@
import json
import logging
-from x_module import XModule, XModuleDescriptor
+from x_module import XModule
+from mako_module import MakoModuleDescriptor
from lxml import etree
+from pkg_resources import resource_string
log = logging.getLogger("mitx.courseware")
+
#-----------------------------------------------------------------------------
-class ModuleDescriptor(XModuleDescriptor):
- pass
+class HtmlModuleDescriptor(MakoModuleDescriptor):
+ """
+ Module for putting raw html in a course
+ """
+ mako_template = "widgets/html-edit.html"
+
+ # TODO (cpennington): Make this into a proper module
+ js = {'coffee': [resource_string(__name__, 'js/module/html.coffee')]}
+
class Module(XModule):
id_attribute = 'filename'
diff --git a/common/lib/xmodule/js/module/html.coffee b/common/lib/xmodule/js/module/html.coffee
new file mode 100644
index 0000000000..5e072c27a3
--- /dev/null
+++ b/common/lib/xmodule/js/module/html.coffee
@@ -0,0 +1,9 @@
+class @HTML
+ constructor: (@id) ->
+ @edit_box = $("##{@id} .edit-box")
+ @preview = $("##{@id} .preview")
+ @edit_box.on('input', =>
+ @preview.empty().append(@edit_box.val())
+ )
+
+ save: -> {text: @edit_box.val()}
diff --git a/common/lib/xmodule/mako_module.py b/common/lib/xmodule/mako_module.py
new file mode 100644
index 0000000000..7887e13c12
--- /dev/null
+++ b/common/lib/xmodule/mako_module.py
@@ -0,0 +1,18 @@
+from x_module import XModuleDescriptor
+from mitxmako.shortcuts import render_to_string
+
+
+class MakoModuleDescriptor(XModuleDescriptor):
+ """
+ Module descriptor intended as a mixin that uses a mako template
+ to specify the module html.
+
+ Expects the descriptor to have the `mako_template` attribute set
+ with the name of the template to render, and it will pass
+ the descriptor as the `module` parameter to that template
+ """
+
+ def get_html(self):
+ return render_to_string(self.mako_template, {
+ 'module': self
+ })
diff --git a/common/lib/xmodule/seq_module.py b/common/lib/xmodule/seq_module.py
index d3877c3c41..f9c5a205ea 100644
--- a/common/lib/xmodule/seq_module.py
+++ b/common/lib/xmodule/seq_module.py
@@ -3,7 +3,8 @@ import logging
from lxml import etree
-from x_module import XModule, XModuleDescriptor
+from x_module import XModule
+from mako_module import MakoModuleDescriptor
from xmodule.progress import Progress
log = logging.getLogger("mitx.common.lib.seq_module")
@@ -12,9 +13,6 @@ log = logging.getLogger("mitx.common.lib.seq_module")
# OBSOLETE: This obsoletes 'type'
class_priority = ['video', 'problem']
-class ModuleDescriptor(XModuleDescriptor):
- pass
-
class Module(XModule):
''' Layout module which lays out content in a temporal sequence
'''
@@ -118,5 +116,5 @@ class Module(XModule):
self.rendered = False
-class SectionDescriptor(XModuleDescriptor):
- pass
+class SectionDescriptor(MakoModuleDescriptor):
+ mako_template = 'widgets/sequence-edit.html'
diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py
index 7f3370ed37..7b67029f34 100644
--- a/common/lib/xmodule/setup.py
+++ b/common/lib/xmodule/setup.py
@@ -5,6 +5,9 @@ setup(
version="0.1",
packages=find_packages(),
install_requires=['distribute'],
+ package_data={
+ '': ['js/*']
+ },
# See http://guide.python-distribute.org/creation.html#entry-points
# for a description of entry_points
@@ -19,6 +22,9 @@ setup(
"TutorialIndex = seq_module:SectionDescriptor",
"Exam = seq_module:SectionDescriptor",
"VideoSegment = video_module:VideoSegmentDescriptor",
+ "ProblemSet = seq_module:SectionDescriptor",
+ "Problem = capa_module:CapaModuleDescriptor",
+ "HTML = html_module:HtmlModuleDescriptor",
]
}
)
diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py
index 043a830500..89df6b96b2 100644
--- a/common/lib/xmodule/x_module.py
+++ b/common/lib/xmodule/x_module.py
@@ -3,7 +3,6 @@ import pkg_resources
import logging
from keystore import Location
-from progress import Progress
log = logging.getLogger('mitx.' + __name__)
@@ -30,6 +29,12 @@ class Plugin(object):
return classes[0].load()
+ @classmethod
+ def load_classes(cls):
+ return [class_.load()
+ for class_
+ in pkg_resources.iter_entry_points(cls.entry_point)]
+
class XModule(object):
''' Implements a generic learning module.
@@ -154,6 +159,7 @@ class XModuleDescriptor(Plugin):
and can generate XModules (which do know about student state).
"""
entry_point = "xmodule.v1"
+ js = {}
@staticmethod
def load_from_json(json_data, system):
@@ -178,6 +184,19 @@ class XModuleDescriptor(Plugin):
"""
return cls(system=system, **json_data)
+ @classmethod
+ def get_javascript(cls):
+ """
+ Return a dictionary containing some of the following keys:
+ coffee: A list of coffeescript fragments that should be compiled and
+ placed on the page
+ js: A list of javascript fragments that should be included on the page
+
+ All of these will be loaded onto the page in the CMS
+ """
+ return cls.js
+
+
def __init__(self,
system,
definition=None,
@@ -202,6 +221,7 @@ class XModuleDescriptor(Plugin):
self.definition = definition if definition is not None else {}
self.name = Location(kwargs.get('location')).name
self.type = Location(kwargs.get('location')).category
+ self.url = Location(kwargs.get('location')).url()
# For now, we represent goals as a list of strings, but this
# is one of the things that we are going to be iterating on heavily
@@ -220,21 +240,27 @@ class XModuleDescriptor(Plugin):
else:
return [child for child in self._child_instances if child.type in categories]
+ def get_html(self):
+ """
+ Return the html used to edit this module
+ """
+ raise NotImplementedError("get_html() must be provided by specific modules")
+
def get_xml(self):
''' For conversions between JSON and legacy XML representations.
'''
- if self.xml:
+ if self.xml:
return self.xml
- else:
+ else:
raise NotImplementedError("JSON->XML Translation not implemented")
def get_json(self):
''' For conversions between JSON and legacy XML representations.
'''
- if self.json:
+ if self.json:
raise NotImplementedError
- return self.json # TODO: Return context as well -- files, etc.
- else:
+ return self.json # TODO: Return context as well -- files, etc.
+ else:
raise NotImplementedError("XML->JSON Translation not implemented")
#def handle_cms_json(self):
diff --git a/common/test/README.md b/common/test/README.md
new file mode 100644
index 0000000000..07c49f7d92
--- /dev/null
+++ b/common/test/README.md
@@ -0,0 +1,6 @@
+Common test infrastructure for LMS + CMS
+===========================
+
+data/ has some test course data.
+
+Once the course validation is separated from django, we should have scripts here that checks that a course consists only of xml that we understand.
diff --git a/common/test/data/full/README.md b/common/test/data/full/README.md
new file mode 100644
index 0000000000..812ca471ce
--- /dev/null
+++ b/common/test/data/full/README.md
@@ -0,0 +1 @@
+This is a realistic course, with many different module types and a lot of structure. It is based on 6.002x.
diff --git a/common/test/data/simple/README.md b/common/test/data/simple/README.md
new file mode 100644
index 0000000000..69ff6b4ed0
--- /dev/null
+++ b/common/test/data/simple/README.md
@@ -0,0 +1,2 @@
+This is a simple, but non-trivial, course using multiple module types and some nested structure.
+
diff --git a/common/test/data/toy/README.md b/common/test/data/toy/README.md
new file mode 100644
index 0000000000..59ab392ed3
--- /dev/null
+++ b/common/test/data/toy/README.md
@@ -0,0 +1 @@
+This is a very very simple course, useful for initial debugging of processing code.
diff --git a/common/test/data/toy/toy_course.xml b/common/test/data/toy/toy_course.xml
new file mode 100644
index 0000000000..ecac9a4776
--- /dev/null
+++ b/common/test/data/toy/toy_course.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
diff --git a/lms/djangoapps/courseware/content_parser.py b/lms/djangoapps/courseware/content_parser.py
index 4f96db5284..62163c86b0 100644
--- a/lms/djangoapps/courseware/content_parser.py
+++ b/lms/djangoapps/courseware/content_parser.py
@@ -26,10 +26,13 @@ import xmodule
''' This file will eventually form an abstraction layer between the
course XML file and the rest of the system.
-
-TODO: Shift everything from xml.dom.minidom to XPath (or XQuery)
'''
+# ==== This section has no direct dependencies on django ====================================
+# NOTE: it does still have some indirect dependencies:
+# util.memcache.fasthash (which does not depend on memcache at all)
+#
+
class ContentException(Exception):
pass
@@ -38,29 +41,6 @@ log = logging.getLogger("mitx.courseware")
def format_url_params(params):
return [ urllib.quote(string.replace(' ','_')) for string in params ]
-def xpath(xml, query_string, **args):
- ''' Safe xpath query into an xml tree:
- * xml is the tree.
- * query_string is the query
- * args are the parameters. Substitute for {params}.
- We should remove this with the move to lxml.
- We should also use lxml argument passing. '''
- doc = etree.fromstring(xml)
- #print type(doc)
- def escape(x):
- # TODO: This should escape the string. For now, we just assume it's made of valid characters.
- # Couldn't figure out how to escape for lxml in a few quick Googles
- valid_chars="".join(map(chr, range(ord('a'),ord('z')+1)+range(ord('A'),ord('Z')+1)+range(ord('0'), ord('9')+1)))+"_ "
- for e in x:
- if e not in valid_chars:
- raise Exception("Invalid char in xpath expression. TODO: Escape")
- return x
-
- args=dict( ((k, escape(args[k])) for k in args) )
- #print args
- results = doc.xpath(query_string.format(**args))
- return results
-
def xpath_remove(tree, path):
''' Remove all items matching path from lxml tree. Works in
place.'''
@@ -69,35 +49,34 @@ def xpath_remove(tree, path):
item.getparent().remove(item)
return tree
-if __name__=='__main__':
- print xpath(' ', '/{search}/problem[@name="{name}"]',
- search='html', name="Bob")
-
def id_tag(course):
''' Tag all course elements with unique IDs '''
default_ids = xmodule.get_default_ids()
# Tag elements with unique IDs
- elements = course.xpath("|".join(['//'+c for c in default_ids]))
+ elements = course.xpath("|".join('//' + c for c in default_ids))
for elem in elements:
if elem.get('id'):
pass
elif elem.get(default_ids[elem.tag]):
- new_id = elem.get(default_ids[elem.tag])
- new_id = "".join([a for a in new_id if a.isalnum()]) # Convert to alphanumeric
- # Without this, a conflict may occur between an hmtl or youtube id
+ new_id = elem.get(default_ids[elem.tag])
+ # Convert to alphanumeric
+ new_id = "".join(a for a in new_id if a.isalnum())
+
+ # Without this, a conflict may occur between an html or youtube id
new_id = default_ids[elem.tag] + new_id
elem.set('id', new_id)
else:
- elem.set('id', "id"+fasthash(etree.tostring(elem)))
+ elem.set('id', "id" + fasthash(etree.tostring(elem)))
def propogate_downward_tag(element, attribute_name, parent_attribute = None):
''' This call is to pass down an attribute to all children. If an element
has this attribute, it will be "inherited" by all of its children. If a
child (A) already has that attribute, A will keep the same attribute and
all of A's children will inherit A's attribute. This is a recursive call.'''
-
- if (parent_attribute is None): #This is the entry call. Select all elements with this attribute
+
+ if (parent_attribute is None):
+ #This is the entry call. Select all elements with this attribute
all_attributed_elements = element.xpath("//*[@" + attribute_name +"]")
for attributed_element in all_attributed_elements:
attribute_value = attributed_element.get(attribute_name)
@@ -118,6 +97,164 @@ def propogate_downward_tag(element, attribute_name, parent_attribute = None):
#to its children later.
return
+
+def course_xml_process(tree):
+ ''' Do basic pre-processing of an XML tree. Assign IDs to all
+ items without. Propagate due dates, grace periods, etc. to child
+ items.
+ '''
+ replace_custom_tags(tree)
+ id_tag(tree)
+ propogate_downward_tag(tree, "due")
+ propogate_downward_tag(tree, "graded")
+ propogate_downward_tag(tree, "graceperiod")
+ propogate_downward_tag(tree, "showanswer")
+ propogate_downward_tag(tree, "rerandomize")
+ return tree
+
+
+def toc_from_xml(dom, active_chapter, active_section):
+ '''
+ Create a table of contents from the course xml.
+
+ Return format:
+ [ {'name': name, 'sections': SECTIONS, 'active': bool}, ... ]
+
+ where SECTIONS is a list
+ [ {'name': name, 'format': format, 'due': due, 'active' : bool}, ...]
+
+ active is set for the section and chapter corresponding to the passed
+ parameters. Everything else comes from the xml, or defaults to "".
+
+ chapters with name 'hidden' are skipped.
+ '''
+ name = dom.xpath('//course/@name')[0]
+
+ chapters = dom.xpath('//course[@name=$name]/chapter', name=name)
+ ch = list()
+ for c in chapters:
+ if c.get('name') == 'hidden':
+ continue
+ sections = list()
+ for s in dom.xpath('//course[@name=$name]/chapter[@name=$chname]/section',
+ name=name, chname=c.get('name')):
+
+ format = s.get("subtitle") if s.get("subtitle") else s.get("format") or ""
+ active = (c.get("name") == active_chapter and
+ s.get("name") == active_section)
+
+ sections.append({'name': s.get("name") or "",
+ 'format': format,
+ 'due': s.get("due") or "",
+ 'active': active})
+
+ ch.append({'name': c.get("name"),
+ 'sections': sections,
+ 'active': c.get("name") == active_chapter})
+ return ch
+
+
+def replace_custom_tags_dir(tree, dir):
+ '''
+ Process tree to replace all custom tags defined in dir.
+ '''
+ tags = os.listdir(dir)
+ for tag in tags:
+ for element in tree.iter(tag):
+ element.tag = 'customtag'
+ impl = etree.SubElement(element, 'impl')
+ impl.text = tag
+
+def parse_course_file(filename, options, namespace):
+ '''
+ Parse a course file with the given options, and return the resulting
+ xml tree object.
+
+ Options should be a dictionary including keys
+ 'dev_content': bool,
+ 'groups' : [list, of, user, groups]
+
+ namespace is used to in searching for the file. Could be e.g. 'course',
+ 'sections'.
+ '''
+ xml = etree.XML(render_to_string(filename, options, namespace=namespace))
+ return course_xml_process(xml)
+
+
+def get_section(section, options, dirname):
+ '''
+ Given the name of a section, an options dict containing keys
+ 'dev_content' and 'groups', and a directory to look in,
+ returns the xml tree for the section, or None if there's no
+ such section.
+ '''
+ filename = section + ".xml"
+
+ if filename not in os.listdir(dirname):
+ log.error(filename + " not in " + str(os.listdir(dirname)))
+ return None
+
+ tree = parse_course_file(filename, options, namespace='sections')
+ return tree
+
+
+def get_module(tree, module, id_tag, module_id, sections_dirname, options):
+ '''
+ Given the xml tree of the course, get the xml string for a module
+ with the specified module type, id_tag, module_id. Looks in
+ sections_dirname for sections.
+
+ id_tag -- use id_tag if the place the module stores its id is not 'id'
+ '''
+ # Sanitize input
+ if not module.isalnum():
+ raise Exception("Module is not alphanumeric")
+
+ if not module_id.isalnum():
+ raise Exception("Module ID is not alphanumeric")
+
+ # Generate search
+ xpath_search='//{module}[(@{id_tag} = "{id}") or (@id = "{id}")]'.format(
+ module=module,
+ id_tag=id_tag,
+ id=module_id)
+
+
+ result_set = tree.xpath(xpath_search)
+ if len(result_set) < 1:
+ # Not found in main tree. Let's look in the section files.
+ section_list = (s[:-4] for s in os.listdir(sections_dirname) if s[-4:]=='.xml')
+ for section in section_list:
+ try:
+ s = get_section(section, options, sections_dirname)
+ except etree.XMLSyntaxError:
+ ex = sys.exc_info()
+ raise ContentException("Malformed XML in " + section +
+ "(" + str(ex[1].msg) + ")")
+ result_set = s.xpath(xpath_search)
+ if len(result_set) != 0:
+ break
+
+ if len(result_set) > 1:
+ log.error("WARNING: Potentially malformed course file", module, module_id)
+
+ if len(result_set)==0:
+ log.error('[content_parser.get_module] cannot find %s in course.xml tree',
+ xpath_search)
+ log.error('tree = %s' % etree.tostring(tree, pretty_print=True))
+ return None
+
+ # log.debug('[courseware.content_parser.module_xml] found %s' % result_set)
+
+ return etree.tostring(result_set[0])
+
+
+
+
+
+
+# ==== All Django-specific code below =============================================
+
def user_groups(user):
if not user.is_authenticated():
return []
@@ -135,154 +272,94 @@ def user_groups(user):
return group_names
- # return [u.name for u in UserTestGroup.objects.raw("select * from auth_user, student_usertestgroup, student_usertestgroup_users where auth_user.id = student_usertestgroup_users.user_id and student_usertestgroup_users.usertestgroup_id = student_usertestgroup.id and auth_user.id = %s", [user.id])]
+
+def get_options(user):
+ return {'dev_content': settings.DEV_CONTENT,
+ 'groups': user_groups(user)}
+
def replace_custom_tags(tree):
- tags = os.listdir(settings.DATA_DIR+'/custom_tags')
- for tag in tags:
- for element in tree.iter(tag):
- element.tag = 'customtag'
- impl = etree.SubElement(element, 'impl')
- impl.text = tag
+ '''Replace custom tags defined in our custom_tags dir'''
+ replace_custom_tags_dir(tree, settings.DATA_DIR+'/custom_tags')
-def course_xml_process(tree):
- ''' Do basic pre-processing of an XML tree. Assign IDs to all
- items without. Propagate due dates, grace periods, etc. to child
- items.
+
+def course_file(user, coursename=None):
+ ''' Given a user, return an xml tree object for the course file.
+
+ Handles getting the right file, and processing it depending on the
+ groups the user is in. Does caching of the xml strings.
'''
- replace_custom_tags(tree)
- id_tag(tree)
- propogate_downward_tag(tree, "due")
- propogate_downward_tag(tree, "graded")
- propogate_downward_tag(tree, "graceperiod")
- propogate_downward_tag(tree, "showanswer")
- propogate_downward_tag(tree, "rerandomize")
- return tree
-
-def course_file(user,coursename=None):
- ''' Given a user, return course.xml'''
if user.is_authenticated():
- filename = UserProfile.objects.get(user=user).courseware # user.profile_cache.courseware
+ # use user.profile_cache.courseware?
+ filename = UserProfile.objects.get(user=user).courseware
else:
filename = 'guest_course.xml'
- # if a specific course is specified, then use multicourse to get the right path to the course XML directory
+ # if a specific course is specified, then use multicourse to get
+ # the right path to the course XML directory
if coursename and settings.ENABLE_MULTICOURSE:
xp = multicourse_settings.get_course_xmlpath(coursename)
filename = xp + filename # prefix the filename with the path
groups = user_groups(user)
- options = {'dev_content':settings.DEV_CONTENT,
- 'groups' : groups}
+ options = get_options(user)
+ # Try the cache...
+ cache_key = "{0}_processed?dev_content:{1}&groups:{2}".format(
+ filename,
+ options['dev_content'],
+ sorted(groups))
- cache_key = filename + "_processed?dev_content:" + str(options['dev_content']) + "&groups:" + str(sorted(groups))
- if "dev" not in settings.DEFAULT_GROUPS:
- tree_string = cache.get(cache_key)
- else:
+ if "dev" in settings.DEFAULT_GROUPS:
tree_string = None
+ else:
+ tree_string = cache.get(cache_key)
- if settings.DEBUG:
- log.info('[courseware.content_parser.course_file] filename=%s, cache_key=%s' % (filename,cache_key))
- # print '[courseware.content_parser.course_file] tree_string = ',tree_string
-
- if not tree_string:
- tree = course_xml_process(etree.XML(render_to_string(filename, options, namespace = 'course')))
- tree_string = etree.tostring(tree)
-
- cache.set(cache_key, tree_string, 60)
- else:
+ if tree_string:
tree = etree.XML(tree_string)
+ else:
+ tree = parse_course_file(filename, options, namespace='course')
+ # Cache it
+ tree_string = etree.tostring(tree)
+ cache.set(cache_key, tree_string, 60)
return tree
-def section_file(user, section, coursename=None, dironly=False):
+
+def sections_dir(coursename=None):
+ ''' Get directory where sections information is stored.
+ '''
+ # if a specific course is specified, then use multicourse to get the
+ # right path to the course XML directory
+ xp = ''
+ if coursename and settings.ENABLE_MULTICOURSE:
+ xp = multicourse_settings.get_course_xmlpath(coursename)
+
+ return settings.DATA_DIR + xp + '/sections/'
+
+
+
+def section_file(user, section, coursename=None):
'''
Given a user and the name of a section, return that section.
This is done specific to each course.
- If dironly=True then return the sections directory.
- TODO: This is a bit weird; dironly should be scrapped.
+
+ Returns the xml tree for the section, or None if there's no such section.
'''
- filename = section+".xml"
+ dirname = sections_dir(coursename)
- # if a specific course is specified, then use multicourse to get the right path to the course XML directory
- xp = ''
- if coursename and settings.ENABLE_MULTICOURSE: xp = multicourse_settings.get_course_xmlpath(coursename)
- dirname = settings.DATA_DIR + xp + '/sections/'
-
- if dironly: return dirname
-
- if filename not in os.listdir(dirname):
- log.error(filename+" not in "+str(os.listdir(dirname)))
- return None
-
- options = {'dev_content':settings.DEV_CONTENT,
- 'groups' : user_groups(user)}
-
- tree = course_xml_process(etree.XML(render_to_string(filename, options, namespace = 'sections')))
- return tree
+ return get_section(section, options, dirname)
def module_xml(user, module, id_tag, module_id, coursename=None):
''' Get XML for a module based on module and module_id. Assumes
- module occurs once in courseware XML file or hidden section. '''
- # Sanitize input
- if not module.isalnum():
- raise Exception("Module is not alphanumeric")
- if not module_id.isalnum():
- raise Exception("Module ID is not alphanumeric")
- # Generate search
- xpath_search='//{module}[(@{id_tag} = "{id}") or (@id = "{id}")]'.format(module=module,
- id_tag=id_tag,
- id=module_id)
- #result_set=doc.xpathEval(xpath_search)
- doc = course_file(user,coursename)
- sdirname = section_file(user,'',coursename,True) # get directory where sections information is stored
- section_list = (s[:-4] for s in os.listdir(sdirname) if s[-4:]=='.xml')
+ module occurs once in courseware XML file or hidden section.
+ '''
+ tree = course_file(user, coursename)
+ sdirname = sections_dir(coursename)
+ options = get_options(user)
- result_set=doc.xpath(xpath_search)
- if len(result_set)<1:
- for section in section_list:
- try:
- s = section_file(user, section, coursename)
- except etree.XMLSyntaxError:
- ex= sys.exc_info()
- raise ContentException("Malformed XML in " + section+ "("+str(ex[1].msg)+")")
- result_set = s.xpath(xpath_search)
- if len(result_set) != 0:
- break
-
- if len(result_set)>1:
- log.error("WARNING: Potentially malformed course file", module, module_id)
- if len(result_set)==0:
- if settings.DEBUG:
- log.error('[courseware.content_parser.module_xml] cannot find %s in course.xml tree' % xpath_search)
- log.error('tree = %s' % etree.tostring(doc,pretty_print=True))
- return None
- if settings.DEBUG:
- log.info('[courseware.content_parser.module_xml] found %s' % result_set)
- return etree.tostring(result_set[0])
- #return result_set[0].serialize()
-
-def toc_from_xml(dom, active_chapter, active_section):
- name = dom.xpath('//course/@name')[0]
-
- chapters = dom.xpath('//course[@name=$name]/chapter', name=name)
- ch=list()
- for c in chapters:
- if c.get('name') == 'hidden':
- continue
- sections=list()
- for s in dom.xpath('//course[@name=$name]/chapter[@name=$chname]/section', name=name, chname=c.get('name')):
- sections.append({'name':s.get("name") or "",
- 'format':s.get("subtitle") if s.get("subtitle") else s.get("format") or "",
- 'due':s.get("due") or "",
- 'active':(c.get("name")==active_chapter and \
- s.get("name")==active_section)})
- ch.append({'name':c.get("name"),
- 'sections':sections,
- 'active':(c.get("name")==active_chapter)})
- return ch
+ return get_module(tree, module, id_tag, module_id, sdirname, options)
diff --git a/lms/djangoapps/courseware/management/commands/check_course.py b/lms/djangoapps/courseware/management/commands/check_course.py
index 755cf089f4..8de29bdd8b 100644
--- a/lms/djangoapps/courseware/management/commands/check_course.py
+++ b/lms/djangoapps/courseware/management/commands/check_course.py
@@ -10,46 +10,98 @@ from courseware.content_parser import course_file
import courseware.module_render
import xmodule
+import mitxmako.middleware as middleware
+middleware.MakoMiddleware()
+
+def check_names(user, course):
+ '''
+ Complain if any problems have alphanumeric names.
+ TODO (vshnayder): there are some in 6.002x that don't. Is that actually a problem?
+ '''
+ all_ok = True
+ print "Confirming all problems have alphanumeric names"
+ for problem in course.xpath('//problem'):
+ filename = problem.get('filename')
+ if not filename.isalnum():
+ print "==============> Invalid (non-alphanumeric) filename", filename
+ all_ok = False
+ return all_ok
+
+def check_rendering(user, course):
+ '''Check that all modules render'''
+ all_ok = True
+ print "Confirming all modules render. Nothing should print during this step. "
+ for module in course.xpath('//problem|//html|//video|//vertical|//sequential|/tab'):
+ module_class = xmodule.modx_modules[module.tag]
+ # TODO: Abstract this out in render_module.py
+ try:
+ module_class(etree.tostring(module),
+ module.get('id'),
+ ajax_url='',
+ state=None,
+ track_function = lambda x,y,z:None,
+ render_function = lambda x: {'content':'','type':'video'})
+ except Exception as ex:
+ print "==============> Error in ", etree.tostring(module)
+ print ""
+ print ex
+ all_ok = False
+ print "Module render check finished"
+ return all_ok
+
+def check_sections(user, course):
+ all_ok = True
+ sections_dir = settings.DATA_DIR + "/sections"
+ print "Checking that all sections exist and parse properly"
+ if os.path.exists(sections_dir):
+ print "Checking all section includes are valid XML"
+ for f in os.listdir(sections_dir):
+ sectionfile = sections_dir + '/' + f
+ #print sectionfile
+ # skip non-xml files:
+ if not sectionfile.endswith('xml'):
+ continue
+ try:
+ etree.parse(sectionfile)
+ except Exception as ex:
+ print "================> Error parsing ", sectionfile
+ print ex
+ all_ok = False
+ print "checked all sections"
+ else:
+ print "Skipping check of include files -- no section includes dir ("+sections_dir+")"
+ return all_ok
+
class Command(BaseCommand):
help = "Does basic validity tests on course.xml."
def handle(self, *args, **options):
- check = True
+ all_ok = True
+
+ # TODO (vshnayder): create dummy user objects. Anon, authenticated, staff.
+ # Check that everything works for each.
+ # The objects probably shouldn't be actual django users to avoid unneeded
+ # dependency on django.
+
+ # TODO: use args as list of files to check. Fix loading to work for other files.
+
sample_user = User.objects.all()[0]
+
+
print "Attempting to load courseware"
course = course_file(sample_user)
- print "Confirming all problems have alphanumeric names"
- for problem in course.xpath('//problem'):
- filename = problem.get('filename')
- if not filename.isalnum():
- print "==============> Invalid (non-alphanumeric) filename", filename
- check = False
- print "Confirming all modules render. Nothing should print during this step. "
- for module in course.xpath('//problem|//html|//video|//vertical|//sequential|/tab'):
- module_class = xmodule.modx_modules[module.tag]
- # TODO: Abstract this out in render_module.py
- try:
- module_class(etree.tostring(module),
- module.get('id'),
- ajax_url='',
- state=None,
- track_function = lambda x,y,z:None,
- render_function = lambda x: {'content':'','type':'video'})
- except:
- print "==============> Error in ", etree.tostring(module)
- check = False
- print "Module render check finished"
- sections_dir = settings.DATA_DIR+"sections"
- if os.path.exists(sections_dir):
- print "Checking all section includes are valid XML"
- for f in os.listdir(sections_dir):
- print f
- etree.parse(sections_dir+'/'+f)
- else:
- print "Skipping check of include files -- no section includes dir ("+sections_dir+")"
+
+ to_run = [check_names,
+ # TODO (vshnayder) : make check_rendering work (use module_render.py),
+ # turn it on
+ # check_rendering,
+ check_sections,
+ ]
+ for check in to_run:
+ all_ok = check(sample_user, course) and all_ok
+
# TODO: print "Checking course properly annotated with preprocess.py"
-
- if check:
+ if all_ok:
print 'Courseware passes all checks!'
else:
print "Courseware fails some checks"
diff --git a/lms/static/coffee/spec/helper.coffee b/lms/static/coffee/spec/helper.coffee
index 1bb92160ce..eed1a07bd2 100644
--- a/lms/static/coffee/spec/helper.coffee
+++ b/lms/static/coffee/spec/helper.coffee
@@ -20,10 +20,12 @@ jasmine.stubRequests = ->
settings.success data: jasmine.stubbedMetadata[match[1]]
else if match = settings.url.match /static\/subs\/(.+)\.srt\.sjson/
settings.success jasmine.stubbedCaption
+ else if settings.url.match /modx\/problem\/.+\/problem_get$/
+ settings.success html: readFixtures('problem_content.html')
else if settings.url == '/calculate' ||
settings.url == '/6002x/modx/sequence/1/goto_position' ||
settings.url.match(/event$/) ||
- settings.url.match(/6002x\/modx\/problem\/.+\/problem_(check|reset|show|save)$/)
+ settings.url.match(/modx\/problem\/.+\/problem_(check|reset|show|save)$/)
# do nothing
else
throw "External request attempted for #{settings.url}, which is not defined."
diff --git a/lms/static/coffee/spec/modules/problem_spec.coffee b/lms/static/coffee/spec/modules/problem_spec.coffee
index 78047db3ba..7537cd3493 100644
--- a/lms/static/coffee/spec/modules/problem_spec.coffee
+++ b/lms/static/coffee/spec/modules/problem_spec.coffee
@@ -13,6 +13,7 @@ describe 'Problem', ->
spyOn($.fn, 'load').andCallFake (url, callback) ->
$(@).html readFixtures('problem_content.html')
callback()
+ jasmine.stubRequests()
describe 'constructor', ->
beforeEach ->
@@ -21,12 +22,6 @@ describe 'Problem', ->
it 'set the element', ->
expect(@problem.element).toBe '#problem_1'
- it 'set the content url', ->
- expect(@problem.content_url).toEqual '/problem/url/problem_get?id=1'
-
- it 'render the content', ->
- expect($.fn.load).toHaveBeenCalledWith @problem.content_url, @problem.bind
-
describe 'bind', ->
beforeEach ->
spyOn window, 'update_schematics'
@@ -57,8 +52,11 @@ describe 'Problem', ->
it 'bind the math input', ->
expect($('input.math')).toHandleWith 'keyup', @problem.refreshMath
- it 'display the math input', ->
- expect(@stubbedJax.root.toMathML).toHaveBeenCalled()
+ it 'replace math content on the page', ->
+ expect(MathJax.Hub.Queue.mostRecentCall.args).toEqual [
+ ['Text', @stubbedJax, ''],
+ [@problem.updateMathML, @stubbedJax, $('#input_example_1').get(0)]
+ ]
describe 'render', ->
beforeEach ->
@@ -77,12 +75,19 @@ describe 'Problem', ->
expect(@problem.bind).toHaveBeenCalled()
describe 'with no content given', ->
+ beforeEach ->
+ spyOn($, 'postWithPrefix').andCallFake (url, callback) ->
+ callback html: "Hello World"
+ @problem.render()
+
it 'load the content via ajax', ->
- expect($.fn.load).toHaveBeenCalledWith @problem.content_url, @bind
+ expect(@problem.element.html()).toEqual 'Hello World'
+
+ it 're-bind the content', ->
+ expect(@problem.bind).toHaveBeenCalled()
describe 'check', ->
beforeEach ->
- jasmine.stubRequests()
@problem = new Problem 1, '/problem/url/'
@problem.answers = 'foo=1&bar=2'
@@ -116,7 +121,6 @@ describe 'Problem', ->
describe 'reset', ->
beforeEach ->
- jasmine.stubRequests()
@problem = new Problem 1, '/problem/url/'
it 'log the problem_reset event', ->
@@ -130,13 +134,13 @@ describe 'Problem', ->
expect($.postWithPrefix).toHaveBeenCalledWith '/modx/problem/1/problem_reset', { id: 1 }, jasmine.any(Function)
it 'render the returned content', ->
- spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) -> callback("Reset!")
+ spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
+ callback html: "Reset!"
@problem.reset()
expect(@problem.element.html()).toEqual 'Reset!'
describe 'show', ->
beforeEach ->
- jasmine.stubRequests()
@problem = new Problem 1, '/problem/url/'
@problem.element.prepend '
'
@@ -154,18 +158,19 @@ describe 'Problem', ->
expect($.postWithPrefix).toHaveBeenCalledWith '/modx/problem/1/problem_show', jasmine.any(Function)
it 'show the answers', ->
- spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback('1_1': 'One', '1_2': 'Two')
+ spyOn($, 'postWithPrefix').andCallFake (url, callback) ->
+ callback answers: '1_1': 'One', '1_2': 'Two'
@problem.show()
expect($('#answer_1_1')).toHaveHtml 'One'
expect($('#answer_1_2')).toHaveHtml 'Two'
it 'toggle the show answer button', ->
- spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback({})
+ spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback(answers: {})
@problem.show()
expect($('.show')).toHaveValue 'Hide Answer'
it 'add the showed class to element', ->
- spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback({})
+ spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback(answers: {})
@problem.show()
expect(@problem.element).toHaveClass 'showed'
@@ -179,7 +184,8 @@ describe 'Problem', ->
'''
it 'set the correct_answer attribute on the choice', ->
- spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback('1_1': [2, 3])
+ spyOn($, 'postWithPrefix').andCallFake (url, callback) ->
+ callback answers: '1_1': [2, 3]
@problem.show()
expect($('label[for="input_1_1_1"]')).not.toHaveAttr 'correct_answer', 'true'
expect($('label[for="input_1_1_2"]')).toHaveAttr 'correct_answer', 'true'
@@ -214,7 +220,6 @@ describe 'Problem', ->
describe 'save', ->
beforeEach ->
- jasmine.stubRequests()
@problem = new Problem 1, '/problem/url/'
@problem.answers = 'foo=1&bar=2'
@@ -236,23 +241,29 @@ describe 'Problem', ->
describe 'refreshMath', ->
beforeEach ->
@problem = new Problem 1, '/problem/url/'
- @stubbedJax.root.toMathML.andReturn ''
$('#input_example_1').val 'E=mc^2'
+ @problem.refreshMath target: $('#input_example_1').get(0)
+
+ it 'should queue the conversion and MathML element update', ->
+ expect(MathJax.Hub.Queue).toHaveBeenCalledWith ['Text', @stubbedJax, 'E=mc^2'],
+ [@problem.updateMathML, @stubbedJax, $('#input_example_1').get(0)]
+
+ describe 'updateMathML', ->
+ beforeEach ->
+ @problem = new Problem 1, '/problem/url/'
+ @stubbedJax.root.toMathML.andReturn ''
describe 'when there is no exception', ->
beforeEach ->
- @problem.refreshMath target: $('#input_example_1').get(0)
+ @problem.updateMathML @stubbedJax, $('#input_example_1').get(0)
- it 'should convert and display the MathML object', ->
- expect(MathJax.Hub.Queue).toHaveBeenCalledWith ['Text', @stubbedJax, 'E=mc^2']
-
- it 'should display debug output in hidden div', ->
+ it 'convert jax to MathML', ->
expect($('#input_example_1_dynamath')).toHaveValue ''
describe 'when there is an exception', ->
beforeEach ->
@stubbedJax.root.toMathML.andThrow {restart: true}
- @problem.refreshMath target: $('#input_example_1').get(0)
+ @problem.updateMathML @stubbedJax, $('#input_example_1').get(0)
it 'should queue up the exception', ->
expect(MathJax.Callback.After).toHaveBeenCalledWith [@problem.refreshMath, @stubbedJax], true
diff --git a/lms/static/coffee/spec/modules/video/video_caption_spec.coffee b/lms/static/coffee/spec/modules/video/video_caption_spec.coffee
index e816da593a..3fa6fa7daa 100644
--- a/lms/static/coffee/spec/modules/video/video_caption_spec.coffee
+++ b/lms/static/coffee/spec/modules/video/video_caption_spec.coffee
@@ -35,6 +35,9 @@ describe 'VideoCaption', ->
it 'bind player resize event', ->
expect($(@player)).toHandleWith 'resize', @caption.onWindowResize
+ it 'bind player seek event', ->
+ expect($(@player)).toHandleWith 'seek', @caption.onUpdatePlayTime
+
it 'bind player updatePlayTime event', ->
expect($(@player)).toHandleWith 'updatePlayTime', @caption.onUpdatePlayTime
diff --git a/lms/static/coffee/src/modules/problem.coffee b/lms/static/coffee/src/modules/problem.coffee
index a1759b28af..f29c9eb72b 100644
--- a/lms/static/coffee/src/modules/problem.coffee
+++ b/lms/static/coffee/src/modules/problem.coffee
@@ -1,7 +1,6 @@
class @Problem
constructor: (@id, url) ->
@element = $("#problem_#{id}")
- @content_url = "#{url}problem_get?id=#{@id}"
@render()
$: (selector) ->
@@ -17,7 +16,7 @@ class @Problem
@$('section.action input.save').click @save
@$('input.math').keyup(@refreshMath).each(@refreshMath)
- update_progress: (response) =>
+ updateProgress: (response) =>
if response.progress_changed
@element.attr progress: response.progress_status
@element.trigger('progressChanged')
@@ -27,10 +26,9 @@ class @Problem
@element.html(content)
@bind()
else
- $.postWithPrefix "/modx/problem/#{@id}/problem_get", '', (response) =>
+ $.postWithPrefix "/modx/problem/#{@id}/problem_get", (response) =>
@element.html(response.html)
@bind()
-
check: =>
Logger.log 'problem_check', @answers
@@ -38,7 +36,7 @@ class @Problem
switch response.success
when 'incorrect', 'correct'
@render(response.contents)
- @update_progress response
+ @updateProgress response
else
alert(response.success)
@@ -46,7 +44,7 @@ class @Problem
Logger.log 'problem_reset', @answers
$.postWithPrefix "/modx/problem/#{@id}/problem_reset", id: @id, (response) =>
@render(response.html)
- @update_progress response
+ @updateProgress response
show: =>
if !@element.hasClass 'showed'
@@ -62,7 +60,7 @@ class @Problem
MathJax.Hub.Queue ["Typeset", MathJax.Hub]
@$('.show').val 'Hide Answer'
@element.addClass 'showed'
- @update_progress response
+ @updateProgress response
else
@$('[id^=answer_], [id^=solution_]').text ''
@$('[correct_answer]').attr correct_answer: null
@@ -74,21 +72,22 @@ class @Problem
$.postWithPrefix "/modx/problem/#{@id}/problem_save", @answers, (response) =>
if response.success
alert 'Saved'
- @update_progress response
+ @updateProgress response
refreshMath: (event, element) =>
element = event.target unless element
target = "display_#{element.id.replace(/^input_/, '')}"
if jax = MathJax.Hub.getAllJax(target)[0]
- MathJax.Hub.Queue ['Text', jax, $(element).val()]
+ MathJax.Hub.Queue ['Text', jax, $(element).val()],
+ [@updateMathML, jax, element]
- try
- output = jax.root.toMathML ''
- $("##{element.id}_dynamath").val(output)
- catch exception
- throw exception unless exception.restart
- MathJax.Callback.After [@refreshMath, jax], exception.restart
+ updateMathML: (jax, element) =>
+ try
+ $("##{element.id}_dynamath").val(jax.root.toMathML '')
+ catch exception
+ throw exception unless exception.restart
+ MathJax.Callback.After [@refreshMath, jax], exception.restart
refreshAnswers: =>
@$('input.schematic').each (index, element) ->
diff --git a/lms/static/coffee/src/modules/video/video_caption.coffee b/lms/static/coffee/src/modules/video/video_caption.coffee
index 7d796245bb..5dde796b78 100644
--- a/lms/static/coffee/src/modules/video/video_caption.coffee
+++ b/lms/static/coffee/src/modules/video/video_caption.coffee
@@ -10,6 +10,7 @@ class @VideoCaption
$(window).bind('resize', @onWindowResize)
$(@player).bind('resize', @onWindowResize)
$(@player).bind('updatePlayTime', @onUpdatePlayTime)
+ $(@player).bind('seek', @onUpdatePlayTime)
$(@player).bind('play', @onPlay)
@$('.hide-subtitles').click @toggle
@$('.subtitles').mouseenter(@onMouseEnter).mouseleave(@onMouseLeave)
diff --git a/lms/static/images/sequence-nav/status/check.png b/lms/static/images/sequence-nav/status/check.png
new file mode 100644
index 0000000000..d13529338b
Binary files /dev/null and b/lms/static/images/sequence-nav/status/check.png differ
diff --git a/lms/static/images/sequence-nav/status/dash.png b/lms/static/images/sequence-nav/status/dash.png
new file mode 100644
index 0000000000..79e7fa8f7f
Binary files /dev/null and b/lms/static/images/sequence-nav/status/dash.png differ
diff --git a/lms/static/images/sequence-nav/status/not-started.png b/lms/static/images/sequence-nav/status/not-started.png
new file mode 100644
index 0000000000..826e28609b
Binary files /dev/null and b/lms/static/images/sequence-nav/status/not-started.png differ
diff --git a/lms/static/images/sequence-nav/status/wrong.png b/lms/static/images/sequence-nav/status/wrong.png
new file mode 100644
index 0000000000..0ae9d34c68
Binary files /dev/null and b/lms/static/images/sequence-nav/status/wrong.png differ
diff --git a/lms/static/sass/README.md b/lms/static/sass/README.md
index dccb3a80c3..e94076e6e0 100644
--- a/lms/static/sass/README.md
+++ b/lms/static/sass/README.md
@@ -23,3 +23,7 @@ The dev server will automatically compile sass files that have changed. Simply s
the server using:
$ rake runserver
+
+To run it along side of development:
+
+ $ sass --watch lms/static/sass:lms/static/sass -r ./lms/static/sass/bourbon/lib/bourbon.rb
diff --git a/lms/static/sass/_textbook.scss b/lms/static/sass/_textbook.scss
index 35902c7a57..ae549d723f 100644
--- a/lms/static/sass/_textbook.scss
+++ b/lms/static/sass/_textbook.scss
@@ -62,6 +62,8 @@ div.book-wrapper {
@extend .clearfix;
li {
+ background-color: darken($cream, 4%);
+
&.last {
display: block;
float: left;
@@ -77,6 +79,10 @@ div.book-wrapper {
display: block;
float: right;
}
+
+ &:hover {
+ background: none;
+ }
}
}
diff --git a/lms/static/sass/base/_extends.scss b/lms/static/sass/base/_extends.scss
index 880f6cd3ca..63c3a9de7a 100644
--- a/lms/static/sass/base/_extends.scss
+++ b/lms/static/sass/base/_extends.scss
@@ -205,7 +205,7 @@ h1.top-header {
border-top: 1px solid #fff;
// @include box-shadow(inset 0 1px 0 #fff, inset 1px 0 0 #fff);
font-size: 12px;
- height:46px;
+ // height:46px;
line-height: 46px;
margin: (-$body-line-height) (-$body-line-height) $body-line-height;
text-shadow: 0 1px 0 #fff;
@@ -224,7 +224,7 @@ h1.top-header {
}
&.block-link {
- background: darken($cream, 5%);
+ // background: darken($cream, 5%);
border-left: 1px solid darken($cream, 20%);
@include box-shadow(inset 1px 0 0 lighten($cream, 5%));
display: block;
diff --git a/lms/static/sass/courseware/_sequence-nav.scss b/lms/static/sass/courseware/_sequence-nav.scss
index 4472724e6d..e02c67f720 100644
--- a/lms/static/sass/courseware/_sequence-nav.scss
+++ b/lms/static/sass/courseware/_sequence-nav.scss
@@ -11,13 +11,14 @@ nav.sequence-nav {
height: 100%;
padding-right: flex-grid(1, 9);
width: 100%;
+ padding-left: 0;
a {
@extend .block-link;
}
li {
- border-left: 1px solid darken($cream, 20%);
+ border-left: 1px solid darken($cream, 10%);
display: table-cell;
min-width: 20px;
@@ -29,18 +30,17 @@ nav.sequence-nav {
background-repeat: no-repeat;
&:hover {
- background-color: lighten($cream, 3%);
+ background-color: lighten($cream, 8%);
}
}
.visited {
- background-color: #DCCDA2;
+ background-color: darken($cream, 4%);
background-repeat: no-repeat;
- @include box-shadow(inset 0 0 3px darken(#dccda2, 10%));
+ @include box-shadow(0);
&:hover {
background-color: $cream;
- background-position: center center;
}
}
@@ -51,7 +51,6 @@ nav.sequence-nav {
&:hover {
background-color: #fff;
- background-position: center;
}
}
@@ -61,86 +60,87 @@ nav.sequence-nav {
cursor: pointer;
display: block;
height: 17px;
- padding: 15px 0 14px;
+ padding: 15px 0 17px;
position: relative;
@include transition(all, .4s, $ease-in-out-quad);
width: 100%;
-
- &.progress {
- border-bottom-style: solid;
- border-bottom-width: 4px;
- }
-
- &.progress-none {
- @extend .progress;
- border-bottom-color: red;
- }
-
- &.progress-some {
- @extend .progress;
- border-bottom-color: yellow;
- }
-
- &.progress-done {
- @extend .progress;
- border-bottom-color: green;
- }
+ background-position: center 8px;
//video
&.seq_video_inactive {
@extend .inactive;
- background-image: url('../images/sequence-nav/video-icon-normal.png');
- background-position: center;
+ background-image: url('../images/sequence-nav/video-icon-visited.png');
}
&.seq_video_visited {
@extend .visited;
- background-image: url('../images/sequence-nav/video-icon-visited.png');
- background-position: center;
+ background-image: url('../images/sequence-nav/video-icon-normal.png');
}
&.seq_video_active {
@extend .active;
background-image: url('../images/sequence-nav/video-icon-current.png');
- background-position: center;
}
//other
&.seq_other_inactive {
@extend .inactive;
- background-image: url('../images/sequence-nav/document-icon-normal.png');
- background-position: center;
+ background-image: url('../images/sequence-nav/document-icon-visited.png');
}
&.seq_other_visited {
@extend .visited;
- background-image: url('../images/sequence-nav/document-icon-visited.png');
- background-position: center;
+ background-image: url('../images/sequence-nav/document-icon-normal.png');
}
&.seq_other_active {
@extend .active;
background-image: url('../images/sequence-nav/document-icon-current.png');
- background-position: center;
}
//vertical & problems
&.seq_vertical_inactive, &.seq_problem_inactive {
@extend .inactive;
- background-image: url('../images/sequence-nav/list-icon-normal.png');
- background-position: center;
+ background-image: url('../images/sequence-nav/list-icon-visited.png');
}
&.seq_vertical_visited, &.seq_problem_visited {
@extend .visited;
- background-image: url('../images/sequence-nav/list-icon-visited.png');
- background-position: center;
+ background-image: url('../images/sequence-nav/list-icon-normal.png');
}
&.seq_vertical_active, &.seq_problem_active {
@extend .active;
background-image: url('../images/sequence-nav/list-icon-current.png');
- background-position: center;
+ }
+
+ &:after {
+ content: " ";
+ display: block;
+ width: 11px;
+ height: 11px;
+ background: url('../images/sequence-nav/status/dash.png') no-repeat center;
+ @include position( absolute, 0 0 6px 50% );
+ margin-left: -5px;
+ }
+
+ //progress
+ &.progress-none {
+ &:after {
+ background: url('../images/sequence-nav/status/not-started.png') no-repeat center;
+ }
+ }
+
+ &.progress-some {
+ &:after {
+ background: url('../images/sequence-nav/status/wrong.png') no-repeat center;
+ }
+ }
+
+ &.progress-done {
+ &:after {
+ background: url('../images/sequence-nav/status/check.png') no-repeat center;
+ }
}
p {
@@ -180,6 +180,8 @@ nav.sequence-nav {
}
&:hover {
+ background-position: center 8px;
+
p {
display: block;
margin-top: 4px;
@@ -215,6 +217,7 @@ nav.sequence-nav {
display: block;
text-indent: -9999px;
@include transition(all, .2s, $ease-in-out-quad);
+ line-height: 49px;
&:hover {
opacity: .5;
@@ -277,6 +280,7 @@ section.course-content {
@include border-radius(3px);
@include box-shadow(inset 0 0 0 1px lighten(#f6efd4, 5%));
@include inline-block();
+ padding-left: 0;
li {
float: left;
diff --git a/lms/static/sass/courseware/_sidebar.scss b/lms/static/sass/courseware/_sidebar.scss
index 44e9d02c28..235426294e 100644
--- a/lms/static/sass/courseware/_sidebar.scss
+++ b/lms/static/sass/courseware/_sidebar.scss
@@ -52,14 +52,14 @@ section.course-index {
padding: 1em 1.5em;
li {
- background: transparent;
- border: 1px solid transparent;
- @include border-radius(4px);
margin-bottom: lh(.5);
- position: relative;
- padding: 5px 36px 5px 10px;
a {
+ border: 1px solid transparent;
+ background: transparent;
+ @include border-radius(4px);
+ position: relative;
+ padding: 5px 36px 5px 10px;
text-decoration: none;
display: block;
color: #666;
@@ -74,67 +74,70 @@ section.course-index {
display: block;
}
}
- }
-
- &:after {
- background: transparent;
- border-top: 1px solid rgb(180,180,180);
- border-right: 1px solid rgb(180,180,180);
- content: "";
- display: block;
- height: 12px;
- margin-top: -6px;
- opacity: 0;
- position: absolute;
- top: 50%;
- right: 30px;
- @include transform(rotate(45deg));
- width: 12px;
- }
-
- &:hover {
- @include background-image(linear-gradient(-90deg, rgba(245,245,245, 0.4), rgba(230,230,230, 0.4)));
- border-color: rgb(200,200,200);
&:after {
- opacity: 1;
- right: 15px;
- @include transition(all, 0.2s, linear);
+ background: transparent;
+ border-top: 1px solid rgb(180,180,180);
+ border-right: 1px solid rgb(180,180,180);
+ content: "";
+ display: block;
+ height: 12px;
+ margin-top: -6px;
+ opacity: 0;
+ position: absolute;
+ top: 50%;
+ right: 30px;
+ @include transform(rotate(45deg));
+ width: 12px;
}
- > a p {
- color: #333;
+ &:hover {
+ @include background-image(linear-gradient(-90deg, rgba(245,245,245, 0.4), rgba(230,230,230, 0.4)));
+ border-color: rgb(200,200,200);
+
+ &:after {
+ opacity: 1;
+ right: 15px;
+ @include transition(all, 0.2s, linear);
+ }
+
+ > a p {
+ color: #333;
+ }
}
- }
- &:active {
- @include box-shadow(inset 0 1px 14px 0 rgba(0,0,0, 0.1));
- top: 1px;
+ &:active {
+ @include box-shadow(inset 0 1px 14px 0 rgba(0,0,0, 0.1));
- &:after {
- opacity: 1;
- right: 15px;
+ &:after {
+ opacity: 1;
+ right: 15px;
+ }
}
}
&.active {
- background: rgb(240,240,240);
- @include background-image(linear-gradient(-90deg, rgb(245,245,245), rgb(230,230,230)));
- border-color: rgb(200,200,200);
font-weight: bold;
- > a p {
- color: #333;
+ > a {
+ background: rgb(240,240,240);
+ @include background-image(linear-gradient(-90deg, rgb(245,245,245), rgb(230,230,230)));
+ border-color: rgb(200,200,200);
+
+ &:after {
+ opacity: 1;
+ right: 15px;
+ }
+
+ p {
+ color: #333;
+ }
}
span.subtitle {
font-weight: normal;
}
- &:after {
- opacity: 1;
- right: 15px;
- }
}
}
}
diff --git a/rakefile b/rakefile
index a3d742cef1..b6fbb18ac6 100644
--- a/rakefile
+++ b/rakefile
@@ -58,6 +58,11 @@ task :pylint => REPORT_DIR do
end
end
+default_options = {
+ :lms => '8000',
+ :cms => '8001',
+}
+
[:lms, :cms].each do |system|
task_name = "test_#{system}"
report_dir = File.join(REPORT_DIR, task_name)
@@ -76,7 +81,7 @@ end
Other useful environments are devplus (for dev testing with a real local database)
desc
task system, [:env, :options] => [] do |t, args|
- args.with_defaults(:env => 'dev', :options => '')
+ args.with_defaults(:env => 'dev', :options => default_options[system])
sh(django_admin(system, args.env, 'runserver', args.options))
end
end
@@ -154,3 +159,15 @@ end
task :publish => :package do
sh("scp #{BUILD_DIR}/#{NORMALIZED_DEPLOY_NAME}_#{PKG_VERSION}*.deb #{PACKAGE_REPO}")
end
+
+namespace :cms do
+ desc "Import course data within the given DATA_DIR variable"
+ task :import do
+ if ENV['DATA_DIR']
+ sh(django_admin(:cms, :dev, :import, ENV['DATA_DIR']))
+ else
+ raise "Please specify a DATA_DIR variable that point to your data directory.\n" +
+ "Example: \`rake cms:import DATA_DIR=../data\`"
+ end
+ end
+end