Commit aee05410 by Jonathan Piacenti

Better handling of JSON.

parent 7f5affef
...@@ -12,6 +12,22 @@ from xblockutils.resources import ResourceLoader ...@@ -12,6 +12,22 @@ from xblockutils.resources import ResourceLoader
from .utils import process_markdown from .utils import process_markdown
# When changing these constants, check the templates as well for places
# where the user is informed about them.
MAX_PARAGRAPH_LEN = 5000
MAX_URL_LEN = 1000
MAX_ANSWER_LEN = 250
# These two don't have mentions in the templates, but will cause error
# messages.
MAX_ANSWERS = 25
MAX_KEY_LEN = 100
class PollBlock(XBlock): class PollBlock(XBlock):
""" """
...@@ -195,53 +211,51 @@ class PollBlock(XBlock): ...@@ -195,53 +211,51 @@ class PollBlock(XBlock):
@XBlock.json_handler @XBlock.json_handler
def studio_submit(self, data, suffix=''): def studio_submit(self, data, suffix=''):
# I wonder if there's something for live validation feedback already. # I wonder if there's something for live validation feedback already.
result = {'success': True, 'errors': []} result = {'success': True, 'errors': []}
if 'question' not in data or not data['question']: question = data.get('question', '').strip()[:MAX_PARAGRAPH_LEN]
feedback = data.get('feedback', '').strip()[:MAX_PARAGRAPH_LEN]
if not question:
result['errors'].append("You must specify a question.") result['errors'].append("You must specify a question.")
result['success'] = False 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: else:
question = data['question'][:4096] source_answers = data['answers']
if 'feedback' not in data or not data['feedback'].strip():
feedback = ''
else:
feedback = data['feedback'][:4096]
# 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', [])
]
# Aggressively clean/sanity check answers list.
answers = {}
for key, value in data.items():
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-', '').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[key] = update_dict
for value in answers.values(): # Set a reasonable limit to the number of answers in a poll.
if 'label' not in value: if len(source_answers) > MAX_ANSWERS:
value['label'] = None result['success'] = False
if 'img' not in value: result['errors'].append("")
value['img'] = None
# 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))
if len(key) > MAX_KEY_LEN:
result['success'] = False
result['errors'].append("Key '{0}' too long.".format(key))
img = answer.get('img', '').strip()[:MAX_URL_LEN]
label = answer.get('label', '').strip()[:MAX_ANSWER_LEN]
if not (img or label):
result['success'] = False
result['errors'].append(
"Answer {0} has no text or img. One is needed.")
answers.append((key, {'label': label, 'img': img}))
if not len(answers) > 1: if not len(answers) > 1:
result['errors'].append( result['errors'].append(
...@@ -251,10 +265,6 @@ class PollBlock(XBlock): ...@@ -251,10 +265,6 @@ class PollBlock(XBlock):
if not result['success']: if not result['success']:
return result return result
# Need to sort the answers.
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
......
...@@ -11,8 +11,11 @@ ...@@ -11,8 +11,11 @@
<div class="poll-move-down">&#9660;</div> <div class="poll-move-down">&#9660;</div>
</div> </div>
</div> </div>
<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> <span class="tip setting-help">
<a href="#" class="button action-button poll-delete-answer">Delete</a> Enter an answer for the user to select. An answer must have an image URL or text, and can have both.
(Text truncated at 250 characters, Image URL at 1000)
</span>
<a href="#" class="button action-button poll-delete-answer" onclick="return false;">Delete</a>
</li> </li>
{{/each}} {{/each}}
</script> </script>
\ No newline at end of file
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<div id="poll-question-editor-container"> <div id="poll-question-editor-container">
<textarea class="input setting-input" name="question" id="poll-question-editor">{{question}}</textarea> <textarea class="input setting-input" name="question" id="poll-question-editor">{{question}}</textarea>
</div> </div>
<span class="tip setting-help">Enter the prompt for the user.</span> <span class="tip setting-help">Enter the prompt for the user. (Truncated after 5000 characters)</span>
</li> </li>
<li class="field comp-setting-entry is-set"> <li class="field comp-setting-entry is-set">
<h2><label for="poll-feedback-editor">Feedback</label></h2> <h2><label for="poll-feedback-editor">Feedback</label></h2>
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
</div> </div>
<span class="tip setting-help"> <span class="tip setting-help">
This text will be displayed for the user as some extra feedback after they have This text will be displayed for the user as some extra feedback after they have
submitted their response to the poll. submitted their response to the poll. (Truncated after 5000 characters)
</span> </span>
</li> </li>
<li class="field comp-setting-entry is-set"> <li class="field comp-setting-entry is-set">
...@@ -34,10 +34,10 @@ ...@@ -34,10 +34,10 @@
<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" class="poll-add-answer-link">Add Answer</a> <a href="#" class="button action-button" class="poll-add-answer-link" 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" /> <input id="poll-submit-options" type="submit" class="button action-primary save-button" value="Save" onclick="return false;" />
</li> </li>
<li class="action-item"> <li class="action-item">
<a href="#" class="button cancel-button">Cancel</a> <a href="#" class="button cancel-button">Cancel</a>
......
...@@ -52,32 +52,35 @@ function PollEditBlock(runtime, element) { ...@@ -52,32 +52,35 @@ function PollEditBlock(runtime, element) {
new_answer.fadeOut(250).fadeIn(250); new_answer.fadeOut(250).fadeIn(250);
}); });
var to_disable = ['#poll-add-answer-link', 'input[type=submit', '.poll-delete-answer'];
for (var selector in to_disable) {
$(selector, element).click(function(event) {
event.preventDefault();
}
)
}
$(element).find('.cancel-button', element).bind('click', function() { $(element).find('.cancel-button', element).bind('click', function() {
runtime.notify('cancel', {}); runtime.notify('cancel', {});
}); });
$(element).find('.save-button', element).bind('click', function() { $(element).find('.save-button', element).bind('click', function() {
var handlerUrl = runtime.handlerUrl(element, 'studio_submit'); var handlerUrl = runtime.handlerUrl(element, 'studio_submit');
var data = {}; var data = {'answers': []};
var poll_order = []; var tracker = [];
$('#poll-form input', element).each(function(i) { $('#poll-form input', element).each(function(i) {
data[this.name] = this.value; var key = 'label';
if (this.name.indexOf('answer-') == 0){ if (this.name.indexOf('answer-') >= 0){
poll_order.push(this.name); 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(); data['title'] = $('#poll-title', element).val();
data['question'] = $('#poll-question-editor', element).val(); data['question'] = $('#poll-question-editor', element).val();
data['feedback'] = $('#poll-feedback-editor', element).val(); data['feedback'] = $('#poll-feedback-editor', element).val();
data['poll_order'] = poll_order;
function check_return(data) { function check_return(data) {
if (data['success']) { if (data['success']) {
window.location.reload(false); window.location.reload(false);
......
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