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
43b1a871
Commit
43b1a871
authored
Oct 19, 2015
by
Sven Marnach
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
First feature-complete version.
parent
49489588
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
462 additions
and
74 deletions
+462
-74
.gitignore
+1
-0
activetable/activetable.py
+148
-51
activetable/cells.py
+66
-0
activetable/parsers.py
+98
-0
activetable/static/css/activetable.css
+79
-6
activetable/static/html/activetable.html
+0
-5
activetable/static/js/src/activetable.js
+26
-12
activetable/templates/html/activetable.html
+43
-0
setup.py
+1
-0
No files found.
.gitignore
View file @
43b1a871
__pycache__/
*.py[cod]
activetable_xblock.egg-info/**
activetable/activetable.py
View file @
43b1a871
"""TO-DO: Write a description of what this XBlock is."""
# -*- coding: utf-8 -*-
"""An XBlock with a tabular problem type that requires students to fill in some cells."""
from
__future__
import
absolute_import
,
division
,
unicode_literals
import
pkg_resources
import
textwrap
from
xblock.core
import
XBlock
from
xblock.fields
import
Scope
,
Integer
from
xblock.fields
import
Dict
,
Float
,
Scope
,
String
from
xblock.fragment
import
Fragment
from
xblock.validation
import
ValidationMessage
from
xblockutils.resources
import
ResourceLoader
from
xblockutils.studio_editable
import
StudioEditableXBlockMixin
from
.cells
import
NumericCell
from
.parsers
import
ParseError
,
parse_table
,
parse_number_list
class
ActiveTableXBlock
(
XBlock
):
"""
TO-DO: document what your XBlock does.
"""
loader
=
ResourceLoader
(
__name__
)
# Fields are defined on the class. You can access them in your code as
# self.<fieldname>.
# TO-DO: delete count, and define your own fields.
count
=
Integer
(
default
=
0
,
scope
=
Scope
.
user_state
,
help
=
"A simple counter, to show something happening"
,
class
ActiveTableXBlock
(
StudioEditableXBlockMixin
,
XBlock
):
"""An XBlock with a tabular problem type that requires students to fill in some cells."""
table_definition
=
String
(
display_name
=
'Table definition'
,
help
=
'The definition of the table in Python-like syntax.'
,
# TODO(smarnach): proper help
scope
=
Scope
.
content
,
multiline_editor
=
True
,
resettable_editor
=
False
,
default
=
textwrap
.
dedent
(
"""
\
[
['Column header 1', 'Column header 2'],
['Enter "answer" here:', String(answer='answer')],
[42, Numeric(answer=42, tolerance=0.0)],
]
"""
)
)
help_text
=
String
(
display_name
=
'Help text'
,
help
=
'The text that gets displayed when clicking the "+help" button. If you do not '
'specify a text, the help feature is disabled.'
,
scope
=
Scope
.
content
,
multiline_editor
=
True
,
resettable_editor
=
False
,
default
=
''
,
)
column_widths
=
String
(
display_name
=
'Column widths'
,
help
=
'Set the width of the columns in pixels. The value should be a Python-like list of '
'numerical values. The total width of the table should not be more than 800. No value '
'will result in equal-width columns with a total width of 800 pixels.'
,
scope
=
Scope
.
content
,
)
row_heights
=
String
(
display_name
=
'Row heights'
,
help
=
'Set the heights of the rows in pixels. The value should be a Python-like list of '
'numerical values. Rows may grow higher than the specified value if the text in some cells '
'in the row is long enough to get wrapped in more than one line.'
,
scope
=
Scope
.
content
,
)
default_tolerance
=
Float
(
display_name
=
'Default tolerance'
,
help
=
'The tolerance in pecent that is used for numerical response cells you did not '
'specify an explicit tolerance for.'
,
scope
=
Scope
.
content
,
default
=
1.0
,
)
editable_fields
=
[
'table_definition'
,
'help_text'
,
'column_widths'
,
'row_heights'
,
'default_tolerance'
]
answers
=
Dict
(
scope
=
Scope
.
user_state
)
def
resource_string
(
self
,
path
):
"""Handy helper for getting resources from our kit."""
data
=
pkg_resources
.
resource_string
(
__name__
,
path
)
return
data
.
decode
(
"utf8"
)
def
__init__
(
self
,
*
args
,
**
kwargs
):
super
(
ActiveTableXBlock
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
self
.
thead
=
None
self
.
tbody
=
None
self
.
_column_widths
=
None
self
.
_row_heights
=
None
self
.
response_cells
=
None
def
parse_fields
(
self
):
"""Parse the user-provided fields into more processing-friendly structured data."""
if
self
.
table_definition
:
self
.
thead
,
self
.
tbody
=
parse_table
(
self
.
table_definition
)
else
:
self
.
thead
=
self
.
tbody
=
None
return
if
self
.
column_widths
:
self
.
_column_widths
=
parse_number_list
(
self
.
column_widths
)
else
:
self
.
_column_widths
=
[
800
/
len
(
self
.
thead
)]
*
len
(
self
.
thead
)
if
self
.
row_heights
:
self
.
_row_heights
=
parse_number_list
(
self
.
row_heights
)
else
:
self
.
_row_heights
=
[
36
]
*
(
len
(
self
.
tbody
)
+
1
)
self
.
response_cells
=
{}
for
row
,
height
in
zip
(
self
.
tbody
,
self
.
_row_heights
[
1
:]):
row
[
'height'
]
=
height
for
cell
in
row
[
'cells'
]:
cell
.
id
=
'cell_{}_{}'
.
format
(
cell
.
index
,
row
[
'index'
])
if
not
cell
.
is_static
:
self
.
response_cells
[
cell
.
id
]
=
cell
cell
.
value
=
self
.
answers
.
get
(
cell
.
id
)
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'
# TO-DO: change this view to display your data your own way.
def
student_view
(
self
,
context
=
None
):
"""
The primary view of the ActiveTableXBlock, shown to students
when viewing courses.
"""
html
=
self
.
resource_string
(
"static/html/activetable.html"
)
frag
=
Fragment
(
html
.
format
(
self
=
self
))
frag
.
add_css
(
self
.
resource_string
(
"static/css/activetable.css"
))
frag
.
add_javascript
(
self
.
resource_string
(
"static/js/src/activetable.js"
))
"""Render the table."""
self
.
parse_fields
()
context
=
dict
(
help_text
=
self
.
help_text
,
total_width
=
sum
(
self
.
_column_widths
)
if
self
.
_column_widths
else
None
,
column_widths
=
self
.
_column_widths
,
head_height
=
self
.
_row_heights
[
0
]
if
self
.
_row_heights
else
None
,
thead
=
self
.
thead
,
tbody
=
self
.
tbody
,
)
html
=
loader
.
render_template
(
'templates/html/activetable.html'
,
context
)
frag
=
Fragment
(
html
)
frag
.
add_css
(
loader
.
load_unicode
(
'static/css/activetable.css'
))
frag
.
add_javascript
(
loader
.
load_unicode
(
'static/js/src/activetable.js'
))
frag
.
initialize_js
(
'ActiveTableXBlock'
)
return
frag
# TO-DO: change this handler to perform your own actions. You may need more
# than one handler, or you may not need any handlers at all.
@XBlock.json_handler
def
increment_count
(
self
,
data
,
suffix
=
''
):
"""
An example handler, which increments the data.
def
check_answers
(
self
,
data
,
suffix
=
''
):
"""Check the answers given by the student.
This handler is called when the "Check" button is clicked.
"""
# Just to show data coming in...
assert
data
[
'hello'
]
==
'world'
self
.
count
+=
1
return
{
"count"
:
self
.
count
}
# TO-DO: change this to create the scenarios you'd like to see in the
# workbench while developing your XBlock.
@staticmethod
def
workbench_scenarios
():
"""A canned scenario for display in the workbench."""
return
[
(
"ActiveTableXBlock"
,
"""<vertical_demo>
<activetable/>
<activetable/>
<activetable/>
</vertical_demo>
"""
),
]
self
.
parse_fields
()
correct_dict
=
{
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
return
correct_dict
def
validate_field_data
(
self
,
validation
,
data
):
def
add_error
(
msg
):
validation
.
add
(
ValidationMessage
(
ValidationMessage
.
ERROR
,
msg
))
try
:
parse_table
(
data
.
table_definition
)
except
ParseError
as
exc
:
add_error
(
'Problem with table definition: '
+
exc
.
message
)
if
data
.
column_widths
:
try
:
parse_number_list
(
data
.
column_widths
)
except
ParseError
as
exc
:
add_error
(
'Problem with column widths: '
+
exc
.
message
)
if
data
.
row_heights
:
try
:
parse_number_list
(
data
.
row_heights
)
except
ParseError
as
exc
:
add_error
(
'Problem with row heights: '
+
exc
.
message
)
activetable/cells.py
0 → 100644
View file @
43b1a871
# -*- coding: utf-8 -*-
"""Classes representing table cells.
These classes are used mainly as namespaces to dump all data associated with a cell. There is no
expectation that all attributes are set in __init__() or that attributes are controlled byt the
classes themselves.
"""
from
__future__
import
absolute_import
,
division
,
unicode_literals
class
StaticCell
(
object
):
"""A static cell with a fixed value in the table body."""
is_static
=
True
def
__init__
(
self
,
value
):
self
.
value
=
value
class
NumericCell
(
object
):
"""A numeric response cell."""
is_static
=
False
placeholder
=
'numeric response'
def
__init__
(
self
,
answer
,
tolerance
=
None
,
min_significant_digits
=
None
,
max_significant_digits
=
None
):
"""Set the correct answer and the allowed relative tolerance in percent."""
self
.
answer
=
answer
self
.
set_tolerance
(
tolerance
)
self
.
min_significant_digits
=
min_significant_digits
self
.
max_significant_digits
=
max_significant_digits
def
set_tolerance
(
self
,
tolerance
):
if
tolerance
is
None
:
self
.
abs_tolerance
=
None
else
:
self
.
abs_tolerance
=
abs
(
self
.
answer
)
*
tolerance
/
100.0
def
check_response
(
self
,
student_response
):
"""Return a Boolean value indicating whether the student response is correct."""
try
:
r
=
float
(
student_response
)
except
ValueError
:
return
False
if
self
.
min_significant_digits
or
self
.
max_significant_digits
:
d
=
len
(
decimal
.
Decimal
(
student_response
)
.
as_tuple
()
.
digits
)
if
self
.
min_significant_digits
and
d
<
self
.
min_significant_digits
:
return
False
if
self
.
max_significant_digits
and
d
>
self
.
max_significant_digits
:
return
False
return
abs
(
r
-
self
.
answer
)
<=
self
.
abs_tolerance
class
StringCell
(
object
):
"""A string response cell."""
is_static
=
False
placeholder
=
'text response'
def
__init__
(
self
,
answer
):
"""Set the correct answer."""
self
.
answer
=
answer
def
check_response
(
self
,
student_response
):
"""Return a Boolean value indicating whether the student response is correct."""
return
student_response
==
self
.
answer
activetable/parsers.py
0 → 100644
View file @
43b1a871
# -*- coding: utf-8 -*-
"""Parsers for structured text data entered by the user."""
from
__future__
import
absolute_import
,
division
,
unicode_literals
import
ast
import
numbers
from
.cells
import
NumericCell
,
StaticCell
,
StringCell
class
ParseError
(
Exception
):
"""The table definition could not be parsed."""
def
_ensure_type
(
node
,
expected_type
):
"""Internal helper function for parse_table."""
if
isinstance
(
node
,
expected_type
):
return
node
raise
ParseError
(
'the structure of the table definition is invalid'
)
def
parse_table
(
table_definition
):
"""Parse the table definition given by the user.
The string table_defintion is parsed as Python source code. The data is extracted from the
parse tree without executing it. The structure is rigidly validated; on error, ParseError is
thrown.
"""
try
:
expr
=
ast
.
parse
(
table_definition
.
strip
(),
mode
=
'eval'
)
except
SyntaxError
as
exc
:
raise
ParseError
(
exc
.
msg
)
row_iter
=
iter
(
_ensure_type
(
expr
.
body
,
ast
.
List
)
.
elts
)
thead
=
[]
for
cell
in
_ensure_type
(
next
(
row_iter
),
ast
.
List
)
.
elts
:
thead
.
append
(
_ensure_type
(
cell
,
ast
.
Str
)
.
s
)
tbody
=
[]
for
i
,
row_node
in
enumerate
(
row_iter
,
1
):
cells
=
[]
for
j
,
cell_node
in
enumerate
(
_ensure_type
(
row_node
,
ast
.
List
)
.
elts
):
if
isinstance
(
cell_node
,
ast
.
Str
):
cell
=
StaticCell
(
cell_node
.
s
)
elif
isinstance
(
cell_node
,
ast
.
Num
):
cell
=
StaticCell
(
cell_node
.
n
)
elif
isinstance
(
cell_node
,
ast
.
Call
):
cell
=
_parse_response_cell
(
cell_node
)
else
:
raise
ParseError
(
'invalid node in row {}, cell {}: {}'
.
format
(
i
,
j
,
type
(
cell_node
)
.
__name__
)
)
cell
.
index
=
j
cells
.
append
(
cell
)
if
len
(
cells
)
!=
len
(
thead
):
raise
ParseError
(
'row {} has a different number of columns than the previous rows ({} vs. {})'
.
format
(
i
,
len
(
cells
),
len
(
thead
))
)
tbody
.
append
(
dict
(
index
=
i
,
cells
=
cells
))
return
thead
,
tbody
def
_parse_response_cell
(
cell_node
):
"""Parse a single student response cell definition.
Response cells are written in function call syntax, either String(...) or Numeric(...). All
arguments must be keyword arguments.
"""
cell_type
=
_ensure_type
(
cell_node
.
func
,
ast
.
Name
)
.
id
if
any
((
cell_node
.
args
,
cell_node
.
starargs
,
cell_node
.
kwargs
)):
raise
ParseError
(
'all arguments to {} must be keyword arguments of the form name=value'
.
format
(
cell_type
)
)
if
cell_type
==
'String'
:
cell_class
=
StringCell
kwargs
=
{
kw
.
arg
:
_ensure_type
(
kw
.
value
,
ast
.
Str
)
.
s
for
kw
in
cell_node
.
keywords
}
elif
cell_type
==
'Numeric'
:
cell_class
=
NumericCell
kwargs
=
{
kw
.
arg
:
_ensure_type
(
kw
.
value
,
ast
.
Num
)
.
n
for
kw
in
cell_node
.
keywords
}
else
:
raise
ParseError
(
'invalid cell input type: {}'
.
format
(
cell_type
))
try
:
return
cell_class
(
**
kwargs
)
except
TypeError
as
exc
:
raise
ParseError
(
exc
.
message
)
def
parse_number_list
(
source
):
"""Parse the given string as a Python list of numbers.
This is used to parse the column_widths and row_heights lists entered by the user.
"""
lst
=
ast
.
literal_eval
(
source
)
if
not
isinstance
(
lst
,
list
):
raise
ParseError
(
'not a list'
)
if
not
all
(
isinstance
(
x
,
numbers
.
Real
)
for
x
in
lst
):
raise
ParseError
(
'all entries must be numbers'
)
return
lst
activetable/static/css/activetable.css
View file @
43b1a871
/* CSS for ActiveTableXBlock */
.activetable_block
.count
{
font-weight
:
bold
;
.activetable_block
table
{
clear
:
both
;
padding
:
0
;
margin
:
0
;
border-collapse
:
collapse
;
}
.activetable_block
p
{
.activetable_block
tr
{
border-width
:
0
;
}
.activetable_block
thead
tr
{
background
:
linear-gradient
(
#f0f0f0
,
#d0d0d0
);
}
.activetable_block
tr
.odd
{
background-color
:
#f0f0f0
;
}
.activetable_block
th
,
.activetable_block
td
{
line-height
:
1.5em
;
padding
:
0
10px
;
border
:
1px
solid
#c0c0c0
;
}
.activetable_block
th
{
text-align
:
left
;
text-shadow
:
0
1px
0
#ffffff
;
}
/* cells that allow user input */
.activetable_block
td
.active
{
padding
:
0
;
}
/* Cells that haven't been checked yet */
.activetable_block
td
.unchecked
{
background-color
:
#ffffe0
;
}
.activetable_block
tr
.odd
td
.unchecked
{
background-color
:
#f0f0d0
;
}
/* cells containing wrong answers after clicking "Check" */
.activetable_block
td
.wrong-answer
{
background-color
:
#ffe0e0
;
}
.activetable_block
tr
.odd
td
.wrong-answer
{
background-color
:
#f0d0d0
;
}
/* cells containing right answers after clicking "Check" */
.activetable_block
td
.right-answer
{
background-color
:
#e0ffe0
;
}
.activetable_block
tr
.odd
td
.right-answer
{
background-color
:
#d0f0d0
;
}
.activetable_block
input
[
type
=
"text"
]
{
font-size
:
1em
;
line-height
:
1.5em
;
border
:
0
;
padding
:
0
10px
;
margin
:
0
;
box-sizing
:
border-box
;
display
:
block
;
width
:
100%
;
background-color
:
transparent
;
}
.activetable_block
input
[
type
=
"text"
]
:hover
{
outline
:
3px
solid
#a0a0ff
;
}
.activetable_block
input
[
type
=
"text"
]
:focus
{
outline
:
3px
solid
#8080ff
;
}
.activetable_block
#activetable-help-text
{
display
:
none
;
padding
:
5px
10px
;
margin
:
0
0
10px
;
border
:
1px
solid
#c0c0c0
;
background-color
:
#f0f0f0
;
}
.activetable_block
#activetable-help-button
{
float
:
right
;
padding
:
6px
10px
0
;
color
:
#009fe6
;
cursor
:
pointer
;
}
.activetable_block
#activetable-help-button
:hover
{
color
:
#bd9730
;
}
activetable/static/html/activetable.html
deleted
100644 → 0
View file @
49489588
<div
class=
"activetable_block"
>
<p>
ActiveTableXBlock: count is now
<span
class=
'count'
>
{self.count}
</span>
(click me to increment).
</p>
</div>
activetable/static/js/src/activetable.js
View file @
43b1a871
/* Javascript for ActiveTableXBlock. */
function
ActiveTableXBlock
(
runtime
,
element
)
{
function
updateCount
(
result
)
{
$
(
'.count'
,
element
).
text
(
result
.
count
);
}
var
checkHandlerUrl
=
runtime
.
handlerUrl
(
element
,
'check_answers'
);
var
handlerUrl
=
runtime
.
handlerUrl
(
element
,
'increment_count'
);
function
markResponseCells
(
correct_dict
)
{
$
.
each
(
correct_dict
,
function
(
cell_id
,
correct
)
{
var
$cell
=
$
(
'#'
+
cell_id
,
element
);
$cell
.
removeClass
(
'right-answer wrong-answer unchecked'
);
if
(
correct
)
$cell
.
addClass
(
'right-answer'
)
else
$cell
.
addClass
(
'wrong-answer'
);
})
}
$
(
'p'
,
element
).
click
(
function
(
eventObject
)
{
function
checkAnswers
(
e
)
{
answers
=
{};
$
(
'td.active'
,
element
).
each
(
function
()
{
answers
[
this
.
id
]
=
$
(
'input'
,
this
).
val
();
});
$
.
ajax
({
type
:
"POST"
,
url
:
h
andlerUrl
,
data
:
JSON
.
stringify
(
{
"hello"
:
"world"
}
),
success
:
updateCount
url
:
checkH
andlerUrl
,
data
:
JSON
.
stringify
(
answers
),
success
:
markResponseCells
,
});
});
}
function
toggleHelp
(
e
)
{
var
$help_text
=
$
(
'#activetable-help-text'
,
element
);
$help_text
.
toggle
();
$
(
this
).
text
(
$help_text
.
is
(
':visible'
)
?
'-help'
:
'+help'
);
}
$
(
function
(
$
)
{
/* Here's where you'd do things on page load. */
});
$
(
'#activetable-help-button'
,
element
).
click
(
toggleHelp
);
$
(
'.action .check'
,
element
).
click
(
checkAnswers
);
}
activetable/templates/html/activetable.html
0 → 100644
View file @
43b1a871
<div
class=
"activetable_block"
>
{% if help_text %}
<div
class=
"activetable-help"
style=
"width: {{ total_width }}px;"
>
<a
id=
"activetable-help-button"
>
+help
</a>
<p
id=
"activetable-help-text"
>
{{ help_text }}
</p>
</div>
{% endif %}
{% if thead %}
<table
id=
"activetable"
>
<colgroup>
{% for width in column_widths %}
<col
style=
"width: {{ width }}px;"
>
{% endfor %}
</colgroup>
<thead>
<tr
style=
"height: {{ head_height }}px;"
>
{% for cell in thead %}
<th>
{{ cell }}
</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in tbody %}
<tr
style=
"height: {{ row.height }}px;"
>
{% for cell in row.cells %}
<td
class=
"{{ cell.classes }}"
id=
"{{ cell.id }}"
>
{% if cell.is_static %}
{{ cell.value }}
{% else %}
<input
type=
"text"
style=
"height: {{ cell.height }}px;"
size=
1
value=
"{{ cell.value|default_if_none:'' }}"
placeholder=
"{{ cell.placeholder }}"
>
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>
This component isn't configured properly and can't be displayed.
</p>
{% endif %}
<div
class=
"status"
></div>
<div
class=
"status-message"
></div>
<div
class=
"action"
>
<button
class=
"check Check"
><span
class=
"sr"
>
your answer
</span><span
class=
"check-label"
>
Check
</span></button>
</div>
</div>
setup.py
View file @
43b1a871
...
...
@@ -29,6 +29,7 @@ setup(
],
install_requires
=
[
'XBlock'
,
'xblock-utils'
,
],
entry_points
=
{
'xblock.v1'
:
[
...
...
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