Commit 7f5affef by Jonathan Piacenti

Several style fixes per review feedback.

parent 15d6d2a0
...@@ -47,33 +47,27 @@ class PollBlock(XBlock): ...@@ -47,33 +47,27 @@ class PollBlock(XBlock):
'total': total, 'feedback': process_markdown(self.feedback), 'total': total, 'feedback': process_markdown(self.feedback),
} }
def get_tally(self): def clean_tally(self):
""" """
Grabs the Tally and cleans it up, if necessary. Scoping prevents us from Cleans the tally. Scoping prevents us from modifying this in the studio
modifying this in the studio and in the LMS the way we want to without and in the LMS the way we want to without undesirable side effects. So
undesirable side effects. So we just clean it up on first access within we just clean it up on first access within the LMS, in case the studio
the LMS, in case the studio has made changes to the answers. has made changes to the answers.
""" """
tally = self.tally
answers = OrderedDict(self.answers) answers = OrderedDict(self.answers)
for key in answers.keys(): for key in answers.keys():
if key not in tally: if key not in self.tally:
tally[key] = 0 self.tally[key] = 0
for key in tally.keys(): for key in self.tally.keys():
if key not in answers: if key not in answers:
del tally[key] del self.tally[key]
return tally
def any_image(self): def any_image(self):
""" """
Find out if any answer has an image, since it affects layout. Find out if any answer has an image, since it affects layout.
""" """
for value in dict(self.answers).values(): return any(value['img'] for value in dict(self.answers).values())
if value['img']:
return True
return False
def tally_detail(self): def tally_detail(self):
""" """
...@@ -83,11 +77,13 @@ class PollBlock(XBlock): ...@@ -83,11 +77,13 @@ class PollBlock(XBlock):
answers = OrderedDict(self.answers) answers = OrderedDict(self.answers)
choice = self.get_choice() choice = self.get_choice()
total = 0 total = 0
source_tally = self.get_tally() self.clean_tally()
source_tally = self.tally
any_img = self.any_image() any_img = self.any_image()
for key, value in answers.items(): for key, value in answers.items():
count = int(source_tally[key])
tally.append({ tally.append({
'count': int(source_tally[key]), 'count': count,
'answer': value['label'], 'answer': value['label'],
'img': value['img'], 'img': value['img'],
'key': key, 'key': key,
...@@ -96,16 +92,13 @@ class PollBlock(XBlock): ...@@ -96,16 +92,13 @@ class PollBlock(XBlock):
'last': False, 'last': False,
'any_img': any_img, 'any_img': any_img,
}) })
total += tally[-1]['count'] total += count
for answer in tally: for answer in tally:
try: try:
percent = (answer['count'] / float(total)) answer['percent'] = int(answer['count'] / float(total)) * 100
answer['percent'] = int(percent * 100)
if answer['key'] == choice: if answer['key'] == choice:
answer['choice'] = True answer['choice'] = True
if answer['img']:
any_img = True
except ZeroDivisionError: except ZeroDivisionError:
answer['percent'] = 0 answer['percent'] = 0
...@@ -129,7 +122,18 @@ class PollBlock(XBlock): ...@@ -129,7 +122,18 @@ class PollBlock(XBlock):
else: else:
return None return None
# TO-DO: change this view to display your data your own way. def create_fragment(self, context, template, css, js, js_init):
html = Template(
self.resource_string(template)).render(Context(context))
frag = Fragment(html)
frag.add_javascript_url(
self.runtime.local_resource_url(
self, 'public/js/vendor/handlebars.js'))
frag.add_css(self.resource_string(css))
frag.add_javascript(self.resource_string(js))
frag.initialize_js(js_init)
return frag
def student_view(self, context=None): def student_view(self, context=None):
""" """
The primary view of the PollBlock, shown to students The primary view of the PollBlock, shown to students
...@@ -159,23 +163,20 @@ class PollBlock(XBlock): ...@@ -159,23 +163,20 @@ class PollBlock(XBlock):
detail, total = self.tally_detail() detail, total = self.tally_detail()
context.update({'tally': detail, 'total': total}) context.update({'tally': detail, 'total': total})
context = Context(context) return self.create_fragment(
html = self.resource_string("public/html/poll.html") context, "public/html/poll.html", "public/css/poll.css",
html = Template(html).render(context) "public/js/poll.js", "PollBlock")
frag = Fragment(html)
frag.add_css(self.resource_string("public/css/poll.css"))
frag.add_javascript_url(
self.runtime.local_resource_url(
self, 'public/js/vendor/handlebars.js'))
frag.add_javascript(self.resource_string("public/js/poll.js"))
frag.initialize_js('PollBlock')
return frag
@XBlock.json_handler @XBlock.json_handler
def load_answers(self, data, suffix=''): def load_answers(self, data, suffix=''):
return {'answers': [{'key': key, 'text': value['label'], 'img': value['img']} return {
for key, value in self.answers 'answers': [
]} {
'key': key, 'text': value['label'], 'img': value['img']
}
for key, value in self.answers
]
}
def studio_view(self, context=None): def studio_view(self, context=None):
if not context: if not context:
...@@ -187,18 +188,9 @@ class PollBlock(XBlock): ...@@ -187,18 +188,9 @@ class PollBlock(XBlock):
'feedback': self.feedback, 'feedback': self.feedback,
'js_template': js_template 'js_template': js_template
}) })
context = Context(context) return self.create_fragment(
html = self.resource_string("public/html/poll_edit.html") context, "public/html/poll_edit.html",
html = Template(html).render(context) "/public/css/poll_edit.css", "public/js/poll_edit.js", "PollEditBlock")
frag = Fragment(html)
frag.add_javascript_url(
self.runtime.local_resource_url(
self, 'public/js/vendor/handlebars.js'))
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')
return frag
@XBlock.json_handler @XBlock.json_handler
def studio_submit(self, data, suffix=''): def studio_submit(self, data, suffix=''):
...@@ -209,7 +201,7 @@ class PollBlock(XBlock): ...@@ -209,7 +201,7 @@ class PollBlock(XBlock):
result['success'] = False result['success'] = False
else: else:
question = data['question'][:4096] question = data['question'][:4096]
if 'feedback' not in data or not data['feedback']: if 'feedback' not in data or not data['feedback'].strip():
feedback = '' feedback = ''
else: else:
feedback = data['feedback'][:4096] feedback = data['feedback'][:4096]
...@@ -293,20 +285,15 @@ class PollBlock(XBlock): ...@@ -293,20 +285,15 @@ class PollBlock(XBlock):
result['errors'].append('No key "{choice}" in answers table.'.format(choice=choice)) result['errors'].append('No key "{choice}" in answers table.'.format(choice=choice))
return result return result
tally = self.get_tally() self.clean_tally()
self.choice = choice self.choice = choice
running_total = tally.get(choice, 0) self.tally[choice] = self.tally.get(choice, 0) + 1
tally[choice] = running_total + 1
# Let the LMS know the user has answered the poll. # Let the LMS know the user has answered the poll.
self.runtime.publish(self, 'progress', {}) self.runtime.publish(self, 'progress', {})
result['success'] = True result['success'] = True
self.tally = tally
return result return result
# TO-DO: change this to create the scenarios you'd like to see in the
# workbench while developing your XBlock.
@staticmethod @staticmethod
def workbench_scenarios(): def workbench_scenarios():
""" """
......
This static directory is for files that should be included in your kit as plain
static files.
You can ask the runtime for a URL that will retrieve these files with:
url = self.runtime.local_resource_url(self, "static/js/lib.js")
The default implementation is very strict though, and will not serve files from
the static directory. It will serve files from a directory named "public".
Create a directory alongside this one named "public", and put files there.
Then you can get a url with code like this:
url = self.runtime.local_resource_url(self, "public/js/lib.js")
The sample code includes a function you can use to read the content of files
in the static directory, like this:
frag.add_javascript(self.resource_string("static/js/my_block.js"))
/* CSS for PollBlock Student View */
.poll-answer { .poll-answer {
margin-left: 1em; margin-left: 1em;
font-weight: bold; font-weight: bold;
......
/* CSS for PollBlock */ /* CSS for PollBlock Studio Menu View */
.poll-delete-answer { .poll-delete-answer {
float: right; float: right;
...@@ -21,11 +21,6 @@ ...@@ -21,11 +21,6 @@
padding: 10px; padding: 10px;
} }
label.poll-label {
font-weight: bold;
font-size: 16pt;
}
.poll-move-up { .poll-move-up {
opacity: .5; opacity: .5;
} }
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
<input id="answer-{{key}}" type="radio" disabled {{#if choice}}checked="True"{{/if}} /> <input id="answer-{{key}}" type="radio" disabled {{#if choice}}checked="True"{{/if}} />
</div> </div>
{{#if any_img}} {{#if any_img}}
<div class="poll-image"> <div class="poll-image result-image">
<label for="answer-{{key}}" class="poll-image-label"> <label for="answer-{{key}}" class="poll-image-label">
{{#if img}} {{#if img}}
<img src="{{img}}" /> <img src="{{img}}" />
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
</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">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> <a href="#" class="button action-button poll-delete-answer">Delete</a>
</li> </li>
{{/each}} {{/each}}
</script> </script>
\ No newline at end of file
...@@ -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" onclick="return false;">Add Answer</a> <a href="#" class="button action-button" class="poll-add-answer-link">Add Answer</a>
</li> </li>
<li class="action-item"> <li class="action-item">
<input type="submit" class="button action-primary save-button" value="Save" onclick="return false;" /> <input type="submit" class="button action-primary save-button" value="Save" />
</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>
......
...@@ -22,12 +22,20 @@ function PollBlock(runtime, element) { ...@@ -22,12 +22,20 @@ function PollBlock(runtime, element) {
}) })
} }
function enableSubmit() {
submit.removeAttr("disabled");
answers.unbind("change.EnableSubmit");
}
// If the submit button doesn't exist, the user has already
// selected a choice.
if (submit.length) { if (submit.length) {
var radios = $('input[name=choice]:checked', element); var radio = $('input[name=choice]:checked', element);
submit.click(function (event) { submit.click(function (event) {
// Refresh. // Refresh.
radios = $(radios.selector, element); radio = $(radio.selector, element);
var choice = radios.val(); var choice = radio.val();
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: voteUrl, url: voteUrl,
...@@ -35,12 +43,10 @@ function PollBlock(runtime, element) { ...@@ -35,12 +43,10 @@ function PollBlock(runtime, element) {
success: getResults success: getResults
}); });
}); });
var answers = $('li', element); // If the user has refreshed the page, they may still have an answer
function enableSubmit() { // selected and the submit button should be enabled.
submit.removeAttr("disabled"); var answers = $('input[type=radio]', element);
answers.unbind("change.EnableSubmit"); if (! radio.val()) {
}
if (! radios.val()) {
answers.bind("change.EnableSubmit", enableSubmit); answers.bind("change.EnableSubmit", enableSubmit);
} else { } else {
enableSubmit(); enableSubmit();
...@@ -48,8 +54,4 @@ function PollBlock(runtime, element) { ...@@ -48,8 +54,4 @@ function PollBlock(runtime, element) {
} else { } else {
getResults({'success': true}); getResults({'success': true});
} }
$(function ($) {
});
} }
\ No newline at end of file
...@@ -4,42 +4,35 @@ function PollEditBlock(runtime, element) { ...@@ -4,42 +4,35 @@ function PollEditBlock(runtime, element) {
var answerTemplate = Handlebars.compile(temp); var answerTemplate = Handlebars.compile(temp);
var pollLineItems =$('#poll-line-items', element); var pollLineItems =$('#poll-line-items', element);
// We probably don't need something this complex, but UUIDs are the
// standard.
function generateUUID(){
var d = new Date().getTime();
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = (d + Math.random()*16)%16 | 0;
d = Math.floor(d/16);
return (c=='x' ? r : (r&0x7|0x8)).toString(16);
})
}
function empowerDeletes(scope) { function empowerDeletes(scope) {
$('.poll-delete-answer', scope).click(function () { $('.poll-delete-answer', scope).click(function () {
$(this).parent().remove(); $(this).parent().remove();
}); });
} }
// Above this point are other settings. /*
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; var starting_point = 3;
function empowerArrows(scope) { function empowerArrows(scope) {
$('.poll-move-up', scope).click(function () { $('.poll-move-up', scope).click(function () {
var tag = $(this).parent().parent().parent(); var tag = $(this).parents('li');
if (tag.index() <= starting_point){ if (tag.index() <= starting_point){
return; return;
} }
tag.prev().before(tag); tag.prev().before(tag);
tag.fadeOut(250).fadeIn(250); tag.fadeOut("fast", "swing").fadeIn("fast", "swing");
}); });
$('.poll-move-down', scope).click(function () { $('.poll-move-down', scope).click(function () {
var tag = $(this).parent().parent().parent(); var tag = $(this).parents('li');
if ((tag.index() >= (tag.parent().children().length - 1))) { if ((tag.index() >= (tag.parent().children().length - 1))) {
return; return;
} }
tag.next().after(tag); tag.next().after(tag);
tag.parent().parent().parent().scrollTop(tag.offset().top); tag.fadeOut("fast", "swing").fadeIn("fast", "swing");
tag.fadeOut(250).fadeIn(250);
}); });
} }
...@@ -50,13 +43,23 @@ function PollEditBlock(runtime, element) { ...@@ -50,13 +43,23 @@ function PollEditBlock(runtime, element) {
} }
$('#poll-add-answer', element).click(function () { $('#poll-add-answer', element).click(function () {
pollLineItems.append(answerTemplate({'answers': [{'key': generateUUID(), 'text': ''}]})); // The degree of precision on date should be precise enough to avoid
// collisions in the real world.
pollLineItems.append(answerTemplate({'answers': [{'key': new Date().getTime(), 'text': ''}]}));
var new_answer = $(pollLineItems.children().last()); var new_answer = $(pollLineItems.children().last());
empowerDeletes(new_answer); empowerDeletes(new_answer);
empowerArrows(new_answer); empowerArrows(new_answer);
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', {});
}); });
......
...@@ -3,4 +3,7 @@ from xblockutils.base_test import SeleniumBaseTest ...@@ -3,4 +3,7 @@ from xblockutils.base_test import SeleniumBaseTest
class PollBaseTest(SeleniumBaseTest): class PollBaseTest(SeleniumBaseTest):
default_css_selector = 'div.poll-block' default_css_selector = 'div.poll-block'
module_name = __name__ module_name = __name__
\ No newline at end of file
def get_submit(self):
return self.browser.find_element_by_css_selector('input[name="poll-submit"]')
\ No newline at end of file
...@@ -20,9 +20,11 @@ class TestDefaults(PollBaseTest): ...@@ -20,9 +20,11 @@ class TestDefaults(PollBaseTest):
self.go_to_page('Defaults') self.go_to_page('Defaults')
button = self.browser.find_element_by_css_selector('input[type=radio]') button = self.browser.find_element_by_css_selector('input[type=radio]')
button.click() button.click()
submit = self.browser.find_element_by_css_selector('input[name="poll-submit"]') submit = self.get_submit()
submit.click() submit.click()
self.wait_until_exists('.poll-percent-display')
# Should now be on the results page. # Should now be on the results page.
self.assertEqual(self.browser.find_element_by_css_selector('.poll-percent-display').text, '100%') self.assertEqual(self.browser.find_element_by_css_selector('.poll-percent-display').text, '100%')
......
...@@ -22,9 +22,9 @@ class TestLayout(PollBaseTest): ...@@ -22,9 +22,9 @@ class TestLayout(PollBaseTest):
# Pics should be within labels. # Pics should be within labels.
pics[0].find_element_by_css_selector('img').click() pics[0].find_element_by_css_selector('img').click()
self.browser.find_element_by_css_selector('input[name=poll-submit]').click() self.get_submit().click()
time.sleep(1) self.wait_until_exists('.poll-image')
self.assertEqual(len(self.browser.find_elements_by_css_selector('.poll-image')), 4) self.assertEqual(len(self.browser.find_elements_by_css_selector('.poll-image')), 4)
...@@ -39,8 +39,8 @@ class TestLayout(PollBaseTest): ...@@ -39,8 +39,8 @@ class TestLayout(PollBaseTest):
pics[0].find_element_by_css_selector('img').click() pics[0].find_element_by_css_selector('img').click()
self.browser.find_element_by_css_selector('input[name=poll-submit]').click() self.get_submit().click()
time.sleep(1) self.wait_until_exists('.poll-image.result-image')
# ...But on the results page, we need four, for table layout. # ...But on the results page, we need four, for table layout.
self.assertEqual(len(self.browser.find_elements_by_css_selector('.poll-image')), 4) self.assertEqual(len(self.browser.find_elements_by_css_selector('.poll-image')), 4)
\ No newline at end of file
...@@ -32,8 +32,9 @@ We shall find out if markdown is respected. ...@@ -32,8 +32,9 @@ We shall find out if markdown is respected.
""" """
self.go_to_page("Markdown") self.go_to_page("Markdown")
self.browser.find_element_by_css_selector('input[type=radio]').click() self.browser.find_element_by_css_selector('input[type=radio]').click()
self.browser.find_element_by_css_selector('input[name="poll-submit"]').click() self.get_submit().click()
self.wait_until_exists('.poll-feedback')
self.assertEqual( self.assertEqual(
self.browser.find_element_by_css_selector('.poll-feedback').text, self.browser.find_element_by_css_selector('.poll-feedback').text,
"""This is some feedback """This is some feedback
......
...@@ -13,8 +13,7 @@ class TestPollFunctions(PollBaseTest): ...@@ -13,8 +13,7 @@ class TestPollFunctions(PollBaseTest):
Checks first load. Checks first load.
Verify that the poll loads with the expected choices, that feedback is Verify that the poll loads with the expected choices, that feedback is
not showing, that the submit button is disabled, and that it is enabled not showing, and that the submit button is disabled.
when a choice is selected.
""" """
self.go_to_page('Poll Functions') self.go_to_page('Poll Functions')
answer_elements = self.browser.find_elements_by_css_selector('label.poll-answer') answer_elements = self.browser.find_elements_by_css_selector('label.poll-answer')
...@@ -23,13 +22,19 @@ class TestPollFunctions(PollBaseTest): ...@@ -23,13 +22,19 @@ class TestPollFunctions(PollBaseTest):
self.assertRaises(NoSuchElementException, self.browser.find_element_by_css_selector, '.poll-feedback') self.assertRaises(NoSuchElementException, self.browser.find_element_by_css_selector, '.poll-feedback')
submit_button = self.browser.find_element_by_css_selector('input[name=poll-submit]') submit_button = self.get_submit()
self.assertFalse(submit_button.is_enabled()) self.assertFalse(submit_button.is_enabled())
def test_submit_enabled(self):
"""
Makes sure the submit button is enabled when selecting an answer.
"""
self.go_to_page('Poll Functions')
answer_elements = self.browser.find_elements_by_css_selector('label.poll-answer')
answer_elements[0].click() answer_elements[0].click()
# When an answer is selected, make sure submit is enabled. # When an answer is selected, make sure submit is enabled.
self.assertTrue(submit_button.is_enabled()) self.wait_until_exists('input[name=poll-submit]:enabled')
def test_poll_submission(self): def test_poll_submission(self):
""" """
...@@ -43,7 +48,7 @@ class TestPollFunctions(PollBaseTest): ...@@ -43,7 +48,7 @@ class TestPollFunctions(PollBaseTest):
# 'Not very long' # 'Not very long'
answer_elements[1].click() answer_elements[1].click()
self.browser.find_element_by_css_selector('input[name=poll-submit]').click() self.get_submit().click()
# Not a good way to wait here, since all the elements we care about # Not a good way to wait here, since all the elements we care about
# tracking don't exist yet. # tracking don't exist yet.
...@@ -68,12 +73,10 @@ class TestPollFunctions(PollBaseTest): ...@@ -68,12 +73,10 @@ class TestPollFunctions(PollBaseTest):
# Not very long # Not very long
answer_elements[1].click() answer_elements[1].click()
self.browser.find_element_by_css_selector('input[name=poll-submit]').click() self.get_submit().click()
time.sleep(1) # Button will be reaplaced with a new disabled copy, not just disabled.
self.wait_until_exists('input[name=poll-submit]:disabled')
submit_button = self.browser.find_element_by_css_selector('input[name=poll-submit]')
self.assertFalse(submit_button.is_enabled())
self.go_to_page('Poll Functions') self.go_to_page('Poll Functions')
self.assertFalse(self.browser.find_element_by_css_selector('input[name=poll-submit]').is_enabled()) self.assertFalse(self.get_submit().is_enabled())
\ No newline at end of file \ 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