Commit f041fc4a by David Ormsbee

Merge pull request #135 from MITx/cpennington/cms-editing

Roundtrip HTML module editing working in the CMS
parents 968ff732 83b3d51f
...@@ -10,6 +10,7 @@ import os.path ...@@ -10,6 +10,7 @@ import os.path
from StringIO import StringIO from StringIO import StringIO
from mako.template import Template from mako.template import Template
from mako.lookup import TemplateLookup from mako.lookup import TemplateLookup
from collections import defaultdict
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from keystore.django import keystore from keystore.django import keystore
...@@ -42,9 +43,7 @@ class Command(BaseCommand): ...@@ -42,9 +43,7 @@ class Command(BaseCommand):
# Simple lists # Simple lists
'chapter': 'Week', 'chapter': 'Week',
'course': 'Course', 'course': 'Course',
'sequential': 'LectureSequence', 'section': defaultdict(lambda: 'Section', {
'vertical': 'ProblemSet',
'section': {
'Lab': 'Lab', 'Lab': 'Lab',
'Lecture Sequence': 'LectureSequence', 'Lecture Sequence': 'LectureSequence',
'Homework': 'Homework', 'Homework': 'Homework',
...@@ -52,8 +51,13 @@ class Command(BaseCommand): ...@@ -52,8 +51,13 @@ class Command(BaseCommand):
'Video': 'VideoSegment', 'Video': 'VideoSegment',
'Midterm': 'Exam', 'Midterm': 'Exam',
'Final': 'Exam', 'Final': 'Exam',
None: 'Section', 'Problems': 'ProblemSet',
}, }),
'videosequence': 'VideoSequence',
'problemset': 'ProblemSet',
'vertical': 'Section',
'sequential': 'Section',
'tab': 'Section',
# True types # True types
'video': 'VideoSegment', 'video': 'VideoSegment',
'html': 'HTML', 'html': 'HTML',
...@@ -78,6 +82,8 @@ class Command(BaseCommand): ...@@ -78,6 +82,8 @@ class Command(BaseCommand):
e.set('url', 'i4x://mit.edu/6002xs12/{category}/{name}'.format( e.set('url', 'i4x://mit.edu/6002xs12/{category}/{name}'.format(
category=category, category=category,
name=name)) name=name))
else:
print "Skipping element with tag", e.tag
def handle_skip(e): def handle_skip(e):
...@@ -150,6 +156,9 @@ class Command(BaseCommand): ...@@ -150,6 +156,9 @@ class Command(BaseCommand):
'sequential': handle_list, 'sequential': handle_list,
'vertical': handle_list, 'vertical': handle_list,
'section': handle_list, 'section': handle_list,
'videosequence': handle_list,
'problemset': handle_list,
'tab': handle_list,
# True types # True types
'video': handle_video, 'video': handle_video,
'html': handle_html, 'html': handle_html,
......
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
from keystore.django import keystore 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): def index(request):
# TODO (cpennington): These need to be read in from the active user # TODO (cpennington): These need to be read in from the active user
org = 'mit.edu' org = 'mit.edu'
...@@ -11,3 +14,20 @@ def index(request): ...@@ -11,3 +14,20 @@ def index(request):
course = keystore().get_item(['i4x', org, course, 'Course', name]) course = keystore().get_item(['i4x', org, course, 'Course', name])
weeks = course.get_children() weeks = course.get_children()
return render_to_response('index.html', {'weeks': weeks}) 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({}))
...@@ -21,6 +21,9 @@ Longer TODO: ...@@ -21,6 +21,9 @@ Longer TODO:
import sys import sys
import tempfile import tempfile
import os.path
import os
import errno
from path import path from path import path
############################ FEATURE CONFIGURATION ############################# ############################ FEATURE CONFIGURATION #############################
...@@ -154,7 +157,38 @@ PIPELINE_CSS = { ...@@ -154,7 +157,38 @@ PIPELINE_CSS = {
PIPELINE_ALWAYS_RECOMPILE = ['sass/base-style.scss'] 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 = { 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 = [ PIPELINE_COMPILERS = [
......
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
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
$(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()
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)
/*!
* 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);
$(document).ready(function(){
$('section.main-content').children().hide();
$(function(){
$('.editable').inlineEdit();
$('.editable-textarea').inlineEdit({control: 'textarea'});
});
var heighest = 0;
$('.cal ol > li').each(function(){
heighest = ($(this).height() > heighest) ? $(this).height() : heighest;
});
$('.cal ol > li').css('height',heighest + 'px');
$('.add-new-section').click(function() {
return false;
});
$('.new-week .close').click( function(){
$(this).parents('.new-week').hide();
$('p.add-new-week').show();
return false;
});
$('.save-update').click(function(){
$(this).parent().parent().hide();
return false;
});
setHeight = function(){
var windowHeight = $(this).height();
var contentHeight = windowHeight - 29;
$('section.main-content > section').css('min-height', contentHeight);
$('body.content .cal').css('height', contentHeight);
$('.edit-week').click( function() {
$('body').addClass('content');
$('body.content .cal').css('height', contentHeight);
$('section.week-new').show();
return false;
});
$('.cal ol li header h1 a').click( function() {
$('body').addClass('content');
$('body.content .cal').css('height', contentHeight);
$('section.week-edit').show();
return false;
});
$('a.sequence-edit').click(function(){
$('body').addClass('content');
$('body.content .cal').css('height', contentHeight);
$('section.sequence-edit').show();
return false;
});
}
$(document).ready(setHeight);
$(window).bind('resize', setHeight);
$('.video-new a').click(function(){
$('section.video-new').show();
return false;
});
$('a.video-edit').click(function(){
$('section.video-edit').show();
return false;
});
$('.problem-new a').click(function(){
$('section.problem-new').show();
return false;
});
$('a.problem-edit').click(function(){
$('section.problem-edit').show();
return false;
});
});
Sass Watch: Sass Watch:
sass --watch cms/static/sass:cms/static/css -r ./cms/static/sass/bourbon/lib/bourbon.rb sass --watch cms/static/sass:cms/static/sass -r ./cms/static/sass/bourbon/lib/bourbon.rb
...@@ -111,3 +111,7 @@ input[type="submit"], .button { ...@@ -111,3 +111,7 @@ input[type="submit"], .button {
display: block; display: block;
float: right; float: right;
} }
textarea {
overflow: auto;
}
section.video-new, section.video-edit, section.problem-new, section.problem-edit { section.video-new,
position: absolute; section.video-edit,
top: 72px; section.problem-new,
right: 0; section.problem-edit,
section.html-edit {
background: #fff; background: #fff;
width: flex-grid(6);
@include box-shadow(0 0 6px #666);
border: 1px solid #333; border: 1px solid #333;
border-right: 0; border-right: 0;
z-index: 4;
> header { > header {
background: #666; background: #eee;
@include clearfix; @include clearfix;
color: #fff;
padding: 6px; padding: 6px;
border-bottom: 1px solid #333; border-bottom: 1px solid #333;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
......
section.problem-new, section.problem-edit { section#unit-wrapper {
> section { > header {
textarea { border-bottom: 2px solid #333;
@include box-sizing(border-box); @include clearfix();
display: block; padding: 6px 20px;
width: 100%;
}
div.preview { h1 {
background: #eee; font-size: 18px;
@include box-sizing(border-box); text-transform: uppercase;
height: 40px; letter-spacing: 1px;
padding: 10px; float: left;
width: 100%;
} }
a.save { p {
float: right;
}
}
> section {
padding: 20px;
a.save-update {
@extend .button; @extend .button;
@include inline-block(); @include inline-block();
margin-top: 20px; margin-top: 20px;
...@@ -22,3 +25,47 @@ section.problem-new, section.problem-edit { ...@@ -22,3 +25,47 @@ section.problem-new, section.problem-edit {
} }
} }
section.problem-new,
section.problem-edit,
section.html-edit {
textarea {
@include box-sizing(border-box);
display: block;
width: 100%;
}
div.preview {
background: #eee;
@include box-sizing(border-box);
min-height: 40px;
padding: 10px;
width: 100%;
margin-top: 10px;
h1 {
font-size: 24px;
margin-bottom: 1em;
}
h2 {
font-size: 20px;
margin-bottom: 1em;
}
h3 {
font-size: 18;
margin-bottom: 1em;
}
ul {
padding-left: 20px;
margin-bottom: 1em;
}
p {
margin-bottom: 1em;
}
}
}
...@@ -23,11 +23,18 @@ ...@@ -23,11 +23,18 @@
<%block name="content"></%block> <%block name="content"></%block>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script> <script type="text/javascript" src="${ STATIC_URL}/js/jquery.min.js"></script>
<script type="text/javascript" src="${ STATIC_URL }/js/markitup/jquery.markitup.js"></script> <script type="text/javascript" src="${ STATIC_URL }/js/markitup/jquery.markitup.js"></script>
<script type="text/javascript" src="${ STATIC_URL }/js/markitup/sets/wiki/set.js"></script> <script type="text/javascript" src="${ STATIC_URL }/js/markitup/sets/wiki/set.js"></script>
% if settings.MITX_FEATURES['USE_DJANGO_PIPELINE']:
<%static:js group='main'/>
% else:
<script src="${ STATIC_URL }/js/main.js"></script> <script src="${ STATIC_URL }/js/main.js"></script>
% endif
<%static:js group='module-js'/>
<script src="${ STATIC_URL }/js/jquery.inlineedit.js"></script> <script src="${ STATIC_URL }/js/jquery.inlineedit.js"></script>
<script src="${ STATIC_URL }/js/jquery.cookie.js"></script>
<script src="${ STATIC_URL }/js/jquery.leanModal.min.js"></script> <script src="${ STATIC_URL }/js/jquery.leanModal.min.js"></script>
<script src="${ STATIC_URL }/js/jquery.tablednd.js"></script> <script src="${ STATIC_URL }/js/jquery.tablednd.js"></script>
</body> </body>
......
...@@ -7,13 +7,9 @@ ...@@ -7,13 +7,9 @@
<%include file="widgets/navigation.html"/> <%include file="widgets/navigation.html"/>
<section class="main-content"> <section class="main-content">
<%include file="widgets/week-edit.html"/> <section class="edit-pane">
<%include file="widgets/week-new.html"/> <div id="module-html"/>
<%include file="widgets/sequnce-edit.html"/> </section>
<%include file="widgets/video-edit.html"/>
<%include file="widgets/video-new.html"/>
<%include file="widgets/problem-edit.html"/>
<%include file="widgets/problem-new.html"/>
</section> </section>
</section> </section>
......
<section id="unit-wrapper" class="${type}">
<header>
<h1 class="editable">${name}</h1>
<p>Unit type: ${type}</p>
<!-- <div class="actions"> -->
<!-- <a href="#" class="cancel">Cancel</a> -->
<!-- <a href="#" class="save-update">Save &amp; Update</a> -->
<!-- </div> -->
</header>
<section>
${contents}
</section>
<section>
<div class="actions">
<a href="" class="save-update">Save &amp; Update</a>
<a href="#" class="cancel">Cancel</a>
</div>
</section>
</section>
...@@ -6,9 +6,6 @@ ...@@ -6,9 +6,6 @@
<a href="#" class="new-module">New Section</a> <a href="#" class="new-module">New Section</a>
</li> </li>
<li> <li>
<a href="#" class="new-module">New Module</a>
</li>
<li>
<a href="#" class="new-module">New Unit</a> <a href="#" class="new-module">New Unit</a>
</li> </li>
</ul> </ul>
......
<section class="html-edit">
<textarea name="" class="edit-box" rows="8" cols="40">${module.definition['data']['text']}</textarea>
<div class="preview">${module.definition['data']['text']}</div>
</section>
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
% for week in weeks: % for week in weeks:
<li> <li>
<header> <header>
<h1><a href="#">${week.name}</a></h1> <h1><a href="#" class="week-edit" id="${week.url}">${week.name}</a></h1>
<ul> <ul>
% if week.goals: % if week.goals:
% for goal in week.goals: % for goal in week.goals:
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
<ul> <ul>
% for module in week.get_children(): % for module in week.get_children():
<li class="${module.type}"> <li class="${module.type}">
<a href="#" class="${module.type}-edit">${module.name}</a> <a href="#" class="module-edit" id="${module.url}">${module.name}</a>
<a href="#" class="draggable">handle</a> <a href="#" class="draggable">handle</a>
</li> </li>
% endfor % endfor
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
<section> <section>
<header> <header>
<h1 class="editable">New Problem</h1> <h1 class="editable">${module.name}</h1>
<section class="author"> <section class="author">
<div> <div>
<h2>Last modified:</h2> <h2>Last modified:</h2>
......
<li>
<img src="http://placehold.it/300x180" alt="" /><h5>Video-file-name</h5>
</li>
<section class="caption-save">
<a href="#" class="close-box">Cancel</a>
<button class="close-box">Save changes</button>
</section>
<section class="sequence-edit"> <section class="sequence-edit">
<header> <header>
<div class="week">
<h2><a href="">Week 1</a></h2>
<ul>
<li>
<p class="editable"><strong>Goal title:</strong> This is the goal body and is where the goal will be further explained</p>
</li>
</ul>
</div>
<div> <div>
<h1 class="editable">Lecture sequence</h1> <h1 class="editable">${module.name}</h1>
<p><strong>Group type:</strong> Ordered Sequence</p> <p><strong>Module Type:</strong>${module.type}</p>
</div> </div>
</header> </header>
...@@ -51,72 +43,12 @@ ...@@ -51,72 +43,12 @@
<ol> <ol>
<li> <li>
<ol> <ol>
% for child in module.get_children():
<li> <li>
<a href="" class="problem-edit">Problem title 11</a> <a href="#" class="module-edit" id="${child.url}">${child.name}</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="sequence-edit">Problem Group</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="problem-edit">Problem title 14</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="video-edit">Video 3</a>
<a href="#" class="draggable">handle</a>
</li>
<li class="group">
<header>
<h3>
<a href="#" class="problem-edit">Problem group</a>
<a href="#" class="draggable">handle</a>
</h3>
</header>
<ol>
<li>
<a href="#" class="problem-edit">Problem title 11</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="problem-edit">Problem title 11</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="problem-edit">Problem title 11</a>
<a href="#" class="draggable">handle</a>
</li>
</ol>
</li>
<li>
<a href="#" class="problem-edit">Problem title 13</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="problem-edit">Problem title 14</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="video-edit">Video 3</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="" class="problem-edit">Problem title 11</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="sequence-edit">Problem Group</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="problem-edit">Problem title 14</a>
<a href="#" class="draggable">handle</a>
</li>
<li>
<a href="#" class="video-edit">Video 3</a>
<a href="#" class="draggable">handle</a> <a href="#" class="draggable">handle</a>
</li> </li>
%endfor
</ol> </ol>
</li> </li>
......
<div class="tooltip">
<ul>
<li><a href="#view" rel="leanModal">View</a></li>
<li><a href="#">Download</a></li>
<li><a href="#" class="delete-speed">Delete</a></li>
</ul>
</div>
<li class="video-box">
<div class="thumb"><img src="http://placehold.it/100x65" /></div>
<div class="meta">
<strong>video-name</strong> 236mb Uploaded 6 hours ago by <em>Anant Agrawal</em>
<p>
<ul class="speed-list">
Speed
<li class="speed">
0.75x
<%include file="speed-tooltip.html"/>
</li>
<li class="speed">Normal
<%include file="speed-tooltip.html"/>
</li>
<li class="speed">1.25x
<%include file="speed-tooltip.html"/>
</li>
<li class="speed">1.5x
<%include file="speed-tooltip.html"/>
</li>
<li style="background: #eee;" ><a href="#upload" rel="leanModal" class="new-upload">+</a></li>
</ul>
</p>
<p>
<a href="#">Download All</a>
<a href="#" style="color: brown;" class="remove">Delete All</a>
<a href="#" class="edit-captions"> Edit Captions </a>
<a href="#" class="use-video">Use clip ⬆</a>
</p>
</div>
<div class="caption-box">
<%include file="captions.html"/>
<%include file="save-captions.html"/>
</div>
</li>
<li class="video-box">
<div class="thumb"><img src="http://placehold.it/155x90" /></div>
<div class="meta">
<strong>video-name</strong> 236mb
<p>Uploaded 6 hours ago by <em>Anant Agrawal</em></p>
<p>
<ul class="speed-list">
Speed
<li class="speed">
0.75x
<%include file="speed-tooltip.html"/>
</li>
<li class="speed">Normal
<%include file="speed-tooltip.html"/>
</li>
<li class="speed">1.25x
<%include file="speed-tooltip.html"/>
</li>
<li class="speed">1.5x
<%include file="speed-tooltip.html"/>
</li>
<li style="background: #eee;" ><a href="#upload" rel="leanModal" class="new-upload">+</a></li>
</ul>
</p>
<p>
<a href="#">Download all</a>
<a href="#" stle="color: brown;" class="remove-video">Remove ⬇ </a>
</p>
</div>
<div style="margin-top: 30px;">
<%include file="captions.html"/>
</div>
</li>
...@@ -6,4 +6,6 @@ from django.conf.urls.defaults import patterns, url ...@@ -6,4 +6,6 @@ from django.conf.urls.defaults import patterns, url
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'^$', 'contentstore.views.index', name='index'), 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'),
) )
...@@ -10,7 +10,8 @@ import StringIO ...@@ -10,7 +10,8 @@ import StringIO
from datetime import timedelta from datetime import timedelta
from lxml import etree 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 progress import Progress
from capa.capa_problem import LoncapaProblem from capa.capa_problem import LoncapaProblem
from capa.responsetypes import StudentInputError from capa.responsetypes import StudentInputError
...@@ -63,8 +64,14 @@ class ComplexEncoder(json.JSONEncoder): ...@@ -63,8 +64,14 @@ class ComplexEncoder(json.JSONEncoder):
return json.JSONEncoder.default(self, obj) return json.JSONEncoder.default(self, obj)
class ModuleDescriptor(XModuleDescriptor): class CapaModuleDescriptor(MakoModuleDescriptor):
pass """
Module implementing problems in the LON-CAPA format,
as implemented by capa.capa_problem
"""
mako_template = 'widgets/problem-edit.html'
class Module(XModule): class Module(XModule):
......
import json import json
import logging import logging
from x_module import XModule, XModuleDescriptor from x_module import XModule
from mako_module import MakoModuleDescriptor
from lxml import etree from lxml import etree
from pkg_resources import resource_string
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
class ModuleDescriptor(XModuleDescriptor): class HtmlModuleDescriptor(MakoModuleDescriptor):
pass """
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): class Module(XModule):
id_attribute = 'filename' id_attribute = 'filename'
......
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()}
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
})
...@@ -3,7 +3,8 @@ import logging ...@@ -3,7 +3,8 @@ import logging
from lxml import etree 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 from xmodule.progress import Progress
log = logging.getLogger("mitx.common.lib.seq_module") log = logging.getLogger("mitx.common.lib.seq_module")
...@@ -12,9 +13,6 @@ log = logging.getLogger("mitx.common.lib.seq_module") ...@@ -12,9 +13,6 @@ log = logging.getLogger("mitx.common.lib.seq_module")
# OBSOLETE: This obsoletes 'type' # OBSOLETE: This obsoletes 'type'
class_priority = ['video', 'problem'] class_priority = ['video', 'problem']
class ModuleDescriptor(XModuleDescriptor):
pass
class Module(XModule): class Module(XModule):
''' Layout module which lays out content in a temporal sequence ''' Layout module which lays out content in a temporal sequence
''' '''
...@@ -117,5 +115,5 @@ class Module(XModule): ...@@ -117,5 +115,5 @@ class Module(XModule):
self.rendered = False self.rendered = False
class SectionDescriptor(XModuleDescriptor): class SectionDescriptor(MakoModuleDescriptor):
pass mako_template = 'widgets/sequence-edit.html'
...@@ -5,6 +5,9 @@ setup( ...@@ -5,6 +5,9 @@ setup(
version="0.1", version="0.1",
packages=find_packages(), packages=find_packages(),
install_requires=['distribute'], install_requires=['distribute'],
package_data={
'': ['js/*']
},
# See http://guide.python-distribute.org/creation.html#entry-points # See http://guide.python-distribute.org/creation.html#entry-points
# for a description of entry_points # for a description of entry_points
...@@ -19,6 +22,9 @@ setup( ...@@ -19,6 +22,9 @@ setup(
"TutorialIndex = seq_module:SectionDescriptor", "TutorialIndex = seq_module:SectionDescriptor",
"Exam = seq_module:SectionDescriptor", "Exam = seq_module:SectionDescriptor",
"VideoSegment = video_module:VideoSegmentDescriptor", "VideoSegment = video_module:VideoSegmentDescriptor",
"ProblemSet = seq_module:SectionDescriptor",
"Problem = capa_module:CapaModuleDescriptor",
"HTML = html_module:HtmlModuleDescriptor",
] ]
} }
) )
...@@ -3,7 +3,6 @@ import pkg_resources ...@@ -3,7 +3,6 @@ import pkg_resources
import logging import logging
from keystore import Location from keystore import Location
from progress import Progress
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger('mitx.' + __name__)
...@@ -30,6 +29,12 @@ class Plugin(object): ...@@ -30,6 +29,12 @@ class Plugin(object):
return classes[0].load() 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): class XModule(object):
''' Implements a generic learning module. ''' Implements a generic learning module.
...@@ -154,6 +159,7 @@ class XModuleDescriptor(Plugin): ...@@ -154,6 +159,7 @@ class XModuleDescriptor(Plugin):
and can generate XModules (which do know about student state). and can generate XModules (which do know about student state).
""" """
entry_point = "xmodule.v1" entry_point = "xmodule.v1"
js = {}
@staticmethod @staticmethod
def load_from_json(json_data, system): def load_from_json(json_data, system):
...@@ -178,6 +184,19 @@ class XModuleDescriptor(Plugin): ...@@ -178,6 +184,19 @@ class XModuleDescriptor(Plugin):
""" """
return cls(system=system, **json_data) 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, def __init__(self,
system, system,
definition=None, definition=None,
...@@ -202,6 +221,7 @@ class XModuleDescriptor(Plugin): ...@@ -202,6 +221,7 @@ class XModuleDescriptor(Plugin):
self.definition = definition if definition is not None else {} self.definition = definition if definition is not None else {}
self.name = Location(kwargs.get('location')).name self.name = Location(kwargs.get('location')).name
self.type = Location(kwargs.get('location')).category 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 # 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 # is one of the things that we are going to be iterating on heavily
...@@ -220,21 +240,27 @@ class XModuleDescriptor(Plugin): ...@@ -220,21 +240,27 @@ class XModuleDescriptor(Plugin):
else: else:
return [child for child in self._child_instances if child.type in categories] 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): def get_xml(self):
''' For conversions between JSON and legacy XML representations. ''' For conversions between JSON and legacy XML representations.
''' '''
if self.xml: if self.xml:
return self.xml return self.xml
else: else:
raise NotImplementedError("JSON->XML Translation not implemented") raise NotImplementedError("JSON->XML Translation not implemented")
def get_json(self): def get_json(self):
''' For conversions between JSON and legacy XML representations. ''' For conversions between JSON and legacy XML representations.
''' '''
if self.json: if self.json:
raise NotImplementedError raise NotImplementedError
return self.json # TODO: Return context as well -- files, etc. return self.json # TODO: Return context as well -- files, etc.
else: else:
raise NotImplementedError("XML->JSON Translation not implemented") raise NotImplementedError("XML->JSON Translation not implemented")
#def handle_cms_json(self): #def handle_cms_json(self):
......
...@@ -58,6 +58,11 @@ task :pylint => REPORT_DIR do ...@@ -58,6 +58,11 @@ task :pylint => REPORT_DIR do
end end
end end
default_options = {
:lms => '8000',
:cms => '8001',
}
[:lms, :cms].each do |system| [:lms, :cms].each do |system|
task_name = "test_#{system}" task_name = "test_#{system}"
report_dir = File.join(REPORT_DIR, task_name) report_dir = File.join(REPORT_DIR, task_name)
...@@ -76,7 +81,7 @@ end ...@@ -76,7 +81,7 @@ end
Other useful environments are devplus (for dev testing with a real local database) Other useful environments are devplus (for dev testing with a real local database)
desc desc
task system, [:env, :options] => [] do |t, args| 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)) sh(django_admin(system, args.env, 'runserver', args.options))
end end
end end
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment