Commit 66006d1f by Eugeny Kolpakov

Merge pull request #3 from open-craft/edx-release

Merging edx-release back to master
parents ca0e6eb4 7b892f3b
# Created by https://www.gitignore.io
tests.integration.*.png
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
......
......@@ -6,7 +6,8 @@ before_install:
- "sh -e /etc/init.d/xvfb start"
install:
- "sh install_test_deps.sh"
- "python setup.py develop"
script: pep8 poll --max-line-length=120 && pylint poll && python run_tests.py --with-coverage --cover-package=poll
notifications:
email: false
addons:
firefox: "36.0"
......@@ -22,6 +22,8 @@ and images. Formatting for images is handled by the XBlock's formatters to keep
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.
These blocks currently do not support grading.
## Installation and configuration
This XBlock relies on [Xblock-utils](https://github.com/edx-solutions/xblock-utils), which should be installed first.
......@@ -59,7 +61,7 @@ is presented to them.
![Poll example results](doc_img/poll_result.png)
The top choice's percentage is shown in *orange* while the user's selection is marked by a selected (but disabled)
The top choice's percentage is highlighted while the user's selection is marked by a selected (but disabled)
radio button on the side.
### Poll variations
......@@ -73,13 +75,14 @@ of the answers:
![Image-only poll](doc_img/img_poll.png)
This poll also contains a feedback section, which is enhanced with Markdown:
Please note that using only images is not accessible as Poll XBlock does not provide means for specifying alternate
text for images. Instead use images *and* texts:
![Image-only poll results](doc_img/img_poll_result.png)
![Image and Label label poll](doc_img/img_and_label_poll.png)
Polls may also have images and with text.
This poll also contains a feedback section, which is enhanced with Markdown:
![Image and Label label poll](doc_img/img_and_label_poll.png)
![Image-only poll results](doc_img/img_poll_result.png)
Or they may have a mix of both.
......@@ -183,8 +186,9 @@ for 'Private Results':
![Private Results](doc_img/private_results.png)
**Notes on Private Results**: Users will be able to change their vote on polls and surveys with this option enabled.
An analytics event will not be fired upon the student viewing the results, as the results are never visible. A user
will see a thank you message and any feedback provided upon submission:
An analytics event will not be fired upon the student viewing the results, as the results are never visible. A user
will see a thank you message, and optionally, any instructor-provided Feedback in an additional "Feedback" section,
when they click submit:
![Private Results Submission](doc_img/private_results_submission.png)
......@@ -211,10 +215,6 @@ 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.
......
# Installs xblock-sdk and dependencies needed to run the tests suite.
# Run this script inside a fresh virtual environment.
pip install -e git://github.com/edx/xblock-sdk.git#egg=xblock-sdk
pip install -r $VIRTUAL_ENV/src/xblock-sdk/requirements.txt
cd $VIRTUAL_ENV/src/xblock-sdk/ && { pip install -r requirements.txt; cd -; }
pip install -r $VIRTUAL_ENV/src/xblock-sdk/test-requirements.txt
python setup.py develop
pip install -r requirements.txt
......@@ -22,32 +22,46 @@
#
from collections import OrderedDict
import functools
import json
from django.template import Template, Context
from markdown import markdown
import pkg_resources
from webob import Response
from xblock.core import XBlock
from xblock.fields import Scope, String, Dict, List, Boolean, Integer
from xblock.fragment import Fragment
from xblockutils.publish_event import PublishEventMixin
from xblockutils.resources import ResourceLoader
from xblockutils.settings import XBlockWithSettingsMixin, ThemableXBlockMixin
HAS_EDX_ACCESS = False
try:
# pylint: disable=import-error
from django.conf import settings
from courseware.access import has_access
from api_manager.models import GroupProfile
HAS_EDX_ACCESS = True
HAS_GROUP_PROFILE = True
except ImportError:
HAS_GROUP_PROFILE = False
try:
# pylint: disable=import-error
from static_replace import replace_static_urls
HAS_STATIC_REPLACE = True
except ImportError:
pass
HAS_STATIC_REPLACE = False
class ResourceMixin(object):
class ResourceMixin(XBlockWithSettingsMixin, ThemableXBlockMixin):
loader = ResourceLoader(__name__)
block_settings_key = 'poll'
default_theme_config = {
'package': 'poll',
'locations': ["public/css/themes/lms.css"]
}
@staticmethod
def resource_string(path):
"""Handy helper for getting resources from our kit."""
......@@ -64,9 +78,11 @@ class ResourceMixin(object):
frag.add_css(self.resource_string(css))
frag.add_javascript(self.resource_string(js))
frag.initialize_js(js_init)
self.include_theme_files(frag)
return frag
@XBlock.wants('settings')
class PollBase(XBlock, ResourceMixin, PublishEventMixin):
"""
Base class for Poll-like XBlocks.
......@@ -82,11 +98,6 @@ class PollBase(XBlock, ResourceMixin, PublishEventMixin):
def send_vote_event(self, choice_data):
# Let the LMS know the user has answered the poll.
self.runtime.publish(self, 'progress', {})
self.runtime.publish(self, 'grade', {
'value': 1,
'max_value': 1,
}
)
# The SDK doesn't set url_name.
event_dict = {'url_name': getattr(self, 'url_name', '')}
event_dict.update(choice_data)
......@@ -100,18 +111,27 @@ class PollBase(XBlock, ResourceMixin, PublishEventMixin):
"""
Find out if any answer has an image, since it affects layout.
"""
return any(value['img'] for value in dict(field).values())
return any(value['img'] for key, value in field)
@staticmethod
def markdown_items(items):
"""
Convert all items' labels into markdown.
"""
return [[key, {'label': markdown(value['label']), 'img': value['img']}]
return [(key, {'label': markdown(value['label']), 'img': value['img'], 'img_alt': value.get('img_alt')})
for key, value in items]
@staticmethod
def gather_items(data, result, noun, field, image=True):
def img_alt_mandatory(self):
"""
Determine whether alt attributes for images are configured to be mandatory. Defaults to True.
"""
settings_service = self.runtime.service(self, "settings")
if not settings_service:
return True
xblock_settings = settings_service.get_settings_bucket(self)
return xblock_settings.get('IMG_ALT_MANDATORY', True)
def gather_items(self, data, result, noun, field, image=True):
"""
Gathers a set of label-img pairs from a data dict and puts them in order.
"""
......@@ -137,6 +157,7 @@ class PollBase(XBlock, ResourceMixin, PublishEventMixin):
result['errors'].append(
"{0} {1} contains no key.".format(noun, item))
image_link = item.get('img', '').strip()
image_alt = item.get('img_alt', '').strip()
label = item.get('label', '').strip()
if not label:
if image and not image_link:
......@@ -153,9 +174,13 @@ class PollBase(XBlock, ResourceMixin, PublishEventMixin):
"All {1}s must have labels. Please check the form. "
"Check the form and explicitly delete {1}s "
"if not needed.".format(noun, noun.lower()))
if image_link and not image_alt and self.img_alt_mandatory():
result['success'] = False
result['errors'].append(
"All images must have an alternative text describing the image in a way that "
"would allow someone to answer the poll if the image did not load.")
if image:
# Labels might have prefixed space for markdown, though it's unlikely.
items.append((key, {'label': label, 'img': image_link.strip()}))
items.append((key, {'label': label, 'img': image_link, 'img_alt': image_alt}))
else:
items.append([key, label])
......@@ -170,31 +195,31 @@ class PollBase(XBlock, ResourceMixin, PublishEventMixin):
"""
Checks to see if the user is permitted to vote. This may not be the case if they used up their max_submissions.
"""
if self.max_submissions == 0:
return True
if self.max_submissions > self.submissions_count:
return True
return False
return self.max_submissions == 0 or self.submissions_count < self.max_submissions
def can_view_private_results(self):
"""
Checks to see if the user has permissions to view private results.
This only works inside the LMS.
"""
if HAS_EDX_ACCESS and hasattr(self.runtime, 'user') and hasattr(self.runtime, 'course_id'):
# Course staff users have permission to view results.
if has_access(self.runtime.user, 'staff', self, self.runtime.course_id):
return True
else:
# Check if user is member of a group that is explicitly granted
# permission to view the results through django configuration.
group_names = getattr(settings, 'XBLOCK_POLL_EXTRA_VIEW_GROUPS', [])
if group_names:
group_ids = self.runtime.user.groups.values_list('id', flat=True)
return GroupProfile.objects.filter(group_id__in=group_ids, name__in=group_names).exists()
else:
if not hasattr(self.runtime, 'user_is_staff'):
return False
# Course staff users have permission to view results.
if self.runtime.user_is_staff:
return True
# Check if user is member of a group that is explicitly granted
# permission to view the results through django configuration.
if not HAS_GROUP_PROFILE:
return False
group_names = getattr(settings, 'XBLOCK_POLL_EXTRA_VIEW_GROUPS', [])
if not group_names:
return False
user = self.runtime.get_real_user(self.runtime.anonymous_student_id)
group_ids = user.groups.values_list('id', flat=True)
return GroupProfile.objects.filter(group_id__in=group_ids, name__in=group_names).exists()
@staticmethod
def get_max_submissions(data, result, private_results):
"""
......@@ -215,6 +240,30 @@ class PollBase(XBlock, ResourceMixin, PublishEventMixin):
result['errors'].append("Private results may not be False when Maximum Submissions is not 1.")
return max_submissions
@classmethod
def static_replace_json_handler(cls, func):
"""A JSON handler that replace all static pseudo-URLs by the actual paths.
The object returned by func is JSON-serialised, and the resulting string is passed to
replace_static_urls() to perform regex-based URL replacing.
We would prefer to explicitly call an API function on single image URLs, but such a function
is not exposed by the LMS API, so we have to fall back to this slightly hacky implementation.
"""
@cls.json_handler
@functools.wraps(func)
def wrapper(self, request_json, suffix=''):
response = json.dumps(func(self, request_json, suffix))
response = replace_static_urls(response, course_id=self.runtime.course_id)
return Response(response, content_type='application/json')
if HAS_STATIC_REPLACE:
# Only use URL translation if it is available
return wrapper
# Otherwise fall back to a standard JSON handler
return cls.json_handler(func)
class PollBlock(PollBase):
"""
......@@ -228,8 +277,12 @@ class PollBlock(PollBase):
# 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})),
default=[
('R', {'label': 'Red', 'img': None, 'img_alt': None}),
('B', {'label': 'Blue', 'img': None, 'img_alt': None}),
('G', {'label': 'Green', 'img': None, 'img_alt': None}),
('O', {'label': 'Other', 'img': None, 'img_alt': None}),
],
scope=Scope.settings, help="The answer options on this poll."
)
tally = Dict(default={'R': 0, 'B': 0, 'G': 0, 'O': 0},
......@@ -245,8 +298,8 @@ class PollBlock(PollBase):
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():
answers = dict(self.answers)
for key in answers:
if key not in self.tally:
self.tally[key] = 0
......@@ -272,6 +325,7 @@ class PollBlock(PollBase):
'count': count,
'answer': value['label'],
'img': value['img'],
'img_alt': value.get('img_alt'),
'key': key,
'first': False,
'choice': False,
......@@ -303,7 +357,7 @@ class PollBlock(PollBase):
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):
if self.choice and self.choice in dict(self.answers):
return self.choice
else:
return None
......@@ -322,7 +376,6 @@ class PollBlock(PollBase):
context.update({
'choice': choice,
# Offset so choices will always be True.
'answers': self.markdown_items(self.answers),
'question': markdown(self.question),
'private_results': self.private_results,
......@@ -369,14 +422,14 @@ class PollBlock(PollBase):
return {
'items': [
{
'key': key, 'text': value['label'], 'img': value['img'],
'key': key, 'text': value['label'], 'img': value['img'], 'img_alt': value.get('img_alt'),
'noun': 'answer', 'image': True,
}
}
for key, value in self.answers
],
}
@XBlock.json_handler
@PollBase.static_replace_json_handler
def get_results(self, data, suffix=''):
if self.private_results and not self.can_view_private_results():
detail, total = {}, None
......@@ -480,10 +533,10 @@ class PollBlock(PollBase):
"""
<poll tally="{'long': 20, 'short': 29, 'not_saying': 15, 'longer' : 35}"
question="## How long have you been studying with us?"
answers='[["longt", {"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}]]'
answers='[["longt", {"label": "A very long time", "img": null, "img_alt": null}],
["short", {"label": "Not very long", "img": null, "img_alt": null}],
["not_saying", {"label": "I shall not say", "img": null, "img_alt": null}],
["longer", {"label": "Longer than you", "img": null, "img_alt": null}]]'
feedback="### Thank you&#10;&#10;for being a valued student."/>
"""),
]
......@@ -503,11 +556,11 @@ class SurveyBlock(PollBase):
scope=Scope.settings, help="Answer choices for this Survey"
)
questions = List(
default=(
('enjoy', {'label': 'Are you enjoying the course?', 'img': None}),
('recommend', {'label': 'Would you recommend this course to your friends?', 'img': None}),
('learn', {'label': 'Do you think you will learn a lot?', 'img': None})
),
default=[
('enjoy', {'label': 'Are you enjoying the course?', 'img': None, 'img_alt': None}),
('recommend', {'label': 'Would you recommend this course to your friends?', 'img': None, 'img_alt': None}),
('learn', {'label': 'Do you think you will learn a lot?', 'img': None, 'img_alt': None}),
],
scope=Scope.settings, help="Questions for this Survey"
)
tally = Dict(
......@@ -522,7 +575,7 @@ class SurveyBlock(PollBase):
def student_view(self, context=None):
"""
The primary view of the PollBlock, shown to students
The primary view of the SurveyBlock, shown to students
when viewing courses.
"""
if not context:
......@@ -609,6 +662,7 @@ class SurveyBlock(PollBase):
tally.append({
'label': value['label'],
'img': value['img'],
'img_alt': value.get('img_alt'),
'answers': [
{
'count': count, 'choice': False,
......@@ -645,8 +699,8 @@ class SurveyBlock(PollBase):
we just clean it up on first access within the LMS, in case the studio
has made changes to the answers.
"""
questions = OrderedDict(self.questions)
answers = OrderedDict(self.answers)
questions = dict(self.questions)
answers = dict(self.answers)
default_answers = {answer: 0 for answer in answers.keys()}
for key in questions.keys():
if key not in self.tally:
......@@ -702,7 +756,7 @@ class SurveyBlock(PollBase):
return None
return self.choices
@XBlock.json_handler
@PollBase.static_replace_json_handler
def get_results(self, data, suffix=''):
if self.private_results and not self.can_view_private_results():
detail, total = {}, None
......@@ -733,7 +787,7 @@ class SurveyBlock(PollBase):
return {
'items': [
{
'key': key, 'text': value['label'], 'img': value['img'],
'key': key, 'text': value['label'], 'img': value['img'], 'img_alt': value.get('img_alt'),
'noun': 'question', 'image': True,
}
for key, value in self.questions
......@@ -838,10 +892,11 @@ class SurveyBlock(PollBase):
"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}]]'
questions='[["q1", {"label": "I feel like this test will pass.", "img": null, "img_alt": null}],
["q2", {"label": "I like testing software", "img": null, "img_alt": null}],
["q3", {"label": "Testing is not necessary", "img": null, "img_alt": null}],
["q4", {"label": "I would fake a test result to get software deployed.", "img": null,
"img_alt": null}]]'
answers='[["sa", "Strongly Agree"], ["a", "Agree"], ["n", "Neutral"],
["d", "Disagree"], ["sd", "Strongly Disagree"]]'
feedback="### Thank you&#10;&#10;for running the tests."/>
......
......@@ -13,7 +13,7 @@
}
.poll-result-input-container {
display: table-cell;
display: inline-block;
vertical-align: middle;
}
......@@ -21,9 +21,15 @@
margin: 0 .5em 0 0;
}
.percentage-gauge-background {
position: absolute;
display: inline-block;
background-color: #fafbfc;
}
.percentage-gauge-container {
display: table-cell;
width: 100%;
display: inline-block;
width: 65%;
vertical-align: middle;
background-color: #fafbfc;
}
......@@ -45,14 +51,10 @@ li.poll-spacer {
height: .25em;
}
ul.poll-answers-results {
display: table;
}
li.poll-result {
div.poll-block ul.poll-results li.poll-result {
/* The selector above needs all three parts to trump some margin setting from the LMS. */
width: 100%;
display: table-row;
padding-bottom: .2em;
margin-bottom: 4px;
}
.poll-answer-label {
......@@ -68,14 +70,12 @@ li.poll-result {
width: 25%;
display: inline-block;
vertical-align: middle;
}
.poll-image {
margin-left: .5em;
}
li.poll-result .poll-image {
display: table-cell;
width: 22%;
display: inline-block;
margin-left: 0;
}
......@@ -92,7 +92,7 @@ li.poll-result .poll-image {
}
.poll-percent-container {
display: table-cell;
display: inline-block;
text-align: left;
padding-left: .2em;
vertical-align: middle;
......@@ -152,7 +152,7 @@ li.poll-result .poll-image {
background: none !important;
}
.survey-table .survey-option {
.survey-table .survey-option label {
text-align: center;
vertical-align: middle;
}
......
/* CSS for PollBlock Studio Menu View */
.poll-delete-answer {
clear: right;
float: right;
margin-top: 1em;
opacity: 0.5;
}
.poll-delete-answer:hover {
opacity: 1;
}
#poll-question-editor-container, #poll-feedback-editor-container{
width: 100%;
......@@ -22,10 +27,12 @@
}
.poll-move-up {
display: block;
opacity: .5;
}
.poll-move-down {
display: block;
opacity: .5;
}
......
.themed-xblock.poll-block .poll-voting-thanks span {
/* default LMS colors - contrast 11.0 */
background-color: #ffffff;
color: #3c3c3c;
}
.themed-xblock.poll-block .poll-top-choice {
/* close to LMS color for links and menu items - contrast 5.5 with white and 4.6 with e5ebee*/
color: #0070a0;
}
.themed-xblock.poll-block .poll-answer-label {
vertical-align: middle;
}
.themed-xblock.poll-block .survey-table thead tr th {
font-size: 1em;
}
.themed-xblock.poll-block .survey-row td {
padding-top: 5px;
padding-bottom: 5px;
}
/* LMS have very specific css selector that sets ~1.5em bottom margin */
.themed-xblock.poll-block table.survey-table .survey-row td p,
.themed-xblock.poll-block ul.poll-answers li.poll-answer .poll-answer p,
.themed-xblock.poll-block ul.poll-results li.poll-result .poll-answer-label p {
margin-bottom: 0;
}
.themed-xblock.poll-block ul.poll-results li.poll-spacer {
margin-bottom: 0;
}
\ No newline at end of file
......@@ -5,17 +5,17 @@
{{#each tally}}
<li class="poll-result">
<div class="poll-result-input-container">
<input id="answer-{{key}}" type="radio" disabled {{#if choice}}checked="True"{{/if}} />
<input id="answer-{{key}}" type="radio" disabled {{#if choice}}checked{{/if}} />
</div>
{{#if any_img}}
{{~#if any_img~}}
<div class="poll-image result-image">
<label for="answer-{{key}}" class="poll-image-label">
{{#if img}}
<img src="{{img}}" />
<img src="{{img}}" alt="{{img_alt}}"/>
{{/if}}
</label>
</div>
{{/if}}
</div><div class="percentage-gauge-background"></div>
{{~/if~}}
<div class="percentage-gauge-container">
<div class="percentage-gauge" style="width:{{percent}}%;">
<label class="poll-answer-label" for="answer-{{key}}">{{{answer}}}</label>
......@@ -25,10 +25,6 @@
<span class="poll-percent-display{{#if first}} 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>
......
......@@ -2,21 +2,29 @@
{{#each items}}
<li class="field comp-setting-entry is-set poll-{{noun}}-studio-item">
<div class="wrapper-comp-setting">
<div class="poll-move">
<button class="poll-move-up">&#9650;<span class="sr">&nbsp;move poll up</span></button>
<button class="poll-move-down">&#9660;<span class="sr">&nbsp;move poll down</span></button>
</div>
<button class="button action-button poll-delete-answer">Delete</button>
<label class="label setting-label poll-setting-label" for="{{noun}}-label-{{key}}">{{noun}}</label>
<input class="input setting-input" name="{{noun}}-label-{{key}}" id="{{noun}}-label-{{key}}" value="{{text}}" type="text" /><br />
{{#if image}}
<label class="label setting-label" for="{{noun}}-img-{{key}}">Image URL</label>
<input class="input setting-input" name="{{noun}}-img-{{key}}" id="{{noun}}-img-{{key}}" value="{{img}}" type="text" />
<input class="input setting-input" name="{{noun}}-img-{{key}}" id="{{noun}}-img-{{key}}" value="{{img}}" type="text" /><br />
<label class="label setting-label" for="{{noun}}-img_alt-{{key}}">Image alternative text</label>
<input class="input setting-input" name="{{noun}}-img_alt-{{key}}" id="{{noun}}-img_alt-{{key}}" value="{{img_alt}}" type="text" /><br />
{{/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">
{{#if image}}This must have an image URL or text, and can have both.{{/if}}
You can make limited use of Markdown in answer texts, preferably only bold and italics.
</span>
<span class="tip setting-help">
{{#if image}}
This must have an image URL or text, and can have both. If you add an image, you must also provide an alternative text
that describes the image in a way that would allow someone to answer the poll if the image did not load.
{{/if}}
</span>
<a href="#" class="button action-button poll-delete-answer" onclick="return false;">Delete</a>
</li>
{{/each}}
</script>
......@@ -14,7 +14,7 @@
<td class="survey-question">
{{#if img}}
<div class="poll-image-td">
<img src="{{img}}" />
<img src="{{img}}" alt="img_alt"/>
</div>
{{/if}}
{{{label}}}
......
{{ js_template|safe }}
<div class="poll-block" data-private="{% if private_results %}1{% endif %}" data-can-vote="{% if can_vote %}1{% endif %}">
<div class="poll-block themed-xblock" data-private="{% if private_results %}1{% endif %}" data-can-vote="{% if can_vote %}1{% endif %}">
{# If no form is present, the Javascript will load the results instead. #}
{% if private_results or not choice %}
<h3 class="poll-header">{{display_name}}</h3>
......@@ -14,9 +14,7 @@
{% 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 %}
<img src="{{value.img}}" alt="{{value.img_alt|default_if_none:''}}"/>
</label>
</div>
{% endif %}
......@@ -42,7 +40,7 @@
{% endif %}
{% if can_view_private_results %}
<div class="view-results-button-wrapper"><a class="view-results-button">View results</a></div>
<div class="view-results-button-wrapper"><button class="view-results-button">View results</button></div>
{% endif %}
{% endif %}
</div>
......@@ -14,18 +14,20 @@
<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>
<textarea class="input setting-input" name="question" id="poll-question-editor"
aria-describedby="poll-question-editor-help">{{question}}</textarea>
</div>
<span class="tip setting-help">Enter the prompt for the user.</span>
<span class="tip setting-help" id="poll-question-editor-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.
<div id="poll-feedback-editor-container">
<textarea class="input setting-input" name="feedback" id="poll-feedback-editor">{{feedback}}</textarea>
<textarea class="input setting-input" name="feedback" id="poll-feedback-editor"
aria-describedby="poll-feedback-editor-help">{{feedback}}</textarea>
</div>
<span class="tip setting-help">
<span class="tip setting-help" id="poll-feedback-editor-help">
This text will be displayed for the user as some extra feedback after they have
submitted their response to the poll.
</span>
......@@ -33,22 +35,24 @@
<li class="field comp-setting-entry is-set">
<div class="wrapper-comp-setting">
<label class="label setting-label poll-setting-label" for="poll-private-results">Private Results</label>
<select id="poll-private-results" class="input setting-input" name="private_results">
<select id="poll-private-results" class="input setting-input" name="private_results"
aria-describedby="poll-private-results-help">
<!-- So far as I can see, there's not a proper style for checkboxes. LTI module does it this way. -->
<option value="true" {% if private_results %} selected{% endif %}>True</option>
<option value="false" {% if not private_results %} selected{% endif %}>False</option>
</select>
</div>
<span class="tip setting-help">
If this is set True, don't display results of the poll to the user.
<span class="tip setting-help" id="poll-private-results-help">
If this is set to True, don't display results of the poll to the user.
</span>
</li>
<li class="field comp-setting-entry is-set">
<div class="wrapper-comp-setting">
<label class="label setting-label poll-setting-label" for="poll-max-submissions">Maximum Submissions</label>
<input id="poll-max-submissions" type="number" min="0" step="1" value="{{ max_submissions }}"/>
<input id="poll-max-submissions" type="number" min="0" step="1" value="{{ max_submissions }}"
aria-describedby="poll-max-submissions-help"/>
</div>
<span class="tip setting-help">
<span class="tip setting-help" id="poll-max-submissions-help">
Maximum number of times a user may submit a poll. <strong>Setting this to a value other than 1 will imply that
'Private Results' should be true.</strong> Setting it to 0 will allow infinite resubmissions.
</span>
......
{{ js_template|safe }}
<div class="poll-block" data-private="{% if private_results %}1{% endif %}" data-can-vote="{% if can_vote %}1{% endif %}">
<div class="poll-block themed-xblock" data-private="{% if private_results %}1{% endif %}" data-can-vote="{% if can_vote %}1{% endif %}">
{# If no form is present, the Javascript will load the results instead. #}
{% if not choices or private_results %}
<h3 class="poll-header">{{block_name}}</h3>
......@@ -18,14 +18,17 @@
<td class="survey-question">
{% if question.img %}
<div class="poll-image-td">
<img src="{{question.img}}" />
<img src="{{question.img}}" alt="{{question.img_alt|default_if_none:''}}"/>
</div>
{% endif %}
{{question.label|safe}}
</td>
{% for answer, answer_details in answers %}
{% for answer, label in answers %}
<td class="survey-option">
<label>
<input type="radio" name="{{key}}" value="{{answer}}"{% if question.choice == answer %} checked{% endif %}/>
<span class="sr">{{label}}</span>
</label>
</td>
{% endfor %}
</tr>
......@@ -49,7 +52,7 @@
{% endif %}
{% if can_view_private_results %}
<div class="view-results-button-wrapper"><a class="view-results-button">View results</a></div>
<div class="view-results-button-wrapper"><button class="view-results-button">View results</button></div>
{% endif %}
{% endif %}
</div>
......@@ -142,6 +142,31 @@ function PollUtil (runtime, element, pollType) {
this.getResults = function () {
// Used if results are not private, to show the user how other students voted.
function adjustGaugeBackground() {
// Adjust the height of the grey background of the the percentage gauges. This
// couldn't be achieved with CSS.
$('ul.poll-results > li', element).each(function() {
var height = 0, width;
$(this).children().each(function() {
height = Math.max(height, $(this).height());
});
width = $('.percentage-gauge-container', this).width();
$('.percentage-gauge-background', this).height(height).width(width);
});
}
function whenImagesLoaded(callback) {
// Wait for all images to be loaded, then call callback.
var missingImages = 1;
$('img', element).each(function() {
if ($(this).height() == 0) {
missingImages++;
$(this).load(function() {
if (--missingImages == 0) callback();
});
}
});
if (--missingImages == 0) callback();
}
$.ajax({
// Semantically, this would be better as GET, but we can use helper
// functions with POST.
......@@ -150,8 +175,9 @@ function PollUtil (runtime, element, pollType) {
data: JSON.stringify({}),
success: function (data) {
$('div.poll-block', element).html(self.resultsTemplate(data));
whenImagesLoaded(adjustGaugeBackground);
}
})
});
};
this.enableSubmit = function () {
......
......@@ -93,7 +93,7 @@ function PollEditUtil(runtime, element, pollType) {
// A 'key' element will have to be added after the fact, since it needs to be
// generated with the current time.
return self.extend({'text': '', 'img': ''}, extra)
return self.extend({'text': '', 'img': '', 'img_alt': ''}, extra)
};
this.empowerDeletes = function (scope) {
......@@ -175,16 +175,19 @@ function PollEditUtil(runtime, element, pollType) {
this.gather = function (scope, tracker, data, prefix, field) {
var key = 'label';
var name = scope.name.replace(prefix + '-', '');
if (name.indexOf('img-') == 0){
if (name.indexOf('img_alt-') == 0) {
name = name.replace('img_alt-', '');
key = 'img_alt'
} else if (name.indexOf('img-') == 0) {
name = name.replace('img-', '');
key = 'img'
} else if (name.indexOf('label-') == 0){
} else if (name.indexOf('label-') == 0) {
name = name.replace('label-', '');
}
if (! (scope.name.indexOf(prefix + '-') >= 0)) {
return
}
if (tracker.indexOf(name) == -1){
if (tracker.indexOf(name) == -1) {
tracker.push(name);
data[field].push({'key': name})
}
......
-e .
markdown
-e git+https://github.com/edx-solutions/xblock-utils.git#egg=xblock-utils
-e git://github.com/edx/xblock-utils.git@213a97a50276d6a2504d8133650b2930ead357a0#egg=xblock-utils
-e git://github.com/nosedjango/nosedjango.git@ed7d7f9aa969252ff799ec159f828eaa8c1cbc5a#egg=nosedjango-dev
ddt
mock
-e .
File mode changed from 100644 to 100755
......@@ -44,19 +44,17 @@ def package_data(pkg, roots):
setup(
name='xblock-poll',
version='0.2',
version='1.0',
description='An XBlock for polling users.',
packages=[
'poll',
],
install_requires=[
'XBlock',
'markdown',
'xblock-utils',
'ddt',
'mock',
],
dependency_links=['http://github.com/edx-solutions/xblock-utils/tarball/master#egg=xblock-utils'],
dependency_links=['http://github.com/edx/xblock-utils/tarball/master#egg=xblock-utils'],
entry_points={
'xblock.v1': [
'poll = poll:PollBlock',
......
......@@ -121,7 +121,7 @@ class TestPrivateResults(PollBaseTest):
@stub_view_permission(True)
def test_results_button(self, page_name, names):
self.go_to_page(page_name)
button = self.browser.find_element_by_css_selector('a.view-results-button')
button = self.browser.find_element_by_css_selector('.view-results-button')
button.click()
self.wait_until_exists('.poll-results')
self.wait_until_exists('.poll-footnote')
<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"}]]' />
answers='[["red", {"label": "Red Fox", "img": "../img/red_fox.png", "img_alt": "Red Fox"}], ["fennec", {"label": "Fennec Fox", "img": "../img/fennec_fox.png", "img_alt": "Fennec Fox"}], ["kit", {"label": "Kit Fox", "img": "../img/kit_fox.png", "img_alt": "Kit Fox"}], ["arctic", {"label": "Arctic Fox", "img": "../img/arctic_fox.png", "img_alt": "Arctic Fox"}]]' />
<poll tally="{'red': 20, 'fennec': 29, 'kit': 15, 'arctic' : 35}"
question="## What is your favorite kind of fox?"
answers='[["red", {"label": "Red Fox", "img": null}], ["fennec", {"label": "Fennec Fox", "img": "../img/fennec_fox.png"}], ["kit", {"label": "Kit Fox", "img": null}], ["arctic", {"label": "Arctic fox", "img": null}]]' />
answers='[["red", {"label": "Red Fox", "img": null}], ["fennec", {"label": "Fennec Fox", "img": "../img/fennec_fox.png", "img_alt": "Fennec Fox"}], ["kit", {"label": "Kit Fox", "img": null}], ["arctic", {"label": "Arctic fox", "img": null}]]' />
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