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
b74a484f
Commit
b74a484f
authored
Oct 31, 2012
by
Vik Paruchuri
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
working on self assessment
parent
64c791b3
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
489 additions
and
153 deletions
+489
-153
common/lib/xmodule/xmodule/capa_module.py
+154
-153
common/lib/xmodule/xmodule/self_assessment_module.py
+335
-0
No files found.
common/lib/xmodule/xmodule/capa_module.py
View file @
b74a484f
...
@@ -29,10 +29,10 @@ TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?)
...
@@ -29,10 +29,10 @@ TIMEDELTA_REGEX = re.compile(r'^((?P<days>\d+?) day(?:s?))?(\s)?((?P<hours>\d+?)
def
only_one
(
lst
,
default
=
""
,
process
=
lambda
x
:
x
):
def
only_one
(
lst
,
default
=
""
,
process
=
lambda
x
:
x
):
"""
"""
If lst is empty, returns default
If lst is empty, returns default
If lst has a single element, applies process to that element and returns it
If lst has a single element, applies process to that element and returns it
Otherwise, raises an exeception
Otherwise, raises an exeception
"""
"""
if
len
(
lst
)
==
0
:
if
len
(
lst
)
==
0
:
return
default
return
default
elif
len
(
lst
)
==
1
:
elif
len
(
lst
)
==
1
:
...
@@ -43,14 +43,14 @@ def only_one(lst, default="", process=lambda x: x):
...
@@ -43,14 +43,14 @@ def only_one(lst, default="", process=lambda x: x):
def
parse_timedelta
(
time_str
):
def
parse_timedelta
(
time_str
):
"""
"""
time_str: A string with the following components:
time_str: A string with the following components:
<D> day[s] (optional)
<D> day[s] (optional)
<H> hour[s] (optional)
<H> hour[s] (optional)
<M> minute[s] (optional)
<M> minute[s] (optional)
<S> second[s] (optional)
<S> second[s] (optional)
Returns a datetime.timedelta parsed from the string
Returns a datetime.timedelta parsed from the string
"""
"""
parts
=
TIMEDELTA_REGEX
.
match
(
time_str
)
parts
=
TIMEDELTA_REGEX
.
match
(
time_str
)
if
not
parts
:
if
not
parts
:
return
return
...
@@ -71,15 +71,15 @@ class ComplexEncoder(json.JSONEncoder):
...
@@ -71,15 +71,15 @@ class ComplexEncoder(json.JSONEncoder):
class
CapaModule
(
XModule
):
class
CapaModule
(
XModule
):
'''
'''
An XModule implementing LonCapa format problems, implemented by way of
An XModule implementing LonCapa format problems, implemented by way of
capa.capa_problem.LoncapaProblem
capa.capa_problem.LoncapaProblem
'''
'''
icon_class
=
'problem'
icon_class
=
'problem'
js
=
{
'coffee'
:
[
resource_string
(
__name__
,
'js/src/capa/display.coffee'
),
js
=
{
'coffee'
:
[
resource_string
(
__name__
,
'js/src/capa/display.coffee'
),
resource_string
(
__name__
,
'js/src/collapsible.coffee'
),
resource_string
(
__name__
,
'js/src/collapsible.coffee'
),
resource_string
(
__name__
,
'js/src/javascript_loader.coffee'
),
resource_string
(
__name__
,
'js/src/javascript_loader.coffee'
),
],
],
'js'
:
[
resource_string
(
__name__
,
'js/src/capa/imageinput.js'
),
'js'
:
[
resource_string
(
__name__
,
'js/src/capa/imageinput.js'
),
resource_string
(
__name__
,
'js/src/capa/schematic.js'
)]}
resource_string
(
__name__
,
'js/src/capa/schematic.js'
)]}
...
@@ -89,7 +89,7 @@ class CapaModule(XModule):
...
@@ -89,7 +89,7 @@ class CapaModule(XModule):
def
__init__
(
self
,
system
,
location
,
definition
,
descriptor
,
instance_state
=
None
,
def
__init__
(
self
,
system
,
location
,
definition
,
descriptor
,
instance_state
=
None
,
shared_state
=
None
,
**
kwargs
):
shared_state
=
None
,
**
kwargs
):
XModule
.
__init__
(
self
,
system
,
location
,
definition
,
descriptor
,
instance_state
,
XModule
.
__init__
(
self
,
system
,
location
,
definition
,
descriptor
,
instance_state
,
shared_state
,
**
kwargs
)
shared_state
,
**
kwargs
)
self
.
attempts
=
0
self
.
attempts
=
0
self
.
max_attempts
=
None
self
.
max_attempts
=
None
...
@@ -100,7 +100,7 @@ class CapaModule(XModule):
...
@@ -100,7 +100,7 @@ class CapaModule(XModule):
if
display_due_date_string
is
not
None
:
if
display_due_date_string
is
not
None
:
self
.
display_due_date
=
dateutil
.
parser
.
parse
(
display_due_date_string
)
self
.
display_due_date
=
dateutil
.
parser
.
parse
(
display_due_date_string
)
#log.debug("Parsed " + display_due_date_string +
#log.debug("Parsed " + display_due_date_string +
#
" to " + str(self.display_due_date))
# " to " + str(self.display_due_date))
else
:
else
:
self
.
display_due_date
=
None
self
.
display_due_date
=
None
...
@@ -109,7 +109,7 @@ class CapaModule(XModule):
...
@@ -109,7 +109,7 @@ class CapaModule(XModule):
self
.
grace_period
=
parse_timedelta
(
grace_period_string
)
self
.
grace_period
=
parse_timedelta
(
grace_period_string
)
self
.
close_date
=
self
.
display_due_date
+
self
.
grace_period
self
.
close_date
=
self
.
display_due_date
+
self
.
grace_period
#log.debug("Then parsed " + grace_period_string +
#log.debug("Then parsed " + grace_period_string +
#
" to closing date" + str(self.close_date))
# " to closing date" + str(self.close_date))
else
:
else
:
self
.
grace_period
=
None
self
.
grace_period
=
None
self
.
close_date
=
self
.
display_due_date
self
.
close_date
=
self
.
display_due_date
...
@@ -137,9 +137,9 @@ class CapaModule(XModule):
...
@@ -137,9 +137,9 @@ class CapaModule(XModule):
elif
self
.
rerandomize
==
"per_student"
and
hasattr
(
self
.
system
,
'id'
):
elif
self
.
rerandomize
==
"per_student"
and
hasattr
(
self
.
system
,
'id'
):
# TODO: This line is badly broken:
# TODO: This line is badly broken:
# (1) We're passing student ID to xmodule.
# (1) We're passing student ID to xmodule.
# (2) There aren't bins of students.
-- we only want 10 or 20 randomizations, and want to assign students
# (2) There aren't bins of students. -- we only want 10 or 20 randomizations, and want to assign students
# to these bins, and may not want cohorts.
So e.g. hash(your-id, problem_id) % num_bins.
# to these bins, and may not want cohorts. So e.g. hash(your-id, problem_id) % num_bins.
#
- analytics really needs small number of bins.
# - analytics really needs small number of bins.
self
.
seed
=
system
.
id
self
.
seed
=
system
.
id
else
:
else
:
self
.
seed
=
None
self
.
seed
=
None
...
@@ -148,7 +148,7 @@ class CapaModule(XModule):
...
@@ -148,7 +148,7 @@ class CapaModule(XModule):
# TODO (vshnayder): move as much as possible of this work and error
# TODO (vshnayder): move as much as possible of this work and error
# checking to descriptor load time
# checking to descriptor load time
self
.
lcp
=
LoncapaProblem
(
self
.
definition
[
'data'
],
self
.
location
.
html_id
(),
self
.
lcp
=
LoncapaProblem
(
self
.
definition
[
'data'
],
self
.
location
.
html_id
(),
instance_state
,
seed
=
self
.
seed
,
system
=
self
.
system
)
instance_state
,
seed
=
self
.
seed
,
system
=
self
.
system
)
except
Exception
as
err
:
except
Exception
as
err
:
msg
=
'cannot create LoncapaProblem {loc}: {err}'
.
format
(
msg
=
'cannot create LoncapaProblem {loc}: {err}'
.
format
(
loc
=
self
.
location
.
url
(),
err
=
err
)
loc
=
self
.
location
.
url
(),
err
=
err
)
...
@@ -175,9 +175,9 @@ class CapaModule(XModule):
...
@@ -175,9 +175,9 @@ class CapaModule(XModule):
@property
@property
def
rerandomize
(
self
):
def
rerandomize
(
self
):
"""
"""
Property accessor that returns self.metadata['rerandomize'] in a
Property accessor that returns self.metadata['rerandomize'] in a
canonical form
canonical form
"""
"""
rerandomize
=
self
.
metadata
.
get
(
'rerandomize'
,
'always'
)
rerandomize
=
self
.
metadata
.
get
(
'rerandomize'
,
'always'
)
if
rerandomize
in
(
""
,
"always"
,
"true"
):
if
rerandomize
in
(
""
,
"always"
,
"true"
):
return
"always"
return
"always"
...
@@ -203,7 +203,7 @@ class CapaModule(XModule):
...
@@ -203,7 +203,7 @@ class CapaModule(XModule):
def
get_progress
(
self
):
def
get_progress
(
self
):
''' For now, just return score / max_score
''' For now, just return score / max_score
'''
'''
d
=
self
.
get_score
()
d
=
self
.
get_score
()
score
=
d
[
'score'
]
score
=
d
[
'score'
]
total
=
d
[
'total'
]
total
=
d
[
'total'
]
...
@@ -220,11 +220,11 @@ class CapaModule(XModule):
...
@@ -220,11 +220,11 @@ class CapaModule(XModule):
'element_id'
:
self
.
location
.
html_id
(),
'element_id'
:
self
.
location
.
html_id
(),
'id'
:
self
.
id
,
'id'
:
self
.
id
,
'ajax_url'
:
self
.
system
.
ajax_url
,
'ajax_url'
:
self
.
system
.
ajax_url
,
})
})
def
get_problem_html
(
self
,
encapsulate
=
True
):
def
get_problem_html
(
self
,
encapsulate
=
True
):
'''Return html for the problem.
Adds check, reset, save buttons
'''Return html for the problem. Adds check, reset, save buttons
as necessary based on the problem config and state.'''
as necessary based on the problem config and state.'''
try
:
try
:
html
=
self
.
lcp
.
get_html
()
html
=
self
.
lcp
.
get_html
()
...
@@ -242,15 +242,15 @@ class CapaModule(XModule):
...
@@ -242,15 +242,15 @@ class CapaModule(XModule):
html
=
msg
html
=
msg
else
:
else
:
# We're in non-debug mode, and possibly even in production. We want
# We're in non-debug mode, and possibly even in production. We want
#
to avoid bricking of problem as much as possible
# to avoid bricking of problem as much as possible
# Presumably, student submission has corrupted LoncapaProblem HTML.
# Presumably, student submission has corrupted LoncapaProblem HTML.
#
First, pull down all student answers
# First, pull down all student answers
student_answers
=
self
.
lcp
.
student_answers
student_answers
=
self
.
lcp
.
student_answers
answer_ids
=
student_answers
.
keys
()
answer_ids
=
student_answers
.
keys
()
# Some inputtypes, such as dynamath, have additional "hidden" state that
# Some inputtypes, such as dynamath, have additional "hidden" state that
#
is not exposed to the student. Keep those hidden
# is not exposed to the student. Keep those hidden
# TODO: Use regex, e.g. 'dynamath' is suffix at end of answer_id
# TODO: Use regex, e.g. 'dynamath' is suffix at end of answer_id
hidden_state_keywords
=
[
'dynamath'
]
hidden_state_keywords
=
[
'dynamath'
]
for
answer_id
in
answer_ids
:
for
answer_id
in
answer_ids
:
...
@@ -258,17 +258,17 @@ class CapaModule(XModule):
...
@@ -258,17 +258,17 @@ class CapaModule(XModule):
if
answer_id
.
find
(
hidden_state_keyword
)
>=
0
:
if
answer_id
.
find
(
hidden_state_keyword
)
>=
0
:
student_answers
.
pop
(
answer_id
)
student_answers
.
pop
(
answer_id
)
#
Next, generate a fresh LoncapaProblem
# Next, generate a fresh LoncapaProblem
self
.
lcp
=
LoncapaProblem
(
self
.
definition
[
'data'
],
self
.
location
.
html_id
(),
self
.
lcp
=
LoncapaProblem
(
self
.
definition
[
'data'
],
self
.
location
.
html_id
(),
state
=
None
,
# Tabula rasa
state
=
None
,
# Tabula rasa
seed
=
self
.
seed
,
system
=
self
.
system
)
seed
=
self
.
seed
,
system
=
self
.
system
)
# Prepend a scary warning to the student
# Prepend a scary warning to the student
warning
=
'<div class="capa_reset">'
\
warning
=
'<div class="capa_reset">'
\
'<h2>Warning: The problem has been reset to its initial state!</h2>'
\
'<h2>Warning: The problem has been reset to its initial state!</h2>'
\
'The problem
\'
s state was corrupted by an invalid submission. '
\
'The problem
\'
s state was corrupted by an invalid submission. '
\
'The submission consisted of:'
\
'The submission consisted of:'
\
'<ul>'
'<ul>'
for
student_answer
in
student_answers
.
values
():
for
student_answer
in
student_answers
.
values
():
if
student_answer
!=
''
:
if
student_answer
!=
''
:
warning
+=
'<li>'
+
cgi
.
escape
(
student_answer
)
+
'</li>'
warning
+=
'<li>'
+
cgi
.
escape
(
student_answer
)
+
'</li>'
...
@@ -292,11 +292,11 @@ class CapaModule(XModule):
...
@@ -292,11 +292,11 @@ class CapaModule(XModule):
# check button is context-specific.
# check button is context-specific.
# Put a "Check" button if unlimited attempts or still some left
# Put a "Check" button if unlimited attempts or still some left
if
self
.
max_attempts
is
None
or
self
.
attempts
<
self
.
max_attempts
-
1
:
if
self
.
max_attempts
is
None
or
self
.
attempts
<
self
.
max_attempts
-
1
:
check_button
=
"Check"
check_button
=
"Check"
else
:
else
:
# Will be final check so let user know that
# Will be final check so let user know that
check_button
=
"Final Check"
check_button
=
"Final Check"
reset_button
=
True
reset_button
=
True
save_button
=
True
save_button
=
True
...
@@ -358,14 +358,14 @@ class CapaModule(XModule):
...
@@ -358,14 +358,14 @@ class CapaModule(XModule):
def
handle_ajax
(
self
,
dispatch
,
get
):
def
handle_ajax
(
self
,
dispatch
,
get
):
'''
'''
This is called by courseware.module_render, to handle an AJAX call.
This is called by courseware.module_render, to handle an AJAX call.
"get" is request.POST.
"get" is request.POST.
Returns a json dictionary:
Returns a json dictionary:
{ 'progress_changed' : True/False,
{ 'progress_changed' : True/False,
'progress' : 'none'/'in_progress'/'done',
'progress' : 'none'/'in_progress'/'done',
<other request-specific values here > }
<other request-specific values here > }
'''
'''
handlers
=
{
handlers
=
{
'problem_get'
:
self
.
get_problem
,
'problem_get'
:
self
.
get_problem
,
'problem_check'
:
self
.
check_problem
,
'problem_check'
:
self
.
check_problem
,
...
@@ -398,7 +398,7 @@ class CapaModule(XModule):
...
@@ -398,7 +398,7 @@ class CapaModule(XModule):
def
answer_available
(
self
):
def
answer_available
(
self
):
''' Is the user allowed to see an answer?
''' Is the user allowed to see an answer?
'''
'''
if
self
.
show_answer
==
''
:
if
self
.
show_answer
==
''
:
return
False
return
False
...
@@ -425,26 +425,26 @@ class CapaModule(XModule):
...
@@ -425,26 +425,26 @@ class CapaModule(XModule):
def
update_score
(
self
,
get
):
def
update_score
(
self
,
get
):
"""
"""
Delivers grading response (e.g. from asynchronous code checking) to
Delivers grading response (e.g. from asynchronous code checking) to
the capa problem, so its score can be updated
the capa problem, so its score can be updated
'get' must have a field 'response' which is a string that contains the
'get' must have a field 'response' which is a string that contains the
grader's response
grader's response
No ajax return is needed. Return empty dict.
No ajax return is needed. Return empty dict.
"""
"""
queuekey
=
get
[
'queuekey'
]
queuekey
=
get
[
'queuekey'
]
score_msg
=
get
[
'xqueue_body'
]
score_msg
=
get
[
'xqueue_body'
]
self
.
lcp
.
update_score
(
score_msg
,
queuekey
)
self
.
lcp
.
update_score
(
score_msg
,
queuekey
)
return
dict
()
# No AJAX return is needed
return
dict
()
# No AJAX return is needed
def
get_answer
(
self
,
get
):
def
get_answer
(
self
,
get
):
'''
'''
For the "show answer" button.
For the "show answer" button.
Returns the answers: {'answers' : answers}
Returns the answers: {'answers' : answers}
'''
'''
event_info
=
dict
()
event_info
=
dict
()
event_info
[
'problem_id'
]
=
self
.
location
.
url
()
event_info
[
'problem_id'
]
=
self
.
location
.
url
()
self
.
system
.
track_function
(
'show_answer'
,
event_info
)
self
.
system
.
track_function
(
'show_answer'
,
event_info
)
...
@@ -453,8 +453,8 @@ class CapaModule(XModule):
...
@@ -453,8 +453,8 @@ class CapaModule(XModule):
else
:
else
:
answers
=
self
.
lcp
.
get_question_answers
()
answers
=
self
.
lcp
.
get_question_answers
()
# answers (eg <solution>) may have embedded images
# answers (eg <solution>) may have embedded images
#
but be careful, some problems are using non-string answer dicts
# but be careful, some problems are using non-string answer dicts
new_answers
=
dict
()
new_answers
=
dict
()
for
answer_id
in
answers
:
for
answer_id
in
answers
:
try
:
try
:
...
@@ -469,18 +469,18 @@ class CapaModule(XModule):
...
@@ -469,18 +469,18 @@ class CapaModule(XModule):
# Figure out if we should move these to capa_problem?
# Figure out if we should move these to capa_problem?
def
get_problem
(
self
,
get
):
def
get_problem
(
self
,
get
):
''' Return results of get_problem_html, as a simple dict for json-ing.
''' Return results of get_problem_html, as a simple dict for json-ing.
{ 'html': <the-html> }
{ 'html': <the-html> }
Used if we want to reconfirm we have the right thing e.g. after
Used if we want to reconfirm we have the right thing e.g. after
several AJAX calls.
several AJAX calls.
'''
'''
return
{
'html'
:
self
.
get_problem_html
(
encapsulate
=
False
)}
return
{
'html'
:
self
.
get_problem_html
(
encapsulate
=
False
)}
@staticmethod
@staticmethod
def
make_dict_of_responses
(
get
):
def
make_dict_of_responses
(
get
):
'''Make dictionary of student responses (aka "answers")
'''Make dictionary of student responses (aka "answers")
get is POST dictionary.
get is POST dictionary.
'''
'''
answers
=
dict
()
answers
=
dict
()
for
key
in
get
:
for
key
in
get
:
# e.g. input_resistor_1 ==> resistor_1
# e.g. input_resistor_1 ==> resistor_1
...
@@ -500,11 +500,11 @@ class CapaModule(XModule):
...
@@ -500,11 +500,11 @@ class CapaModule(XModule):
def
check_problem
(
self
,
get
):
def
check_problem
(
self
,
get
):
''' Checks whether answers to a problem are correct, and
''' Checks whether answers to a problem are correct, and
returns a map of correct/incorrect answers:
returns a map of correct/incorrect answers:
{'success' : bool,
{'success' : bool,
'contents' : html}
'contents' : html}
'''
'''
event_info
=
dict
()
event_info
=
dict
()
event_info
[
'state'
]
=
self
.
lcp
.
get_state
()
event_info
[
'state'
]
=
self
.
lcp
.
get_state
()
event_info
[
'problem_id'
]
=
self
.
location
.
url
()
event_info
[
'problem_id'
]
=
self
.
location
.
url
()
...
@@ -527,11 +527,11 @@ class CapaModule(XModule):
...
@@ -527,11 +527,11 @@ class CapaModule(XModule):
# Problem queued. Students must wait a specified waittime before they are allowed to submit
# Problem queued. Students must wait a specified waittime before they are allowed to submit
if
self
.
lcp
.
is_queued
():
if
self
.
lcp
.
is_queued
():
current_time
=
datetime
.
datetime
.
now
()
current_time
=
datetime
.
datetime
.
now
()
prev_submit_time
=
self
.
lcp
.
get_recentmost_queuetime
()
prev_submit_time
=
self
.
lcp
.
get_recentmost_queuetime
()
waittime_between_requests
=
self
.
system
.
xqueue
[
'waittime'
]
waittime_between_requests
=
self
.
system
.
xqueue
[
'waittime'
]
if
(
current_time
-
prev_submit_time
)
.
total_seconds
()
<
waittime_between_requests
:
if
(
current_time
-
prev_submit_time
)
.
total_seconds
()
<
waittime_between_requests
:
msg
=
'You must wait at least
%
d seconds between submissions'
%
waittime_between_requests
msg
=
'You must wait at least
%
d seconds between submissions'
%
waittime_between_requests
return
{
'success'
:
msg
,
'html'
:
''
}
# Prompts a modal dialog in ajax callback
return
{
'success'
:
msg
,
'html'
:
''
}
# Prompts a modal dialog in ajax callback
try
:
try
:
old_state
=
self
.
lcp
.
get_state
()
old_state
=
self
.
lcp
.
get_state
()
...
@@ -540,13 +540,13 @@ class CapaModule(XModule):
...
@@ -540,13 +540,13 @@ class CapaModule(XModule):
except
StudentInputError
as
inst
:
except
StudentInputError
as
inst
:
# TODO (vshnayder): why is this line here?
# TODO (vshnayder): why is this line here?
#self.lcp = LoncapaProblem(self.definition['data'],
#self.lcp = LoncapaProblem(self.definition['data'],
#
id=lcp_id, state=old_state, system=self.system)
# id=lcp_id, state=old_state, system=self.system)
log
.
exception
(
"StudentInputError in capa_module:problem_check"
)
log
.
exception
(
"StudentInputError in capa_module:problem_check"
)
return
{
'success'
:
inst
.
message
}
return
{
'success'
:
inst
.
message
}
except
Exception
,
err
:
except
Exception
,
err
:
# TODO: why is this line here?
# TODO: why is this line here?
#self.lcp = LoncapaProblem(self.definition['data'],
#self.lcp = LoncapaProblem(self.definition['data'],
#
id=lcp_id, state=old_state, system=self.system)
# id=lcp_id, state=old_state, system=self.system)
if
self
.
system
.
DEBUG
:
if
self
.
system
.
DEBUG
:
msg
=
"Error checking problem: "
+
str
(
err
)
msg
=
"Error checking problem: "
+
str
(
err
)
msg
+=
'
\n
Traceback:
\n
'
+
traceback
.
format_exc
()
msg
+=
'
\n
Traceback:
\n
'
+
traceback
.
format_exc
()
...
@@ -564,99 +564,99 @@ class CapaModule(XModule):
...
@@ -564,99 +564,99 @@ class CapaModule(XModule):
success
=
'incorrect'
success
=
'incorrect'
# NOTE: We are logging both full grading and queued-grading submissions. In the latter,
# NOTE: We are logging both full grading and queued-grading submissions. In the latter,
#
'success' will always be incorrect
# 'success' will always be incorrect
event_info
[
'correct_map'
]
=
correct_map
.
get_dict
()
event_info
[
'correct_map'
]
=
correct_map
.
get_dict
()
event_info
[
'success'
]
=
success
event_info
[
'success'
]
=
success
event_info
[
'attempts'
]
=
self
.
attempts
event_info
[
'attempts'
]
=
self
.
attempts
self
.
system
.
track_function
(
'save_problem_check'
,
event_info
)
self
.
system
.
track_function
(
'save_problem_check'
,
event_info
)
if
hasattr
(
self
.
system
,
'psychometrics_handler'
):
# update PsychometricsData using callback
if
hasattr
(
self
.
system
,
'psychometrics_handler'
):
# update PsychometricsData using callback
self
.
system
.
psychometrics_handler
(
self
.
get_instance_state
())
self
.
system
.
psychometrics_handler
(
self
.
get_instance_state
())
# render problem into HTML
# render problem into HTML
html
=
self
.
get_problem_html
(
encapsulate
=
False
)
html
=
self
.
get_problem_html
(
encapsulate
=
False
)
return
{
'success'
:
success
,
return
{
'success'
:
success
,
'contents'
:
html
,
'contents'
:
html
,
}
}
def
save_problem
(
self
,
get
):
def
save_problem
(
self
,
get
):
'''
'''
Save the passed in answers.
Save the passed in answers.
Returns a dict { 'success' : bool, ['error' : error-msg]},
Returns a dict { 'success' : bool, ['error' : error-msg]},
with the error key only present if success is False.
with the error key only present if success is False.
'''
'''
event_info
=
dict
()
event_info
=
dict
()
event_info
[
'state'
]
=
self
.
lcp
.
get_state
()
event_info
[
'state'
]
=
self
.
lcp
.
get_state
()
event_info
[
'problem_id'
]
=
self
.
location
.
url
()
event_info
[
'problem_id'
]
=
self
.
location
.
url
()
answers
=
self
.
make_dict_of_responses
(
get
)
answers
=
self
.
make_dict_of_responses
(
get
)
event_info
[
'answers'
]
=
answers
event_info
[
'answers'
]
=
answers
# Too late. Cannot submit
# Too late. Cannot submit
if
self
.
closed
():
if
self
.
closed
():
event_info
[
'failure'
]
=
'closed'
event_info
[
'failure'
]
=
'closed'
self
.
system
.
track_function
(
'save_problem_fail'
,
event_info
)
self
.
system
.
track_function
(
'save_problem_fail'
,
event_info
)
return
{
'success'
:
False
,
return
{
'success'
:
False
,
'error'
:
"Problem is closed"
}
'error'
:
"Problem is closed"
}
# Problem submitted. Student should reset before saving
# Problem submitted. Student should reset before saving
# again.
# again.
if
self
.
lcp
.
done
and
self
.
rerandomize
==
"always"
:
if
self
.
lcp
.
done
and
self
.
rerandomize
==
"always"
:
event_info
[
'failure'
]
=
'done'
event_info
[
'failure'
]
=
'done'
self
.
system
.
track_function
(
'save_problem_fail'
,
event_info
)
self
.
system
.
track_function
(
'save_problem_fail'
,
event_info
)
return
{
'success'
:
False
,
return
{
'success'
:
False
,
'error'
:
"Problem needs to be reset prior to save."
}
'error'
:
"Problem needs to be reset prior to save."
}
self
.
lcp
.
student_answers
=
answers
self
.
lcp
.
student_answers
=
answers
# TODO: should this be save_problem_fail?
Looks like success to me...
# TODO: should this be save_problem_fail?
Looks like success to me...
self
.
system
.
track_function
(
'save_problem_fail'
,
event_info
)
self
.
system
.
track_function
(
'save_problem_fail'
,
event_info
)
return
{
'success'
:
True
}
return
{
'success'
:
True
}
def
reset_problem
(
self
,
get
):
def
reset_problem
(
self
,
get
):
''' Changes problem state to unfinished -- removes student answers,
''' Changes problem state to unfinished -- removes student answers,
and causes problem to rerender itself.
and causes problem to rerender itself.
Returns problem html as { 'html' : html-string }.
Returns problem html as { 'html' : html-string }.
'''
'''
event_info
=
dict
()
event_info
=
dict
()
event_info
[
'old_state'
]
=
self
.
lcp
.
get_state
()
event_info
[
'old_state'
]
=
self
.
lcp
.
get_state
()
event_info
[
'problem_id'
]
=
self
.
location
.
url
()
event_info
[
'problem_id'
]
=
self
.
location
.
url
()
if
self
.
closed
():
if
self
.
closed
():
event_info
[
'failure'
]
=
'closed'
event_info
[
'failure'
]
=
'closed'
self
.
system
.
track_function
(
'reset_problem_fail'
,
event_info
)
self
.
system
.
track_function
(
'reset_problem_fail'
,
event_info
)
return
{
'success'
:
False
,
return
{
'success'
:
False
,
'error'
:
"Problem is closed"
}
'error'
:
"Problem is closed"
}
if
not
self
.
lcp
.
done
:
if
not
self
.
lcp
.
done
:
event_info
[
'failure'
]
=
'not_done'
event_info
[
'failure'
]
=
'not_done'
self
.
system
.
track_function
(
'reset_problem_fail'
,
event_info
)
self
.
system
.
track_function
(
'reset_problem_fail'
,
event_info
)
return
{
'success'
:
False
,
return
{
'success'
:
False
,
'error'
:
"Refresh the page and make an attempt before resetting."
}
'error'
:
"Refresh the page and make an attempt before resetting."
}
self
.
lcp
.
do_reset
()
self
.
lcp
.
do_reset
()
if
self
.
rerandomize
in
[
"always"
,
"onreset"
]:
if
self
.
rerandomize
in
[
"always"
,
"onreset"
]:
# reset random number generator seed (note the self.lcp.get_state()
# reset random number generator seed (note the self.lcp.get_state()
# in next line)
# in next line)
self
.
lcp
.
seed
=
None
self
.
lcp
.
seed
=
None
self
.
lcp
=
LoncapaProblem
(
self
.
definition
[
'data'
],
self
.
lcp
=
LoncapaProblem
(
self
.
definition
[
'data'
],
self
.
location
.
html_id
(),
self
.
lcp
.
get_state
(),
self
.
location
.
html_id
(),
self
.
lcp
.
get_state
(),
system
=
self
.
system
)
system
=
self
.
system
)
event_info
[
'new_state'
]
=
self
.
lcp
.
get_state
()
event_info
[
'new_state'
]
=
self
.
lcp
.
get_state
()
self
.
system
.
track_function
(
'reset_problem'
,
event_info
)
self
.
system
.
track_function
(
'reset_problem'
,
event_info
)
return
{
'html'
:
self
.
get_problem_html
(
encapsulate
=
False
)}
return
{
'html'
:
self
.
get_problem_html
(
encapsulate
=
False
)}
class
CapaDescriptor
(
RawDescriptor
):
class
CapaDescriptor
(
RawDescriptor
):
"""
"""
Module implementing problems in the LON-CAPA format,
Module implementing problems in the LON-CAPA format,
as implemented by capa.capa_problem
as implemented by capa.capa_problem
"""
"""
module_class
=
CapaModule
module_class
=
CapaModule
...
@@ -665,7 +665,7 @@ class CapaDescriptor(RawDescriptor):
...
@@ -665,7 +665,7 @@ class CapaDescriptor(RawDescriptor):
template_dir_name
=
'problem'
template_dir_name
=
'problem'
# Capa modules have some additional metadata:
# Capa modules have some additional metadata:
# TODO (vshnayder): do problems have any other metadata?
Do they
# TODO (vshnayder): do problems have any other metadata? Do they
# actually use type and points?
# actually use type and points?
metadata_attributes
=
RawDescriptor
.
metadata_attributes
+
(
'type'
,
'points'
)
metadata_attributes
=
RawDescriptor
.
metadata_attributes
+
(
'type'
,
'points'
)
...
@@ -677,13 +677,13 @@ class CapaDescriptor(RawDescriptor):
...
@@ -677,13 +677,13 @@ class CapaDescriptor(RawDescriptor):
return
[
return
[
'problems/'
+
path
[
8
:],
'problems/'
+
path
[
8
:],
path
[
8
:],
path
[
8
:],
]
]
def
__init__
(
self
,
*
args
,
**
kwargs
):
def
__init__
(
self
,
*
args
,
**
kwargs
):
super
(
CapaDescriptor
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
super
(
CapaDescriptor
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
weight_string
=
self
.
metadata
.
get
(
'weight'
,
None
)
weight_string
=
self
.
metadata
.
get
(
'weight'
,
None
)
if
weight_string
:
if
weight_string
:
self
.
weight
=
float
(
weight_string
)
self
.
weight
=
float
(
weight_string
)
else
:
else
:
self
.
weight
=
None
self
.
weight
=
None
\ No newline at end of file
common/lib/xmodule/xmodule/self_assessment_module.py
View file @
b74a484f
...
@@ -108,6 +108,341 @@ class SelfAssessmentModule(XModule):
...
@@ -108,6 +108,341 @@ class SelfAssessmentModule(XModule):
'ajax_url'
:
self
.
system
.
ajax_url
,
'ajax_url'
:
self
.
system
.
ajax_url
,
})
})
def
get_problem_html
(
self
,
encapsulate
=
True
):
'''Return html for the problem. Adds check, reset, save buttons
as necessary based on the problem config and state.'''
try
:
html
=
self
.
lcp
.
get_html
()
except
Exception
,
err
:
log
.
exception
(
err
)
# TODO (vshnayder): another switch on DEBUG.
if
self
.
system
.
DEBUG
:
msg
=
(
'[courseware.capa.capa_module] <font size="+1" color="red">'
'Failed to generate HTML for problem
%
s</font>'
%
(
self
.
location
.
url
()))
msg
+=
'<p>Error:</p><p><pre>
%
s</pre></p>'
%
str
(
err
)
.
replace
(
'<'
,
'<'
)
msg
+=
'<p><pre>
%
s</pre></p>'
%
traceback
.
format_exc
()
.
replace
(
'<'
,
'<'
)
html
=
msg
else
:
# We're in non-debug mode, and possibly even in production. We want
# to avoid bricking of problem as much as possible
# Presumably, student submission has corrupted LoncapaProblem HTML.
# First, pull down all student answers
student_answers
=
self
.
lcp
.
student_answers
answer_ids
=
student_answers
.
keys
()
# Some inputtypes, such as dynamath, have additional "hidden" state that
# is not exposed to the student. Keep those hidden
# TODO: Use regex, e.g. 'dynamath' is suffix at end of answer_id
hidden_state_keywords
=
[
'dynamath'
]
for
answer_id
in
answer_ids
:
for
hidden_state_keyword
in
hidden_state_keywords
:
if
answer_id
.
find
(
hidden_state_keyword
)
>=
0
:
student_answers
.
pop
(
answer_id
)
# Next, generate a fresh LoncapaProblem
self
.
lcp
=
LoncapaProblem
(
self
.
definition
[
'data'
],
self
.
location
.
html_id
(),
state
=
None
,
# Tabula rasa
seed
=
self
.
seed
,
system
=
self
.
system
)
# Prepend a scary warning to the student
warning
=
'<div class="capa_reset">'
\
'<h2>Warning: The problem has been reset to its initial state!</h2>'
\
'The problem
\'
s state was corrupted by an invalid submission. '
\
'The submission consisted of:'
\
'<ul>'
for
student_answer
in
student_answers
.
values
():
if
student_answer
!=
''
:
warning
+=
'<li>'
+
cgi
.
escape
(
student_answer
)
+
'</li>'
warning
+=
'</ul>'
\
'If this error persists, please contact the course staff.'
\
'</div>'
html
=
warning
try
:
html
+=
self
.
lcp
.
get_html
()
except
Exception
,
err
:
# Couldn't do it. Give up
log
.
exception
(
err
)
raise
content
=
{
'name'
:
self
.
display_name
,
'html'
:
html
,
'weight'
:
self
.
descriptor
.
weight
,
}
# We using strings as truthy values, because the terminology of the
# check button is context-specific.
# Put a "Check" button if unlimited attempts or still some left
if
self
.
max_attempts
is
None
or
self
.
attempts
<
self
.
max_attempts
-
1
:
check_button
=
"Check"
else
:
# Will be final check so let user know that
check_button
=
"Final Check"
reset_button
=
True
save_button
=
True
# If we're after deadline, or user has exhausted attempts,
# question is read-only.
if
self
.
closed
():
check_button
=
False
reset_button
=
False
save_button
=
False
# User submitted a problem, and hasn't reset. We don't want
# more submissions.
if
self
.
lcp
.
done
and
self
.
rerandomize
==
"always"
:
check_button
=
False
save_button
=
False
# Only show the reset button if pressing it will show different values
if
self
.
rerandomize
not
in
[
"always"
,
"onreset"
]:
reset_button
=
False
# User hasn't submitted an answer yet -- we don't want resets
if
not
self
.
lcp
.
done
:
reset_button
=
False
# We may not need a "save" button if infinite number of attempts and
# non-randomized. The problem author can force it. It's a bit weird for
# randomization to control this; should perhaps be cleaned up.
if
(
self
.
force_save_button
==
"false"
)
and
(
self
.
max_attempts
is
None
and
self
.
rerandomize
!=
"always"
):
save_button
=
False
context
=
{
'problem'
:
content
,
'id'
:
self
.
id
,
'check_button'
:
check_button
,
'reset_button'
:
reset_button
,
'save_button'
:
save_button
,
'answer_available'
:
self
.
answer_available
(),
'ajax_url'
:
self
.
system
.
ajax_url
,
'attempts_used'
:
self
.
attempts
,
'attempts_allowed'
:
self
.
max_attempts
,
'progress'
:
self
.
get_progress
(),
}
html
=
self
.
system
.
render_template
(
'problem.html'
,
context
)
if
encapsulate
:
html
=
'<div id="problem_{id}" class="problem" data-url="{ajax_url}">'
.
format
(
id
=
self
.
location
.
html_id
(),
ajax_url
=
self
.
system
.
ajax_url
)
+
html
+
"</div>"
# cdodge: OK, we have to do two rounds of url reference subsitutions
# one which uses the 'asset library' that is served by the contentstore and the
# more global /static/ filesystem based static content.
# NOTE: rewrite_content_links is defined in XModule
# This is a bit unfortunate and I'm sure we'll try to considate this into
# a one step process.
html
=
rewrite_links
(
html
,
self
.
rewrite_content_links
)
# now do the substitutions which are filesystem based, e.g. '/static/' prefixes
return
self
.
system
.
replace_urls
(
html
,
self
.
metadata
[
'data_dir'
])
def
handle_ajax
(
self
,
dispatch
,
get
):
'''
This is called by courseware.module_render, to handle an AJAX call.
"get" is request.POST.
Returns a json dictionary:
{ 'progress_changed' : True/False,
'progress' : 'none'/'in_progress'/'done',
<other request-specific values here > }
'''
handlers
=
{
'problem_get'
:
self
.
get_problem
,
'problem_check'
:
self
.
check_problem
,
'problem_reset'
:
self
.
reset_problem
,
'problem_save'
:
self
.
save_problem
,
'problem_show'
:
self
.
get_answer
,
'score_update'
:
self
.
update_score
,
}
if
dispatch
not
in
handlers
:
return
'Error'
before
=
self
.
get_progress
()
d
=
handlers
[
dispatch
](
get
)
after
=
self
.
get_progress
()
d
.
update
({
'progress_changed'
:
after
!=
before
,
'progress_status'
:
Progress
.
to_js_status_str
(
after
),
})
return
json
.
dumps
(
d
,
cls
=
ComplexEncoder
)
def
closed
(
self
):
''' Is the student still allowed to submit answers? '''
if
self
.
attempts
==
self
.
max_attempts
:
return
True
if
self
.
close_date
is
not
None
and
datetime
.
datetime
.
utcnow
()
>
self
.
close_date
:
return
True
return
False
def
answer_available
(
self
):
''' Is the user allowed to see an answer?
'''
if
self
.
show_answer
==
''
:
return
False
if
self
.
show_answer
==
"never"
:
return
False
# Admins can see the answer, unless the problem explicitly prevents it
if
self
.
system
.
user_is_staff
:
return
True
if
self
.
show_answer
==
'attempted'
:
return
self
.
attempts
>
0
if
self
.
show_answer
==
'answered'
:
return
self
.
lcp
.
done
if
self
.
show_answer
==
'closed'
:
return
self
.
closed
()
if
self
.
show_answer
==
'always'
:
return
True
return
False
# Figure out if we should move these to capa_problem?
def
get_problem
(
self
,
get
):
''' Return results of get_problem_html, as a simple dict for json-ing.
{ 'html': <the-html> }
Used if we want to reconfirm we have the right thing e.g. after
several AJAX calls.
'''
return
{
'html'
:
self
.
get_problem_html
(
encapsulate
=
False
)}
@staticmethod
def
make_dict_of_responses
(
get
):
'''Make dictionary of student responses (aka "answers")
get is POST dictionary.
'''
answers
=
dict
()
for
key
in
get
:
# e.g. input_resistor_1 ==> resistor_1
_
,
_
,
name
=
key
.
partition
(
'_'
)
# This allows for answers which require more than one value for
# the same form input (e.g. checkbox inputs). The convention is that
# if the name ends with '[]' (which looks like an array), then the
# answer will be an array.
if
not
name
.
endswith
(
'[]'
):
answers
[
name
]
=
get
[
key
]
else
:
name
=
name
[:
-
2
]
answers
[
name
]
=
get
.
getlist
(
key
)
return
answers
def
check_problem
(
self
,
get
):
''' Checks whether answers to a problem are correct, and
returns a map of correct/incorrect answers:
{'success' : bool,
'contents' : html}
'''
event_info
=
dict
()
event_info
[
'state'
]
=
self
.
lcp
.
get_state
()
event_info
[
'problem_id'
]
=
self
.
location
.
url
()
answers
=
self
.
make_dict_of_responses
(
get
)
event_info
[
'answers'
]
=
convert_files_to_filenames
(
answers
)
parsed_answer
=
False
if
(
answer
[
answers
.
keys
()[
0
]]
==
"True"
):
parsed_answer
=
True
# Too late. Cannot submit
if
self
.
closed
():
event_info
[
'failure'
]
=
'closed'
self
.
system
.
track_function
(
'save_problem_check_fail'
,
event_info
)
raise
NotFoundError
(
'Problem is closed'
)
try
:
old_state
=
self
.
lcp
.
get_state
()
lcp_id
=
self
.
lcp
.
problem_id
correct_map
=
self
.
lcp
.
grade_answers
(
answers
)
correct_map
.
set
(
correctness
=
parsed_answer
)
except
StudentInputError
as
inst
:
# TODO (vshnayder): why is this line here?
#self.lcp = LoncapaProblem(self.definition['data'],
# id=lcp_id, state=old_state, system=self.system)
log
.
exception
(
"StudentInputError in capa_module:problem_check"
)
return
{
'success'
:
inst
.
message
}
except
Exception
,
err
:
# TODO: why is this line here?
#self.lcp = LoncapaProblem(self.definition['data'],
# id=lcp_id, state=old_state, system=self.system)
if
self
.
system
.
DEBUG
:
msg
=
"Error checking problem: "
+
str
(
err
)
msg
+=
'
\n
Traceback:
\n
'
+
traceback
.
format_exc
()
return
{
'success'
:
msg
}
log
.
exception
(
"Error in capa_module problem checking"
)
raise
Exception
(
"error in capa_module"
)
self
.
attempts
=
self
.
attempts
+
1
self
.
lcp
.
done
=
True
# success = correct if ALL questions in this problem are correct
success
=
'correct'
for
answer_id
in
correct_map
:
if
not
correct_map
.
is_correct
(
answer_id
):
success
=
'incorrect'
# NOTE: We are logging both full grading and queued-grading submissions. In the latter,
# 'success' will always be incorrect
event_info
[
'correct_map'
]
=
correct_map
.
get_dict
()
event_info
[
'success'
]
=
success
event_info
[
'attempts'
]
=
self
.
attempts
self
.
system
.
track_function
(
'save_problem_check'
,
event_info
)
# render problem into HTML
html
=
self
.
get_problem_html
(
encapsulate
=
False
)
return
{
'success'
:
success
,
'contents'
:
html
,
}
def
save_problem
(
self
,
get
):
'''
Save the passed in answers.
Returns a dict { 'success' : bool, ['error' : error-msg]},
with the error key only present if success is False.
'''
event_info
=
dict
()
event_info
[
'state'
]
=
self
.
lcp
.
get_state
()
event_info
[
'problem_id'
]
=
self
.
location
.
url
()
answers
=
self
.
make_dict_of_responses
(
get
)
event_info
[
'answers'
]
=
answers
# Too late. Cannot submit
if
self
.
closed
():
event_info
[
'failure'
]
=
'closed'
self
.
system
.
track_function
(
'save_problem_fail'
,
event_info
)
return
{
'success'
:
False
,
'error'
:
"Problem is closed"
}
# Problem submitted. Student should reset before saving
# again.
if
self
.
lcp
.
done
and
self
.
rerandomize
==
"always"
:
event_info
[
'failure'
]
=
'done'
self
.
system
.
track_function
(
'save_problem_fail'
,
event_info
)
return
{
'success'
:
False
,
'error'
:
"Problem needs to be reset prior to save."
}
self
.
lcp
.
student_answers
=
answers
# TODO: should this be save_problem_fail? Looks like success to me...
self
.
system
.
track_function
(
'save_problem_fail'
,
event_info
)
return
{
'success'
:
True
}
class
SelfAssessmentDescriptor
(
XmlDescriptor
,
EditingDescriptor
):
class
SelfAssessmentDescriptor
(
XmlDescriptor
,
EditingDescriptor
):
...
...
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