Commit 866f7529 by Jonathan Piacenti

Studio view and images implemented for Surveys.

parent 21ab57d0
......@@ -54,58 +54,68 @@ class PollBase(XBlock, ResourceMixin, PublishEventMixin):
choice_data,
)
@XBlock.json_handler
def load_answers(self, data, suffix=''):
return {
'items': [
{
'key': key, 'text': value['label'], 'img': value['img'],
'noun': 'answer', 'image': True,
}
for key, value in self.answers
],
}
@XBlock.json_handler
def get_results(self, data, suffix=''):
self.publish_event_from_dict(self.event_namespace + '.view_results', {})
detail, total = self.tally_detail()
return {
'question': markdown(self.question), 'tally': detail,
'total': total, 'feedback': markdown(self.feedback),
'plural': total > 1, 'display_name': self.display_name,
}
@XBlock.json_handler
def vote(self, data, suffix=''):
@staticmethod
def any_image(field):
"""
Sets the user's vote.
Find out if any answer has an image, since it affects layout.
"""
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
return any(value['img'] for value in dict(field).values())
self.clean_tally()
self.choice = choice
self.tally[choice] = self.tally.get(choice, 0) + 1
@staticmethod
def gather_items(data, result, noun, field, image=True):
"""
Gathers a set of label-img pairs from a data dict and puts them in order.
"""
items = []
if field not in data or not isinstance(data[field], list):
source_items = []
result['success'] = False
result['errors'].append(
"'{0}' is not present, or not a JSON array.".format(field))
else:
source_items = data[field]
result['success'] = True
# Make sure all components are present and clean them.
for item in source_items:
if not isinstance(item, dict):
result['success'] = False
result['errors'].append(
"{0} {1} not a javascript object!".format(noun, item))
continue
key = item.get('key', '').strip()
if not key:
result['success'] = False
result['errors'].append(
"{0} {1} contains no key.".format(noun, item))
image_link = item.get('img', '').strip()
label = item.get('label', '').strip()
if not label:
if image and not image_link:
result['success'] = False
result['errors'].append(
"{0} has no text or img. Please make sure all {0}s "
"have one or the other, or both.".format(noun))
elif not image:
result['success'] = False
# If there's a bug in the code or the user just forgot to relabel a question,
# votes could be accidentally lost if we assume the omission was an
# intended deletion.
result['errors'].append("{0} was added with no label. "
"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:
# Labels might have prefixed space for markdown, though it's unlikely.
items.append((key, {'label': label, 'img': image_link.strip()}))
else:
items.append([key, label])
self.send_vote_event({'choice': self.choice})
if not len(items) > 1:
result['errors'].append(
"You must include at least two {0}s.".format(noun.lower()))
result['success'] = False
return result
return items
class PollBlock(PollBase):
......@@ -145,12 +155,6 @@ class PollBlock(PollBase):
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):
"""
Return a detailed dictionary from the stored tally that the
......@@ -162,7 +166,7 @@ class PollBlock(PollBase):
total = 0
self.clean_tally()
source_tally = self.tally
any_img = self.any_image()
any_img = self.any_image(self.answers)
for key, value in answers.items():
count = int(source_tally[key])
tally.append({
......@@ -225,7 +229,7 @@ class PollBlock(PollBase):
# Mustache is treating an empty string as true.
'feedback': markdown(self.feedback) or False,
'js_template': js_template,
'any_img': self.any_image(),
'any_img': self.any_image(self.answers),
# The SDK doesn't set url_name.
'url_name': getattr(self, 'url_name', ''),
"display_name": self.display_name,
......@@ -246,6 +250,7 @@ class PollBlock(PollBase):
js_template = self.resource_string('/public/handlebars/poll_studio.handlebars')
context.update({
'question': self.question,
'display_name': self.display_name,
'feedback': self.feedback,
'js_template': js_template
})
......@@ -254,57 +259,79 @@ class PollBlock(PollBase):
"/public/css/poll_edit.css", "public/js/poll_edit.js", "PollEdit")
@XBlock.json_handler
def load_answers(self, data, suffix=''):
return {
'items': [
{
'key': key, 'text': value['label'], 'img': value['img'],
'noun': 'answer', 'image': True,
}
for key, value in self.answers
],
}
@XBlock.json_handler
def get_results(self, data, suffix=''):
self.publish_event_from_dict(self.event_namespace + '.view_results', {})
detail, total = self.tally_detail()
return {
'question': markdown(self.question), 'tally': detail,
'total': total, 'feedback': markdown(self.feedback),
'plural': total > 1, 'display_name': self.display_name,
}
@XBlock.json_handler
def vote(self, data, suffix=''):
"""
Sets the user's vote.
"""
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
result['success'] = True
self.send_vote_event({'choice': self.choice})
return result
@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()
feedback = data.get('feedback', '').strip()
display_name = data.get('display_name', '').strip()
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']
# 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))
img = answer.get('img', '').strip()
label = answer.get('label', '').strip()
if not (img or label):
result['success'] = False
result['errors'].append(
"Answer {0} has no text or img. One is needed.".format(answer))
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
answers = self.gather_items(data, result, 'Answer', 'answers')
self.answers = answers
self.question = question
self.feedback = feedback
self.display_name = display_name
# Tally will not be updated until the next attempt to use it, per
# scoping limitations.
......@@ -379,6 +406,7 @@ class SurveyBlock(PollBase):
'answers': self.answers,
'js_template': js_template,
'questions': self.questions,
'any_img': self.any_image(self.questions),
# Mustache is treating an empty string as true.
'feedback': markdown(self.feedback) or False,
# The SDK doesn't set url_name.
......@@ -396,13 +424,14 @@ class SurveyBlock(PollBase):
js_template = self.resource_string('/public/handlebars/poll_studio.handlebars')
context.update({
'question': self.question,
'feedback': self.feedback,
'js_template': js_template
'display_name': self.display_name,
'js_template': js_template,
'multiquestion': True,
})
return self.create_fragment(
context, "public/html/poll_edit.html",
"/public/css/poll_edit.css", "public/js/poll_edit.js", "SurveyEditBlock")
"/public/css/poll_edit.css", "public/js/poll_edit.js", "SurveyEdit")
def tally_detail(self):
"""
......@@ -569,6 +598,30 @@ class SurveyBlock(PollBase):
return result
@XBlock.json_handler
def studio_submit(self, data, suffix=''):
# I wonder if there's something for live validation feedback already.
result = {'success': True, 'errors': []}
feedback = data.get('feedback', '').strip()
display_name = data.get('display_name', '').strip()
answers = self.gather_items(data, result, 'Answer', 'answers', image=False)
questions = self.gather_items(data, result, 'Question', 'questions')
if not result['success']:
return result
self.answers = answers
self.questions = questions
self.feedback = feedback
self.display_name = display_name
# Tally will not be updated until the next attempt to use it, per
# scoping limitations.
return result
@staticmethod
def workbench_scenarios():
"""
......
......@@ -78,10 +78,18 @@ li.poll-result .poll-image {
margin-left: 0;
}
.poll-image img{
.poll-image-td {
width: 25%;
}
.poll-image img, .poll-image-td img{
width: 100%;
}
.poll-image-td{
display: inline-block;
}
.poll-percent-container {
display: table-cell;
text-align: left;
......@@ -154,7 +162,9 @@ li.poll-result .poll-image {
th.survey-answer {
text-align: center;
width: 2em;
width: 7%;
line-height: 1em;
padding-bottom: .25em;
}
.poll-header {
......@@ -168,6 +178,7 @@ th.survey-answer {
.survey-question {
font-weight: bold;
vertical-align: middle;
}
.survey-choice {
......
<script id="answer-form-component" type="text/html">
<script id="poll-form-component" type="text/html">
{{#each items}}
<li class="field comp-setting-entry is-set poll-{{noun}}-studio-item">
<div class="wrapper-comp-setting">
......
......@@ -11,6 +11,12 @@
</thead>
{{#each tally}}
<tr class="survey-row">
{{#if img}}
<div class="poll-image-td">
<img src="{{img}}" />
</div>
{{/if}}
{% endif %}
<td class="survey-question">{{{text}}}</td>
{{#each answers}}
<td class="survey-percentage survey-option{{#if choice}} survey-choice{{/if}}{{#if top}} poll-top-choice{{/if}}">{{percent}}%</td>
......
......@@ -3,13 +3,21 @@
<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.</span>
<div class="wrapper-comp-setting">
<label class="label setting-label poll-setting-label" for="display_name">Display Name</label>
<input class="input setting-input" name="display_name" id="poll-display-name" value="{{ display_name }}" type="text" />
</div>
</li>
{% if not multiquestion %}
<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.</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.
......@@ -29,15 +37,29 @@
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>
{% if multiquestion %}
<p>
Questions must be similarly cared for. If a question's text is changed, any votes for that question will remain.
If a question is deleted, any student who previously took the survey will be permitted to retake it, but will not
lose course progress.
</p>
{% endif %}
</li>
<li id="poll-answer-marker"></li>
<li id="poll-answer-end-marker">
<li id="poll-answer-end-marker"></li>
<li id="poll-question-marker"></li>
<li id="poll-question-end-marker"></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>
<a href="#" class="button action-button" class="poll-add-item-link" onclick="return false;">Add Answer</a>
</li>
{% if multiquestion %}
<li class="action-item" id="poll-add-question">
<a href="#" class="button action-button" class="poll-add-item-link" onclick="return false;">Add Question</a>
</li>
{% endif %}
<li class="action-item">
<input id="poll-submit-options" type="submit" class="button action-primary save-button" value="Save" onclick="return false;" />
</li>
......
......@@ -16,6 +16,11 @@
{% for key, question in questions %}
<tr class="survey-row">
<td class="survey-question">
{% if question.img %}
<div class="poll-image-td">
<img src="{{question.img}}" />
</div>
{% endif %}
{{question.label}}
</td>
{% for answer, answer_details in answers %}
......
......@@ -2,49 +2,60 @@
function PollEditUtil(runtime, element, pollType) {
var self = this;
// These URLs aren't validated in real time, so even if they don't exist for a type of block
// we can create a reference to them.
this.loadAnswers = runtime.handlerUrl(element, 'load_answers');
this.loadQuestions = runtime.handlerUrl(element, 'load_questions');
this.init = function () {
// Set up the editing form for a Poll or Survey.
self.loadAnswers = runtime.handlerUrl(element, 'load_answers');
var temp = $('#answer-form-component', element).html();
var temp = $('#poll-form-component', element).html();
self.answerTemplate = Handlebars.compile(temp);
$(element).find('.cancel-button', element).bind('click', function() {
runtime.notify('cancel', {});
});
var mapping = self.mappings[pollType]['buttons'];
for (var key in mapping) {
if (mapping.hasOwnProperty(key)) {
var button_mapping = self.mappings[pollType]['buttons'];
for (var key in button_mapping) {
if (button_mapping.hasOwnProperty(key)) {
$(key, element).click(
// The nature of the closure forces us to make a custom function here.
function (context_key, topMarker, bottomMarker) {
function (context_key) {
return function () {
// The degree of precision on date should be precise enough to avoid
// collisions in the real world.
var bottom = $(bottomMarker);
$(self.answerTemplate(mapping[context_key]['itemList'])).before(bottom);
var new_item = bottom.prev();
var bottom = $(button_mapping[context_key]['bottomMarker']);
var new_item = $(self.answerTemplate(button_mapping[context_key]['itemList']));
bottom.before(new_item);
self.empowerDeletes(new_item);
self.empowerArrows(
new_item, mapping[context_key]['topMarker'],
mapping[context_key]['bottomMarker']
new_item, button_mapping[context_key]['topMarker'],
button_mapping[context_key]['bottomMarker']
);
new_item.fadeOut(250).fadeIn(250);
}
}(key, self.mappings[pollType])
}(key)
)
}
}
$(element).find('.save-button', element).bind('click', self.pollSubmitHandler);
$(function ($) {
$.ajax({
type: "POST",
url: self.loadAnswers,
data: JSON.stringify({}),
success: self.displayAnswers
});
});
var mapping = self.mappings[pollType]['onLoad'];
for (var task in mapping) {
function load (taskItem){
$(function ($) {
$.ajax({
type: "POST",
url: taskItem['url'],
data: JSON.stringify({}),
success: taskItem['function']
});
});
}
load(mapping[task]);
}
};
this.extend = function (obj1, obj2) {
......@@ -65,32 +76,6 @@ function PollEditUtil(runtime, element, pollType) {
return self.extend({'key': new Date().getTime(), 'text': '', 'img': ''}, extra)
};
// This object is used to swap out values which differ between Survey and Poll blocks.
this.mappings = {
'poll': {
'buttons': {
'#poll-add-answer': {
'itemList': {'items': [self.makeNew({'image': true, 'noun': 'answer'})]},
'topMarker': '#poll-answer-marker', 'bottomMarker': '#poll-answer-end-marker'
}
},
'onLoad': {
}
},
'survey': {
'buttons': {
'#poll-add-answer': {
'itemList': {'items': [self.makeNew({'image': false, 'noun': 'answer'})]},
'topMarker': '#poll-answer-marker', 'bottomMarker': '#poll-answer-end-marker'
},
'#poll-add-question': {
'itemList': {'items': [self.makeNew({'image': true, 'noun': 'question'})]}
}
}
}
};
this.empowerDeletes = function (scope) {
// Activates the delete buttons on rendered line items.
$('.poll-delete-answer', scope).click(function () {
......@@ -122,9 +107,44 @@ function PollEditUtil(runtime, element, pollType) {
self.displayItems(data, '#poll-answer-marker', '#poll-answer-end-marker')
};
this.displayQuestions = function (data) {
self.displayItems(data, "#poll-question-marker", '#poll-question-end-marker')
};
// This object is used to swap out values which differ between Survey and Poll blocks.
this.mappings = {
'poll': {
'buttons': {
'#poll-add-answer': {
'itemList': {'items': [self.makeNew({'image': true, 'noun': 'answer'})]},
'topMarker': '#poll-answer-marker', 'bottomMarker': '#poll-answer-end-marker'
}
},
'onLoad': [{'url': self.loadAnswers, 'function': self.displayAnswers}],
'gather': [{'prefix': 'answer', 'field': 'answers'}]
},
'survey': {
'buttons': {
'#poll-add-answer': {
'itemList': {'items': [self.makeNew({'image': false, 'noun': 'answer'})]},
'topMarker': '#poll-answer-marker', 'bottomMarker': '#poll-answer-end-marker'
},
'#poll-add-question': {
'itemList': {'items': [self.makeNew({'image': true, 'noun': 'question'})]},
'topMarker': '#poll-question-marker', 'bottomMarker': '#poll-question-end-marker'
}
},
'onLoad': [
{'url': self.loadQuestions, 'function': self.displayQuestions},
{'url': self.loadAnswers, 'function': self.displayAnswers}
],
'gather': [{'prefix': 'answer', 'field': 'answers'}, {'prefix': 'question', 'field': 'questions'}]
}
};
this.displayItems = function(data, topMarker, bottomMarker) {
// Loads the initial set of items that the block needs to edit.
$('#poll-answer-end-marker').before(self.answerTemplate(data));
$(bottomMarker).before(self.answerTemplate(data));
self.empowerDeletes(element, topMarker, bottomMarker);
self.empowerArrows(element, topMarker, bottomMarker);
};
......@@ -142,31 +162,44 @@ function PollEditUtil(runtime, element, pollType) {
alert(data['errors'].join('\n'));
};
this.pollSubmitHandler = function() {
this.gather = function (scope, tracker, data, prefix, field) {
var key = 'label';
var name = scope.name.replace(prefix + '-', '');
if (scope.name.indexOf('img-') == 0){
name = name.replace('img-', '');
key = 'img'
}
if (! (scope.name.indexOf(prefix + '-') >= 0)) {
return
}
if (tracker.indexOf(name) == -1){
tracker.push(name);
data[field].push({'key': name})
}
var index = tracker.indexOf(name);
console.log(data[field]);
console.log(index);
data[field][index][key] = scope.value;
return true
};
this.pollSubmitHandler = function () {
// Take all of the fields, serialize them, and pass them to the
// server for saving.
var handlerUrl = runtime.handlerUrl(element, 'studio_submit');
var data = {'answers': []};
var tracker = [];
$('#poll-form input', element).each(function() {
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();
var data = {};
var tracker;
var gatherings = self.mappings[pollType]['gather'];
for (var gathering in gatherings) {
tracker = [];
var field = gatherings[gathering]['field'];
var prefix = gatherings[gathering]['prefix'];
data[field] = [];
$('#poll-form input', element).each(function () {
self.gather(this, tracker, data, prefix, field)
});
}
data['display_name'] = $('#poll-display-name', element).val();
data['question'] = $('#poll-question-editor', element).val();
data['feedback'] = $('#poll-feedback-editor', element).val();
......
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