Commit b3c323f5 by Jonathan Piacenti

More markdown areas, scroll handling, README, tests.

parent 866f7529
# XBlock-Poll
> A user-friendly way to query students.
## Introduction
This XBlock enables a course author to create survey/poll elements to get
feedback from students. The XBlocks can either be *poll* or *survey* XBlocks. *Poll* XBlocks have one
question, and a series of answers. *Survey* XBlocks have several questions and a handful of (terse) answers that
a student is expect to answer each one from (Such as 'True', and 'False', or 'Agree' or 'Disagree')
## Features
Survey and Poll are both designed to minimize the amount of fiddling a course author will have to
do in order to create the user experience they desire. By default, answers in polls and questions in surveys
are able to be enhanced with markdown (though it is not recommended to do more than line formatting with it)
and images. Formatting for images is handled by the XBlock's formatters to keep a consistent and sane user experience.
The feedback section of a poll or survey is shown after a user has completed the block. It, along with a poll block's
question field, are intended to make full use of markdown.
## Notes
A poll or survey should not be deployed until its construction is finalized. Changing an answer or question can
cause previous respondent's answers to remap and give an inaccurate picture of the responses.
If a poll has changed enough that it leaves a previous voter's choice ambiguous, their response will be eliminated
from the tally upon their next visit, and they will be permitted to vote again. However, they will not lose progress
or their score.
Things that could make a poll's previous answers ambiguous include adding or removing a question, or adding or
removing an answer.
## Grading
Each block has a score value of 1, credited to the student upon creation of the block.
......@@ -62,6 +62,14 @@ class PollBase(XBlock, ResourceMixin, PublishEventMixin):
return any(value['img'] for value in dict(field).values())
@staticmethod
def markdown_items(items):
"""
Convert all items' labels into markdown.
"""
return [[key, {'label': markdown(value['label']), 'img': value['img']}]
for key, value in items]
@staticmethod
def gather_items(data, result, noun, field, image=True):
"""
Gathers a set of label-img pairs from a data dict and puts them in order.
......@@ -161,7 +169,7 @@ class PollBlock(PollBase):
Handlebars template can use.
"""
tally = []
answers = OrderedDict(self.answers)
answers = OrderedDict(self.markdown_items(self.answers))
choice = self.get_choice()
total = 0
self.clean_tally()
......@@ -224,7 +232,7 @@ class PollBlock(PollBase):
context.update({
'choice': choice,
# Offset so choices will always be True.
'answers': self.answers,
'answers': self.markdown_items(self.answers),
'question': markdown(self.question),
# Mustache is treating an empty string as true.
'feedback': markdown(self.feedback) or False,
......@@ -405,7 +413,7 @@ class SurveyBlock(PollBase):
# Offset so choices will always be True.
'answers': self.answers,
'js_template': js_template,
'questions': self.questions,
'questions': self.markdown_items(self.questions),
'any_img': self.any_image(self.questions),
# Mustache is treating an empty string as true.
'feedback': markdown(self.feedback) or False,
......@@ -439,7 +447,7 @@ class SurveyBlock(PollBase):
Handlebars template can use.
"""
tally = []
questions = OrderedDict(self.questions)
questions = OrderedDict(self.markdown_items(self.questions))
default_answers = OrderedDict([(answer, 0) for answer, __ in self.answers])
choices = self.get_choices()
total = 0
......@@ -636,11 +644,11 @@ class SurveyBlock(PollBase):
"""),
("Survey Functions",
"""
<vertical_demo>
<survey tally='{"q1": {"sa": 5, "a": 5, "n": 3, "d": 2, "sd": 5}, "q2": {"sa": 3, "a": 2, "n": 3, "d": 10, "sd": 2}, "q3": {"sa": 2, "a": 7, "n": 1, "d": 4, "sd": 6}, "q4": {"sa": 1, "a": 2, "n": 8, "d": 4, "sd": 5}}'
questions='[["q1", "I feel like this test will pass."], ["q2", "I like testing software"], ["q3", "Testing is not necessary"], ["q4", "I would fake a test result to get software deployed."]]'
answers='[["sa", {"label": "Strongly Agree"}], ["a", {"label": "Agree"}], ["n", {"label": "Neutral"}], ["d", {"label": "Disagree"}], ["sd", {"label": "Strongly Disagree"}]]'
feedback="### Thank you&#10;&#10;for running the tests."/>
</vertical_demo>
<vertical_demo>
<survey tally='{"q1": {"sa": 5, "a": 5, "n": 3, "d": 2, "sd": 5}, "q2": {"sa": 3, "a": 2, "n": 3, "d": 10, "sd": 2}, "q3": {"sa": 2, "a": 7, "n": 1, "d": 4, "sd": 6}, "q4": {"sa": 1, "a": 2, "n": 8, "d": 4, "sd": 5}}'
questions='[["q1", {"label": "I feel like this test will pass.", "img": null}], ["q2", {"label": "I like testing software", "img": null}], ["q3", {"label": "Testing is not necessary", "img": null}], ["q4", {"label": "I would fake a test result to get software deployed.", "img": null}]]'
answers='[["sa", "Strongly Agree"], ["a", "Agree"], ["n", "Neutral"], ["d", "Disagree"], ["sd", "Strongly Disagree"]]'
feedback="### Thank you&#10;&#10;for running the tests."/>
</vertical_demo>
""")
]
......@@ -184,3 +184,10 @@ th.survey-answer {
.survey-choice {
background-color: #e5ebee;
}
/* Counteract Markdown's wrapping in paragraphs */
.poll-answer p, .survey-question p, .poll-answer-label p{
margin: 0;
padding: 0;
}
<script id="poll-results-template" type="text/html">
<h2 class="poll-header">{{display_name}}</h2>
{{{question}}}
<div class="poll-question-container">{{{question}}}</div>
<ul class="poll-answers-results">
{{#each tally}}
<li class="poll-result">
......@@ -18,7 +18,7 @@
{{/if}}
<div class="percentage-gauge-container">
<div class="percentage-gauge" style="width:{{percent}}%;">
<label class="poll-answer-label" for="answer-{{key}}">{{answer}}</label>
<label class="poll-answer-label" for="answer-{{key}}">{{{answer}}}</label>
</div>
</div>
<div class="poll-percent-container">
......
......@@ -5,7 +5,7 @@
<tr>
<th></th>
{{#each answers}}
<th class="survey-answer">{{this}}</th>
<th class="survey-answer">{{{this}}}</th>
{{/each}}
</tr>
</thead>
......@@ -16,7 +16,6 @@
<img src="{{img}}" />
</div>
{{/if}}
{% endif %}
<td class="survey-question">{{{text}}}</td>
{{#each answers}}
<td class="survey-percentage survey-option{{#if choice}} survey-choice{{/if}}{{#if top}} poll-top-choice{{/if}}">{{percent}}%</td>
......
......@@ -20,7 +20,7 @@
</label>
</div>
{% endif %}
<label class="poll-answer" for="{{url_name}}-answer-{{key}}">{{value.label}}</label>
<label class="poll-answer" for="{{url_name}}-answer-{{key}}">{{value.label|safe}}</label>
</li>
{% endfor %}
</ul>
......
......@@ -21,7 +21,7 @@
<img src="{{question.img}}" />
</div>
{% endif %}
{{question.label}}
{{question.label|safe}}
</td>
{% for answer, answer_details in answers %}
<td class="survey-option">
......
......@@ -2,7 +2,6 @@
function PollEditUtil(runtime, element, pollType) {
var self = this;
// These URLs aren't validated in real time, so even if they don't exist for a type of block
// we can create a reference to them.
this.loadAnswers = runtime.handlerUrl(element, 'load_answers');
......@@ -33,7 +32,8 @@ function PollEditUtil(runtime, element, pollType) {
new_item, button_mapping[context_key]['topMarker'],
button_mapping[context_key]['bottomMarker']
);
new_item.fadeOut(250).fadeIn(250);
self.scrollTo(new_item);
new_item.fadeOut(0).fadeIn('slow', 'swing');
}
}(key)
)
......@@ -58,6 +58,13 @@ function PollEditUtil(runtime, element, pollType) {
}
};
this.scrollTo = function (item){
// Scrolls to the center of a particular item in the settings, then flash it.
var parent = $('#poll-line-items');
var item_center = parent.scrollTop() + item.position().top - parent.height()/2 + item.height() / 2;
parent.animate({ scrollTop: item_center }, "slow");
};
this.extend = function (obj1, obj2) {
// Mimics similar extend functions, making obj1 contain obj2's properties.
for (var attrname in obj2) {
......@@ -91,7 +98,8 @@ function PollEditUtil(runtime, element, pollType) {
return;
}
tag.prev().before(tag);
tag.fadeOut("fast", "swing").fadeIn("fast", "swing");
tag.fadeOut(0).fadeIn('slow', 'swing');
self.scrollTo(tag)
});
$('.poll-move-down', scope).click(function () {
var tag = $(this).parents('li');
......@@ -99,7 +107,8 @@ function PollEditUtil(runtime, element, pollType) {
return;
}
tag.next().after(tag);
tag.fadeOut("fast", "swing").fadeIn("fast", "swing");
tag.fadeOut(0).fadeIn('slow', 'swing');
self.scrollTo(tag)
});
};
......@@ -144,12 +153,13 @@ function PollEditUtil(runtime, element, pollType) {
this.displayItems = function(data, topMarker, bottomMarker) {
// Loads the initial set of items that the block needs to edit.
$(bottomMarker).before(self.answerTemplate(data));
self.empowerDeletes(element, topMarker, bottomMarker);
self.empowerArrows(element, topMarker, bottomMarker);
var result = $(self.answerTemplate(data));
$(bottomMarker).before(result);
self.empowerDeletes(result, topMarker, bottomMarker);
self.empowerArrows(result, topMarker, bottomMarker);
};
this.check_return = function(data) {
this.checkReturn = function(data) {
// Handle the return value JSON from the server.
// It would be better if we could have a different function
// for errors, as AJAX calls normally allow, but our version of XBlock
......@@ -177,8 +187,6 @@ function PollEditUtil(runtime, element, pollType) {
data[field].push({'key': name})
}
var index = tracker.indexOf(name);
console.log(data[field]);
console.log(index);
data[field][index][key] = scope.value;
return true
};
......@@ -207,7 +215,7 @@ function PollEditUtil(runtime, element, pollType) {
type: "POST",
url: handlerUrl,
data: JSON.stringify(data),
success: self.check_return
success: self.checkReturn
});
};
......
-e .
markdown
-e git+https://github.com/edx-solutions/xblock-utils.git#egg=xblock-utils
-e git://github.com/nosedjango/nosedjango.git@ed7d7f9aa969252ff799ec159f828eaa8c1cbc5a#egg=nosedjango-dev
ddt
"""
Contains a list of lists that will be used as the DDT arguments for the markdown test.
"""
ddt_scenarios = [
[
"Poll Markdown", '.poll-question-container',
"""<h2>This is a test</h2>
<h1>This is only a &gt;&lt;test</h1>
<ul>
<li>One</li>
<li>Two</li>
<li>
<p>Three</p>
</li>
<li>
<p>First</p>
</li>
<li>Second</li>
<li>Third</li>
</ul>
<p>We shall find out if markdown is respected.</p>
<blockquote>
<p>"I have not yet begun to code."</p>
</blockquote>"""
],
[
"Poll Markdown", '.poll-feedback',
"""<h3>This is some feedback</h3>
<p><a href="http://www.example.com">This is a link</a></p>
<p><a href="http://www.example.com" target="_blank">This is also a link.</a></p>
<p>This is a paragraph with <em>emphasized</em> and <strong>bold</strong> text, and <strong><em>both</em></strong>.</p>""",
False
],
[
"Poll Markdown", "label.poll-answer", "<p>I <em>feel</em> like this test will <strong>pass</strong><code>test</code>.</p>",
True, False
],
[
"Poll Markdown", "label.poll-answer-label", "<p>I <em>feel</em> like this test will <strong>pass</strong><code>test</code>.</p>",
False, True
],
[
"Survey Markdown", '.survey-question', "<p>I <em>feel</em> like this test will <strong>pass</strong><code>test</code>.</p>"
],
[
"Survey Markdown", '.poll-feedback',
"""<h3>This is some feedback</h3>
<p><a href="http://www.example.com">This is a link</a></p>
<p><a href="http://www.example.com" target="_blank">This is also a link.</a></p>
<p>This is a paragraph with <em>emphasized</em> and <strong>bold</strong> text, and <strong><em>both</em></strong>.</p>""",
False
],
]
"""
Tests to make sure that markdown is both useful and secure.
"""
from ddt import ddt, unpack, data
from .markdown_scenarios import ddt_scenarios
from .base_test import PollBaseTest
@ddt
class MarkdownTestCase(PollBaseTest):
"""
Tests for the Markdown functionality.
"""
def test_question_markdown(self):
"""
Ensure Markdown is parsed for questions.
"""
self.go_to_page("Poll Markdown")
self.assertEqual(
self.browser.find_element_by_css_selector('.poll-question-container').text,
"""This is a test
This is only a ><test
One
Two
Three
First
Second
Third
We shall find out if markdown is respected.
"I have not yet begun to code.\"""")
def test_feedback_markdown(self):
"""
Ensure Markdown is parsed for feedback.
def get_selector_text(self, selector):
return self.browser.find_element_by_css_selector(selector).get_attribute('innerHTML').strip()
@data(*ddt_scenarios)
@unpack
def test_markdown(self, page, selector, result, front=True, back=True):
"""
self.go_to_page("Poll Markdown")
self.browser.find_element_by_css_selector('input[type=radio]').click()
self.get_submit().click()
Test Markdown for a field.
selector is a CSS selector to check for markdown results
result is the desired result string
front means the check will be done before the form is submitted
back means it will be done afterward.
self.wait_until_exists('.poll-feedback')
self.assertEqual(
self.browser.find_element_by_css_selector('.poll-feedback').text,
"""This is some feedback
This is a link
This is also a link.
This is a paragraph with emphasized and bold text, and both.""")
Both are checked by default.
"""
self.go_to_page(page)
if front:
self.assertEqual(self.get_selector_text(selector), result)
if back:
self.browser.find_element_by_css_selector('input[type=radio]').click()
self.get_submit().click()
self.wait_until_exists('.poll-feedback')
self.assertEqual(self.get_selector_text(selector), result)
<vertical_demo>
<poll tally="{'red': 20, 'fennec': 29, 'kit': 15, 'arctic' : 35}"
<poll tally='{"red": 20, "fennec": 29, "kit": 15, "arctic" : 35}'
question="## What is your favorite kind of fox?"
answers="[['red', {'label': 'Red Fox', 'img': '../img/red_fox.png'}], ['fennec', {'label': 'Fennec Fox', 'img': '../img/fennec_fox.png'}], ['kit', {'label': 'Kit Fox', 'img': '../img/kit_fox.png'}], ['arctic', {'label': 'Arctic fox', 'img': '../img/arctic_fox.png'}]]" />
</vertical_demo>
\ No newline at end of file
answers='[["red", {"label": "Red Fox", "img": "../img/red_fox.png"}], ["fennec", {"label": "Fennec Fox", "img": "../img/fennec_fox.png"}], ["kit", {"label": "Kit Fox", "img": "../img/kit_fox.png"}], ["arctic", {"label": "Arctic fox", "img": "../img/arctic_fox.png"}]]' />
</vertical_demo>
<vertical_demo>
<poll tally="{'long': 20, 'short': 29, 'not_saying': 15, 'longer' : 35}"
question="## How long have you been studying with us?"
answers="[['long', {'label': 'A very long time', 'img': None}], ['short', {'label': 'Not very long', 'img': None}], ['not_saying', {'label': 'I shall not say', 'img': None}], ['longer', {'label': 'Longer than you', 'img': None}]]"
answers='[["long", {"label": "A very long time", "img": null}], ["short", {"label": "Not very long", "img": null}], ["not_saying", {"label": "I shall not say", "img": null}], ["longer", {"label": "Longer than you", "img": null}]]'
feedback="### Thank you&#10;&#10;for being a valued student."/>
</vertical_demo>
\ No newline at end of file
</vertical_demo>
<vertical_demo>
<poll url_name="markdown" question="## This is a test&#10;&#10;&lt;h1&gt;This is only a &amp;gt;&amp;lt;test&lt;/h1&gt;&#10;&#10;* One&#10;* Two&#10;* Three&#10;&#10;1. First&#10;2. Second&#10;3. Third&#10;&#10;We shall find out if markdown is respected.&#10;&#10;&gt; &quot;I have not yet begun to code.&quot;" feedback="### This is some feedback&#10;&#10;[This is a link](http://www.example.com)&#10;&#10;&lt;a href=&quot;http://www.example.com&quot; target=&quot;_blank&quot;&gt;This is also a link.&lt;/a&gt;&#10;&#10;This is a paragraph with *emphasized* and **bold** text, and **_both_**." />
</vertical_demo>
\ No newline at end of file
<poll url_name="markdown" question="## This is a test&#10;&#10;&lt;h1&gt;This is only a &amp;gt;&amp;lt;test&lt;/h1&gt;&#10;&#10;* One&#10;* Two&#10;* Three&#10;&#10;1. First&#10;2. Second&#10;3. Third&#10;&#10;We shall find out if markdown is respected.&#10;&#10;&gt; &quot;I have not yet begun to code.&quot;"
feedback="### This is some feedback&#10;&#10;[This is a link](http://www.example.com)&#10;&#10;&lt;a href=&quot;http://www.example.com&quot; target=&quot;_blank&quot;&gt;This is also a link.&lt;/a&gt;&#10;&#10;This is a paragraph with *emphasized* and **bold** text, and **_both_**."
answers='[["long", {"label": "I *feel* like this test will **pass**&lt;code&gt;test&lt;/code&gt;.", "img": null}]]'/>
</vertical_demo>
<vertical_demo>
<survey url_name="markdown" questions='[["q1", {"label": "I *feel* like this test will **pass**&lt;code&gt;test&lt;/code&gt;.", "img": null}]]' feedback="### This is some feedback&#10;&#10;[This is a link](http://www.example.com)&#10;&#10;&lt;a href=&quot;http://www.example.com&quot; target=&quot;_blank&quot;&gt;This is also a link.&lt;/a&gt;&#10;&#10;This is a paragraph with *emphasized* and **bold** text, and **_both_**." />
</vertical_demo>
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