Commit 685ebd24 by Piotr Mitros

Add Studio view. Remerge student view from instructor view. Build out icon sets, etc.

parent a801462b
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""
This file is just the bok_choy pages file for our icons. bok_choy
is a test framework, so we follow this split into pages/icon, although
it is a little artifical in this case.
"""
from bok_choy.page_object import PageObject from bok_choy.page_object import PageObject
class IconsPage(PageObject): class IconsPage(PageObject):
""" """
GitHub's search page We render our icons from HTML using bok_choy. bok_choy prefers
working from a real web URL, and this is a page for the URL of the
HTML file on github at the time of development
(`pmitros/ux-revamp` branch). If we continue developing, this URL
may need to change to `master` or otherwise. This should move from
pmitros/ux-revamp branch to master at some point
""" """
url = 'https://rawgit.com/pmitros/RateXBlock/pmitros/ux-revamp/makeicons/raw_icons.html' url = '''
https://rawgit.com/pmitros/RateXBlock/pmitros/ux-revamp/makeicons/raw_icons.html
'''.strip()
def is_browser_on_page(self): def is_browser_on_page(self):
'''
Check whether we have the fifth selected numeric icon. This is
towards the bottom of the page.
'''
return self.q(css='#snum5').is_present() return self.q(css='#snum5').is_present()
...@@ -26,8 +26,8 @@ class TestIcons(WebAppTest): ...@@ -26,8 +26,8 @@ class TestIcons(WebAppTest):
for i in range(5): for i in range(5):
for icon in ["face", "num"]: for icon in ["face", "num"]:
for style in "ais": for style in "ais":
self.assertScreenshot("#"+style+icon+str(i+1), style+icon+str(i+1)) self.assertScreenshot("#"+style+icon+str(i+1),
style+icon+str(i+1))
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()
...@@ -5,6 +5,7 @@ course resources, and to think and synthesize about their experience ...@@ -5,6 +5,7 @@ course resources, and to think and synthesize about their experience
in the course. in the course.
""" """
import cgi
import random import random
import pkg_resources import pkg_resources
...@@ -13,20 +14,24 @@ from xblock.core import XBlock ...@@ -13,20 +14,24 @@ 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
We provide default text which is designed to elicit student thought. We'd # like instructors to customize this to something highly structured (not
like instructors to customize this to something highly structured (not # "What did you think?" and "How did you like it?".
"What did you think?" and "How did you like it?". DEFAULT_FREEFORM = "What did you learn from this? What was missing?"
""" DEFAULT_LIKERT = "How would you rate this as a learning experience?"
default_freeform = "What did you learn from this? What was missing?" DEFAULT_DEFAULT = "Think about the material, and try to synthesize key " \
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." "lessons learned, as well as key gaps in our presentation."
default_placeholder = "Take a little bit of time to reflect here. " \ DEFAULT_PLACEHOLDER = "Take a little bit of time to reflect here. " \
"Research shows that a meaningful synthesis will help " \ "Research shows that a meaningful synthesis will help " \
"you better understand and remember material from this" \ "you better understand and remember material from this" \
"course." "course."
default_icon = "face" DEFAULT_ICON = "face"
DEFAULT_SCALETEXT = ["Excellent", "Good", "Average", "Fair", "Poor"]
ICON_SETS = {'face': u"😁😊😐😞😭",
'num': u"12345",
'midface': u"😞😐😊😐😞"}
@XBlock.needs('i18n') @XBlock.needs('i18n')
class RateXBlock(XBlock): class RateXBlock(XBlock):
...@@ -42,10 +47,12 @@ class RateXBlock(XBlock): ...@@ -42,10 +47,12 @@ 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': default_freeform, {'freeform': DEFAULT_FREEFORM,
'default_text': default_default, 'default_text': DEFAULT_DEFAULT,
'likert': default_likert, 'likert': DEFAULT_LIKERT,
'placeholder': default_placeholder} 'placeholder': DEFAULT_PLACEHOLDER,
'scale_text': DEFAULT_SCALETEXT,
'icon_set': DEFAULT_ICON}
], ],
scope=Scope.settings, scope=Scope.settings,
help="Freeform user prompt", help="Freeform user prompt",
...@@ -62,6 +69,7 @@ class RateXBlock(XBlock): ...@@ -62,6 +69,7 @@ class RateXBlock(XBlock):
help="How user voted. -1 if didn't vote" help="How user voted. -1 if didn't vote"
) )
# pylint: disable=invalid-name
p = Float( p = Float(
default=100, scope=Scope.settings, default=100, scope=Scope.settings,
help="What percent of the time should this show?" help="What percent of the time should this show?"
...@@ -86,7 +94,8 @@ class RateXBlock(XBlock): ...@@ -86,7 +94,8 @@ class RateXBlock(XBlock):
scopde=Scope.settings scopde=Scope.settings
) )
def resource_string(self, path): @classmethod
def resource_string(cls, path):
"""Handy helper for getting resources from our kit.""" """Handy helper for getting resources from our kit."""
data = pkg_resources.resource_string(__name__, path) data = pkg_resources.resource_string(__name__, path)
return data.decode("utf8") return data.decode("utf8")
...@@ -101,6 +110,14 @@ class RateXBlock(XBlock): ...@@ -101,6 +110,14 @@ class RateXBlock(XBlock):
index = self.prompt_choice index = self.prompt_choice
_ = self.runtime.service(self, 'i18n').ugettext _ = self.runtime.service(self, 'i18n').ugettext
# This is the default prompt if something is not specified in the
# settings dictionary. Note that this is not the same as the default
# above. The default above is the prompt the instructor starts from
# in a tool like Studio. This is a fallback in case some JSON fields
# are left unpopulated (e.g. if someone manually tweaks the database,
# in case of OLX authoring, and similar). The examplar above is
# intended as a well-structured, coherent response. This is designed
# as generic, to work with any content as a safe fallback.
prompt = { prompt = {
'freeform': _("Please reflect on this course material"), 'freeform': _("Please reflect on this course material"),
'default_text': _("Please take time to meaningfully reflect " 'default_text': _("Please take time to meaningfully reflect "
...@@ -112,8 +129,8 @@ class RateXBlock(XBlock): ...@@ -112,8 +129,8 @@ class RateXBlock(XBlock):
_("Average"), _("Average"),
_("Fair"), _("Fair"),
_("Poor")], _("Poor")],
'icons': [u"😁", u"😊", u"😐", u"😞", u"😭"], 'icon_set': 'num',
'placeholder': ["Please take a moment to thoughtfully reflect."] 'placeholder': "Please take a moment to thoughtfully reflect."
} }
prompt.update(self.prompts[index]) prompt.update(self.prompts[index])
...@@ -146,7 +163,7 @@ class RateXBlock(XBlock): ...@@ -146,7 +163,7 @@ class RateXBlock(XBlock):
# We have five Likert fields right now, but we'd like this to # We have five Likert fields right now, but we'd like this to
# be dynamic # be dynamic
indexes = range(len(prompt['icons'])) indexes = range(5)
# If the user voted before, we'd like to show that # 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 ""
...@@ -159,20 +176,27 @@ class RateXBlock(XBlock): ...@@ -159,20 +176,27 @@ class RateXBlock(XBlock):
# We grab the icons. This should move to a Filesystem field so # We grab the icons. This should move to a Filesystem field so
# instructors can upload new ones # instructors can upload new ones
ina_templ = 'public/default_icons/iface{i}.png' def get_url(icon_type, i):
act_templ = 'public/default_icons/aface{i}.png' '''
sel_templ = 'public/default_icons/sface{i}.png' Helper function to generate the URL for the icons shown in the
ina_urls = [self.runtime.local_resource_url(self, ina_templ.format(i=i)) tool. Takes the type of icon (active, inactive, etc.) and
for i in range(1,6)] the number of the icon.
act_urls = [self.runtime.local_resource_url(self, act_templ.format(i=i))
for i in range(1,6)] Note that some icon types may not be actively used in the
sel_urls = [self.runtime.local_resource_url(self, sel_templ.format(i=i)) styling. For example, at the time of this writing, we do
for i in range(1,6)] selected through CSS, rather than by using those icons.
img_urls = [i if active else a '''
for (i, active, a) templates = {'inactive': 'public/default_icons/i{set}{i}.png',
in zip(ina_urls, active_vote, act_urls)] 'active': 'public/default_icons/a{set}{i}.png',
'selected': 'public/default_icons/s{set}{i}.png'}
# Render the template = templates[icon_type]
icon_file = template.format(i=i, set=prompt['icon_set'])
return self.runtime.local_resource_url(self, icon_file)
ina_urls = [get_url('inactive', i) for i in range(1, 6)]
act_urls = [get_url('active', i) for i in range(1, 6)]
sel_urls = [get_url('selected', i) for i in range(1, 6)]
# Render the Likert scale (not the whole page)
scale = u"".join( scale = u"".join(
scale_item.format(scale_text=scale_text, scale_item.format(scale_text=scale_text,
unicode_icon=unicode_icon, unicode_icon=unicode_icon,
...@@ -191,7 +215,7 @@ class RateXBlock(XBlock): ...@@ -191,7 +215,7 @@ class RateXBlock(XBlock):
ina_icon, ina_icon,
sel_icon) in sel_icon) in
zip(prompt['scale_text'], zip(prompt['scale_text'],
prompt['icons'], ICON_SETS[(prompt['icon_set'])],
indexes, indexes,
active_vote, active_vote,
votes, votes,
...@@ -209,7 +233,8 @@ class RateXBlock(XBlock): ...@@ -209,7 +233,8 @@ class RateXBlock(XBlock):
scale=scale, scale=scale,
freeform_prompt=prompt['freeform'], freeform_prompt=prompt['freeform'],
likert_prompt=prompt['likert'], likert_prompt=prompt['likert'],
response=response) response=response,
placeholder=prompt['placeholder'])
# We initialize self.p_user if not initialized -- this sets whether # We initialize self.p_user if not initialized -- this sets whether
# or not we show it. From there, if it is less than odds of showing, # or not we show it. From there, if it is less than odds of showing,
...@@ -236,6 +261,8 @@ class RateXBlock(XBlock): ...@@ -236,6 +261,8 @@ class RateXBlock(XBlock):
""" """
html_str = self.resource_string("static/html/studio_view.html") html_str = self.resource_string("static/html/studio_view.html")
prompt = self.get_prompt(0) prompt = self.get_prompt(0)
for idx in range(len(prompt['scale_text'])):
prompt['likert{i}'.format(i=idx)] = prompt['scale_text'][idx]
frag = Fragment(unicode(html_str).format(**prompt)) frag = Fragment(unicode(html_str).format(**prompt))
js_str = self.resource_string("static/js/src/studio.js") js_str = self.resource_string("static/js/src/studio.js")
frag.add_javascript(unicode(js_str)) frag.add_javascript(unicode(js_str))
...@@ -247,12 +274,30 @@ class RateXBlock(XBlock): ...@@ -247,12 +274,30 @@ class RateXBlock(XBlock):
""" """
Called when submitting the form in Studio. Called when submitting the form in Studio.
""" """
self.prompts[0]['freeform'] = data.get('freeform') print "Received: ", data
self.prompts[0]['likert'] = data.get('likert') print "Old prompt: ", self.prompts[0]
for item in ['freeform', 'likert', 'placeholder', 'icon_set']:
item_submission = data.get(item, None)
if item_submission and len(item_submission) > 0:
print "Setting", item
self.prompts[0][item] = cgi.escape(item_submission)
for i in range(5):
likert = data.get('likert{i}'.format(i=i), None)
if likert and len(likert) > 0:
print "Setting", i
self.prompts[0]['scale_text'][i] = cgi.escape(likert)
print "New prompt: ", self.prompts[0]
return {'result': 'success'} return {'result': 'success'}
def init_vote_aggregate(self): def init_vote_aggregate(self):
# Make sure we're initialized '''
There are a lot of places we read the aggregate vote counts. We
start out with these uninitialized. This guarantees they are
initialized. We'd prefer to do it this way, rather than default
value, since we do plan to not force scale length to be 5 in the
future.
'''
if not self.vote_aggregate: if not self.vote_aggregate:
self.vote_aggregate = [0] * (len(self.get_prompt()['scale_text'])) self.vote_aggregate = [0] * (len(self.get_prompt()['scale_text']))
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
<label class="rate_header" for="rate_freeform_textarea">{freeform_prompt}</label> <label class="rate_header" for="rate_freeform_textarea">{freeform_prompt}</label>
</div> </div>
<div class="rate_freeform_input"> <div class="rate_freeform_input">
<textarea id="rate_freeform_textarea" class="rate_freeform_area" rows="6" cols="50">{self.user_freeform}</textarea> <textarea id="rate_freeform_textarea" class="rate_freeform_area" rows="6" cols="45" placeholder="{placeholder}">{self.user_freeform}</textarea>
<div class="rate_thank_you" aria-live=polite>{response}</div> <div class="rate_thank_you" aria-live=polite>{response}</div>
</div> </div>
<button type="submit" class="rate_submit_feedback"> Submit Feedback </button> <button type="submit" class="rate_submit_feedback"> Submit Feedback </button>
......
<div style="display:inline-block; text-align:center; padding:0px 10px 0px 10px;"> <div class="rate_likert_rating">
<label title="{scale_text}"> <label title="{scale_text}" class="rate_likert_label">
<input id="radio_{idx}" name="rate_scale" class="rate_radio" type="radio" {active}/> <input id="radio_{idx}" name="rate_scale" class="rate_radio" type="radio" {active}/>
<img src="{ina_icon}"/> <span class="rate_icon rate_icon_inactive">
<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/>
<span>{scale_text}</span> <span>{scale_text}</span>
</label> </label>
......
...@@ -2,21 +2,15 @@ ...@@ -2,21 +2,15 @@
<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 text feedback needs to be analyzed as part
of the <a href="https://edx.readthedocs.org/en/latest/">edX
research dumps</a>. For now, we do not provide any kind of visual
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
<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>
<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">
<p> Prior to using this block, we recommend reading about best
practices in the edX documentation. In particular, we do recommend
asking for structured, specific feedback, and we advise using this
in conjunction with
the <a href="http://edx.readthedocs.org/projects/edx-partner-course-staff/en/latest/content_experiments/content_experiments_configure.html">RCT
framework</a> for matrix sampling.</p>
<ul class="list-input settings-list">
<li class="field comp-setting-entry is-set"> <li class="field comp-setting-entry is-set">
<div class="wrapper-comp-setting"> <div class="wrapper-comp-setting">
<label class="label setting-label" for="freeform">Freeform prompt</label> <label class="label setting-label" for="freeform">Freeform prompt</label>
...@@ -27,12 +21,71 @@ ...@@ -27,12 +21,71 @@
<li class="field comp-setting-entry is-set"> <li class="field comp-setting-entry is-set">
<div class="wrapper-comp-setting"> <div class="wrapper-comp-setting">
<label class="label setting-label" for="placeholder">Freeform placeholder</label>
<input class="input setting-input" name="placeholder" id="placeholder" value="{placeholder}" type="text" />
</div>
<span class="tip setting-help">This is shown as grayed out text before the student has answered.</span>
</li>
<li class="field comp-setting-entry is-set">
<div class="wrapper-comp-setting">
<label class="label setting-label" for="icon_set">Likert icon set</label>
<select name="icon_set">
<option value="face">Faces (happy-to-sad)</option>
<option value="midface">Faces (sad-to-happy-to-sad)</option>
<option value="num">Numeric</option></select>
</div>
<span class="tip setting-help">We can either show happy/sad faces, or numbers 1-5.</span>
</li>
<li class="field comp-setting-entry is-set">
<div class="wrapper-comp-setting">
<label class="label setting-label" for="likert">Likert prompt</label> <label class="label setting-label" for="likert">Likert prompt</label>
<input class="input setting-input" name="likert" id="likert" value="{likert}" type="text" /> <input class="input setting-input" name="likert" id="likert" value="{likert}" type="text" />
</div> </div>
<span class="tip setting-help">Example: Please rate your overall experience with this section.</span> <span class="tip setting-help">Example: Please rate your overall experience with this section.</span>
</li> </li>
<li class="field comp-setting-entry is-set">
<div class="wrapper-comp-setting">
<label class="label setting-label" for="likert0">Likert option 1</label>
<input class="input setting-input" name="likert0" id="likert0" value="{likert0}" type="text" />
</div>
<span class="tip setting-help">Example: Excellent!</span>
</li>
<li class="field comp-setting-entry is-set">
<div class="wrapper-comp-setting">
<label class="label setting-label" for="likert1">Likert option 2</label>
<input class="input setting-input" name="likert1" id="likert1" value="{likert1}" type="text" />
</div>
<span class="tip setting-help">Example: Good</span>
</li>
<li class="field comp-setting-entry is-set">
<div class="wrapper-comp-setting">
<label class="label setting-label" for="likert2">Likert option 3</label>
<input class="input setting-input" name="likert2" id="likert2" value="{likert2}" type="text" />
</div>
<span class="tip setting-help">Example: Average</span>
</li>
<li class="field comp-setting-entry is-set">
<div class="wrapper-comp-setting">
<label class="label setting-label" for="likert3">Likert option 4</label>
<input class="input setting-input" name="likert3" id="likert3" value="{likert3}" type="text" />
</div>
<span class="tip setting-help">Example: Fair</span>
</li>
<li class="field comp-setting-entry is-set">
<div class="wrapper-comp-setting">
<label class="label setting-label" for="likert4">Likert option 5</label>
<input class="input setting-input" name="likert4" id="likert4" value="{likert4}" type="text" />
</div>
<span class="tip setting-help">Example: Poor</span>
</li>
<div class="xblock-actions"> <div class="xblock-actions">
<ul> <ul>
<li class="action-item"> <li class="action-item">
......
...@@ -3,7 +3,14 @@ function RateBlock(runtime, element) { ...@@ -3,7 +3,14 @@ function RateBlock(runtime, element) {
var handlerUrl = runtime.handlerUrl(element, 'studio_submit'); var handlerUrl = runtime.handlerUrl(element, 'studio_submit');
var data = { var data = {
likert: $(element).find('input[name=likert]').val(), likert: $(element).find('input[name=likert]').val(),
freeform: $(element).find('input[name=freeform]').val() likert0: $(element).find('input[name=likert0]').val(),
likert1: $(element).find('input[name=likert1]').val(),
likert2: $(element).find('input[name=likert2]').val(),
likert3: $(element).find('input[name=likert3]').val(),
likert4: $(element).find('input[name=likert4]').val(),
freeform: $(element).find('input[name=freeform]').val(),
placeholder: $(element).find('input[name=placeholder]').val(),
icon_set: $(element).find('select[name=icon_set]').val()
}; };
runtime.notify('save', {state: 'start'}); runtime.notify('save', {state: 'start'});
$.post(handlerUrl, JSON.stringify(data)).done(function(response) { $.post(handlerUrl, JSON.stringify(data)).done(function(response) {
......
...@@ -78,7 +78,7 @@ class TestRate(XBlockTestCase): ...@@ -78,7 +78,7 @@ class TestRate(XBlockTestCase):
self.submit_feedback('rate_0', self.submit_feedback('rate_0',
{'freeform': 'Worked well', 'vote': 3}, {'freeform': 'Worked well', 'vote': 3},
{'freeform': 'Worked well', 'vote': 3, {'freeform': 'Worked well', 'vote': 3,
'response': vote_str, 'success': True}) 'response': feedback_str, 'success': True})
self.submit_feedback('rate_0', self.submit_feedback('rate_0',
{'vote': 4}, {'vote': 4},
{'freeform': 'Worked well', 'vote': 4, {'freeform': 'Worked well', 'vote': 4,
......
...@@ -23,7 +23,7 @@ def package_data(pkg, roots): ...@@ -23,7 +23,7 @@ def package_data(pkg, roots):
setup( setup(
name='rate-xblock', name='rate-xblock',
version='0.0', version='0.0',
description='rate XBlock', # TODO: write a better description. description='XBlock for providing feedback on course content',
packages=[ packages=[
'rate', 'rate',
'ratetests' 'ratetests'
......
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