Commit 108358bf by Jonathan Piacenti

Polished up backend. Markdown needs extraction. Frontend needs further CSS.

parent f56c62d4
"""TO-DO: Write a description of what this XBlock is."""
from collections import OrderedDict
from django.template import Template, Context
from bleach import clean
from markdown import markdown
import pkg_resources
from xblock.core import XBlock
from xblock.fields import Scope, List, Integer, String, Dict
from xblock.fields import Scope, List, String, Dict, UserScope, BlockScope
from xblock.fragment import Fragment
......@@ -21,12 +24,8 @@ class PollBlock(XBlock):
scope=Scope.settings, help="The questions on this poll."
)
tally = Dict(default={'Red': 0, 'Blue': 0, 'Green': 0, 'Other': 0},
scope=Scope.user_state_summary,
scope=Scope.content,
help="Total tally of answers from students.")
# No default. Hopefully this will yield 'None', or do something
# distinctive when queried.
# Choices are always one above their place in the index so that choice
# is never false if it's provided.
choice = String(scope=Scope.user_state, help="The student's answer")
def resource_string(self, path):
......@@ -36,7 +35,7 @@ class PollBlock(XBlock):
@XBlock.json_handler
def get_results(self, data, suffix=''):
return {'question': self.question, 'tally': self.tally_detail()}
return {'question': markdown(clean(self.question)), 'tally': self.tally_detail()}
def tally_detail(self):
"""
......@@ -44,13 +43,15 @@ class PollBlock(XBlock):
"""
tally = []
answers = OrderedDict(self.answers)
choice = self.get_choice()
total = 0
for key, value in answers.items():
tally.append({
'count': int(self.tally.get(key, 0)),
'answer': value,
'key': key,
'top': False
'top': False,
'choice': False
})
total += tally[-1]['count']
......@@ -58,6 +59,8 @@ class PollBlock(XBlock):
try:
percent = (answer['count'] / float(total))
answer['percent'] = int(percent * 100)
if answer['key'] == choice:
answer['choice'] = True
except ZeroDivisionError:
answer['percent'] = 0
......@@ -69,6 +72,17 @@ class PollBlock(XBlock):
tally[0]['top'] = True
return tally
def get_choice(self):
"""
It's possible for the choice to have been removed since
the student answered the poll. We don't want to take away
the user's progress, but they should be able to vote again.
"""
if self.choice and self.choice in OrderedDict(self.answers):
return self.choice
else:
return None
# TO-DO: change this view to display your data your own way.
def student_view(self, context=None):
"""
......@@ -81,11 +95,13 @@ class PollBlock(XBlock):
js_template = self.resource_string(
'/public/handlebars/results.handlebars')
choice = self.get_choice()
context.update({
'choice': self.choice,
'choice': choice,
# Offset so choices will always be True.
'answers': self.answers,
'question': self.question,
'question': markdown(clean(self.question)),
'js_template': js_template,
})
......@@ -116,7 +132,7 @@ class PollBlock(XBlock):
js_template = self.resource_string('/public/handlebars/studio.handlebars')
context.update({
'question': self.question,
'question': clean(self.question),
'js_template': js_template
})
context = Context(context)
......@@ -126,6 +142,15 @@ class PollBlock(XBlock):
frag.add_javascript_url(
self.runtime.local_resource_url(
self, 'public/js/vendor/handlebars.js'))
frag.add_javascript_url(
self.runtime.local_resource_url(
self, 'public/js/vendor/pen.js'))
frag.add_javascript_url(
self.runtime.local_resource_url(
self, 'public/js/vendor/markdown.js'))
frag.add_css_url(
self.runtime.local_resource_url(
self, 'public/css/vendor/pen.css'))
frag.add_css(self.resource_string('/public/css/poll_edit.css'))
frag.add_javascript(self.resource_string("public/js/poll_edit.js"))
frag.initialize_js('PollEditBlock')
......@@ -135,31 +160,49 @@ class PollBlock(XBlock):
@XBlock.json_handler
def studio_submit(self, data, suffix=''):
# I wonder if there's something for live validation feedback already.
result = {'success': True, 'errors': {'fields': {}, 'general': []}}
result = {'success': True, 'errors': []}
if 'question' not in data or not data['question']:
result['errors']['question'] = "This field is required."
result['errors'].append("You must specify a question.")
result['success'] = False
# 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 = OrderedDict(
(key.replace('answer-', '', 1), value.strip()[:250])
for key, value in data.items()
if (key.startswith('answer-') and not key == 'answer-')
and not value.isspace()
)
answers = []
for key, value in data.items():
if not key.startswith('answer-'):
continue
key = key.replace('answer-', '')
if not key or key.isspace():
continue
value = value.strip()[:250]
if not value or value.isspace():
continue
if key in poll_order:
answers.append((key, value))
if not len(answers) > 1:
result['errors']['general'].append(
result['errors'].append(
"You must include at least two answers.")
result['success'] = False
if not result['success']:
return result
self.answers = answers.items()
# Need to sort the answers.
answers.sort(key=lambda x: poll_order.index(x[0]), reverse=True)
self.answers = answers
self.question = data['question']
print answers
tally = self.tally
answers = OrderedDict(answers)
# Update tracking schema.
for key, value in answers.items():
if key not in tally:
......@@ -176,24 +219,28 @@ class PollBlock(XBlock):
"""
An example handler, which increments the data.
"""
result = {'success': False}
if self.choice is not None:
result['message'] = 'You cannot vote twice.'
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['message'] = 'Answer not included with request.'
result['errors'].append('Answer not included with request.')
return result
# Just to show data coming in...
try:
OrderedDict(self.answers)[choice]
except KeyError:
result['message'] = 'No key "{choice}" in answers table.'.format(choice=choice)
result['errors'].append('No key "{choice}" in answers table.'.format(choice=choice))
return result
self.choice = choice
self.tally[choice] += 1
tally = self.tally.copy()
tally[choice] = self.tally.get(choice, 0)
self.tally = tally
# Let the LMS know the user has answered the poll.
self.runtime.publish(self, 'progress', {})
result['success'] = True
return result
......
.poll-answer {
margin-left: 1em;
font-weight: bold;
}
\ No newline at end of file
......@@ -2,4 +2,25 @@
.poll-delete-answer {
float: right;
}
#question-editor-container{
width: 100%;
text-align: center;
}
#question-editor{
width: 98%;
height: 7em;
text-align: left;
color: #4C4C4C;
margin-top: 0.5em;
margin-bottom: 0.5em;
box-shadow: 0px 0px 9px #555 inset;
border: 1px solid #B2B2B2;
border-radius: 3px;
padding: 10px;
}
h2 label {
font-weight: bold;
font-size: 16pt;
}
\ No newline at end of file
/*! Licensed under MIT, https://github.com/sofish/pen */
/* basic reset */
.pen, .pen-menu, .pen-input, .pen textarea{font:400 1.16em/1.45 Palatino, Optima, Georgia, serif;color:#331;}
.pen:focus{outline:none;}
.pen fieldset, img {border: 0;}
.pen blockquote{padding-left:10px;margin-left:-14px;border-left:4px solid #1abf89;}
.pen a{color:#1abf89;}
.pen del{text-decoration:line-through;}
.pen sub, .pen sup {font-size:75%;position:relative;vertical-align:text-top;}
:root .pen sub, :root .pen sup{vertical-align:baseline; /* for ie9 and other mordern browsers */}
.pen sup {top:-0.5em;}
.pen sub {bottom:-0.25em;}
.pen hr{border:none;border-bottom:1px solid #cfcfcf;margin-bottom:25px;*color:pink;*filter:chroma(color=pink);height:10px;*margin:-7px 0 15px;}
.pen small{font-size:0.8em;color:#888;}
.pen em, .pen b, .pen strong{font-weight:700;}
.pen pre{white-space:pre-wrap;padding:0.85em;background:#f8f8f8;}
/* block-level element margin */
.pen p, .pen pre, .pen ul, .pen ol, .pen dl, .pen form, .pen table, .pen blockquote{margin-bottom:16px;}
/* headers */
.pen h1, .pen h2, .pen h3, .pen h4, .pen h5, .pen h6{margin-bottom:16px;font-weight:700;line-height:1.2;}
.pen h1{font-size:2em;}
.pen h2{font-size:1.8em;}
.pen h3{font-size:1.6em;}
.pen h4{font-size:1.4em;}
.pen h5, .pen h6{font-size:1.2em;}
/* list */
.pen ul, .pen ol{margin-left:1.2em;}
.pen ul, .pen-ul{list-style:disc;}
.pen ol, .pen-ol{list-style:decimal;}
.pen li ul, .pen li ol, .pen-ul ul, .pen-ul ol, .pen-ol ul, .pen-ol ol{margin:0 2em 0 1.2em;}
.pen li ul, .pen-ul ul, .pen-ol ul{list-style: circle;}
/* pen menu */
.pen-menu [class^="icon-"], .pen-menu [class*=" icon-"] { /* reset to avoid conflicts with Bootstrap */
background: transparent;
background-image: none;
}
.pen-menu, .pen-input{font-size:14px;line-height:1;}
.pen-menu{white-space:nowrap;box-shadow:1px 2px 3px -2px #222;background:#333;background-image:linear-gradient(to bottom, #222, #333);opacity:0.9;position:fixed;height:36px;border:1px solid #333;border-radius:3px;display:none;z-index:1000;}
.pen-menu:after {top:100%;border:solid transparent;content:" ";height:0;width:0;position:absolute;pointer-events:none;}
.pen-menu:after {border-color:rgba(51, 51, 51, 0);border-top-color:#333;border-width:6px;left:50%;margin-left:-6px;}
.pen-menu-below:after {top: -11px; display:block; -moz-transform: rotate(180deg); -webkit-transform: rotate(180deg); -ms-transform: rotate(180deg); -o-transform: rotate(180deg); transform: rotate(180deg);}
.pen-icon{font:normal 900 16px/40px Georgia serif;min-width:20px;display:inline-block;padding:0 10px;height:36px;overflow:hidden;color:#fff;text-align:center;cursor:pointer;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none;}
.pen-icon:first-of-type{border-top-left-radius:3px;border-bottom-left-radius:3px;}
.pen-icon:last-of-type{border-top-right-radius:3px;border-bottom-right-radius:3px;}
.pen-icon:hover{background:#000;}
.pen-icon.active{color:#1abf89;background:#000;box-shadow:inset 2px 2px 4px #000;}
.pen-input{position:absolute;width:100%;left:0;top:0;height:36px;line-height:20px;background:#333;color:#fff;border:none;text-align:center;display:none;font-family:arial, sans-serif;}
.pen-input:focus{outline:none;}
.pen-textarea{display:block;background:#f8f8f8;padding:20px;}
.pen textarea{font-size:14px;border:none;background:none;width:100%;_height:200px;min-height:200px;resize:none;}
@font-face {
font-family: 'pen';
src: url('font/fontello.eot?370dad08');
src: url('font/fontello.eot?370dad08#iefix') format('embedded-opentype'),
url('font/fontello.woff?370dad08') format('woff'),
url('font/fontello.ttf?370dad08') format('truetype'),
url('font/fontello.svg?370dad08#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
.pen-menu [class^="icon-"]:before, .pen-menu [class*=" icon-"]:before {
font-family: "pen";
font-style: normal;
font-weight: normal;
speak: none;
display: inline-block;
text-decoration: inherit;
width: 1em;
margin-right: .2em;
text-align: center;
font-variant: normal;
text-transform: none;
line-height: 1em;
margin-left: .2em;
}
.pen-menu .icon-location:before { content: '\e815'; } /* '' */
.pen-menu .icon-fit:before { content: '\e80f'; } /* '' */
.pen-menu .icon-bold:before { content: '\e805'; } /* '' */
.pen-menu .icon-italic:before { content: '\e806'; } /* '' */
.pen-menu .icon-justifyleft:before { content: '\e80a'; } /* '' */
.pen-menu .icon-justifycenter:before { content: '\e80b'; } /* '' */
.pen-menu .icon-justifyright:before { content: '\e80c'; } /* '' */
.pen-menu .icon-justifyfull:before { content: '\e80d'; } /* '' */
.pen-menu .icon-outdent:before { content: '\e800'; } /* '' */
.pen-menu .icon-indent:before { content: '\e801'; } /* '' */
.pen-menu .icon-mode:before { content: '\e813'; } /* '' */
.pen-menu .icon-fullscreen:before { content: '\e80e'; } /* '' */
.pen-menu .icon-insertunorderedlist:before { content: '\e802'; } /* '' */
.pen-menu .icon-insertorderedlist:before { content: '\e803'; } /* '' */
.pen-menu .icon-strikethrough:before { content: '\e807'; } /* '' */
.pen-menu .icon-underline:before { content: '\e804'; } /* '' */
.pen-menu .icon-blockquote:before { content: '\e814'; } /* '' */
.pen-menu .icon-undo:before { content: '\e817'; } /* '' */
.pen-menu .icon-code:before { content: '\e816'; } /* '' */
.pen-menu .icon-pre:before { content: '\e816'; } /* '' */
.pen-menu .icon-unlink:before { content: '\e811'; } /* '' */
.pen-menu .icon-superscript:before { content: '\e808'; } /* '' */
.pen-menu .icon-subscript:before { content: '\e809'; } /* '' */
.pen-menu .icon-inserthorizontalrule:before { content: '\e818'; } /* '' */
.pen-menu .icon-pin:before { content: '\e812'; } /* '' */
.pen-menu .icon-createlink:before { content: '\e810'; } /* '' */
.pen-menu .icon-h1:before { content: 'H1'; }
.pen-menu .icon-h2:before { content: 'H2'; }
.pen-menu .icon-h3:before { content: 'H3'; }
.pen-menu .icon-h4:before { content: 'H4'; }
.pen-menu .icon-h5:before { content: 'H5'; }
.pen-menu .icon-h6:before { content: 'H6'; }
.pen-menu .icon-p:before { content: 'P'; }
.pen {
position: relative;
}
.pen.hinted h1:before,
.pen.hinted h2:before,
.pen.hinted h3:before,
.pen.hinted h4:before,
.pen.hinted h5:before,
.pen.hinted h6:before,
.pen.hinted blockquote:before,
.pen.hinted hr:before {
color: #eee;
position: absolute;
right: 100%;
white-space: nowrap;
padding-right: 10px;
}
.pen.hinted blockquote { border-left: 0; margin-left: 0; padding-left: 0; }
.pen.hinted blockquote:before {
color: #1abf89;
content: ">";
font-weight: bold;
vertical-align: center;
}
.pen.hinted h1:before { content: "#";}
.pen.hinted h2:before { content: "##";}
.pen.hinted h3:before { content: "###";}
.pen.hinted h4:before { content: "####";}
.pen.hinted h5:before { content: "#####";}
.pen.hinted h6:before { content: "######";}
.pen.hinted hr:before { content: "﹘﹘﹘"; line-height: 1.2; vertical-align: bottom; }
.pen.hinted pre:before, .pen.hinted pre:after {
content: "```";
display: block;
color: #ccc;
}
.pen.hinted ul { list-style: none; }
.pen.hinted ul li:before {
content: "*";
color: #999;
line-height: 1;
vertical-align: bottom;
margin-left: -1.2em;
display: inline-block;
width: 1.2em;
}
.pen.hinted b:before, .pen.hinted b:after { content: "**"; color: #eee; font-weight: normal; }
.pen.hinted i:before, .pen.hinted i:after { content: "*"; color: #eee; }
.pen.hinted a { text-decoration: none; }
.pen.hinted a:before {content: "["; color: #ddd; }
.pen.hinted a:after { content: "](" attr(href) ")"; color: #ddd; }
.pen-placeholder { color: #999; }
......@@ -3,8 +3,9 @@
<ul>
{{#each tally}}
<li class="poll-result">
<input id="answer-{{key}}" type="radio" disabled {{#if choice}}checked="True"{{/if}} />
<span class="percentage-guage" style="width:{{percentage}}%;"></span>
<span>{{answer}}</span>
<label for="answer-{{key}}">{{answer}}</label>
<span class="poll-percent-display{{#if top}} poll-top-choice{{/if}}">{{percent}}%</span>
</li>
{{/each}}
......
......@@ -3,16 +3,17 @@
{# If no form is present, the Javascript will load the results instead. #}
{% if not choice %}
<form>
<h2>{{ question }}</h2>
<h2>Poll</h2>
{{question|safe}}
<ul>
{% for key, answer in answers %}
<li class="poll-answer">
<label for="answer-{{number}}">{{answer}}</label>
<input type="radio" name="choice" id="answer-{{number}}" value="{{key}}">
<label class="poll-answer" for="answer-{{number}}">{{answer}}</label>
</li>
{% endfor %}
</ul>
<input type="submit" name="poll-submit" onclick="return false;" disabled>
<input type="submit" name="poll-submit" onclick="return false;" value="Submit" disabled>
</form>
{% endif %}
</div>
\ No newline at end of file
......@@ -2,12 +2,21 @@
<div class="wrapper-comp-settings is-active editor-with-buttons" id="settings-tab">
<form id="poll-form">
<ul class="list-input settings-list" id="poll-line-items">
<h2><label for="question-editor">Question/Prompt</label></h2>
Select text to bring up formatting options.
<li class="field comp-setting-entry is-set">
<div class="wrapper-comp-setting">
<label class="label setting-label" for="question">Question</label>
<input class="input setting-input" name="question" id="question" value="{{question}}" type="text" />
</div>
<span class="tip setting-help">Example: 'What is your favorite color?'</span>
<div id="question-editor-container">
<div class="input setting-input" name="question" id="question-editor">{{question|safe}}</div>
</div>
</li>
<li class="field comp-setting-entry is-set">
<p>
<strong>Notes:</strong>
If you change an answer's text, all students who voted for that choice will have their votes updated to
the new text. You'll want to avoid changing an answer from something like 'True' to 'False', accordingly.
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>
</li>
</ul>
<div class="xblock-actions">
......
......@@ -7,6 +7,9 @@ function PollBlock(runtime, element) {
var submit = $('input[type=submit]', element);
var resultsTemplate = Handlebars.compile($("#results", element).html());
function getResults(data) {
if (! data['success']) {
alert(data['errors'].join('\n'));
}
$.ajax({
// Semantically, this would be better as GET, but I can use helper
// functions with POST.
......@@ -44,7 +47,7 @@ function PollBlock(runtime, element) {
enableSubmit();
}
} else {
getResults();
getResults({'success': true});
}
$(function ($) {
......
/* Javascript for ReferenceBlock. */
function PollEditBlock(runtime, element) {
var loadAnswers = runtime.handlerUrl(element, 'load_answers');
var temp = $('#answer-form-component', element).html();
......@@ -40,14 +39,26 @@ function PollEditBlock(runtime, element) {
$(element).find('.save-button').bind('click', function() {
var handlerUrl = runtime.handlerUrl(element, 'studio_submit');
var data = {};
$('#poll-form input', element).each(function() {
var poll_order = []
$('#poll-form input', element).each(function(i) {
data[this.name] = this.value;
if (this.name.indexOf('answer-') >= 0){
poll_order.push(this.name);
}
});
data['poll_order'] = poll_order;
function check_return(data) {
if (data['success']) {
window.location.reload(false);
return;
}
alert(data['errors'].join('\n'));
}
$.ajax({
type: "POST",
url: handlerUrl,
data: JSON.stringify(data),
success: function () {}
success: check_return
});
});
......@@ -58,5 +69,7 @@ function PollEditBlock(runtime, element) {
data: JSON.stringify({}),
success: displayAnswers
});
var pen = new Pen("#question-editor");
$.focus(pen)
});
}
\ No newline at end of file
/*! Licensed under MIT, https://github.com/sofish/pen */
(function(root) {
// only works with Pen
if(!root.Pen) return;
// markdown covertor obj
var covertor = {
keymap: { '96': '`', '62': '>', '49': '1', '46': '.', '45': '-', '42': '*', '35': '#'},
stack : []
};
// return valid markdown syntax
covertor.valid = function(str) {
var len = str.length;
if(str.match(/[#]{1,6}/)) {
return ['h' + len, len];
} else if(str === '```') {
return ['pre', len];
} else if(str === '>') {
return ['blockquote', len];
} else if(str === '1.') {
return ['insertorderedlist', len];
} else if(str === '-' || str === '*') {
return ['insertunorderedlist', len];
} else if(str.match(/(?:\.|\*|\-){3,}/)) {
return ['inserthorizontalrule', len];
}
};
// parse command
covertor.parse = function(e) {
var code = e.keyCode || e.which;
// when `space` is pressed
if(code === 32) {
var cmd = this.stack.join('');
this.stack.length = 0;
return this.valid(cmd);
}
// make cmd
if(this.keymap[code]) this.stack.push(this.keymap[code]);
return false;
};
// exec command
covertor.action = function(pen, cmd) {
// only apply effect at line start
if(pen.selection.focusOffset > cmd[1]) return;
var node = pen.selection.focusNode;
node.textContent = node.textContent.slice(cmd[1]);
pen.execCommand(cmd[0]);
};
// init covertor
covertor.init = function(pen) {
pen.on('keypress', function(e) {
var cmd = covertor.parse(e);
if(cmd) return covertor.action(pen, cmd);
});
};
// append to Pen
root.Pen.prototype.markdown = covertor;
}(window));
-e .
bleach
\ 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