Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
X
xblock-activetable
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
xblock-activetable
Commits
072ab3ed
Commit
072ab3ed
authored
Oct 26, 2015
by
Sven Marnach
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add maximum number of attempts and Save button.
parent
29bb31c7
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
138 additions
and
54 deletions
+138
-54
activetable/activetable.py
+68
-30
activetable/static/js/src/activetable.js
+58
-22
activetable/templates/css/activetable.css
+7
-1
activetable/templates/html/activetable.html
+5
-1
No files found.
activetable/activetable.py
View file @
072ab3ed
...
@@ -78,6 +78,12 @@ class ActiveTableXBlock(StudioEditableXBlockMixin, XBlock):
...
@@ -78,6 +78,12 @@ class ActiveTableXBlock(StudioEditableXBlockMixin, XBlock):
scope
=
Scope
.
settings
,
scope
=
Scope
.
settings
,
default
=
1.0
,
default
=
1.0
,
)
)
max_attempts
=
Integer
(
display_name
=
'Maximum attempts'
,
help
=
'Defines the number of times a student can try to answer this problem. If the value '
'is not set, infinite attempts are allowed.'
,
scope
=
Scope
.
settings
,
)
editable_fields
=
[
editable_fields
=
[
'display_name'
,
'display_name'
,
...
@@ -87,17 +93,35 @@ class ActiveTableXBlock(StudioEditableXBlockMixin, XBlock):
...
@@ -87,17 +93,35 @@ class ActiveTableXBlock(StudioEditableXBlockMixin, XBlock):
'row_heights'
,
'row_heights'
,
'default_tolerance'
,
'default_tolerance'
,
'max_score'
,
'max_score'
,
'max_attempts'
,
]
]
# Dictionary mapping cell ids to the student answers.
# Dictionary mapping cell ids to the student answers.
answers
=
Dict
(
scope
=
Scope
.
user_state
)
answers
=
Dict
(
scope
=
Scope
.
user_state
)
# Number of correct answers.
# Dictionary mapping cell ids to Boolean values indicating whether the cell was answered
num_correct_answers
=
Integer
(
scope
=
Scope
.
user_state
)
# correctly at the last check.
answers_correct
=
Dict
(
scope
=
Scope
.
user_state
,
default
=
None
)
# The number of points awarded.
# The number of points awarded.
score
=
Float
(
scope
=
Scope
.
user_state
)
score
=
Float
(
scope
=
Scope
.
user_state
)
# The number of attempts used.
attempts
=
Integer
(
scope
=
Scope
.
user_state
,
default
=
0
)
has_score
=
True
has_score
=
True
@property
def
num_correct_answers
(
self
):
"""The number of correct answers during the last check."""
if
self
.
answers_correct
is
None
:
return
None
return
sum
(
self
.
answers_correct
.
itervalues
())
@property
def
num_total_answers
(
self
):
"""The total number of answers during the last check."""
if
self
.
answers_correct
is
None
:
return
None
return
len
(
self
.
answers_correct
)
def
parse_fields
(
self
):
def
parse_fields
(
self
):
"""Parse the user-provided fields into more processing-friendly structured data."""
"""Parse the user-provided fields into more processing-friendly structured data."""
if
self
.
table_definition
:
if
self
.
table_definition
:
...
@@ -130,16 +154,23 @@ class ActiveTableXBlock(StudioEditableXBlockMixin, XBlock):
...
@@ -130,16 +154,23 @@ class ActiveTableXBlock(StudioEditableXBlockMixin, XBlock):
cell
.
id
=
'cell_{}_{}'
.
format
(
cell
.
index
,
row
[
'index'
])
cell
.
id
=
'cell_{}_{}'
.
format
(
cell
.
index
,
row
[
'index'
])
if
not
cell
.
is_static
:
if
not
cell
.
is_static
:
self
.
response_cells
[
cell
.
id
]
=
cell
self
.
response_cells
[
cell
.
id
]
=
cell
cell
.
classes
=
'active'
cell
.
value
=
self
.
answers
.
get
(
cell
.
id
)
cell
.
value
=
self
.
answers
.
get
(
cell
.
id
)
cell
.
height
=
height
-
2
cell
.
height
=
height
-
2
if
isinstance
(
cell
,
NumericCell
)
and
cell
.
abs_tolerance
is
None
:
if
isinstance
(
cell
,
NumericCell
)
and
cell
.
abs_tolerance
is
None
:
cell
.
set_tolerance
(
self
.
default_tolerance
)
cell
.
set_tolerance
(
self
.
default_tolerance
)
if
cell
.
value
is
None
:
cell
.
classes
=
'active unchecked'
def
get_status
(
self
):
elif
cell
.
check_response
(
cell
.
value
):
"""Status dictionary passed to the frontend code."""
cell
.
classes
=
'active right-answer'
return
dict
(
else
:
answers_correct
=
self
.
answers_correct
,
cell
.
classes
=
'active wrong-answer'
num_correct_answers
=
self
.
num_correct_answers
,
num_total_answers
=
self
.
num_total_answers
,
score
=
self
.
score
,
max_score
=
self
.
max_score
,
attempts
=
self
.
attempts
,
max_attempts
=
self
.
max_attempts
,
)
def
student_view
(
self
,
context
=
None
):
def
student_view
(
self
,
context
=
None
):
"""Render the table."""
"""Render the table."""
...
@@ -153,6 +184,7 @@ class ActiveTableXBlock(StudioEditableXBlockMixin, XBlock):
...
@@ -153,6 +184,7 @@ class ActiveTableXBlock(StudioEditableXBlockMixin, XBlock):
head_height
=
self
.
_row_heights
[
0
]
if
self
.
_row_heights
else
None
,
head_height
=
self
.
_row_heights
[
0
]
if
self
.
_row_heights
else
None
,
thead
=
self
.
thead
,
thead
=
self
.
thead
,
tbody
=
self
.
tbody
,
tbody
=
self
.
tbody
,
max_attempts
=
self
.
max_attempts
,
)
)
html
=
loader
.
render_template
(
'templates/html/activetable.html'
,
context
)
html
=
loader
.
render_template
(
'templates/html/activetable.html'
,
context
)
...
@@ -166,39 +198,45 @@ class ActiveTableXBlock(StudioEditableXBlockMixin, XBlock):
...
@@ -166,39 +198,45 @@ class ActiveTableXBlock(StudioEditableXBlockMixin, XBlock):
frag
=
Fragment
(
html
)
frag
=
Fragment
(
html
)
frag
.
add_css
(
css
)
frag
.
add_css
(
css
)
frag
.
add_javascript
(
loader
.
load_unicode
(
'static/js/src/activetable.js'
))
frag
.
add_javascript
(
loader
.
load_unicode
(
'static/js/src/activetable.js'
))
frag
.
initialize_js
(
'ActiveTableXBlock'
,
dict
(
frag
.
initialize_js
(
'ActiveTableXBlock'
,
self
.
get_status
())
num_correct_answers
=
self
.
num_correct_answers
,
num_total_answers
=
len
(
self
.
answers
)
if
self
.
answers
is
not
None
else
None
,
score
=
self
.
score
,
max_score
=
self
.
max_score
,
))
return
frag
return
frag
@XBlock.json_handler
def
check_and_save_answers
(
self
,
data
):
def
check_answers
(
self
,
data
,
unused_suffix
=
''
):
"""Common implementation for the check and save handlers."""
"""Check the answers given by the student.
if
self
.
max_attempts
and
self
.
attempts
>=
self
.
max_attempts
:
# The "Check" button is hidden when the maximum number of attempts has been reached, so
This handler is called when the "Check" button is clicked.
# we can only get here by manually crafted requests. We simply return the current
"""
# status without rechecking or storing the answers in that case.
return
self
.
get_status
()
self
.
parse_fields
()
self
.
parse_fields
()
self
.
postprocess_table
()
self
.
postprocess_table
()
correct
=
{
answers_
correct
=
{
cell_id
:
self
.
response_cells
[
cell_id
]
.
check_response
(
value
)
cell_id
:
self
.
response_cells
[
cell_id
]
.
check_response
(
value
)
for
cell_id
,
value
in
data
.
iteritems
()
for
cell_id
,
value
in
data
.
iteritems
()
}
}
# Since the previous statement executed without error, the data is well-formed enough to be
# Since the previous statement executed without error, the data is well-formed enough to be
# stored. We now know it's a dictionary and all the keys are valid cell ids.
# stored. We now know it's a dictionary and all the keys are valid cell ids.
self
.
answers
=
data
self
.
answers
=
data
self
.
num_correct_answers
=
sum
(
correct
.
itervalues
())
return
answers_correct
self
.
score
=
self
.
num_correct_answers
*
self
.
max_score
/
len
(
correct
)
@XBlock.json_handler
def
check_answers
(
self
,
data
,
unused_suffix
=
''
):
"""Check the answers given by the student.
This handler is called when the "Check" button is clicked.
"""
self
.
answers_correct
=
self
.
check_and_save_answers
(
data
)
self
.
attempts
+=
1
self
.
score
=
self
.
num_correct_answers
*
self
.
max_score
/
len
(
self
.
answers_correct
)
self
.
runtime
.
publish
(
self
,
'grade'
,
dict
(
value
=
self
.
score
,
max_value
=
self
.
max_score
))
self
.
runtime
.
publish
(
self
,
'grade'
,
dict
(
value
=
self
.
score
,
max_value
=
self
.
max_score
))
return
dict
(
return
self
.
get_status
()
correct
=
correct
,
num_correct_answers
=
self
.
num_correct_answers
,
@XBlock.json_handler
num_total_answers
=
len
(
correct
),
def
save_answers
(
self
,
data
,
unused_suffix
=
''
):
score
=
self
.
score
,
"""Save the answers given by the student without checking them."""
max_score
=
self
.
max_score
,
self
.
check_and_save_answers
(
data
)
)
self
.
answers_correct
=
None
return
self
.
get_status
()
def
validate_field_data
(
self
,
validation
,
data
):
def
validate_field_data
(
self
,
validation
,
data
):
"""Validate the data entered by the user.
"""Validate the data entered by the user.
...
...
activetable/static/js/src/activetable.js
View file @
072ab3ed
...
@@ -2,49 +2,84 @@
...
@@ -2,49 +2,84 @@
function
ActiveTableXBlock
(
runtime
,
element
,
init_args
)
{
function
ActiveTableXBlock
(
runtime
,
element
,
init_args
)
{
var
checkHandlerUrl
=
runtime
.
handlerUrl
(
element
,
'check_answers'
);
var
checkHandlerUrl
=
runtime
.
handlerUrl
(
element
,
'check_answers'
);
var
saveHandlerUrl
=
runtime
.
handlerUrl
(
element
,
'save_answers'
);
function
updateStatus
(
data
)
{
function
markResponseCells
(
data
)
{
if
(
data
.
answers_correct
)
{
$
.
each
(
data
.
answers_correct
,
function
(
cell_id
,
correct
)
{
var
$cell
=
$
(
'#'
+
cell_id
,
element
);
$cell
.
removeClass
(
'right-answer wrong-answer unchecked'
);
if
(
correct
)
{
$cell
.
addClass
(
'right-answer'
);
$cell
.
prop
(
'title'
,
'correct'
);
}
else
{
$cell
.
addClass
(
'wrong-answer'
);
$cell
.
prop
(
'title'
,
'incorrect'
);
}
});
}
else
{
$
(
'td.active'
,
element
).
removeClass
(
'right-answer wrong-answer'
).
addClass
(
'unchecked'
);
}
}
function
updateStatusMessage
(
data
)
{
var
$status
=
$
(
'.status'
,
element
);
var
$status
=
$
(
'.status'
,
element
);
var
$status_message
=
$
(
'.status-message'
,
element
);
var
$status_message
=
$
(
'.status-message'
,
element
);
if
(
data
.
num_total_answers
==
data
.
num_correct_answers
)
{
if
(
!
data
.
answers_correct
)
{
$status
.
removeClass
(
'incorrect correct'
);
$status
.
text
(
'unanswered'
);
$status_message
.
text
(
''
);
}
else
if
(
data
.
num_total_answers
==
data
.
num_correct_answers
)
{
$status
.
removeClass
(
'incorrect'
).
addClass
(
'correct'
);
$status
.
removeClass
(
'incorrect'
).
addClass
(
'correct'
);
$status
.
text
(
'correct'
);
$status
.
text
(
'correct'
);
$status_message
.
text
(
'Great job!
('
+
data
.
score
+
'/'
+
data
.
max_score
+
' points)
'
);
$status_message
.
text
(
'Great job! '
);
}
else
{
}
else
{
$status
.
removeClass
(
'correct'
).
addClass
(
'incorrect'
);
$status
.
removeClass
(
'correct'
).
addClass
(
'incorrect'
);
$status
.
text
(
'incorrect'
);
$status
.
text
(
'incorrect'
);
$status_message
.
text
(
$status_message
.
text
(
'You have '
+
data
.
num_correct_answers
+
' out of '
+
data
.
num_total_answers
+
'You have '
+
data
.
num_correct_answers
+
' out of '
+
data
.
num_total_answers
+
' cells correct.
('
+
data
.
score
+
'/'
+
data
.
max_score
+
' points)
'
' cells correct.'
);
);
}
}
}
}
function
markResponseCells
(
data
)
{
function
updateFeedback
(
data
)
{
$
.
each
(
data
.
correct
,
function
(
cell_id
,
correct
)
{
var
feedback_msg
;
var
$cell
=
$
(
'#'
+
cell_id
,
element
);
if
(
data
.
score
===
null
)
{
$cell
.
removeClass
(
'right-answer wrong-answer unchecked'
);
feedback_msg
=
'('
+
data
.
max_score
+
' points possible)'
;
if
(
correct
)
{
}
else
{
$cell
.
addClass
(
'right-answer'
);
feedback_msg
=
'('
+
data
.
score
+
'/'
+
data
.
max_score
+
' points)'
;
$cell
.
prop
(
'title'
,
'correct'
);
}
}
else
{
if
(
data
.
max_attempts
)
{
$cell
.
addClass
(
'wrong-answer'
);
feedback_msg
=
'You have used '
+
data
.
attempts
+
' of '
+
data
.
max_attempts
+
$cell
.
prop
(
'title'
,
'incorrect'
);
' submissions '
+
feedback_msg
;
if
(
data
.
attempts
==
data
.
max_attempts
-
1
)
{
$
(
'.action .check .check-label'
,
element
).
text
(
'Final check'
);
}
}
});
else
if
(
data
.
attempts
>=
data
.
max_attempts
)
{
updateStatus
(
data
);
$
(
'.action .check, .action .save'
,
element
).
hide
();
}
}
$
(
'.submission-feedback'
,
element
).
text
(
feedback_msg
);
}
function
updateStatus
(
data
)
{
markResponseCells
(
data
);
updateStatusMessage
(
data
);
updateFeedback
(
data
);
}
}
function
c
heckAnswers
(
e
)
{
function
c
allHandler
(
url
)
{
answers
=
{};
var
answers
=
{};
$
(
'td.active'
,
element
).
each
(
function
()
{
$
(
'td.active'
,
element
).
each
(
function
()
{
answers
[
this
.
id
]
=
$
(
'input'
,
this
).
val
();
answers
[
this
.
id
]
=
$
(
'input'
,
this
).
val
();
});
});
$
.
ajax
({
$
.
ajax
({
type
:
"POST"
,
type
:
"POST"
,
url
:
checkHandlerU
rl
,
url
:
u
rl
,
data
:
JSON
.
stringify
(
answers
),
data
:
JSON
.
stringify
(
answers
),
success
:
markResponseCell
s
,
success
:
updateStatu
s
,
});
});
}
}
...
@@ -55,6 +90,7 @@ function ActiveTableXBlock(runtime, element, init_args) {
...
@@ -55,6 +90,7 @@ function ActiveTableXBlock(runtime, element, init_args) {
}
}
$
(
'#activetable-help-button'
,
element
).
click
(
toggleHelp
);
$
(
'#activetable-help-button'
,
element
).
click
(
toggleHelp
);
$
(
'.action .check'
,
element
).
click
(
checkAnswers
);
$
(
'.action .check'
,
element
).
click
(
function
(
e
)
{
callHandler
(
checkHandlerUrl
);
});
if
(
init_args
.
num_total_answers
)
updateStatus
(
init_args
);
$
(
'.action .save'
,
element
).
click
(
function
(
e
)
{
callHandler
(
saveHandlerUrl
);
});
updateStatus
(
init_args
);
}
}
activetable/templates/css/activetable.css
View file @
072ab3ed
...
@@ -110,8 +110,14 @@
...
@@ -110,8 +110,14 @@
.activetable_block
.status-message
{
.activetable_block
.status-message
{
margin
:
0.5em
0
1.5em
;
margin
:
0.5em
0
1.5em
;
}
}
.activetable_block
.action
.check
{
.activetable_block
.action
button
{
height
:
40px
;
height
:
40px
;
margin-right
:
10px
;
font-weight
:
600
;
font-weight
:
600
;
text-transform
:
uppercase
;
text-transform
:
uppercase
;
}
}
.activetable_block
.submission-feedback
{
display
:
inline-block
;
font-style
:
italic
;
margin-left
:
10px
;
}
activetable/templates/html/activetable.html
View file @
072ab3ed
...
@@ -35,9 +35,13 @@
...
@@ -35,9 +35,13 @@
{% else %}
{% else %}
<p>
This component isn't configured properly and can't be displayed.
</p>
<p>
This component isn't configured properly and can't be displayed.
</p>
{% endif %}
{% endif %}
<p
class=
"status"
>
unanswered
</p>
<p
class=
"status"
></p>
<div
class=
"status-message"
></div>
<div
class=
"status-message"
></div>
<div
class=
"action"
>
<div
class=
"action"
>
<button
class=
"check"
><span
class=
"check-label"
>
Check
</span><span
class=
"sr"
>
your answer
</span></button>
<button
class=
"check"
><span
class=
"check-label"
>
Check
</span><span
class=
"sr"
>
your answer
</span></button>
{% if max_attempts %}
<button
class=
"save"
>
Save
<span
class=
"sr"
>
your answer
</span></button>
<div
class=
"submission-feedback"
></div>
{% endif %}
</div>
</div>
</div>
</div>
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