Commit 0d2a0c70 by Brian Wilson

merge out from origin/master

parents 1b8cdcd4 27cffb93
...@@ -7,3 +7,4 @@ python ...@@ -7,3 +7,4 @@ python
yuicompressor yuicompressor
node node
graphviz graphviz
mysql
import logging
import sys
from django.contrib.auth.models import User, Group
from django.core.exceptions import PermissionDenied
from xmodule.modulestore import Location
# define a couple of simple roles, we just need ADMIN and EDITOR now for our purposes
ADMIN_ROLE_NAME = 'admin'
EDITOR_ROLE_NAME = 'editor'
# we're just making a Django group for each location/role combo
# to do this we're just creating a Group name which is a formatted string
# of those two variables
def get_course_groupname_for_role(location, role):
loc = Location(location)
groupname = loc.course_id + ':' + role
return groupname
def get_users_in_course_group_by_role(location, role):
groupname = get_course_groupname_for_role(location, role)
group = Group.objects.get(name=groupname)
return group.user_set.all()
'''
Create all permission groups for a new course and subscribe the caller into those roles
'''
def create_all_course_groups(creator, location):
create_new_course_group(creator, location, ADMIN_GROUP_NAME)
create_new_course_group(creator, location, EDITOR_GROUP_NAME)
def create_new_course_group(creator, location, role):
groupname = get_course_groupname_for_role(location, role)
(group, created) =Group.get_or_create(name=groupname)
if created:
group.save()
creator.groups.add(group)
creator.save()
return
def add_user_to_course_group(caller, user, location, role):
# only admins can add/remove other users
if not is_user_in_course_group_role(caller, location, ADMIN_ROLE_NAME):
raise PermissionDenied
if user.is_active and user.is_authenticated:
groupname = get_course_groupname_for_role(location, role)
group = Group.objects.get(name=groupname)
user.groups.add(group)
user.save()
return True
return False
def get_user_by_email(email):
user = None
# try to look up user, return None if not found
try:
user = User.objects.get(email=email)
except:
pass
return user
def remove_user_from_course_group(caller, user, location, role):
# only admins can add/remove other users
if not is_user_in_course_group_role(caller, location, ADMIN_ROLE_NAME):
raise PermissionDenied
# see if the user is actually in that role, if not then we don't have to do anything
if is_user_in_course_group_role(user, location, role) == True:
groupname = get_course_groupname_for_role(location, role)
group = Group.objects.get(name=groupname)
user.groups.remove(group)
user.save()
def is_user_in_course_group_role(user, location, role):
if user.is_active and user.is_authenticated:
return user.groups.filter(name=get_course_groupname_for_role(location,role)).count() > 0
return False
from xmodule.templates import update_templates
update_templates()
...@@ -26,4 +26,4 @@ class Command(BaseCommand): ...@@ -26,4 +26,4 @@ class Command(BaseCommand):
print "Importing. Data_dir={data}, course_dirs={courses}".format( print "Importing. Data_dir={data}, course_dirs={courses}".format(
data=data_dir, data=data_dir,
courses=course_dirs) courses=course_dirs)
import_from_xml(modulestore(), data_dir, course_dirs) import_from_xml(modulestore(), data_dir, course_dirs, load_error_modules=False)
from xmodule.modulestore import Location
from xmodule.modulestore.django import modulestore
'''
cdodge: for a given Xmodule, return the course that it belongs to
NOTE: This makes a lot of assumptions about the format of the course location
Also we have to assert that this module maps to only one course item - it'll throw an
assert if not
'''
def get_course_location_for_item(location):
item_loc = Location(location)
# check to see if item is already a course, if so we can skip this
if item_loc.category != 'course':
# @hack! We need to find the course location however, we don't
# know the 'name' parameter in this context, so we have
# to assume there's only one item in this query even though we are not specifying a name
course_search_location = ['i4x', item_loc.org, item_loc.course, 'course', None]
courses = modulestore().get_items(course_search_location)
# make sure we found exactly one match on this above course search
found_cnt = len(courses)
if found_cnt == 0:
raise BaseException('Could not find course at {0}'.format(course_search_location))
if found_cnt > 1:
raise BaseException('Found more than one course at {0}. There should only be one!!!'.format(course_search_location))
location = courses[0].location
return location
...@@ -86,7 +86,12 @@ def export_to_github(course, commit_message, author_str=None): ...@@ -86,7 +86,12 @@ def export_to_github(course, commit_message, author_str=None):
If author_str is specified, uses it in the commit. If author_str is specified, uses it in the commit.
''' '''
course_dir = course.metadata.get('data_dir', course.location.course) course_dir = course.metadata.get('data_dir', course.location.course)
repo_settings = load_repo_settings(course_dir) try:
repo_settings = load_repo_settings(course_dir)
except InvalidRepo:
log.warning("Invalid repo {0}, not exporting data to xml".format(course_dir))
return
git_repo = setup_repo(repo_settings) git_repo = setup_repo(repo_settings)
fs = OSFS(git_repo.working_dir) fs = OSFS(git_repo.working_dir)
......
...@@ -19,6 +19,8 @@ DEFAULT_FILE_STORAGE = 'storages.backends.s3boto.S3BotoStorage' ...@@ -19,6 +19,8 @@ DEFAULT_FILE_STORAGE = 'storages.backends.s3boto.S3BotoStorage'
with open(ENV_ROOT / "cms.env.json") as env_file: with open(ENV_ROOT / "cms.env.json") as env_file:
ENV_TOKENS = json.load(env_file) ENV_TOKENS = json.load(env_file)
LMS_BASE = ENV_TOKENS.get('LMS_BASE')
SITE_NAME = ENV_TOKENS['SITE_NAME'] SITE_NAME = ENV_TOKENS['SITE_NAME']
LOG_DIR = ENV_TOKENS['LOG_DIR'] LOG_DIR = ENV_TOKENS['LOG_DIR']
......
...@@ -88,6 +88,8 @@ TEMPLATE_CONTEXT_PROCESSORS = ( ...@@ -88,6 +88,8 @@ TEMPLATE_CONTEXT_PROCESSORS = (
'django.core.context_processors.csrf', # necessary for csrf protection 'django.core.context_processors.csrf', # necessary for csrf protection
) )
LMS_BASE = None
################################# Jasmine ################################### ################################# Jasmine ###################################
JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee' JASMINE_TEST_DIRECTORY = PROJECT_ROOT + '/static/coffee'
...@@ -116,6 +118,7 @@ TEMPLATE_LOADERS = ( ...@@ -116,6 +118,7 @@ TEMPLATE_LOADERS = (
) )
MIDDLEWARE_CLASSES = ( MIDDLEWARE_CLASSES = (
'contentserver.middleware.StaticContentServer',
'django.middleware.cache.UpdateCacheMiddleware', 'django.middleware.cache.UpdateCacheMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
...@@ -128,7 +131,7 @@ MIDDLEWARE_CLASSES = ( ...@@ -128,7 +131,7 @@ MIDDLEWARE_CLASSES = (
'track.middleware.TrackMiddleware', 'track.middleware.TrackMiddleware',
'mitxmako.middleware.MakoMiddleware', 'mitxmako.middleware.MakoMiddleware',
'django.middleware.transaction.TransactionMiddleware', 'django.middleware.transaction.TransactionMiddleware'
) )
############################ SIGNAL HANDLERS ################################ ############################ SIGNAL HANDLERS ################################
...@@ -193,6 +196,7 @@ STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage' ...@@ -193,6 +196,7 @@ STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage'
# prep it for use in pipeline js # prep it for use in pipeline js
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
from xmodule.raw_module import RawDescriptor from xmodule.raw_module import RawDescriptor
from xmodule.error_module import ErrorDescriptor
js_file_dir = PROJECT_ROOT / "static" / "coffee" / "module" js_file_dir = PROJECT_ROOT / "static" / "coffee" / "module"
css_file_dir = PROJECT_ROOT / "static" / "sass" / "module" css_file_dir = PROJECT_ROOT / "static" / "sass" / "module"
module_styles_path = css_file_dir / "_module-styles.scss" module_styles_path = css_file_dir / "_module-styles.scss"
...@@ -208,7 +212,7 @@ for dir_ in (js_file_dir, css_file_dir): ...@@ -208,7 +212,7 @@ for dir_ in (js_file_dir, css_file_dir):
js_fragments = set() js_fragments = set()
css_fragments = defaultdict(set) css_fragments = defaultdict(set)
for descriptor in XModuleDescriptor.load_classes() + [RawDescriptor]: for _, descriptor in XModuleDescriptor.load_classes() + [(None, RawDescriptor), (None, ErrorDescriptor)]:
descriptor_js = descriptor.get_javascript() descriptor_js = descriptor.get_javascript()
module_js = descriptor.module_class.get_javascript() module_js = descriptor.module_class.get_javascript()
...@@ -321,6 +325,7 @@ INSTALLED_APPS = ( ...@@ -321,6 +325,7 @@ INSTALLED_APPS = (
# For CMS # For CMS
'contentstore', 'contentstore',
'auth',
'github_sync', 'github_sync',
'student', # misleading name due to sharing with lms 'student', # misleading name due to sharing with lms
......
...@@ -14,7 +14,6 @@ LOGGING = get_logger_config(ENV_ROOT / "log", ...@@ -14,7 +14,6 @@ LOGGING = get_logger_config(ENV_ROOT / "log",
tracking_filename="tracking.log", tracking_filename="tracking.log",
debug=True) debug=True)
MODULESTORE = { MODULESTORE = {
'default': { 'default': {
'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore', 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
...@@ -29,6 +28,17 @@ MODULESTORE = { ...@@ -29,6 +28,17 @@ MODULESTORE = {
} }
} }
# cdodge: This is the specifier for the MongoDB (using GridFS) backed static content store
# This is for static content for courseware, not system static content (e.g. javascript, css, edX branding, etc)
CONTENTSTORE = {
'ENGINE': 'xmodule.contentstore.mongo.MongoContentStore',
'OPTIONS': {
'host': 'localhost',
'db' : 'xcontent',
}
}
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
...@@ -36,6 +46,8 @@ DATABASES = { ...@@ -36,6 +46,8 @@ DATABASES = {
} }
} }
LMS_BASE = "http://localhost:8000"
REPOS = { REPOS = {
'edx4edx': { 'edx4edx': {
'branch': 'master', 'branch': 'master',
......
...@@ -2,14 +2,27 @@ class CMS.Models.Module extends Backbone.Model ...@@ -2,14 +2,27 @@ class CMS.Models.Module extends Backbone.Model
url: '/save_item' url: '/save_item'
defaults: defaults:
data: '' data: ''
children: ''
metadata: {}
loadModule: (element) -> loadModule: (element) ->
elt = $(element).find('.xmodule_edit').first() elt = $(element).find('.xmodule_edit').first()
@module = XModule.loadModule(elt) @module = XModule.loadModule(elt)
# find the metadata edit region which should be setup server side,
# so that we can wire up posting back those changes
@metadata_elt = $(element).find('.metadata_edit')
editUrl: -> editUrl: ->
"/edit_item?#{$.param(id: @get('id'))}" "/edit_item?#{$.param(id: @get('id'))}"
save: (args...) -> save: (args...) ->
@set(data: JSON.stringify(@module.save())) if @module @set(data: @module.save()) if @module
# cdodge: package up metadata which is separated into a number of input fields
# there's probably a better way to do this, but at least this lets me continue to move onwards
if @metadata_elt
_metadata = {}
# walk through the set of elments which have the 'xmetadata_name' attribute and
# build up a object to pass back to the server on the subsequent POST
_metadata[$(el).data("metadata-name")]=el.value for el in $('[data-metadata-name]', @metadata_elt)
@set(metadata: _metadata)
super(args...) super(args...)
class CMS.Models.NewModule extends Backbone.Model
url: '/clone_item'
newUrl: ->
"/new_item?#{$.param(parent_location: @get('parent_location'))}"
...@@ -7,7 +7,8 @@ class CMS.Views.Module extends Backbone.View ...@@ -7,7 +7,8 @@ class CMS.Views.Module extends Backbone.View
previewType = @$el.data('preview-type') previewType = @$el.data('preview-type')
moduleType = @$el.data('type') moduleType = @$el.data('type')
CMS.replaceView new CMS.Views.ModuleEdit CMS.replaceView new CMS.Views.ModuleEdit
model: new CMS.Models.Module model: new CMS.Models.Module
id: @$el.data('id') id: @$el.data('id')
type: if moduleType == 'None' then null else moduleType type: if moduleType == 'None' then null else moduleType
previewType: if previewType == 'None' then null else previewType previewType: if previewType == 'None' then null else previewType
class CMS.Views.ModuleAdd extends Backbone.View
tagName: 'section'
className: 'add-pane'
events:
'click .cancel': 'cancel'
'click .save': 'save'
initialize: ->
@$el.load @model.newUrl()
save: (event) ->
event.preventDefault()
@model.save({
name: @$el.find('.name').val()
template: $(event.target).data('template-id')
}, {
success: -> CMS.popView()
error: -> alert('Create failed')
})
cancel: (event) ->
event.preventDefault()
CMS.popView()
...@@ -13,18 +13,22 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -13,18 +13,22 @@ class CMS.Views.ModuleEdit extends Backbone.View
# Load preview modules # Load preview modules
XModule.loadModules('display') XModule.loadModules('display')
@$children = @$el.find('#sortable')
@enableDrag() @enableDrag()
enableDrag: -> enableDrag: =>
# Enable dragging things in the #sortable div (if there is one) # Enable dragging things in the #sortable div (if there is one)
if $("#sortable").length > 0 if @$children.length > 0
$("#sortable").sortable({ @$children.sortable(
placeholder: "ui-state-highlight" placeholder: "ui-state-highlight"
}) update: (event, ui) =>
$("#sortable").disableSelection(); @model.set(children: @$children.find('.module-edit').map(
(idx, el) -> $(el).data('id')
).toArray())
save: (event) -> )
@$children.disableSelection()
save: (event) =>
event.preventDefault() event.preventDefault()
@model.save().done((previews) => @model.save().done((previews) =>
alert("Your changes have been saved.") alert("Your changes have been saved.")
...@@ -35,7 +39,7 @@ class CMS.Views.ModuleEdit extends Backbone.View ...@@ -35,7 +39,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
) )
XModule.loadModules('display') XModule.loadModules('display')
).fail(-> ).fail( ->
alert("There was an error saving your changes. Please try again.") alert("There was an error saving your changes. Please try again.")
) )
......
class CMS.Views.Week extends Backbone.View class CMS.Views.Week extends Backbone.View
events: events:
'click .week-edit': 'edit' 'click .week-edit': 'edit'
'click .new-module': 'new'
initialize: -> initialize: ->
CMS.on('content.show', @resetHeight) CMS.on('content.show', @resetHeight)
...@@ -23,3 +24,9 @@ class CMS.Views.Week extends Backbone.View ...@@ -23,3 +24,9 @@ class CMS.Views.Week extends Backbone.View
resetHeight: => resetHeight: =>
@$el.height('') @$el.height('')
new: (event) =>
event.preventDefault()
CMS.replaceView new CMS.Views.ModuleAdd
model: new CMS.Models.NewModule
parent_location: @$el.data('id')
...@@ -141,11 +141,15 @@ textarea { ...@@ -141,11 +141,15 @@ textarea {
} }
} }
// .wip { .wip {
// outline: 1px solid #f00 !important; outline: 1px solid #f00 !important;
// position: relative; position: relative;
// } &:after {
content: "WIP";
.hidden { font-size: 8px;
display: none; padding: 2px;
background: #f00;
color: #fff;
@include position(absolute, 0px 0px 0 0);
}
} }
...@@ -10,5 +10,7 @@ ...@@ -10,5 +10,7 @@
<section class="main-content"> <section class="main-content">
</section> </section>
<%include file="widgets/upload_assets.html"/>
</section> </section>
</%block> </%block>
<%inherit file="base.html" />
<%block name="title">Course Editor Manager</%block>
<%include file="widgets/header.html"/>
<%block name="content">
<section class="main-container">
<h2>Course Editors</h2>
<ul>
% for user in editors:
<li>${user.email} (${user.username})</li>
% endfor
</ul>
<form action="add_user" id="addEditorsForm">
<label>email:&nbsp;</label><input type="text" name="email" placeholder="email@example.com..." />
<input type="submit" value="add editor" />
</form>
<div id="result"></div>
<script>
$("#addEditorsForm").submit(function(event) {
event.preventDefault();
var $form = $(this),
email = $form.find('input[name="email"]').val(),
url = $form.attr('action');
$.post(url, {email:email},
function(data) {
if(data['Status'] != 'OK')
$("#result").empty().append(data['ErrMsg']);
else
location.reload();
});
});
</script>
</section>
</%block>
<section>
<div>${parent_name}</div>
<div>${parent_location}</div>
<input type="text" class="name"/>
<div>
% for module_type, module_templates in templates:
<div>
<div>${module_type}</div>
<div>
% for template in module_templates:
<a class="save" data-template-id="${template.location.url()}">${template.display_name}</a>
% endfor
</div>
</div>
% endfor
</div>
<a class='cancel'>Cancel</a>
</section>
...@@ -42,6 +42,9 @@ ...@@ -42,6 +42,9 @@
</section> </section>
</section> </section>
${contents} ${contents}
% if lms_link is not None:
<a class="lms-link" href="${lms_link}">View in LMS</a>
% endif
<section class="previews"> <section class="previews">
% for preview in previews: % for preview in previews:
<section class="preview"> <section class="preview">
...@@ -49,7 +52,7 @@ ...@@ -49,7 +52,7 @@
</section> </section>
% endfor % endfor
</section> </section>
<div class="actions wip"> <div class="actions">
<a href="" class="save-update">Save &amp; Update</a> <a href="" class="save-update">Save &amp; Update</a>
<a href="#" class="cancel">Cancel</a> <a href="#" class="cancel">Cancel</a>
</div> </div>
......
<%include file="metadata-edit.html" />
<section class="html-edit"> <section class="html-edit">
<textarea name="" class="edit-box" rows="8" cols="40">${data}</textarea> <textarea name="" class="edit-box" rows="8" cols="40">${data}</textarea>
</section> </section>
% if metadata:
<section class="metadata_edit">
<h3>Metadata</h3>
<ul>
% for keyname in editable_metadata_fields:
<li>${keyname}: <input type='text' data-metadata-name='${keyname}' value='${metadata[keyname]}' size='60' /></li>
% endfor
</ul>
</section>
% endif
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
<form> <form>
<ul> <ul>
<li> <li>
<input type="text" name="" id="" placeholder="Moldule title" /> <input type="text" name="" id="" placeholder="Module title" />
</li> </li>
<li> <li>
<select> <select>
......
...@@ -60,39 +60,13 @@ ...@@ -60,39 +60,13 @@
data-type="${module.js_module_name}" data-type="${module.js_module_name}"
data-preview-type="${module.module_class.js_module_name}"> data-preview-type="${module.module_class.js_module_name}">
<a href="#" class="module-edit">${module.url_name}</a> <a href="#" class="module-edit">${module.display_name}</a>
<a href="#" class="draggable">handle</a>
</li> </li>
% endfor % endfor
<%include file="module-dropdown.html"/> <%include file="module-dropdown.html"/>
</ul> </ul>
</li> </li>
%endfor %endfor
<li class="wip">
<header>
<h1>Course Scratch Pad</h1>
</header>
<ul>
<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 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>
<%include file="module-dropdown.html"/>
</ul>
</li>
</ol> </ol>
<section class="new-section"> <section class="new-section">
......
<%include file="metadata-edit.html" />
<section class="raw-edit"> <section class="raw-edit">
<textarea name="" class="edit-box" rows="8" cols="40">${data | h}</textarea> <textarea name="" class="edit-box" rows="8" cols="40">${data | h}</textarea>
</section> </section>
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
</ul> </ul>
</section> </section>
<%include file="metadata-edit.html" />
<div class="content"> <div class="content">
<section class="modules"> <section class="modules">
<ol> <ol>
...@@ -39,7 +40,7 @@ ...@@ -39,7 +40,7 @@
<a href="#" class="module-edit" <a href="#" class="module-edit"
data-id="${child.location.url()}" data-id="${child.location.url()}"
data-type="${child.js_module_name}" data-type="${child.js_module_name}"
data-preview-type="${child.module_class.js_module_name}">${child.url_name}</a> data-preview-type="${child.module_class.js_module_name}">${child.display_name}</a>
<a href="#" class="draggable">handle</a> <a href="#" class="draggable">handle</a>
</li> </li>
%endfor %endfor
...@@ -48,61 +49,6 @@ ...@@ -48,61 +49,6 @@
</ol> </ol>
</section> </section>
<section class="scratch-pad wip">
<ol>
<li class="new-module">
<%include file="new-module.html"/>
</li>
<li>
<header>
<h2>Section Scratch</h2>
</header>
<ul>
<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 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>
</ul>
</li>
<li>
<header>
<h2>Course Scratch</h2>
</header>
<ul>
<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 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>
</ul>
</li>
</ol>
</section>
</div> </div>
</section> </section>
<section>
<div class="assset-upload">
You can upload file assets (such as images) to reference in your courseware
<form action="${upload_asset_callback_url}" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="Upload File">
</form>
<div class="progress" style="position:relative; width:400px; border: 1px solid #ddd; padding: 1px; border-radius: 3px;">
<div class="bar" style="background-color: #B4F5B4; width:0%; height:20px; border-radius: 3px;"></div>
<div class="percent">0%</div>
</div>
<div id="status"></div>
</div>
</section>
<script src="http://malsup.github.com/jquery.form.js"></script>
<script>
(function() {
var bar = $('.bar');
var percent = $('.percent');
var status = $('#status');
$('form').ajaxForm({
beforeSend: function() {
status.empty();
var percentVal = '0%';
bar.width(percentVal)
percent.html(percentVal);
},
uploadProgress: function(event, position, total, percentComplete) {
var percentVal = percentComplete + '%';
bar.width(percentVal)
percent.html(percentVal);
},
complete: function(xhr) {
status.html(xhr.responseText);
}
});
})();
</script>
<section class="week-new">
<header>
<div>
<h1 class="editable">Week 3</h1>
<p><a href="#">+ new goal</a></p>
</div>
<section class="goals">
<ul>
<li>
<p><strong>Please add a goal for this section</strong> </p>
</li>
</ul>
</section>
</header>
<section class="content">
<section class="filters">
<ul>
<li>
<label for="">Sort by</label>
<select>
<option value="">Recently Modified</option>
</select>
</li>
<li>
<label for="">Display</label>
<select>
<option value="">All content</option>
</select>
</li>
<li>
<select>
<option value="">Internal Only</option>
</select>
</li>
<li class="advanced">
<a href="#">Advanced filters</a>
</li>
<li>
<input type="search" name="" id="" value="" />
</li>
</ul>
</section>
<div>
<section class="modules empty">
<p>This are no groups or units in this section yet</p>
<a href="#">Add a Group</a>
<a href="#">Add a Unit</a>
</section>
<section class="scratch-pad">
<ol>
<li>
<header>
<h2>Section Scratch</h2>
</header>
<ul>
<li class="empty">
<p><a href="#">There are no units in this scratch yet. Add one</a></p>
</li>
</ul>
</li>
<li>
<header>
<h2>Course Scratch</h2>
</header>
<ul>
<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>
</li>
</ul>
</li>
<li class="new-module">
<%include file="new-module.html"/>
</li>
</ol>
</section>
</div>
</section>
</section>
...@@ -9,13 +9,24 @@ import django.contrib.auth.views ...@@ -9,13 +9,24 @@ import django.contrib.auth.views
urlpatterns = ('', urlpatterns = ('',
url(r'^$', 'contentstore.views.index', name='index'), url(r'^$', 'contentstore.views.index', name='index'),
url(r'^new_item$', 'contentstore.views.new_item', name='new_item'),
url(r'^edit_item$', 'contentstore.views.edit_item', name='edit_item'), url(r'^edit_item$', 'contentstore.views.edit_item', name='edit_item'),
url(r'^save_item$', 'contentstore.views.save_item', name='save_item'), url(r'^save_item$', 'contentstore.views.save_item', name='save_item'),
url(r'^clone_item$', 'contentstore.views.clone_item', name='clone_item'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)$', url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)$',
'contentstore.views.course_index', name='course_index'), 'contentstore.views.course_index', name='course_index'),
url(r'^github_service_hook$', 'github_sync.views.github_post_receive'), url(r'^github_service_hook$', 'github_sync.views.github_post_receive'),
url(r'^preview/modx/(?P<preview_id>[^/]*)/(?P<location>.*?)/(?P<dispatch>[^/]*)$', url(r'^preview/modx/(?P<preview_id>[^/]*)/(?P<location>.*?)/(?P<dispatch>[^/]*)$',
'contentstore.views.preview_dispatch', name='preview_dispatch') 'contentstore.views.preview_dispatch', name='preview_dispatch'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)/upload_asset$',
'contentstore.views.upload_asset', name='upload_asset'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)/manage_users$',
'contentstore.views.manage_users', name='manage_users'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)/add_user$',
'contentstore.views.add_user', name='add_user'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<name>[^/]+)/remove_user$',
'contentstore.views.remove_user', name='remove_user')
) )
# User creation and updating views # User creation and updating views
......
...@@ -107,3 +107,15 @@ def instance_key(model, instance_or_pk): ...@@ -107,3 +107,15 @@ def instance_key(model, instance_or_pk):
model._meta.module_name, model._meta.module_name,
getattr(instance_or_pk, 'pk', instance_or_pk), getattr(instance_or_pk, 'pk', instance_or_pk),
) )
def content_key(filename):
return 'content:%s' % (filename)
def set_cached_content(content):
cache.set(content_key(content.filename), content)
def get_cached_content(filename):
return cache.get(content_key(filename))
def del_cached_content(filename):
cache.delete(content_key(filename))
import logging
import time
from django.http import HttpResponse, Http404, HttpResponseNotModified
from xmodule.contentstore.django import contentstore
from xmodule.contentstore.content import StaticContent, XASSET_LOCATION_TAG
from cache_toolbox.core import get_cached_content, set_cached_content
from xmodule.exceptions import NotFoundError
class StaticContentServer(object):
def process_request(self, request):
# look to see if the request is prefixed with 'c4x' tag
if request.path.startswith('/' + XASSET_LOCATION_TAG):
# first look in our cache so we don't have to round-trip to the DB
content = get_cached_content(request.path)
if content is None:
# nope, not in cache, let's fetch from DB
try:
content = contentstore().find(request.path)
except NotFoundError:
raise Http404
# since we fetched it from DB, let's cache it going forward
set_cached_content(content)
else:
# @todo: we probably want to have 'cache hit' counters so we can
# measure the efficacy of our caches
pass
# see if the last-modified at hasn't changed, if not return a 302 (Not Modified)
# convert over the DB persistent last modified timestamp to a HTTP compatible
# timestamp, so we can simply compare the strings
last_modified_at_str = content.last_modified_at.strftime("%a, %d-%b-%Y %H:%M:%S GMT")
# see if the client has cached this content, if so then compare the
# timestamps, if they are the same then just return a 304 (Not Modified)
if 'HTTP_IF_MODIFIED_SINCE' in request.META:
if_modified_since = request.META['HTTP_IF_MODIFIED_SINCE']
if if_modified_since == last_modified_at_str:
return HttpResponseNotModified()
response = HttpResponse(content.data, content_type=content.content_type)
response['Last-Modified'] = last_modified_at_str
return response
"""A openid store using django cache"""
from openid.store.interface import OpenIDStore
from openid.store import nonce
from django.core.cache import cache
import logging
import time
DEFAULT_ASSOCIATIONS_TIMEOUT = 60
DEFAULT_NONCE_TIMEOUT = 600
ASSOCIATIONS_KEY_PREFIX = 'openid.provider.associations.'
NONCE_KEY_PREFIX = 'openid.provider.nonce.'
log = logging.getLogger('DjangoOpenIDStore')
def get_url_key(server_url):
key = ASSOCIATIONS_KEY_PREFIX + server_url
return key
def get_nonce_key(server_url, timestamp, salt):
key = '{prefix}{url}.{ts}.{salt}'.format(prefix=NONCE_KEY_PREFIX,
url=server_url,
ts=timestamp,
salt=salt)
return key
class DjangoOpenIDStore(OpenIDStore):
def __init__(self):
log.info('DjangoStore cache:' + str(cache.__class__))
def storeAssociation(self, server_url, assoc):
key = get_url_key(server_url)
log.info('storeAssociation {0}'.format(key))
associations = cache.get(key, {})
associations[assoc.handle] = assoc
cache.set(key, associations, DEFAULT_ASSOCIATIONS_TIMEOUT)
def getAssociation(self, server_url, handle=None):
key = get_url_key(server_url)
log.info('getAssociation {0}'.format(key))
associations = cache.get(key, {})
assoc = None
if handle is None:
# get best association
valid_assocs = [a for a in associations if a.getExpiresIn() > 0]
if valid_assocs:
valid_assocs.sort(lambda a: a.getExpiresIn(), reverse=True)
assoc = valid_assocs.sort[0]
else:
assoc = associations.get(handle)
# check expiration and remove if it has expired
if assoc and assoc.getExpiresIn() <= 0:
if handle is None:
cache.delete(key)
else:
associations.pop(handle)
cache.set(key, associations, DEFAULT_ASSOCIATIONS_TIMEOUT)
assoc = None
return assoc
def removeAssociation(self, server_url, handle):
key = get_url_key(server_url)
log.info('removeAssociation {0}'.format(key))
associations = cache.get(key, {})
removed = False
if associations:
if handle is None:
cache.delete(key)
removed = True
else:
assoc = associations.pop(handle)
if assoc:
cache.set(key, associations, DEFAULT_ASSOCIATIONS_TIMEOUT)
removed = True
return removed
def useNonce(self, server_url, timestamp, salt):
key = get_nonce_key(server_url, timestamp, salt)
log.info('useNonce {0}'.format(key))
if abs(timestamp - time.time()) > nonce.SKEW:
return False
anonce = cache.get(key)
found = False
if anonce is None:
cache.set(key, '-', DEFAULT_NONCE_TIMEOUT)
found = False
else:
found = True
return found
def cleanupNonces(self):
# not necesary, keys will timeout
return 0
def cleanupAssociations(self):
# not necesary, keys will timeout
return 0
...@@ -7,6 +7,7 @@ import string ...@@ -7,6 +7,7 @@ import string
import fnmatch import fnmatch
from external_auth.models import ExternalAuthMap from external_auth.models import ExternalAuthMap
from external_auth.djangostore import DjangoOpenIDStore
from django.conf import settings from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login from django.contrib.auth import REDIRECT_FIELD_NAME, authenticate, login
...@@ -30,7 +31,6 @@ from openid.consumer.consumer import SUCCESS ...@@ -30,7 +31,6 @@ from openid.consumer.consumer import SUCCESS
from openid.server.server import Server from openid.server.server import Server
from openid.server.trustroot import TrustRoot from openid.server.trustroot import TrustRoot
from openid.store.filestore import FileOpenIDStore
from openid.extensions import ax, sreg from openid.extensions import ax, sreg
import student.views as student_views import student.views as student_views
...@@ -271,10 +271,7 @@ def get_xrds_url(resource, request): ...@@ -271,10 +271,7 @@ def get_xrds_url(resource, request):
""" """
Return the XRDS url for a resource Return the XRDS url for a resource
""" """
host = request.META['HTTP_HOST'] host = request.get_host()
if not host.endswith('edx.org'):
return None
location = host + '/openid/provider/' + resource + '/' location = host + '/openid/provider/' + resource + '/'
...@@ -296,6 +293,8 @@ def add_openid_simple_registration(request, response, data): ...@@ -296,6 +293,8 @@ def add_openid_simple_registration(request, response, data):
sreg_data['email'] = data['email'] sreg_data['email'] = data['email']
elif field == 'fullname' and 'fullname' in data: elif field == 'fullname' and 'fullname' in data:
sreg_data['fullname'] = data['fullname'] sreg_data['fullname'] = data['fullname']
elif field == 'nickname' and 'nickname' in data:
sreg_data['nickname'] = data['nickname']
# construct sreg response # construct sreg response
sreg_response = sreg.SRegResponse.extractResponse(sreg_request, sreg_response = sreg.SRegResponse.extractResponse(sreg_request,
...@@ -400,7 +399,7 @@ def provider_login(request): ...@@ -400,7 +399,7 @@ def provider_login(request):
return default_render_failure(request, "Invalid OpenID request") return default_render_failure(request, "Invalid OpenID request")
# initialize store and server # initialize store and server
store = FileOpenIDStore('/tmp/openid_provider') store = DjangoOpenIDStore()
server = Server(store, endpoint) server = Server(store, endpoint)
# handle OpenID request # handle OpenID request
...@@ -489,13 +488,22 @@ def provider_login(request): ...@@ -489,13 +488,22 @@ def provider_login(request):
url = endpoint + urlquote(user.username) url = endpoint + urlquote(user.username)
response = openid_request.answer(True, None, url) response = openid_request.answer(True, None, url)
return provider_respond(server, # TODO: for CS50 we are forcibly returning the username
openid_request, # instead of fullname. In the OpenID simple registration
response, # extension, we don't have to return any fields we don't
{ # want to, even if they were marked as required by the
'fullname': profile.name, # Consumer. The behavior of what to do when there are
'email': user.email # missing fields is up to the Consumer. The proper change
}) # should only return the username, however this will likely
# break the CS50 client. Temporarily we will be returning
# username filling in for fullname in addition to username
# as sreg nickname.
results = {
'nickname': user.username,
'email': user.email,
'fullname': user.username
}
return provider_respond(server, openid_request, response, results)
request.session['openid_error'] = True request.session['openid_error'] = True
msg = "Login failed - Account not active for user {0}".format(username) msg = "Login failed - Account not active for user {0}".format(username)
......
...@@ -3,8 +3,7 @@ from staticfiles.storage import staticfiles_storage ...@@ -3,8 +3,7 @@ from staticfiles.storage import staticfiles_storage
from pipeline_mako import compressed_css, compressed_js from pipeline_mako import compressed_css, compressed_js
%> %>
<%def name='url(file)'> <%def name='url(file)'><%
<%
try: try:
url = staticfiles_storage.url(file) url = staticfiles_storage.url(file)
except: except:
......
"""
A tiny app that checks for a status message.
"""
from django.conf import settings
import json
import logging
import os
import sys
log = logging.getLogger(__name__)
def get_site_status_msg(course_id):
"""
Look for a file settings.STATUS_MESSAGE_PATH. If found, read it,
parse as json, and do the following:
* if there is a key 'global', include that in the result list.
* if course is not None, and there is a key for course.id, add that to the result list.
* return "<br/>".join(result)
Otherwise, return None.
If something goes wrong, returns None. ("is there a status msg?" logic is
not allowed to break the entire site).
"""
try:
if os.path.isfile(settings.STATUS_MESSAGE_PATH):
with open(settings.STATUS_MESSAGE_PATH) as f:
content = f.read()
else:
return None
status_dict = json.loads(content)
msg = status_dict.get('global', None)
if course_id in status_dict:
msg = msg + "<br>" if msg else ''
msg += status_dict[course_id]
return msg
except:
log.exception("Error while getting a status message.")
return None
from django.conf import settings
from django.test import TestCase
import os
from override_settings import override_settings
from tempfile import NamedTemporaryFile
from status import get_site_status_msg
# Get a name where we can put test files
TMP_FILE = NamedTemporaryFile(delete=False)
TMP_NAME = TMP_FILE.name
# Close it--we just want the path.
TMP_FILE.close()
@override_settings(STATUS_MESSAGE_PATH=TMP_NAME)
class TestStatus(TestCase):
"""Test that the get_site_status_msg function does the right thing"""
no_file = None
invalid_json = """{
"global" : "Hello, Globe",
}"""
global_only = """{
"global" : "Hello, Globe"
}"""
toy_only = """{
"edX/toy/2012_Fall" : "A toy story"
}"""
global_and_toy = """{
"global" : "Hello, Globe",
"edX/toy/2012_Fall" : "A toy story"
}"""
# json to use, expected results for course=None (e.g. homepage),
# for toy course, for full course. Note that get_site_status_msg
# is supposed to return global message even if course=None. The
# template just happens to not display it outside the courseware
# at the moment...
checks = [
(no_file, None, None, None),
(invalid_json, None, None, None),
(global_only, "Hello, Globe", "Hello, Globe", "Hello, Globe"),
(toy_only, None, "A toy story", None),
(global_and_toy, "Hello, Globe", "Hello, Globe<br>A toy story", "Hello, Globe"),
]
def setUp(self):
"""
Fake course ids, since we don't have to have full django
settings (common tests run without the lms settings imported)
"""
self.full_id = 'edX/full/2012_Fall'
self.toy_id = 'edX/toy/2012_Fall'
def create_status_file(self, contents):
"""
Write contents to settings.STATUS_MESSAGE_PATH.
"""
with open(settings.STATUS_MESSAGE_PATH, 'w') as f:
f.write(contents)
def remove_status_file(self):
"""Delete the status file if it exists"""
if os.path.exists(settings.STATUS_MESSAGE_PATH):
os.remove(settings.STATUS_MESSAGE_PATH)
def tearDown(self):
self.remove_status_file()
def test_get_site_status_msg(self):
"""run the tests"""
for (json_str, exp_none, exp_toy, exp_full) in self.checks:
self.remove_status_file()
if json_str:
self.create_status_file(json_str)
print "checking results for {0}".format(json_str)
print "course=None:"
self.assertEqual(get_site_status_msg(None), exp_none)
print "course=toy:"
self.assertEqual(get_site_status_msg(self.toy_id), exp_toy)
print "course=full:"
self.assertEqual(get_site_status_msg(self.full_id), exp_full)
from optparse import make_option
from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth.models import User, Group
class Command(BaseCommand):
option_list = BaseCommand.option_list + (
make_option('--list',
action='store_true',
dest='list',
default=False,
help='List available groups'),
make_option('--create',
action='store_true',
dest='create',
default=False,
help='Create the group if it does not exist'),
make_option('--remove',
action='store_true',
dest='remove',
default=False,
help='Remove the user from the group instead of adding it'),
)
args = '<user|email> <group>'
help = 'Add a user to a group'
def print_groups(self):
print 'Groups available:'
for group in Group.objects.all().distinct():
print ' ', group.name
def handle(self, *args, **options):
if options['list']:
self.print_groups()
return
if len(args) != 2:
raise CommandError('Usage is add_to_group {0}'.format(self.args))
name_or_email, group_name = args
if '@' in name_or_email:
user = User.objects.get(email=name_or_email)
else:
user = User.objects.get(username=name_or_email)
try:
group = Group.objects.get(name=group_name)
except Group.DoesNotExist:
if options['create']:
group = Group(name=group_name)
group.save()
else:
raise CommandError('Group {} does not exist'.format(group_name))
if options['remove']:
user.groups.remove(group)
else:
user.groups.add(group)
print 'Success!'
from optparse import make_option
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand, CommandError
import re
class Command(BaseCommand):
option_list = BaseCommand.option_list + (
make_option('--unset',
action='store_true',
dest='unset',
default=False,
help='Set is_staff to False instead of True'),
)
args = '<user|email> [user|email ...]>'
help = """
This command will set is_staff to true for one or more users.
Lookup by username or email address, assumes usernames
do not look like email addresses.
"""
def handle(self, *args, **options):
if len(args) < 1:
raise CommandError('Usage is set_staff {0}'.format(self.args))
for user in args:
if re.match('[^@]+@[^@]+\.[^@]+', user):
try:
v = User.objects.get(email=user)
except:
raise CommandError("User {0} does not exist".format(user))
else:
try:
v = User.objects.get(username=user)
except:
raise CommandError("User {0} does not exist".format(user))
if options['unset']:
v.is_staff = False
else:
v.is_staff = True
v.save()
print 'Success!'
...@@ -39,6 +39,8 @@ from collections import namedtuple ...@@ -39,6 +39,8 @@ from collections import namedtuple
from courseware.courses import get_courses_by_university from courseware.courses import get_courses_by_university
from courseware.access import has_access from courseware.access import has_access
from statsd import statsd
log = logging.getLogger("mitx.student") log = logging.getLogger("mitx.student")
Article = namedtuple('Article', 'title url author image deck publication publish_date') Article = namedtuple('Article', 'title url author image deck publication publish_date')
...@@ -52,6 +54,10 @@ def csrf_token(context): ...@@ -52,6 +54,10 @@ def csrf_token(context):
' name="csrfmiddlewaretoken" value="%s" /></div>' % (csrf_token)) ' name="csrfmiddlewaretoken" value="%s" /></div>' % (csrf_token))
# NOTE: This view is not linked to directly--it is called from
# branding/views.py:index(), which is cached for anonymous users.
# This means that it should always return the same thing for anon
# users. (in particular, no switching based on query params allowed)
def index(request, extra_context={}, user=None): def index(request, extra_context={}, user=None):
''' '''
Render the edX main page. Render the edX main page.
...@@ -200,7 +206,13 @@ def change_enrollment(request): ...@@ -200,7 +206,13 @@ def change_enrollment(request):
return {'success': False, return {'success': False,
'error': 'enrollment in {} not allowed at this time' 'error': 'enrollment in {} not allowed at this time'
.format(course.display_name)} .format(course.display_name)}
org, course_num, run=course_id.split("/")
statsd.increment("common.student.enrollment",
tags=["org:{0}".format(org),
"course:{0}".format(course_num),
"run:{0}".format(run)])
enrollment, created = CourseEnrollment.objects.get_or_create(user=user, course_id=course.id) enrollment, created = CourseEnrollment.objects.get_or_create(user=user, course_id=course.id)
return {'success': True} return {'success': True}
...@@ -208,6 +220,13 @@ def change_enrollment(request): ...@@ -208,6 +220,13 @@ def change_enrollment(request):
try: try:
enrollment = CourseEnrollment.objects.get(user=user, course_id=course_id) enrollment = CourseEnrollment.objects.get(user=user, course_id=course_id)
enrollment.delete() enrollment.delete()
org, course_num, run=course_id.split("/")
statsd.increment("common.student.unenrollment",
tags=["org:{0}".format(org),
"course:{0}".format(course_num),
"run:{0}".format(run)])
return {'success': True} return {'success': True}
except CourseEnrollment.DoesNotExist: except CourseEnrollment.DoesNotExist:
return {'success': False, 'error': 'You are not enrolled for this course.'} return {'success': False, 'error': 'You are not enrolled for this course.'}
...@@ -256,12 +275,19 @@ def login_user(request, error=""): ...@@ -256,12 +275,19 @@ def login_user(request, error=""):
log.info("Login success - {0} ({1})".format(username, email)) log.info("Login success - {0} ({1})".format(username, email))
try_change_enrollment(request) try_change_enrollment(request)
statsd.increment("common.student.successful_login")
return HttpResponse(json.dumps({'success': True})) return HttpResponse(json.dumps({'success': True}))
log.warning("Login failed - Account not active for user {0}".format(username)) log.warning("Login failed - Account not active for user {0}, resending activation".format(username))
reactivation_email_for_user(user)
not_activated_msg = "This account has not been activated. We have " + \
"sent another activation message. Please check your " + \
"e-mail for the activation instructions."
return HttpResponse(json.dumps({'success': False, return HttpResponse(json.dumps({'success': False,
'value': 'This account has not been activated. Please check your e-mail for the activation instructions.'})) 'value': not_activated_msg}))
@ensure_csrf_cookie @ensure_csrf_cookie
...@@ -457,7 +483,9 @@ def create_account(request, post_override=None): ...@@ -457,7 +483,9 @@ def create_account(request, post_override=None):
log.debug('bypassing activation email') log.debug('bypassing activation email')
login_user.is_active = True login_user.is_active = True
login_user.save() login_user.save()
statsd.increment("common.student.account_created")
js = {'success': True} js = {'success': True}
return HttpResponse(json.dumps(js), mimetype="application/json") return HttpResponse(json.dumps(js), mimetype="application/json")
...@@ -513,6 +541,17 @@ def password_reset(request): ...@@ -513,6 +541,17 @@ def password_reset(request):
''' Attempts to send a password reset e-mail. ''' ''' Attempts to send a password reset e-mail. '''
if request.method != "POST": if request.method != "POST":
raise Http404 raise Http404
# By default, Django doesn't allow Users with is_active = False to reset their passwords,
# but this bites people who signed up a long time ago, never activated, and forgot their
# password. So for their sake, we'll auto-activate a user for whome password_reset is called.
try:
user = User.objects.get(email=request.POST['email'])
user.is_active = True
user.save()
except:
log.exception("Tried to auto-activate user to enable password reset, but failed.")
form = PasswordResetForm(request.POST) form = PasswordResetForm(request.POST)
if form.is_valid(): if form.is_valid():
form.save(use_https = request.is_secure(), form.save(use_https = request.is_secure(),
...@@ -525,7 +564,6 @@ def password_reset(request): ...@@ -525,7 +564,6 @@ def password_reset(request):
return HttpResponse(json.dumps({'success': False, return HttpResponse(json.dumps({'success': False,
'error': 'Invalid e-mail'})) 'error': 'Invalid e-mail'}))
@ensure_csrf_cookie @ensure_csrf_cookie
def reactivation_email(request): def reactivation_email(request):
''' Send an e-mail to reactivate a deactivated account, or to ''' Send an e-mail to reactivate a deactivated account, or to
...@@ -536,25 +574,22 @@ def reactivation_email(request): ...@@ -536,25 +574,22 @@ def reactivation_email(request):
except User.DoesNotExist: except User.DoesNotExist:
return HttpResponse(json.dumps({'success': False, return HttpResponse(json.dumps({'success': False,
'error': 'No inactive user with this e-mail exists'})) 'error': 'No inactive user with this e-mail exists'}))
return reactivation_email_for_user(user)
if user.is_active: def reactivation_email_for_user(user):
return HttpResponse(json.dumps({'success': False,
'error': 'User is already active'}))
reg = Registration.objects.get(user=user) reg = Registration.objects.get(user=user)
reg.register(user)
d = {'name': UserProfile.get(user=user).name, d = {'name': user.profile.name,
'key': r.activation_key} 'key': reg.activation_key}
subject = render_to_string('reactivation_email_subject.txt', d) subject = render_to_string('emails/activation_email_subject.txt', d)
subject = ''.join(subject.splitlines()) subject = ''.join(subject.splitlines())
message = render_to_string('reactivation_email.txt', d) message = render_to_string('emails/activation_email.txt', d)
res = user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL) res = user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
return HttpResponse(json.dumps({'success': True})) return HttpResponse(json.dumps({'success': True}))
@ensure_csrf_cookie @ensure_csrf_cookie
def change_email_request(request): def change_email_request(request):
...@@ -638,9 +673,12 @@ def confirm_email_change(request, key): ...@@ -638,9 +673,12 @@ def confirm_email_change(request, key):
meta['old_emails'].append([user.email, datetime.datetime.now().isoformat()]) meta['old_emails'].append([user.email, datetime.datetime.now().isoformat()])
up.set_meta(meta) up.set_meta(meta)
up.save() up.save()
# Send it to the old email...
user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
user.email = pec.new_email user.email = pec.new_email
user.save() user.save()
pec.delete() pec.delete()
# And send it to the new email...
user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL) user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
return render_to_response("email_change_successful.html", d) return render_to_response("email_change_successful.html", d)
...@@ -661,9 +699,12 @@ def change_name_request(request): ...@@ -661,9 +699,12 @@ def change_name_request(request):
pnc.rationale = request.POST['rationale'] pnc.rationale = request.POST['rationale']
if len(pnc.new_name) < 2: if len(pnc.new_name) < 2:
return HttpResponse(json.dumps({'success': False, 'error': 'Name required'})) return HttpResponse(json.dumps({'success': False, 'error': 'Name required'}))
if len(pnc.rationale) < 2:
return HttpResponse(json.dumps({'success': False, 'error': 'Rationale required'}))
pnc.save() pnc.save()
# The following automatically accepts name change requests. Remove this to
# go back to the old system where it gets queued up for admin approval.
accept_name_change_by_id(pnc.id)
return HttpResponse(json.dumps({'success': True})) return HttpResponse(json.dumps({'success': True}))
...@@ -698,14 +739,9 @@ def reject_name_change(request): ...@@ -698,14 +739,9 @@ def reject_name_change(request):
return HttpResponse(json.dumps({'success': True})) return HttpResponse(json.dumps({'success': True}))
@ensure_csrf_cookie def accept_name_change_by_id(id):
def accept_name_change(request):
''' JSON: Name change process. Course staff clicks 'accept' on a given name change '''
if not request.user.is_staff:
raise Http404
try: try:
pnc = PendingNameChange.objects.get(id=int(request.POST['id'])) pnc = PendingNameChange.objects.get(id=id)
except PendingNameChange.DoesNotExist: except PendingNameChange.DoesNotExist:
return HttpResponse(json.dumps({'success': False, 'error': 'Invalid ID'})) return HttpResponse(json.dumps({'success': False, 'error': 'Invalid ID'}))
...@@ -724,3 +760,17 @@ def accept_name_change(request): ...@@ -724,3 +760,17 @@ def accept_name_change(request):
pnc.delete() pnc.delete()
return HttpResponse(json.dumps({'success': True})) return HttpResponse(json.dumps({'success': True}))
@ensure_csrf_cookie
def accept_name_change(request):
''' JSON: Name change process. Course staff clicks 'accept' on a given name change
We used this during the prototype but now we simply record name changes instead
of manually approving them. Still keeping this around in case we want to go
back to this approval method.
'''
if not request.user.is_staff:
raise Http404
return accept_name_change_by_id(int(request.POST['id']))
...@@ -6,7 +6,9 @@ import json ...@@ -6,7 +6,9 @@ import json
def expect_json(view_function): def expect_json(view_function):
@wraps(view_function) @wraps(view_function)
def expect_json_with_cloned_request(request, *args, **kwargs): def expect_json_with_cloned_request(request, *args, **kwargs):
if request.META['CONTENT_TYPE'] == "application/json": # cdodge: fix postback errors in CMS. The POST 'content-type' header can include additional information
# e.g. 'charset', so we can't do a direct string compare
if request.META['CONTENT_TYPE'].lower().startswith("application/json"):
cloned_request = copy.copy(request) cloned_request = copy.copy(request)
cloned_request.POST = cloned_request.POST.copy() cloned_request.POST = cloned_request.POST.copy()
cloned_request.POST.update(json.loads(request.body)) cloned_request.POST.update(json.loads(request.body))
......
import datetime import datetime
import json import json
import pprint
import sys import sys
from django.conf import settings from django.conf import settings
...@@ -91,3 +92,30 @@ def accepts(request, media_type): ...@@ -91,3 +92,30 @@ def accepts(request, media_type):
"""Return whether this request has an Accept header that matches type""" """Return whether this request has an Accept header that matches type"""
accept = parse_accept_header(request.META.get("HTTP_ACCEPT", "")) accept = parse_accept_header(request.META.get("HTTP_ACCEPT", ""))
return media_type in [t for (t, p, q) in accept] return media_type in [t for (t, p, q) in accept]
def debug_request(request):
"""Return a pretty printed version of the request"""
return HttpResponse("""<html>
<h1>request:</h1>
<pre>{0}</pre>
<h1>request.GET</h1>:
<pre>{1}</pre>
<h1>request.POST</h1>:
<pre>{2}</pre>
<h1>request.REQUEST</h1>:
<pre>{3}</pre>
</html>
""".format(
pprint.pformat(request),
pprint.pformat(dict(request.GET)),
pprint.pformat(dict(request.POST)),
pprint.pformat(dict(request.REQUEST)),
))
...@@ -35,7 +35,7 @@ def wrap_xmodule(get_html, module, template): ...@@ -35,7 +35,7 @@ def wrap_xmodule(get_html, module, template):
return _get_html return _get_html
def replace_course_urls(get_html, course_id, module): def replace_course_urls(get_html, course_id):
""" """
Updates the supplied module with a new get_html function that wraps Updates the supplied module with a new get_html function that wraps
the old get_html function and substitutes urls of the form /course/... the old get_html function and substitutes urls of the form /course/...
...@@ -46,7 +46,7 @@ def replace_course_urls(get_html, course_id, module): ...@@ -46,7 +46,7 @@ def replace_course_urls(get_html, course_id, module):
return replace_urls(get_html(), staticfiles_prefix='/courses/'+course_id, replace_prefix='/course/') return replace_urls(get_html(), staticfiles_prefix='/courses/'+course_id, replace_prefix='/course/')
return _get_html return _get_html
def replace_static_urls(get_html, prefix, module): def replace_static_urls(get_html, prefix):
""" """
Updates the supplied module with a new get_html function that wraps Updates the supplied module with a new get_html function that wraps
the old get_html function and substitutes urls of the form /static/... the old get_html function and substitutes urls of the form /static/...
...@@ -125,7 +125,7 @@ def add_histogram(get_html, module, user): ...@@ -125,7 +125,7 @@ def add_histogram(get_html, module, user):
mstart = getattr(module.descriptor,'start') mstart = getattr(module.descriptor,'start')
if mstart is not None: if mstart is not None:
is_released = "<font color='red'>Yes!</font>" if (now > mstart) else "<font color='green'>Not yet</font>" is_released = "<font color='red'>Yes!</font>" if (now > mstart) else "<font color='green'>Not yet</font>"
staff_context = {'definition': module.definition.get('data'), staff_context = {'definition': module.definition.get('data'),
'metadata': json.dumps(module.metadata, indent=4), 'metadata': json.dumps(module.metadata, indent=4),
'location': module.location, 'location': module.location,
...@@ -133,6 +133,7 @@ def add_histogram(get_html, module, user): ...@@ -133,6 +133,7 @@ def add_histogram(get_html, module, user):
'source_file' : source_file, 'source_file' : source_file,
'source_url': '%s/%s/tree/master/%s' % (giturl,data_dir,source_file), 'source_url': '%s/%s/tree/master/%s' % (giturl,data_dir,source_file),
'category': str(module.__class__.__name__), 'category': str(module.__class__.__name__),
# Template uses element_id in js function names, so can't allow dashes
'element_id': module.location.html_id().replace('-','_'), 'element_id': module.location.html_id().replace('-','_'),
'edit_link': edit_link, 'edit_link': edit_link,
'user': user, 'user': user,
......
...@@ -48,7 +48,7 @@ general_whitespace = re.compile('[^\w]+') ...@@ -48,7 +48,7 @@ general_whitespace = re.compile('[^\w]+')
def check_variables(string, variables): def check_variables(string, variables):
''' Confirm the only variables in string are defined. '''Confirm the only variables in string are defined.
Pyparsing uses a left-to-right parser, which makes the more Pyparsing uses a left-to-right parser, which makes the more
elegant approach pretty hopeless. elegant approach pretty hopeless.
...@@ -56,7 +56,8 @@ def check_variables(string, variables): ...@@ -56,7 +56,8 @@ def check_variables(string, variables):
achar = reduce(lambda a,b:a|b ,map(Literal,alphas)) # Any alphabetic character achar = reduce(lambda a,b:a|b ,map(Literal,alphas)) # Any alphabetic character
undefined_variable = achar + Word(alphanums) undefined_variable = achar + Word(alphanums)
undefined_variable.setParseAction(lambda x:UndefinedVariable("".join(x)).raiseself()) undefined_variable.setParseAction(lambda x:UndefinedVariable("".join(x)).raiseself())
varnames = varnames | undefined_variable''' varnames = varnames | undefined_variable
'''
possible_variables = re.split(general_whitespace, string) # List of all alnums in string possible_variables = re.split(general_whitespace, string) # List of all alnums in string
bad_variables = list() bad_variables = list()
for v in possible_variables: for v in possible_variables:
...@@ -71,7 +72,8 @@ def check_variables(string, variables): ...@@ -71,7 +72,8 @@ def check_variables(string, variables):
def evaluator(variables, functions, string, cs=False): def evaluator(variables, functions, string, cs=False):
''' Evaluate an expression. Variables are passed as a dictionary '''
Evaluate an expression. Variables are passed as a dictionary
from string to value. Unary functions are passed as a dictionary from string to value. Unary functions are passed as a dictionary
from string to function. Variables must be floats. from string to function. Variables must be floats.
cs: Case sensitive cs: Case sensitive
...@@ -108,6 +110,7 @@ def evaluator(variables, functions, string, cs=False): ...@@ -108,6 +110,7 @@ def evaluator(variables, functions, string, cs=False):
if string.strip() == "": if string.strip() == "":
return float('nan') return float('nan')
ops = {"^": operator.pow, ops = {"^": operator.pow,
"*": operator.mul, "*": operator.mul,
"/": operator.truediv, "/": operator.truediv,
...@@ -169,14 +172,19 @@ def evaluator(variables, functions, string, cs=False): ...@@ -169,14 +172,19 @@ def evaluator(variables, functions, string, cs=False):
def func_parse_action(x): def func_parse_action(x):
return [all_functions[x[0]](x[1])] return [all_functions[x[0]](x[1])]
number_suffix = reduce(lambda a, b: a | b, map(Literal, suffixes.keys()), NoMatch()) # SI suffixes and percent # SI suffixes and percent
number_suffix = reduce(lambda a, b: a | b, map(Literal, suffixes.keys()), NoMatch())
(dot, minus, plus, times, div, lpar, rpar, exp) = map(Literal, ".-+*/()^") (dot, minus, plus, times, div, lpar, rpar, exp) = map(Literal, ".-+*/()^")
number_part = Word(nums) number_part = Word(nums)
inner_number = (number_part + Optional("." + number_part)) | ("." + number_part) # 0.33 or 7 or .34
number = Optional(minus | plus) + inner_number + \ # 0.33 or 7 or .34
Optional(CaselessLiteral("E") + Optional("-") + number_part) + \ inner_number = (number_part + Optional("." + number_part)) | ("." + number_part)
Optional(number_suffix) # 0.33k or -17
# 0.33k or -17
number = (Optional(minus | plus) + inner_number
+ Optional(CaselessLiteral("E") + Optional("-") + number_part)
+ Optional(number_suffix))
number = number.setParseAction(number_parse_action) # Convert to number number = number.setParseAction(number_parse_action) # Convert to number
# Predefine recursive variables # Predefine recursive variables
...@@ -201,9 +209,11 @@ def evaluator(variables, functions, string, cs=False): ...@@ -201,9 +209,11 @@ def evaluator(variables, functions, string, cs=False):
varnames.setParseAction(lambda x: map(lambda y: all_variables[y], x)) varnames.setParseAction(lambda x: map(lambda y: all_variables[y], x))
else: else:
varnames = NoMatch() varnames = NoMatch()
# Same thing for functions. # Same thing for functions.
if len(all_functions) > 0: if len(all_functions) > 0:
funcnames = sreduce(lambda x, y: x | y, map(lambda x: CasedLiteral(x), all_functions.keys())) funcnames = sreduce(lambda x, y: x | y,
map(lambda x: CasedLiteral(x), all_functions.keys()))
function = funcnames + lpar.suppress() + expr + rpar.suppress() function = funcnames + lpar.suppress() + expr + rpar.suppress()
function.setParseAction(func_parse_action) function.setParseAction(func_parse_action)
else: else:
......
...@@ -3,25 +3,27 @@ ...@@ -3,25 +3,27 @@
# #
# Used by responsetypes and capa_problem # Used by responsetypes and capa_problem
class CorrectMap(object): class CorrectMap(object):
''' """
Stores map between answer_id and response evaluation result for each question Stores map between answer_id and response evaluation result for each question
in a capa problem. The response evaluation result for each answer_id includes in a capa problem. The response evaluation result for each answer_id includes
(correctness, npoints, msg, hint, hintmode). (correctness, npoints, msg, hint, hintmode).
- correctness : either 'correct' or 'incorrect' - correctness : either 'correct' or 'incorrect'
- npoints : None, or integer specifying number of points awarded for this answer_id - npoints : None, or integer specifying number of points awarded for this answer_id
- msg : string (may have HTML) giving extra message response (displayed below textline or textbox) - msg : string (may have HTML) giving extra message response
- hint : string (may have HTML) giving optional hint (displayed below textline or textbox, above msg) (displayed below textline or textbox)
- hint : string (may have HTML) giving optional hint
(displayed below textline or textbox, above msg)
- hintmode : one of (None,'on_request','always') criteria for displaying hint - hintmode : one of (None,'on_request','always') criteria for displaying hint
- queuestate : Dict {key:'', time:''} where key is a secret string, and time is a string dump - queuestate : Dict {key:'', time:''} where key is a secret string, and time is a string dump
of a DateTime object in the format '%Y%m%d%H%M%S'. Is None when not queued of a DateTime object in the format '%Y%m%d%H%M%S'. Is None when not queued
Behaves as a dict. Behaves as a dict.
''' """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.cmap = dict() # start with empty dict # start with empty dict
self.cmap = dict()
self.items = self.cmap.items self.items = self.cmap.items
self.keys = self.cmap.keys self.keys = self.cmap.keys
self.set(*args, **kwargs) self.set(*args, **kwargs)
...@@ -33,7 +35,15 @@ class CorrectMap(object): ...@@ -33,7 +35,15 @@ class CorrectMap(object):
return self.cmap.__iter__() return self.cmap.__iter__()
# See the documentation for 'set_dict' for the use of kwargs # See the documentation for 'set_dict' for the use of kwargs
def set(self, answer_id=None, correctness=None, npoints=None, msg='', hint='', hintmode=None, queuestate=None, **kwargs): def set(self,
answer_id=None,
correctness=None,
npoints=None,
msg='',
hint='',
hintmode=None,
queuestate=None, **kwargs):
if answer_id is not None: if answer_id is not None:
self.cmap[answer_id] = {'correctness': correctness, self.cmap[answer_id] = {'correctness': correctness,
'npoints': npoints, 'npoints': npoints,
...@@ -56,12 +66,13 @@ class CorrectMap(object): ...@@ -56,12 +66,13 @@ class CorrectMap(object):
''' '''
Set internal dict of CorrectMap to provided correct_map dict Set internal dict of CorrectMap to provided correct_map dict
correct_map is saved by LMS as a plaintext JSON dump of the correctmap dict. This means that correct_map is saved by LMS as a plaintext JSON dump of the correctmap dict. This
when the definition of CorrectMap (e.g. its properties) are altered, existing correct_map dict means that when the definition of CorrectMap (e.g. its properties) are altered,
not coincide with the newest CorrectMap format as defined by self.set. an existing correct_map dict will not coincide with the newest CorrectMap format as
defined by self.set.
For graceful migration, feed the contents of each correct map to self.set, rather than For graceful migration, feed the contents of each correct map to self.set, rather than
making a direct copy of the given correct_map dict. This way, the common keys between making a direct copy of the given correct_map dict. This way, the common keys between
the incoming correct_map dict and the new CorrectMap instance will be written, while the incoming correct_map dict and the new CorrectMap instance will be written, while
mismatched keys will be gracefully ignored. mismatched keys will be gracefully ignored.
...@@ -69,14 +80,20 @@ class CorrectMap(object): ...@@ -69,14 +80,20 @@ class CorrectMap(object):
If correct_map is a one-level dict, then convert it to the new dict of dicts format. If correct_map is a one-level dict, then convert it to the new dict of dicts format.
''' '''
if correct_map and not (type(correct_map[correct_map.keys()[0]]) == dict): if correct_map and not (type(correct_map[correct_map.keys()[0]]) == dict):
self.__init__() # empty current dict # empty current dict
for k in correct_map: self.set(k, correct_map[k]) # create new dict entries self.__init__()
# create new dict entries
for k in correct_map:
self.set(k, correct_map[k])
else: else:
self.__init__() self.__init__()
for k in correct_map: self.set(k, **correct_map[k]) for k in correct_map:
self.set(k, **correct_map[k])
def is_correct(self, answer_id): def is_correct(self, answer_id):
if answer_id in self.cmap: return self.cmap[answer_id]['correctness'] == 'correct' if answer_id in self.cmap:
return self.cmap[answer_id]['correctness'] == 'correct'
return None return None
def is_queued(self, answer_id): def is_queued(self, answer_id):
...@@ -94,14 +111,18 @@ class CorrectMap(object): ...@@ -94,14 +111,18 @@ class CorrectMap(object):
return npoints return npoints
elif self.is_correct(answer_id): elif self.is_correct(answer_id):
return 1 return 1
return 0 # if not correct and no points have been assigned, return 0 # if not correct and no points have been assigned, return 0
return 0
def set_property(self, answer_id, property, value): def set_property(self, answer_id, property, value):
if answer_id in self.cmap: self.cmap[answer_id][property] = value if answer_id in self.cmap:
else: self.cmap[answer_id] = {property: value} self.cmap[answer_id][property] = value
else:
self.cmap[answer_id] = {property: value}
def get_property(self, answer_id, property, default=None): def get_property(self, answer_id, property, default=None):
if answer_id in self.cmap: return self.cmap[answer_id].get(property, default) if answer_id in self.cmap:
return self.cmap[answer_id].get(property, default)
return default return default
def get_correctness(self, answer_id): def get_correctness(self, answer_id):
......
"""
This has custom renderers: classes that know how to render certain problem tags (e.g. <math> and
<solution>) to html.
These tags do not have state, so they just get passed the system (for access to render_template),
and the xml element.
"""
from registry import TagRegistry
import logging
import re
import shlex # for splitting quoted strings
import json
from lxml import etree
import xml.sax.saxutils as saxutils
from registry import TagRegistry
log = logging.getLogger('mitx.' + __name__)
registry = TagRegistry()
#-----------------------------------------------------------------------------
class MathRenderer(object):
tags = ['math']
def __init__(self, system, xml):
'''
Render math using latex-like formatting.
Examples:
<math>$\displaystyle U(r)=4 U_0 $</math>
<math>$r_0$</math>
We convert these to [mathjax]...[/mathjax] and [mathjaxinline]...[/mathjaxinline]
TODO: use shorter tags (but this will require converting problem XML files!)
'''
self.system = system
self.xml = xml
mathstr = re.sub('\$(.*)\$', r'[mathjaxinline]\1[/mathjaxinline]', xml.text)
mtag = 'mathjax'
if not r'\displaystyle' in mathstr:
mtag += 'inline'
else:
mathstr = mathstr.replace(r'\displaystyle', '')
self.mathstr = mathstr.replace('mathjaxinline]', '%s]' % mtag)
def get_html(self):
"""
Return the contents of this tag, rendered to html, as an etree element.
"""
# TODO: why are there nested html tags here?? Why are there html tags at all, in fact?
html = '<html><html>%s</html><html>%s</html></html>' % (
self.mathstr, saxutils.escape(self.xml.tail))
try:
xhtml = etree.XML(html)
except Exception as err:
if self.system.DEBUG:
msg = '<html><div class="inline-error"><p>Error %s</p>' % (
str(err).replace('<', '&lt;'))
msg += ('<p>Failed to construct math expression from <pre>%s</pre></p>' %
html.replace('<', '&lt;'))
msg += "</div></html>"
log.error(msg)
return etree.XML(msg)
else:
raise
return xhtml
registry.register(MathRenderer)
#-----------------------------------------------------------------------------
class SolutionRenderer(object):
'''
A solution is just a <span>...</span> which is given an ID, that is used for displaying an
extended answer (a problem "solution") after "show answers" is pressed.
Note that the solution content is NOT rendered and returned in the HTML. It is obtained by an
ajax call.
'''
tags = ['solution']
def __init__(self, system, xml):
self.system = system
self.id = xml.get('id')
def get_html(self):
context = {'id': self.id}
html = self.system.render_template("solutionspan.html", context)
return etree.XML(html)
registry.register(SolutionRenderer)
""" Standard resistor codes. """
Standard resistor codes.
http://en.wikipedia.org/wiki/Electronic_color_code http://en.wikipedia.org/wiki/Electronic_color_code
""" """
E6 = [10, 15, 22, 33, 47, 68] E6 = [10, 15, 22, 33, 47, 68]
E12 = [10, 12, 15, 18, 22, 27, 33, 39, 47, 56, 68, 82] E12 = [10, 12, 15, 18, 22, 27, 33, 39, 47, 56, 68, 82]
E24 = [10, 12, 15, 18, 22, 27, 33, 39, 47, 56, 68, 82, 11, 13, 16, 20, 24, 30, 36, 43, 51, 62, 75, 91] E24 = [10, 12, 15, 18, 22, 27, 33, 39, 47, 56, 68, 82, 11, 13, 16, 20, 24, 30, 36, 43, 51, 62, 75, 91]
E48 = [100, 121, 147, 178, 215, 261, 316, 383, 464, 562, 681, 825, 105, 127, 154, 187, 226, 274, 332, 402, 487, 590, 715, 866, 110, 133, 162, 196, 237, 287, 348, 422, 511, 619, 750, 909, 115, 140, 169, 205, 249, 301, 365, 442, 536, 649, 787, 953] E48 = [100, 121, 147, 178, 215, 261, 316, 383, 464, 562, 681, 825, 105, 127, 154, 187, 226, 274, 332, 402, 487, 590, 715, 866, 110, 133, 162, 196, 237, 287, 348, 422, 511, 619, 750, 909, 115, 140, 169, 205, 249, 301, 365, 442, 536, 649, 787, 953]
E96 = [100, 121, 147, 178, 215, 261, 316, 383, 464, 562, 681, 825, 102, 124, 150, 182, 221, 267, 324, 392, 475, 576, 698, 845, 105, 127, 154, 187, 226, 274, 332, 402, 487, 590, 715, 866, 107, 130, 158, 191, 232, 280, 340, 412, 499, 604, 732, 887, 110, 133, 162, 196, 237, 287, 348, 422, 511, 619, 750, 909, 113, 137, 165, 200, 243, 294, 357, 432, 523, 634, 768, 931, 115, 140, 169, 205, 249, 301, 365, 442, 536, 649, 787, 953, 118, 143, 174, 210, 255, 309, 374, 453, 549, 665, 806, 976] E96 = [100, 121, 147, 178, 215, 261, 316, 383, 464, 562, 681, 825, 102, 124, 150, 182, 221, 267, 324, 392, 475, 576, 698, 845, 105, 127, 154, 187, 226, 274, 332, 402, 487, 590, 715, 866, 107, 130, 158, 191, 232, 280, 340, 412, 499, 604, 732, 887, 110, 133, 162, 196, 237, 287, 348, 422, 511, 619, 750, 909, 113, 137, 165, 200, 243, 294, 357, 432, 523, 634, 768, 931, 115, 140, 169, 205, 249, 301, 365, 442, 536, 649, 787, 953, 118, 143, 174, 210, 255, 309, 374, 453, 549, 665, 806, 976]
E192 = [100, 121, 147, 178, 215, 261, 316, 383, 464, 562, 681, 825, 101, 123, 149, 180, 218, 264, 320, 388, 470, 569, 690, 835, 102, 124, 150, 182, 221, 267, 324, 392, 475, 576, 698, 845, 104, 126, 152, 184, 223, 271, 328, 397, 481, 583, 706, 856, 105, 127, 154, 187, 226, 274, 332, 402, 487, 590, 715, 866, 106, 129, 156, 189, 229, 277, 336, 407, 493, 597, 723, 876, 107, 130, 158, 191, 232, 280, 340, 412, 499, 604, 732, 887, 109, 132, 160, 193, 234, 284, 344, 417, 505, 612, 741, 898, 110, 133, 162, 196, 237, 287, 348, 422, 511, 619, 750, 909, 111, 135, 164, 198, 240, 291, 352, 427, 517, 626, 759, 920, 113, 137, 165, 200, 243, 294, 357, 432, 523, 634, 768, 931, 114, 138, 167, 203, 246, 298, 361, 437, 530, 642, 777, 942, 115, 140, 169, 205, 249, 301, 365, 442, 536, 649, 787, 953, 117, 142, 172, 208, 252, 305, 370, 448, 542, 657, 796, 965, 118, 143, 174, 210, 255, 309, 374, 453, 549, 665, 806, 976, 120, 145, 176, 213, 258, 312, 379, 459, 556, 673, 816, 988] E192 = [100, 121, 147, 178, 215, 261, 316, 383, 464, 562, 681, 825, 101, 123, 149, 180, 218, 264, 320, 388, 470, 569, 690, 835, 102, 124, 150, 182, 221, 267, 324, 392, 475, 576, 698, 845, 104, 126, 152, 184, 223, 271, 328, 397, 481, 583, 706, 856, 105, 127, 154, 187, 226, 274, 332, 402, 487, 590, 715, 866, 106, 129, 156, 189, 229, 277, 336, 407, 493, 597, 723, 876, 107, 130, 158, 191, 232, 280, 340, 412, 499, 604, 732, 887, 109, 132, 160, 193, 234, 284, 344, 417, 505, 612, 741, 898, 110, 133, 162, 196, 237, 287, 348, 422, 511, 619, 750, 909, 111, 135, 164, 198, 240, 291, 352, 427, 517, 626, 759, 920, 113, 137, 165, 200, 243, 294, 357, 432, 523, 634, 768, 931, 114, 138, 167, 203, 246, 298, 361, 437, 530, 642, 777, 942, 115, 140, 169, 205, 249, 301, 365, 442, 536, 649, 787, 953, 117, 142, 172, 208, 252, 305, 370, 448, 542, 657, 796, 965, 118, 143, 174, 210, 255, 309, 374, 453, 549, 665, 806, 976, 120, 145, 176, 213, 258, 312, 379, 459, 556, 673, 816, 988]
class TagRegistry(object):
"""
A registry mapping tags to handlers.
(A dictionary with some extra error checking.)
"""
def __init__(self):
self._mapping = {}
def register(self, cls):
"""
Register cls as a supported tag type. It is expected to define cls.tags as a list of tags
that it implements.
If an already-registered type has registered one of those tags, will raise ValueError.
If there are no tags in cls.tags, will also raise ValueError.
"""
# Do all checks and complain before changing any state.
if len(cls.tags) == 0:
raise ValueError("No tags specified for class {0}".format(cls.__name__))
for t in cls.tags:
if t in self._mapping:
other_cls = self._mapping[t]
if cls == other_cls:
# registering the same class multiple times seems silly, but ok
continue
raise ValueError("Tag {0} already registered by class {1}."
" Can't register for class {2}"
.format(t, other_cls.__name__, cls.__name__))
# Ok, should be good to change state now.
for t in cls.tags:
self._mapping[t] = cls
def registered_tags(self):
"""
Get a list of all the tags that have been registered.
"""
return self._mapping.keys()
def get_class_for_tag(self, tag):
"""
For any tag in registered_tags(), returns the corresponding class. Otherwise, will raise
KeyError.
"""
return self._mapping[tag]
<section id="chemicalequationinput_${id}" class="chemicalequationinput">
<div class="script_placeholder" data-src="${previewer}"/>
% if status == 'unsubmitted':
<div class="unanswered" id="status_${id}">
% elif status == 'correct':
<div class="correct" id="status_${id}">
% elif status == 'incorrect':
<div class="incorrect" id="status_${id}">
% elif status == 'incomplete':
<div class="incorrect" id="status_${id}">
% endif
<input type="text" name="input_${id}" id="input_${id}" value="${value|h}"
% if size:
size="${size}"
% endif
/>
<p class="status">
% if status == 'unsubmitted':
unanswered
% elif status == 'correct':
correct
% elif status == 'incorrect':
incorrect
% elif status == 'incomplete':
incomplete
% endif
</p>
<div id="input_${id}_preview" class="equation">
</div>
<p id="answer_${id}" class="answer"></p>
% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
</div>
% endif
</section>
<form class="choicegroup capa_inputtype" id="inputtype_${id}"> <form class="choicegroup capa_inputtype" id="inputtype_${id}">
<div class="indicator_container">
% for choice_id, choice_description in choices: % if status == 'unsubmitted':
<label for="input_${id}_${choice_id}"> <input type="${input_type}" name="input_${id}${name_array_suffix}" id="input_${id}_${choice_id}" value="${choice_id}" <span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% if choice_id in value: % elif status == 'correct':
checked="true" <span class="correct" id="status_${id}"></span>
% endif % elif status == 'incorrect':
/> ${choice_description} </label> <span class="incorrect" id="status_${id}"></span>
% endfor % elif status == 'incomplete':
<span id="answer_${id}"></span> <span class="incorrect" id="status_${id}"></span>
% endif
</div>
<fieldset>
% for choice_id, choice_description in choices:
<label for="input_${id}_${choice_id}"> <input type="${input_type}" name="input_${id}${name_array_suffix}" id="input_${id}_${choice_id}" value="${choice_id}"
% if choice_id in value:
checked="true"
% endif
/> ${choice_description} </label>
% endfor
<span id="answer_${id}"></span>
</fieldset>
% if state == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'correct':
<span class="correct" id="status_${id}"></span>
% elif state == 'incorrect':
<span class="incorrect" id="status_${id}"></span>
% elif state == 'incomplete':
<span class="incorrect" id="status_${id}"></span>
% endif
</form> </form>
...@@ -5,16 +5,14 @@ ...@@ -5,16 +5,14 @@
% endif % endif
>${value|h}</textarea> >${value|h}</textarea>
<span id="answer_${id}"></span>
<div class="grader-status"> <div class="grader-status">
% if state == 'unsubmitted': % if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}">Unanswered</span> <span class="unanswered" style="display:inline-block;" id="status_${id}">Unanswered</span>
% elif state == 'correct': % elif status == 'correct':
<span class="correct" id="status_${id}">Correct</span> <span class="correct" id="status_${id}">Correct</span>
% elif state == 'incorrect': % elif status == 'incorrect':
<span class="incorrect" id="status_${id}">Incorrect</span> <span class="incorrect" id="status_${id}">Incorrect</span>
% elif state == 'queued': % elif status == 'queued':
<span class="processing" id="status_${id}">Queued</span> <span class="processing" id="status_${id}">Queued</span>
<span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span> <span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span>
% endif % endif
...@@ -23,9 +21,11 @@ ...@@ -23,9 +21,11 @@
<div style="display:none;" name="${hidden}" inputid="input_${id}" /> <div style="display:none;" name="${hidden}" inputid="input_${id}" />
% endif % endif
<p class="debug">${state}</p> <p class="debug">${status}</p>
</div> </div>
<span id="answer_${id}"></span>
<div class="external-grader-message"> <div class="external-grader-message">
${msg|n} ${msg|n}
</div> </div>
...@@ -42,7 +42,12 @@ ...@@ -42,7 +42,12 @@
lineWrapping: true, lineWrapping: true,
indentUnit: "${tabsize}", indentUnit: "${tabsize}",
tabSize: "${tabsize}", tabSize: "${tabsize}",
indentWithTabs: true, indentWithTabs: false,
extraKeys: {
"Tab": function(cm) {
cm.replaceSelection("${' '*tabsize}", "end");
}
},
smartIndent: false smartIndent: false
}); });
}); });
......
### <section id="inputtype_${id}" class="capa_inputtype" >
### version of textline.html which does dynammic math <div id="holder" style="width:${width};height:${height}"></div>
### <div class="script_placeholder" data-src="/static/js/raphael.js"></div>
<section class="text-input-dynamath"> <div class="script_placeholder" data-src="/static/js/sylvester.js"></div>
% if state == 'unsubmitted': <div class="script_placeholder" data-src="/static/js/underscore-min.js"></div>
<div class="script_placeholder" data-src="/static/js/crystallography.js"></div>
% if status == 'unsubmitted':
<div class="unanswered" id="status_${id}"> <div class="unanswered" id="status_${id}">
% elif state == 'correct': % elif status == 'correct':
<div class="correct" id="status_${id}"> <div class="correct" id="status_${id}">
% elif state == 'incorrect': % elif status == 'incorrect':
<div class="incorrect" id="status_${id}"> <div class="incorrect" id="status_${id}">
% elif state == 'incomplete': % elif status == 'incomplete':
<div class="incorrect" id="status_${id}"> <div class="incorrect" id="status_${id}">
% endif % endif
% if hidden: % if hidden:
<div style="display:none;" name="${hidden}" inputid="input_${id}" /> <div style="display:none;" name="${hidden}" inputid="input_${id}" />
% endif % endif
<input type="text" name="input_${id}" id="input_${id}" value="${value}" class="math" size="${size if size else ''}" <input type="text" name="input_${id}" id="input_${id}" value="${value|h}"
% if hidden: % if size:
style="display:none;" size="${size}"
% endif % endif
/> % if hidden:
style="display:none;"
% endif
/>
<p class="status"> <p class="status">
% if state == 'unsubmitted': % if status == 'unsubmitted':
unanswered unanswered
% elif state == 'correct': % elif status == 'correct':
correct correct
% elif state == 'incorrect': % elif status == 'incorrect':
incorrect incorrect
% elif state == 'incomplete': % elif status == 'incomplete':
incomplete incomplete
% endif % endif
</p> </p>
<p id="answer_${id}" class="answer"></p> <p id="answer_${id}" class="answer"></p>
<div id="display_${id}" class="equation">`{::}`</div> % if msg:
<span class="message">${msg|n}</span>
</div> % endif
<textarea style="display:none" id="input_${id}_dynamath" name="input_${id}_dynamath"> </textarea> % if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
% if msg: </div>
<span class="message">${msg|n}</span> % endif
% endif
</section> </section>
<section id="filesubmission_${id}" class="filesubmission"> <section id="filesubmission_${id}" class="filesubmission">
<div class="grader-status file"> <div class="grader-status file">
% if state == 'unsubmitted': % if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}">Unanswered</span> <span class="unanswered" style="display:inline-block;" id="status_${id}">Unanswered</span>
% elif state == 'correct': % elif status == 'correct':
<span class="correct" id="status_${id}">Correct</span> <span class="correct" id="status_${id}">Correct</span>
% elif state == 'incorrect': % elif status == 'incorrect':
<span class="incorrect" id="status_${id}">Incorrect</span> <span class="incorrect" id="status_${id}">Incorrect</span>
% elif state == 'queued': % elif status == 'queued':
<span class="processing" id="status_${id}">Queued</span> <span class="processing" id="status_${id}">Queued</span>
<span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span> <span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span>
% endif % endif
<p class="debug">${state}</p> <p class="debug">${status}</p>
<input type="file" name="input_${id}" id="input_${id}" value="${value}" multiple="multiple" data-required_files="${required_files}" data-allowed_files="${allowed_files}"/> <input type="file" name="input_${id}" id="input_${id}" value="${value}" multiple="multiple" data-required_files="${required_files|h}" data-allowed_files="${allowed_files|h}"/>
</div> </div>
<div class="message">${msg|n}</div> <div class="message">${msg|n}</div>
</section> </section>
...@@ -4,13 +4,13 @@ ...@@ -4,13 +4,13 @@
<img src="/static/green-pointer.png" id="cross_${id}" style="position: absolute;top: ${gy}px;left: ${gx}px;" /> <img src="/static/green-pointer.png" id="cross_${id}" style="position: absolute;top: ${gy}px;left: ${gx}px;" />
</div> </div>
% if state == 'unsubmitted': % if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span> <span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'correct': % elif status == 'correct':
<span class="correct" id="status_${id}"></span> <span class="correct" id="status_${id}"></span>
% elif state == 'incorrect': % elif status == 'incorrect':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}"></span>
% elif state == 'incomplete': % elif status == 'incomplete':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}"></span>
% endif % endif
</span> </span>
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<input type="hidden" name="input_${id}" id="input_${id}" class="javascriptinput_input"/> <input type="hidden" name="input_${id}" id="input_${id}" class="javascriptinput_input"/>
<div class="javascriptinput_data" data-display_class="${display_class}" <div class="javascriptinput_data" data-display_class="${display_class}"
data-problem_state="${problem_state}" data-params="${params}" data-problem_state="${problem_state}" data-params="${params}"
data-submission="${value}" data-evaluation="${evaluation}"> data-submission="${value|h}" data-evaluation="${msg|h}">
</div> </div>
<div class="script_placeholder" data-src="/static/js/${display_file}"></div> <div class="script_placeholder" data-src="/static/js/${display_file}"></div>
<div class="javascriptinput_container"></div> <div class="javascriptinput_container"></div>
......
...@@ -18,13 +18,13 @@ ...@@ -18,13 +18,13 @@
<textarea style="display:none" id="input_${id}_fromjs" name="input_${id}_fromjs"></textarea> <textarea style="display:none" id="input_${id}_fromjs" name="input_${id}_fromjs"></textarea>
% endif % endif
% if state == 'unsubmitted': % if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span> <span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'correct': % elif status == 'correct':
<span class="correct" id="status_${id}"></span> <span class="correct" id="status_${id}"></span>
% elif state == 'incorrect': % elif status == 'incorrect':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}"></span>
% elif state == 'incomplete': % elif status == 'incomplete':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}"></span>
% endif % endif
% if msg: % if msg:
......
...@@ -12,13 +12,13 @@ ...@@ -12,13 +12,13 @@
<span id="answer_${id}"></span> <span id="answer_${id}"></span>
% if state == 'unsubmitted': % if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span> <span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'correct': % elif status == 'correct':
<span class="correct" id="status_${id}"></span> <span class="correct" id="status_${id}"></span>
% elif state == 'incorrect': % elif status == 'incorrect':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}"></span>
% elif state == 'incomplete': % elif status == 'incomplete':
<span class="incorrect" id="status_${id}"></span> <span class="incorrect" id="status_${id}"></span>
% endif % endif
</form> </form>
...@@ -12,13 +12,13 @@ ...@@ -12,13 +12,13 @@
</script> </script>
<span id="answer_${id}"></span> <span id="answer_${id}"></span>
% if state == 'unsubmitted': % if status == 'unsubmitted':
<span class="ui-icon ui-icon-bullet" style="display:inline-block;" id="status_${id}"></span> <span class="ui-icon ui-icon-bullet" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'correct': % elif status == 'correct':
<span class="ui-icon ui-icon-check" style="display:inline-block;" id="status_${id}"></span> <span class="ui-icon ui-icon-check" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'incorrect': % elif status == 'incorrect':
<span class="ui-icon ui-icon-close" style="display:inline-block;" id="status_${id}"></span> <span class="ui-icon ui-icon-close" style="display:inline-block;" id="status_${id}"></span>
% elif state == 'incomplete': % elif status == 'incomplete':
<span class="ui-icon ui-icon-close" style="display:inline-block;" id="status_${id}"></span> <span class="ui-icon ui-icon-close" style="display:inline-block;" id="status_${id}"></span>
% endif % endif
</span> </span>
......
<% doinline = "inline" if inline else "" %> <% doinline = "inline" if inline else "" %>
<section id="textinput_${id}" class="textinput ${doinline}" > <section id="inputtype_${id}" class="${'text-input-dynamath' if do_math else ''} capa_inputtype ${doinline}" >
% if state == 'unsubmitted':
% if preprocessor is not None:
<div class="text-input-dynamath_data" data-preprocessor="${preprocessor['class_name']}"/>
<div class="script_placeholder" data-src="${preprocessor['script_src']}"/>
% endif
% if status == 'unsubmitted':
<div class="unanswered ${doinline}" id="status_${id}"> <div class="unanswered ${doinline}" id="status_${id}">
% elif state == 'correct': % elif status == 'correct':
<div class="correct ${doinline}" id="status_${id}"> <div class="correct ${doinline}" id="status_${id}">
% elif state == 'incorrect': % elif status == 'incorrect':
<div class="incorrect ${doinline}" id="status_${id}"> <div class="incorrect ${doinline}" id="status_${id}">
% elif state == 'incomplete': % elif status == 'incomplete':
<div class="incorrect ${doinline}" id="status_${id}"> <div class="incorrect ${doinline}" id="status_${id}">
% endif % endif
% if hidden: % if hidden:
<div style="display:none;" name="${hidden}" inputid="input_${id}" /> <div style="display:none;" name="${hidden}" inputid="input_${id}" />
% endif % endif
<input type="text" name="input_${id}" id="input_${id}" value="${value}" <input type="text" name="input_${id}" id="input_${id}" value="${value|h}"
% if size: % if do_math:
size="${size}" class="math"
% endif % endif
% if hidden: % if size:
style="display:none;" size="${size}"
% endif % endif
% if hidden:
style="display:none;"
% endif
/> />
<p class="status"> <p class="status">
% if state == 'unsubmitted': % if status == 'unsubmitted':
unanswered unanswered
% elif state == 'correct': % elif status == 'correct':
correct correct
% elif state == 'incorrect': % elif status == 'incorrect':
incorrect incorrect
% elif state == 'incomplete': % elif status == 'incomplete':
incomplete incomplete
% endif % endif
</p> </p>
<p id="answer_${id}" class="answer"></p> <p id="answer_${id}" class="answer"></p>
% if do_math:
<div id="display_${id}" class="equation">`{::}`</div>
<textarea style="display:none" id="input_${id}_dynamath" name="input_${id}_dynamath">
</textarea>
% endif
% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
</div>
% endif
% if msg: % if msg:
<span class="message">${msg|n}</span> <span class="message">${msg|n}</span>
% endif % endif
% if state in ['unsubmitted', 'correct', 'incorrect', 'incomplete'] or hidden:
</div>
% endif
</section> </section>
<section id="inputtype_${id}" class="capa_inputtype" >
<table><tr><td height='600'>
<div id="vsepr_div_${id}" style="position:relative;" data-molecules="${molecules}" data-geometries="${geometries}">
<canvas id="vsepr_canvas_${id}" width="${width}" height="${height}">
</canvas>
</div>
</td><td valign ='top'>
<select class="molecule_select" id="molecule_select_${id}" size="18">
</select>
</td></tr></table>
<div class="script_placeholder" data-src="/static/js/vsepr/vsepr.js"></div>
% if status == 'unsubmitted':
<div class="unanswered" id="status_${id}">
% elif status == 'correct':
<div class="correct" id="status_${id}">
% elif status == 'incorrect':
<div class="incorrect" id="status_${id}">
% elif status == 'incomplete':
<div class="incorrect" id="status_${id}">
% endif
<input type="text" name="input_${id}" id="input_${id}" value="${value|h}"
style="display:none;"
/>
<p class="status">
% if status == 'unsubmitted':
unanswered
% elif status == 'correct':
correct
% elif status == 'incorrect':
incorrect
% elif status == 'incomplete':
incomplete
% endif
</p>
<p id="answer_${id}" class="answer"></p>
% if msg:
<span class="message">${msg|n}</span>
% endif
% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
</div>
% endif
</section>
import fs
import fs.osfs
import os
from mock import Mock
import xml.sax.saxutils as saxutils
TEST_DIR = os.path.dirname(os.path.realpath(__file__))
def tst_render_template(template, context):
"""
A test version of render to template. Renders to the repr of the context, completely ignoring
the template name. To make the output valid xml, quotes the content, and wraps it in a <div>
"""
return '<div>{0}</div>'.format(saxutils.escape(repr(context)))
test_system = Mock(
ajax_url='courses/course_id/modx/a_location',
track_function=Mock(),
get_module=Mock(),
render_template=tst_render_template,
replace_urls=Mock(),
user=Mock(),
filestore=fs.osfs.OSFS(os.path.join(TEST_DIR, "test_files")),
debug=True,
xqueue={'interface':None, 'callback_url':'/', 'default_queuename': 'testqueue', 'waittime': 10},
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
anonymous_student_id = 'student'
)
from lxml import etree
import unittest
import xml.sax.saxutils as saxutils
from . import test_system
from capa import customrender
# just a handy shortcut
lookup_tag = customrender.registry.get_class_for_tag
def extract_context(xml):
"""
Given an xml element corresponding to the output of test_system.render_template, get back the
original context
"""
return eval(xml.text)
def quote_attr(s):
return saxutils.quoteattr(s)[1:-1] # don't want the outer quotes
class HelperTest(unittest.TestCase):
'''
Make sure that our helper function works!
'''
def check(self, d):
xml = etree.XML(test_system.render_template('blah', d))
self.assertEqual(d, extract_context(xml))
def test_extract_context(self):
self.check({})
self.check({1, 2})
self.check({'id', 'an id'})
self.check({'with"quote', 'also"quote'})
class SolutionRenderTest(unittest.TestCase):
'''
Make sure solutions render properly.
'''
def test_rendering(self):
solution = 'To compute unicorns, count them.'
xml_str = """<solution id="solution_12">{s}</solution>""".format(s=solution)
element = etree.fromstring(xml_str)
renderer = lookup_tag('solution')(test_system, element)
self.assertEqual(renderer.id, 'solution_12')
# our test_system "renders" templates to a div with the repr of the context
xml = renderer.get_html()
context = extract_context(xml)
self.assertEqual(context, {'id' : 'solution_12'})
class MathRenderTest(unittest.TestCase):
'''
Make sure math renders properly.
'''
def check_parse(self, latex_in, mathjax_out):
xml_str = """<math>{tex}</math>""".format(tex=latex_in)
element = etree.fromstring(xml_str)
renderer = lookup_tag('math')(test_system, element)
self.assertEqual(renderer.mathstr, mathjax_out)
def test_parsing(self):
self.check_parse('$abc$', '[mathjaxinline]abc[/mathjaxinline]')
self.check_parse('$abc', '$abc')
self.check_parse(r'$\displaystyle 2+2$', '[mathjax] 2+2[/mathjax]')
# NOTE: not testing get_html yet because I don't understand why it's doing what it's doing.
...@@ -8,8 +8,14 @@ Hello</p></text> ...@@ -8,8 +8,14 @@ Hello</p></text>
<text>Click on the image where the top skier will stop momentarily if the top skier starts from rest.</text> <text>Click on the image where the top skier will stop momentarily if the top skier starts from rest.</text>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" rectangle="(242,202)-(296,276)"/> <imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" rectangle="(242,202)-(296,276)"/>
<text>Click on the image where the lower skier will stop momentarily if the lower skier starts from rest.</text> <text>Click on the image where the lower skier will stop momentarily if the lower skier starts from rest.</text>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" rectangle="(490,11)-(556,98);(242,202)-(296,276)"/>
<text>Click on either of the two positions as discussed previously.</text>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" rectangle="(490,11)-(556,98);(242,202)-(296,276)"/>
<text>Click on either of the two positions as discussed previously.</text>
<imageinput src="/static/Physics801/Figures/Skier-conservation of energy.jpg" width="560" height="388" rectangle="(490,11)-(556,98);(242,202)-(296,276)"/>
<text>Click on either of the two positions as discussed previously.</text>
<hintgroup showoncorrect="no"> <hintgroup showoncorrect="no">
<text><p>Use conservation of energy.</p></text> <text><p>Use conservation of energy.</p></text>
</hintgroup> </hintgroup>
</imageresponse> </imageresponse>
</problem> </problem>
\ No newline at end of file
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
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