Commit 7fc0f64f by Jonathan Piacenti

Presentation tweaks and scaffolding for Survey.

parent 0482f8a1
from .poll import PollBlock from .poll import PollBlock, SurveyBlock
\ No newline at end of file
...@@ -9,21 +9,45 @@ import pkg_resources ...@@ -9,21 +9,45 @@ import pkg_resources
from xblock.core import XBlock from xblock.core import XBlock
from xblock.fields import Scope, String, Dict, List from xblock.fields import Scope, String, Dict, List
from xblock.fragment import Fragment from xblock.fragment import Fragment
from xblockutils.publish_event import PublishEventMixin
from xblockutils.resources import ResourceLoader from xblockutils.resources import ResourceLoader
class PollBlock(XBlock): class ResourceMixin(object):
loader = ResourceLoader(__name__)
@staticmethod
def resource_string(path):
"""Handy helper for getting resources from our kit."""
data = pkg_resources.resource_string(__name__, path)
return data.decode("utf8")
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
class PollBlock(XBlock, ResourceMixin, PublishEventMixin):
""" """
Poll XBlock. Allows a teacher to poll users, and presents the results so Poll XBlock. Allows a teacher to poll users, and presents the results so
far of the poll to the user when finished. far of the poll to the user when finished.
""" """
display_name = String(default='Poll')
question = String(default='What is your favorite color?') question = String(default='What is your favorite color?')
# This will be converted into an OrderedDict. # This will be converted into an OrderedDict.
# Key, (Label, Image path) # Key, (Label, Image path)
answers = List( answers = List(
default=(('R', {'label': 'Red', 'img': None}), ('B', {'label': 'Blue', 'img': None}), default=(('R', {'label': 'Red', 'img': None}), ('B', {'label': 'Blue', 'img': None}),
('G', {'label': 'Green', 'img': None}), ('O', {'label': 'Other', 'img': None})), ('G', {'label': 'Green', 'img': None}), ('O', {'label': 'Other', 'img': None})),
scope=Scope.settings, help="The question on this poll." scope=Scope.settings, help="The answer options on this poll."
) )
feedback = String(default='', help="Text to display after the user votes.") feedback = String(default='', help="Text to display after the user votes.")
tally = Dict(default={'R': 0, 'B': 0, 'G': 0, 'O': 0}, tally = Dict(default={'R': 0, 'B': 0, 'G': 0, 'O': 0},
...@@ -31,19 +55,14 @@ class PollBlock(XBlock): ...@@ -31,19 +55,14 @@ class PollBlock(XBlock):
help="Total tally of answers from students.") help="Total tally of answers from students.")
choice = String(scope=Scope.user_state, help="The student's answer") 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 @XBlock.json_handler
def get_results(self, data, suffix=''): def get_results(self, data, suffix=''):
self.publish_event_from_dict('xblock.poll.view_results', {})
detail, total = self.tally_detail() detail, total = self.tally_detail()
return { return {
'question': markdown(self.question), 'tally': detail, 'question': markdown(self.question), 'tally': detail,
'total': total, 'feedback': markdown(self.feedback), 'total': total, 'feedback': markdown(self.feedback),
'plural': total > 1,
} }
def clean_tally(self): def clean_tally(self):
...@@ -95,7 +114,7 @@ class PollBlock(XBlock): ...@@ -95,7 +114,7 @@ class PollBlock(XBlock):
for answer in tally: for answer in tally:
try: try:
answer['percent'] = int(answer['count'] / float(total)) * 100 answer['percent'] = round(answer['count'] / float(total) * 100)
if answer['key'] == choice: if answer['key'] == choice:
answer['choice'] = True answer['choice'] = True
except ZeroDivisionError: except ZeroDivisionError:
...@@ -121,18 +140,6 @@ class PollBlock(XBlock): ...@@ -121,18 +140,6 @@ class PollBlock(XBlock):
else: else:
return None 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): def student_view(self, context=None):
""" """
The primary view of the PollBlock, shown to students The primary view of the PollBlock, shown to students
...@@ -141,7 +148,7 @@ class PollBlock(XBlock): ...@@ -141,7 +148,7 @@ class PollBlock(XBlock):
if not context: if not context:
context = {} context = {}
js_template = self.resource_string( js_template = self.resource_string(
'/public/handlebars/results.handlebars') '/public/handlebars/poll_results.handlebars')
choice = self.get_choice() choice = self.get_choice()
...@@ -160,7 +167,7 @@ class PollBlock(XBlock): ...@@ -160,7 +167,7 @@ class PollBlock(XBlock):
if self.choice: if self.choice:
detail, total = self.tally_detail() detail, total = self.tally_detail()
context.update({'tally': detail, 'total': total}) context.update({'tally': detail, 'total': total, 'plural': total > 1})
return self.create_fragment( return self.create_fragment(
context, "public/html/poll.html", "public/css/poll.css", context, "public/html/poll.html", "public/css/poll.css",
...@@ -275,6 +282,17 @@ class PollBlock(XBlock): ...@@ -275,6 +282,17 @@ class PollBlock(XBlock):
self.tally[choice] = self.tally.get(choice, 0) + 1 self.tally[choice] = self.tally.get(choice, 0) + 1
# Let the LMS know the user has answered the poll. # Let the LMS know the user has answered the poll.
self.runtime.publish(self, 'progress', {}) self.runtime.publish(self, 'progress', {})
self.runtime.publish(self, 'grade', {
'value': 1,
'max_value': 1,
})
self.publish_event_from_dict(
'xblock.poll.submitted',
{
'choice': self.choice
},
)
result['success'] = True result['success'] = True
return result return result
...@@ -301,3 +319,66 @@ class PollBlock(XBlock): ...@@ -301,3 +319,66 @@ class PollBlock(XBlock):
</vertical_demo> </vertical_demo>
"""), """),
] ]
class SurveyBlock(XBlock, ResourceMixin, PublishEventMixin):
display_name = String(default='Survey')
answers = List(
default=(
('Y', {'label': 'Yes', 'img': None}), ('N', {'label': 'No', 'img': None}),
('M', {'label': 'Maybe', 'img': None})),
scope=Scope.settings, help="Answer choices for this Survey"
)
questions = Dict(
default={
'enjoy': 'Are you enjoying the course?', 'recommend': 'Would you recommend this course to your friends?',
'learn': 'Do you think you will learn a lot?'
},
scope=Scope.settings, help="Questions for this Survey"
)
feedback = String(default='', help="Text to display after the user votes.")
tally = Dict(
default={
'enjoy': {'Y': 0, 'N': 0, 'M': 0}, 'recommend': {'Y': 0, 'N': 0, 'M': 0},
'learn': {'Y': 0, 'N': 0, 'M': 0}},
scope=Scope.user_state_summary,
help="Total tally of answers from students."
)
choices = Dict(help="The user's answers")
def student_view(self, context=None):
"""
The primary view of the PollBlock, shown to students
when viewing courses.
"""
if not context:
context = {}
context.update({
'choices': self.choices,
# Offset so choices will always be True.
'answers': self.answers,
'questions': self.questions,
# Mustache is treating an empty string as true.
'feedback': markdown(self.feedback) or False,
# The SDK doesn't set url_name.
'url_name': getattr(self, 'url_name', ''),
})
return self.create_fragment(
context, "public/html/survey.html", "public/css/poll.css",
"public/js/poll.js", "PollBlock")
@staticmethod
def workbench_scenarios():
"""
Canned scenarios for display in the workbench.
"""
return [
("Default Survey",
"""
<vertical_demo>
<survey />
</vertical_demo>
"""),
]
<script id="results" type="text/html"> <script id="poll-results-template" type="text/html">
{{{question}}} {{{question}}}
<ul class="poll-answers-results"> <ul class="poll-answers-results">
{{#each tally}} {{#each tally}}
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
{{/each}} {{/each}}
</ul> </ul>
<input class="input-main" type="button" name="poll-submit" value="Submit" disabled> <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}} {{#if feedback}}
<hr /> <hr />
<div class="poll-feedback"> <div class="poll-feedback">
......
<div class="survey-block">
{# If no form is present, the Javascript will load the results instead. #}
{% if not choices %}
<form>
<table>
<thead>
<tr>
<th></th>
{% for answer, details in answers %}
<th>{{details.label}}</th>
{% endfor %}
</tr>
</thead>
{% for key, question in questions.items %}
<tr>
<td>
{{question}}
</td>
{% for answer, answer_details in answers %}
<td>
<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. */ /* Javascript for PollBlock. */
function PollBlock(runtime, element) {
var voteUrl = runtime.handlerUrl(element, 'vote'); var PollUtil = {
var tallyURL = runtime.handlerUrl(element, 'get_results');
init: function(runtime, element) {
this.voteUrl = runtime.handlerUrl(element, 'vote');
this.tallyURL = runtime.handlerUrl(element, 'get_results');
this.element = element;
this.runtime = runtime;
this.submit = $('input[type=button]', element);
this.resultsTemplate = Handlebars.compile($("#poll-results-template", element).html());
},
poll_init: function(){
// If the submit button doesn't exist, the user has already
// selected a choice.
var self = this;
if (self.submit.length) {
var radio = $('input[name=choice]:checked', self.element);
self.submit.click(function (event) {
// Refresh.
radio = $(radio.selector, element);
var choice = radio.val();
$.ajax({
type: "POST",
url: self.voteUrl,
data: JSON.stringify({"choice": choice}),
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]', self.element);
if (! radio.val()) {
answers.bind("change.EnableSubmit", self.enableSubmit);
} else {
self.enableSubmit();
}
} else {
self.getResults({'success': true});
}
},
var submit = $('input[type=button]', element); getResults: function(data) {
var resultsTemplate = Handlebars.compile($("#results", element).html()); var self = this;
function getResults(data) {
if (! data['success']) { if (! data['success']) {
alert(data['errors'].join('\n')); alert(data['errors'].join('\n'));
} }
...@@ -14,44 +50,21 @@ function PollBlock(runtime, element) { ...@@ -14,44 +50,21 @@ function PollBlock(runtime, element) {
// Semantically, this would be better as GET, but we can use helper // Semantically, this would be better as GET, but we can use helper
// functions with POST. // functions with POST.
type: "POST", type: "POST",
url: tallyURL, url: self.tallyURL,
data: JSON.stringify({}), data: JSON.stringify({}),
success: function (data) { success: function (data) {
$('div.poll-block', element).html(resultsTemplate(data)); $('div.poll-block', self.element).html(self.resultsTemplate(data));
} }
}) })
} },
function enableSubmit() { enableSubmit: function () {
submit.removeAttr("disabled"); this.submit.removeAttr("disabled");
answers.unbind("change.EnableSubmit"); this.answers.unbind("change.EnableSubmit");
} }
};
// If the submit button doesn't exist, the user has already function PollBlock(runtime, element) {
// selected a choice. PollUtil.init(runtime, element);
if (submit.length) { PollUtil.poll_init();
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
...@@ -22,7 +22,7 @@ def package_data(pkg, roots): ...@@ -22,7 +22,7 @@ def package_data(pkg, roots):
setup( setup(
name='xblock-poll', name='xblock-poll',
version='0.1', version='0.2',
description='An XBlock for polling users.', description='An XBlock for polling users.',
packages=[ packages=[
'poll', 'poll',
...@@ -30,14 +30,14 @@ setup( ...@@ -30,14 +30,14 @@ setup(
install_requires=[ install_requires=[
'XBlock', 'XBlock',
'markdown', 'markdown',
'bleach',
'xblock-utils', 'xblock-utils',
], ],
dependency_links=['http://github.com/edx-solutions/xblock-utils/tarball/master#egg=xblock-utils'], dependency_links=['http://github.com/edx-solutions/xblock-utils/tarball/master#egg=xblock-utils'],
entry_points={ entry_points={
'xblock.v1': [ 'xblock.v1': [
'poll = poll:PollBlock', 'poll = poll:PollBlock',
'survey = poll:SurveyBlock',
] ]
}, },
package_data=package_data("poll", ["static", "public"]), package_data=package_data("poll", ["static", "public"]),
) )
\ No newline at end of file
...@@ -58,7 +58,7 @@ class TestPollFunctions(PollBaseTest): ...@@ -58,7 +58,7 @@ class TestPollFunctions(PollBaseTest):
"Thank you\nfor being a valued student.") "Thank you\nfor being a valued student.")
self.assertEqual(self.browser.find_element_by_css_selector('.poll-footnote').text, 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()) self.assertFalse(self.browser.find_element_by_css_selector('input[name=poll-submit]').is_enabled())
...@@ -79,4 +79,4 @@ class TestPollFunctions(PollBaseTest): ...@@ -79,4 +79,4 @@ class TestPollFunctions(PollBaseTest):
self.wait_until_exists('input[name=poll-submit]:disabled') self.wait_until_exists('input[name=poll-submit]:disabled')
self.go_to_page('Poll Functions') self.go_to_page('Poll Functions')
self.assertFalse(self.get_submit().is_enabled()) self.assertFalse(self.get_submit().is_enabled())
\ 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