Commit 21ab57d0 by Jonathan Piacenti

Several bits of refactoring while preparing for Survey studio view.

parent 9ac33a23
...@@ -57,12 +57,13 @@ class PollBase(XBlock, ResourceMixin, PublishEventMixin): ...@@ -57,12 +57,13 @@ class PollBase(XBlock, ResourceMixin, PublishEventMixin):
@XBlock.json_handler @XBlock.json_handler
def load_answers(self, data, suffix=''): def load_answers(self, data, suffix=''):
return { return {
'answers': [ 'items': [
{ {
'key': key, 'text': value['label'], 'img': value['img'] 'key': key, 'text': value['label'], 'img': value['img'],
'noun': 'answer', 'image': True,
} }
for key, value in self.answers for key, value in self.answers
] ],
} }
@XBlock.json_handler @XBlock.json_handler
...@@ -290,7 +291,7 @@ class PollBlock(PollBase): ...@@ -290,7 +291,7 @@ class PollBlock(PollBase):
if not (img or label): if not (img or label):
result['success'] = False result['success'] = False
result['errors'].append( result['errors'].append(
"Answer {0} has no text or img. One is needed.") "Answer {0} has no text or img. One is needed.".format(answer))
answers.append((key, {'label': label, 'img': img})) answers.append((key, {'label': label, 'img': img}))
if not len(answers) > 1: if not len(answers) > 1:
...@@ -338,15 +339,15 @@ class SurveyBlock(PollBase): ...@@ -338,15 +339,15 @@ class SurveyBlock(PollBase):
display_name = String(default='Survey') display_name = String(default='Survey')
answers = List( answers = List(
default=( default=(
('Y', {'label': 'Yes', 'img': None}), ('N', {'label': 'No', 'img': None}), ('Y', 'Yes'), ('N', 'No'),
('M', {'label': 'Maybe', 'img': None})), ('M', 'Maybe')),
scope=Scope.settings, help="Answer choices for this Survey" scope=Scope.settings, help="Answer choices for this Survey"
) )
questions = List( questions = List(
default=( default=(
('enjoy', 'Are you enjoying the course?'), ('enjoy', {'label': 'Are you enjoying the course?', 'img': None}),
('recommend', 'Would you recommend this course to your friends?'), ('recommend', {'label': 'Would you recommend this course to your friends?', 'img': None}),
('learn', 'Do you think you will learn a lot?') ('learn', {'label': 'Do you think you will learn a lot?', 'img': None})
), ),
scope=Scope.settings, help="Questions for this Survey" scope=Scope.settings, help="Questions for this Survey"
) )
...@@ -426,7 +427,7 @@ class SurveyBlock(PollBase): ...@@ -426,7 +427,7 @@ class SurveyBlock(PollBase):
answer_set = OrderedDict(default_answers) answer_set = OrderedDict(default_answers)
answer_set.update(source_tally[key]) answer_set.update(source_tally[key])
tally.append({ tally.append({
'text': value, 'text': value['label'],
'answers': [ 'answers': [
{ {
'count': count, 'choice': False, 'count': count, 'choice': False,
...@@ -500,12 +501,36 @@ class SurveyBlock(PollBase): ...@@ -500,12 +501,36 @@ class SurveyBlock(PollBase):
detail, total = self.tally_detail() detail, total = self.tally_detail()
return { return {
'answers': [ 'answers': [
value['label'] for value in OrderedDict(self.answers).values()], value for value in OrderedDict(self.answers).values()],
'tally': detail, 'total': total, 'feedback': markdown(self.feedback), 'tally': detail, 'total': total, 'feedback': markdown(self.feedback),
'plural': total > 1, 'display_name': self.display_name, 'plural': total > 1, 'display_name': self.display_name,
} }
@XBlock.json_handler @XBlock.json_handler
def load_answers(self, data, suffix=''):
return {
'items': [
{
'key': key, 'text': value,
'noun': 'answer', 'image': False,
}
for key, value in self.answers
],
}
@XBlock.json_handler
def load_questions(self, data, suffix=''):
return {
'items': [
{
'key': key, 'text': value['label'], 'img': value['img'],
'noun': 'question', 'image': True,
}
for key, value in self.questions
]
}
@XBlock.json_handler
def vote(self, data, suffix=''): def vote(self, data, suffix=''):
questions = dict(self.questions) questions = dict(self.questions)
answers = dict(self.answers) answers = dict(self.answers)
......
...@@ -37,4 +37,8 @@ ...@@ -37,4 +37,8 @@
.poll-move { .poll-move {
float: right; float: right;
} }
\ No newline at end of file
.poll-setting-label {
text-transform: capitalize;
}
<script id="answer-form-component" type="text/html"> <script id="answer-form-component" type="text/html">
{{#each answers}} {{#each items}}
<li class="field comp-setting-entry is-set"> <li class="field comp-setting-entry is-set poll-{{noun}}-studio-item">
<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 poll-setting-label" for="{{noun}}-{{key}}">{{noun}}</label>
<input class="input setting-input" name="answer-{{key}}" id="answer-{{key}}" value="{{text}}" type="text" /><br /> <input class="input setting-input" name="{{noun}}-{{key}}" id="{{noun}}-{{key}}" value="{{text}}" type="text" /><br />
<label class="label setting-label" for="img-answer-{{key}}">Image URL</label> {{#if image}}
<input class="input setting-input" name="img-answer-{{key}}" id="img-answer-{{key}}" value="{{img}}" type="text" /> <label class="label setting-label" for="img-{{noun}}-{{key}}">Image URL</label>
<input class="input setting-input" name="img-{{noun}}-{{key}}" id="img-{{noun}}-{{key}}" value="{{img}}" type="text" />
{{/if}}
<div class="poll-move"> <div class="poll-move">
<div class="poll-move-up">&#9650;</div> <div class="poll-move-up">&#9650;</div>
<div class="poll-move-down">&#9660;</div> <div class="poll-move-down">&#9660;</div>
</div> </div>
</div> </div>
<span class="tip setting-help"> <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. {{#if image}}This must have an image URL or text, and can have both.{{/if}}
</span> </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>
......
...@@ -30,6 +30,8 @@ ...@@ -30,6 +30,8 @@
may vote again, but will not lose course progress. may vote again, but will not lose course progress.
</p> </p>
</li> </li>
<li id="poll-answer-marker"></li>
<li id="poll-answer-end-marker">
</ul> </ul>
<div class="xblock-actions"> <div class="xblock-actions">
<ul> <ul>
......
...@@ -8,15 +8,15 @@ ...@@ -8,15 +8,15 @@
<thead> <thead>
<tr> <tr>
<th></th> <th></th>
{% for answer, details in answers %} {% for answer, label in answers %}
<th class="survey-answer">{{details.label}}</th> <th class="survey-answer">{{label}}</th>
{% endfor %} {% endfor %}
</tr> </tr>
</thead> </thead>
{% for key, question in questions %} {% for key, question in questions %}
<tr class="survey-row"> <tr class="survey-row">
<td class="survey-question"> <td class="survey-question">
{{question}} {{question.label}}
</td> </td>
{% for answer, answer_details in answers %} {% for answer, answer_details in answers %}
<td class="survey-option"> <td class="survey-option">
......
...@@ -9,7 +9,7 @@ function PollUtil (runtime, element, pollType) { ...@@ -9,7 +9,7 @@ function PollUtil (runtime, element, pollType) {
this.tallyURL = runtime.handlerUrl(element, 'get_results'); this.tallyURL = runtime.handlerUrl(element, 'get_results');
this.submit = $('input[type=button]', element); this.submit = $('input[type=button]', element);
this.answers = $('input[type=radio]', element); this.answers = $('input[type=radio]', element);
this.resultsTemplate = Handlebars.compile($("#" + self.pollType + "-results-template", element).html()); this.resultsTemplate = Handlebars.compile($("#" + pollType + "-results-template", element).html());
// If the submit button doesn't exist, the user has already // If the submit button doesn't exist, the user has already
// selected a choice. Render results instead of initializing machinery. // selected a choice. Render results instead of initializing machinery.
if (! self.submit.length) { if (! self.submit.length) {
...@@ -114,7 +114,6 @@ function PollUtil (runtime, element, pollType) { ...@@ -114,7 +114,6 @@ function PollUtil (runtime, element, pollType) {
self.answers.unbind("change.enableSubmit"); self.answers.unbind("change.enableSubmit");
}; };
this.pollType = pollType;
var run_init = this.init(); var run_init = this.init();
if (run_init) { if (run_init) {
var init_map = {'poll': self.pollInit, 'survey': self.surveyInit}; var init_map = {'poll': self.pollInit, 'survey': self.surveyInit};
......
function PollEditUtil(runtime, element) {
function PollEditUtil(runtime, element, pollType) {
var self = this; var self = this;
this.init = function () { this.init = function () {
// Set up the editing form for a Poll or Survey.
self.loadAnswers = runtime.handlerUrl(element, 'load_answers'); self.loadAnswers = runtime.handlerUrl(element, 'load_answers');
var temp = $('#answer-form-component', element).html(); var temp = $('#answer-form-component', element).html();
self.answerTemplate = Handlebars.compile(temp); self.answerTemplate = Handlebars.compile(temp);
self.pollLineItems =$('#poll-line-items', element);
$(element).find('.cancel-button', element).bind('click', function() { $(element).find('.cancel-button', element).bind('click', function() {
runtime.notify('cancel', {}); runtime.notify('cancel', {});
}); });
var mapping = self.mappings[pollType]['buttons'];
$('#poll-add-answer', element).click(function () { for (var key in mapping) {
// The degree of precision on date should be precise enough to avoid if (mapping.hasOwnProperty(key)) {
// collisions in the real world. $(key, element).click(
self.pollLineItems.append(self.answerTemplate({'answers': [{'key': new Date().getTime(), 'text': ''}]})); // The nature of the closure forces us to make a custom function here.
var new_answer = $(self.pollLineItems.children().last()); function (context_key, topMarker, bottomMarker) {
self.empowerDeletes(new_answer); return function () {
self.empowerArrows(new_answer); // The degree of precision on date should be precise enough to avoid
new_answer.fadeOut(250).fadeIn(250); // collisions in the real world.
}); var bottom = $(bottomMarker);
$(self.answerTemplate(mapping[context_key]['itemList'])).before(bottom);
var new_item = bottom.prev();
self.empowerDeletes(new_item);
self.empowerArrows(
new_item, mapping[context_key]['topMarker'],
mapping[context_key]['bottomMarker']
);
new_item.fadeOut(250).fadeIn(250);
}
}(key, self.mappings[pollType])
)
}
}
$(element).find('.save-button', element).bind('click', self.pollSubmitHandler); $(element).find('.save-button', element).bind('click', self.pollSubmitHandler);
...@@ -33,23 +47,62 @@ function PollEditUtil(runtime, element) { ...@@ -33,23 +47,62 @@ function PollEditUtil(runtime, element) {
}); });
}; };
this.extend = function (obj1, obj2) {
// Mimics similar extend functions, making obj1 contain obj2's properties.
for (var attrname in obj2) {
if (obj2.hasOwnProperty(attrname)) {
obj1[attrname] = obj2[attrname]
}
}
return obj1;
};
this.makeNew = function(extra){
// Make a new empty line item, like a question or an answer.
// 'extra' should contain 'image', a boolean value that determines whether
// an image path field should be provided, and 'noun', which should be either
// 'question' or 'answer' depending on what is needed.
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) { this.empowerDeletes = function (scope) {
// Activates the delete buttons on rendered line items.
$('.poll-delete-answer', scope).click(function () { $('.poll-delete-answer', scope).click(function () {
$(this).parent().remove(); $(this).parent().remove();
}); });
}; };
this.empowerArrows = function(scope) { this.empowerArrows = function(scope, topMarker, bottomMarker) {
/* // Activates the arrows on rendered line items.
The poll answers need to be reorderable. As the UL they are in is not
easily isolated, we need to start checking their position to make
sure they aren't ordered above the other settings, which are also
in the list.
*/
var starting_point = 3;
$('.poll-move-up', scope).click(function () { $('.poll-move-up', scope).click(function () {
var tag = $(this).parents('li'); var tag = $(this).parents('li');
if (tag.index() <= starting_point){ if (tag.index() <= ($(topMarker).index() + 1)){
return; return;
} }
tag.prev().before(tag); tag.prev().before(tag);
...@@ -57,7 +110,7 @@ function PollEditUtil(runtime, element) { ...@@ -57,7 +110,7 @@ function PollEditUtil(runtime, element) {
}); });
$('.poll-move-down', scope).click(function () { $('.poll-move-down', scope).click(function () {
var tag = $(this).parents('li'); var tag = $(this).parents('li');
if ((tag.index() >= (tag.parent().children().length - 1))) { if ((tag.index() >= ($(bottomMarker).index() - 1))) {
return; return;
} }
tag.next().after(tag); tag.next().after(tag);
...@@ -65,13 +118,23 @@ function PollEditUtil(runtime, element) { ...@@ -65,13 +118,23 @@ function PollEditUtil(runtime, element) {
}); });
}; };
this.displayAnswers = function(data) { this.displayAnswers = function (data){
self.pollLineItems.append(self.answerTemplate(data)); self.displayItems(data, '#poll-answer-marker', '#poll-answer-end-marker')
self.empowerDeletes(element); };
self.empowerArrows(element);
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));
self.empowerDeletes(element, topMarker, bottomMarker);
self.empowerArrows(element, topMarker, bottomMarker);
}; };
this.check_return = function(data) { this.check_return = function(data) {
// Handle the return value JSON from the server.
// It would be better if we could have a different function
// for errors, as AJAX calls normally allow, but our version of XBlock
// does not support status codes other than 200 for JSON encoded
// responses.
if (data['success']) { if (data['success']) {
window.location.reload(false); window.location.reload(false);
return; return;
...@@ -80,10 +143,12 @@ function PollEditUtil(runtime, element) { ...@@ -80,10 +143,12 @@ function PollEditUtil(runtime, element) {
}; };
this.pollSubmitHandler = function() { 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 handlerUrl = runtime.handlerUrl(element, 'studio_submit');
var data = {'answers': []}; var data = {'answers': []};
var tracker = []; var tracker = [];
$('#poll-form input', element).each(function(i) { $('#poll-form input', element).each(function() {
var key = 'label'; var key = 'label';
if (this.name.indexOf('answer-') >= 0){ if (this.name.indexOf('answer-') >= 0){
var name = this.name.replace('answer-', ''); var name = this.name.replace('answer-', '');
...@@ -117,9 +182,9 @@ function PollEditUtil(runtime, element) { ...@@ -117,9 +182,9 @@ function PollEditUtil(runtime, element) {
} }
function PollEdit(runtime, element) { function PollEdit(runtime, element) {
new PollEditUtil(runtime, element); new PollEditUtil(runtime, element, 'poll');
} }
function SurveyEdit(runtime, element) { function SurveyEdit(runtime, element) {
new PollEditUtil(runtime, element); new PollEditUtil(runtime, element, 'survey');
} }
<vertical_demo> <vertical_demo>
<survey tally='{"q1": {"sa": 5, "a": 5, "n": 3, "d": 2, "sd": 5}, "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}}' <survey tally='{"q1": {"sa": 5, "a": 5, "n": 3, "d": 2, "sd": 5}, "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", "I feel like this test will pass."], ["q2", "I like testing software"], ["q3", "Testing is not necessary"], ["q4", "I would fake a test result to get software deployed."]]' 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}]]'
answers='[["sa", {"label": "Strongly Agree"}], ["a", {"label": "Agree"}], ["n", {"label": "Neutral"}], ["d", {"label": "Disagree"}], ["sd", {"label": "Strongly Disagree"}]]' answers='[["sa", "Strongly Agree"], ["a", "Agree"], ["n", "Neutral"], ["d", "Disagree"], ["sd", "Strongly Disagree"]]'
feedback="### Thank you&#10;&#10;for running the tests."/> feedback="### Thank you&#10;&#10;for running the tests."/>
</vertical_demo> </vertical_demo>
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