Commit 1b9c44da by Xavier Antoviaque

Merge pull request #2 from open-craft/survey

Version 2 with Survey
parents 59758e5f 42dc00e3
# 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 completion of the block.
## Analytics
Two events are fired by the XBlocks-- one for viewing the results of a poll, and one for submitting the user's choice.
The resulting events look like this for polls:
{"username": "staff", "host": "precise64", "event_source": "server", "event_type": "xblock.poll.submitted", "context": {"course_user_tags": {}, "user_id": 1, "org_id": "JediAcademy", "module": {"display_name": "Poll"}, "course_id": "JediAcademy/FW301/2015", "path": "/courses/JediAcademy/FW301/2015/xblock/i4x:;_;_JediAcademy;_FW301;_poll;_2d25e451be884aa7a15b33860d7c9647/handler/vote"}, "time": "2015-01-12T19:13:39.199098+00:00", "ip": "10.0.2.2", "event": {"url_name": "2d25e451be884aa7a15b33860d7c9647", "choice": "B"}, "agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:34.0) Gecko/20100101 Firefox/34.0", "page": "x_module"}
{"username": "staff", "host": "precise64", "event_source": "server", "event_type": "xblock.poll.view_results", "context": {"course_user_tags": {}, "user_id": 1, "org_id": "JediAcademy", "module": {"display_name": "Poll"}, "course_id": "JediAcademy/FW301/2015", "path": "/courses/JediAcademy/FW301/2015/xblock/i4x:;_;_JediAcademy;_FW301;_poll;_2d25e451be884aa7a15b33860d7c9647/handler/get_results"}, "time": "2015-01-12T19:13:39.474514+00:00", "ip": "10.0.2.2", "event": {}, "agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:34.0) Gecko/20100101 Firefox/34.0", "page": "x_module"}
...And like this for surveys:
{"username": "staff", "host": "precise64", "event_source": "server", "event_type": "xblock.survey.submitted", "context": {"course_user_tags": {}, "user_id": 1, "org_id": "JediAcademy", "module": {"display_name": "Survey"}, "course_id": "JediAcademy/FW301/2015", "path": "/courses/JediAcademy/FW301/2015/xblock/i4x:;_;_JediAcademy;_FW301;_survey;_e4975240b6c64a1e988bad86ea917070/handler/vote"}, "time": "2015-01-12T19:13:13.115038+00:00", "ip": "10.0.2.2", "event": {"url_name": "e4975240b6c64a1e988bad86ea917070", "choices": {"enjoy": "Y", "learn": "M", "recommend": "N"}}, "agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:34.0) Gecko/20100101 Firefox/34.0", "page": "x_module"}
{"username": "staff", "host": "precise64", "event_source": "server", "event_type": "xblock.survey.view_results", "context": {"course_user_tags": {}, "user_id": 1, "org_id": "JediAcademy", "module": {"display_name": "Survey"}, "course_id": "JediAcademy/FW301/2015", "path": "/courses/JediAcademy/FW301/2015/xblock/i4x:;_;_JediAcademy;_FW301;_survey;_e4975240b6c64a1e988bad86ea917070/handler/get_results"}, "time": "2015-01-12T19:13:13.513909+00:00", "ip": "10.0.2.2", "event": {}, "agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:34.0) Gecko/20100101 Firefox/34.0", "page": "x_module"}
from .poll import PollBlock
\ No newline at end of file
from .poll import PollBlock, SurveyBlock
......@@ -61,6 +61,7 @@ li.poll-result {
display: inline-block;
margin-bottom: 5px;
margin-top: 5px;
white-space: nowrap;
}
.poll-image {
......@@ -78,10 +79,18 @@ li.poll-result .poll-image {
margin-left: 0;
}
.poll-image img{
.poll-image-td {
width: 25%;
}
.poll-image img, .poll-image-td img{
width: 100%;
}
.poll-image-td{
display: inline-block;
}
.poll-percent-container {
display: table-cell;
text-align: left;
......@@ -101,4 +110,87 @@ li.poll-result .poll-image {
margin-top: 1em;
margin-bottom: 1em;
font-size: smaller;
}
\ No newline at end of file
}
.survey-table {
width: 100%;
border-collapse: collapse;
border: 0;
}
.survey-table td, .survey-table th {
border: 1px solid #CCC;
border-top: 0;
}
.survey-table tr:last-child td {
border-bottom: 0;
}
.survey-table tr td:first-child,
.survey-table tr th:first-child {
border-left: 0;
}
.survey-table tr td:last-child,
.survey-table tr th:last-child {
border-right: 0;
}
.survey-table thead {
border-top: 0;
background: none;
}
.survey-table thead tr th {
font-weight: normal;
font-size: .8rem;
}
.survey-table .survey-row {
background: none !important;
}
.survey-table .survey-option {
text-align: center;
vertical-align: middle;
}
.survey-option input {
margin: 0;
padding: 0;
}
th.survey-answer {
text-align: center;
width: 7%;
line-height: 1em;
padding-bottom: .25em;
}
.poll-header {
text-transform: uppercase;
}
.survey-percentage {
font-weight: bolder;
font-size: 1.2rem;
}
.survey-question {
font-weight: bold;
vertical-align: middle;
padding-top: .15em;
padding-bottom: .15em;
}
.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;
}
......@@ -37,4 +37,8 @@
.poll-move {
float: right;
}
\ No newline at end of file
}
.poll-setting-label {
text-transform: capitalize;
}
<script id="results" type="text/html">
{{{question}}}
<script id="poll-results-template" type="text/html">
<h2 class="poll-header">{{display_name}}</h2>
<div class="poll-question-container">{{{question}}}</div>
<ul class="poll-answers-results">
{{#each tally}}
<li class="poll-result">
......@@ -17,11 +18,11 @@
{{/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">
<span class="poll-percent-display{{#if top}} poll-top-choice{{/if}}">{{percent}}%</span>
<span class="poll-percent-display{{#if first}} poll-top-choice{{/if}}">{{percent}}%</span>
</div>
</li>
{{^last}}
......@@ -31,11 +32,12 @@
{{/each}}
</ul>
<input class="input-main" type="button" name="poll-submit" value="Submit" disabled>
<div class="poll-footnote">Results gathered from {{total}} respondent(s).</div>
<div class="poll-footnote">Results gathered from {{total}} respondent{{#if plural}}s{{/if}}.</div>
{{#if feedback}}
<hr />
<h2 class="poll-header">Feedback</h2>
<div class="poll-feedback">
{{{feedback}}}
</div>
{{/if}}
</script>
\ No newline at end of file
</script>
<script id="answer-form-component" type="text/html">
{{#each answers}}
<li class="field comp-setting-entry is-set">
<script id="poll-form-component" type="text/html">
{{#each items}}
<li class="field comp-setting-entry is-set poll-{{noun}}-studio-item">
<div class="wrapper-comp-setting">
<label class="label setting-label" for="answer-{{key}}">Answer</label>
<input class="input setting-input" name="answer-{{key}}" id="answer-{{key}}" value="{{text}}" type="text" /><br />
<label class="label setting-label" for="img-answer-{{key}}">Image URL</label>
<input class="input setting-input" name="img-answer-{{key}}" id="img-answer-{{key}}" value="{{img}}" type="text" />
<label class="label setting-label poll-setting-label" for="{{noun}}-{{key}}">{{noun}}</label>
<input class="input setting-input" name="{{noun}}-{{key}}" id="{{noun}}-{{key}}" value="{{text}}" type="text" /><br />
{{#if image}}
<label class="label setting-label" for="img-{{noun}}-{{key}}">Image URL</label>
<input class="input setting-input" name="img-{{noun}}-{{key}}" id="img-{{noun}}-{{key}}" value="{{img}}" type="text" />
{{/if}}
<div class="poll-move">
<div class="poll-move-up">&#9650;</div>
<div class="poll-move-down">&#9660;</div>
</div>
</div>
<span class="tip setting-help">
Enter an answer for the user to select. An answer must have an image URL or text, and can have both.
(Text truncated at 250 characters, Image URL at 1000)
{{#if image}}This must have an image URL or text, and can have both.{{/if}}
</span>
<a href="#" class="button action-button poll-delete-answer" onclick="return false;">Delete</a>
</li>
{{/each}}
</script>
\ No newline at end of file
</script>
<script id="survey-results-template" type="text/html">
<h3 class="poll-header">{{display_name}}</h3>
<table class="survey-table">
<thead>
<tr>
<th></th>
{{#each answers}}
<th class="survey-answer">{{{this}}}</th>
{{/each}}
</tr>
</thead>
{{#each tally}}
<tr class="survey-row">
<td class="survey-question">
{{#if img}}
<div class="poll-image-td">
<img src="{{img}}" />
</div>
{{/if}}
{{{label}}}
</td>
{{#each answers}}
<td class="survey-percentage survey-option{{#if choice}} survey-choice{{/if}}{{#if top}} poll-top-choice{{/if}}">{{percent}}%</td>
{{/each}}
</tr>
{{/each}}
</table>
<input class="input-main" type="button" name="poll-submit" value="Submit" disabled>
<div class="poll-footnote">Results gathered from {{total}} respondent{{#if plural}}s{{/if}}.</div>
{{#if feedback}}
<hr />
<h2 class="poll-header">Feedback</h2>
<div class="poll-feedback">
{{{feedback}}}
</div>
{{/if}}
</script>
......@@ -2,6 +2,7 @@
<div class="poll-block">
{# If no form is present, the Javascript will load the results instead. #}
{% if not choice %}
<h2 class="poll-header">{{display_name}}</h2>
<form>
<div class="poll-question-container">
{{question|safe}}
......@@ -19,11 +20,11 @@
</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>
<input class="input-main" type="button" name="poll-submit" value="Submit" disabled />
</form>
{% endif %}
</div>
\ No newline at end of file
</div>
......@@ -3,13 +3,21 @@
<form id="poll-form">
<ul class="list-input settings-list" id="poll-line-items">
<li class="field comp-setting-entry is-set">
<h2><label for="poll-question-editor">Question/Prompt</label></h2>
<a href="//daringfireball.net/projects/markdown/syntax" target="_blank">Markdown Syntax</a> is supported.
<div id="poll-question-editor-container">
<textarea class="input setting-input" name="question" id="poll-question-editor">{{question}}</textarea>
</div>
<span class="tip setting-help">Enter the prompt for the user. (Truncated after 5000 characters)</span>
<div class="wrapper-comp-setting">
<label class="label setting-label poll-setting-label" for="display_name">Display Name</label>
<input class="input setting-input" name="display_name" id="poll-display-name" value="{{ display_name }}" type="text" />
</div>
</li>
{% if not multiquestion %}
<li class="field comp-setting-entry is-set">
<h2><label for="poll-question-editor">Question/Prompt</label></h2>
<a href="//daringfireball.net/projects/markdown/syntax" target="_blank">Markdown Syntax</a> is supported.
<div id="poll-question-editor-container">
<textarea class="input setting-input" name="question" id="poll-question-editor">{{question}}</textarea>
</div>
<span class="tip setting-help">Enter the prompt for the user.</span>
</li>
{% endif %}
<li class="field comp-setting-entry is-set">
<h2><label for="poll-feedback-editor">Feedback</label></h2>
<a href="//daringfireball.net/projects/markdown/syntax" target="_blank">Markdown Syntax</a> is supported.
......@@ -18,7 +26,7 @@
</div>
<span class="tip setting-help">
This text will be displayed for the user as some extra feedback after they have
submitted their response to the poll. (Truncated after 5000 characters)
submitted their response to the poll.
</span>
</li>
<li class="field comp-setting-entry is-set">
......@@ -29,13 +37,29 @@
If you delete an answer, any votes for that answer will also be deleted. Students whose choices are deleted
may vote again, but will not lose course progress.
</p>
{% if multiquestion %}
<p>
Questions must be similarly cared for. If a question's text is changed, any votes for that question will remain.
If a question is deleted, any student who previously took the survey will be permitted to retake it, but will not
lose course progress.
</p>
{% endif %}
</li>
<li id="poll-answer-marker"></li>
<li id="poll-answer-end-marker"></li>
<li id="poll-question-marker"></li>
<li id="poll-question-end-marker"></li>
</ul>
<div class="xblock-actions">
<ul>
<li class="action-item" id="poll-add-answer">
<a href="#" class="button action-button" class="poll-add-answer-link" onclick="return false;">Add Answer</a>
<a href="#" class="button action-button" class="poll-add-item-link" onclick="return false;">Add Answer</a>
</li>
{% if multiquestion %}
<li class="action-item" id="poll-add-question">
<a href="#" class="button action-button" class="poll-add-item-link" onclick="return false;">Add Question</a>
</li>
{% endif %}
<li class="action-item">
<input id="poll-submit-options" type="submit" class="button action-primary save-button" value="Save" onclick="return false;" />
</li>
......@@ -45,4 +69,4 @@
</ul>
</div>
</form>
</div>
\ No newline at end of file
</div>
{{ js_template|safe }}
<div class="poll-block">
{# If no form is present, the Javascript will load the results instead. #}
{% if not choices %}
<h3 class="poll-header">{{display_name}}</h3>
<form>
<table class="survey-table">
<thead>
<tr>
<th></th>
{% for answer, label in answers %}
<th class="survey-answer">{{label}}</th>
{% endfor %}
</tr>
</thead>
{% for key, question in questions %}
<tr class="survey-row">
<td class="survey-question">
{% if question.img %}
<div class="poll-image-td">
<img src="{{question.img}}" />
</div>
{% endif %}
{{question.label|safe}}
</td>
{% for answer, answer_details in answers %}
<td class="survey-option">
<input type="radio" name="{{key}}" value="{{answer}}" />
</td>
{% endfor %}
</tr>
{% endfor %}
</table>
<input class="input-main" type="button" name="poll-submit" value="Submit" disabled />
</form>
{% endif %}
</div>
/* Javascript for PollBlock. */
function PollBlock(runtime, element) {
var voteUrl = runtime.handlerUrl(element, 'vote');
var tallyURL = runtime.handlerUrl(element, 'get_results');
function PollUtil (runtime, element, pollType) {
var self = this;
var submit = $('input[type=button]', element);
var resultsTemplate = Handlebars.compile($("#results", element).html());
function getResults(data) {
if (! data['success']) {
alert(data['errors'].join('\n'));
this.init = function() {
// Initialization function used for both Poll Types
this.voteUrl = runtime.handlerUrl(element, 'vote');
this.tallyURL = runtime.handlerUrl(element, 'get_results');
this.submit = $('input[type=button]', element);
this.answers = $('input[type=radio]', element);
this.resultsTemplate = Handlebars.compile($("#" + pollType + "-results-template", element).html());
// If the submit button doesn't exist, the user has already
// selected a choice. Render results instead of initializing machinery.
if (! self.submit.length) {
self.getResults({'success': true});
return false;
}
$.ajax({
// Semantically, this would be better as GET, but we can use helper
// functions with POST.
type: "POST",
url: tallyURL,
data: JSON.stringify({}),
success: function (data) {
$('div.poll-block', element).html(resultsTemplate(data));
}
})
}
return true;
};
this.pollInit = function(){
// Initialization function for PollBlocks.
function enableSubmit() {
submit.removeAttr("disabled");
answers.unbind("change.EnableSubmit");
}
// If the submit button doesn't exist, the user has already
// selected a choice.
if (submit.length) {
var radio = $('input[name=choice]:checked', element);
submit.click(function (event) {
self.submit.click(function () {
// Refresh.
radio = $(radio.selector, element);
var choice = radio.val();
$.ajax({
type: "POST",
url: voteUrl,
url: self.voteUrl,
data: JSON.stringify({"choice": choice}),
success: getResults
success: self.getResults
});
});
// If the user has refreshed the page, they may still have an answer
// selected and the submit button should be enabled.
var answers = $('input[type=radio]', element);
if (! radio.val()) {
answers.bind("change.EnableSubmit", enableSubmit);
answers.bind("change.enableSubmit", self.enableSubmit);
} else {
enableSubmit();
self.enableSubmit();
}
} else {
getResults({'success': true});
};
this.surveyInit = function () {
// Initialization function for Survey Blocks
self.answers.bind("change.enableSubmit", self.verifyAll);
self.submit.click(function () {
$.ajax({
type: "POST",
url: self.voteUrl,
data: JSON.stringify(self.surveyChoices()),
success: self.getResults
})
});
// If the user has refreshed the page, they may still have an answer
// selected and the submit button should be enabled.
self.verifyAll();
};
this.surveyChoices = function () {
// Grabs all selections for survey answers, and returns a mapping for them.
var choices = {};
self.answers.each(function(index, el) {
el = $(el);
choices[el.prop('name')] = $(self.checkedElement(el)).val();
});
return choices;
};
this.checkedElement = function (el) {
// Given the DOM element of a radio, get the selector for the checked element
// with the same name.
return "input[name='" + el.prop('name') + "']:checked"
};
this.verifyAll = function () {
// Verify that all questions have an answer selected.
var doEnable = true;
self.answers.each(function (index, el) {
if (! $(self.checkedElement($(el)), element).length) {
doEnable = false;
return false
}
});
if (doEnable){
self.enableSubmit();
}
};
this.getResults = function (data) {
// Fetch the results from the server and render them.
if (!data['success']) {
alert(data['errors'].join('\n'));
}
$.ajax({
// Semantically, this would be better as GET, but we can use helper
// functions with POST.
type: "POST",
url: self.tallyURL,
data: JSON.stringify({}),
success: function (data) {
console.log(self);
$('div.poll-block', element).html(self.resultsTemplate(data));
}
})
};
this.enableSubmit = function () {
// Enable the submit button.
self.submit.removeAttr("disabled");
self.answers.unbind("change.enableSubmit");
};
var run_init = this.init();
if (run_init) {
var init_map = {'poll': self.pollInit, 'survey': self.surveyInit};
init_map[pollType]()
}
}
\ No newline at end of file
}
function PollBlock(runtime, element) {
new PollUtil(runtime, element, 'poll');
}
function SurveyBlock(runtime, element) {
new PollUtil(runtime, element, 'survey');
}
-e .
bleach
markdown
-e git+https://github.com/edx-solutions/xblock-utils.git#egg=xblock-utils
\ No newline at end of file
-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
......@@ -22,7 +22,7 @@ def package_data(pkg, roots):
setup(
name='xblock-poll',
version='0.1',
version='0.2',
description='An XBlock for polling users.',
packages=[
'poll',
......@@ -30,14 +30,14 @@ setup(
install_requires=[
'XBlock',
'markdown',
'bleach',
'xblock-utils',
],
dependency_links=['http://github.com/edx-solutions/xblock-utils/tarball/master#egg=xblock-utils'],
entry_points={
'xblock.v1': [
'poll = poll:PollBlock',
'survey = poll:SurveyBlock',
]
},
package_data=package_data("poll", ["static", "public"]),
)
\ No newline at end of file
)
"""
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
],
]
......@@ -7,9 +7,9 @@ from selenium.common.exceptions import NoSuchElementException
from .base_test import PollBaseTest
class TestDefaults(PollBaseTest):
class TestDefault(PollBaseTest):
"""
Tests to run against the default poll.
Tests to run against the default XBlock configurations.
"""
def test_default_poll(self):
"""
......@@ -17,7 +17,7 @@ class TestDefaults(PollBaseTest):
the tally displays afterward. Verifies that the feedback section does
not load since it is not enabled by default.
"""
self.go_to_page('Defaults')
self.go_to_page('Poll Defaults')
button = self.browser.find_element_by_css_selector('input[type=radio]')
button.click()
submit = self.get_submit()
......@@ -29,4 +29,30 @@ class TestDefaults(PollBaseTest):
self.assertEqual(self.browser.find_element_by_css_selector('.poll-percent-display').text, '100%')
# No feedback section.
self.assertRaises(NoSuchElementException, self.browser.find_element_by_css_selector, '.poll-feedback')
\ No newline at end of file
self.assertRaises(NoSuchElementException, self.browser.find_element_by_css_selector, '.poll-feedback')
def test_default_survey(self):
"""
Verifies that a default survey loads, that it can be voted on, and
that the tally displays afterward. Verifies that the feedback section
does not load since it is not enabled by default.
"""
self.go_to_page('Survey Defaults')
names = ['enjoy', 'recommend', 'learn']
# Select the first answer for each.
for name in names:
self.browser.find_element_by_css_selector('input[name="%s"]' % name).click()
submit = self.get_submit()
submit.click()
self.wait_until_exists('.survey-percentage')
# Should now be on the results page.
for element in self.browser.find_elements_by_css_selector('table > tr'):
# First element is question, second is first answer result.
self.assertEqual(element.find_elements_by_css_selector('td')[1].text, '100%')
# No feedback section.
self.assertRaises(NoSuchElementException, self.browser.find_element_by_css_selector, '.poll-feedback')
......@@ -50,15 +50,13 @@ class TestPollFunctions(PollBaseTest):
self.get_submit().click()
# Not a good way to wait here, since all the elements we care about
# tracking don't exist yet.
time.sleep(1)
self.wait_until_exists('.poll-feedback')
self.assertTrue(self.browser.find_element_by_css_selector('.poll-feedback').text,
"Thank you\nfor being a valued student.")
self.assertEqual(self.browser.find_element_by_css_selector('.poll-footnote').text,
'Results gathered from 100 respondent(s).')
'Results gathered from 100 respondents.')
self.assertFalse(self.browser.find_element_by_css_selector('input[name=poll-submit]').is_enabled())
......@@ -75,8 +73,134 @@ class TestPollFunctions(PollBaseTest):
self.get_submit().click()
# Button will be reaplaced with a new disabled copy, not just disabled.
# Button will be replaced with a new disabled copy, not just disabled.
self.wait_until_exists('input[name=poll-submit]:disabled')
self.go_to_page('Poll Functions')
self.assertFalse(self.get_submit().is_enabled())
\ No newline at end of file
self.assertFalse(self.get_submit().is_enabled())
class TestSurveyFunctions(PollBaseTest):
@staticmethod
def chunk_list(chunkable, max_size):
"""
Subdivides a list into several smaller lists.
"""
result = []
in_list = False
for index, item in enumerate(chunkable, start=1):
if not in_list:
result.append([])
in_list = True
result[-1].append(item)
if not index % max_size:
in_list = False
return result
def test_first_load(self):
"""
Checks the first load of the survey.
Verifies that the poll loads with the expected questions,
that the answers are shown in the expected order, that feedback is
not showing, and that the submit button is disabled.
"""
self.go_to_page('Survey Functions')
self.assertEqual(
[element.text for element in self.browser.find_elements_by_css_selector('.survey-question')],
[
"I feel like this test will pass.", "I like testing software", "Testing is not necessary",
"I would fake a test result to get software deployed."
]
)
self.assertEqual(
[element.text for element in self.browser.find_elements_by_css_selector('.survey-answer')],
[
"Strongly Agree", "Agree", "Neutral", "Disagree", "Strongly Disagree"
]
)
self.assertRaises(NoSuchElementException, self.browser.find_element_by_css_selector, '.poll-feedback')
submit_button = self.get_submit()
self.assertFalse(submit_button.is_enabled())
def fill_survey(self, assert_submit=False):
"""
Fills out the survey. Optionally checks if the submit button is
in the right state along the way.
"""
elements = self.browser.find_elements_by_css_selector('.survey-option input[type=radio]')
# Answers should be in sets of five.
questions = self.chunk_list(elements, 5)
# Disabled to start...
submit_button = self.get_submit()
if assert_submit:
self.assertFalse(submit_button.is_enabled())
# Strongly Agree: I feel like this test will pass.
questions[0][0].click()
if assert_submit:
self.assertFalse(submit_button.is_enabled())
# Disagree: Testing is not necessary
questions[2][3].click()
if assert_submit:
self.assertFalse(submit_button.is_enabled())
# Agree: I like testing software
questions[1][1].click()
if assert_submit:
self.assertFalse(submit_button.is_enabled())
# Strongly Disagree: I would fake a test result to get software deployed.
questions[3][4].click()
if assert_submit:
# Submit button should now be enabled!
self.assertTrue(submit_button.is_enabled())
def test_submit_enabled(self):
"""
Verify that the submit button is enabled only when every question
has an answer.
"""
self.go_to_page('Survey Functions')
self.fill_survey(assert_submit=True)
def test_survey_submission(self):
"""
Verify that the user can submit his or her vote and that the vote counts.
Also check that feedback is displayed afterward.
"""
self.go_to_page('Survey Functions')
self.fill_survey()
self.get_submit().click()
self.wait_until_exists('.poll-feedback')
self.assertEqual(self.browser.find_element_by_css_selector('.poll-footnote').text,
'Results gathered from 21 respondents.')
self.assertTrue(self.browser.find_element_by_css_selector('.poll-feedback').text,
"Thank you\nfor running the tests.")
def test_submit_not_enabled_on_revisit(self):
"""
Verify that revisiting the page post-vote does not re-enable the submit button.
"""
self.go_to_page('Survey Functions')
self.fill_survey()
self.get_submit().click()
# Button will be replaced with a new disabled copy, not just disabled.
self.wait_until_exists('input[name=poll-submit]:disabled')
self.go_to_page('Poll Functions')
self.assertFalse(self.get_submit().is_enabled())
......@@ -16,7 +16,7 @@ class TestLayout(PollBaseTest):
"""
Verify img tags are created for answers when they're all set.
"""
self.go_to_page('All Pictures')
self.go_to_page('Poll All Pictures')
pics = self.browser.find_elements_by_css_selector('.poll-image')
self.assertEqual(len(pics), 4)
......@@ -32,7 +32,7 @@ class TestLayout(PollBaseTest):
"""
Verify layout is sane when only one answer has an image.
"""
self.go_to_page('One Picture')
self.go_to_page('Poll One Picture')
pics = self.browser.find_elements_by_css_selector('.poll-image')
# On the polling page, there should only be one pics div.
self.assertEqual(len(pics), 1)
......@@ -43,4 +43,4 @@ class TestLayout(PollBaseTest):
self.wait_until_exists('.poll-image.result-image')
# ...But on the results page, we need four, for table layout.
self.assertEqual(len(self.browser.find_elements_by_css_selector('.poll-image')), 4)
\ No newline at end of file
self.assertEqual(len(self.browser.find_elements_by_css_selector('.poll-image')), 4)
"""
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("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("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.""")
\ No newline at end of file
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}"
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
<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
<vertical_demo>
<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>
<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_**."
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="defaults"/>
</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>
<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>
from unittest import TestCase
from poll.utils import process_markdown
from markdown import markdown
class ProcessMarkdownTest(TestCase):
......@@ -37,6 +37,6 @@ This is a paragraph of text, despite being just one sentence.
<blockquote>
<p>This is going to be a blockquote.</p>
</blockquote>
&lt;script type="text/javascript"&gt;breakstuff();&lt;/script&gt;"""
<script type="text/javascript">breakstuff();</script>"""
)
self.assertEqual(end_string, process_markdown(start_string))
\ No newline at end of file
self.assertEqual(end_string, markdown(start_string))
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