Commit 1786a2c8 by Tim Krones

Merge pull request #1 from open-craft/initial-implementation

Initial implementation
parents e5487a6b 5125445e
__pycache__/
*.py[cod]
activetable_xblock.egg-info/**
.coverage
tests.integration.*.log
tests.integration.*.png
var/
language: python
python:
- 2.7
before_install:
- export DISPLAY=:99
- sh -e /etc/init.d/xvfb start
install:
- pip install -r test-requirements.txt
- pip install -r $VIRTUAL_ENV/src/xblock-sdk/requirements/base.txt
- pip install -r $VIRTUAL_ENV/src/xblock-sdk/requirements/test.txt
- pip install -r $VIRTUAL_ENV/src/xblock/requirements.txt
script:
- pep8 --max-line-length=100 activetable
- pylint activetable
- ./run_tests.py --with-coverage --cover-package=activetable
notifications:
email: false
addons:
firefox: 36.0
ActiveTable XBlock
==================
This XBlock provides a tabular problem type, where students have to fill in some of the cells of a
table.
Running the tests
-----------------
Install the test prerequisites:
pip install -r test-requirements
Run pep8:
pep8 --max-line-length=100 activetable
Run pylint:
pylint activetable
Run the unit and integration tests:
./run-tests.sh --with-coverage --cover-package=activetable
The table definition
--------------------
The table definition is entered in a Python-like syntax (actually in a strict subset of Python). It
must be a list of lists, with all inner lists having the same lengths. The elements of the inner
lists correspond to the cells of the table. The first line contains the column headers and can only
contain string literals. All further lines represent the table body. Cells can be either string
literals, e.g. `'a string'`, numbers, e.g. `6.23`, or response cell declarations. There are two
types of response cells:
Numeric(answer=<correct_answer>, tolerance=<tolerance in percent>,
min_significant_digits=<number>, max_significant_digits=<number>)
A cell that expects a numeric answer. The tolerance is optional, and will default to the default
tolerance specified above. The restrictions for the number of significant digits are optional as
well. Significant digits are counted started from the first non-zero digit specified by the
student, and include trailing zeros.
Text(answer='<correct answer>')
A cell that expects a string answer.
An example of a table definition:
[
['Event', 'Year'],
['French Revolution', Numeric(answer=1789)],
['Krakatoa volcano explosion', Numeric(answer=1883)],
["Proof of Fermat's last theorem", Numeric(answer=1994)],
]
"""ActiveTable XBlock top-level package.
See activetable.activetable for more information.
"""
from .activetable import ActiveTableXBlock
# -*- 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
import decimal
class Cell(object):
"""Abstract base class for all cells."""
is_static = False
def __eq__(self, other):
"""Test for equality based on type and attribute values."""
return type(self) is type(other) and vars(self) == vars(other)
class StaticCell(Cell):
"""A static cell with a fixed value in the table body."""
is_static = True
def __init__(self, value):
self.value = value
class NumericCell(Cell):
"""A numeric response cell."""
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.abs_tolerance = None
self.set_tolerance(tolerance)
self.min_significant_digits = min_significant_digits
self.max_significant_digits = max_significant_digits
def set_tolerance(self, tolerance):
"""Set the tolerance to the specified value, if it is not None."""
if tolerance is not None:
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:
value = float(student_response)
except ValueError:
return False
if self.min_significant_digits or self.max_significant_digits:
digits = len(decimal.Decimal(student_response).as_tuple().digits)
if self.min_significant_digits and digits < self.min_significant_digits:
return False
if self.max_significant_digits and digits > self.max_significant_digits:
return False
return abs(value - self.answer) <= self.abs_tolerance
class TextCell(Cell):
"""A string response cell."""
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.strip() == self.answer.strip()
# -*- 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, TextCell
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 Text(...) 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 == 'Text':
cell_class = TextCell
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 Exception 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.
"""
try:
lst = ast.literal_eval(source)
except (SyntaxError, ValueError) as exc:
msg = getattr(exc, 'msg', getattr(exc, 'message', None))
raise ParseError(msg)
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
/* Javascript for ActiveTableXBlock. */
function ActiveTableXBlock(runtime, element, init_args) {
var checkHandlerUrl = runtime.handlerUrl(element, 'check_answers');
var saveHandlerUrl = runtime.handlerUrl(element, 'save_answers');
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.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!');
} 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.'
);
}
}
function updateFeedback(data) {
var feedback_msg;
if (data.score === null) {
feedback_msg = '(' + data.max_score + ' points possible)';
} else {
feedback_msg = '(' + data.score + '/' + data.max_score + ' points)';
}
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 callHandler(url) {
var answers = {};
$('td.active', element).each(function() {
answers[this.id] = $('input', this).val();
});
$.ajax({
type: "POST",
url: url,
data: JSON.stringify(answers),
success: updateStatus,
});
}
function toggleHelp(e) {
var $help_text = $('#activetable-help-text', element), visible;
$help_text.toggle();
visible = $help_text.is(':visible');
$(this).text(visible ? '-help' : '+help');
$(this).attr('aria-expanded', visible);
}
$('#activetable-help-button', element).click(toggleHelp);
$('.action .check', element).click(function (e) { callHandler(checkHandlerUrl); });
$('.action .save', element).click(function (e) { callHandler(saveHandlerUrl); });
updateStatus(init_args);
}
.activetable_block table {
clear: both;
padding: 0;
margin: 0;
border-collapse: collapse;
}
.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;
vertical-align: middle;
}
.activetable_block th {
text-align: left;
text-shadow: 0 1px 0 #ffffff;
font-weight: bold;
}
/* 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;
border-radius: initial;
background-image: initial;
box-shadow: initial;
}
.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;
line-height: 2em;
box-shadow: none;
text-shadow: none;
background: none;
border: none;
outline: none;
}
.activetable_block #activetable-help-button:hover {
color: #bd9730;
}
.activetable_block .status {
display: inline-block;
margin: 1.5em 0 0.5em !important;
width: 20px;
height: 20px;
text-indent: 100%;
white-space: nowrap;
overflow: hidden;
background: url("{{ unanswered_icon }}") center no-repeat;
}
.activetable_block .status.correct {
background-image: url("{{ correct_icon }}");
}
.activetable_block .status.incorrect {
background-image: url("{{ incorrect_icon }}");
}
.activetable_block .status-message {
margin: 0.5em 0 1.5em;
}
.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;
}
<div class="activetable_block">
{% if help_text %}
<div class="activetable-help" style="width: {{ total_width }}px;">
<button id="activetable-help-button" aria-controls="activetable-help-text" aria-haspopup="true"
aria-expanded="false">+help</button>
<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 scope="col">{{ cell }}</th>{% endfor %}
</tr>
</thead>
<tbody>
{% for row in tbody %}
<tr class="{{ row.class }}" style="height: {{ row.height }}px;">
{% for cell in row.cells %}
<td class="{{ cell.classes }}" id="{{ cell.id }}">
{% if cell.is_static %}
{{ cell.value }}
{% else %}
<label class="sr" for="input_{{ cell.id }}">{{ cell.col_label }}</label>
<input id="input_{{ cell.id }}" 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 %}
<p class="status" aria-live="polite"></p>
<div class="status-message" aria-live="polite"></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" aria-live="polite"></div>
{% endif %}
</div>
</div>
[REPORTS]
reports=no
[FORMAT]
max-line-length=100
[MESSAGES CONTROL]
disable=
I,
attribute-defined-outside-init,
maybe-no-member,
star-args,
too-few-public-methods,
too-many-ancestors,
too-many-instance-attributes,
too-many-public-methods
[VARIABLES]
dummy-variables-rgx=_$|dummy|unused
django>=1.8, <1.9
pyyaml
git+https://github.com/edx/XBlock.git@xblock-0.4.2#egg=XBlock
git+https://github.com/edx/xblock-utils.git@v1.0.0#egg=xblock-utils
.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Run tests for the ActiveTableXBlock.
This script is required to run the Selenium tests inside the xblock-sdk workbench
because the workbench SDK's settings file is not inside any python module.
"""
import os
import logging
import sys
from django.conf import settings
from django.core.management import execute_from_command_line
logging_level_overrides = {
'workbench.views': logging.ERROR,
'django.request': logging.ERROR,
'workbench.runtime': logging.ERROR,
}
if __name__ == '__main__':
# Use the workbench settings file:
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'workbench.settings')
# Configure a range of ports in case the default port of 8081 is in use
os.environ.setdefault('DJANGO_LIVE_TEST_SERVER_ADDRESS', 'localhost:8081-8099')
try:
os.mkdir('var')
except OSError:
# May already exist.
pass
settings.INSTALLED_APPS += ('activetable', )
for noisy_logger, log_level in logging_level_overrides.iteritems():
logging.getLogger(noisy_logger).setLevel(log_level)
args_iter = iter(sys.argv[1:])
options = []
paths = []
for arg in args_iter:
if arg == '--':
break
if arg.startswith('-'):
options.append(arg)
else:
paths.append(arg)
paths.extend(args_iter)
if not paths:
paths = ['tests/']
execute_from_command_line([sys.argv[0], 'test'] + options + ['--'] + paths)
"""Setup for activetable XBlock."""
import os
from setuptools import setup
def package_data(pkg, roots):
"""Generic function to find package_data.
All of the files under each of the `roots` will be declared as package
data for package `pkg`.
"""
data = []
for root in roots:
for dirname, _, files in os.walk(os.path.join(pkg, root)):
for fname in files:
data.append(os.path.relpath(os.path.join(dirname, fname), pkg))
return {pkg: data}
setup(
name='activetable-xblock',
version='0.1',
description='activetable XBlock', # TODO: write a better description.
packages=[
'activetable',
],
install_requires=[
'XBlock',
'xblock-utils',
],
entry_points={
'xblock.v1': [
'activetable = activetable:ActiveTableXBlock',
]
},
package_data=package_data("activetable", ["static", "public"]),
)
-r requirements.txt
-e git+https://github.com/edx/xblock-sdk.git@4e8e713e7dd886b8d2eb66b5001216b66b9af81a#egg=xblock-sdk
ddt
selenium==2.48.0
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
from selenium.webdriver.support.ui import WebDriverWait
from xblockutils.resources import ResourceLoader
from xblockutils.studio_editable_test import StudioEditableBaseTest
loader = ResourceLoader(__name__) # pylint: disable=invalid-name
class TestActiveTable(StudioEditableBaseTest):
"""Test the Student View of ActiveTableXBlock."""
def load_scenario(self, path):
self.set_scenario_xml(loader.load_unicode(path))
self.element = self.go_to_view("student_view")
def enter_answers(self, answers):
for cell_id, value in answers.iteritems():
text_input = self.element.find_element_by_css_selector('#{id} input'.format(id=cell_id))
text_input.clear()
text_input.send_keys(value)
def get_current_answers(self):
return {
cell.get_attribute('id'): cell.get_attribute('value')
for cell in self.element.find_elements_by_css_selector('td.active')
}
def check(self, expected_message, expected_status_class):
"""Click the check button and verify the status message and icon."""
status_message_div = self.element.find_element_by_class_name('status-message')
old_message = status_message_div.text
check_button = self.element.find_element_by_css_selector('.action button.check')
check_button.click()
wait = WebDriverWait(status_message_div, self.timeout)
wait.until(
lambda e: e.text != old_message,
'Timeout while waiting for status message to change.'
)
self.assertEqual(status_message_div.text, expected_message)
self.wait_until_exists('.status.' + expected_status_class)
def verify_cell_classes(self, answers_correct):
for cell in self.element.find_elements_by_css_selector('td.active'):
cell_id = cell.get_attribute('id')
text_input = cell.find_element_by_tag_name('input')
cell_classes = cell.get_attribute('class')
if answers_correct[cell_id]:
self.assertIn('right-answer', cell_classes)
else:
self.assertIn('wrong-answer', cell_classes)
def answer_and_check(self, answers, answers_correct, expected_message):
self.enter_answers(answers)
expected_status_class = 'correct' if all(answers_correct.values()) else 'incorrect'
self.check(expected_message, expected_status_class)
self.verify_cell_classes(answers_correct)
def test_basic_answering(self):
def cell_dict(*values):
return dict(zip(['cell_1_1', 'cell_2_1', 'cell_3_1'], values))
self.load_scenario('xml/basic.xml')
self.answer_and_check(
answers=cell_dict('', '', ''),
answers_correct=cell_dict(False, False, False),
expected_message='You have 0 out of 3 cells correct.',
)
self.answer_and_check(
answers=cell_dict('1789', '1984', '1994'),
answers_correct=cell_dict(True, False, True),
expected_message='You have 2 out of 3 cells correct.',
)
self.answer_and_check(
answers=cell_dict('1789', '1883', '1994'),
answers_correct=cell_dict(True, True, True),
expected_message='Great job!',
)
def test_save_and_reload(self):
answers = dict(cell_1_1='1', cell_2_1='2', cell_3_1='3')
self.load_scenario('xml/max_attempts.xml')
self.enter_answers(answers)
self.element.find_element_by_css_selector('.action button.save').click()
vertical = self.load_root_xblock()
activetable_block = vertical.runtime.get_block(vertical.children[0])
self.assertEqual(activetable_block.answers, answers)
# I tried to implement this as reloading the page and testing whether the old values are
# loaded again. I can't make that work (while it works perfectly fine when doing it
# manually).
<vertical_demo>
<activetable url_name="basic">
[
['Event', 'Year'],
['French Revolution', Numeric(answer=1789)],
['Krakatoa volcano explosion', Numeric(answer=1883)],
["Proof of Fermat's last theorem", Numeric(answer=1994)],
]
</activetable>
</vertical_demo>
<vertical_demo>
<activetable url_name="max_attempts" max_attempts="2">
[
['Event', 'Year'],
['French Revolution', Numeric(answer=1789)],
['Krakatoa volcano explosion', Numeric(answer=1883)],
["Proof of Fermat's last theorem", Numeric(answer=1994)],
]
</activetable>
</vertical_demo>
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
import unittest
import mock
from xblock.field_data import DictFieldData
from xblock.runtime import Runtime
from xblock.validation import Validation
from activetable.activetable import ActiveTableXBlock
class ActiveTableTest(unittest.TestCase):
def setUp(self):
self.runtime_mock = mock.Mock(spec=Runtime)
self.block = ActiveTableXBlock(self.runtime_mock, DictFieldData({}), mock.Mock())
def verify_validation(self, data, expect_success):
validation = Validation('xblock_id')
self.block.validate_field_data(validation, data)
self.assertEqual(bool(validation), expect_success)
def test_validate_field_data(self):
data = mock.Mock()
data.content = 'invalid'
data.column_widths = ''
data.row_heights = ''
self.verify_validation(data, False)
data.content = '[["header"], [6.283]]'
self.verify_validation(data, True)
data.column_widths = 'invalid'
self.verify_validation(data, False)
data.column_widths = '[1, 2]'
self.verify_validation(data, False)
data.column_widths = '[1]'
self.verify_validation(data, True)
data.row_heights = 'invalid'
self.verify_validation(data, False)
data.row_heights = '[1, 2, 3]'
self.verify_validation(data, False)
data.row_heights = '[1, 2]'
self.verify_validation(data, True)
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
import unittest
from activetable.cells import NumericCell, TextCell
class CellTest(unittest.TestCase):
def test_numeric_cell(self):
cell = NumericCell(answer=42, tolerance=1.0)
self.assertTrue(cell.check_response('42'))
self.assertTrue(cell.check_response('42.4'))
self.assertTrue(cell.check_response('41.6'))
self.assertFalse(cell.check_response('41.5'))
self.assertFalse(cell.check_response('43'))
self.assertFalse(cell.check_response('Hurz!'))
cell.set_tolerance(10.0)
self.assertTrue(cell.check_response('42'))
self.assertTrue(cell.check_response('46'))
self.assertFalse(cell.check_response('37'))
def test_significant_digits(self):
cell = NumericCell(
answer=6.238, tolerance=10.0, min_significant_digits=3, max_significant_digits=4
)
self.assertTrue(cell.check_response('6.24'))
self.assertTrue(cell.check_response('6.238'))
self.assertFalse(cell.check_response('6.2'))
self.assertFalse(cell.check_response('6.2382'))
def test_string_cell(self):
cell = TextCell('OpenCraft')
self.assertTrue(cell.check_response('OpenCraft'))
self.assertTrue(cell.check_response(' OpenCraft \t\r\n'))
self.assertFalse(cell.check_response('giraffe'))
cell = TextCell('ÖpenCräft')
self.assertTrue(cell.check_response('ÖpenCräft'))
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, unicode_literals
import ddt
import unittest
from activetable.cells import Cell, NumericCell, StaticCell, TextCell
from activetable.parsers import ParseError, parse_table, parse_number_list
@ddt.ddt
class ParserTest(unittest.TestCase):
def test_parse_table(self):
table_definition = """
[
['Event', 'Year'],
['French Revolution', Numeric(answer=1789)],
['Volcano exploded in 1883', Text(answer='Krakatoa')],
[6.283, 123],
]
"""
thead, tbody = parse_table(table_definition)
expected = eval(table_definition.strip(), dict(Numeric=NumericCell, Text=TextCell))
expected_body = []
for i, row in enumerate(expected[1:], 1):
cells = []
for j, cell in enumerate(row):
if not isinstance(cell, Cell):
cell = StaticCell(cell)
cell.index = j
cells.append(cell)
expected_body.append(dict(index=i, cells=cells))
self.assertEqual(thead, expected[0])
self.assertEqual(tbody, expected_body)
@ddt.data(
'syntax error',
'"wrong type"',
'["wrong type"]',
'[["wrong type in header", Numeric(answer=3)]],',
'[["header", "header"], "wrong type in body"]',
'[["header", "header"], ["illegal expression", 1 + 1]]',
'[["header", "header"], ["inconsistent", "row", "length"]]',
'[["header", "header"], ["wrong function name", Numerical(answer=3)]]',
'[["header", "header"], ["wrong argument class", Numeric(3)]]',
'[["header", "header"], ["wrong argument name", Numeric(giraffe=3)]]',
'[["header", "header"], ["wrong argument value", Numeric(giraffe="3")]]',
)
def test_parse_table_errors(self, table_definition):
with self.assertRaises(ParseError):
parse_table(table_definition)
def test_parse_number_list(self):
self.assertEquals(parse_number_list('[1, 2.3]'), [1, 2.3])
for string in [']', '123', '["123"]', '[1j]', 'malformed']:
with self.assertRaises(ParseError):
parse_number_list(string)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment