Commit ee9b9d04 by chrisndodge

Merge pull request #1249 from MITx/feature/cas/speed-editor

Feature/cas/speed editor
parents 171d4c34 d1c888b1
......@@ -73,6 +73,10 @@ class XModuleItemFactory(Factory):
@classmethod
def _create(cls, target_class, *args, **kwargs):
"""
kwargs must include parent_location, template. Can contain display_name
target_class is ignored
"""
DETACHED_CATEGORIES = ['about', 'static_tab', 'course_info']
......
......@@ -22,6 +22,8 @@ from xmodule.modulestore.django import modulestore
from xmodule.contentstore.django import contentstore
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.xml_exporter import export_to_xml
from cms.djangoapps.contentstore.utils import get_modulestore
from xmodule.capa_module import CapaDescriptor
def parse_json(response):
"""Parse response, which is assumed to be json"""
......@@ -438,13 +440,24 @@ class ContentStoreTest(TestCase):
self.assertContains(resp, '/c4x/edX/full/asset/handouts_schematic_tutorial.pdf')
def test_capa_module(self):
"""Test that a problem treats markdown specially."""
CourseFactory.create(org='MITx', course='999', display_name='Robot Super Course')
problem_data = {
'parent_location' : 'i4x://MITx/999/course/Robot_Super_Course',
'template' : 'i4x://edx/templates/problem/Empty'
}
resp = self.client.post(reverse('clone_item'), problem_data)
self.assertEqual(resp.status_code, 200)
payload = parse_json(resp)
problem_loc = payload['id']
problem = get_modulestore(problem_loc).get_item(problem_loc)
# should be a CapaDescriptor
self.assertIsInstance(problem, CapaDescriptor, "New problem is not a CapaDescriptor")
context = problem.get_context()
self.assertIn('markdown', context, "markdown is missing from context")
self.assertIn('markdown', problem.metadata, "markdown is missing from metadata")
self.assertNotIn('markdown', problem.editable_metadata_fields, "Markdown slipped into the editable metadata fields")
\ No newline at end of file
......@@ -271,6 +271,8 @@ def edit_unit(request, location):
component_templates[template.location.category].append((
template.display_name,
template.location.url(),
'markdown' in template.metadata,
template.location.name == 'Empty'
))
components = [
......
......@@ -55,7 +55,7 @@ class CMS.Views.ModuleEdit extends Backbone.View
clickSaveButton: (event) =>
event.preventDefault()
data = @module.save()
data.metadata = @metadata()
data.metadata = _.extend(data.metadata, @metadata())
$modalCover.hide()
@model.save(data).done( =>
# # showToastMessage("Your changes have been saved.", null, 3)
......
......@@ -59,6 +59,9 @@ class CMS.Views.UnitEdit extends Backbone.View
type = $(event.currentTarget).data('type')
@$newComponentTypePicker.slideUp(250)
@$(".new-component-#{type}").slideDown(250)
$('html, body').animate({
scrollTop: @$(".new-component-#{type}").offset().top
}, 500)
closeNewComponent: (event) =>
event.preventDefault()
......
......@@ -98,69 +98,69 @@
}
.upload-modal {
display: none;
width: 640px !important;
margin-left: -320px !important;
.modal-body {
height: auto !important;
overflow-y: auto !important;
text-align: center;
}
.file-input {
display: none;
}
.choose-file-button {
@include blue-button;
padding: 10px 82px 12px;
font-size: 17px;
}
.progress-bar {
display: none;
width: 350px;
height: 50px;
margin: 30px auto 10px;
border: 1px solid $blue;
&.loaded {
border-color: #66b93d;
.progress-fill {
background: #66b93d;
}
}
}
.progress-fill {
width: 0%;
height: 50px;
background: $blue;
color: #fff;
line-height: 48px;
}
h1 {
float: none;
margin: 40px 0 30px;
font-size: 34px;
font-weight: 300;
}
.close-button {
@include white-button;
position: absolute;
top: 0;
right: 15px;
width: 29px;
height: 29px;
padding: 0 !important;
border-radius: 17px !important;
line-height: 29px;
text-align: center;
}
display: none;
width: 640px !important;
margin-left: -320px !important;
.modal-body {
height: auto !important;
overflow-y: auto !important;
text-align: center;
}
.file-input {
display: none;
}
.choose-file-button {
@include blue-button;
padding: 10px 82px 12px;
font-size: 17px;
}
.progress-bar {
display: none;
width: 350px;
height: 50px;
margin: 30px auto 10px;
border: 1px solid $blue;
&.loaded {
border-color: #66b93d;
.progress-fill {
background: #66b93d;
}
}
}
.progress-fill {
width: 0%;
height: 50px;
background: $blue;
color: #fff;
line-height: 48px;
}
h1 {
float: none;
margin: 40px 0 30px;
font-size: 34px;
font-weight: 300;
}
.close-button {
@include white-button;
position: absolute;
top: 0;
right: 15px;
width: 29px;
height: 29px;
padding: 0 !important;
border-radius: 17px !important;
line-height: 29px;
text-align: center;
}
.embeddable {
display: none;
......@@ -178,9 +178,9 @@
width: 400px;
}
.copy-button {
@include white-button;
display: none;
margin-bottom: 100px;
}
.copy-button {
@include white-button;
display: none;
margin-bottom: 100px;
}
}
\ No newline at end of file
// -------------------------------------
//
// Universal
// Universal
//
// -------------------------------------
......
......@@ -56,6 +56,15 @@
z-index: 10;
margin: 20px 40px;
.title {
margin: 0 0 15px 0;
color: $mediumGrey;
.value {
}
}
&.new-component-item {
padding: 20px;
border: none;
......@@ -116,7 +125,7 @@
a {
position: relative;
border: 1px solid $darkGreen;
background: $green;
background: tint($green,20%);
color: #fff;
@include transition(background-color .15s);
......@@ -129,23 +138,71 @@
.new-component-template {
margin-bottom: 20px;
li:first-child {
li:last-child {
a {
border-radius: 3px 3px 0 0;
border-radius: 0 0 3px 3px;
border-bottom: 1px solid $darkGreen;
}
}
li:last-child {
li:nth-child(2) {
a {
border-radius: 0 0 3px 3px;
border-radius: 3px 3px 0 0;
}
}
a {
@include clearfix();
display: block;
padding: 7px 20px;
border-bottom: none;
font-weight: 300;
.name {
float: left;
.ss-icon {
@include transition(opacity .15s);
position: relative;
top: 1px;
font-size: 13px;
margin-right: 5px;
opacity: 0.5;
}
}
.editor-indicator {
@include transition(opacity .15s);
float: right;
position: relative;
top: 3px;
font-size: 12px;
opacity: 0.1;
}
&:hover {
.ss-icon {
opacity: 1.0;
}
.editor-indicator {
opacity: 1.0;
}
}
}
// specific editor types
.empty {
@include box-shadow(0 1px 3px rgba(0,0,0,0.2));
margin-bottom: 10px;
a {
border-bottom: 1px solid $darkGreen;
border-radius: 3px;
font-weight: 500;
background: $green;
}
}
}
......
......@@ -5,6 +5,7 @@
<%block name="title">CMS Unit</%block>
<%block name="jsextra">
<script type='text/javascript'>
$(document).ready(function() {
new CMS.Views.UnitEdit({
el: $('.main-wrapper'),
model: new CMS.Models.Module({
......@@ -12,7 +13,14 @@
state: '${unit_state}'
})
});
$('.new-component-template').each(function(){
$emptyEditor = $(this).find('.empty');
$(this).prepend($emptyEditor);
});
});
</script>
</%block>
<%block name="content">
<div class="main-wrapper edit-state-${unit_state}" data-id="${unit_location}">
......@@ -46,20 +54,43 @@
% endfor
</ul>
</div>
% for type, templates in sorted(component_templates.items()):
<div class="new-component-templates new-component-${type}">
<ul class="new-component-template">
% for name, location in templates:
<li>
<a href="#" data-location="${location}">
<span class="name">${name}</span>
</a>
</li>
% endfor
</ul>
<a href="#" class="cancel-button">Cancel</a>
</div>
% endfor
% for type, templates in sorted(component_templates.items()):
<div class="new-component-templates new-component-${type}">
<h3 class="title">Select <span class="type">${type}</span> component type:</h3>
<ul class="new-component-template">
% for name, location, has_markdown, is_empty in templates:
% if is_empty:
<li class="editor-md empty">
<a href="#" data-location="${location}">
<span class="name"><i class="ss-icon ss-symbolicons-block">&#xE714;</i> ${name}</span>
<span class="editor-indicator">Simple <span class="sr">Editor</span></span>
</a>
</li>
% elif has_markdown:
<li class="editor-md">
<a href="#" data-location="${location}">
<span class="name"><i class="ss-icon ss-symbolicons-block">&#xE714;</i> ${name}</span>
<span class="editor-indicator">Simple <span class="sr">Editor</span></span>
</a>
</li>
% else:
<li class="editor-manual">
<a href="#" data-location="${location}">
<span class="name"><i class="ss-icon ss-symbolicons-block">&#x1F527;</i> ${name}</span>
<span class="editor-indicator">Advanced <span class="sr">Editor</span></span>
</a>
</li>
% endif
%endfor
</ul>
<a href="#" class="cancel-button">Cancel</a>
</div>
% endfor
</li>
</ol>
</article>
......
<%include file="metadata-edit.html" />
<section class="problem-editor editor">
<div class="row">
%if markdown != '' or data == '<problem>\n</problem>\n':
<div class="editor-bar">
<ul class="format-buttons">
<li><a href="#" class="multiple-choice-button" data-tooltip="Multiple Choice"><span
class="problem-editor-icon multiple-choice"></span></a></li>
<li><a href="#" class="checks-button" data-tooltip="Check Multiple"><span
class="problem-editor-icon checks"></span></a></li>
<li><a href="#" class="string-button" data-tooltip="String Response"><span
class="problem-editor-icon string"></span></a></li>
<li><a href="#" class="number-button" data-tooltip="Numerical Response"><span
class="problem-editor-icon number"></span></a></li>
<li><a href="#" class="dropdown-button" data-tooltip="Dropdown"><span
class="problem-editor-icon dropdown"></span></a></li>
</ul>
<ul class="editor-tabs">
<li><a href="#" class="xml-tab tab" data-tab="xml">Use Advanced Editor</a></li>
<li><a href="#" class="cheatsheet-toggle" data-tooltip="Toggle Cheatsheet">?</a></li>
</ul>
</div>
<textarea class="markdown-box">${markdown}</textarea>
%endif
<textarea class="xml-box" rows="8" cols="40">${data | h}</textarea>
</div>
</section>
<script type="text/template" id="simple-editor-cheatsheet">
<article class="simple-editor-cheatsheet">
<div class="cheatsheet-wrapper">
<div class="row">
<h6>Multiple Choice</h6>
<div class="col sample">
<img src="/static/img/choice-example.png" />
</div>
<div class="col">
<pre><code>( ) red
( ) green
(x) blue</code></pre>
</div>
</div>
<div class="row">
<h6>Multiple Check</h6>
<div class="col sample">
<img src="/static/img/multi-example.png" />
</div>
<div class="col">
<pre><code>[x] earth
[ ] wind
[x] water</code></pre>
</div>
</div>
<div class="row">
<h6>String Response</h6>
<div class="col sample">
<img src="/static/img/string-example.png" />
</div>
<div class="col">
<pre><code>= dog</code></pre>
</div>
</div>
<div class="row">
<h6>Numerical Response</h6>
<div class="col sample">
<img src="/static/img/number-example.png" />
</div>
<div class="col">
<pre><code>= 3.14 +- 2%</code></pre>
</div>
</div>
<div class="row">
<h6>Option Response</h6>
<div class="col sample">
<img src="/static/img/select-example.png" />
</div>
<div class="col">
<pre><code>[[wrong, (right)]]</code></pre>
</div>
</div>
</div>
</article>
</script>
......@@ -670,11 +670,29 @@ class CapaDescriptor(RawDescriptor):
stores_state = True
has_score = True
template_dir_name = 'problem'
mako_template = "widgets/problem-edit.html"
js = {'coffee': [resource_string(__name__, 'js/src/problem/edit.coffee')]}
js_module_name = "MarkdownEditingDescriptor"
css = {'scss': [resource_string(__name__, 'css/problem/edit.scss')]}
# Capa modules have some additional metadata:
# TODO (vshnayder): do problems have any other metadata? Do they
# actually use type and points?
metadata_attributes = RawDescriptor.metadata_attributes + ('type', 'points')
def get_context(self):
_context = RawDescriptor.get_context(self)
_context.update({'markdown': self.metadata.get('markdown', '')})
return _context
@property
def editable_metadata_fields(self):
"""Remove metadata from the editable fields since it has its own editor"""
subset = super(CapaDescriptor,self).editable_metadata_fields
if 'markdown' in subset:
subset.remove('markdown')
return subset
# VS[compat]
# TODO (cpennington): Delete this method once all fall 2012 course are being
......
.editor-bar {
position: relative;
@include linear-gradient(top, #d4dee8, #c9d5e2);
padding: 5px;
border: 1px solid #3c3c3c;
border-radius: 3px 3px 0 0;
border-bottom-color: #a5aaaf;
@include clearfix;
a {
display: block;
float: left;
padding: 3px 10px 7px;
margin-left: 7px;
border-radius: 2px;
&:hover {
background: rgba(255, 255, 255, .5);
}
}
.editor-tabs {
position: absolute;
top: 10px;
right: 10px;
li {
float: left;
}
.tab {
height: 24px;
padding: 7px 20px 3px;
border: 1px solid #a5aaaf;
border-radius: 3px 3px 0 0;
@include linear-gradient(top, rgba(0, 0, 0, 0) 87%, rgba(0, 0, 0, .06));
background-color: #e5ecf3;
font-size: 13px;
color: #3c3c3c;
box-shadow: 1px -1px 1px rgba(0, 0, 0, .05);
&.current {
background: #fff;
border-bottom-color: #fff;
}
}
.cheatsheet-toggle {
width: 21px;
height: 21px;
padding: 0;
margin: 3px 5px 0 16px;
border-radius: 22px;
border: 1px solid #a5aaaf;
background: #e5ecf3;
font-size: 13px;
font-weight: 700;
color: #565d64;
text-align: center;
}
}
}
.simple-editor-cheatsheet {
position: absolute;
top: 0;
left: 100%;
width: 0;
border-radius: 0 3px 3px 0;
@include linear-gradient(left, rgba(0, 0, 0, .1), rgba(0, 0, 0, 0) 15px);
background-color: #fff;
overflow: hidden;
@include transition(width .3s);
&.shown {
width: 300px;
}
.cheatsheet-wrapper {
width: 240px;
padding: 20px 30px;
}
h6 {
margin-bottom: 7px;
font-size: 15px;
font-weight: 700;
}
.row {
@include clearfix;
padding-bottom: 5px !important;
margin-bottom: 10px !important;
border-bottom: 1px solid #ddd !important;
&:last-child {
border-bottom: none !important;
margin-bottom: 0 !important;
}
}
.col {
float: left;
&.sample {
width: 60px;
margin-right: 30px;
}
}
pre {
font-size: 12px;
line-height: 18px;
}
code {
padding: 0;
background: none;
}
}
.problem-editor-icon {
display: inline-block;
width: 26px;
height: 21px;
vertical-align: middle;
background: url(../img/problem-editor-icons.png) no-repeat;
}
.problem-editor-icon.multiple-choice {
background-position: 0 0;
}
.problem-editor-icon.checks {
background-position: -56px 0;
}
.problem-editor-icon.string {
width: 28px;
background-position: -111px 0;
}
.problem-editor-icon.number {
width: 24px;
background-position: -168px 0;
}
.problem-editor-icon.dropdown {
width: 17px;
background-position: -220px 0;
}
......@@ -142,7 +142,7 @@ describe 'Problem', ->
xdescribe 'when the response is undetermined', ->
it 'alert the response', ->
spyOn window, 'alert'
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
spyOn($, 'postWithPrefix').andCallFake (url, answers, callback) ->
callback(success: 'Number Only!')
@problem.check()
expect(window.alert).toHaveBeenCalledWith 'Number Only!'
......
---
metadata:
display_name: Empty
rerandomize: never
showanswer: always
markdown: ""
data: |
<problem>
</problem>
children: []
......@@ -3,6 +3,46 @@ metadata:
display_name: Multiple Choice
rerandomize: never
showanswer: always
markdown:
"A multiple choice problem presents radio buttons for student input. Students can only select a single
option presented. Multiple Choice questions have been the subject of many areas of research due to the early
invention and adoption of bubble sheets.
One of the main elements that goes into a good multiple choice question is the existence of good distractors.
That is, each of the alternate responses presented to the student should be the result of a plausible mistake
that a student might make.
What Apple device competed with the portable CD player?
( ) The iPad
( ) Napster
(x) The iPod
( ) The vegetable peeler
( ) Android
( ) The Beatles
<solution>
<div class='detailed-solution'>
Explanation
The release of the iPod allowed consumers to carry their entire music library with them in a
format that did not rely on fragile and energy-intensive spinning disks.
</div>
</solution>
"
data: |
<problem>
<p>
......@@ -25,7 +65,7 @@ data: |
<solution>
<div class="detailed-solution">
<p>Explanation</p>
<p>The release of the iPod allowed consumers to carry their entire music library with them in a format that did not rely on fragile and enery-intensive spinning disks. </p>
<p>The release of the iPod allowed consumers to carry their entire music library with them in a format that did not rely on fragile and energy-intensive spinning disks. </p>
</div>
</solution>
</problem>
......
......@@ -3,6 +3,53 @@ metadata:
display_name: Numerical Response
rerandomize: never
showanswer: always
markdown:
"A numerical response problem accepts a line of text input from the
student, and evaluates the input for correctness based on its
numerical value.
The answer is correct if it is within a specified numerical tolerance
of the expected answer.
Enter the numerical value of Pi:
= 3.14159 +- .02
Enter the approximate value of 502*9:
= 4518 +- 15%
Enter the number of fingers on a human hand:
= 5
<solution>
<div class='detailed-solution'>
Explanation
Pi, or the the ratio between a circle's circumference to its diameter, is an irrational number
known to extreme precision. It is value is approximately equal to 3.14.
Although you can get an exact value by typing 502*9 into a calculator, the result will be close to
500*10, or 5,000. The grader accepts any response within 15% of the true value, 4518, so that you
can use any estimation technique that you like.
If you look at your hand, you can count that you have five fingers.
</div>
</solution>
"
data: |
<problem>
<p>
......
......@@ -3,6 +3,36 @@ metadata:
display_name: Option Response
rerandomize: never
showanswer: always
markdown:
"OptionResponse gives a limited set of options for students to respond with, and presents those options
in a format that encourages them to search for a specific answer rather than being immediately presented
with options from which to recognize the correct answer.
The answer options and the identification of the correct answer is defined in the <b>optioninput</b> tag.
Translation between Option Response and __________ is extremely straightforward:
[[(Multiple Choice), String Response, Numerical Response, External Response, Image Response]]
<solution>
<div class='detailed-solution'>
Explanation
Multiple Choice also allows students to select from a variety of pre-written responses, although the
format makes it easier for students to read very long response options. Optionresponse also differs
slightly because students are more likely to think of an answer and then search for it rather than
relying purely on recognition to answer the question.
</div>
</solution>
"
data: |
<problem>
<p>OptionResponse gives a limited set of options for students to respond with, and presents those options
......
......@@ -3,6 +3,37 @@ metadata:
display_name: String Response
rerandomize: never
showanswer: always
# Note, the extra newlines are needed to make the yaml parser add blank lines instead of folding
markdown:
"A string response problem accepts a line of text input from the
student, and evaluates the input for correctness based on an expected
answer within each input box.
The answer is correct if it matches every character of the expected answer. This can be a problem with
international spelling, dates, or anything where the format of the answer is not clear.
Which US state has Lansing as its capital?
= Michigan
<solution>
<div class='detailed-solution'>
Explanation
Lansing is the capital of Michigan, although it is not Michgan's largest city,
or even the seat of the county in which it resides.
</div>
</solution>
"
data: |
<problem showanswer="always">
<p>
......
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