Commit 59758e5f by Xavier Antoviaque

Merge pull request #1 from open-craft/first-version

(WIP) First Version.
parents fe27bd5f aee05410
# Created by https://www.gitignore.io
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
nosetests.xml
coverage.xml
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm
/*.iml
## Directory-based project format:
.idea/
# if you remove the above rule, at least ignore the following:
# User-specific stuff:
# .idea/workspace.xml
# .idea/tasks.xml
# .idea/dictionaries
# Sensitive or high-churn files:
# .idea/dataSources.ids
# .idea/dataSources.xml
# .idea/sqlDataSources.xml
# .idea/dynamic.xml
# .idea/uiDesigner.xml
# Gradle:
# .idea/gradle.xml
# .idea/libraries
# Mongo Explorer plugin:
# .idea/mongoSettings.xml
## File-based project format:
*.ipr
*.iws
## Plugin-specific files:
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
### vim ###
[._]*.s[a-w][a-z]
[._]s[a-w][a-z]
*.un~
Session.vim
.netrwhist
*~
### Linux ###
*~
# KDE directory preferences
.directory
from .poll import PollBlock
\ No newline at end of file
"""TO-DO: Write a description of what this XBlock is."""
from collections import OrderedDict
from django.template import Template, Context
import pkg_resources
from xblock.core import XBlock
from xblock.fields import Scope, String, Dict, List
from xblock.fragment import Fragment
from xblockutils.resources import ResourceLoader
from .utils import process_markdown
# When changing these constants, check the templates as well for places
# where the user is informed about them.
MAX_PARAGRAPH_LEN = 5000
MAX_URL_LEN = 1000
MAX_ANSWER_LEN = 250
# These two don't have mentions in the templates, but will cause error
# messages.
MAX_ANSWERS = 25
MAX_KEY_LEN = 100
class PollBlock(XBlock):
"""
Poll XBlock. Allows a teacher to poll users, and presents the results so
far of the poll to the user when finished.
"""
question = String(default='What is your favorite color?')
# This will be converted into an OrderedDict.
# Key, (Label, Image path)
answers = List(
default=(('R', {'label': 'Red', 'img': None}), ('B', {'label': 'Blue', 'img': None}),
('G', {'label': 'Green', 'img': None}), ('O', {'label': 'Other', 'img': None})),
scope=Scope.settings, help="The question on this poll."
)
feedback = String(default='', help="Text to display after the user votes.")
tally = Dict(default={'R': 0, 'B': 0, 'G': 0, 'O': 0},
scope=Scope.user_state_summary,
help="Total tally of answers from students.")
choice = String(scope=Scope.user_state, help="The student's answer")
loader = ResourceLoader(__name__)
def resource_string(self, path):
"""Handy helper for getting resources from our kit."""
data = pkg_resources.resource_string(__name__, path)
return data.decode("utf8")
@XBlock.json_handler
def get_results(self, data, suffix=''):
detail, total = self.tally_detail()
return {
'question': process_markdown(self.question), 'tally': detail,
'total': total, 'feedback': process_markdown(self.feedback),
}
def clean_tally(self):
"""
Cleans the tally. Scoping prevents us from modifying this in the studio
and in the LMS the way we want to without undesirable side effects. So
we just clean it up on first access within the LMS, in case the studio
has made changes to the answers.
"""
answers = OrderedDict(self.answers)
for key in answers.keys():
if key not in self.tally:
self.tally[key] = 0
for key in self.tally.keys():
if key not in answers:
del self.tally[key]
def any_image(self):
"""
Find out if any answer has an image, since it affects layout.
"""
return any(value['img'] for value in dict(self.answers).values())
def tally_detail(self):
"""
Tally all results.
"""
tally = []
answers = OrderedDict(self.answers)
choice = self.get_choice()
total = 0
self.clean_tally()
source_tally = self.tally
any_img = self.any_image()
for key, value in answers.items():
count = int(source_tally[key])
tally.append({
'count': count,
'answer': value['label'],
'img': value['img'],
'key': key,
'top': False,
'choice': False,
'last': False,
'any_img': any_img,
})
total += count
for answer in tally:
try:
answer['percent'] = int(answer['count'] / float(total)) * 100
if answer['key'] == choice:
answer['choice'] = True
except ZeroDivisionError:
answer['percent'] = 0
tally.sort(key=lambda x: x['count'], reverse=True)
# This should always be true, but on the off chance there are
# no answers...
if tally:
# Mark the top item to make things easier for Handlebars.
tally[0]['top'] = True
tally[-1]['last'] = True
return tally, total
def get_choice(self):
"""
It's possible for the choice to have been removed since
the student answered the poll. We don't want to take away
the user's progress, but they should be able to vote again.
"""
if self.choice and self.choice in OrderedDict(self.answers):
return self.choice
else:
return None
def create_fragment(self, context, template, css, js, js_init):
html = Template(
self.resource_string(template)).render(Context(context))
frag = Fragment(html)
frag.add_javascript_url(
self.runtime.local_resource_url(
self, 'public/js/vendor/handlebars.js'))
frag.add_css(self.resource_string(css))
frag.add_javascript(self.resource_string(js))
frag.initialize_js(js_init)
return frag
def student_view(self, context=None):
"""
The primary view of the PollBlock, shown to students
when viewing courses.
"""
if not context:
context = {}
js_template = self.resource_string(
'/public/handlebars/results.handlebars')
choice = self.get_choice()
context.update({
'choice': choice,
# Offset so choices will always be True.
'answers': self.answers,
'question': process_markdown(self.question),
# Mustache is treating an empty string as true.
'feedback': process_markdown(self.feedback) or False,
'js_template': js_template,
'any_img': self.any_image(),
# The SDK doesn't set url_name.
'url_name': getattr(self, 'url_name', ''),
})
if self.choice:
detail, total = self.tally_detail()
context.update({'tally': detail, 'total': total})
return self.create_fragment(
context, "public/html/poll.html", "public/css/poll.css",
"public/js/poll.js", "PollBlock")
@XBlock.json_handler
def load_answers(self, data, suffix=''):
return {
'answers': [
{
'key': key, 'text': value['label'], 'img': value['img']
}
for key, value in self.answers
]
}
def studio_view(self, context=None):
if not context:
context = {}
js_template = self.resource_string('/public/handlebars/studio.handlebars')
context.update({
'question': self.question,
'feedback': self.feedback,
'js_template': js_template
})
return self.create_fragment(
context, "public/html/poll_edit.html",
"/public/css/poll_edit.css", "public/js/poll_edit.js", "PollEditBlock")
@XBlock.json_handler
def studio_submit(self, data, suffix=''):
# I wonder if there's something for live validation feedback already.
result = {'success': True, 'errors': []}
question = data.get('question', '').strip()[:MAX_PARAGRAPH_LEN]
feedback = data.get('feedback', '').strip()[:MAX_PARAGRAPH_LEN]
if not question:
result['errors'].append("You must specify a question.")
result['success'] = False
answers = []
if 'answers' not in data or not isinstance(data['answers'], list):
source_answers = []
result['success'] = False
result['errors'].append(
"'answers' is not present, or not a JSON array.")
else:
source_answers = data['answers']
# Set a reasonable limit to the number of answers in a poll.
if len(source_answers) > MAX_ANSWERS:
result['success'] = False
result['errors'].append("")
# Make sure all components are present and clean them.
for answer in source_answers:
if not isinstance(answer, dict):
result['success'] = False
result['errors'].append(
"Answer {0} not a javascript object!".format(answer))
continue
key = answer.get('key', '').strip()
if not key:
result['success'] = False
result['errors'].append(
"Answer {0} contains no key.".format(answer))
if len(key) > MAX_KEY_LEN:
result['success'] = False
result['errors'].append("Key '{0}' too long.".format(key))
img = answer.get('img', '').strip()[:MAX_URL_LEN]
label = answer.get('label', '').strip()[:MAX_ANSWER_LEN]
if not (img or label):
result['success'] = False
result['errors'].append(
"Answer {0} has no text or img. One is needed.")
answers.append((key, {'label': label, 'img': img}))
if not len(answers) > 1:
result['errors'].append(
"You must include at least two answers.")
result['success'] = False
if not result['success']:
return result
self.answers = answers
self.question = question
self.feedback = feedback
# Tally will not be updated until the next attempt to use it, per
# scoping limitations.
return result
@XBlock.json_handler
def vote(self, data, suffix=''):
"""
An example handler, which increments the data.
"""
result = {'success': False, 'errors': []}
if self.get_choice() is not None:
result['errors'].append('You have already voted in this poll.')
return result
try:
choice = data['choice']
except KeyError:
result['errors'].append('Answer not included with request.')
return result
# Just to show data coming in...
try:
OrderedDict(self.answers)[choice]
except KeyError:
result['errors'].append('No key "{choice}" in answers table.'.format(choice=choice))
return result
self.clean_tally()
self.choice = choice
self.tally[choice] = self.tally.get(choice, 0) + 1
# Let the LMS know the user has answered the poll.
self.runtime.publish(self, 'progress', {})
result['success'] = True
return result
@staticmethod
def workbench_scenarios():
"""
Canned scenarios for display in the workbench.
"""
return [
("Default Poll",
"""
<vertical_demo>
<poll />
</vertical_demo>
"""),
("Customized Poll",
"""
<vertical_demo>
<poll url_name="poll_functions" question="## How long have you been studying with us?"
feedback="### Thank you&#10;&#10;for being a valued student."
tally="{'long': 20, 'short': 29, 'not_saying': 15, 'longer' : 35}"
answers="[['long', 'A very long time'], ['short', 'Not very long'], ['not_saying', 'I shall not say'], ['longer', 'Longer than you']]"/>
</vertical_demo>
"""),
]
\ No newline at end of file
/* CSS for PollBlock Student View */
.poll-answer {
margin-left: 1em;
font-weight: bold;
display: inline-block;
}
.percentage-gauge {
display: inline-block;
background-color: #e5ebee;
position: relative;
z-index: 1;
}
.poll-result-input-container {
display: table-cell;
vertical-align: middle;
}
.poll-result-input-container input {
margin: 0 .5em 0 0;
}
.percentage-gauge-container {
display: table-cell;
width: 100%;
vertical-align: middle;
background-color: #fafbfc;
}
ul.poll-answers, ul.poll-answers-results {
list-style-type: none !important;
max-width: 80%;
}
li.poll-answer {
display: block;
border-bottom-width: .5em;
vertical-align: middle;
margin-top: 5px;
margin-bottom: 5px;
}
li.poll-spacer {
height: .25em;
}
ul.poll-answers-results {
display: table;
}
li.poll-result {
width: 100%;
display: table-row;
padding-bottom: .2em;
}
.poll-answer-label {
margin-left: .2em;
font-weight: bold;
display: inline-block;
margin-bottom: 5px;
margin-top: 5px;
}
.poll-image {
width: 25%;
display: inline-block;
vertical-align: middle;
}
.poll-image {
margin-left: .5em;
}
li.poll-result .poll-image {
display: table-cell;
margin-left: 0;
}
.poll-image img{
width: 100%;
}
.poll-percent-container {
display: table-cell;
text-align: left;
padding-left: .2em;
vertical-align: middle;
}
.poll-percent-display {
font-weight: bold;
}
.poll-top-choice {
color: #e37222;
}
.poll-footnote {
margin-top: 1em;
margin-bottom: 1em;
font-size: smaller;
}
\ No newline at end of file
/* CSS for PollBlock Studio Menu View */
.poll-delete-answer {
float: right;
margin-top: 1em;
}
#poll-question-editor-container, #poll-feedback-editor-container{
width: 100%;
text-align: center;
}
#poll-question-editor, #poll-feedback-editor{
width: 98%;
height: 7em;
text-align: left;
color: #4C4C4C;
margin-top: 0.5em;
margin-bottom: 0.5em;
box-shadow: 0 0 9px #555 inset;
border: 1px solid #B2B2B2;
border-radius: 3px;
padding: 10px;
}
.poll-move-up {
opacity: .5;
}
.poll-move-down {
opacity: .5;
}
.poll-move-down:hover, .poll-move-up:hover {
opacity: 1;
transition: opacity .4s;
cursor: pointer
}
.poll-move {
float: right;
}
\ No newline at end of file
<script id="results" type="text/html">
{{{question}}}
<ul class="poll-answers-results">
{{#each tally}}
<li class="poll-result">
<div class="poll-result-input-container">
<input id="answer-{{key}}" type="radio" disabled {{#if choice}}checked="True"{{/if}} />
</div>
{{#if any_img}}
<div class="poll-image result-image">
<label for="answer-{{key}}" class="poll-image-label">
{{#if img}}
<img src="{{img}}" />
{{/if}}
</label>
</div>
{{/if}}
<div class="percentage-gauge-container">
<div class="percentage-gauge" style="width:{{percent}}%;">
<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>
</div>
</li>
{{^last}}
<li class="poll-spacer">
</li>
{{/last}}
{{/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>
{{#if feedback}}
<hr />
<div class="poll-feedback">
{{{feedback}}}
</div>
{{/if}}
</script>
\ No newline at end of file
<script id="answer-form-component" type="text/html">
{{#each answers}}
<li class="field comp-setting-entry is-set">
<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" />
<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)
</span>
<a href="#" class="button action-button poll-delete-answer" onclick="return false;">Delete</a>
</li>
{{/each}}
</script>
\ No newline at end of file
{{ js_template|safe }}
<div class="poll-block">
{# If no form is present, the Javascript will load the results instead. #}
{% if not choice %}
<form>
<div class="poll-question-container">
{{question|safe}}
</div>
<ul class="poll-answers">
{% for key, value in answers %}
<li class="poll-answer">
<input type="radio" name="choice" id="{{url_name}}-answer-{{key}}" value="{{key}}" />
{% if value.img %}
<div class="poll-image">
<label for="{{url_name}}-answer-{{key}}" class="poll-image-label">
{% if value.img %}
<img src="{{value.img}}" />
{% endif %}
</label>
</div>
{% endif %}
<label class="poll-answer" for="{{url_name}}-answer-{{key}}">{{value.label}}</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
{{js_template|safe}}
<div class="wrapper-comp-settings is-active editor-with-buttons" id="settings-tab">
<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>
</li>
<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.
<div id="poll-feedback-editor-container">
<textarea class="input setting-input" name="feedback" id="poll-feedback-editor">{{feedback}}</textarea>
</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)
</span>
</li>
<li class="field comp-setting-entry is-set">
<p>
<strong>Notes:</strong>
If you change an answer's text, all students who voted for that choice will have their votes updated to
the new text. You'll want to avoid changing an answer from something like 'True' to 'False', accordingly.
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>
</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>
</li>
<li class="action-item">
<input id="poll-submit-options" type="submit" class="button action-primary save-button" value="Save" onclick="return false;" />
</li>
<li class="action-item">
<a href="#" class="button cancel-button">Cancel</a>
</li>
</ul>
</div>
</form>
</div>
\ No newline at end of file
/* Javascript for PollBlock. */
function PollBlock(runtime, element) {
var voteUrl = runtime.handlerUrl(element, 'vote');
var tallyURL = runtime.handlerUrl(element, 'get_results');
var submit = $('input[type=button]', element);
var resultsTemplate = Handlebars.compile($("#results", element).html());
function getResults(data) {
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: tallyURL,
data: JSON.stringify({}),
success: function (data) {
$('div.poll-block', element).html(resultsTemplate(data));
}
})
}
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) {
// Refresh.
radio = $(radio.selector, element);
var choice = radio.val();
$.ajax({
type: "POST",
url: voteUrl,
data: JSON.stringify({"choice": choice}),
success: 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);
} else {
enableSubmit();
}
} else {
getResults({'success': true});
}
}
\ No newline at end of file
function PollEditBlock(runtime, element) {
var loadAnswers = runtime.handlerUrl(element, 'load_answers');
var temp = $('#answer-form-component', element).html();
var answerTemplate = Handlebars.compile(temp);
var pollLineItems =$('#poll-line-items', element);
function empowerDeletes(scope) {
$('.poll-delete-answer', scope).click(function () {
$(this).parent().remove();
});
}
/*
The poll answers need to be reorderable. As the UL they are in is not
easily isolated, we need to start checking their position to make
sure they aren't ordered above the other settings, which are also
in the list.
*/
var starting_point = 3;
function empowerArrows(scope) {
$('.poll-move-up', scope).click(function () {
var tag = $(this).parents('li');
if (tag.index() <= starting_point){
return;
}
tag.prev().before(tag);
tag.fadeOut("fast", "swing").fadeIn("fast", "swing");
});
$('.poll-move-down', scope).click(function () {
var tag = $(this).parents('li');
if ((tag.index() >= (tag.parent().children().length - 1))) {
return;
}
tag.next().after(tag);
tag.fadeOut("fast", "swing").fadeIn("fast", "swing");
});
}
function displayAnswers(data) {
pollLineItems.append(answerTemplate(data));
empowerDeletes(element);
empowerArrows(element);
}
$('#poll-add-answer', element).click(function () {
// The degree of precision on date should be precise enough to avoid
// collisions in the real world.
pollLineItems.append(answerTemplate({'answers': [{'key': new Date().getTime(), 'text': ''}]}));
var new_answer = $(pollLineItems.children().last());
empowerDeletes(new_answer);
empowerArrows(new_answer);
new_answer.fadeOut(250).fadeIn(250);
});
$(element).find('.cancel-button', element).bind('click', function() {
runtime.notify('cancel', {});
});
$(element).find('.save-button', element).bind('click', function() {
var handlerUrl = runtime.handlerUrl(element, 'studio_submit');
var data = {'answers': []};
var tracker = [];
$('#poll-form input', element).each(function(i) {
var key = 'label';
if (this.name.indexOf('answer-') >= 0){
var name = this.name.replace('answer-', '');
if (this.name.indexOf('img-') == 0){
name = name.replace('img-', '');
key = 'img'
}
if (tracker.indexOf(name) == -1){
tracker.push(name);
data['answers'].push({'key': name})
}
var index = tracker.indexOf(name);
data['answers'][index][key] = this.value;
return
}
data[this.name] = this.value
});
data['title'] = $('#poll-title', element).val();
data['question'] = $('#poll-question-editor', element).val();
data['feedback'] = $('#poll-feedback-editor', element).val();
function check_return(data) {
if (data['success']) {
window.location.reload(false);
return;
}
alert(data['errors'].join('\n'));
}
$.ajax({
type: "POST",
url: handlerUrl,
data: JSON.stringify(data),
success: check_return
});
});
$(function ($) {
$.ajax({
type: "POST",
url: loadAnswers,
data: JSON.stringify({}),
success: displayAnswers
});
});
}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
import bleach
from markdown import markdown
ALLOWED_TAGS = {
'h1': [], 'h2': [], 'h3': [], 'h4': [], 'h5': [], 'h6': [],
'a': ['target', 'href', 'class'], 'strong': [], 'em': [], 'blockquote': [],
'pre': [], 'li': [], 'ul': [], 'ol': [], 'code': ['class'], 'p': [],
}
def process_markdown(raw_text):
return bleach.clean(markdown(raw_text), tags=ALLOWED_TAGS, strip_comments=False)
\ No newline at end of file
-e .
bleach
markdown
-e git+https://github.com/edx-solutions/xblock-utils.git#egg=xblock-utils
\ No newline at end of file
"""Setup for poll XBlock."""
import os
from setuptools import setup
def package_data(pkg, roots):
"""Generic function to find package_data.
All of the files under each of the `roots` will be declared as package
data for package `pkg`.
"""
data = []
for root in roots:
for dirname, _, files in os.walk(os.path.join(pkg, root)):
for fname in files:
data.append(os.path.relpath(os.path.join(dirname, fname), pkg))
return {pkg: data}
setup(
name='xblock-poll',
version='0.1',
description='An XBlock for polling users.',
packages=[
'poll',
],
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',
]
},
package_data=package_data("poll", ["static", "public"]),
)
\ No newline at end of file
from xblockutils.base_test import SeleniumBaseTest
class PollBaseTest(SeleniumBaseTest):
default_css_selector = 'div.poll-block'
module_name = __name__
def get_submit(self):
return self.browser.find_element_by_css_selector('input[name="poll-submit"]')
\ No newline at end of file
"""
Tests to verify a default poll XBlock is a functional demo.
Deeper investigation should be tested in test_poll_functions.
"""
from selenium.common.exceptions import NoSuchElementException
from .base_test import PollBaseTest
class TestDefaults(PollBaseTest):
"""
Tests to run against the default poll.
"""
def test_default_poll(self):
"""
Verifies that a default poll 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('Defaults')
button = self.browser.find_element_by_css_selector('input[type=radio]')
button.click()
submit = self.get_submit()
submit.click()
self.wait_until_exists('.poll-percent-display')
# Should now be on the results page.
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
"""
Test to make sure the layout for results is sane when taking images into
account.
"""
from tests.integration.base_test import PollBaseTest
import time
class TestLayout(PollBaseTest):
"""
Do tests to verify that the layout of elements makes sense depeneding on
the number of images.
"""
def test_all_images(self):
"""
Verify img tags are created for answers when they're all set.
"""
self.go_to_page('All Pictures')
pics = self.browser.find_elements_by_css_selector('.poll-image')
self.assertEqual(len(pics), 4)
# Pics should be within labels.
pics[0].find_element_by_css_selector('img').click()
self.get_submit().click()
self.wait_until_exists('.poll-image')
self.assertEqual(len(self.browser.find_elements_by_css_selector('.poll-image')), 4)
def test_one_image(self):
"""
Verify layout is sane when only one answer has an image.
"""
self.go_to_page('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)
pics[0].find_element_by_css_selector('img').click()
self.get_submit().click()
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
"""
Tests to make sure that markdown is both useful and secure.
"""
from .base_test import PollBaseTest
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.
"""
self.go_to_page("Markdown")
self.browser.find_element_by_css_selector('input[type=radio]').click()
self.get_submit().click()
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
"""
Tests a realistic, configured Poll to make sure that everything works as it
should.
"""
from selenium.common.exceptions import NoSuchElementException
import time
from .base_test import PollBaseTest
class TestPollFunctions(PollBaseTest):
def test_first_load(self):
"""
Checks first load.
Verify that the poll loads with the expected choices, that feedback is
not showing, and that the submit button is disabled.
"""
self.go_to_page('Poll Functions')
answer_elements = self.browser.find_elements_by_css_selector('label.poll-answer')
answers = [element.text for element in answer_elements]
self.assertEqual(['A very long time', 'Not very long', 'I shall not say', 'Longer than you'], answers)
self.assertRaises(NoSuchElementException, self.browser.find_element_by_css_selector, '.poll-feedback')
submit_button = self.get_submit()
self.assertFalse(submit_button.is_enabled())
def test_submit_enabled(self):
"""
Makes sure the submit button is enabled when selecting an answer.
"""
self.go_to_page('Poll Functions')
answer_elements = self.browser.find_elements_by_css_selector('label.poll-answer')
answer_elements[0].click()
# When an answer is selected, make sure submit is enabled.
self.wait_until_exists('input[name=poll-submit]:enabled')
def test_poll_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('Poll Functions')
answer_elements = self.browser.find_elements_by_css_selector('label.poll-answer')
# 'Not very long'
answer_elements[1].click()
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.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).')
self.assertFalse(self.browser.find_element_by_css_selector('input[name=poll-submit]').is_enabled())
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('Poll Functions')
answer_elements = self.browser.find_elements_by_css_selector('label.poll-answer')
# Not very long
answer_elements[1].click()
self.get_submit().click()
# Button will be reaplaced 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
<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="defaults"/>
</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': None}], ['fennec', {'label': 'Fennec Fox', 'img': '../img/fennec_fox.png'}], ['kit', {'label': 'Kit Fox', 'img': None}], ['arctic', {'label': 'Arctic fox', 'img': None}]]" />
</vertical_demo>
\ No newline at end of file
<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}]]"
feedback="### Thank you&#10;&#10;for being a valued student."/>
</vertical_demo>
\ No newline at end of file
from unittest import TestCase
from poll.utils import process_markdown
class ProcessMarkdownTest(TestCase):
def test_markdown_escapes(self):
start_string = (
"""
## This is an H2.
<h3>This is an H3</h3>
<a href="http://example.com">This link should be preserved.</a>
[This link should be converted.](http://www.example.com)
This is a paragraph of text, despite being just one sentence.
&lt; This should be a less-than symbol.
< So should this, since it's not attached to anything.
> This is going to be a blockquote.
<script type="text/javascript">breakstuff();</script>
"""
)
end_string = (
"""<h2>This is an H2.</h2>
<h3>This is an H3</h3>
<p><a href="http://example.com">This link should be preserved.</a>
<a href="http://www.example.com">This link should be converted.</a></p>
<p>This is a paragraph of text, despite being just one sentence.</p>
<p>&lt; This should be a less-than symbol.</p>
<p>&lt; So should this, since it's not attached to anything.</p>
<blockquote>
<p>This is going to be a blockquote.</p>
</blockquote>
&lt;script type="text/javascript"&gt;breakstuff();&lt;/script&gt;"""
)
self.assertEqual(end_string, process_markdown(start_string))
\ No newline at end of file
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