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): ...@@ -62,6 +62,14 @@ class PollBase(XBlock, ResourceMixin, PublishEventMixin):
return any(value['img'] for value in dict(field).values()) return any(value['img'] for value in dict(field).values())
@staticmethod @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): 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. Gathers a set of label-img pairs from a data dict and puts them in order.
...@@ -161,7 +169,7 @@ class PollBlock(PollBase): ...@@ -161,7 +169,7 @@ class PollBlock(PollBase):
Handlebars template can use. Handlebars template can use.
""" """
tally = [] tally = []
answers = OrderedDict(self.answers) answers = OrderedDict(self.markdown_items(self.answers))
choice = self.get_choice() choice = self.get_choice()
total = 0 total = 0
self.clean_tally() self.clean_tally()
...@@ -224,7 +232,7 @@ class PollBlock(PollBase): ...@@ -224,7 +232,7 @@ class PollBlock(PollBase):
context.update({ context.update({
'choice': choice, 'choice': choice,
# Offset so choices will always be True. # Offset so choices will always be True.
'answers': self.answers, 'answers': self.markdown_items(self.answers),
'question': markdown(self.question), 'question': markdown(self.question),
# Mustache is treating an empty string as true. # Mustache is treating an empty string as true.
'feedback': markdown(self.feedback) or False, 'feedback': markdown(self.feedback) or False,
...@@ -405,7 +413,7 @@ class SurveyBlock(PollBase): ...@@ -405,7 +413,7 @@ class SurveyBlock(PollBase):
# Offset so choices will always be True. # Offset so choices will always be True.
'answers': self.answers, 'answers': self.answers,
'js_template': js_template, 'js_template': js_template,
'questions': self.questions, 'questions': self.markdown_items(self.questions),
'any_img': self.any_image(self.questions), 'any_img': self.any_image(self.questions),
# Mustache is treating an empty string as true. # Mustache is treating an empty string as true.
'feedback': markdown(self.feedback) or False, 'feedback': markdown(self.feedback) or False,
...@@ -439,7 +447,7 @@ class SurveyBlock(PollBase): ...@@ -439,7 +447,7 @@ class SurveyBlock(PollBase):
Handlebars template can use. Handlebars template can use.
""" """
tally = [] tally = []
questions = OrderedDict(self.questions) questions = OrderedDict(self.markdown_items(self.questions))
default_answers = OrderedDict([(answer, 0) for answer, __ in self.answers]) default_answers = OrderedDict([(answer, 0) for answer, __ in self.answers])
choices = self.get_choices() choices = self.get_choices()
total = 0 total = 0
...@@ -638,8 +646,8 @@ class SurveyBlock(PollBase): ...@@ -638,8 +646,8 @@ class SurveyBlock(PollBase):
""" """
<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}}' <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."]]' 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", {"label": "Strongly Agree"}], ["a", {"label": "Agree"}], ["n", {"label": "Neutral"}], ["d", {"label": "Disagree"}], ["sd", {"label": "Strongly Disagree"}]]' answers='[["sa", "Strongly Agree"], ["a", "Agree"], ["n", "Neutral"], ["d", "Disagree"], ["sd", "Strongly Disagree"]]'
feedback="### Thank you&#10;&#10;for running the tests."/> feedback="### Thank you&#10;&#10;for running the tests."/>
</vertical_demo> </vertical_demo>
""") """)
......
...@@ -184,3 +184,10 @@ th.survey-answer { ...@@ -184,3 +184,10 @@ th.survey-answer {
.survey-choice { .survey-choice {
background-color: #e5ebee; 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"> <script id="poll-results-template" type="text/html">
<h2 class="poll-header">{{display_name}}</h2> <h2 class="poll-header">{{display_name}}</h2>
{{{question}}} <div class="poll-question-container">{{{question}}}</div>
<ul class="poll-answers-results"> <ul class="poll-answers-results">
{{#each tally}} {{#each tally}}
<li class="poll-result"> <li class="poll-result">
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
{{/if}} {{/if}}
<div class="percentage-gauge-container"> <div class="percentage-gauge-container">
<div class="percentage-gauge" style="width:{{percent}}%;"> <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> </div>
<div class="poll-percent-container"> <div class="poll-percent-container">
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
<tr> <tr>
<th></th> <th></th>
{{#each answers}} {{#each answers}}
<th class="survey-answer">{{this}}</th> <th class="survey-answer">{{{this}}}</th>
{{/each}} {{/each}}
</tr> </tr>
</thead> </thead>
...@@ -16,7 +16,6 @@ ...@@ -16,7 +16,6 @@
<img src="{{img}}" /> <img src="{{img}}" />
</div> </div>
{{/if}} {{/if}}
{% endif %}
<td class="survey-question">{{{text}}}</td> <td class="survey-question">{{{text}}}</td>
{{#each answers}} {{#each answers}}
<td class="survey-percentage survey-option{{#if choice}} survey-choice{{/if}}{{#if top}} poll-top-choice{{/if}}">{{percent}}%</td> <td class="survey-percentage survey-option{{#if choice}} survey-choice{{/if}}{{#if top}} poll-top-choice{{/if}}">{{percent}}%</td>
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
</label> </label>
</div> </div>
{% endif %} {% 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> </li>
{% endfor %} {% endfor %}
</ul> </ul>
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
<img src="{{question.img}}" /> <img src="{{question.img}}" />
</div> </div>
{% endif %} {% endif %}
{{question.label}} {{question.label|safe}}
</td> </td>
{% for answer, answer_details in answers %} {% for answer, answer_details in answers %}
<td class="survey-option"> <td class="survey-option">
......
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
function PollEditUtil(runtime, element, pollType) { function PollEditUtil(runtime, element, pollType) {
var self = this; var self = this;
// These URLs aren't validated in real time, so even if they don't exist for a type of block // 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. // we can create a reference to them.
this.loadAnswers = runtime.handlerUrl(element, 'load_answers'); this.loadAnswers = runtime.handlerUrl(element, 'load_answers');
...@@ -33,7 +32,8 @@ function PollEditUtil(runtime, element, pollType) { ...@@ -33,7 +32,8 @@ function PollEditUtil(runtime, element, pollType) {
new_item, button_mapping[context_key]['topMarker'], new_item, button_mapping[context_key]['topMarker'],
button_mapping[context_key]['bottomMarker'] button_mapping[context_key]['bottomMarker']
); );
new_item.fadeOut(250).fadeIn(250); self.scrollTo(new_item);
new_item.fadeOut(0).fadeIn('slow', 'swing');
} }
}(key) }(key)
) )
...@@ -58,6 +58,13 @@ function PollEditUtil(runtime, element, pollType) { ...@@ -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) { this.extend = function (obj1, obj2) {
// Mimics similar extend functions, making obj1 contain obj2's properties. // Mimics similar extend functions, making obj1 contain obj2's properties.
for (var attrname in obj2) { for (var attrname in obj2) {
...@@ -91,7 +98,8 @@ function PollEditUtil(runtime, element, pollType) { ...@@ -91,7 +98,8 @@ function PollEditUtil(runtime, element, pollType) {
return; return;
} }
tag.prev().before(tag); 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 () { $('.poll-move-down', scope).click(function () {
var tag = $(this).parents('li'); var tag = $(this).parents('li');
...@@ -99,7 +107,8 @@ function PollEditUtil(runtime, element, pollType) { ...@@ -99,7 +107,8 @@ function PollEditUtil(runtime, element, pollType) {
return; return;
} }
tag.next().after(tag); 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) { ...@@ -144,12 +153,13 @@ function PollEditUtil(runtime, element, pollType) {
this.displayItems = function(data, topMarker, bottomMarker) { this.displayItems = function(data, topMarker, bottomMarker) {
// Loads the initial set of items that the block needs to edit. // Loads the initial set of items that the block needs to edit.
$(bottomMarker).before(self.answerTemplate(data)); var result = $(self.answerTemplate(data));
self.empowerDeletes(element, topMarker, bottomMarker); $(bottomMarker).before(result);
self.empowerArrows(element, topMarker, bottomMarker); 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. // Handle the return value JSON from the server.
// It would be better if we could have a different function // It would be better if we could have a different function
// for errors, as AJAX calls normally allow, but our version of XBlock // for errors, as AJAX calls normally allow, but our version of XBlock
...@@ -177,8 +187,6 @@ function PollEditUtil(runtime, element, pollType) { ...@@ -177,8 +187,6 @@ function PollEditUtil(runtime, element, pollType) {
data[field].push({'key': name}) data[field].push({'key': name})
} }
var index = tracker.indexOf(name); var index = tracker.indexOf(name);
console.log(data[field]);
console.log(index);
data[field][index][key] = scope.value; data[field][index][key] = scope.value;
return true return true
}; };
...@@ -207,7 +215,7 @@ function PollEditUtil(runtime, element, pollType) { ...@@ -207,7 +215,7 @@ function PollEditUtil(runtime, element, pollType) {
type: "POST", type: "POST",
url: handlerUrl, url: handlerUrl,
data: JSON.stringify(data), data: JSON.stringify(data),
success: self.check_return success: self.checkReturn
}); });
}; };
......
-e . -e .
markdown markdown
-e git+https://github.com/edx-solutions/xblock-utils.git#egg=xblock-utils -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. 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 from .base_test import PollBaseTest
@ddt
class MarkdownTestCase(PollBaseTest): class MarkdownTestCase(PollBaseTest):
""" """
Tests for the Markdown functionality. 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): 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):
""" """
Ensure Markdown is parsed for feedback. 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.
Both are checked by default.
""" """
self.go_to_page("Poll Markdown") 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.browser.find_element_by_css_selector('input[type=radio]').click()
self.get_submit().click() self.get_submit().click()
self.wait_until_exists('.poll-feedback') self.wait_until_exists('.poll-feedback')
self.assertEqual( self.assertEqual(self.get_selector_text(selector), result)
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.""")
<vertical_demo> <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?" 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'}]]" /> 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>
<vertical_demo> <vertical_demo>
<poll tally="{'long': 20, 'short': 29, 'not_saying': 15, 'longer' : 35}" <poll tally="{'long': 20, 'short': 29, 'not_saying': 15, 'longer' : 35}"
question="## How long have you been studying with us?" 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."/> feedback="### Thank you&#10;&#10;for being a valued student."/>
</vertical_demo> </vertical_demo>
<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_**." /> <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>
<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