Commit ec85362e by Jonathan Piacenti

Tightened up tests, added image support, question reordering.

parent 5d7171e3
......@@ -19,13 +19,15 @@ class PollBlock(XBlock):
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=(('Red', 'Red'), ('Blue', 'Blue'), ('Green', 'Green'),
('Other', 'Other')),
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={'Red': 0, 'Blue': 0, 'Green': 0, 'Other': 0},
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")
......@@ -45,6 +47,34 @@ class PollBlock(XBlock):
'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):
"""
Tally all results.
......@@ -53,14 +83,18 @@ class PollBlock(XBlock):
answers = OrderedDict(self.answers)
choice = self.get_choice()
total = 0
source_tally = self.get_tally()
any_img = self.any_image()
for key, value in answers.items():
tally.append({
'count': int(self.tally.get(key, 0)),
'answer': value,
'count': int(source_tally[key]),
'answer': value['label'],
'img': value['img'],
'key': key,
'top': False,
'choice': False,
'last': False,
'any_img': any_img,
})
total += tally[-1]['count']
......@@ -70,6 +104,8 @@ class PollBlock(XBlock):
answer['percent'] = int(percent * 100)
if answer['key'] == choice:
answer['choice'] = True
if answer['img']:
any_img = True
except ZeroDivisionError:
answer['percent'] = 0
......@@ -114,6 +150,7 @@ class PollBlock(XBlock):
# Mustache is treating an empty string as true.
'feedback': process_markdown(self.feedback) or False,
'js_template': js_template,
'any_img': self.any_image()
})
if self.choice:
......@@ -134,8 +171,8 @@ class PollBlock(XBlock):
@XBlock.json_handler
def load_answers(self, data, suffix=''):
return {'answers': [{'key': key, 'text': answer}
for key, answer in self.answers
return {'answers': [{'key': key, 'text': value['label'], 'img': value['img']}
for key, value in self.answers
]}
def studio_view(self, context=None):
......@@ -177,22 +214,40 @@ class PollBlock(XBlock):
# Need this meta information, otherwise the questions will be
# shuffled by Python's dictionary data type.
poll_order = [key.strip().replace('answer-', '')
for key in data.get('poll_order', [])
poll_order = [
key.strip().replace('answer-', '')
for key in data.get('poll_order', [])
]
print poll_order
# Aggressively clean/sanity check answers list.
answers = []
answers = {}
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
key = key.replace('answer-', '')
key = key.replace('answer-', '').replace('img-', '')
if not key or key.isspace():
continue
value = value.strip()[:250]
if not value or value.isspace():
continue
update_dict = {img or text: value}
if key in answers:
answers[key].update(update_dict)
continue
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:
result['errors'].append(
......@@ -203,23 +258,15 @@ class PollBlock(XBlock):
return result
# 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.question = question
self.feedback = feedback
tally = self.tally
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]
# Tally will not be updated until the next attempt to use it, per
# scoping limitations.
return result
......@@ -244,13 +291,16 @@ class PollBlock(XBlock):
result['errors'].append('No key "{choice}" in answers table.'.format(choice=choice))
return result
tally = self.get_tally()
self.choice = choice
running_total = self.tally.get(choice, 0)
self.tally[choice] = running_total + 1
running_total = tally.get(choice, 0)
tally[choice] = running_total + 1
# Let the LMS know the user has answered the poll.
self.runtime.publish(self, 'progress', {})
result['success'] = True
self.tally = tally
return result
# TO-DO: change this to create the scenarios you'd like to see in the
......
......@@ -6,14 +6,19 @@
.percentage-gauge {
display: inline-block;
background-color: #e5ebee;
position: relative;
z-index: 1;
}
.poll-result-input-container {
display: table-cell;
padding-right: .2em;
vertical-align: middle;
}
.poll-result-input-container input {
margin-right: .5em;
}
.percentage-gauge-container {
display: table-cell;
width: 100%;
......@@ -29,7 +34,11 @@ ul.poll-answers, ul.poll-answers-results {
li.poll-answer {
display: block;
border-bottom-width: .5em;
vertical-align: middle;
margin-top: 5px;
margin-bottom: 5px;
}
li.poll-spacer {
height: .25em;
}
......@@ -44,9 +53,31 @@ li.poll-result {
padding-bottom: .2em;
}
.poll-answer-text {
.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 {
......
......@@ -2,6 +2,7 @@
.poll-delete-answer {
float: right;
margin-top: 1em;
}
#poll-question-editor-container, #poll-feedback-editor-container{
width: 100%;
......@@ -14,7 +15,7 @@
color: #4C4C4C;
margin-top: 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-radius: 3px;
padding: 10px;
......@@ -23,4 +24,22 @@
label.poll-label {
font-weight: bold;
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 @@
<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">
<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-text" for="answer-{{key}}">{{answer}}</label>
<label class="poll-answer-label" for="answer-{{key}}">{{answer}}</label>
</div>
</div>
<div class="poll-percent-container">
......
......@@ -3,9 +3,15 @@
<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" />
<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.</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>
</li>
{{/each}}
......
......@@ -7,10 +7,19 @@
{{question|safe}}
</div>
<ul class="poll-answers">
{% for key, answer in answers %}
{% for key, value in answers %}
<li class="poll-answer">
<input type="radio" name="choice" id="answer-{{key}}" value="{{key}}">
<label class="poll-answer" for="answer-{{key}}">{{answer}}</label>
<input type="radio" name="choice" id="answer-{{key}}" value="{{key}}" />
{% 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>
{% endfor %}
</ul>
......
......@@ -34,7 +34,7 @@
<div class="xblock-actions">
<ul>
<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 class="action-item">
<input type="submit" class="button action-primary save-button" value="Save" onclick="return false;" />
......
......@@ -17,7 +17,9 @@ function PollBlock(runtime, element) {
url: tallyURL,
data: JSON.stringify({}),
success: function (data) {
$(element).fadeOut(300);
$('div.poll-block', element).html(resultsTemplate(data));
$(element).fadeIn(300);
}
})
}
......@@ -28,7 +30,6 @@ function PollBlock(runtime, element) {
// Refresh.
radios = $(radios.selector);
var choice = radios.val();
console.log(choice);
$.ajax({
type: "POST",
url: voteUrl,
......
......@@ -15,21 +15,46 @@ function PollEditBlock(runtime, element) {
})
}
function empowerDeletes() {
$('.poll-delete-answer', element).click(function () {
function empowerDeletes(scope) {
$('.poll-delete-answer', scope).click(function () {
$(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) {
pollLineItems.append(answerTemplate(data));
empowerDeletes();
empowerDeletes(element);
empowerArrows(element);
}
$('#poll-add-answer', element).click(function () {
pollLineItems.append(answerTemplate({'answers': [{'key': generateUUID(), 'text': ''}]}));
empowerDeletes();
pollLineItems.last().scrollTop();
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() {
......@@ -42,7 +67,7 @@ function PollEditBlock(runtime, element) {
var poll_order = [];
$('#poll-form input', element).each(function(i) {
data[this.name] = this.value;
if (this.name.indexOf('answer-') >= 0){
if (this.name.indexOf('answer-') == 0){
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