Commit ec85362e by Jonathan Piacenti

Tightened up tests, added image support, question reordering.

parent 5d7171e3
...@@ -19,13 +19,15 @@ class PollBlock(XBlock): ...@@ -19,13 +19,15 @@ class PollBlock(XBlock):
far of the poll to the user when finished. far of the poll to the user when finished.
""" """
question = String(default='What is your favorite color?') question = String(default='What is your favorite color?')
# This will be converted into an OrderedDict.
# Key, (Label, Image path)
answers = List( answers = List(
default=(('Red', 'Red'), ('Blue', 'Blue'), ('Green', 'Green'), default=(('R', {'label': 'Red', 'img': None}), ('B', {'label': 'Blue', 'img': None}),
('Other', 'Other')), ('G', {'label': 'Green', 'img': None}), ('O', {'label': 'Other', 'img': None})),
scope=Scope.settings, help="The question on this poll." scope=Scope.settings, help="The question 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={'Red': 0, 'Blue': 0, 'Green': 0, 'Other': 0}, tally = Dict(default={'R': 0, 'B': 0, 'G': 0, 'O': 0},
scope=Scope.user_state_summary, scope=Scope.user_state_summary,
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")
...@@ -45,6 +47,34 @@ class PollBlock(XBlock): ...@@ -45,6 +47,34 @@ class PollBlock(XBlock):
'total': total, 'feedback': process_markdown(self.feedback), 'total': total, 'feedback': process_markdown(self.feedback),
} }
def get_tally(self):
"""
Grabs the Tally and cleans it up, if necessary. 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.
"""
tally = self.tally
answers = OrderedDict(self.answers)
for key in answers.keys():
if key not in tally:
tally[key] = 0
for key in tally.keys():
if key not in answers:
del tally[key]
return tally
def any_image(self):
"""
Find out if any answer has an image, since it affects layout.
"""
for value in dict(self.answers).values():
if value['img']:
return True
return False
def tally_detail(self): def tally_detail(self):
""" """
Tally all results. Tally all results.
...@@ -53,14 +83,18 @@ class PollBlock(XBlock): ...@@ -53,14 +83,18 @@ class PollBlock(XBlock):
answers = OrderedDict(self.answers) answers = OrderedDict(self.answers)
choice = self.get_choice() choice = self.get_choice()
total = 0 total = 0
source_tally = self.get_tally()
any_img = self.any_image()
for key, value in answers.items(): for key, value in answers.items():
tally.append({ tally.append({
'count': int(self.tally.get(key, 0)), 'count': int(source_tally[key]),
'answer': value, 'answer': value['label'],
'img': value['img'],
'key': key, 'key': key,
'top': False, 'top': False,
'choice': False, 'choice': False,
'last': False, 'last': False,
'any_img': any_img,
}) })
total += tally[-1]['count'] total += tally[-1]['count']
...@@ -70,6 +104,8 @@ class PollBlock(XBlock): ...@@ -70,6 +104,8 @@ class PollBlock(XBlock):
answer['percent'] = int(percent * 100) answer['percent'] = int(percent * 100)
if answer['key'] == choice: if answer['key'] == choice:
answer['choice'] = True answer['choice'] = True
if answer['img']:
any_img = True
except ZeroDivisionError: except ZeroDivisionError:
answer['percent'] = 0 answer['percent'] = 0
...@@ -114,6 +150,7 @@ class PollBlock(XBlock): ...@@ -114,6 +150,7 @@ class PollBlock(XBlock):
# Mustache is treating an empty string as true. # Mustache is treating an empty string as true.
'feedback': process_markdown(self.feedback) or False, 'feedback': process_markdown(self.feedback) or False,
'js_template': js_template, 'js_template': js_template,
'any_img': self.any_image()
}) })
if self.choice: if self.choice:
...@@ -134,8 +171,8 @@ class PollBlock(XBlock): ...@@ -134,8 +171,8 @@ class PollBlock(XBlock):
@XBlock.json_handler @XBlock.json_handler
def load_answers(self, data, suffix=''): def load_answers(self, data, suffix=''):
return {'answers': [{'key': key, 'text': answer} return {'answers': [{'key': key, 'text': value['label'], 'img': value['img']}
for key, answer in self.answers for key, value in self.answers
]} ]}
def studio_view(self, context=None): def studio_view(self, context=None):
...@@ -177,22 +214,40 @@ class PollBlock(XBlock): ...@@ -177,22 +214,40 @@ class PollBlock(XBlock):
# Need this meta information, otherwise the questions will be # Need this meta information, otherwise the questions will be
# shuffled by Python's dictionary data type. # shuffled by Python's dictionary data type.
poll_order = [key.strip().replace('answer-', '') poll_order = [
for key in data.get('poll_order', []) key.strip().replace('answer-', '')
for key in data.get('poll_order', [])
] ]
print poll_order
# Aggressively clean/sanity check answers list. # Aggressively clean/sanity check answers list.
answers = [] answers = {}
for key, value in data.items(): for key, value in data.items():
if not key.startswith('answer-'): img = False
text = False
if key.startswith('answer-'):
text = 'label'
if key.startswith('img-answer-'):
img = 'img'
if not (text or img):
continue continue
key = key.replace('answer-', '') key = key.replace('answer-', '').replace('img-', '')
if not key or key.isspace(): if not key or key.isspace():
continue continue
value = value.strip()[:250] value = value.strip()[:250]
if not value or value.isspace(): if not value or value.isspace():
continue continue
update_dict = {img or text: value}
if key in answers:
answers[key].update(update_dict)
continue
if key in poll_order: if key in poll_order:
answers.append((key, value)) answers[key] = update_dict
for value in answers.values():
if 'label' not in value:
value['label'] = None
if 'img' not in value:
value['img'] = None
if not len(answers) > 1: if not len(answers) > 1:
result['errors'].append( result['errors'].append(
...@@ -203,23 +258,15 @@ class PollBlock(XBlock): ...@@ -203,23 +258,15 @@ class PollBlock(XBlock):
return result return result
# Need to sort the answers. # Need to sort the answers.
answers.sort(key=lambda x: poll_order.index(x[0]), reverse=True) answers = list(answers.items())
answers.sort(key=lambda x: poll_order.index(x[0]))
self.answers = answers self.answers = answers
self.question = question self.question = question
self.feedback = feedback self.feedback = feedback
tally = self.tally # Tally will not be updated until the next attempt to use it, per
# scoping limitations.
answers = OrderedDict(answers)
# Update tracking schema.
for key, value in answers.items():
if key not in tally:
tally[key] = 0
for key, value in tally.items():
if key not in answers:
del tally[key]
return result return result
...@@ -244,13 +291,16 @@ class PollBlock(XBlock): ...@@ -244,13 +291,16 @@ class PollBlock(XBlock):
result['errors'].append('No key "{choice}" in answers table.'.format(choice=choice)) result['errors'].append('No key "{choice}" in answers table.'.format(choice=choice))
return result return result
tally = self.get_tally()
self.choice = choice self.choice = choice
running_total = self.tally.get(choice, 0) running_total = tally.get(choice, 0)
self.tally[choice] = running_total + 1 tally[choice] = running_total + 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', {})
result['success'] = True result['success'] = True
self.tally = tally
return result return result
# TO-DO: change this to create the scenarios you'd like to see in the # TO-DO: change this to create the scenarios you'd like to see in the
......
...@@ -6,14 +6,19 @@ ...@@ -6,14 +6,19 @@
.percentage-gauge { .percentage-gauge {
display: inline-block; display: inline-block;
background-color: #e5ebee; background-color: #e5ebee;
position: relative;
z-index: 1;
} }
.poll-result-input-container { .poll-result-input-container {
display: table-cell; display: table-cell;
padding-right: .2em;
vertical-align: middle; vertical-align: middle;
} }
.poll-result-input-container input {
margin-right: .5em;
}
.percentage-gauge-container { .percentage-gauge-container {
display: table-cell; display: table-cell;
width: 100%; width: 100%;
...@@ -29,7 +34,11 @@ ul.poll-answers, ul.poll-answers-results { ...@@ -29,7 +34,11 @@ ul.poll-answers, ul.poll-answers-results {
li.poll-answer { li.poll-answer {
display: block; display: block;
border-bottom-width: .5em; border-bottom-width: .5em;
vertical-align: middle;
margin-top: 5px;
margin-bottom: 5px;
} }
li.poll-spacer { li.poll-spacer {
height: .25em; height: .25em;
} }
...@@ -44,9 +53,31 @@ li.poll-result { ...@@ -44,9 +53,31 @@ li.poll-result {
padding-bottom: .2em; padding-bottom: .2em;
} }
.poll-answer-text { .poll-answer-label {
margin-left: .2em; margin-left: .2em;
font-weight: bold; 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 { .poll-percent-container {
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
.poll-delete-answer { .poll-delete-answer {
float: right; float: right;
margin-top: 1em;
} }
#poll-question-editor-container, #poll-feedback-editor-container{ #poll-question-editor-container, #poll-feedback-editor-container{
width: 100%; width: 100%;
...@@ -14,7 +15,7 @@ ...@@ -14,7 +15,7 @@
color: #4C4C4C; color: #4C4C4C;
margin-top: 0.5em; margin-top: 0.5em;
margin-bottom: 0.5em; margin-bottom: 0.5em;
box-shadow: 0px 0px 9px #555 inset; box-shadow: 0 0 9px #555 inset;
border: 1px solid #B2B2B2; border: 1px solid #B2B2B2;
border-radius: 3px; border-radius: 3px;
padding: 10px; padding: 10px;
...@@ -23,4 +24,22 @@ ...@@ -23,4 +24,22 @@
label.poll-label { label.poll-label {
font-weight: bold; font-weight: bold;
font-size: 16pt; font-size: 16pt;
}
.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
...@@ -6,9 +6,18 @@ ...@@ -6,9 +6,18 @@
<div class="poll-result-input-container"> <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="True"{{/if}} />
</div> </div>
{{#if any_img}}
<div class="poll-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-container">
<div class="percentage-gauge" style="width:{{percent}}%;"> <div class="percentage-gauge" style="width:{{percent}}%;">
<label class="poll-answer-text" for="answer-{{key}}">{{answer}}</label> <label class="poll-answer-label" for="answer-{{key}}">{{answer}}</label>
</div> </div>
</div> </div>
<div class="poll-percent-container"> <div class="poll-percent-container">
......
...@@ -3,9 +3,15 @@ ...@@ -3,9 +3,15 @@
<li class="field comp-setting-entry is-set"> <li class="field comp-setting-entry is-set">
<div class="wrapper-comp-setting"> <div class="wrapper-comp-setting">
<label class="label setting-label" for="answer-{{key}}">Answer</label> <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" /> <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> </div>
<span class="tip setting-help">Enter an answer for the user to select.</span> <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.</span>
<a href="#" class="button action-button poll-delete-answer" onclick="return false;">Delete</a> <a href="#" class="button action-button poll-delete-answer" onclick="return false;">Delete</a>
</li> </li>
{{/each}} {{/each}}
......
...@@ -7,10 +7,19 @@ ...@@ -7,10 +7,19 @@
{{question|safe}} {{question|safe}}
</div> </div>
<ul class="poll-answers"> <ul class="poll-answers">
{% for key, answer in answers %} {% for key, value in answers %}
<li class="poll-answer"> <li class="poll-answer">
<input type="radio" name="choice" id="answer-{{key}}" value="{{key}}"> <input type="radio" name="choice" id="answer-{{key}}" value="{{key}}" />
<label class="poll-answer" for="answer-{{key}}">{{answer}}</label> {% if value.img %}
<div class="poll-image">
<label for="answer-{{key}}" class="poll-image-label">
{% if value.img %}
<img src="{{value.img}}" />
{% endif %}
</label>
</div>
{% endif %}
<label class="poll-answer" for="answer-{{key}}">{{value.label}}</label>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
......
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
<div class="xblock-actions"> <div class="xblock-actions">
<ul> <ul>
<li class="action-item" id="poll-add-answer"> <li class="action-item" id="poll-add-answer">
<a href="#" class="button action-button" onclick="return false;">Add Question</a> <a href="#" class="button action-button" onclick="return false;">Add Answer</a>
</li> </li>
<li class="action-item"> <li class="action-item">
<input type="submit" class="button action-primary save-button" value="Save" onclick="return false;" /> <input type="submit" class="button action-primary save-button" value="Save" onclick="return false;" />
......
...@@ -17,7 +17,9 @@ function PollBlock(runtime, element) { ...@@ -17,7 +17,9 @@ function PollBlock(runtime, element) {
url: tallyURL, url: tallyURL,
data: JSON.stringify({}), data: JSON.stringify({}),
success: function (data) { success: function (data) {
$(element).fadeOut(300);
$('div.poll-block', element).html(resultsTemplate(data)); $('div.poll-block', element).html(resultsTemplate(data));
$(element).fadeIn(300);
} }
}) })
} }
...@@ -28,7 +30,6 @@ function PollBlock(runtime, element) { ...@@ -28,7 +30,6 @@ function PollBlock(runtime, element) {
// Refresh. // Refresh.
radios = $(radios.selector); radios = $(radios.selector);
var choice = radios.val(); var choice = radios.val();
console.log(choice);
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: voteUrl, url: voteUrl,
......
...@@ -15,21 +15,46 @@ function PollEditBlock(runtime, element) { ...@@ -15,21 +15,46 @@ function PollEditBlock(runtime, element) {
}) })
} }
function empowerDeletes() { function empowerDeletes(scope) {
$('.poll-delete-answer', element).click(function () { $('.poll-delete-answer', scope).click(function () {
$(this).parent().remove(); $(this).parent().remove();
}); });
} }
// Above this point are other settings.
var starting_point = 3;
function empowerArrows(scope) {
$('.poll-move-up', scope).click(function () {
var tag = $(this).parent().parent().parent();
if (tag.index() <= starting_point){
return;
}
tag.prev().before(tag);
tag.fadeOut(250).fadeIn(250);
});
$('.poll-move-down', scope).click(function () {
var tag = $(this).parent().parent().parent();
if ((tag.index() >= (tag.parent().children().length - 1))) {
return;
}
tag.next().after(tag);
tag.parent().parent().parent().scrollTop(tag.offset().top);
tag.fadeOut(250).fadeIn(250);
});
}
function displayAnswers(data) { function displayAnswers(data) {
pollLineItems.append(answerTemplate(data)); pollLineItems.append(answerTemplate(data));
empowerDeletes(); empowerDeletes(element);
empowerArrows(element);
} }
$('#poll-add-answer', element).click(function () { $('#poll-add-answer', element).click(function () {
pollLineItems.append(answerTemplate({'answers': [{'key': generateUUID(), 'text': ''}]})); pollLineItems.append(answerTemplate({'answers': [{'key': generateUUID(), 'text': ''}]}));
empowerDeletes(); var new_answer = $(pollLineItems.children().last());
pollLineItems.last().scrollTop(); empowerDeletes(new_answer);
empowerArrows(new_answer);
new_answer.fadeOut(250).fadeIn(250);
}); });
$(element).find('.cancel-button', element).bind('click', function() { $(element).find('.cancel-button', element).bind('click', function() {
...@@ -42,7 +67,7 @@ function PollEditBlock(runtime, element) { ...@@ -42,7 +67,7 @@ function PollEditBlock(runtime, element) {
var poll_order = []; var poll_order = [];
$('#poll-form input', element).each(function(i) { $('#poll-form input', element).each(function(i) {
data[this.name] = this.value; data[this.name] = this.value;
if (this.name.indexOf('answer-') >= 0){ if (this.name.indexOf('answer-') == 0){
poll_order.push(this.name); poll_order.push(this.name);
} }
}); });
......
<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
<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
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