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
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
137 additions
and
53 deletions
+137
-53
activetable/activetable.py
+68
-30
activetable/static/js/src/activetable.js
+57
-21
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):
scope
=
Scope
.
settings
,
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
=
[
'display_name'
,
...
...
@@ -87,17 +93,35 @@ class ActiveTableXBlock(StudioEditableXBlockMixin, XBlock):
'row_heights'
,
'default_tolerance'
,
'max_score'
,
'max_attempts'
,
]
# Dictionary mapping cell ids to the student answers.
answers
=
Dict
(
scope
=
Scope
.
user_state
)
# Number of correct answers.
num_correct_answers
=
Integer
(
scope
=
Scope
.
user_state
)
# Dictionary mapping cell ids to Boolean values indicating whether the cell was answered
# correctly at the last check.
answers_correct
=
Dict
(
scope
=
Scope
.
user_state
,
default
=
None
)
# The number of points awarded.
score
=
Float
(
scope
=
Scope
.
user_state
)
# The number of attempts used.
attempts
=
Integer
(
scope
=
Scope
.
user_state
,
default
=
0
)
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
):
"""Parse the user-provided fields into more processing-friendly structured data."""
if
self
.
table_definition
:
...
...
@@ -130,16 +154,23 @@ class ActiveTableXBlock(StudioEditableXBlockMixin, XBlock):
cell
.
id
=
'cell_{}_{}'
.
format
(
cell
.
index
,
row
[
'index'
])
if
not
cell
.
is_static
:
self
.
response_cells
[
cell
.
id
]
=
cell
cell
.
classes
=
'active'
cell
.
value
=
self
.
answers
.
get
(
cell
.
id
)
cell
.
height
=
height
-
2
if
isinstance
(
cell
,
NumericCell
)
and
cell
.
abs_tolerance
is
None
:
cell
.
set_tolerance
(
self
.
default_tolerance
)
if
cell
.
value
is
None
:
cell
.
classes
=
'active unchecked'
elif
cell
.
check_response
(
cell
.
value
):
cell
.
classes
=
'active right-answer'
else
:
cell
.
classes
=
'active wrong-answer'
def
get_status
(
self
):
"""Status dictionary passed to the frontend code."""
return
dict
(
answers_correct
=
self
.
answers_correct
,
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
):
"""Render the table."""
...
...
@@ -153,6 +184,7 @@ class ActiveTableXBlock(StudioEditableXBlockMixin, XBlock):
head_height
=
self
.
_row_heights
[
0
]
if
self
.
_row_heights
else
None
,
thead
=
self
.
thead
,
tbody
=
self
.
tbody
,
max_attempts
=
self
.
max_attempts
,
)
html
=
loader
.
render_template
(
'templates/html/activetable.html'
,
context
)
...
...
@@ -166,39 +198,45 @@ class ActiveTableXBlock(StudioEditableXBlockMixin, XBlock):
frag
=
Fragment
(
html
)
frag
.
add_css
(
css
)
frag
.
add_javascript
(
loader
.
load_unicode
(
'static/js/src/activetable.js'
))
frag
.
initialize_js
(
'ActiveTableXBlock'
,
dict
(
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
,
))
frag
.
initialize_js
(
'ActiveTableXBlock'
,
self
.
get_status
())
return
frag
@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.
"""
def
check_and_save_answers
(
self
,
data
):
"""Common implementation for the check and save handlers."""
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
# 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
.
postprocess_table
()
correct
=
{
answers_
correct
=
{
cell_id
:
self
.
response_cells
[
cell_id
]
.
check_response
(
value
)
for
cell_id
,
value
in
data
.
iteritems
()
}
# 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.
self
.
answers
=
data
self
.
num_correct_answers
=
sum
(
correct
.
itervalues
())
self
.
score
=
self
.
num_correct_answers
*
self
.
max_score
/
len
(
correct
)
return
answers_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
))
return
dict
(
correct
=
correct
,
num_correct_answers
=
self
.
num_correct_answers
,
num_total_answers
=
len
(
correct
),
score
=
self
.
score
,
max_score
=
self
.
max_score
,
)
return
self
.
get_status
()
@XBlock.json_handler
def
save_answers
(
self
,
data
,
unused_suffix
=
''
):
"""Save the answers given by the student without checking them."""
self
.
check_and_save_answers
(
data
)
self
.
answers_correct
=
None
return
self
.
get_status
()
def
validate_field_data
(
self
,
validation
,
data
):
"""Validate the data entered by the user.
...
...
activetable/static/js/src/activetable.js
View file @
072ab3ed
...
...
@@ -2,49 +2,84 @@
function
ActiveTableXBlock
(
runtime
,
element
,
init_args
)
{
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_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
.
text
(
'correct'
);
$status_message
.
text
(
'Great job!
('
+
data
.
score
+
'/'
+
data
.
max_score
+
' points)
'
);
$status_message
.
text
(
'Great job! '
);
}
else
{
$status
.
removeClass
(
'correct'
).
addClass
(
'incorrect'
);
$status
.
text
(
'incorrect'
);
$status_message
.
text
(
'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
)
{
$
.
each
(
data
.
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'
);
function
updateFeedback
(
data
)
{
var
feedback_msg
;
if
(
data
.
score
===
null
)
{
feedback_msg
=
'('
+
data
.
max_score
+
' points possible)'
;
}
else
{
$cell
.
addClass
(
'wrong-answer'
);
$cell
.
prop
(
'title'
,
'incorrect'
);
feedback_msg
=
'('
+
data
.
score
+
'/'
+
data
.
max_score
+
' points)'
;
}
});
updateStatus
(
data
);
if
(
data
.
max_attempts
)
{
feedback_msg
=
'You have used '
+
data
.
attempts
+
' of '
+
data
.
max_attempts
+
' 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
)
{
$
(
'.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
)
{
answers
=
{};
function
c
allHandler
(
url
)
{
var
answers
=
{};
$
(
'td.active'
,
element
).
each
(
function
()
{
answers
[
this
.
id
]
=
$
(
'input'
,
this
).
val
();
});
$
.
ajax
({
type
:
"POST"
,
url
:
checkHandlerU
rl
,
url
:
u
rl
,
data
:
JSON
.
stringify
(
answers
),
success
:
markResponseCell
s
,
success
:
updateStatu
s
,
});
}
...
...
@@ -55,6 +90,7 @@ function ActiveTableXBlock(runtime, element, init_args) {
}
$
(
'#activetable-help-button'
,
element
).
click
(
toggleHelp
);
$
(
'.action .check'
,
element
).
click
(
checkAnswers
);
if
(
init_args
.
num_total_answers
)
updateStatus
(
init_args
);
$
(
'.action .check'
,
element
).
click
(
function
(
e
)
{
callHandler
(
checkHandlerUrl
);
});
$
(
'.action .save'
,
element
).
click
(
function
(
e
)
{
callHandler
(
saveHandlerUrl
);
});
updateStatus
(
init_args
);
}
activetable/templates/css/activetable.css
View file @
072ab3ed
...
...
@@ -110,8 +110,14 @@
.activetable_block
.status-message
{
margin
:
0.5em
0
1.5em
;
}
.activetable_block
.action
.check
{
.activetable_block
.action
button
{
height
:
40px
;
margin-right
:
10px
;
font-weight
:
600
;
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 @@
{% else %}
<p>
This component isn't configured properly and can't be displayed.
</p>
{% endif %}
<p
class=
"status"
>
unanswered
</p>
<p
class=
"status"
></p>
<div
class=
"status-message"
></div>
<div
class=
"action"
>
<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>
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