Commit 2d828eed by Piotr Mitros

Starting to revamp UX as per Francis' feedback. Breaking backwards-compatibility

parent 472e6dbc
icons: icons:
nosetests test_icons.py --with-save-baseline nosetests test_icons.py --with-save-baseline
mv screenshots/baseline/face* ../rate/public/default_icons/ mv screenshots/baseline/*png ../rate/public/default_icons/
rm *.log
rm *.pyc
rm *~
rm *test_page*png
...@@ -6,7 +6,7 @@ class IconsPage(PageObject): ...@@ -6,7 +6,7 @@ class IconsPage(PageObject):
GitHub's search page GitHub's search page
""" """
url = 'https://rawgit.com/pmitros/RateXBlock/master/makeicons/raw_icons.html' url = 'https://rawgit.com/pmitros/RateXBlock/pmitros/ux-revamp/makeicons/raw_icons.html'
def is_browser_on_page(self): def is_browser_on_page(self):
return self.q(css='#face5').is_present() return self.q(css='#snum5').is_present()
...@@ -4,19 +4,101 @@ ...@@ -4,19 +4,101 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script> <script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<style> <style>
.face { .icon {
width: 60px; width: 57px;
text-align: center; text-align: center;
display:inline; display: inline-block;
}
.face {
border-radius: 15px;
}
.num {
border-radius: 30px;
border-style:solid;
width: 45px;
height: 42px;
padding-top:3px
}
.face {
border-radius: 15px;
//border-style:solid;
//border-color: rgba(255,255,255,0);
} }
.inactive {
background-color: rgb(255,255,255);
color: rgb(6,86,131);
}
.selected {
background-color: rgb(166, 207, 230);
color: rgb(80,41,59);
}
.active {
color: rgb(255,255,255);
background-color: rgb(0, 121, 188);
}
.inactive .num {
background-color: rgb(255,255,255);
color: rgb(6,86,131);
border-radius: 30px;
border-style: solid;
}
.active .num {
color: rgb(255,255,255);
background-color: rgb(0, 121, 188);
border-radius: 30px;
border-color: rgba(255,255,255,0);
}
</style> </style>
</head> </head>
<bod> <body>
<font size="7"> <font size="7">
<div id="face1" class="face">😁</div> <div id="iface1" class="icon inactive face">😁</div>
<div id="face2" class="face">😊</div> <div id="iface2" class="icon inactive face">😊</div>
<div id="face3" class="face">😐</div> <div id="iface3" class="icon inactive face">😐</div>
<div id="face4" class="face">😞</div> <div id="iface4" class="icon inactive face">😞</div>
<div id="face5" class="face">😭</div> <div id="iface5" class="icon inactive face">😭</div>
<br>
<div id="aface1" class="icon active face">😁</div>
<div id="aface2" class="icon active face">😊</div>
<div id="aface3" class="icon active face">😐</div>
<div id="aface4" class="icon active face">😞</div>
<div id="aface5" class="icon active face">😭</div>
<br>
<div id="sface1" class="icon selected face">😁</div>
<div id="sface2" class="icon selected face">😊</div>
<div id="sface3" class="icon selected face">😐</div>
<div id="sface4" class="icon selected face">😞</div>
<div id="sface5" class="icon selected face">😭</div>
<br>
</font>
<font size="6">
<br>
<div id="inum1" class="icon inactive num">1</div>
<div id="inum2" class="icon inactive num">2</div>
<div id="inum3" class="icon inactive num">3</div>
<div id="inum4" class="icon inactive num">4</div>
<div id="inum5" class="icon inactive num">5</div>
<br>
<div id="anum1" class="icon active num">1</div>
<div id="anum2" class="icon active num">2</div>
<div id="anum3" class="icon active num">3</div>
<div id="anum4" class="icon active num">4</div>
<div id="anum5" class="icon active num">5</div>
<br>
<div id="snum1" class="icon selected num">1</div>
<div id="snum2" class="icon selected num">2</div>
<div id="snum3" class="icon selected num">3</div>
<div id="snum4" class="icon selected num">4</div>
<div id="snum5" class="icon selected num">5</div>
</font> </font>
</body> </body>
...@@ -23,11 +23,10 @@ class TestIcons(WebAppTest): ...@@ -23,11 +23,10 @@ class TestIcons(WebAppTest):
""" """
page = IconsPage(self.browser) page = IconsPage(self.browser)
page.visit() page.visit()
self.assertScreenshot("#face1", "face1") for i in range(5):
self.assertScreenshot("#face2", "face2") for icon in ["face", "num"]:
self.assertScreenshot("#face3", "face3") for style in "ais":
self.assertScreenshot("#face4", "face4") self.assertScreenshot("#"+style+icon+str(i+1), style+icon+str(i+1))
self.assertScreenshot("#face5", "face5")
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
......
# coding: utf-8 # coding: utf-8
""" """
This is an XBlock designed to allow people to provide feedback on our This is an XBlock designed to allow people to provide feedback on our
course resources. course resources, and to think and synthesize about their experience
in the course.
""" """
import random import random
...@@ -12,6 +13,22 @@ from xblock.core import XBlock ...@@ -12,6 +13,22 @@ from xblock.core import XBlock
from xblock.fields import Scope, Integer, String, List, Float from xblock.fields import Scope, Integer, String, List, Float
from xblock.fragment import Fragment from xblock.fragment import Fragment
"""
We provide default text which is designed to elicit student thought. We'd
like instructors to customize this to something highly structured (not
"What did you think?" and "How did you like it?".
We
"""
default_freeform = "What did you learn from this? What was missing?"
default_likert = "How would you rate this as a learning experience?"
default_default = "Think about the material, and try to synthesize key " \
"lessons learned, as well as key gaps in our presentation."
default_placeholder = "Take a little bit of time to reflect here. " \
"Research shows that a meaningful synthesis will help " \
"you better understand and remember material from this" \
"course."
default_icon = "face"
@XBlock.needs('i18n') @XBlock.needs('i18n')
class RateXBlock(XBlock): class RateXBlock(XBlock):
...@@ -27,8 +44,9 @@ class RateXBlock(XBlock): ...@@ -27,8 +44,9 @@ class RateXBlock(XBlock):
# will default to the ones in default_prompt. # will default to the ones in default_prompt.
prompts = List( prompts = List(
default=[ default=[
{'freeform': "Please provide us feedback on this section", {'freeform': default_freeform,
'likert': "Please rate your overall experience with this section"} 'default_text': default_default,
'likert': default_likert}
], ],
scope=Scope.settings, scope=Scope.settings,
help="Freeform user prompt", help="Freeform user prompt",
...@@ -85,10 +103,12 @@ class RateXBlock(XBlock): ...@@ -85,10 +103,12 @@ class RateXBlock(XBlock):
_ = self.runtime.service(self, 'i18n').ugettext _ = self.runtime.service(self, 'i18n').ugettext
prompt = { prompt = {
'freeform': _("Please provide us feedback on this section."), 'freeform': _("Please reflect on this course material"),
'likert': _("Please rate your overall experience " 'default_text': _("Please take time to meaningfully reflect "
"with this section."), "on your experience with this course "
'mouseovers': [_("Excellent"), "material."),
'likert': _("Please rate your overall experience"),
'scale_text': [_("Excellent"),
_("Good"), _("Good"),
_("Average"), _("Average"),
_("Fair"), _("Fair"),
...@@ -112,25 +132,73 @@ class RateXBlock(XBlock): ...@@ -112,25 +132,73 @@ class RateXBlock(XBlock):
self.prompt_choice = random.randint(0, len(self.prompts) - 1) self.prompt_choice = random.randint(0, len(self.prompts) - 1)
prompt = self.get_prompt() prompt = self.get_prompt()
# Now, we render the RateXBlock. This may be redundant, since we # Now, we render the RateXBlock.
# don't always show it.
html = self.resource_string("static/html/rate.html") html = self.resource_string("static/html/rate.html")
# The replace allows us to format the HTML nicely without getting
# extra whitespace # Staff see vote totals, so we have slightly different HTML here.
if self.vote_aggregate and self.is_staff(): if self.vote_aggregate and self.is_staff():
scale_item = self.resource_string("static/html/staff_item.html") scale_item = self.resource_string("static/html/staff_item.html")
else: else:
scale_item = self.resource_string("static/html/scale_item.html") scale_item = self.resource_string("static/html/scale_item.html")
# The replace allows us to format the HTML nicely without getting
# extra whitespace
scale_item = scale_item.replace('\n', '') scale_item = scale_item.replace('\n', '')
# We have five Likert fields right now, but we'd like this to
# be dynamic
indexes = range(len(prompt['icons'])) indexes = range(len(prompt['icons']))
# If the user voted before, we'd like to show that
active_vote = ["checked" if i == self.user_vote else "" active_vote = ["checked" if i == self.user_vote else ""
for i in indexes] for i in indexes]
# Confirm that we do have vote totals (this may be uninitialized
# otherwise). This should probably go into __init__ or similar.
self.init_vote_aggregate() self.init_vote_aggregate()
votes = self.vote_aggregate votes = self.vote_aggregate
# We grab the icons. This should move to a Filesystem field so
# instructors can upload new ones
ina_templ = 'public/default_icons/iface{i}.png'
act_templ = 'public/default_icons/aface{i}.png'
sel_templ = 'public/default_icons/sface{i}.png'
ina_urls = [self.runtime.local_resource_url(self, ina_templ.format(i=i))
for i in range(1,6)]
act_urls = [self.runtime.local_resource_url(self, act_templ.format(i=i))
for i in range(1,6)]
sel_urls = [self.runtime.local_resource_url(self, sel_templ.format(i=i))
for i in range(1,6)]
img_urls = [i if active else a
for (i, active, a)
in zip(ina_urls, active_vote, act_urls)]
# Render the
scale = u"".join( scale = u"".join(
scale_item.format(level=l, icon=icon, i=i, active=a, votes=v) for scale_item.format(scale_text=scale_text,
(l, icon, i, a, v) in unicode_icon=unicode_icon,
zip(prompt['mouseovers'], prompt['icons'], indexes, active_vote, votes) idx=idx,
active=active,
vote_cnt=vote_cnt,
ina_icon=ina_icon,
act_icon=act_icon,
sel_icon=sel_icon) for
(scale_text,
unicode_icon,
idx,
active,
vote_cnt,
act_icon,
ina_icon,
sel_icon) in
zip(prompt['scale_text'],
prompt['icons'],
indexes,
active_vote,
votes,
act_urls,
ina_urls,
sel_urls
)
) )
if self.user_vote != -1: if self.user_vote != -1:
_ = self.runtime.service(self, 'i18n').ugettext _ = self.runtime.service(self, 'i18n').ugettext
...@@ -187,7 +255,7 @@ class RateXBlock(XBlock): ...@@ -187,7 +255,7 @@ class RateXBlock(XBlock):
# Make sure we're initialized # Make sure we're initialized
print self.get_prompt() print self.get_prompt()
if not self.vote_aggregate: if not self.vote_aggregate:
self.vote_aggregate = [0] * (len(self.get_prompt()['mouseovers'])) self.vote_aggregate = [0] * (len(self.get_prompt()['scale_text']))
def vote(self, data): def vote(self, data):
""" """
...@@ -226,14 +294,6 @@ class RateXBlock(XBlock): ...@@ -226,14 +294,6 @@ class RateXBlock(XBlock):
self.runtime.publish(self, self.runtime.publish(self,
'edx.ratexblock.nothing_provided', 'edx.ratexblock.nothing_provided',
{}) {})
if 'freeform' in data:
response = {"success": True,
"response": _("Thank you for your feedback!")}
self.runtime.publish(self,
'edx.ratexblock.freeform_provided',
{'old_freeform': self.user_freeform,
'new_freeform': data['freeform']})
self.user_freeform = data['freeform']
if 'vote' in data: if 'vote' in data:
response = {"success": True, response = {"success": True,
"response": _("Thank you for voting!")} "response": _("Thank you for voting!")}
...@@ -242,6 +302,14 @@ class RateXBlock(XBlock): ...@@ -242,6 +302,14 @@ class RateXBlock(XBlock):
{'old_vote': self.user_vote, {'old_vote': self.user_vote,
'new_vote': data['vote']}) 'new_vote': data['vote']})
self.vote(data) self.vote(data)
if 'freeform' in data:
response = {"success": True,
"response": _("Thank you for your feedback!")}
self.runtime.publish(self,
'edx.ratexblock.freeform_provided',
{'old_freeform': self.user_freeform,
'new_freeform': data['freeform']})
self.user_freeform = data['freeform']
response.update({ response.update({
"freeform": self.user_freeform, "freeform": self.user_freeform,
...@@ -261,9 +329,9 @@ class RateXBlock(XBlock): ...@@ -261,9 +329,9 @@ class RateXBlock(XBlock):
return [ return [
("RateXBlock", ("RateXBlock",
"""<vertical_demo> """<vertical_demo>
<rate p="50"/> <rate p="100"/>
<rate p="50"/> <!--rate p="50"/>
<rate p="50"/> <rate p="50"/-->
</vertical_demo> </vertical_demo>
"""), """),
] ]
......
/* CSS for RateXBlock */ /* CSS for RateXBlock */
/* Overall block. We limit width, and put a very faint
border around it. */
.rate_block { .rate_block {
text-align: center; max-width: 450px;
max-width: 400px;
border-width: 1px; border-width: 1px;
border-style: solid; border-style: solid;
border-color: rgba(0,0,0,0.1); border-color: rgba(0,0,0,0.1);
padding: 10px padding: 10px
} }
/* Little thank you message div after people vote */
.rate_thank_you { .rate_thank_you {
color: green; color: green;
} }
.rate_block .rate_header { /* Label for the freeform text input. We want a little
font-weight: bold; space between this and the Likert input.*/
font-size: large; .rate_block .rate_header_div {
margin-top:1em;
}
/* Fieldset for the Likert radio buttons */
.rate_block .rate_likert_field {
padding: 0px;
margin: 0px;
} }
/* The div around everything with a radio button */
.rate_block .rate_likert_rating { .rate_block .rate_likert_rating {
cursor: pointer; cursor: pointer;
border-radius:5px; border-radius:5px;
display:inline-block;
text-align:center;
padding:0px 10px 0px 10px;
} }
.rate_block .rate_likert_header { /* Hide checked icon */
.rate_icon_active { display:none; }
.rate_icon_inactive { display:inline-block; }
/* But show it if we are checked... */
.rate_block input[type="radio"]:checked ~ .rate_icon_active{
display: inline-block;
}
/* ... while hiding the unchecked icon */
.rate_block input[type="radio"]:checked ~ .rate_icon_inactive{
display: none;
}
.rate_icon {
border-style:solid;
border-width: 1px;
border-color: rgba(255,255,255,0);
padding:1px;
height:60px;
}
.rate_block input[type="radio"]:focus ~ .rate_icon {
border-color: #999999;
}
.rate_block input[type="radio"]:hover ~ .rate_icon {
border-color: #999999;
}
.rate_block .rate_likert_label {
cursor: pointer;
} }
.rate_block .rate_freeform_input { .rate_block .rate_freeform_input {
...@@ -53,8 +95,8 @@ ...@@ -53,8 +95,8 @@
opacity:0; opacity:0;
width:1px; width:1px;
height:1px; height:1px;
padding:0; padding:0px;
margin:0; margin:0px;
position:absolute; position:absolute;
clip:rect(1px,1px,1px,1px); clip:rect(1px,1px,1px,1px);
left:-10000px; left:-10000px;
...@@ -68,18 +110,6 @@ ...@@ -68,18 +110,6 @@
.rate_block .rate_likert_field { .rate_block .rate_likert_field {
border-style:none; border-style:none;
} }
.rate_block input[type="radio"]:checked + span{
background-color:yellow;
font-weight:bold;
}
.rate_block input[type="radio"]:focus + span{
border-style:solid;
border-width: 1px;
border-color: #999999;
}
.rate_block .rate_submit_feedback { .rate_block .rate_submit_feedback {
width:100%; width:100%;
} }
<div class="rate_block"> <div class="rate_block">
<form action="" onsubmit="return false;"> <form action="" onsubmit="return false;">
<label class="rate_header" for="rate_freeform_textarea">{freeform_prompt}</label> <fieldset class="rate_likert_field">
<div class="rate_freeform_input"> <legend class="rate_likert_header">{likert_prompt}</legend>
<textarea id="rate_freeform_textarea" class="rate_freeform_area" rows="4" cols="30">{self.user_freeform}</textarea> <div class="rate_likert_scale">{scale}</div>
<div class="rate_thank_you" aria-live=polite>{response}</div> </fieldset>
</div>
<fieldset class="rate_likert_field"> <div class="rate_header_div">
<legend class="rate_likert_header">{likert_prompt}</legend> <label class="rate_header" for="rate_freeform_textarea">{freeform_prompt}</label>
<div class="rate_likert_scale">{scale}</div> </div>
</fieldset> <div class="rate_freeform_input">
<button type="submit" class="rate_submit_feedback"> Submit Feedback </button> <textarea id="rate_freeform_textarea" class="rate_freeform_area" rows="6" cols="50">{self.user_freeform}</textarea>
</form> <div class="rate_thank_you" aria-live=polite>{response}</div>
</div>
<button type="submit" class="rate_submit_feedback"> Submit Feedback </button>
</form>
</div> </div>
<label title="{level}"> <div style="display:inline-block; text-align:center; padding:0px 10px 0px 10px;">
<input id="radio_{i}" name="rate_scale" class="rate_radio" type="radio" {active}> <label title="{scale_text}">
<span aria-hidden="true">{icon}</span> <input id="radio_{idx}" name="rate_scale" class="rate_radio" type="radio" {active}/>
<span class="rate_sr_text">{level}</span> <img src="{ina_icon}"/>
</label> <br/>
<span>{scale_text}</span>
</label>
</div>
<div style="display:inline-block; text-align:center; padding:0px 10px 0px 10px;"> <div class="rate_likert_rating">
<label title="{level}"> <label title="{scale_text}" class="rate_likert_label">
<input id="radio_{i}" name="rate_scale" class="rate_radio" type="radio" {active}/> <input id="radio_{idx}" name="rate_scale" class="rate_radio" type="radio" {active}/>
<span aria-hidden="true">{icon}</span> <span class="rate_icon rate_icon_inactive">
<span class="rate_sr_text">{level}</span> <img src="{ina_icon}" alt="{unicode_icon}"/>
</span>
<span class="rate_icon rate_icon_active">
<img src="{act_icon}" alt="{unicode_icon}"/>
</span>
<br/> <br/>
({votes}) <span>{scale_text}</span>
<br/>
({vote_cnt})
</label> </label>
</div> </div>
...@@ -2,13 +2,19 @@ ...@@ -2,13 +2,19 @@
<p> This XBlock allows you to collect student feedback on pieces of <p> This XBlock allows you to collect student feedback on pieces of
the course. This may be helpful either for course improvement, or the course. This may be helpful either for course improvement, or
to give students a chance to reflect on what they have done.</p> to give students a chance to reflect on what they have done.</p>
<p>Please note that the feedback needs to be analyzed as part of <p>Please note that the text feedback needs to be analyzed as part
the <a href="https://edx.readthedocs.org/en/latest/">edX research of the <a href="https://edx.readthedocs.org/en/latest/">edX
dumps</a>. For now, we do not provide any kind of visual analytics research dumps</a>. For now, we do not provide any kind of visual
associated with this block.</p> analytics associated with this block. We do display numbers of
students giving specific feedback.</p>
<p>We do recommend matrix sampling. This can be done by using the <p>We do recommend matrix sampling. This can be done by using the
<a href="http://edx.readthedocs.org/projects/edx-partner-course-staff/en/latest/content_experiments/content_experiments_configure.html">randomized <a href="http://edx.readthedocs.org/projects/edx-partner-course-staff/en/latest/content_experiments/content_experiments_configure.html">randomized
control trials framework</a>.</p> control trials framework</a>.</p>
<p> We suggest asking specific questions which provide students
clear guidance. For the Likert prompt, it is helpful to be very
specific: Was this assignment easy to understand? Was the length
appropriate? For the freeform response, you should provide students
clear guidance on what kind of feedback you would like.</p>
<ul class="list-input settings-list"> <ul class="list-input settings-list">
<li class="field comp-setting-entry is-set"> <li class="field comp-setting-entry is-set">
......
...@@ -3,45 +3,57 @@ ...@@ -3,45 +3,57 @@
if (typeof Logger === 'undefined') { if (typeof Logger === 'undefined') {
var Logger = { var Logger = {
log: function(a, b) { log: function(a, b) {
console.log("<<Log>>"); console.log(JSON.stringify(a)+"/"+JSON.stringify(a));
console.log(a);
console.log(b);
console.log("<</Log>>");
} }
}; };
} }
function RateXBlock(runtime, element) { function RateXBlock(runtime, element) {
var feedback_handler = runtime.handlerUrl(element, 'feedback'); function likert_vote() {
$(".rate_submit_feedback", element).click(function(eventObject) {
var freeform = $(".rate_freeform_area", element).val();
var vote = 0; var vote = 0;
if ($(".rate_radio:checked", element).length === 0) { if ($(".rate_radio:checked", element).length === 0) {
vote = -1; vote = -1;
} else { } else {
vote = parseInt($(".rate_radio:checked", element).attr("id").split("_")[1]); vote = parseInt($(".rate_radio:checked", element).attr("id").split("_")[1]);
} }
var feedback = {"freeform": freeform, return vote;
"vote": vote}; }
function feedback() {
return $(".rate_freeform_area", element).val();
}
function submit_feedback(freeform, vote) {
var feedback = {};
if(freeform) {
feedback['freeform'] = freeform;
}
if(vote != -1) {
feedback['vote'] = vote;
}
Logger.log("edx.ratexblock.submitted", feedback); Logger.log("edx.ratexblock.submitted", feedback);
$.ajax({ $.ajax({
type: "POST", type: "POST",
url: feedback_handler, url: runtime.handlerUrl(element, 'feedback'),
data: JSON.stringify(feedback), data: JSON.stringify(feedback),
success: function(data) {$('.rate_thank_you', element).text(data.response);} success: function(data) {
$('.rate_thank_you', element).text("");
$('.rate_thank_you', element).text(data.response);
}
}); });
}
$(".rate_submit_feedback", element).click(function(eventObject) {
submit_feedback(feedback(), -1);
}); });
$('.rate_radio', element).change(function(eventObject) { $('.rate_radio', element).change(function(eventObject) {
var target_id = eventObject.target.id; Logger.log("edx.ratexblock.likert_changed", {"vote":likert_vote()});
var vote = parseInt(target_id.split('_')[1]); submit_feedback(false, likert_vote());
Logger.log("edx.ratexblock.likert_clicked", {"vote":vote});
}); });
$('.rate_freeform_area', element).change(function(eventObject) { $('.rate_freeform_area', element).change(function(eventObject) {
var freeform = eventObject.currentTarget.value; Logger.log("edx.ratexblock.freeform_changed", {"freeform":feedback()});
Logger.log("edx.ratexblock.freeform_changed", {"freeform":freeform});
}); });
} }
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