Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
6f6a4517
Commit
6f6a4517
authored
Jun 12, 2013
by
Felix Sun
Committed by
Felix Sun
Jul 01, 2013
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Working prototype of crowdsourced hinting module.
Conflicts: common/static/coffee/src/logger.coffee
parent
712f17a4
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
356 additions
and
1 deletions
+356
-1
common/lib/xmodule/setup.py
+1
-0
common/lib/xmodule/xmodule/crowdsource_hinter.py
+262
-0
common/lib/xmodule/xmodule/js/src/capa/display.coffee
+1
-0
common/lib/xmodule/xmodule/js/src/crowdsource_hinter/display.coffee
+63
-0
common/static/coffee/src/logger.coffee
+29
-1
No files found.
common/lib/xmodule/setup.py
View file @
6f6a4517
...
...
@@ -55,6 +55,7 @@ setup(
"word_cloud = xmodule.word_cloud_module:WordCloudDescriptor"
,
"hidden = xmodule.hidden_module:HiddenDescriptor"
,
"raw = xmodule.raw_module:RawDescriptor"
,
"crowdsource_hinter = xmodule.crowdsource_hinter:CrowdsourceHinterDescriptor"
,
],
'console_scripts'
:
[
'xmodule_assets = xmodule.static_content:main'
,
...
...
common/lib/xmodule/xmodule/crowdsource_hinter.py
0 → 100644
View file @
6f6a4517
import
logging
import
copy
import
json
import
os
import
re
import
string
import
random
from
pkg_resources
import
resource_listdir
,
resource_string
,
resource_isdir
from
lxml
import
etree
from
xmodule.modulestore
import
Location
from
xmodule.modulestore.exceptions
import
ItemNotFoundError
from
xmodule.x_module
import
XModule
from
xmodule.xml_module
import
XmlDescriptor
from
xblock.core
import
XBlock
,
Scope
,
String
,
Integer
,
Float
,
Object
,
Boolean
from
django.utils.html
import
escape
log
=
logging
.
getLogger
(
__name__
)
class
CrowdsourceHinterFields
(
object
):
has_children
=
True
hints
=
Object
(
help
=
'''A dictionary mapping answers to lists of [hint, number_of_votes] pairs.
'''
,
scope
=
Scope
.
content
,
default
=
{
'4'
:
[[
'This is a hint.'
,
5
],
[
'This is hint 2'
,
3
],
[
'This is hint 3'
,
2
],
[
'This is hint 4'
,
1
]]})
'''
Testing data for hints:
'''
previous_answers
=
Object
(
help
=
'''A list of previous answers this student made to this problem.
Of the form (answer, (hint_id_1, hint_id_2, hint_id_3)) for each problem. hint_id's are
None if the hint was not given.'''
,
scope
=
Scope
.
user_state
,
default
=
[])
user_voted
=
Boolean
(
help
=
'Specifies if the user has voted on this problem or not.'
,
scope
=
Scope
.
user_state
,
default
=
False
)
class
CrowdsourceHinterModule
(
CrowdsourceHinterFields
,
XModule
):
''' An Xmodule that makes crowdsourced hints.
'''
icon_class
=
'crowdsource_hinter'
js
=
{
'coffee'
:
[
resource_string
(
__name__
,
'js/src/crowdsource_hinter/display.coffee'
),
],
'js'
:
[]}
js_module_name
=
"Hinter"
def
__init__
(
self
,
system
,
location
,
descriptor
,
model_data
):
XModule
.
__init__
(
self
,
system
,
location
,
descriptor
,
model_data
)
def
get_html
(
self
):
'''
Does a regular expression find and replace to change the AJAX url.
- Dependent on lon-capa problem.
'''
# Reset the user vote, for debugging only! Remove for prod.
self
.
user_voted
=
False
for
child
in
self
.
get_display_items
():
out
=
child
.
get_html
()
# The event listener uses the ajax url to find the child.
child_url
=
child
.
system
.
ajax_url
break
# Wrap the module in a <section>. This lets us pass data attributes to the javascript.
out
+=
'<section class="crowdsource-wrapper" data-url="'
+
self
.
system
.
ajax_url
+
\
'" data-child-url = "'
+
child_url
+
'"> </section>'
return
out
def
capa_make_answer_hashable
(
self
,
answer
):
'''
Capa answer format: dict[problem name] -> [list of answers]
Output format: ((problem name, (answers)))
'''
out
=
[]
for
problem
,
a
in
answer
.
items
():
out
.
append
((
problem
,
tuple
(
a
)))
return
str
(
tuple
(
sorted
(
out
)))
def
ans_to_text
(
self
,
answer
):
'''
Converts capa answer format to a string representation
of the answer.
-Lon-capa dependent.
'''
return
answer
.
values
()[
0
][
0
]
def
handle_ajax
(
self
,
dispatch
,
get
):
'''
This is the landing method for AJAX calls.
'''
if
dispatch
==
'get_hint'
:
return
self
.
get_hint
(
get
)
if
dispatch
==
'get_feedback'
:
return
self
.
get_feedback
(
get
)
if
dispatch
==
'vote'
:
return
self
.
tally_vote
(
get
)
if
dispatch
==
'submit_hint'
:
return
self
.
submit_hint
(
get
)
def
get_hint
(
self
,
get
):
'''
The student got the incorrect answer found in get. Give him a hint.
'''
print
self
.
hints
answer
=
self
.
ans_to_text
(
get
)
# Look for a hint to give.
if
answer
not
in
self
.
hints
:
# No hints to give. Return.
self
.
previous_answers
+=
[(
answer
,
(
None
,
None
,
None
))]
return
json
.
dumps
({
'contents'
:
' '
})
# Get the top hint, plus two random hints.
n_hints
=
len
(
self
.
hints
[
answer
])
best_hint_index
=
max
(
xrange
(
n_hints
),
key
=
lambda
i
:
self
.
hints
[
answer
][
i
][
1
])
best_hint
=
self
.
hints
[
answer
][
best_hint_index
][
0
]
if
len
(
self
.
hints
[
answer
])
==
1
:
rand_hint_1
=
''
rand_hint_2
=
''
self
.
previous_answers
+=
[(
answer
,
(
0
,
None
,
None
))]
elif
len
(
self
.
hints
[
answer
])
==
2
:
best_hint
=
self
.
hints
[
answer
][
0
][
0
]
rand_hint_1
=
self
.
hints
[
answer
][
1
][
0
]
rand_hint_2
=
''
self
.
previous_answers
+=
[(
answer
,
(
0
,
1
,
None
))]
else
:
hint_index_1
,
hint_index_2
=
random
.
sample
(
xrange
(
len
(
self
.
hints
[
answer
])),
2
)
rand_hint_1
=
self
.
hints
[
answer
][
hint_index_1
][
0
]
rand_hint_2
=
self
.
hints
[
answer
][
hint_index_2
][
0
]
self
.
previous_answers
+=
[(
answer
,
(
best_hint_index
,
hint_index_1
,
hint_index_2
))]
hint_text
=
best_hint
+
'<br />'
+
rand_hint_1
+
'<br />'
+
rand_hint_2
return
json
.
dumps
({
'contents'
:
hint_text
})
def
get_feedback
(
self
,
get
):
'''
The student got it correct. Ask him to vote on hints, or submit a hint.
'''
# The student got it right.
# Did he submit at least one wrong answer?
out
=
' '
if
len
(
self
.
previous_answers
)
==
0
:
# No. Nothing to do here.
return
json
.
dumps
({
'contents'
:
out
})
# Make a hint-voting interface for each wrong answer. The student will only
# be allowed to make one vote / submission, but he can choose which wrong answer
# he wants to look at.
pretty_answers
=
[]
for
i
in
xrange
(
len
(
self
.
previous_answers
)):
answer
,
hints_offered
=
self
.
previous_answers
[
i
]
pretty_answers
.
append
(
answer
)
# If there are previous hints for this answer, ask the student to vote on one.
if
answer
in
self
.
hints
:
out
+=
'<div class = "previous-answer" id="previous-answer-'
+
str
(
i
)
+
\
'" style="display:none"> Which hint was most helpful when you got the wrong answer of '
\
+
answer
+
'?'
# Add each hint to the html string, with a vote button.
for
j
,
hint_id
in
enumerate
(
hints_offered
):
if
hint_id
!=
None
:
out
+=
'<br /><input class="vote" data-answer="'
+
str
(
i
)
+
'" data-hintno="'
+
str
(
j
)
+
\
'" type="button" value="Vote"> '
+
self
.
hints
[
answer
][
hint_id
][
0
]
# Or, let the student create his own hint
out
+=
'''<br /> If you didn
\'
t like any of these, plese submit your own: <br />
<textarea cols="50" id="custom-hint-'''
+
str
(
i
)
+
'''">
What would you say to help someone who got this wrong answer?
(Don't give away the answer, please.)
</textarea>'''
out
+=
'<input class="submit-hint" data-answer="'
+
str
(
i
)
+
'" type="button" value="submit">'
# Close the .previous-answer div.
out
+=
'</div>'
# Add preamble.
out2
=
'''Help us improve our hinting system by voting on the hint that was most helpful
to you. Start by picking one of your previous incorrect answers from below: <br />
<select id="feedback-select">'''
for
i
,
answer
in
enumerate
(
pretty_answers
):
out2
+=
'<option value='
+
str
(
i
)
+
'>'
+
str
(
answer
)
+
'</option>'
out2
+=
'</select><br />'
return
json
.
dumps
({
'contents'
:
out2
+
out
})
def
tally_vote
(
self
,
get
):
'''
Tally a user's vote on his favorite hint.
get:
'answer': ans_no (index in previous_answers)
'hint': hint_no
'''
if
self
.
user_voted
:
return
json
.
dumps
({
'contents'
:
'Sorry, but you have already voted!'
})
ans_no
=
int
(
get
[
'answer'
])
hint_no
=
int
(
get
[
'hint'
])
answer
=
self
.
previous_answers
[
ans_no
][
0
]
temp_dict
=
self
.
hints
temp_dict
[
answer
][
hint_no
][
1
]
+=
1
# Awkward, but you need to do a direct write for the database to update.
self
.
hints
=
temp_dict
# Don't let the user vote again!
self
.
user_voted
=
True
# Reset self.previous_answers.
self
.
previous_answers
=
[]
# In the future, return a list of how many votes each hint got, maybe?
return
json
.
dumps
({
'contents'
:
'Congrats, you
\'
ve voted!'
})
def
submit_hint
(
self
,
get
):
'''
Take a hint submission and add it to the database.
get:
'answer': answer index in previous_answers
'hint': text of the new hint that the user is adding
'''
# Do html escaping. Perhaps in the future do profanity filtering, etc. as well.
hint
=
escape
(
get
[
'hint'
])
answer
=
self
.
previous_answers
[
int
(
get
[
'answer'
])][
0
]
# Add the new hint to self.hints. (Awkward because a direct write
# is necessary.)
temp_dict
=
self
.
hints
temp_dict
[
answer
]
.
append
([
hint
,
1
])
# With one vote (the user himself).
self
.
hints
=
temp_dict
# Mark the user has having voted; reset previous_answers
self
.
user_voted
=
True
self
.
previous_answers
=
[]
return
json
.
dumps
({
'contents'
:
'Thank you for your hint!'
})
class
CrowdsourceHinterDescriptor
(
CrowdsourceHinterFields
,
XmlDescriptor
):
module_class
=
CrowdsourceHinterModule
stores_state
=
True
@classmethod
def
definition_from_xml
(
cls
,
xml_object
,
system
):
children
=
[]
for
child
in
xml_object
:
try
:
children
.
append
(
system
.
process_xml
(
etree
.
tostring
(
child
,
encoding
=
'unicode'
))
.
location
.
url
())
except
Exception
as
e
:
log
.
exception
(
"Unable to load child when parsing CrowdsourceHinter. Continuing..."
)
if
system
.
error_tracker
is
not
None
:
system
.
error_tracker
(
"ERROR: "
+
str
(
e
))
continue
return
{},
children
def
definition_to_xml
(
self
,
resource_fs
):
xml_object
=
etree
.
Element
(
'crowdsource_hinter'
)
for
child
in
self
.
get_children
():
xml_object
.
append
(
etree
.
fromstring
(
child
.
export_to_xml
(
resource_fs
)))
return
xml_object
\ No newline at end of file
common/lib/xmodule/xmodule/js/src/capa/display.coffee
View file @
6f6a4517
...
...
@@ -223,6 +223,7 @@ class @Problem
@
el
.
removeClass
'showed'
else
@
gentle_alert
response
.
success
Logger
.
log
'problem_graded'
,
[
@
answers
,
response
.
contents
],
@
url
reset
:
=>
Logger
.
log
'problem_reset'
,
@
answers
...
...
common/lib/xmodule/xmodule/js/src/crowdsource_hinter/display.coffee
0 → 100644
View file @
6f6a4517
class
@
Hinter
constructor
:
(
element
)
->
@
el
=
$
(
element
).
find
(
'.crowdsource-wrapper'
)
@
url
=
@
el
.
data
(
'url'
)
Logger
.
listen
(
'problem_graded'
,
@
el
.
data
(
'child-url'
),
@
capture_problem
)
# The line below will eventually be generated by Python.
@
render
()
capture_problem
:
(
event_type
,
data
,
element
)
=>
# After a problem gets graded, we get the info here.
# We want to send this info to the server in another AJAX
# request.
answers
=
data
[
0
]
response
=
data
[
1
]
if
response
.
search
(
/class="correct "/
)
==
-
1
# Incorrect. Get hints.
$
.
postWithPrefix
"
#{
@
url
}
/get_hint"
,
answers
,
(
response
)
=>
@
render
(
response
.
contents
)
else
# Correct. Get feedback from students.
$
.
postWithPrefix
"
#{
@
url
}
/get_feedback"
,
answers
,
(
response
)
=>
@
render
(
response
.
contents
)
$
:
(
selector
)
->
$
(
selector
,
@
el
)
bind
:
=>
window
.
update_schematics
()
@
$
(
'input.vote'
).
click
@
vote
@
$
(
'#feedback-select'
).
change
@
feedback_ui_change
@
$
(
'input.submit-hint'
).
click
@
submit_hint
vote
:
(
eventObj
)
=>
target
=
@
$
(
eventObj
.
currentTarget
)
post_json
=
{
'answer'
:
target
.
data
(
'answer'
),
'hint'
:
target
.
data
(
'hintno'
)}
$
.
postWithPrefix
"
#{
@
url
}
/vote"
,
post_json
,
(
response
)
=>
@
render
(
response
.
contents
)
submit_hint
:
(
eventObj
)
=>
target
=
@
$
(
eventObj
.
currentTarget
)
textarea_id
=
'#custom-hint-'
+
target
.
data
(
'answer'
)
console
.
debug
(
textarea_id
)
post_json
=
{
'answer'
:
target
.
data
(
'answer'
),
'hint'
:
@
$
(
textarea_id
).
val
()}
$
.
postWithPrefix
"
#{
@
url
}
/submit_hint"
,
post_json
,
(
response
)
=>
@
render
(
response
.
contents
)
feedback_ui_change
:
=>
# Make all of the previous-answer divs hidden.
@
$
(
'.previous-answer'
).
css
(
'display'
,
'none'
)
# But, now find the selected div, and make it visible.
selector
=
'#previous-answer-'
+
@
$
(
'#feedback-select option:selected'
).
attr
(
'value'
)
@
$
(
selector
).
css
(
'display'
,
'inline'
)
render
:
(
content
)
->
if
content
@
el
.
html
(
content
)
JavascriptLoader
.
executeModuleScripts
@
el
,
()
=>
@
bind
()
@
$
(
'#previous-answer-0'
).
css
(
'display'
,
'inline'
)
\ No newline at end of file
common/static/coffee/src/logger.coffee
View file @
6f6a4517
class
@
Logger
# events we want sent to Segment.io for tracking
SEGMENT_IO_WHITELIST
=
[
"seq_goto"
,
"seq_next"
,
"seq_prev"
,
"problem_check"
,
"problem_reset"
,
"problem_show"
,
"problem_save"
]
@
log
:
(
event_type
,
data
)
->
# listeners[event_type][element] -> list of callbacks
listeners
=
{}
@
log
:
(
event_type
,
data
,
element
=
null
)
->
# Segment.io event tracking
if
event_type
in
SEGMENT_IO_WHITELIST
# to avoid changing the format of data sent to our servers, we only massage it here
...
...
@@ -11,11 +14,36 @@ class @Logger
else
analytics
.
track
event_type
,
data
# Check to see if we're listening for the event type.
if
event_type
of
listeners
# Cool. Do the elements also match?
# null element in the listener dictionary means any element will do.
# null element in the @log call means we don't know the element name.
if
null
of
listeners
[
event_type
]
# Make the callbacks.
for
callback
in
listeners
[
event_type
][
null
]
callback
(
event_type
,
data
,
element
)
else
if
element
of
listeners
[
event_type
]
for
callback
in
listeners
[
event_type
][
element
]
callback
(
event_type
,
data
,
element
)
# Regardless of whether any callbacks were made, log this event.
$
.
getWithPrefix
'/event'
,
event_type
:
event_type
event
:
JSON
.
stringify
(
data
)
page
:
window
.
location
.
href
@
listen
:
(
event_type
,
element
,
callback
)
->
# Add a listener. If you want any element to trigger this listener,
# do element = null
if
event_type
not
of
listeners
listeners
[
event_type
]
=
{}
if
element
not
of
listeners
[
event_type
]
listeners
[
event_type
][
element
]
=
[
callback
]
else
listeners
[
event_type
][
element
].
push
callback
@
bind
:
->
window
.
onunload
=
->
$
.
ajaxWithPrefix
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment