Merge branch 'cas' into bug/lyla/templates
This commit is contained in:
@@ -37,6 +37,7 @@ from xmodule.error_module import ErrorDescriptor
|
||||
from xmodule.errortracker import exc_info_to_str
|
||||
from github_sync import export_to_github
|
||||
from static_replace import replace_urls
|
||||
from external_auth.views import ssl_login_shortcut
|
||||
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
from xmodule.modulestore.django import modulestore
|
||||
@@ -88,7 +89,7 @@ def signup(request):
|
||||
csrf_token = csrf(request)['csrf_token']
|
||||
return render_to_response('signup.html', {'csrf': csrf_token})
|
||||
|
||||
|
||||
@ssl_login_shortcut
|
||||
@ensure_csrf_cookie
|
||||
def login_page(request):
|
||||
"""
|
||||
@@ -109,7 +110,7 @@ def index(request):
|
||||
courses = modulestore().get_items(['i4x', None, None, 'course', None])
|
||||
|
||||
# filter out courses that we don't have access to
|
||||
courses = filter(lambda course: has_access(request.user, course.location) and course.location.course != 'templates', courses)
|
||||
courses = filter(lambda course: has_access(request.user, course.location) and course.location.course != 'templates' and course.location.org!='' and course.location.course!='' and course.location.name!='', courses)
|
||||
|
||||
return render_to_response('index.html', {
|
||||
'new_course_template' : Location('i4x', 'edx', 'templates', 'course', 'Empty'),
|
||||
@@ -118,7 +119,8 @@ def index(request):
|
||||
course.location.org,
|
||||
course.location.course,
|
||||
course.location.name]))
|
||||
for course in courses]
|
||||
for course in courses],
|
||||
'user': request.user
|
||||
})
|
||||
|
||||
|
||||
@@ -272,6 +274,26 @@ def edit_unit(request, location):
|
||||
containing_section_locs = modulestore().get_parent_locations(containing_subsection.location)
|
||||
containing_section = modulestore().get_item(containing_section_locs[0])
|
||||
|
||||
# cdodge hack. We're having trouble previewing drafts via jump_to redirect
|
||||
# so let's generate the link url here
|
||||
|
||||
# need to figure out where this item is in the list of children as the preview will need this
|
||||
index =1
|
||||
for child in containing_subsection.get_children():
|
||||
if child.location == item.location:
|
||||
break
|
||||
index = index + 1
|
||||
|
||||
preview_lms_link = '//{preview}{lms_base}/courses/{org}/{course}/{course_name}/courseware/{section}/{subsection}/{index}'.format(
|
||||
preview='preview.',
|
||||
lms_base=settings.LMS_BASE,
|
||||
org=course.location.org,
|
||||
course=course.location.course,
|
||||
course_name=course.location.name,
|
||||
section=containing_section.location.name,
|
||||
subsection=containing_subsection.location.name,
|
||||
index=index)
|
||||
|
||||
unit_state = compute_unit_state(item)
|
||||
|
||||
try:
|
||||
@@ -697,8 +719,18 @@ def upload_asset(request, org, course, coursename):
|
||||
#then commit the content
|
||||
contentstore().save(content)
|
||||
del_cached_content(content.location)
|
||||
|
||||
# readback the saved content - we need the database timestamp
|
||||
readback = contentstore().find(content.location)
|
||||
|
||||
response = HttpResponse('Upload completed')
|
||||
response_payload = {'displayname' : content.name,
|
||||
'uploadDate' : get_date_display(readback.last_modified_at),
|
||||
'url' : StaticContent.get_url_path_from_location(content.location),
|
||||
'thumb_url' : StaticContent.get_url_path_from_location(thumbnail_content.location) if thumbnail_content is not None else None,
|
||||
'msg' : 'Upload completed'
|
||||
}
|
||||
|
||||
response = HttpResponse(json.dumps(response_payload))
|
||||
response['asset_url'] = StaticContent.get_url_path_from_location(content.location)
|
||||
return response
|
||||
|
||||
@@ -710,7 +742,7 @@ This view will return all CMS users who are editors for the specified course
|
||||
def manage_users(request, location):
|
||||
|
||||
# check that logged in user has permissions to this item
|
||||
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME):
|
||||
if not has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME) and not has_access(request.user, location, role=EDITOR_ROLE_NAME):
|
||||
raise PermissionDenied()
|
||||
|
||||
course_module = modulestore().get_item(location)
|
||||
@@ -720,7 +752,9 @@ def manage_users(request, location):
|
||||
'context_course': course_module,
|
||||
'staff': get_users_in_course_group_by_role(location, STAFF_ROLE_NAME),
|
||||
'add_user_postback_url' : reverse('add_user', args=[location]).rstrip('/'),
|
||||
'remove_user_postback_url' : reverse('remove_user', args=[location]).rstrip('/')
|
||||
'remove_user_postback_url' : reverse('remove_user', args=[location]).rstrip('/'),
|
||||
'allow_actions' : has_access(request.user, location, role=INSTRUCTOR_ROLE_NAME),
|
||||
'request_user_id' : request.user.id
|
||||
})
|
||||
|
||||
|
||||
@@ -845,6 +879,10 @@ def asset_index(request, org, course, name):
|
||||
|
||||
course_reference = StaticContent.compute_location(org, course, name)
|
||||
assets = contentstore().get_all_content_for_course(course_reference)
|
||||
|
||||
# sort in reverse upload date order
|
||||
assets = sorted(assets, key=lambda asset: asset['uploadDate'], reverse=True)
|
||||
|
||||
thumbnails = contentstore().get_all_content_thumbnails_for_course(course_reference)
|
||||
asset_display = []
|
||||
for asset in assets:
|
||||
|
||||
@@ -32,7 +32,8 @@ from xmodule.static_content import write_descriptor_styles, write_descriptor_js,
|
||||
MITX_FEATURES = {
|
||||
'USE_DJANGO_PIPELINE': True,
|
||||
'GITHUB_PUSH': False,
|
||||
'ENABLE_DISCUSSION_SERVICE': False
|
||||
'ENABLE_DISCUSSION_SERVICE': False,
|
||||
'AUTH_USE_MIT_CERTIFICATES' : False,
|
||||
}
|
||||
|
||||
# needed to use lms student app
|
||||
|
||||
16
cms/envs/dev_ike.py
Normal file
16
cms/envs/dev_ike.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# dev environment for ichuang/mit
|
||||
|
||||
# FORCE_SCRIPT_NAME = '/cms'
|
||||
|
||||
from .common import *
|
||||
from logsettings import get_logger_config
|
||||
from .dev import *
|
||||
import socket
|
||||
|
||||
MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES'] = True
|
||||
|
||||
MITX_FEATURES['USE_DJANGO_PIPELINE']=False # don't recompile scss
|
||||
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOCOL', 'https') # django 1.4 for nginx ssl proxy
|
||||
|
||||
|
||||
@@ -57,12 +57,12 @@ class CMS.Views.ModuleEdit extends Backbone.View
|
||||
data = @module.save()
|
||||
data.metadata = @metadata()
|
||||
@model.save(data).done( =>
|
||||
alert("Your changes have been saved.")
|
||||
showToastMessage("Your changes have been saved.", null, 3)
|
||||
@module = null
|
||||
@render()
|
||||
@$el.removeClass('editing')
|
||||
).fail( ->
|
||||
alert("There was an error saving your changes. Please try again.")
|
||||
showToastMessage("There was an error saving your changes. Please try again.", null, 3)
|
||||
)
|
||||
|
||||
clickCancelButton: (event) ->
|
||||
|
||||
@@ -117,6 +117,8 @@ class CMS.Views.UnitEdit extends Backbone.View
|
||||
@model.save()
|
||||
|
||||
deleteComponent: (event) =>
|
||||
if not confirm 'Are you sure you want to delete this component? This action cannot be undone.'
|
||||
return
|
||||
$component = $(event.currentTarget).parents('.component')
|
||||
$.post('/delete_item', {
|
||||
id: $component.data('id')
|
||||
@@ -183,6 +185,7 @@ class CMS.Views.UnitEdit.NameEdit extends Backbone.View
|
||||
# Treat the metadata dictionary as immutable
|
||||
metadata = $.extend({}, @model.get('metadata'))
|
||||
metadata.display_name = @$('.unit-display-name-input').val()
|
||||
$('.unit-location .editing .unit-name').html(metadata.display_name)
|
||||
@model.set('metadata', metadata)
|
||||
|
||||
class CMS.Views.UnitEdit.LocationState extends Backbone.View
|
||||
|
||||
@@ -80,11 +80,17 @@ $(document).ready(function() {
|
||||
});
|
||||
|
||||
function showImportSubmit(e) {
|
||||
$('.file-name').html($(this).val())
|
||||
$('.file-name-block').show();
|
||||
$('.import .choose-file-button').hide();
|
||||
$('.submit-button').show();
|
||||
$('.progress').show();
|
||||
var filepath = $(this).val();
|
||||
if(filepath.substr(filepath.length - 6, 6) == 'tar.gz') {
|
||||
$('.error-block').hide();
|
||||
$('.file-name').html($(this).val());
|
||||
$('.file-name-block').show();
|
||||
$('.import .choose-file-button').hide();
|
||||
$('.submit-button').show();
|
||||
$('.progress').show();
|
||||
} else {
|
||||
$('.error-block').html('File format not supported. Please upload a file with a <code>tar.gz</code> extension.').show();
|
||||
}
|
||||
}
|
||||
|
||||
function syncReleaseDate(e) {
|
||||
@@ -321,7 +327,7 @@ function startUpload(e) {
|
||||
|
||||
function resetUploadBar(){
|
||||
var percentVal = '0%';
|
||||
$('.upload-modal .progress-fill').width(percentVal)
|
||||
$('.upload-modal .progress-fill').width(percentVal);
|
||||
$('.upload-modal .progress-fill').html(percentVal);
|
||||
}
|
||||
|
||||
@@ -335,9 +341,21 @@ function displayFinishedUpload(xhr) {
|
||||
if(xhr.status = 200){
|
||||
markAsLoaded();
|
||||
}
|
||||
var resp = JSON.parse(xhr.responseText);
|
||||
$('.upload-modal .copy-button').attr('href', xhr.getResponseHeader('asset_url'));
|
||||
$('.upload-modal .progress-fill').html(xhr.responseText);
|
||||
$('.upload-modal .progress-fill').html(resp.msg);
|
||||
$('.upload-modal .choose-file-button').html('Load Another File').show();
|
||||
$('.upload-modal .progress-fill').width('100%');
|
||||
|
||||
// see if this id already exists, if so, then user must have updated an existing piece of content
|
||||
$("tr[data-id='" + resp.url + "']").remove();
|
||||
|
||||
var template = $('#new-asset-element').html();
|
||||
var html = Mustache.to_html(template, resp);
|
||||
$('table > tbody > tr:first').before(html);
|
||||
|
||||
$("tr[data-id='" + resp.url + "'] a.show-xml").toggle(showEmbeddableXML, hideEmbeddableXML);
|
||||
|
||||
}
|
||||
|
||||
function markAsLoaded() {
|
||||
@@ -483,7 +501,7 @@ function addNewCourse(e) {
|
||||
e.preventDefault();
|
||||
var $newCourse = $($('#new-course-template').html());
|
||||
$('.new-course-button').after($newCourse);
|
||||
$newCourse.find('.new-course-org').focus().select();
|
||||
$newCourse.find('.new-course-name').focus().select();
|
||||
$newCourse.find('.new-course-save').bind('click', saveNewCourse);
|
||||
$newCourse.find('.new-course-cancel').bind('click', cancelNewCourse);
|
||||
}
|
||||
@@ -491,14 +509,17 @@ function addNewCourse(e) {
|
||||
function saveNewCourse(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var $newCourse = $(this).closest('.new-course');
|
||||
|
||||
template = $(this).data('template');
|
||||
|
||||
org = $(this).prevAll('.new-course-org').val();
|
||||
number = $(this).prevAll('.new-course-number').val();
|
||||
display_name = $(this).prevAll('.new-course-name').val();
|
||||
org = $newCourse.find('.new-course-org').val();
|
||||
number = $newCourse.find('.new-course-number').val();
|
||||
display_name = $newCourse.find('.new-course-name').val();
|
||||
|
||||
if (org == '' || number == '' || display_name == ''){
|
||||
alert('You must specify all fields in order to create a new course.')
|
||||
alert('You must specify all fields in order to create a new course.');
|
||||
return;
|
||||
}
|
||||
|
||||
$.post('/create_new_course',
|
||||
|
||||
@@ -45,7 +45,7 @@ input.courseware-unit-search-input {
|
||||
}
|
||||
|
||||
header {
|
||||
height: 67px;
|
||||
height: 47px;
|
||||
|
||||
.item-details {
|
||||
float: left;
|
||||
@@ -80,6 +80,7 @@ input.courseware-unit-search-input {
|
||||
}
|
||||
|
||||
h4 {
|
||||
display: none;
|
||||
font-size: 12px;
|
||||
color: #878e9d;
|
||||
|
||||
@@ -102,6 +103,17 @@ input.courseware-unit-search-input {
|
||||
border-top-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.new-section {
|
||||
header {
|
||||
height: auto;
|
||||
@include clearfix;
|
||||
}
|
||||
|
||||
.expand-collapse-icon {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.new-section-name,
|
||||
@@ -114,12 +126,14 @@ input.courseware-unit-search-input {
|
||||
@include blue-button;
|
||||
padding: 2px 20px 5px;
|
||||
margin: 0 5px;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.new-section-name-cancel,
|
||||
.new-subsection-name-cancel {
|
||||
@include white-button;
|
||||
padding: 2px 20px 5px;
|
||||
color: #8891a1 !important;
|
||||
}
|
||||
|
||||
.dummy-calendar {
|
||||
|
||||
@@ -41,4 +41,57 @@
|
||||
display: block;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.new-course {
|
||||
padding: 15px 25px;
|
||||
margin-top: 20px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $darkGrey;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, .1);
|
||||
@include clearfix;
|
||||
|
||||
.row {
|
||||
margin-bottom: 15px;
|
||||
@include clearfix;
|
||||
}
|
||||
|
||||
.column {
|
||||
float: left;
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
.column:first-child {
|
||||
margin-right: 4%;
|
||||
}
|
||||
|
||||
.course-info {
|
||||
width: 600px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.new-course-org,
|
||||
.new-course-number,
|
||||
.new-course-name {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.new-course-name {
|
||||
font-size: 19px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.new-course-save {
|
||||
@include blue-button;
|
||||
}
|
||||
|
||||
.new-course-cancel {
|
||||
@include white-button;
|
||||
}
|
||||
}
|
||||
42
cms/static/sass/_import.scss
vendored
42
cms/static/sass/_import.scss
vendored
@@ -11,11 +11,14 @@
|
||||
margin-right: 3%;
|
||||
font-size: 14px;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 20px;
|
||||
font-size: 18px;
|
||||
h2 {
|
||||
font-weight: 700;
|
||||
font-size: 19px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
color: $error-red;
|
||||
}
|
||||
|
||||
p + p {
|
||||
@@ -39,12 +42,17 @@
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.file-name-block {
|
||||
.file-name-block,
|
||||
.error-block {
|
||||
display: none;
|
||||
margin-bottom: 15px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.error-block {
|
||||
color: $error-red;
|
||||
}
|
||||
|
||||
.choose-file-button {
|
||||
@include blue-button;
|
||||
padding: 10px 50px 11px;
|
||||
@@ -67,4 +75,28 @@
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
display: none;
|
||||
width: 350px;
|
||||
height: 30px;
|
||||
margin: 30px auto 10px;
|
||||
border: 1px solid $blue;
|
||||
|
||||
&.loaded {
|
||||
border-color: #66b93d;
|
||||
|
||||
.progress-fill {
|
||||
background: #66b93d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
width: 0%;
|
||||
height: 30px;
|
||||
background: $blue;
|
||||
color: #fff;
|
||||
line-height: 48px;
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@
|
||||
border: 1px solid #d1ddec;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
@include transition(border-color .15s);
|
||||
@include transition(none);
|
||||
|
||||
&:hover {
|
||||
border-color: #6696d7;
|
||||
@@ -253,6 +253,8 @@
|
||||
|
||||
.CodeMirror {
|
||||
border: 1px solid #3c3c3c;
|
||||
background: #fff;
|
||||
color: #3c3c3c;
|
||||
}
|
||||
|
||||
h3 {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
@import 'reset';
|
||||
@import 'mixins';
|
||||
|
||||
@import "fonts";
|
||||
@import "variables";
|
||||
@import "cms_mixins";
|
||||
@import "base";
|
||||
|
||||
@@ -1,10 +1,38 @@
|
||||
<%inherit file="base.html" />
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%block name="bodyclass">assets</%block>
|
||||
<%block name="title">CMS Courseware Overview</%block>
|
||||
<%block name="title">Courseware Assets</%block>
|
||||
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
|
||||
<%block name="jsextra">
|
||||
<script src="${static.url('js/vendor/mustache.js')}"></script>
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
|
||||
<script type="text/template" id="new-asset-element">
|
||||
<tr data-id='{{url}}'>
|
||||
<td class="thumb-col">
|
||||
<div class="thumb">
|
||||
{{#thumb_url}}
|
||||
<img src="{{thumb_url}}">
|
||||
{{/thumb_url}}
|
||||
</div>
|
||||
</td>
|
||||
<td class="name-col">
|
||||
<a href="{{url}}" class="filename">{{displayname}}</a>
|
||||
<div class="embeddable-xml"></div>
|
||||
</td>
|
||||
<td class="date-col">
|
||||
{{uploadDate}}
|
||||
</td>
|
||||
<td class="embed-col">
|
||||
<a class="show-xml" href="{{url}}">XML</a>
|
||||
</td>
|
||||
</tr>
|
||||
</script>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="inner-wrapper">
|
||||
<h1>Asset Library</h1>
|
||||
@@ -22,9 +50,9 @@
|
||||
<th class="embed-col">Embed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody id="asset_table_body">
|
||||
% for asset in assets:
|
||||
<tr>
|
||||
<tr data-id="${asset['url']}">
|
||||
<td class="thumb-col">
|
||||
<div class="thumb">
|
||||
% if asset['thumb_url'] is not None:
|
||||
|
||||
@@ -11,21 +11,23 @@
|
||||
<h1>Import</h1>
|
||||
<article class="import-overview">
|
||||
<div class="description">
|
||||
<h3>Importing a new course will delete all course content currently associated with your course
|
||||
and replace it with the contents of the uploaded file.</h3>
|
||||
<h2>Please <a href="https://edge.edx.org/courses/edX/edx101/edX_Studio_Reference/about" target="_blank">read the documentation</a> before attempting an import!</h2>
|
||||
<p><strong>Importing a new course will delete all content currently associated with your course
|
||||
and replace it with the contents of the uploaded file.</strong></p>
|
||||
<p>File uploads must be zip files containing, at a minimum, a <code>course.xml</code> file.</p>
|
||||
<p>Please note that if your course has any problems with auto-generated <code>url_name</code> nodes,
|
||||
re-importing your course could cause the loss of student data associated with those problems.</p>
|
||||
</div>
|
||||
<form action="${reverse('import_course', kwargs=dict(org=context_course.location.org, course=context_course.location.course, name=context_course.location.name))}" method="post" enctype="multipart/form-data" class="import-form">
|
||||
<h2>Course to import:</h2>
|
||||
<a href="#" class="choose-file-button">Choose File</a>
|
||||
<p class="error-block"></p>
|
||||
<a href="#" class="choose-file-button">Choose File</a>
|
||||
<p class="file-name-block"><span class="file-name"></span><a href="#" class="choose-file-button-inline">change</a></p>
|
||||
<input type="file" name="course-data" class="file-input">
|
||||
<input type="submit" value="Replace my course with the one above" class="submit-button">
|
||||
<div class="progress" style="position:relative; margin-top:5px; width:250px; height:15px; border: 1px solid #ddd; padding: 1px; border-radius: 3px; display:none;">
|
||||
<div class="bar" style="background-color: #B4F5B4; width:0%; height:10px; border-radius: 3px;"></div>
|
||||
<div class="percent" style="margin-top:5px;">0%</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill"></div>
|
||||
<div class="percent">0%</div>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
@@ -37,29 +39,35 @@
|
||||
<script>
|
||||
(function() {
|
||||
|
||||
var bar = $('.bar');
|
||||
var bar = $('.progress-bar');
|
||||
var fill = $('.progress-fill');
|
||||
var percent = $('.percent');
|
||||
var status = $('#status');
|
||||
var submitBtn = $('.submit-button');
|
||||
|
||||
$('form').ajaxForm({
|
||||
beforeSend: function() {
|
||||
status.empty();
|
||||
var percentVal = '0%';
|
||||
bar.width(percentVal)
|
||||
bar.show();
|
||||
fill.width(percentVal);
|
||||
percent.html(percentVal);
|
||||
submitBtn.hide();
|
||||
},
|
||||
uploadProgress: function(event, position, total, percentComplete) {
|
||||
var percentVal = percentComplete + '%';
|
||||
bar.width(percentVal)
|
||||
fill.width(percentVal);
|
||||
percent.html(percentVal);
|
||||
},
|
||||
complete: function(xhr) {
|
||||
if (xhr.status == 200) {
|
||||
alert('Your import has been successful.');
|
||||
alert('Your import was successful.');
|
||||
window.location = '${successful_import_redirect_url}';
|
||||
}
|
||||
else
|
||||
alert('Your import has failed.\n\n' + xhr.responseText);
|
||||
submitBtn.show();
|
||||
bar.hide();
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -4,16 +4,29 @@
|
||||
|
||||
<%block name="header_extras">
|
||||
<script type="text/template" id="new-course-template">
|
||||
<section class="courseware-section new-course">
|
||||
<header>
|
||||
<div class="item-details">
|
||||
<h3 class="course-info">
|
||||
<input type="text" placeholder="Organization" class="new-course-org" />
|
||||
<input type="text" placeholder="Course Number" class="new-course-number" />
|
||||
<input type="text" placeholder="Course Name" class="new-course-name" />
|
||||
<a href="#" class="new-course-save" data-template="${new_course_template}">Save</a><a href="#" class="new-course-cancel">Cancel</a></h3>
|
||||
</div>
|
||||
</header>
|
||||
<section class="new-course">
|
||||
<div class="item-details">
|
||||
<form class="course-info">
|
||||
<div class="row">
|
||||
<label>Course Name</label>
|
||||
<input type="text" class="new-course-name" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<label>Organization</label>
|
||||
<input type="text" class="new-course-org" />
|
||||
</div>
|
||||
<div class="column">
|
||||
<label>Course Number</label>
|
||||
<input type="text" class="new-course-number" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<a href="#" class="new-course-save" data-template="${new_course_template}">Save</a>
|
||||
<a href="#" class="new-course-cancel">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</script>
|
||||
</%block>
|
||||
@@ -23,6 +36,7 @@
|
||||
<div class="inner-wrapper">
|
||||
<h1>My Courses</h1>
|
||||
<article class="my-classes">
|
||||
% if user.is_active:
|
||||
<a href="#" class="new-course-button"><span class="plus-icon"></span> New Course</a>
|
||||
<ul class="class-list">
|
||||
%for course, url in courses:
|
||||
@@ -35,8 +49,15 @@
|
||||
-->
|
||||
</a>
|
||||
</li>
|
||||
%endfor
|
||||
%endfor
|
||||
</ul>
|
||||
% else:
|
||||
<div class='warn-msg'>
|
||||
<p>
|
||||
In order to start authoring courses using edX studio, please click on the activation link in your email.
|
||||
</p>
|
||||
</div>
|
||||
% endif
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -55,10 +55,11 @@
|
||||
if(json.success) {
|
||||
location.href = "${reverse('index')}";
|
||||
} else if($('#login_error').length == 0) {
|
||||
$('#login_form').prepend('<div id="login_error">Email or password is incorrect.</div>');
|
||||
$('#login_form').prepend('<div id="login_error">' + json.value + '</div>');
|
||||
$('#login_error').slideDown(150);
|
||||
} else {
|
||||
$('#login_error').stop().slideDown(150);
|
||||
$('#login_error').html(json.value);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -16,20 +16,26 @@
|
||||
<span class="plus-icon"></span>New User
|
||||
</a>
|
||||
</div>
|
||||
%if allow_actions:
|
||||
<div class="new-user-form">
|
||||
<label>email: </label><input type="text" id="email" class="email-input" autocomplete="off" placeholder="email@example.com">
|
||||
<a href="#" id="add_user" class="add-button">save</a>
|
||||
<a href="#" class="cancel-button">cancel</a>
|
||||
</div>
|
||||
%endif
|
||||
<div>
|
||||
<ol class="user-list">
|
||||
% for user in staff:
|
||||
<li>
|
||||
<span class="user-name">${user.username}</span>
|
||||
<span class="user-email">${user.email}</span>
|
||||
%if allow_actions :
|
||||
<div class="item-actions">
|
||||
<a href="#" class="delete-button"><span class="delete-icon"></span></a>
|
||||
%if request_user_id != user.id:
|
||||
<a href="#" class="delete-button remove-user" data-id="${user.email}"><span class="delete-icon"></span></a>
|
||||
%endif
|
||||
</div>
|
||||
%endif
|
||||
</li>
|
||||
% endfor
|
||||
</ol>
|
||||
|
||||
@@ -3,28 +3,24 @@
|
||||
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
<%block name="content">
|
||||
<section class="container activation">
|
||||
|
||||
<section class="message">
|
||||
%if not already_active:
|
||||
<h1 class="valid">Activation Complete!</h1>
|
||||
%else:
|
||||
<h1>Account already active!</h1>
|
||||
%endif
|
||||
<hr class="horizontal-divider">
|
||||
|
||||
<p>
|
||||
|
||||
<p style='padding-top:100px; text-align:center;'>
|
||||
%if not already_active:
|
||||
Thanks for activating your account.
|
||||
Thanks for activating your account.
|
||||
%else:
|
||||
This account has already been activated.
|
||||
%endif
|
||||
|
||||
|
||||
%if user_logged_in:
|
||||
Visit your <a href="${reverse('dashboard')}">dashboard</a> to see your courses.
|
||||
Visit your <a href="/">dashboard</a> to see your courses.
|
||||
%else:
|
||||
You can now <a href="#login-modal" rel="leanModal">login</a>.
|
||||
%endif
|
||||
</p>
|
||||
</section>
|
||||
</section>
|
||||
</%block>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<%inherit file="base.html" />
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
|
||||
<%block name="title">Sign up</%block>
|
||||
<%block name="bodyclass">no-header</%block>
|
||||
|
||||
@@ -82,7 +84,7 @@
|
||||
submit_data,
|
||||
function(json) {
|
||||
if(json.success) {
|
||||
$('#register').html(json.value);
|
||||
location.href = "${reverse('index')}";
|
||||
} else {
|
||||
$('#register_error').html(json.value).stop().slideDown(150);
|
||||
}
|
||||
|
||||
@@ -108,9 +108,7 @@
|
||||
<span class="folder-icon"></span>
|
||||
<span class="subsection-name"><span class="subsection-name-value">${subsection.display_name}</span></span>
|
||||
</a>
|
||||
<ol>
|
||||
${units.enum_units(subsection, actions=False, selected=unit.location)}
|
||||
</ol>
|
||||
${units.enum_units(subsection, actions=False, selected=unit.location)}
|
||||
</li>
|
||||
</ol>
|
||||
</li>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<a href="${reverse('course_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}" class="class-name">${context_course.display_name}</a>
|
||||
<ul class="class-nav">
|
||||
<li><a href="${reverse('course_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}" id='courseware-tab'>Courseware</a></li>
|
||||
<li><a href="${reverse('static_pages', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, coursename=ctx_loc.name))}" id='pages-tab' class="wip-box">Pages</a></li>
|
||||
<li><a href="${reverse('static_pages', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, coursename=ctx_loc.name))}" id='pages-tab' style="display:none">Pages</a></li>
|
||||
<li><a href="${reverse('asset_index', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}" id='assets-tab'>Assets</a></li>
|
||||
<li><a href="${reverse('manage_users', kwargs=dict(location=ctx_loc))}" id='users-tab'>Users</a></li>
|
||||
<li><a href="${reverse('import_course', kwargs=dict(org=ctx_loc.org, course=ctx_loc.course, name=ctx_loc.name))}" id='import-tab'>Import</a></li>
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
% if metadata:
|
||||
<%
|
||||
import hashlib
|
||||
hlskey = hashlib.md5(module.location.url()).hexdigest()
|
||||
%>
|
||||
<section class="metadata_edit">
|
||||
<h3>Metadata</h3>
|
||||
<ul>
|
||||
% for keyname in editable_metadata_fields:
|
||||
<li><label>${keyname}:</label> <input type='text' data-metadata-name='${keyname}' value='${metadata[keyname]}' size='60' /></li>
|
||||
<li>
|
||||
% if keyname=='source_code':
|
||||
<a href="#hls-modal-${hlskey}" style="color:yellow;" id="hls-trig-${hlskey}" >Edit High Level Source</a>
|
||||
% else:
|
||||
<label>${keyname}:</label>
|
||||
<input type='text' data-metadata-name='${keyname}' value='${metadata[keyname]}' size='60' />
|
||||
% endif
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
|
||||
% if 'source_code' in editable_metadata_fields:
|
||||
<%include file="source-edit.html" />
|
||||
% endif
|
||||
|
||||
</section>
|
||||
% endif
|
||||
|
||||
112
cms/templates/widgets/source-edit.html
Normal file
112
cms/templates/widgets/source-edit.html
Normal file
@@ -0,0 +1,112 @@
|
||||
<%
|
||||
import hashlib
|
||||
hlskey = hashlib.md5(module.location.url()).hexdigest()
|
||||
%>
|
||||
|
||||
<section id="hls-modal-${hlskey}" class="upload-modal modal" style="width:90%!important; left:5%!important; margin-left:0px!important; height:90%; overflow:auto; background:#ECF7D3;" >
|
||||
<a href="#" class="close-button"><span class="close-icon"></span></a>
|
||||
<div id="hls-div">
|
||||
<header>
|
||||
<h2>High Level Source Editing</h2>
|
||||
</header>
|
||||
|
||||
<form id="hls-form">
|
||||
<section class="source-edit">
|
||||
<textarea name="" data-metadata-name="source_code" class="source-edit-box hls-data" rows="8" cols="40">${metadata['source_code']|h}</textarea>
|
||||
</section>
|
||||
<div class="submit">
|
||||
<button type="reset" class="hls-compile">Save & Compile to edX XML</button>
|
||||
<button type="reset" class="hls-save">Save</button>
|
||||
<button type="reset" class="hls-refresh">Refresh</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script type="text/javascript" src="/static/js/vendor/CodeMirror/stex.js"></script>
|
||||
<script type="text/javascript">
|
||||
$('#hls-trig-${hlskey}').leanModal({ top:40, overlay:0.8, closeButton: ".close-button"});
|
||||
|
||||
$('#hls-modal-${hlskey}').data('editor',CodeMirror.fromTextArea($('#hls-modal-${hlskey}').find('.hls-data')[0], {lineNumbers: true, mode: 'stex'}));
|
||||
|
||||
$('#hls-trig-${hlskey}').click(function(){slow_refresh_hls($('#hls-modal-${hlskey}'))})
|
||||
|
||||
// refresh button
|
||||
|
||||
$('#hls-modal-${hlskey}').find('.hls-refresh').click(function(){refresh_hls($('#hls-modal-${hlskey}'))});
|
||||
|
||||
function refresh_hls(el){
|
||||
el.data('editor').refresh();
|
||||
}
|
||||
|
||||
function slow_refresh_hls(el){
|
||||
el.delay(200).queue(function(){
|
||||
refresh_hls(el);
|
||||
$(this).dequeue();
|
||||
});
|
||||
// resize the codemirror box
|
||||
h = el.height();
|
||||
el.find('.CodeMirror-scroll').height(h-100);
|
||||
}
|
||||
|
||||
// compile & save button
|
||||
|
||||
$('#hls-modal-${hlskey}').find('.hls-compile').click(compile_hls_${hlskey});
|
||||
|
||||
function compile_hls_${hlskey}(){
|
||||
|
||||
editor = $('#hls-modal-${hlskey}').data('editor')
|
||||
var myquery = { latexin: editor.getValue() };
|
||||
|
||||
$.ajax({
|
||||
url: '${metadata.get('source_processor_url','https://qisx.mit.edu:5443/latex2edx')}',
|
||||
type: 'GET',
|
||||
contentType: 'application/json',
|
||||
data: escape(JSON.stringify(myquery)),
|
||||
crossDomain: true,
|
||||
dataType: 'jsonp',
|
||||
jsonpCallback: 'process_return_${hlskey}',
|
||||
beforeSend: function (xhr) { xhr.setRequestHeader ("Authorization", "Basic eHFhOmFnYXJ3YWw="); },
|
||||
timeout : 7000,
|
||||
success: function(result) {
|
||||
console.log(result);
|
||||
},
|
||||
error: function() {
|
||||
alert('Error: cannot connect to latex2edx server');
|
||||
console.log('error!');
|
||||
}
|
||||
});
|
||||
// $('#hls-modal-${hlskey}').hide();
|
||||
}
|
||||
|
||||
function process_return_${hlskey}(datadict){
|
||||
// datadict is json of array with "xml" and "message"
|
||||
// if "xml" value is '' then the conversion failed
|
||||
xml = datadict.xml;
|
||||
console.log('xml:');
|
||||
console.log(xml);
|
||||
if (xml.length==0){
|
||||
alert('Conversion failed! error:'+ datadict.message);
|
||||
}else{
|
||||
set_raw_edit_box(xml,'${hlskey}');
|
||||
save_hls($('#hls-modal-${hlskey}'));
|
||||
}
|
||||
}
|
||||
|
||||
function set_raw_edit_box(data,key){
|
||||
// get the codemirror editor for the raw-edit-box
|
||||
// it's a CodeMirror-wrap class element
|
||||
$('#hls-modal-'+key).closest('.component').find('.CodeMirror-wrap')[0].CodeMirror.setValue(data);
|
||||
}
|
||||
|
||||
// save button
|
||||
|
||||
$('#hls-modal-${hlskey}').find('.hls-save').click(function(){save_hls($('#hls-modal-${hlskey}'))});
|
||||
|
||||
function save_hls(el){
|
||||
el.find('.hls-data').val(el.data('editor').getValue());
|
||||
el.closest('.component').find('.save-button').click();
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -18,7 +18,7 @@ This def will enumerate through a passed in subsection and list all of the units
|
||||
<div class="section-item ${selected_class}">
|
||||
<a href="${reverse('edit_unit', args=[unit.location])}" class="${unit_state}-item">
|
||||
<span class="${unit.category}-icon"></span>
|
||||
${unit.display_name}
|
||||
<span class="unit-name">${unit.display_name}</span>
|
||||
</a>
|
||||
% if actions:
|
||||
<div class="item-actions">
|
||||
|
||||
@@ -215,6 +215,52 @@ def ssl_dn_extract_info(dn):
|
||||
else:
|
||||
return None
|
||||
return (user, email, fullname)
|
||||
|
||||
|
||||
def ssl_get_cert_from_request(request):
|
||||
"""
|
||||
Extract user information from certificate, if it exists, returning (user, email, fullname).
|
||||
Else return None.
|
||||
"""
|
||||
certkey = "SSL_CLIENT_S_DN" # specify the request.META field to use
|
||||
|
||||
cert = request.META.get(certkey, '')
|
||||
if not cert:
|
||||
cert = request.META.get('HTTP_' + certkey, '')
|
||||
if not cert:
|
||||
try:
|
||||
# try the direct apache2 SSL key
|
||||
cert = request._req.subprocess_env.get(certkey, '')
|
||||
except Exception:
|
||||
return ''
|
||||
|
||||
return cert
|
||||
|
||||
(user, email, fullname) = ssl_dn_extract_info(cert)
|
||||
return (user, email, fullname)
|
||||
|
||||
|
||||
def ssl_login_shortcut(fn):
|
||||
"""
|
||||
Python function decorator for login procedures, to allow direct login
|
||||
based on existing ExternalAuth record and MIT ssl certificate.
|
||||
"""
|
||||
def wrapped(*args, **kwargs):
|
||||
if not settings.MITX_FEATURES['AUTH_USE_MIT_CERTIFICATES']:
|
||||
return fn(*args, **kwargs)
|
||||
request = args[0]
|
||||
cert = ssl_get_cert_from_request(request)
|
||||
if not cert: # no certificate information - show normal login window
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
(user, email, fullname) = ssl_dn_extract_info(cert)
|
||||
return external_login_or_signup(request,
|
||||
external_id=email,
|
||||
external_domain="ssl:MIT",
|
||||
credentials=cert,
|
||||
email=email,
|
||||
fullname=fullname)
|
||||
return wrapped
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@@ -234,17 +280,7 @@ def ssl_login(request):
|
||||
|
||||
Else continues on with student.views.index, and no authentication.
|
||||
"""
|
||||
certkey = "SSL_CLIENT_S_DN" # specify the request.META field to use
|
||||
|
||||
cert = request.META.get(certkey, '')
|
||||
if not cert:
|
||||
cert = request.META.get('HTTP_' + certkey, '')
|
||||
if not cert:
|
||||
try:
|
||||
# try the direct apache2 SSL key
|
||||
cert = request._req.subprocess_env.get(certkey, '')
|
||||
except Exception:
|
||||
cert = None
|
||||
cert = ssl_get_cert_from_request(request)
|
||||
|
||||
if not cert:
|
||||
# no certificate information - go onward to main index
|
||||
|
||||
@@ -63,7 +63,7 @@ class StudentInputError(Exception):
|
||||
|
||||
|
||||
class LoncapaResponse(object):
|
||||
'''
|
||||
"""
|
||||
Base class for CAPA responsetypes. Each response type (ie a capa question,
|
||||
which is part of a capa problem) is represented as a subclass,
|
||||
which should provide the following methods:
|
||||
@@ -89,7 +89,7 @@ class LoncapaResponse(object):
|
||||
- required_attributes : list of required attributes (each a string) on the main response XML stanza
|
||||
- hint_tag : xhtml tag identifying hint associated with this response inside hintgroup
|
||||
|
||||
'''
|
||||
"""
|
||||
__metaclass__ = abc.ABCMeta # abc = Abstract Base Class
|
||||
|
||||
response_tag = None
|
||||
@@ -164,6 +164,8 @@ class LoncapaResponse(object):
|
||||
- renderer : procedure which produces HTML given an ElementTree
|
||||
'''
|
||||
tree = etree.Element('span') # render ourself as a <span> + our content
|
||||
if self.xml.get('inline',''): # problem author can make this span display:inline
|
||||
tree.set('class','inline')
|
||||
for item in self.xml:
|
||||
item_xhtml = renderer(item) # call provided procedure to do the rendering
|
||||
if item_xhtml is not None: tree.append(item_xhtml)
|
||||
|
||||
@@ -618,12 +618,14 @@ class CapaModule(XModule):
|
||||
if self.closed():
|
||||
event_info['failure'] = 'closed'
|
||||
self.system.track_function('reset_problem_fail', event_info)
|
||||
return "Problem is closed"
|
||||
return {'success': False,
|
||||
'error': "Problem is closed"}
|
||||
|
||||
if not self.lcp.done:
|
||||
event_info['failure'] = 'not_done'
|
||||
self.system.track_function('reset_problem_fail', event_info)
|
||||
return "Refresh the page and make an attempt before resetting."
|
||||
return {'success': False,
|
||||
'error': "Refresh the page and make an attempt before resetting."}
|
||||
|
||||
self.lcp.do_reset()
|
||||
if self.rerandomize in ["always", "onreset"]:
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
class @HTMLModule
|
||||
|
||||
constructor: (@element) ->
|
||||
@el = $(@element)
|
||||
@setCollapsibles()
|
||||
@el = $(@element)
|
||||
@setCollapsibles()
|
||||
MathJax.Hub.Queue ["Typeset", MathJax.Hub, @el[0]]
|
||||
|
||||
$: (selector) ->
|
||||
$(selector, @el)
|
||||
|
||||
23
common/lib/xmodule/xmodule/templates/html/latex_html.yaml
Normal file
23
common/lib/xmodule/xmodule/templates/html/latex_html.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: E-text Written in LaTeX
|
||||
source_processor_url: https://qisx.mit.edu:5443/latex2edx
|
||||
source_code: |
|
||||
\subsection{Example of E-text in LaTeX}
|
||||
|
||||
It is very convenient to write complex equations in LaTeX.
|
||||
|
||||
\begin{equation}
|
||||
x = \frac{-b\pm\sqrt{b^2-4*a*c}}{2a}
|
||||
\end{equation}
|
||||
|
||||
Seize the moment.
|
||||
|
||||
data: |
|
||||
<html>
|
||||
<h2>Example: E-text page</h2>
|
||||
<p>
|
||||
It is very convenient to write complex equations in LaTeX.
|
||||
</p>
|
||||
</html>
|
||||
children: []
|
||||
218
common/lib/xmodule/xmodule/templates/problem/latex_problem.yaml
Normal file
218
common/lib/xmodule/xmodule/templates/problem/latex_problem.yaml
Normal file
@@ -0,0 +1,218 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: Problem Written in LaTeX
|
||||
source_processor_url: https://qisx.mit.edu:5443/latex2edx
|
||||
source_code: |
|
||||
% Nearly any kind of edX problem can be authored using Latex as
|
||||
% the source language. Write latex as usual, including equations. The
|
||||
% key new feature is the \edXabox{} macro, which specifies an "Answer
|
||||
% Box" that queries students for a response, and specifies what the
|
||||
% epxected (correct) answer is.
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\subsection{Example "option" problem}
|
||||
|
||||
Where is the earth?
|
||||
|
||||
\edXabox{options='up','down' expect='down'}
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\subsection{Example "symbolic" problem}
|
||||
|
||||
What is Einstein's equation for the energy equivalent of a mass $m$?
|
||||
|
||||
\edXabox{type='symbolic' size='90' expect='m*c^2' }
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\subsection{Example "numerical" problem}
|
||||
|
||||
Estimate the energy savings (in J/y) if all the people
|
||||
($3\times 10^8$) in the U.~S. switched from U.~S. code to low flow
|
||||
shower heads.
|
||||
|
||||
\edXinline{Energy saved = }\edXabox{expect="0.52" type="numerical" tolerance='0.02' inline='1' } %
|
||||
\edXinline{~EJ/year}
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\subsection{Example "multiple choice" problem}
|
||||
|
||||
What color is a bannana?
|
||||
|
||||
\edXabox{ type="multichoice" expect="Yellow" options="Red","Green","Yellow","Blue" }
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\subsection{Example "string response" problem}
|
||||
|
||||
In what U.S. state is Detroit located?
|
||||
|
||||
\edXabox{ type="string" expect="Michigan" options="ci" }
|
||||
|
||||
An explanation of the answer can be provided by using the edXsolution
|
||||
macro. Click on "Show Answer" to see the solution.
|
||||
|
||||
\begin{edXsolution}
|
||||
Detroit is near Canada, but it is actually in the United States.
|
||||
\end{edXsolution}
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\subsection{Example "custom response" problem}
|
||||
|
||||
This problem demonstrates the use of a custom python script used for
|
||||
checking the answer.
|
||||
|
||||
\begin{edXscript}
|
||||
def sumtest(expect,ans):
|
||||
(a1,a2) = map(float,eval(ans))
|
||||
return (a1+a2)==10
|
||||
\end{edXscript}
|
||||
|
||||
Enter a python list of two numbers which sum to 10, eg [9,1]:
|
||||
|
||||
\edXabox{expect="[1,9]" type="custom" cfn="sumtest"}
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\subsection{Example image}
|
||||
|
||||
Include image by using the edXxml macro:
|
||||
|
||||
\edXxml{<img src="http://autoid.mit.edu/images/mit_dome.jpg"/>}
|
||||
|
||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||
\subsection{Example show/hide explanation}
|
||||
|
||||
Extra explanations can be tucked away behind a "showhide" toggle flag:
|
||||
|
||||
\edXshowhide{sh1}{More explanation}{This is a hidden explanation. It
|
||||
can contain equations: $\alpha = \frac{2}{\sqrt{1+\gamma}}$ }
|
||||
|
||||
This is some text after the showhide example.
|
||||
|
||||
data: |
|
||||
<?xml version="1.0"?>
|
||||
<problem>
|
||||
<text>
|
||||
<p>
|
||||
<h4>Example "option" problem</h4>
|
||||
</p>
|
||||
<p>
|
||||
Where is the earth? </p>
|
||||
<p>
|
||||
<optionresponse>
|
||||
<optioninput options="('up','down')" correct="down"/>
|
||||
</optionresponse>
|
||||
</p>
|
||||
<p>
|
||||
<h4>Example "symbolic" problem</h4>
|
||||
</p>
|
||||
<p>
|
||||
What is Einstein's equation for the energy equivalent of a mass [mathjaxinline]m[/mathjaxinline]? </p>
|
||||
<p>
|
||||
<symbolicresponse expect="m*c^2">
|
||||
<textline size="90" correct_answer="m*c^2" math="1"/>
|
||||
</symbolicresponse>
|
||||
</p>
|
||||
<p>
|
||||
<h4>Example "numerical" problem</h4>
|
||||
</p>
|
||||
<p>
|
||||
Estimate the energy savings (in J/y) if all the people ([mathjaxinline]3\times 10^8[/mathjaxinline]) in the U. S. switched from U. S. code to low flow shower heads. </p>
|
||||
<p>
|
||||
<p style="display:inline">Energy saved = </p>
|
||||
<numericalresponse inline="1" answer="0.52">
|
||||
<textline inline="1">
|
||||
<responseparam description="Numerical Tolerance" type="tolerance" default="0.02" name="tol"/>
|
||||
</textline>
|
||||
</numericalresponse>
|
||||
<p style="display:inline"> EJ/year</p>
|
||||
</p>
|
||||
<p>
|
||||
<h4>Example "multiple choice" problem</h4>
|
||||
</p>
|
||||
<p>
|
||||
What color is a bannana? </p>
|
||||
<p>
|
||||
<choiceresponse>
|
||||
<checkboxgroup>
|
||||
<choice correct="false" name="1">
|
||||
<text>Red</text>
|
||||
</choice>
|
||||
<choice correct="false" name="2">
|
||||
<text>Green</text>
|
||||
</choice>
|
||||
<choice correct="true" name="3">
|
||||
<text>Yellow</text>
|
||||
</choice>
|
||||
<choice correct="false" name="4">
|
||||
<text>Blue</text>
|
||||
</choice>
|
||||
</checkboxgroup>
|
||||
</choiceresponse>
|
||||
</p>
|
||||
<p>
|
||||
<h4>Example "string response" problem</h4>
|
||||
</p>
|
||||
<p>
|
||||
In what U.S. state is Detroit located? </p>
|
||||
<p>
|
||||
<stringresponse answer="Michigan">
|
||||
<textline/>
|
||||
</stringresponse>
|
||||
</p>
|
||||
<p>
|
||||
An explanation of the answer can be provided by using the edXsolution macro: </p>
|
||||
<p>
|
||||
<solution>
|
||||
<font color="blue">Answer: </font>
|
||||
<font color="blue">Detroit is near Canada, but it is actually in the United States. </font>
|
||||
</solution>
|
||||
</p>
|
||||
<p>
|
||||
<h4>Example "custom response" problem</h4>
|
||||
</p>
|
||||
<p>
|
||||
This problem demonstrates the use of a custom python script used for checking the answer. </p>
|
||||
<script type="text/python" system_path="python_lib">
|
||||
def sumtest(expect,ans):
|
||||
(a1,a2) = map(float,eval(ans))
|
||||
return (a1+a2)==10
|
||||
</script>
|
||||
<p>
|
||||
Enter a python list of two numbers which sum to 10, eg [9,1]: </p>
|
||||
<p>
|
||||
<customresponse cfn="sumtest" expect="[1,9]">
|
||||
<textline correct_answer="[1,9]"/>
|
||||
</customresponse>
|
||||
</p>
|
||||
<p>
|
||||
<h4>Example image</h4>
|
||||
</p>
|
||||
<p>
|
||||
Include image by using the edXxml macro: </p>
|
||||
<p>
|
||||
<img src="http://autoid.mit.edu/images/mit_dome.jpg"/>
|
||||
</p>
|
||||
<p>
|
||||
<h4>Example show/hide explanation</h4>
|
||||
</p>
|
||||
<p>
|
||||
Extra explanations can be tucked away behind a "showhide" toggle flag: </p>
|
||||
<p>
|
||||
<table class="wikitable collapsible collapsed">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th> More explanation [<a href="javascript:$('#sh1').toggle()" id="sh1l">show</a>]</th>
|
||||
</tr>
|
||||
<tr id="sh1" style="display:none">
|
||||
<td>
|
||||
<p>
|
||||
This is a hidden explanation. It can contain equations: [mathjaxinline]\alpha = \frac{2}{\sqrt {1+\gamma }}[/mathjaxinline] </p>
|
||||
<p>
|
||||
This is some text after the showhide example. </p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</p>
|
||||
</text>
|
||||
</problem>
|
||||
children: []
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
metadata:
|
||||
display_name: Problem with Adaptive Hint
|
||||
source_processor_url: https://qisx.mit.edu:5443/latex2edx
|
||||
source_code: |
|
||||
\subsection{Problem With Adaptive Hint}
|
||||
|
||||
% Adaptive hints are messages provided to students which depend on
|
||||
% student input. These hints are produced using a script embedded
|
||||
% within the problem (written in Python).
|
||||
%
|
||||
% Here is an example. This example uses LaTeX as a high-level
|
||||
% soure language for the problem. The problem can also be coded
|
||||
% directly in XML.
|
||||
|
||||
This problem demonstrates a question with hints, based on using the
|
||||
{\tt hintfn} method.
|
||||
|
||||
\begin{edXscript}
|
||||
def test_str(expect, ans):
|
||||
print expect, ans
|
||||
ans = ans.strip("'")
|
||||
ans = ans.strip('"')
|
||||
return expect == ans.lower()
|
||||
|
||||
def hint_fn(answer_ids, student_answers, new_cmap, old_cmap):
|
||||
aid = answer_ids[0]
|
||||
ans = str(student_answers[aid]).lower()
|
||||
print 'hint_fn called, ans=', ans
|
||||
hint = ''
|
||||
if 'java' in ans:
|
||||
hint = 'that is only good for drinking'
|
||||
elif 'perl' in ans:
|
||||
hint = 'not that rich'
|
||||
elif 'pascal' in ans:
|
||||
hint = 'that is a beatnick language'
|
||||
elif 'fortran' in ans:
|
||||
hint = 'those were the good days'
|
||||
elif 'clu' in ans:
|
||||
hint = 'you must be invariant'
|
||||
if hint:
|
||||
hint = "<font color='blue'>Hint: {0}</font>".format(hint)
|
||||
new_cmap.set_hint_and_mode(aid,hint,'always')
|
||||
\end{edXscript}
|
||||
|
||||
What is the best programming language that exists today? You may
|
||||
enter your answer in upper or lower case, with or without quotes.
|
||||
|
||||
\edXabox{type="custom" cfn='test_str' expect='python' hintfn='hint_fn'}
|
||||
|
||||
data: |
|
||||
<?xml version="1.0"?>
|
||||
<problem>
|
||||
<text>
|
||||
<p>
|
||||
<h4>Problem With Adaptive Hint</h4>
|
||||
</p>
|
||||
<p>
|
||||
This problem demonstrates a question with hints, based on using the <tt class="tt">hintfn</tt> method. </p>
|
||||
<script type="text/python" system_path="python_lib">
|
||||
def test_str(expect, ans):
|
||||
print expect, ans
|
||||
ans = ans.strip("'")
|
||||
ans = ans.strip('"')
|
||||
return expect == ans.lower()
|
||||
|
||||
def hint_fn(answer_ids, student_answers, new_cmap, old_cmap):
|
||||
aid = answer_ids[0]
|
||||
ans = str(student_answers[aid]).lower()
|
||||
print 'hint_fn called, ans=', ans
|
||||
hint = ''
|
||||
if 'java' in ans:
|
||||
hint = 'that is only good for drinking'
|
||||
elif 'perl' in ans:
|
||||
hint = 'not that rich'
|
||||
elif 'pascal' in ans:
|
||||
hint = 'that is a beatnick language'
|
||||
elif 'fortran' in ans:
|
||||
hint = 'those were the good days'
|
||||
elif 'clu' in ans:
|
||||
hint = 'you must be invariant'
|
||||
if hint:
|
||||
hint = "<font color='blue'>Hint: {0}</font>".format(hint)
|
||||
new_cmap.set_hint_and_mode(aid,hint,'always')
|
||||
</script>
|
||||
<p>
|
||||
What is the best programming language that exists today? You may enter your answer in upper or lower case, with or without quotes. </p>
|
||||
<p>
|
||||
<customresponse cfn="test_str" expect="python">
|
||||
<textline correct_answer="python"/>
|
||||
<hintgroup hintfn="hint_fn"/>
|
||||
</customresponse>
|
||||
</p>
|
||||
</text>
|
||||
</problem>
|
||||
children: []
|
||||
@@ -369,6 +369,9 @@ class ResourceTemplates(object):
|
||||
return []
|
||||
|
||||
for template_file in resource_listdir(__name__, dirname):
|
||||
if not template_file.endswith('.yaml'):
|
||||
log.warning("Skipping unknown template file %s" % template_file)
|
||||
continue
|
||||
template_content = resource_string(__name__, os.path.join(dirname, template_file))
|
||||
template = yaml.load(template_content)
|
||||
templates.append(Template(**template))
|
||||
|
||||
182
common/static/js/vendor/CodeMirror/stex.js
vendored
Normal file
182
common/static/js/vendor/CodeMirror/stex.js
vendored
Normal file
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Author: Constantin Jucovschi (c.jucovschi@jacobs-university.de)
|
||||
* Licence: MIT
|
||||
*/
|
||||
|
||||
CodeMirror.defineMode("stex", function(cmCfg, modeCfg)
|
||||
{
|
||||
function pushCommand(state, command) {
|
||||
state.cmdState.push(command);
|
||||
}
|
||||
|
||||
function peekCommand(state) {
|
||||
if (state.cmdState.length>0)
|
||||
return state.cmdState[state.cmdState.length-1];
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
function popCommand(state) {
|
||||
if (state.cmdState.length>0) {
|
||||
var plug = state.cmdState.pop();
|
||||
plug.closeBracket();
|
||||
}
|
||||
}
|
||||
|
||||
function applyMostPowerful(state) {
|
||||
var context = state.cmdState;
|
||||
for (var i = context.length - 1; i >= 0; i--) {
|
||||
var plug = context[i];
|
||||
if (plug.name=="DEFAULT")
|
||||
continue;
|
||||
return plug.styleIdentifier();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function addPluginPattern(pluginName, cmdStyle, brackets, styles) {
|
||||
return function () {
|
||||
this.name=pluginName;
|
||||
this.bracketNo = 0;
|
||||
this.style=cmdStyle;
|
||||
this.styles = styles;
|
||||
this.brackets = brackets;
|
||||
|
||||
this.styleIdentifier = function(content) {
|
||||
if (this.bracketNo<=this.styles.length)
|
||||
return this.styles[this.bracketNo-1];
|
||||
else
|
||||
return null;
|
||||
};
|
||||
this.openBracket = function(content) {
|
||||
this.bracketNo++;
|
||||
return "bracket";
|
||||
};
|
||||
this.closeBracket = function(content) {
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
var plugins = new Array();
|
||||
|
||||
plugins["importmodule"] = addPluginPattern("importmodule", "tag", "{[", ["string", "builtin"]);
|
||||
plugins["documentclass"] = addPluginPattern("documentclass", "tag", "{[", ["", "atom"]);
|
||||
plugins["usepackage"] = addPluginPattern("documentclass", "tag", "[", ["atom"]);
|
||||
plugins["begin"] = addPluginPattern("documentclass", "tag", "[", ["atom"]);
|
||||
plugins["end"] = addPluginPattern("documentclass", "tag", "[", ["atom"]);
|
||||
|
||||
plugins["DEFAULT"] = function () {
|
||||
this.name="DEFAULT";
|
||||
this.style="tag";
|
||||
|
||||
this.styleIdentifier = function(content) {
|
||||
};
|
||||
this.openBracket = function(content) {
|
||||
};
|
||||
this.closeBracket = function(content) {
|
||||
};
|
||||
};
|
||||
|
||||
function setState(state, f) {
|
||||
state.f = f;
|
||||
}
|
||||
|
||||
function normal(source, state) {
|
||||
if (source.match(/^\\[a-zA-Z@]+/)) {
|
||||
var cmdName = source.current();
|
||||
cmdName = cmdName.substr(1, cmdName.length-1);
|
||||
var plug;
|
||||
if (plugins.hasOwnProperty(cmdName)) {
|
||||
plug = plugins[cmdName];
|
||||
} else {
|
||||
plug = plugins["DEFAULT"];
|
||||
}
|
||||
plug = new plug();
|
||||
pushCommand(state, plug);
|
||||
setState(state, beginParams);
|
||||
return plug.style;
|
||||
}
|
||||
|
||||
// escape characters
|
||||
if (source.match(/^\\[$&%#{}_]/)) {
|
||||
return "tag";
|
||||
}
|
||||
|
||||
// white space control characters
|
||||
if (source.match(/^\\[,;!\/]/)) {
|
||||
return "tag";
|
||||
}
|
||||
|
||||
var ch = source.next();
|
||||
if (ch == "%") {
|
||||
// special case: % at end of its own line; stay in same state
|
||||
if (!source.eol()) {
|
||||
setState(state, inCComment);
|
||||
}
|
||||
return "comment";
|
||||
}
|
||||
else if (ch=='}' || ch==']') {
|
||||
plug = peekCommand(state);
|
||||
if (plug) {
|
||||
plug.closeBracket(ch);
|
||||
setState(state, beginParams);
|
||||
} else
|
||||
return "error";
|
||||
return "bracket";
|
||||
} else if (ch=='{' || ch=='[') {
|
||||
plug = plugins["DEFAULT"];
|
||||
plug = new plug();
|
||||
pushCommand(state, plug);
|
||||
return "bracket";
|
||||
}
|
||||
else if (/\d/.test(ch)) {
|
||||
source.eatWhile(/[\w.%]/);
|
||||
return "atom";
|
||||
}
|
||||
else {
|
||||
source.eatWhile(/[\w-_]/);
|
||||
return applyMostPowerful(state);
|
||||
}
|
||||
}
|
||||
|
||||
function inCComment(source, state) {
|
||||
source.skipToEnd();
|
||||
setState(state, normal);
|
||||
return "comment";
|
||||
}
|
||||
|
||||
function beginParams(source, state) {
|
||||
var ch = source.peek();
|
||||
if (ch == '{' || ch == '[') {
|
||||
var lastPlug = peekCommand(state);
|
||||
var style = lastPlug.openBracket(ch);
|
||||
source.eat(ch);
|
||||
setState(state, normal);
|
||||
return "bracket";
|
||||
}
|
||||
if (/[ \t\r]/.test(ch)) {
|
||||
source.eat(ch);
|
||||
return null;
|
||||
}
|
||||
setState(state, normal);
|
||||
lastPlug = peekCommand(state);
|
||||
if (lastPlug) {
|
||||
popCommand(state);
|
||||
}
|
||||
return normal(source, state);
|
||||
}
|
||||
|
||||
return {
|
||||
startState: function() { return { f:normal, cmdState:[] }; },
|
||||
copyState: function(s) { return { f: s.f, cmdState: s.cmdState.slice(0, s.cmdState.length) }; },
|
||||
|
||||
token: function(stream, state) {
|
||||
var t = state.f(stream, state);
|
||||
var w = stream.current();
|
||||
return t;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
CodeMirror.defineMIME("text/x-stex", "stex");
|
||||
CodeMirror.defineMIME("text/x-latex", "stex");
|
||||
613
common/static/js/vendor/mustache.js
vendored
Normal file
613
common/static/js/vendor/mustache.js
vendored
Normal file
@@ -0,0 +1,613 @@
|
||||
/*!
|
||||
* mustache.js - Logic-less {{mustache}} templates with JavaScript
|
||||
* http://github.com/janl/mustache.js
|
||||
*/
|
||||
|
||||
var Mustache;
|
||||
|
||||
(function (exports) {
|
||||
if (typeof module !== "undefined") {
|
||||
module.exports = exports; // CommonJS
|
||||
} else if (typeof define === "function") {
|
||||
define(exports); // AMD
|
||||
} else {
|
||||
Mustache = exports; // <script>
|
||||
}
|
||||
}(function () {
|
||||
var exports = {};
|
||||
|
||||
exports.name = "mustache.js";
|
||||
exports.version = "0.5.1-dev";
|
||||
exports.tags = ["{{", "}}"];
|
||||
|
||||
exports.parse = parse;
|
||||
exports.clearCache = clearCache;
|
||||
exports.compile = compile;
|
||||
exports.compilePartial = compilePartial;
|
||||
exports.render = render;
|
||||
|
||||
exports.Scanner = Scanner;
|
||||
exports.Context = Context;
|
||||
exports.Renderer = Renderer;
|
||||
|
||||
// This is here for backwards compatibility with 0.4.x.
|
||||
exports.to_html = function (template, view, partials, send) {
|
||||
var result = render(template, view, partials);
|
||||
|
||||
if (typeof send === "function") {
|
||||
send(result);
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
var whiteRe = /\s*/;
|
||||
var spaceRe = /\s+/;
|
||||
var nonSpaceRe = /\S/;
|
||||
var eqRe = /\s*=/;
|
||||
var curlyRe = /\s*\}/;
|
||||
var tagRe = /#|\^|\/|>|\{|&|=|!/;
|
||||
|
||||
// Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
|
||||
// See https://github.com/janl/mustache.js/issues/189
|
||||
function testRe(re, string) {
|
||||
return RegExp.prototype.test.call(re, string);
|
||||
}
|
||||
|
||||
function isWhitespace(string) {
|
||||
return !testRe(nonSpaceRe, string);
|
||||
}
|
||||
|
||||
var isArray = Array.isArray || function (obj) {
|
||||
return Object.prototype.toString.call(obj) === "[object Array]";
|
||||
};
|
||||
|
||||
// OSWASP Guidelines: escape all non alphanumeric characters in ASCII space.
|
||||
var jsCharsRe = /[\x00-\x2F\x3A-\x40\x5B-\x60\x7B-\xFF\u2028\u2029]/gm;
|
||||
|
||||
function quote(text) {
|
||||
var escaped = text.replace(jsCharsRe, function (c) {
|
||||
return "\\u" + ('0000' + c.charCodeAt(0).toString(16)).slice(-4);
|
||||
});
|
||||
|
||||
return '"' + escaped + '"';
|
||||
}
|
||||
|
||||
function escapeRe(string) {
|
||||
return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
|
||||
}
|
||||
|
||||
var entityMap = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
"/": '/'
|
||||
};
|
||||
|
||||
function escapeHtml(string) {
|
||||
return String(string).replace(/[&<>"'\/]/g, function (s) {
|
||||
return entityMap[s];
|
||||
});
|
||||
}
|
||||
|
||||
// Export these utility functions.
|
||||
exports.isWhitespace = isWhitespace;
|
||||
exports.isArray = isArray;
|
||||
exports.quote = quote;
|
||||
exports.escapeRe = escapeRe;
|
||||
exports.escapeHtml = escapeHtml;
|
||||
|
||||
function Scanner(string) {
|
||||
this.string = string;
|
||||
this.tail = string;
|
||||
this.pos = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the tail is empty (end of string).
|
||||
*/
|
||||
Scanner.prototype.eos = function () {
|
||||
return this.tail === "";
|
||||
};
|
||||
|
||||
/**
|
||||
* Tries to match the given regular expression at the current position.
|
||||
* Returns the matched text if it can match, `null` otherwise.
|
||||
*/
|
||||
Scanner.prototype.scan = function (re) {
|
||||
var match = this.tail.match(re);
|
||||
|
||||
if (match && match.index === 0) {
|
||||
this.tail = this.tail.substring(match[0].length);
|
||||
this.pos += match[0].length;
|
||||
return match[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Skips all text until the given regular expression can be matched. Returns
|
||||
* the skipped string, which is the entire tail of this scanner if no match
|
||||
* can be made.
|
||||
*/
|
||||
Scanner.prototype.scanUntil = function (re) {
|
||||
var match, pos = this.tail.search(re);
|
||||
|
||||
switch (pos) {
|
||||
case -1:
|
||||
match = this.tail;
|
||||
this.pos += this.tail.length;
|
||||
this.tail = "";
|
||||
break;
|
||||
case 0:
|
||||
match = null;
|
||||
break;
|
||||
default:
|
||||
match = this.tail.substring(0, pos);
|
||||
this.tail = this.tail.substring(pos);
|
||||
this.pos += pos;
|
||||
}
|
||||
|
||||
return match;
|
||||
};
|
||||
|
||||
function Context(view, parent) {
|
||||
this.view = view;
|
||||
this.parent = parent;
|
||||
this.clearCache();
|
||||
}
|
||||
|
||||
Context.make = function (view) {
|
||||
return (view instanceof Context) ? view : new Context(view);
|
||||
};
|
||||
|
||||
Context.prototype.clearCache = function () {
|
||||
this._cache = {};
|
||||
};
|
||||
|
||||
Context.prototype.push = function (view) {
|
||||
return new Context(view, this);
|
||||
};
|
||||
|
||||
Context.prototype.lookup = function (name) {
|
||||
var value = this._cache[name];
|
||||
|
||||
if (!value) {
|
||||
if (name === ".") {
|
||||
value = this.view;
|
||||
} else {
|
||||
var context = this;
|
||||
|
||||
while (context) {
|
||||
if (name.indexOf(".") > 0) {
|
||||
var names = name.split("."), i = 0;
|
||||
|
||||
value = context.view;
|
||||
|
||||
while (value && i < names.length) {
|
||||
value = value[names[i++]];
|
||||
}
|
||||
} else {
|
||||
value = context.view[name];
|
||||
}
|
||||
|
||||
if (value != null) {
|
||||
break;
|
||||
}
|
||||
|
||||
context = context.parent;
|
||||
}
|
||||
}
|
||||
|
||||
this._cache[name] = value;
|
||||
}
|
||||
|
||||
if (typeof value === "function") {
|
||||
value = value.call(this.view);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
function Renderer() {
|
||||
this.clearCache();
|
||||
}
|
||||
|
||||
Renderer.prototype.clearCache = function () {
|
||||
this._cache = {};
|
||||
this._partialCache = {};
|
||||
};
|
||||
|
||||
Renderer.prototype.compile = function (tokens, tags) {
|
||||
if (typeof tokens === "string") {
|
||||
tokens = parse(tokens, tags);
|
||||
}
|
||||
|
||||
var fn = compileTokens(tokens),
|
||||
self = this;
|
||||
|
||||
return function (view) {
|
||||
return fn(Context.make(view), self);
|
||||
};
|
||||
};
|
||||
|
||||
Renderer.prototype.compilePartial = function (name, tokens, tags) {
|
||||
this._partialCache[name] = this.compile(tokens, tags);
|
||||
return this._partialCache[name];
|
||||
};
|
||||
|
||||
Renderer.prototype.render = function (template, view) {
|
||||
var fn = this._cache[template];
|
||||
|
||||
if (!fn) {
|
||||
fn = this.compile(template);
|
||||
this._cache[template] = fn;
|
||||
}
|
||||
|
||||
return fn(view);
|
||||
};
|
||||
|
||||
Renderer.prototype._section = function (name, context, callback) {
|
||||
var value = context.lookup(name);
|
||||
|
||||
switch (typeof value) {
|
||||
case "object":
|
||||
if (isArray(value)) {
|
||||
var buffer = "";
|
||||
|
||||
for (var i = 0, len = value.length; i < len; ++i) {
|
||||
buffer += callback(context.push(value[i]), this);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
return value ? callback(context.push(value), this) : "";
|
||||
case "function":
|
||||
// TODO: The text should be passed to the callback plain, not rendered.
|
||||
var sectionText = callback(context, this),
|
||||
self = this;
|
||||
|
||||
var scopedRender = function (template) {
|
||||
return self.render(template, context);
|
||||
};
|
||||
|
||||
return value.call(context.view, sectionText, scopedRender) || "";
|
||||
default:
|
||||
if (value) {
|
||||
return callback(context, this);
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
Renderer.prototype._inverted = function (name, context, callback) {
|
||||
var value = context.lookup(name);
|
||||
|
||||
// From the spec: inverted sections may render text once based on the
|
||||
// inverse value of the key. That is, they will be rendered if the key
|
||||
// doesn't exist, is false, or is an empty list.
|
||||
if (value == null || value === false || (isArray(value) && value.length === 0)) {
|
||||
return callback(context, this);
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
Renderer.prototype._partial = function (name, context) {
|
||||
var fn = this._partialCache[name];
|
||||
|
||||
if (fn) {
|
||||
return fn(context, this);
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
Renderer.prototype._name = function (name, context, escape) {
|
||||
var value = context.lookup(name);
|
||||
|
||||
if (typeof value === "function") {
|
||||
value = value.call(context.view);
|
||||
}
|
||||
|
||||
var string = (value == null) ? "" : String(value);
|
||||
|
||||
if (escape) {
|
||||
return escapeHtml(string);
|
||||
}
|
||||
|
||||
return string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Low-level function that compiles the given `tokens` into a
|
||||
* function that accepts two arguments: a Context and a
|
||||
* Renderer. Returns the body of the function as a string if
|
||||
* `returnBody` is true.
|
||||
*/
|
||||
function compileTokens(tokens, returnBody) {
|
||||
var body = ['""'];
|
||||
var token, method, escape;
|
||||
|
||||
for (var i = 0, len = tokens.length; i < len; ++i) {
|
||||
token = tokens[i];
|
||||
|
||||
switch (token.type) {
|
||||
case "#":
|
||||
case "^":
|
||||
method = (token.type === "#") ? "_section" : "_inverted";
|
||||
body.push("r." + method + "(" + quote(token.value) + ", c, function (c, r) {\n" +
|
||||
" " + compileTokens(token.tokens, true) + "\n" +
|
||||
"})");
|
||||
break;
|
||||
case "{":
|
||||
case "&":
|
||||
case "name":
|
||||
escape = token.type === "name" ? "true" : "false";
|
||||
body.push("r._name(" + quote(token.value) + ", c, " + escape + ")");
|
||||
break;
|
||||
case ">":
|
||||
body.push("r._partial(" + quote(token.value) + ", c)");
|
||||
break;
|
||||
case "text":
|
||||
body.push(quote(token.value));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to a string body.
|
||||
body = "return " + body.join(" + ") + ";";
|
||||
|
||||
// Good for debugging.
|
||||
// console.log(body);
|
||||
|
||||
if (returnBody) {
|
||||
return body;
|
||||
}
|
||||
|
||||
// For great evil!
|
||||
return new Function("c, r", body);
|
||||
}
|
||||
|
||||
function escapeTags(tags) {
|
||||
if (tags.length === 2) {
|
||||
return [
|
||||
new RegExp(escapeRe(tags[0]) + "\\s*"),
|
||||
new RegExp("\\s*" + escapeRe(tags[1]))
|
||||
];
|
||||
}
|
||||
|
||||
throw new Error("Invalid tags: " + tags.join(" "));
|
||||
}
|
||||
|
||||
/**
|
||||
* Forms the given linear array of `tokens` into a nested tree structure
|
||||
* where tokens that represent a section have a "tokens" array property
|
||||
* that contains all tokens that are in that section.
|
||||
*/
|
||||
function nestTokens(tokens) {
|
||||
var tree = [];
|
||||
var collector = tree;
|
||||
var sections = [];
|
||||
var token, section;
|
||||
|
||||
for (var i = 0; i < tokens.length; ++i) {
|
||||
token = tokens[i];
|
||||
|
||||
switch (token.type) {
|
||||
case "#":
|
||||
case "^":
|
||||
token.tokens = [];
|
||||
sections.push(token);
|
||||
collector.push(token);
|
||||
collector = token.tokens;
|
||||
break;
|
||||
case "/":
|
||||
if (sections.length === 0) {
|
||||
throw new Error("Unopened section: " + token.value);
|
||||
}
|
||||
|
||||
section = sections.pop();
|
||||
|
||||
if (section.value !== token.value) {
|
||||
throw new Error("Unclosed section: " + section.value);
|
||||
}
|
||||
|
||||
if (sections.length > 0) {
|
||||
collector = sections[sections.length - 1].tokens;
|
||||
} else {
|
||||
collector = tree;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
collector.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure there were no open sections when we're done.
|
||||
section = sections.pop();
|
||||
|
||||
if (section) {
|
||||
throw new Error("Unclosed section: " + section.value);
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines the values of consecutive text tokens in the given `tokens` array
|
||||
* to a single token.
|
||||
*/
|
||||
function squashTokens(tokens) {
|
||||
var lastToken;
|
||||
|
||||
for (var i = 0; i < tokens.length; ++i) {
|
||||
var token = tokens[i];
|
||||
|
||||
if (lastToken && lastToken.type === "text" && token.type === "text") {
|
||||
lastToken.value += token.value;
|
||||
tokens.splice(i--, 1); // Remove this token from the array.
|
||||
} else {
|
||||
lastToken = token;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Breaks up the given `template` string into a tree of token objects. If
|
||||
* `tags` is given here it must be an array with two string values: the
|
||||
* opening and closing tags used in the template (e.g. ["<%", "%>"]). Of
|
||||
* course, the default is to use mustaches (i.e. Mustache.tags).
|
||||
*/
|
||||
function parse(template, tags) {
|
||||
tags = tags || exports.tags;
|
||||
|
||||
var tagRes = escapeTags(tags);
|
||||
var scanner = new Scanner(template);
|
||||
|
||||
var tokens = [], // Buffer to hold the tokens
|
||||
spaces = [], // Indices of whitespace tokens on the current line
|
||||
hasTag = false, // Is there a {{tag}} on the current line?
|
||||
nonSpace = false; // Is there a non-space char on the current line?
|
||||
|
||||
// Strips all whitespace tokens array for the current line
|
||||
// if there was a {{#tag}} on it and otherwise only space.
|
||||
var stripSpace = function () {
|
||||
if (hasTag && !nonSpace) {
|
||||
while (spaces.length) {
|
||||
tokens.splice(spaces.pop(), 1);
|
||||
}
|
||||
} else {
|
||||
spaces = [];
|
||||
}
|
||||
|
||||
hasTag = false;
|
||||
nonSpace = false;
|
||||
};
|
||||
|
||||
var type, value, chr;
|
||||
|
||||
while (!scanner.eos()) {
|
||||
value = scanner.scanUntil(tagRes[0]);
|
||||
|
||||
if (value) {
|
||||
for (var i = 0, len = value.length; i < len; ++i) {
|
||||
chr = value.charAt(i);
|
||||
|
||||
if (isWhitespace(chr)) {
|
||||
spaces.push(tokens.length);
|
||||
} else {
|
||||
nonSpace = true;
|
||||
}
|
||||
|
||||
tokens.push({type: "text", value: chr});
|
||||
|
||||
if (chr === "\n") {
|
||||
stripSpace(); // Check for whitespace on the current line.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Match the opening tag.
|
||||
if (!scanner.scan(tagRes[0])) {
|
||||
break;
|
||||
}
|
||||
|
||||
hasTag = true;
|
||||
type = scanner.scan(tagRe) || "name";
|
||||
|
||||
// Skip any whitespace between tag and value.
|
||||
scanner.scan(whiteRe);
|
||||
|
||||
// Extract the tag value.
|
||||
if (type === "=") {
|
||||
value = scanner.scanUntil(eqRe);
|
||||
scanner.scan(eqRe);
|
||||
scanner.scanUntil(tagRes[1]);
|
||||
} else if (type === "{") {
|
||||
var closeRe = new RegExp("\\s*" + escapeRe("}" + tags[1]));
|
||||
value = scanner.scanUntil(closeRe);
|
||||
scanner.scan(curlyRe);
|
||||
scanner.scanUntil(tagRes[1]);
|
||||
} else {
|
||||
value = scanner.scanUntil(tagRes[1]);
|
||||
}
|
||||
|
||||
// Match the closing tag.
|
||||
if (!scanner.scan(tagRes[1])) {
|
||||
throw new Error("Unclosed tag at " + scanner.pos);
|
||||
}
|
||||
|
||||
tokens.push({type: type, value: value});
|
||||
|
||||
if (type === "name" || type === "{" || type === "&") {
|
||||
nonSpace = true;
|
||||
}
|
||||
|
||||
// Set the tags for the next time around.
|
||||
if (type === "=") {
|
||||
tags = value.split(spaceRe);
|
||||
tagRes = escapeTags(tags);
|
||||
}
|
||||
}
|
||||
|
||||
squashTokens(tokens);
|
||||
|
||||
return nestTokens(tokens);
|
||||
}
|
||||
|
||||
// The high-level clearCache, compile, compilePartial, and render functions
|
||||
// use this default renderer.
|
||||
var _renderer = new Renderer();
|
||||
|
||||
/**
|
||||
* Clears all cached templates and partials.
|
||||
*/
|
||||
function clearCache() {
|
||||
_renderer.clearCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* High-level API for compiling the given `tokens` down to a reusable
|
||||
* function. If `tokens` is a string it will be parsed using the given `tags`
|
||||
* before it is compiled.
|
||||
*/
|
||||
function compile(tokens, tags) {
|
||||
return _renderer.compile(tokens, tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* High-level API for compiling the `tokens` for the partial with the given
|
||||
* `name` down to a reusable function. If `tokens` is a string it will be
|
||||
* parsed using the given `tags` before it is compiled.
|
||||
*/
|
||||
function compilePartial(name, tokens, tags) {
|
||||
return _renderer.compilePartial(name, tokens, tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* High-level API for rendering the `template` using the given `view`. The
|
||||
* optional `partials` object may be given here for convenience, but note that
|
||||
* it will cause all partials to be re-compiled, thus hurting performance. Of
|
||||
* course, this only matters if you're going to render the same template more
|
||||
* than once. If so, it is best to call `compilePartial` before calling this
|
||||
* function and to leave the `partials` argument blank.
|
||||
*/
|
||||
function render(template, view, partials) {
|
||||
if (partials) {
|
||||
for (var name in partials) {
|
||||
compilePartial(name, partials[name]);
|
||||
}
|
||||
}
|
||||
|
||||
return _renderer.render(template, view);
|
||||
}
|
||||
|
||||
return exports;
|
||||
|
||||
}()));
|
||||
@@ -325,6 +325,12 @@ def jump_to(request, course_id, location):
|
||||
except NoPathToItem:
|
||||
raise Http404("This location is not in any class: {0}".format(location))
|
||||
|
||||
# cdodge: the CAS is generating a link to the LMS for 'subsections' (aka sequentials)
|
||||
# and there is no associated 'Position' for this. The above Path_to_location is returning None for Position
|
||||
# however, this ends up producing a 404 on the redirect
|
||||
if position is None:
|
||||
position = 0
|
||||
|
||||
# Rely on index to do all error handling and access control.
|
||||
return redirect('courseware_position',
|
||||
course_id=course_id,
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</div>
|
||||
<div class="row submit">
|
||||
<input name="submit" type="submit" value="Log In" class="log-in-submit-button" tabindex="3">
|
||||
<a href="#" class="forgot-button">Forgot password?</a>
|
||||
<a href="#forgot-password-modal" rel="leanModal" class="pwd-reset forgot-button">Forgot password?</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -56,4 +56,5 @@
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
<%include file="../signup_modal.html" />
|
||||
<%include file="../signup_modal.html" />
|
||||
<%include file="../forgot_password_modal.html" />
|
||||
Reference in New Issue
Block a user